1. 为什么要在ST和梯形图之间做混编
第一次接触ST和梯形图混编,是在一个包装线改造项目上。当时设备要增加一段配方管理逻辑,用梯形图写状态机已经画了满满三屏,维护的师傅每次改参数都要在几百个网络里翻找,改一次骂一次。后来我把配方计算和字符串处理的部分抽出来用ST写,逻辑部分保留梯形图,整个程序从三屏缩到一屏半,改参数只需要在一个结构体里改数值。那次之后我就开始认真琢磨这两种语言混编的边界到底在哪里。
ST(Structured Text,结构化文本)和梯形图(Ladder Diagram,LD)是IEC 61131-3标准里最常用的两种编程语言。梯形图脱胎于继电器逻辑,图形化、直观,电气背景的人上手快;ST类似Pascal,擅长数学运算、循环、条件判断和数据结构处理。两者不是替代关系,而是互补关系。混编的核心价值在于:让每种语言干它最擅长的事,同时保持程序的可读性和可维护性。
这篇文章适合已经能独立写梯形图或ST、但还没系统尝试过混编的工程师。我会从实际项目出发,把两种混编写法的差异、各自的适用场景、踩过的坑和验证方法都摊开讲。你不需要有很深的编程功底,但至少要能看懂基本的梯形图网络和ST的IF语句。
先明确一个前提:混编不是把两种语言随便塞进同一个程序就行。不同品牌的PLC对混编的支持程度差异很大,有的允许在同一个POU(程序组织单元)里混写,有的要求分POU调用,有的甚至对ST里调用梯形图功能块有额外限制。我下面讲的内容以主流支持IEC 61131-3的平台为基准,具体到你的平台需要对照手册确认。
2. 两种混编写法的本质差异
2.1 写法一:分POU调用,各管一摊
这是最稳妥、兼容性最好的混编方式。具体做法是:把逻辑控制部分写成一个梯形图POU,把数据处理部分写成一个ST的POU或函数,然后在主程序里调用它们。
我通常这样划分:梯形图POU负责所有跟硬件IO直接打交道的逻辑——急停链、安全门、气缸动作顺序、电机启停互锁。这些逻辑的特点是状态明确、时序要求严格、调试时需要在线上看到每个触点的通断。ST的POU负责配方计算、单位换算、报警阈值判断、数据记录、通信报文组装。这些逻辑的特点是运算密集、条件分支多、用图形画出来反而绕。
这种写法的好处是边界清晰。梯形图POU里不出现复杂的数学公式,ST的POU里不直接操作物理输出。调试的时候,梯形图部分可以在线监控每个网络,ST部分可以设断点单步执行。维护的人各看各的,互不干扰。
但这里有个容易忽略的细节:两个POU之间的数据交换必须通过全局变量或结构体,不能靠临时变量传递。我见过有人在梯形图POU里算了一个中间值,直接赋给一个局部变量,然后在ST的POU里想读这个值,结果读不到。原因是局部变量的生命周期只在本POU的扫描周期内有效。正确做法是定义一个全局结构体,比如stRecipe,梯形图里把需要传递的值写进这个结构体的成员,ST里从同一个结构体读。
2.2 写法二:同POU内混写,按网络切换语言
部分平台允许在同一个POU里,一部分网络用梯形图,另一部分网络用ST。这种写法看起来更紧凑,不用来回跳POU,但实际用起来限制不少。
首先,不是所有平台都支持。有的平台虽然菜单里能选,但混写之后编译会报奇怪的错误,或者在线监控时ST部分显示不正常。其次,即使平台支持,同一个POU内的变量作用域是共享的,梯形图网络里用的变量和ST里用的变量混在一起,命名冲突的风险大增。我踩过一次坑:梯形图里有个线圈叫Start,ST里有个局部变量也叫Start,编译通过了,但运行时梯形图的Start被ST的赋值覆盖,排查了半天才发现是重名。
同POU混写的另一个问题是扫描顺序。梯形图网络是按从上到下的顺序执行的,ST代码块在POU里的位置决定了它什么时候执行。如果你把ST块放在梯形图网络中间,那ST的执行结果会影响后面梯形图网络的判断。这个特性有时候有用,比如用ST算出一个条件再让梯形图去执行动作;但更多时候是麻烦,因为维护的人容易忽略执行顺序。
我的建议是:除非你的平台对同POU混写支持得非常好,并且你有明确的理由必须这么做,否则优先选分POU调用。分POU调用的调试体验和维护性都明显更好。
2.3 两种写法的对比表格
| 对比维度 | 分POU调用 | 同POU内混写 |
|---|---|---|
| 平台兼容性 | 几乎所有支持IEC 61131-3的平台 | 部分平台支持,部分平台有坑 |
| 变量作用域 | 各自独立,通过全局结构体交换 | 共享,重名风险高 |
| 执行顺序 | 由主程序的调用顺序决定,清晰 | 由POU内位置决定,容易忽略 |
| 在线调试 | 可分别监控,互不干扰 | 混在一起,监控界面可能混乱 |
| 代码复用 | ST函数可被多个梯形图POU调用 | 复用困难 |
| 适合场景 | 逻辑与运算分离明确的项目 | 小型项目,逻辑简单且平台支持良好 |
3. 梯形图里调用ST函数的实操细节
3.1 函数接口设计:参数怎么传
在梯形图里调用ST函数,最直接的方式是把ST函数定义成带输入输出参数的函数块(FB)或函数(FC)。梯形图里像调用普通功能块一样调用它,输入引脚接变量,输出引脚接线圈或下一个网络的输入。
这里的关键是接口设计。我见过有人把ST函数写成没有参数、直接读写全局变量的形式,然后在梯形图里调用。这样也能跑,但调试的时候你根本不知道这个函数改了哪些全局变量,出了问题只能一行行看ST代码。正确的做法是:所有输入通过输入参数传入,所有输出通过输出参数返回,函数内部不直接读写全局变量(除了必要的状态保持)。
举个例子,假设我要在梯形图里调用一个ST写的单位换算函数,把脉冲数转换成毫米。函数接口这样设计:
FUNCTION PulseToMM : REAL VAR_INPUT PulseCount : DINT; // 脉冲数 PulsePerRev : DINT; // 每转脉冲数 MMPerRev : REAL; // 每转毫米数 END_VAR VAR Temp : REAL; END_VAR IF PulsePerRev = 0 THEN PulseToMM := 0.0; // 防止除零 RETURN; END_IF Temp := DINT_TO_REAL(PulseCount) / DINT_TO_REAL(PulsePerRev); PulseToMM := Temp * MMPerRev; END_FUNCTION梯形图里调用时,PulseCount接编码器计数值,PulsePerRev和MMPerRev接配方参数,输出直接接比较指令或赋值给位置变量。这样接口清晰,换一个项目只要改参数,函数本身不用动。
3.2 数据类型匹配:最容易翻车的地方
梯形图里的数据类型和ST里的数据类型看起来一样,实际混编时经常出问题。最常见的是INT和DINT的混用。梯形图里一个计数器输出是INT,你把它接到ST函数的DINT输入引脚上,有的平台会自动转换,有的平台直接报类型不匹配。更隐蔽的是REAL和LREAL的精度问题,梯形图里比较两个REAL数相等,ST里算出来的结果因为浮点误差差了一点点,比较结果就是FALSE。
我的经验是:在ST函数的接口上明确标注数据类型,梯形图里连接之前先确认两边类型一致。如果平台支持隐式转换,也要在ST函数内部做显式转换,不要依赖平台的自动转换。比如上面那个函数,PulseCount我定义成DINT,如果梯形图里给的是INT,我会在梯形图里先做INT_TO_DINT转换再接入,而不是让ST函数去猜。
还有一个坑是BOOL数组和位变量的映射。梯形图里经常用M0.0、M0.1这样的位变量,ST里如果定义一个BOOL数组Status : ARRAY[0..15] OF BOOL,两者能不能直接对应取决于平台的内存映射方式。有的平台支持直接映射,有的需要显式赋值。我一般避免这种映射,而是在梯形图里用Status[0]这样的数组元素来替代M0.0,两边统一用数组,省得出问题。
3.3 执行时机:什么时候调用ST函数
梯形图是循环扫描执行的,每个扫描周期梯形图网络按顺序执行一遍。ST函数在梯形图里被调用时,也是在那个网络被执行到的时候才运行。这意味着ST函数的执行频率和梯形图网络一样,都是每个扫描周期一次。
但有些ST函数不需要每个周期都执行。比如配方计算,只在配方号改变的时候算一次就够了。如果每个周期都算,浪费CPU时间不说,还可能因为浮点运算的累积误差导致结果漂移。这时候可以用梯形图里的上升沿触点来触发ST函数的调用,或者用EN引脚控制。
还有一种情况是ST函数里有循环或延时操作。梯形图里调用这样的函数会阻塞整个扫描周期,导致看门狗报警。我踩过一次:在ST函数里写了一个WHILE循环等待某个条件成立,结果条件一直不成立,扫描周期从10ms涨到500ms,PLC直接停机。后来改成用状态机在梯形图里分周期处理,问题解决。所以ST函数里尽量避免循环等待,尤其是跟硬件状态相关的等待。
4. ST里调用梯形图功能块的写法与限制
4.1 把梯形图封装成功能块再调用
反过来,在ST里调用梯形图逻辑,通常的做法是把梯形图写成一个功能块(FB),然后在ST里实例化并调用。这个功能块的接口用梯形图的变量声明区定义,ST里像调用普通FB一样调用它。
这种写法适合什么场景?比如你有一个标准的气缸动作控制逻辑,用梯形图写好了,包含伸出、缩回、超时报警、互锁。现在有一个新的设备,动作顺序用ST的状态机来描述,但每个动作的具体执行想复用那个气缸功能块。这时候就在ST的状态机里调用气缸FB,传入使能信号和方向信号,读回状态和报警。
这里的关键是功能块的接口要设计得干净。输入只有使能、方向、超时时间这些参数,输出只有到位、报警、忙状态这些信号。功能块内部用梯形图实现,外部完全不用关心里面怎么写的。这样ST的状态机代码可以写得很简洁,每个状态调用对应的功能块,读返回值决定下一个状态。
4.2 调用时的实例化问题
在ST里调用梯形图FB,必须给每个调用点分配独立的实例。如果两个地方调用同一个FB但共用一个实例,两个调用的状态会互相干扰。我见过有人在ST里写了一个气缸FB的实例,然后在两个不同的状态里都调用这个实例,结果第一个状态还没执行完,第二个状态就把实例的状态覆盖了。
正确做法是每个物理气缸对应一个独立的FB实例。比如有四个气缸,就声明四个实例:
VAR Cyl1 : FB_Cylinder; Cyl2 : FB_Cylinder; Cyl3 : FB_Cylinder; Cyl4 : FB_Cylinder; END_VAR然后在ST代码里分别调用Cyl1(...)、Cyl2(...)。这样每个气缸的状态完全独立,互不影响。
4.3 梯形图FB里的定时器和计数器
梯形图FB里如果用了定时器(TON、TOF)或计数器(CTU、CTD),这些功能块也需要实例。在梯形图里写的时候,平台通常会自动分配实例,但封装成FB之后,实例是FB内部的静态变量,每个FB实例有自己的定时器。这个特性是好的,但要注意FB的调用周期必须稳定。如果ST里调用这个FB的周期不固定,定时器的计时就不准。
我一般会在ST里用一个固定周期的任务来调用这些FB,或者确保调用它们的ST代码在每个扫描周期都执行到。如果某个状态下才调用,那定时器只在那个状态下计时,这有时候是想要的效果,有时候不是,取决于你的逻辑设计。
5. 混编项目的变量管理与命名规范
5.1 全局结构体:混编的桥梁
分POU混编时,梯形图和ST之间的数据交换全靠全局变量。如果全局变量散落各处,今天加一个Temp1,明天加一个Flag2,用不了多久就没人记得每个变量是干什么的。我的做法是定义一个全局结构体,把所有跨POU交换的数据都放进去,按功能分组。
比如一个包装机项目,我定义这样的结构体:
TYPE stMachineData : STRUCT // 配方参数 RecipeNo : INT; SpeedSet : REAL; LengthSet : REAL; // 实时状态 ActualSpeed : REAL; ActualLength : REAL; PulseCount : DINT; // 报警 AlarmCode : WORD; AlarmActive : BOOL; // 命令 CmdStart : BOOL; CmdStop : BOOL; CmdReset : BOOL; END_STRUCT END_TYPE梯形图里用stMachineData.CmdStart这样的方式访问,ST里也用同样的方式。这样不管谁来看程序,都知道数据在哪里,不会到处找。
5.2 命名前缀:一眼看出变量归属
除了结构体,局部变量也要有命名规范。我的习惯是:梯形图POU里的局部变量加前缀LD_,ST POU里的局部变量加前缀ST_。这样在交叉引用里一眼就能看出这个变量是在哪里定义的,调试的时候不用猜。
比如梯形图里的一个中间继电器叫LD_Step1Done,ST里的一个循环变量叫ST_i。虽然平台可能不允许在变量名里用下划线开头,但中间加下划线一般没问题。这个习惯看起来小事,但在大型项目里能省很多排查时间。
5.3 避免在ST里直接写输出
这是我最想强调的一条经验:ST代码里尽量不要直接写物理输出。物理输出的写操作集中在梯形图里,ST只负责算出“应该输出什么”,然后通过全局变量告诉梯形图,由梯形图去执行实际的输出。
为什么?因为物理输出往往有互锁、急停、安全条件。这些条件用梯形图写最直观,也最容易验证。如果ST里直接写输出,一旦ST的逻辑有bug,可能绕过安全条件直接动作,风险很大。我见过一个项目,ST里直接控制了电机的启动输出,结果调试的时候ST的一个条件判断写反了,电机在安全门打开的情况下启动了,幸好是空载调试。
正确做法是:ST算出stMachineData.CmdMotorRun,梯形图里用这个变量加上安全条件、急停条件、互锁条件,最终驱动物理输出。这样安全逻辑始终在梯形图里,看得见摸得着。
6. 调试与验证:混编程序怎么排查问题
6.1 在线监控的局限性
混编程序在线监控时,梯形图部分可以看每个触点的通断,ST部分可以看变量值,但两者的监控界面通常是分开的。如果一个问题涉及梯形图和ST的交互,你需要在两个界面之间来回切换,很容易看漏。
我的做法是在关键交互点加“探针”。比如在梯形图调用ST函数之前,把输入参数复制到一组专门的监控变量里;ST函数返回之后,把输出也复制到监控变量里。这样在梯形图的监控界面就能看到ST函数的输入输出,不用切到ST界面。这些监控变量在正式版本里可以删掉,调试阶段非常有用。
6.2 强制变量的风险
调试时经常需要强制某个变量来看效果。混编程序里强制变量要特别小心,因为梯形图和ST可能同时读写同一个变量。你强制了一个值,梯形图扫描时把它改了,ST又读到了改后的值,结果跟你预期的不一样。
我一般只在确认某个变量只被一个POU读写时才强制它。如果必须强制一个共享变量,我会先把另一个POU里对这个变量的写操作临时屏蔽掉,强制完再恢复。这个操作有风险,正式运行的设备上绝对不要这么做。
6.3 扫描周期的影响
混编程序里,ST函数的执行时间会计入扫描周期。如果ST函数里有复杂的循环或大量的数学运算,扫描周期会明显变长。我一般会在ST函数里加一个执行时间测量,用平台的系统时钟读一个时间戳,函数结束时再读一次,差值就是执行时间。如果超过扫描周期的10%,就要考虑优化或者拆分到多个周期执行。
还有一个隐蔽的问题是ST函数的执行时间不稳定。比如一个FOR循环,循环次数取决于某个变量,这个变量在运行时变化,那ST函数的执行时间就跟着变。如果最坏情况下执行时间超过看门狗设定值,PLC就会停机。所以ST函数里的循环次数最好有上限,或者用状态机分周期处理。
7. 从实际项目里总结的几条硬经验
第一条:混编的边界要在项目开始时就划好,不要写到一半才决定哪部分用ST哪部分用梯形图。我一般会在纸上画一个简单的框图,标出哪些功能块用梯形图,哪些用ST,它们之间怎么交换数据。这个框图不用很正式,但要有,不然写到后面容易乱。
第二条:ST函数的接口要稳定。一旦梯形图里调用了某个ST函数,函数的输入输出参数就不要轻易改。改一个参数类型,所有调用点都要跟着改,漏一个就编译不过。如果确实要改,先在ST里加一个新的函数,梯形图逐步迁移,迁移完再删旧的。
第三条:梯形图里的ST调用块要加注释。梯形图里调用ST函数时,那个方块上只有函数名和引脚,看不出里面在干什么。我习惯在方块上面加一行注释,写清楚这个函数做什么、输入输出是什么含义。维护的人不用打开ST代码就能知道大概。
第四条:版本管理要跟上。混编程序的文件比纯梯形图或纯ST多,改动的频率也高。我一般用平台的版本对比功能,每次改完对比一下,确认只改了预期的地方。如果没有版本对比功能,至少要把改前的程序另存一份,出问题可以回退。
第五条:不要为了混编而混编。有些逻辑用梯形图写很清楚,就不要硬搬到ST里;有些运算用ST写很简洁,就不要硬画成梯形图。混编的目的是让程序更好维护,不是展示技术。我见过一个项目,所有逻辑都用ST写,梯形图里只有一个空的主程序调用ST,结果电气维护的人完全看不懂,每次改点动逻辑都要找程序员。这就是过度混编的反面教材。
最后说一个我自己的习惯:每次混编项目完成后,我会写一份简单的“语言分布说明”,一页纸,列出每个POU用什么语言、负责什么功能、跟哪些POU交换数据。这份说明不放进程序里,但交给维护的人,他们接手的时候能快速理解程序结构。这个习惯坚持了几年,反馈一直不错。