Addressable LEDs with Raspberry Pi Zero: Complete Guide

What Are Addressable LEDs?

Addressable LEDs (WS2812B/NeoPixel) are RGB LEDs where each pixel is individually controllable via a single data line. Perfect for:

Hardware Requirements

Shopping List

Item Qty Cost Notes
Pi Zero 2W 1 $15 With 40-pin header
WS2812B LED Strip 1m $10 30 LEDs/meter
5V Power Supply 1 $8 2A minimum for 30 LEDs
470Ω Resistor 1 $0.10 Data line protection
Capacitor 1000µF 1 $0.50 Power smoothing
Breadboard 1 $3 For prototyping
Jumper Wires - $5 Various sizes

Total: ~$40

Wiring Diagram

Pi Zero GPIO 18 (PWM) → 470Ω Resistor → WS2812B Data (DIN)
Pi Zero 5V → Capacitor → WS2812B Power (5V)
Pi Zero GND → WS2812B GND (GND)

Power Supply:
5V Power → LED Strip Power (5V)
GND Power → LED Strip GND + Pi Zero GND (shared ground)

Pinout Reference

Pi Zero 40-pin Header:
PIN 1:  3.3V
PIN 2:  5V          ← Use for capacitor positive
PIN 6:  GND         ← Shared ground
PIN 12: GPIO 18     ← Data line (PWM)
PIN 14: GND
PIN 39: GND
PIN 40: GPIO 21

Installation and Setup

1. Install Required Libraries

sudo apt update
sudo apt install python3-pip

# Install Adafruit NeoPixel library
pip3 install adafruit-circuitpython-neopixel

# Or for more control
pip3 install rpi_ws281x
pip3 install adafruit-blinka

2. Enable SPI (for some libraries)

sudo raspi-config
# Interfacing Options → SPI → Enable

3. Test Setup

#!/usr/bin/env python3
import board
import neopixel
import time

# Configure LED strip (30 pixels on GPIO 18)
pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.5)

# Test: Red
pixels.fill((255, 0, 0))
pixels.show()
time.sleep(1)

# Test: Green
pixels.fill((0, 255, 0))
pixels.show()
time.sleep(1)

# Test: Blue
pixels.fill((0, 0, 255))
pixels.show()

Run it:

python3 test_leds.py

Basic Animations

Rainbow Effect

#!/usr/bin/env python3
import board
import neopixel
import time

pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.8)

def wheel(pos):
    """Generate rainbow colors"""
    if pos < 0 or pos > 255:
        return (0, 0, 0)
    if pos < 85:
        return (255 - pos * 3, 0, pos * 3)
    elif pos < 170:
        pos -= 85
        return (0, pos * 3, 255 - pos * 3)
    else:
        pos -= 170
        return (pos * 3, 255 - pos * 3, 0)

def rainbow_cycle(wait):
    """Rainbow animation"""
    for j in range(256):
        for i in range(len(pixels)):
            rc_index = (i * 256 // len(pixels)) + j
            pixels[i] = wheel(rc_index & 255)
        pixels.show()
        time.sleep(wait)

# Run animation
try:
    while True:
        rainbow_cycle(0.02)
except KeyboardInterrupt:
    pixels.fill((0, 0, 0))
    pixels.show()

Chase Effect

def chase(color, wait):
    """Chase animation"""
    for i in range(len(pixels)):
        pixels[i] = color
        pixels.show()
        time.sleep(wait)
        pixels[i] = (0, 0, 0)
        
while True:
    chase((255, 0, 0), 0.05)
    chase((0, 255, 0), 0.05)
    chase((0, 0, 255), 0.05)

Breathing Effect

import math

def breathing(color, cycle_time=1.0):
    """Smooth breathing animation"""
    start_time = time.time()
    
    while True:
        elapsed = (time.time() - start_time) % cycle_time
        brightness = (math.sin(elapsed / cycle_time * 2 * math.pi) + 1) / 2
        
        r, g, b = color
        pixels.fill((
            int(r * brightness),
            int(g * brightness),
            int(b * brightness)
        ))
        pixels.show()
        time.sleep(0.02)

breathing((0, 255, 0), cycle_time=2.0)

Motion-Activated Lights

PIR Sensor Integration

#!/usr/bin/env python3
import board
import neopixel
import RPi.GPIO as GPIO
import time

pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.6)
pir_pin = 27

GPIO.setmode(GPIO.BCM)
GPIO.setup(pir_pin, GPIO.IN)

def light_on(color=(255, 255, 255)):
    """Turn lights on"""
    pixels.fill(color)
    pixels.show()

def light_off():
    """Turn lights off"""
    pixels.fill((0, 0, 0))
    pixels.show()

try:
    while True:
        if GPIO.input(pir_pin):
            print("Motion detected!")
            light_on((100, 150, 255))  # Cool white
            time.sleep(30)  # Stay on for 30 seconds
            light_off()
        time.sleep(0.5)
        
except KeyboardInterrupt:
    light_off()
    GPIO.cleanup()

Temperature-Based Color Change

Using Temperature Sensor

#!/usr/bin/env python3
import board
import neopixel
import adafruit_dht
import time

pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.7)
sensor = adafruit_dht.DHT22(board.D4)

def temp_to_color(temp):
    """Map temperature to color"""
    if temp < 15:
        return (0, 0, 255)      # Blue (cold)
    elif temp < 20:
        return (0, 255, 255)    # Cyan
    elif temp < 25:
        return (0, 255, 0)      # Green (comfortable)
    elif temp < 30:
        return (255, 255, 0)    # Yellow
    else:
        return (255, 0, 0)      # Red (hot)

try:
    while True:
        temp = sensor.temperature
        color = temp_to_color(temp)
        pixels.fill(color)
        pixels.show()
        print(f"Temperature: {temp:.1f}°C → Color: {color}")
        time.sleep(60)
        
except Exception as err:
    print(f"Error: {err}")
    pixels.fill((0, 0, 0))
    pixels.show()

Status Indicators

Multi-Color Status

#!/usr/bin/env python3
import board
import neopixel
import subprocess
import time

pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.8)

def get_system_status():
    """Check system health"""
    # CPU temperature
    result = subprocess.run(['vcgencmd', 'measure_temp'], 
                           capture_output=True, text=True)
    temp = float(result.stdout.split('=')[1].split("'")[0])
    
    # Disk usage
    result = subprocess.run(['df', '/'], capture_output=True, text=True)
    lines = result.stdout.split('\n')
    disk_usage = int(lines[1].split()[4].rstrip('%'))
    
    return {
        'temp': temp,
        'disk': disk_usage,
    }

def status_to_color(status):
    """Convert status to color"""
    if status['temp'] > 80:
        return (255, 0, 0)          # Red: Overheating
    elif status['disk'] > 90:
        return (255, 165, 0)        # Orange: Disk full
    elif status['temp'] > 70:
        return (255, 255, 0)        # Yellow: Warm
    else:
        return (0, 255, 0)          # Green: OK

try:
    while True:
        status = get_system_status()
        color = status_to_color(status)
        pixels.fill(color)
        pixels.show()
        print(f"Temp: {status['temp']}°C, Disk: {status['disk']}%")
        time.sleep(30)
        
except KeyboardInterrupt:
    pixels.fill((0, 0, 0))
    pixels.show()

Music Reactive Lights

Audio Visualization

#!/usr/bin/env python3
import board
import neopixel
import numpy as np
from pyaudio import PyAudio
import time

pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.7)

# Audio setup
p = PyAudio()
stream = p.open(format=8,
                channels=1,
                rate=44100,
                input=True,
                frames_per_buffer=1024)

try:
    while True:
        # Read audio data
        data = np.frombuffer(stream.read(1024), dtype=np.uint8)
        
        # Calculate frequency bands
        fft = np.fft.fft(data)
        magnitude = np.abs(fft[:512])
        
        # Map frequencies to LED positions
        for i in range(30):
            freq_band = int(magnitude[i * 17] / 256 * 255)
            
            # Create color from frequency
            hue = int(i * 12 % 360)  # Spectrum of colors
            pixels[i] = hsv_to_rgb(hue, 255, freq_band)
        
        pixels.show()
        
except KeyboardInterrupt:
    stream.stop_stream()
    stream.close()
    p.terminate()
    pixels.fill((0, 0, 0))
    pixels.show()

def hsv_to_rgb(h, s, v):
    """Convert HSV to RGB"""
    import colorsys
    r, g, b = colorsys.hsv_to_rgb(h/360, s/255, v/255)
    return (int(r*255), int(g*255), int(b*255))

Web Interface for Control

Flask App

#!/usr/bin/env python3
from flask import Flask, render_template, request, jsonify
import board
import neopixel
import threading

app = Flask(__name__)
pixels = neopixel.NeoPixel(board.D18, 30, brightness=0.8)

current_color = [255, 255, 255]

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/color', methods=['GET', 'POST'])
def color():
    global current_color
    if request.method == 'POST':
        data = request.json
        current_color = [data['r'], data['g'], data['b']]
        pixels.fill(tuple(current_color))
        pixels.show()
        return jsonify({'status': 'ok'})
    return jsonify({'color': current_color})

@app.route('/api/brightness', methods=['POST'])
def brightness():
    data = request.json
    pixels.brightness = data['value'] / 100
    pixels.show()
    return jsonify({'status': 'ok'})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)

HTML Template

<!DOCTYPE html>
<html>
<head>
    <title>LED Control</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; }
        .control { margin: 20px 0; }
        input[type="color"] { width: 200px; height: 50px; }
        input[type="range"] { width: 100%; }
    </style>
</head>
<body>
    <h1>LED Control Panel</h1>
    
    <div class="control">
        <label>Color:</label>
        <input type="color" id="colorPicker" value="#ffffff">
    </div>
    
    <div class="control">
        <label>Brightness:</label>
        <input type="range" id="brightness" min="0" max="100" value="80">
        <span id="brightnessValue">80%</span>
    </div>
    
    <script>
        document.getElementById('colorPicker').addEventListener('change', (e) => {
            const hex = e.target.value;
            const r = parseInt(hex.substr(1,2), 16);
            const g = parseInt(hex.substr(3,2), 16);
            const b = parseInt(hex.substr(5,2), 16);
            
            fetch('/api/color', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({r, g, b})
            });
        });
        
        document.getElementById('brightness').addEventListener('input', (e) => {
            const value = e.target.value;
            document.getElementById('brightnessValue').textContent = value + '%';
            
            fetch('/api/brightness', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({value})
            });
        });
    </script>
</body>
</html>

Troubleshooting

LEDs Not Lighting

# Check GPIO permissions
ls -la /dev/gpiomem
sudo usermod -a -G gpio pi

# Verify power connection
# Use multimeter: should read ~5V across power pins

Colors Look Wrong

Noise/Interference

Performance Tips

# Avoid repeated operations
# BAD: inside loop
import time

# GOOD: move outside
start = time.time()

# Batch updates
pixels[0:10] = [(255, 0, 0)] * 10  # Set multiple at once
pixels.show()  # Only update once

Power Calculation

Each LED maximum: 60mA (at full white)
30 LEDs: 30 × 60mA = 1.8A

Safe: 2A power supply minimum
Recommended: 3A+ for brightness headroom

Advanced Projects

  1. Spectrum Analyzer: Sync with music
  2. Room Occupancy Indicator: Motion → color
  3. Weather Display: Forecast → LED color
  4. Gaming Light Sync: Response to game events
  5. Clock Display: Time represented as colors

Conclusion

Addressable LEDs on Raspberry Pi Zero create amazing visual effects with minimal components. Start simple with solid colors and progress to complex animations.

Resources