Adding Color

The Adafruit NeoPixel (WS2812) is an RGB LED that you can program via a serial protocol. We show you how to control the LED with a few lines of MicroPython.

The WS2812, also known as the Adafruit NeoPixel, is a programmable RGB light-emitting diode (LED). It consists of a controller and three LEDs in the colors red, green, and blue. There is a data byte in the controller for the brightness of each individual LED. This means that a NeoPixel can display up to 16,777,216 colors. NeoPixels are available both as a single component and pre-installed on various carriers. The best-known forms include the LED strip, the matrix, and rings of various sizes.

Each NeoPixel has four connectors, two of which are for the 5V operating voltage. The other two connectors DI (Data In) and DO (Data Out) are used for asynchronous serial data transmission. The output (DO) of one WS2812 can be connected to the input (DI) of the next WS2812, so in theory, you can create an arbitrarily long chain of NeoPixels. One thing you need to bear in mind, though, is that data transmission can take a while. If it takes too long, the LEDs will start to flicker. But if you have 1,024 LEDs in series, it is still possible to supply them with data 30 times per second, and flicker is undetectable for us humans at this frame rate.

The signal for controlling the LEDs has a very special structure: The individual data bits are coded with a pulse width modulation (PWM) signal. The pulse-pause ratio of the signal defines whether it is a one or a zero. This method reduces susceptibility to interference. To avoid overloading this text with technical details, I’ll just point you in the direction of the WS2812 data sheet instead. It describes precisely how the protocol works, and it has the full details of the WS2812.

The brighter the LEDs light up, the more current they require. The current flow generates a voltage drop. With LED strips, this is very noticeable due to differences in brightness. If possible, you will want to feed in the operating voltage at several points or generally reduce the brightness. Note that each of the three LEDs can draw up to 20mA of current. In other words, a single WS2812 has a maximum current draw of 60mA, although you’ll rarely see this value in practice.

Test Setup

My test setup consists of an ESP32 DevKit and two LED rings with 32 LEDs each (Figure 1). The ESP32 runs at 3.3V, and the WS2812 requires 5V. If you compare the data sheets of the two semiconductors, it looks like the voltage of the ESP32 would normally be too low to drive the LED, but my practical experience is that it usually works all the same. You will find numerous examples that prove this on the Internet.

b01_neopixel-ring.tif
Figure 1: My test setup consists of an ESP32 DevKit and two LED rings.

To be on the safe side, I will add a buffer (74HC125) to adjust the voltage levels of the two semiconductors in my sample project. Again, if you want to dig deeper into the details, you can check the data sheet for this chip. Figure 2 shows its pinout, which should help you with the physical build steps. Figure 3 shows the complete circuit.

b02_74HC125-pinout.tif
Figure 2: The pinout of the 74HC125 helps with the physical build in this project.
b03_project-diagram.tif
Figure 3: The entire circuit diagram of the test setup is not too complex.

I’ve already mentioned the power requirements of the WS2812. For my test setup, I have to plan for a maximum current of 64 times 60mA = 3.84A in total: definitely too much for a USB interface. That is why I wrote the test programs so as to avoid getting into this range. If you really do want all 64 LEDs to light up at full intensity, you will need an additional power supply.

Programming

I originally intended to use the Mu IDE for this project, but the project is no longer maintained, and the last available version 1.2.0 does not want to play ball with the current version of MicroPython (1.23.0). After an hour of trial and error, I decided to switch to Thonny, which worked as expected straight away.

Back to the NeoPixels and the need for precise timing to control them. This is virtually impossible with plain vanilla Python, but MicroPython has an internal library that gets the job done. There is a class in the library that returns a NeoPixel object. To create the object, you need the pin to which the NeoPixel is connected and the number of individual pixels. The object primarily consists of an array that matches the number of LEDs. You can store an RGB color (red, green, blue) as a 3-tuple at each index in this array. Once that’s done, you need to transfer the array to the physical LEDs via the object’s write() method. Listing 1 shows a simple example that activates the first three LEDs and lets them display full red (255,0,0), full green (0,255,0), and full blue (0,0,255), respectively; the next three LEDs show the same colors but with a sixteenth of the intensity, which still makes them very visible.

Listing 1: Simple Example

import machine, neopixel
np = neopixel.Neopixel(machine.Pin(26), 64)
np[0] = (255, 0, 0)
np[1] = (0, 255, 0)
np[2] = (0, 0, 255)
np[3] = (16, 0, 0)
np[4] = (0, 16, 0)
np[5] = (0, 0, 16)
np.write()

Infinite Loop

Listing 2 contains a slightly more complex example in which a single dot whizzes through the two rings. I have arranged them in the shape of the infinity symbol. The tricky thing is that the LED rings all have the same structure. If you connect them as shown in Figure 1, the dots need to rotate in two different directions: First, you need to actuate LEDs 0 to 31 counterclockwise and then LEDs 63 to 32 clockwise on the second ring. To achieve this, you can distinguish the two cases with an if statement (line 5 of Listing 2) and compute the matching values from the loop variable i.

Listing 2: Looping Around

01 import machine, neopixel,time
02 np = neopixel.Neopixel(machine.Pin(26), 64)
03 while(True):
04   for i in range (64):
05     if (i<32):
06       np[i%64] = (0, 0, 255)
07     else:
08       np[((95‑i)%64)] = (0, 0, 255)
09     np[(i‑1)%64] = (0, 0, 0)
10     np[((95‑i+1)%64)] = (0, 0, 0)
11     time.sleep(0.02)
12     np.write()

Lines 9 and 10 switch the LEDs off again by writing all-zero tuples (0,0,0). The modulo operation (%64) ensures that the program only uses the values from 0 to 63 to address the array. Finally, the entire logic needs to be wrapped in an infinite loop while(True): (line 3). The speed is controlled with the time.sleep(0.02) command (line 11).

Conclusions

Thanks to the library integrated in MicroPython, controlling NeoPixels is as easy as pie. Various ready-made modules (stripe, ring, or matrix) are available on the market. Properly addressing the individual LEDs can be tricky. I think that NeoPixels can enrich many projects, as they open up a wide range of possibilities.