05 | 从 C/Python 转 SCL: 27 个常见陷阱与最佳实践 (S7-1200 + TIA Portal)
面向有 10 年 C/Python 经验、刚接触 PLC 的工程师。所有行为描述以 S7-1200 + TIA Portal V16~V20 为准; 不确定处标注 (待核实)。SCL 代码中 # 前缀是块内局部/接口变量, "..." 是全局 DB/块名。
本文技术断言经三轮独立复核(运行语义 / 版本适用性 / C 类比误导性)修正, 存疑处已按官方文档改写。权威出处: TIA Portal Information System (docs.tia.siemens.cloud) 与 S7-1200 System Manual (Entry 109977302)。
目录:
- 陷阱 1~27: 每条 = 现象 / 根因 / 正确写法 / C/Python 对照
- 最佳实践: 命名规范、UDT 优先、状态机惯用法(完整骨架)、FB 单一职责、库化复用、TIA 项目版本管理、在线下载对 DB 的影响
陷阱 1: := 与 = 写反
现象: 编译报错, 或逻辑永远不成立。最常见三种写错: IF #a := 1 THEN、#b = 5;、以及从 C 带来的 #a == 1。
根因: SCL 里赋值是 :=, 比较是 =。没有 C 的 ==, 也没有 !=(不等是 <>)。赋值是语句, 比较是表达式, 二者语法位置不同。
正确写法:
// 声明区 (FB 接口)
// Input: cmd : Bool
// setpoint : Int
// Output: done : Bool
// Static: step : Int
// 代码区
IF #cmd = TRUE THEN // 比较: =
#step := #setpoint; // 赋值: :=
END_IF;
#done := (#step = 100); // 比较产生 Bool 表达式, 可直接赋给 Bool
C/Python 对照: 相当于 C 把 = 与 == 的职责分给两个 token。C 里 if (a = b) 是合法但致命的 bug; SCL 里 IF #a := #b THEN 直接编译失败——错误在编译期被拦住, 这是好事。反过来, 读别人代码时 IF a = b 是“等于“, 别按 C 直觉当成赋值。
陷阱 2: VAR_TEMP 不初始化, 残留脏数据
现象: FC/FB 里临时变量第一次用就读到随机值, 且 bug 间歇性出现——改了别处代码, 这个块的脏数据就变了。
根因: 临时局部数据(VAR_TEMP)在 L 栈上, 官方明确: 只在一个扫描周期内有效, “必须在读取它的那个周期内先写入”; 标准访问的 FC 中未初始化的 Temp “值可能是随机的”。它读到的是上一次恰好压在同一栈位置的其他块留下的内容。
正确写法:
// FC "FC_Avg" 接口
// Input: value : Real
// Output: avg : Real
// Temp: sum : Real
// i : Int
// 代码区: 先写后读, 每周期开头清零
#sum := 0.0;
FOR #i := 1 TO 10 DO
#sum := #sum + #value;
END_FOR;
#avg := #sum / 10.0;
需要跨周期记忆的值放 Static(FB)或全局 DB, 绝不放 Temp。
C/Python 对照: 就是“读未初始化的栈变量“——和 C 的 float sum; 未初始化一样, 值是栈上残留。Python 没有这个问题(名字必须先绑定), 所以 Python 背景的人最容易踩。类比成立之处: 都读的是内存残留; 不成立之处: PLC 的 L 栈内容在两个扫描周期之间“看起来还在“, 让人误以为 Temp 能存状态——不能。
陷阱 3: 数组下标非 0 基, 以及越界的真实后果
现象: ARRAY[1..8] 被当成 ARRAY[0..7] 用, #a[0] 或 #a[8] 越界; 更隐蔽的是 FOR 循环边界抄错。程序看起来还在正常跑, 但某个阀门永远不动。
根因: SCL 数组上下界任你声明([1..8]、[0..7]、甚至 [-10..10] 都合法), 与 C 的“数组名即首地址、恒 0 基“不同。越界的后果在 1200 上反直觉: 官方文档明确, S7-1200 运行期访问声明限值之外的数组元素, CPU 记入诊断缓冲并保持 RUN——不 STOP, ENO 也不会被置 FALSE(ENO 置 FALSE 的是 S7-300/400 且勾选 Check ARRAY limits 的场景; S7-1500 则直接 STOP)。
正确写法:
// FC "FC_Scan" 接口
// InOut: data : Array[0..15] of Int // 习惯上声明成 0 基, 与 C 直觉一致
// Input: enable : Bool
// Temp: i : DInt
// Output: firstNeg : Int
#firstNeg := 0;
FOR #i := 0 TO 15 DO // 边界直接用常量, 或见下
IF #data[#i] < 0 THEN
#firstNeg := #data[#i];
EXIT;
END_IF;
END_FOR;
接收“任意长度数组“的通用写法用 ARRAY[*] 形参 + LOWER_BOUND/UPPER_BOUND(S7-1200 需优化块 + 固件 >= V4.2; 按系统手册表: FC 中只能声明在 Input 与 InOut 区, FB 中只能放 InOut, Output/Return 不可声明):
// FC "FC_SumAny" — InOut: arr : Array[*] of DInt; Temp: i : DInt; Return: sum : DInt
#sum := 0;
FOR #i := LOWER_BOUND(ARR := #arr, DIM := 1) TO UPPER_BOUND(ARR := #arr, DIM := 1) DO
#sum := #sum + #arr[#i];
END_FOR;
C/Python 对照: C 越界是未定义行为(可能段错误); Python 越界抛 IndexError。S7-1200 越界既不崩也不抛——只有诊断缓冲里一条记录, 产线继续跑, 这是比崩溃更危险的行为。养成习惯: 数组边界永远来自声明本身(常量或 LOWER/UPPER_BOUND), 不手写魔数。注意 FOR 的循环变量是 DInt/Int 等整型, 不要用 Bool 之类。
陷阱 4: 优化 DB vs 标准 DB: 绝对地址/AT/PEEK 全部失效
现象: 第三方设备用 GET 按地址读不到你 DB 里的数据; PEEK 报错或读到错位数据; AT 覆盖声明编译不过; HMI 写 InOut 偶发丢值。
根因: TIA 新建 DB 默认勾选 “Optimized block access”。优化 DB 的元素只有符号名、没有固定绝对地址(%DBn.DBX... 不再有意义), CPU 自动紧凑排放。于是: PEEK/POKE 访问 DB 区要求标准 DB; AT 覆盖在优化块里只允许覆盖保持性(retentive)标签(标准块可覆盖任意标签); 跨 CPU 的 PUT/GET 类绝对地址访问在伙伴侧要求标准 DB; WSTRING 只能声明在优化块; ARRAY[*] 在 1200 上也要求优化块。标准 DB 则是固定偏移、兼容 S7-300/400 生态。
| 特性 | 优化 DB (默认) | 标准 DB |
|---|---|---|
| 符号访问 | 支持 | 支持 |
绝对地址 %DBn.DBXm.y | 无意义 | 有效 |
| PEEK/POKE 访问 DB 区 | 不可用 | 可用 |
| AT 覆盖 | 仅保持性标签 | 任意标签 |
| 伙伴 CPU 用 GET 绝对读 | 不可用 | 可用 |
| WSTRING 声明 | 可 | 不可 |
| 存储紧凑/防错位 | 是 | 有对齐空隙, 插变量会移动后面所有偏移 |
正确写法: 默认用优化 DB(安全、防地址错位); 只有确需按字节偏移映射(通信表、AT 覆盖、PEEK/POKE)的 DB 才取消勾选优化。AT 做“位视图“的例子(标准访问的 FB 内, 这是 AT 的传统合法位置):
// UDT "UDT_WordBits"(PLC 数据类型):
// STRUCT b0, b1, ... b15 : Bool; END_STRUCT
// -- 16 个 Bool 成员在 Struct 内按位打包, 共 2 字节
// FB "FB_Raw"(标准访问) 接口 Static:
// raw : Word;
// bits : "UDT_WordBits" AT raw; // AT 覆盖: 与 raw 共享同一 2 字节
// (TIA 中 AT 写在接口声明的"覆盖"列)
// 代码区
#raw := 16#00FF;
IF #bits.b0 THEN ... END_IF;
AT 的硬规则(易错点): 覆盖体大小不得超过被覆盖变量; Array of Bool 的存储布局与块访问模式有关——标准块中每元素 1 字节(16 个 Bool 占 16 字节, 无法覆盖 2 字节的 Word), 优化块中才按位打包, 想稳妥地给 Word 做位视图应使用 16 个 Bool 成员的 Struct/UDT(Struct 内 Bool 按位打包, 共 2 字节); AT 声明传统上只能写在 FB/FC/OB 的接口区(全局 DB 内的 AT 需 TIA V16+ 且 1200 固件 >= V4.4 的标准 DB, 兼容性差, 不推荐); 优化块内的 AT 有限制(仅保持性标签, 以官方页面 “Accessing a tag with an AT overlay” 为准)——所以实践建议: AT 只在标准块里用。
C/Python 对照: 优化 DB 像 Python 对象——只能按属性名访问, 拿不到稳定偏移; 标准 DB 像 C 结构体——有 offsetof, 能 memcpy, 但插入字段会把后面全移位(所以标准 DB 的通信表一旦定型不要中间插变量)。类比基本成立, 但注意优化 DB 不是 Python 的动态对象, 类型仍编译期固定。
陷阱 5: REAL 相等比较的浮点误差
现象: IF #level = 10.0 THEN 永远不触发; 累加 0.1 十次得到 1.0000001(16#3F800001)而不是精确的 1.0; #ratio = #a / #b 判断不稳定。
根因: REAL 是 IEEE 754 单精度, 二进制无法精确表示 0.1 这类十进制小数, 与 C/Python 的 float 完全同源。另外 SCL 的 / 在两个整型之间做整型除法(截断), 1/3 是 0——想算实数必须让操作数至少一方是 Real。
正确写法:
// 声明区 (FB Static)
// setpoint : Real := 10.0
// eps : Real := 1.0E-6
// tenth : Real
// acc : Real
IF ABS(#level - #setpoint) < #eps THEN // 容差比较
#ok := TRUE;
END_IF;
#acc := 0.0;
FOR #i := 1 TO 10 DO
#acc := #acc + 0.1; // 累加误差, 与 C float 相同
END_FOR;
#ratio := #a * 1.0 / #b; // 或 REAL#1.0/3.0; 两个 Int 相除会截断
C/Python 对照: 与 fabs(x-y) < eps、math.isclose 一一对应, 类比完全成立。差异点: SCL 里 REAL 是 32 位, 精度只有约 7 位十进制有效数字——比 Python 默认的 64 位 double 差得多(Python 里 sum([0.1]*10) 得 0.9999999999999999, SCL 的 REAL 得 1.0000001, 都是错, 错法不同), 误差更早暴露; 需要高精度用 LREAL(1200 支持 LREAL)。
陷阱 6: 边沿检测: R_TRIG/F_TRIG 是 FB, 必须有实例
现象: 想在按钮按下瞬间动作一次, 结果写成 IF #btn THEN ..., 按住一秒动作了几百次(每个 scan cycle 一次); 或者把 R_TRIG 的调用放在 IF 里, 边沿时灵时不灵。
根因: PLC 的输入是“电平“, 每个 cycle 都会被重新求值, 没有“事件回调“。边沿检测本质是“本周期值 vs 上周期值“的比较, 需要一块跨周期记忆的存储——这正是 FB + 实例 DB。R_TRIG/F_TRIG 就是这样的 FB, 调用时必须指定实例(单实例 DB 或多重实例)。
正确写法(多重实例, 推荐):
// FB 接口
// Input: btn : Bool
// Output: pulse : Bool
// Static: trigBtn : R_TRIG // 多重实例, 放 Static; 无条件每周期调用
#trigBtn(CLK := #btn); // 无条件调用, 让它每周期都刷新记忆
#pulse := #trigBtn.Q;
IF #pulse THEN
#count := #count + 1; // 只在 0->1 的那个周期执行一次
END_IF;
手写等价物(理解原理用):
// Static: btnLast : Bool
#pulse := #btn AND NOT #btnLast; // 上升沿
#btnLast := #btn; // 每周期末尾保存, 供下一周期比较
C/Python 对照: 就是 C 的 edge = cur && !last; last = cur; 或 Python 边沿检测库里的 was_pressed()。R_TRIG 的实例 DB 相当于那个 static bool last。类比的边界: 实例 DB 是持久状态, 掉电后按保持性设置决定是否恢复; C 的 static 变量重启即丢——保持性(retentive)是 PLC 独有概念。另见陷阱 7: 把 R_TRIG 放进条件分支调用会破坏 last 的连续性。
陷阱 7: 条件调用 FB: 不调用的周期输出保持旧值
现象: IF #mode = 2 THEN #motorFB(...); END_IF;——切到别的模式后, #motorFB 的输出(速度、运行反馈、故障字)冻结在最后一次调用时的值, HMI 上显示一个不存在的旧转速。
根因: FB 的输出与内部状态全部存在实例 DB 里。FB 不被调用的周期, 实例 DB 原样保留, 输出自然不动。没有任何机制帮你“清零“。
正确写法: 二选一。方案 A(推荐): FB 每周期无条件调用, “使能“作为 FB 的一个输入, 由 FB 内部决定输出:
// FB "FB_Heater" 接口
// Input: enable, fault, cmd
// Output: power : Real
// Static: state : Int
// 调用侧 (每周期一次):
// #heater(enable := #modeOk, cmd := #cmd, fault := #fault);
// FB 内部: 不使能时显式给输出赋安全值
IF NOT #enable THEN
#power := 0.0;
#state := 0;
RETURN; // 直接返回, 后面逻辑不执行
END_IF;
方案 B: 调用侧在跳过分支手工清理输出/内部状态。
C/Python 对照: 相当于一个长生命周期的对象, 不调用 update() 它的字段就停在旧值——C 里全局 struct、Python 里的单例都一样。类比成立之处: 状态持久; 不成立之处: PLC 里“跳过调用“是常见控制流(按模式切换), 而软件里通常不会整轮跳过 update, 危险性更高。附带提醒: FB 内部的 IEC 定时器基于时间累计, FB 长时间不调用后重新调用, 其 TON 可能直接判为已到期(待核实, 取决于固件对 IEC_Timer 结构的时间戳处理), 跨周期的长延迟逻辑要显式设计。
陷阱 8: WHILE 阻塞扫描: 没有 sleep, 定时必须 TON 跨周期
现象: 想等 500 ms 再动作, 写了 WHILE 循环空转; 或者在 OB1 里处理一大批数据耗时 300 ms——CPU 一会儿就 STOP, 诊断缓冲显示 cycle time 超时。
根因: PLC 程序不是常驻进程, 而是 scan cycle 模型: 读输入 -> 逐条执行 OB1 -> 写输出 -> 内务 -> 下一轮。OB1 必须在最大扫描循环时间(S7-1200 范围 1~6000 ms, 默认 150 ms, 恒启用)内完成。超时触发“时间错误“事件: 若编程了时间错误 OB(默认优先级 22, 可改 22~26)则执行它并保持 RUN; 未编程时按系统手册(不同章节措辞略有差异, 建议以你固件版本手册为准): 同一扫描周期内第一次超时通常被容忍、保持 RUN, 第二次超时(周期耗时达 2 倍最大循环时间)记入诊断缓冲并 STOP。没有 sleep/delay 函数, 空转 WHILE 就是烧掉整个周期预算; 且阻塞期间所有输出刷新、低优先级任务全部停摆。
正确写法: 延时用 TON, 让“等待“分布在多个扫描周期里:
// FB 接口
// Input: cmd : Bool
// Static: tOn : TON_TIME // 多重实例
// state : Int
// Output: done : Bool
// 代码区
#tOn(IN := (#state = 1), PT := T#500MS); // 无条件调用, IN 由状态驱动
IF #cmd AND (#state = 0) THEN
#state := 1;
ELSIF #state = 1 AND #tOn.Q THEN // 500 ms 后跨周期推进
#state := 2;
#done := TRUE;
END_IF;
大批量数据处理同样拆分: 每周期处理 N 条, 用 Static 游标记住进度。确有一段逻辑必须长时间运行时, 可用 RE_TRIGR 重启周期监视(当前周期 < 10 倍最大值时 ENO=TRUE), 但这是最后手段。
C/Python 对照: 相当于裸机嵌入式的主循环 + 看门狗: while(1) { read_inputs(); step(); write_outputs(); feed_watchdog(); }——你在 step() 里 sleep(1) 整个系统就死。time.sleep() 在 PLC 世界没有对应物, “经过时间“只能靠定时器状态机这种协作式调度表达。类比完全成立, 只是 PLC 的看门狗超时默认直接 STOP 整台 CPU, 比多数嵌入式更狠。
陷阱 9: 除零/数学错误的真实行为: 不崩溃, 静默返回 0
现象: 除数为 0, 程序“没事“继续跑, 结果悄悄变成 0, 控制量跳变; 浮点非法运算得到 NaN 往下游传播。
根因: S7-1200 官方行为表: 整数除零(IN2=0)时“结果未定义并返回 0“, ENO=0, CPU 保持 RUN, 不触发任何错误 OB; REAL/LREAL 的 NaN/INF 非法运算同样 ENO=0 返回 NaN。而且 S7-1200 上不存在编程错误 OB(OB121)/I/O 访问错误 OB(OB122)——这类事件在 1200 上要么走诊断缓冲保持 RUN, 要么在启用了“局部错误处理“的块里用 GET_ERROR/GET_ERROR_ID 读取。CONV 转换溢出: ENO=0(OUT 的具体取值不同版本文档表述有差异——不要依赖 OUT, 以 ENO 为准); 字符串转数值(STRG_VAL/S_CONV)遇非法字符或超范围: ENO=0, OUT=0。全部保持 RUN。
正确写法: 除法前显式判零, 数学指令的错误要么预检要么事后校验:
// 声明区 (FB Static)
// denom : Real, numer : Real, ratio : Real, ratioOk : Bool
IF ABS(#denom) < 1.0E-9 THEN // 预检, 别指望运行时拦截
#ratio := 0.0;
#ratioOk := FALSE;
ELSE
#ratio := #numer / #denom;
#ratioOk := TRUE;
END_IF;
在块内放置 GET_ERROR/GET_ERROR_ID 指令, 即启用该块的局部错误处理(注意: 这不是块属性开关, 而是“块里有没有这对指令“), 这是 1200 上替代 OB121 的机制。
C/Python 对照: C 整数除零是未定义行为(通常 SIGFPE 崩溃), Python 抛 ZeroDivisionError——你习惯了“除零必炸“。S7-1200 恰恰相反: 静默给 0 且继续 RUN。类比不成立之处必须记住: 这里的错误哲学是“带 ENO 的容错“, 与其等异常, 不如把守卫写在前面。
陷阱 10: 通信字节序(大端)与 SWAP
现象: 从 Modbus TCP / 第三方设备收到的 INT 值“怪怪的“: 0x1234 变成 0x3412, 4660 变成 13330; REAL 完全是乱码。
根因: S7 CPU 的数据存储与协议字节序是大端(big-endian, 高字节在低地址), 与 x86/ARM 上 C 的小端相反。两个寄存器拼 DINT/REAL 时还有“高低字顺序“问题(不同厂商对 32 位值的两个字先后排列不统一)。Python 的 struct.pack/unpack 默认按本机小端, 同样要显式 > 才是大端。
正确写法:
// 声明区 (FB Static)
// hiB, loB : Byte; wRaw, wFix : Word; vInt : Int;
// wHi, wLo : Word; dw : DWord; vReal : Real;
// 两字节 -> INT (大端帧): 高字节左移 8 位
#wRaw := SHL(IN := BYTE_TO_WORD(#hiB), N := 8) OR BYTE_TO_WORD(#loB);
#vInt := WORD_TO_INT(#wRaw);
// 字节序颠倒时的修复: SWAP 交换 WORD 内两字节
#wFix := SWAP(IN := #wRaw); // 16#3412 -> 16#1234
// 两字 -> DINT/REAL (确认高字在前):
#dw := SHL(IN := WORD_TO_DWORD(#wHi), N := 16) OR WORD_TO_DWORD(#wLo);
#vReal := DWORD_TO_REAL(#dw); // DWORD->REAL 按位搬运, 不做数值转换
调试手段: 用 PEEK_WORD/PEEK_DWORD 对标准 DB 或 I 区按字节偏移检查原始帧(区域码 16#81=I, 16#82=Q, 16#83=M, 16#84=DB; DB 区必须标准 DB)。
C/Python 对照: 等价于 C 的 ntohs/ntohl 与手动移位拼包, 等价于 Python struct.unpack('>h', data)。类比成立。额外注意: DWORD_TO_REAL 这类等宽转换是“位模式搬运“(类似 C 的 memcpy / 指针重解释), 而 DINT_TO_REAL 是数值转换(类似 (float)i)——用错语义就出鬼数据。
陷阱 11: STRING 定长、截断与形参传递
现象: 赋值后字符串尾部莫名丢失; 一个“字符串变量“占了 256 字节; 传参后长度变化; 拼接结果变短。
根因: String 是带 2 字节头的定长缓冲: 第 1 字节最大长度, 第 2 字节当前长度, 后跟字符, 总占用 = 最大长度 + 2。声明 myStr : String 不写 [n] 时默认 String[254], 即占 256 字节。赋给更短的 String 时从右侧截断到目标最大长度。形参也有自己的声明长度, 实参过长同样截断。String 不能赋给 I/Q 存储区。比较用 =/<>(按字符比较)。子串/查找等操作靠 LEN、LEFT、RIGHT、MID、CONCAT、DELETE、INSERT、REPLACE、FIND 等 FC。
正确写法:
// 声明区
// name : String[16] // 占 18 字节, 按需声明, 别默认 254
// line : String[32]
// n : Int
#name := 'PUMP-01'; // 当前长度 7, 实际存 7 个字符
#line := CONCAT(IN1 := #name, IN2 := ' OK'); // 超过 32 截断
#n := LEN(IN := #name); // 7
IF #name = 'PUMP-01' THEN ... END_IF;
// 传参: 形参声明 String[16]; 传入 String[254] 的实参会被截到 16
C/Python 对照: String 既不是 C 的 char*(无指针、无 ‘\0’ 结尾, 长度显式存在头部), 也不是 Python str(非不可变对象, 长度上限编译期固定)。最接近的类比是 C 的 struct { char max, len; char buf[N]; } 定长字段。注意 WString(UTF-16)在 1200 上只能声明在优化块中; 字符串指令(LEN/CONCAT/FIND 等)的参数表本身同时接受 String 与 WString 操作数, 但 WString 进不了标准 DB 与通信映射表, 实际项目仍以 String 为主。
陷阱 12: 多 OB 中断并发访问共享数据的一致性
现象: OB1 里算出的流量偶尔跳变一个离谱值; 累计值偶尔少加一次; 状态机偶发“跳状态“。
根因: S7-1200 的程序循环 OB(OB1, 优先级 1, 最低)“可被所有其它事件类型中断”——循环中断 OB30~38(默认优先级 8~17)、硬件中断(优先级 18)、延时中断 OB20~23 等随时打断 OB1(中断延迟约 175 us)。共享的全局 DB 数据没有任何锁; OB1 里“读-改-写“序列(如 #total := #total + #x;)或读取多字段结构的过程中断, 就会出现撕裂。抢占规则完全由优先级决定: 任意高优先级 OB 都能打断正在执行的任何低优先级 OB——硬件中断(默认优先级 18)可打断循环中断(OB30~38, 默认 8~17), 循环中断可打断 OB1; 同优先级的多个事件只排队(先来先服务), 不互相打断。所以风险不只是“打断 OB1“: 任意两个不同优先级的 OB 之间, 只要共享数据就可能撕裂。
正确写法:
// 硬件中断 OB (>=123): 只做最小工作, 原子地更新快照
"DB_Shared".snapshot := "DB_Shared".rawValue; // 单个 Real 赋值, 一条指令完成
"DB_Shared".seq := "DB_Shared".seq + 1; // 序号, 供 OB1 校验
// OB1: 用"序号未变则不取"的模式读一致快照
#seq1 := "DB_Shared".seq;
#val := "DB_Shared".snapshot;
#seq2 := "DB_Shared".seq;
IF #seq1 = #seq2 THEN // 序号相同 => 读取期间未被中断改写
#use := #val;
END_IF;
原则: (1) 共享量控制在单个对齐的字/双字内(单条指令搬运, 天然原子); (2) 多字段一致性用双缓冲 + 序号; (3) 能在中断 OB 里完成的算术就别放 OB1; (4) 绝不在两个不同优先级 OB 里做同一变量的读-改-写。
C/Python 对照: 中断 OB 就是 signal handler / 硬件 ISR——共享变量没有锁, data race 与 torn read 的模型完全一致, volatile/memory barrier 那套直觉可以迁移。不成立之处: 这里的“线程“是按优先级严格抢占且不可阻塞, 也没有 mutex 原语可用, 只能靠“缩短临界区 + 数据布局“解决, 类似无锁编程的约束。
陷阱 13: TIME 回绕(约 24.8 天)
现象: 设备连续运行 24.8 天后, 基于“自上电自由计时“做差值比较的逻辑集体异常: 延时瞬间触发或永不触发。
根因: TIME 是 32 位有符号毫秒, 范围 T#-24d_20h_31m_23s_648ms ~ T#+24d_20h_31m_23s_647ms(即 -2147483648 ~ +2147483647 ms), 任何自由运行计时到约 24.8 天即回绕; 直接比较 now >= deadline 在回绕点附近必然出错。平台大坑: 教程里常见的 TIME_TCK() 是 S7-1500 专属指令, 在 S7-1200 上编译直接报错(“Block TIME_TCK is not supported by the CPU”)。1200 上测耗时用 RUNTIME()(返回 LReal 秒, 基于内部高频计数器, 溢出时会返回 <= 0, 该次读数应忽略)、RTM 运行时间计量, 或 RD_SYS_T 读日历时间做差。
正确写法: 差值比较(对计数回绕安全的惯用法):
// 声明区 (FB Static)
// lastT : LReal, interval : LReal := 1.0 // 秒
#now := RUNTIME(); // S7-1200 可用; LReal 秒
IF #now > 0.0 THEN // 丢弃溢出时的无效读数(<=0)
IF (#now - #lastT) >= #interval THEN // 差值法, 对回绕安全
#lastT := #now;
#fire := TRUE;
ELSE
#fire := FALSE;
END_IF;
END_IF;
一般的延时逻辑优先用 TON(IEC 定时器自己处理时间累计), 只有自研调度/秒表才碰自由计时; 对 TIME 类型做算术时, “有符号差值 >= 间隔“的补码回绕惯用法依然成立且推荐。1200 上无 LTIME(1500 独占), DTL 可用于日历时刻(1970~2262)。
C/Python 对照: 与 Arduino 的 millis() 回绕、Linux jiffies 完全同构, (uint32_t)(now - last) >= dt 这个惯用法原样迁移(这里用 DInt 有符号数, 补码回绕行为等效)。类比成立; 额外坑是 TIME 字面量写法 T#24d_20h 与类型检查, 混用 TIME/DInt 需显式 TIME_TO_DINT。
陷阱 14: 符号大小写不敏感撞名
现象: 块里已声明 motor, 再加 Motor 直接报“标识符已使用“; 两个 FB 一个叫 fbPump 一个叫 FbPump, 在跨块引用/库导入时报重名冲突。
根因: TIA 对 PLC 标签/块名的唯一性检查不区分大小写——Motor 与 MOTOR 视为同名。另外保留关键字(指令名、IEC 函数名、数据类型名如 Int、Time)不得用作标识符; 含空格/特殊字符的名字要加双引号。
正确写法: 同一作用域内不要用大小写区分两个名字; 命名规范里显式规定风格(见最佳实践), 并避免:
// 编译失败示例:
// Input: cmd : Bool
// Static: Cmd : Int // 与 cmd 同名 (大小写不敏感)
// 正确: 语义不同就换词
// Input: cmd : Bool
// Static: cmdCount : Int
C/Python 对照: C 与 Python 都大小写敏感, Total/total 是两个变量——这个直觉在 SCL 失效。可以理解为“所有标识符都被 lower-case 归一化后再查重“, 类似某些配置系统对环境变量名的处理。团队协作时特别危险: 你觉得是两个人定的不同名字, 编译器认为是重名。
陷阱 15: 巨型 OB1 与全局变量滥用
现象: OB1 写到上千行, 几十个 FB 直接连着几十个全局 DB, 逻辑互相穿插; 改一处, 三台设备行为都变; 没法在 PLCSIM 里单测任何一段。
根因: C/Python 里“一个 main + 一堆全局变量“的反模式在 PLC 界同样致命, 但更隐蔽——因为全局 DB 在 TIA 里用起来毫无摩擦(双引号即达), 且早期 LAD 习惯就是把所有东西放 M 区/全局 DB。后果: 命名冲突面大(全局且大小写不敏感)、隐式耦合(FB 内部直接读全局 DB, 签名上看不出依赖)、下载影响面失控、无法复用到下一台机器。
正确写法: OB1 只当调度器; 每个工艺对象一个 FB + 实例 DB; 全局只留物理 I/O 映射和真正的共享资源:
// OB1 (SCL) — 只做调度
// 全局 DB "DB_IO": 仅物理量 (sensorRaw, cmdPulse ...) 与 HMI 共享量
// FB "FB_Conveyor": 封装整条输送机逻辑; FB "FB_Pump": 封装泵逻辑
"FB_Conveyor_DB"(sensor := "DB_IO".convSensor,
motorCmd => "DB_IO".convMotor);
"FB_Pump_DB"(enable := "FB_Conveyor_DB".pumpReq,
feedback := "DB_IO".pumpFb,
cmd => "DB_IO".pumpCmd);
判断标准: FB 的所有输入输出都出现在接口里、不读写全局 DB 的内部状态, 才能整体拷贝到下一个项目。
C/Python 对照: OB1 = main(), FB + 实例 DB = 类 + 实例, 全局 DB = 全局单例, UDT = struct 定义。整个重构直觉(mains should be thin, 依赖走参数)可以直接迁移。不成立之处: FB 的“实例“是数据块, 与代码分离——更像“一个函数 + 一块专属内存“, 没有方法表与继承。
陷阱 16: 临时区大数组吃 L 区
现象: FC 里声明 Temp: buf : Array[1..4096] of DInt 或十几个 String(默认各 256 字节), 编译过但运行行为异常, 或深调用链上某块直接出错——L 栈耗尽。
根因: VAR_TEMP 分配在局部数据栈(L 区)上, 每层调用都占一份, 大小按 CPU 型号有限(典型仅若干 KB, 具体查 CPU 规格表)(待核实: 各型 1200 的 L 区精确上限)。S7 用户程序也不支持递归调用, 深层嵌套 + 大 Temp 是最常见的爆栈方式。另外 Temp 不初始化(陷阱 2), 大缓冲读脏数据的概率更高。
正确写法: 大缓冲放 DB(全局 DB 或 FB 的 Static/InOut), 通过引用传递:
// FC "FC_Filter" 接口 — 大数组走 InOut (按引用传递, 不占 L 栈)
// InOut: buf : Array[0..4095] of DInt
// Input: len : DInt
// Return: avg : DInt
#sum := 0;
FOR #i := 0 TO #len - 1 DO
#sum := #sum + #buf[#i];
END_FOR;
IF #len > 0 THEN #avg := #sum / #len; ELSE #avg := 0; END_IF;
C/Python 对照: 与“不要在栈上放 16 KB 数组, 改用堆/memcpy 传指针“一致; InOut 传参相当于传指针(C 的 int buf[] 参数 / Python 传列表引用)。类比成立之处: 栈容量有限、按值拷贝昂贵; 不成立之处: PLC 没有 malloc/堆, “堆“的角色由 DB 承担, 且 DB 同时是持久存储。
陷阱 17: 调试方式差异: 没有断点, 没有 print
现象: 想 printf 一个中间值发现没有控制台; 想打断点单步, TIA 界面上的断点按钮是灰的。
根因: 官方明确: 断点测试仅支持 STL 与 SCL 代码, 但支持的 CPU 只有 S7-300/400 和固件 >= V2.5 的 S7-1500——S7-1200 不支持断点(包括 PLCSIM 仿真 1200 的场景)。也没有任何 print/控制台: CPU 上没有 stdio。
正确替代手段(按用途选):
| 需求 | 1200 上的做法 |
|---|---|
| 看变量实时值 | Program status(在线监视 SCL 代码行旁的值) |
| 盯一组关键量 | Watch table(监视表), 可在线修改/强制(force) |
| 看时序波形 | Trace(示波器式采集; 1200 固件 V4.0 起支持, 需真实 CPU——PLCSIM 不支持录制) |
| “打印“日志 | 写入专用诊断 DB(环形缓冲 + 时间戳), HMI 或监控端读取 |
| 事后分析 | CPU 诊断缓冲(在线与诊断 -> 诊断缓冲) |
| 仿真调试 | PLCSIM + 上述手段(注意 PID_Compact V2.x 不支持对经典 1200 仿真) |
调试用诊断 DB 的最小实现:
// 全局 DB "DB_Diag"
// idx : DInt
// log : Array[0..99] of String[40]
// 需要记录处:
"DB_Diag".idx := ("DB_Diag".idx + 1) MOD 100;
"DB_Diag".log["DB_Diag".idx] := CONCAT(IN1 := 'E', IN2 := INT_TO_STRING(#errorCode));
C/Python 对照: 心智模型从 gdb/IDE 断点 + print 切换到“永远在线的仪表“: Watch table 像调试器的 watch 面板(但常驻、免断点), Trace 像 logic analyzer/示波器, 诊断 DB 像结构化日志。关键差异: PLC 的“调试“不打断被控过程——设备在转, 你不能暂停真实世界, 这也是断点在低危 CPU 上被砍掉的根本原因。
陷阱 18: FC 不是纯函数——它有副作用, 形参也会“脏“
现象: 把 FC 当无副作用的纯函数用, 后来发现它悄悄改了全局 DB; 标准访问的 FC 里读到的输入形参值有时不对。
根因: FC 只是没有自己的持久存储, 并不妨碍它读写全局 DB 与 M 区——“FC=纯函数“是错觉。且标准访问的 FC 中, 未连接/未写入的形参与 Temp 同源, 值不确定。
正确写法: FC 内部不直接读写全局 DB(依赖全走形参); 形参先赋值再读; 需要跨周期记忆就换 FB。
C/Python 对照: C 函数也能改全局变量, 这半条类比成立; 但 C 没有“未初始化形参“, 形参那半条是 PLC 特有。
陷阱 19: 优化块的 Temp 会被预置默认值——坏习惯被掩盖
现象: 在(默认的)优化块里 Temp 不初始化也“没事“; 把代码搬到标准块(为做 AT/PEEK/通信表)后, 同一段代码静默读出脏数据。
根因: S7-1200 固件 >= V4(及 S7-1500 全系)的优化块中, Temp 每次调用前被预置默认值(数值 0); 标准访问块没有这层保护。
正确写法: 始终按陷阱 2 的“Temp 必脏“纪律写代码, 不因当前块恰是优化块而偷懒——代码是会被搬家的。
C/Python 对照: 相当于“Debug 构建清零、Release 构建不清零“的经典差异: 行为依环境而变, 唯一安全假设是“未初始化即未知“。
陷阱 20: 布尔表达式不短路求值
现象: IF #i <= #n AND #a[#i] > 0 THEN 在 #i 越界时照样触发访问错误——守卫没起作用。
根因: SCL 的 AND/OR 对两个操作数都求值, 没有短路语义(不同于 C 的 && 与 Python 的 and)。
正确写法: 守卫拆成嵌套 IF:
IF #i <= #n THEN
IF #a[#i] > 0 THEN
// 安全: 只有第一层成立才求值第二层
END_IF;
END_IF;
C/Python 对照: 这是 Pascal 系语言的经典差异; 所有依赖短路的条件表达式(ptr && ptr->val、i < n and a[i])都必须重写成嵌套 IF, 无一例外。
陷阱 21: 同一输出多处赋值——最后写者胜, 且周期末才刷新
现象: 两个 FB 都驱动同一个 Q, 输出“偶发不对“, 无任何编译告警; 写了 Q 后立即读回, 读到的是旧值。
根因: 一个扫描周期内对同一地址多次赋值, 只有最后一次生效; 物理输出在周期末统一刷新; 读 %Q 读到的是输出映像而非端子电平。
正确写法: 一个输出只有一个“属主“代码路径; 模式切换用显式优先级 IF 或 SEL/MUX 收敛到一次赋值。确需本周期立即生效时写 %Q:P(直写外设, 慎用)。
C/Python 对照: 像多写者竞争, 但没有竞态——执行顺序确定(自上而下), 只是反直觉: “赋值即生效“的事件驱动直觉不迁移。
陷阱 22: 输入映像每周期只采样一次——窄脉冲整体消失
现象: 按钮/传感器的短脉冲偶尔“没收到“, 以为是程序 bug。
根因: %I 在每个扫描周期开始时快照一次; 比扫描周期短的电平变化(或恰好落在两次采样之间的脉冲)完全不可见。
正确写法: 短脉冲/高频信号用硬件中断 OB(编号 >= 123)、HSC 高速计数器, 或 %I:P 直读外设; 普通按钮无需处理(按压时长远大于周期)。
C/Python 对照: 采样率不足的 ADC——Nyquist 直觉直接迁移: 扫描周期就是你的输入采样周期(典型 1~10 ms)。
陷阱 23: 两次 FB 调用共用一个实例 DB——状态串扰
现象: 两台“同型号设备“互相影响: 一台复位, 另一台的定时器/状态也乱了; 偶发性与陷阱 7 相似, 极难排查。
根因: FB 的全部状态在实例 DB 里; 同一实例接两台设备 = 两个“对象“共享同一块内存。
正确写法: 每个“对象“一个实例——多重实例(在父 FB 的 Static 里声明)或独立实例 DB; 排查偶发状态异常时, 第一步检查实例 DB 是否被两处复用。
C/Python 对照: 误共享 static/单例的经典事故, 完全同构。
陷阱 24: 在线改值会被用户程序在下一个周期改回去
现象: Watch table 给变量写了新值, “没反应“或一闪而过; 以为通信或程序坏了。
根因: 只要程序每周期给该变量赋值, 手写的值立即被覆盖——与 gdb set var + continue 的预期完全不同。
正确写法: 调试用 force(仅 I/Q 可强制); 或程序里预留 manual/auto 使能分支; 或临时禁用写它的逻辑(记得恢复)。
C/Python 对照: 调试器改值后被主循环覆盖; PLC 里这是常态而非异常, 设计阶段就要给“人工干预“留门。
陷阱 25: 非保持数据掉电即回初始值
现象: HMI 上改好的设定值, 一次停电后“自己变回去“; 现场投诉高发。
根因: DB/M 区数据默认不保持: 暖启动后回到组态初始值; 只有显式设置保持性(retentive, 容量有上限)或写入存储卡/配方机制的数据才掉电保留。
正确写法: 运行期可写参数在 DB 声明中设保持性(注意保持性存储容量上限); 大批量参数用配方/存储卡; 关键累计量显式设计掉电策略。
C/Python 对照: “所有变量都是易失的, 除非你显式放进 EEPROM”——PLC 把这个选择交给程序员, 默认易失。
陷阱 26: “OB1 = main()“类比的边界
现象: 以为整个程序只有一个入口; 多 OB 的执行时机与预期不符。
根因(类比必须标注的边界): 启动 OB(100 或 >= 123)在进入循环前执行一次(≈ init/全局构造); 多个程序循环 OB(同为优先级 1)每周期按编号依次执行, 不是并行; 任何 OB 执行中都可能在语句级被更高优先级 OB 抢占(见陷阱 12); OB 不可重入。
正确写法: 初始化放启动 OB; 程序循环只留 OB1, 不同频率的任务用循环中断 OB(OB30~38)而不是多个循环 OB。
C/Python 对照: 更接近“固定优先级抢占式 RTOS 的任务集“而不是单线程 main: 每个 OB 是一个任务, 优先级表就是调度器; main 只对应了其中一个任务。
陷阱 27: 整型运算静默回绕——Python 背景毫无防备
现象: Int 计数器到 32767 后变成 -32768; 大数乘法/累加结果莫名变负; 无任何告警。
根因: SCL 整型就是 C 的定宽补码整数: 溢出回绕, 编译器与运行时都不报。Python 的 int 无限精度, 从 Python 过来毫无防备(C 背景至少有肌肉记忆)。
正确写法: 计数/累计/大数组下标一律 DInt/UDInt; 关键累加处显式判上限:
IF #count < #COUNT_MAX THEN
#count := #count + 1;
END_IF;
C/Python 对照: 与 C 完全一致(细节上 C 的有符号溢出是 UB, SCL 是定义良好的回绕, 工程上同样不能依赖); 与 Python 完全不同。
最佳实践
1. 命名规范
原则: 全项目统一; 不靠大小写区分语义(陷阱 14); 避开保留字。推荐一套可直接用的:
| 对象 | 风格 | 示例 |
|---|---|---|
| FB/FC/OB | PascalCase + 前缀 | FB_Conveyor, FC_CalcAvg |
| 全局 DB | DB_ 前缀 | DB_IO, DB_Recipe |
| UDT | PascalCase | MotorConfig |
| 实例 DB | 块名 + _Inst | FB_Conveyor_Inst |
| 局部变量/Input/Output | camelCase | cmdStart, speedActual |
| 常量(CONST) | UPPER_SNAKE | ST_RUNNING, CMD_TIMEOUT_MS |
| I/O tag | 物理功能名 | ixStart(i=输入 x=Bool)之类按需定 |
变量名里就编码类型/方向是团队自选项, 但常量用 UPPER_SNAKE 与状态名前缀 ST_ 强烈推荐——它们是 SCL 里枚举的替身(见下)。
2. UDT 优先
凡“同一结构出现在多处“(每台电机一组参数、每段配方一组字段), 先定义 UDT(用户数据类型), 再在 FB 接口、全局 DB、实例 DB 中复用:
TYPE "MotorConfig" :
STRUCT
maxSpeed : Real;
accelTime : Time;
invert : Bool;
END_STRUCT;
END_TYPE
// FB 接口: Input: cfg : "MotorConfig"
// 全局 DB: motors : Array[1..8] of "MotorConfig"
等于 C 的共享 struct 头文件: 改一处定义, 全部使用点编译期强制同步; 且 SCL 的 Array of UDT 天然替代“多台同型设备“的复制粘贴。类比成立, 差异是 UDT 改定义后所有引用块需重新编译下载。
3. 状态机惯用法: CONSTANT + INT 的枚举替身(完整骨架)
SCL 没有枚举类型。惯用法: 块内 CONST 定义状态码, Static 里存 state : Int, CASE 驱动。完整可抄的骨架(含延时、互锁、故障、HMI 状态码):
// ============ FB "FB_Pump" 接口(声明区) ============
// CONST
// ST_IDLE : Int := 0;
// ST_STARTING : Int := 1;
// ST_RUNNING : Int := 2;
// ST_STOPPING : Int := 3;
// ST_FAULT : Int := 99;
// END_CONST
// Input:
// cmdStart : Bool; // 电平
// cmdStop : Bool;
// cmdReset : Bool;
// faultSig : Bool; // 外部故障(电平)
// startDelay : Time := T#3S;
// stopDelay : Time := T#5S;
// Output:
// runCmd : Bool; // 接触器命令
// stateId : Int; // 给 HMI 显示
// ready : Bool;
// Static:
// state : Int;
// faultLatch : Bool;
// tmrStart : TON_TIME; // 多重实例
// tmrStop : TON_TIME;
// ============ 代码区 ============
// 1) 故障处理最优先: 任意状态 -> ST_FAULT
IF #faultSig THEN
#faultLatch := TRUE;
END_IF;
IF #faultLatch AND #cmdReset AND NOT #faultSig THEN
#faultLatch := FALSE;
END_IF;
// 2) 定时器集中无条件调用(避免陷阱 6/7), IN 由状态译码
#tmrStart(IN := (#state = ST_STARTING), PT := #startDelay);
#tmrStop (IN := (#state = ST_STOPPING), PT := #stopDelay);
// 3) 状态转移与输出
CASE #state OF
ST_IDLE:
#runCmd := FALSE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #cmdStart AND NOT #cmdStop THEN
#state := ST_STARTING;
END_IF;
ST_STARTING:
#runCmd := TRUE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #tmrStart.Q THEN
#state := ST_RUNNING;
END_IF;
ST_RUNNING:
#runCmd := TRUE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #cmdStop OR NOT #cmdStart THEN
#state := ST_STOPPING;
END_IF;
ST_STOPPING:
#runCmd := FALSE;
IF #tmrStop.Q THEN
#state := ST_IDLE;
END_IF;
ST_FAULT:
#runCmd := FALSE;
IF NOT #faultLatch THEN
#state := ST_IDLE;
END_IF;
ELSE // 非法状态(如下载后 DB 脏值)兜底回 IDLE
#state := ST_IDLE;
END_CASE;
// 4) 集中导出对外状态
#stateId := #state;
#ready := (#state = ST_IDLE) AND NOT #faultLatch;
要点: 定时器实例的 IN 用状态译码驱动、调用永不进分支; 状态码 0 给初始态(Static 默认 0, 下载初始化即回 IDLE); ELSE 分支兜底; HMI 只读 stateId, 不读内部 flag。C/Python 对照: 这就是函数指针表/字典分发的 switch 状态机, CONST 相当于 enum { ST_IDLE = 0, ... }; 差异是没有真正的枚举类型——状态码本质是裸 Int, 类型系统不防你把 stateId 赋成 47, 所以不要复用状态变量存任意数值。
4. FB 单一职责
一个 FB 只管一个工艺对象(一台泵、一段输送机、一个配方槽)。判据: 名字能用一句话说清; 接口不超过约 15 个信号; 不读写自己的全局 DB; 内部不调用“隔壁设备“的 FB(设备间协同放到上层 FB 或 OB1 编排)。层层嵌套 FB 是允许且推荐的(顶层 FB 用多重实例持有子 FB), 与 C 的模块分层同构。
5. 库化复用
把验证过的 FB/FC/UDT/全局 DB 组拖入 TIA 的 project library / global library, 从库里拖出使用而非复制粘贴; 库支持版本化(库版本 = 你的 release)。修改流程: 改库内对象 -> 升库版本 -> 各项目更新引用。配合 UDT, “第三台产线“的搭建方式是“从库拖件 + 改参数”, 不是“复制整个项目改名字“。类比: global library = 内部包仓库(nuget/pypi), 库版本 = 语义化发布的依赖。注意跨项目复制 FB 时其依赖(UDT、被调 FB)必须一起带走, TIA 的“组复制“(copy group)就是为此设计。
6. TIA 项目的版本管理思路
TIA 项目文件(.ap17/.ap18/…)是二进制容器, git 无法做有意义的 diff/merge。现实可行的组合:
- 块级源码入库: 用 TIA 的外部源(external source)功能把 SCL 块导出为 .scl 文本文件(或用 TIA Openness API 批量导出块/PLC 变量为 XML)纳入 git, 代码评审、diff、 blame 都落在这一层; 二进制 .ap 文件也提交(可配 LFS), 但只当“完整快照“。
- 变更对比: TIA 自带项目/块比较工具(离线 vs 在线、项目 vs 项目), 用于人工比对两版本差异, 相当于图形化 diff。
- 里程碑用库版本管理: 发布给其他项目的块冻结为库版本, 项目里记录“用的哪个库版本“。
- 提交纪律: 二进制无法 diff, 所以 commit message 必须写清改了哪些块、为什么——相当于强制性的 CHANGELOG。
Openness API 是官方脚本化接口(可编程导出/导入、自动编译下载), 把“导出源码 + 编译验证“做成 CI 步骤是成熟做法(具体 API 版本随 TIA 版本变化, 使用前查对应版本文档)。
7. 在线下载对 DB 值的初始化影响
下载不是无害操作。关键事实与策略:
- 下载改动过的 DB 时, 默认会用组态的初始值重新初始化当前值——运行时累计的计数值、当前状态码会丢。关键事实(系统手册 15.16): 下载不会清除保持性存储器中的现值(想清除只能恢复出厂设置); S7-1200(固件 V4.x)支持“不重新初始化下载“(Download without reinitialization), 但仅限优化块内新增标签且不超过内存预留(新建块默认预留 100 字节, 可在块属性中调整; 新增保持性标签需另配保持性内存预留); 修改既有标签的名称/数据类型/数组长度/保持性, 仍会强制重新初始化(当前值复位为初始值)。
- 策略 1: FB 的 Static 里, 所有“可从外部重建“的初值写进声明默认值(state := 0 就是设计的一部分, 见状态机骨架)。
- 策略 2: 关键初始化逻辑放启动 OB(S7-1200 上启动 OB 编号 100 或 >=123, CPU 每次暖启动都会执行它), 不依赖下载时的默认值行为——这等于 C 程序的
init()显式初始化, 比“相信加载器清零“可靠。 - 策略 3: 改 DB 结构前先想清楚哪些是“运行时数据“(计数值、批次号), 把它们隔离到独立 DB, 与“参数/组态“分开, 下载影响面就可控。
- 策略 4: 生产设备上的下载遵守流程: PLCSIM 验证 -> 停机窗口 -> 下载 -> 核对初始化效果。RUN 模式下载仅对兼容改动可用, 不兼容改动会触发 CPU 停止/重启。
附: 陷阱速查表
| # | 陷阱 | 一句话防御 |
|---|---|---|
| 1 | := 与 = | 赋值 :=, 比较 =, 不等 <> |
| 2 | Temp 脏数据 | 先写后读; 跨周期记忆放 Static |
| 3 | 数组越界 | 0 基声明 + LOWER/UPPER_BOUND; 1200 越界不 STOP |
| 4 | 优化 DB | 默认优化; 需绝对地址/AT/PEEK 才用标准 DB |
| 5 | REAL 相等 | 容差比较; Int/Int 相除是整除 |
| 6 | 边沿 | R_TRIG 实例化, 无条件调用 |
| 7 | 条件调用 FB | 每周期调用, enable 做输入; 不使能时显式清输出 |
| 8 | WHILE 阻塞 | 延时一律 TON; 大任务分片跨周期 |
| 9 | 除零 | 预检分母; 错误是 ENO=0 + 返回 0, 不是异常 |
| 10 | 字节序 | 大端; SWAP/移位拼装; DWORD_TO_REAL 是位搬运 |
| 11 | STRING | 定长 n+2 字节; 默认 254; 赋值截断 |
| 12 | 中断并发 | 共享量单字原子 + 序号校验; 临界区放高优先级 OB |
| 13 | TIME 回绕 | 差值法; 优先 TON |
| 14 | 大小写 | 全局不区分大小写, 勿靠大小写区分名字 |
| 15 | 巨型 OB1 | OB1 只调度; FB 封装; 全局只留 I/O |
| 16 | 大 Temp | 大数组走 DB + InOut 引用 |
| 17 | 调试 | 1200 无断点: Watch/Trace/程序状态/诊断 DB |
| 18 | FC 非纯函数 | FC 可写全局 DB/M 区; 形参先赋值再读; 要记忆用 FB |
| 19 | 优化块 Temp 预置默认值 | 始终按“Temp 必脏“写代码, 搬到标准块才不炸 |
| 20 | 布尔不短路 | AND/OR 全操作数求值, 守卫拆嵌套 IF |
| 21 | 多处驱动同一输出 | last-write-wins; 立即生效写 %Q:P(慎用) |
| 22 | 窄脉冲丢失 | 输入映像每周期采样一次; 用硬件中断/HSC/%I:P |
| 23 | 共用实例 DB | 一个对象一个实例, 严禁两处共用 |
| 24 | 在线改值被覆盖 | 周期程序会改回去; 用 force 或 manual 分支 |
| 25 | 掉电丢设定值 | 可写参数设保持性(retentive)或存配方/存储卡 |
| 26 | OB1=main 类比边界 | 启动 OB 先行; 多循环 OB 按序; 语句级可抢占; 不可重入 |
| 27 | 整型静默回绕 | Int 32767 翻负无告警; 计数用 DInt 并设上限 |
| - | 平台专属易踩指令 | TIME_TCK/REF/POINTER/ANY/LTIME/断点均为 1500 独占, 1200 不可用 |