问题现象与背景
在ESP32上使用UART接收不定长帧时,常见方案是DMA搬运数据+空闲中断(IDLE)判断帧结束。但许多开发者反馈:每帧第一个字节总是丢失,后续数据正常。例如发送0x01 0x02 0x03,接收缓冲区得到0x02 0x03。
该问题在Modbus RTU、自定义二进制协议中尤为致命。本文基于ESP-IDF v5.x,从原理到代码完整解析。
原理分析:为什么首字节会丢?
1. UART FIFO与DMA触发机制
ESP32 UART内置128字节FIFO。当DMA使能时,数据流向为:RX引脚→UART FIFO→DMA→内存。DMA传输由FIFO阈值触发(默认UART_FIFO_LEN/2即64字节)。
关键点:空闲中断在总线空闲时触发,但此时FIFO中可能残留少量数据(不足DMA触发阈值)。若中断服务程序(ISR)直接读取DMA缓冲区,会遗漏FIFO中未搬运的数据——首字节往往就在其中。
2. 空闲中断的时序陷阱
空闲中断标志在检测到RX线空闲(通常为1个字符时间)后置位。但DMA搬运存在延迟:若帧很短(如3字节),DMA可能尚未启动,数据全在FIFO中。此时ISR若仅处理DMA缓冲区,自然丢失首字节。
3. 常见错误配置
- 未使能
UART_INTR_RX_FIFO_FULL或UART_INTR_RX_FIFO_TOUT,仅依赖IDLE中断。 - DMA描述符未正确链接,或
dma_burst_size设置不当。 - ISR中未清除中断标志,导致重复进入或漏处理。
排查步骤
步骤1:确认数据丢失位置
在ISR入口读取UART_STATUS_REG的rx_fifo_cnt字段,打印FIFO中剩余字节数。若>0,说明数据卡在FIFO。
uint32_t status = REG_READ(UART_STATUS_REG(0));
uint8_t fifo_cnt = (status >> 0) & 0xFF; // RX_FIFO_CNT
ESP_DRAM_LOGI("ISR", "fifo_cnt=%d", fifo_cnt);
步骤2:检查DMA配置
确认dma_burst_size与FIFO阈值匹配。建议设置dma_burst_size = 4(字节),并启用UART_INTR_RX_FIFO_TOUT(超时中断),超时时间设为2个字符时间。
步骤3:验证中断优先级
空闲中断与DMA中断可能竞争。确保UART中断优先级高于DMA,或在ISR中先关中断再处理。
修复方案与完整代码
核心思路:空闲中断触发后,先读取FIFO剩余数据,再处理DMA缓冲区。同时启用FIFO超时中断作为兜底。
配置代码
#include "driver/uart.h"
#include "soc/uart_reg.h"
#include "esp_intr_alloc.h"
#define BUF_SIZE 256
static uint8_t dma_buf[BUF_SIZE];
static size_t frame_len = 0;
static void IRAM_ATTR uart_isr(void *arg) {
uint32_t intr_status = REG_READ(UART_INT_ST_REG(0));
// 处理空闲中断
if (intr_status & UART_INTR_RX_IDLE) {
REG_WRITE(UART_INT_CLR_REG(0), UART_INTR_RX_IDLE);
// 1. 读取FIFO剩余数据(关键!)
uint32_t status = REG_READ(UART_STATUS_REG(0));
uint8_t fifo_cnt = status & 0xFF;
for (int i = 0; i < fifo_cnt; i++) {
uint8_t ch = REG_READ(UART_FIFO_REG(0));
if (frame_len < BUF_SIZE) dma_buf[frame_len++] = ch;
}
// 2. 处理DMA已搬运数据(此处简化,实际需查询DMA描述符)
// 假设DMA已搬运到dma_buf,需根据描述符更新frame_len
// 3. 帧结束,通知任务
// xTaskNotifyFromISR(...);
}
// 处理FIFO超时中断(兜底)
if (intr_status & UART_INTR_RX_FIFO_TOUT) {
REG_WRITE(UART_INT_CLR_REG(0), UART_INTR_RX_FIFO_TOUT);
// 类似处理
}
}
void uart_dma_init() {
uart_config_t cfg = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(UART_NUM_0, &cfg);
uart_driver_install(UART_NUM_0, BUF_SIZE, 0, 0, NULL, 0);
// 使能DMA
uart_enable_rx_dma(UART_NUM_0);
// 配置FIFO阈值与超时
REG_SET_FIELD(UART_CONF1_REG(0), UART_RX_TOUT_EN, 1);
REG_SET_FIELD(UART_CONF1_REG(0), UART_RX_TOUT_THRHD, 2); // 2字符时间
// 使能中断
uart_enable_rx_intr(UART_NUM_0);
uart_enable_intr_mask(UART_NUM_0, UART_INTR_RX_IDLE | UART_INTR_RX_FIFO_TOUT);
// 注册ISR
esp_intr_alloc(ETS_UART0_INTR_SOURCE, ESP_INTR_FLAG_IRAM, uart_isr, NULL, NULL);
}
关键修复点
-
ISR中先读FIFO:空闲中断后立即读取
UART_FIFO_REG,将残留字节追加到帧尾(注意顺序:FIFO数据在DMA数据之后)。 - 启用FIFO超时中断:当帧长小于DMA阈值时,超时中断可提前触发,避免依赖空闲中断。
-
DMA描述符同步:若使用链表描述符,需在ISR中检查
OWN位,确保数据已搬运。
注意事项
- ISR必须加
IRAM_ATTR,避免Flash操作导致延迟。 - 读取FIFO时需循环直到
fifo_cnt为0,防止数据堆积。 - 多帧连续到达时,需在ISR中重置
frame_len并拷贝数据到任务队列。 - 若使用ESP-IDF的
uart_read_bytes,需关闭DMA,否则冲突。 - 调试时可用逻辑分析仪抓取RX线,确认首字节是否到达引脚,排除硬件问题。
总结
丢首字节的本质是空闲中断与DMA搬运的时序不同步。通过ISR中主动读取FIFO残留数据,并辅以FIFO超时中断,可彻底解决。该方案已在实际项目中稳定运行,支持最高3Mbps波特率下的不定长帧接收。
暂无评论,来抢沙发~