首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
ST编程语法入门:从梯形图到结构化文本的思维跃迁
📅 2026/10/11 11:11:07
✍️ 爱科研究院
👁 阅读 3,247
1. ST编程语法入门从继电器逻辑到结构化文本的思维跃迁第一次接触STStructured Text的人十有八九是被梯形图“劝退”之后才转过来的。我当年在产线上调试一台包装机逻辑分支多到梯形图横竖画了七八屏改一个条件要来回翻三页改完还容易漏。后来一位老师傅丢给我一句“你用ST写二十行搞定。”从那以后我就彻底转向了结构化文本。ST是IEC 61131-3标准里定义的五种编程语言之一和梯形图LD、功能块图FBD、顺序功能图SFC、指令表IL并列。它的定位很明确面向复杂逻辑、数学运算、数据处理和流程控制。如果你只是控制一个气缸来回动作梯形图确实更直观但一旦涉及配方管理、PID整定、数组遍历、字符串处理、状态机跳转ST的代码密度和可维护性会甩开梯形图好几条街。这篇文章适合三类人一是从梯形图转过来、想系统补齐ST语法短板的电气工程师二是刚入行、面对PLC编程不知从何下手的新人三是写过一些ST但总觉得“能跑就行”、想搞清楚底层原理和规范写法的从业者。我会把ST的核心语法、实操要点、常见坑和排查技巧全部拆开讲尽量做到你读完就能上手写、写完就能跑、跑完还能改。提示本文所有示例基于IEC 61131-3标准语法不同品牌的PLC如西门子、三菱、欧姆龙、汇川等在细节上会有差异我会在关键处标注通用写法和品牌差异点。2. ST编程语法的核心骨架与设计逻辑2.1 为什么ST长这样从Pascal继承来的基因ST的语法风格脱胎于Pascal这一点从它的关键字就能看出来IF...THEN...ELSIF...END_IF、FOR...TO...DO...END_FOR、CASE...OF...END_CASE全是Pascal那套。为什么选Pascal而不是C因为IEC 61131-3制定的时候目标用户是电气工程师和自动化人员不是软件程序员。Pascal的语法更严格、更啰嗦但可读性强不容易写出隐晦的bug。这个设计哲学直接影响了你写ST的方式。比如ST里没有i这种写法你必须写i : i 1;。看起来麻烦但好处是每一处赋值都清清楚楚不会出现“到底先加还是先用”的歧义。再比如ST的是比较运算符赋值用:这个区分在调试时能救命——我见过太多人把IF a b写成IF a : b结果编译报错还算好的有些环境直接给你赋了值再判断逻辑全乱。2.2 数据类型别再用WORD走天下了新手写ST最容易犯的错就是所有变量都声明成WORD或INT。能跑但迟早出事。ST的数据类型体系比梯形图丰富得多用对了不仅省内存还能避免溢出和精度丢失。类型类别常用类型典型用途注意事项布尔型BOOL开关量、状态标志不要用INT代替浪费空间且易混淆整型INT、DINT、SINT计数、索引、状态码INT范围-32768~32767超了用DINT无符号整型UINT、UDINT地址、掩码减法时注意下溢浮点型REAL、LREAL模拟量、PID、数学运算REAL精度约7位LREAL约15位字符串STRING、WSTRING配方名、报警文本长度要预留够赋值会截断时间型TIME、TOD、DATE延时、时间戳TIME单位是毫秒写T#1s更直观数组ARRAY[1..10] OF INT批量数据、缓冲区下标从1开始还是0开始看品牌结构体STRUCT配方、设备参数组嵌套别太深否则调试痛苦我个人的经验是模拟量一律用REAL计数一律用DINT状态码用INT开关量用BOOL。这套组合能覆盖90%的工业场景。曾经有个项目同事用INT存编码器计数值结果转到32767就溢出了设备直接反向跑差点撞机。换成DINT之后问题消失。这种坑看文档是看不出来的只有踩过才知道。2.3 变量声明区VAR、VAR_INPUT、VAR_OUTPUT到底怎么选ST的程序组织单元POU分为程序PROGRAM、功能块FUNCTION_BLOCK和函数FUNCTION。每个POU都有自己的变量声明区用不同的关键字区分变量的作用域和方向。FUNCTION_BLOCK FB_MotorControl VAR_INPUT Start : BOOL; // 启动命令 Stop : BOOL; // 停止命令 SpeedSet : REAL; // 速度设定 END_VAR VAR_OUTPUT Running : BOOL; // 运行状态 ActualSpeed : REAL; // 实际速度 FaultCode : INT; // 故障码 END_VAR VAR InternalTimer : TON; // 内部定时器 State : INT : 0; // 内部状态带初始值 END_VAR VAR_IN_OUT SpeedRef : REAL; // 引用传递可读写 END_VAR这里有几个关键点。VAR_INPUT是外部传进来的功能块内部只能读不能写有些品牌允许写但强烈不建议。VAR_OUTPUT是往外送的内部可以写外部可以读。VAR是纯内部变量外部看不见。VAR_IN_OUT是引用传递传的是地址不是值适合大数组或结构体避免拷贝开销。注意VAR_IN_OUT在调用时传的必须是变量不能是常量或表达式。我见过有人写FB_MotorControl(SpeedRef : 100.0)编译直接报错因为100.0是常量没有地址。2.4 运算符优先级括号不是可选项ST的运算符优先级和大多数语言类似但有几个地方容易搞混。AND、OR、XOR的优先级低于比较运算符所以IF a 0 AND b 10是没问题的。但如果你写IF a AND b 0它会先算b 0再算AND结果可能不是你想要的。我的习惯是只要涉及混合运算一律加括号。多打两个字符省下半小时调试时间。比如IF (a 0) AND (b 10) THEN一眼就能看出逻辑关系别人接手你的代码也不用猜。算术运算符里MOD是取余/是除法。注意整数除法会截断7 / 2结果是3不是3.5。要得到小数结果至少有一个操作数得是REAL。这个坑在算百分比的时候特别常见比如(current / total) * 100如果current和total都是INT结果永远是0或100。3. 核心语法细节与实操要点拆解3.1 IF语句嵌套别超过三层IF是ST里最常用的条件判断基本结构如下IF Condition1 THEN // 条件1成立时执行 ELSIF Condition2 THEN // 条件2成立时执行 ELSE // 都不成立时执行 END_IF;看起来简单但实际写起来最容易出问题的是嵌套。我见过一个状态机写了七层IF嵌套最后连作者自己都理不清哪个ELSE对应哪个IF。经验法则是IF嵌套不超过三层超过就用CASE或者拆成独立的功能块。另外ELSIF不是ELSE IF少打一个E编译不过。有些品牌支持ELSEIF连写有些必须ELSIF这个看具体环境。还有每个IF必须有对应的END_IF分号不能漏这是语法硬性要求。3.2 CASE语句状态机的最佳拍档CASE是ST里我最喜欢的语法没有之一。它天生适合写状态机比一堆IF-ELSIF清晰十倍。CASE State OF 0: // 待机 Running : FALSE; IF Start THEN State : 10; END_IF; 10: // 启动中 Running : TRUE; IF ActualSpeed SpeedSet THEN State : 20; END_IF; 20: // 运行中 IF Stop THEN State : 30; END_IF; 30: // 停止中 Running : FALSE; State : 0; ELSE State : 0; // 异常状态复位 END_CASE;CASE的取值必须是整数类型INT、DINT等不能是REAL。每个分支用冒号分隔分支体可以有多条语句。ELSE分支处理未列出的值强烈建议加上否则状态跑飞了都没人知道。实操心得状态机的状态值不要用0、1、2、3连续编号用10、20、30这种间隔编号。这样以后要在中间插入新状态不用重新编号直接插15、25就行。这个技巧是我从一个老工程师那学来的省了无数次重构。3.3 FOR与WHILE循环什么时候用哪个FOR循环适合已知次数的遍历WHILE适合条件驱动的循环。// FOR遍历数组求最大值 MaxValue : ArrayData[1]; FOR i : 2 TO 10 DO IF ArrayData[i] MaxValue THEN MaxValue : ArrayData[i]; END_IF; END_FOR; // WHILE等待条件满足 WHILE NOT SensorReady DO WaitTime : WaitTime 1; IF WaitTime 1000 THEN FaultCode : 99; // 超时故障 EXIT; // 跳出循环 END_IF; END_WHILE;FOR循环的步长默认是1可以用BY指定比如FOR i : 10 TO 1 BY -1 DO倒序遍历。WHILE循环一定要有退出条件否则PLC扫描周期会被卡死看门狗直接报错。我调试时就干过这事写了个WHILE TRUE DO忘了加EXIT下载进去PLC直接停机现场尴尬得要命。EXIT是跳出整个循环CONTINUE是跳过本次进入下一次。这两个关键字在批量处理数据时特别好用比如跳过无效数据FOR i : 1 TO 100 DO IF DataBuffer[i] 0 THEN CONTINUE; // 跳过0值 END_IF; Sum : Sum DataBuffer[i]; END_FOR;3.4 功能块调用实例化与参数传递ST里调用功能块FB必须先声明实例再调用。这和梯形图里直接拖一个块出来不太一样新手容易懵。VAR Motor1 : FB_MotorControl; // 声明实例 Motor2 : FB_MotorControl; // 另一个独立实例 END_VAR // 调用Motor1 Motor1(Start : StartBtn1, Stop : StopBtn1, SpeedSet : 1500.0); Running1 : Motor1.Running; Fault1 : Motor1.FaultCode; // 调用Motor2 Motor2(Start : StartBtn2, Stop : StopBtn2, SpeedSet : 2000.0);每个实例有自己独立的内存空间Motor1和Motor2互不干扰。参数传递用:不是。输出参数可以直接读比如Motor1.Running。注意功能块实例必须在VAR区声明不能在代码中间临时创建。有些品牌支持动态创建但工业场景下不建议内存管理太复杂容易出玄学问题。函数FUNCTION和功能块的区别在于函数没有内部状态同样的输入永远得到同样的输出调用时不需要实例化。比如ABS()、SQRT()、LIMIT()这些都是函数。功能块有记忆比如定时器、计数器、PID必须实例化。3.5 定时器与计数器ST里的标准写法定时器和计数器是工业编程里用得最多的功能块ST里的标准调用方式如下VAR DelayTimer : TON; // 接通延时定时器 PulseTimer : TP; // 脉冲定时器 RunCounter : CTU; // 加计数器 END_VAR // 接通延时IN为TRUE后经过PT时间Q才为TRUE DelayTimer(IN : StartSignal, PT : T#5S); IF DelayTimer.Q THEN // 5秒后执行 END_IF; // 加计数器CU上升沿计数达到PV时Q为TRUE RunCounter(CU : ProximitySensor, RESET : ResetBtn, PV : 100); IF RunCounter.Q THEN // 计数到100时执行 END_IF;T#5S是时间字面量表示5秒。也可以写T#5000MS效果一样。时间单位支持D天、H小时、M分钟、S秒、MS毫秒。我习惯用T#1S这种写法比T#1000MS直观。计数器的CU是计数输入检测上升沿。RESET是复位输入PV是预设值。CV是当前计数值可以读出来显示。4. 完整实操流程从零写一个ST控制程序4.1 需求分析与变量规划假设我们要控制一个简单的传送带系统按下启动按钮传送带运行光电传感器检测到物料时计数器加一计数到10个物料后传送带停止指示灯亮按下复位按钮计数器清零系统回到待机。先规划变量变量名类型方向说明StartBtnBOOL输入启动按钮ResetBtnBOOL输入复位按钮PhotoSensorBOOL输入光电传感器ConveyorMotorBOOL输出传送带电机IndicatorLampBOOL输出完成指示灯MaterialCountINT内部物料计数SystemStateINT内部系统状态SensorEdgeR_TRIG内部传感器上升沿检测这里用R_TRIG做上升沿检测因为光电传感器可能会持续为TRUE一段时间直接计数会重复计。R_TRIG是标准功能块检测输入从FALSE变TRUE的瞬间。4.2 状态机设计与代码实现系统状态定义0待机10运行20完成。PROGRAM ConveyorControl VAR_INPUT StartBtn : BOOL; ResetBtn : BOOL; PhotoSensor : BOOL; END_VAR VAR_OUTPUT ConveyorMotor : BOOL; IndicatorLamp : BOOL; END_VAR VAR MaterialCount : INT : 0; SystemState : INT : 0; SensorEdge : R_TRIG; END_VAR // 传感器上升沿检测 SensorEdge(CLK : PhotoSensor); // 状态机 CASE SystemState OF 0: // 待机 ConveyorMotor : FALSE; IndicatorLamp : FALSE; IF StartBtn THEN MaterialCount : 0; SystemState : 10; END_IF; 10: // 运行 ConveyorMotor : TRUE; IF SensorEdge.Q THEN MaterialCount : MaterialCount 1; END_IF; IF MaterialCount 10 THEN SystemState : 20; END_IF; 20: // 完成 ConveyorMotor : FALSE; IndicatorLamp : TRUE; IF ResetBtn THEN SystemState : 0; END_IF; ELSE SystemState : 0; END_CASE; END_PROGRAM这段代码不到40行实现了完整的启动、计数、停止、复位逻辑。如果用梯形图至少得画三屏而且计数器的复位逻辑很容易画错。4.3 参数计算与边界处理计数器的预设值10是硬编码的实际项目里应该做成可配置的参数。另外MaterialCount是INT类型最大32767如果一天生产几万个物料可能会溢出。稳妥的做法是用DINT或者在每次启动时清零。光电传感器的响应时间也要考虑。如果传送带速度很快物料通过传感器的时间可能只有几十毫秒而PLC的扫描周期是10毫秒左右理论上能捕捉到。但如果传感器信号抖动R_TRIG可能会误触发多次。实际调试时我通常会在传感器输入后加一个TON做防抖延时20到50毫秒确认信号稳定后再计数。VAR DebounceTimer : TON; StableSensor : BOOL; END_VAR DebounceTimer(IN : PhotoSensor, PT : T#30MS); StableSensor : DebounceTimer.Q; SensorEdge(CLK : StableSensor);这个防抖逻辑看起来简单但在实际产线上能过滤掉90%的误计数。尤其是光电传感器表面有灰尘或者物料边缘不规则的时候信号抖动特别明显。4.4 下载调试与在线监控代码写完之后编译下载。大多数PLC编程环境支持在线监控可以看到每个变量的实时值。调试时我习惯先把SystemState、MaterialCount、SensorEdge.Q这几个关键变量加到监控表里然后手动触发按钮观察状态跳转是否符合预期。如果状态卡在某个值不动先检查跳转条件是否满足。比如卡在10说明MaterialCount没到10或者SensorEdge.Q一直没触发。这时候可以强制MaterialCount : 10看状态能不能跳到20。如果能跳说明计数逻辑有问题如果不能跳说明CASE结构写错了。实操心得在线监控时不要只看变量值还要看扫描周期。如果扫描周期突然变大说明有循环卡住了或者功能块调用太频繁。我遇到过一次扫描周期从5ms涨到50ms最后发现是WHILE循环里忘了加退出条件PLC在空转。5. 常见问题与排查技巧实录5.1 编译报错速查表报错信息常见原因解决方法未定义的变量变量没声明或拼写错误检查VAR区注意大小写类型不匹配INT赋给REAL没转换用INT_TO_REAL()显式转换缺少END_IFIF没闭合每个IF配一个END_IF分号缺失语句末尾漏分号每条语句以分号结尾实例未声明FB调用前没声明实例在VAR区声明FB实例常量不能赋值对VAR_INPUT或常量赋值检查变量方向类型转换是新手最容易踩的坑。ST是强类型语言INT和REAL之间不会自动转换。必须用标准转换函数INT_TO_REAL()、REAL_TO_INT()、BOOL_TO_INT()等。注意REAL_TO_INT是截断不是四舍五入要四舍五入得用REAL_TO_INT(x 0.5)。5.2 运行时故障排查思路程序下载进去不跑或者跑起来行为不对按这个顺序排查看PLC是否在RUN模式。有些品牌下载后默认是STOP需要手动切到RUN。看输入输出映射。变量声明了不代表自动映射到物理点需要在硬件配置里绑定。看扫描周期。周期过大说明有死循环或阻塞操作。看变量初始值。有些变量没给初始值上电后是随机值可能导致状态机跑飞。看功能块实例。定时器和计数器需要至少一个扫描周期才能更新输出不能上电瞬间就判断。我遇到过一个经典问题定时器设了5秒但实际10秒才动作。查了半天发现是扫描周期太长定时器每个周期才更新一次累积误差导致实际延时翻倍。解决办法是把定时器放在快速任务里或者用硬件定时器。5.3 代码可维护性的几个硬规矩写了多年ST我给自己定了几个规矩分享出来供参考变量命名用英文加下划线不用拼音不用缩写。ConveyorMotor比SSDJ好一万倍。每个功能块不超过200行超了就拆。200行大概是一屏能看完的量超过就得来回翻。状态机状态值用10的倍数方便插入新状态。注释写“为什么”不写“是什么”。// 启动按钮没意义// 启动前先清零计数防止上次残留才有价值。所有输出在状态机里统一赋值不要在多个地方给同一个输出赋值否则最后谁生效都搞不清。5.4 不同品牌ST的差异点虽然IEC 61131-3是标准但各家实现有差异。西门子的SCL在ST基础上扩展了很多比如支持REGION折叠、TYPE自定义数据类型更灵活。三菱的ST和梯形图可以混编但语法细节有出入。汇川、信捷这些国产PLC基本遵循标准ST但功能块库和系统函数不一样。跨品牌移植代码时重点检查这几处时间字面量格式、类型转换函数名、功能块引脚名、数组下标起始值。我移植过一次代码原平台数组从0开始新平台从1开始结果所有数据错位查了一整天才发现。6. ST进阶从能跑到好用的几个关键跨越6.1 结构体与数组批量数据的利器当设备有多个相同工位时用结构体加数组能大幅简化代码。比如8个温控区TYPE TempZone : STRUCT SetPoint : REAL; ActualValue : REAL; HeaterOn : BOOL; Alarm : BOOL; END_STRUCT END_TYPE VAR Zones : ARRAY[1..8] OF TempZone; i : INT; END_VAR FOR i : 1 TO 8 DO Zones[i].HeaterOn : (Zones[i].SetPoint - Zones[i].ActualValue) 2.0; Zones[i].Alarm : Zones[i].ActualValue Zones[i].SetPoint 10.0; END_FOR;8个工位的逻辑一个FOR循环搞定。如果每个工位单独写代码量翻8倍改一个参数要改8个地方。6.2 字符串处理配方和报警文本ST的字符串处理能力比梯形图强太多。配方名、报警文本、设备编号这些用STRING类型加标准函数就能搞定。VAR RecipeName : STRING[20]; AlarmMsg : STRING[80]; RecipeIndex : INT; END_VAR CASE RecipeIndex OF 1: RecipeName : 配方A; 2: RecipeName : 配方B; 3: RecipeName : 配方C; ELSE RecipeName : 未知配方; END_CASE; AlarmMsg : CONCAT(当前配方, RecipeName);CONCAT是字符串拼接LEN取长度LEFT、RIGHT、MID截取子串。注意STRING要指定长度不指定的话默认长度可能不够赋值会被截断。6.3 指针与引用谨慎使用ST支持指针POINTER和引用REFERENCE但工业场景下能不用就不用。指针操作一旦出错轻则数据错乱重则PLC停机。如果确实需要传递大块数据优先用VAR_IN_OUT引用传递比指针安全。VAR_IN_OUT BigArray : ARRAY[1..1000] OF REAL; END_VAR这样传的是地址不拷贝数据效率高且不会出错。指针只在极少数需要动态内存操作的场景才用比如自定义的环形缓冲区。6.4 任务调度与程序组织大型项目里ST程序不是一股脑全写在一个POU里而是按功能拆成多个程序分配到不同任务中。比如高速任务1ms急停、限位、安全逻辑中速任务10msPID、运动控制低速任务100msHMI通信、数据记录、报警处理每个任务独立扫描互不阻塞。这样即使低速任务卡了高速任务照样跑安全逻辑不受影响。这个架构在复杂设备里是标配新手往往把所有逻辑塞一个任务里结果一个通信超时就把整个系统拖死。7. 我个人在实际操作中的体会ST这门语言入门容易精通难。语法就那么几十个关键字一天就能看完但真正写好、写稳、写到别人能接手需要大量的项目积累。我最大的体会是不要追求炫技要追求可读。一段代码如果三个月后你自己都看不懂那它就是失败的哪怕它跑得再好。另外ST不是万能的。有些逻辑用梯形图就是更直观比如简单的互锁、手动/自动切换。强行用ST写反而绕。我的做法是混合编程安全逻辑和简单互锁用梯形图复杂运算和状态机用ST各取所长。最后分享一个小技巧写完ST代码后先别急着下载用纸笔把状态机的跳转画一遍或者用Excel列个状态转移表。很多逻辑错误在纸上就能发现比下载到PLC里再查快得多。这个习惯我坚持了五年帮我省下的调试时间至少有好几百个小时。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/11 11:11:07
DeepSeek-V2国产大模型工程化落地实战指南
2026/10/11 11:11:07
Linux下USB触摸屏驱动配置与坐标校准:从设备识别到hid-multitouch绑定
2026/10/11 11:06:07
我如何用Cursor提升AI编程效率:从补全到Agent实战
2026/10/11 12:56:47
diffpdf 实战:PDF 内容比对原理、模式选型与自动化核对流水线
2026/10/11 12:56:47
LangAlpha 架构深度解析:多 Worker 后端、SSE 流式与沙箱文件持久化如何支撑 AI 金融研究助手
2026/10/11 12:56:47
基于UDP的Linux Socket编程:简单聊天室实现与踩坑指南
2026/10/11 12:56:47
Linux软链接与硬链接深度解析:原理、操作与避坑指南
2026/10/11 12:56:47
STL list容器深度解析:双向链表、迭代器失效与性能误区
2026/10/11 12:51:47
杭州拱墅区在哪里办宽带最便宜 亚平宽带地址覆盖与装机流程解析
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/10 3:41:56
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/10 3:41:54
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/9 11:36:17
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)