1. 为什么需要自定义串口通讯协议
在嵌入式开发中,串口通讯是最基础也最常用的外设接口之一。我刚开始接触STM32时,直接使用HAL库提供的串口收发函数,发现当传输数据量增大或需要可靠传输时,简单的收发会出现各种问题:
- 数据包不完整(只收到部分数据)
- 粘包问题(多条消息粘连在一起)
- 无法判断数据有效性(干扰或错误数据)
- 缺乏校验机制(传输错误无法发现)
通过示波器抓取波形发现,即使物理层信号完好,应用层的数据解析也可能出错。这促使我设计了一套简易但可靠的串口通讯协议,经过多个项目验证,稳定性显著提升。
2. 协议框架设计要点
2.1 帧结构定义
一个完整的协议帧包含以下部分(以字节为单位):
code复制| 帧头(2B) | 长度(1B) | 命令字(1B) | 数据(NB) | 校验(1B) | 帧尾(2B) |
- 帧头:固定为0xAA 0x55,用于标识帧开始
- 长度:数据部分的字节数(N),最大255
- 命令字:区分不同功能指令
- 数据:实际传输的有效载荷
- 校验:简单的累加和校验(所有字节相加取低8位)
- 帧尾:固定为0x0D 0x0A(CRLF),便于文本工具查看
实际项目中可根据需求调整各部分长度,如工业场景常用4字节帧头提高抗干扰能力
2.2 状态机实现
协议解析采用状态机模式,典型实现如下:
c复制typedef enum {
STATE_HEADER1,
STATE_HEADER2,
STATE_LENGTH,
STATE_CMD,
STATE_DATA,
STATE_CHECKSUM,
STATE_FOOTER1,
STATE_FOOTER2
} ParserState;
ParserState state = STATE_HEADER1;
uint8_t buffer[256];
uint8_t idx = 0;
uint8_t dataLength = 0;
void parseByte(uint8_t byte) {
switch(state) {
case STATE_HEADER1:
if(byte == 0xAA) state = STATE_HEADER2;
break;
case STATE_HEADER2:
if(byte == 0x55) state = STATE_LENGTH;
else state = STATE_HEADER1;
break;
// 其他状态处理...
case STATE_FOOTER2:
if(byte == 0x0A) processPacket();
state = STATE_HEADER1;
break;
}
}
3. STM32硬件层实现
3.1 CubeMX配置
- 在Pinout视图启用USARTx
- 配置参数:波特率115200,8数据位,无校验,1停止位
- 开启全局中断(NVIC Settings)
- 生成代码时选择DMA模式(可选)
实测发现,使用DMA+空闲中断方式效率最高,CPU占用率低于1%
3.2 中断服务例程
c复制#define BUF_SIZE 256
uint8_t rxBuf[BUF_SIZE];
uint8_t rxFlag = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART1) {
rxFlag = 1;
HAL_UART_Receive_IT(huart, rxBuf, BUF_SIZE);
}
}
4. 协议栈软件实现
4.1 数据打包函数
c复制typedef struct {
uint8_t cmd;
uint8_t len;
uint8_t data[250];
} ProtocolPacket;
uint8_t packData(ProtocolPacket *pkt, uint8_t *outBuf) {
uint8_t checksum = 0;
uint8_t i = 0;
outBuf[i++] = 0xAA;
outBuf[i++] = 0x55;
outBuf[i++] = pkt->len;
checksum += pkt->len;
outBuf[i++] = pkt->cmd;
checksum += pkt->cmd;
for(uint8_t j=0; j<pkt->len; j++) {
outBuf[i++] = pkt->data[j];
checksum += pkt->data[j];
}
outBuf[i++] = checksum;
outBuf[i++] = 0x0D;
outBuf[i++] = 0x0A;
return i; // 返回总长度
}
4.2 数据解包示例
c复制void processPacket(uint8_t *data, uint8_t len) {
uint8_t calcSum = 0;
for(uint8_t i=2; i<len-3; i++) { // 跳过帧头,不校验帧尾
calcSum += data[i];
}
if(calcSum != data[len-3]) {
printf("Checksum error!\r\n");
return;
}
uint8_t cmd = data[3];
switch(cmd) {
case 0x01:
handleTemperatureData(&data[4], data[2]);
break;
// 其他命令处理...
}
}
5. 实际项目中的优化技巧
5.1 超时重传机制
在工业现场测试时发现,单纯依赖校验无法保证100%可靠传输。增加以下机制:
c复制#define MAX_RETRY 3
uint8_t sendWithRetry(UART_HandleTypeDef *huart, uint8_t *data, uint8_t len) {
uint8_t retry = 0;
while(retry < MAX_RETRY) {
HAL_UART_Transmit(huart, data, len, 100);
if(waitAck(1000)) { // 自定义等待应答函数
return 1;
}
retry++;
}
return 0;
}
5.2 数据压缩技巧
当传输传感器数据时,可采用以下压缩方案:
- 浮点数转为定点数(如温度23.45℃转为2345)
- 使用差分编码(只传变化量)
- 对于状态量使用位域压缩
实测可使数据量减少40%以上
6. 常见问题排查指南
6.1 数据接收不完整
可能原因及解决方案:
- 波特率不匹配 - 用示波器测量实际波特率
- 缓冲区溢出 - 增大接收缓冲区或提高处理速度
- 中断优先级冲突 - 调整USART中断优先级
6.2 校验频繁失败
典型排查步骤:
- 检查物理连接(接地是否良好)
- 测试不同波特率下的误码率
- 改用更可靠的CRC校验算法
- 在信号线上增加磁环抑制干扰
7. 协议扩展思路
当基础协议成熟后,可考虑:
- 增加加密层(如AES-128)
- 支持分包传输大数据块
- 添加心跳包检测连接状态
- 实现协议版本协商机制
我在智能家居项目中采用分层协议设计,底层保持简单帧结构,上层实现JSON格式的业务数据封装,既保证可靠性又方便应用开发。
