1. 为什么要在ST和梯形图之间做混编第一次接触ST和梯形图混编是在一个包装线改造项目上。当时设备要增加一段配方管理逻辑用梯形图写状态机已经画了满满三屏维护的师傅每次改参数都要在几百个网络里翻找改一次骂一次。后来我把配方计算和字符串处理的部分抽出来用ST写逻辑部分保留梯形图整个程序从三屏缩到一屏半改参数只需要在一个结构体里改数值。那次之后我就开始认真琢磨这两种语言混编的边界到底在哪里。STStructured Text结构化文本和梯形图Ladder DiagramLD是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里用的变量混在一起命名冲突的风险大增。我踩过一次坑梯形图里有个线圈叫StartST里有个局部变量也叫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涨到500msPLC直接停机。后来改成用状态机在梯形图里分周期处理问题解决。所以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_Step1DoneST里的一个循环变量叫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交换数据。这份说明不放进程序里但交给维护的人他们接手的时候能快速理解程序结构。这个习惯坚持了几年反馈一直不错。
阅读完成 · 觉得有帮助?