做运动控制的朋友应该都经历过这样的场景。程序一行一行码完了,逻辑看着没毛病,MC_Power的Enable给了TRUE,MC_Home的Execute也给了TRUE,MC_MoveRelative的距离和速度都填好了,一上电,轴纹丝不动;或者更邪门,轴动了,但不管你怎么发相对运动指令,它都往一个方向跑;再或者,轴一使能就直接往限位方向冲,吓得人手忙脚乱去按急停。
这些问题的根子,绝大多数不在参数配错,而在三个功能块的调用顺序没有按照PLCopen的轴状态机规则来。MC_Power负责把轴的功率级打开,MC_Home负责建立坐标系,MC_MoveRelative负责在这个坐标系里做相对位移,它们之间不是三个独立的功能块排排坐,而是一条必须严格递进的因果链:先使能、等确认、再回零、等完成、才能运动。把这条链用ST的状态机代码串起来,就是标题里说的"状态机串联"。
这篇文章我会把这套链条从头到尾拆开讲。先讲PLCopen轴状态机的流转逻辑,再逐个拆MC_Power、MC_Home、MC_MoveRelative的接口细节和工程坑点,最后给出一份可直接套用的ST代码框架,附带调试实测中的时序问题和报警排查思路。适合刚接触运动控制、被"轴为什么不动"折磨过的工程师,也适合已经在用但想搞清楚底层状态机逻辑的同行。
1. 轴状态机不是文档里的装饰图:它定义了你所有指令的合法顺序
1.1 一条轴的完整生命周期
PLCopen为单轴定义了一套标准状态机。大多数运动控制库(无论是CODESYS生态、TwinCAT还是各家国产平台)都遵循这个模型,只是命名的细节略有出入。核心状态就这么几个:
- Disabled:轴被禁用,功率级关闭,任何运动指令在这个状态下基本都会被拒绝,或者报"轴未使能"之类的错误。
- Standstill:轴已使能,处于静止待命状态,坐标参考有效,可以接受MC_Home、MC_MoveAbsolute、MC_MoveRelative等指令。
- Homing:正在执行回零流程,轴可能正在低速找原点,这个状态下大多数非回零指令会被拒绝或导致CommandAborted。
- Discrete Motion:正在执行离散运动(相对移动、绝对移动、点动都属于这一类),指令完成后自动回到Standstill。
- ErrorStop:任何功能块报错后轴会进入错误停止状态,必须执行MC_Reset等复位操作才能退出。
这条链的关键在于:指令不是你想发就能发。每个MC功能块都有它前置的合法状态。用一句话概括:从Disabled到Standstill只能靠MC_Power拉起来,从Standstill到Homing只能靠MC_Home,从Standstill到Discrete Motion只能靠运动指令。
1.2 三个功能块各自管哪一段
我把这几个指令和状态机的对应关系列成表格,调试的时候对照着看非常直观:
| 功能块 | 合法前置状态 | 触发后状态 | 完成回调 |
|---|---|---|---|
| MC_Power(Enable=TRUE) | Disabled | Standstill | Enabled=TRUE |
| MC_Home(Execute上升沿) | Standstill | Homing | Done=TRUE,回到Standstill |
| MC_MoveRelative(Execute上升沿) | Standstill | Discrete Motion | Done=TRUE,回到Standstill |
| MC_Reset | ErrorStop | 按实现回到Standstill或Disabled | Done=TRUE |
这里有个容易忽略的点:MC_Home和MC_MoveRelative的前置状态都是Standstill。也就是说,你必须在MC_Power的Enabled输出变成TRUE之后,才能发回零指令;在回零的Done没到之前,不能发任何运动指令。不然轻则指令被丢弃,重则回零被中途打断(CommandAborted),轴停在半路,你再发啥它都处于一种说不清的状态。
1.3 不看状态机的典型翻车现场
我见过最多的三种错法。第一种是梯形图画串并行:把MC_Power、MC_Home、MC_MoveRelative三行指令全放在同一个扫描周期里执行,觉得"反正程序从上往下扫,总会按顺序来"。问题是运动控制功能块内部有自己的状态机,同一周期内你让轴同时使能、回零、移动,库根本分不清谁先谁后,最终结果就是某条指令被忽略,或者后一条把前一条中止掉。
第二种是使能后不等Enabled直接发回零。MC_Power的Enable从TRUE到轴真正进入Standstill,中间有功率级建立的时间,可能是几十毫秒到几百毫秒。你在这个窗口期发MC_Home,大多数库直接报错,少数库会默默把指令吞掉,之后怎么都等不到Done,程序卡死在回零状态。
第三种最隐蔽:回零没完成就发绝对运动指令。有些平台的绝对运动指令在没有完成回零时会按"机械坐标"执行,而此时轴的坐标还是上电后的默认值(通常为0),指令一去不复返,轴直接往极限位置跑。这种问题一旦出现就是撞机级别的事故。
2. MC_Power使能细节:电平触发、状态判定与掉使能行为
2.1 接口拆解
MC_Power这个功能块看起来是几个BOOL管脚,实际用起来比表象复杂。标准的PLCopen接口如下:
- Axis:轴引用,这是功能块的第一个参数,必须绑定到已经配置好的轴对象上,很多人调试半天发现指令发不出去,最后才意识到轴引用根本没绑对。
- Enable:电平型输入,TRUE表示持续请求使能,FALSE则请求掉使能。注意它是电平信号,不是上升沿,你必须一直给它TRUE才能维持使能状态。
- EnablePositive / EnableNegative:分别控制正方向和负方向使能。很多项目里这两个引脚需要单独接,缺一条会导致某个方向发指令时报"方向未使能"。
- Stop:停止输入,TRUE时会触发轴的停止流程(减速停车),但它和Enable不一样,Stop置TRUE再复位,轴并不会掉使能,只是停在那里。
输出方面最重要的三个:Enabled(轴已完成使能,即已进入Standstill可接受指令)、Busy(正在处理使能/掉使能请求)、Error/ErrorID(错误标志和错误码)。
2.2 使能完成怎么判定
这段内容的核心经验是:看Enabled,不要看Enable。你把Enable置TRUE之后,功能块的内部驱动、功率级建立需要时间,Busy可能处于TRUE,而Enabled还要晚一步。只有Enabled变为TRUE,才代表轴已经进入Standstill,可以安全地发下一级指令了。
有个细节值得注意:很多库在掉使能时,Enabled从TRUE变FALSE的过程也伴随Busy。如果你在掉使能过程中发运动指令,同样会被拒绝。所以状态机里"等使能完成"和"等掉使能完成"是两个对称的等待条件,都要写。
另外,部分平台还提供读轴状态的辅助功能块(比如MC_ReadAxisStatus之类的接口),能把Standstill、Homing、Discrete Motion这些状态直接读出来。使能完成后建议顺手读一下轴状态确认确实是Standstill,而不是停留在某些异常状态,这比单纯看Enabled更保险。
2.3 掉使能时飞行中的轴会发生什么
这是新手容易忽略的坑。Enable从TRUE变FALSE,轴如果正在运动,不同库的处理策略不一样,常见的有两种:一种是立即切断功率,轴靠机械制动或惯性滑行(危险,容易冲出位置);另一种是触发停止流程,等轴停稳后再掉使能(安全)。我这边的经验是,生产设备上绝对不要让轴在运动过程中随意掉使能,停机逻辑应该走MC_Power的Stop输入或者单独的运动停止指令,等轴真正停到Standstill再断开使能。这个习惯能帮你避免一堆莫名其妙的跟踪误差和撞机事故。
注意:Stop信号通常也是电平型的。Stop置TRUE轴会停车并保持使能,Stop复位后轴回到Standstill,可以继续接受指令。这比掉使能再重新使能要快得多,是单轴往复运动场景下的首选停机方式。
3. MC_Home回零:坐标系建立过程中的模式、时序与失败处理
3.1 回零模式选择
MC_Home的HomingMode决定了轴用什么方式找原点。常见的模式包括:
- 找原点开关加Z相:轴先低速朝一个方向找原点开关,找到后再反向低速找编码器Z相脉冲,以Z相当作精确零点。这是绝大多数伺服轴的标准做法,精度最高。
- 仅找原点开关:碰到开关沿就算回零完成,精度取决于开关的重复性,适合要求不高的场合。
- 当前位置设零:轴不动,直接把当前坐标设为Position参数值。适用于已经有人为校过零位的轴,或者非必须回零的场合。
- 找限位开关:轴朝限位方向运动,撞到限位后反向找原点开关,常用于原点开关不在行程中间、需要先跑到端头再折返的机构。
选择模式的核心依据是机构上装了哪些传感器。原点开关装在行程中间就用前两种,装在边上就要用带限位折返的模式。这块最好在设备机械设计时就定下来,因为回零路径直接关系到调试效率。
Position参数也容易被忽略。它表示回零完成后,把当前位置标记为哪个坐标值。多数项目默认设为0,但如果你有多段流水线,每段轴的机械原点可能对应不同的工件坐标系坐标,这里可以直接填一个偏移值,省掉后续的坐标补偿。
3.2 回零的触发与Done握手
MC_Home的Execute是上升沿触发。回零指令发出后,Busy变为TRUE,轴开始执行回零动作;回零完成后Busy变FALSE,Done输出一个脉冲(或者一个周期的高电平,取决于库的实现)。这个Done信号不会自己维持,你必须在它出现的那个扫描周期里抓住它,并且把Execute复位。
我见过不少新手在这里犯的错:Execute保持TRUE,然后每个扫描周期都检查Done,想当然地以为"Execute一直是TRUE,回零完了Done也一直是TRUE"。但实际操作是,回零完成后,如果你不把Execute拉低,功能块无法完成握手复位,部分库会认为你要再次触发回零,或者干脆让Done消失,程序永远等不到那个信号。正确写法是:状态机进入回零状态时置Execute=TRUE,检测到Done上升沿的同一周期内立刻把Execute清FALSE,然后切到下一状态。
3.3 回零失败怎么办
回零失败太常见了。最常见的原因是原点开关信号没接对、开关坏了,或者回零方向选反,轴一路跑到限位都没碰到原点。这时候轴会进入ErrorStop,或者一直Busy下去,表现是程序卡在回零状态,轴低速顶着限位不动,电机嗡嗡响。
工程上必须给回零加超时保护。我一般在回零启动时同时启动一个TON定时器,时间设定为"从当前机械位置到最远端可能原点的最大行程 ÷ 回零低速速度 × 1.5",几秒到十几秒不等。超时未完成立刻终止回零流程,置错误状态,在HMI上报"回零超时,请检查原点开关"。这个保护看似简单,实际能省掉大量现场蹲守排查的时间。
另一个细节:回零中途如果误发了其他运动指令,MC_Home会被CommandAborted,轴停在半路,坐标可能已经被写成了乱七八糟的值。所以回零状态下的互锁非常关键,任何旋钮、按钮、上位机指令想打断回零,都应该走"先取消回零、再进入停止状态、确认坐标有效后再重新回零"的流程,而不是直接怼一条运动指令进来。
4. MC_MoveRelative相对运动:参数语义与命令仲裁规则
4.1 参数逐个说清楚
MC_MoveRelative在PLCopen里的关键参数有这几个:
- Distance:相对位移量,单位是轴的用户单位(毫米、度等)。正负号表示方向。
- Velocity:目标速度。注意有的库传的是恒速段的速度,有的库传的是最大速度(包含加减速过程的速度规划上界),读库文档时要分清。
- Acceleration / Deceleration:加速段和减速段的加速度,单位通常是mm/s²。很多库允许填0,表示使用轴配置里的默认值。
- Jerk:加加速度,即加速度变化率。老库不支持,新库和高端伺服一般都有;如果轴运动时有明显顿挫,可以在这里做S型加减速平滑。
- Direction:方向枚举,有正向、负向、当前方向等。取当前方向时,Distance的正负才有意义;如果你显式指定了方向,又给了符号相反的Distance,部分库会直接报参数错误。
还有一个参数层面的经验:相对运动的速度和加速度在连续往复定位时,加减速阶段占整个运动周期的比重很大。如果设备节拍不够,与其盲目提高Velocity,不如检查Acceleration和Jerk设置,很多时候平滑加减速之后整体节拍反而更快,因为冲击减小了,伺服跟踪性能更好。
4.2 为什么必须等Home的Done
这个问题的答案是状态机强制的:MC_MoveRelative的前置状态是Standstill,而完成回零是轴从Homing回到Standstill的必要条件。从工程逻辑上讲也说得通:没有回零,轴的坐标参考是"上电默认值",虽然相对运动理论上只需要一个可重复的起点,但你对"当前位置"没有定义,任何跟位置相关的判断(比如限位检查、软限位判断)都是失真的,库干脆不允许你在这种状态下做运动。
所以串联顺序就是铁律:MC_Power的Enabled为TRUE → 执行MC_Home → 等待Done → 这时轴才具备执行MC_MoveRelative的资格。
4.3 运动中来了新指令怎么办
PLCopen里的MC_MoveRelative默认是非缓冲执行,也就是说,如果你在轴还在运动(Active=TRUE)时,给MC_MoveRelative的Execute一个新的上升沿,库会中止当前运动(触发当前指令的CommandAborted),然后立刻开始执行新指令。这在连续定位场景下是个大坑——如果你用模式开关切换目标位置,逻辑上可能想让轴"走完当前这段再走下一段",但非缓冲模式下它会直接打断当前运动,从中间位置重新规划轨迹,导致加工面上出现明显的停顿接痕。
正确的连续运动做法有两个方向:一是等Done再发下一条,代码里就是"当前指令Done后,Execute清零,再在下一状态置Execute=TRUE",这是最简单也最可控的;二是使用带缓冲区模式的功能块(很多库提供带Buffered/Aborting模式参数的绝对运动指令),让新指令在前一条的终点衔接,轨迹连续无停顿。如果你的库不支持缓冲模式,老老实实等Done,不要贪快。
注意:一条MC_MoveRelative指令完成后,轴的最终坐标等于起点坐标加上Distance,而且轴回到Standstill。你可以在Done后的同一周期里读取轴的当前位置做验证,这是排查"轴为什么没走到位"最直接的手段。
5. 三段式状态机串联的ST代码:从R_TRIG到CASE的完整实现
5.1 为什么必须用上升沿而不是布尔赋值
很多初学者写ST,上来就是MC_Home.Execute := TRUE;然后就完了。问题在于,MC_Home的Execute是边沿触发,如果你在每个扫描周期都给它TRUE,它只会在第一次边沿时触发一次,之后一直保持TRUE并不会重复触发。这本身不是问题,但当你需要在"回零完成后再回零"(比如设备每次启动都回零)时,就麻烦了:你没办法用一个脉冲去"再次触发"。正确做法是给Execute信号做边沿控制,先把外部请求做成R_TRIG脉冲,再在合适的状态里把Execute置TRUE,完成后再清FALSE。
R_TRIG的标准用法是:定义一个R_TRIG实例,把外部按钮或上位机指令作为CLK输入,Q输出就是一个仅持续一个扫描周期的脉冲。这个脉冲用来触发状态机的切换,而功能块的Execute信号在状态机内部管理,两者互不干扰。这也是"状态机串联"和"直接调用功能块"的本质区别:状态机负责管"什么时候干什么",功能块负责"干这件事"。
5.2 一份可直接套用的CASE状态机代码
下面这段ST代码是我在多个项目里复用过的骨架,去掉了具体轴名和品牌,逻辑可直接移植。引脚名称在不同平台略有差别(比如有的平台叫bEnable,有的叫Enable),对应调整即可。
VAR nState : INT := 0; nPrevState : INT := 0; xStartCmd : BOOL; // 外部启动按钮 xMoveRequest : BOOL; // 外部相对运动请求 xResetRequest : BOOL; // 外部复位请求 rMoveDistance : LREAL := 100.0; rMoveVelocity : LREAL := 50.0; triStart : R_TRIG; triMove : R_TRIG; triReset : R_TRIG; fbPower : MC_Power; fbHome : MC_Home; fbMove : MC_MoveRelative; fbReset : MC_Reset; tonHome : TON; tonMove : TON; xHomeTimeout : BOOL; xMoveTimeout : BOOL; END_VAR主逻辑放在程序段里:
triStart(CLK := xStartCmd); triMove(CLK := xMoveRequest); triReset(CLK := xResetRequest); // 状态切换时统一复位定时器 IF nState <> nPrevState THEN tonHome(IN := FALSE, PT := T#5S); tonMove(IN := FALSE, PT := T#10S); nPrevState := nState; END_IF CASE nState OF 0: // 空闲待机 fbPower.Enable := FALSE; fbHome.Execute := FALSE; fbMove.Execute := FALSE; IF triStart.Q THEN nState := 1; END_IF 1: // 请求使能并等待 fbPower.Enable := TRUE; fbPower.EnablePositive := TRUE; fbPower.EnableNegative := TRUE; IF fbPower.Enabled THEN nState := 2; ELSIF fbPower.Error THEN nState := 99; END_IF 2: // 触发回零并等待完成 fbHome.Execute := TRUE; tonHome(IN := TRUE, PT := T#5S); xHomeTimeout := tonHome.Q; IF fbHome.Done THEN fbHome.Execute := FALSE; nState := 3; ELSIF fbHome.Error OR xHomeTimeout THEN fbHome.Execute := FALSE; nState := 99; END_IF 3: // 等待外部运动请求 fbHome.Execute := FALSE; fbMove.Execute := FALSE; IF triMove.Q THEN nState := 4; END_IF 4: // 执行一次相对移动 fbMove.Execute := TRUE; fbMove.Distance := rMoveDistance; fbMove.Velocity := rMoveVelocity; fbMove.Acceleration := 100.0; fbMove.Deceleration := 100.0; tonMove(IN := TRUE, PT := T#10S); xMoveTimeout := tonMove.Q; IF fbMove.Done THEN fbMove.Execute := FALSE; nState := 3; // 回到待机,可再次请求 ELSIF fbMove.Error OR xMoveTimeout THEN fbMove.Execute := FALSE; nState := 99; END_IF 99: // 错误处理,等待复位 fbPower.Enable := FALSE; fbHome.Execute := FALSE; fbMove.Execute := FALSE; IF triReset.Q THEN fbReset.Execute := TRUE; IF fbReset.Done THEN fbReset.Execute := FALSE; nState := 0; // 复位完成后重新走流程 END_IF END_IF END_CASE这段代码有几个细节值得说明。状态0里把三个功能块的Enable/Execute全部清FALSE,是为了保证上电后所有功能块处于一个干净的初始状态。状态1里Enable和EnablePositive/EnableNegative一起置TRUE,避免方向使能缺失。状态2里回零和超时定时器同时启动,目的是给回零设一个硬性时间上限。状态3是一个"等待指令"的空转态,正因为有了这个状态,MC_MoveRelative每次触发都是一个干净的上升沿,不会出现连续运动时的重触发问题。
5.3 超时计时器的一个小坑
TON的IN输入必须是电平,我这边在状态2和状态4里把IN接到TRUE,同时依赖"状态切换时统一复位"这个逻辑来清零。如果你忘了那段"状态切换时统一复位定时器"的代码,TON会在离开这个状态后仍然保持计时,下次进状态时Q可能直接就是TRUE,导致误报超时。这是我在现场踩过的坑,代码注释里专门标了出来。
5.4 上升沿信号在状态机里怎么过渡
你可能会问:triMove.Q这个脉冲只持续一个扫描周期,状态3里捕捉到它切到状态4,那状态4里triMove.Q早就没了,fbMove.Execute靠谁来置TRUE?答案是,fbMove.Execute在状态4里是直接赋TRUE的,它不依赖外部脉冲,只依赖"当前状态是4"这个事实。外部脉冲的作用仅仅是触发状态切换,一旦切进来,功能块的执行信号完全由状态机接管,这就是状态机和直接调用的另一个区别:指令的执行权在状态机手里,不在外部信号手里。
6. 实测调试笔记:报警排查、Done脉冲宽度与超时设计
6.1 三个高频报警的排查链路
先说"轴未使能就发指令"这类报警。表现形式通常是MC_Home或MC_MoveRelative的ErrorID返回一个"轴未使能"或"不允许在当前状态执行"的代码。排查链路由三句组成:一是查MC_Power.Enabled是否真的为TRUE(不是查Enable输入),二是查轴当前是否处于Standstill而不是ErrorStop,三是查你的状态机是否在上一个等待条件没满足时就提前切到了下一个状态。前两个查库,第三个查你自己的程序逻辑,多半问题出在第三个。
再说CommandAborted。这个报警本身不是"故障",而是库在告诉你"有别的指令插进来把当前指令打断了"。排查时先分清是谁打断了谁:是不是状态机在等待Done期间又给同一个轴发了其他指令?是不是上位机在回零过程中点了一下启动?是不是定时器误报触发了错误复位流程?解法是靠状态机内部的互锁逻辑把各种指令分隔开,一条轴在同一时刻只允许一个运动源持有控制权。
最后说ErrorStop卡死。轴进入ErrorStop后,MC_Reset是唯一的出口,但很多人忽略了一个关键顺序:必须先把导致错误的根因消除,再执行MC_Reset。比如回零超时是因为原点开关坏了,你换了开关不按复位,轴还是ErrorStop;你按了复位但开关还是坏的,下次回零照样超时。正确的流程是:消除根因(修信号、改参数)→ MC_Reset复位状态机 → 重新走"使能-回零-运动"的完整链路。特别注意,ErrorStop状态下直接置MC_Power.Enable=TRUE不一定能拉出这个状态,必须先Reset。
6.2 关于Done信号宽度的实测经验
不同品牌库的Done信号宽度不一样,有的只在指令完成后的第一个扫描周期拉高一个周期,有的会一直保持TRUE直到Execute复位,还有的在Execute保持TRUE的情况下会周期性重复拉高。我实测过几套库,最舒服的是"Done在Execute复位后变FALSE"这种实现,因为它给了你足够的周期窗口去锁存。遇到"Done只有单周期脉冲"的库,必须在状态机的同一扫描周期里同时完成"抓到Done → 清Execute → 切状态"三个动作,代码里就要写成状态2里看到fbHome.Done就立刻把Execute清掉再转移,不能隔一个周期再清。
6.3 我的几个习惯性写法
第一,所有运动相关功能块集中放在同一个程序组织单元里,统一放在状态机里调度,绝不在HMI、上位机、梯形图里分散调用。第二,每个运动指令都配一个TON超时,超时时置错误状态并复位相关输出,宁可误报停机也不要让轴傻跑。第三,在状态机的进入和退出处加调试变量(nState、nPrevState),在线监控状态跳转,出问题看状态码比看功能块的ErrorID直观得多。
这些习惯不是从文档里学的,是一趟趟现场调试熬出来的。状态机串联这件事,本质上不是代码技巧,而是让你对"轴现在处于什么状态、下一步合法动作是什么"始终保持一个清晰的预期。把这份预期固化成代码结构,运动控制项目的稳定性会立刻上一个台阶。