Raspberry Pi Pico & Little Light Stick (v1)

A couple of months after its release, ultimately, I got a hand on the Raspberry Pi Pico, and posted a few hobby projects here. The RPi Pico is a microcontroller board centered on the RP2040 which’s in fact a microcontroller chip designed by Raspberry Pi that features a dual-core Arm Cortex-M0+ processor with 264KB internal RAM and support for up to 16MB of off-chip Flash (https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf).

Looking for a new Rpi Pico project idea? This tutorial is a simple guide on building a fancy light stick with Raspberry Pi Pico!

Light Stick Lead

Before anything else, let me say this is a portable electronics project. This simply means the design is proposed to be put inside a translucent homemade or 3D printed shell with or without a built-in battery pack. If you opt for the non-battery version, simply use an external USB power supply as the power source.

Since the RPi Pico power supply option is also user-friendly, you can power your RPi Pico from 5V through micro-USB but it also accepts voltages from 1.8V up to 5.5V using an RT6150 based dc-dc buck-boost converter on its VSYS pin. This also allows the use of a rechargeable lithium battery or just a set of two (or three) NiMH batteries.

RPi Pico View

The below figure from the datasheet (https://datasheets.raspberrypi.org/pico/pico-datasheet.pdf) shows the options to connect several power sources.

Pi Pico Power ORing

Right now, keep it in mind for consideration that the simplest way is to plug in the micro-USB, which will power VSYS (and therefore the system) from the USB 5V (VBUS), so VSYS becomes VBUS minus the Schottky diode (D1) drop. If the USB port is the only power source, VSYS and VBUS can be safely shorted together to eliminate the Schottky diode drop to improve efficiency and reduce ripple on VSYS. If the USB port is not going to be used, it is safe to connect VSYS to your preferred power source (V) preferably through a Schottky diode (D).

Back to the main theme, apart from the RPi Pico board, a WS2812-8 addressable RGB LED (also known as NeoPixel) stick is all that you needed to follow this project. Below you will see the basic hardware setup diagram.

Pico Pixel Setup

Now create a new script in Thonny and paste into the following code.

import array, time
from machine import Pin
import rp2

#  Number of WS2812 LEDs
NUM_LEDS = 8
PIN_NUM = 22
brightness = 0.2

@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()


# Create the StateMachine with the WS2812 program, outputting on pin
sm = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM))

# Start the StateMachine - It will wait for data on its FIFO.
sm.active(1)

# Display a pattern on the LEDs via an array of LED RGB values
ar = array.array("I", [0 for _ in range(NUM_LEDS)])

#####################################################
def pixels_show():
    dimmer_ar = array.array("I", [0 for _ in range(NUM_LEDS)])
    for i,c in enumerate(ar):
        r = int(((c >> 8) & 0xFF) * brightness)
        g = int(((c >> 16) & 0xFF) * brightness)
        b = int((c & 0xFF) * brightness)
        dimmer_ar[i] = (g<<16) + (r<<8) + b
    sm.put(dimmer_ar, 8)
    time.sleep_ms(10)

def pixels_set(i, color):
    ar[i] = (color[1]<<16) + (color[0]<<8) + color[2]

def pixels_fill(color):
    for i in range(len(ar)):
        pixels_set(i, color)

def color_chase(color, wait):
    for i in range(NUM_LEDS):
        pixels_set(i, color)
        time.sleep(wait)
        pixels_show()
    time.sleep(0.2)

def wheel(pos):
    # Input a value 0 to 255 to get a colour value
    # The colour transitions loop R-G-B
    if pos < 0 or pos > 255:
        return (0, 0, 0)
    if pos < 85:
        return (255 - pos * 3, pos * 3, 0)
    if pos < 170:
        pos -= 85
        return (0, 255 - pos * 3, pos * 3)
    pos -= 170
    return (pos * 3, 0, 255 - pos * 3)


def rainbow_cycle(wait):
    for j in range(255):
        for i in range(NUM_LEDS):
            rc_index = (i * 256 // NUM_LEDS) + j
            pixels_set(i, wheel(rc_index & 255))
        pixels_show()
        time.sleep(wait)

BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)

print("fills")
for color in COLORS:
    pixels_fill(color)
    pixels_show()
    time.sleep(0.2)

print("chases")
for color in COLORS:
    color_chase(color, 0.01)

print("rainbow")
rainbow_cycle(0)

If you haven’t already set up Thonny for programming your RPi Pico, do it first by going back to the previous RPi Pico articles published here. And then, make any changes you find necessary and run the final script. Your LEDs should render a few colorful patterns. Hope you get it right!

Array Issue MicroPython

If you see the error message “no module named array”, try the latest firmware for RPi Pico (https://micropython.org/resources/firmware/rp2-pico-20210806-unstable-v1.16-160-ga3675294a.uf2). Also, go through this thread to get some quick hints https://www.raspberrypi.org/forums/viewtopic.php?t=303233

By this point, there are a few other important arguments to be aware of:

  • NUM_LEDs is the number of LEDs in your WS2812 LED stick
  • The DI (data input) pin is the control pin for the WS2812-8 LED stick. Likewise, DO (data output) pin on the same LED stick allows you to daisy-chain more WS2812-8 LED sticks, not tried in this project though
  • PIN_NUM is the RPi GPIO pin for the data input of the WS2812 LED stick

Not to mention that but RPi Pico is a 3.3V microcontroller. So, there might be a communications mismatch when one of its data pins tries to talk to the NeoPixel’s data input, and results in a miscellany of odd behaviors, including incorrect colors, flickering, dimness, and seemingly dead pixels. A “logic level converter” is a cheerful solution to this problem as it listens to the 3.3V microcontroller data, translates it to a 5V-logic compatible message, and passes that along to the 5V NeoPixel

Logic Level Converter 3v3 5v 2Channell

Most of the time the WS2822-8 LED stick will work without a logic level converter, and if that does not work, add one logic level translator to your light stick circuitry. Below is a bidirectional single-channel logic level converter circuit that seems to be suitable for this purpose. If you do not want to buy a prewired module, do it yourself with a 2N700x or BS170 MOSFET!

LLC Schematic v1

This is a very simple and extremely useful bit of electronics. When the low side (RPi Pico) outputs a ‘HIGH’ (3V3) the MOSFET is off. So, the high side (WS2812) pullup is active and pulls the high side to 5V. And, when the low side outputs a ‘LOW’ (0V) the MOSFET is on. The MOSFET then conducts to pull down the high side. How is it?

Rpi Pico Pixel Light Stick

Ultimately, I prepared this entry-level article to respond to a couple of followers on Facebook who asked me how I got Pixel LEDs to work with Raspberry Pi Pico since they couldn’t. That was just after I posted a random lab snap on my Facebook page. In not much time I’d managed to wire up the entire setup, and I had a thing that worked.

Rpi Pico Pixel Light Stick

Postscript

Here’s another code for the same RPi Pico Light Stick setup. As before, this code is also pretty simple and flexible. Enjoy!

import array, time
from machine import Pin
import rp2

#
############################################
# RP2040 PIO and Pin Configuration
############################################
#
# WS2812 LED Stick Configuration
led_count = 8 # Total NeoPixels
PIN_NUM = 22 # NeoPixel DI Pin
brightness = 0.2 # 0.1 = darker, 1.0 = brightest

@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT,
             autopull=True, pull_thresh=24) # PIO configuration

# define WS2812 parameters
def ws2812():
    T1 = 2
    T2 = 5
    T3 = 3
    wrap_target()
    label("bitloop")
    out(x, 1)               .side(0)    [T3 - 1]
    jmp(not_x, "do_zero")   .side(1)    [T1 - 1]
    jmp("bitloop")          .side(1)    [T2 - 1]
    label("do_zero")
    nop()                   .side(0)    [T2 - 1]
    wrap()


# Create the StateMachine with the ws2812 program, outputting on pre-defined pin
# @ 8MHz frequency
state_mach = rp2.StateMachine(0, ws2812, freq=8_000_000, sideset_base=Pin(PIN_NUM))

# Activate the state machine
state_mach.active(1)

# Range of LEDs stored in an array
pixel_array = array.array("I", [0 for _ in range(led_count)])
#
############################################
# Functions for RGB Color
############################################
#
def update_pix(brightness_input=brightness): # Dimming colors and updating state machine (state_mach)
    dimmer_array = array.array("I", [0 for _ in range(led_count)])
    for ii,cc in enumerate(pixel_array):
        r = int(((cc >> 8) & 0xFF) * brightness_input) # 8-bit Red dimmed to brightness
        g = int(((cc >> 16) & 0xFF) * brightness_input) # 8-bit Green dimmed to brightness
        b = int((cc & 0xFF) * brightness_input) # 8-bit Blue dimmed to brightness
        dimmer_array[ii] = (g<<16) + (r<<8) + b # 24-bit colour dimmed to brightness
    state_mach.put(dimmer_array, 8) # Update the state machine with new colors
    time.sleep_ms(10)

def set_24bit(ii, color): # set colors to 24-bit format inside pixel_array
    pixel_array[ii] = (color[1]<<16) + (color[0]<<8) + color[2] # Set 24-bit color

#
############################################
# Main Loops and Calls
############################################
#
color = (255,0,0) # Looping color
blank = (125,125,0) # Color for other pixels: Now Dim Yellow
cycles = 12 # Number of times to cycle 360-degrees
for ii in range(int(cycles*len(pixel_array))+1):
    for jj in range(len(pixel_array)):
        if jj==int(ii%led_count): # In case we go over number of pixels in array
            set_24bit(jj,color) # Colour and loop a single pixel
        else:
            set_24bit(jj,blank) # Turn others OFF
    update_pix() # Update pixel colors
    time.sleep(0.05) # Halt 50ms

Sources of Inspiration: https://core-electronics.com.au/, https://realpython.com/, https://makersportal.com/, https://www.settorezero.com/wordpress/

Leave a Reply

Your email address will not be published. Required fields are marked *