引言

WS2812B 作为单总线 RGB LED,对时序要求严苛(±150ns 容差)。ESP32 的 RMT 外设虽能生成精确波形,但中断服务程序(ISR)的响应延迟(通常 1-20μs)会破坏连续帧传输,导致闪烁或颜色偏移。本文从原理出发,提出一种结合硬件定时器与 DMA 的补偿机制,彻底消除中断抖动。

1. RMT 外设与 WS2812B 时序基础

1.1 RMT 工作原理

RMT(Remote Control)模块本质是一个可编程脉冲发生器,内部包含 8 个通道,每个通道有 64 个 32 位 RAM 槽位。每个槽位存储 {duration, level} 对,硬件自动按顺序输出,无需 CPU 干预。

1.2 WS2812B 时序要求

  • T0H: 0.35μs ±150ns(高电平 0.35μs)
  • T0L: 0.80μs ±150ns(低电平 0.80μs)
  • T1H: 0.70μs ±150ns(高电平 0.70μs)
  • T1L: 0.60μs ±150ns(低电平 0.60μs)
  • RESET: >50μs 低电平

RMT 时钟源通常为 APB(80MHz),每个 tick 为 12.5ns,因此 0.35μs 对应 28 ticks,0.80μs 对应 64 ticks。

2. 中断延迟的来源与影响

2.1 延迟来源

  • 中断响应时间:从硬件触发到 ISR 执行,平均 12 个时钟周期(约 150ns),但若 CPU 正在执行关键段(如 Flash 读取),可能延长至 20μs。
  • ISR 处理时间:RMT 中断处理中,若需重新填充 RAM 槽位,涉及寄存器操作和内存拷贝,耗时 5-10μs。
  • 任务调度:若使用 FreeRTOS,任务切换可能引入额外延迟。

2.2 影响分析

当 RMT 发送完当前帧(例如 60 个 LED),硬件产生中断,ISR 需在下一个 RESET 周期前填充新数据。若延迟超过 RESET 时间(50μs),LED 会误认为传输结束,导致数据丢失。即使未超时,延迟也会造成帧间间隔不一致,引起视觉闪烁。

3. 补偿策略:硬件定时器 + DMA 双缓冲

3.1 核心思想

利用 DMA 自动搬运数据到 RMT RAM,避免 ISR 直接填充。同时,使用硬件定时器(如 GPTimer)在帧间产生精确的 RESET 信号,并提前触发 DMA 传输,从而隐藏中断延迟。

3.2 架构设计

  • RMT 通道:配置为连续发送模式,RAM 槽位由 DMA 从内存缓冲区填充。
  • DMA 描述符:使用双缓冲(ping-pong),一个缓冲区用于当前帧,另一个用于下一帧。
  • GPTimer:在帧结束前 T_reset 时间触发中断,启动 DMA 传输下一帧数据。

4. 配置步骤与代码实现

4.1 硬件连接

  • GPIO18 连接 WS2812B DIN,电源 5V,共地。

4.2 初始化 RMT 与 DMA

#include "driver/rmt_tx.h"
#include "driver/gptimer.h"
#include "esp_heap_caps.h"

#define RMT_TX_CHANNEL 0
#define GPIO_NUM 18
#define LED_NUM 60

// 定义 RMT 符号(基于 80MHz 时钟)
#define T0H_TICKS 28
#define T0L_TICKS 64
#define T1H_TICKS 56
#define T1L_TICKS 48

// 编码一个字节
void ws2812_encode_byte(uint8_t byte, rmt_symbol_word_t *sym) {
    for (int i = 0; i < 8; i++) {
        if (byte & (1 << (7 - i))) {
            sym[i].level0 = 1; sym[i].duration0 = T1H_TICKS;
            sym[i].level1 = 0; sym[i].duration1 = T1L_TICKS;
        } else {
            sym[i].level0 = 1; sym[i].duration0 = T0H_TICKS;
            sym[i].level1 = 0; sym[i].duration1 = T0L_TICKS;
        }
    }
}

// 初始化 RMT 通道
void rmt_init() {
    rmt_tx_channel_config_t tx_chan_config = {
        .gpio_num = GPIO_NUM,
        .clk_src = RMT_CLK_SRC_APB,
        .resolution_hz = 80 * 1000 * 1000, // 80MHz
        .mem_block_symbols = 64,
        .trans_queue_depth = 4,
        .flags.invert_out = false,
    };
    rmt_new_tx_channel(&tx_chan_config, &tx_channel);

    rmt_tx_encoder_config_t encoder_config = {
        .encode_cb = ws2812_encode_cb, // 自定义编码回调
    };
    rmt_new_bytes_encoder(&encoder_config, &encoder);
}

// 编码回调:将 RGB 数据转为 RMT 符号
size_t ws2812_encode_cb(const void *data, size_t data_len, rmt_encode_state_t *state, rmt_symbol_word_t *symbols, size_t max_symbols) {
    uint8_t *rgb = (uint8_t*)data;
    size_t sym_idx = 0;
    for (int i = 0; i < data_len; i++) {
        if (sym_idx + 8 > max_symbols) break;
        ws2812_encode_byte(rgb[i], &symbols[sym_idx]);
        sym_idx += 8;
    }
    state->encoded = sym_idx;
    return sym_idx;
}

4.3 使用 GPTimer 触发 DMA 传输

gptimer_handle_t gptimer;

void gptimer_init() {
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_APB,
        .resolution_hz = 80 * 1000 * 1000,
        .alarm_count = 1000, // 1μs 示例,实际根据帧长计算
    };
    gptimer_new_timer(&timer_config, &gptimer);

    gptimer_event_callbacks_t cbs = {
        .on_alarm = on_timer_alarm,
    };
    gptimer_register_event_callbacks(gptimer, &cbs, NULL);
    gptimer_enable(gptimer);
    gptimer_start(gptimer);
}

// 定时器中断回调:启动下一帧传输
bool on_timer_alarm(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
    // 启动 DMA 传输(非阻塞)
    rmt_transmit(tx_channel, encoder, next_frame_data, frame_size, &tx_config);
    return true; // 不唤醒高优先级任务
}

4.4 主程序逻辑

void app_main() {
    // 初始化 RMT 和 GPTimer
    rmt_init();
    gptimer_init();

    // 分配双缓冲
    uint8_t *frame_buf[2];
    for (int i = 0; i < 2; i++) {
        frame_buf[i] = heap_caps_malloc(LED_NUM * 3, MALLOC_CAP_DMA);
    }

    // 循环更新 LED
    while (1) {
        // 更新当前帧数据(例如从颜色计算)
        update_led_colors(frame_buf[current]);
        // 等待定时器触发传输(或手动触发)
        vTaskDelay(pdMS_TO_TICKS(10));
        current ^= 1;
    }
}

5. 注意事项与优化建议

  • DMA 缓冲区必须放在内部 RAM,且 4 字节对齐,使用 MALLOC_CAP_DMA
  • RMT 通道 RAM 大小有限(64 符号),若 LED 数量多,需分段传输,但分段会增加中断次数,建议使用 DMA 连续模式。
  • GPTimer 的 alarm 值需精确计算:帧传输时间 = LED数 * 24 * (T0H+T0L) 或 (T1H+T1L) 的平均值,加上 RESET 时间。提前 10μs 触发 DMA 传输,以覆盖中断延迟。
  • 中断优先级:将 GPTimer 中断优先级设为最高(如 1),确保及时响应。
  • 避免在 ISR 中调用阻塞函数,如 printfvTaskDelay
  • 测试不同时钟源:APB 时钟可能受 WiFi 影响,可考虑使用 PLL 或外部晶振。

6. 总结

通过硬件定时器预触发 DMA 传输,将中断延迟从关键路径中移除,RMT 输出时序完全由硬件控制,抖动可控制在 ±50ns 以内。此方案不仅适用于 WS2812B,也可推广到其他严格时序的单总线协议(如 DHT11、DS18B20)。开发者应根据实际硬件调整定时器参数,并利用逻辑分析仪验证波形。