Skip to content

Overview

SPI (Serial Peripheral Interface) is a high-speed synchronous serial bus; just SCLK (clock) + MOSI (master out, slave in) are enough for one-way high-speed transfer. It's commonly used for OLED screens, Flash, SD cards, LED strips and more. This tutorial uses SPI + DMA at a 6.6MHz clock to drive 8 WS2812 RGB LEDs with a breathing color effect.

In plain words: SPI is like a "conveyor belt for parcels": the SCLK clock line is the belt's beat (tick, tick, tick), and every tick sends one bit (0 or 1) on the MOSI data line. This tutorial uses SPI to send a specially crafted 0/1 stream at high speed, fooling the WS2812 LEDs into thinking they received the standard light-control signal, so they change color.

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/peripherals/spi_ws2812; the code can be found directly in your local SDK. The applications/peripherals/spi/demo_spi directory's project skeleton corresponds to this example's SSD1306 scenario description.

🎯Page GoalDrive 8 WS2812 LEDs through SPI0 at a 6.6MHz clock + DMA with a breathing color effect; learn SPI init, DMA send and data encoding.
🧰Prerequisites① Ai-WB2 development board, WS2812 LED strip/module (3.3V powered), jumper wires ② Environment set up per [SDK Installation](../sdk/sdk_intro).
🔗RelatedDMA fundamentals: [DMA Transfer](./dma); another bus protocol: [I2C Protocol](./i2c).

Hardware Wiring

Wire per the official example (SPI0 MOSI is IO12, connected to the WS2812 data input):

Ai-WB2 Pin WS2812 Pin
IO12 (SPI0 MOSI) DI (data input)
3V3 VCC (WS2812 3.3V powered)
GND GND

💡 WS2812 is a single-wire protocol; only a data line is needed to daisy-chain many LEDs. SPI sends a special bit stream at 6.6MHz, using the clock signal’s edges to emulate WS2812’s 0/1 code timing. For long strips (>10 LEDs) use 5V power plus an external level shifter; 3V3 is fine for this example’s 8 LEDs.

Enter the Example Project

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

cd ~/Ai-Thinker-WB2/applications/peripherals/spi_ws2812

Note: cd is the “change directory” command; this enters the spi_ws2812 example project. All subsequent make build and make flash commands must run inside this directory first.

Write the Code

Open app/main.c. The full 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/peripherals/spi_ws2812/app/main.c).

Code highlights:

Code Purpose
GLB_GPIO_Func_Init(GPIO_FUN_SPI, &pin_mosi, 1) Switches IO12 to the SPI function (multiplexing = one pin doing many jobs); without it the pin sends no data
SPI_Init(DEMO_SPI_ID, &spiCfg) Initialize SPI0 (8-bit frames, MSB first, idle low); wrong parameters and the LEDs can’t recognize the timing
SPI_SetClock(DEMO_SPI_ID, 6666666) Set the SPI clock to 6.6MHz; the frequency must match WS2812’s timing requirements
SPI_FifoConfig(...) Enable the SPI send DMA request (FIFO threshold 1); without it data can’t go out and the program hangs
spi_send_by_dma(...) SPI+DMA send: memory → SPI FIFO with a fixed non-incrementing address (DMA = the “porter” carrying data for the CPU)
led_conv_buff(...) WS2812 encoding: each bit is represented by 0xFC (1 code) or 0xC0 (0 code); wrong encoding makes the LEDs flash randomly
rgb_table_deal() Generates the RGB breathing effect data (brightness cycling 5~120); without it the colors never change
Build the Project

Build in the project directory:

make -j8

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

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

Flash the Firmware

Keep the board connected via USB, confirm the serial device, 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 port — 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; wait for the progress bar to complete — that means the flash succeeded.

Run and Verify

After flashing, the board automatically restarts; 8 WS2812 LEDs show a blue-purple breathing gradient, and the serial outputs:

demo task start...

💡 Change the RGB parameters in rgb_table_deal() to change colors, e.g. led_conv_buff(i, 255, 0, 0) for solid red. Observe IO12 with a logic analyzer to see the 6.6MHz SPI bit stream (see the official example img/logic_analyzer.jpg).

Seeing the 8 LEDs breathing with gradient colors and demo task start... printed means success; if the strip doesn’t light at all or colors are scrambled, check the “FAQ & Troubleshooting” section at the end.


API Summary for This Tutorial

GLB_GPIO_Func_Init(fun, pins, cnt)

Batches multiplex a group of pins to peripheral functions such as SPI (multiplexing IO12 as SPI0 MOSI in this tutorial).

Parameters:

  • fun: multiplexed function number, values: GPIO_FUN_SPI (SPI function)
  • pins: pin array pointer, e.g. {GPIO_PIN_12}
  • cnt: pin count (array length), values: 1~4

Return: none

SPI_Init(spiNo, spiCfg)

Configures the SPI controller's frame format, clock polarity/phase, master/slave mode, etc. (8-bit frames, MSB first, idle low in this tutorial).

Parameters:

  • spiNo: SPI controller number, values: 0/1 (SPI0/SPI1)
  • spiCfg: SPI_CFG_Type struct pointer. Key fields: frameFormat (values: SPI_FRAME_SIZE_8 8-bit frames), bitInverse (values: SPI_BIT_INVERSE_MSB_FIRST MSB first), clkPolarity (values: SPI_CLK_POLARITY_LOW idle low), clkPhase (values: SPI_CLK_PHASE_INVERSE_0 phase 0, mode 0)

Return: none

SPI_FifoConfig(spiNo, fifoCfg)

Sets the TX/RX FIFO thresholds and DMA request enables (enabling the send DMA request with FIFO threshold 1 in this tutorial).

Parameters:

  • spiNo: SPI controller number, values: 0/1
  • fifoCfg: SPI_FifoCfg_Type struct pointer (txThreshold send threshold / rxThreshold receive threshold / dmaEnable whether DMA requests are enabled)

Return: none

SPI_SetClock(spiNo, clk)

Sets the SPI communication clock (determines the transfer rate; 6.6MHz in this tutorial).

Parameters:

  • spiNo: SPI controller number, values: 0/1
  • clk: clock frequency (Hz), values: e.g. 1000000 (1MHz, within the slave's allowed range)

Return: none

SPI_Enable(spiNo, mode)

Turns on the SPI controller to start working per the configuration.

Parameters:

  • spiNo: SPI controller number, values: 0/1
  • mode: working mode, values: SPI_WORK_MODE_MASTER (master) / SPI_WORK_MODE_SLAVE (slave)

Return: none

SPI_Disable(spiNo, mode)

Turns off the SPI controller (call before re-initializing).

Parameters:

  • spiNo: SPI controller number, values: 0/1
  • mode: working mode, values: SPI_WORK_MODE_MASTER (master) / SPI_WORK_MODE_SLAVE (slave)

Return: none

hosal_dma_chan_request(flag)

Requests a DMA channel for SPI data transfer.

Parameters:

  • flag: request flag, values: HOSAL_DMA_TYPE_NORMAL / HOSAL_DMA_TYPE_LLI (LLI linked list)

Return: channel number (0~7) on success; negative error code on failure

DMA_LLI_Init(ch, lliCfg)

Configures the DMA linked-list transfer direction and width (this tutorial uses dir = DMA_TRNS_M2P memory-to-peripheral).

Parameters:

  • ch: DMA channel number (DMA_CH0~DMA_CH7)
  • lliCfg: DMA_LLI_Cfg_Type struct pointer; dir values: DMA_TRNS_M2M/DMA_TRNS_M2P/DMA_TRNS_P2M

Return: none

DMA_LLI_Update(ch, LLI)

Loads the prepared LLI linked list address into the DMA channel; starting the channel then executes per the list.

Parameters:

  • ch: DMA channel number
  • LLI: linked list struct pointer (DMA_LLI_Ctrl_Type array; with multiple segments, next points to the next segment)

Return: none

hosal_dma_irq_callback_set(chan, pfn, p_arg)

Registers the DMA transfer-complete/error interrupt callback (this tutorial sets spi_dma_txing to 0 in the callback to mark the send done).

Parameters:

  • chan: DMA channel number
  • pfn: callback function pointer, shaped void cb(void *arg, uint32_t flag), required
  • p_arg: callback argument; pass NULL if none

Return: 0 on success; negative error code on failure

hosal_dma_chan_start(chan)

Starts the DMA channel to begin data transfer.

Parameters:

  • chan: DMA channel number

Return: 0 on success; negative error code on failure


Full Code

Below is the complete app/main.c source, identical to the official example (applications/peripherals/spi_ws2812/app/main.c):

📜 Click to expand the full app/main.c code
c
/** @brief      spi to led demo.
 *
 *  @copyright  Copyright (C) 2025, Shenzhen Anxinke Technology Co., Ltd
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

#include "hosal_spi.h"
#include "bl602_spi.h"
#include "bl602_glb.h"
#include "bl602_pwm.h"
#include "bl_irq.h"
#include "bl_dma.h"
#include "bl602_dma.h"
#include "hosal_dma.h"

#define LED_NUM     8
#define LED_T0      0xC0
#define LED_T1      0xFC

#define DEMO_SPI_ID      0
#define DEMO_SPI_MOSI    GLB_GPIO_PIN_12
#define DEMO_SPI_CLK_HZ  6666666
#define DEMO_DMA_LLI_CNT 2

static uint8_t led_buffer[3 * 8 * LED_NUM];
static volatile int spi_dma_txing;
static hosal_dma_chan_t spi_dma_chan;
static DMA_LLI_Ctrl_Type spi_dma_lli[DEMO_DMA_LLI_CNT];

static void spi_dma_int_handler(void *arg, uint32_t flag)
{
    // bl_dma_int_clear(spi_dma_chan);
    // puts("spi_dma_int_handler\r\n");

    spi_dma_txing = 0;

    return;
}

static void inline spi_dma_whait_txdone(void)
{
    while (spi_dma_txing) {
        ;
    }
}

static int spi_dma_lli_list_init(uint8_t *data, uint32_t length)
{
    struct DMA_Control_Reg dmactrl;

    dmactrl.SBSize = DMA_BURST_SIZE_1;
    dmactrl.DBSize = DMA_BURST_SIZE_1;
    dmactrl.SWidth = DMA_TRNS_WIDTH_8BITS;
    dmactrl.DWidth = DMA_TRNS_WIDTH_8BITS;
    dmactrl.Prot = 0;
    dmactrl.SLargerD = 0;

    dmactrl.TransferSize = length;
    dmactrl.I = 0;

    dmactrl.SI = DMA_MINC_ENABLE;
    dmactrl.DI = DMA_MINC_DISABLE;

    spi_dma_lli[0].srcDmaAddr = (uint32_t)(data);
    spi_dma_lli[0].destDmaAddr = (uint32_t)(SPI_BASE + SPI_FIFO_WDATA_OFFSET);
    spi_dma_lli[0].dmaCtrl = dmactrl;
    spi_dma_lli[0].nextLLI = 0;

    return 0;
}

static int spi_send_by_dma(uint8_t *data, uint32_t length)
{
    DMA_LLI_Cfg_Type txllicfg;

    // if (length > LLI_BUFF_SIZE)
    //     assert(0);

    txllicfg.dir = DMA_TRNS_M2P;
    txllicfg.srcPeriph = DMA_REQ_NONE; 
    txllicfg.dstPeriph = DMA_REQ_SPI_TX;

    spi_dma_lli_list_init(data, length);

    DMA_LLI_Init(spi_dma_chan, &txllicfg);
    DMA_LLI_Update(spi_dma_chan, (uint32_t)spi_dma_lli);
    hosal_dma_irq_callback_set(spi_dma_chan, spi_dma_int_handler, NULL);
    hosal_dma_chan_start(spi_dma_chan);

    return 0;
}

static void spi_gpio_init(void)
{
    GLB_GPIO_Type pin_mosi;

    pin_mosi = DEMO_SPI_MOSI;
    GLB_GPIO_Func_Init(GPIO_FUN_SPI, &pin_mosi, 1);

    GLB_Set_SPI_0_ACT_MOD_Sel(GLB_SPI_PAD_ACT_AS_MASTER);

    return;
}

static void led_spi_init(void)
{
    SPI_CFG_Type spiCfg = {
        DISABLE,                      /* De-glitch function */
        ENABLE,                       /* Master continuous transfer mode */
        SPI_BYTE_INVERSE_BYTE0_FIRST, /* The byte 0 is sent first in SPI transfer */
        SPI_BIT_INVERSE_MSB_FIRST,    /* MSB is sent first in SPI transfer */
        SPI_CLK_PHASE_INVERSE_0,      /* SPI clock phase */
        SPI_CLK_POLARITY_LOW,         /* SPI clock plarity */
        SPI_FRAME_SIZE_8
    };

    SPI_FifoCfg_Type fifoCfg = {
        1,      /* SPI tx FIFO threshold */
        0,      /* SPI rx FIFO threshold */
        ENABLE, /* Enable or disable tx dma req/ack interface */
        DISABLE /* Enable or disable rx dma req/ack interface */
    };

    spi_dma_chan = hosal_dma_chan_request(0);

    SPI_Disable(DEMO_SPI_ID, SPI_WORK_MODE_MASTER);
    // SPI_IntMask(DEMO_SPI_ID, SPI_INT_ALL, MASK);
    SPI_Init(DEMO_SPI_ID, &spiCfg);
    SPI_FifoConfig(DEMO_SPI_ID, &fifoCfg);
    SPI_SetClock(DEMO_SPI_ID, DEMO_SPI_CLK_HZ);
    SPI_Enable(DEMO_SPI_ID, SPI_WORK_MODE_MASTER);
}

static void led_conv_buff(int32_t led_id, uint8_t r, uint8_t g, uint8_t b)
{
    for (int32_t i = 0; i < 8; i++) {
        led_buffer[led_id * 24 + i + 0] = ((g >> (8 - i)) & 0x01) ? LED_T1 : LED_T0;
    }
    for (int32_t i = 0; i < 8; i++) {
        led_buffer[led_id * 24 + i + 8] = ((r >> (8 - i)) & 0x01) ? LED_T1 : LED_T0;
    }
    for (int32_t i = 0; i < 8; i++) {
        led_buffer[led_id * 24 + i + 16] = ((b >> (8 - i)) & 0x01) ? LED_T1 : LED_T0;
    }
}

static void rgb_table_deal(void)
{
#define BRIGHT_MAX 120
#define BRIGHT_MIN 5
#define BRIGHT_SETP 1
    static uint8_t level;
    static int8_t direction = BRIGHT_SETP;

    for (int i = 0; i < LED_NUM; i++) {
        led_conv_buff(i, level, level, (130 - level) % 100);
    }

    if (level > BRIGHT_MAX) {
        direction = -BRIGHT_SETP;
    } else if (level < BRIGHT_MIN) {
        direction = BRIGHT_SETP;
    }
    level += direction;
}

int main(void)
{
    spi_gpio_init();
    led_spi_init();

    printf("demo task start...\r\n");
    while (1)
    {
        spi_dma_whait_txdone();
        rgb_table_deal();

        // SPI_Send_8bits(0, led_buffer, sizeof led_buffer, SPI_TIMEOUT_DISABLE);
        spi_send_by_dma(led_buffer, sizeof led_buffer);

        vTaskDelay(10);
    }
}

FAQ & Troubleshooting

⚠️ The LED strip doesn't light
Cause: WS2812 data line connected to the wrong pin, insufficient power, or no common ground
Fix: confirm DI connects to IO12 (SPI0 MOSI), 3V3/GND properly powered and sharing ground with the board; for more than 8 LEDs use separate 5V power

⚠️ Wrong colors or flickering
Cause: the SPI clock doesn't match WS2812's timing (WS2812 needs 800kHz-class timing, achieved via 6.6MHz bit-stream encoding)
Fix: keep DEMO_SPI_CLK_HZ 6666666 unchanged; if you must change speed, the 0/1 codes (LED_T0/LED_T1) and the clock must be adjusted together

⚠️ DMA send dead-waits (stuck in spi_dma_whait_txdone)
Cause: the SPI FIFO's DMA request isn't enabled, or the channel is occupied by another module
Fix: confirm the tx dma enable in SPI_FifoConfig is ENABLE; confirm no other peripheral occupies the DMA channel

⚠️ Want to drive other SPI devices like OLED
Cause: the demo_spi directory's project skeleton only keeps the SSD1306 wiring notes, no driver code
Fix: refer to the official best-practice/ssd1306 application or this tutorial's SPI init flow, and write the timing per the device datasheet

⚠️ Flashing keeps waiting, the progress bar doesn't move
Cause: download mode wasn't entered, or the cable only charges and can't transfer data
Fix: press and hold EN to enter download mode when prompted; try a Type-C cable that can transfer data and retry

⚠️ Running make reports no Makefile found
Cause: the build command ran in the wrong directory (it must run inside the example project)
Fix: first cd ~/Ai-Thinker-WB2/applications/peripherals/spi_ws2812 into the project directory, then run make -j8

Self-Check

The 8 WS2812 LEDs show a breathing gradient and the serial outputs demo task start... — SPI communication is verified.

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