WS2812B 是嵌入式开发用得最多的可编程 LED:一根数据线串联几百颗灯珠,每颗 24-bit 颜色。它没有时钟线,靠脉冲宽度区分 0/1,时序要求苛刻(允许误差约 ±150 ns)——这也正是初学驱动时最容易翻车的地方。本文从时序本质讲起,给出 ESP32 上用 RMT 外设 + DMA 实现零 CPU 占用的完整方案和踩坑点。
一、先看懂 WS2812B 的时序协议
WS2812B 单线协议,数据靠脉冲宽度编码:
- 逻辑 0:0.4 μs 高电平 + 0.85 μs 低电平
- 逻辑 1:0.85 μs 高电平 + 0.4 μs 低电平
- 复位:低电平保持 >50 μs
ESP32 主频 240 MHz,一个指令周期 4.17 ns——意味着翻转一个 GPIO 只有约 36 个时钟周期的余量。误差超过 ±150 ns,灯珠就可能误读、颜色闪烁。
Arduino 教程最常见的做法是delayMicroseconds()+digitalWrite()。能点亮,但性能代价很大:刷一颗灯珠就要 30 μs,120 颗刷新一次 3.6 ms,期间 CPU 被硬延时占满,WiFi、蓝牙、传感器全部暂停。
ESP32 解决这个问题有一个专用硬件模块:RMT。
二、RMT 是什么:可编程数字波形发生器
RMT(Remote Control)最初为红外遥控设计,本质是一个可编程的数字波形发生器——独立的硬件状态机,不需要 CPU 介入就能按预设脉冲序列精确翻转 GPIO。
ESP32 有 8 个 RMT 通道,数据流:
DMA 引擎 → 64×32-bit FIFO 缓冲区 → GPIO 输出 ↑ RMT 硬件状态机(产生精确时序)每个 32-bit RMT 数据项编码一对电平:
- 低 15 位:高电平持续的时钟周期数(
duration0) - 高 15 位:低电平持续的时钟周期数(
duration1) - bit 15 / bit 31:电平标志
RMT 输入 80 MHz 基准时钟(APB 时钟分频),1 tick = 12.5 ns。WS2812B 需要的 0.4 μs = 32 ticks、0.85 μs = 68 ticks,整除无误差。
三、核心思路:预编码脉冲序列 + DMA 自动喂给硬件
一颗灯珠需要 24 bit(GRB 各 8 bit),120 颗 = 2880 bit。每个 bit 一个rmt_item32_t,共 2880 个 ≈ 11.5 KB 缓冲区。
关键步骤:内存里预先算好整条灯带的脉冲序列,DMA 自动搬运给 RMT 硬件。CPU 只参与一次预编码,之后完全不碰 GPIO。
#include"driver/rmt.h"#defineWS2812B_T0H32// 0.4 μs / 12.5 ns = 32 ticks#defineWS2812B_T0L68// 0.85 μs#defineWS2812B_T1H68// 0.85 μs#defineWS2812B_T1L32// 0.4 μsvoidws2812b_encode_pixel(rmt_item32_t*items,uint8_tg,uint8_tr,uint8_tb){uint32_tgrb=((uint32_t)g<<16)|((uint32_t)r<<8)|b;for(inti=0;i<24;i++){if(grb&(1<<(23-i))){items[i].duration0=WS2812B_T1H;items[i].level0=1;items[i].duration1=WS2812B_T1L;items[i].level1=0;}else{items[i].duration0=WS2812B_T0H;items[i].level0=1;items[i].duration1=WS2812B_T0L;items[i].level1=0;}}}这段代码没有delay、没有忙等循环,是纯数据转换——把颜色值变成 RMT 硬件能直接理解的电平-持续时间对。
四、配置 RMT 并启动传输
voidws2812b_init(intgpio_num,intchannel){rmt_config_tconfig=RMT_DEFAULT_CONFIG_TX(gpio_num,channel);config.clk_div=2;// 80 MHz / 2 = 40 MHz → 1 tick = 25 nsrmt_config(&config);rmt_driver_install(channel,0,0);}voidws2812b_send(intchannel,rmt_item32_t*items,intnum_items){rmt_write_items(channel,items,num_items,true);rmt_wait_tx_done(channel,portMAX_DELAY);}两个容易忽略的点:
rmt_write_items第三个参数true= 发送完最后一项后总线保持低电平——正好是 WS2812B 需要的 >50 μs 复位信号。rmt_wait_tx_done会阻塞当前任务,但这是自愿让出 CPU——FreeRTOS 在这期间调度其他任务。RMT 硬件产生波形时,CPU 可以运行任何代码,与delayMicroseconds()的硬阻塞有本质区别。
五、完整示例:120 颗灯珠彩虹色轮
#defineLED_COUNT120#defineRMT_CHANNELRMT_CHANNEL_0#defineLED_GPIO18staticrmt_item32_tled_buffer[LED_COUNT*24];voidset_all_leds(uint8_tr,uint8_tg,uint8_tb){for(inti=0;i<LED_COUNT;i++)ws2812b_encode_pixel(&led_buffer[i*24],g,r,b);}voidled_task(void*arg){ws2812b_init(LED_GPIO,RMT_CHANNEL);inthue=0;while(1){for(inti=0;i<LED_COUNT;i++){intpixel_hue=(hue+i*256/LED_COUNT)%256;uint8_tr,g,b;hsv2rgb(pixel_hue,255,128,&r,&g,&b);ws2812b_encode_pixel(&led_buffer[i*24],g,r,b);}ws2812b_send(RMT_CHANNEL,led_buffer,LED_COUNT*24);hue++;vTaskDelay(pdMS_TO_TICKS(50));}}120 颗 × 24 bit × 1.25 μs/bit ≈ 3.6 ms 传输时间。这 3.6 ms 里 RMT 硬件在工作,CPU 可同时处理 WiFi、读传感器、跑控制算法。对比delayMicroseconds()方案同样 3.6 ms CPU 被锁定、连中断都无法及时响应——这就是硬件 DMA 和硬延时的差距。
六、为什么 RMT 能精确到 12.5 ns
RMT 内部有独立分频器和 8-bit 计数器。以clk_div = 2为例:80 MHz / 2 = 40 MHz,tick = 25 ns,0.4 μs = 16 ticks、0.85 μs = 34 ticks,整除无误差。
duration0/duration1是 15-bit 字段(最大 32767),25 ns/tick 下最大脉冲 ≈ 819 μs,远超 WS2812B 需求(最长 0.85 μs)。
关键在于 RMT 状态机输出一个脉冲对时完全自主——从 FIFO 读数据项、解析 duration/level、依次输出,全程无 CPU load/store、无中断延迟、无指令流水线停顿、无缓存未命中。这是它比 bit-banging 时序更稳的根本原因。
七、方案对比
| 方案 | CPU 占用 | 并发能力 | 时序稳定性 |
|---|---|---|---|
| RMT + DMA | 几乎为零 | WiFi/BLE/SPI/I2C 可同时跑 | 硬件状态机,不受中断影响 |
| bit-banging(IRQ) | 约 15% | WiFi 偶发丢包 | 中断抢占引入几十~上百 ns 抖动 |
| delayMicroseconds() | 100%(刷新期间) | 无 | 无抖动但 CPU 完全锁定 |
bit-banging 每次中断来临都会引入不确定性,在 WS2812B 的 800 Kbps 速率下,偶尔导致灯珠误读和闪烁。RMT 硬件状态机的输出不受这些因素干扰。
八、局限说清楚
GPIO/内存限制。8 个 RMT 通道共享同一块 RAM(512×32-bit),多路并行时每通道 DMA 缓冲深度受限(ESP32-S3 扩展到 768×32-bit)。
内存开销。每颗灯珠 24 × 4 = 96 字节 RMT 缓冲区,120 颗 = 11.5 KB;delayMicroseconds()方案只需 120 × 3 = 360 字节颜色数组——多用了约 30 倍内存。ESP32 的 520 KB SRAM 没问题,STM32F103 的 20 KB SRAM 就要掂量。
不可移植。RMT 是 Espressif 特有硬件,同样的 WS2812B 驱动到 STM32/nRF52 需重写(STM32 用 TIM PWM + DMA 可实现类似效果,但 API 完全不同)。
复位时序依赖阻塞。当前实现靠rmt_wait_tx_done+ RMT「最后一 bit 后保持低电平」。若改非阻塞发送(rmt_write_items传false),需自己保证 >50 μs 复位时序。
九、ESP-IDF 新 RMT 驱动的说明
ESP-IDF v5.x 起 RMT 驱动重构为rmt_tx_channel_config_t风格(rmt_new_tx_channel/rmt_transmit),旧driver/rmt.h的rmt_write_itemsAPI 标记为 legacy。功能等价,但新 API 用rmt_bytes_encoder_config_t编码位流,更适合批量灯带。
两套 API 的区别值得说明:
| 对比项 | legacy(driver/rmt.h) | 新 API(driver/rmt_tx.h) |
|---|---|---|
| 初始化 | rmt_config+rmt_driver_install | rmt_new_tx_channel |
| 发送 | rmt_write_items | rmt_transmit |
| 编码方式 | 手工填rmt_item32_t | rmt_bytes_encoder/rmt_symbol_encoder |
| 适用 IDF | v4.x 为主 | v5.x+ 推荐 |
rmt_symbol_encoder可以用符号(电平对)直接编码 WS2812B 的 0/1 波形,配合rmt_transmit的拷贝模式(RMT_TRANSMIT_MODE_BLOCKING)实现。手工填rmt_item32_t的思路仍是理解 RMT 本质的基础,两套 API 底层都是同一套硬件时序逻辑。
如果用的是较新 IDF,参考官方led_strip组件(esp-idf/examples/peripherals/rmt/led_strip)更省事——它已封装好 WS2812B 驱动,内部就是 RMT + DMA 方案。
十、下一步
- 读 ESP32 RMT 编程指南
- 把灯带驱动的延时改成 RMT 编码,上面代码可直接跑
- 在 FreeRTOS 任务里刷新灯带同时开 WiFi——之前被硬延时吃掉的功能会突然都回来
RMT 只是 ESP32 硬件生态的一个缩影。这片芯片上几乎所有「看起来需要 bit-banging」的事——WS2812B、DHT22、红外遥控、步进电机 PWM——都有对应硬件模块可以零 CPU 占用搞定。