ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PLC子程序与程序控制逻辑:从结构化设计到工程实战

PLC子程序与程序控制逻辑:从结构化设计到工程实战 写这篇文章的起因很直接我带过不少刚入行的电气工程师和调试人员发现大部分人学 PLC 编程时梯形图都能画定时器计数器也都会用但一遇到项目稍微复杂一点——十几个工位、几十个气缸、多台伺服联动——程序就开始乱成一锅粥。要么是重复的代码块到处复制改一个动作要翻半天要么是主程序里塞了几千条指令扫描周期被拖得很长要么是设备一报警根本不知道程序跑到了哪里。这些问题的共同解药就是今天要聊的 PLC 程序控制指令里的子程序以及它背后的整套程序控制逻辑。这篇内容我会结合三菱、西门子、台达、汇川这些我调过的品牌从指令原理讲到工程实战包含调用方式、扫描周期理解、和中断的配合、HMI/通信场景下的调试经验还有我踩过的坑。适合正在做实际项目的PLC工程师、刚学完基础指令想进阶的新手以及带项目的电气负责人参考。我尽量讲得接地气不堆手册原文因为那些直接翻官方文档就行真正缺的是“为什么要这么写”和“现场出了状况怎么办”。1. 程序控制指令的全家福子程序不是孤立存在的1.1 先看清整个“程序控制指令”家族在没聊子程序之前得先把程序控制指令这个家族理清楚。因为子程序只是其中一张牌其他牌还有跳转指令、循环指令、中断指令、主程序结束指令。很多初学的人把这些混为一谈导致程序里乱用跳转或者把子程序和中断划等号这是比较典型的认知误区。以三菱 FX3U 为例程序控制指令主要包含CALL / CALLP无条件/脉冲执行子程序调用SRET子程序返回FEND主程序结束CJ / SCJ / JMP条件跳转和无条件跳转FOR / NEXT程序循环执行EI / DI / IRET中断允许、禁止和中断返回西门子 S7-200 SMART 里对应的就是SBRSubroutine Routine调用指令用 CALL 调用子程序条件满足时执行子程序末尾用无条件返回结束跳转则是 JMP / LBL 标签方式。汇川的 H5U、AM 系列沿用了类似三菱的 CALL 体系因为很多工程师就是从三菱生态转过来的而 CODESYS 平台比如倍福 TwinCAT、汇川中型机则更偏向结构化文本直接用函数块FB和函数FC来表达子程序的概念被更大一级的“程序组织单元POU”给覆盖了。这里要说明一点广义上“子程序”是一个工程概念不单指某一两条指令。我们今天主要聚焦传统中小型 PLC 里 CALL 子程序区的用法后面也会提到它和中断、和函数块之间的边界关系。1.2 为什么要用子程序三个最刚性的理由第一个理由最简单也最硬核代码复用。一个项目里往往有多个相同结构的工位比如四工位转盘每个工位都有夹紧、检测、顶升三个动作。如果你不拆子程序就得在主程序里把同样的逻辑复制四遍。到时候改一个气缸动作时间得小心翼翼地改四个地方——漏改一处就是调试现场的定时炸弹。把这一套动作逻辑写成一个子程序主程序里四处 CALL 同一个子程序改逻辑时只动一处所有工位同时生效。第二个理由是扫描周期优化。PLC 是按扫描周期循环执行主程序的。主程序内容越多周期越长。写一个 10ms 扫描周期的程序和写一个 150ms 扫描周期的程序对高速设备的控制效果完全两个世界。子程序允许你把一些大段的、非每周期必须执行的逻辑比如配方计算、通信报文解析、复杂的工艺数据处理放到“需要时才调用”的地方从结构上帮助主程序瘦身。第三个理由我开始做项目时没体会到后来被坑了才懂程序的可读性和可维护性。老工程师看程序第一步不是逐行看梯形图而是看程序结构。设备动作是主流程报警处理放哪了通信逻辑放哪了手动调试逻辑放哪了——清清楚楚。子程序用得好程序就像一本有目录的书用得不好就是一卷没剪开的胶带谁接手谁崩溃。2. 子程序调用的硬核细节从指令格式到调用条件2.1 一条 CALL 指令的前世今生以三菱 FX3U 为例子程序调用格式极其简洁CALL P10这行指令放在主程序区意思是“调用编号为 P10 的子程序”。子程序区从 P10 开始写到子程序结束前必须有一条SRETSubroutine Return指令执行完 SRET 后就返回到 CALL 指令的下一条继续扫描。主程序区全部逻辑写完后必须用FEND指令标记主程序结束子程序区必须放在 FEND 之后——很多人第一次写子程序报错就是因为把子程序码放在了 FEND 之前扫描走不到或者直接变成顺序执行。西门子 S7-200 SMART 的写法更直观CALL SBR_0主程序调用子程序 SBR_0子程序内部网络写逻辑块末尾不用显式写返回。西门子这边把子程序的调用支持带参数传递三菱 FX 传统指令则更多是靠全局软元件直接传递数据这一点后面单独说。条件调用就更好理解了CALL 指令前面串一个常开触点或者比较指令触点通了才调用。这样“设备在自动状态才执行某段工艺”“报警复位时才刷新某组数据”就变得非常自然。注意 CALL 的脉冲执行版本比如 CALLP只在条件从 OFF 变 ON 的上升沿触发一次。这个在“按钮触发一次计算”“检测到一次到位信号才执行一次动作流程”的场景里非常实用。2.2 参数传递子程序的“管道”怎么搭如果子程序只是共用全局软元件那其实和直接在主程序里复制代码没有本质区别只是“贴得紧凑了一些”。真正让子程序发挥威力的是参数化设计——同一个子程序给不同的“入口参数”就能干不同的事。在西门子 S7-200 SMART 中可以在调用子程序时指定输入输出参数// 子程序 SBR_0 定义 // IN: 启动信号, 设定时间 // OUT: 完成标志, 当前计时值 CALL SBR_0, I0.0, VD100, M0.0, VD104这样 SBR_0 内部的逻辑完全通用调用的时候把不同的输入输出地址填进去就行。比如写一个“延时启动子程序”三个不同气缸都可以 CALL 它只是每次传的启动信号、延时时间、完成标志不同。而三菱 FX3U 用传统指令来实现参数传递稍显别扭——它没有显式的函数参数通常做法是约定几个全局寄存器比如 D100~D105作为子程序的“输入输出接口区”。虽然也能用但有个隐患如果两个调用方相隔很远中间不小心把 D100 写乱了子程序执行结果就错了。所以我在三菱平台做稍微复杂一点的子程序时更倾向专门划分一块“子程序接口变量区”建立一份表格写清楚占用了哪些 D 寄存器谁写入、谁读出避免软元件冲突。这属于工程规范层面的东西不写进去后续大概率会出问题。2.3 子程序嵌套与调用限制能套几层能调几个子程序是可以嵌套的A 子程序里可以再调用 B 子程序。这种设计能将大功能拆成最小单元比如一个“焊接控制子程序”内部再调用“焊枪预热子程序”“焊接过程监控子程序”“焊后清理子程序”每个子程序都短小精悍。但要注意几个硬限制不同品牌限制不同三菱 FX3U 嵌套层数一般允许 5 层左右具体看型号和手册调用次数也有上限比如最多 512 次这种级别。S7-200 SMART 嵌套层数通常是 8 层。实际工程中超过 4 层嵌套代码已经很难跟踪了我一般控制在 3 层以内。调用次数不是无限所谓“每个扫描周期可以被调用多次”和“项目里总共能写多少个 CALL 指令”是两回事后者有数量上限。写到大项目时最好提前统计一下。子程序区大小的限制FX3U 子程序区从 P0 到 P63 左右西门子则是以 SBR_x 编号。结构多了以后编号规划要提前做不然后期在程序里找“P23 是干嘛的”要疯掉。2.4 子程序里的“软元件性格”问题定时器、计数器别踩坑这是我在现场调过最久的一个坑必须单独拎出来讲子程序里能不能用定时器 T能用但行为可能和你预期不一样。在三菱 FX3U 里某些型号的定时器线圈即使在子程序里执行每次扫描到它时也会正常计时。但问题出在“子程序不是每次扫描都调用”的场景假如设备在等待状态不调用某子程序子程序里的定时器就不会被刷新计数值时间自然就“不走了”。等下次调用时它会从之前的状态继续给人一种“定时器卡住了”的错觉。更坑的是如果你在某一个扫描周期里调用子程序的次数超过一次——比如主程序调一次中断里又调一次——同一个定时器线圈可能在同一个扫描周期里被“接通”两次时间继电器和普通定时器的累计方式就会出偏差或者出现双线圈冲突。这种问题的表现极其隐蔽排查时看梯形图怎么都对就是现象不对。我的实际经验是子程序内部尽量使用“局部软元件”或者“专用接口寄存器”来做中间计算不要直接怼同一个全局定时器。如果确实要用定时器保证“同一时刻只有一个调用路径会激活它”这通常靠软件互锁来解决。比如两个子程序入口端子互斥或者调用条件里加正在执行的标志位。西门子这边情况略好因为它子程序里可以直接用局部变量表L 区来存中间状态天然隔离。汇川和台达的较小机型跟三菱思路接近同样需要注意。3. 从梯形图到结构化子程序设计方法和实操记录3.1 我第一次用子程序重构程序的案例复盘有一年我接手一台老设备改造原来的程序是一个 4000 多步的主程序初始化和手动、自动、报警全部堆在一起。光找“哪个触点控制哪个输出”就耗了一整天。后来我花了一个晚上把程序拆成了七个子程序大概结构是这样主程序设备启动停止逻辑、模式切换、调用下面这些子程序SBR 初始化上电参数加载、气缸复位、数据区清零SBR 手动操作每个气缸的手动按钮控制带互锁SBR 自动流程按工序步骤跳转核心工艺逻辑SBR 报警处理故障信息的判断、锁存、HMI 显示SBR 通信处理和变频器、仪表的数据读写Modbus 轮询报文SBR 数据运算几个重量数据的滤波计算、配方数据校验改造完成后效果很明显查报警直接从报警子程序里看改手动逻辑不会碰到自动流程通信逻辑缩到一个角落主程序只有三十多行。这种“各自独立又协同”的模式正是子程序的本意。3.2 一个最简单的三菱平台子程序示例为了防止变成空谈我写一个最简单又最常见的示例控制某个气缸伸出并延时缩回。传统新手写法是在主程序里直接写一大堆拆成子程序之后主程序只需要// 主程序 LD M100 // 启动信号 CALL P10 // 调用“单气缸动作”子程序 FEND // 子程序区 P10: LD M100 SET Y0 LD T0 RST Y0 LD M100 OUT T0 K30 SRET粗看还行但如果你有四个气缸要分别控制就得复制四份。这时候更好的做法不是纯指令能搞定的而是在每个动作子程序里用不同的软元件地址或者用“带接口规则”的方式。西门子在这方面由于支持参数传递同样的逻辑只需要写一个 SBR_0四个调用传不同地址就行。3.3 设计子程序时我会死守的几个边界第一一个子程序只干一件事。如果一个子程序里既有通信解析又有电机控制又有报警那它本质上还是一个“小主程序”只是把问题换了个位置藏起来。子程序的命名要直接表达功能比如“气缸延时控制”“Modbus读从站1”“配方校验”等千万别叫什么“程序1”“子程序A”。第二调用条件要做全不能裸奔。比如自动流程子程序必须前提条件是“已初始化 非急停 在自动模式 无严重报警”。把这些挂在 CALL 前面比在子程序内部开头慢慢判断要更保险一点因为外部直接决定了这个子程序是否进入“执行池”。第三子程序内部出口状态要明确。我要求自己写的每个子程序至少有一个“执行完成”标志或者“当前状态字”能够在 HMI 上显示它到底干活干到哪一步了。调试时你会感谢这个习惯。3.4 子程序和扫描周期的实测观察很多人对“子程序能优化扫描周期”这句话半信半疑。我以前用程序监控功能实测过一台 FX3U把通信报文处理、配方运算这种“非每个扫描周期都必须执行”的逻辑拿进子程序把调用频率改成每 100ms 调一次或事件触发调用扫描周期从 40ms 降到了 12ms 左右。对于一台 200ms 节拍的高速装配设备这个提升是决定性的。但也要泼一盆冷水子程序并不能降低 CPU 的总工作量它只是把“每周期都做的活”变成“需要时才做的活”。如果设备工艺本身就要求每个扫描周期处理完所有数据那拆不拆子程序对扫描周期影响都不大。把这一点理解清楚就不会在选型阶段产生“多写子程序就能让老 PLC 提速”的幻觉。4. 子程序、中断程序与通信协同作战的场景解析4.1 子程序和中断的边界别再搞混了现场经常有人把子程序当中断用——这是比较危险的误区。子程序是在 CPU 的正常扫描周期内由 CALL 指令触发执行的必须“被轮到”才会跑而中断程序是当特定事件发生比如高速计数器到达目标值、外部输入上升沿、定时中断立即暂停当前主程序正在执行的任务跳去执行中断服务程序执行完再回到原来的地方继续扫描。举一个场景NJ/汇川/三菱设备上想用高速脉冲输出控制步进电机每发完一批脉冲想立刻切换下一批。在子程序里干这个事如果主程序扫描周期是 10ms那判断“脉冲发完”的时机可能滞后 10ms速度稍微一快就丢脉冲。这时候必须用中断程序来响应高速计数器的比较中断。子程序是“按计划做工作”中断是“突发状况随时插队”两者逻辑完全不同。实际工程中它们的搭配很常见主程序决定什么时候干什么子程序负责具体干中断只处理不能等的紧急事件。比如伺服报警信号如果只在主程序扫描周期里查从物理信号出现到程序响应可能已经过了好几个扫描周期但如果你把伺服报警接到高速中断输入上中断程序里立即停机响应时间可以压到微秒级。当然具体能不能这样接要看伺服报警信号是不是接到了 PLC 的中断输入端上这个在硬件设计阶段就要确认。4.2 通信相关场景Modbus 和 OPC UA 读取状态数据时的子程序配合现在做设备联网的越来越多经常是上位机、MES 通过 Modbus 或 OPC UA 来读 PLC 里的设备运行状态数据。这里子程序的作用远比很多人想的大。拿 Modbus 通讯举例PLC 作为 Modbus 从站时上位机要读设备状态、启停状态、节拍计数、报警代码。这些数据如果散落在各个子程序的临时软元件里上位机还真不一定要得到所以我会单独写一个“数据映射子程序”在每个扫描周期或者数据变化时把子程序内部的状态字、计数器、结果值统一整理到专门的 Modbus 保持寄存器区域比如 D2000~D2100。这样上位机来读只读这一块连续区域就行了不用关心你内部有多少个子程序。类似的OPC UA 服务器在 PLC 侧比如汇川、倍福这些支持 OPC UA 的 PLC采集标签时同样受益于这种“接口区集中映射”的做法。还有一种是 PLC 作为主站轮询传感器、变频器、仪表数据。轮询协议解析、数据校验、数据滤波这些逻辑很适合放到一个独立的通信子程序里或者按“每个从站一个解析子程序”去拆。这样好处非常直接某个从站坏了只需看对应子程序的状态字新增一个从站新写一个解析子程序主程序只加一行调用就行。4.3 在触摸屏 HMI 上查看子程序运行状态我调试设备时喜欢在触摸屏上做一页“程序状态页”主程序运行到什么步骤、各个子程序当前是“运行中/待机/未调用”、报警子程序有没有锁存。这个玩法一开始只是为了自己调试方便后来发现客户调试、售后维修都非常依赖它甚至成了设备验收的加分项。实现思路不复杂每个子程序的入口和出口处各放一个特殊标志位子程序一旦被调用就置位“运行中”执行完清除。子程序内部再放一个“当前步骤码”传到专用的 D 寄存器。HMI 直接读这些软元件显示就好了。可能有人担心这会不会拖慢系统其实就几条位操作和寄存器写入开销可以忽略不计。5. 常见问题与排查技巧子程序工程实战避坑手册5.1 排查实录调用条件明明满足子程序却不执行这个现象我见过好多次。有次排查一台三菱设备启动信号已经亮了CALL 前面的触点也导通了但子程序内的动作就是没反应。在线监控一看子程序的 “OUT Y0” 确实被点亮了但实际输出端没有动静——最后发现是被另一个子程序里同一个输出线圈“二次赋值”了。主程序一个扫描周期内先调用了子程序 A 置位了 Y0后调用的子程序 B 又将 Y0 复位了最终 Y0 的状态取决于最后一个执行到的地方。这种“双线圈/多线圈”是子程序化之后的高发问题因为同一个动作可能在手动子程序和自动子程序里各出现一次。排查方法很土但有效在软件里交叉引用搜索这个输出/中间继电器到底被哪些地方操作过逐一审视调用顺序。最终修正办法是保证同一软元件在同一时刻只有一个写入路径建立模式互锁。5.2 排查实录子程序里定时器“跑不准”前面提到过这个话题这里说一个具体排查套路如果发现定时器有时候正常有时候不准先看它所在的子程序是否被多路径调用。我用过的 FX3U 项目里因为“初始化子程序”和“自动运行子程序”都会调用同一个“延时复位子程序”结果操作员某次操作时初始化和自动运行几乎同时触发延时一直被重置设备表现就是“该动的时候不动好久之后突然动一下”。后来把两个调用路径加互锁问题立刻消失。另外FX3U 里的普通定时器线圈如果在子程序里断电保持型使用涉及 RST 的问题也要特别注意。子程序没被调用时里面的 RST 不会执行定时器可能一直保持旧状态。我习惯在子程序最开始做一个“如果调用条件不满足则复位中间变量”的保护逻辑但要注意别误伤其他状态数据。5.3 常见问题速查表我把多年遇到的子程序相关问题整理成一张速查表不一定覆盖所有人但至少能帮大家少走弯路。现象可能原因处理办法子程序不执行但调用条件已满足子程序区位置错误或 CALL 编号写错检查 FEND 是否在主程序之后、子程序编号是否越界子程序执行了但输出不对双线圈冲突、IO 被其他子程序覆盖交叉引用搜索该软元件统一写入路径子程序内定时器不准多路径调用、定时器未被周期性刷新用互锁保证单路径调用必要时使用保持型定时器并做好复位程序运行变慢子程序内部逻辑过重、被频繁调用改用事件触发调用、限制调用频率或优化内部逻辑调用嵌套很深时程序混乱金字塔式嵌套缺少状态机拆平为“步骤状态每个状态一个子程序”下载后子程序丢失或版本不对程序块未编译完整、注释未保存全编译后在工程设置里确认程序块存在上传时反向比对5.4 子程序的命名规范和注释约定这部分是我吃过亏后立下的规矩供参考子程序命名功能 对象比如“气缸控制_夹紧”“M101 原料输送”“报警锁存处理”,尽量避免“sub_01”子程序头部注释用途、输入软元件、输出软元件、修改历史、调用者关键步骤注释在梯形图里给每一步写一句话说明“满足什么条件才能到这里”接口变量表如果是三菱这种全局软元件方案把所有用于子程序接口的 D/M 地址列一张表放在工程注释里可能有人觉得这是形式主义但说实话如果你一个月后还要回来改自己的旧程序或者同事接手时能少打三个求助电话这些“规矩”就值回票价了。6. 子程序设计模式三种我常用的结构好的子程序体系不是把代码随便拆开就完事而是有一套结构特征。这里分享三种我过了很多项目考验的典型模式。6.1 状态机驱动模式这是自动流程控制里最常用的结构主程序维护一个“当前步骤号”每个步骤对应一个子程序。第 1 步到第 10 步每步是一个独立子程序。执行完一步更新步骤号并调用下一步子程序。这样最大的好处是查问题只看当前步骤对应的子程序改某一步工艺不会波及前后步骤。举个例子一个冷库监控系统的自动制冷流程可能分为检测库温步骤、判断压缩机启动步骤、启动风机步骤、等待压力稳定步骤、记录数据步骤。每一步独立最终整个系统逻辑清晰。6.2 模式判断分发模式设备有多种模式手动、半自动、全自动、调试模式不同模式执行不同控制逻辑。这种场景我不建议写一个巨大的“模式选择梯形图”更建议在主程序里对当前模式做一个判断然后调用对应模式子程序。手动模式子程序只处理手动按钮自动模式子程序只处理流程两者互不干扰切换模式时只需要边缘处理一把“退出模式前的复位逻辑”。6.3 通信接口映射模式专门处理外部数据交互上位机需要什么数据设备需要从外部拿什么数据全部通过一个“通信映射子程序”统一处理。我在做温控设备和 MES 对接时特别喜欢用这个模式。因为现场调通信的时候如果状态变量分散在各个子程序上位机工程师会跟你吵一架把数据都归拢到专门的接口寄存器区通信调试直接对着地址表查非常省事。7. 给不同基础阅读者的一些参考路径刚入门的同学我建议你先把 CALL 和 FEND 的关系弄清楚然后在模拟器里做一个“按钮调用子程序改变指示灯状态”的小实验。先别管什么参数传递、嵌套、中断协同把这些基础跑通之后再用状态机写一个小项目练手比如十字路口红绿灯、八人抢答器这类经典练习都很适合因为它们天然能拆成“初始化、计数计时、输出控制”多个子程序。有经验的工程师我更建议把注意力放在“子程序和中断/通信任务怎么配合”以及“程序规范、软元件规划”上。这部分不是看懂指令就能拿下的一定要回到项目里从维护性、可调试性的角度去重新审视自己之前写过的程序——你会发现以前很多觉得“没错”的写法其实埋着不小的雷。另外如果你在用支持结构化文本的平台比如 CODESYS/倍福 TwinCAT你会发现子程序的思路被升级成了函数块FB和函数FC。但底层思想没变模块化、参数化、隔离复杂逻辑。所以即使在传统梯形图平台上把子程序用顺了将来转到 PC-Based 控制、或者做 PLC 和视觉/AI 上位机联合调试时你依然会吃这些基本功的红利。现在很多设备上已经出现 AI 辅助生成 PLC 代码、上位机直接走 OPC UA 读数据的玩法但不管工具怎么变一个程序结构清晰、子程序边界明确的 PLC 工程永远比玄学堆代码好调、好改、好交付。我之前带过一个小伙他就靠把一套包装设备的程序拆成“自动流程、通信采集、报警处理、配方管理”四个模块硬是把原来梳理了一周都梳理不清的旧代码变成了新同事三天就能上手改动的工程。我倒不是夸这种拆法多高级而是想说PLC 程序控制指令的终点从来不是指令本身而是工程交付的效率和人命关天的设备可靠性。希望这篇总结能给你一些可以拿去用的思路。
返回列表