Overview
The WS2812 is an RGB LED with a built-in driver chip (RGB = red green blue, mixed together to make any color); it needs just one data wire to daisy-chain many LEDs (cascade: the first LED's DO connects to the second's DI, data passes down one by one like a relay race) into a light strip. This tutorial uses the official demo_ws2812 example (default SPI+DMA driving, data pin IO12) to drive 46 LEDs with a smooth red ↔ blue gradient, and demonstrates single-LED color and brightness control.
In plain words: a WS2812 strip is like the fairy lights on a Christmas tree — a string of lights with only one data wire: the first LED "takes" its color from the data, the rest of the data passes on to the next one, so no matter how long the string, it needs just one wire. What color each LED gets has an order: LEDs receive data in GRB (green-red-blue) order, so when you send "red", it receives the green bit first, then the red bit. This tutorial uses SPI to "feed" color data to the strip at high speed, then a gradient algorithm to slowly transition the color from red to blue.
This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version
release_bl_iot_sdk_1.6.40) exampleapplications/iot-solution/demo_ws2812; the code can be found directly in the local SDK. The basic tutorial SPI Protocol uses theapplications/peripherals/spi_ws2812project (8 LEDs, bare SPI driver); this page uses the iot-solution version with a more complete strip driver API (components/stage/ws2812) — just change the struct parameters to fit any LED count.
Wire per the official example (default SPI driving, data pin IO12):
| Ai-WB2 Pin | WS2812 Strip |
|---|---|
| IO12 | DIN (data input) |
| 5V | VCC |
| GND | GND |
💡 Cascade: each LED’s DO (data output) connects to the next one’s DI (data input); data passes backward from the first, so no matter how many LEDs, only one data wire is needed. Watch the direction when wiring: DIN to IO12 — don’t feed DO into the input by mistake.
⚡ 46 LEDs all lit draw significant current (each full-white LED is about 60mA) — per the official wiring, VCC to 5V (the strip’s supply must share GND with the board; common ground = the two grounds joined, otherwise voltage has no reference point and communication garbles).
Open a terminal and enter the official demo_ws2812 example project directory:
cd ~/Ai-Thinker-WB2/applications/iot-solution/demo_ws2812
Note:
cdis the “change directory” command — entering the demo_ws2812 project directory; all subsequentmakebuild andmake flashflash commands must run in this directory first.
Project structure:
| File | Purpose |
|---|---|
main/main.c |
Main program source, the main file this tutorial looks at |
proj_config.mk |
Project config; CONFIG_WS2812_MODE:=SPI_MODE selects the driving mode (default SPI, can be changed to IR_MODE) |
Makefile |
Build entry, usually no changes needed |
Open main/main.c — the complete code for this step has been moved to the end of this page:
📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, identical to the official example (
applications/iot-solution/demo_ws2812/main/main.c).
Code highlights:
| Code | Purpose |
|---|---|
bl_sys_init() |
Initializes the system’s low-level resources (clock, etc.) — the first call in every SDK project |
ws2812_init(&ws2812_strip) |
Initializes the strip per the struct (46 LEDs, brightness 0.05, IO12); without telling the driver how many LEDs, it can’t allocate memory |
ws2812_set_pixel_color(0, 0xff, 0x00, 0x00) |
Sets LED 0 to red (r/g/b); the library already arranges GRB order — wrong colors mean the order is wrong |
ws2812_set_pixel_brightness(0, 0.5) |
Raises LED 0’s brightness to 50% individually; handy when the global 0.05 is too dim |
ws2812_show_leds() |
Actually “sends” the arranged colors in memory to the LEDs; without it the LEDs never change color |
smoothcolorTransition(RED, BLUE, 500, cb, NULL) |
Gradients the whole strip from red to blue over 500 steps, refreshing once per step via the callback; without it the colors stay still |
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program file) the board can run;-j8builds with 8 parallel CPU cores, faster.
On success a firmware build_out/demo_ws2812.bin is generated.
Keep the board connected via USB, confirm the serial device (usually /dev/ttyUSB0 on Linux), and flash:
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the board’s chip. Afterp=comes the serial device (change it to your computer’s actual one — check withls /dev/ttyUSB*),b=is the flash baud rate (transfer speed).
⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode (some boards enter automatically); wait for the progress bar to complete — that means the flash succeeded.
After flashing, the board automatically restarts and runs; watch the strip:
- On startup, LED 0 lights red once on its own (the code sets the single-LED color and sends it first).
- Then the whole strip smoothly gradients between red ↔ blue in a loop (500 steps per direction).
💡 Change colors: edit the r/g/b values of the
RED/BLUEstructs inmain.c(e.g.{0x00, 0xff, 0x00}for green); change the LED count: edit.led_countinws2812_strip; too dim: raise.brightness(0.05→0.5).
Seeing LED 0 light red on its own first, then the whole strip gradient smoothly between red and blue, means success; if the strip doesn’t light at all, only a few LEDs light, or the colors are wrong, it hasn’t succeeded yet — check the FAQ at the end.
API Summary for This Tutorial
bl_sys_init()
Initializes the system's low-level resources (system clock, interrupts, etc.) — the first call in every SDK project's main function.
Parameters: none
Return: none
ws2812_init(ws2812_strip)
Initializes the WS2812 strip per the strip struct (self-developed driver interface, source in components/stage/ws2812/ws2812.c). Internally allocates memory per LED according to led_count and configures pin as the data output pin.
Parameters:
ws2812_strip:ws2812_strip_tstruct pointer, key fields:led_count(LED count, this tutorial46),brightness(global brightness0.0~1.0, this tutorial0.05),pin(data pin, this tutorial12)
Return: none (prints an error log and returns if the struct is empty or led_count is 0)
ws2812_set_pixel_color(index, r, g, b)
Sets the color of the single LED at the given position (self-developed driver interface, source in components/stage/ws2812/ws2812.c). The color data is arranged in the WS2812-required GRB order internally; this API just takes ordinary RGB arguments.
Parameters:
index: LED position, values:0~(led_count-1), this tutorial LED 0r: red component,0x00~0xFF, this tutorial0xffg: green component,0x00~0xFFb: blue component,0x00~0xFF
Return: none
ws2812_set_pixel_brightness(index, brightness)
Adjusts a single LED's brightness individually (self-developed driver interface, source in components/stage/ws2812/ws2812.c).
Parameters:
index: LED position, values:0~(led_count-1)brightness: brightness factor, values:0.0~1.0, this tutorial sets LED 0 to0.5
Return: none
ws2812_show_leds()
Actually sends all the arranged color data in memory to the LEDs through the data pin (self-developed driver interface, source in components/stage/ws2812/ws2812.c). After setting colors you must call it, or the LEDs won't change.
Parameters: none
Return: none
smoothcolorTransition(start, end, steps, updateCallback, userData)
Smoothly gradients from the start color to the end color (self-developed interface, source in components/stage/ws2812/color_mode.c). Internally interpolates across steps steps, calling the callback once per step to refresh the LEDs.
Parameters:
start: start color,color_tstruct{r, g, b}, this tutorialREDend: end color, this tutorialBLUEsteps: gradient step count, values: positive integer, this tutorial500(larger = slower and finer gradient)updateCallback: gradient callback function, shapedvoid cb(color_t color, void *arg), called on every stepuserData: user data passed to the callback, passNULLif not needed
Return: none
dns_init()
Initializes the lwIP protocol stack's DNS domain-name resolution module (network-related). This example has no networking feature — keep the official call, don't delete it.
Parameters: none
Return: none
vTaskDelay(ms)
Suspends the current task for the given milliseconds, yielding the CPU to other tasks (FreeRTOS system API; this tutorial uses pdMS_TO_TICKS to convert milliseconds to system ticks).
Parameters:
ms: delay in milliseconds, values: any non-negative integer (this tutorial delays5ms in the gradient callback to pace the gradient speed)
Return: none
Full Code
Below is the complete main/main.c source, identical to the official example (applications/iot-solution/demo_ws2812/main/main.c):
📜 Click to expand the full main/main.c code
/**
* @file main.c
* @author Seahi-Mo (seahi-mo@foxmail.com)
* @brief
* @version 0.1
* @date 2025-07-23
*
* @copyright Ai-Thinker co.,ltd (c) 2025
*
*/
#include <bl_ir.h>
#include <stdio.h>
#include <FreeRTOS.h>
#include <task.h>
#include <bl_irq.h>
#include <bl_sys.h>
#include "ws2812.h"
#include "color_mode.h"
#include "blog.h"
color_t RED = {0xff, 0x00, 0x00};
color_t GREEN = {0x00, 0xff, 0x00};
color_t BLUE = {0x00, 0x00, 0xff};
/**
* @brief 定义WS2812灯带 只能使用 GPIO11 并且外部需要提供 1K~2K 的上拉电阻
*
*/
static ws2812_strip_t ws2812_strip = {
.led_count = 46,
.brightness = 0.05,
.pin = 12,
};
static void smoothcolorTransition_callbark(color_t color, void *arg)
{
ws2812_set_all_pixels_color(color.r, color.g, color.b, ws2812_strip.brightness);
vTaskDelay(pdMS_TO_TICKS(5));
}
void main(void)
{
bl_sys_init(); // 初始化系统
ws2812_init(&ws2812_strip);
ws2812_set_pixel_color(0, 0xff, 0x00, 0x00);
ws2812_set_pixel_brightness(0, 0.5);
ws2812_show_leds();
dns_init();
// blog_info("ws2812 demo start");
// ws2812_set_all_pixels_color(0xFF, 0x00, 0x00, 0.5);
// vTaskDelay(pdMS_TO_TICKS(1000));
// ws2812_set_all_pixels_color(0x00, 0xFF, 0x00, 0.5);
// vTaskDelay(pdMS_TO_TICKS(1000));
// ws2812_set_all_pixels_color(0x00, 0x00, 0xFF, 0.5);
vTaskDelay(pdMS_TO_TICKS(1000));
while (1)
{
// 颜色渐变模式
smoothcolorTransition(RED, BLUE, 500, smoothcolorTransition_callbark, NULL);
smoothcolorTransition(BLUE, RED, 500, smoothcolorTransition_callbark, NULL);
}
}FAQ & Troubleshooting
⚠️ The strip doesn't light at all
Cause: wrong data pin (DIN not on IO12), DO/DI direction reversed, no common ground, or insufficient supply
Fix: confirm DIN is on IO12 (DO is the output end — wrong direction receives no data); VCC to 5V and share GND with the board; for long strips, power separately and share ground
⚠️ Wrong colors (red shows green/blue, etc.)
Cause: WS2812 receives data in GRB (green-red-blue) order; wrong argument order scrambles the colors
Fix: this driver library already arranges GRB internally — just pass r/g/b; if you write your own driver, send the green component first
⚠️ The lights are too dim, barely visible
Cause: the official example's global brightness is only 0.05 (5%)
Fix: raise .brightness in ws2812_strip to 0.5, or brighten individually with ws2812_set_pixel_brightness
⚠️ Only the first LED lights, the rest don't
Cause: the cascade is broken — the first LED's DO isn't connected to the second's DI
Fix: check the cascade wiring; you can also change led_count to the actual count (e.g. 8) to verify single-LED wiring
⚠️ Want to drive in IR mode
Cause: IR mode has special pin requirements
Fix: per the official note, IR driving only supports GPIO11 and needs a 1K~2K pull-up resistor; change CONFIG_WS2812_MODE:=IR_MODE in proj_config.mk, then rebuild and reflash
⚠️ Flashing reports cannot open the serial port / keeps waiting
Cause: wrong serial device, insufficient permission, download mode not entered, or the cable only charges
Fix: confirm the device with ls /dev/ttyUSB*; if permission denied run sudo usermod -aG dialout $USER; press and hold EN during flashing as prompted; try a Type-C data-capable cable
Self-Check
LED 0 lights red once on its own, then the whole 46-LED strip gradients smoothly between red ↔ blue in a loop — the WS2812 strip driver is verified.

