
I’ve had a Sipeed I2S mic array board for a while now, and I am just starting to play with it. Before I get to the fun part of extracting data from the mic array, I thought I’d take a stab at lighting up the SK9822 LED ring that’s on the board. I am going to use the Pico 2W for this project, because that’s my favourite embedded platform these days. I built Mico, a PDM to USB microphone with it, as well as prototyped a Lattice iCE40 FPGA board with it. I’ve also used it for commercial consulting projects. I love the architecture of the RP2040 and the new RP2350 chips – especially the PIO peripheral, and the way in which the parent company Raspberry Pi has been supporting development on their hardware.
The SK9822 Protocol
Unlike the WS2812B LEDs which have a finicky 1-wire protocol, SK9822 uses a sensible 2-wire CLK/DATA protocol similar to SPI. Here’s the expected signal from the datasheet.

The LED data has to be sent sandwiched between 32-bit start and end frames, as shown in the figure below from the datasheet. Pololu has a note that suggests a different end frame format for long strips. In my case, with 12 LEDs, following the spec seems to work fine.

You need as many middle frames as the number of LEDs in your strip, and each 32-bit middle frame is formatted as follows:
- First 3 bits are `111`
- Next 5 bits represent brighness value
- 8 bits of blue
- 8 bits of green
- 8 bits of red
The data is sent MSB first.
The PIO Program
Now let’s look at the star of our show, the PIO program.
.program sk9822
.side_set 1
.wrap_target
set y, 31 side 0 ; set a counter for shifting 32 bits, and set clk to 0
bitloop:
out pins, 1 side 0 ; shift out bit from OSR, and set clk to 0
jmp y-- bitloop side 1 ; jmp if y-- > 0, and set clk to 1
.wrap
The PIO program and the SM (State Machine) it runs on are configured as follows:
- One SIDE GPIO pin for sneaky side instructions that execute in the same cycle.
- One OUT GPIO pin to send out data from the OSR (output shift register).
- autopull so we don’t have to do a pull in the code.
- Program wrapping, the magic that avoids a jmp instruction for looping the program.
Here’s what the PIO code above does:
The code shifts 32 bits from the OSR, one bit at a time, and sends it to the DATA GPIO pin. The OSR data is auto-pulled from TX FIFO, which in turn is filled by C code with one 32-bit frame at a time. The CLK value is set so that it is high (1) when the DATA bit value is current on the GPIO.
The PIO configuration I mentioned is all done by the C code below, which is embedded right into sk9822.pio:
% c-sdk {
static inline void sk9822_program_init(PIO pio, uint sm, uint offset,
uint pin_clk, uint pin_data) {
// get config
pio_sm_config c = sk9822_program_get_default_config(offset);
// set OUT pins
sm_config_set_out_pins(&c, pin_data, 1);
// set SIDE pins
sm_config_set_sideset_pins(&c, pin_clk);
// set auto pull
sm_config_set_out_shift(&c, false, true, 32);
// set pin dirs
pio_sm_set_consecutive_pindirs(pio, sm, pin_clk, 2, true);
// init gpio
pio_gpio_init(pio, pin_clk);
pio_gpio_init(pio, pin_data);
// set clock div
sm_config_set_clkdiv(&c, 6250);
// Load our configuration, and jump to the start of the program
pio_sm_init(pio, sm, offset, &c);
// Set the state machine running
pio_sm_set_enabled(pio, sm, true);
}
%}
In the above code, I am using a clock divider of 6250. If you look at the loop above, you’ll see that it takes 2 instructions for a CLK cycle. With the Pico 2W running at 150 MHz, our output CLK frequency is 150 * 1000000 / (6250 * 2) = 12kHz. The rate at which you can update your LED strip is limited by CLK and the number of LEDs in your strip. In my case, I need 32 bits * (12 + 2) * 1/(12000) = 37.3 milliseconds to update the strip.
If you are new to PIO, please read the corresponding sections in the RP2350 datasheet and the Pico C SDK documentation from Raspberry Pi. They are excellent and have many examples which will help you understand how the PIO peripheral works.
The C Code
I’ll just go through the most relevant parts of the code here. You can see the whole code in the GitHub link in Downloads.
Here’s the C code that creates a 32-bit LED frame.
// create a 32 bit frame
uint32_t create_frame(uint8_t brightness, uint8_t red, uint8_t green, uint8_t blue) {
return (0b111 << 29) | // 3 bits '111' at the MSB
((brightness & 0x1F) << 24) | // 5-bit brightness (masked)
((blue & 0xFF) << 16) | // 8-bit blue
((green & 0xFF) << 8) | // 8-bit green
(red & 0xFF); // 8-bit red
}
Here’s the code that sends the frames needed to light up the LEDs for one cycle.
// send start + 12 x colors + end frame
void send_frames()
{
// sent start frame
pio_sm_put_blocking(pio, sm, 0);
// middle frames
for (size_t i = 0; i < NLEDS; i++) {
uint32_t frame = create_frame(brightness, colors[i][0], colors[i][1], colors[i][2]);
pio_sm_put_blocking(pio, sm, frame);
}
// send end frame
pio_sm_put_blocking(pio, sm, 0xffffffff);
}
The above code sends a starting frame of 0
s, followed by NLEDS frames of color, followed by an end frame of 1
s. pio_sm_put_blocking()
puts a 32-bit value into the TX FIFO, which our PIO autopull configuration will put into the OSR.
Now to create some patterns. This one is sort of a direction indicator with a red in the center, flanked by two yellow on both sides, with cyan for the rest, and the whole thing rotates.
// send an LED pattern
void send_pattern()
{
// fill with c3
for (size_t i = 0; i < NLEDS; i++) {
memcpy(colors[i], col3, 3);
}
// copy c1
memcpy(colors[colIndex], col1, 3);
// copy c2
memcpy(colors[(colIndex + 1) % NLEDS], col2, 3);
memcpy(colors[(colIndex + 2) % NLEDS], col2, 3);
memcpy(colors[(colIndex - 1) % NLEDS], col2, 3);
memcpy(colors[(colIndex - 2) % NLEDS], col2, 3);
// incr
colIndex = (colIndex + 1) % NLEDS;
// update colors
send_frames();
}
Note that the values latch. So once you send some colors, they will remain on the LEDs till you do an update.
Hardware Hookup
Here’s how I hooked up the hardware. I used a Pico 2W board, but this project should work with any Pico board.
Pico 2W | Sipeed Mic Array |
GP14 | LED_CK |
GP15 | LED_DA |
5V | VIN |
GND | GND |
The Sipeed Mic Array has a built-in regulator and level converter, so you can hook up power to 5V and GND of the Pico. If you are using a separate SK9822 LED strip, make sure that the strip is powered using 5V, and that CLK and DATA lines pass through a 3V3 to 5V level converter.
Output
Here’s the DATA and CLK output from a logic analyzer. You can see that it matches the datasheet.

Downloads
The code for the project is available at the link below:
https://github.com/mkvenkit/electronut_blog/tree/main/pico_sk9822
The project was created using the official Raspberry Pi Pico Visual Studio Code extension, so once you have that setup, you should be able to load the above code as a project directly. I also used the Raspberry Pi Debug Probe for debugging and flashing the Pico board. This is a must-have tool if you work with Pico boards.
References
- SK9822 Datasheet
- Pico C SDK Documentation
- RP2350 datasheet
- Sipeed Mic Array Datasheet