写PLC程序这几年,我最大的体会是:很多设备逻辑不复杂,但梯形图一多就乱,改一个动作要找半天触点,调试的时候谁都救不了你。后来我把程序里那些反复出现的逻辑全拆成了子程序,整个工程量直接从“一团毛线”变成了“抽屉柜”,每个抽屉拉开就是一块完整功能,互不干扰。这篇文章就围绕PLC里的程序控制指令,重点讲讲子程序(Subroutine)这套东西怎么用、为什么该用、以及我踩过哪些坑。
先说清楚一个概念:PLC的程序控制指令,是一类“改变程序走向”的指令的总称,不只是子程序。它包含条件跳转(CJ)、子程序调用(CALL)、子程序返回(RET)、循环控制(FOR/NEXT)、中断返回(IRET/DI/EI)这些。子程序是其中用得最多、也最能改善程序结构的一类。这篇文章适合刚入门想着把程序写规整的PLC新手,也适合已经在做非标设备、觉得梯形图越来越难维护的调试工程师。我会结合三菱FX3U、西门子S7-200 SMART、汇川H5U这三种主流PLC的实际指令,把子程序的调用机制、实操写法、常见故障全部过一遍。
1. 先认识整个程序控制指令家族:子程序到底“被谁调用、又往哪返回”
我一直觉得,理解子程序的前提是先把程序控制指令这个家族捋清楚。PLC虽然叫“可编程逻辑控制器”,但它的执行方式并不是像电脑那样“每次执行一行,遇到函数就跳走”,而是按照扫描周期,从左到右、从上到下把用户程序从头执行到尾。程序控制指令干的事,就是在这个固有扫描流程里“人为改变方向”。
常见控制指令分四类:第一种是跳转指令,三菱里是CJ Pxxx,西门子S7-200 SMART里是JMP,作用就是跳过中间的某段程序不执行。第二种是子程序调用指令,三菱是CALL Pxxx,西门子是CALL SBRx,作用是跳到子程序里执行,执行完再回到主程序的断点继续往下走。第三种是循环指令,三菱是FOR/NEXT,用于重复执行某段逻辑固定的次数。第四种是中断指令,比如三菱的EI/DI、IRET,它不是靠扫描周期来触发的,而是靠硬件事件(比如高速计数器、定时中断)触发,优先级比主程序高得多。
子程序指令在这四个类别里最特殊,因为它是唯一一个“既要跳出去、又必须跳回来”的指令。跳转指令跳完就不回来了,循环指令是在原地打转,中断指令干脆进入另一个独立的程序环境。只有子程序,调用完之后还要回到原来的位置,继续执行CALL指令下面的那一条。这个“返回断点”的机制,是子程序能够模块化的根基。你要是把这个机制理解错了,后面写多层嵌套的时候一定会乱套。
再往细了说,三菱的子程序调用指令有CALL、CALLP,还有FCALL这种所谓“文件调用”。这里的CALL是指定程序编号调用,P后面跟的是程序号,比如CALL P1就是调用程序号为1的子程序。CALLP则是上升沿触发方式的调用,只在条件从OFF变ON的那一个扫描周期里执行调用动作。FCALL更特殊一点,它允许在子程序内部再调用另一个子程序文件,但实际上在FX3U这类小型机上用得很少,绝大多数项目用CALL和CALLP就够了。
西门子的方式和三菱不太一样。S7-200 SMART里,主程序(MAIN)下面可以建很多子程序SBR_0、SBR_1……调用的时候直接用CALL指令加子程序名,比如CALL SBR_0, 也可以带参数。西门子的主程序和子程序的关系更像“主程序是总调度,子程序是被调度的工位”。它不像三菱那样有1~64的程序编号概念,而是用符号名来区分,这个在写复杂项目的时候反而更直观。
这里有个关键认知:子程序不是“复制粘贴的捷径”,而是“控制程序执行边界的规则”。说它控制执行边界,是因为PLC扫描周期的固有时序被你用CALL指令打断了。主程序执行到CALL时,扫描周期并不会停止,只是CPU把当前执行位置记下来,然后跳到子程序入口开始执行,执行到RET再恢复。大家要明白,这个“当前执行位置”的保存,是靠PLC内部的程序堆栈完成的。堆栈有深度限制,三菱FX3U的嵌套层数最多是5级,也就是说CALL套CALL不能超过5层,超了会直接报程序错误或者运行时异常。这也解释了为什么子程序虽好用,但不能无限嵌套——你的程序结构一旦深到五六层,本身就说明设计不合理了。
2. 子程序的底层设计逻辑:为什么一段功能要单独“封装”起来
很多人第一次接触子程序,会觉得“这不就是把一段程序挪个位置吗”?还真不是。子程序的价值不在于移动代码,而在于建立“被调用”和“被忽略”两种状态。主程序里没调用某个子程序的时候,那个子程序里面的代码一个扫描周期都不会去执行,里面的输出点不会刷新,定时器不计时,计数器不计数。这一点极其重要,因为它是子程序能同时实现“功能隔离”和“执行效率优化”的根本原因。
2.1 用结构化编程的思路来理解子程序
我经常打一个比方:一台整机设备就像一条流水线,主程序是车间主任,子程序是每个工位的操作规程。车间主任不可能每个工位都亲自上手干活的——那样他一天到晚就在流水线上来回跑,效率低还容易漏事。他只需要按流程说一句“一号工位开始”“二号工位停止”,工位上的工人就按照自己手里的规程干活。子程序就是这个“工人手里的规程”,主程序只负责决定“什么时候调用哪个工位”。
这样拆完以后,整个设备的逻辑就变成了:
- 主程序:只关注设备整体状态流程,比如上电复位、启动条件判断、自动/手动切换、报警汇总。
- 子程序1:控制气缸A的伸出、缩回、到位检测、超时报警。
- 子程序2:控制电机B的正转、反转、停止,以及电流过载保护。
- 子程序3:处理触摸屏上设置的参数,包括配方切换、数据上下限判断。
这个好处是立竿见影的。你不必在主程序里翻几百行梯形图找气缸A的动作了,直接看子程序1就行。改气缸A的时间参数,也不会影响电机B的逻辑。这就像把原来写在一张大草稿纸上的所有公式,重新整理成一本带目录的笔记本,每一章只管一件事。
我在做一个“十字路口红绿灯PLC程序”的练手项目时,就在主程序里用了一个“交通模式状态机”,把东西直行、东西左转、南北直行、南北左转四个相位分别放在四个子程序里。主程序里只有二十来行,负责根据时间计数器切换调用哪个相位子程序。要改某个相位的绿灯时间,只要进到对应子程序里改定时器设定值,完全不影响其他相位。这种“大程序拆小块”的思路,才是子程序真正的设计价值所在。
2.2 扫描周期与调用机制:为什么子程序能“按需执行”
子程序的第二个底层优势,是它能够做到“按需执行”,这个能力源于扫描周期的工作方式。PLC的扫描周期分成输入采样、程序执行、输出刷新三个阶段。程序执行阶段,CPU按顺序扫描所有指令。如果一条CALL指令的条件不满足,CPU就直接跳过它,继续往下扫描主程序的其他指令。只有当条件满足,CALL才会把执行权交给对应的子程序。
所以我常跟人说:子程序调用是“条件驱动”的。也就是说,子程序里那些代码,不是每个扫描周期都在跑,而是只有当触发条件成立的那个扫描周期才开始跑。假设你的主程序有500行,子程序合起来还有1000行。如果不拆子程序,那么每个扫描周期CPU都要把1500行全部扫一遍;拆了子程序以后,主程序可能只有200行,剩下的1300行分布在各个子程序里,只有被调用的时候才扫描。在CPU型号固定、扫描周期要求很短(比如伺服跟随控制)的项目里,这种优化是实打实的性能提升。
但这里又引出一个容易被忽略的问题:子程序调用指令本身是有“执行次数”和“触发沿”的讲究的。很多人都知道,普通CALL指令的输出条件如果是X0常开触点,那么只要X0一直为ON,每个扫描周期CALL指令都会被执行。如果X0一直为ON,子程序就会被无数个扫描周期连续执行。这在很多场合其实是没问题的,因为子程序执行完就回到原位继续扫描主程序,属于“重复进入”,不是“嵌套”。但如果你在子程序里用了定时器、计数器这类有状态保持的器件,连续反复调用与只调用一次,最终产生的结果和时序是完全不同的。这个坑我后面会专门讲。
2.3 全局变量与子程序局部器件:哪些数据会“串味”
子程序设计还有一个绕不开的话题——变量作用域。三菱FX3U的编程里,虽然有局部变量这个概念,但很多初学者用的还是全局软元件,比如M中间继电器、D数据寄存器。在主程序里用了M100,子程序里也用了M100,那么它们其实是同一个M100,谁先执行谁就改变了它的状态。西门子S7-200 SMART要好一些,它的子程序支持形式参数(IN、OUT、IN_OUT)和局部变量表,真正的局部变量只影响子程序内部,不会往外“串味”。
我的建议是,在中小型设备项目里,哪怕用的是三菱这种全局软元件为主的环境,也要人为规定一套“变量分区”规则。比如:
- M0~M99:主程序状态标志位。
- M100~M199:子程序1内部使用的标志位。
- M200~M299:子程序2内部使用的标志位。
- D0~D99:触摸屏交互参数。
- D100~D199:子程序1内部计算数据。
- D200~D299:子程序2内部计算数据。
这样虽然不能从语法上强制隔离,但能在习惯上避免“串味”。我看到过不止一次现场故障,就是两个子程序无意中用了同一个M点,导致设备动作乱跳。如果项目是用西门子或者汇川的CODESYS平台,能用局部变量就尽量用局部变量,语法层面的隔离比任何人为约定都可靠。
3. 三大主流PLC的子程序实操对比:从指令写法到参数传递
光讲理论有点虚,直接上手看三个平台的实操写法。我分别用三菱FX3U(GX Works2)、西门子S7-200 SMART(Micro/WIN SMART)、汇川H5U(CODESYS)做例子。
3.1 三菱FX3U:CALL指令与程序编号
三菱FX3U的程序结构里,主程序后面可以挂最多64个子程序(P0~P63)。每个子程序以P编号开始,以RET结束。主程序结束用FEND指令,这是个很容易忘的大坑——FEND是主程序结束的标志,如果忘了写FEND,CPU会以为主程序还没结束,直接顺序往下“走”进子程序区域,子程序就会在非调用状态下被反复当主程序执行。
调用格式很简单。假设我有一个子程序P1负责温度PID算法,触发条件是M50:
LD M50 CALL P1这里的执行语义是:当M50为ON时,每个扫描周期都会跳去执行一次P1,执行完RET回到CALL下面一行继续扫描。
如果我只想在M50从OFF变ON的瞬间调用一次,就要用CALLP:
LD M50 CALLP P1CALLP是脉冲沿调用。那这种“只调用一次”有什么意义呢?常见的就是数据初始化。比如设备上电时把配方参数从掉电保持区D区加载到运算区,你只需要上电沿触发这一次就够了,没必要每个扫描周期反复加载。这种场景如果用普通CALL,程序没有任何实质错误,但是每个周期都在做重复的无意义读取,浪费扫描时间。用CALLP一次搞定,干净利落。
三菱的嵌套写法,比如P1里又调用P2,最多5层,超出就报错“CALL OVER 5 LEVEL”。我实际写程序一般控制在2~3层,能用一层解决就不上两层。另外还要注意,三菱子程序里的输出点,如果对应的硬件输出被强制了,子程序里的写入是不生效的,这在调试强制输出的时候很容易把人搞懵。
子程序的“程序号”要避开主程序占用的编号,同时要养成给程序编号写注释的习惯。GX Works2里双击P编号就可以给子程序命名,比如P1注释写“温度PID计算”,P2写“气缸顺序控制”,这样维护起来一目了然。我见过一个设备程序里几十个子程序全是P0、P1这种无注释名字,出问题的时候根本没法快速定位是哪个功能块——这种程序维护成本非常高。
3.2 西门子S7-200 SMART:SBR子程序与带参调用
西门子的组织方式更“现代”一点。项目树里默认有主程序MAIN,可以添加SBR_0、SBR_1等多个子程序。调用方式是在梯形图里放置CALL指令,下拉选择要调用的子程序,比如CALL SBR_0。如果没有参数传递,写法就是:
CALL SBR_0S7-200 SMART的梯形图里CALL指令有个梯形图符号,条件满足时执行。它默认的调用方式和三菱的CALL一致——条件为ON时每个扫描周期都调用。如果想做“上升沿调用”,梯形图里要在调用指令前面串联一个“上升沿检测触点”(P触点),或者用SM0.0(常ON)配合上升沿。
西门子子程序的精华在于支持带参数调用。你在SBR_0的属性里可以编辑局部变量表,定义IN类型输入参数、OUT类型输出参数、IN_OUT类型输入输出参数。比如我要做一个“电机启动延时统计”的子程序,输入参数是启动信号bStart,输出参数是运行时间iRunTime:
// SBR_0 局部变量定义 // L0.0 IN bStart : BOOL // 启动信号 // LW1 OUT iRunTime : INT // 运行时间调用的时候直接写:
CALL SBR_0, I0.0, VW100这样I0.0的值作为输入传给子程序内部的bStart,子程序算出的运行时间写到VW100里。好处是同一个SBR可以被不同设备部位复用了——只要传入不同的输入参数、输出地址,一套逻辑就能管好几个数据块。这种“一套代码,多处复用”是子程序最有价值的使用方式,比三菱那种纯全局软元件的方式更安全。
在Micro/WIN SMART里新建子程序以后,别忘了给它起个有意义的名字。我习惯用“设备_功能”的格式,比如“Mixer_StirCtrl”“Pump_AlarmMon”。S7-200 SMART的CALL指令支持中文注释,但子程序名最好用英文,不然部分版本在符号表里显示会乱码。
3.3 汇川H5U:从FB函数块到结构化文本
到了汇川H5U这种支持CODESYS平台的PLC,子程序这个概念就更高级了。它不再叫“子程序”,而是叫POU(Program Organization Unit,程序组织单元),包含程序PROGRAM、函数功能块FB、函数FUN三种类型。和传统PLC子程序最贴近的是PROGRAM类型,它相当于一个独立的程序单元,可以被主程序调用;FB则是一种带内部存储的“类子程序”,支持输入输出参数和内部变量,适合封装需要记忆状态的控制逻辑。
在汇川H5U里调用一个FB的写法大概是这样的,用结构化文本ST语言写:
stPump := PumpControl( bEnable := xEnable, bRunCmd := xManualRun, rSetPoint := rSpeedRef, rFeedback := rActualSpeed, eFaultCode => wFaultCode );这里PumpControl是一个功能块,每次给它传入使能信号、运行命令、设定值、反馈值,它内部处理完以后把故障码输出到wFaultCode。这个FB可以被调用多次,分别控制三台泵,只需定义三个不同的FB实例变量。这比传统PLC子程序单副本的方式又进了一步——传统子程序的内部软元件是固定的,同一个子程序被多段逻辑同时用到时还得小心数据打架,而FB天然支持“多实例”,每个实例有自己的内部存储空间。
对刚从三菱、西门子转过来的工程师,我建议先从PROGRAM入手,把它理解成“西门子带参数的SBR”就行;等把结构化文本的语法跑顺了,再过渡到FB。不要一上来就啃面向对象那一套,PLC程序的本质仍然是“输入-处理-输出”,FB封装的是处理逻辑,不是业务模型。
4. 子程序的进阶用法与现实场景拆解:从PID到数采再到多工位调度
子程序不是只能做“把功能拆出来”这么简单,它真正强大的地方在于把一套逻辑模板化,然后用不同参数去实例化。下面结合几个真实项目场景讲讲怎么玩。
4.1 温度PID控制:把算法封装进子程序
很多初学者在PID控制上栽跟头,主要是因为PID运算和模拟量采集、输出刷新混在主程序里,扫描周期不固定导致微分项抖动。我做一个“基于PLC的冷库监控系统设计”时,直接在子程序里封装了温度PID模块。子程序接受五个输入:设定温度SV、当前温度PV、比例带P、积分时间I、微分时间D,输出是阀门开度控制值MV。
关键点是,PID运算有内部状态——积分项累积值和上一次的偏差值——这些状态必须在子程序内部用保持型寄存器保存。如果用普通寄存器,每次子程序调用结束以后内部变量被清零,积分项就永远累积不起来,温度会一直稳态偏差。我的做法是用带断电保持区的D寄存器保存积分项,并把“采样周期”作为一个参数也传进去,确保PID运算的扫描频率是可控的。
这里有个非常实用的子程序写法:用SM412(三菱里每10ms变化一次的时钟脉冲)或者用固定扫描周期模式。不要让PID运算跟着主程序扫描周期“随缘跑”,否则温度PID的波动会很大,温差忽高忽低。说实话,网上很多人问“PLC温度PID波动温差大如何调节”,十有八九不是PID参数不对,而是子程序调用方式和采样周期没固定住,导致运算间隔不均匀。把子程序做成“定时调用”后,温差一般能收敛一半以上。
4.2 多工位设备调度:状态机+子程序的组合拳
非标设备里最常见的结构是多个工位轮流动作。我之前做过一个三工位装配机,每个工位有独立的夹紧、进给、检测动作。用子程序实现的核心思路是:主程序里放一个状态机,状态寄存器的值决定“调用哪个工位的子程序”。比如状态值为1时CALL P11(工位1动作),状态值为2时CALL P12(工位2动作),状态值为0时什么都不调,设备在等待。
这种设计最大的好处是——工位子程序内部逻辑完全独立,工位1的调试不影响工位2。而且状态机本身只进行状态判断、转移,结构极清晰。在设备联调的时候,我可以单独把某个工位的子程序强制调用起来测试,不影响其他工位的执行。调试效率比单块大梯形图高太多了。
你还可以利用子程序“不被调用就不执行”的特性做安全联锁。比如在手动模式下,主程序根本不调用自动运行子程序,那么自动运行子程序里即使有驱动电机的指令也不会执行,相当于多了一层软安全保护。这种“通过调用与否来控制整个功能块是否激活”的思路,比在子程序内部加一堆使能位判断要可靠得多,因为不执行的代码逻辑上再也不可能误动作。
4.3 通讯与数采场景:子程序如何管理Modbus与OPC UA数据流
聊到热词里频繁出现的Modbus、OPC UA读取PLC数据,子程序在这种场景下的角色往往是“数据帧组织/解析”和“周期刷新”。我做数据采集时,比如用上位机(组态王、WINCC)通过Modbus TCP读PLC,往往会在PLC里写一个通讯协议处理子程序。子程序负责把设备状态、温度、压力等运行数据打包到连续的保持寄存器区,这样上位机只需要连续读一段寄存器就能拿到所有数据,不需要一个地址一个地址去读。
这里有个很典型的组织方式:PLC内部数据区分成“运行数据区”和“参数设定区”。子程序每隔固定时间把分散在各处的运行数据(比如设备状态字、当前温度、当前压力)搬运到连续的“下发数据区”D100~D120;同时把“参数设定区”D200~D220的数据搬运到各功能子程序里去使用。这个搬运和映射的过程全部在一个“数据管家”子程序里完成,主程序不碰这些地址。这样Modbus通讯和OPC UA服务器读到的数据始终是有序且无冲突的,项目上位机联调的时候很少出现数据错位问题。
还有一点值得提,现在很多人关注“AI PLC代码生成”,其实好的AI辅助编程也特别依赖子程序结构。你把一个设备的I/O表、动作流程写成注释或伪代码发给AI,AI比较容易生成带清晰的模块划分的子程序框架。如果你把整个程序需求一股脑给AI让它生成一个巨型梯形图,生成出来的东西基本没法用。子程序化不仅有利于人维护,也有利于AI生成更规范的PLC代码,这一点算是新技术时代对程序结构提出的新要求了。
4.4 多轴机械臂与伺服轴控:子程序里的运动控制封装
热词里还有“PLC管理六轴机械臂伺服”“PLC电机顺启逆停定时器”,这些场景同样适合用子程序封装。机械臂的控制通常有两种模式:PLC直接发脉冲/总线指令控制伺服,或者PLC与机械臂控制器通过Modbus/IO通讯交换指令。无论哪种,主程序都不应该把整条机械臂动作宏命令码写在主扫描区,而是应该封装成“动作执行子程序”。
比如某机械臂需要依次完成“抓取→搬运→放置”流程。我定义了一个“机械臂搬运”子程序,子程序内部通过步进状态字一步步发指令、等待完成反馈、再发下一步指令。主程序只需要在到达搬运时机时调用这个子程序,并根据子程序输出的“完成标志”来切换主状态机。子程序内部的等待时间、超时判定都放在里面,主程序不被这些细节拖累。
伺服控制还有一点要注意:伺服的使能信号和急停信号这种安全关键信号,尽量不要放在子程序里,而要放在主程序每周期都扫描的段里,保证急停响应的时间最短。子程序适合“命令类动作”,安全类逻辑永远保持在全时扫描的主程序里,这个边界一定要分清楚。我在实际调试里见过有人把急停逻辑放进了子程序,结果条件没触发就不扫描,急停按下毫无反应,这种设计错误是绝对应该避免的。
5. 踩坑实录与排查技巧:这些年我掉进去过的子程序的坑
子程序用好了是神器,用不好是隐形炸弹。下面这几个问题几乎每个项目都大概率碰到过,我整理成速查表,每条都附上排查思路。
| 常见问题 | 现象 | 根因 | 排查思路 |
|---|---|---|---|
| 子程序里的线圈在主程序也被使用 | 设备动作乱跳、输出异常 | 双重线圈,同一Y地址被两处赋值 | 查找Y地址交叉引用,统一所有权 |
| 子程序里用了定时器但不被触发 | 定时器偶发不动作 | 子程序处于非调用状态,定时器不计时 | 确认调用条件,必要时改用累计型定时器 |
| 主程序忘了FEND/RET | 子程序代码被当主程序反复执行 | 程序区域边界未定义 | 检查FEND、RET是否成对出现 |
| 嵌套层数超过限制 | 报程序错误,CPU停机 | CALL套CALL超过5层 | 查看错误码,减少嵌套层数 |
| 子程序内部用了全局M与另一子程序冲突 | 设备偶尔误动 | 变量作用域未隔离 | 规划变量分区,或改用局部变量/FB |
| 条件调用与上升沿混用 | 子程序执行次数不对 | 对CALL与CALLP语义不熟 | 对比需求选择普通调用或脉冲调用 |
| 子程序内PID采样时间间隔不均 | 温度波动大、PID发散 | PID运算扫描周期未固定 | 固定采样周期,子程序定时调用 |
| 带参数调用时参数类型不匹配 | 编译报错或结果错误 | IN/OUT参数定义错误 | 检查局部变量表类型和长度 |
5.1 双重线圈问题怎么查
双重线圈是子程序最容易引发的隐性故障。一个Y输出点,在主程序通电了一部分,又在子程序里控制同一动作,两个地方都写了线圈,PLC在编译时往往只是警告不报错。程序扫描到最后一次写入的线圈值才生效——子程序调用顺序靠前,主程序的线圈写在后面,那么主程序会覆盖;反过来也一样。排查方法就是利用编程软件里的交叉引用表,搜一下这个Y点在哪些程序块里出现过,一旦出现两处以上线圈赋值,就必须重构。
另外三菱的编程软件里,双击错误信息能跳转到出现重复线圈的位置;西门子Micro/WIN SMART也能看到符号表多出红色的重复标记。实际排查的时候不要只盯着梯形图看,要用交叉引用和地址使用一览表,效率高得多。
5.2 嵌套堆栈到底怎么理解
三菱的嵌套限制是5层,西门子没有明确说层数,但也不建议超过8层。理解这个限制的关键在于“堆栈是有空间上限的”。每次CALL都往堆栈里压入一个返回地址,RET时弹出。压入次数太多,堆栈溢出,程序直接进入停机状态。这个报错通常出现在程序运行中而不是下载时,所以现场突然停机排查起来很迷惑。
我一般会做一个“调用层级清单”,把程序涉及的所有子程序关系画成一张树状图(纯手写或者用表格列)。每一层最多几个子程序、哪个子程序由谁调用,都列清楚。超过三层的就要简化——把子程序内部再拆分,或者把参数化的逻辑合并。这套方法在维护旧设备程序时尤其有用,能快速看明白整个程序的调度骨架。
5.3 条件调用中的定时器与计数器陷阱
这一条简直是我强调过最多遍的坑。三菱FX3U的T0~T199是普通定时器,它的计时是在扫描周期里累计的。如果T0所在的子程序在某个扫描周期没被调用,那个周期T0就不计时。这意味着子程序的定时时间不完全等于“真实世界的时间”,而是约等于“被调用的扫描周期次数 × 每个扫描周期时间”。如果有中间几个周期没调用,定时时间就会拉长。
很多人在十字路口红绿灯、抢答器这类项目里,觉得红绿灯切换时间不准、抢答锁定时间漂移,原因就在这。解决的办法有三个:一是把定时器放在主程序里,主程序每周期都扫描,计时准;二是用累计型定时器配合触发沿做累计计时;三是用专门的定时中断子程序,把定时功能放到固定扫描周期里执行。我在项目的定时精度要求从严的地方,比如触摸屏上设定1秒钟的等待时间,都是用第一种方案——把时间到标志做成位变量,子程序里判断这个标志。
5.4 编译错误与运行错误速查
三菱FX3U常见报错6405、6409这些,往往和子程序区域有问题有关。6409经常表示“无FEND”或者“程序步超出范围”;西门子常见的错误是“无RET”“调用参数的局部变量表没有正确初始化”。碰到编译报错先别急着改程序,按这个顺序检查:看FEND/RET是否配对、看CALL的目标编号/名称是否存在、看嵌套层数是否超限、看符号表是否有重名。
运行中偶发故障怎么抓?建议开着编程软件的在线监控,重点观察子程序调用条件的变化时机。如果某次故障发生前,X0、M20有一个瞬时抖动,子程序被误调用了一下,那问题就在外围信号或者触点抖动上。子程序调用条件的可靠性,是整个程序能不能稳定运行的关键——宁可多写一个联锁条件,也不要裸奔触发。
调试子程序还有一个非常实用的技巧:在子程序入口放一个M0上升沿,把入口时间戳存到D寄存器里;在RET前面再放一个M0下降沿,把出口时间戳存到另一个D寄存器。这样就能精确测得这个子程序每次实际占用了多少扫描时间。这个技能在排查扫描周期超时问题的时候,简直是“照妖镜”,能秒杀很多找不到原因的偶发故障。
6. 最后分享一点个人的习惯
我在实际项目里一般会把子程序当作“设备操作说明书”来写——每个子程序开头用注释写清楚这个模块的I/O清单、运行前提、输出结果,里面用注释标注每一步动作的工艺目的。有人觉得注释浪费时间,但等到设备卖了三年、客户换了两拨工程师、原程序再拿出来维护的时候,那几句注释比什么都值钱。编写规范的习惯具体到动作上,就是给每个子程序起名和注释、给每个参数写类型和范围,越是大型项目越要坚持。
再给一个小技巧:子程序里的步进逻辑,尽量用“当前步号寄存器+比较触点”的方式写,不要用一堆连续的SET/RST去控制M点。步进寄存器加调用,配合触摸屏显示当前步号,排查问题的时候严重事半功倍。这套模式我在三菱、西门子、汇川上都用过,结构几乎一模一样,只是指令助记符略有差别。
子程序这东西,初看是“指令”,用久了你就会理解它其实是“程序架构”。把整个设备拆成一个个边界清晰的子程序,维护、调试、交接都会轻松很多。这篇文章从指令机制讲到三大平台的实操写法,再到具体应用和排查技巧,基本覆盖了我这些年做PLC项目时和子程序打交道的全部经验。如果后面你在项目里遇到子程序相关的奇怪故障,顺着上面的排查表一条条过,大概率能在几分钟内找到方向。