概述
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 灯珠的眼睛,让它以为收到了标准的灯控信号,从而变色。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/peripherals/spi_ws2812编写,代码可在本地 SDK 中直接找到。官方applications/peripherals/spi/demo_spi目录的工程骨架即对应本示例的 SSD1306 场景说明。
按官方示例接线(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)spiCfg:SPI_CFG_Type结构体指针。关键字段:frameFormat(可选值:SPI_FRAME_SIZE_88 位帧)、bitInverse(可选值:SPI_BIT_INVERSE_MSB_FIRSTMSB 先行)、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/1fifoCfg:SPI_FifoCfg_Type结构体指针(txThreshold发送阈值 /rxThreshold接收阈值 /dmaEnable是否使能 DMA 请求)
返回值:无
SPI_SetClock(spiNo, clk)
设置 SPI 通信时钟(决定传输速率,本教程 6.6MHz)。
参数:
spiNo:SPI 控制器号,可选值:0/1clk:时钟频率(Hz),可选值:如1000000(1MHz,从设备允许范围内)
返回值:无
SPI_Enable(spiNo, mode)
打开 SPI 控制器,开始按配置工作。
参数:
spiNo:SPI 控制器号,可选值:0/1mode:工作模式,可选值:SPI_WORK_MODE_MASTER(主机)/SPI_WORK_MODE_SLAVE(从机)
返回值:无
SPI_Disable(spiNo, mode)
关闭 SPI 控制器(重新初始化前调用)。
参数:
spiNo:SPI 控制器号,可选值:0/1mode:工作模式,可选值: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)lliCfg:DMA_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;失败返回负值错误码
完整代码
以下为 app/main.c 完整源码,与官方示例(applications/peripherals/spi_ws2812/app/main.c)完全一致:
📜 点击展开 app/main.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

