接到这个标题的时候,我第一反应是想起几年前一次不算愉快的加班经历。当时设备从两轴扩到六轴,程序从一千多行涨到七千多行,用的还是那种"哪个动作需要走位,就在哪个段里直接调 MC_MoveAbsolute"的写法。结果就是:查找某一个轴的定位指令要全局搜三十多处,改一个加减速参数要翻好几个程序段,新来的同事看代码半天不敢动一行。那时候我意识到一个问题,做 CODESYS 多轴运动控制,最紧缺的不是指令手册,而是一套能把逻辑和运动分开的框架。
这篇文章就想把我在实际项目中反复打磨出来的这套框架思路讲清楚:为什么要做逻辑与运动分离,CODESYS 里哪些机制在支撑这种架构,框架具体怎么分层,定位、点动、电子齿轮、CNC 插补这些功能怎么统一接入,以及落地过程中最容易翻车的几个现场。内容偏工程实践,适合已经在用 CODESYS 做设备开发的工程师,也适合准备从单轴往多轴、从写死流程往做系列化设备转型的团队参考。
1. 逻辑与运动纠缠的代价:一次从两轴到六轴的重构
1.1 运动指令散落在工艺代码里的维护困境
先还原一个场景。早期用 CODESYS 做三轴设备,程序结构很简单:主程序里按流程顺序调用动作,每个动作段里直接写 MC_Power、MC_MoveAbsolute、MC_MoveVelocity,再配一堆延时和中间变量。轴少、动作少,这种写法确实直接,一个下午就能把逻辑理清。
问题出在设备变复杂之后。轴数从两个变成六个,动作从几十步变成上百步,程序从一千行涨到七千行。此时"运动指令散落在工艺代码里"的后果全部暴露:
- 需要确认某个轴当前在哪里被调用,只能全局搜索关键词,搜出来的几十处分布在十几个不同的程序段里;
- 想调整一个运动的加减速曲线,必须逐个找到所有调用点手动修改,漏一个就可能出现"大部分定位正常、某一个动作震得厉害"的怪病;
- 新同事维护时根本不敢动任何一行,因为不知道这一行运动指令在整条工艺链路里被哪些条件依赖着。
这不是指令不会用的问题,而是程序架构的问题。运动的"动作"和工艺的"流程"没有边界,混在一起之后就变成了一个难以维护的大泥球。
1.2 设备要并联进产线时的对接压力
第二个场景发生在设备需要并入产线通讯的时候。上位机要求下发"启动配方A""暂停当前流程""切换机型"这样的指令,还要实时读回当前工艺步骤和轴状态。
本来以为只是加个通讯模块的事情,结果发现没有分层架构的程序根本没法对接。因为工艺状态变量散落在几十个功能块里,轴状态也散布在各处,想整理出一个完整的状态字反馈给上位机,工作量几乎等于重写一遍逻辑。而如果当初就做了逻辑层和运动层的隔离,通讯层只需要对接工艺层的状态接口,不需要关心内部到底调了哪个运动块。
也是因为这次对接,我下定决心做重构。
1.3 三种机型复用一套程序时的配置难题
第三个场景是系列化设备。同一套机械平台要出三种不同型号的机器,工艺顺序类似但具体参数、行程、速度完全不同。
在没有分层架构的程序里,切换机型的做法是复制一份代码,然后手工改里面的运动参数。两个型号还能忍,三个型号就有三套程序、三个版本、三处 bug 修复要同步。后来做分层之后,机型切换只变成切换一组配置数据和一张工艺序列表,程序本身完全不动。这也是逻辑与运动分离带来的最直接收益。
1.4 什么时候值得搭框架
也不是所有项目都非得搭框架。我自己的判断标准有两个:
- 运动指令出现的次数超过二十到三十次,或者轴的数量超过四个;
- 明确知道要做系列化设备,而不是做单台验证机。
如果项目还在"一台样机打通功能"阶段,直接写最快,不用上来就分三层。框架的价值在"变化多、复用多"的场景里才体现得出来。真到了那个规模再重构,成本远高于一开始就分层。
2. CODESYS 的三块架构基石:任务配置、轴对象与 PLCopen 状态机
2.1 任务配置:让逻辑和运动各跑各的节拍
CODESYS 的任务配置是整个框架的第一个支点。它允许在同一个工程里配置多个周期任务,而且每个任务可以有不同的周期和优先级。运动控制的实时性要求高,一般需要独立的高优先级周期任务;工艺逻辑、HMI 通讯、顺序控制则可以在较低优先级的任务里跑。
我在实际项目中通常是这么分配的:运动任务周期 1ms 到 2ms,优先级给 0 或 1;逻辑任务周期 5ms 到 10ms,优先级给 10 左右。这样做的意义是,运动计算不会被工艺代码里的复杂分支拖累,同时逻辑层的改动也不会影响运动任务的时序稳定性。
有人会问,一个任务全跑不行吗?小项目可以,但只要轴一多、插补一上,运动任务里混入冗长的逻辑计算,周期抖动就会直接在运动曲线上体现出来。框架里把任务分开,本质上就是给逻辑和运动划出各自的调度域。
2.2 轴对象:一切以 AXIS_REF 为锚点
CODESYS SoftMotion 里,轴的抽象做得非常干净。每一个轴都是轴对象(AXIS_REF),要么映射到真实驱动器(EtherCAT、CANopen 等),要么作为虚拟轴在仿真环境里运行。轴的位置、速度、状态、参数都封装在这个对象里,程序通过轴引用去操作它。
这块对架构的意义很大。程序里不需要关心轴背后是 EtherCAT 伺服还是虚拟轴,只要拿到 AXIS_REF 就能调用 SoftMotion 功能块。这意味着框架的运动执行层可以做得和设备硬件解耦,仿真时用虚拟轴,实机时把配置切到真实驱动即可。
轴对象同时还是一个天然的"集中管理点"。轴使能、回零、软限位、加减速默认值,全部在轴配置里定义。上层程序不用反复读取硬件信息,直接从这个对象拿状态就行。
2.3 PLCopen 状态机:每个运动块的通用行为
第三个重要支点是 PLCopen 规范定义的运动控制状态机。CODESYS SoftMotion 的所有运动控制功能块,无论是 MC_Power、MC_Home、MC_MoveAbsolute,还是 MC_GearIn、MC_CamIn,都遵循相似的反馈模式:Busy 置位表示正在执行,Done 表示完成,Error 和 ErrorID 表示出错信息。
这个设计等于官方替我们画好了一张轴状态转换图。上层不需要理解每个具体功能块的内部原理,只需要按照"轴是否 Ready、是否运动中、是否有错误"这几个通用状态去做判断。
这就是为什么 CODESYS 天然适合做逻辑与运动分离。它的功能块本身就是封装好的执行单元,任务配置支持分离调度,轴对象支持抽象引用,状态机标准又统一。框架要做的,只是把这些机制组合起来,定义出属于自己的分层接口。
3. 框架落地:三层职责、一份工艺指令表、一组执行器功能块
3.1 三层划分,每层能做什么不能做什么
我最终沉淀下来的框架结构是三层,职责边界非常明确:
| 层级 | 职责 | 禁止事项 |
|---|---|---|
| 工艺逻辑层 | 流程编排、模式切换、信号互锁、HMI 交互、配方选型 | 禁止直接调用任何 MC_ 开头的运动功能块 |
| 运动执行层 | 接收工艺指令、分解成具体运动块调用、管理轴状态机、多轴同步、插补组管理 | 不关心工艺下一步该做什么 |
| 驱动封装层 | 轴配置、驱动器映射、限位/急停 IO 处理、参数初始化 | 不面向具体工艺逻辑 |
这个划分的逻辑很简单:工艺层只表达"我想要什么"(比如:1 号轴走到 200mm 位置,速度 50mm/s),运动层负责"怎么做到"(调用哪个软运动功能块、怎么处理轴组同步),驱动层负责"底层接什么"(这个轴是哪个伺服,软限位在哪个 IO 上)。
类比一下就是餐馆的点餐逻辑:顾客只告诉服务员要什么菜,后厨自己决定用什么锅炒、什么时候放盐。顾客不用进厨房,后厨也不用管顾客为什么点这道菜。
3.2 层间接口:工艺指令结构体与轴状态结构体
层与层之间不直接散传参数,而是通过统一的结构体通信。这是整个框架里最值得抄作业的部分。
工艺指令结构体:
TYPE ST_MotionRequest : STRUCT nMode : INT; // 0=无动作,1=绝对定位,2=点动,3=连续速度,4=电子齿轮,5=电子凸轮,20+=CNC程序号 nAxisID : INT; // 轴逻辑编号,从1开始 fPos : LREAL; // 目标位置,单位mm fVel : LREAL; // 目标速度,单位mm/s fAcc : LREAL; // 加速度,单位mm/s² fDec : LREAL; // 减速度,单位mm/s² fRatio : LREAL; // 齿轮比或凸轮比例 xExecute : BOOL; // 上升沿触发指令执行 xAbort : BOOL; // 请求中止当前运动 END_STRUCT END_TYPE轴状态反馈结构体:
TYPE ST_AxisStatus : STRUCT xReady : BOOL; // 轴已使能且无故障 xHomed : BOOL; // 已完成回零 xMoving : BOOL; // 轴正在运动 xInPosition : BOOL; // 轴已到位 fActPos : LREAL; // 实际位置 fActVel : LREAL; // 实际速度 nAxisState : INT; // 面向工艺的轴状态枚举 nErrID : INT; // 错误码 END_STRUCT END_TYPE这套接口最大的好处是可扩展。以后要加一个什么新功能,比如"力矩模式""速度前馈",只需要在结构体里增加字段,而不需要改动已经写好的工艺层调用代码。而如果当初设计的是"每个功能块直接传十几个形参",扩展一次就得把调用链全改一遍。
3.3 运动执行层的核心功能块骨架
运动执行层内部用一个大功能块 FB_MotionExecutor 做指令分发。它接收工艺指令结构体,解析 nMode,然后调用对应的 SoftMotion 功能块。
核心逻辑类似这样:
FUNCTION_BLOCK FB_MotionExecutor VAR_EXTERNAL stAxisCfg : ARRAY[1..MAX_AXIS] OF ST_AxisConfig; END_VAR VAR fbPower : MC_Power; fbHome : MC_Home; fbMoveAbs : MC_MoveAbsolute; fbMoveVel : MC_MoveVelocity; fbMoveJog : MC_MoveJog; stReq : ST_MotionRequest; stStatus : ST_AxisStatus; END_VAR CASE stReq.nMode OF 0: (* 无动作 *) 1: fbMoveAbs.Execute := stReq.xExecute; fbMoveAbs.Position := stReq.fPos; fbMoveAbs.Velocity := stReq.fVel; fbMoveAbs.Acceleration := stReq.fAcc; fbMoveAbs.Deceleration := stReq.fDec; fbMoveAbs( Axis := stAxisCfg[stReq.nAxisID].AxisRef, Done => stStatus.xInPosition ); 2: (* 点动,调用 MC_MoveJog *) 3: (* 连续速度,调用 MC_MoveVelocity *) 4: (* 电子齿轮,调用 MC_GearIn *) END_CASE;这里的关键细节是:运动执行层内部自己维护使能、回零、急停、故障复位等底层逻辑,工艺层只问"轴 Ready 了吗",而不用关心为什么没 Ready、怎么才能让它 Ready。
3.4 程序组织单元与命名规范
工程文件的组织划分也是框架的一部分。建议至少分成三个库或三个程序区:
- 工艺逻辑区:存放 PRG_Main、PRG_Flow_Recipe、PRG_HMI_Interface
- 运动执行区:存放 FB_MotionExecutor、FB_AxisManager、FB_CNCManager,做成独立库,工艺区引用它
- 配置区:存放 GVL_AxisCfg、GVL_SystemParam
命名建议保持一致性。我常用的前缀规则是:ST_ 表示结构体、FB_ 表示功能块、PRG_ 表示程序、GVL_ 表示全局变量列表、MOT_ 表示运动相关、PLC_ 表示工艺相关。
这样做的好处是,当程序规模变大,团队协作时每个人凭前缀就知道该改哪里的代码,不该动哪里的代码。
4. 多种控制功能的接入:定位、点动、电子齿轮到 CNC 插补
4.1 单轴类功能:绝对定位、点动、连续速度
单轴类功能是最常见的。在框架里它们统一走 nMode=1/2/3 这条路径,工艺层只需要设置目标位置、速度、加速度,然后给一个执行上升沿。
实际项目里,我习惯把加减速参数尽量收敛到配置区或配方表里,工艺层传参通常只传一个"速度倍率"或"目标位置",而不是每次都要把加速度、减速度全部传一遍。原因是加减速本质上是轴特性,不是工艺流程特性,放配置里更合理。现场调试时调整加减速,也只需要改配置,不必动程序逻辑。
对于点动,CODESYS 的 MC_MoveJog 支持正向反向点动,这里要特别注意点动结束后的停止方式。框架里我一般会让点动的减速度用专门的参数,而不是和定位共用,否则定位调顺了,点动会因为减速度太大让设备点头。
4.2 多轴协调类功能:电子齿轮、电子凸轮、虚轴同步
电子齿轮和电子凸轮在多轴设备里非常常用。以典型的飞剪或者追剪为例,从动轴需要跟随主动轴的运行位置按比例运动,或者按一条凸轮曲线跟随。
在分层框架里,这类功能的处理方式和单轴完全不同。工艺层不需要关心主轴是从动轴的关系怎么建立,只需要下一条"建立同步"或"解除同步"的指令,运动执行层内部去调用 MC_GearIn、MC_CamIn 并管理主从关系。
多轴同步在框架里最容易踩的坑是从动轴状态切换。比如从动轴正在做齿轮同步,这时候工艺突然要求它去做一次绝对定位。如果不做处理直接切,从动轴会先往一个方向加速再掉头,机械冲击非常大。我的做法是在运动执行层加一个规则:接收任何新的单轴运动指令之前,先检查该轴是否处于同步状态,如果是,必须先执行解除同步并等轴完全停止,再执行新的指令。
还会用到虚轴。比如两个物理轴需要严格同步走一个轨迹,最好的做法是一个虚轴做速度规划,两个物理轴以电子齿轮方式跟随虚轴。这个机制在框架里也收在运动执行层内部,工艺层看到的还是一句"轴组按曲线运行"。
4.3 插补类功能:把 CNC 能力收进框架
CODESYS SoftMotion 支持 CNC 插补,插补路径通常来自 G 代码或者凸轮表文件。插补和单轴运动不是一个量级的东西,它涉及插补器、路径文件读取、轴组管理。
框架接入插补的方式也很"分离":工艺层只下发"执行 CNC 程序 3 号"、"暂停当前插补"、"停止当前插补"这几种指令,具体初始化插补器、加载文件、建立轴组这些工作全部由运动执行层的 FB_CNCManager 完成。
插补对应的 nMode 我一般用 20 以上的编号,因为插补指令里除了模式号还要带一个程序号,结构体里的 nAxisID 可以扩展成"轴组编号或者程序号"。
在这类控制器里,插补任务的周期稳定性要求更高。如果运动任务周期出现抖动,插补轨迹上就会出现肉眼可见的"锯齿"或"停顿"。因此框架里我会单独检查插补模式下的任务周期波动,一旦发现周期超差,第一时间排查逻辑任务里是不是有长耗时流程阻塞了 CPU。
4.4 各功能的统一入口对照表
我把常用功能的接入方式整理成一张对照表,方便在框架里"对号入座":
| 控制功能 | 内部实际调用 | 框架指令字 | 需要额外配置 |
|---|---|---|---|
| 绝对定位 | MC_MoveAbsolute | nMode=1 | 回零参数、软限位 |
| 点动 | MC_MoveJog | nMode=2 | 点动加减速 |
| 连续速度 | MC_MoveVelocity | nMode=3 | 最大速度限制 |
| 电子齿轮 | MC_GearIn / MC_GearOut | nMode=4 | 主从轴定义、从动轴使能 |
| 电子凸轮 | MC_CamIn / MC_CamOut | nMode=5 | 凸轮表文件 |
| 插补运行 | SMC_Interpolator + SMC_FileReader | nMode=20+程序号 | CNC 程序文件、轴组配置 |
| 回零 | MC_Home | 由执行层自动管理 | 回零方式、方向、速度 |
这张表本质上是"框架能力清单"。每新增一种控制功能,Table 里多一行,工艺层只需要知道指令字和参数,不需要知道底层实现。这也是逻辑与运动分离在扩展性上的最大红利。
5. 任务周期、急停复位、在线下载:三个高发事故现场的复盘
5.1 逻辑任务与运动任务不同周期导致的"已到位"误判
这个坑我印象特别深,现象是设备动作偶发衔接卡顿:工艺层明明已经看到 xInPosition 变成 TRUE,往下发了一段新动作指令,但轴就是不按预期走,偶尔报故障码。
排查了很久才发现根因在任务周期错配。逻辑任务 10ms 刷一次状态,运动任务 1ms 刷一次状态,两个任务共享的 BOOL 变量在读写时没有同步保护。逻辑层读到 xInPosition = TRUE 的那个时刻,运动层的实际状态可能是刚进入到位区间但还没稳定;逻辑层紧接着发的下一个指令,因为其内部状态判断还在旧值上,导致指令先被丢弃或误触发。
解决方式分两步。第一步,所有轴的最终状态不是在逻辑任务里实时查询,而是在运动任务内部统一刷新到全局状态区,逻辑任务直接读这个区域,避免直接读轴对象内部变量。第二步,启用"指令握手":工艺层发出新指令后,必须以运动执行层返回的执行确认信号为准,而不是以某个 BOOL 量自己推断。
这个坑告诫我:分层之后,层间通信的数据一致性绝不能靠运气,必须在架构层面定义好谁写、谁读、何时刷新。
5.2 急停复位后必须强制回零:不能把安全过程交给现场随意改写
急停是运动控制设备里最容易出问题的环节。硬件急停按下后,伺服使能通常会断开,机械轴会因负载惯性继续滑行一小段,此时控制器记录的"绝对位置"其实已经和机械真实位置偏离了。
如果急停复位后直接重新使能,继续按原来的坐标跑,轻则定位不准,重则撞到机械限位。运动控制设备里,位置数据一旦在执行急停后失真,必须在再次允许自动运行之前重新回零或重新标定位置。
我见过一些项目里把"急停复位后是否回零"做成了 HMI 上的一个选择开关,操作员嫌麻烦就关掉,结果设备在加工工件时全部报废。在框架里,我把这项做成了强制逻辑:急停信号有效一次,轴位置有效标志立即清除,自动运行的条件必须包含"位置有效"标志。HMI 只能查看状态,不能绕过这个条件。
注意区分 MC_Stop 和硬急停。MC_Stop 是程序控制下的停止指令,轴仍然使能;硬急停是安全回路断开,通常还需要 PLC 侧在复位后重新执行使能流程。框架里要分别处理,不能混为一谈。
5.3 在线下载与程序更新:运动总线断开的连锁反应
CODESYS 支持在线修改工程并下载,这在纯逻辑项目里很常用。但多轴运动控制项目里要格外小心,因为工程下载会重新初始化用于运动控制的库实例,运动总线对应的驱动映射可能被重置,轴使能会断开,设备会突然停机。
现场调试时候,如果是一边跑自动流程一边在线下载,下载完成那一刻所有轴失去使能,设备可能在重力作用下直接下滑,非常危险。
我的做法是:项目交付时在操作手册里明确规定,运行中的设备必须先下急停、设备完全停止、伺服允许信号关闭,之后才能进行程序下载。开发阶段尽量统一在仿真环境里验证逻辑层改动,不要总在实机上在线改。
此外,下载之前要确认运动执行层的库没有做会影响轴映射的底层改动。如果只是改工艺层的流程代码,轴配置部分不受影响;一旦改了轴配置或者库引用,下载后必须重新完整回零。
5.4 虚拟轴仿真通过,为什么一上实机就撞限位
最后一类常见问题是仿真和实机不一致。虚拟轴没有限位开关,没有负载惯性,也没有驱动器电流限制。框架里用虚拟轴能把逻辑流程跑顺,但一下到实机就会出现:定位指令发出后轴冲过了软限位,因为虚拟轴的加速度值在实机上根本达不到;或者限位 IO 映射根本没有配置,PLC 不知道机械限位已经触发。
针对这个问题,框架里要求所有轴的软限位必须在轴配置里明确设置,并且在运动执行层增加一致性校验:切换实机模式时,程序启动就检查限位 IO 信号是否有效映射、软限位参数是否与配置表一致,不一致不允许使能。虚拟轴和实机轴之间的配置切换也通过一个全局配置开关完成,不修改代码本身。
6. 框架从能跑到好用的三条路径:调试顺序、参数收敛、迭代节奏
6.1 先打通最小闭环,再往上加功能
框架不是一天定型的。我第一次搭这个框架的时候,也犯过"想一步到位"的错误,花了一周把轴组、插补、凸轮全写进去,结果上机调试时连最基本的定位都没跑稳,排查问题还得在一大堆代码里翻。
后来的思路变了。先建立一个最小可用的闭环:一个轴使能、回零、点动、绝对定位,全部跑通。这个阶段只验证四件事——任务周期稳定、使能复位正常、定位精度达标、状态反馈正确。验证通过后再往上加第二个轴,然后加电子齿轮,最后才加插补和 CNC。每加一层,确认之前的功能没有退化,有配置位就保留,不要删除。
这样做的另一个好处是,基础层的稳定性得到了充分验证,后面出问题时很快能定位到是新加的功能有问题,而不是底层架构有缺陷。
6.2 参数收敛到配置区,不要在多个程序段里散落
设备调试的过程就是参数收敛的过程。位移、速度、加速度、软限位这些参数,一开始可以在程序里临时改,但框架稳定后,必须全部收敛到配置区或配方表里。
参数散落最典型的后果是"同一个数据两个名字"。比如某设备的加工速度,HMI 上传的参数名是 Speed,程序段里用的变量名是 fSpeed,配置区里又有一个 MaxSpeed。看上去差不多,实际一不一致全靠运气。
框架的参数管理我推荐三级结构:设备级参数(硬件特性如每圈脉冲数)放轴配置;机型级参数(工艺速度、软限位)放配方;运行级参数(用户临时调整的倍率)放 HMI 区。每一级都有自己的命名前缀,程序里不直接在运动指令里写字面量参数。
6.3 框架是长出来的,不是憋出来的
最后一个建议是关于心态的。一个成熟的多轴控制框架,通常不是在办公室里设计出来的,而是在两三个现场项目中不断踩坑、补丁、修正之后沉淀出来的。
第一个项目用这套框架,可能只用了它 60% 的能力,但已经比散写逻辑好用得多;第二个项目把它扩展到新设备类型,会暴露出原来接口设计不合理的地方,这时候再改;第三个项目,框架才真正变成"换机型只改配置、不动程序"的状态。
我在最后想分享一个体会:做逻辑与运动分离这件事,最大的障碍不是技术,而是总想"等设计完美了再动手"。实际上,CODESYS 里搭一个最基本的三层结构,把轴状态管理收拢到一个功能块里,只需要一两天。先让一个工艺跑起来,哪怕接口设计得不那么优雅,也比完全没有边界、未来越改越乱要好得多。框架的收益是用时间换回来的,越早开始,复利越明显。