在嵌入式开发中,自定义通信协议是家常便饭。常规做法是直接把封包和解包的函数写好,业务层直接调用。但这样会带来一个明显的问题:协议和业务代码耦合得太紧。一旦需要更换协议,比如从帧头为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:编译构建脚本。
设计亮点
- 接口抽象:通过
protocol_t结构体定义统一接口,所有具体协议实现它,符合“面向接口编程”原则。
- 动态注册:通过
proto_register将协议实例注册到全局列表,扩展新协议时无需修改框架核心代码。
- 自动匹配:通过
proto_automatch遍历已注册协议,调用各自的match接口自动识别数据帧所属的协议。
- 多通道独立:支持多个通道绑定不同的协议实例,适应多路串口或总线独立协议的场景。
- 私有数据:每个协议实例可通过
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 展示了如何实现两个具体的协议:0x5AA5 和 0xFF。这里体现了面向接口编程的具体实践。
#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);
}
未来扩展新协议只需三步
当产品需要支持第三种、第四种协议时,代码的改动被降到最低:
- 复制并修改:参照
proto_5aa5 或 proto_ff,实现新的协议解析和组包逻辑。
- 配置信息:赋予新协议一个唯一的ID和名字。
- 注册入库:在
protocol_init() 函数中调用 proto_register(&新协议实例)。
完成这三步,新的协议立刻就能被框架自动管理,所有现有的业务代码无需任何修改即可支持。这种设计极大地提升了代码的可维护性和可扩展性,是嵌入式系统中处理多协议或协议升级场景的优雅方案。