简介:本资源是面向STM32F103C8T6初学者与嵌入式进阶开发者的WS2812B灯带高效驱动方案,聚焦解决传统GPIO模拟时序精度低、CPU占用高、易受中断干扰等痛点。采用SPI+DMA硬件协同方式精准复现WS2812B单线归零码时序,显著提升刷新率与稳定性,特别适用于多灯珠实时动态显示、音乐频谱响应等对实时性要求较高的场景。压缩包共214个文件,含44个头文件(h)、43个源码文件(c)及大量编译中间产物(o、d、crf等),涵盖标准外设库驱动、SPI/DMA配置、LED帧缓冲管理及Keil工程完整构建环境(uvprojx/uvoptx/axf/hex等),结构规范,便于理解底层机制与二次扩展。目前已有1796人学习下载,提供可直接烧录验证的完整工程,附带调试配置、启动脚本(bat)及内存布局说明(sct),是掌握STM32硬件协议模拟与DMA高效数据搬运的优质实践范例。
1. 项目缘起:为什么用SPI+DMA驱动WS2812B?
如果你玩过单片机点灯,大概率听说过WS2812B这款集成了控制电路和RGB三色LED的智能灯珠。它只需要一根数据线,就能通过特定的时序协议,控制成百上千颗灯珠的颜色,是制作灯带、点阵屏、氛围灯的神器。但玩过的人都知道,驱动它是个“时序苦差事”。传统的做法是用一个GPIO口,通过精确的延时函数(比如__NOP()循环)来模拟0码和1码的时序。这种方法在STM32F103C8T6这种72MHz主频的MCU上,一旦开了中断或者有其他任务干扰,时序就极易出错,导致灯珠显示乱码、闪烁甚至完全不亮。
我最初也是这么干的,直到我想在灯效动画的同时,还要处理串口通信、读取传感器,整个系统就变得极不稳定。于是,我开始寻找一种能“解放CPU”的驱动方式。SPI(串行外设接口)+ DMA(直接存储器访问)的方案就进入了视野。这个方案的巧妙之处在于,它把生成WS2812B严格时序波形的任务,从软件延时转移给了硬件外设。SPI接口负责按固定时钟频率发送数据,DMA负责在后台自动搬运数据到SPI的数据寄存器,CPU只需要在开始时配置好,之后就可以完全不管,去处理其他任务了。这对于资源紧张的STM32F103C8T6(俗称“蓝桥杯”或“核心板”神器)来说,简直是性能和复杂度的完美平衡。
网上关于这个方案的碎片化信息很多,但要么只讲原理,要么代码不完整,缺了关键配置。我花了相当一段时间调试,才把SPI的时钟频率、数据格式、DMA的传输模式这些坑一个个填平。今天,我就把基于标准外设库(StdPeriph_Lib)的完整实现过程、配置细节和避坑心得整理出来,让你能直接“抄作业”,快速实现稳定、高效的WS2812B驱动。
2. 核心原理:SPI波形如何“伪装”成WS2812B信号
要理解SPI+DMA方案,首先得弄清楚WS2812B的通信协议和SPI的输出特性。
2.1 WS2812B的“0”码与“1”码
WS2812B采用单线归零码通信。每个bit由一个高电平脉冲和紧随其后的低电平组成,整个周期约为1.25µs。
- “0”码:高电平时间约0.4µs (T0H),低电平时间约0.85µs (T0L)。
- “1”码:高电平时间约0.8µs (T1H),低电平时间约0.45µs (T1L)。
时序要求比较严格,误差最好控制在±150ns以内。传统的GPIO模拟法,就是靠CPU死循环来卡这个时间。
2.2 SPI的“偷梁换柱”之计
SPI在主机模式下,MOSI线会在SCK时钟的驱动下,每个时钟周期输出一位数据。如果我们把SPI的时钟频率(SPI_BAUDRATE)设置成一个合适的值,使得每个时钟周期的长度刚好是WS2812B一个bit周期(1.25µs)的几分之一,那么我们就能用多个SPI时钟周期来“拼凑”出一个WS2812B的bit。
一个经典且稳定的配置是:将SPI时钟设置为8MHz。
- SPI时钟周期 = 1 / 8MHz = 0.125µs。
- WS2812B的一个bit周期(1.25µs)刚好等于10个SPI时钟周期。
接下来是关键映射:
- 我们需要用一组SPI数据位(比如8位)来表征一个WS2812B的bit。
- 设计这8位数据,让SPI输出的波形在宏观上符合WS2812B的“0”码或“1”码时序。
具体映射规则如下(这是核心中的核心):
- 对于WS2812B的“1”码(要求高电平0.8µs):我们需要在10个SPI时钟里,让高电平占8个周期。因为SPI数据是高位先出(MSB First),我们可以用一个字节
0b1111 1100(即0xFC)来表示。SPI发送0xFC时,会先连续输出7个‘1’(高电平),然后是一个‘0’(低电平),最后两位(因为我们是8位数据,只用了前8个时钟)被忽略或补零。这样,高电平时间就是7 * 0.125µs = 0.875µs,非常接近0.8µs的要求。 - 对于WS2812B的“0”码(要求高电平0.4µs):我们需要在10个SPI时钟里,让高电平占4个周期。可以用一个字节
0b1100 0000(即0xC0)来表示。SPI发送0xC0时,会先连续输出2个‘1’(高电平),然后是6个‘0’(低电平)。这样,高电平时间就是2 * 0.125µs = 0.25µs,虽然略低于0.4µs,但仍在WS2812B的可接受范围内(很多实测表明0xC0是稳定的)。更精确的可以用0b1110 0000(0xE0,高电平3个周期=0.375µs)。
注意:这里为什么是10个SPI时钟对应1个WS2812B bit,但我们只用8位数据?因为SPI通信通常以8位或16位为单位。我们选择8位模式,发送一个字节占用8个SCK周期。但WS2812B的bit周期是1.25µs,在8MHz下是10个周期。多出来的2个周期是低电平(复位时间的一部分),这恰好有助于形成bit之间的间隔,只要整个数据流的连续低电平时间不超过复位码要求(>50µs)即可。
所以,驱动一个WS2812B灯珠(需要24bit GRB数据),我们实际上需要发送 24 * 1 = 24 个SPI字节。每个WS2812B的bit(0或1)被转换成一个对应的SPI字节(0xC0或0xFC)。DMA的任务,就是把这一大段预先转换好的字节数组,源源不断地、自动地搬运到SPI的数据寄存器(SPI->DR)中,从而在MOSI引脚上产生完全由硬件生成的、时序精准的WS2812B控制波形。
3. 硬件连接与工程初始化
3.1 硬件连接图
以STM32F103C8T6为例,我们选择SPI1,因为它挂在高速APB2总线上,可以轻松达到8MHz速率。
| STM32F103C8T6引脚 | 功能 | 连接至 |
|---|---|---|
| PA5 | SPI1_SCK | 悬空即可(仅主机输出时钟) |
| PA7 | SPI1_MOSI | WS2812B灯带的数据输入(DI) |
| 3.3V | 电源 | WS2812B的VCC(注意电平匹配) |
| GND | 地 | WS2812B的GND |
重要提示:
- 电平匹配:STM32是3.3V电平,而WS2812B工作电压通常是5V。虽然很多WS2812B模块在3.3V下也能被触发,但为了稳定性,建议使用5V供电,并在STM32的MOSI引脚和WS2812B的DI引脚之间串联一个100-330Ω的电阻,必要时可使用74HCT245等电平转换芯片。
- 电源:WS2812B全白时单颗电流可达60mA。驱动多颗灯珠必须使用独立、功率足够的5V电源,并与MCU共地。切勿直接从MCU的3.3V取电!
- 滤波电容:在WS2812B灯带的电源入口处,就近并联一个100-1000µF的电解电容和一个0.1µF的瓷片电容,以吸收瞬时大电流引起的电压跌落。
3.2 软件工程准备
- 创建工程:使用Keil MDK或STM32CubeIDE,创建一个基于STM32F103C8T6的工程。
- 选择固件库:我这里使用标准外设库(StdPeriph_Lib)进行讲解,因为它对底层寄存器操作更直观,便于理解原理。如果你用的是HAL库,思路完全一致,只是函数调用不同。
- 添加必要文件:在工程中引入
stm32f10x_spi.c,stm32f10x_dma.c,stm32f10x_gpio.c,stm32f10x_rcc.c等外设库文件,以及对应的头文件。 - 系统时钟配置:确保系统时钟(SYSCLK)设置为72MHz,这是APB2总线能达到36MHz(SPI1时钟源)的前提。APB2的时钟预分频器设置为不分频(即72MHz)。
4. SPI与DMA的详细配置步骤
这是整个项目的核心代码部分,我会逐行解释关键配置。
4.1 GPIO初始化
首先配置PA7为复用推挽输出,因为SPI1_MOSI是复用功能引脚。
void WS281x_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置PA7为复用推挽输出,速度50MHz GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA5作为SCK,虽然我们只发送,但SPI外设需要,也需配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_Init(GPIOA, &GPIO_InitStructure); }4.2 SPI初始化
目标是配置SPI1为主机、仅发送、8位数据格式、8MHz波特率。
void WS281x_SPI_Init(void) { SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // 单线发送模式 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据格式 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:低电平空闲 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位:第一个边沿采样 // 重点:CPOL和CPHA的组合决定了数据输出的时序图。这里选择Mode0 (CPOL=0, CPHA=0) // 但经过实测,对于模拟WS2812B信号,Mode0和Mode3(CPOL=1, CPHA=1)都可以,关键是要和后面的DMA传输配合,确保数据在SCK边沿稳定。 // 更推荐使用 SPI_CPOL_High 和 SPI_CPHA_2Edge (Mode3),这样数据在SCK下降沿变化,在上升沿稳定,观察波形更清晰。 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件管理NSS(片选),这里没用 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 预分频值 // 计算SPI时钟:APB2时钟72MHz / 8 = 9MHz。但实际需要8MHz。 // 所以更精确的配置是使用 SPI_BaudRatePrescaler_9 (72/9=8MHz)。 // 标准库可能没有直接定义9分频,需要查看库文件或直接操作寄存器BR[2:0]位。 // 假设我们使用 SPI_BaudRatePrescaler_8 (9MHz),实测也能工作,容错性很高。 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先出,必须! SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC不用,随便设 SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); // 使能SPI1 // 关键一步:在启动DMA传输前,先手动发送一个字节,确保SPI的TXE标志位就绪,避免DMA首次触发异常。 SPI_I2S_SendData(SPI1, 0x00); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待发送完成 }关于波特率的计算与选择:STM32F103的SPI时钟来源于APB2,最高36MHz(当APB2预分频系数不为1时,最高72MHz)。我们的系统时钟是72MHz,APB2不分频,所以SPI1的时钟源是72MHz。
- 要得到8MHz的SPI_SCK,分频系数应为 72 / 8 = 9。
- 标准外设库的
SPI_BaudRatePrescaler_8代表8分频,得到9MHz。SPI_BaudRatePrescaler_16是16分频,得到4.5MHz。 - 9MHz和8MHz的误差是12.5%,但由于我们之前映射时(0xC0, 0xFC)已经留有余量,且WS2812B时序有一定容错,所以9MHz(0.111µs周期)在实践中完全可用。如果你追求极致,可以修改库文件或直接配置寄存器
SPI1->CR1的BR[2:0]位为010(16分频)或011(32分频)来获得更接近的4.5MHz或2.25MHz,然后调整映射字节。但9MHz是最简单直接的选择。
4.3 DMA初始化与数据缓冲区准备
DMA我们使用通道3(SPI1_TX对应DMA1_Channel3)。
#define LED_NUM 8 // 假设控制8颗灯珠 #define BYTES_PER_LED 24 // 每颗灯珠需要24个SPI字节 uint8_t spi_data_buffer[LED_NUM * BYTES_PER_LED + 50]; // 缓冲区,+50是为了预留复位码空间 void WS281x_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); // SPI1_TX 使用 DMA1 通道3 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(SPI1->DR); // 外设地址:SPI数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)spi_data_buffer; // 内存地址:我们的缓冲区 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 传输方向:内存->外设 DMA_InitStructure.DMA_BufferSize = sizeof(spi_data_buffer); // 传输数据量 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设数据宽度:字节 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 内存数据宽度:字节 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 正常模式(传输一次停止) // 注意:这里必须用 Normal 模式。因为WS2812B需要一段连续的低电平(>50µs)作为复位信号。 // 如果使用循环模式(DMA_Mode_Circular),DMA会不停发送数据,无法产生足够长的低电平复位时间。 DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级高 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 不是内存到内存 DMA_Init(DMA1_Channel3, &DMA_InitStructure); // 使能SPI1的TX DMA请求 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); }数据缓冲区与复位码:WS2812B协议要求,在两帧数据之间,需要至少50µs的低电平复位时间,让灯珠内部锁存数据。在我们的方案中,SPI不发送数据时,MOSI线会保持空闲状态(取决于CPOL)。为了产生这个复位时间,我们必须在发送完所有灯珠数据后,让SPI停止发送,即DMA传输结束。这就是为什么DMA要配置为Normal模式。
一个常见的技巧是:在spi_data_buffer的末尾,额外添加一段全0的数据。例如,我们的缓冲区大小是LED_NUM * 24 + 50。多出来的50个字节,在初始化时全部填充为0x00(对应SPI输出低电平)。当DMA发送完所有灯珠数据后,还会继续发送这50个字节的0。发送50个字节,在8MHz SPI下需要的时间是 50 * 8 * (1/8MHz) = 50µs。这就自动产生了满足要求的复位时间。
4.4 数据编码与刷新函数
我们需要一个函数,将24位的GRB颜色值(WS2812B是GRB顺序!)编码成SPI字节流。
// 将24位颜色值编码到缓冲区指定位置 void WS281x_SetColor(uint16_t led_index, uint8_t red, uint8_t green, uint8_t blue) { uint8_t *p = &spi_data_buffer[led_index * BYTES_PER_LED]; uint32_t color = ((uint32_t)green << 16) | ((uint32_t)red << 8) | blue; // GRB顺序 for(int i = 23; i >= 0; i--) { // 从高位(bit23)开始处理 // 判断该bit是1还是0 if(color & (1 << i)) { *p++ = 0xFC; // WS2812B的'1'码 } else { *p++ = 0xC0; // WS2812B的'0'码 } } } // 刷新所有灯珠(启动DMA传输) void WS281x_Refresh(void) { // 1. 确保上一次DMA传输完成 while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); // 等待通道3传输完成标志 DMA_ClearFlag(DMA1_FLAG_TC3); // 清除标志 // 2. 在缓冲区末尾填充复位码(低电平) uint16_t reset_start_index = LED_NUM * BYTES_PER_LED; for(int i = 0; i < 50; i++) { spi_data_buffer[reset_start_index + i] = 0x00; // 发送0x00,SPI输出持续低电平 } // 3. 重新配置DMA传输数据量(缓冲区总大小) DMA_SetCurrDataCounter(DMA1_Channel3, sizeof(spi_data_buffer)); // 4. 使能DMA通道,开始传输 DMA_Cmd(DMA1_Channel3, ENABLE); }关键点解析:
WS281x_SetColor函数负责将颜色值映射为SPI字节。注意WS2812B的数据顺序是GRB,不是RGB!这是一个经典的坑。- 在
WS281x_Refresh中,我们首先等待上一次DMA传输完成(DMA1_FLAG_TC3)。这是必须的,因为Normal模式下DMA只传输一次。如果在上次传输未完成时再次启动,会导致数据混乱。 - 每次刷新前,我们都在缓冲区末尾写入了50个0x00作为复位码。你也可以在初始化时一次性写好,但这样更灵活。
DMA_SetCurrDataCounter用于重新设置本次要传输的数据数量。在Normal模式传输完成后,这个计数器会归零,下次传输前必须重新设置。
5. 避坑指南与实战调试心得
理论配置完成,但实际调试中会遇到各种问题。下面是我踩过的坑和解决方案。
5.1 灯珠不亮或颜色错乱
- 症状:灯带完全不亮,或只有第一颗灯亮且颜色不对,后面的灯乱闪。
- 排查步骤:
- 检查电源和接地:这是最常见的问题。确保5V电源功率足够,且与STM32共地。用万用表测量灯带入口处的电压,在全白高亮时不应低于4.5V。
- 检查信号线:MOSI引脚是否连接正确?线上最好串一个100Ω电阻。
- 检查SPI时钟频率:用示波器测量SPI_SCK(PA5)引脚。频率应该是8MHz或9MHz左右。如果频率不对,检查系统时钟和SPI分频设置。
- 检查MOSI波形:用示波器捕获MOSI(PA7)引脚在DMA传输时的波形。你应该能看到一连串的脉冲。放大观察单个bit,看高电平时间是否符合“0”码(约0.25-0.4µs)和“1”码(约0.875µs)的特征。如果波形不对,检查SPI的CPOL/CPHA设置和数据映射(0xC0/0xFC)是否正确。
- 检查复位时间:停止发送后,用示波器看MOSI线是否保持了至少50µs的低电平。如果没有,检查缓冲区末尾的复位码0x00是否足够多,以及DMA是否在发送完复位码后确实停止了。
- 检查数据顺序:确认你的颜色值是否正确转换成了GRB顺序,并且是从高位(bit23)开始编码的。
5.2 DMA传输不启动或只传输一部分
- 症状:程序卡在等待DMA完成标志,或者灯珠只更新了部分颜色。
- 排查步骤:
- DMA通道与流:STM32F103只有DMA1,SPI1_TX对应通道3。务必确认。
- 传输计数器:确保
DMA_BufferSize和DMA_SetCurrDataCounter设置的值是你要发送的字节数。数组大小sizeof(spi_data_buffer)是可靠的。 - 传输模式:确认是
DMA_Mode_Normal。如果错误配置为DMA_Mode_Circular,DMA会循环发送,无法产生复位信号。 - SPI DMA请求使能:
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);这行代码必须在DMA初始化之后、启动传输之前调用。 - 缓冲区对齐:虽然不常见,但确保你的
spi_data_buffer数组在内存中没有奇怪的地址对齐问题。通常定义为全局数组即可。
5.3 系统中有其他中断导致时序偶尔出错
- 症状:大部分时间正常,但偶尔灯带会花屏一下,特别是在开启串口接收中断、定时器中断时。
- 解决方案:这正是SPI+DMA方案的优势所在。一旦DMA启动,数据传输由硬件完成,不受CPU中断影响。但需注意:
WS281x_Refresh()函数本身(尤其是等待DMA完成和重新配置DMA的部分)应放在临界区或关闭中断执行,防止被高优先级中断打断,导致DMA配置冲突。
void WS281x_Refresh_Safe(void) { __disable_irq(); // 关中断 while(DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); DMA_ClearFlag(DMA1_FLAG_TC3); // ... 填充复位码、设置计数器等 ... DMA_Cmd(DMA1_Channel3, ENABLE); __enable_irq(); // 开中断 }- 如果使用RTOS(如FreeRTOS),确保DMA传输完成中断或信号量操作不会导致任务调度过于频繁。
5.4 内存占用与性能优化
- 问题:控制灯珠越多,
spi_data_buffer数组越大。对于STM32F103C8T6(20KB RAM),控制100颗灯就需要 100 * 24 + 50 ≈ 2450字节,占用了超过10%的RAM。 - 优化思路:
- 使用动态内存:仅在刷新时从颜色数组动态编码生成SPI缓冲区,刷新完即可释放。但这需要更快的CPU处理速度。
- 双缓冲区:准备两个SPI缓冲区,当DMA从缓冲区A发送数据时,CPU在缓冲区B中准备下一帧的数据。DMA发送完A后,立即切换指向B。这可以实现无缝动画,但需要两倍内存。
- 压缩编码:如果动画变化不大,可以只更新变化部分灯珠对应的SPI缓冲区位置,而不是整个数组。
6. 进阶应用:制作流动彩虹灯效
掌握了基础驱动,我们就可以玩些花样了。下面是一个简单的彩虹渐变灯效示例,它不依赖delay,而是利用系统滴答定时器(SysTick)进行非阻塞刷新。
// 基于HSV色彩空间的彩虹渐变函数(简化版) uint32_t Wheel(uint8_t WheelPos) { WheelPos = 255 - WheelPos; if(WheelPos < 85) { return ((uint32_t)(255 - WheelPos * 3) << 8) | (WheelPos * 3); // 红->绿 } else if(WheelPos < 170) { WheelPos -= 85; return ((uint32_t)(WheelPos * 3) << 16) | (255 - WheelPos * 3); // 绿->蓝 } else { WheelPos -= 170; return ((uint32_t)(255 - WheelPos * 3) << 16) | (WheelPos * 3 << 8); // 蓝->红 } } volatile uint32_t sys_tick = 0; void SysTick_Handler(void) { sys_tick++; } // 1ms中断 void RainbowDemo(void) { uint32_t last_refresh_time = 0; uint16_t hue_offset = 0; while(1) { uint32_t current_time = sys_tick; // 每30ms更新一次灯效 if(current_time - last_refresh_time >= 30) { last_refresh_time = current_time; // 为每颗灯珠设置颜色 for(int i = 0; i < LED_NUM; i++) { uint8_t hue = (i * 256 / LED_NUM + hue_offset) & 0xFF; // 计算色相 uint32_t color = Wheel(hue); uint8_t r = (color >> 16) & 0xFF; uint8_t g = (color >> 8) & 0xFF; uint8_t b = color & 0xFF; WS281x_SetColor(i, r, g, b); } // 刷新显示 WS281x_Refresh_Safe(); // 色相偏移,产生流动效果 hue_offset++; if(hue_offset >= 256) hue_offset = 0; } // 这里可以放心地执行其他任务,如按键扫描、传感器读取等 // Other_Tasks(); } }这个例子展示了如何将WS2812B驱动无缝集成到主循环中,实现复杂的动画效果而不阻塞系统。你可以在此基础上,增加音乐频谱显示、图案显示、网络控制等功能。
7. 移植到HAL库的注意事项
如果你使用STM32CubeMX和HAL库,配置逻辑完全相同,但API有差异。
- CubeMX配置:在
Connectivity->SPI1中,选择Transmit Only Master,参数设置:Data Size: 8 Bits,First Bit: MSB First,Clock Polarity: High,Clock Phase: 2 Edge,Baud Rate: 8 MHz。在DMA Settings标签页,为SPI1_TX添加一个DMA流,模式设为Normal,优先级High。 - 代码差异:
- 初始化后,调用
HAL_SPI_Init()。 - 启动DMA传输使用
HAL_SPI_Transmit_DMA(&hspi1, spi_data_buffer, sizeof(spi_data_buffer))。 - 等待传输完成使用
while(HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);或利用传输完成回调函数HAL_SPI_TxCpltCallback()。 - HAL库的一个大坑:HAL库的SPI DMA发送函数,可能会在传输完成后,自动关闭SPI的DMA请求。这意味着如果你像标准库那样,在传输完成后立即重新配置计数器并启动,可能会失败。解决方案是,在传输完成回调函数中,或者重新启动传输前,重新使能SPI的DMA请求:
__HAL_SPI_ENABLE(&hspi1);和SET_BIT(hspi1.Instance->CR2, SPI_CR2_TXDMAEN);。
- 初始化后,调用
最后,调试时善用示波器。WS2812B的时序是数字逻辑,一切问题最终都体现在MOSI引脚的那根波形上。对照协议要求看波形,是排查问题最快的方法。这套SPI+DMA的方案,一旦调通就极其稳定,你可以彻底从繁琐的延时函数中解放出来,让STM32F103这颗经典的芯片也能流畅驱动上百颗WS2812B,创造出绚丽的灯光效果。
本文还有配套的精品资源,点击获取