资讯动态

Cx-Programmer中ST语言编程实战:从梯形图到结构化文本

发布时间:2026/10/21 2:01:41 来源:尧图企业网站定制
在Cx-Programmer里用ST语言写PLC程序这事我做了好几年了。刚开始从梯形图转过来时我也觉得那一片文本没有梯形图直白可真到了数据处理密集、逻辑分支多的场合梯形图的线圈和触点堆在一起改一行要翻好几页ST的优势一下就出来了。这篇文章不打算讲那种通篇堆砌概念的教材内容就把我在Cx-Programmer里实际创建ST程序、写逻辑、编译调试的经验拆开讲哪些语法必须注意、功能块怎么和梯形图配合、编译报错怎么快速定位。无论你是刚接触结构化文本的新手还是长期用梯形图想拓展技能的老工程师都可以参考。1. 为什么在Cx-Programmer里我会推荐用ST语言1.1 Cx-Programmer的编程语言生态里ST排在什么位置Cx-Programmer支持IEC 61131-3标准下的多种编程语言梯形图、指令表、结构化文本(ST)、顺序功能图(SFC)。实际项目里用得最多的是梯形图这是事实因为梯形图直观现场维护的时候电工师傅也看得懂。但IEC标准里定义ST语言本来就不是为了取代梯形图而是为了补上梯形图不擅长的短板。梯形图的本质是图形化表达开关逻辑触点串联并联、线圈输出一眼看过去很清楚。但遇到复杂计算、数组处理、循环遍历、协议解析这类任务图形化的表达方式就显得特别笨重。比如要算一组数据的平均值梯形图可能得拉好几行比较和运算指令可读性很差换成ST一个FOR循环加上几行赋值语句就写完了别人看代码时也能很快理解。ST在Cx-Programmer里的定位就是“高级语言工具”。它允许你用类似Pascal或C语言的结构化写法组织逻辑支持变量声明、条件分支、循环、功能块调用。它不是万金油但确实是很锋利的一把刀。我现在遇到稍微复杂的项目基本上都是梯形图和ST混合着用什么时候用哪个后面专门展开说。1.2 ST真正擅长的典型任务结合我自己的项目经验下面几类场景在Cx-Programmer里用ST写会舒服很多。第一类批量模拟量处理。设备上有十几路甚至几十路温度、压力、流量信号每路都要做工程量换算、滤波、超限报警判断。用梯形图的话每一路的处理逻辑都要复制一遍改滤波系数得改十几处用ST的话把传感器数据放到数组里一个循环就处理完了想改参数就改一处。第二类配方与参数管理。多品种设备的配方参数往往几十上百个需要按产品号切换把一组配方数据从数据块搬到工作区。这种批量数据搬移和结构体赋值梯形图写起来非常痛苦ST里一个FOR循环或者一条结构体赋值就搞定。第三类通信协议解析。和变频器、仪表、扫码枪做串口或总线通信时收到的报文是一串字节要解析帧头、命令字、数据段、校验和。这种按字节处理的逻辑梯形图很难下手ST可以像写高级语言一样把缓冲区拆开处理。第四类状态机逻辑。设备有一个动作流程比如上料、定位、夹紧、加工、松开、退回每一步有进入条件、执行动作、超时保护。ST里的CASE语句天生就是写状态机的好工具每个状态一目了然比梯形图的步进继电器方案清晰得多。1.3 梯形图与ST的配合边界我现在的习惯是安全逻辑和操作逻辑用梯形图计算和数据处理用ST。比如急停回路、门锁联锁、手动自动切换这些必须让现场维护人员一眼能看懂的地方用梯形图故障原因判断、模拟量换算、参数匹配这类藏在背后的逻辑用ST。举个例子。一台设备的手动模式按钮按下就启动电机梯形图里就是触点串线圈清清楚楚但按下启动按钮之后设备要判断当前温度是否在允许范围、气压是否足够、前一个工位是否已经让出位置这三个条件还要经过滤波和迟滞处理再决定电机能不能转。这部分判断逻辑放梯形图里会很乱放ST里只需要一个IF条件判断或者一个功能块的输入输出计算。这样搭配的好处是现场看梯形图时看到的是“按下启动按钮满足综合条件后启动电机”背后的综合条件哪里算出来的在ST里查改计算逻辑时不用去梯形图里翻继电器直接在ST里改算法两边都不折腾。2. 从零创建一个可运行的ST程序2.1 新建ST程序单元的最小操作路径在Cx-Programmer里新建ST程序操作不算复杂但有几个地方容易踩坑。打开工程树找到目标PLC节点下的“程序”目录右键选择插入程序。弹出的对话框里会让你选语言类型这里选择“结构化文本(ST)”然后给程序起个名字。程序名不要跟PLC里的全局符号、I/O注释重名这个我吃过亏编译的时候一堆人查不出来为什么符号冲突最后发现是名字撞了。新建完之后编辑器窗口会变成一个文本编辑区。ST程序和梯形图程序不一样梯形图是图形化拖拽的ST就是纯文本输入。代码从变量声明开始写写完逻辑之后点编译按钮检查语法。还有一点要注意Cx-Programmer的不同版本、不同CPU机型对ST的支持程度有差别。老一点的软件版本或者入门级CPU可能只在程序类型里允许梯形图选不了ST。新建之前先确认一下目标机型支持不支持ST别辛辛苦苦写完了下载时才发现机型不支持。2.2 变量声明分清VAR、VAR_INPUT和VAR_OUTPUTST代码的第一部分是变量声明。在Cx-Programmer的ST程序里最常用的声明区是VAR和END_VAR之间的内容。这样写VAR nCount : INT; fValue : REAL; bDone : BOOL; tDuration : TIME; END_VAR这里的nCount、fValue这些变量是这个ST程序内部的局部变量只在当前程序里有效。每个变量后面是冒号加数据类型整行以分号结束。如果你在写功能块这时候变量的分类更讲究。功能块的输入接口用VAR_INPUT声明输出接口用VAR_OUTPUT声明内部中间量用VAR声明。普通程序里一般不强制分输入输出但功能块必须分清楚因为梯形图调用功能块时看的就是这几个接口。还有一个很重要的概念全局变量。在Cx-Programmer的符号表里定义的全局变量ST代码里可以直接用不用再在VAR里声明。比如你在符号表里定义了一个全局BOOL变量叫StartSignalST里直接写IF StartSignal THEN就行。但是如果ST里声明了一个和全局变量同名的局部变量局部变量会覆盖全局变量这个优先级容易把人搞晕建议尽量避免同名。2.3 数组、结构体与数据类型转换ST处理批量数据的核心能力来自数组和结构体。数组声明的基本格式是VAR arrTemp : ARRAY[0..15] OF REAL; END_VAR这样声明了一个下标从0到15、共16个元素的REAL数组。使用时直接赋值arrTemp[0] : 25.5;。在ST循环里遍历数组比梯形图逐个复制不知道高到哪里去了。结构体在Cx-Programmer里需要在工程的数据类型管理器里先建好类型然后在ST里引用。比如定义一个表示三维坐标的结构体类型PositionType包含X、Y、Z三个REAL字段然后在ST里声明变量VAR currentPos : PositionType; END_VAR使用时写currentPos.X : 100.0;。结构体很适合管理一组相关数据比如一个轴的位置、速度、加速度、跟随误差打包成一个结构体变量整个功能块传参都方便。数据类型转换这块ST里经常踩坑。INT和REAL不能直接随便混用比如把一个INT变量赋给REAL变量或者INT除以INT想得到小数编译时会报类型不匹配。这时候需要显式转换nInt : REAL_TO_INT(fValue); fValue : INT_TO_REAL(nInt);把REAL转INT的时候要注意精度问题。不同版本对取整规则处理有细微差别涉及关键控制量时先拿几个边界值实测一下再决定用不用这种方式。3. ST核心语法逐个过3.1 赋值、比较与运算符别把和:搞混ST里赋值操作符是:比较操作符是。这跟C语言完全不一样新手最容易在这里翻车。写赋值时少写一个冒号编译直接报错更隐蔽的是你可能本来想写比较结果写了赋值逻辑上不对还编译能过。正确的用法是这样fOutput : fInput * 2.0; (* 赋值 *) IF nMode 1 THEN (* 比较 *) bFlag : TRUE; END_IF;ST的逻辑运算符是AND、OR、NOT、XOR不是C语言里的、||、!。多条件判断时可以这样写IF bEnable AND NOT bFault THEN runMotor : TRUE; END_IF;数学函数可以直接调用比如ABS、SQRT、SIN、COS。计算平方根直接写SQRT(x)绝对值写ABS(x)。这些函数在Cx-Programmer的ST里是内置的不用额外导入。实数比较有一个经典坑两个浮点数直接判等很可能因为精度问题永远不成立。比如计算结果是1.0但实际存储可能是0.99999999。安全做法是比较差值IF ABS(fValue - 1.0) 0.001 THEN bValid : TRUE; END_IF;这种做法在做模拟量阈值判断时非常实用相当于加了一个死区。3.2 条件分支IF与CASE怎么选IF语句处理的是“条件是否成立”的分支适合条件不多、每个条件优先级不同的场景IF bEnable AND nMode 0 THEN nResult : 10; ELSIF bSecond THEN nResult : 20; ELSE nResult : 0; END_IF;注意ELSIF的拼写不是ELSEIF这个拼错也是常见报错原因。CASE语句则适合对同一个变量的不同取值做分发尤其是状态机。写法是这样CASE nStep OF 0: bIdle : TRUE; 1: bRun : TRUE; 2, 3: bReady : TRUE; ELSE bAlarm : TRUE; END_CASE;CASE后面的表达式只能是整数类型或枚举类型不能是REAL。分支值可以用逗号并列多个比如上例里的2, 3。我强烈建议每个CASE都写上ELSE兜底否则万一跑到没定义的分支输出状态会保持上一个周期的值容易出问题。什么时候用IF什么时候用CASE我的经验是条件超过两三个、而且都是针对同一个变量的取值优先CASE条件是不同变量的组合判断用IF。比如“温度过高或压力不足且不在手动模式”这种复合条件只能IF。3.3 循环结构FOR与WHILE的正确用法FOR循环适合次数确定的遍历FOR i : 0 TO 15 DO arrFilter[i] : arrRaw[i] * 0.9 arrFilter[i] * 0.1; END_FOR;这条代码就是对一个16元素的数组做一阶低通滤波梯形图写这样的逻辑起码要几十条指令。FOR循环还支持步长。比如把一个数组的每个元素往后移一位方便做历史数据记录FOR i : 15 TO 1 BY -1 DO arrData[i] : arrData[i - 1]; END_FOR;这里的BY -1表示每次减1是IEC标准ST的写法。WHILE循环用于次数未知、靠条件退出的场景WHILE nPos 32 DO bBuffer[nPos] : pattern; nPos : nPos 1; END_WHILE;说一个极其重要的坑ST里的循环是在PLC扫描周期内执行的不像电脑上跑程序那样有独立的时间片。如果在Cx-Programmer里写了一个没有退出条件的死循环整个扫描周期会被卡住PLC的看门狗超时CPU直接报警停机。这个后果很严重现场生产会被打断。所以写WHILE循环一定要确保退出条件在有限步骤内必然满足最好再加上安全上限nGuard : 0; WHILE nPos 32 AND nGuard 100 DO bBuffer[nPos] : pattern; nPos : nPos 1; nGuard : nGuard 1; END_WHILE;这样即使逻辑有意外循环也不会无限执行下去。3.4 在ST中调用定时器TON和计数器CTUD定时器在ST里的用法跟梯形图差别很大。梯形图里TON是一个指令块用触点驱动ST里它变成了一个功能块实例你要先声明实例再按函数调用的方式调它。声明定时器实例VAR fbDelay : TON; bDelayDone : BOOL; etDelay : TIME; END_VAR调用方式fbDelay(In : bInput, Pt : T#2s, Q bDelayDone, ET etDelay);这里In是触发条件Pt是预设时间T#2s表示2秒。Q是定时器的输出ET是当前计时值。用把功能块的输出接到变量上。引脚名的大小写不同版本显示可能有细微差别以你工程里实际显示的为准常见写法是In、Pt、Q、ET。有一个我踩过的坑定时器在ST里不能像梯形图那样看到定时器触点你必须用Q输出的BOOL变量去写后续逻辑。还有如果In一直为TRUE定时器计时到之后不会自动复位它会保持Q为TRUE直到In变成FALSE。用定时器做报警延时、启动延时一定要想清楚复位条件。计数器也是同样思路。声明一个CTUD实例然后调用fbCount(CU : bUp, CD : bDown, R : bReset, PV : 100, Q bOver, CV nCurrent);CU是加计数触发CD是减计数触发R是复位PV是预设值Q是到达预设值输出CV是当前值。功能块实例名在整个PLC里要唯一不同程序段不能共用同一个实例名。举个比较完整的综合例子一个带超时保护的两步启动流程CASE nStep OF 0: IF bStart THEN nStep : 1; fbDelay(In : FALSE, Pt : T#5s, Q bTimeUp, ET etTime); END_IF; 1: fbDelay(In : TRUE, Pt : T#5s, Q bTimeUp, ET etTime); IF bTimeUp THEN nStep : 2; END_IF; 2: bFinish : TRUE; END_CASE;注意在步骤0里面已经把定时器的In置为FALSE所以当进入步骤1时定时器是从零开始计时的。如果不在步骤切换时复位定时器超时输出可能直接就为TRUE状态机会瞬间跳变这是ST写状态机最常见的隐形BUG。4. 功能块封装与梯形图协作4.1 用ST写一个功能块声明接口是第一步功能块是ST语言里复用性最强的组件。在Cx-Programmer里右键工程树下的“功能块”节点选择插入功能块名称比如FB_MotorCtrl语言选ST。功能块编辑器里通常分接口声明区、变量声明区和逻辑代码区。接口区的VAR_INPUT是外部输入VAR_OUTPUT是外部输出VAR是内部变量。比如写一个电机控制功能块VAR_INPUT bRunCmd : BOOL; bFault : BOOL; END_VAR VAR_OUTPUT bRunning : BOOL; bError : BOOL; END_VAR VAR fbDelay : TON; bDelayDone : BOOL; END_VAR逻辑部分可以这样写IF bRunCmd AND NOT bFault THEN bRunning : TRUE; bError : FALSE; ELSE bRunning : FALSE; bError : bFault; END_IF;接口声明为什么重要因为功能块封装的就是对外契约。别人调用这个功能块时只看到输入引脚和输出引脚内部怎么写完全不影响外面的逻辑。这样写出来的代码可以像搭积木一样复用到多个设备上。4.2 在梯形图里挂接ST功能块ST功能块写好了不一定全部逻辑都在ST里写。很多时候你需要在梯形图里调用这个ST功能块把按钮、传感器信号接进去把输出接给接触器、指示灯。操作方式不复杂从功能块列表里把FB_MotorCtrl拖到梯形图的某一条回路上然后给它起一个实例名比如Motor1。实例名是这个功能块在项目里的具体身份同一个功能块类型可以创建多个实例每个实例的内部变量互相独立。就像同一张图纸造了多台机器每台机器都有自己的状态。拖进梯形图之后功能块会显示成带引脚的一个方块。把输入引脚bRunCmd连接到启动按钮的常开触点bFault连接到故障信号输出引脚bRunning连接电机接触器线圈bError连接故障指示灯。连接时要注意引脚类型BOOL引脚接BOOL量如果接错类型编译会报错。功能块内部如果用了定时器定时器的实例要声明在功能块的VAR区而不是放在全局。这样可以保证每个电机实例有自己独立的定时器互不影响。4.3 内置函数与自建函数的使用Cx-Programmer的ST内置了一批常用函数可以直接调用类型转换的INT_TO_REAL、REAL_TO_INT数学计算的ABS、SQRT、MIN、MAX还有比较和限幅的LIMIT函数。这些函数返回单一值不需要声明实例随用随调非常方便。比如要把一个模拟量从0到10V的原始值转换成0到100的百分比可以直接写fPercent : LIMIT(0, (fVoltage - 0.0) / 10.0 * 100.0, 100);LIMIT函数接收三个参数下限、原值、上限返回值会被限制在这两个界限之间。做模拟量处理时防止数值越界很好用。如果你用的Cx-Programmer版本支持自定义函数也可以自己写。函数和功能块的区别是函数没有内部记忆同样输入永远得到同样输出适合做纯计算。结构大致是这样FUNCTION FC_Scale VAR_INPUT fRaw : REAL; fMin : REAL; fMax : REAL; END_VAR VAR END_VAR FC_Scale : (fRaw - fMin) / (fMax - fMin) * 100.0; END_FUNCTION调用的时候直接函数名加名字参数fPercent : FC_Scale(fRaw : 1.5, fMin : 0.0, fMax : 5.0);自定义函数在版本兼容性上不如功能块稳如果你只需要封装一段计算逻辑而且不想折腾函数模板的格式直接用功能块也行。功能块还允许你保留内部状态做滤波、累计这类需要记忆的计算反而更灵活。5. 编译错误与调试实战5.1 我工作中常见的四类编译错误ST编译报错看多了之后会发现就那么几类。我整理成了一张速查表错误原因典型表现解决办法赋值写成编译报赋值语法错误赋值一律用:变量未声明或拼写不一致报未定义符号代码里找不到对应声明检查变量名、大小写、全局符号是否一致类型不匹配报表达式类型错误比如INT赋给REAL用INT_TO_REAL或REAL_TO_INT显式转换语句结尾没写分号/括号不匹配报语法错误定位到某一行附近补分号、核对括号配对CASE分支值类型不符编译直接提示CASE类型错误确保CASE表达式和分支值都是整数类型还遇到过一种情况报错行号跟实际问题对不上。比如嵌套了好几层IF和CASE编译器只把错误定位到某个END_IF附近实际上真正写错的地方在几十行前面。这时候不要死盯着报错行往上一段一段看尤其是检查每个赋值语句有没有漏写分号、每个END_IF是不是配对。5.2 在线监视与强制变量的调试思路Cx-Programmer对ST的在线调试没有高级语言那种断点单步的功能但基础的在线监视是有的。程序下载后进入监视模式ST编辑器里能看到每个变量的当前值。不过一次看很多变量不太方便我一般把关键输出变量添加到监视窗口或者直接在程序里建立一个调试用的临时变量把它们收到一个数组里方便一起观察。更实用的方法是强制变量。找到问题后给某个输入条件强制成TRUE或FALSE或者给某个数值强制成特定值然后观察后续逻辑的输出变化。比如怀疑温度报警阈值判断有问题就直接把温度变量强制到阈值附近看报警输出是不是正确翻转。还有一个土办法我管它叫探针变量。在ST里临时加一个INT变量每扫描周期加1下载运行后观察它是否在持续增长。如果探针变量增长正常说明程序在正常扫描循环问题出在逻辑本身如果探针变量不动了说明某个分支或者循环可能把扫描卡住了。排查ST程序卡死问题这个办法特别有效。查完记得把探针变量删掉。5.3 扫描周期、任务类型与ST代码的性能原则ST代码是在PLC的周期任务里执行的一个扫描周期内代码从第一行执行到最后一行然后又开始下一轮。这就带来几个需要注意的点。第一循环次数一定要控制。ST里写个FOR循环遍历一千个元素如果每个周期都从头到尾跑一遍扫描周期会被明显拉长影响整体响应。处理大规模数据时我习惯分批做。比如128个数据分8批每周期处理16个用一个批次计数器记录当前处理到哪一组这样一个周期里ST只跑一小部分扫描周期波动小很多。第二中断任务里的ST要特别精简。Cx-Programmer支持把程序挂到中断任务下中断程序里的代码执行频率高不适合做数组循环、大批量计算更不能调用耗时长的功能块。中断里最好只做输入捕捉、输出置位复位这类短小操作其他繁重计算放到周期任务里做。第三ST里不能有动态内存分配、不能有无限等待所有逻辑必须在一个扫描周期内有明确的进出口。写代码的时候要时刻记住这个程序每几十毫秒就要从头到尾跑一遍跟PC上跑一次就结束的控制台程序完全不同。我之前遇到过一次CPU看门狗超时最后定位到就是WHILE循环里的退出条件因为一个变量被误修改导致循环退不出来整个PLC扫描周期被拖死。从那以后我写ST循环一律加保护上限宁可在逻辑上多写两行也不想再让现场冒一次停机的风险。功能块里的状态变量、定时器实例也都消耗CPU内部资源数量不是越多越好。项目里如果需要上百个功能块实例要考虑CPU的内存容量和扫描周期要求别把资源用满。另外说一个在线修改的习惯。Cx-Programmer支持在线修改程序但ST代码的在线修改限制比梯形图严格改动量大时可能会要求重新下载整个程序。重新下载意味着PLC停机现场如果有生产任务一定要提前确认停机窗口。我在现场都是先在离线工程里把ST代码改好、编译通过、模拟验证过再挑设备空闲时段下载。关于ST语言和Cx-Programmer的配合我最后再分享一个个人习惯拿到一个新的ST程序段我会先搭一个能编译的空骨架包括变量声明和一条最简赋值语句编译通过后再一点一点往里面填逻辑。每填一小段就编译一次让错误暴露得越早、越小后面排查越省力。这种习惯看起来慢实际效率比憋一个大块头再编译高得多。Cx-Programmer的ST编辑器有语法高亮但行号提示偶尔会偏差真出错时宁可手动对照源代码也别完全依赖错误跳转。ST在Cx-Programmer里说到底是为了解决梯形图不擅长的那些问题。我的原则很明确安全回路和操作习惯相关的逻辑留在梯形图里计算、数组、状态机、协议解析这些交给ST。用熟了之后你会发现复杂逻辑写起来舒服维护起来也清楚。希望这些经验能帮你少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑