如何用C优雅实现RS-232上的ASCII文本通信协议?
RS-232 ASCII文本通信协议优雅实现方案
问题核心分析
你之前采用结构体内存映射缓冲区的方案,本质是把ASCII文本协议当成固定字节偏移的二进制协议处理,天然存在两个致命问题:
- 无法适配ASCII协议的分号分隔可变长度字段逻辑,结构体的固定内存布局和文本协议的动态字段长度不匹配;
- 固定长度数组会发送冗余空字节,改用指针则仅传输指针地址,完全不符合文本字段的传输需求。
解决方案思路
ASCII文本协议的核心是基于字符串的动态拼接与解析,而非二进制内存拷贝。我们需要按照协议定义的帧结构,分步构建完整帧:
- 用格式化字符串拼接生成分号分隔的ASCII数据段;
- 计算数据段的CRC-16(二进制字节);
- 依次拼接SOH、数据段、CRC-16、EOT到缓冲区;
- 逐字节发送缓冲区内容。
发送端实现代码示例
#include <string.h> #include <stdio.h> // 协议宏定义 #define SOH 0x01 // 帧起始符 #define EOT 0x04 // 帧结束符 #define BUFF_SIZE 256 // 缓冲区大小,根据最大帧长度调整 // 假设已定义的类型和函数 typedef enum { STATUS_OK, STATUS_ERR, STATUS_CRC_ERR } enum_status; typedef enum { OK, ERR } enum_RETURN_VALUE; typedef struct { // 假设包含时间、日期、配置等字段 int time; int date; struct { char source; char id1[20]; char id2[20]; } config; } handle_t; #define HEADER 'H' // HEADER帧的code字段 // CRC-16计算函数(示例,需和接收端算法一致) uint16_t calcCRC(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= data[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; } // 发送HEADER帧函数 static enum_status sendHeader(handle_t *handle) { enum_RETURN_VALUE rs232_err = OK; enum_status err = STATUS_OK; int data_segment_len; uint16_t crc_val; uint16_t total_frame_len; uint8_t DATA_BUFF[BUFF_SIZE]; // 改用函数内缓冲区,避免全局变量污染 // 1. 拼接ASCII数据段:{code};{ver};{time};{date};{src};{id1};{id2} // 预留SOH(1)、CRC(2)、EOT(1)的空间,避免缓冲区溢出 data_segment_len = snprintf((char*)DATA_BUFF + 1, BUFF_SIZE - 4, "%c;%d;%06d;%08d;%c;%s;%s", HEADER, 10, // 版本号,TODO替换为实际值 handle->time,// 假设Time_t为整数,格式HHMMSS handle->date,// 假设Date_t为整数,格式YYYYMMDD handle->config->source, handle->config->id1, handle->config->id2); // 检查数据段是否溢出 if (data_segment_len < 0 || data_segment_len >= (BUFF_SIZE - 4)) { return STATUS_ERR; } // 2. 填充帧起始符SOH DATA_BUFF[0] = SOH; // 3. 计算数据段的CRC-16(仅计算数据段部分) crc_val = calcCRC(DATA_BUFF + 1, data_segment_len); // 4. 填充CRC-16(大端字节序,需和接收端约定) DATA_BUFF[1 + data_segment_len] = (crc_val >> 8) & 0xFF; // 高字节 DATA_BUFF[1 + data_segment_len + 1] = crc_val & 0xFF; // 低字节 // 5. 填充帧结束符EOT DATA_BUFF[1 + data_segment_len + 2] = EOT; // 计算总帧长度 total_frame_len = 1 + data_segment_len + 2 + 1; // 6. 逐字节发送帧 for (uint16_t i = 0; i < total_frame_len; i++) { rs232_err = rs232_tx_send(DATA_BUFF[i], 1); if (rs232_err != OK) { err = STATUS_ERR; break; } } // 重发逻辑(按需补充,比如接收端未应答时重发) // do { // // 发送逻辑... // } while (need_retransmit && retry_count < MAX_RETRY); return err; }
接收端实现思路与代码示例
接收端需要按帧结构反向解析,核心步骤是帧边界识别、CRC验证、字段分割:
帧接收与CRC验证
// 解析后的数据结构体(仅保存有效字段) typedef struct { char code; int ver; int time; int date; char src; char id1[20]; char id2[20]; } stdHeaderParsed_t; // 接收完整帧 static enum_status receiveFrame(uint8_t *buff, uint16_t *frame_len) { uint8_t byte; uint16_t pos = 0; enum_RETURN_VALUE rs232_err; enum_status err = STATUS_OK; // 1. 等待SOH,丢弃之前的无效字节 do { rs232_err = rs232_rx_receive(&byte, 1); if (rs232_err != OK) { return STATUS_ERR; } } while (byte != SOH); buff[pos++] = byte; // 2. 接收直到EOT,完成帧捕获 while (pos < BUFF_SIZE) { rs232_err = rs232_rx_receive(&byte, 1); if (rs232_err != OK) { err = STATUS_ERR; break; } buff[pos++] = byte; if (byte == EOT) { break; } } // 检查帧最小长度(SOH+至少1字节数据+CRC+EOT) if (err != STATUS_OK || pos < 4) { return STATUS_ERR; } // 3. 验证CRC uint16_t data_len = pos - 4; // 数据段长度=总长度-SOH(1)-CRC(2)-EOT(1) uint16_t received_crc = ((uint16_t)buff[pos-3] << 8) | buff[pos-2]; uint16_t calculated_crc = calcCRC(buff+1, data_len); if (received_crc != calculated_crc) { return STATUS_CRC_ERR; } *frame_len = pos; return STATUS_OK; }
解析HEADER帧数据段
// 解析数据段的分号分隔字段 static enum_status parseHeader(uint8_t *data_segment, uint16_t len, stdHeaderParsed_t *parsed) { // 临时拷贝数据段,避免破坏原缓冲区(strtok会修改原字符串) char temp_buff[BUFF_SIZE]; if (len >= sizeof(temp_buff)) { return STATUS_ERR; } memcpy(temp_buff, data_segment, len); temp_buff[len] = '\0'; // 添加字符串终止符 // 分割字段 char *token = strtok(temp_buff, ";"); if (!token) return STATUS_ERR; parsed->code = token[0]; token = strtok(NULL, ";"); if (!token) return STATUS_ERR; parsed->ver = atoi(token); token = strtok(NULL, ";"); if (!token) return STATUS_ERR; parsed->time = atoi(token); token = strtok(NULL, ";"); if (!token) return STATUS_ERR; parsed->date = atoi(token); token = strtok(NULL, ";"); if (!token) return STATUS_ERR; parsed->src = token[0]; token = strtok(NULL, ";"); if (!token) return STATUS_ERR; strncpy(parsed->id1, token, sizeof(parsed->id1)-1); parsed->id1[sizeof(parsed->id1)-1] = '\0'; // 确保字符串终止 token = strtok(NULL, ";"); if (!token) return STATUS_ERR; strncpy(parsed->id2, token, sizeof(parsed->id2)-1); parsed->id2[sizeof(parsed->id2)-1] = '\0'; return STATUS_OK; }
关键注意事项
- 缓冲区安全:使用
snprintf而非sprintf,通过返回值判断是否溢出;解析时用strncpy避免字符串缓冲区溢出。 - 字节序约定:CRC-16的两个字节发送顺序(大端/小端)必须和接收端严格一致,否则CRC验证失败。
- 字段格式一致性:时间、日期等数值型字段的ASCII格式(如是否补零)必须和接收端提前约定,确保解析正确。
- 全局变量替代:发送端改用函数内静态缓冲区或局部缓冲区,避免全局缓冲区的线程安全问题。
内容的提问来源于stack exchange,提问作者wirelabs
相关产品推荐
相关产品推荐

