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 数据流设计:数据是怎么跑的?

数据流设计,说白了就是搞清楚数据从哪来、到哪去、中间经过谁。我习惯画一张数据流图,把每个环节的输入输出标清楚。

运动控制协议栈数据流图 第4层:应用层 用户指令 → 轨迹规划 → 生成运动参数 下发运动参数 第3层:控制层 插补计算 → PID调节 → 生成控制指令 下发控制指令 第2层:协议层 帧封装 → CRC校验 → 超时重传管理 发送数据帧 第1层:物理层 UART/CAN/EtherCAT 硬件收发 状态反馈/错误上报

数据流其实就两条线:下行指令流上行状态流。下行流从应用层一路往下,最终变成物理信号发出去。上行流反过来,从物理层收数据,一层层解析,最终交给应用层处理。

这里有个关键点:每一层都要做自己的错误处理。比如协议层发现CRC校验失败,它自己就处理了重传,不会把错误数据往上抛。我曾经见过一个系统,协议层不处理错误,结果控制层收到乱码,电机直接飞车了。

避坑指南: 数据流中一定要留「心跳」机制。我做过一个远程控制项目,物理层断了,上层完全不知道,还在傻等数据。后来每层都加了超时检测,问题才解决。

3.5 接口设计的一个实战案例

拿我之前做的一个四轴机械臂项目来说。应用层需要下发一个「点到点运动」指令。数据流是这样的:

  1. 应用层:把用户输入的坐标 (x, y, z, r) 转成运动参数,调用控制层接口 ctrl_move_to(pos_t* target)
  2. 控制层:计算插补点,生成每个关节的角度值,调用协议层接口 proto_send(proto_packet_t* pkt)
  3. 协议层:把关节角度封装成帧,加上帧头、CRC、序号,调用物理层接口 phy_send_bytes(uint8_t* buf, uint16_t len)
  4. 物理层:通过CAN总线把数据发出去

你看,每一层只做自己的事,接口清晰,出了问题也好定位。我当时调试时发现某个关节角度不对,直接定位到控制层的插补算法,半小时就修好了。

3.6 关于接口稳定性的建议

接口一旦定下来,就别轻易改。我有个血的教训:项目做到一半,觉得某个接口参数不合理,顺手改了。结果下游三个模块都得跟着改,测试用例全废了,加班一周才搞定。

所以我的建议是:接口设计阶段多花点时间,想清楚再动手。可以先用伪代码把接口写出来,找团队评审一遍。等大家都认可了,再开始实现。

我的习惯: 接口定义单独放在一个头文件里,比如 proto_interface.h。实现文件可以随便改,但接口头文件一旦定稿,就加入版本管理,改它要走变更流程。

好了,协议栈的架构设计就聊到这儿。记住一句话:好的架构是改出来的,但好的接口是想出来的。下一节咱们会深入协议层的具体实现,到时候再细聊帧结构和CRC算法。


专注资料整理