SCL 语法完全参考 (S7-1200 + TIA Portal V16~V20)
本文是仓库的核心工具书: 按“语言参考手册“组织, 覆盖 SCL (Structured Control Language) 在西门子 S7-1200 + TIA Portal V16~V20 环境下的全部常用语法与运行时行为。事实性结论均以官方手册为准 (TIA Portal V20 在线文档, S7-1200 Manual Collection); 个别未能 100% 确认的点在句后标注 (待核实)。
阅读前提: 已读过本仓库 01/02 篇, 理解 scan cycle (扫描周期)、OB (Organization Block)、FC/FB/DB 的基本分工。本文只关注“语言本身“。
目录速览
- SCL 是什么: 定位与心法
- 词法与字面量
- 变量声明: 接口区、初始化、ARRAY/STRUCT/STRING
- 运算符与表达式 (含类型转换)
- 控制流
- FC 与返回值
- FB 与调用
- 定时器与计数器 (重点)
- 高级类型与机制: VARIANT / ARRAY[*] / PEEK/POKE / AT / 位访问
- 字符串实战
- 时间与日期
- 运行时错误与错误处理
- 字节序与位操作
- 附录A: C/Python ↔ SCL 语法速查大表
- 附录B: 同一状态机的 C 与 SCL 双版本
- 附录C: 常见编译报错与解决方法
1. SCL 是什么: 定位与心法
1.1 语言血统: IEC 61131-3 ST, Pascal 家族
SCL 是 IEC 61131-3 标准中 ST (Structured Text) 语言的西门子方言。ST 的语法血统是 Pascal 而不是 C:
- 语句用关键字块封闭 (
IF ... END_IF), 不用花括号; - 赋值是
:=, 相等比较是=(Pascal 传统); - 标识符不区分大小写;
- 函数声明与变量声明分区书写, 声明区和语句区严格分离。
对 C/Python 程序员来说, SCL 读起来像“去掉了指针、加上 PLC 数据类型的 Pascal“。它是完全编译型语言: 编译成 S7 CPU 的本地机器码, 没有解释器、没有虚拟机、没有运行时异常机制 (try/catch 不存在, 见第 12 节)。
1.2 大小写不敏感 (官方行为)
标识符、关键字全部不区分大小写: Motor、MOTOR、motor 是同一个变量; if、IF、If 都是合法的 IF 关键字。官方文档明确: PLC 变量名的唯一性检查不区分大小写。因此 Motor 和 MOTOR 不能同时声明, 编译器按重复标识符报错。
工程实践: 团队内统一一种风格 (本文用关键字全大写、变量首字母小写驼峰), 不要靠大小写区分含义 —— 那在 SCL 里是编译错误。
1.3 C 程序员 60 秒上手对照表
| 你想做的事 | C 写法 | SCL 写法 |
|---|---|---|
| 赋值 | x = y; | x := y; |
| 相等比较 | x == y | x = y |
| 不等比较 | x != y | x <> y |
| 分支 | if (c) { } else if () { } else { } | IF c THEN ; ELSIF THEN ; ELSE ; END_IF; |
| 多分支 | switch (x) { case 1: ... break; } | CASE x OF 1: ... END_CASE; (无 fallthrough) |
| 计数循环 | for (i=0; i<n; i++) { } | FOR i := 0 TO n-1 BY 1 DO ; END_FOR; |
| 条件循环 | while (c) { } | WHILE c DO ; END_WHILE; |
| 后测试循环 | do { } while (!c); | REPEAT ; UNTIL c END_REPEAT; |
| 跳出/下一轮 | break; / continue; | EXIT; / CONTINUE; |
| 返回 | return v; | 块名 := v; RETURN; |
| 逻辑与/或/非 | && || ! | AND OR NOT (AND 也可写作 &) |
| 位与/异或/取反 | & ^ ~ | AND XOR NOT (同一套关键字, 按操作数类型自动按位) |
| 取模 | a % b | a MOD b |
| 幂 | pow(a,b) | a ** b |
| 行注释/块注释 | // /* */ | // (* *) |
| 空语句 | ; | ; |
| 类型前缀字面量 | 1.0f、0x1FUL | REAL#1.0、WORD#16#00FF |
1.4 类比边界: 哪些 C/Python 直觉不能用
类比是学习加速器, 但以下边界必须现在划清:
- FB ≈ class, 实例 DB ≈ object: 类比大体成立 (见第 7 节), 但 FB 没有构造函数; 更重要的是“不调用 FB“是合法且常见的控制手段, 此时其内部状态冻结在最后一次调用的样子 (C++ 对象不存在“不调用“的概念)。
- VAR_TEMP ≈ 未初始化的 C 栈变量: 类比成立且被官方文档强化 —— 官方明确警告临时变量“使用前必须初始化, 否则值可能是随机的“。
- 没有短路求值:
A AND B不保证先算 A、B 为副作用表达式时不会省略 (SCL 里赋值根本不是表达式, 但函数调用是, 两个都会被求值)。C 的if (p != NULL && p->x)模式在 SCL 无对应物。 - 没有 ++/–、没有三元 ?:、赋值不是表达式:
x := y := 0;非法;a = c ? b : d要写成 IF/ELSE 或独立 FC。 - 除法语义同 C99, 不同于 Python:
7 / 2 = 3(整除截断),-7 / 2 = -3(Python 的//向下取整得 -4);MOD结果符号同被除数,-7 MOD 3 = -1(C 同, Python 得 2)。 - 没有堆: 所有内存在编译期确定, 无 malloc/new/GC; “动态数据结构“只能用定长数组 + 计数器模拟。
- 程序不会“结束“: OB1 每个 scan cycle 被操作系统重新调用一次, 更像 Arduino 的
loop(); 顶层代码不是一次性的main。
2. 词法与字面量
2.1 标识符规则
- 组成: 字母、数字、下划线; 不得以数字开头。建议只用 ASCII。
- 保留字 (关键字如
IF、FOR、MOD、指令名如TON、类型名如INT) 不得用作变量名。 - 含空格/特殊字符/中文的标识符必须整体用双引号包裹 (如
"我的变量#1"), 这是为 HMI 兼容保留的逃生门, 日常不要用。 - 大小写不敏感 (见 1.2)。
- TIA 的 SCL 编辑器会自动为块内变量加
#前缀、为全局符号加双引号; 手写外部源 (External source) 时要自己写对。
2.2 注释
// 单行注释: 到行尾结束
(* 块注释: 可跨多行,
且 (* 可以嵌套 *) 像这样 *)
块注释 (* ... *) 支持嵌套, 这点与 C 的 /* */ (不可嵌套) 不同。// 与 (* *) 可以互相包裹。
2.3 数值字面量
| 写法 | 含义 | 十进制值 |
|---|---|---|
42 | 十进制整型 (类型由上下文推断) | 42 |
-42 | 负十进制 | -42 |
2#1010_1010 | 二进制, _ 仅作可读性分隔 | 170 |
8#777 | 八进制 | 511 |
16#FF | 十六进制 | 255 |
16#DEAD_BEEF | 十六进制 + 分隔符 | 3735928559 |
1_000_000 | 十进制 + 分隔符 | 1000000 |
3.14 | REAL (必须有小数点) | 3.14 |
INT#300 | 带类型前缀的 INT | 300 |
DINT#100000 | 带类型前缀的 DINT | 100000 |
REAL#1.5 | 带类型前缀的 REAL | 1.5 |
WORD#16#00FF | 类型前缀 + 进制前缀组合 | 255 |
BOOL#TRUE / TRUE | 布尔 | 真 |
两个高频坑:
- 未定型字面量的推断:
#w := 16#FFFF;若#w是 WORD 没问题; 但#i := 16#FFFF;(#i为 INT) 会因超出 INT 范围报错。写位模式请养成用类型前缀的习惯 (WORD#16#FFFF), 这相当于 C 的(uint16_t)0xFFFF之于裸0xFFFF。 - 进制前缀是
基数#, 不是 C 的0x/0b。
2.4 TIME 与日期时间字面量
// TIME: 32 位有符号毫秒数 (本质是 DINT 的语义包装)
T#5ms // 5 毫秒
T#2s500ms // 单位必须从大到小排列: d > h > m(分钟!) > s > ms
T#1h30m15s // 1 小时 30 分 15 秒
TIME#10s // T# 与 TIME# 等价
T#25d // 接近上限; 合法范围约 ±24d20h31m23s647ms
// 注意 1m 是 1 分钟 (不是 1 米), 与 C 的 m 后缀习惯不同
// 负时长: 不要写 T#-5s (编译器拒绝; 个别版本行为待核实),
// 负值由运算产生: #t := 0 - T#5s; TIME 值域本身包含负数
// 日期/时间
D#2026-09-05 // DATE (16 位, 1990~2168 年)
DATE#2026-09-05 // 等价
TOD#13:45:30.123 // TIME_OF_DAY, 时:分:秒.毫秒
TIME_OF_DAY#06:00:00 // 等价写法
DTL#2026-09-05-13:45:30 // DTL 字面量, 注意年月日之间用 '-' 连成一段 (小数秒位数写法以编译器接受为准, 待核实)
2.5 字符与字符串字面量
'C' // CHAR 字面量, 单引号
'Hello' // STRING 字面量, 同样是单引号 (不是 C 的双引号!)
'It''s' // 串内的单引号写两遍转义 (相当于 C 的 \')
没有 C 风格的 \n、\t 转义序列; 需要控制字符时用字符码转换或 CHAR 变量运算。STRING 的内存布局见第 10 节。
2.6 三种限定符: #、"名字"、%地址
SCL 里访问一个变量, 由前缀说明它“住在哪里“:
| 前缀/形式 | 含义 | 例子 |
|---|---|---|
#名字 | 当前块内声明的变量 (接口区/临时/常量) | #counter, #tonDelay.Q |
"名字" | 全局作用域: 全局 DB 及其成员、块名、UDT 名 | "DataDB".setpoint, "MyFC"(...), "MotorCtrl" |
%I../%Q../%M.. | 绝对地址 (过程映像输入/输出/位存储区) | %I0.0, %QW4, %MD100 |
绝对地址的完整写法:
%I0.0 // 输入位 (等价 %IX0.0); %IB0 字节, %IW0 字, %ID0 双字
%Q0.3 // 输出位
%M0.0 // 位存储区位; %MB0, %MW0, %MD0
组合访问路径可以串起来, 类似 C 的结构链式取成员:
"ProductionDB".batches[3].weight // DB 内数组的结构成员
"ProductionDB".batches[3].flags.X2 // 再加位片访问 (见 9.6)
绝对地址是向后兼容的遗产机制 (来自 S5/S7-300 时代): 新项目应优先用符号名 + 全局 DB; %M 区仅在对接老程序或 HMI 强制要求时使用。
3. 变量声明
3.1 块骨架: 声明区与代码区严格分离
SCL 每个代码块 = 声明区 (interface) + BEGIN + 语句区。三种块的骨架:
// ---------- FB: 有静态变量 (状态), 需要实例 ----------
FUNCTION_BLOCK "MotorCtrl"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
running : BOOL;
END_VAR
VAR // 静态变量: 状态, 存入实例 DB
tonDelay : TON; // 系统类型实例声明 (见 3.10)
stepCnt : INT;
END_VAR
VAR_TEMP
i : INT;
END_VAR
VAR CONSTANT
MAX_RETRY : INT := 3;
END_VAR
BEGIN
// 语句区
#tonDelay(IN := #cmd, PT := T#3s);
#running := #tonDelay.Q;
END_FUNCTION_BLOCK
// ---------- FC: 无状态, 可有返回值 ----------
FUNCTION "Clamp" : INT
VAR_INPUT
v, mn, mx : INT;
END_VAR
VAR_TEMP
r : INT;
END_VAR
BEGIN
IF #v < #mn THEN #r := #mn;
ELSIF #v > #mx THEN #r := #mx;
ELSE #r := #v;
END_IF;
"Clamp" := #r; // 函数名当左值 → 返回值 (见第 6 节)
END_FUNCTION
// ---------- OB: 程序入口 (由 PLC 操作系统调用) ----------
ORGANIZATION_BLOCK "Main" // OB1
VAR_TEMP
x : REAL;
END_VAR
BEGIN
;
END_ORGANIZATION_BLOCK
TIA 生成代码时会插入
{ S7_Optimized_Access := 'TRUE' }之类的块属性行, 属于工程配置不是语言逻辑, 本文示例省略。
3.2 接口区总表: 哪种块能声明什么
| 声明区 | FB | FC | OB | 存放位置 | 类比 |
|---|---|---|---|---|---|
VAR_INPUT | 是 | 是 | 是 (仅事件信息) | 调用时的实参副本 | 函数形参 (值语义) |
VAR_OUTPUT | 是 | 是 | 否 | 实例 DB / 调用方 | 输出形参 (引用回写) |
VAR_IN_OUT | 是 | 是 | 否 | 调用方变量本体 | C 的 T* 指针形参 |
VAR (静态) | 是 | 否 | 否 | 实例 DB | 类的成员变量 |
VAR_TEMP | 是 | 是 | 是 | L 栈 (不保存) | 未初始化的栈变量 |
VAR CONSTANT | 是 | 是 | 是 | 编译期常量 | const/constexpr |
| Return (返回值) | 否 | 是 | 否 | - | 函数返回值 |
关键结论: FC 和 OB 没有静态变量, 因此不能拥有状态; 想要“带状态的函数“必须用 FB。OB 的 VAR_INPUT 由系统填入事件信息 (如启动 OB 的保持性丢失标志), 用户不能在调用时传参 —— 因为调用者是操作系统。
3.3 VAR CONSTANT
VAR CONSTANT
BUF_SIZE : INT := 64;
GAP_TOL : REAL := 0.01;
READY : INT := 0; // 状态机状态码, 充当 "枚举"
RUNNING : INT := 1;
FAULT : INT := 2;
END_VAR
常量必须在声明处用 := 赋值, 编译期内联, 不占运行时存储。SCL 没有真正的 enum 类型, “INT 常量集“是标准替代品 (可在 CASE 标签处直接使用常量名)。
3.4 保持性: TIA 里没有 VAR RETAIN 关键字
IEC 61131-3 文本标准里有 VAR RETAIN 区, 但 TIA Portal 不用这个语法: 保持性是接口编辑器里每个变量上的 Retain 复选框 (FB 静态变量、全局 DB 变量逐个勾选)。SCL 文本中看不到任何 RETAIN 字样。
- 断电后: Retain 变量保持最后值, 非 Retain 变量恢复为声明初值。
- M 存储区的保持字节数在 CPU 设备组态里设置。
- 编程初始化 (如“上电必须复位“的逻辑) 放启动 OB (OB100) 里做, 不要依赖初值语义。
3.5 声明时初始化 :=
VAR
count : INT := 0;
gain : REAL := 1.0;
mode : STRING[10] := 'AUTO';
table : ARRAY[1..3] OF INT := 10, 20, 30; // 数组按序赋初值
pos : ARRAY[1..2, 1..2] OF REAL := 1.0, 0.0, 0.0, 1.0;
END_VAR
初值在下载组态、CPU 暖启动 (warm restart) 时生效。例外: VAR_TEMP 不提供运行时初始化 —— 即使某版本编辑器允许填写初值, 也不要依赖 (待核实), 见下节。
3.6 VAR_TEMP 警告: 读前必写
官方文档的原文警告, 翻译过来: 临时局部数据只保留一个扫描周期 (对 FC/FB 而言是一次调用), 读取前必须先写入, 否则值可能是随机的。这就是 C 的“读未初始化栈变量“的 UB, SCL 侧同样真实存在。
VAR_TEMP
sum : INT; // 每次进入块, 内容不可预知
buf : STRING[32]; // 尤其字符串: 头部的"当前长度"是垃圾 → LEN() 返回垃圾
END_VAR
BEGIN
sum := 0; // 正确: 先写
buf := ''; // 正确: 空串初始化 (当前长度置 0)
...
规则: 每个 VAR_TEMP 变量在被读之前必须有明确赋值路径, 包括 STRING (先 := '')、结构体、数组 (循环清零或整体赋值)。Temp 不存入实例 DB, 不消耗保持性/掉电存储。
3.7 ARRAY: 下界可以是任意整数 (不是 0 基!)
这是 C/Python 程序员最容易栽的声明差异: S7 数组上下界由你指定, 下界不必是 0 也不必是 1。
VAR
a : ARRAY[0..9] OF INT; // C 风格 0 基, 完全合法
b : ARRAY[1..10] OF REAL; // 传统 PLC 风格 1 基
c : ARRAY[-5..5] OF INT; // 负下界合法, 11 个元素
d : ARRAY[2020..2030] OF WORD; // 年份当下标, 合法
m : ARRAY[1..3, 0..5] OF BOOL; // 二维
END_VAR
// 访问: 多维用逗号, 不是 a[i][j]
#x := #a[#i] + #d[2026];
#flag := #m[2, 4];
- 元素个数 = 上界 - 下界 + 1 (每维); 越界行为见第 12 节 (1200 上不停机!)。
- 下标可以是常量或整型变量 (最长 32 位整型); DB 中的数组用
"DB名".arr[#i]变址访问, 这就是 1200 上最常用的“指针替身“。 - 数组名作为整体可以赋值/传参 (类似 Python 的列表切片语义? 不, 类似 C 的 struct 拷贝, 整块复制)。
ARRAY[*]变长数组形参是另一套机制, 见 9.2。
3.8 STRING[n] 与默认长度
VAR
s1 : STRING; // 未写长度 → 默认最大 254 字符, 占 256 字节!
s2 : STRING[20]; // 最大 20 字符, 占 22 字节 (头 2 字节, 见第 10 节)
END_VAR
n上限 254; 总占用 = n + 2 字节。- 声明不写
[n]会静默占用 256 字节 —— 在 DB 里是实打实的存储, 多个字符串时务必显式给长度。 - STRING 不能赋给 I/Q (过程映像) 存储区的变量。
- WSTRING 只能在“优化访问“的块中声明; 字符串指令参数表本身同时接受 STRING 与 WSTRING, 但 WSTRING 进不了标准 DB 与通信映射表, 实际项目以 STRING 为主。
3.9 STRUCT 与 UDT 嵌套
匿名结构 (内联) 与命名结构 (UDT, TIA 里叫 PLC data type, 独立于 SCL 块存在):
// UDT "RecipeStep" (单独的 PLC 数据类型对象):
// STRUCT
// setpoint : REAL;
// holdTime : TIME;
// END_STRUCT
FUNCTION_BLOCK "Batch"
VAR
// 匿名嵌套结构
pos : STRUCT
x : REAL;
y : REAL;
END_STRUCT;
// UDT 类型的数组与标量
steps : ARRAY[1..10] OF "RecipeStep";
active : "RecipeStep";
END_VAR
BEGIN
#pos.x := 1.5;
#steps[3].setpoint := 80.0; // 逐级点号访问
#active.holdTime := T#30s;
END_FUNCTION_BLOCK
嵌套深度没有实际限制; UDT 相当于 C 的 typedef struct, 定义一次、处处可用, 且改 UDT 会同步改所有使用者。
3.10 系统类型实例声明 (定时器/计数器)
IEC 定时器/计数器是系统 FB 类型, 声明一个变量就是声明一个实例 (多重背景, multi-instance):
FUNCTION_BLOCK "Pump"
VAR
tonDelay : TON; // 通电延时定时器实例 (16 字节 IEC_TIMER 结构)
tofStop : TOF; // 断电延时
cntParts : CTU; // 加计数器
END_VAR
也可以把实例放在全局 DB 里 ("TimerDB".t1 : TON;), 供 OB 直接使用。实例不能放在 VAR_TEMP (Temp 无状态语义)。详细用法见第 8 节。
3.11 优化块 vs 标准块 (影响一串语法能力)
TIA 新建 DB/FB 默认是 optimized block access (优化访问: 无固定地址、按符号紧凑存放)。两种模式的能力差异 (官方):
| 能力 | 优化块 | 标准块 |
|---|---|---|
| PEEK/POKE 按 DB 区读写 | 不可 | 可 |
| AT 覆盖 | 仅保持性变量可 | 任意变量可 |
ARRAY[*] 形参 (1200, 固件 >= V4.2) | 需要优化块 | 不可 |
| WSTRING 声明 | 可 | 不可 |
| 与 S7-300/400、经典工具/HMI 老驱动兼容 | 弱 | 强 |
结论: 默认用优化块; 只有需要 PEEK/POKE 直捣 DB 或对接老系统时才用标准块。
4. 运算符与表达式
4.1 := 是赋值, = 是比较 (与 C 相反)
高危警示: 方向与 C 相反。
#x := 1;是赋值语句 (C 的x = 1;)#x = 1是比较表达式 (C 的x == 1;)- SCL 不存在
==、也不存在把赋值当表达式。因此 C 的两大经典事故在这里形态不同:
- C 的
if (x = 1)(编译通过、逻辑错误) 在 SCL 写成IF x := 1 THEN会直接编译报错 —— 赋值不是表达式, 这是 SCL 比 C 安全的地方;- 但反过来, 顺手写
#x == 1;或条件里写:=之外, 还要注意IF #x = 1 THEN的=是“等于“, 不是赋值 —— 肌肉记忆必须重训。
<> 是不等于 (Pascal 传统), 没有 tampoco !=。
4.2 算术运算符
| 运算符 | 含义 | 示例 | 备注 |
|---|---|---|---|
+ - | 加减 | #a - #b | 数值与 TIME 均可 |
* | 乘 | #a * #b | |
/ | 除 | 7 / 2 = 3 | 两个整型 → 整除, 向零截断 (同 C99, 异 Python) |
MOD | 取模 | -7 MOD 3 = -1 | 结果符号同被除数 (同 C, Python 得 2) |
** | 幂 | 2.0 ** 10 | REAL 运算; 两个整型操作数会先转 REAL, 结果为 REAL (待核实) |
整型运算先按操作数类型计算: INT + INT 的结果若溢出 INT 范围, ENO=FALSE 且结果无效 (第 12 节)。不同宽度的整型混算会按隐式转换规则先对齐 (见 4.7)。
TIME 之间可直接 + - 和比较 (本质是毫秒数运算):
VAR t1, t2, dt : TIME; END_VAR
#dt := #t2 - #t1; // TIME - TIME = TIME
IF #dt > T#500ms THEN ... END_IF;
4.3 逻辑与位运算: 同一套关键字
AND OR XOR NOT 对 BOOL 是逻辑运算, 对位串类型 (BYTE/WORD/DWORD, 以及整型) 是按位运算, 由操作数类型决定:
// BOOL 逻辑
#ok := #ready AND NOT #fault;
#go := #auto OR #manual;
// 位串按位 (类似 C 的 & | ^ ~)
VAR
mask : WORD := WORD#16#0F0F;
val : WORD := WORD#16#FF00;
END_VAR
#y := #val AND #mask; // WORD#16#0F00
#z := #val XOR WORD#16#FFFF; // 按位取反也可 NOT #val
AND 可以写成 &。没有短路求值 (见 1.4)。
4.4 比较运算符
= <> < <= > >=, 全部返回 BOOL。操作数类型要兼容 (数值间可隐式转换后比较)。SCL 没有 C 的链式写法 a < b < c (会按 (a<b) < c 把 BOOL 和数值比较, 编译报错), 也没有 Python 的 a < b < c 链式语义, 必须写 #a < #b AND #b < #c。
4.5 完整优先级表 (从高到低)
| 优先级 | 运算 | C 对应 | 与 C 的差异说明 |
|---|---|---|---|
| 1 (最高) | ( ) 括号 | 同 | |
| 2 | 函数调用求值 f(x) | 同 | |
| 3 | ** 幂 | pow() 是函数 | |
| 4 | 一元负 -, NOT | -x, !x | |
| 5 | * / MOD | * / % | |
| 6 | + - | 同 | |
| 7 | < <= > >= | 同 | |
| 8 | = <> | == != | 注意符号 |
| 9 | AND (&) | && 与 & | SCL 的 AND 比 = 更低, 与 C 的 && 一致, 但修复了 C 的 & 低于 == 的坑 |
| 10 | XOR | ^ | SCL 中 XOR 介于 AND/OR 之间, 同 C |
| 11 (最低) | OR | || |
实用结论: 优先级结构与 C 的逻辑层基本同构, 最需要记的是 NOT a = b 解析为 (NOT a) = b (NOT 优先级高于比较), 与 C 的 !a == b 相同 —— 都要小心, 习惯加括号。
4.6 求值语义: 无副作用表达式、无短路
- 赋值是语句不是表达式; 没有
++/--; 逗号表达式不存在。 - 表达式中的函数调用不保证求值顺序、不保证省略 (无短路), 不要在表达式里放“有副作用“的调用 —— SCL 里能改变状态的语句其实很少 (主要是 FC 输出参数), 但要意识到这点。
4.7 类型转换: 隐式规则与显式函数
隐式转换规则 (官方): 当目标类型的值域完全覆盖源类型值域时, 编译器自动转换。可隐式的方向 (S7-1200):
- 整型加宽:
SINT→INT→DINT,USINT→UINT→UDINT, 以及一切“窄→宽且值域覆盖“的组合 (如BYTE→WORD,UINT→DINT)。 - 整型→浮点:
INT→REAL,DINT→REAL(官方确认 DINT→REAL 可隐式; 但 REAL 只有 24 位尾数, 大 DINT 会丢精度, 编译器不报警)。 REAL→LREAL。- 字符:
CHAR→STRING赋值。
不可隐式 (必须显式转换):
- 一切“窄化“:
REAL→INT,DINT→INT,WORD→BYTE,UINT→INT(值域 65535 > 32767 覆盖不了)。 - TIME 与整型互转、数值与 STRING 互转。
显式转换函数族是 TO_<类型>:
#i := TO_INT(#r); // REAL → INT (截断小数? 见下表区别)
#d := TO_DINT(#i);
#r := TO_REAL(#i);
#w := TO_WORD(#b);
#s := TO_STRING(#i); // 数 → STRING (非格式化)
REAL/LREAL 取整有四个语义不同的指令, SCL 中默认返回 DINT (可显式指定其他整型):
| 指令 | 语义 | TO/C 类比 | 示例 (输入 REAL) |
|---|---|---|---|
ROUND(#r) | 四舍五入到最近整数 | C 的 round() | 2.5→3, -2.5→-3 |
TRUNC(#r) | 向零截断 | C 强转 (int) | 2.9→2, -2.9→-2 |
CEIL(#r) | 向上取整 | C 的 ceil() | 2.1→3, -2.9→-2 |
FLOOR(#r) | 向下取整 | C 的 floor()、Python 的 // 结果方向 | 2.9→2, -2.1→-3 |
陷阱清单:
TO_INT(#r)与TRUNC的区别容易混: 对 REAL 输入,TO_INT等价截断转换; 想要四舍五入必须显式用ROUND。- 转换结果超出目标值域 (如
TO_INT(1.0E9)) → ENO=0, 输出值无效 (OUT 取值不同版本文档表述有差异——以 ENO 为准, 不要依赖 OUT), CPU 保持 RUN (第 12 节)。 - STRING↔数值用
S_CONV/STRG_VAL/VAL_STRG(见 10.4), 非法输入 → ENO=0、OUT=0。
4.8 REAL 精度问题
REAL = IEEE 754 单精度 (24 位有效尾数, 约 7 位十进制有效数字); LREAL = 双精度。经典事实:
// 0.1 无法被二进制精确表示
#r := 0.1 + 0.2; // 结果 ≠ 0.3, 打印为 0.30000001192...
// 2^24 以上整数间隔 2, 加 1 会丢失
#a := 16777216.0;
#a := #a + 1.0; // #a 仍是 16777216.0
规则: 不要对 REAL 用 = 直接判等, 用容差:
IF ABS(#a - #b) < 1.0e-6 THEN ... END_IF;
累加大量数值时用 DINT/LREAL 或补偿算法; 标定/计量场合优先 LREAL。
4.9 内置数学函数
ABS MIN MAX LIMIT SQRT EXP LN (还有 EXP_D? 以指令帮助为准) 以及三角函数 SIN COS TAN (弧度制!)、反三角 ASIN ACOS ATAN。
#y := LIMIT(MN := 0.0, IN := #x, MX := 100.0); // 三参钳位, 相当于 clamp()
#m := MIN(#a, #b); // 注意: 参数名是 IN1/IN2? 直接位置传参最稳妥: MIN(#a, #b)
5. 控制流
5.1 IF / ELSIF / ELSE
// C: if (t > 80) hot=1; else if (t > 40) warm=1; else cold=1;
IF #t > 80 THEN
#hot := TRUE;
ELSIF #t > 40 THEN
#warm := TRUE;
ELSE
#cold := TRUE;
END_IF;
条件必须是 BOOL; 写 IF #count THEN (整型当条件, C 习气) 是编译错误。关键字块必须以 END_IF; 收尾 (带分号)。
5.2 CASE: 无 fallthrough
CASE #state OF
0: // 单值
#y := 1;
1, 3, 5: // 多个离散值并列
#y := 2;
10..20: // 闭区间范围 (含两端)
#y := 3;
ELSE // 可选; 相当于 C 的 default
#y := 0;
END_CASE;
与 C switch 的差异表:
| 特性 | C switch | SCL CASE |
|---|---|---|
| fallthrough | 有 (忘 break 出事故) | 没有, 每个分支天然独立结束 |
| 多值并列 | case 1: case 3: 靠 fallthrough | 1, 3: 直接写 |
| 值范围 | 无 | 10..20 语法内建 |
| 标签类型 | 整型常量 | 整型常量 / 命名常量 (VAR CONSTANT); 不允许变量 |
| 选择器类型 | 整型/字符/枚举 | 整型 (INT/DINT/BYTE 等) |
| default | default: | ELSE |
5.3 FOR: 边界含端点
VAR_TEMP
i : INT;
sum : INT;
END_VAR
// C: for (i = 1; i <= 10; i++) sum += a[i];
sum := 0;
FOR #i := 1 TO 10 BY 1 DO // BY 1 可省略
sum := sum + #arr[#i];
END_FOR;
// 倒数: BY -1
FOR #i := 10 TO 1 BY -1 DO
;
END_FOR;
精确语义 (与 C 的差异逐条列出):
- 边界含端点:
TO 10包含 10 (C 的i <= 10, 不是i < 10)。 - 循环变量必须是本块声明的整型临时变量; 循环体内不允许给循环变量赋值 (编译报错) —— 不能像 C 一样用
i++跳出。 - 起止表达式在进入循环时求值一次; 若初值已越过终值 (按步进方向), 循环体一次都不执行。
EXIT= C 的break(跳出整个循环);CONTINUE= C 的continue(进入下一轮)。- 正常结束后循环变量的值是“最后一个被执行的值 + 步长“ (IEC 语义, 类似 C 循环变量出界值; 不要依赖它)。
- 循环体内不要调用耗时指令或死等外设 —— 整个循环在一个 scan cycle 里跑完, 循环太长会喂狗失败 (第 12.5 节)。
5.4 WHILE: 先测试
// C: while (buf[n] <> 0) n++;
WHILE #buf[#n] <> 0 DO
#n := #n + 1;
END_WHILE;
注意: 1200 的 WHILE 循环仍是“单扫描周期内跑完“的忙循环, 不是跨扫描周期的等待。想跨周期等条件, 用“状态 + 下一周期再判断“的模式 (PLC 的常态写法)。
5.5 REPEAT / UNTIL: 等价 do..while(取反)
// C: do { n++; } while (arr[n] <> 0);
REPEAT
#n := #n + 1;
UNTIL #arr[#n] = 0 // 注意: UNTIL 是"退出条件", 相当于 C while 里的条件取反
END_REPEAT;
至少执行一次。UNTIL c 为真时退出 —— 写成 C 的循环条件会逻辑颠倒, 这是移植代码时的高发 bug。
5.6 RETURN、空语句与 REGION
// RETURN: 立即结束当前块 (相当于 C 的 return; 但 FC 返回值仍是"函数名 := 值")
IF #dataInvalid THEN
RETURN;
END_IF;
// 空语句: 单独一个分号是合法语句, 可作占位
;
// REGION: 仅影响编辑器代码折叠, 无编译语义, 可嵌套
REGION 初始化_如需中文名请实测
#x := 0;
END_REGION
GOTO 标签名; + 标签名: ... 属于经典 STEP 7 SCL 的遗留能力; TIA Portal 各版本 SCL 对它的支持不一致——以你所用版本的编译器实测为准 (待核实)。即使可用, 新代码也不要用它: SCL 的结构化控制流 (IF/CASE/循环 + EXIT/RETURN) 足以表达一切。
6. FC 与返回值
FC 的返回值通过函数名当左值赋值 (Pascal 传统):
FUNCTION "Scale" : REAL
VAR_INPUT
raw, inMin, inMax, outMin, outMax : REAL;
END_VAR
VAR_TEMP
r : REAL;
END_VAR
BEGIN
IF #inMax = #inMin THEN
"Scale" := 0.0; // ← 给函数名赋值 = 设定返回值
RETURN; // ← 提前返回
END_IF;
r := (#raw - #inMin) / (#inMax - #inMin);
"Scale" := r * (#outMax - #outMin) + #outMin;
END_FUNCTION
调用 (返回值可当表达式用):
#v := "Scale"(raw := #ai0, inMin := 0.0, inMax := 27648.0,
outMin := 0.0, outMax := 100.0);
要点与惯例:
- 忘了给函数名赋值就走到 END_FUNCTION, 返回值是未定义的 (TIA 可能给编译警告, 但不保证拦截)。每条路径都要赋值, 类似 C 的“所有路径 return“。
FUNCTION "X" : VOID声明无返回值的过程。- 行业惯例: 多输出参数优于返回值。返回值一次只能带出一个数; 把结果放
VAR_OUTPUT(可多个) 更清晰, 也方便扩展:
FUNCTION "ParseCmd" : VOID
VAR_INPUT cmd : STRING[32]; END_VAR
VAR_OUTPUT ok : BOOL; code : INT; value : REAL; END_VAR
...
- FC 无静态变量, 调用之间不留状态; 需要状态就用 FB。
7. FB 与调用
7.1 FB 需要实例 (背景 DB 或多重背景)
FB 的静态变量必须落在实例存储区里, 两种落地方式:
- 单实例 (single instance): 为 FB 生成一个专属 DB (调用时由 TIA 提示创建), DB 与 FB 同构。
- 多重背景 (multi-instance): 在调用者 FB 的静态区里声明实例变量 (见 3.10), 状态存在调用者的实例 DB 里, 不产生独立 DB。这是首选方式: 一个“设备 FB“声明十个泵实例, DB 树状收纳。
FUNCTION_BLOCK "Pump"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
running : BOOL;
END_VAR
VAR
tonDelay : TON;
END_VAR
BEGIN
#tonDelay(IN := #cmd, PT := T#2s);
#running := #tonDelay.Q;
END_FUNCTION_BLOCK
7.2 调用语法: 按名传参, := 输入、=> 输出
// ---- 在另一个 FB 里: 多重背景实例 pump1 声明于本 FB 的 VAR 区 ----
#pump1(cmd := #startCmd, running => #pumpRun);
// 输入参数用 :=, 输出参数用 =>, InOut 用 := (双向)
// 尾部参数可省略; 被省略的输出不会被写入
// 输出也可以不接 =>, 调用后直接读实例的成员:
#pump1(cmd := #startCmd);
#pumpRun := #pump1.running; // 从实例读输出, 同样合法且常见
// ---- 在 OB 里: 单实例 (DB 名直接当实例名) ----
"PumpDB"(cmd := "bStartPump", running => "bPumpRun");
注意 FB 调用是语句不是表达式, 不能 #x := #pump1(...) (除非… 不行, FB 无返回值; 要结果就接输出参数或读实例成员)。
7.3 条件调用语义: 不调用 = 状态冻结
IF #enable THEN
#pump1(cmd := #startCmd);
END_IF;
被跳过的 FB 既不执行也不复位: 其定时器停在半路、输出保持最后一次的值。这与 C 对象根本不同 (C 里方法不调用这个动作本身不存在)。常见用法与事故:
- 用途: 调试旁路、模式切换时“挂起“某设备逻辑。
- 事故: 把定时器调用放进
IF里, 导致 IN 上升沿丢失或 ET 行为不可预期。定时器/计数器实例应当每个扫描周期无条件调用, 用输入参数控制, 而不是用调用本身控制 (第 8 节的所有示例都遵守这一点)。
8. 定时器与计数器 (程序员最容易懵的一节)
8.1 心智模型: 定时器是“有状态对象“, 不是“函数“
C/Python 里的 sleep(1000) 是阻塞等待; SCL 里没有 sleep —— scan cycle 必须短。IEC 定时器是一个每次调用让它推进一次的状态机: 你每个周期调用它, 告诉它当前 IN 和 PT, 它根据“内部记录的已运行时间“刷新 Q/ET。状态 (剩余时间/已计时) 保存在实例里, 跨扫描周期存活。
S7-1200 提供 (SCL/LAD 通用): TON (通电延时)、TOF (断电延时)、TP (脉冲生成)、TONR (累计定时, 带 R 复位输入); 另有 SCL 专用的 RESET_TIMER、PRESET_TIMER。每个实例是 16 字节 IEC_Timer 结构。(SIMATIC 遗留定时器 S_PULSE/S_ODT/S_OFFDT 等整族是 S7-1500 独占, 1200 没有, 一句带过不再展开。)
四个定时器的行为表 (IN=输入, PT=预设时长, Q=输出, ET=已计时):
| 定时器 | Q 何时为 TRUE | ET 行为 | 一句话 |
|---|---|---|---|
TON | IN 为 TRUE 且持续满 PT 之后 | IN 变 FALSE 立即清零 | 延时启动 (on-delay) |
TOF | IN 为 TRUE 期间, 以及 IN 变 FALSE 后再保持 PT | IN 下降后开始累计到 PT | 延时停止 (off-delay) |
TP | IN 上升沿触发起, 恰好 PT (期间 IN 再变化不影响; 不可重触发) | 脉冲期间累计到 PT | 定宽脉冲 |
TONR | 累计通电时长达到 PT (IN 断开不丢已计时, R 复位) | 断开保持, R 清零 | 累计计时器 |
8.2 通电延时 (TON): 延时启动
FUNCTION_BLOCK "MotorStart"
VAR_INPUT
cmd : BOOL; // 启动命令
END_VAR
VAR_OUTPUT
contactor : BOOL; // 接触器
END_VAR
VAR
tonOn : TON; // ← 状态在实例里
END_VAR
BEGIN
// 每个周期无条件调用; 参数按名传递
#tonOn(IN := #cmd, PT := T#3s);
#contactor := #tonOn.Q; // cmd=TRUE 满 3s 后 Q 才为 TRUE
END_FUNCTION_BLOCK
ET (已计时) 也可以读: #elapsed := #tonOn.ET; (#elapsed : TIME)。
8.3 断电延时 (TOF): 延时停止 + 组合示例
// 需求: 停止命令后风扇再吹 5s 散热
FUNCTION_BLOCK "FanControl"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
fan : BOOL;
END_VAR
VAR
tofCool : TOF;
END_VAR
BEGIN
#tofCool(IN := #cmd, PT := T#5s);
#fan := #tofCool.Q; // cmd 掉电后 Q 继续保持 5s
END_FUNCTION_BLOCK
启动延时 + 停止延时组合 (TON 后接 TOF):
VAR
tOn : TON;
tOff : TOF;
END_VAR
// 启动: cmd=TRUE 2s 后输出; 停止: cmd=FALSE 5s 后才真停
#tOn(IN := #cmd, PT := T#2s);
#tOff(IN := #tOn.Q, PT := T#5s);
#output := #tOff.Q;
8.4 振荡器 (闪烁): 两个 TON 交叉耦合
经典振荡器 (占空比可调的方波), 不依赖任何系统时钟函数:
FUNCTION_BLOCK "Blinker"
VAR_OUTPUT
heartbeat : BOOL;
END_VAR
VAR
tOn : TON;
tOff : TON;
END_VAR
BEGIN
// 每个 TON 的输入 = 另一个 TON 输出的反相
#tOn(IN := NOT #tOff.Q, PT := T#600ms);
#tOff(IN := #tOn.Q, PT := T#400ms);
#heartbeat := #tOn.Q; // 周期 1s, 高电平 600ms
END_FUNCTION_BLOCK
推导一遍 (上电首周期两 Q 均 FALSE): tOn 开始计时 → 600ms 后 tOn.Q=TRUE → tOff 开始计时 → 400ms 后 tOff.Q=TRUE → tOn 的 IN 变 FALSE → tOn.Q 立即 FALSE → tOff 的 IN 变 FALSE → tOff.Q 立即 FALSE → 回到起点。周期 = 600+400ms。
单定时器自复位脉冲 (Q 每 PT 产生一个“一个扫描周期宽“的脉冲):
#pulseT(IN := #enable AND NOT #pulseT.Q, PT := T#200ms);
// enable=TRUE 时: 每 200ms, Q 高电平恰好一个 scan cycle
// 注意: 脉冲宽度 = 扫描周期, 若消费方在同一周期内没看到它就丢了
8.5 TONR 与 RESET_TIMER / PRESET_TIMER
VAR
tAcc : TONR;
END_VAR
// IN 断开不清零, 断续累计; R 输入复位
#tAcc(IN := #sensor, R := #manualReset, PT := T#10s);
IF #tAcc.Q THEN
#tooLong := TRUE; // 累计通电满 10s
END_IF;
// SCL 专用指令 (LAD 里对应 RT/PT 线圈):
RESET_TIMER(T := #tAcc); // 程序化硬复位
PRESET_TIMER(T := #tOn, PT := T#9s); // 程序化改 PT
TP 用法同理: #tp(IN := #trigger, PT := T#500ms); #lamp := #tp.Q; —— 触发后亮固定 500ms, 期间再触发无效。
8.6 IEC 计数器: CTU / CTD / CTUD
FUNCTION_BLOCK "Batcher"
VAR_INPUT
partPassed : BOOL; // 光电信号 (每件产品一个上升沿)
manualReset : BOOL;
END_VAR
VAR_OUTPUT
batchDone : BOOL;
END_VAR
VAR
cntParts : CTU; // 加计数器实例 (IEC_Counter 结构)
cntStock : CTUD; // 双向计数器
END_VAR
BEGIN
// CU 检测的是输入信号的上升沿 (内部边沿检测), 直接接布尔即可
#cntParts(CU := #partPassed, R := #manualReset, PV := 100);
#batchDone := #cntParts.Q; // Q: CV >= PV 时为 TRUE
// CV (当前值) 也可直接读:
// #count := #cntParts.CV;
// CTUD: CU 加 / CD 减 / R 复位 / LD 置初值 (=PV)
#cntStock(CU := #partIn, CD := #partOut, R := #manualReset,
LD := #loadInit, PV := 0);
#n := #cntStock.CV; // QU=到达 PV, QD=减到 0
END_FUNCTION_BLOCK
PV (预设值) 与 CV 是整型 (SINT..UDINT 可选, 常用 INT/DINT)。减计数器 CTD 单独存在 (CD, LD, PV → Q, CV)。SIMATIC 遗留计数器 S_CU/S_CD/S_CUD 同样是 S7-1500 独占, 1200 忽略。
8.7 TIME 的加减运算与回绕
TIME + TIME = TIME,TIME - TIME = TIME, 可与T#字面量直接运算和比较 (见 4.2)。- TIME 是 32 位有符号毫秒: 范围约 -24d20h31m23s648ms ~ +24d20h31m23s647ms。加法累计约 24.8 天后溢出回绕 —— 长周期累计要用 DINT 天数+TIME 毫秒拆分, 或用 DTL (第 11 节)。
- 两个时刻求差: 若两个时刻都是 DTL, 用扩展指令
T_DIFF得 TIME; 若都是“某 TON 的 ET“, 相减即可。 - TIME 转毫秒数 DINT 没有隐式路径, 用
T_CONV指令 (待核实具体签名)。
9. 高级类型与机制 (逐项标注 1200 支持度)
9.1 总表
| 机制 | S7-1200 支持 | 说明 |
|---|---|---|
VARIANT 通用形参 + VariantGet/VariantPut/CountOfElements | 支持 | 传“任意类型“的正解 |
TypeOf / TypeOfElements / IS_ARRAY | 支持 | 运行时类型检查 |
TypeOfDB | 不支持 (1500 独占) | |
ARRAY[*] 变长数组形参 | 支持 (需优化块 + 固件 >= V4.2) | FC 仅 Input/InOut 区可声明; FB 只能 InOut (Output/Return 不可) |
PEEK/POKE 家族 | 支持 | 仅 SCL; DB 区必须标准 DB; 外设输入区 16#1 是 1500 独占 |
AT 覆盖 | 支持 (受限) | 标准块任意变量 / 优化块仅保持性变量 |
Slice 访问 .X/.B/.W/.D | 支持 | SCL 对整型 slice 需关闭 IEC 检查 |
REF/引用类型 REF_TO | 不支持 (1500 独占) | 传引用语义用 InOut/VARIANT 替代 |
POINTER/ANY 参数类型 | 不支持 (1500 独占; ANY 在 1500 也仅限标准块 Temp) | |
DB_ANY 间接 DB 访问 | 部分支持 (官方间接寻址页含 1200; 作接口形参类型标注 1500, 待核实) | 1200 上更常用变址数组 |
| LTIME/LTOD/LDT、ARRAY DB、遗留 S 定时器/计数器、断点调试 | 不支持 (1500 独占) | 1200 在线调试只有监视/强制/trace |
9.2 VARIANT: 通用形参 (1200 可用)
VARIANT 只能作形参 (Input/InOut/Temp) 且调用时必须连接实参, 运行时携带实参的类型与地址信息。典型“泛型 FC“:
// FC "AvgRealArr": 对任意长度/任意边界的 REAL 数组求均值
FUNCTION "AvgRealArr" : REAL
VAR_INPUT
arr : VARIANT; // 调用时接任意 ARRAY OF REAL
END_VAR
VAR_TEMP
n, i : DINT;
sum : REAL;
tmp : ARRAY[1..64] OF REAL; // 中转缓冲 (演示上限 64)
END_VAR
BEGIN
sum := 0.0;
#tmp := []; // 不可行, 数组无此初始化; 见下——逐个不用, VariantGet 整体复制
n := CountOfElements(#arr); // 实参数组的元素个数
IF n > 64 THEN n := 64; END_IF; // 演示用途的硬上限
VariantGet(SRC := #arr, DST => #tmp); // 复制到具体类型 (类型不符会失败, ENO 可查)
FOR #i := 1 TO #n DO
sum := sum + #tmp[#i];
END_FOR;
IF #n > 0 THEN
"AvgRealArr" := sum / TO_REAL(#n);
END_IF;
END_FUNCTION
// 调用 (实参是 DB 里的任意 REAL 数组):
#avg := "AvgRealArr"(arr := "DataDB".measurements);
配套: VariantPut (向 VARIANT 指向的目标写入)、IS_ARRAY(#v)、TypeOf(#v) 与 TypeOf(#otherVar) 比较。参数名 (SRC/DST 等) 以编辑器 IntelliSense 为准。注意上面 #tmp := [] 那行是错误示范 (SCL 数组没有这种清零语法), 实际靠 VariantGet 整体覆盖。
9.3 ARRAY[*]: 变长数组形参 (需优化块 + FW >= 4.2)
比 VARIANT 更轻的“任意边界数组“方案, 搭配 LOWER_BOUND / UPPER_BOUND:
FUNCTION "SumArr" : DINT
VAR_IN_OUT
a : ARRAY[*] OF DINT; // FC 中任意声明区可用; FB 中仅 InOut
END_VAR
VAR_TEMP
i : DINT;
s : DINT := 0; // Temp 初值不要依赖, 这里演示; 正式代码仍应在体内赋 0
END_VAR
BEGIN
s := 0;
FOR #i := LOWER_BOUND(a, 1) TO UPPER_BOUND(a, 1) DO // (数组, 维号)
s := s + a[#i];
END_FOR;
"SumArr" := s;
END_FUNCTION
实参可以是任意 [下界..上界] 的 DINT 数组, 循环自动适配边界 —— 修复了“数组边界不一致“这个 PLC 经典耦合问题。
9.4 PEEK / POKE: 按地址读写 (1200 的“指针“)
经典 S7-1200 没有 POINTER 类型的指针间接寻址; SCL 提供按存储区/偏移读写的指令族 (仅 SCL 可用):
| 指令 | 作用 | 返回/参数 |
|---|---|---|
PEEK(area, dbNumber, byteOffset) | 读 1 字节 | 返回 BYTE |
PEEK_WORD(area, dbNumber, byteOffset) | 读 2 字节 | WORD |
PEEK_DWORD(area, dbNumber, byteOffset) | 读 4 字节 | DWORD |
PEEK_BOOL(area, dbNumber, byteOffset, bitOffset) | 读 1 位 | BOOL |
POKE(area, dbNumber, byteOffset, value) | 写字节/字/双字 | |
POKE_BOOL(area, dbNumber, byteOffset, bitOffset, value) | 写 1 位 | |
POKE_BLK(area_src, dbNumber_src, byteOffset_src, area_dst, dbNumber_dst, byteOffset_dst, length) | 块复制 (类似 memcpy) |
区域码: 16#81 = I (过程映像输入), 16#82 = Q, 16#83 = M, 16#84 = DB (外设输入区 16#1 为 1500 独占)。DB 区必须用标准 (非优化) DB。所有变体支持 ENO => #ok 检测访问错误。
VAR
b : BYTE;
w : WORD;
flag : BOOL;
END_VAR
// 读 M100.0
#flag := PEEK_BOOL(area := 16#83, dbNumber := 0, byteOffset := 100, bitOffset := 0);
// 读标准 DB (假设 DB1) 偏移 10 处的 WORD
#w := PEEK_WORD(area := 16#84, dbNumber := 1, byteOffset := 10);
// 把 M 区 0..9 共 10 字节复制到 DB1 偏移 20 处
POKE_BLK(area_src := 16#83, dbNumber_src := 0, byteOffset_src := 0,
area_dst := 16#84, dbNumber_dst := 1, byteOffset_dst := 20,
length := 10, ENO => #ok);
这是与通信报文、第三方协议打交道的最后手段; 正常逻辑优先用符号名 + 数组变址。
9.5 AT: 同一内存多视图 (受限的 union/reinterpret_cast)
// UDT "UDT_ByteBits" (PLC 数据类型): STRUCT b0, b1, ... b7 : BOOL; END_STRUCT
// -- 8 个 Bool 成员在 Struct 内按位打包, 共 1 字节
FUNCTION_BLOCK "RawFrame" // 建议: AT 示例使用标准访问块 (非优化)
VAR
frame : ARRAY[1..4] OF BYTE; // 通信原始帧 (4 字节)
asDWord : DWORD AT frame; // 同一内存按 DWORD 看 (大小相等, 合法)
modeByte : BYTE;
modeBits : "UDT_ByteBits" AT modeByte; // 字节按位拆开 (TIA 中 AT 写在声明的"覆盖"列)
END_VAR
BEGIN
IF #modeBits.b0 THEN
;
END_IF;
#crc := #asDWord; // 4 字节一读
END_FUNCTION_BLOCK
限制 (官方): 不能覆盖 VARIANT; 覆盖体大小不得超过被覆盖变量; 优化块中只能覆盖保持性变量 (以官方页面 “Accessing a tag with an AT overlay” 为准), 实践建议 AT 只在标准块用。注意 Array of Bool 的存储布局与块访问模式有关 (标准块中每元素 1 字节; 优化块中按位打包)——拿它做位视图前先确认布局, 更稳的位视图是 Bool 成员的 Struct 或 slice 访问 (9.6)。AT 是 reinterpret_cast, 所有类型双关 (type punning) 的注意义务归你。
9.6 布尔数组与位访问汇总
SCL 没有 C 的 bits.field 位域, 但有四种位级手段:
ARRAY OF BOOL: 存储布局与块访问模式有关——标准块中每元素 1 字节 (8 个元素占 8 字节), 优化块中按位打包。拿它做位级视图前先确认实际布局; 位视图更稳的写法是 Bool 成员的 Struct (按位打包) 或 slice 访问。- Slice 访问: 对 (D)WORD/整型变量用
.X<位>.B<字节>.W<字>.D<双字>切片读写:
VAR
statusWord : WORD;
big : DWORD;
END_VAR
// 注意: SCL 中对整型做 slice 需在块属性里关闭 IEC 检查; STRUCT/常量/AT 覆盖变量不可 slice
IF #statusWord.X7 THEN ... END_IF; // X0 是最低位
#statusWord.X7 := TRUE; // 也可写
#nibble := #big.B0; // B0 最低字节 (大端布局下注意, 见第 13 节)
- 绝对位地址:
%M0.0、%I0.0(2.6 节)。 PEEK_BOOL/POKE_BOOL(9.4 节)。
10. 字符串实战
10.1 内存布局: 头 2 字节 + 数据
STRING[n] 占 n+2 字节 (官方):
偏移 0: 最大长度 (声明时的 n)
偏移 1: 当前长度 (0..n)
偏移 2..n+1: 字符数据 (ASCII, 未用位置内容不定)
'AB' 存进 STRING[10]: [10][2]['A']['B'][?][?]...。所有函数的下标从 1 开始 (不是 0), MID(s, 1, 1) 是第一个字符。
10.2 空串与未初始化
- 空串 = 当前长度 0 (
''), 与“全零字节“不同, 但 DB 里 STRING 的默认初值就是''。 - VAR_TEMP 里的 STRING 未初始化时, 头部“当前长度“是随机值 →
LEN()返回垃圾 → 所有函数跟着出错。永远先#s := '';(3.6 节)。 - 赋值超出目标容量的串: 运行时截断且 ENO=0 (编译不报错), CPU 继续 RUN。
- 有效串要求
0 < 最大长度 < 255且当前长度 <= 最大长度。
10.3 内置函数全清单
下表 s/s1/s2 为 STRING, n/l/p 为 INT, 下标 1 基, 全部返回 STRING (除注明):
| 函数 | 语义 | 示例 (结果) |
|---|---|---|
LEN(IN := s) → INT | 当前长度 | LEN('abc') = 3 |
LEFT(IN := s, L := l) | 前 l 个字符 | LEFT('PLC123', 3) = 'PLC' |
RIGHT(IN := s, L := l) | 后 l 个字符 | RIGHT('PLC123', 3) = '123' |
MID(IN := s, L := l, P := p) | 从位置 p 起 l 个字符 | MID('PLC123', 3, 2) = 'LC1' |
CONCAT(IN1 := s1, IN2 := s2) | 拼接 | CONCAT('a','b') = 'ab' |
DELETE(IN := s, L := l, P := p) | 删除从 p 起 l 个字符 | DELETE('ABCDEF',2,3) = 'ABEF' |
INSERT(IN1 := s1, IN2 := s2, P := p) | 把 s2 插到 s1 的 p 位置前 | INSERT('ADEF','BC',2) = 'ABCDEF' |
REPLACE(IN1 := s1, IN2 := s2, L := l, P := p) | s1 从 p 起 l 个字符换成 s2 | REPLACE('ABCDEF','XY',2,3) = 'ABXYEF' |
FIND(IN1 := s1, IN2 := s2) → INT | s2 在 s1 中首次出现位置; 找不到 0 | FIND('ABCABC','CA') = 3 |
EQ_STRNG(IN1, IN2) / NE_STRNG | 串相等/不等 (LAD 用; SCL 直接写 = <> 即可) | 'a' = 'a' 为 TRUE |
S_CONV(IN := x, OUT => y) | 串与数互转 (方向由参数类型定) | 见 10.4 |
STRG_VAL(...) | STRING → 数值, 带 P/Q 格式参数 | 见 10.4 |
VAL_STRG(...) | 数值 → STRING, 带 SIZE/P/Q 格式参数 | 见 10.4 |
注意: SCL 对 STRING 的比较运算只有 = 和 <> (没有 >/< 的字典序比较)。
10.4 串数互转
VAR
s : STRING[16];
v : INT;
r : REAL;
END_VAR
// 数 → 串: S_CONV 最简单; 需要定宽/小数位时用 VAL_STRG (SIZE 总宽, P 整数位, Q 小数位, 具体语义以帮助为准)
S_CONV(IN := 123, OUT => #s); // #s = '123'
// 串 → 数: S_CONV 从首字符起转换, 遇非法字符/超范围 → ENO=0 且输出 0
S_CONV(IN := '42', OUT => #v); // #v = 42
// STRG_VAL/VAL_STRG 提供符号/小数/宽度控制, 处理 '3.14'、'-5' 等:
STRG_VAL(IN := #s, OUT => #r); // 参数 P/Q 省略时按默认格式
10.5 FOR 逐字符处理: 转大写 (SCL 没有 s[i] 下标与 ORD())
SCL 不支持 #s[i] 直接下标到字符, 也没有 ORD/CHR。逐字符的标准姿势: MID 取出“1 字符的 STRING“, 用 FIND 在字母表里定位, 再从另一张表取对应字符:
FUNCTION "ToUpper" : VOID
VAR_IN_OUT
s : STRING[64];
END_VAR
VAR CONSTANT
lo : STRING[26] := 'abcdefghijklmnopqrstuvwxyz';
up : STRING[26] := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
END_VAR
VAR_TEMP
i, n, pos : INT;
one : STRING[1]; // MID 结果截断为 1 字符
out : STRING[64];
END_VAR
BEGIN
out := ''; // 初始化!
n := LEN(IN := #s);
FOR #i := 1 TO #n DO
one := MID(IN := #s, L := 1, P := #i); // 第 i 个字符
pos := FIND(IN1 := #lo, IN2 := #one); // 是小写字母?
IF #pos > 0 THEN
out := CONCAT(IN1 := out, IN2 := MID(IN := #up, L := 1, P := #pos));
ELSE
out := CONCAT(IN1 := out, IN2 := #one); // 非字母原样保留
END_IF;
END_FOR;
#s := out;
END_FUNCTION
逐字符解析数字 (不使用 STRG_VAL 的手写版, 展示模式):
FUNCTION "ParseInt" : INT
VAR_INPUT s : STRING[10]; END_VAR
VAR CONSTANT digits : STRING[10] := '0123456789'; END_VAR
VAR_TEMP i, n, pos, val : INT; one : STRING[1]; END_VAR
BEGIN
n := LEN(IN := #s); val := 0;
IF n = 0 THEN
"ParseInt" := -1; RETURN;
END_IF;
FOR #i := 1 TO #n DO
one := MID(IN := #s, L := 1, P := #i);
pos := FIND(IN1 := #digits, IN2 := #one);
IF #pos <= 0 THEN // 非数字字符
"ParseInt" := -1; RETURN;
END_IF;
val := val * 10 + (#pos - 1); // FIND 的位置恰好 = 数值 + 1
END_FOR;
"ParseInt" := val;
END_FUNCTION
性能提示: 逐字符 CONCAT 是 O(n^2); 大量文本处理应攒进定长缓冲或干脆在 HMI/上位机做。
11. 时间与日期
11.1 类型总表
| 类型 | 宽度 | 语义/范围 | 备注 |
|---|---|---|---|
TIME | 32 位 | 有符号毫秒, 约 ±24d20h31m23s647ms | 唯一能直接加减的时长类型; 会回绕 (8.7) |
DATE | 16 位 | 日历日, 1990-01-01 ~ 2168 年 | 字面量 D#2026-09-05 |
TIME_OF_DAY (TOD) | 32 位 | 毫秒精度的日内时间 | 字面量 TOD#13:45:30.123 |
DTL | 12 字节 | 1970-01-01 ~ 2262-04-11, 结构体 | 1200 上读系统时间用它 |
DT (DATE_AND_TIME) | 8 字节 | 兼容旧系统 | 1200 上优先 DTL |
LTIME/LDT/LTOD | - | 纳秒级/长时型 | S7-1500 独占, 1200 无 |
11.2 DTL: 结构体字段访问
VAR
now : DTL;
END_VAR
// 字段: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND (USINT), NANOSECOND (UDINT)
#now.MONTH := 9;
IF #now.HOUR >= 22 OR #now.HOUR < 6 THEN
#nightMode := TRUE;
END_IF;
11.3 读系统时间: RD_SYS_T / RD_LOC_T
VAR_TEMP
now : DTL;
w : WORD;
END_VAR
RD_SYS_T(OUT => #now); // 系统时间 (UTC 相关); 有可选返回值/RET_VAL, 以帮助为准
// RD_LOC_T(OUT => #now); // 本地时间 (含组态时区/夏令时)
毫秒级测耗时的稳妥做法: 不是解析 DTL, 而是用 TON.ET 或 TIME 差值 (8.7); 或读两次 #now 转 TIME 累计 (要自己处理字段进位, 不推荐)。系统时间断电是否保持取决于 CPU 供电 (电池/电容), 不要假设。
12. 运行时错误与错误处理
12.1 先立世界观: 没有 SIGSEGV, 没有 exception
C 程序员需要重建的预期: S7-1200 的设计哲学是错误尽量不挂机。大多数“非法操作“的结果是 ENO=0 或诊断缓冲记录 + CPU 继续 RUN。真正会转 STOP 的主要是看门狗超时 (12.5) 等少数情况。
12.2 三大典型错误在 S7-1200 上的真实后果 (官方)
| 错误 | C 里的直觉 | S7-1200 实际行为 | S7-1500 对照 |
|---|---|---|---|
数组越界 (#a[99], 声明 [1..10]) | 段错误/未定义行为 | 记入诊断缓冲, CPU 保持 RUN; 访问被放弃; ENO 不会被置 FALSE (勿想靠 ENO 捕获) | 转 STOP, 可用 OB121 或局部错误处理拦截 |
整数除零 (#a / 0) | SIGFPE 崩溃 | ENO=0, 返回值置 0 (结果未定义), CPU 保持 RUN, 无错误 OB | 类似 |
| REAL 非法 (0.0/0.0 → NaN, 溢出) | inf/NaN 静默传播 | ENO=0, 返回 NaN/INF 语义值, 保持 RUN | 类似 |
CONV 溢出 (TO_INT(1.0E9)) | 截断 | ENO=0 (OUT 取值以 ENO 为准、不要依赖), 保持 RUN | 类似 |
串转数非法 ('12a') | - | ENO=0, OUT=0 (从首字符转到第一个非法字符为止), 保持 RUN | 类似 |
整型算术结果溢出 (32767+1 on INT) | 回绕 | ENO=0, OUT 结果无效 | 类似 |
// 除零防护的正确写法 (自防, 别指望运行时):
IF #divisor <> 0 THEN
#q := #a / #divisor;
ELSE
#q := 0;
#mathErr := TRUE;
END_IF;
12.3 OB121/OB122 在 1200 上不存在; 用 GET_ERROR
S7-1500/300/400 的“编程错误 OB121 / I/O 访问错误 OB122“机制 在 S7-1200 上不存在。1200 的对应物:
- 编程错误: 启用块内局部错误处理后, 错误写入错误结构, 用
GET_ERROR/GET_ERROR_ID读取; 启用全局错误处理时写诊断缓冲并保持 RUN。 - I/O 访问错误: 首次发生记诊断缓冲, 保持 RUN。
VAR_TEMP
err : ErrorStruct; // 系统类型: 含 ErrorID 等
id : WORD;
bad : BOOL;
END_VAR
// 在疑似出错的语句之后:
GET_ERROR(ERROR => #err); // 取最近一次本块错误详情 (ENO=TRUE 表示确实发生了错误)
// id := GET_ERROR_ID(); // 只取 16 位错误码 (调用签名以指令帮助为准)
IF #err.ErrorID <> WORD#16#0000 THEN
#bad := TRUE;
END_IF;
ErrorStruct 的高字节是错误类别、低字节是具体原因; 完整字段表查指令帮助。诊断缓冲 (diagnostics buffer) 在 TIA 在线视图 & CPU 诊断里可读, 是事后排障的第一入口。
12.4 能让 S7-1200 转 STOP 的事 (短清单)
- 同一扫描周期内看门狗二次超时 (见下);
- 某些不可恢复的硬件/系统故障;
- (1500 上会 STOP 的越界/编程错误在 1200 上不STOP —— 两代产品策略不同, 迁移代码时务必重验。)
12.5 看门狗 (watchdog) 与扫描时间
- 最大扫描循环时间: 组态范围 1~6000 ms, 默认 150 ms, 恒启用 (设备组态 → CPU 属性 → Cycle)。
- 超时触发“时间错误“事件: 编程了对应的错误 OB 时执行它并保持 RUN; 未编程时: 第一次超时被容忍、保持 RUN; 同一扫描周期内第二次超时 (周期耗时达 2 倍最大循环时间) → 记诊断缓冲并 STOP (不同固件版本的手册章节措辞略有差异, 以你的固件手册为准)。
- 主动续命:
RE_TRIGR();重启监视计时 (当前周期 < 10 倍最大值时成功, ENO=TRUE)。放在长循环体内:
FOR #i := 1 TO 100000 DO
// ...重活...
IF #i MOD 1000 = 0 THEN
RE_TRIGR(); // 喂狗, 防止长循环触发时间错误
END_IF;
END_FOR;
- 更好的做法不是喂狗而是拆任务: 用状态机把长工作切片到多个扫描周期 (PLC 正道)。
13. 字节序与位操作
13.1 S7 是大端 (big-endian)
一个 WORD 在内存/寄存器中: 低地址放高字节。%MW0 由 %MB0(高) + %MB1(低) 组成; %MD0 = MB0..MB3, MB0 是最高字节。位编号: .X0 = 最低有效位。与 x86 (小端) 相反, 与网络字节序相同。
WORD 值 16#ABCD 存于 MW0:
MB0 = 16#AB (高字节, 低地址)
MB1 = 16#CD (低字节, 高地址)
Modbus、很多传感器协议是小端 —— 通信数据经常要换序, 这是 SWAP 存在的理由。
13.2 位移/循环移位/字节交换指令
| 指令 | 语义 | C 对应 |
|---|---|---|
SHL(IN := x, N := n) | 左移 (补 0) | x << n |
SHR(IN := x, N := n) | 右移 | x >> n (无符号语义) |
ROL(IN := x, N := n) | 循环左移 | 无 (要手写) |
ROR(IN := x, N := n) | 循环右移 | 无 |
SWAP(IN := x) | 字节序交换 (WORD/DWORD) | 无内置 |
VAR
bHi, bLo : BYTE;
w : WORD;
END_VAR
// 两个 BYTE 拼一个 WORD (协议解析常见):
#w := SHL(IN := TO_WORD(#bHi), N := 8) OR #w#... // 写法见下, 一步到位:
w := SHL(IN := TO_WORD(#bHi), N := 8) OR TO_WORD(#bLo);
// 拆开:
#bHi := TO_BYTE(SHR(IN := #w, N := 8));
#bLo := TO_BYTE(#w);
// 字节交换 (大小端转换):
#netOrder := SWAP(IN := #hostOrder);
// 循环移位流水灯:
#lights := ROL(IN := #lights, N := 1);
以上 #w := ... 与 w := ... 是同一变量 (大小写不敏感), 示例保留笔迹提醒读者注意。
13.3 位操作速查
- 取某位:
IF #statusWord.X7 THEN(slice, 需关 IEC 检查) 或PEEK_BOOL。 - 置/清位:
#statusWord.X7 := TRUE;或掩码:#statusWord := #statusWord OR WORD#16#0080;清位:#statusWord := #statusWord AND (NOT WORD#16#0080); - 测多位掩码:
IF #status AND WORD#16#0F0F = WORD#16#0F0F THEN(注意加括号:(#status AND WORD#16#0F0F) = WORD#16#0F0F, 虽然 AND 优先级低于比较, 显式括号可读性更好)。
14. 附录A: C/Python ↔ SCL 语法速查大表
| # | 语义 | C | Python | SCL |
|---|---|---|---|---|
| 1 | 块注释 | /* */ | 无 | (* *) (可嵌套) |
| 2 | 行注释 | // | # | // |
| 3 | 赋值 | x = y; | x = y | x := y; |
| 4 | 相等 / 不等 | == / != | == / != | = / <> |
| 5 | if / else | if () {} else {} | if: else: | IF THEN ELSE END_IF; |
| 6 | else-if | else if | elif | ELSIF |
| 7 | switch/case | switch+case+break | match (3.10) | CASE x OF ... ELSE ... END_CASE; |
| 8 | case 范围 | 无 | case 10|20 | 10..20: |
| 9 | for 计数循环 | for(i=0;i<n;i++) | for i in range(n) | FOR i := 0 TO n-1 BY 1 DO |
| 10 | while 先测 | while (c) | while c: | WHILE c DO |
| 11 | do-while 后测 | do {} while(!c) | 无 | REPEAT UNTIL c END_REPEAT |
| 12 | break / continue | break / continue | break / continue | EXIT / CONTINUE |
| 13 | 返回值 | return v; | return v | 块名 := v; RETURN; |
| 14 | 三元表达式 | c ? a : b | a if c else b | 无 (IF/ELSE 语句或 FC) |
| 15 | 逻辑与/或/非 | && || ! | and or not | AND OR NOT (无短路) |
| 16 | 位与/或/异或/反 | & | ^ ~ | & | ^ ~ | AND OR XOR NOT |
| 17 | 移位 | << >> | << >> | SHL() SHR() |
| 18 | 循环移位 | 手写 | 手写 | ROL() ROR() |
| 19 | 取模 | a % b (符号同被除数) | a % b (符号同除数) | a MOD b (符号同被除数, 同 C) |
| 20 | 幂 | pow(a,b) | a ** b | a ** b |
| 21 | 整除 | 7/2=3 (截断) | 7//2=3, -7//2=-4 | 7/2=3 (截断, 同 C) |
| 22 | 布尔字面量 | 1/0 | True/False | TRUE/FALSE |
| 23 | 十六进制 | 0xFF | 0xFF | 16#FF / WORD#16#00FF |
| 24 | 二进制 | 0b1010 (C23) | 0b1010 | 2#1010 |
| 25 | 数字分隔符 | ' (C14) | _ | _ (1_000_000) |
| 26 | 字符字面量 | 'a' | 'a' | 'a' (CHAR) |
| 27 | 字符串字面量 | "abc" | "abc" | 'abc' (单引号) |
| 28 | 串内引号转义 | "a\"b" | 'a"b' | 'It''s' (双写) |
| 29 | 字符串拼接 | strcat() | a + b | CONCAT(IN1:=a, IN2:=b) |
| 30 | 字符串长度 | strlen() | len(s) | LEN(IN := s) |
| 31 | 子串 | strncpy | s[a:b] | LEFT/RIGHT/MID |
| 32 | 查找 | strstr | s.find(t) | FIND(IN1, IN2) (1 基, 0=未找到) |
| 33 | 数组声明 | int a[10]; | a = [0]*10 | a : ARRAY[1..10] OF INT; (任意下界) |
| 34 | 二维数组 | int m[3][4] | 列表套列表 | ARRAY[1..3, 1..4] OF INT, 访问 m[2,3] |
| 35 | 数组长度 | sizeof(a)/sizeof(a[0]) | len(a) | UPPER_BOUND(a,1) (需 ARRAY[*]) 或常量 |
| 36 | 结构体 | struct {..} | 无 (类/dataclass) | STRUCT .. END_STRUCT / UDT |
| 37 | 成员访问 | s.f / p->f | s.f | s.f (无指针) |
| 38 | 枚举 | enum | Enum | 无; 用 INT 常量集 |
| 39 | 常量 | const/#define | 大写约定 | VAR CONSTANT x : T := v; END_VAR |
| 40 | 函数定义 | int f(int x){..} | def f(x): | FUNCTION "f" : INT + 接口区 |
| 41 | 无返回函数 | void f() | (返回 None) | FUNCTION "f" : VOID |
| 42 | 多返回值 | 指针参数/结构体 | 元组 | VAR_OUTPUT 多个输出参数 |
| 43 | 引用传参 | int *x / T& | 可变对象 | VAR_IN_OUT |
| 44 | 全局变量 | 文件级变量 | 模块级变量 | 全局 DB 变量 / PLC tags |
| 45 | 静态局部变量 | static | 函数属性/闭包 | (FB 的 VAR 静态区) |
| 46 | 空语句 | ; | pass | ; |
| 47 | NULL/None | NULL | None | 无 (无指针); 可用“有效标志“ BOOL |
| 48 | 类型转换 | (int)x | int(x) | TO_INT(x) 等 TO_* 族 |
| 49 | 舍入取整 | (int)x 截断 / round | int() / round() | TRUNC / ROUND / CEIL / FLOOR |
| 50 | 绝对值/钳位 | abs 手写 | abs min max | ABS MIN MAX LIMIT |
15. 附录B: 同一状态机的 C 与 SCL 双版本
需求: 电机序列控制。IDLE –start–> STARTING (开接触器等反馈, 2s 超时判故障) –> RUNNING (开阀) –stop–> STOPPING (500ms 冷却) –> IDLE。
15.1 C 版本 (裸机轮询风格, dt 毫秒由调度器喂入)
#include <stdbool.h>
#include <stdint.h>
typedef enum { IDLE = 0, STARTING = 1, RUNNING = 2, STOPPING = 3 } MotorState;
typedef struct {
MotorState state;
uint32_t t_in_state_ms; // 进入当前状态后累计时间
} MotorSm;
void motor_sm_step(MotorSm* m, bool start, bool stop, bool feedback,
uint32_t dt_ms,
bool* contactor, bool* valve, bool* fault)
{
*contactor = false; *valve = false; *fault = false;
m->t_in_state_ms += dt_ms; // 时间由外部推进
switch (m->state) {
case IDLE:
if (start) { m->state = STARTING; m->t_in_state_ms = 0; }
break;
case STARTING:
*contactor = true;
if (feedback) { m->state = RUNNING; m->t_in_state_ms = 0; }
else if (m->t_in_state_ms > 2000)
{ m->state = STOPPING; m->t_in_state_ms = 0; *fault = true; }
break;
case RUNNING:
*contactor = true; *valve = true;
if (stop) { m->state = STOPPING; m->t_in_state_ms = 0; }
break;
case STOPPING:
if (m->t_in_state_ms > 500) { m->state = IDLE; m->t_in_state_ms = 0; }
break;
}
}
15.2 SCL 版本 (时间交给 TON, 不需要 dt 管道)
FUNCTION_BLOCK "MotorSm"
VAR_INPUT
start : BOOL; // 启动按钮
stopCmd : BOOL; // 停止按钮
feedback : BOOL; // 接触器反馈 (运行确认)
END_VAR
VAR_OUTPUT
contactor : BOOL; // 接触器命令
valve : BOOL; // 工艺阀
fault : BOOL; // 启动超时故障 (单周期脉冲)
stateNum : INT; // 当前状态码 (供 HMI/监视)
END_VAR
VAR
state : INT := 0; // 0=IDLE 1=STARTING 2=RUNNING 3=STOPPING
tState : TON; // "进入当前状态多久了"计时器, IN 恒真, 转移时复位
END_VAR
VAR CONSTANT
T_START : TIME := T#2s;
T_COOL : TIME := T#500ms;
END_VAR
BEGIN
// 状态计时器: 每周期无条件调用; ET 在达到 PT 后不再增长,
// 因此 PT 要设得比所有阈值大
#tState(IN := TRUE, PT := T#10s);
// 输出默认值 (对应 C 版函数开头的清零)
#contactor := FALSE;
#valve := FALSE;
#fault := FALSE;
CASE #state OF
0: // IDLE
IF #start THEN
#state := 1;
RESET_TIMER(T := #tState); // 对应 C 的 t_in_state_ms = 0
END_IF;
1: // STARTING: 等反馈, 2s 超时 → 故障
#contactor := TRUE;
IF #feedback THEN
#state := 2;
RESET_TIMER(T := #tState);
ELSIF #tState.ET >= #T_START THEN
#fault := TRUE;
#state := 3;
RESET_TIMER(T := #tState);
END_IF;
2: // RUNNING
#contactor := TRUE;
#valve := TRUE;
IF #stopCmd THEN
#state := 3;
RESET_TIMER(T := #tState);
END_IF;
3: // STOPPING: 500ms 冷却
IF #tState.ET >= #T_COOL THEN
#state := 0;
RESET_TIMER(T := #tState);
END_IF;
END_CASE;
#stateNum := #state;
END_FUNCTION_BLOCK
移植对照要点:
- C 版自带
t_in_state_ms += dt_ms的时间推进管道; SCL 版把时间委托给 TON (PLC 的惯用法), 转移时RESET_TIMER对应清零。 - C 的
switch无初值概念; SCL 的state : INT := 0用声明初值定义上电状态 (实例 DB 的初值在下载/暖启动时生效)。 - C 版枚举是类型; SCL 用 INT + 注释/常量,
stateNum输出给 HMI 显示。 - 两版的
fault都只保持一个周期 (对应 C 版每轮重算), 要锁存就加faultLatch。
16. 附录C: 常见编译报错与解决方法
报错文案随 TIA 版本变化, 下列为典型措辞与根因 (按出现频率排序):
| # | 典型报错 (英, 措辞近似) | 根因 | 解决 |
|---|---|---|---|
| 1 | The operand '#x' is not defined / identifier not found | 变量未声明; 或全局名没加双引号; 或大小写拼错 | 声明/补 # (块内) 或 "名" (全局); 记住大小写不敏感, 重名冲突也报这附近 |
| 2 | Duplicate identifier 'MOTOR' | 声明了 motor 又声明 MOTOR | 改名; 大小写不敏感 |
| 3 | The data types of the operands are not compatible / cannot be converted implicitly | 窄化赋值 (REAL→INT、DINT→INT、WORD→BYTE) 或 INT 与 STRING 混算 | 显式 TO_INT() 等; 见 4.7 隐式规则 |
| 4 | Value out of range / constant out of value range | #i := 16#FFFF; (#i 是 INT) 或 #i := 65535; | 用类型前缀 WORD#16#FFFF 或换宽类型 |
| 5 | 语法错在 ==、!=、&&、++ | C 运算符带进来了 | 改 =、<>、AND、+1 |
| 6 | Syntax error 在 IF #x := 1 THEN | 把比较写成了赋值 | IF #x = 1 THEN; 反方向: 语句里把赋值写成 = 同理报错 |
| 7 | Cannot convert ... to BOOL / condition not BOOL | IF #count THEN 整型当条件 | 写全 IF #count > 0 THEN |
| 8 | Keyword 'xxx' is reserved / keyword cannot be used | 变量名撞关键字/指令名/类型名 (FOR、TON、INT…) | 改名 |
| 9 | A function block call requires an instance DB / no instance specified | 直接调 FB 没带实例 | 用实例 DB 名或块内声明的多重背景实例调用; 见第 7 节 |
| 10 | The parameter 'IN' is unknown (调用内置函数/指令时) | 参数名写错 (如 LEN(S := ..)) | 查函数表参数名: LEN 用 IN, MID 用 IN/L/P; 见 10.3 |
| 11 | The index ... is outside the limits of the array | 常量下标越界 (编译期可查的部分) | 检查边界; 注意声明是 [1..10] 不是 [0..9] 时首元素是 1 |
| 12 | Invalid TIME literal 'T#30s1h' | TIME 单位未从大到小排列/重复 | T#1h30m; 负值用 0 - T#30s |
| 13 | Invalid string length / length must be 0..254 | STRING[300] | 上限 254; 记得 [n] 不写就是 254 |
| 14 | Constants expected (CASE 标签处) | CASE 标签写了变量或表达式 | 标签只能是常量/命名常量 |
| 15 | The control variable of the FOR loop ... (赋值被拒) | 循环体内给循环变量赋值 | 用 EXIT/CONTINUE 控制流, 不要动循环变量 |
| 16 | 警告: function value not set / 未赋值路径 | FC 某路径没给函数名赋值 | 每条路径 块名 := 值; (返回值未定义是运行时垃圾) |
| 17 | POINTER / ANY / REF_TO ... not supported for this CPU | 用了 1500 独占特性 | 1200 替代: VARIANT / ARRAY[*] / InOut / PEEK-POKE; 见第 9 节 |
| 18 | 警告: IEC check 未关导致 .X0 slice 报错 | 对整型做位片访问 | 块属性关闭 IEC 检查, 或改用掩码运算 |
运行时 (不报编译错但行为怪) 三巨头, 一并挂在这里备忘: 数组运行期越界 (1200 不 STOP, 见 12.2)、Temp 未初始化 (3.6)、优化 DB 上 PEEK/POKE (9.4)。
17. 参考出处 (事实主源)
- SIMATIC S7-1200 Manual Collection (TIA Portal V20 在线文档): OB 优先级与队列、循环时间监视 (150 ms 默认)、数组越界/除零/转换错误行为、GET_ERROR、PEEK/POKE、AT 覆盖、STRING 布局、数据类型表。 https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20
- SCL 指令参考 (S7-1200, S7-1500), V20 (各条目标题即标注 CPU 支持): https://docs.tia.siemens.cloud/r/en-us/v20/scl-s7-1200-s7-1500
- 块接口合法数据类型 (V20, VARIANT/ANY/POINTER/REF_TO/ARRAY[*] 差异): https://docs.tia.siemens.cloud/r/en-us/v20/declaring-the-block-interface/rules-for-declaring-the-block-interface/valid-data-types-in-the-block-interface-s7-1200-s7-1500
- S7-1200 数据类型转换总览 (隐式/显式): https://docs.tia.siemens.cloud/r/en-us/v20/data-types/data-type-conversion-for-s7-1200-s7-1200/overview-of-data-type-conversion-s7-1200
- ROUND/TRUNC/CEIL/FLOOR (SCL 默认返回 DINT): S7-1200 Manual Collection, Conversion operations 章节。
本文所有示例代码均按 S7-1200 + TIA Portal V16~V20 SCL 语法书写 (变量声明区与语句区分离,
BEGIN/END_*完整闭合), 可直接粘贴到外部源或块编辑器中编译 (个别演示全局 DB/UDT 需先建)。发现与官方手册冲突之处, 以手册为准并欢迎在本仓库提 issue。