How to power loads of LEDs with a single Raspberry Pi Pico

Lovely Ben from HackSpace magazine shows us how to power up to 26 strips of NeoPixels from a single Raspberry Pi Pico.

WS2812B LEDs – sometimes known as NeoPixels – are a great way of adding colour to your project. They’re chainable RGB LEDs, which means that you can connect lots of them to a single GPIO pin on a microcontroller. If you’re adding a few tens of LEDs, this works really well. However, if you try to chain together too many, you get a few problems, including slow updates and the potential for one broken LED to stop your entire project working.

neopixels pico LED

However, even if you have spare GPIO pins, it’s not always possible to connect NeoPixels to additional GPIO pins. These LEDs speak a slightly unusual protocol, and it can be resource-intensive to send data out.

Fortunately, Raspberry Pi Pico ($4) has a trick up its sleeve. Its Programmable IO (PIO) lets you create state machines that can send data to external devices using almost any protocol with very little CPU overhead. You can send data to NeoPixels using state machines. Pico has eight state machines available, so you can send data to NeoPixels from eight GPIO pins. This is a bit better than just one, but what if we want to blaze all the lights. We have 26 GPIO pins; why can’t we connect a NeoPixel to each one?

neopixels pico LED

Well, we can! Graham Sanderson – one of the team of software engineers working on the PicoSDK – created a PIO program that can control strings of NeoPixels. We have adapted it slightly to make it easier to understand. If you just want to make LEDs flash, you can grab the code using this link.

The PIO program is deceptively simple:

.wrap_target
    out x, 32
    mov pins, !null [T1-1]
    mov pins, x     [T2-1]
    mov pins, null  [T3-2]
.wrap

The protocol is basically a continuous series of pulses. A short high followed by a longer low is a digital 0, while a long high followed by a short 0 is a logical 1.

It reads in 32 bits of data and then sends them to the GPIO pins. Each 32-bit word of data we send to this PIO program contains 1 bit of data for each of the chains of NeoPixels. Data is handled in 32-bit registers, so it’s not possible to make this an arbitrary number. It’s possible to set it to be some factor of 32 (such as 16), but this will affect the way data is stored.

The main code has to now create and manage a structure that can be used to send data in this form.

uint32_t pixels[NUM_PIXELS*24];

uint32_t set_pixel_colour(int pixel, int channel,
uint8_t r, uint8_t g, uint8_t b) {
   uint32_t colour_value = (b << 16 | r << 8 | g);

      for(int i=0; i<24;i++) {
       if (colour_value & (1u<<i)) {
           pixels[(pixel*24) + i] |= 1u << 
(channel);
       }
       else {
                     pixels[(pixel*24) + i]
&= ~(1u<<channel);
              }

      }
}

Here, we have a global variable called pixels that is an array 24 times the number of pixels (each pixel has 24 bits of data – eight each for red, green, and blue).

To set the colour value in this array, you have to loop through all 24 bits of the colour, and use bit-masks to set the right bit in each 32-bit value. This is done using either:

pixels[(pixel*24) + i] |= 1u << (channel);

…to set a bit high while leaving the other 31 bits unchanged, or

pixels[(pixel*24) + i] &= ~(1u<<channel);

…to set it low.

The |= and &= are used to OR or AND (respectively) the left-hand value with the right-hand value to apply the mask.

We need to fire out a lot of data. We could use the CPU to load data into the PIO state machine, but it would take a lot of the CPU’s time, and we can free this up by using Direct Memory Access (DMA). This is an off-CPU system that will shuttle data from one location to another – in our case, the pixels array to the PIO’s transmit state machine.

There’s a bit of code to set this up (and make sure the relevant interrupts are firing), but the main code to keep it running is:

int64_t dma_start() {
         dma_channel_set_read_addr(dma_chan, 
pixels, true);
         return 0;
}

void dma_handler() {
         dma_irqn_acknowledge_channel(DMA_
IRQ_0,dma_chan);

         add_alarm_in_us(RESET_TIME_US, dma_start, NULL, false);
}

Each time it gets to the end of the array, the DMA system raises an interrupt which is caught by dma_handler (see the main code to see how this is set up). We need to pause for 400 microseconds to reset the LED strip before sending more data. You can’t use sleep in interrupt handlers (or the program will crash), so we use an alarm to call another function at the appropriate time. The second function (dma_start()) just kicks off the DMA transfer again. These two functions will just keep shuffling the latest data out to the LEDs with almost no CPU resources used.

neopixels pico LED

In the main loop, you can set the pixels to be whatever colour you want, and this DMA/PIO system will update the LEDs as soon as possible.

This is an incredibly powerful way of controlling large numbers of LEDs in a way that’s quick and has some redundancy should the hardware in a single LED fail.

4 comments
Jump to the comment form

Avatar

I’ve been looking for this! I wanna put an RGB led ring around my 3d printer extruder. Think it would look really cool, thank you!

Reply to Geo

Avatar

This looks great. Can the Pico pass enough power to all these strips of LEDs to power them like the diagram, given a powerful enough power supply – or would you need to wire them to a separate +5v and Ground to get enough current?

Reply to Duncan

Avatar

For a large number you will need the separate power supply, I think each led needs 60milli amps at full brightness, so you could check the pico spec and see what the 5v can deliver.

Reply to Steve

Avatar

I made this ring to indicate that a landline phone is ringing at work. A very interesting thing. And all this can be done on mircopython if you use raspberry, or even on tiny85. And you can also save on LEDs and use Chinese sk6812

Reply to Oleg

Leave a Comment