Skip to content

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) example applications/iot-solution/demo_ws2812; the code can be found directly in the local SDK. The basic tutorial SPI Protocol uses the applications/peripherals/spi_ws2812 project (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.

🎯Page GoalDrive 46 WS2812 LEDs with a smooth red-blue gradient, mastering cascade wiring and the strip driver API (single LED control, brightness, gradient).
🧰Prerequisites① An Ai-WB2 development board, a WS2812 light strip/module (at least 46 LEDs), dupont wires ② Development environment set up per [SDK Installation](../sdk/sdk_intro).
🔗RelatedSPI+DMA fundamentals: [SPI Protocol](../basic/spi) and [DMA Transfer](../basic/dma); previous: [SSD1306 OLED Display](./ssd1306).

Hardware Wiring

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).

Enter the Example Project

Open a terminal and enter the official demo_ws2812 example project directory:

cd ~/Ai-Thinker-WB2/applications/iot-solution/demo_ws2812

Note: cd is the “change directory” command — entering the demo_ws2812 project directory; all subsequent make build and make flash flash 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
Write the Code

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 the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (the program file) the board can run; -j8 builds with 8 parallel CPU cores, faster.

On success a firmware build_out/demo_ws2812.bin is generated.

Flash the Firmware

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 flash is the “flash” command, writing the compiled firmware into the board’s chip. After p= comes the serial device (change it to your computer’s actual one — check with ls /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.

Run and Verify

After flashing, the board automatically restarts and runs; watch the strip:

  1. On startup, LED 0 lights red once on its own (the code sets the single-LED color and sends it first).
  2. 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 / BLUE structs in main.c (e.g. {0x00, 0xff, 0x00} for green); change the LED count: edit .led_count in ws2812_strip; too dim: raise .brightness (0.050.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_t struct pointer, key fields: led_count (LED count, this tutorial 46), brightness (global brightness 0.0~1.0, this tutorial 0.05), pin (data pin, this tutorial 12)

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 0
  • r: red component, 0x00~0xFF, this tutorial 0xff
  • g: green component, 0x00~0xFF
  • b: 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 to 0.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_t struct {r, g, b}, this tutorial RED
  • end: end color, this tutorial BLUE
  • steps: gradient step count, values: positive integer, this tutorial 500 (larger = slower and finer gradient)
  • updateCallback: gradient callback function, shaped void cb(color_t color, void *arg), called on every step
  • userData: user data passed to the callback, pass NULL if 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 delays 5ms 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
c
/**
 * @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.

Released under the MIT License. Build Time 2026-09-11 14:52:23