一、为什么需要通用协议解析框架

STM32外设模块(指纹、人脸、语音、4G等)几乎都采用UART透传,但协议格式千差万别。常见的有:

  • 固定长度帧:如指纹模块AS608,帧长固定为12字节(含头尾)。
  • 变长帧:如人脸模块K210,数据段长度由长度字段决定。
  • 纯文本帧:如某些GPS模块,以$开头,逗号分隔。

若每个模块都写一套解析代码,不仅冗余,而且容易在边界条件(如丢字节、粘包)上出错。一个健壮的通用解析框架应具备:

  • 状态机驱动:逐字节解析,不依赖阻塞等待。
  • DMA+空闲中断:高效接收不定长数据。
  • 可配置协议参数:通过结构体描述帧格式,适配不同模块。

二、协议解析核心原理

2.1 通用帧结构模型

绝大多数二进制协议可抽象为:

| 帧头(1-2B) | 长度(1-2B) | 命令(1B) | 数据(NB) | 校验(1-2B) | 帧尾(可选) |
  • 帧头:固定值,如0xEF 0xAA,用于同步。
  • 长度:表示命令+数据+校验的长度,或数据长度。
  • 命令:功能码,如指纹比对、人脸识别。
  • 数据:参数或结果。
  • 校验:常见有累加和、CRC16,用于错误检测。

2.2 状态机解析流程

状态机是解析的核心,每个状态对应一个解析阶段:

IDLE -> HEADER1 -> HEADER2 -> LENGTH -> COMMAND -> DATA -> CHECK -> DONE
  • IDLE状态等待帧头第一个字节,匹配后进入下一状态。
  • 若中途字节不匹配,则回退到IDLE重新同步。
  • 当长度字段解析后,根据长度值决定后续需要接收的字节数。
  • 数据收齐后,校验并触发回调。

三、STM32实现:DMA+空闲中断接收

3.1 硬件配置

使用STM32的UART+DMA接收,并开启空闲中断(IDLE)。当一帧数据发送完毕后,总线空闲,触发IDLE中断,此时DMA已自动接收全部字节。

配置步骤

  1. 使能UART时钟和GPIO时钟,配置TX/RX引脚为复用功能。
  2. 配置UART参数(波特率、数据位等),使能DMA接收。
  3. 配置DMA为循环模式,接收缓冲区大小设为最大帧长(如256)。
  4. 使能UART空闲中断,在中断服务函数中处理数据。

3.2 代码实现

// 协议解析状态机定义
typedef enum {
    PARSER_IDLE,
    PARSER_HEADER1,
    PARSER_HEADER2,
    PARSER_LENGTH,
    PARSER_COMMAND,
    PARSER_DATA,
    PARSER_CHECK,
    PARSER_DONE
} ParserState;

// 协议描述结构体
typedef struct {
    uint8_t header1;        // 帧头1
    uint8_t header2;        // 帧头2(可为0)
    uint8_t len_offset;     // 长度字段偏移(从帧头后开始)
    uint8_t len_size;       // 长度字段字节数(1或2)
    uint8_t cmd_offset;     // 命令字段偏移
    uint8_t data_offset;    // 数据字段偏移
    uint8_t check_offset;   // 校验字段偏移(从帧头开始)
    uint8_t check_size;     // 校验字节数(1或2)
    uint8_t (*check_func)(uint8_t *buf, uint16_t len); // 校验函数
} ProtocolCfg;

// 解析器实例
typedef struct {
    ParserState state;
    uint8_t buffer[256];
    uint16_t index;
    uint16_t data_len;
    uint16_t frame_len;
    ProtocolCfg cfg;
    void (*frame_handler)(uint8_t *buf, uint16_t len);
} Parser;

// 状态机核心函数
void parser_byte(Parser *p, uint8_t byte) {
    switch (p->state) {
        case PARSER_IDLE:
            if (byte == p->cfg.header1) {
                p->buffer[0] = byte;
                p->index = 1;
                p->state = (p->cfg.header2) ? PARSER_HEADER1 : PARSER_LENGTH;
            }
            break;
        case PARSER_HEADER1:
            if (byte == p->cfg.header2) {
                p->buffer[1] = byte;
                p->index = 2;
                p->state = PARSER_LENGTH;
            } else {
                p->state = PARSER_IDLE; // 重新同步
            }
            break;
        case PARSER_LENGTH:
            p->buffer[p->index++] = byte;
            if (p->index == p->cfg.len_offset + p->cfg.len_size) {
                // 计算帧总长度(假设长度字段表示数据长度)
                p->data_len = (p->cfg.len_size == 2) ? 
                    (p->buffer[p->cfg.len_offset] << 8 | p->buffer[p->cfg.len_offset+1]) :
                    p->buffer[p->cfg.len_offset];
                p->frame_len = p->cfg.data_offset + p->data_len + p->cfg.check_size;
                if (p->frame_len > sizeof(p->buffer)) {
                    p->state = PARSER_IDLE; // 超长错误
                } else {
                    p->state = PARSER_COMMAND;
                }
            }
            break;
        case PARSER_COMMAND:
            p->buffer[p->index++] = byte;
            if (p->index == p->cfg.data_offset) {
                p->state = (p->data_len > 0) ? PARSER_DATA : PARSER_CHECK;
            }
            break;
        case PARSER_DATA:
            p->buffer[p->index++] = byte;
            if (p->index == p->cfg.data_offset + p->data_len) {
                p->state = PARSER_CHECK;
            }
            break;
        case PARSER_CHECK:
            p->buffer[p->index++] = byte;
            if (p->index == p->frame_len) {
                // 校验
                if (p->cfg.check_func(p->buffer, p->frame_len - p->cfg.check_size) == 
                    p->buffer[p->frame_len - p->cfg.check_size]) {
                    p->state = PARSER_DONE;
                    if (p->frame_handler) p->frame_handler(p->buffer, p->frame_len);
                }
                p->state = PARSER_IDLE; // 无论校验成功与否,都回到IDLE
            }
            break;
        default:
            p->state = PARSER_IDLE;
            break;
    }
}

// DMA空闲中断处理示例
void UART_IDLE_Callback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        uint16_t len = sizeof(rx_buf) - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        for (uint16_t i = 0; i < len; i++) {
            parser_byte(&finger_parser, rx_buf[i]);
        }
        __HAL_DMA_SET_COUNTER(&hdma_usart1_rx, sizeof(rx_buf));
        // 重新启动DMA接收(循环模式无需重启,但需清空标志)
    }
}

四、实战适配:指纹模块AS608与K210人脸模块

4.1 指纹模块AS608

AS608协议帧结构:

帧头(2B: 0xEF 0xAA) + 地址(4B) + 包标识(1B) + 长度(2B) + 指令(1B) + 数据(NB) + 校验和(2B)

适配配置:

ProtocolCfg as608_cfg = {
    .header1 = 0xEF,
    .header2 = 0xAA,
    .len_offset = 7,   // 帧头2+地址4+包标识1 = 7
    .len_size = 2,
    .cmd_offset = 9,   // 长度字段后
    .data_offset = 10, // 命令后
    .check_offset = 10 + 数据长度, // 动态计算
    .check_size = 2,
    .check_func = as608_check, // 累加和校验
};

注意:AS608的长度字段表示“指令+数据+校验和”的长度,因此帧总长需额外加上帧头、地址等固定部分。可在解析器中增加fixed_header_len字段来调整。

4.2 人脸模块K210(MaixPy)

K210常用协议为:

帧头(2B: 0xAA 0x55) + 长度(1B,数据长度) + 命令(1B) + 数据(NB) + 校验(1B,累加和)

适配配置:

ProtocolCfg k210_cfg = {
    .header1 = 0xAA,
    .header2 = 0x55,
    .len_offset = 2,
    .len_size = 1,
    .cmd_offset = 3,
    .data_offset = 4,
    .check_offset = 4 + 数据长度,
    .check_size = 1,
    .check_func = sum_check,
};

五、注意事项与进阶技巧

  • 超时处理:若帧头匹配后长时间未收满,应重置状态机。可在定时器中断中检查index是否长时间未变化。
  • 粘包与拆包:DMA可能一次收到多帧,状态机天然支持逐字节解析,无需额外处理。但需确保缓冲区足够大。
  • 校验函数:建议将校验函数作为函数指针,便于扩展CRC16等复杂校验。
  • 多模块复用:可创建多个Parser实例,每个UART对应一个,互不干扰。
  • 调试技巧:在解析器中加入debug钩子,打印错误状态(如校验失败、超长),便于定位问题。

六、总结

本文提出的通用串口协议解析框架,基于状态机和DMA空闲中断,可高效适配各种STM32外设模块。通过配置ProtocolCfg结构体,即可快速接入指纹、人脸等模块,大幅减少重复代码,提高健壮性。实际项目中,还可进一步扩展为支持转义字符、动态内存分配等特性,但核心思想不变:将协议解析从业务逻辑中解耦,用状态机应对不确定性