electronut Programming & Embedded Systems - by Mahesh Venkitachalam

Driving WS2812B LEDs using I2S on the Nordic nRF52832 BLE SoC


It’s hard not to like a project with blinking LEDs. Red, Green, Blue, Yellow,… and then there are RGB LEDs where you have three lines to control the colour. In 2010 WorldSemi launched the WS2812 - an RGB LED and driver chip integrated into one package, which could be daisy-chained to any number of other similar LEDs, and here’s the best thing - you could control all of them with a single wire. Thus the internet exploded with a colourful overabundance of LED projects.

In this project, we will drive WS2812B (the improved WS2812) using the Nordic nRF52832, and control the output using a smartphone.

But what about the umpteen number of WS2812 libraries and examples that already exist, you ask. Well the difference here is that we will use (or misuse, rather) the I2S (Inter-IC Sound) peripheral of the nRF52832 to do it.

Note: This work is inspired by that of Takafumi Naka (Japanese article). I have only taken the idea; the research and code is all mine.

Driving WS2812Bs

Driving WS2812B LEDs requires only one data line, but it’s quite sensitive to the timing of the signals on that line. Here’s relevant information which I gleaned from the data sheet.

WS2812B signals

So the LEDs require specific waveforms for ON and OFF. These are grouped together as 8 “bits” per channel, and hence 24 per LED, multiplied by N, the number of LEDs in the daisy chain. To send the next frame to the LEDs, you need to sent the RESET signal, which is just a LOW pulse greater than 50 microseconds.

Now, let’s look at the I2S protocol.

The I2S Protocol

The I2S (Inter-IC Sound) was developed by Philips Semiconductor in 1986, and it’s a means of transimitting digital audio between devices. The Nordic nRF52832 comes built-in with an I2S peripheral, which is what we are trying to leverage. Here’s an I2S signal diagram example from the nRF52832 data sheet.


So, as you can see above, there are four relevant signals - SCK (serial clock), LRCK (left right clock), SDIN (input data) and SDOUT (ouput data). SCK pulses once for each bit of data transferred on SDIN/SDOUT, and LRCK is used to select right/left channels (remember this is a protocol for sound). In addition, there is the MCK (master clock) which is used to generate SCK and LRCK when we operate in Master mode (our case).

So all we have to do is generate the correct WS2812B waveform on SDOUT. We do so by setting up I2S as follows:

  • MCK to 3.2 MHz (period 0.3125 us)
  • 16 bit stereo
  • LRCK = MCK / 32
  • SCK = 2 * LRCK * 16

Now, we represent a WS2812B ON/OFF “bit” as 4 actual bits, where each bit is a pulse of SCK. ON is 1110 and OFF is 1000. You can see above that 0.3125 us falls within the 0.4 us +- 0.15 us pulse width dictated by the WS2812B datasheet.

Now let’s see how to hook the hardware up.

Hardware Hookup

We need an nRF52832 board, and in this case we are using our beautiful Bluey nRF52 dev board. We’ll use a 16 LED ring of WS2812Bs, and the other piece of hardware we need is a level shifter on the SDOUT line.

level shifter

The above is required because the nRF52832 runs at 3.3 V and the WS2812B data line needs atleast 3.5 V to work reliably.

In our case, SDOUT is on P0.27, and although you don’t need to hook these up, you can see SCK on P0.31 and LRCK on P0.30.

Here’s what these signal looks like in the real world, by the way:

I2S logic

Channel 1, 2 and 3 above show SDOUT, LRCK and SCK respectively. Now, onwards to the code.

The Code

The code is setup to do the following. We use the Nordic UART service to control start/stop of I2C as well as the colour of the LEDs. The Nordic SDK has a driver for the I2S peripheral, and we will make use of it.

First, here are the global I2S buffers and related variables:

Here’s the I2S initialisation:

We don’t need to setup an Rx buffer in the above call. The data_handler is defined as follows:

Why aren’t we doing anything in the handler above? Well, sometimes Nordic’s APIs are a bit confusing. In this case they’ve implemented a scheme where only half the Tx buffer is transmitted in one shot and they keep flipping the pointers. This is not what the undelying I2S hardware does and seems unnecessary. (The hardware does implement double buffering which to my understanding is something entirely different.) So to simplify the code, I just modify the Tx buffer as I need in the main loop by calling a set_led_data function.

In the above code, the Tx buffer is being set according to the pattern we want the LEDs to flash in. Here’s the implementation of caclChannelValue:

The above function sets up a 32 bit value for a channel (R/G/B). A channel has 8 x 4-bit codes. Code 0xe is HIGH and 0x8 is LOW. So a level of 128 would be represented as 0xe8888888. At the end, the 16 bit values need to be swapped because of the way I2S sends data - right/left channels. So for the above example, final value sent would be 0x8888e888.

And here’s the main loop:

In the above code, I2S is started and stopped, and the colour set based on a flags which are set in the
NUS (Nordic UART Serice) handler below.

Now let’s see how this all works.

In Action

To test our code, we use the Nordic nRFToolbox app. Set up the UART keypad as follows:

Key Value (string)
1 1
2 2
3 3
[ ] S
> P

You can see it in action here:


There you have it - a fun misuse of the I2S peripheral to drive some LEDs!


You can download the code for this project from the git repo below:


If you liked this article, please consider supporting my efforts by purchasing my book.

Python Playground, published by No Starch Press, USA, is a collection of imaginative programming projects that will inspire you to use Python to make art and music, build simulations of real-world phenomena, and interact with hardware like the Arduino and Raspberry Pi. Do check it out!