PyRaspBeta

NeoPixel level meter

A level meter is the friendliest way to show an analog reading: turn the knob and a bar of light grows along the NeoPixel stick. Under the hood a potentiometer feeds an MCP3008 — the Pi has no analog inputs of its own — and the Pi paints the bar, one pixel per eighth of the knob's travel.

Every part is in the shop:

PartPriceJob
Potentiometer$1.50the knob, as a 0–3.3 V divider
MCP3008$4digitises the wiper over SPI
NeoPixel stick (8)$7the bar of addressable LEDs

The fast version replaces the pot and the ADC's fiddly eleven-wire breadboard circuit with the knob + ADC board ($9). The NeoPixel stick is hand-wired in both versions — no kit covers the pixels — but it only needs three wires.

The cheap version

The pot's two ends go across 3.3 V and ground, so its wiper can never exceed 3.3 V; the wiper goes to channel 0 of the ADC. The MCP3008 runs from 3.3 V too, with VREF tied to VDD, which makes the full scale 3.3 V.

circuit.py
from wiring import *

pi = Pi()

pot = Potentiometer(10000)
adc = MCP3008()
stick = NeoPixelStick()

pi.V3_3 >> pot.pin3            # clockwise end to 3.3 V
pot.pin1 >> pi.GND             # anticlockwise end to ground
pot.wiper >> adc.ch0           # wiper: 0–3.3 V follows the knob

pi.V3_3 >> adc.vdd
adc.vdd >> adc.vref             # reference = 3.3 V, so full scale is 3.3 V
adc.agnd >> pi.GND
adc.dgnd >> pi.GND
pi.SCLK >> adc.clk
pi.MISO >> adc.dout
pi.MOSI >> adc.din
pi.CE0 >> adc.cs

pi.GPIO18 >> stick.din          # the data line belongs on GPIO18
pi.V5 >> stick.v5               # 5 V supply for the LEDs
stick.gnd >> pi.GND
main.py
import board
import neopixel
from gpiozero import MCP3008
from time import sleep

knob = MCP3008(channel=0)
pixels = neopixel.NeoPixel(board.D18, 8, brightness=0.3, auto_write=False)

while True:
    level = knob.value
    lit = round(level * len(pixels))
    pixels.fill((0, 0, 0))
    for led in range(lit):
        pixels[led] = (255, 0, 0) if led == len(pixels) - 1 else (0, 255, 0)
    pixels.show()
    print(f"{level * 100:5.1f}%  {lit}/8 LEDs")
    sleep(0.2)

The fast version

The knob + ADC board is the potentiometer and the MCP3008 pre-wired wiper-to-channel-0, with VREF tied to VDD and both grounds joined — the five internal wires beginners get wrong are already done. Wire its SPI pins and power, then the stick exactly as before; main.py never changes, because the kit exposes the same channel over the same bus.

circuit.py
from wiring import *

pi = Pi()

knob = kits.KnobADC()
stick = NeoPixelStick()

pi.V3_3 >> knob.vdd
knob.gnd >> pi.GND
pi.SCLK >> knob.clk
pi.MISO >> knob.dout
pi.MOSI >> knob.din
pi.CE0 >> knob.cs

# still hand-wired: no kit covers the NeoPixel stick
pi.GPIO18 >> stick.din
pi.V5 >> stick.v5
stick.gnd >> pi.GND
main.py
import board
import neopixel
from gpiozero import MCP3008
from time import sleep

knob = MCP3008(channel=0)
pixels = neopixel.NeoPixel(board.D18, 8, brightness=0.3, auto_write=False)

while True:
    level = knob.value
    lit = round(level * len(pixels))
    pixels.fill((0, 0, 0))
    for led in range(lit):
        pixels[led] = (255, 0, 0) if led == len(pixels) - 1 else (0, 255, 0)
    pixels.show()
    print(f"{level * 100:5.1f}%  {lit}/8 LEDs")
    sleep(0.2)

How it works

  • The pot is a voltage divider: one end on 3V3, the other on GND, and the wiper somewhere between. Turning the shaft moves the wiper voltage from 0 V to 3.3 V. The Pi cannot read that directly, so it goes to the ADC — the analog inputs primer explains why.
  • MCP3008(channel=0) samples channel 0 and returns value as the input divided by VREF, a number from 0.0 to 1.0. Behind the scenes gpiozero frames the request over SPI: CE0 selects the chip, SCLK clocks, MOSI carries the channel, MISO brings the 10-bit code back.
  • round(level * len(pixels)) maps 0.0–1.0 onto 0–8 LEDs. pixels.fill((0, 0, 0)) blanks the framebuffer first, the loop sets the lit pixels green, the top one red when the bar is full, and show() latches the frame — nothing changes on the board before that.
  • brightness=0.3 is practical, not cosmetic: eight LEDs at full white would pull about 480 mA from the 5 V pin, and dimming keeps the current and the glare sensible. The stick page has the per-LED numbers.
  • The loop polls every 0.2 s, so sleep is what lets simulated time advance while the knob is turned.

Try it

Press Run: the shaft starts at 50 % (the datafeed default), so four pixels light and the console prints something like 50.0% 4/8 LEDs. Drag the pot's Shaft position in the Inspector, or grab the knob on the board, and the bar follows within a frame; at the ends it empties or fills exactly.

As a contest submission this is a pixels objective: the judge can set the pot feed to a value and check the light bar with a pixels check — "at 75 %, six pixels green" — while the console line shows the same. Keep the loop polling, don't block between readings, and expect the ADC's modelled noise to wobble the last pixel by one; check a range, not an exact count.

Variations

  • The ring: the 12-LED ring uses the same neopixel object — change the count to 12 and the bar wraps a circle. A stick and a ring chained DOUT-to-DIN share one data line and one NeoPixel object.
  • Peak hold: keep the highest recent level in a variable, light one white pixel there, and decay it by a count every loop — a poor maker's VU meter.
  • Colour ramp: give each pixel a colour by position instead of one green, so the bar runs from blue at the bottom to red at the top. With auto_write=False the whole ramp still updates in one show().

Going deeper