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:
| Part | Price | Job |
|---|---|---|
| Potentiometer | $1.50 | the knob, as a 0–3.3 V divider |
| MCP3008 | $4 | digitises the wiper over SPI |
| NeoPixel stick (8) | $7 | the 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.
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.GNDimport 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.
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.GNDimport 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 returnsvalueas the input divided byVREF, 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, andshow()latches the frame — nothing changes on the board before that.brightness=0.3is 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
sleepis 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
neopixelobject — 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 oneNeoPixelobject. - 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=Falsethe whole ramp still updates in oneshow().
Going deeper
- The parts: potentiometer, MCP3008 and the NeoPixel stick.
- The fast build: knob + ADC board.
- Libraries: gpiozero (
MCP3008) and neopixel (NeoPixel, slices, brightness). - Primer: analog inputs and ADCs, plus PWM for dimming the bar.