资讯动态

STM32F103 PWM+DMA驱动WS2812B灯带:时序换算与代码实现

发布时间:2026/9/14 4:37:52 来源:尧图企业网站定制
简介面向STM32F103嵌入式开发者的WS2812B灯带驱动实例采用PWM方式产生精确时序提供可直接调用的API函数适用于需要实现全彩RGB灯效的项目。资源包共1035个文件以C源文件559个与头文件243个为主另外包含汇编、链接脚本、编译中间文件及IAR/STM32CubeMX工程配置等压缩后约23.77MB便于在现有工程中对比参考。已有919人学习下载。代码保留了完整的驱动框架与底层寄存器配置可帮助理解WS2812B的1码/0码时序生成原理不过开发者需留意原作者提示程序尚需微调实际使用时应结合晶振频率与延时参数进行针对性修改方可稳定驱动灯带。1. WS2812B 灯带为什么值得用 STM32F103 的 PWM 去做跑一个 60 灯的 WS2812B 灯带用 GPIO 翻转加延时也能亮但只要把灯数加到 200、刷新率提到 60Hz再往主循环里塞一个按键扫描或串口打印中断一多就会出现残影和串色。换成 STM32F103 的定时器 PWM 输出后真正干活的变成了硬件比较器每隔 1.25us 定时器溢出一次DMA 自动把下一个 bit 的电平宽度写进 CCR 寄存器CPU 只在换帧的时候参与一次。这套「STM32F103 控制 WS2812B 实例代码新-PWM 方式」把驱动拆成了三块PWM 初始化、DMA 搬运、颜色填充适合手上有 F103 最小系统、想从纯延时驱动升级到硬件时序的人。原作者备注里写了还要微调实际就是指 ARR/CCR 参数和复位信号下面逐个算清楚。2. WS2812B 时序换算ARR 与占空比怎么映射到 72 MHz 定时器WS2812B 的物理协议比串口简单每颗 LED 的 24 bit 颜色数据都通过周期为 1.25us 的脉冲序列发送周期内高电平宽度表示 0 还是 1。0 码典型高电平 0.35us1 码典型高电平 0.7us。整帧数据发完后需要一个至少 50us 的低电平 RESET 信号灯带才会把这一帧颜色锁存到内部寄存器。如果用 GPIO 延时模拟定时中断和任务切换都会打乱节奏用 PWM 方式定时器每次溢出正好是 1.25us 边界溢出时 DMA 把下一个 CCR 值写进比较寄存器下一个周期立即生效。因此关键参数只有三个计数时钟频率、ARR 周期长度、每个 bit 写入的 CCR 值。2.1 从 1.25us 反推 ARR 和预分频F103 默认经过 PLL 后主频是 72MHzTIMx 挂在 APB1 上但倍频后计数时钟也是 72MHz。要让计数溢出周期等于 1.25us最简单是不分频PSC 0计数器时钟 72MHz一个周期需要的计数个数72MHz × 1.25us 90ARR 90 - 1 89TIM_TimeBaseInitTypeDef tb; tb.TIM_Prescaler 0; // 不分频72MHz 计数 tb.TIM_Period 89; // 0~89 共 90 个计数正好 1.25us tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, tb);为什么是 89 而不是 90因为计数器 CNT 从 0 增加到 ARR一共是 ARR1 个计数节拍89 对应 90 个节拍。如果直接写 90实际周期变成 91/72MHz ≈ 1.264us虽然多数 WS2812B 的容差能吃下这 1%但灯一多累计误差会变大后续调色阶时容易在灯带末端看到颜色漂移。2.2 0 码和 1 码的 CCR 取值PWM1 模式、极性为高时输出在 CNT 小于 CCR 期间为高到达 CCR 后拉低直到溢出重新拉高。所以 CCR 的值就是高电平持续了多少个 72MHz 计数。信号典型高电平72MHz 对应计数建议 CCR0 码0.35us25.2281 码0.70us50.460周期1.25us90ARR1 90CCR 28 时高电平 0.389usCCR 60 时高电平 0.833us。相对 50% 采样点45 个计数两个电平的余量分别是 17 和 15 个计数约 0.21~0.24us足够应对芯片个体差异和温度漂移。常见误区是严格按数据手册写 25 和 50结果某批灯珠识别不稳定。手册给的是典型值不是采样窗口中心往中间偏一点成功率反而更高。2.3 为什么必须关闭比较预装载TIM 的 OC3 有一个预装载功能开启后运行中写 CCR3新值要到下一次更新事件才生效。普通 PWM 调光感觉不到但 WS2812B 要求每个 bit 周期都换 CCR预装载会让 DMA 写入的值晚一个周期生效等于把当前 bit 重复了一遍帧结构整体错位。配置输出比较时要显式关掉oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; oc.TIM_OCPolarity TIM_OCPolarity_High; oc.TIM_OCPreload TIM_OCPreload_Disable; TIM_OC3Init(TIM3, oc);TIM_OCPreload_Disable让 DMA 在更新事件写入 CCR 后当前周期立刻按新比较值输出。这一步是 PWM 方式稳定驱动 WS2812B 的前提很多人把 GPIO、DMA 都配好了灯还是乱闪最后发现是预装载没关。3. 用 TIM3 PWM DMA 写 WS2812B 驱动 API库函数与缓冲设计3.1 引脚和定时器选择F103 上 TIM3_CH3 默认映射到 PB0不占用 PA9/PA10 串口方便后续用串口打印调试。PB0 配置为复用推挽输出注意打开 AFIO 时钟否则复用功能不生效。初始化顺序是 GPIO → 定时器时基 → 输出比较 → DMA。static void ws2812_gpio_tim_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); gpio.GPIO_Pin GPIO_Pin_0; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); }这里没有做 TIM3 重映射PB0 就是默认的 TIM3_CH3。如果板子把 PB0 占了F103C8T6 没有足够引脚做重映射建议直接换 TIM2 或 TIM4 的通道而不是去改复用表。3.2 DMA 映射和缓冲数组在 1.25us 内用中断切换 CCR 并不稳定中断入口到写寄存器的时间抖动可能超过 100ns灯多时会偶发错位。我一般用 DMA1 的 Channel3 承载 TIM3_UP 更新事件每次定时器溢出DMA 把缓冲数组中的一个 uint16_t 写入 TIM3-CCR3。#define WS2812_LED_NUM 300 #define WS2812_CCR0 28 #define WS2812_CCR1 60 static uint16_t ws2812_dma_buf[WS2812_LED_NUM * 24]; static void ws2812_dma_init(void) { DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); dma.DMA_PeripheralBaseAddr (uint32_t)TIM3-CCR3; dma.DMA_MemoryBaseAddr (uint32_t)ws2812_dma_buf; dma.DMA_DIR DMA_DIR_PeripheralDST; dma.DMA_BufferSize WS2812_LED_NUM * 24; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Normal; dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dma); TIM_DMACmd(TIM3, TIM_DMA_Update, ENABLE); DMA_ClearFlag(DMA1_FLAG_TC3); }DMA 请求源要看参考手册TIM3_UP对应 DMA1_Channel3不是 Channel5 或 Channel6。用错通道的表现是定时器在跑、DMA 也 Enable但 CCR 始终不被更新PB0 停在初始化时的低电平。我调试时用逻辑分析仪量 PB0 才发现是 DMA 映射错误。DMA_BufferSize是半字数量一个 bit 一个值。300 灯就是 7200 个 uint16_t约 14.4KB。F103C8T6 的 SRAM 是 20KB足够放一整帧如果是 512 灯就快顶满要改用两段缓冲或分批发送否则加几个全局变量就溢出。3.3 颜色填充与帧发送 APIWS2812B 收到的 24 bit 顺序是 G7~G0、R7~R0、B7~B0不是常见的 RGB。填充 API 必须先把 RGB 转成 GRB再从高位往低位比较void ws2812_set_pixel_rgb(uint16_t index, uint8_t r, uint8_t g, uint8_t b) { uint32_t grb ((uint32_t)g 16) | ((uint32_t)r 8) | b; uint32_t mask 0x800000; uint32_t pos (uint32_t)index * 24; for (uint8_t i 0; i 24; i) { ws2812_dma_buf[pos i] (grb mask) ? WS2812_CCR1 : WS2812_CCR0; mask 1; } }mask从 bit23 开始右移表示先发最高位和芯片内部移位寄存器顺序一致。调用方写ws2812_set_pixel_rgb(0, 255, 0, 0)是绿色如果显示成红色问题基本在外部调用传入的 RGB 顺序和这里拼接顺序不一致两个地方必须统一。发送帧要处理两件事上一帧 DMA 有没有完成、RESET 低电平有没有保持够。下面这个同步版本最直观void ws2812_show(void) { while (DMA_GetFlagStatus(DMA1_FLAG_TC3) RESET) { /* 等上一帧发完避免 DMA 正在读取时覆盖缓冲 */ } DMA_ClearFlag(DMA1_FLAG_TC3); TIM_SetCompare3(TIM3, 0); delay_us(80); // 保证至少 80us 低电平覆盖 50us RESET DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, WS2812_LED_NUM * 24); DMA_Cmd(DMA1_Channel3, ENABLE); }注意TIM_SetCompare3(TIM3, 0)必须在启动 DMA 之前做而不是之后。如果先启动 DMA更新事件很可能马上把第一个 CCR 值写入RESET 时间就被吞掉了。delay_us(80)是阻塞的300 灯整帧约 9ms这 80us 只占不到 1%对主循环影响很小。4. 微调实例代码参数边界、复位信号和常见错色定位原作者备注明显写了「还有点问题需要自己去微调程序」实际调的时候按下面的顺序走能最快定位到问题。4.1 先确认 RESET 没有被吞新板子第一次上电最常见的现象不是颜色不对而是灯带完全无反应。先用示波器量 PB0理想波形是一串 1.25us 左右的连续脉冲。如果脉冲正常但灯不亮把delay_us(80)改成 120us 试试。部分合封 WS2812B 对 RESET 低电平时间的下限接近 80us80us 只是保守值下限。如果波形正常、第一颗灯能亮后面不亮通常是发送的 bit 数和实际灯数不一致。检查WS2812_LED_NUM * 24是否和灯数匹配。多一个或少一个 bit最后一颗灯会把下一帧的 RESET 当数据出现整帧位移。4.2 DMA 传输完成标志会骗人用DMA_GetFlagStatus(DMA1_FLAG_TC3)等上一帧传输完成时如果初始化后忘了清一次标志第一次ws2812_show会直接跳过等待在上一帧从未发送的情况下启动 DMA表现是首帧正常、第二帧被吞或整条灯带乱跳。所以在 DMA 初始化最后加一行DMA_ClearFlag(DMA1_FLAG_TC3)很有必要。还要注意 DMA 优先级。F103 的DMA_Priority_High只代表 DMA 内部仲裁CPU 中断仍然可能抢占总线。如果 NVIC 里挂了很多高频中断DMA 在 1.25us 窗口内没写完 CCR会出现偶发错位。把串口、按键这类中断频率压一压或者把 DMA 传输放在没有高优先级中断的服务函数里触发。4.3 CCR 微调方向不同批次灯珠的采样窗口有差异给一个实际项目里可操作的调整表现象调整项方向0 被读成 1WS2812_CCR028 - 251 被读成 0WS2812_CCR160 - 63偶发串色两个值均衡CCR0 加 2CCR1 减 2后端灯乱闪供电和地线加粗 GND就近退耦0 码小于 22 个计数时高电平只有 0.306us在长线传输下容易被噪声吃平1 码不要大于 65否则低电平少于 25 个计数可能违反协议里 THTL 的约束。我习惯每次只改一个值验证通过后再动另一个避免两个变量互相掩盖。4.4 用逻辑分析仪验证一帧时序16MHz 以上采样率的逻辑分析仪抓 PB0量三个点周期长度、T0H、T1H。如果周期不是稳定的 1.25us 而是在 1.08us 和 1.40us 之间跳说明有代码在运行中改了定时器分频或者 DMA 和 CPU 同时在写 CCR3。如果 T1H 后面出现一段异常宽电平多半是复位期间 CCR 被意外改成非 0检查输出比较中断是否误开。还有一个很容易忽略的点调参时不要同时改 CCR0 和 CCR1。先用单色测试把 0 码调到稳定再调 1 码最后做混色压测。这样即使灯珠批次变了也能从历史参数里找到规律。5. 在项目里继续压榨这套 PWM 驱动5.1 把发送改成异步让 DMA 传完整帧再唤醒ws2812_show里的while (DMA_GetFlagStatus(...) RESET)会阻塞到整帧传完。300 灯大约 9ms对交互类项目可以接受但如果你还要驱动屏幕或处理网络协议这 9ms 就有感知了。改成传输完成中断void DMA1_Channel3_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC3)) { DMA_ClearITPendingBit(DMA1_IT_TC3); TIM_SetCompare3(TIM3, 0); ws2812_tx_done 1; } }传输完成瞬间把 CCR 清 0PWM 输出立即变低RESET 从此刻开始计时。主循环里下一帧启动前检查ws2812_tx_done再delay_us(80)补足 RESET然后重新设置 DMA 计数器并启动。这样 CPU 只有填充颜色和启动 DMA 两个动作帧率和亮灯效果都不受主循环策略影响。5.2 帧率上限和灯数预算WS2812B 每颗灯固定 24 bit每 bit 1.25us所以一帧时间大约是 30×N 微秒再加上 RESET 的 50us。300 灯的帧率约 110fps600 灯约 55fps。如果项目对帧率要求是 30fps灯数可以放宽到 1000 颗但要注意 DMA 缓冲已经超过 F103C8T6 的 SRAM这种规模建议把堆缓冲区精简到位域压缩或分段发送单纯扩大uint16_t数组不现实。5.3 加一条 gamma 表解决低亮度断层WS2812B 内置的是 8bit 线性 PWM低亮度下 1~10 的视觉变化非常跳跃。项目里我常预计算一条 256 级 gamma 表通道值经过查表后再送进填充函数static uint8_t gamma8[256]; void ws2812_gamma_init(void) { for (int i 0; i 256; i) { gamma8[i] (uint8_t)(powf(i / 255.0f, 2.2f) * 255.0f 0.5f); } }调用时把r、g、b先映射到gamma8[]再进ws2812_set_pixel_rgb。亮度渐变和暖白混色会平滑很多代价只是 256 字节 SRAM。配合上面的异步 DMA 发送CPU 占用可以控制在 5% 以下剩下的大部分时间还能留给传感器轮询和通信协议。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价