· Nordic  · 8 min read

Controlling WS2812 LED Strips with SPI on nRF52832 using nRF Connect SDK

WS2812 LEDs, also known as NeoPixels, are popular for creating colorful lighting effects with just a single data line. The challenge lies in their strict timing requirements, which can be tricky to handle on microcontrollers. In this post, we’ll show how to control a WS2812 LED strip on an nRF52832 custom board using the nRF Connect SDK. By repurposing the SPI driver to generate the precise waveforms, we can drive the LEDs reliably without bit-banging.

Addressable LED strips based on the WS2812 (often called NeoPixels) are a favorite choice for adding colorful lighting effects to embedded projects. They only require a single data line to control an entire chain of LEDs, making them ideal for wearables, IoT devices, and creative lighting installations. However, the communication protocol they use is highly timing-sensitive, which makes them tricky to control directly in software.

The nRF52832, one of Nordic’s most popular Bluetooth SoCs, does not include a dedicated peripheral for generating WS2812 signals. Fortunately, by using the SPI peripheral in an unconventional way, you can reliably generate the waveforms these LEDs require. In this post, we’ll explore how WS2812 LEDs work, the challenges of driving them, and how to implement a working driver in the nRF Connect SDK using Zephyr’s SPI API.

How WS2812 LEDs Communicate

Each WS2812 LED package combines three LEDs (red, green, and blue) with a tiny controller chip. The controller listens to a single input line, latches color values for its LED, and passes the remaining data down the line to the next LED. This creates a shift-register effect, allowing hundreds of LEDs to be daisy-chained while still being individually addressable.

The protocol operates at about 800 kHz. Each bit of color information is represented by a pulse that lasts 1.25 microseconds in total, with the duty cycle of the high period defining whether the bit is a zero or a one. A logical one is a high signal for roughly 0.7 microseconds followed by low for 0.6 microseconds. A logical zero is a high signal for 0.35 microseconds and a low for about 0.8 microseconds. After all data is sent, a reset condition is generated by holding the line low for at least 50 microseconds.

Each LED consumes 24 bits of data, arranged in GRB order: eight bits for green, eight bits for red, and eight bits for blue. Once it has received 24 bits, the LED displays its new color and forwards the rest of the stream to the next device.

This protocol is elegant in its simplicity but extremely unforgiving in timing. A jitter of even a few hundred nanoseconds can cause the LEDs to flicker or ignore commands.


Why SPI Works as a Signal Generator

On the nRF52832, software bit-banging is impractical because the processor can be interrupted by Bluetooth events and other system tasks. To ensure reliable operation, the data line must be driven by a hardware peripheral that guarantees precise timing. Pulse-width modulation (PWM) with DMA is one option, but another powerful and often simpler approach is to use the SPI peripheral.

By configuring SPI to run at 4 MHz, each SPI bit lasts 250 nanoseconds. Since each WS2812 bit requires about 1.25 microseconds, you can map each WS2812 bit onto a sequence of SPI bits that create the correct duty cycle. For example, a logical one can be encoded as 0x70 (high for 0.75 us, low for 1 us), and a logical zero as 0x40 (high for 0.25 us, low for 1.25 us). These approximations fall comfortably within the WS2812 timing tolerances.

This method effectively transforms the WS2812 problem into an encoding problem. Once the data is converted into the appropriate SPI bitstream, the SPI peripheral transmits it with hardware accuracy.

Hardware Setup

For this project, we assume a custom board based on the nRF52832. The WS2812 strip is powered at 5 V. The ground of the LED strip and the nRF52832 must be common. The data input of the strip is connected to pin P0.06 on the nRF52832. Since the SoC operates at 3.3 V logic, some strips accept this directly, while others require a level shifter. For robust operation, a logic-level shifter is recommended.

The strip used in this example has eight LEDs, but the same technique scales to much longer strips.

Project Configuration

The application is built with the nRF Connect SDK using Zephyr. To begin, create a new application and configure it with the following options in prj.conf:

CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=3
CONFIG_LOG_BACKEND_UART=y
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LED_STRIP=y
CONFIG_SPI=y
CONFIG_SOC_NRF52832_ALLOW_SPIM_DESPITE_PAN_58=y

This enables SPI support and sets up logging.

Next, create a dts file or a board overlay file to configure SPI0 for outputting the WS2812 data stream

#include <zephyr/dt-bindings/led/led.h>

ws2812_spi: &spi0 { 
	status = "okay";
	pinctrl-0 = <&spi0_default>;
	pinctrl-1 = <&spi0_sleep>;
	pinctrl-names = "default", "sleep";
	compatible = "nordic,nrf-spim";
	led_strip: ws2812@0 {
		compatible = "worldsemi,ws2812-spi";
		reg = <0>; /* ignored, but necessary for SPI bindings */
		spi-max-frequency = <4000000>;
		chain-length = <8>;
		color-mapping = <LED_COLOR_ID_GREEN
				 		 LED_COLOR_ID_RED
				 		 LED_COLOR_ID_BLUE>;
		spi-one-frame = <0x70>;
		spi-zero-frame = <0x40>;
		status = "okay";
	};
};

&pinctrl {
	spi0_default: spi0_default {
		group1 {
			psels = <NRF_PSEL(SPIM_SCK, 0, 13)>,
				    <NRF_PSEL(SPIM_MOSI, 0, 14)>;
		};
	};
	spi0_sleep: spi0_sleep {
		group1 {
			psels = <NRF_PSEL(SPIM_SCK, 0, 13)>,
				    <NRF_PSEL(SPIM_MOSI, 0, 14)>;
			low-power-enable;
		};
	};
};

Even though SPI normally uses both clock and MOSI pins, in this application only MOSI is routed to the LED strip. The clock pin is not physically connected.

Application Example

We take a look at the led_strip example in nRF Connect SDK.

#include <zephyr/kernel.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
#include <string.h>

#define STRIP_NODE		    DT_NODELABEL(led_strip)
#define STRIP_NUM_PIXELS	DT_PROP(DT_ALIAS(led_strip), chain_length)
#define RGB(_r, _g, _b) { .r = (_r), .g = (_g), .b = (_b) }

static const struct led_rgb colors[] = {
	RGB(0x0f, 0x00, 0x00), /* red */
	RGB(0x00, 0x0f, 0x00), /* green */
	RGB(0x00, 0x00, 0x0f), /* blue */
};

static struct led_rgb pixels[STRIP_NUM_PIXELS];
static const struct device *const strip = DEVICE_DT_GET(STRIP_NODE);

int main(void)
{
	size_t color = 0;
	int rc;

	if (device_is_ready(strip)) {
		LOG_INF("Found LED strip device %s", strip->name);
	} else {
		LOG_ERR("LED strip device %s is not ready", strip->name);
		return 0;
	}

	LOG_INF("Displaying pattern on strip");
	while (1) {
		for (size_t cursor = 0; cursor < ARRAY_SIZE(pixels); cursor++) {
			memset(&pixels, 0x00, sizeof(pixels));
			memcpy(&pixels[cursor], &colors[color], sizeof(struct led_rgb));

			rc = led_strip_update_rgb(strip, pixels, STRIP_NUM_PIXELS);
			if (rc) {
				LOG_ERR("couldn't update strip: %d", rc);
			}

			k_sleep(K_MSEC(50));
		}

		color = (color + 1) % ARRAY_SIZE(colors);
	}

	return 0;
}

This program:

  • Loops forever.
  • For each position (cursor) along the strip:
    • Clears all LEDs (memset).
    • Sets one LED to the current color (memcpy).
    • Sends the pixel buffer to the strip using led_strip_update_rgb().
    • Waits for the configured delay.
  • After finishing one pass, it changes to the next color in the colors array.
  • The effect is a moving pixel animation that cycles through red, green, and blue.

Observations and Performance

The SPI method scales well. With eight LEDs, the buffer size is small, but even with hundreds of LEDs, the nRF52832 can drive the strip efficiently. Each LED requires nine bytes of SPI data, so a strip of 100 LEDs needs 900 bytes per update. At 8 MHz, this transmits in under a millisecond, leaving plenty of CPU time for Bluetooth and other tasks.

Since the SPI peripheral uses EasyDMA, large transfers can run without CPU involvement. This means the method is robust against interrupts, unlike software-based approaches. The reset delay is short compared to the transfer time, so it does not limit performance significantly.

Practical Considerations

Driving long LED strips can consume a lot of power. For testing, keep brightness low or use short strips. For larger installations, use a dedicated power supply and inject power at multiple points along the strip.

Another important detail is signal integrity. WS2812 LEDs use a relatively high data rate and are sensitive to noise. Keep the data line short, use a series resistor close to the MCU output pin, and consider a level shifter if your strip does not reliably register 3.3 V signals.

Extensions and Enhancements

Once the basic driver works, it can be extended in several ways. A higher-level library can provide functions for setting colors with HSV values, applying gradients, or running animations such as fades and rainbows. The encoding and transfer routines can be abstracted into a dedicated driver module so that applications simply work with arrays of RGB values.

Integration with Bluetooth opens up exciting possibilities. A smartphone app could control the LED patterns wirelessly, turning the custom board into a smart light controller. With Zephyr’s modular design, it is straightforward to combine the WS2812 driver with Bluetooth services such as Nordic’s UART Service or a custom GATT profile.

Conclusion

The WS2812 LED strip is simple to connect but demands precise timing to control. On the nRF52832, this requirement can be met by creatively using the SPI peripheral as a waveform generator. By encoding each LED bit into a sequence of SPI bits, the data stream matches the WS2812 timing, and the SPI hardware guarantees accurate transmission.

Using the nRF Connect SDK, this solution is clean and maintainable. The code fits naturally into the Zephyr framework. With this foundation, you can build everything from simple color demos to complex Bluetooth-controlled lighting systems.

By understanding the inner workings of WS2812 LEDs and applying the flexibility of the nRF52832, you can unlock the full potential of colorful LED lighting in your embedded projects.

    Related articles

    View All Articles »

    Understanding Work Queues in Zephyr RTOS

    Work queues in Zephyr RTOS are a lightweight way to defer work, move processing out of interrupt context, and keep applications responsive without creating extra threads. In this article, we explain how work queues work, explore system and custom queues, show how to schedule delayed tasks, and demonstrate a real-world Bluetooth advertising example where work queues ensure safe and efficient operation.

    Build Configurations & Kconfig in nRF Connect SDK

    Build configurations in the nRF Connect SDK let you fine-tune your application at compile time, controlling everything from logging verbosity to Bluetooth Low Energy parameters. In this post, we explore how to use prj.conf and menuconfig to manage Kconfig options, highlight common configurations such as logging, peripherals, and BLE settings, and walk through a practical example of enabling debug logging and tuning BLE buffers for higher performance.

    Understanding Device Tree in nRF Connect SDK

    Device Tree in nRF Connect SDK is a structured way of describing hardware, making your applications more portable and maintainable. Learn about DT hierarchy, nodes, properties, overlays, and practical examples like blinking LEDs, reading buttons, and adding custom sensors.

    Introduction to Zephyr Build System

    Getting started with the nRF52832 in the nRF Connect SDK requires understanding how Zephyr’s build system works. This post introduces the core components of building applications, including CMake, west, and key configuration files like prj.conf and CMakeLists.txt. We walk through how west orchestrates the build process, how to configure project features, and provide a step-by-step example of building and flashing a simple Hello World application. By mastering these fundamentals, you’ll be ready to develop scalable and maintainable embedded applications with Zephyr.