Skip to content

概述

SPI(Serial Peripheral Interface)是一种高速同步串行总线,至少需要 SCLK(时钟)+ MOSI(主出从入) 两根线即可单向高速传输,常用于 OLED 屏、Flash、SD 卡、LED 灯带等。本教程通过 SPI + DMA 以 6.6MHz 时钟驱动 8 颗 WS2812 RGB LED,实现呼吸变色效果。

用大白话讲:SPI 就像一台「快递传送带」:SCLK 时钟线像传送带的节拍(嗒、嗒、嗒),每响一下,MOSI 数据线上就送出一个比特(0 或 1)。本教程用 SPI 高速传送特制的 0/1 码流,骗过 WS2812 灯珠的眼睛,让它以为收到了标准的灯控信号,从而变色。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/peripherals/spi_ws2812 编写,代码可在本地 SDK 中直接找到。官方 applications/peripherals/spi/demo_spi 目录的工程骨架即对应本示例的 SSD1306 场景说明。

🎯本页目标通过 SPI0 以 6.6MHz 时钟 + DMA 发送驱动 8 颗 WS2812 LED 呼吸变色,掌握 SPI 初始化、DMA 发送与数据编码。
🧰前置条件① Ai-WB2 开发板、WS2812 LED 灯带/模块(3.3V 供电)、杜邦线 ② 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建。
🔗相关章节DMA 原理见 [DMA 传输](./dma);另一总线协议见 [I2C 协议](./i2c)。

硬件接线

按官方示例接线(SPI0 MOSI 为 IO12,接 WS2812 数据输入):

Ai-WB2 引脚 WS2812 引脚
IO12(SPI0 MOSI) DI(数据输入)
3V3 VCC(WS2812 3.3V 供电)
GND GND

💡 WS2812 为单线协议,只需要数据线即可级联多颗。SPI 以 6.6MHz 发送特定位流,用时钟信号的边沿模拟 WS2812 的 0/1 码时序。灯带较长(>10 颗)建议 5V 供电并外接电平转换,本示例 8 颗用 3V3 即可。

进入示例工程

打开终端,进入官方 spi_ws2812 示例工程目录:

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

说明:cd 是「进入目录」命令,这里进入 spi_ws2812 示例工程目录;后续的 make 编译、make flash 烧录命令都必须先在这个目录里执行。

编写代码

打开 app/main.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/peripherals/spi_ws2812/app/main.c)完全一致。

代码要点:

代码 作用
GLB_GPIO_Func_Init(GPIO_FUN_SPI, &pin_mosi, 1) 把 IO12 引脚切换成 SPI 功能(复用 = 一根针脚干多种活),不做这步引脚不会发数据
SPI_Init(DEMO_SPI_ID, &spiCfg) 初始化 SPI0(8 位帧、MSB 先行、空闲低电平),参数配错灯珠无法识别时序
SPI_SetClock(DEMO_SPI_ID, 6666666) 设置 SPI 时钟 6.6MHz,频率必须匹配 WS2812 的时序要求
SPI_FifoConfig(...) 使能 SPI 发送 DMA 请求(FIFO 阈值 1),不使能数据就送不出去、程序会卡死
spi_send_by_dma(...) SPI+DMA 发送:内存 → SPI FIFO,地址固定不增(DMA = 帮 CPU 搬数据的「搬运工」)
led_conv_buff(...) WS2812 编码:每 bit 用 0xFC(1 码)或 0xC0(0 码)表示,编码错了灯就乱闪
rgb_table_deal() 生成 RGB 呼吸效果数据(亮度 5~120 循环),没有它灯色就不会变化
编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译工程」命令,把代码变成开发板能运行的固件(程序文件);-j8 表示用 8 个核并行编译,速度更快。

编译成功后生成固件 build_out/spi_ws2812.bin

烧录固件

开发板保持 USB 连接,确认串口设备号后执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件写进开发板芯片。p= 后面是串口设备号(改成你电脑上实际的串口,可用 ls /dev/ttyUSB* 查看),b= 是烧录波特率(传输速度)。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行,8 颗 WS2812 LED 呈蓝紫色呼吸渐变效果,串口输出:

demo task start...

💡 修改 rgb_table_deal() 中的 RGB 参数即可改变颜色,例如 led_conv_buff(i, 255, 0, 0) 为红色常亮。用逻辑分析仪观察 IO12,可看到 6.6MHz 的 SPI 位流(见官方示例 img/logic_analyzer.jpg)。

看到 8 颗灯珠呈呼吸渐变效果、串口打印 demo task start... 即为成功;如果灯带完全不亮或颜色错乱,说明还没成功,对照文末「常见问题与踩坑提示」排查。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

GLB_GPIO_Func_Init(fun, pins, cnt)

批量把一组引脚复用为 SPI 等外设功能(本教程将 IO12 复用为 SPI0 MOSI)。

参数

  • fun:复用功能号,可选值:GPIO_FUN_SPI(SPI 功能)
  • pins:引脚数组指针,如 {GPIO_PIN_12}
  • cnt:引脚数量(数组长度),可选值:1~4

返回值:无

SPI_Init(spiNo, spiCfg)

配置 SPI 控制器帧格式、时钟极性/相位、主从模式等(本教程配置 8 位帧、MSB 先行、空闲低电平)。

参数

  • spiNo:SPI 控制器号,可选值:0/1(对应 SPI0/SPI1)
  • spiCfgSPI_CFG_Type 结构体指针。关键字段:frameFormat(可选值:SPI_FRAME_SIZE_8 8 位帧)、bitInverse(可选值:SPI_BIT_INVERSE_MSB_FIRST MSB 先行)、clkPolarity(可选值:SPI_CLK_POLARITY_LOW 空闲低电平)、clkPhase(可选值:SPI_CLK_PHASE_INVERSE_0 相位 0,模式 0)

返回值:无

SPI_FifoConfig(spiNo, fifoCfg)

设置收发 FIFO 阈值与 DMA 请求使能(本教程使能发送 DMA 请求,FIFO 阈值 1)。

参数

  • spiNo:SPI 控制器号,可选值:0/1
  • fifoCfgSPI_FifoCfg_Type 结构体指针(txThreshold 发送阈值 / rxThreshold 接收阈值 / dmaEnable 是否使能 DMA 请求)

返回值:无

SPI_SetClock(spiNo, clk)

设置 SPI 通信时钟(决定传输速率,本教程 6.6MHz)。

参数

  • spiNo:SPI 控制器号,可选值:0/1
  • clk:时钟频率(Hz),可选值:如 1000000(1MHz,从设备允许范围内)

返回值:无

SPI_Enable(spiNo, mode)

打开 SPI 控制器,开始按配置工作。

参数

  • spiNo:SPI 控制器号,可选值:0/1
  • mode:工作模式,可选值:SPI_WORK_MODE_MASTER(主机)/ SPI_WORK_MODE_SLAVE(从机)

返回值:无

SPI_Disable(spiNo, mode)

关闭 SPI 控制器(重新初始化前调用)。

参数

  • spiNo:SPI 控制器号,可选值:0/1
  • mode:工作模式,可选值:SPI_WORK_MODE_MASTER(主机)/ SPI_WORK_MODE_SLAVE(从机)

返回值:无

hosal_dma_chan_request(flag)

申请一个 DMA 通道用于 SPI 数据搬运。

参数

  • flag:申请标志,可选值:HOSAL_DMA_TYPE_NORMAL / HOSAL_DMA_TYPE_LLI(LLI 链表)

返回值:成功返回通道号(0~7);失败返回负值错误码

DMA_LLI_Init(ch, lliCfg)

配置 DMA 链表搬运方向与位宽(本教程 dir = DMA_TRNS_M2P 内存到外设)。

参数

  • ch:DMA 通道号(DMA_CH0~DMA_CH7
  • lliCfgDMA_LLI_Cfg_Type 结构体指针,dir 可选值:DMA_TRNS_M2M/DMA_TRNS_M2P/DMA_TRNS_P2M

返回值:无

DMA_LLI_Update(ch, LLI)

将编排好的 LLI 链表地址装入 DMA 通道,之后启动通道即按链表执行。

参数

  • ch:DMA 通道号
  • LLI:链表结构体指针(DMA_LLI_Ctrl_Type 数组,多段时 next 指向下一段)

返回值:无

hosal_dma_irq_callback_set(chan, pfn, p_arg)

注册 DMA 搬运完成/错误中断回调(本教程在回调中将 spi_dma_txing 置 0 表示发送完成)。

参数

  • chan:DMA 通道号
  • pfn:回调函数指针,形如 void cb(void *arg, uint32_t flag),必填
  • p_arg:回调参数,无参传 NULL

返回值:成功返回 0;失败返回负值错误码

hosal_dma_chan_start(chan)

启动 DMA 通道,开始数据搬运。

参数

  • chan:DMA 通道号

返回值:成功返回 0;失败返回负值错误码


完整代码

以下为 app/main.c 完整源码,与官方示例(applications/peripherals/spi_ws2812/app/main.c)完全一致:

📜 点击展开 app/main.c 完整代码
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);
    }
}

常见问题与踩坑提示

⚠️ 灯带不亮
原因:WS2812 数据线接错引脚、供电不足或未共地
解决:确认 DI 接 IO12(SPI0 MOSI)、3V3/GND 正确供电且与开发板共地;灯带超过 8 颗建议独立 5V 供电

⚠️ 颜色显示错误或闪烁
原因:SPI 时钟不匹配 WS2812 时序(WS2812 要求 800kHz 级时序,靠 6.6MHz 位流编码实现)
解决:保持 DEMO_SPI_CLK_HZ 6666666 不变;如需调速,0/1 码(LED_T0/LED_T1)与时钟需同步调整

⚠️ DMA 发送死等(卡在 spi_dma_whait_txdone
原因:SPI FIFO 的 DMA 请求未使能,或通道被其他模块占用
解决:确认 SPI_FifoConfig 中 tx dma 使能为 ENABLE;确认无其他外设占用 DMA 通道

⚠️ 想驱动 OLED 等其他 SPI 设备
原因:demo_spi 目录的工程骨架只保留 SSD1306 接线说明,未包含驱动代码
解决:参考官方 best-practice/ssd1306 应用 或本教程的 SPI 初始化流程,按设备数据手册编写时序

⚠️ 烧录一直卡住等待,进度条不动
原因:未进入下载模式,或数据线只能充电不能传数据
解决:烧录时按提示长按 EN 键进入下载模式;换一根能传数据的 Type-C 数据线后重试

⚠️ 执行 make 报找不到 Makefile
原因:在错误的目录执行了编译命令(必须在示例工程目录内)
解决:先执行 cd ~/Ai-Thinker-WB2/applications/peripherals/spi_ws2812 进入工程目录,再执行 make -j8

运行自检

8 颗 WS2812 LED 呈呼吸渐变效果,且串口输出 demo task start...,即 SPI 通信验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

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