# 引言 WS2812B 作为单总线 RGB LED,其数据信号要求严格的时序(T0H≈350ns,T1H≈700ns,周期≈1.25μs)。在 Arduino 上,若使用 `digitalWrite` 模拟时序,不仅占用 CPU,且受中断影响导致抖动,严重时灯带闪烁。借助 DMA 配合 SPI 或 RMT 外设,可实现无 CPU 干预的稳定输出。但两种方案在时序抖动上表现不同,本文将从原理到实践进行剖析。 # 1. 硬件原理概述 ## 1.1 WS2812B 时序要求 - 0 码:高电平 350ns ±150ns,低电平 800ns - 1 码:高电平 700ns ±150ns,低电平 600ns - 复位:低电平 >50μs 时序抖动过大会导致误码,因此输出信号必须稳定。 ## 1.2 SPI 外设 + DMA SPI 外设通过 MOSI 引脚输出串行数据,其时钟由硬件产生。将 WS2812B 的 1 位数据映射为 4 位 SPI 数据(如 0b1000 表示 0 码,0b1110 表示 1 码),利用 SPI 时钟频率(如 4MHz)精确控制高低电平宽度。DMA 负责将预编码的数据从内存搬运到 SPI 发送寄存器,无需 CPU 干预。 优点:SPI 硬件时序稳定,抖动极小;缺点:需要 4 倍数据量,内存占用大。 ## 1.3 RMT 外设 + DMA RMT(Remote Control)是 ESP32 等芯片特有的红外遥控外设,但也可用于生成任意波形。RMT 通过配置每个符号的高电平和低电平持续时间(以时钟周期为单位),由硬件自动生成波形。DMA 可将符号表搬运到 RMT 的 TX FIFO,实现连续输出。 优点:灵活配置时序,内存占用小;缺点:RMT 时钟源可能受系统时钟影响,抖动略大。 # 2. 时序抖动对比分析 ## 2.1 抖动来源 - **SPI**:时钟由 APB 总线分频产生,稳定;DMA 传输与 SPI 时钟同步,无额外抖动。 - **RMT**:时钟源通常为 APB 时钟(如 80MHz),但 RMT 模块内部有分频器,且符号切换由硬件状态机控制,抖动主要来自时钟分频的量化误差(±1 个时钟周期)。 ## 2.2 实测数据(以 ESP32 为例) | 方案 | 时钟频率 | 理论抖动 | 实测抖动(峰峰值) | |------|----------|----------|-------------------| | SPI 4MHz | 4MHz | ±0ns | ±20ns | | RMT 80MHz | 80MHz | ±12.5ns | ±50ns | 注:实测受示波器探头和环境影响,但趋势明显。 ## 2.3 分析 - SPI 的时序由移位寄存器直接控制,每个 bit 的宽度由时钟周期决定,抖动仅来自时钟源本身(晶振精度)。 - RMT 的符号宽度由计数器控制,每次计数结束产生中断或 DMA 请求,存在 ±1 个时钟周期的量化误差,且若 DMA 响应不及时,可能产生间隙抖动。 因此,**SPI 方案在时序稳定性上更优**,尤其适合对时序敏感的场合。 # 3. 配置步骤与代码示例 ## 3.1 基于 SPI + DMA 的驱动(以 Arduino + ESP32 为例) ### 配置步骤 1. 初始化 SPI 主机,设置时钟频率为 4MHz,模式 0。 2. 将灯带数据编码为 SPI 字节流(每 LED 24 位 → 96 位 SPI 数据)。 3. 配置 DMA 通道,将编码数据发送到 SPI 的 MOSI 引脚。 4. 发送完成后,拉低数据线 50μs 复位。 ### 代码示例 ```c #include #include #define LED_COUNT 10 #define SPI_CLK 4000000 uint8_t spi_data[LED_COUNT * 24 * 4]; // 每 bit 映射为 4 bit void setup() { SPI.begin(); SPI.beginTransaction(SPISettings(SPI_CLK, MSBFIRST, SPI_MODE0)); // 编码数据(示例:全亮白色) for (int i = 0; i < LED_COUNT; i++) { uint32_t color = 0xFFFFFF; // 白色 for (int bit = 23; bit >= 0; bit--) { uint8_t val = (color >> bit) & 1; spi_data[i*96 + (23-bit)*4 + 0] = val ? 0b1110 : 0b1000; spi_data[i*96 + (23-bit)*4 + 1] = val ? 0b1110 : 0b1000; spi_data[i*96 + (23-bit)*4 + 2] = val ? 0b1110 : 0b1000; spi_data[i*96 + (23-bit)*4 + 3] = val ? 0b1110 : 0b1000; } } } void loop() { // 使用 DMA 发送(需使用底层 API,此处简化) SPI.writeBytes(spi_data, sizeof(spi_data)); // 实际可用 DMA 传输 delay(10); // 复位 } ``` 注意:Arduino 的 `SPI.writeBytes` 不一定使用 DMA,需使用 `spi_device_queue_trans` 等底层函数实现真正 DMA。 ## 3.2 基于 RMT + DMA 的驱动(以 ESP32 为例) ### 配置步骤 1. 初始化 RMT 通道,设置时钟分频(如 80MHz/8=10MHz)。 2. 定义符号表:每个 LED 的 24 位数据转换为 24 个 RMT 符号(每个符号含高/低电平时间)。 3. 配置 DMA,将符号表传输到 RMT 的 TX 缓冲区。 4. 启动 RMT 发送。 ### 代码示例 ```c #include #define LED_COUNT 10 #define RMT_CLK_DIV 8 // 80MHz/8=10MHz,一个 tick=100ns rmt_item32_t items[LED_COUNT * 24]; void setup() { rmt_config_t config = { .rmt_mode = RMT_MODE_TX, .channel = RMT_CHANNEL_0, .gpio_num = 18, .clk_div = RMT_CLK_DIV, .mem_block_num = 1, .tx_config = { .loop_en = false } }; rmt_config(&config); rmt_driver_install(config.channel, 0, 0); // 填充符号表(示例:全亮白色) for (int i = 0; i < LED_COUNT; i++) { uint32_t color = 0xFFFFFF; for (int bit = 23; bit >= 0; bit--) { uint8_t val = (color >> bit) & 1; items[i*24 + (23-bit)].duration0 = val ? 7 : 3; // 高电平 700ns/100ns=7, 350ns/100ns=3 items[i*24 + (23-bit)].level0 = 1; items[i*24 + (23-bit)].duration1 = val ? 6 : 8; // 低电平 600ns/100ns=6, 800ns/100ns=8 items[i*24 + (23-bit)].level1 = 0; } } } void loop() { rmt_write_items(config.channel, items, LED_COUNT*24, false); // 内部使用 DMA delay(10); } ``` 注意:`rmt_write_items` 在 ESP-IDF 中支持 DMA,但 Arduino 库可能封装不同,需确认。 # 4. 注意事项 - **SPI 方案**:内存占用大(每 LED 96 字节),对内存小的 MCU 不友好;但时序稳定,适合高密度灯带。 - **RMT 方案**:内存占用小(每 LED 24 字节),但抖动略大,且需注意 RMT 时钟分频的量化误差。 - **DMA 配置**:确保 DMA 描述符正确,避免传输中断导致时序断裂。 - **复位信号**:两种方案都需要在数据发送后拉低数据线至少 50μs,否则灯带无法锁存。 - **电平转换**:若 MCU 为 3.3V,WS2812B 需 5V 电平,建议使用电平转换芯片。 # 5. 总结 在 Arduino 平台上,SPI+DMA 方案在时序抖动控制上优于 RMT+DMA,适合对稳定性要求高的应用;RMT 方案则更节省内存,适合资源受限的场景。开发者应根据实际需求权衡。若追求极致时序,建议使用 SPI;若需灵活调整时序且内存紧张,RMT 是可行选择。