主从板卡间如何通过UART传输应答结构体?
嘿,这个问题我之前做板卡间UART通信的时候刚好踩过坑,其实核心就是把结构体序列化(拆成字节流)和反序列化(把字节流拼回结构体),再配合简单的通信协议避免丢包粘包。结合你的M/S板卡请求-应答场景,给你捋个落地的实现思路:
核心思路:结构体的字节流转换
UART本身只能按单个字节传输,所以要把User这种复杂结构体转换成连续的字节数组(序列化),接收端再把字节数组还原成结构体(反序列化)。同时必须加一套简单的帧协议,不然传输中容易出现数据错乱、粘包的问题。
第一步:统一结构体与命令定义
M板卡和S板卡必须用完全一致的结构体、命令码定义,不然两边解析出来的数据会完全不对。还要注意内存对齐问题——不同编译器的对齐规则可能不一样,最好强制1字节对齐:
// 强制1字节对齐,避免结构体内存布局差异 #pragma pack(1) // 命令码枚举,两边必须同步 typedef enum { GET_USER_INFO = 0x01, // 后续扩展其他命令都加在这里 } CmdType; // User结构体定义 typedef struct { uint8_t id; char name[20]; uint16_t age; uint32_t score; } User; #pragma pack() // 恢复默认对齐
第二步:设计简单的通信帧协议
因为UART没有帧同步机制,我们需要给数据包加帧头、帧尾、长度和校验,用来标记数据包的起始/结束,同时检测传输错误。推荐的帧格式:
帧头(1字节) + 命令码(1字节) + 数据长度(2字节) + 数据内容(N字节) + 校验码(1字节) + 帧尾(1字节)
举个具体例子:
- M板卡发请求:
0xAA(帧头) +0x01(GET_USER_INFO命令) +0x0000(无请求参数,长度为0) + 校验码 +0x55(帧尾) - S板卡应答:
0xAA(帧头) +0x01(对应请求命令) +sizeof(User)(数据长度) + User结构体的字节数据 + 校验码 +0x55(帧尾)
校验码用简单的异或校验就行:把帧头到数据内容的所有字节依次异或,接收端重新计算校验码并对比,不一致就丢弃数据。
第三步:M板卡发送请求的实现
以STM32的HAL库为例,把请求命令打包成符合协议的帧,再通过UART发送:
// 发送GET_USER_INFO请求命令 void send_get_user_cmd(UART_HandleTypeDef *huart) { uint8_t frame[6]; // 帧头+命令+长度+校验+帧尾,共6字节 frame[0] = 0xAA; // 固定帧头 frame[1] = GET_USER_INFO; // 命令码 frame[2] = 0x00; // 数据长度高字节(无参数,长度为0) frame[3] = 0x00; // 数据长度低字节 // 计算异或校验码 frame[4] = frame[0] ^ frame[1] ^ frame[2] ^ frame[3]; frame[5] = 0x55; // 固定帧尾 // UART发送数据,实际项目别用HAL_MAX_DELAY,设合理超时 HAL_UART_Transmit(huart, frame, sizeof(frame), 1000); }
第四步:S板卡接收请求并返回结构体
S板卡先接收完整的请求帧,验证格式和校验后,把User结构体序列化打包成应答帧发送:
// 处理UART接收的请求并返回应答 void handle_uart_request(UART_HandleTypeDef *huart) { uint8_t rx_frame[6]; // 接收完整的请求帧 if (HAL_UART_Receive(huart, rx_frame, sizeof(rx_frame), 1000) != HAL_OK) { return; // 接收超时或错误,直接返回 } // 验证帧头、帧尾和校验码 if (rx_frame[0] != 0xAA || rx_frame[5] != 0x55) return; uint8_t check_sum = rx_frame[0] ^ rx_frame[1] ^ rx_frame[2] ^ rx_frame[3]; if (check_sum != rx_frame[4]) return; // 处理GET_USER_INFO命令 if (rx_frame[1] == GET_USER_INFO) { // 构造要返回的User数据 User user = { .id = 1, .name = "Alice", .age = 25, .score = 95 }; // 打包应答帧 uint16_t data_len = sizeof(User); // 帧总长度:帧头(1)+命令(1)+长度(2)+数据(data_len)+校验(1)+帧尾(1) uint8_t tx_frame[4 + data_len + 2]; tx_frame[0] = 0xAA; tx_frame[1] = GET_USER_INFO; tx_frame[2] = (data_len >> 8) & 0xFF; // 长度高字节 tx_frame[3] = data_len & 0xFF; // 长度低字节 // 把结构体拷贝到数据区(序列化) memcpy(&tx_frame[4], &user, data_len); // 重新计算校验码 check_sum = 0; for (int i = 0; i < 4 + data_len; i++) { check_sum ^= tx_frame[i]; } tx_frame[4 + data_len] = check_sum; tx_frame[4 + data_len + 1] = 0x55; // 发送应答帧 HAL_UART_Transmit(huart, tx_frame, sizeof(tx_frame), 1000); } }
第五步:M板卡接收并解析结构体
M板卡发送请求后,等待接收应答帧,再把字节流反序列化成User结构体:
// 接收S板卡的应答并解析成User结构体 User receive_user_info(UART_HandleTypeDef *huart) { User user = {0}; uint8_t rx_header[4]; // 先接收帧头、命令和数据长度 if (HAL_UART_Receive(huart, rx_header, sizeof(rx_header), 1000) != HAL_OK) { return user; } // 验证帧头和命令匹配 if (rx_header[0] != 0xAA || rx_header[1] != GET_USER_INFO) { return user; } // 解析数据长度并验证是否匹配User结构体大小 uint16_t data_len = (rx_header[2] << 8) | rx_header[3]; if (data_len != sizeof(User)) { return user; } // 接收数据内容、校验码和帧尾 uint8_t rx_data[data_len + 2]; if (HAL_UART_Receive(huart, rx_data, sizeof(rx_data), 1000) != HAL_OK) { return user; } // 验证校验码和帧尾 uint8_t check_sum = rx_header[0] ^ rx_header[1] ^ rx_header[2] ^ rx_header[3]; for (int i = 0; i < data_len; i++) { check_sum ^= rx_data[i]; } if (check_sum != rx_data[data_len] || rx_data[data_len + 1] != 0x55) { return user; } // 反序列化:把字节数组拷贝到结构体 memcpy(&user, rx_data, data_len); return user; }
关键注意事项
- 内存对齐:一定要用
#pragma pack(1)强制1字节对齐,否则不同编译器编译出来的结构体大小、内存布局会不一样,解析完全错误。 - 字节序问题:如果结构体里有
uint16_t、uint32_t这类多字节类型,要注意CPU的大小端。如果M/S板卡CPU字节序不同,需要在序列化/反序列化时转换字节序(比如转成网络字节序)。 - 超时处理:实际项目中绝对不要用
HAL_MAX_DELAY,要设置合理的超时时间,避免板卡因为等待数据而卡死。 - 异常处理:代码里要加接收错误、校验失败、长度不匹配的处理逻辑,增强通信的稳定性。
内容的提问来源于stack exchange,提问作者Pryda
相关产品推荐
相关产品推荐

