找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4512

积分

0

好友

624

主题
发表于 2 小时前 | 查看: 2| 回复: 0

在嵌入式开发中,自定义通信协议是家常便饭。常规做法是直接把封包和解包的函数写好,业务层直接调用。但这样会带来一个明显的问题:协议和业务代码耦合得太紧。一旦需要更换协议,比如从帧头为0x5AA5的协议换到帧头为0xFF的协议,整个软件模块可能都要大动干戈。

有没有一种设计方案,能让协议切换像换模块一样简单,业务代码几乎不用改?答案就在面向对象和抽象接口的设计思想里。这套方法堪称解耦的“万能钥匙”,而在C语言中,实现抽象接口的核心工具就是函数指针

核心设计思路

整个框架的设计目标清晰明了:

  • 定义一套统一的协议操作接口:包括解包(unpack)、组帧(pack)、校验(match)等,所有协议都必须遵守这套“契约”。
  • 每个具体协议独立实现接口:例如,0x5AA5协议和0xFF协议分别实现自己的封包解包逻辑。
  • 动态注册协议:将所有协议实例注册到一个全局管理表中。
  • 上层通过标识获取接口:业务代码通过协议ID或名字,从管理表中获取对应的协议接口进行操作。
  • 业务与协议解耦:业务层只知道统一的接口,完全不用关心底层具体是哪个协议在干活。

用一句话总结就是:协议 = 虚接口 + 实例 + 自动注册 + 自动匹配

项目文件结构

.
├── main.c
├── makefile
├── protocol.c
├── protocol.h
├── protocol_obj.c
└── protocol_obj.h
  • protocol.h:定义协议抽象接口(protocol_t)、通用数据结构、错误码、核心API声明。
  • protocol.c:实现协议的注册、查找、自动匹配、多通道绑定等核心管理逻辑。
  • protocol_obj.c:实现具体协议(如5AA5/FF)的实例化(包含pack/unpack/match接口实现和私有数据)、协议初始化注册。
  • protocol_obj.h:声明协议初始化函数,对外暴露协议注册入口。
  • main.c:业务层示例,演示如何使用框架进行协议自动识别、解包和组包。
  • makefile:编译构建脚本。

设计亮点

  1. 接口抽象:通过protocol_t结构体定义统一接口,所有具体协议实现它,符合“面向接口编程”原则。
  2. 动态注册:通过proto_register将协议实例注册到全局列表,扩展新协议时无需修改框架核心代码。
  3. 自动匹配:通过proto_automatch遍历已注册协议,调用各自的match接口自动识别数据帧所属的协议。
  4. 多通道独立:支持多个通道绑定不同的协议实例,适应多路串口或总线独立协议的场景。
  5. 私有数据:每个协议实例可通过priv指针持有独立的私有数据(如接收帧计数),方便进行协议级别的状态管理。

头文件实现(抽象层与对外接口)

protocol.h 定义了整个框架的骨架和对外契约。

#ifndef __PROTOCOL_H__
#define __PROTOCOL_H__

#include <stdint.h>
#include <string.h>

#define PROTO_MAX_NAME_LEN    16
#define PROTO_MAX_REGISTER    8    // 最大支持协议数
#define PROTO_MAX_DATA_LEN    64

// 解析结果
typedef enum {
    PROTO_OK = 0,
    PROTO_ERR_HEAD,
    PROTO_ERR_LEN,
    PROTO_ERR_CHECK,
} proto_status_t;

// 统一数据包(上层只认这个)
typedef struct {
    uint8_t cmd;
    uint8_t data[PROTO_MAX_DATA_LEN];
    uint8_t data_len;
} proto_packet_t;

// 协议虚接口(核心!所有协议必须实现)
typedef struct protocol protocol_t;
struct protocol {
    // 基础信息
    uint8_t id;    // 协议ID
    char    name[PROTO_MAX_NAME_LEN]; // 协议名

    // 统一接口
    proto_status_t (*unpack)(protocol_t *proto, uint8_t *buf, uint8_t len, proto_packet_t *pkt);
    uint8_t        (*pack)(protocol_t *proto, uint8_t *buf, uint8_t cmd, uint8_t *data, uint8_t data_len);

    // 自动识别:判断是不是本协议帧
    uint8_t        (*match)(uint8_t *buf, uint8_t len);

    // 私有数据(每个协议独立)
    void *priv;
};

// 动态注册协议
uint8_t proto_register(protocol_t *proto);

// 按ID查找协议
protocol_t *proto_find_by_id(uint8_t id);

// 按名字查找协议
protocol_t *proto_find_by_name(const char *name);

// 自动识别:输入一帧,自动返回匹配的协议
protocol_t *proto_automatch(uint8_t *buf, uint8_t len);

// 每个通道可以绑定独立协议
void proto_setchannelproto(uint8_t ch, protocol_t *proto);
protocol_t *proto_getchannelproto(uint8_t ch);

#endif

C文件实现(核心框架,稳定不变)

protocol.c 实现了协议管理器,这部分代码在增加新协议时通常无需修改。

#include "protocol.h"

static protocol_t *s_proto_list[PROTO_MAX_REGISTER] = {0};
static uint8_t s_proto_count = 0;

// 通道绑定协议(支持多路串口同时不同协议)
static protocol_t *s_ch_proto[4] = {0};

//============================================================================
// 动态注册
//============================================================================
uint8_t proto_register(protocol_t *proto)
{
    if (s_proto_count >= PROTO_MAX_REGISTER) return 0;
    s_proto_list[s_proto_count++] = proto;
    return 1;
}

//============================================================================
// 按 ID / 名字查找
//============================================================================
protocol_t *proto_find_by_id(uint8_t id)
{
    for (uint8_t i = 0; i < s_proto_count; i++) {
        if (s_proto_list[i]->id == id)
            return s_proto_list[i];
    }
    return NULL;
}

protocol_t *proto_find_by_name(const char *name)
{
    for (uint8_t i = 0; i < s_proto_count; i++) {
        if (strcmp(s_proto_list[i]->name, name) == 0)
            return s_proto_list[i];
    }
    return NULL;
}

//============================================================================
// 自动识别协议(来什么帧,自动匹配)
//============================================================================
protocol_t *proto_automatch(uint8_t *buf, uint8_t len)
{
    for (uint8_t i = 0; i < s_proto_count; i++) {
        if (s_proto_list[i]->match && s_proto_list[i]->match(buf, len))
            return s_proto_list[i];
    }
    return NULL;
}

//============================================================================
// 多通道独立协议
//============================================================================
void proto_setchannelproto(uint8_t ch, protocol_t *proto)
{
    if (ch < 4) s_ch_proto[ch] = proto;
}

protocol_t *proto_getchannelproto(uint8_t ch)
{
    if (ch < 4) return s_ch_proto[ch];
    return NULL;
}

具体协议实现实例

protocol_obj.c 展示了如何实现两个具体的协议:0x5AA50xFF。这里体现了面向接口编程的具体实践。

#include "protocol_obj.h"

// 协议1: 0x5AA5 的私有数据
typedef struct {
    uint32_t rx_count;
} proto_5aa5_priv_t;
static proto_5aa5_priv_t s_priv_5aa5;

// 匹配:判断帧头是否为 0x5AA5
static uint8_t proto_5aa5_match(uint8_t *buf, uint8_t len)
{
    if (len < 2) return 0;
    uint16_t head = (buf[0] << 8) | buf[1];
    return (head == 0x5AA5);
}

// 解包:解析 0x5AA5 协议格式的数据帧
static proto_status_t proto_5aa5_unpack(protocol_t *proto, uint8_t *buf, uint8_t len, proto_packet_t *pkt)
{
    proto_5aa5_priv_t *p = (proto_5aa5_priv_t *)proto->priv;
    p->rx_count++; // 更新私有状态

    if (len < 5) return PROTO_ERR_LEN;
    uint16_t head = (buf[0] << 8) | buf[1];
    if (head != 0x5AA5) return PROTO_ERR_HEAD;

    uint8_t frame_len = buf[2];
    if (len < frame_len) return PROTO_ERR_LEN;

    pkt->cmd = buf[3];
    pkt->data_len = frame_len - 5;
    for (uint8_t i = 0; i < pkt->data_len; i++)
        pkt->data[i] = buf[4 + i];

    // 校验和检查
    uint8_t sum = 0;
    for (uint8_t i = 0; i < frame_len - 1; i++) sum += buf[i];
    if (sum != buf[frame_len - 1]) return PROTO_ERR_CHECK;

    return PROTO_OK;
}

// 组包:打包成 0x5AA5 协议格式
static uint8_t proto_5aa5_pack(protocol_t *proto, uint8_t *buf, uint8_t cmd, uint8_t *data, uint8_t data_len)
{
    uint8_t idx = 0;
    buf[idx++] = 0x5A;
    buf[idx++] = 0xA5;
    uint8_t frame_len = 5 + data_len;
    buf[idx++] = frame_len;
    buf[idx++] = cmd;
    for (uint8_t i = 0; i < data_len; i++) buf[idx++] = data[i];

    uint8_t sum = 0;
    for (uint8_t i = 0; i < idx; i++) sum += buf[i];
    buf[idx++] = sum;

    return idx;
}

// 0x5AA5 协议实例
static protocol_t proto_5aa5 = {
    .id     = 1,
    .name   = "proto_5aa5",
    .unpack = proto_5aa5_unpack,
    .pack   = proto_5aa5_pack,
    .match  = proto_5aa5_match,
    .priv   = &s_priv_5aa5,
};

// 协议2: 0xFF 的私有数据
typedef struct {
    uint32_t rx_count;
} proto_ff_priv_t;
static proto_ff_priv_t s_priv_ff;

// 匹配:判断帧头是否为 0xFF
static uint8_t proto_ff_match(uint8_t *buf, uint8_t len)
{
    if (len < 1) return 0;
    return (buf[0] == 0xFF);
}

// 解包:解析 0xFF 协议格式的数据帧
static proto_status_t proto_ff_unpack(protocol_t *proto, uint8_t *buf, uint8_t len, proto_packet_t *pkt)
{
    proto_ff_priv_t *p = (proto_ff_priv_t *)proto->priv;
    p->rx_count++;

    if (len < 4) return PROTO_ERR_LEN;
    if (buf[0] != 0xFF) return PROTO_ERR_HEAD;

    uint8_t frame_len = buf[1];
    if (len < frame_len) return PROTO_ERR_LEN;

    pkt->cmd = buf[2];
    pkt->data_len = frame_len - 4;
    for (uint8_t i = 0; i < pkt->data_len; i++)
        pkt->data[i] = buf[3 + i];

    uint8_t sum = 0;
    for (uint8_t i = 0; i < frame_len - 1; i++) sum += buf[i];
    if (sum != buf[frame_len - 1]) return PROTO_ERR_CHECK;

    return PROTO_OK;
}

// 组包:打包成 0xFF 协议格式
static uint8_t proto_ff_pack(protocol_t *proto, uint8_t *buf, uint8_t cmd, uint8_t *data, uint8_t data_len)
{
    uint8_t idx = 0;
    buf[idx++] = 0xFF;

    uint8_t frame_len = 4 + data_len;
    buf[idx++] = frame_len;
    buf[idx++] = cmd;

    for (uint8_t i = 0; i < data_len; i++) buf[idx++] = data[i];

    uint8_t sum = 0;
    for (uint8_t i = 0; i < idx; i++) sum += buf[i];
    buf[idx++] = sum;

    return idx;
}

// 0xFF 协议实例
static protocol_t proto_ff = {
    .id     = 2,
    .name   = "proto_ff",
    .unpack = proto_ff_unpack,
    .pack   = proto_ff_pack,
    .match  = proto_ff_match,
    .priv   = &s_priv_ff,
};

// 协议初始化(动态注册)
void protocol_init(void)
{
    proto_register(&proto_5aa5);
    proto_register(&proto_ff);
}

初始化与注册

在系统启动时,调用一次protocol_init(),即可完成所有协议实例的自动注册。

void protocol_init(void)
{
    proto_register(&proto_5aa5);
    proto_register(&proto_ff);
}

上层业务代码使用示例

业务层代码变得非常简洁和统一,完全与具体协议解耦。

1. 主动选择协议

你可以通过协议名或ID来获取指定的协议实例。

protocol_t *proto = proto_find_by_name("proto_5aa5");
// 或者
protocol_t *proto = proto_find_by_id(1);

2. 自动识别并解包(推荐)

在实际接收数据时,你甚至可以不关心具体协议,让框架自动匹配。

void recv_task(uint8_t *buf, uint8_t len)
{
    proto_packet_t pkt;

    protocol_t *proto = proto_automatch(buf, len);
    if (proto == NULL) {
        return; // 没有协议能识别此帧
    }

    if (proto->unpack(proto, buf, len, &pkt) == PROTO_OK) {
        // 统一的业务逻辑,使用 pkt.cmd, pkt.data
    }
}

3. 发送数据

发送时,指定协议进行组包。

void send_55aa_proto(uint8_t cmd, uint8_t *data, uint8_t len)
{
    protocol_t *proto = proto_find_by_name("proto_5aa5");
    if (proto == NULL) {
        return;
    }

    uint8_t buf[255] = {0};
    uint8_t size = proto->pack(proto, buf, cmd, data, len);
    // uart_send(buf, size);
    // can_send(buf, size);
    // socket_send(buf, size);
}

未来扩展新协议只需三步

当产品需要支持第三种、第四种协议时,代码的改动被降到最低:

  1. 复制并修改:参照 proto_5aa5proto_ff,实现新的协议解析和组包逻辑。
  2. 配置信息:赋予新协议一个唯一的ID和名字。
  3. 注册入库:在 protocol_init() 函数中调用 proto_register(&新协议实例)

完成这三步,新的协议立刻就能被框架自动管理,所有现有的业务代码无需任何修改即可支持。这种设计极大地提升了代码的可维护性和可扩展性,是嵌入式系统中处理多协议或协议升级场景的优雅方案。




上一篇:森林公园散步时,我与AI龙虾的几次深度对话
下一篇:VS Code 2024高效开发指南:掌握快捷键、多光标与全局搜索核心技巧
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-26 06:11 , Processed in 1.026960 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表