You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

主从板卡间如何通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 08:45:13