3. 运动控制程序架构设计:分层架构
做运动控制这么多年,我见过太多「一锅粥」式的代码了。什么叫一锅粥?就是中断里算轨迹,驱动里做插补,UI线程里还直接操作寄存器。这种代码,改一个地方,崩三个功能。
所以今天咱们聊聊分层架构。说白了,就是把程序拆成三个独立的楼层——应用层、算法层、驱动层。每层只管自己的事,层与层之间通过明确的接口对话。
核心原则:上层依赖下层,下层不依赖上层。每一层都可以独立替换、独立测试。
3.1 为什么必须分层?
我刚开始做运动控制时,也写过「大泥球」代码。有一次客户要换驱动器,从脉冲型换成EtherCAT总线型。结果呢?整个程序几乎重写,因为插补算法和驱动代码混在一起,动一处就牵全身。
分层架构的好处,你想想看:
- 可替换性:换驱动器?只改驱动层。换算法?只改算法层。
- 可测试性:每层都能单独写单元测试。我习惯先写驱动层的测试,再写算法层的,最后才是应用层。
- 可读性:新人接手,看接口定义就能明白数据怎么流,不用翻遍所有代码。
我的习惯:项目一开始,先花半天时间把三层接口定义好。哪怕后面要改,也比没有接口强十倍。
3.2 三层架构详解
3.2.1 应用层
应用层是离用户最近的一层。它负责:
- 接收用户的指令(比如「走一个圆形轨迹」)
- 管理工艺流程(比如「先回零,再定位,最后点胶」)
- 处理状态机(空闲、运行、报警、急停等)
应用层不关心底层怎么发脉冲,也不关心插补算法具体怎么算。它只调用算法层提供的接口。
// 应用层代码示例
void Application_StartCircle(double centerX, double centerY, double radius) {
// 调用算法层接口,生成轨迹点
TrajectoryPoint points[100];
int pointCount = Algorithm_GenerateCircle(centerX, centerY, radius, points);
// 调用算法层接口,执行轨迹
Algorithm_ExecuteTrajectory(points, pointCount);
}
3.2.2 算法层
算法层是运动控制的「大脑」。它负责:
- 轨迹规划(直线插补、圆弧插补、S形加减速)
- 位置/速度/力矩控制(PID、前馈、陷波滤波)
- 坐标系变换(比如从笛卡尔坐标转关节坐标)
算法层不直接操作硬件。它把计算结果(比如每个轴的目标位置)通过接口传给驱动层。
注意:算法层里不要出现任何硬件相关的代码。我曾经见过有人在插补算法里直接写GPIO置位,这种代码一换平台就废了。
// 算法层接口示例
int Algorithm_GenerateCircle(double cx, double cy, double r, TrajectoryPoint* outPoints);
int Algorithm_ExecuteTrajectory(TrajectoryPoint* points, int count);
void Algorithm_SetSpeed(double speed);
void Algorithm_SetAcceleration(double acc);
3.2.3 驱动层
驱动层是「手脚」。它负责:
- 与硬件通信(脉冲输出、EtherCAT、CANopen)
- 读取编码器反馈
- 处理IO信号(限位、原点、急停)
驱动层是最底层的代码,它只做一件事:把算法层算好的目标位置,变成硬件能理解的信号。
// 驱动层接口示例
int Driver_SetTargetPosition(int axis, double position);
int Driver_GetActualPosition(int axis, double* position);
int Driver_EnableAxis(int axis);
int Driver_DisableAxis(int axis);
3.3 接口定义:层与层的「契约」
接口是分层架构的灵魂。接口定义得好,各层可以并行开发;接口定义得烂,后面全是坑。
我个人习惯用C语言的结构体来定义接口参数,这样清晰且易于扩展:
// 轨迹点结构体
typedef struct {
double x; // X轴位置(mm)
double y; // Y轴位置(mm)
double z; // Z轴位置(mm)
double speed; // 该点速度(mm/s)
uint32_t timestamp; // 时间戳(us)
} TrajectoryPoint;
// 轴状态结构体
typedef struct {
double actualPos; // 实际位置
double targetPos; // 目标位置
double actualVel; // 实际速度
uint32_t errorCode; // 错误码
bool isEnabled; // 是否使能
} AxisStatus;
接口设计原则:
- 参数类型明确,不要用void*
- 返回值统一:0表示成功,负数表示错误码
- 每个接口只做一件事
3.4 数据流设计
数据怎么在三层之间流动?我画了一张图来说明:
数据流其实很简单:
- 下行流:应用层 → 算法层 → 驱动层。指令从上往下传。
- 上行流:驱动层 → 算法层 → 应用层。反馈从下往上走。
这里有个关键点:反馈数据不能直接跳过算法层跑到应用层。比如编码器位置,驱动层读到后先传给算法层做闭环控制,算法层再把处理后的状态传给应用层。为什么?因为应用层不需要知道原始编码器值,它只需要知道「轴现在在哪个位置」就够了。
避坑指南:我曾经在项目里让应用层直接读编码器,结果应用层代码里到处都是单位换算和滤波处理。后来重构时,我把这些全塞回算法层,应用层清爽多了。
3.5 实际项目中的分层示例
拿一个三轴点胶机来说:
| 层级 | 职责 | 典型接口 |
|---|---|---|
| 应用层 | 接收G代码、管理点胶工艺、处理用户暂停/急停 | StartProgram(), Pause(), Resume(), Stop() |
| 算法层 | G代码解析、直线/圆弧插补、速度前瞻、胶量补偿 | ParseGCode(), InterpolateLine(), LookAhead() |
| 驱动层 | 脉冲输出、编码器读取、胶阀控制、IO检测 | SetPulse(), ReadEncoder(), SetValve() |
你看,每层的职责非常清晰。换一个胶阀?只改驱动层。换一种插补算法?只改算法层。上层代码完全不用动。
再次强调:千万不要在应用层里直接调用驱动层的函数。哪怕你觉得「就这一次,图个方便」,后面也会变成灾难。我吃过这个亏,真的。
好了,分层架构的核心就这些。记住:接口是契约,数据流是血脉,各层各司其职。做到这三点,你的运动控制程序就能经得起折腾。