ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32 PWM+DMA驱动WS2812B:时序计算与调试指南

STM32 PWM+DMA驱动WS2812B:时序计算与调试指南 简介一套基于STM32的WS2812BNeoPixel灯带控制工程源码包面向嵌入式开发者和智能灯控爱好者解决利用PWMDMA方式高效驱动WS2812B灯串、实现流畅动态灯光效果的问题。资源共216个文件压缩包约7.53MB以C源码、头文件为主辅以编译生成的o、crf、d等中间文件及hex、axf烧录文件包含定时器配置、DMA传输、时序编码等核心代码便于直接查阅、二次开发和烧录验证。已有1699人学习下载适合需要掌握STM32高级定时器与DMA协同驱动WS2812B的开发者参考。内容涵盖WS2812B单线时序协议、PWM输出配置、DMA缓冲区加载与同步机制同时通过工程示例展示如何同步PWM更新事件与DMA传输避免数据丢失可帮助读者理解并实践高效LED控制方案结合工程源码与编译产物能够快速上手移植和调试适用于智能家居、氛围灯、广告屏等场景。1. WS2812BPWMDMA把 NeoPixel 颜色流拆成定时器的占空比序列网络上下载的驱动工程里经常看到“WS2812BPWMDMA”这种压缩包名光看名字其实已经把路线说清楚了不是教你写 GPIO 翻转加延时而是用 STM32 定时器生成 800kHz 左右的 PWM 波再把每个数据位的占空比交给 DMA 连续写入。WS2812B 这类 NeoPixel 灯的难点在于 0 码和 1 码都靠同一根线上的脉宽区分CPU 参与越少越不容易被中断打断。本文把这条链路的时序计算、CubeMX 配置、常用参数和调试手段列一遍适合手上有 STM32 项目、需要驱动几十颗以上灯珠的中级工程师也适合第一次用逻辑分析仪验证信号的人。先把“频率、比较值、DMA 长度”这三个数算对后面所有坑都能在示波器上解释清楚。2. WS2812B 时序与定时器 PWM 参数计算为什么一个位就是一次 ARR 溢出2.1 先看懂 WS2812B 的比特编码WS2812B 数据线是单线制以大约 800kbit/s 的频率连续发送颜色数据。每个 LED 按 GRB 顺序接收 24bit数据全部进入移位寄存器后复位信号50µs 低电平触发锁存。关键点在于0 码和 1 码不是靠电平高低区分而是靠高电平持续时间区分。编码高电平宽度低电平宽度典型周期0 码0.25~0.5µs0.7~1.05µs1.25µs1 码0.55~0.9µs0.3~0.7µs1.25µs复位持续低 ≥50µs——从表中能看出0 码和 1 码的高电平宽度并不要求绝对精确但低电平不能过短否则下一颗灯采样时可能把前一位的下降沿误判成复位。软件延时方案最大的问题不是常数算错而是中断会随时打断延时循环。一个定时器 PWM 周期固定成 1.25µs用硬件比较器来决定每个位的高电平宽度天然避开中断抖动。2.2 PWM 模式下一个比特等于一次 ARR 溢出当定时器工作在 PWM 模式 1 且向上计数时输出电平由计数器 CNT 和比较寄存器 CCR 决定CNT CCR 时输出高CNT ≥ CCR 时输出低。这里把 ARR 设置为 89计数器从 0 数到 89 后溢出周期正好是 90 个计数。如果定时器时钟是 72MHz90 个计数就是 1.25µs也就是 WS2812B 的一个数据位。这样设计后每个 PWM 周期的 CCR 值就代表这一位的高电平时间#define TIM_CLK 72000000UL // TIM2 实际时钟不是系统主频 #define BIT_PERIOD 1250UL // 位周期单位 ns #define T0H_NS 420UL // 0 码高电平目标值 #define T1H_NS 830UL // 1 码高电平目标值 #define PERIOD_ARR (TIM_CLK * BIT_PERIOD / 1000000000UL - 1UL) #define CCR_FOR_0 (TIM_CLK * T0H_NS / 1000000000UL) #define CCR_FOR_1 (TIM_CLK * T1H_NS / 1000000000UL)这组宏的含义是先算出一位需要的时钟周期数并减 1 得到 ARR再用目标高电平时间乘定时器频率得到 CCR。72MHz 下算出来 ARR89CCR_030CCR_159 左右实际工程里我会直接取 CCR_030、CCR_160让 0 码高电平 0.42µs、1 码高电平 0.83µs两边都留出余量。注意 ARR 和 CCR 的单位都是“定时器计数个数”不是纳秒。2.3 用 DMA 连续请求把 CCR 表灌进定时器PWM 定时器本身只负责产生固定周期的波真正让每一位的脉宽不同要靠 DMA 在每个 PWM 周期把下一个比较值写进 CCR。DMA 配置成内存到外设、内存地址自增、外设地址固定外设地址就指向定时器的 CCR1 寄存器。每次触发 DMA 的是定时器比较事件或更新事件这就是“连续请求”的工作方式。为了让写入的新 CCR 不在位周期中间生效必须开启 CCR 预装载。否则 CCR 在计数过程中被改写输出就会在同一个周期内产生一个不必要的毛刺边沿。开启预装载后DMA 写入的值先保存在预装载寄存器等下一次 ARR 溢出才进入工作寄存器正好对应下一个 PWM 周期。实际显示中启动 DMA 后的第一个周期会保持初始 CCR0整段低电平相当于复位的一部分不会影响第一颗灯。2.4 常用主频下的 ARR 与 CCR 对照表不同 STM32 系列的定时器时钟来源不同F1 的 APB1 定时器可能有 2 倍频F4 也可能有最好以 CubeMX 时钟树显示的实际 TIMxCLK 为准。下面这组参数按一个位周期 1.25µs 计算TIM_CLKARRCCR_0CCR_10 码高/低1 码高/低60 位复位时间72MHz8930600.42µs / 0.83µs0.83µs / 0.42µs75µs84MHz10435700.42µs / 0.83µs0.83µs / 0.42µs75µs64MHz7926520.41µs / 0.84µs0.81µs / 0.44µs78µs实际灯珠对时序容差比数据手册标称值宽0 码高电平 0.4µs 上下 0.1µs 都能正常工作。如果换了 MCU 或改了主频直接用 ARR TIM_CLK × 1.25µs 再减 1 重新计算即可。3. 用 STM32CubeMX 把 PWMDMA 跑起来配置、位编码、启动3.1 CubeMX 中 TIM2 的 PWM 与 DMA 最小配置以 STM32F103C8T6 为例先打开 TIM2选择 Internal ClockChannel1 设为 PWM Generation CH1。参数页里 Prescaler 填 0Counter Period 填 89Counter Mode 选 Up。PWM 模式选 PWM1Pulse 初始值填 0Polarity 保持 High。然后在 DMA Settings 页添加请求。不同系列和 CubeMX 版本显示的名称略有差异常见是 TIM2_CH1 或 TIM2_CH1/UP这一路对应 PWM 通道的比较事件HAL 的 HAL_TIM_PWM_Start_DMA 内部走的就是这个请求。如果只看到 TIM2_UPDATE启动和停止逻辑需要按更新中断重写不建议和 PWM Start_DMA 混用。配置项值说明Prescaler0定时器时钟不分频Counter Period8990 个计数 1.25µsPWM ModePWM1CNT CCR 输出高Pulse0初始 CCR启动时输出低CCR PreloadEnable关键必须开启DMA DirectionMemory to Peripheral内存向 CCR 写值Mem IncEnable每周期取下一个比较值Periph IncDisable目标始终是 CCR1Data WidthHalf WordCCR 是 16 位寄存器DMA ModeNormal 或 Circular一帧一次用 Normal持续刷新用 Circular有个容易漏掉的初始化顺序CubeMX 生成的代码里MX_DMA_Init 必须在 MX_TIM2_Init 之前调用否则 DMA 句柄未初始化HAL_TIM_Base_Init 之后找不到通道。代码生成后检查 main 函数调用顺序这是“工程能编译但灯不亮”的高频原因之一。3.2 把 RGB 颜色编码成 CCR 比较值表WS2812B 发送顺序是 G、R、B不是常用的 R、G、B。下面的函数把每颗灯的 24bit 展开成 24 个 CCR 值每个 bit 为 1 时填 CCR_1为 0 时填 CCR_0最后补一段全零比较值作为复位信号。#define LED_COUNT 24 #define RESET_BITS 60 uint16_t ws2812_ccr_buf[LED_COUNT * 24 RESET_BITS]; void ws2812_fill_frame(uint8_t *rgb) { for (uint16_t led 0; led LED_COUNT; led) { uint8_t g rgb[led * 3 0]; uint8_t r rgb[led * 3 1]; uint8_t b rgb[led * 3 2]; uint16_t *dst ws2812_ccr_buf[led * 24]; for (int bit 0; bit 8; bit) { dst[bit] (g (0x80 bit)) ? CCR_1 : CCR_0; dst[bit 8] (r (0x80 bit)) ? CCR_1 : CCR_0; dst[bit 16] (b (0x80 bit)) ? CCR_1 : CCR_0; } } // 复位段全部写 0CCR0 时 PWM 整个位周期保持低电平 memset(ws2812_ccr_buf LED_COUNT * 24, 0, RESET_BITS * sizeof(uint16_t)); }这里用 0x80 配合右移取出一个字节的最高位到最低位保证先发高位。发送顺序按 G、R、B 排进 24bit 窗口。注意缓冲区类型是 uint16_t每个比较值占 2 字节RESET_BITS 取 60 是为了让复位时间达到 75µs比数据手册要求的 50µs 多出 50% 余量。如果用的是 RGB 顺序写代码第一颗灯的颜色就会固定错位。3.3 启动 DMA 并控制一帧的结束缓冲区填好后启动传输只有一行 HAL 调用HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t *)ws2812_ccr_buf, LED_COUNT * 24 RESET_BITS);第三个参数被 HAL 声明成 uint32_t*但缓冲区实际是 uint16_t 数组强制转换只是满足函数签名DMA 仍按半字宽度从内存读取长度计数单位是“半字个数”不是字节数。第四个参数是总比较值个数24 颗灯就是 576 个颜色位加 60 个复位位。DMA 用 Normal 模式时传输结束自动停但定时器 PWM 还会继续按最后一个 CCR 值输出。要在新帧写入前把输出拉低最干净的做法是在 PulseFinished 回调里停掉通道并把 CCR 清零void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_TIM_PWM_Stop_DMA(htim, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, 0); ws2812_busy 0; } }ws2812_busy 是全局标志主循环在发送下一帧前先等它变为 0。不要在回调里直接做动画计算或长时间阻塞操作只清标志耗时的颜色数据生成放到主循环里。如果每帧刷新率不高这种“填表、启动、等待完成”的模式已经足够稳定。4. WS2812BPWMDMA调试清单从灯不亮到颜色错乱4.1 引脚有信号但灯全灭先检查极性和初始 CCR灯完全不亮时先把示波器或逻辑分析仪夹在数据引脚上看是否有周期性的 1.25µs 波形。如果波形存在但呈现“高电平时间长、低电平时间短”的反向状态多半是 PWM 极性选成了 Low。WS2812B 是在上升沿采样低电平为主的数据流会被芯片解读为复位。另一个容易被忽略的问题是 PWM 模式选错。PWM1 和 PWM2 的 CNT 与 CCR 比较方向相反CSDN 上不少“PWM 故障保护”相关讨论最后都落在这一项。把 OC Mode 固定成 PWM1CCR0 时应整周期为低这一步在启动代码里加一句__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 0)即可验证。4.2 第一颗灯正常、后面全灭复位长度和 DMA 长度不对级联情况下所有灯的数据是流水线式的第一颗灯接收并锁存自己的 24bit剩余数据继续往下一颗传。只有复位信号才会让每颗灯把移位寄存器里的数据锁存到输出。如果复位段短于 50µs最后一颗灯可能没有收满 24bit整条灯带的数据整体错位。检查复位段最简单的独立测试如下uint16_t reset_probe[80]; memset(reset_probe, 0, sizeof(reset_probe)); HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t *)reset_probe, 80);正常时数据线应保持低电平 100µs。如果这里能测到完整低电平问题就回到主发送函数DMA 长度是否写成了LED_COUNT * 24而漏了复位位或者数组越界把复位段覆盖掉了。还有一类情况是 DMA 的 MemDataAlignment 配成了 Word此时 DMA 每次从内存取 32 位而 CCR 只读低 16 位位序会每隔一个周期跳变表现为只有第一颗灯偶尔闪。4.3 颜色不对GRB 顺序和 CCR 取值偏差先发个全绿测试色代码里写入rgb[0]0, rgb[1]255, rgb[2]0。如果第一颗灯显示红色说明发送顺序被写成了 RGB如果显示白色或偏色说明 0 码和 1 码的高电平宽度偏离太多。部分灯珠对低电平比较敏感CCR_0 如果超过 0.5µs0 码会被识别成 1 码CCR_1 如果低于 0.55µs1 码又会掉成 0 码。用示波器抓一个字节中最简单的 0xAA 序列应当是“0 码高 0.4µs 左右、低 0.85µs 左右1 码高 0.83µs 左右、低 0.42µs 左右”交替。我一般把 CCR_0 固定为 30CCR_1 在 58~62 之间微调优先保证 1 码高电平不低于 0.8µs因为绝大多数 WS2812B 兼容芯片对高电平宽度更敏感。4.4 定时器时钟不是 72MHzAPB1 预分频的典型坑F103 的 TIM2 挂在 APB1 总线上CubeMX 时钟树里 APB1 显示 36MHz并不代表 TIM2 时钟是 36MHz。只要 APB1 分频系数大于 1定时器时钟会自动乘 2得到 72MHz。如果手工初始化工程时算错这一层用 ARR89 配 36MHz 定时器时钟位周期会变成 2.5µs灯带会以远低于规格的速率采样出现颜色错乱或整条不亮。判断方法很简单示波器量一个完整位周期。如果测到 1.25µs 说明 ARR 算对如果测到 2.5µs先把 APB1 分频逻辑改对。不要直接改 ARR 到 44 来凑 1.25µs因为 CCR_0 和 CCR_1 也要同步缩放否则占空比比例不对。4.5 多外设共用 DMA 时的优先级和连续请求冲突串口 DMA、ADC 多通道 DMA、灯带 DMA 同时工作时偶发花屏先查 DMA 通道优先级。灯带对实时性要求不高但要求每个 1.25µs 周期内 DMA 能及时响应一次半字传输。把灯带 DMA 优先级设为 Very High串口和 ADC 降一级通常能解决大部分偶发错帧。如果现象是固定在某几帧出错检查是否在中断函数里反复启停 DMA。动态刷帧时先停止 DMA、改缓冲区、再重新启动这个过程不能放在高优先级中断里否则 DMA 会请求还没清完就又重新使能造成第一个周期的 CCR 值错位。把启停动作放到主循环或低优先级任务中中断里只置标志位。5. 灯带多了之后的可靠写法双帧缓冲与参数复用5.1 双缓冲让动画刷新不闪烁单缓冲刷新时主循环一边往缓冲区写数据DMA 一边往外发同一段内存容易出现画面撕裂。做法是准备两块长度相同的比较值缓冲区DMA 正在发送 A 区时主循环往 B 区生成下一帧DMA 完成回调里把对比值切换过去。#define FRAME_LEN (LED_COUNT * 24 RESET_BITS) uint16_t ws2812_buf[2][FRAME_LEN]; volatile uint8_t ws2812_use_buf 0; volatile uint8_t ws2812_busy 1; void hal_tim_pwm_pulsefinishedcallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { HAL_TIM_PWM_Stop_DMA(htim, TIM_CHANNEL_1); __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, 0); ws2812_use_buf ^ 1; ws2812_busy 0; } } void ws2812_draw(uint8_t *rgb_frame) { while (ws2812_busy) { // 等待上一帧走完 } ws2812_fill_frame(ws2812_buf[ws2812_use_buf], rgb_frame); ws2812_busy 1; HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t *)ws2812_buf[ws2812_use_buf], FRAME_LEN); }这段代码里ws2812_use_buf 在回调中翻转主循环始终写当前发送缓冲区的对侧。要注意为 0 的全局变量初值在未开始 DMA 时把 busy 置 1防止第一次调用时不等待直接覆盖缓冲区。如果追求更高帧率可以把 DMA 改成 Circular 模式并在 Half Transfer 和 Transfer Complete 两个回调中交换缓冲但那样必须保证每次填入的数据长度完全一致。5.2 大点数灯带注意 16 位 DMA 长度上限STM32 DMA1 的传输计数器 CNDTR 是 16 位一次最多传 65535 个数据项。对 WS2812B 来说一个数据项对应一个位周期所以最多约 2728 颗灯65535 减去复位位再除以 24。超过这个数量时把 DMA 配成 Circular 模式分多次更新缓冲区但复位段必须完整放入每一轮。实际项目中 1024 颗以内用 Normal 模式更简单每帧完成后拉低引脚复位期间灯带自然熄灭。5.3 换芯片时直接套用的参数公式换到 F401、G0、H7 系列时不要照抄 ARR89而是先确认 TIMx 的实际输入时钟。用下面这组公式计算ARR TIM_CLK × 1.25µs - 1 CCR_0 TIM_CLK × 0.42µs CCR_1 TIM_CLK × 0.83µs RESET_BITS 60G0 系列如果主频是 64MHzARR 应为 79CCR_0 取 26CCR_1 取 53H7 系列如果 TIM 时钟 240MHzARR 应为 299CCR_0 取 96CCR_1 取 199。注意 H7 的 DMA 请求映射和 F1 不完全一样HAL_TIM_PWM_Start_DMA 在部分 H7 型号上需要把 DMA 的 FIFO 关闭或开启 Direct Mode否则数据宽度和突发长度设置不当会导致 CCR 更新滞后。测试时把测试颜色换成 R255再观察是不是第一颗灯先亮红色确认改写的顺序函数没有在字节序上再翻一次。本文还有配套的精品资源点击获取
返回列表