empowering creative people

WS2812 / NeoPixel Addressable LEDs: Pycom Quickstart Guide

Sometimes all a good Pycom project needs is some colourful lights! This tutorial demonstrates how to control WS2812 LEDs with the Pycom Wipy and Lopy4. WS2812 or WS2812B LEDs (otherwise known as NeoPixels) are the most commonly used RGB LEDs on the market, because of their low cost and ease of use. This MicroPython Library for the WS2812 will allow you to finally shine some colourful light on your Pycom Project!

Parts Required for this project:

Creating the Circuit

Pycom boards use 3.3V logic, and WS2812 LEDs use 5V logic. We have found that WS2812 will often run with 3.3V data. The same goes for powering the LEDs, they are made to run on 5V, but will happily run on 3.3V power (just a bit dimmer).

We have made our circuit using a Logic Level Shifter to convert the 3.3V signal into a 5V signal that is more ideal for the LEDs. Go ahead and try to wire your LEDs directly to your Pycom board without the level shifter and see if it works, any flickering or unusual LED behaviour would indicate that your LEDs require a level shifter.

For testing, we will be powering the Pycom and LEDs from the USB port, but in practice, you always want to power your LEDs directly from the power supply, so you don’t draw too much current through the Expansion Board.  WS2812/NeoPixels draw about 60mA per LED when white and at full brightness. Under normal conditions, they will typically draw about 20mA per LED. This adds up quite a lot of current very quickly, be sure to use an appropriate power supply.

Pycom-ws2812-neopixel-wiring-schematic

The Code

You can find the latest version of this code at our GitHub Repository:

Clone directly into Atom with this link: https://github.com/CoreElectronics/pycom-ws2812.git

The library is based on the MicroPython Library created by @JanBednarik, with Lopy functionality added by @aureleq. We updated the library to work with the Lopy4 and current versions of the Wipy.

We then went one step further and included a bunch of animation functions in the main file to make it easier to get dynamic lighting effects from your LEDs. People who have used the NeoPixel Library for Arduino will find these very familiar!

Set-Up and Test

First, change the numLed to reflect the number of LEDs you are driving, then set the brightness and the pin that the LEDs are connected to. If you are using the Wipy then it must be pin “P11” (G22 on the Expansion Board). On the Lopy4 you are able to change the pin assignment to other pins.

# set the number of LEDs
numLed = 43
# initialize LEDs chain = WS2812( ledNumber=numLed, brightness=10, dataPin='P11' ) # dataPin is for LoPy board only

main.py Code

from ws2812 import WS2812
import utime
import uos
import pycom

# disable LED heartbeat (probably not needed in this application)
pycom.heartbeat(False)

# set the number of LEDs
numLed = 43
# initialize LEDs
chain = WS2812( ledNumber=numLed, brightness=10, dataPin='P11' ) # dataPin is for LoPy board only
# initialize LEDs Off data = [(0,0,0)] * numLed ####################################################### ### Animation Functions -- edit at your own risk! ### ### Scroll down for the Main Loop to edit ### ####################################################### # Input a value 0 to 255 to get a color value. # The colours are a transition r - g - b - back to r. # Do not change this! def Wheel(WheelPos): WheelPos = 255 - WheelPos if WheelPos < 85: return (255 - WheelPos * 3, 0, WheelPos * 3) if WheelPos < 170: WheelPos -= 85 return (0, WheelPos * 3, 255 - WheelPos * 3) WheelPos -= 170 return (WheelPos * 3, 255 - WheelPos * 3, 0) # Cycles all the lights through rainbow colors def rainbow(wait): for j in range (0,256,1): for i in range (0,numLed,1): data[i] = Wheel((i+j & 255)) chain.show( data ) utime.sleep_ms(wait) # Slightly different, this makes the rainbow equally distributed throughout def rainbowCycle(wait): for j in range (0,256,1): for i in range (0,numLed,1): data[i] = Wheel(int((i * 256 / numLed) + j) & 255) chain.show( data ) utime.sleep_ms(wait) # Fill the dots one after the other with a color def colorWipe(c, wait): for i in range(0, numLed, 1): data[i] = c chain.show( data ) utime.sleep_ms(wait) # Theatre-style crawling lights. def theaterChase(c, wait): for j in range(0, 10, 1): # do 10 cycles of chasing for q in range(0, 3, 1): for i in range(0, numLed, 3): try: data[i+q] = c # turn every third pixel on except: # if i+q is out of the list then ignore pass chain.show( data ) utime.sleep_ms(wait) for i in range(0, numLed, 3): try: data[i+q] = (0,0,0) # turn every third pixel off except: # if i+q is out of the list then ignore pass def theaterChaseRainbow(wait): for j in range(0, 256, 1): # cycle all 256 colors in the wheel for q in range(0, 3, 1): for i in range(0, numLed, 3): try: data[i+q] = Wheel((i + j) % 255) #Wheel( int((i+j)) % 255)) # turn every third pixel on except: # if i+q is out of the list then ignore pass chain.show( data ) utime.sleep_ms(wait) for i in range(0, numLed, 3): try: data[i+q] = (0,0,0) # turn every third pixel off except: # if i+q is out of the list then ignore pass # Fill the dots one after the other with a color def scrollWipe(wait): for j in range(0, 256, 16): # Transition through all colors of the wheel skip every 16 so the change is visible for i in range(0, numLed, 1): data[i] = Wheel((j) & 255) chain.show( data ) utime.sleep_ms(wait) # sparkle the LEDs to the set color def sparkle(c, wait): pixel = int.from_bytes(os.urandom(1), "big") % numLed pixel2 = int.from_bytes(os.urandom(1), "big") % numLed data[pixel] = c data[pixel2] = c chain.show( data ) utime.sleep_ms(wait) data[pixel] = (0,0,0) data[pixel2] = (0,0,0) # Fade the brightness up down and update a brightness parameter for other modes. def fade(c, wait): # Increases brightness for i in range(0, 50, 1): chain.set_brightness(i) # Slows brightness change when dim #if i

ws2812.py Code

# Based on https://github.com/JanBednarik/micropython-ws2812
# Adapted for LoPy by @aureleq, lopy4 updates by Stephen Haynes

import gc
from machine import SPI
from machine import Pin
from machine import disable_irq
from machine import enable_irq
from uos import uname

class WS2812:
    """
    Driver for WS2812 RGB LEDs. May be used for controlling single LED or chain
    of LEDs.

    Example of use:

        chain = WS2812( ledNumber=4, brightness=10, dataPin='P11' )
        data = [
            (255, 0, 0),    # red
            (0, 255, 0),    # green
            (0, 0, 255),    # blue
            (85, 85, 85),   # white
        ]
        chain.show(data)

    """
    # Values to put inside SPI register for each color's bit
    buf_bytes = (b'\xE0\xE0', # 0  0b00
                 b'\xE0\xFC', # 1  0b01
                 b'\xFC\xE0', # 2  0b10
                 b'\xFC\xFC') # 3  0b11

    def __init__(self, ledNumber=1, brightness=100, dataPin='P11'):
        """
        Params:
        * ledNumber = count of LEDs
        * brightness = light brightness (integer : 0 to 100%)
        * dataPin = pin to connect data channel (LoPy4 only)
        """
        self.ledNumber = ledNumber
        self.brightness = brightness

        # Prepare SPI data buffer (8 bytes for each color)
        self.buf_length = self.ledNumber * 3 * 8
        self.buf = bytearray(self.buf_length)

        # SPI init
        # Bus 0, 8MHz => 125 ns by bit, 8 clock cycle when bit transfert+2 clock cycle between each transfert
        # => 125*10=1.25 us required by WS2812
        if uname().sysname == 'LoPy4':
            self.spi = SPI(0, SPI.MASTER, baudrate=8000000, polarity=0, phase=1, pins=(None, dataPin, None))
             # Enable pull down (not working for LoPy4)
            # Pin(dataPin, mode=Pin.OUT, pull=Pin.PULL_DOWN)
        else: #WiPy
            self.spi = SPI(0, SPI.MASTER, baudrate=8000000, polarity=0, phase=1)
            # Enable pull down
            # Pin('P11', mode=Pin.OUT, pull=Pin.PULL_DOWN)

        # Turn LEDs off
        self.show([])

    def show(self, data):
        """
        Show RGB data on LEDs. Expected data = [(R, G, B), ...] where R, G and B
        are intensities of colors in range from 0 to 255. One RGB tuple for each
        LED. Count of tuples may be less than count of connected LEDs.
        """
        self.fill_buf(data)
        self.send_buf()

    def send_buf(self):
        """
        Send buffer over SPI.
        """
        disable_irq()
        self.spi.write(self.buf)
        enable_irq()
        gc.collect()

    def update_buf(self, data, start=0):
        """
        Fill a part of the buffer with RGB data.

        Order of colors in buffer is changed from RGB to GRB because WS2812 LED
        has GRB order of colors. Each color is represented by 4 bytes in buffer
        (1 byte for each 2 bits).

        Returns the index of the first unfilled LED

        Note: If you find this function ugly, it's because speed optimisations
        beated purity of code.
        """

        buf = self.buf
        buf_bytes = self.buf_bytes
        brightness = self.brightness

        index = start * 24
        for red, green, blue in data:
            red = int(red * brightness // 100)
            green = int(green * brightness // 100)
            blue = int(blue * brightness // 100)

            buf[index:index+2] = buf_bytes[green >> 6 & 0b11]
            buf[index+2:index+4] = buf_bytes[green >> 4 & 0b11]
            buf[index+4:index+6] = buf_bytes[green >> 2 & 0b11]
            buf[index+6:index+8] = buf_bytes[green & 0b11]

            buf[index+8:index+10] = buf_bytes[red >> 6 & 0b11]
            buf[index+10:index+12] = buf_bytes[red >> 4 & 0b11]
            buf[index+12:index+14] = buf_bytes[red >> 2 & 0b11]
            buf[index+14:index+16] = buf_bytes[red & 0b11]

            buf[index+16:index+18] = buf_bytes[blue >> 6 & 0b11]
            buf[index+18:index+20] = buf_bytes[blue >> 4 & 0b11]
            buf[index+20:index+22] = buf_bytes[blue >> 2 & 0b11]
            buf[index+22:index+24] = buf_bytes[blue & 0b11]

            index += 24

        return index // 24

    def fill_buf(self, data):
        """
        Fill buffer with RGB data.

        All LEDs after the data are turned off.
        """
        end = self.update_buf(data)

        # Turn off the rest of the LEDs
        buf = self.buf
        off = self.buf_bytes[0]
        for index in range(end * 24, self.buf_length):
            buf[index:index+2] = off
            index += 2

    def set_brightness(self, brightness):
        """
        Set brighness of all leds
        """
        self.brightness = brightness

Going Further

That’s all you need to get your LED project off the ground with Pycom! This is just an exhaustive example to help get you started though! If you want to learn more about Pycom, check out our Pycom Tutorials category, and turn your project into the IoT device it deserves to be! If you are using the Pycom Lopy4, then check out our The Things Network Tutorials. They walk you through everything you need to get connected to TTN using LoRaWAN!

Are you looking to add WS2812/NeoPixel LEDs to your other project but you aren't using Pycom? Check out our other guides to get you started on other hardware types:

Sometimes all a good Pycom project needs is some colourful lights! This tutorial demonstrates how to control WS2812 LEDs...

Have a question? Ask the Author of this guide today!