Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

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 的基本分工。本文只关注“语言本身“。

目录速览

  1. SCL 是什么: 定位与心法
  2. 词法与字面量
  3. 变量声明: 接口区、初始化、ARRAY/STRUCT/STRING
  4. 运算符与表达式 (含类型转换)
  5. 控制流
  6. FC 与返回值
  7. FB 与调用
  8. 定时器与计数器 (重点)
  9. 高级类型与机制: VARIANT / ARRAY[*] / PEEK/POKE / AT / 位访问
  10. 字符串实战
  11. 时间与日期
  12. 运行时错误与错误处理
  13. 字节序与位操作
  14. 附录A: C/Python ↔ SCL 语法速查大表
  15. 附录B: 同一状态机的 C 与 SCL 双版本
  16. 附录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 大小写不敏感 (官方行为)

标识符、关键字全部不区分大小写: MotorMOTORmotor同一个变量; ifIFIf 都是合法的 IF 关键字。官方文档明确: PLC 变量名的唯一性检查不区分大小写。因此 MotorMOTOR 不能同时声明, 编译器按重复标识符报错。

工程实践: 团队内统一一种风格 (本文用关键字全大写、变量首字母小写驼峰), 不要靠大小写区分含义 —— 那在 SCL 里是编译错误。

1.3 C 程序员 60 秒上手对照表

你想做的事C 写法SCL 写法
赋值x = y;x := y;
相等比较x == yx = y
不等比较x != yx <> 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 % ba MOD b
pow(a,b)a ** b
行注释/块注释// /* */// (* *)
空语句;;
类型前缀字面量1.0f0x1FULREAL#1.0WORD#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。
  • 保留字 (关键字如 IFFORMOD、指令名如 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.14REAL (必须有小数点)3.14
INT#300带类型前缀的 INT300
DINT#100000带类型前缀的 DINT100000
REAL#1.5带类型前缀的 REAL1.5
WORD#16#00FF类型前缀 + 进制前缀组合255
BOOL#TRUE / TRUE布尔

两个高频坑:

  1. 未定型字面量的推断: #w := 16#FFFF;#w 是 WORD 没问题; 但 #i := 16#FFFF; (#i 为 INT) 会因超出 INT 范围报错。写位模式请养成用类型前缀的习惯 (WORD#16#FFFF), 这相当于 C 的 (uint16_t)0xFFFF 之于裸 0xFFFF
  2. 进制前缀是 基数#, 不是 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 接口区总表: 哪种块能声明什么

声明区FBFCOB存放位置类比
VAR_INPUT是 (仅事件信息)调用时的实参副本函数形参 (值语义)
VAR_OUTPUT实例 DB / 调用方输出形参 (引用回写)
VAR_IN_OUT调用方变量本体C 的 T* 指针形参
VAR (静态)实例 DB类的成员变量
VAR_TEMPL 栈 (不保存)未初始化的栈变量
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 ** 10REAL 运算; 两个整型操作数会先转 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= <>== !=注意符号
9AND (&)&&&SCL 的 AND 比 = 更低, 与 C 的 && 一致, 但修复了 C 的 & 低于 == 的坑
10XOR^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

陷阱清单:

  1. TO_INT(#r)TRUNC 的区别容易混: 对 REAL 输入, TO_INT 等价截断转换; 想要四舍五入必须显式用 ROUND
  2. 转换结果超出目标值域 (如 TO_INT(1.0E9)) → ENO=0, 输出值无效 (OUT 取值不同版本文档表述有差异——以 ENO 为准, 不要依赖 OUT), CPU 保持 RUN (第 12 节)。
  3. 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 switchSCL CASE
fallthrough有 (忘 break 出事故)没有, 每个分支天然独立结束
多值并列case 1: case 3: 靠 fallthrough1, 3: 直接写
值范围10..20 语法内建
标签类型整型常量整型常量 / 命名常量 (VAR CONSTANT); 不允许变量
选择器类型整型/字符/枚举整型 (INT/DINT/BYTE 等)
defaultdefault: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 的静态变量必须落在实例存储区里, 两种落地方式:

  1. 单实例 (single instance): 为 FB 生成一个专属 DB (调用时由 TIA 提示创建), DB 与 FB 同构。
  2. 多重背景 (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_TIMERPRESET_TIMER。每个实例是 16 字节 IEC_Timer 结构。(SIMATIC 遗留定时器 S_PULSE/S_ODT/S_OFFDT 等整族是 S7-1500 独占, 1200 没有, 一句带过不再展开。)

四个定时器的行为表 (IN=输入, PT=预设时长, Q=输出, ET=已计时):

定时器Q 何时为 TRUEET 行为一句话
TONIN 为 TRUE 且持续满 PT 之后IN 变 FALSE 立即清零延时启动 (on-delay)
TOFIN 为 TRUE 期间, 以及 IN 变 FALSE 后再保持 PTIN 下降后开始累计到 PT延时停止 (off-delay)
TPIN 上升沿触发起, 恰好 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 位域, 但有四种位级手段:

  1. ARRAY OF BOOL: 存储布局与块访问模式有关——标准块中每元素 1 字节 (8 个元素占 8 字节), 优化块中按位打包。拿它做位级视图前先确认实际布局; 位视图更稳的写法是 Bool 成员的 Struct (按位打包) 或 slice 访问。
  2. 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 节)
  1. 绝对位地址: %M0.0%I0.0 (2.6 节)。
  2. 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 个字符换成 s2REPLACE('ABCDEF','XY',2,3) = 'ABXYEF'
FIND(IN1 := s1, IN2 := s2) → INTs2 在 s1 中首次出现位置; 找不到 0FIND('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 类型总表

类型宽度语义/范围备注
TIME32 位有符号毫秒, 约 ±24d20h31m23s647ms唯一能直接加减的时长类型; 会回绕 (8.7)
DATE16 位日历日, 1990-01-01 ~ 2168 年字面量 D#2026-09-05
TIME_OF_DAY (TOD)32 位毫秒精度的日内时间字面量 TOD#13:45:30.123
DTL12 字节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 语法速查大表

#语义CPythonSCL
1块注释/* */(* *) (可嵌套)
2行注释//#//
3赋值x = y;x = yx := y;
4相等 / 不等== / !=== / !== / <>
5if / elseif () {} else {}if: else:IF THEN ELSE END_IF;
6else-ifelse ifelifELSIF
7switch/caseswitch+case+breakmatch (3.10)CASE x OF ... ELSE ... END_CASE;
8case 范围case 10|2010..20:
9for 计数循环for(i=0;i<n;i++)for i in range(n)FOR i := 0 TO n-1 BY 1 DO
10while 先测while (c)while c:WHILE c DO
11do-while 后测do {} while(!c)REPEAT UNTIL c END_REPEAT
12break / continuebreak / continuebreak / continueEXIT / CONTINUE
13返回值return v;return v块名 := v; RETURN;
14三元表达式c ? a : ba if c else b无 (IF/ELSE 语句或 FC)
15逻辑与/或/非&& || !and or notAND OR NOT (无短路)
16位与/或/异或/反& | ^ ~& | ^ ~AND OR XOR NOT
17移位<< >><< >>SHL() SHR()
18循环移位手写手写ROL() ROR()
19取模a % b (符号同被除数)a % b (符号同除数)a MOD b (符号同被除数, 同 C)
20pow(a,b)a ** ba ** b
21整除7/2=3 (截断)7//2=3, -7//2=-47/2=3 (截断, 同 C)
22布尔字面量1/0True/FalseTRUE/FALSE
23十六进制0xFF0xFF16#FF / WORD#16#00FF
24二进制0b1010 (C23)0b10102#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 + bCONCAT(IN1:=a, IN2:=b)
30字符串长度strlen()len(s)LEN(IN := s)
31子串strncpys[a:b]LEFT/RIGHT/MID
32查找strstrs.find(t)FIND(IN1, IN2) (1 基, 0=未找到)
33数组声明int a[10];a = [0]*10a : 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->fs.fs.f (无指针)
38枚举enumEnum无; 用 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;
47NULL/NoneNULLNone无 (无指针); 可用“有效标志“ BOOL
48类型转换(int)xint(x)TO_INT(x)TO_*
49舍入取整(int)x 截断 / roundint() / round()TRUNC / ROUND / CEIL / FLOOR
50绝对值/钳位abs 手写abs min maxABS 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

移植对照要点:

  1. C 版自带 t_in_state_ms += dt_ms 的时间推进管道; SCL 版把时间委托给 TON (PLC 的惯用法), 转移时 RESET_TIMER 对应清零。
  2. C 的 switch 无初值概念; SCL 的 state : INT := 0 用声明初值定义上电状态 (实例 DB 的初值在下载/暖启动时生效)。
  3. C 版枚举是类型; SCL 用 INT + 注释/常量, stateNum 输出给 HMI 显示。
  4. 两版的 fault 都只保持一个周期 (对应 C 版每轮重算), 要锁存就加 faultLatch

16. 附录C: 常见编译报错与解决方法

报错文案随 TIA 版本变化, 下列为典型措辞与根因 (按出现频率排序):

#典型报错 (英, 措辞近似)根因解决
1The operand '#x' is not defined / identifier not found变量未声明; 或全局名没加双引号; 或大小写拼错声明/补 # (块内) 或 "名" (全局); 记住大小写不敏感, 重名冲突也报这附近
2Duplicate identifier 'MOTOR'声明了 motor 又声明 MOTOR改名; 大小写不敏感
3The data types of the operands are not compatible / cannot be converted implicitly窄化赋值 (REAL→INT、DINT→INT、WORD→BYTE) 或 INT 与 STRING 混算显式 TO_INT() 等; 见 4.7 隐式规则
4Value out of range / constant out of value range#i := 16#FFFF; (#i 是 INT) 或 #i := 65535;用类型前缀 WORD#16#FFFF 或换宽类型
5语法错在 ==!=&&++C 运算符带进来了=<>AND+1
6Syntax errorIF #x := 1 THEN把比较写成了赋值IF #x = 1 THEN; 反方向: 语句里把赋值写成 = 同理报错
7Cannot convert ... to BOOL / condition not BOOLIF #count THEN 整型当条件写全 IF #count > 0 THEN
8Keyword 'xxx' is reserved / keyword cannot be used变量名撞关键字/指令名/类型名 (FORTONINT…)改名
9A function block call requires an instance DB / no instance specified直接调 FB 没带实例用实例 DB 名或块内声明的多重背景实例调用; 见第 7 节
10The parameter 'IN' is unknown (调用内置函数/指令时)参数名写错 (如 LEN(S := ..))查函数表参数名: LEN 用 IN, MID 用 IN/L/P; 见 10.3
11The index ... is outside the limits of the array常量下标越界 (编译期可查的部分)检查边界; 注意声明是 [1..10] 不是 [0..9] 时首元素是 1
12Invalid TIME literal 'T#30s1h'TIME 单位未从大到小排列/重复T#1h30m; 负值用 0 - T#30s
13Invalid string length / length must be 0..254STRING[300]上限 254; 记得 [n] 不写就是 254
14Constants expected (CASE 标签处)CASE 标签写了变量或表达式标签只能是常量/命名常量
15The control variable of the FOR loop ... (赋值被拒)循环体内给循环变量赋值EXIT/CONTINUE 控制流, 不要动循环变量
16警告: function value not set / 未赋值路径FC 某路径没给函数名赋值每条路径 块名 := 值; (返回值未定义是运行时垃圾)
17POINTER / 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。