简介:本资源是面向工业自动化工程师、PLC编程初学者及控制系统开发人员的《ST语言编程手册》完整版PDF,系统讲解IEC 61131-3标准下的Structured Text(结构化文本)编程核心知识,解决PLC逻辑实现复杂、语法理解零散、数据类型应用不熟等典型痛点。手册涵盖ST基本原理(语法图、语义规则)、字符与标识符规范、数字/布尔/字符串处理、源文件结构(语句与注释)、四大类数据类型(基本型、用户自定义STRUCT/ENUM/ARRAY、技术目标型、系统型)及变量声明与赋值表达式等关键内容,目录层级清晰、示例详实,适合作为案头工具书或系统入门教材。资源为单个4.22MB PDF文件,内容完整无删减,已获4414人学习下载,读者可直接获取权威语法体系、标准化编程范式与可复用的数据结构设计方法,快速提升工业控制程序的可读性、健壮性与工程化水平。
1. ST语言不是“高级Python”:它专为PLC控制逻辑而生,写错一行就停机,但写对了能扛住产线7×24小时连续运行
你手头这份《(完整版)ST语言编程手册》,不是教你怎么写更优雅的算法,而是教你如何在毫秒级响应、零容错、无垃圾回收、内存固定分配的工业控制器里,把“按下启动按钮→电机正转→温度超限自动停机→故障灯闪烁”这一串物理动作,翻译成CPU能咬住不放、永不崩溃的确定性指令。ST(Structured Text)是IEC 61131-3五大标准编程语言中最接近高级语言的一种,但它骨子里仍是硬实时控制语言——没有try-catch,没有动态内存申请,变量声明即绑定物理地址,数组越界不会抛异常,只会静默覆盖隔壁寄存器,导致气缸突然伸出或安全门意外解锁。我曾在某汽车焊装线调试中,因一个未初始化的BOOL数组索引,在凌晨三点整条产线急停,排查三小时才发现是ST里FOR i := 0 TO 9 DO循环中i被意外赋值为-1,触发了隐式类型截断。这本手册的价值,不在于语法有多像C,而在于它告诉你:哪些语法糖能用、哪些看似合法的写法在PLC扫描周期里会变成黑匣子、哪些注释位置会影响编译器优化路径。适合正在啃国产PLC开发文档的现场工程师、刚从MATLAB/Simulink转向实际产线部署的控制算法工程师,以及需要把仿真模型落地为可交付ST代码的系统集成商。
2. 从空白工程到第一个可下载程序:用Codesys V3.5完成ST最小闭环验证
2.1 创建符合IEC 61131-3标准的ST项目结构
Codesys是当前最主流的开源PLC开发环境(支持多厂商硬件),其项目结构严格遵循IEC 61131-3标准。新建项目时,必须包含以下四个核心元素,缺一不可:
- 设备(Device):选择目标PLC硬件型号(如:Beckhoff CX5140、倍福CX5140、汇川H3U等),决定可用资源(IO点数、任务周期、内存大小);
- 配置(Configuration):定义通信总线(EtherCAT/Profinet)、IO模块映射、任务调度策略;
- 资源(Resource):对应PLC的CPU核心,每个资源可挂载多个任务(Task);
- 任务(Task):真正执行ST代码的调度单元,需设置执行周期(如10ms)、优先级、触发方式(周期/事件)。
提示:新手常误将全部逻辑写进
MAIN程序组织单元(POU)却不配置任务——代码编译通过,但永远不会被执行。任务是ST代码的“发动机”,POU只是“燃料配方”。
创建步骤(以Codesys V3.5.18.20为例):
# 1. File → New Project → Standard project (IEC 61131-3) # 2. Device: 选择 "Beckhoff CX5140"(模拟环境用) # 3. Configuration: 默认即可(含EtherCAT主站) # 4. Resource: 默认 "Resource1" # 5. 在 Resources → Resource1 → Tasks 下右键 → Add Task # - Name: "MainTask" # - Interval: "10 ms" # - Priority: "10"(数值越小优先级越高) # 6. 右键 MainTask → Add Object → Program → Name: "MAIN"此时生成的MAIN是一个空的ST程序组织单元,位于PLC_PRG命名空间下,等待你填入第一行可执行逻辑。
2.2 写出能点亮LED的ST代码:从变量声明到循环扫描
PLC程序本质是无限循环扫描:读输入→执行用户逻辑→写输出→等待下一个周期。ST代码必须嵌入这个框架。以下是最小可运行示例(实现“启动按钮按下时,输出Q0.0置位并保持,松开后延时2秒复位”):
// 文件:MAIN.st(位于 PLC_PRG 下) PROGRAM MAIN VAR // 输入映射(需在设备配置中关联物理端口) StartBtn : BOOL; // %I0.0,启动按钮(常开触点) // 输出映射 MotorRun : BOOL; // %Q0.0,电机运行指示灯 // 内部变量(非保持型) Timer1 : TON; // IEC标准定时器功能块 bStart : BOOL := FALSE;// 启动标志(初始值必须显式声明!) END_VAR // 主逻辑:边沿检测 + 定时器调用 bStart := StartBtn AND NOT bStart; // 上升沿检测(防抖关键!) IF bStart THEN MotorRun := TRUE; ELSIF Timer1.Q THEN MotorRun := FALSE; END_IF; // 定时器调用(必须放在逻辑块末尾,且每次扫描都调用) Timer1(IN := NOT MotorRun, PT := T#2S);关键参数说明与逻辑拆解:
TON是IEC 61131-3标准定时器功能块,IN为使能输入,PT为预设时间(T#2S = 2秒),Q为输出(计时完成为TRUE);bStart := StartBtn AND NOT bStart是经典上升沿检测写法,依赖变量在上一周期的值,因此bStart必须声明初始值(:= FALSE),否则首次扫描结果不可预测;Timer1(...)必须在每次扫描中无条件调用,否则定时器内部状态无法更新——这是ST与普通高级语言最根本差异:功能块不是函数调用,而是状态机实例;- 所有变量名前缀
%I/%Q为Codesys自动映射的物理地址标识,实际部署时需在设备配置中将StartBtn绑定到具体输入端口(如LocalBus.EtherCAT.Device1.Inputs.StartButton)。
编译成功后,点击Online → Login连接模拟PLC,再点击Download下载程序。此时在Online → Observe Variables中添加StartBtn和MotorRun,手动修改StartBtn值为TRUE再FALSE,可观察到MotorRun先变TRUE,2秒后自动变FALSE——最小闭环验证完成。
3. ST语言三大“玄学陷阱”:为什么编译通过却行为诡异?
3.1 隐式类型转换:BOOL参与算术运算时的静默截断
现象:
某温度控制逻辑中,IF (TempActual > TempSetPoint) THEN CoolValve := TRUE; END_IF;正常工作;但加入调试变量后:DebugVar := TempActual - TempSetPoint;导致冷却阀失控常开。
原因:DebugVar被声明为INT,而TempActual和TempSetPoint为REAL。ST中REAL - REAL结果仍为REAL,但赋值给INT时发生隐式截断(非四舍五入),且无编译警告。更致命的是,当TempActual < TempSetPoint时,差值为负REAL,截断为负INT,而后续某处代码误将DebugVar当作无符号数使用(如ARRAY[0..10] OF INT索引),负值被解释为极大正数,越界访问数组。
解决:
- 所有跨类型赋值必须显式转换:
DebugVar := INT(ROUND(TempActual - TempSetPoint)); - 在Codesys中启用
Project → Options → Compiler → Warnings → Enable all warnings,并勾选Warning for implicit type conversion; - 对关键控制变量(如阀门状态、电机使能)禁用
INT/REAL/BOOL混用,统一用BOOL或带范围检查的USINT。
3.2 数组索引越界:不报错,但覆盖相邻变量
现象:
某包装机计数逻辑中,CounterArray : ARRAY[0..9] OF UINT;用于记录10个工位产量。当Index := 15; CounterArray[Index] := CounterArray[Index] + 1;执行后,MotorEnable变量(紧邻数组声明之后)意外变为FALSE,导致主电机停机。
原因:
ST中数组不进行运行时边界检查。CounterArray[15]实际访问的是CounterArray起始地址+15×2字节(UINT=2字节)处的内存,恰好覆盖了下一个变量MotorEnable(BOOL占1字节,但按字节对齐可能占1或2字节)。该问题在不同PLC厂商编译器中表现不一:有的填充padding,有的紧凑排列,导致同一份代码在A品牌PLC正常,在B品牌PLC崩溃。
解决:
- 声明数组时预留安全余量:
CounterArray : ARRAY[0..15] OF UINT;(即使只用0~9); - 所有数组访问前强制校验:
IF Index >= 0 AND Index <= 9 THEN CounterArray[Index] := CounterArray[Index] + 1; ELSE // 写入诊断寄存器或触发报警 DiagCode := 101; // 数组索引越界 END_IF; - 在Codesys中启用
Memory layout view(View → Memory Layout),查看变量实际内存地址,确认关键数组前后无敏感变量。
3.3 功能块实例重用:同一个FB被多个POU调用时的状态污染
现象:MAIN程序中调用PID_Controller(FB_PID)控制温度;另一独立程序SECURITY中也调用同一个FB_PID实例控制安全阀。当温度PID正在积分饱和时,安全阀突然全开,且无法关闭。
原因:FB_PID是一个功能块(Function Block),其内部变量(如IntegralTerm,LastError)属于实例状态。若两个POU共用同一个实例名(如都写PID1(...)),则它们共享同一套内部状态。温度环的积分项被错误地应用到了安全阀控制上。
解决:
- 每个逻辑上下文必须使用独立实例:
// MAIN.st 中 TempPID : FB_PID; // 实例1 TempPID(...); // SECURITY.st 中 SafetyPID : FB_PID; // 实例2,完全独立 SafetyPID(...); - 禁止在全局变量区(
GVL)声明功能块类型变量(如GVL.PIDInstance : FB_PID;),因其会被所有POU共享; - 对于需跨POU传递的计算结果,使用
VAR_IN_OUT接口或全局结构体(GVL中声明STRUCT,内含REAL/BOOL等基础类型),而非功能块实例。
4. 把手册变成生产力:ST代码自动生成与跨平台适配实战
4.1 用Python脚本批量生成标准化ST模块(以Modbus RTU从站寄存器映射为例)
工业现场常需为数十个传感器/执行器生成一致的ST读写逻辑。手动编写易出错且维护困难。我们采用“配置表驱动”方式:用Excel定义寄存器地址、数据类型、缩写名,Python脚本生成ST代码。
配置表(modbus_config.xlsx)结构:
| RegisterAddr | DataType | VarName | Description | Scale |
|---|---|---|---|---|
| 40001 | UINT | TankLevel | 水箱液位(0-100%) | 1.0 |
| 40002 | INT | PumpSpeed | 水泵转速(rpm) | 0.1 |
| 40003 | BOOL | ValveOpen | 出水阀开关状态 | — |
生成脚本(gen_modbus_st.py)核心逻辑:
import pandas as pd def generate_st_code(config_file: str) -> str: df = pd.read_excel(config_file) st_lines = [] # 1. 生成变量声明 st_lines.append("VAR_GLOBAL") for _, row in df.iterrows(): dtype_map = {"UINT": "USINT", "INT": "INT", "BOOL": "BOOL"} st_lines.append(f" {row['VarName']} : {dtype_map[row['DataType']]};") st_lines.append("END_VAR\n") # 2. 生成Modbus读取逻辑(假设已存在ModbusRead函数) st_lines.append("PROGRAM MAIN") st_lines.append("VAR") st_lines.append(" ModbusData : ARRAY[0..999] OF BYTE; // 假设Modbus缓冲区") st_lines.append("END_VAR\n") # 3. 生成寄存器解析逻辑 for _, row in df.iterrows(): addr = int(row['RegisterAddr']) - 40001 # 转换为数组索引 if row['DataType'] == 'UINT': st_lines.append(f" {row['VarName']} := WORD_TO_USINT( (ModbusData[{addr*2}] * 256) + ModbusData[{addr*2+1}] );") elif row['DataType'] == 'INT': st_lines.append(f" {row['VarName']} := BYTE_TO_INT( (ModbusData[{addr*2}] * 256) + ModbusData[{addr*2+1}] );") elif row['DataType'] == 'BOOL': bit_pos = (addr % 8) # 假设BOOL存于字节低位 st_lines.append(f" {row['VarName']} := (ModbusData[{addr//8}] AND (16#01 SHL {bit_pos})) <> 0;") return "\n".join(st_lines) # 生成代码 with open("modbus_mapping.st", "w", encoding="utf-8") as f: f.write(generate_st_code("modbus_config.xlsx"))生成效果(modbus_mapping.st):
VAR_GLOBAL TankLevel : USINT; PumpSpeed : INT; ValveOpen : BOOL; END_VAR PROGRAM MAIN VAR ModbusData : ARRAY[0..999] OF BYTE; END_VAR TankLevel := WORD_TO_USINT( (ModbusData[0] * 256) + ModbusData[1] ); PumpSpeed := BYTE_TO_INT( (ModbusData[2] * 256) + ModbusData[3] ); ValveOpen := (ModbusData[0] AND (16#01 SHL 0)) <> 0;血泪经验:脚本生成的代码必须人工审核三点——① 地址偏移是否正确(尤其4xxxx/3xxxx寄存器起始差异);② 字节序(大端/小端)是否匹配设备手册;③ BOOL位操作是否与实际硬件接线一致(有些设备用高位bit表示第一个BOOL)。生成只是起点,验证才是关键。
4.2 一份ST代码适配三家PLC:通过编译器指令实现硬件抽象
国产PLC(汇川、信捷、禾川)底层IO映射规则不同,直接写%Q0.0会导致代码无法移植。解决方案:用#IFDEF编译器指令封装硬件层。
硬件抽象层(Hardware_Abstraction.st):
// 根据编译器宏定义选择IO映射 #IFDEF HUICHUAN #DEFINE INPUT_START_BTN %IX0.0 #DEFINE OUTPUT_MOTOR_RUN %QX0.0 #ELSIF XINJIE #DEFINE INPUT_START_BTN %IX1.0 #DEFINE OUTPUT_MOTOR_RUN %QX1.0 #ELSIF HECHUAN #DEFINE INPUT_START_BTN %IX2.0 #DEFINE OUTPUT_MOTOR_RUN %QX2.0 #ELSE #ERROR "Please define target PLC vendor: HUICHUAN, XINJIE or HECHUAN" #ENDIF // 全局变量声明(统一使用抽象名) VAR_GLOBAL StartBtn : BOOL; MotorRun : BOOL; END_VAR // 硬件映射逻辑(在MAIN中调用) PROGRAM MAIN VAR // 无需声明物理地址,由宏定义接管 END_VAR // 将物理输入映射到逻辑变量 StartBtn := INPUT_START_BTN; // 将逻辑变量映射到物理输出 OUTPUT_MOTOR_RUN := MotorRun;在Codesys中设置编译宏:Project → Options → Compiler → Preprocessor Definitions→ 添加HUICHUAN(或XINJIE/HECHUAN)。
这样,业务逻辑层(MAIN中除映射外的所有控制算法)完全不感知硬件差异,更换PLC只需改一个宏定义,重新编译即可。我曾用此方法在48小时内完成某灌装线从汇川H3U到信捷XD5E的整机迁移,代码复用率92%,仅IO映射部分需调整。
5. 验证ST代码可靠性的三把尺子:静态检查、仿真测试、现场灰度发布
5.1 用Codesys Static Analysis做代码健康度扫描
Codesys内置静态分析器可发现90%的低级错误。启用路径:Tools → Static Analysis → Configure,勾选关键规则:
| 规则ID | 检查项 | 严重等级 | 为什么必须开 |
|---|---|---|---|
| SA001 | 未初始化的变量赋值 | High | 防止启动瞬间随机值触发误动作 |
| SA007 | 数组索引超出声明范围 | Critical | 直接导致内存覆盖,安全红线 |
| SA012 | 功能块未调用(未连接IN) | Medium | 定时器/PID等不调用=不工作,逻辑失效 |
| SA025 | 浮点数直接等于比较(=) | High | REAL精度问题导致条件永远不满足 |
运行分析后,双击告警可跳转到问题行。例如SA001会标出Counter : UINT;声明后未赋初值,应改为Counter : UINT := 0;。注意:静态分析不能替代逻辑验证,但它能帮你把“人肉找错”时间从3小时压缩到15分钟。
5.2 在PLCsim Advanced中做闭环仿真测试
Codesys官方仿真器PLCsim Advanced支持真实PLC固件级仿真,可加载.cpx文件(Codesys编译产物)并连接虚拟HMI/IO设备。
仿真流程:
- 编译项目生成
MyProject.cpx; - 启动PLCsim Advanced →
File → Load Project→ 选择.cpx; - 在
IO Configuration中添加虚拟输入模块(如Digital Input 8ch),将StartBtn映射到Channel 0; - 启动仿真(
Online → Start),在Online → Observe Variables中监控MotorRun; - 在仿真器界面点击
Channel 0按钮,观察MotorRun是否按预期动作; - 进阶:用
Scripting功能编写自动化测试脚本,模拟1000次启停循环,验证定时器累积误差。
关键技巧:在仿真中开启
Trace功能(Online → Trace → Start),可记录每个扫描周期内所有变量值,导出CSV后用Python分析时序偏差——这是发现“毫秒级抖动”的唯一可靠方法。
5.3 现场灰度发布:用“双逻辑并行运行”实现零风险上线
新ST程序上线最怕“一上就停机”。某食品厂采用“双逻辑并行”策略:旧逻辑(V1)与新逻辑(V2)同时运行,但仅V1控制物理输出,V2输出接入诊断寄存器,持续比对两者结果。
实施步骤:
- 在
MAIN中并行调用两套逻辑:// 旧逻辑(继续控制设备) OldLogic(StartBtn, MotorRun_Out); // 新逻辑(仅诊断) NewLogic(StartBtn, MotorRun_Diag); - 添加一致性校验:
IF MotorRun_Out <> MotorRun_Diag THEN DiagCounter := DiagCounter + 1; IF DiagCounter > 100 THEN // 连续100次不一致 AlarmCode := 999; // 触发高级别报警 END_IF; ELSE DiagCounter := 0; // 清零计数器 END_IF; - 输出控制仍走旧逻辑:
%Q0.0 := MotorRun_Out;; - 连续运行72小时,
DiagCounter始终为0,证明V2逻辑完全等效; - 切换控制权:
%Q0.0 := MotorRun_Diag;,旧逻辑降级为备份。
这种做法让新代码在真实负载下“热身”充分,比单纯仿真更可信。我坚持用这招上线过17个产线升级项目,零次非计划停机。
最后说句实在话:这本《(完整版)ST语言编程手册)》真正的价值,不在它写了多少语法,而在于它逼你养成一种肌肉记忆——看到任何一行ST代码,第一反应不是“它能不能编译”,而是“它在第几个扫描周期会执行?它的变量在内存里占哪几个字节?如果网络中断100ms,这段逻辑会不会卡死?” 当你开始用PLC的思维去思考时间、内存和确定性,你就真的入门了。希望帮到你。
本文还有配套的精品资源,点击获取