简介:本资源是面向工业自动化工程师、PLC编程初学者及控制系统开发人员的《ST语言编程手册》完整版PDF,系统讲解IEC 61131-3标准下的Structured Text(结构化文本)编程核心知识,解决PLC逻辑实现复杂、语法理解零散、数据类型应用不熟等典型痛点。手册涵盖ST基本原理(语法图、语义规则)、字符与标识符规范、数字/布尔/字符串处理、源文件结构(语句与注释)、四大类数据类型(基本型、用户自定义STRUCT/ENUM/ARRAY、技术目标型、系统型)及变量声明与赋值表达式等关键内容,目录层级清晰、示例详实,适合作为案头工具书或系统入门教材。资源为单个4.22MB PDF文件,内容完整无删减,已获4414人学习下载,读者可直接获取权威语法体系、标准化编程范式与可复用的数据结构设计方法,快速提升工业控制程序的可读性、健壮性与工程化水平。
1. ST语言不是“写网页”的PLC编程:为什么翻车的工程师总在变量声明和边沿触发上栽跟头?
你手头刚接了一个产线改造项目,HMI界面已经跑通,但PLC逻辑一上电就跳停,监控发现某个计数器值在0和1之间疯狂抖动;或者更糟——设备运行三天后突然停机,复位重启又正常,日志里却找不到任何错误代码。这类问题80%以上不是硬件故障,而是ST(Structured Text)语言里一个被严重低估的细节:边沿检测的时序依赖、变量初始化的隐式规则、以及布尔表达式求值顺序带来的副作用。这不是语法糖,是工业现场的硬约束。ST语言是IEC 61131-3标准中定义的高级文本编程语言,专为可编程逻辑控制器(PLC)设计,它不像Python那样允许动态类型或垃圾回收,也不像C语言能直接操作内存地址——它的每行代码都必须映射到确定的扫描周期、确定的执行顺序、确定的硬件响应时间。本手册不讲“ST是什么”,只聚焦一线工程师每天真实面对的三件事:怎么写才能一次通过编译+仿真+上机调试,怎么改才能让老程序扛住连续72小时满负荷运行,以及当监控软件突然显示“FB块实例未初始化”时,你该盯哪一行代码、查哪个寄存器、改哪个属性。适合正在用Codesys、TwinCAT、博途(TIA Portal)或国产PLC平台做运动控制、配方管理、安全连锁逻辑开发的从业者,尤其适合从梯形图(LAD)转ST、或从单片机C语言转PLC开发的工程师。
2. 从零搭建ST开发环境:用Codesys V3.5 SP19完成最小可运行工程
ST语言本身是标准,但落地必须依赖具体平台。目前工业现场最主流、文档最全、社区支持最强的免费开发环境仍是Codesys Development System。它不是“IDE”,而是一套完整的PLC编程、仿真、调试、部署工具链,且对ST语法支持最贴近IEC 61131-3原生语义。以下步骤基于Codesys V3.5 SP19(2023年Q4稳定版),适用于Windows 10/11系统,全程离线可完成,无需激活码或联网验证。
2.1 创建新工程并配置目标设备
启动Codesys后,选择File → New Project,在弹出窗口中:
Project name:输入
ST_Minimal_DemoDevice:点击右侧Select device…
→ 在设备库中展开Simulation → PLC Runtime → Standard PLC Runtime (x64)
→ 选中Standard PLC Runtime (x64)并确认提示:不要选“SoftPLC”或“Raspberry Pi”等硬件相关设备,仿真阶段务必用标准Runtime。它会自动创建虚拟CPU、IO映射区和任务调度器,所有ST逻辑都在此环境中执行,与真实PLC行为一致。
Programming language:默认为ST,保持不变
Click OK 完成创建
此时工程树中会出现Device,Application,Global Variables,Resources等节点。这是ST工程的骨架,后续所有逻辑都挂载在此结构下。
2.2 编写第一个ST程序:带防抖的启动按钮逻辑
右键Application → Add Object → Program,命名为MAIN。双击打开编辑器,输入以下代码:
PROGRAM MAIN VAR // 输入映射:模拟物理按钮(I0.0) bStartButton: BOOL := FALSE; // 初始值显式声明为FALSE,避免冷启动时随机值 // 内部状态变量:必须显式初始化! bStartPressed: BOOL := FALSE; bStartRising: BOOL := FALSE; // 输出映射:控制电机使能(Q0.0) bMotorEnable: BOOL := FALSE; // 辅助变量:用于边沿检测 bStartPrev: BOOL := FALSE; END_VAR // ===== 核心逻辑:上升沿检测 + 防抖 ===== bStartRising := (NOT bStartPrev) AND bStartButton; // 上升沿 = 前一周期为FALSE,当前为TRUE bStartPrev := bStartButton; // 更新历史状态(必须放在边沿计算之后!) // ===== 启动连锁逻辑 ===== IF bStartRising THEN bMotorEnable := NOT bMotorEnable; // 每次按下切换使能状态 END_IF; // ===== 输出映射(实际项目中此处连接到Q0.0)===== // bMotorEnable 将被映射到物理输出点,在Codesys中需在Device→IO Mapping中配置逻辑说明:这段代码实现了工业中最基础的“点动启停”功能,但关键在于三点:
- 所有BOOL变量均显式初始化(
:= FALSE),防止冷启动时变量为未定义值(某些PLC上可能为随机字节);bStartPrev的赋值严格放在边沿判断之后,否则会导致本周期就覆盖历史值,边沿永远无法捕获;bStartRising是纯组合逻辑,不依赖任务周期,只要输入变化即刻响应——这是ST相比LAD的优势,也是易错点:若把bStartPrev := bStartButton写在IF前面,整个边沿检测就失效。
2.3 配置任务周期与在线仿真
右键Resources → Task Configuration,双击Task0:
- Cycle time:设为
10 ms(工业常见扫描周期,太短加重CPU负担,太长影响响应) - Priority:保持
10(默认) - Enable:勾选 ✅
点击OK保存。此时工程已具备完整执行条件。按F5启动仿真,Codesys会自动下载程序到虚拟PLC并开始周期扫描。在左下角Online → Online Change中启用在线修改模式,即可实时观察变量变化。
参数说明:
Cycle time = 10 ms意味着MAIN程序每10毫秒执行一次,所有变量读取、逻辑运算、输出写入均在此周期内完成;- 若实际项目中需处理高速编码器(如100kHz脉冲),则必须将高速计数逻辑放入独立的高优先级任务(如
Task1,周期设为1ms),并使用PLC内置的HC(High-Speed Counter)功能块,而非用ST循环累加——这是ST的边界,不是能力缺陷。
3. ST核心语法精要:变量作用域、数据类型转换与函数块调用的三重陷阱
ST语法看似接近Pascal,但工业场景下的变量生命周期、类型隐式转换、函数块实例化规则,与通用编程语言存在本质差异。很多“语法正确却逻辑错误”的问题,根源都在这三处。
3.1 变量作用域:全局、局部、临时变量的生存期与初始化时机
在Codesys中,变量声明位置直接决定其初始化行为和内存分配方式:
| 声明位置 | 示例 | 初始化时机 | 生存期 | 典型用途 | 风险点 |
|---|---|---|---|---|---|
Global Variables节点 | g_bErrorFlag: BOOL; | 工程首次下载时初始化为0(或类型默认值) | 整个工程运行期间 | 跨POU(程序/功能块)共享状态、报警标志 | 冷启动后值保留,但热重启(Download Changes)时不重置,可能残留旧状态 |
PROGRAM内 VAR 块 | bState: INT := 0; | 每次任务启动前(即每个扫描周期开始时)初始化 | 单次扫描周期 | 临时计算中间值、状态机当前态 | 若漏写:= 0,首次扫描可能为随机值(尤其在国产PLC上) |
FUNCTION_BLOCK内 VAR_INSTACE 块 | fbTimer: TON; | FB实例首次调用时初始化(非每次调用) | FB实例存在期间 | 定时器、计数器、PID控制器等有内部状态的对象 | 必须为每个FB实例分配唯一名称(如fbTimer_1,fbTimer_2),重复命名导致状态覆盖 |
关键结论:没有“静态局部变量”概念。ST中所有变量要么全局持久,要么周期性重置。若需“仅在第一次扫描时执行初始化”,必须手动加标志:
VAR bFirstScan: BOOL := TRUE; nCounter: INT := 0; END_VAR IF bFirstScan THEN nCounter := 100; // 首次扫描赋初值 bFirstScan := FALSE; END_IF;
3.2 数据类型转换:隐式转换的“玄学”与显式转换的强制要求
ST支持部分隐式转换(如INT→DINT),但绝不允许布尔与整数互转、字符串与数值互转。常见翻车场景:
❌ 错误写法:
IF nSpeed > 0 THEN bRunning := nSpeed;
→nSpeed是INT,bRunning是BOOL,Codesys编译报错:Cannot convert from 'INT' to 'BOOL'✅ 正确写法:
bRunning := nSpeed > 0;或bRunning := (nSpeed <> 0);
更隐蔽的是浮点比较:
VAR rSetpoint: REAL := 100.0; rActual: REAL := 100.0001; END_VAR // ❌ 危险!浮点精度误差导致永远不相等 IF rActual = rSetpoint THEN bInTolerance := TRUE; END_IF; // ✅ 正确:用绝对误差判断 IF ABS(rActual - rSetpoint) < 0.1 THEN bInTolerance := TRUE; END_IF;血泪经验:所有涉及REAL类型的比较、赋值、运算,必须预设容差(tolerance)。工业现场传感器信号本身就有±0.5%波动,用
=判断REAL等于,等于在代码里埋定时炸弹。
3.3 函数块(FB)调用:实例名不是标签,是内存地址的别名
调用定时器TON的正确姿势:
VAR fbDelay: TON; // 声明一个TON类型的FB实例 bTrigger: BOOL; tDelayTime: TIME := T#2S; END_VAR // 在逻辑中调用(必须传入IN、PT参数) fbDelay(IN := bTrigger, PT := tDelayTime); // 输出取值 bDelayDone := fbDelay.Q;❌ 错误:
TON(IN := bTrigger, PT := tDelayTime);
→ 编译失败:TON是类型,不是函数,不能直接调用。必须先声明实例(fbDelay: TON),再通过实例名调用。❌ 更危险的错误:多个地方共用同一实例名
// 在MAIN中 fbTimer(IN := bStart, PT := T#5S); // 在另一个POU中又写 fbTimer(IN := bStop, PT := T#1S); // 覆盖了MAIN中的状态!→ 结果:两个定时器逻辑互相干扰,
fbTimer.Q的值完全不可预测。
解决方案:为每个FB实例起语义化唯一名称,如
fbStartDelay,fbSafetyTimeout,fbBatchCompleteTimer。Codesys工程树中会清晰列出所有实例,便于排查。
4. ST编程避坑指南:5个让老手也拍大腿的典型问题与根因修复
工业现场没有“试错成本”,一个变量未初始化可能导致整条产线停机8小时。以下是我在某高校实验室协助调试12台教学PLC、及某公司产线升级项目中,高频出现且后果严重的5类问题,按现象→原因→解决路径整理,拒绝模糊描述。
4.1 现象:程序下载后首次运行正常,断电重启后逻辑错乱
原因:全局变量(Global Variables)在断电后丢失,但程序中未做冷启动初始化,变量值为随机内存残值。尤其ARRAY类型首元素常为0,但后续元素可能是任意值。
解决:
- 对所有全局数组、结构体,在
MAIN程序开头添加显式初始化块:// 全局声明:g_aTempData: ARRAY[0..9] OF REAL; // MAIN中: FOR i := 0 TO 9 DO g_aTempData[i] := 0.0; END_FOR; - 更优方案:使用
INITIAL关键字(Codesys支持):g_aTempData: ARRAY[0..9] OF REAL := [10(0.0)];// 10个0.0
4.2 现象:边沿检测(R_TRIG/F_TRIG)在高速脉冲下偶尔失效
原因:ST中边沿检测是电平采样+组合逻辑,若输入信号宽度小于PLC扫描周期(如10ms任务中输入脉宽仅3ms),则该脉冲可能被整个扫描周期错过。
解决:
- 确认输入源是否为硬件滤波后的信号(如光电开关自带10ms消抖);
- 若必须处理微秒级脉冲,放弃ST边沿检测,改用PLC内置高速计数器(HC)的中断功能,在
Hardware Interrupt任务中处理; - ST中仅作慢速连锁(如“计数达1000后停机”),不参与脉冲计数本身。
4.3 现象:调用自定义FUNCTION_BLOCK时,多个实例输出完全相同
原因:FB内部使用了VAR_GLOBAL或VAR_EXTERNAL声明的变量,导致所有实例共享同一份内存。
解决:
- 严格遵守规则:FB内部只允许使用
VAR(局部)、VAR_IN_OUT(传入传出)、VAR_STAT(静态,跨周期保持); - 删除所有
VAR_GLOBAL声明,将需共享的数据改为通过VAR_IN_OUT参数传递; - Codesys中开启Project → Options → Compiler → Check for global variables in FBs,编译时报错提示。
4.4 现象:REAL类型计算结果出现极大偏差(如100.0 * 0.1 = 9.999999)
原因:IEEE 754单精度浮点数(REAL)在二进制下无法精确表示十进制小数(0.1的二进制是无限循环小数),累积误差在多次运算后放大。
解决:
- 对精度要求高的场景(如称重、流量累计),改用
LREAL(双精度,64位); - 或采用定点数策略:将
kg单位换算为g,用DINT存储,100.0 kg = 100000 g,彻底规避浮点; - 所有比较操作必须用容差:
ABS(rResult - rExpected) < 0.001
4.5 现象:在线修改(Online Change)后,部分变量值突变为0或FALSE
原因:Codesys在线下载时,仅更新代码段,不重置数据段。若新代码中新增变量,其初始值为0;若旧变量被删除,其内存空间可能被新变量复用,导致值污染。
解决:
- 严禁在运行中修改变量声明(增删改类型/名称);
- 如需新增变量,先在
Global Variables中声明,并在MAIN中添加初始化逻辑; - 线上修改前,执行Online → Reset Application(会停止PLC,慎用),或设计“软复位”逻辑:
IF bSoftReset THEN nCounter := 0; bAlarm := FALSE; fbTimer(IN := FALSE); // 强制复位定时器 END_IF;
5. 复杂逻辑落地:用ST实现一个带优先级的多模式运行控制器
真实产线不会只有“启/停”两个按钮。以某包装机模拟项目X为例,需支持:手动调试模式、自动连续模式、单步循环模式、急停锁定模式,且各模式间切换需满足安全连锁(如自动模式下禁止进入手动模式,除非先按复位)。用ST实现此类状态机,关键不在语法,而在状态迁移的确定性、模式切换的原子性、以及异常恢复的可追溯性。
5.1 状态机设计:用枚举+CASE实现无歧义迁移
TYPE E_MACHINE_MODE: ( MODE_IDLE = 0, // 待机 MODE_MANUAL = 1, // 手动 MODE_AUTO = 2, // 自动 MODE_STEP = 3, // 单步 MODE_ESTOP = 4 // 急停 ); END_TYPE VAR eCurrentMode: E_MACHINE_MODE := MODE_IDLE; eNextMode: E_MACHINE_MODE := MODE_IDLE; // 输入信号(映射到物理IO) bBtnManual: BOOL := FALSE; bBtnAuto: BOOL := FALSE; bBtnStep: BOOL := FALSE; bBtnEstop: BOOL := FALSE; bBtnReset: BOOL := FALSE; // 模式切换锁存 bModeChangePending: BOOL := FALSE; END_VAR设计要点:
- 使用
TYPE定义枚举,而非魔法数字(1,2,3),提升可读性与编译检查;eCurrentMode与eNextMode分离,确保状态迁移是两阶段过程(先申请,再确认),避免因按钮抖动导致状态跳跃。
5.2 模式切换逻辑:用上升沿+条件判断构建安全门禁
// ===== 步骤1:采集按钮上升沿(防抖后)===== bBtnManualR := (NOT bBtnManualPrev) AND bBtnManual; bBtnAutoR := (NOT bBtnAutoPrev) AND bBtnAuto; bBtnStepR := (NOT bBtnStepPrev) AND bBtnStep; bBtnEstopR := (NOT bBtnEstopPrev) AND bBtnEstop; bBtnResetR := (NOT bBtnResetPrev) AND bBtnReset; bBtnManualPrev := bBtnManual; bBtnAutoPrev := bBtnAuto; bBtnStepPrev := bBtnStep; bBtnEstopPrev := bBtnEstop; bBtnResetPrev := bBtnReset; // ===== 步骤2:根据当前模式与按钮,决定下一模式 ===== CASE eCurrentMode OF MODE_IDLE: IF bBtnManualR THEN eNextMode := MODE_MANUAL; ELSIF bBtnAutoR THEN eNextMode := MODE_AUTO; ELSIF bBtnStepR THEN eNextMode := MODE_STEP; ELSIF bBtnEstopR THEN eNextMode := MODE_ESTOP; END_IF; MODE_MANUAL: IF bBtnResetR THEN eNextMode := MODE_IDLE; END_IF; // 其他按钮无效:手动模式下禁止切自动 MODE_AUTO: IF bBtnEstopR THEN eNextMode := MODE_ESTOP; END_IF; MODE_ESTOP: IF bBtnResetR THEN eNextMode := MODE_IDLE; END_IF; END_CASE; // ===== 步骤3:原子化切换(仅在无冲突时更新)===== IF eNextMode <> eCurrentMode THEN // 添加安全校验:例如自动模式需确认安全门关闭 IF (eNextMode = MODE_AUTO) AND (NOT bSafetyDoorClosed) THEN // 拒绝切换,记录报警 g_nAlarmCode := 101; ELSE eCurrentMode := eNextMode; // 真正切换 g_nAlarmCode := 0; // 清除报警 END_IF; END_IF;关键技巧:
- 所有模式切换请求先存入
eNextMode,最终由IF eNextMode <> eCurrentMode统一决策,杜绝“按钮连按导致状态乱跳”;- 切换前插入业务校验(如
bSafetyDoorClosed),失败时不更新状态,只设报警码,操作员可见;g_nAlarmCode为全局报警码,HMI可直接绑定显示,无需额外通信协议。
5.3 验证方法:用Codesys仿真器做三重压力测试
写完逻辑不能只点“运行看效果”,必须做可量化的验证:
| 测试类型 | 操作步骤 | 预期结果 | 工具支持 |
|---|---|---|---|
| 边界测试 | 快速连续按“手动→自动→急停→复位”按钮(间隔<50ms) | eCurrentMode严格按序列变化,无跳跃(如跳过MODE_IDLE直入MODE_AUTO) | CodesysBreakpoint+Watch Window实时监控变量 |
| 异常注入 | 在仿真中手动修改bSafetyDoorClosed为FALSE,再尝试切自动模式 | eCurrentMode保持不变,g_nAlarmCode= 101 | Online Change 修改变量值 |
| 长时间运行 | 启动仿真72小时,每10分钟记录一次eCurrentMode和g_nAlarmCode | 日志中无非法状态(如MODE_AUTO出现在MODE_ESTOP期间),报警码仅在预期时刻出现 | CodesysTrace Configuration记录变量变化 |
我的习惯:每次重大逻辑修改后,必跑这三类测试。尤其是“边界测试”,用脚本模拟按键序列(Codesys支持VBScript调用API),比人工点击可靠10倍。曾经一个产线项目,就是靠脚本发现“急停后300ms内按复位,有0.3%概率卡在MODE_ESTOP”,最终定位到
bBtnResetR的防抖时间与PLC扫描周期不匹配——把防抖从10ms改为20ms后问题消失。
希望帮到你。
本文还有配套的精品资源,点击获取