一、问题背景:为什么不定长帧会半包、粘包
在嵌入式项目中,ESP32通过UART对接传感器、上位机或总线设备时,帧长往往不固定。若用轮询或逐字节中断接收,常见两类错误:
- 半包:一帧数据尚未收完,程序就读取并处理,导致解析失败。
- 粘包:两帧数据在缓冲区中连在一起,被当成一帧处理。
根因是“接收完成”的判定时机不准确。UART本身没有帧概念,只有字节流。要正确切帧,必须借助总线空闲这一物理特征:一帧结束后,线路会保持空闲电平一段时间。ESP32的UART外设支持空闲中断(UART_IDLE),配合DMA可高效判定帧边界。
二、原理:DMA环形缓冲 + 空闲中断
ESP32的UART DMA接收采用环形缓冲(ring buffer):DMA持续把RX FIFO数据搬入内存环形区,写指针循环前进。CPU只需在空闲中断中读取“当前写位置”,即可知道这一帧的结束点。
关键点:
- 空闲中断触发条件:接收线上连续空闲超过设定阈值(ESP32以波特率周期计),说明一帧传输结束。
- 长度快照:中断中读取DMA已写入的字节数,与上次处理位置比较,差值即本帧长度。
- 半包避免:只有空闲中断到来才认为帧结束,避免“读到一半就处理”。
- 粘包避免:若两帧间隔极短未触发空闲,需在应用层按协议头/长度字段二次拆分,或用超时兜底。
三、配置步骤(ESP-IDF)
- 配置UART参数:波特率、数据位、停止位、校验位。
- 安装UART驱动,开启
uart_driver_install并指定RX环形缓冲大小。 - 使能空闲中断:
uart_enable_rx_intr(UART_NUM_1, UART_INTR_IDLE)。 - 注册中断回调或在任务中等待事件,读取
uart_get_buffered_data_len获取当前帧长。 - 应用层做帧解析与粘包拆分。
四、完整代码示例
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "string.h"
#define UART_PORT UART_NUM_1
#define UART_TX_PIN 17
#define UART_RX_PIN 16
#define BUF_SIZE 1024
static QueueHandle_t uart_queue;
// 帧解析:按协议头0xAA55 + 长度字段拆分粘包
static void parse_frame(const uint8_t *data, int len)
{
int i = 0;
while (i + 4 <= len) {
if (data[i] == 0xAA && data[i+1] == 0x55) {
uint8_t payload_len = data[i+2];
int frame_len = 4 + payload_len + 1; // 头2+长度1+负载+校验1
if (i + frame_len <= len) {
// 完整帧,交给业务处理
// handle_frame(&data[i], frame_len);
i += frame_len;
continue;
} else {
break; // 半包,等待后续数据
}
}
i++; // 未找到帧头,滑动
}
}
static void uart_rx_task(void *arg)
{
uint8_t *buf = malloc(BUF_SIZE);
uart_event_t event;
while (1) {
if (xQueueReceive(uart_queue, &event, portMAX_DELAY)) {
if (event.type == UART_DATA || event.type == UART_IDLE) {
size_t len = 0;
uart_get_buffered_data_len(UART_PORT, &len);
if (len > 0) {
int n = uart_read_bytes(UART_PORT, buf, len, pdMS_TO_TICKS(10));
if (n > 0) {
parse_frame(buf, n);
}
}
} else if (event.type == UART_FIFO_OVF || event.type == UART_BUFFER_FULL) {
uart_flush_input(UART_PORT);
xQueueReset(uart_queue);
}
}
}
}
void uart_dma_idle_init(void)
{
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,
.source_clk = UART_SCLK_DEFAULT,
};
uart_driver_install(UART_PORT, BUF_SIZE * 2, 0, 20, &uart_queue, 0);
uart_param_config(UART_PORT, &cfg);
uart_set_pin(UART_PORT, UART_TX_PIN, UART_RX_PIN,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 使能空闲中断,空闲阈值约 2 个字符时间
uart_enable_rx_intr(UART_PORT, UART_INTR_IDLE);
xTaskCreate(uart_rx_task, "uart_rx", 4096, NULL, 10, NULL);
}
五、避免半包与粘包的关键策略
- 只在空闲中断处理:不要每收到一个字节就解析,等空闲中断确认帧结束。
-
长度快照:中断中先
uart_get_buffered_data_len取长度,再一次性uart_read_bytes,避免边读边写导致半包。 -
协议层二次拆分:空闲中断只能切“物理帧”,若两帧间隔小于空闲阈值会粘包,必须在
parse_frame中按帧头+长度字段拆分。 - 超时兜底:对固定长度协议,可加软件定时器,超时未收满则丢弃并复位解析状态。
-
环形缓冲足够大:
uart_driver_install的RX缓冲应大于最大帧长×2,防止溢出。
六、注意事项
- 空闲中断阈值与波特率相关,高波特率下阈值过小易误触发,建议实测调整。
-
uart_read_bytes带超时参数,任务中不要用portMAX_DELAY,防止阻塞。 - 发生
UART_FIFO_OVF说明处理不及时,需增大缓冲或提高任务优先级。 - DMA与CPU共享环形缓冲,读取时建议关中断或使用驱动提供的原子接口,避免读写指针竞争。
- 调试时可用逻辑分析仪观察帧间隔,验证空闲中断触发点是否与帧尾对齐。
暂无评论,来抢沙发~