UART串口不定长数据接收方法
·
一、基本概念与问题
在嵌入式系统中,串口(UART)通信时,数据通常以不定长的“帧”为单位发送。串口硬件本身只能识别单个字节的接收完成,无法自动判断一帧数据何时开始和结束。因此,需要通过软件方法来解决帧边界识别问题。
所有方法都基于一个基本的数据管理结构:
#define MAX_BUF_SIZE 200
typedef struct {
uint8_t buffer[MAX_BUF_SIZE]; // 数据存储区
uint16_t count; // 已接收字节数
uint16_t length; // 帧长度
uint8_t complete_flag; // 帧完成标志
} UartRxManager;
UartRxManager uart1_rx;
二、四种基础实现方法
1. 空闲中断检测法
原理
利用串口硬件的空闲检测功能:当RX引脚在一个字节传输时间后保持高电平,硬件会触发空闲中断,标志着一帧数据传输结束。
实现要点
初始化配置:
// 开启空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 启动DMA接收
HAL_UART_Receive_DMA(&huart1, uart1_rx.buffer, MAX_BUF_SIZE);
中断处理:
void handle_idle_interrupt(UART_HandleTypeDef *huart) {
if(__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 停止DMA并计算接收长度
HAL_UART_DMAStop(huart);
uart1_rx.length = MAX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart->hdmarx);
uart1_rx.complete_flag = 1;
// 重新启动接收
HAL_UART_Receive_DMA(huart, uart1_rx.buffer, MAX_BUF_SIZE);
}
}
2. 协议解析法
原理
在通信协议中定义固定的帧结构,通过识别帧头、帧长等信息来确定帧边界。
协议示例
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 0 | 0x5A | 帧头1 |
| 1 | 0xA5 | 帧头2 |
| 2 | N | 数据长度 |
| 3~N+2 | 数据 | 有效载荷 |
实现代码
typedef enum {
WAIT_HEADER1,
WAIT_HEADER2,
WAIT_LENGTH,
RECEIVING_DATA
} RxState;
void process_received_byte(uint8_t byte) {
static RxState state = WAIT_HEADER1;
static uint8_t expected_len = 0;
switch(state) {
case WAIT_HEADER1:
if(byte == 0x5A) {
uart1_rx.count = 0;
uart1_rx.buffer[uart1_rx.count++] = byte;
state = WAIT_HEADER2;
}
break;
case WAIT_HEADER2:
if(byte == 0xA5) {
uart1_rx.buffer[uart1_rx.count++] = byte;
state = WAIT_LENGTH;
} else {
state = WAIT_HEADER1; // 重新同步
}
break;
case WAIT_LENGTH:
expected_len = byte;
uart1_rx.buffer[uart1_rx.count++] = byte;
state = RECEIVING_DATA;
break;
case RECEIVING_DATA:
uart1_rx.buffer[uart1_rx.count++] = byte;
if(uart1_rx.count >= (expected_len + 3)) {
uart1_rx.complete_flag = 1;
uart1_rx.length = uart1_rx.count;
state = WAIT_HEADER1;
}
break;
}
}
3. 超时判断法
原理
基于数据连续性假设:如果在一定时间内没有收到新数据,则认为当前帧已结束。
时间计算
以9600波特率为例:
-
1个字节传输时间 ≈ 1.04ms (10位/字节 ÷ 9600位/秒)
-
超时时间建议:1.5-2倍字节时间 ≈ 2ms
实现代码
volatile uint32_t last_receive_time = 0;
#define TIMEOUT_MS 2
// 接收中断中调用
void on_byte_received(uint8_t byte) {
uart1_rx.buffer[uart1_rx.count++] = byte;
last_receive_time = get_current_time(); // 更新时间戳
}
// 主循环中检查
void check_timeout(void) {
uint32_t current_time = get_current_time();
if(uart1_rx.count > 0 &&
(current_time - last_receive_time > TIMEOUT_MS)) {
uart1_rx.complete_flag = 1;
uart1_rx.length = uart1_rx.count;
uart1_rx.count = 0; // 准备接收下一帧
}
}
4. 环形缓冲区法
原理
中断只负责将数据存入缓冲区,主程序从缓冲区读取并解析数据,实现接收与处理的解耦。
数据结构
typedef struct {
uint8_t *data;
uint16_t size;
uint16_t head; // 写入位置
uint16_t tail; // 读取位置
uint16_t count; // 数据数量
} RingBuffer;
void rb_init(RingBuffer *rb, uint8_t *buf, uint16_t size) {
rb->data = buf;
rb->size = size;
rb->head = rb->tail = rb->count = 0;
}
uint8_t rb_write(RingBuffer *rb, uint8_t byte) {
if(rb->count >= rb->size) return 0;
rb->data[rb->head] = byte;
rb->head = (rb->head + 1) % rb->size;
rb->count++;
return 1;
}
uint8_t rb_read(RingBuffer *rb, uint8_t *byte) {
if(rb->count == 0) return 0;
*byte = rb->data[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
rb->count--;
return 1;
}
使用方式
// 中断服务程序(极简)
void USART1_IRQHandler(void) {
uint8_t byte = USART1->DR;
rb_write(&rx_buffer, byte);
}
// 主程序处理
int main(void) {
uint8_t byte;
while(1) {
if(rb_read(&rx_buffer, &byte)) {
// 解析协议或处理数据
process_received_byte(byte);
}
}
}
三、方法对比与选择
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 空闲中断 | 硬件支持、效率高 | 需要硬件支持 | 高速连续数据 |
| 协议解析 | 可靠性高、有校验 | 实现较复杂 | 有固定协议 |
| 超时判断 | 实现简单 | 精度依赖定时 | 低速应用 |
| 环形缓冲区 | 解耦接收与处理 | 内存占用较大 | 多任务系统 |
四、选择建议
-
简单应用:从超时判断法开始,最容易理解和实现
-
可靠通信:选择协议解析法,具备错误检测能力
-
高效接收:使用空闲中断+DMA,减少CPU干预
-
复杂系统:采用环形缓冲区,便于扩展和维护
实际应用中常组合使用,如:空闲中断+协议解析,或环形缓冲区+超时判断。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)