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

如何用C优雅实现RS-232上的ASCII文本通信协议?

RS-232 ASCII文本通信协议优雅实现方案

问题核心分析

你之前采用结构体内存映射缓冲区的方案,本质是把ASCII文本协议当成固定字节偏移的二进制协议处理,天然存在两个致命问题:

  1. 无法适配ASCII协议的分号分隔可变长度字段逻辑,结构体的固定内存布局和文本协议的动态字段长度不匹配;
  2. 固定长度数组会发送冗余空字节,改用指针则仅传输指针地址,完全不符合文本字段的传输需求。

解决方案思路

ASCII文本协议的核心是基于字符串的动态拼接与解析,而非二进制内存拷贝。我们需要按照协议定义的帧结构,分步构建完整帧:

  1. 用格式化字符串拼接生成分号分隔的ASCII数据段;
  2. 计算数据段的CRC-16(二进制字节);
  3. 依次拼接SOH、数据段、CRC-16、EOT到缓冲区;
  4. 逐字节发送缓冲区内容。

发送端实现代码示例

#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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 04:50:22