如何在嵌入式系统中实现串口数据的解析(说明一)?
如何在嵌入式系统中实现串口数据的解析?
·
一. 简介
本文简单学习一下,如何在嵌入式系统中实现串口数据的解析。也就是解析思路方法。
二. 如何在嵌入式系统中实现串口数据的解析?
在嵌入式系统中,串口数据解析的核心目标是将接收到的字节流转换为结构化的有效数据(如传感器数值、控制指令等),同时需应对嵌入式环境的资源限制(内存小、CPU 算力有限)和实时性要求。
实现过程需围绕 “协议定义→数据缓冲→帧同步→校验→字段解析→异常处理” 展开,以下是具体实现方法和关键技术:
1. 明确串口通信协议(核心前提)
串口数据解析的基础是通信协议(双方约定的数据格式),协议需包含以下关键要素(示例格式):
[帧头] [长度] [数据字段1] [数据字段2] ... [校验位] [帧尾]
2B 1B nB nB 1-2B 1-2B
- 帧头 / 帧尾:用于定位一帧数据的开始和结束(如
0xAA 0x55作为帧头,0xCC作为帧尾),避免字节流粘连。 - 长度字段:标识数据字段的总字节数(可选,用于快速定位帧尾,减少无效解析)。
- 数据字段:实际有效数据(如温度值、指令码,需约定每个字段的类型和长度,如 “前 2B 为温度(int16),后 1B 为湿度(uint8)”)。
- 校验位:用于验证数据完整性(如 Checksum、CRC8/16,避免传输错误导致的解析异常)。
2. 数据接收与缓冲
串口数据是流式接收的(可能分多次到达),需先通过缓冲区暂存,再从缓冲区中提取完整帧。嵌入式系统中常用环形缓冲区(FIFO) 作为缓冲结构,优点是:
- 内存占用固定(预设大小,如 256 字节),适合资源受限场景;
- 读写操作高效(通过头尾指针循环操作,无需频繁移动数据)。
实现方法(举例说明):
(1) 定义缓冲区结构:
#define BUFFER_SIZE 256
typedef struct {
uint8_t buffer[BUFFER_SIZE]; //缓冲区数据
uint16_t head; //写入指针(指向待写入的位置)
uint16_t tail; //读取指针(指向待读取的位置)
uint16_t data_len; //当前有效数据长度
}RingBuffer;
(2) 在串口接收中断中快速写入数组(避免中断阻塞):
void UART_IRQHandler(void) {
if (UART_GetFlagStatus(UARTx, UART_FLAG_RXNE) == SET) { // 接收非空
uint8_t byte = UART_ReceiveData(UARTx); // 读1字节
if (ring_buf.len < BUFFER_SIZE) { // 缓冲区未满
ring_buf.data[ring_buf.head] = byte;
ring_buf.head = (ring_buf.head + 1) % BUFFER_SIZE;
ring_buf.len++;
}
}
}
3. 帧同步:从字节流中提取完整数据帧
帧同步是解析的核心步骤,目标是从缓冲区中找到完整且有效的一帧数据。常用方法有两种:
(1) 基于帧头+帧尾的同步(适合固定格式数据帧)
- 循环从缓冲区读取字节,查找预设的帧头(如
0xAA 0x55); - 找到帧头后,继续读取后续字节,直到匹配帧尾(如
0xCC); - 若包含长度字段,可先读取长度,再按长度提取数据字段,避免帧尾误判(如数据中恰好包含
0xCC)。
示例如下:
uint8_t frame_buf[128]; // 暂存提取的一帧数据
uint8_t frame_len = 0; // 当前帧长度
uint8_t state = 0; // 解析状态:0-等待帧头,1-接收帧头第2字节,2-接收数据/帧尾
void parse_frame() {
while (ring_buf.len > 0) { // 缓冲区有数据时处理
uint8_t byte = ring_buf.data[ring_buf.tail]; // 读1字节
ring_buf.tail = (ring_buf.tail + 1) % BUFFER_SIZE;
ring_buf.len--;
switch (state) {
case 0: // 等待帧头第1字节
if (byte == 0xAA) {
state = 1;
}
break;
case 1: // 等待帧头第2字节
if (byte == 0x55) { // 帧头完整
frame_buf[0] = 0xAA;
frame_buf[1] = 0x55;
frame_len = 2;
state = 2;
} else {
state = 0; // 帧头不完整,重置
}
break;
case 2: // 接收数据/帧尾
frame_buf[frame_len++] = byte;
if (byte == 0xCC) { // 检测到帧尾,完成一帧
process_complete_frame(frame_buf, frame_len); // 处理完整帧
frame_len = 0;
state = 0; // 重置状态,准备下一帧
} else if (frame_len >= 128) { // 帧过长,防止缓冲区溢出
frame_len = 0;
state = 0;
}
break;
}
}
}
(2) 不定长+超时判断
- 数据长度不固定,常用于简单协议(如调试信息输出、传感器动态上报数据等)。
原理:它通过检测“一段时间内没有新数据到达”来判断一帧数据已经接收完毕,从而触发解析。
步骤1:串口中断服务函数(接收一个字节)
#define RX_BUFFER_SIZE 64
#define TIMEOUT_MS 10 // 超时阈值10ms 无新数据认为帧结束
uint8_t rx_buffer[RX_BUFFER_SIZE];
uint16_t rx_index = 0; //当前写入位置
uint32_t last_rx_time = 0; //最后一次收到数据的时间
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) { // 接收寄存器非空
uint8_t byte = USART1->DR; // 读取数据
// 防止缓冲区溢出
if (rx_index < RX_BUFFER_SIZE) {
rx_buffer[rx_index++] = byte;
}
// 更新最后接收时间
last_rx_time = HAL_GetTick();
}
}
注意:这里不处理不完整的数据帧。
步骤2:主循环中检测超时并解析
while (1) {
uint32_t current_time = HAL_GetTick();
// 检查是否超时,且有数据
if (rx_index > 0 && (current_time - last_rx_time) > TIMEOUT_MS) {
// 超时,认为一帧数据接收完成
process_received_frame(rx_buffer, rx_index);
// 清空缓冲区
rx_index = 0;
}
// 其他任务...
// delay(1); // 可选:降低CPU占用
}
步骤 3:处理接收到的数据帧
void process_received_frame(uint8_t *data, uint16_t len) {
//打印接收到的数据
for (int i = 0; i < len; i++) {
printf("%c", data[i]);
}
printf("\n");
// 或解析协议,如:
// if (strstr((char*)data, "OK")) { ... }
// if (memcmp(data, "AT", 2) == 0) { ... }
}
4. 数据校验:确保数据帧的完整性
5. 字段解析:转化为有效数据
6. 异常处理:保证系统的稳定性
下一篇文章继续用示例来说明,上面剩下的 三个步骤(4,5,6)。
更多推荐
所有评论(0)