简介:面向工业自动化领域的PLC工程师与调试人员,这份模板程序聚焦三菱FX5U系列PLC的轴控制开发,使用ST(结构化文本)语言编写了可复用的功能块(FB),覆盖原点复归、点动和定位运动三大核心功能。FB块支持多轴重复调用,能显著减少重复编程工作量,适合多轴运动控制系统,如自动化生产线、机器人手臂和精密装配设备的快速构建与调试。压缩包共8个文件,容量990KB,主要包含HTML说明文档、TXT示例代码及6张程序截图,配合源码目录可完整展示FB块的实现思路与调用方式。已有1069人学习下载,适合具备一定PLC基础、希望提升运动控制编程效率的工程师参考。通过这份模板,读者可直接理解用ST语言组织轴控制逻辑的方法,学习FB块封装与多轴实例化的实践技巧,并参照文档和截图快速移植到实际项目中,降低从零编写底层运动控制代码的难度。
1. 为什么说 FX5U 的轴控代码值得封装成 ST 写的 FB 块?
做非标自动化的人大概都有这种经历:设备上有 4 根轴,每根轴的逻辑几乎一样,无非是原点复归、点动、定位三个动作,可梯形图里就得为每根轴复制一遍程序,改软元件地址改到眼花。某次项目里我手上有两根轴,因为复制时漏改了一个 D 地址,轴直接朝反方向顶过去,幸亏急停按得快,不然工件和气缸都要报废。那之后我就把轴控制逻辑全部收进 FB 块,用 ST 语言写成统一的轴功能块,每加一根轴就是声明一个实例、传一组参数,梯形图那边只剩调用,再也不碰重复的定位逻辑。
三菱 FX5U 这类小型 PLC 做多轴定位控制时,最头疼的不是指令不会用,而是重复代码一多,维护和排错都变成体力活。用 ST 语言编写轴 FB 块,把原点复归、点动、定位运动封装成标准接口,最大的价值在于:一个项目里轴数越多,节省的编程量越明显,而且每根轴的行为完全一致,调试时只需要盯着 FB 内部的状态变量看,不用满屏找软元件。这篇文章面向的是用 FX5U 做单机设备、需要控制 2 到 8 根轴的工程师,以及想把手里的轴控程序做成可复用模板的同行。看完你可以直接照这个结构写自己的轴 FB,也能避开我踩过的那些坑。
2. 轴 FB 块的设计思路:从定位指令到参数化接口
2.1 ST 语言为什么适合写轴控制 FB,梯形图差在哪
很多工程师的习惯是梯形图里拖 ZRN、DRVI、DRVA 这些定位指令,一个动作一行,看着直接。但一旦动作变多,梯形图的问题就暴露出来:动作之间的互锁、状态切换、完成信号保持,都需要靠辅助继电器一点点堆,轴多了以后梯形图就是一张蜘蛛网。ST 语言更适合的原因是它有完整的编程结构:IF、CASE、FOR 循环、函数封装,可以把一个轴的全部行为浓缩成一个状态机。你在 ST 里写“当前处于什么状态、哪些条件触发状态切换、每个状态下执行什么指令”,读程序的人一眼能看明白,不像梯形图那样需要顺着辅助继电器一路查。
另一个实际原因是 GX Works3 对 ST 的支持已经很成熟,FB 内部可以用 ST 写,外部调用时可以按功能块拖拽,也可以按 ST 语句调用。这样程序架构可以做成“底层 FB 用 ST、上层组织用梯形图或 ST 混排”,兼顾团队里不同习惯的人。FB 块内部用的变量都是私有的,不占用全局软元件;实例与实例之间天然隔离,这就是多轴重复的基础。梯形图里做复用只能靠复制粘贴,然后祈祷别改漏地址,这是本质差别。
2.2 FB 接口设计:VAR_INPUT、VAR_OUTPUT、VAR_IN_OUT 怎么分工
轴 FB 块能不能被多轴重复用,关键在接口设计。我的习惯是把所有和“轴工艺”相关的量全部做成引脚,而不是让 FB 内部直接读写全局软元件。VAR_INPUT 放的是命令类信号:使能、原点复归请求、点动正/负、定位启动、目标位置、速度、加减速时间、轴号。VAR_OUTPUT 放的是状态类信号:空闲、回原点中、点动中、定位中、完成脉冲、报警。VAR_IN_OUT 放的是需要和外部共享的软元件区间,比如当前位置、当前速度这些需要触摸屏实时读取的量。
接口设计的一个原则是:所有动作请求都设计成“上升沿触发”而不是“电平触发”。电平触发的问题是,触摸屏上一个按钮如果忘了复位,FB 就会反复执行同一动作,设备可能冲出限位。上升沿触发需要 FB 内部自己存上一次的输入状态,ST 里写一个边沿检测就是两行的事,但能避免大量现场问题。另一个原则是速度、位置这类参数用 DINT 类型,单位是脉冲数和脉冲/秒,不要在 FB 内部做单位换算。单位换算放触摸屏或者上位机做,FB 只管脉冲层面,这样轴参数标定只在触摸屏一侧完成,不会因为 PLC 程序下载把标定值冲掉。
2.3 为什么参数要“每轴一套”而不是全局变量
新手写 FB 最容易犯的错:内部直接引用 D8140、D8340 这类全局定位软元件,结果两个实例同时运行时数据互相覆盖。FB 的 VAR 区变量是每个实例独立的,这才是多轴复用的前提。轴号 iAxisNo 作为输入参数传入后,FB 内部通过自带的轴控制指令完成脉冲输出,而不是自己去读写全局脉冲寄存器。这样每根轴的当前位置、目标位置、速度都在各实例内部管理,互不干扰。
我一般会在 FB 内部定义两个私有变量:diCurPos 和 diTmpSpeed,分别记录当前脉冲位置和中间速度值。这两个值只在 FB 内部使用,外部想读,通过 VAR_IN_OUT 引出来。这样即使后期要换轴卡、换驱动器,FB 内部逻辑不需要动,只改硬件映射层。参数“每轴一套”的本质,是把轴的差异隔离在实例参数里,而不是散落在程序各处。
3. 用 ST 把原点复归、点动、定位写进一个 FB:代码与参数说明
3.1 状态机骨架:一个 FB 里管理三种运动模式
一个轴 FB 内部最核心的就是状态机。我用 0、1、2、3 四个状态:0 空闲、1 原点复归中、2 点动中、3 定位中。任何时刻只有一个状态生效,进入状态前先做互锁判断,这样就不会出现“又回原点又定位”的冲突。
FUNCTION_BLOCK FB_Axis VAR_INPUT bEnable: BOOL; // 轴使能,TRUE 才响应动作请求 bHomeReq: BOOL; // 原点复归请求,上升沿触发 bJogP: BOOL; // 正向点动,电平信号 bJogN: BOOL; // 反向点动,电平信号 bMoveReq: BOOL; // 定位启动,上升沿触发 diTargetPos: DINT; // 定位目标位置,单位:脉冲 diSpeed: DINT; // 定位速度,单位:脉冲/秒 diJogSpeed: DINT; // 点动速度,单位:脉冲/秒 diAccTime: DINT; // 加速时间,单位:毫秒 diDecTime: DINT; // 减速时间,单位:毫秒 iAxisNo: INT; // 轴号:1~4,对应 FX5U 高速脉冲输出通道 END_VAR VAR_OUTPUT bIdle: BOOL; // 空闲状态 bHoming: BOOL; // 正在原点复归 bJogging: BOOL; // 正在点动 bPositioning: BOOL; // 正在定位 bDone: BOOL; // 当前动作完成(一个扫描周期) bError: BOOL; // 报警输出 END_VAR VAR eState: INT := 0; // 0 空闲,1 回原点,2 点动,3 定位 bHomeEdge: BOOL := FALSE; // 回原点请求上升沿缓存 bMoveEdge: BOOL := FALSE; // 定位请求上升沿缓存 bJogActive: BOOL := FALSE; // 点动激活标志 END_VAR这段接口定义里,bEnable 是总闸,所有动作只有在使能为 TRUE 时才响应。iAxisNo 是轴号,在 FX5U 上对应高速脉冲输出通道的编号,多轴实例就是靠它区分物理轴的。diTargetPos、diSpeed、diJogSpeed 都用 DINT,因为 FX5U 定位指令的软元件本身就是 32 位整数,用 DINT 可以避免转换误差和符号问题。bDone 设计成脉冲信号,只持续一个扫描周期,这样上位机可以按一次完成信号抓一次,不会重复计数。
3.2 状态切换逻辑:上升沿检测与互锁
状态切换是整个 FB 的大脑。我在 ST 里用 CASE 语句处理,每个状态只做自己该做的事,出口条件清晰。
// 上升沿检测:只有检测到请求信号从 FALSE 变 TRUE 才触发 bHomeEdge := bHomeReq AND NOT bHomeEdgePrev; bMoveEdge := bMoveReq AND NOT bMoveEdgePrev; bHomeEdgePrev := bHomeReq; bMoveEdgePrev := bMoveReq; CASE eState OF 0: // 空闲状态 bIdle := TRUE; bHoming := FALSE; bJogging := FALSE; bPositioning := FALSE; bDone := FALSE; IF bHomeEdge THEN eState := 1; ELSIF bMoveEdge THEN eState := 3; ELSIF bJogP OR bJogN THEN eState := 2; END_IF; 1: // 原点复归中 bHoming := TRUE; bIdle := FALSE; // 执行原点复归指令,轴号由 iAxisNo 指定 // 指令调用方式以 GX Works3 中实际选用的定位指令为准 IF bHomeDone THEN eState := 0; bDone := TRUE; END_IF; 2: // 点动中 bJogging := TRUE; bIdle := FALSE; IF bJogP OR bJogN THEN // 保持点动输出,速度取 diJogSpeed ELSE // 停止点动输出 eState := 0; END_IF; 3: // 定位中 bPositioning := TRUE; bIdle := FALSE; IF bMoveDone THEN eState := 0; bDone := TRUE; END_IF; END_CASE;这里有个很容易忽视的细节:在空闲状态判断里,我把原点复归的优先级排在最前面,其次定位,最后才是点动。原因是回原点和定位都是完整运动过程,如果点动信号一直按着,优先级高会让其他指令永远得不到执行。点动作为一种手动操作行为,放在最后是合理的。bHomeDone 和 bMoveDone 是内部标志,表示系统定位指令执行完成,由调用具体定位指令后的完成信号写入,下面小节会说明。
3.3 把 FX5U 定位指令接进 FB:指令调用与完成信号处理
状态机定好了,接下来要解决“谁去真正发脉冲”。FX5U 的原点回归、定位指令在 ST 里可以直接调用,操作数包含目标位置、速度、加减速、轴号等。调用完后,指令会输出一个“定位结束”的位软元件,FB 把它映射到内部的 bHomeDone/bMoveDone 上。
// 原点复归指令调用示意(GX Works3 ST 编辑器) // 指令名和操作数个数以所用定位指令为准,这里给出调用骨架 IF eState = 1 THEN // 原点复归:速度、加减速、轴号通过 FB 输入引脚传入 // 执行后,将该指令的结束标志位赋值给 bHomeDone END_IF; IF eState = 3 THEN // 绝对定位或相对定位:目标位置、速度、加减速、轴号 // 执行后,将该指令的结束标志位赋值给 bMoveDone END_IF;注意一点:FX5U 的定位指令分为脉冲输出型和伺服定位型,脉冲输出型适合步进和脉冲型伺服,伺服定位型适合通过内置定位表运行的场景。这个 FB 面向的是脉冲输出型,即 PLC 发脉冲给驱动器,驱动器负责电流环和位置环。如果项目用的是带定位模块的扩展设备,接口要重新设计,但状态机架构可以直接复用。完成信号不要直接用系统完成标志去触发状态切换,最好在 FB 内部做一个去抖处理,连续两个扫描周期都是完成状态才真正切回空闲,避免现场干扰造成误判。
3.4 参数怎么设:一张表说清接口和推荐值
参数设置直接决定这个 FB 好不好用,我把常用参数整理一张表,方便在 GX Works3 里录入 FB 时对照。
| 参数名称 | 类型 | 典型值 | 说明 |
|---|---|---|---|
| bEnable | BOOL | TRUE | 总使能,急停时整组复位 |
| bHomeReq | BOOL | 按钮上升沿 | 触摸屏或物理按钮触发 |
| bJogP / bJogN | BOOL | 电平信号 | 按住动,松开停 |
| bMoveReq | BOOL | 按钮上升沿 | 触发一次执行一次定位 |
| diTargetPos | DINT | 0~2147483647 | 绝对位置,单位脉冲 |
| diSpeed | DINT | 根据机械节拍 | 定位速度,建议不超过额定 80% |
| diJogSpeed | DINT | 定位速度的 20% | 点动速度,设置高了危险 |
| diAccTime / diDecTime | DINT | 50~200ms | 加减速时间,伺服/步进不同 |
| iAxisNo | INT | 1~4 | 与 FX5U 高速脉冲输出通道对应 |
我一般把点动速度默认设为定位速度的 20%,因为点动是手动操作,速度太快会来不及停;加减速时间先给 100ms,等设备跑起来看振动和丢步情况再调。iAxisNo 是整数,声明实例时直接赋常量,比如 1、2、3,不要在运行中修改。
4. 多轴重复调用:实例化、程序架构与硬件映射
4.1 在程序中创建多个实例:一行声明一根轴
FB 写好后,多轴使用比想象中简单。在主程序 POU 的变量区声明多个实例,每个实例独立占用一份 FB 内部变量,互不干扰。在 GX Works3 里这样写:
VAR stAxis1: FB_Axis; // 1 号轴实例,对应硬件通道 1 stAxis2: FB_Axis; // 2 号轴实例,对应硬件通道 2 stAxis3: FB_Axis; // 3 号轴实例,对应硬件通道 3 END_VAR然后每个扫描周期调用一次每个实例,把实际的输入信号接上去。这里注意:FB 实例的调用和普通指令调用一样,写在程序中就会被执行,如果条件不满足,FB 内部自己保持状态,不会自动复位。所以调用前不用加额外的使能分支。
// 1 号轴调用示例 stAxis1( bEnable := bAxis1Enable, bHomeReq := bAxis1HomeBtn, bJogP := bAxis1JogP, bJogN := bAxis1JogN, bMoveReq := bAxis1MoveBtn, diTargetPos := diAxis1Target, diSpeed := diAxis1Speed, diJogSpeed := diAxis1JogSpeed, diAccTime := diAxis1Acc, diDecTime := diAxis1Dec, iAxisNo := 1, bIdle => bAxis1Idle, bHoming => bAxis1Homing, bJogging => bAxis1Jogging, bPositioning => bAxis1Positioning, bDone => bAxis1Done, bError => bAxis1Error );这个调用块看起来长,但内容全是信号对接,没有逻辑。设备上如果有 4 根轴,就复制三份,把轴号改成 2、3、4,把输入输出变量换成各自的名字。复制时唯一的注意点:iAxisNo 千万不要漏改,否则两根轴会同时被同一个通道驱动。我见过一次现场问题就是这个原因,设备两根轴对着顶,当时排查了很久。
4.2 硬件映射:轴号与 FX5U 高速脉冲输出通道的关系
FX5U 的高速脉冲输出通道是固定的,比如晶体管输出型的 FX5U 本体通常有 4 路高速脉冲输出,对应不同的输出端子。FB 里的 iAxisNo 要和硬件通道一一对应,方向信号、DOG 原点信号也要接到对应的接线端子上。我一般会让电气接线图和程序注释保持同一种命名规则:轴 1 对应输出 Y0、方向 Y1、原点传感器 X0;轴 2 对应 Y2、方向 Y3、原点传感器 X1。
一个容易踩的坑:原点复归的 DOG(近原点信号)如果接错通道,回原点时传感器信号根本没进 PLC,轴会一直走到硬限位才停。FX5U 的定位指令默认使用指定轴号对应的 DOG 输入端子,不会因为你把传感器接在别的 X 点上就自动识别。接线时务必对照硬件手册确认每个通道的 DOG 输入点编号,然后在程序注释里写清楚,方便以后维护的人查。
4.3 程序架构:把 FB 调用集中在轴控子程序中,保持扫描时间稳定
我习惯的做法是单独建一个子程序或者单独的 POU 放所有轴 FB 调用,专门叫“轴控制组”,主程序里只调用这个轴控 POU。这样做的原因:定位指令的执行对扫描周期有要求,如果 FB 分散在程序各处,中间隔了太多业务逻辑,定位指令的脉冲输出会受扫描抖动影响。
// 主程序中的调用 CALL 轴控制组; // 轴控制组内部 stAxis1(...); stAxis2(...); stAxis3(...); stAxis4(...);在轴控制组内部,FB 调用顺序尽量固定,不要穿插其他复杂运算。FX5U 高速脉冲输出的实际脉冲生成由 CPU 内部硬件定时器完成,不占用扫描周期,但指令的目标位置、速度写入时机由程序扫描决定,所以放在固定的调用位置能减少定位触发延迟抖动。另外,不要在中断程序中调用这个 FB,定位指令和中断冲突时偶发丢脉冲,排查起来非常费劲。
5. 轴 FB 块上机避坑:五个必须注意的排查点
5.1 现象:两个实例一运行,速度互相干扰
某项目里 2 号轴定位时经常偷停,查程序没有逻辑错误。最后发现 2 号轴的 FB 内部速度寄存器被 1 号轴的定位指令覆盖了。原因:我在定义 FB 时把速度参数用了 VAR 区,没有通过输入引脚传入,导致多实例共享了同一个存储区。解决:把所有速度、位置、加减速参数全部改成 VAR_INPUT,每个实例调用时从外部变量赋值,内部 VAR 只保留状态和边沿缓存。这条经验后来我写进了自己的检查清单:FB 里不允许出现直接赋值数值的 VAR,所有需外部调整的量全部做成引脚。
5.2 现象:原点复归每次都冲过 DOG 才停
回原点时轴飞速冲过原点传感器,继续跑一段才停。原因有两类:一类是原点复归速度设置太高,DOG 信号从 OFF 到 ON 的响应滞后导致过冲;另一类是 FB 里没有使用 FX5U 原点回归指令的“DOG 下降沿停止”模式,而是自己用 DOG 信号做停止判定,扫描周期内的信号延迟被放大。解决:原点复归直接用定位指令内置的 DOG 搜索功能,并把回原速度控制在 20mm/s 以下,爬行速度给到 5mm/s 再捕捉零点。不要试图用普通输入点自己判断 DOG 沿,那是最容易翻车的做法。
5.3 现象:定位完成信号只闪了一下,上位机没抓到
bDone 设计成单扫描周期脉冲后,上位机如果扫描频率低,或者中途程序被别的块占用,就会漏掉完成信号。解决:在 FB 输出中增加一个“完成锁定”信号,即 bDoneLatched,动作完成时置 TRUE,外部程序确认后通过复位引脚清除。FB 内部用 RS 触发器实现:
// 完成锁定信号 IF bMoveDone THEN bDoneLatched := TRUE; END_IF; IF bResetDone THEN bDoneLatched := FALSE; END_IF;这种锁存方式在触摸屏上用“完成状态指示灯”最合适,操作工看一眼就知道动作结束了,不用等脉冲信号。对自动流程控制来说,单扫描周期的 bDone 仍然保留,两个信号各司其职。
5.4 现象:ST 里写了 IF bHomeReq THEN,结果轴反复回原点
我最早写这个 FB 时,把原点复归请求当电平信号处理,结果触摸屏按钮一旦没有手动复位,每次扫描都触发回原点。后来发现,FB 里所有“启动类”动作请求必须做上升沿检测,不能直接 IF 判断。上升沿检测的原理很简单:把当前请求信号和上一次的缓存值做比较,只有 FALSE 变 TRUE 才触发。这个动作在 ST 里要放在 CASE 状态机前面,确保每个扫描周期只检测一次,否则状态机内部判断时会漏沿。具体写法在 3.2 节已经给出,核心是 bHomeReq AND NOT bHomeEdgePrev。
5.5 现象:定位中途修改速度无效,甚至导致定位停止
运行时通过触摸屏改 diSpeed,发现轴已经走完的位置不会变,没走完的位置直接停住。原因:定位指令启动后,速度参数只在启动瞬间读取一次,中途修改不会生效,部分指令还会因为参数意外变化触发异常停止。解决:如果需要在运动过程中变速,要使用专门的变速定位指令;普通定位场景下,把速度修改放在动作完成后下一次启动前。FB 内部我在状态机加了一个判断:eState 不等于 0 时,外部输入的速度值只缓存不写入定位指令,等回到空闲状态再更新。这样既避免了异常,又保证下一次动作用的是最新速度。
6. 进阶技巧:把轴配置和状态打包成 UDT,一个引脚传一整轴数据
多轴项目里 FB 引脚太多,调用时信号对接代码占了半个屏幕,看起来累,维护也费劲。我后来的做法是:定义一个用户自定义结构体 UDT,把每根轴的配置参数、命令位、状态位全部装进去,FB 的 VAR_IN_OUT 只留一个 UDT 引脚。这样触摸屏和 PLC 程序都可以直接对结构体成员赋值,调用代码缩短一半。
TYPE ST_AxisData : STRUCT // 配置区 iAxisNo: INT; // 轴号 diTargetPos: DINT; // 目标位置 diSpeed: DINT; // 定位速度 diJogSpeed: DINT; // 点动速度 diAccTime: DINT; // 加速时间 diDecTime: DINT; // 减速时间 // 命令区 bEnable: BOOL; // 使能 bHomeReq: BOOL; // 回原点请求 bMoveReq: BOOL; // 定位请求 bJogP: BOOL; // 正点动 bJogN: BOOL; // 负点动 // 状态区 bIdle: BOOL; // 空闲 bHoming: BOOL; // 回原点中 bPositioning: BOOL; // 定位中 bJogging: BOOL; // 点动中 bDone: BOOL; // 完成 bError: BOOL; // 报警 // 反馈区 diCurPos: DINT; // 当前位置 END_STRUCT END_TYPEFB 内部把原来的输入输出引脚全部删掉,只保留一个 ST_AxisData 类型的 VAR_IN_OUT,命名 axisData。所有命令从 axisData 读取,所有状态写回 axisData。程序里的调用变成:
// 轴 1 调用 stAxis1(axisData := stAxis1Data); // 轴 2 调用 stAxis2(axisData := stAxis2Data);触摸屏那边用 GX Works3 的全局标签直接映射 stAxis1Data.diTargetPos、stAxis1Data.bHomeReq,不用为每个引脚建标签。这个做法的好处是,轴数量增加时,主程序只需要声明一组新的 UDT 变量和一个 FB 实例,其他全部不动。我自己后来养成的习惯是所有轴参数集中在 UDT 里统一管理,每根轴一个数据块,回原点、定位、点动、报警全都能在一个结构里看到。这比满屏引脚清晰得多,也避免了前面说的“复制时漏改轴号”的问题。希望这个思路能帮到你。
本文还有配套的精品资源,点击获取