☰
ST编程语法精讲:从梯形图到结构化文本的工控实战
2026/10/20 13:04:13 网站建设 项目流程

ST编程语法这东西,说实话刚接触的时候很多人会有点发怵。毕竟一提PLC,大家脑子里浮现的都是梯形图那种继电器电路的样子,密密麻麻的触点线圈,电工老师傅看得津津有味。但一旦项目里要写PID、要做配方管理、要解析通信报文、要处理上百个模拟量数据,梯形图那点体面就撑不住了。这时候ST(Structured Text,结构化文本)就该上场了。它是IEC 61131-3标准里五大编程语言之一,语法风格跟Pascal、C这类高级语言很接近,本质上就是用文本来写PLC逻辑。

这篇东西不打算给你念教科书,我直接按这些年实际跑项目踩出来的经验,讲清楚ST语法到底怎么用、和梯形图怎么分工、哪些环节最容易翻车。不管你是刚接触PLC的新手,还是从梯形图转ST的老工程师,看完应该都能少走不少弯路。

1. 为什么值得学ST语法

1.1 ST不是梯形图的替代品,是另一个工具箱

很多做电气控制的朋友一听到“用ST写程序”就摇头,觉得梯形图是PLC的根,文本化编程太“软件化”,看不懂也不放心。但说句实在话,ST能成为IEC 61131-3的官方语言,是有它不可替代的位置的。

IEC 61131-3标准定义了五种PLC编程语言:指令表IL、梯形图LD、功能块图FBD、顺序功能图SFC和结构化文本ST。这五种不是互相淘汰的关系,而是按场景分工。梯形图和功能块图走的是“图形化直观路线”,适合继电器电路逻辑和简单的信号流处理。ST走的则是“算法化精确路线”,特别擅长连续多步计算、循环处理、复杂判断分支。举个例子,你要对一百个温度数据进行平均值计算,再判断最大值最小值落在哪个区间。梯形图想干这活,得写一大堆中间变量和比较网络,看得人头大。ST里几行循环加条件判断就完事,注释写清楚,别人维护也轻松。

所以ST的角色更像“PLC里的C语言”,它不是要替代我说的梯形图,而是补上图形化语言在复杂逻辑处理上的短板。越大的项目、越复杂的设备,ST占比越高。现在新出的很多控制系统里,ST已经是标配,有些场合甚至不允许写纯粹的梯形图,全工艺都跑ST。

1.2 梯形图和ST该怎么分工

我个人在实际项目里的分工原则很粗暴:逻辑要素是“一通一断”的开关量互锁、启停、手自动切换,就用梯形图,因为电气人员看得明白,现场排查也方便。一旦涉及公式计算、单位换算、数据搬运、循环查找、数组操作、状态机跳转,立刻切ST。

举个例子,一台包装机的控制,真空电磁阀、气缸、伺服轴的硬接线逻辑我全用梯形图写,但一个生产批次的数据统计、不同配方的参数装载、根据产品长度动态计算切刀位置,这些绝对用ST。梯形图里做浮点运算时,那一堆功能块连线能缠死人,而且公式变动一次就要重新拖线,维护成本太高。用ST的话,改个系数、加个括号,几秒钟的事。

从调试角度来看,梯形图在线监控接触点和线圈确实直观,但ST的在线监视能看到变量实时值和每行代码的执行状态,写算法的时候比梯形图好追踪得多。两者的工具属性不同,不是谁取代谁的关系。

1.3 哪些场景必须用ST

ST基本是“躲不掉”的。我随便列几个实战中遇到过的场景,你看看有没有共鸣:

  • 通信报文解析:很多仪表和设备走串口或以太网通信,返回的报文是字节数组。你要把4个字节拼成一个浮点数,再按位取某个状态字里的一个BIT,这种处理用梯形图写那叫一个痛苦,ST里位移加或运算分分钟解决。
  • 配方管理:一道工艺有几十个参数,存到结构体数组里,生产时按产品型号装载。用ST可以优雅地通过数组索引和FOR循环批量复制,梯形图你要是能用功能块搭出来,我佩服你的耐心。
  • PID调节与温度控制:虽然多数PLC有PID指令块,但实际项目里经常要做抗积分饱和、误差变化率限制、输出限幅等处理。这些逻辑写ST就像写普通高级语言一样顺,写成独立的功能块,多个回路反复调用。
  • 状态机控制:全自动设备的工序切换、多工位协同、异常暂停和复位,这是ST的看家本领。用CASE语句做状态跳转,逻辑清晰,扩展方便。

所以不管你是哪个行业的自动化工程师,只要设备复杂度上来了,ST早晚要学。早一点掌握,项目里能少受很多罪。

2. ST语法核心要素拆解

2.1 变量声明:ST的骨架

ST程序跟C语言一样,先声明后使用。常见的声明区域有这么几种:VAR是内部变量,VAR_INPUT是输入参数,VAR_OUTPUT是输出参数,VAR_IN_OUT是既能读又能写的参数,VAR_GLOBAL是全局变量,VAR_TEMP是临时变量。

VAR_INPUT 启停信号 : BOOL; 目标温度 : REAL; 设定压力 : REAL; END_VAR

VAR_OUTPUT 加热输出 : REAL := 0.0; 运行状态 : BOOL := FALSE; END_VAR

VAR_TEMP 温差 : REAL; 计数 : INT; END_VAR

VAR CONSTANT 最大温度 : REAL := 120.0; 设备编号 : STRING := ‘PACK-01’; END_VAR

这里有个容易忽略的细节:VAR_TEMP里的变量是临时存储,每次扫描周期执行完就释放。同一个功能块被多个实例调用时,TEMP变量之间不会互相隔离,容易出现“疑似数据错乱”的问题。所以跨周期要保持的中间值不要放VAR_TEMP,老老实实用VAR或者功能块的静态变量区。另外VAR_IN_OUT传进去的根本不是参数的拷贝,而是引用本身,功能块内部改了它,外面跟着变。写程序时要清楚这一点,否则容易造成莫名其妙的“外部变量被篡改”事故。

2.2 基本数据类型:先搞清楚字节和字

ST的数据类型比梯形图里的“位、字、双字”要细得多。最基础的BOOL是位,然后是BYTE、WORD、DWORD这些无符号类型,再有SINT、INT、DINT、LINT带符号整数,以及REAL、LREAL浮点数。很多人刚转过来时容易踩的第一个坑,就是把WORD直接拿来当整数用,结果比较大小的时候全是错的。原因很简单:WORD是无符号字,你不把它转换成INT,解释出来的数值和你想的完全不是一回事。

举个例子,某通信协议里读回一个状态寄存器,它的原始数据是WORD类型,里面的0~3位是内部状态。你想判断某个位的开关,就必须做位运算,比如状态字 AND 16#0008 不等于0,就代表第3位是1。但如果你把这WORD直接用“>”比较,就会犯低级错误。

浮点类型里,我基本默认用REAL就够了,单精度7位有效数字在绝大多数工艺控制里绰绰有余。但涉及大数累加、累积量结算(比如流量累计)时,一定要用LREAL,否则累到一定数值后,REAL会因为精度不够把增量“吃掉”,做计量就是事故。

2.3 运算符和表达式:ST的计算肌肉

ST的运算符分几组:算术运算符有加减乘除和MOD取余,比较运算符有=、<>、<、>、<=、>=,逻辑运算符有AND、OR、NOT、XOR,位运算符有SHL、SHR、ROL、ROR,还有赋值运算符“:=”。这里必须强调一下,ST里的“=”是比较相等,不是赋值。写惯了C语言的人最容易在这翻车,想赋值打一个等号,系统直接报错或者产生完全不同含义的表达式。ST赋值一定要用冒号等号。

再有就是整除和取余的坑。比如两个整数相除,INT除法得到的结果还是整数商,小数直接舍去。你想得到4.5这种结果,必须把至少一个操作数转成REAL再除,否则算出来的就是4。MOD取余要注意负数的情况,结果的正负号和被除数保持一致。温度控制里经常要用MOD做分段处理,比如说把时间戳转换成某周期内的相对位置,忘了负数取余的特性,一不留神就出现负索引。

ST里还支持AND_THEN、OR_ELSE这种短路求值运算符,左边条件不满足时,右边根本不会执行。这个特性特别适合做“先判空再处理”的防御式编程。比如数组下标是否越界的校验,如果不用短路求值,判断数组元素时已经越界访问,程序直接崩了。

2.4 程序控制语句:ST的逻辑骨架

ST最常用的控制语句是IF条件语句和CASE多分支语句,还有FOR、WHILE、REPEAT三种循环。

IF语句跟其他语言的套路差不多,支持ELSIF——注意是ELSIF,不是ELSEIF,少个E。很多人从别的语言转过来,在编辑器里打ELSEIF,结果编译报错找了半天。CASE语句跟C里的switch很像,但写法差异很大,ST里CASE数值后面跟着语句组,用冒号分隔,END_CASE收尾。

FOR i := 0 TO 99 DO sum := sum + dataArray[i]; END_FOR

WHILE 条件 DO ... END_WHILE

REPEAT ... UNTIL 条件 END_REPEAT

FOR循环默认步进是1,指定步长用BY,比如FOR i := 100 TO 0 BY -2 DO,可以递减。WHILE是条件满足就执行,REPEAT是先执行一次再判断条件,两者正好相反。实际编程里我建议循环体不要写太长,每次扫描周期循环几百次可能没事,几万次就会拖慢扫描周期。之前帮同事排查过一台检测设备,PLC里有个循环去遍历一个几百KB的数组做校验,结果扫描周期直接被拖到100多毫秒,用了定时分片处理才把周期压回10毫秒以内。这个问题在后文还会细说。

EXIT语句可以跳出当前循环,RETURN可以直接结束当前程序或功能块的执行。在初始化分支里,某些前置条件不满足时直接RETURN,可以避免后半段大量代码白跑,是个很实用的习惯。

2.5 函数与功能块:ST的复用核心

ST里有两个重要的复用单位:函数(FUN)和功能块(FB)。函数没有内部状态,同样的输入永远产生同样的输出,适合做纯计算,比如单位转换、滤波计算、校验和计算。功能块有内部存储,能保持上次调用后的数据,适合做有状态的逻辑,比如电机控制块、PID控制器、状态机。

功能块的调用方式很讲究:要先声明实例,然后在代码里给它的输入输出赋值。声明实例就像C里声明一个对象,同一个电机控制块你声明两次,就是两个完全独立的“电机对象”。很多刚学的朋友没有实例化的概念,直接在程序里写FB的名字,编译报错才意识到要先“创建对象”。

FUNCTON_BLOCK FB温度控制 VAR_INPUT 实际值 : REAL; 设定值 : REAL; END_VAR VAR_OUTPUT 输出值 : REAL; END_VAR VAR 上次误差 : REAL; 积分累计 : REAL; END_VAR

方法体里就可以写PID增量计算了,积分累计这个静态变量在每次扫描周期之间是保留的,这就是功能块和函数最本质的区别。

3. ST代码实战:从简单到复杂

3.1 一个简单的星三角启停逻辑

说了半天理论,还是上手写一段实际代码。假设有个风机控制,有启动按钮、停止按钮、过载保护,启动后先星形接法运行3秒,再切换到三角形接法。传统梯形图写这个要几个定时器加互锁网络,ST照样能干净利落地实现。

VAR_INPUT 启动按钮 : BOOL; 停止按钮 : BOOL; 过载保护 : BOOL; END_VAR VAR_OUTPUT 星形接触器 : BOOL; 三角形接触器 : BOOL; 主接触器 : BOOL; END_VAR VAR 运行标志 : BOOL; 切换定时器 : TON; END_VAR

切换定时器(IN := 运行标志, PT := T#3S);

星形接触器 := 运行标志 AND NOT 切换定时器.Q; 三角形接触器 := 运行标志 AND 切换定时器.Q;

IF 启动按钮 AND NOT 运行标志 AND NOT 过载保护 THEN 运行标志 := TRUE; END_IF

IF 停止按钮 OR 过载保护 THEN 运行标志 := FALSE; END_IF

主接触器 := 运行标志;

这段代码里值得注意的是,定时器“切换定时器”也是一个实例,TON是系统自带的功能块。调用时IN连接运行标志,PT设置为3秒,输出Q在计时结束后变TRUE。定时器必须在每个扫描周期都调用一次,否则不会正常计时。这段逻辑已经涵盖了基本的启停、互锁、定时切换,现场调试也验证过,稳定得很。

但这里有个隐蔽问题:一旦按下停止按钮,运行标志置FALSE,星形和三角形同时断开,电机自由停车。如果工艺要求“先停止给星形信号,电机惯性滑行一段时间”,就得另加一个ON延时定时器来保持“停机前星形短暂接通”。实际做设备时,必须先把工艺需求理清,否则代码再精简也是白搭。

3.2 数值运算实例:模拟量滤波与转换

PLC处理模拟量时,最常用的运算就是滤波和工程量转换。一阶低通滤波的公式是当前滤波值 = 上一次滤波值 + 系数 * (新采样值 - 上一次滤波值)。用ST写这个很直白。

VAR_INPUT 原始值 : REAL; END_VAR VAR_OUTPUT 滤波后 : REAL; END_VAR VAR 上次值 : REAL := 0.0; 滤波系数 : REAL := 0.15; END_VAR

滤波后 := 上次值 + 滤波系数 * (原始值 - 上次值); 上次值 := 滤波后;

滤波系数取值越小,滤波效果越平滑,响应也越慢。如果现场传感器噪声大,就把系数压到0.05~0.1;如果要求响应快,系数放到0.3~0.5。我见过有些人把系数搞成0.9,那滤波效果基本等于没有,波动照样上头。

工程量转换就是标准4~20mA对应0~100度的换算。公式是实际值 = 零点偏移 + (测量原始值 - 量程下限原始值) * (工程上限 - 工程下限) / (量程上限原始值 - 量程下限原始值)。这类纯计算写一个函数就行,全局复用。

3.3 状态机写法:让设备按工序走

全自动设备最怕的就是控制逻辑乱成麻。用梯形图写互锁和步进,一旦工序多了,交叉条件能把人绕晕。状态机是ST的杀手锏应用。

假设一台四工位转盘设备,工位1上料,工位2检测,工位3打标,工位4下料,转盘每次转90度。我用一个INT变量表示当前状态,定义好每个状态码的含义:

VAR 当前状态 : INT := 0; END_VAR CASE 当前状态 OF 0: // 待机状态 IF 启动信号 THEN 当前状态 := 10; END_IF

10: // 工位1上料 上料气缸 := TRUE; IF 上料到位 THEN 上料气缸 := FALSE; 当前状态 := 20; END_IF 20: // 工位2检测 检测开始 := TRUE; IF 检测完成 THEN 当前状态 := 30; END_IF 30: // 工位3打标 打标启动 := TRUE; IF 打标完成 THEN 当前状态 := 40; END_IF 40: // 工位4下料 下料气缸 := TRUE; IF 下料完成 THEN 下料气缸 := FALSE; 当前状态 := 50; END_IF 50: // 转盘旋转 转盘动作 := TRUE; IF 转盘到位 THEN 转盘动作 := FALSE; 当前状态 := 10; // 回到上料,开始下一轮 END_IF

ELSE // 异常状态处理 当前状态 := 0; END_CASE

这是个最简化的状态机,实际工程里还要考虑急停、暂停后复位、报警联锁。但核心思路已经出来了:用CASE把每个工序封装成一个状态块,每个状态里只处理当前工序的动作和跳转条件。状态之间天然隔离,加新工序非常快。状态号建议用10、20、30这种“步进10”的编号,留出中间插入扩展位的位置,不然以后加一个工序,整个状态号全要重排,那才是真痛苦。

4. 编译报错与运行调试的避坑指南

4.1 变量类型不匹配:ST第一坑

ST是强类型语言,类型不匹配的编译错误是新手遇到最多的。REAL和INT混用是最典型的。比如你把一个INT赋值给REAL变量,通常没问题,PLC会自动转换。但反过来,REAL赋值给INT就会报错,因为可能丢失小数精度。解决办法是强制类型转换,INT转REAL直接赋值就行,REAL转INT要用REAL_TO_INT(变量)这种转换函数。

有个更隐蔽的问题:WORD和INT明明都是16位,但互不兼容。你把WORD类型的变址寄存器内容拿来和整数比较,编译直接挂掉,必须做WORD_TO_INT转换。BYTE、WORD、DWORD这些无符号类型和SINT、INT、DINT这些有符号类型是两条体系,虽然是同一块内存,解释方式不同,不能混用。调试时看到负数变超大的正数,或者16#FFFF变成-1,不用怀疑,就是类型惹的祸。

4.2 变量作用域的迷惑

同一个变量名,在全局区和局部区都声明了,程序里不同位置引用的却是不同的东西。这不算严格意义的语法问题,但排查起来很费神。我的习惯是全局变量统一加前缀,比如G_设备状态,功能块内部变量用本地名称。这样一眼就能区分作用域,省去大量查重时间。还有一个常见的做法是把全局变量集中放在一个专门的全局变量列表里,别散落在各个程序里,否则后期维护翻代码翻到崩溃。

4.3 扫描周期危机:别把ST写成死循环

PLC是循环扫描执行的,程序必须在扫描周期内跑完。ST里如果写了无出口的WHILE循环,或者REPEAT条件永远不满足,程序就会卡在当前周期里出不来,看门狗超时、CPU停机报警。这种故障在实机上非常吓人,设备正在干活突然就停了。我在调试一个新算法时,一定会先做边界条件审查,找出所有可能永不满足的循环条件,并在循环体里加一个“跳出保险计数”,超过一万人次就强制退出并报警。这个习惯帮我堵过好几个潜在的隐患。

即便不是死循环,循环次数过大也会拖慢扫描周期。比如某个通信协议要求对发送缓冲区逐字节校验,原版程序一遍FOR循环遍历几百字节,没问题。但项目扩展后,缓冲区里加了超大缓存,遍历次数直接翻几倍,结果扫描周期从8毫秒飙到80毫秒,整个设备的响应速度跟喝醉了一样。解决思路是分片处理:每个扫描周期只处理一部分数据,多个周期拼完整个操作,或者把计算放到定时中断里。总之别把一次性大运算塞进正常的扫描流。

4.4 在线调试:ST也能逐行看

ST代码的调试体验比其他语言好很多,主流编程IDE基本都能在线监视每行代码的当前状态。程序跑在PLC里,你在电脑上可以看每个变量的实时数值,甚至给变量“强制”赋值。调试时最常用的手段是设置断点:程序停在指定某行,然后单步执行观察变量变化。不过要注意,断点调试时PLC的输出是停止刷新的,设备现场会处于半停摆状态,调完之后一定记得清掉所有断点再放行生产。

另外,在线监视ST代码时,碰到功能块的内部变量、数组元素,直接看监控表不方便,最好是把需要观察的变量集中添加到监视视图里。很多人喜欢在程序里插临时变量来追踪中间量,用完不删,这是最让我头疼的陋习。临时变量堆积会造成内存浪费,还容易在多次在线修改时引发一致性问题。调试完顺手清理临时变量,是成熟的ST写手和业余选手的分水岭。

还有一个实用技巧:写ST时尽量让每行做一件事,别把十几个表达式挤在一行。我见过有人写出一行上百字符、嵌套五层括号的ST语句,编译能过,但调试时根本没法断点。把长表达式拆成中间变量,虽然增加了几行代码,但可读性和可调试性提升了一个档次。尤其涉及浮点运算和单位换算时,拆成几步还能在监视表里看到每一步的中间结果,定位问题效率极高。

5. 我对ST编程的几点个人体会

ST语法本身的学习曲线并不陡,真正难的是养成“结构清晰、逻辑严谨”的习惯。我写过几年梯形图再转ST的时候,最大的障碍还不是语法,而是思维方式。梯形图天然是并行联想式的,一个输出可以由很多条件并联驱动。ST天然是串行顺序式的,你得自己编排“先后顺序”,想清楚哪些条件先判、哪些后判,这反而逼着人对工艺逻辑做更深的梳理。

命名规范这件事,值得多说一句。PLC项目只要超过一千行ST,变量名乱起的人写的代码基本上就是一笔糊涂账。我的习惯是所有变量用有意义的英文组合,比如加热区实际温度就写HeaterActualTemp,状态码就写STATE_CODE_INITING这种带前缀的枚举。流程块、功能块、全局变量、局部变量全部加不同的前缀,别人接手维护时能少死很多脑细胞。

版本管理也是很多人忽略的。ST代码本质上是文本,完全可以用版本管理工具来管。每次改动前先备份,改动后记录一下变更说明。我吃过一次亏:现场调试改了一段状态跳转逻辑,当时觉得没问题,后来反反复复调别的参数,最后发现设备动作异常,但因为没做记录,已经想不起来是哪一处改坏了。后来养成了每次“在线修改”前都先把上一版导出备份的习惯,从此再没出现过“改坏了回不去”的窘境。

最后再分享一个小技巧:ST里写注释不要惜字如金,但要写“为什么”,别写“是什么”。比如“设定温度 = 实测温度 + 补偿值 // 此为热电阻线阻补偿”这种注释才是有价值的。下一回你自己翻代码看那个补偿系数,为什么不写某个数字而要写另一个,有注释和没注释,效率差的不是一点半点。

ST这语言,看着挺学术,用起来其实很踏实。只要把基础语法吃透,多写多磨,慢慢你会发现它成了你手里最顺手的控制工具。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询