3. 协议栈架构设计:分层设计思想、协议栈的层次划分、接口定义、数据流设计
做运动控制这么多年,我见过太多人一上来就写代码,结果写到一半发现改不动了。说白了,协议栈这东西,架构没想好,后面全是坑。今天咱们就聊聊,怎么把协议栈的骨架搭稳。
3.1 为什么一定要分层?
你想想看,一个运动控制系统,从最底层的电机驱动,到最上层的轨迹规划,中间要经过多少环节?如果所有代码都揉在一起,改一个地方就得动全局。我早期做过一个项目,就因为没分层,客户想换个通信协议,我硬是重构了三个月。
分层设计的好处,说白了就三个字:解耦。每一层只管自己的事,层与层之间只通过接口说话。这样,底层换了,上层不用动;上层改了,底层不受影响。
3.2 协议栈的层次划分
我个人习惯把运动控制协议栈分成四层。嗯,这四层是我在多个项目中反复打磨出来的,你可以直接拿来用。
| 层次 | 名称 | 职责 | 典型模块 |
|---|---|---|---|
| 第4层 | 应用层 | 用户指令解析、轨迹规划 | G代码解析、点位运动 |
| 第3层 | 控制层 | 运动控制算法、插补计算 | PID、S曲线、直线插补 |
| 第2层 | 协议层 | 数据封装、校验、重传 | 帧结构、CRC、超时管理 |
| 第1层 | 物理层 | 硬件接口、信号收发 | UART、CAN、EtherCAT |
这里我要特别说一下协议层。很多人觉得,不就是发个数据包吗?其实没那么简单。我在做六轴机器人项目时,遇到过数据丢包导致关节抖动的问题。后来在协议层加了序号和重传机制,才彻底解决。
3.3 接口定义:层与层之间的契约
接口定义是分层设计的灵魂。我见过最糟糕的情况,是层与层之间直接传递结构体指针,改一个字段,所有层都得跟着改。
我的做法是:每层只暴露一组纯函数接口。举个例子,协议层向上层提供的接口大概长这样:
/* 协议层接口定义 */
typedef struct {
uint8_t* data; /* 数据指针 */
uint16_t len; /* 数据长度 */
uint32_t timestamp; /* 时间戳 */
} proto_packet_t;
/* 发送接口 */
int proto_send(proto_packet_t* pkt);
/* 接收接口(回调方式) */
typedef void (*proto_recv_cb_t)(proto_packet_t* pkt);
int proto_register_recv_cb(proto_recv_cb_t cb);
/* 状态查询 */
int proto_get_status(void);
你看,接口里只传递必要的数据,不暴露内部实现。这样,就算我把协议从CAN换成EtherCAT,上层代码一行都不用改。
3.4 数据流设计:数据是怎么跑的?
数据流设计,说白了就是搞清楚数据从哪来、到哪去、中间经过谁。我习惯画一张数据流图,把每个环节的输入输出标清楚。
数据流其实就两条线:下行指令流和上行状态流。下行流从应用层一路往下,最终变成物理信号发出去。上行流反过来,从物理层收数据,一层层解析,最终交给应用层处理。
这里有个关键点:每一层都要做自己的错误处理。比如协议层发现CRC校验失败,它自己就处理了重传,不会把错误数据往上抛。我曾经见过一个系统,协议层不处理错误,结果控制层收到乱码,电机直接飞车了。
3.5 接口设计的一个实战案例
拿我之前做的一个四轴机械臂项目来说。应用层需要下发一个「点到点运动」指令。数据流是这样的:
- 应用层:把用户输入的坐标 (x, y, z, r) 转成运动参数,调用控制层接口
ctrl_move_to(pos_t* target) - 控制层:计算插补点,生成每个关节的角度值,调用协议层接口
proto_send(proto_packet_t* pkt) - 协议层:把关节角度封装成帧,加上帧头、CRC、序号,调用物理层接口
phy_send_bytes(uint8_t* buf, uint16_t len) - 物理层:通过CAN总线把数据发出去
你看,每一层只做自己的事,接口清晰,出了问题也好定位。我当时调试时发现某个关节角度不对,直接定位到控制层的插补算法,半小时就修好了。
3.6 关于接口稳定性的建议
接口一旦定下来,就别轻易改。我有个血的教训:项目做到一半,觉得某个接口参数不合理,顺手改了。结果下游三个模块都得跟着改,测试用例全废了,加班一周才搞定。
所以我的建议是:接口设计阶段多花点时间,想清楚再动手。可以先用伪代码把接口写出来,找团队评审一遍。等大家都认可了,再开始实现。
proto_interface.h。实现文件可以随便改,但接口头文件一旦定稿,就加入版本管理,改它要走变更流程。
好了,协议栈的架构设计就聊到这儿。记住一句话:好的架构是改出来的,但好的接口是想出来的。下一节咱们会深入协议层的具体实现,到时候再细聊帧结构和CRC算法。