各位同学大家好,今天讲串口协议解析部分—查表法,也叫表驱动法。

一、解释说明

我们以如下专业版协议格式为例,来讲查表法。
在这里插入图片描述

  1. 什么是查表法?
    查表法是通过建立协议表结构,将解析到的字段数据代入表中进行查找,如果表中有该字段并且与之匹配,那么我们就对该数据进行处理,反之不处理。
    例如在上方协议中我以“功能ID、命令ID”为依据建立表结构,那么表的框架就是
    { [功能ID1、命令ID1、处理函数1],
    [功能ID2、命令ID2、处理函数2] ,
    [ … ] }
    意思就是说在解析协议数据的时候如果“功能ID和命令ID”存在这个表中并且ID与之匹配,那我们就认为是有效的,就对这帧数据进行相应处理,也就是触发对应的命令处理函数。

  2. 在代码中是怎么建表的?
    首先看表的框架可以看出它是一个数组的模样,有很多元素,但与普通数组不同的是它每个元素都可以存放多个不同的数据类型变量或者函数,那么结构体刚好就可以满足元素的特征,即

    typedef struct {
       uint8_t func_id;
       uint8_t cmd_id;
       CommandHandler handler;
    } List;
    

    所以表实质可以理解为一个结构体数组。
    代码建表示例:List cmd_table[] = {
    {功能ID1, 命令ID1, 处理函数1},
    {功能ID2, 命令ID2, 处理函数2}
    };

  3. 什么是函数指针?
    你可以理解它就是一个指向函数的指针,像上面结构体当中的CommandHandler 就是一个函数指针类型,具体函数指针是怎么定义的呢,示例见:void (*CommandHandler)(uint8_t*, uint16_t); 我们一般常在前面加一个“typedef”,即:typedef void (*CommandHandler)(uint8_t*, uint16_t);那么typedef起什么作用呢?主要是用于起别名,方便定义,那么在这里的作用就是:CommandHandler == void (*)(uint8_t*, uint16_t) ,所以在结构体中可以直接用CommandHandler 去定义处理函数

  4. 代码建表示例
    通过上面的讲解,我们来建一个表结构,具体如下:
    ①定义函数指针

    typedef void (*CommandHandler)(uint8_t*, uint16_t);
    

    ②定义结构体

    typedef struct {
            uint8_t func_id;               // 功能ID
            uint8_t cmd_id;                // 命令ID
            CommandHandler handler;      // 处理函数
            char    desc[32];              // 功能说明
    } CommandList;
    

    ③建表

    static CommandList cmd_table[] = {
        {0x01, 0x01, handle_system_reboot, "系统重启命令"},
        {0x01, 0x02, handle_sensor_read,   "传感器读取命令"}
    };
    

二、代码设计

1. 定义协议字段

#pragma pack(1)
typedef struct {
    uint16_t start_flag;    // 起始位
    uint16_t config;         // 配置位
    uint16_t seq_num;        // 序列号
    uint16_t total_len;     // 总长度
    uint16_t header_crc;    // 头校验
    uint8_t  src_addr;       // 源地址
    uint8_t  dst_addr;       // 目的地址
    uint8_t  func_id;        // 功能类型ID
    uint8_t  cmd_id;         // 命令ID
    uint16_t data_len;      // 数据长度
    uint8_t* data;           // 数据指针
    uint16_t tail_crc;      // 尾校验
} ProtocolFrame;
#pragma pack()

解析说明:
#pragma pack(1):强制编译器按1字节对齐
#pragma pack():恢复编译器默认的对齐方式
因为在发送端是标准一个字节一个字节组合帧发送的,而在接收端用结构体去解析,考虑存在字节对齐填充问题,所以需要设置为1字节对齐。

2. 对各字段数据建立表结构

在解析数据帧的时候也可以通过查表方式去处理各字段数据
①定义字段处理函数

static void parse_header(void* field);       // 起始位字段
static void parse_config(void* field);        // 配置位字段
static void parse_sequence(void* field);     // 序列号字段
static void parse_length(void* field);        // 帧总长度字段
static void verify_header(void* field);       // 头校验字段
static void parse_src_address(void* field);   // 源地址字段
static void parse_dest_address(void* field);  // 目标地址字段
static void parse_func_id(void* field);       // 功能类型ID字段
static void parse_cmd_id(void* field);       // 命令ID字段
static void parse_data_len(void* field);      // 数据长度字段
static void parse_data_ptr(void* field);      // 数据内容字段
static void verify_tail(void* field);          // 尾校验字段

②定义结构体

typedef struct {
    uint16_t offset;            // 字段偏移量
    uint8_t  size;             // 字段大小
    char    name[16];        // 解释说明
    void    (*parser)(void*);   // 处理函数
} FieldMap;

③建表

static FieldMap protocol_map[] = {
    {offsetof(ProtocolFrame, start_flag),  sizeof(uint16_t), "起始位",  parse_header},
    {offsetof(ProtocolFrame, config),     sizeof(uint16_t), "配置位",  parse_config},
    {offsetof(ProtocolFrame, seq_num),   sizeof(uint16_t), "序列号",  parse_sequence},
    {offsetof(ProtocolFrame, total_len),   sizeof(uint16_t), "总长度",  parse_length},
    {offsetof(ProtocolFrame, header_crc), sizeof(uint16_t), "头校验",  verify_header},
    {offsetof(ProtocolFrame, src_addr),   sizeof(uint8_t),  "源地址",  parse_src_address},
    {offsetof(ProtocolFrame, dst_addr),   sizeof(uint8_t),  "目的地址",parse_dest_address},
    {offsetof(ProtocolFrame, func_id),    sizeof(uint8_t),  "功能ID",  parse_func_id},
    {offsetof(ProtocolFrame, cmd_id),    sizeof(uint8_t),  "命令ID",  parse_cmd_id},
    {offsetof(ProtocolFrame, data_len),   sizeof(uint16_t), "数据长度",parse_data_len},
    {offsetof(ProtocolFrame, data),      sizeof(uint8_t*), "数据指针",parse_data_ptr},
    {offsetof(ProtocolFrame, tail_crc),    sizeof(uint16_t), "尾校验",  verify_tail}
};

3. 建立功能ID、命令ID处理表

typedef void (*CommandHandler)(uint8_t*, uint16_t);
static void handle_system_reboot(uint8_t* data, uint16_t len);
static void handle_sensor_read(uint8_t* data, uint16_t len);
static void default_handler(uint8_t* data, uint16_t len);

typedef struct {
    uint8_t func_id;
    uint8_t cmd_id;
    CommandHandler handler;
    char    desc[32];
} CommandMap;


static CommandMap cmd_table[] = {
    {0x01, 0x01, handle_system_reboot, "系统重启命令"},
    {0x02, 0x01, handle_sensor_read,   "传感器读取命令"}
};

4. 完整数据帧解析流程

step1:将接收到的数据代入解析函数
step2:强制转换为结构体协议帧类型
step3:对每个字段进行查表处理
step4:如果各字段查表处理通过,则执行最终的命令表处理

void parse_frame(uint8_t *raw_data) 
{
    ProtocolFrame *frame = (ProtocolFrame*)raw_data;

    // 字段表驱动解析
for(size_t i=0; i<sizeof(protocol_map)/sizeof(FieldMap); i++) 
{
        uint8_t *field_ptr = raw_data + protocol_map[i].offset;
        printf("解析字段: %s\n", protocol_map[i].name);
        protocol_map[i].parser(field_ptr);
    }

// 执行最终处理
if( flag == true)
    	parse_data(frame->data, frame->data_len, frame->func_id, frame->cmd_id);
}

三、视频讲解

嵌入式串口通信高阶实战(3/3)——协议解析(查表法)

嵌入式串口通信高阶实战(3/3)——协议解析(查表法)

四、技术交流

感兴趣同学联系主页wx(Lntt-xbc)加入交流群

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐