Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

S7-1200 + SCL 系统学习仓库

为有 10 年 C/Python 经验、零 PLC 基础的软件工程师定制。 全部材料于 2026-09 整理:资源经联网逐条核实,技术断言经多轮独立复核修正,存疑处一律标注 (待核实)。 目标:约 3 个月(每天 1~2 小时)从零到能独立交付 SCL 工程。

本地阅读(网站版)

本仓库同时是一份可构建的静态网站(全部 Rust 工具链: mdBook + Pagefind + 自定义渲染后端), 支持中文全文搜索与 SCL 语法高亮:

build.cmd                     :: 一键构建(产物在 book\html\, 可部署到任何静态托管)
tools\mdbook.exe serve        :: 边写边看: http://localhost:3000(改动自动重建, 搜索索引同步刷新)
路径角色
根目录 *.md唯一的内容源(直接编辑这些文件)
src\book\构建中间产物与产物(build.cmd 自动生成, 已 gitignore)
book.toml / SUMMARY.md站点配置与章节目录
scl-highlight.jsSCL 语法定义(注册进 mdBook 自带的 highlight.js, 高亮 VAR_INPUT/TON/%I0.0/T#3s 等)
tools\mdBook 与 Pagefind 的 Windows 二进制
pagefind-renderer\Rust 小工具: 注册为 mdBook 渲染后端, 每次构建自动重建中文搜索索引

一句话定位

SCL 语法对你不是难点(它是 Pascal 方言,一周末就能上手)——真正的学习曲线是三件事: 扫描周期思维(程序不是事件驱动,是每周期整体重入)、块与存储模型(OB/FB/FC/DB,没有 heap 没有递归)、TIA Portal 工作流与调试手段(没有断点没有 printf)。本仓库按这个优先级组织。

仓库导航

文件内容什么时候读
01-环境搭建.mdTIA Portal 选版/试用/安装排坑, PLCSIM 能力边界, CODESYS 替代, 硬件选购, 第一个项目分步走查第 1 周, 边装边读
02-PLC核心概念-面向C与Python开发者.md扫描周期/看门狗/存储体系/数据类型/块模型/接口语义/寻址/PROFINET/诊断 + 概念对照总表与类比失效清单第 2 周, 思维转变的关键章
03-SCL语法完全参考.md核心工具书: 词法/声明/运算符/控制流/FC/FB/定时器计数器/VARIANT/字符串/时间/运行时错误/字节序 + C/Python 速查大表第 2~4 周通读, 此后随写随查
04-标准库与常用功能.md指令体系, 模拟量 27648 与 NORM_X/SCALE_X, PID_Compact, HSC, 运动控制, 全部通信方式, 库工程化, 诊断指令第 5~8 周, 按需精读
05-常见陷阱与最佳实践.md27 个陷阱(现象/根因/正确写法/C 对照) + 命名/UDT/状态机骨架/版本管理/下载语义第 4 周先通读一遍, 之后每次踩坑回查
06-练习项目阶梯.mdL0~L3 共 13 题: 起保停 → 定时/计数 → 滤波/解析/排序 → 交通灯/电机FB/PID/Modbus/HMI/小产线与各阶段同步做, 是主线
07-学习资源清单.md45 条已核实资源: 官方文档/书/视频/课程/论坛/示例代码 + 使用顺序 + 答疑地图随时查

学习路线图

阶段 0: 破冰(第 1 周)

  • : 01 全文(跟着把 TIA Portal + PLCSIM 装好, 跑通第一个起保停)。
  • : 练习 0.1(观察扫描计数器, 理解“程序被周期性执行“)。
  • 里程碑: 30 分钟内独立完成“新建项目 → SCL FB → OB1 调用 → 编译 0 error → 下载仿真 → Watch table 驱动“全流程; 能解释 Modify 在仿真与真机上的语义差异。
  • 提示: 21 天试用一次性生效, 留给集中练习; 日常语法手感可用免费 CODESYS 过渡(语法约六七成相通, 见 01 §5)。

阶段 1: 思维与语法(第 2~4 周)

  • : 02(执行模型与块模型——最重要的一章) → 03(语法参考通读一遍, 别背, 建立索引)。
  • : 练习 L1 全部(1.1~1.4) + L2 的 2.4(体会“就是普通编程“与 TEMP/短路求值两个 PLC 特有的坑)。
  • 里程碑: 能不查书写出启保停/延时/振荡器/计数/单按钮启停; 能说清 FC 与 FB 的区别、VAR_TEMP 为什么不能存状态、TON 为什么是“有状态对象“; :== 不再写反。
  • 检验方法: 把 03 附录 B 的 C 状态机裸译成 SCL, 一次编译通过。

阶段 2: 工程化(第 5~8 周)

  • : 05 通读(27 个陷阱是别人交过学费的清单) → 04 精读(模拟量/PID/指令体系) → 07 的 Styleguide 条目。
  • : 练习 2.1~2.3 + 3.1(交通灯状态机) + 3.2(通用电机 FB 多实例)。
  • 里程碑: 写 FB 前先想“哪些状态放 static、哪些走 InOut“; 能用 CASE+CONSTANT 手写状态机骨架; 能解释优化 DB 与标准 DB 的取舍、为什么通信映射要标准 DB; 模拟量换算带质量码与滞回。
  • 同步开始: 用 05 最佳实践 6 的方法把 SCL 块导出为文本源纳入 git(TIA V21 起有原生 Git 集成)。

阶段 3: 系统集成(第 9~12 周)

  • : 练习 3.3(PID + 仿真对象) → 3.4(Modbus/通信, 注意 PLCSIM 限制与替代) → 3.5(HMI) → 3.6(小产线综合项目)。
  • : 04 通信与库工程化章节回炉; 07 按需(进阶方向)。
  • 里程碑: 独立完成 3.6 并能在 20 分钟内讲清块结构与数据流; 遇到报错先查诊断缓冲再查答疑地图(07 §9)。

之后的方向

S7-1500 / PROFINET 深入与诊断 / 安全 PLC(F-CPU+PROFIsafe) / 运动控制(Technology Objects) / SCADA(WinCC Professional) / 官方认证(TIA-PRO1→PRO2→PRO3, 见 07)。

十条速记(反直觉事实, 现在就记住)

  1. 赋值是 :=, 比较是 =——与 C 相反; 没有 ==
  2. 看门狗默认 150 ms 恒启用(1~6000 ms 可配): 阻塞循环两次超时直接 STOP, PLC 没有 sleep。
  3. S7-1200 没有断点调试(断点是 300/400/1500 的能力); 调试 = Watch table + 程序状态 + Trace + 诊断缓冲。
  4. TIME_TCK()、REF 引用、POINTER/ANY、LTIME、SIMATIC 遗留定时器都是 S7-1500 专属, 1200 编译报错; 测耗时用 RUNTIME()
  5. Array[*] 变长形参 1200 支持, 但要优化块 + 固件 ≥ V4.2(FC 仅 Input/InOut 区)。
  6. 除零不炸: 结果 0 + ENO=0 + 继续 RUN; 数组越界在 1200 上也只记诊断缓冲——错误哲学是“留痕不中断“。
  7. SCL 布尔表达式不短路: i <= n AND a[i] > 0 的守卫无效, 必须拆嵌套 IF。
  8. TIA 新建 DB 默认优化访问: 没有 % 地址、PEEK/POKE/AT/通信映射都要改用标准 DB。
  9. PLCSIM(标准版)不仿真 Modbus TCP/真实 TCP/OPC UA/Web server/Trace; PID_Compact V2.x 不能在一代 1200 上仿真; PLCSIM Advanced 不支持一代 1200。
  10. 标识符大小写不敏感: MotorMOTOR 是同一个变量(重名报错)。

学习心法(给程序员的)

  • 文字优先, 视频辅助: 对你而言官方手册与教材的信噪比远高于视频; 卡在“看不见的操作“时再看对应视频(07 有映射)。
  • 每个阶段以“能写出并下载一个可运行的块“为完成标志, 不以读了多少为准。
  • 类比是脚手架: 02 §12 的“类比何时失效“清单每条都是真事故来源, 与 05 的 27 个陷阱对照读。
  • 把 F1(在线帮助)和 TIA Portal Information System 当 cppreference 用——所有中文教程都会过时, 它们不会。
  • 存疑即标注: 仓库所有 (待核实) 都鼓励你用 F1 实测补全, 这本身就是学习。

环境速览(详见 01)

  • TIA Portal 当前最新 V21(2025-11/12 发布); 学习用 V17~V21 皆可, 跟新教材注意版本差异; 项目文件只能“高版本开低版本“。
  • 试用版 21 天一次性(SIOS Entry 109989774); PLCSIM 随 TIA 免费仿真, 无需硬件即可完成 06 的 L0~L2 全部与 L3 大部分(通信/HMI 题有替代方案)。
  • 免费替代: CODESYS(IDE 免费, 运行时 2 小时 Demo)练 ST 手感。

本仓库由 Claude Code 生成于 2026-09-05: 22 个调研/写作 agent 并行工作, 45 条资源逐条核实(全部通过), 《常见陷阱》经 3 名独立审稿人对照官方文档复核修正。所有事实性断言以西门子官方文档为准(docs.tia.siemens.cloud 与 S7-1200 System Manual, Entry 109977302)。

S7-1200 SCL 学习环境搭建: 不买硬件也能完整学习

面向读者的定位: 你有 10 年 C/Python 经验, 零 PLC 基础。本章结束时, 你的 PC 上会有一套可用的 “IDE + 编译器 + 目标模拟器 + 调试器”, 并且已经跑通第一个 SCL 程序(起保停), 完成一次完整的 edit-compile-download-debug 循环。

先建立类比框架(后文反复使用):

PLC 世界C/Python 世界类比是否成立
TIA PortalVisual Studio / PyCharm(IDE + 工具链一体)基本成立, 但 TIA 还兼任硬件组态(HMI、网络、模块)
SCL 语言C / Python(文本语言)成立; SCL 属 IEC 61131-3 Structured Text 方言
S7-PLCSIM 仿真器QEMU / 模拟器部分成立: PLCSIM 是指令级仿真, 非周期精确, 高速硬件不仿真
OB / FB / DBmain() / class / object粗略成立, 细节差异后文逐条指出
scan cyclewhile(1) { read_input(); run(); write_output(); }成立, 这是最重要的一条心智模型
CPU watchdog看门狗定时器成立: 单周期超时即停机, 不像 PC 程序可以慢慢算

目录

  1. 软件选型: STEP 7 Basic 够不够
  2. 授权与获取: 正版 / 21 天试用 / 教育渠道
  3. 安装实操与常见坑
  4. 仿真: S7-PLCSIM 能做什么、不能做什么
  5. 无官方环境时的替代: CODESYS / OpenPLC
  6. 可选硬件: CPU 选型、供电与接线常识
  7. 第一个项目分步走查(精确到菜单名)
  8. 调试工具速览
  9. 验收 checklist
  10. 参考链接汇总

1. 软件选型: STEP 7 Basic 够不够

1.1 Basic vs Professional

TIA Portal 里的编程软件叫 STEP 7, 分两个档位。官方定义: STEP 7 Basic 是 STEP 7 Professional 的 “price-optimized subset”(价格优化子集)。

能力STEP 7 BasicSTEP 7 Professional
S7-1200 编程(含 SCL)支持支持
S7-1500 编程不支持支持
S7-300/400(经典系列)不支持支持(注: V20 起迁移经典工程改用独立迁移工具)
WinCC Basic(组态 KTP Basic 触摸屏)自带自带
WinCC Advanced/Professional(PC 级 HMI)不含
仿真对象仅 S7-1200S7-1200 + S7-1500 等

结论: 只学 S7-1200 的 SCL, Basic 完全够用, 而且试用版(Trial)直接给的是全家桶 Professional, 学习期内你甚至感知不到差别。

1.2 TIA Portal 版本选择

一年一个大版本, 近年发布节奏(V17 之前同理):

版本发布时间备注
V172021大量中文教材基于 V15~V17
V182022
V192023-11
V202024-11首个支持 S7-1200 G2 的版本; PLCSIM Basic 更名 S7-PLCSIM(Standard)并换新 UI
V212025-11/122026-09 当前最新, 已出 Update 2 + Hotfix 1

选版规则(程序员视角):

  • 项目文件只能“高开低“, 不能“低开高“: 高版本 TIA 可打开并单向升级低版本项目, 反之不行。类似只提供单向迁移的 IDE 工程格式, 没有兼容模式。
  • Trial 下载通常只提供最新大版本(V21)。你的 21 天试用注定跑在 V21 上。
  • 教材用 V17 而你装 V21: 界面有差异(主要是配色和 PLCSIM 新 UI), 菜单名基本一致, 可对照。
  • 新买硬件注意: S7-1200 G2(新一代, 2024 底发布)必须 TIA V20+; 经典 S7-1200(一代)用 V16+ 均可。

建议: 学习直接用当前最新版(V21 + 已发布 Update), 除非你要跟的课程/同事工程锁定在旧版。

1.3 Windows 兼容性与硬件要求

官方要求以每个版本安装手册的 “System requirements” 页为准。V21 数字(官方全文核实):

最低推荐
CPUIntel Core i3-6100UCore i5-12600HE 级别
内存8 GB16 GB, 大项目 32 GB
磁盘20 GB 空闲(S-ATA)SSD, 50 GB
显示1024x768更高
网络100 Mbit千兆

操作系统(V21): Windows 10 64-bit(Enterprise 21H2 / LTSC 2021)、Windows 11 Pro / Enterprise 24H2、23H2、LTSC、Server 2022/2025。特别注意脚注: Windows 11 Home 24H2 只允许安装 Basic 版。经验规律: TIA 大版本大约只认证到发布日前 2~3 年的 Windows 构建, 新笔记本 + 旧版 TIA(V17/V18)极易装不上或装完起不来; 反过来旧 Win10 装新版一般没问题。虚拟机官方支持 ESXi 8+、VMware Workstation/Player 17+、Hyper-V 2019+, 学习用 VM + 快照是合理选择。

2. 授权与获取: 正版 / 试用 / 教育渠道

2.1 许可机制

授权由 Automation License Manager(ALM)管理, 许可可存本机硬盘或 USB 加密狗, 支持单机(perpetual)、租期(lease/rent)、浮动网络许可、升级(upgrade)等形态。类比: 更接近商业软件的 license file + 硬件指纹, 而不是 npm 生态的“下载即用“。Windows 大版本更新后常需升级 ALM 本身, 否则许可识别失败。

2.2 试用版(Trial): 21 天, 一次性

  • 时长与次数: 激活起 21 天, 官方明确 “expires after 21 days”, 不可延长、不可重激活。不要把学习计划寄托在“重装系统能重置“上(未经官方确认, 待核实)。
  • 内容: STEP 7 Basic/Professional + Safety + S7-PLCSIM + WinCC, 全家桶。
  • 获取步骤:
    1. 注册免费的 Siemens Industry Online Support(SIOS)账号。
    2. 打开 SIOS Entry 109989774 “SIMATIC STEP 7 incl. Safety, S7-PLCSIM and WinCC V21 TRIAL Download”。
    3. 下载(体量大, 十 GB 级, 留足磁盘与时间)。
    4. 安装后首次启动时按提示激活 Trial。
  • 零安装替代: TIA Portal Cloud 试用同样 21 天(SIOS 109772248), 浏览器里跑, 适合低配电脑, 但网络延迟影响手感。

2.3 正版与教育渠道

  • 正版: 通过西门子授权经销商或 Industry Mall(正在整合进新门户 SiePortal)购买; STEP 7 Basic 授权相对便宜, Professional 明显更贵。具体价格随地区变动, 不在此报数。

  • 教育: SCE(Siemens Automation Cooperates with Education)提供免费教材与配套项目(含 SCL 模块 052-201, 入口见第 10 节); 面向院校另有教学许可, 个人能否单独购买(待核实)。中国区还有 1847 工业学习平台与 SITRAIN 课程(对应课程 A7115 “SIMATIC S7-1200 系统课程”)。

  • 学习节奏建议: 把 21 天试用留给“跟着本仓库做完走查 + 集中练习“; 日常语法练习用免费的 CODESYS(见第 5 节)保持手感, 两者语法同源。

3. 安装实操

3.1 安装前准备 checklist

  1. 确认 Windows 版本在目标 TIA 版本支持列表(见 1.3), Home 版注意只能装 Basic。
  2. 磁盘留 50 GB 以上(最低要求 20 GB, 全家桶装完 40 GB+ 很常见), 装在 SSD。
  3. 关闭杀毒软件实时防护(或把安装目录加白名单): 杀软扫描会显著拖慢甚至中断安装。
  4. Windows 更新做完并重启一次; 暂停其他后台安装任务。
  5. 用户名、安装路径、后续项目路径全部用英文短路径, 例如 C:\TIAC:\Projects。Windows 用户名含中文/特殊字符是高频安装失败原因。
  6. 用管理员权限运行 setup.exe
  7. 笔记本接电源, 避免安装中断电。

3.2 组件勾选

安装器里按需勾选。只学 S7-1200 SCL 的话:

组件建议
STEP 7(Basic 或 Professional)必装(编程 + SCL)
SIMATIC Safety(可选)不装, 安全编程用不到
WinCC Basic随 STEP 7 Basic 自带; 想练触摸屏再留
WinCC Advanced/Professional不装, 体积大且学习期用不到
WinCC Unified不装
S7-PLCSIM装上(V20 起随 STEP 7 一起安装, 即 Standard 版)
TIA Portal Openness(自动化 API)程序员可玩(用脚本操控 TIA), 非必需

3.3 常见安装坑与解法

症状原因解法
反复提示 “Please reboot before installing any more programs”, 重启无效(重启循环)Windows 里有残留的 PendingFileRenameOperations 注册表值, 安装器每次都判定“有待完成的重启“官方 FAQ 8861819: 运行 regedit, 定位 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager, 右键删除整个 PendingFileRenameOperations 值, 然后不重启, 立即以管理员身份运行 setup; TIA 会连续调用多个子安装器, 每次报此错都重复该操作。改注册表前先导出备份
卸载残留后重装失败/仍提示旧版本HKLM\SOFTWARE\Siemens\AUTSW 下产品子键残留 InternalVersion 等安装状态备份后删除对应产品子键(社区通用做法; 路径随产品变化, 操作前导出)
安装中后期失败, 提示驱动/运行库错误缺 VC++ 运行库或杀软拦截先装 VC++ 运行库合集, 杀软加白名单后重试
项目打不开/创建报错项目路径超 143 字符(TIA 自身上限), 或触发 Windows 260 字符 MAX_PATH用短路径如 C:\Projects\SCL_Demo
Windows 大更新后 TIA 起不来/许可丢失ALM 版本过旧单独升级 Automation License Manager
安装极慢杀软实时扫描 + 机械盘关杀软, 装 SSD

耗时预期: 全家桶安装常见 30 分钟到 2 小时(取决于磁盘与杀软), 下载另计。预算一个晚上。

4. 仿真: S7-PLCSIM 能做什么、不能做什么

4.1 定位与启动

S7-PLCSIM 是随 TIA Portal 附带的 S7-1200/S7-1500 指令级仿真器(早期叫 PLCSIM Basic, V20 起更名 Standard 并换新 UI)。启动方式: 菜单 Online > Simulation(或工具栏 Start simulation 图标)。官方许可文档明确: 启动仿真不需要任何额外许可; STEP 7 Basic 许可下只能仿真 S7-1200。

类比: 相当于 qemu-system-arm 跑目标固件, 编译产物下载进虚拟 CPU, 与真机共用同一套调试前端(TIA 的在线监视)。注意与单元测试框架的区别: PLCSIM 仿真的是整个 CPU 扫描循环, 不是函数级 mock。

4.2 标准 PLCSIM 与真实 CPU 的差异清单

能力标准 PLCSIM真实 CPU
用户程序(OB/FB/FC/DB)、定时器、计数器、系统功能仿真
PUT/GET(S7 通信)、开放用户通信 TSEND_C/TRCV_C 等仅经本机 Softbus 在同一 PC 内工作真实通信
真实 TCP/IP 栈(抓包可见)
Web Server、OPC UA Server有(视 CPU/固件)
Modbus TCP(MB_CLIENT/MB_SERVER)不仿真(第三方多方结论, 非官方原文)
Trace(变量时序录制)不支持; 录制仅 PLCSIM Advanced(上限 8 条), V16 起才可在 TIA 中查看 Trace支持
HSC 高速计数、PWM/PTO 高速输出不仿真支持(且高速输出仅晶体管型 CPU)
PID_Compact V2.x一代 S7-1200 不支持仿真(官方: 仅 S7-1500 与 S7-1200 G2 可仿真); 且仿真中自整定可能因采样时间误差中断, 需设 CycleTime 的 EnEstimation=FALSE、EnMonitoring=FALSE支持
存储卡、固件更新、实际接线诊断不适用
时间行为近似: 扫描周期与真机有偏差, 依赖真实定时的逻辑结果可能不同真实
并发实例最多 2 个仿真 CPU-

结论: 学语法、学 FB/DB 组织、学调试, 标准 PLCSIM 覆盖 90% 需求; 学通信、高速 IO、PID 自整定时要么上真机, 要么认清局限。

4.3 PLCSIM Advanced 不支持(一代)S7-1200

PLCSIM Advanced 是另一个更强的产品(真实虚拟网卡、API、Trace)。官方对比表明: Advanced 面向 S7-1500 家族; 一代 S7-1200 不在支持列表。从 Advanced V8.0(配 TIA V21)起新增对 S7-1200 G2 的支持。所以: 你学经典 S7-1200, 仿真只能用内置标准 PLCSIM; Advanced 帮不上忙(它自带 Trial 许可, 但对一代 1200 无意义)。

4.4 仿真里改输入的精确语义

  • 在 PLCSIM 里用 Watch table Modify 一个输入(%I0.0): 值会一直保持, 因为没有物理外设去刷新输入映像(process image)。
  • 在真实 CPU 上 Modify 输入映像: 下一个扫描周期开始时被物理输入重新覆盖, 修改“站不住“。真机上要持续固定输入须用 Force(S7-1200 支持 Watch and Force table, 官方手册有专章; Force 激活期间会阻碍下载, 调试完务必停掉, 否则触发 “Test and commissioning function active” 类诊断事件)。
  • 这条差异是“仿真通过、现场翻车“的经典来源之一, 现在就记住。

5. 无官方环境时的替代: CODESYS / OpenPLC

没有 TIA(试用到期、机器不满足)时的免费路径。两者都实现 IEC 61131-3, 其中 Structured Text(ST)与 SCL 同源, SCL 本质是西门子对 ST 的方言。

5.1 CODESYS

  • IDE 在 CODESYS Store 标价免费(€0.00); 本地 SoftPLC 运行时(Control Win)Demo 模式全功能, 但无许可时每 2 小时自动退出, 重启即可(官方行为, 不是破解)。
  • 语法相通度: IF/ELSIF/CASE/FOR/WHILE/REPEAT、FUNCTION_BLOCK/FUNCTION、数组/结构/枚举、赋值与表达式几乎一致, 约 60~70% 的 ST 经验可直接迁移。
  • 工程组织差异(移植时的真正成本):
维度CODESYS STTIA SCL
接口声明POU 内直接文本声明 VAR_INPUT ... END_VAR图形化接口表(也可经外部源文本导入, 见 5.3)
全局变量GVL(Global Variable List)PLC tag table + Global DB
FB 调用声明实例变量 fbInst: FB_Motor;fbInst(...)必须经 instance DB, 写作 "FB_Motor_DB"(...); 或多重实例嵌在调用者的 Static 区
变量引用局部直接写名字#局部, "全局"(双引号), 语法层面强制区分作用域
直接地址%IX0.0 / %QX0.0 / %MX0.0%I0.0 / %Q0.0 / M0.0
库管理Library Manager, 版本化TIA 库, 拖拽式, 概念不同
指针/引用POINTER TO / REFERENCES7-1200 的 SCL 无指针运算; 参数传递用 Input/Output/InOut 语义
定时器Standard 库 TON 等, 直接声明TON 也是 FB, 但实例化方式服从 TIA 规则(IEC_TIMER 实例)

结论(与调研一致): 用 CODESYS 练 ST 手感可行且免费, 但 TIA 的工程化用法(instance DB、多重实例、优化访问、库、在线监控)必须在 TIA 里学, 不可替代。

5.2 OpenPLC

开源免费(OpenPLC project / GitHub OpenPLC_v3), 支持 IEC 61131-3 ST, 跨平台, 能跑在树莓派上接真实 IO。定位: 语法练习 + 开源硬件玩具。工程模型与 TIA 差异比 CODESYS 更大, 没有仿真-调试一体化体验, 学西门子技术栈的价值有限, 作为备选项了解即可。

5.3 迁移回 TIA 的注意点

TIA 提供文本源通道: 项目树 External sources > Add new external source > New external source (SCL), 粘贴源码后右键 Generate blocks from source 生成块。源码语法与 CODESYS 的 POU 文本声明接近, 但需改写:

FUNCTION_BLOCK "FB_Motor"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      Start : Bool;
      Stop : Bool;
   END_VAR
   VAR_OUTPUT
      Motor : Bool;
   END_VAR
BEGIN
   #Motor := (#Motor OR #Start) AND NOT #Stop;
END_FUNCTION_BLOCK

迁移 checklist:

  1. 把 GVL 拆成 PLC tag table(管 IO 地址)与 Global DB(管数据)。
  2. FB 调用点全部改写为 instance DB 形式, 补建 instance DB。
  3. %IX0.0 风格地址改为 %I0.0; 位/字节序访问方式核对。
  4. 删掉 CODESYS 特有(AT 覆盖、继承、指针、REF_TO)代码, 用 TIA 等价物重写。
  5. 逐块编译, 优先处理 Optimized access 与非优化访问差异。

6. 可选硬件: 想买也没问题

纯学习不买硬件完全可行(本文主线即仿真)。要买, 看本节。

6.1 CPU 选型对比(经典一代, DC/DC/DC 型号)

CPU板载 DI/DO板载 AI/AOPRONET 口定位新品价格区间(2026-09 检索, 波动大)
1211C6/42/01最小实验约 US$178-251(第三方/eBay)
1212C8/62/01入门G2 版约 138 欧元净价
1214C14/102/01学习/小项目主力(教程事实标准)欧洲 366-478 欧; 美国 $350-378; 国行渠道约 5084 元(未税)
1215C14/102/22(内置交换机)中型, 自带 AO约 632 欧元净价
1217C更多 IO(含多路模拟量, 具体规格见系统手册)2大点数紧凑型约 1018 英镑

不存在 1213C/1216C。扩展能力差异: 均可加 Signal Board(SB, 插在 CPU 前)与 Signal Module(SM, 右侧扩展), 以及通信模块(CM); 各 CPU 可挂数量不同, 选型时查系统手册。工作内存/装载内存规格亦随型号与固件不同, 以手册为准(不在此报数, 待核实项)。

G2 与停产传闻: S7-1200 G2(2024 发布)目前只有 1212C/1214C(+FC 变体), 硬件与一代不兼容, 且不支持设备更换式迁移, 必须 TIA V20+。多个第三方渠道称一代 S7-1200 将于 2026-11-01 停产停售(未见西门子官方原文, 待核实); 采买前向经销商确认。若买全新且面向未来, 倾向 G2; 若跟老教材/买二手, 一代便宜且资料最多。

6.2 Starter Kit(套装)

经典套装 6AV6651-7KA01-3AA4: CPU 1212C AC/DC/RLY + KTP400 Basic 触摸屏 + STEP 7 Basic V10.5 许可 + SIM 1274 输入模拟器 + 网线 + 文档, 第三方现货约 1004 欧。注意其软件许可停在 V10.5, 属清仓货, 许可通常可按规则升级到新版本(需另购升级授权, 待核实)。G2 时代有新的 “Basic Package”(如 1212C G2 + KTP400)形式。套装相对单买划算, 且附输入模拟器, 适合不想自己接线的初学者。

6.3 24V 供电与干接点常识(零基础必读)

  • 型号后缀解读(以 1214C 为例, 订货号 6ES7214-1xG4x-0xB0): 第一个字母编码供电+输出组合 —— AG=24VDC 供电 + 晶体管输出(DC/DC/DC), BG=AC 供电 + 继电器输出(AC/DC/RLY), HG=24VDC 供电 + 继电器输出(DC/DC/RLY)。末段数字与硬件/固件代际相关(如 G4 对应 V4.x 一代, G5 见于 G2)。
  • 供电: DC 型号需要 24VDC 电源(建议工业开关电源, 如 SITOP), AC 型号直接接 110/230VAC。24V 电源的 0V 必须与 CPU 的 M 端共地。
  • 干接点(dry contact): 指无极性、无源位的机械触点。继电器输出(RLY)就是干接点: 可切交流或直流小负载(每点约 2A 级, 查手册), 与机械寿命和负载类型相关, 感性负载(接触器线圈、电磁阀)要加续流二极管或 RC 吸收。
  • 晶体管输出(DC): 24VDC、有极性(源型/PNP), 无触点磨损, 支持高频输出(PWM/PTO 脉试探步进伺服)—— 只有晶体管型支持 HSC/PWM/PTO。
  • 数字量输入: S7-1200 板载 DI 可按源型(PNP, 常见于欧洲/国内主流)或漏型(NPN)接线, 由公共端接法决定。按钮、开关一端接 24V, 另一端接 DI, 公共端回 0V/24V 按手册接。绝对不要把 220VAC 接进 DI。
  • 安全常识: 接线前断电; 强电回路(接触器、电机)与 24V 控制回路的线槽/端子分开; 学习阶段建议全部用 24V 安全电压 + 面包板按钮, 或直接用 SIM 1274 模拟器。

6.4 二手注意

  1. 固件版本: 设备组态里目标固件要匹配 CPU 实际固件; 太旧的 CPU(如 V2.x 硬件)无法升级到 V4 特性, 老 TIA 工程也打不开新特性。先问卖家固件版本。
  2. 密码与 know-how 保护: 被 CPU 级密码保护或块加密的二手 CPU 很难恢复(整体复位出厂设置可去密码, 但块保护仍可能在源工程里, 你拿不到工程)。
  3. 检查: PROFINET 口是否完好、端子排螺丝是否滑丝、外壳挡板是否缺失、是否进过水/粉尘。
  4. 一代停产传闻若属实, 二手一代件会更充裕, 价格更低, 学完再出掉损失不大。

6.5 KTP 触摸屏(可选)

KTP400 Basic(4.3 寸)/KTP700 Basic 等面板用 WinCC Basic 组态, 而 WinCC Basic 随 STEP 7 Basic 免费, 且可无许可仿真 Basic 面板(做 HMI 联调)。学 SCL 阶段完全非必需; 到 HMI 章节再买。价格数百到千余元, 波动大(待核实)。PC 级 WinCC Runtime 仿真无许可时每次只能激活 1 小时(官方规则)。

7. 第一个项目分步走查: 起保停

目标: Start 按下后 Motor 得电并自保持, Stop 按下后断电; Stop 优先。这是 PLC 的 “Hello, World + 闭包” —— 它展示 PLC 与 PC 程序最本质的差别: 程序被周而复始地执行, 状态存在变量里而非调用栈里。

先看 C 类比(仅一条, 成立):

/* 伪代码: CPU 的循环, 你写的所有程序都在 run() 里被反复调用 */
while (1) {
    read_physical_inputs();   /* 刷新输入映像 %I */
    run_ob1();                /* 执行 OB1: 你的 FB 在这里被调用 */
    write_physical_outputs(); /* 写输出映像 %Q */
    self_diagnostics();       /* watchdog 在这里计时, 超时停机 */
}

注意类比失效处: PC 里你可以 sleep() 或阻塞等事件; PLC 的 OB1 里任何阻塞都会吃掉 scan time, 超 watchdog 上限 CPU 直接停机。PLC 不是事件回调模型。

7.1 新建项目

启动 TIA Portal, 在 Portal 视图选 Create new project, Project name 填 SCL_Demo, Path 填 C:\Projects(短英文路径), 点 Create

7.2 添加 CPU(纯仿真也选真实 CPU 型号)

CPU 型号即“编译目标“, 类比 GCC 的 target triple —— PLCSIM 仿真的是这个型号, 不存在单独的“S7-1200 仿真设备“条目。

  1. Portal 视图点 Devices & Networks > Add new device
  2. PLC 按钮, 展开 SIMATIC S7-1200 > CPU 1214C DC/DC/DC (6ES7214-1AG40-0XB0), 固件选 V4.4, 勾选 Open device view, 点 OK
  3. 项目树出现 PLC_1

7.3 新建 SCL 语言的 FB

  1. 项目树展开 PLC_1 > Program blocks, 双击 Add new block
  2. Function block (FB), Name 填 FB_Motor, Language 选 SCL, 点 OK。生成 FB_Motor [FB1] 并打开编辑器。

接口声明(编辑器上方的接口表, 相当于 class 的成员声明区; C 类比: Input=const 形参, Output=出参, InOut=指针形参, Static=私有成员, Temp=局部变量):

NameData type区(Interface)Comment
StartBoolInput启动按钮(点动)
StopBoolInput停止按钮(点动)
MotorBoolOutput电机输出

代码区:

// FB_Motor (FB1) - SCL
// 起保停: Start 置位并自保持, Stop 复位; Stop 优先
// 注意: FB 的 Output 变量可读可写, 上一周期的 Motor 值被当作状态用,
// 这正是"自保持"的实现方式 —— 状态存在 FB 实例(instance DB)里, 而不是栈上
IF #Stop THEN
    #Motor := FALSE;
ELSIF #Start THEN
    #Motor := TRUE;
END_IF;

等价的一行组合逻辑写法(电工图直译, 结果相同; 两者都要求 Stop 为“常开按钮信号取反“的语义):

#Motor := (#Motor OR #Start) AND NOT #Stop;

# 前缀 = 块接口局部变量; 之后引用全局 PLC tag 时用双引号 "Start"。这是作用域在语法层面的强制区分, 类比 Python 的 local 与 module 命名空间, 但 SCL 用符号而非作用域规则。

7.4 建 PLC tag(全局符号表)

项目树 PLC tags > Default tag table, 双击打开, 添加三行:

NameData typeAddress
StartBool%I0.0
StopBool%I0.1
MotorBool%Q0.0

7.5 在 OB1 里调用 FB

默认的 Main [OB1] 是 LAD 语言。两种做法, 任选:

做法 A(LAD 网络, 最常见): 双击打开 Main [OB1], 从项目树 Program blocks 里把 FB_Motor 拖到 Network 1 的梯级上; 弹出 Call options 对话框, 选 Single instance DB, 名字默认 FB_Motor_DB, 点 OK(TIA 自动创建 instance DB, 相当于给 FB 这个 class 实例化一个 object); 点击块引脚上的 ??.?.?? 占位符, 依次输入 StartStopMotor 并回车绑定。

做法 B(全 SCL): 右键 Main [OB1] > Delete 删除默认 OB1, 再 Program blocks > Add new block > Organization block, 选 Main program cycle (OB1), Language 选 SCL, OK; 代码区写:

// Main (OB1) - SCL
// 每个 scan cycle 调用一次 FB 实例; 形参用 => 只对输出参数
"FB_Motor_DB"(Start := "Start",
              Stop  := "Stop",
              Motor => "Motor");

7.6 编译

项目树右键 Program blocks > Compile(或工具栏 Compile 按钮)。底部 Info > Compile 选项卡必须显示 0 error; warning 可先忽略(常见: 未使用的变量)。类比: 这是纯编译/一致性检查, 还没链接到任何目标。

7.7 启动仿真

项目树选中 PLC_1, 菜单 Online > Simulation(或工具栏 Start simulation)。S7-PLCSIM 启动, 出现虚拟 CPU 窗口(RUN/STOP 开关在此切换)。无需任何额外许可。

7.8 下载到仿真 CPU

  1. 工具栏 Download to device(或菜单 Online > Download to device)。
  2. 弹出 Load preview, 列出将下载的块, 点 Load
  3. 若提示模块启动状态, 选 Start modules / RUN, 点 Finish
  4. PLCSIM 窗口确认 CPU 处于 RUN。

下载失败先查: PG/PC 接口是否指向 PLCSIM(通常自动); CPU 是否被前一次在线会话占用(关闭旧的在线连接)。

7.9 Watch table: 改输入、看输出

  1. 项目树 PLC_1 > Watch tables > Add new watch table, 双击打开。
  2. Name 列依次输入 StartStopMotor(自动补全 tag)。
  3. 菜单/工具栏 Go online 切到在线; 点 Monitor all(眼镜图标)开始监视。
  4. Start 行的 Modify value 列输入 1(TRUE), 点工具栏 Modify all(一次性写入); 观察 Motor 变 1。
  5. Start 改回 0: Motor 保持 1(自保持生效 —— 状态在 instance DB 里, 不依赖输入保持)。
  6. Stop1: Motor 变 0; Stop 清零后 Motor 仍为 0, 直到下一次 Start。

记住 4.4 节的语义: 仿真中 Modify 输入会保持; 真机输入映像每周期被物理量覆盖, 固定输入要用 Force。

7.10 在线监控 SCL 代码

回到 FB_Motor 编辑器, 在线状态下工具栏点 Monitoring(程序状态监视), 代码各行直接内联显示变量当前值(绿色为当前值)。这就是 PLC 版的 “debugger watch”, 但注意它默认按扫描周期连续刷新, 不是断点暂停模型; TIA 的断点调试(可选)才类似单步, 此处不展开(后续章节讲)。

8. 调试工具速览

工具入口适用场景注意
Watch table(监控表)项目树 Watch tables批量观测 + Modify 变量, 联调首选Modify I 的真机语义见 4.4
Force table(强制表)项目树 Watch tables 体系真机上持续固定 I/Q, 绕过物理信号仿真中用不到; 忘记关闭会阻塞下载并产生诊断事件
Program status(程序状态)块编辑器内 Monitoring看单块逻辑的实时数据流, 定位分支走向只看, 不改
Trace(轨迹)项目树 Traces示波器式录制变量时序, 调 PID/振荡标准 PLCSIM 不支持; 需真机或 PLCSIM Advanced(后者不含一代 1200)
Diagnostics buffer(诊断缓冲区)Online & diagnostics > Diagnostics bufferCPU 级事件日志: 停机原因、块错误、上电记录类比 dmesg; CPU 停机后第一件事看这里

9. 验收 checklist

学完本章, 以下每条都应能独立完成(不看本文):

  1. 说出 STEP 7 Basic 与 Professional 对学 S7-1200 的实际差别(结论: 无差别)。
  2. 从 SIOS 下载 Trial 并完成安装, 知道 21 天一次性、不可延长的规则。
  3. 独立排掉重启循环安装故障(PendingFileRenameOperations 注册表解法)。
  4. 新建项目(短英文路径)、添加 CPU 1214C、新建 SCL FB、在 OB1 以 instance DB 方式调用、编译到 0 error —— 全程 30 分钟内。
  5. 启动 PLCSIM, 下载, 切 RUN。
  6. 用 Watch table 的 Modify 驱动起保停的全部三种状态(启动/保持/停止), 并解释为什么 Start 松开后 Motor 还是 1。
  7. 在 SCL 编辑器里打开程序状态监视并读值。
  8. 打开诊断缓冲区并读懂一条事件。
  9. 列出标准 PLCSIM 不可用的功能(Trace、HSC/PWM、真实 TCP/IP、Modbus TCP、一代 1200 的 PID_Compact 仿真)。
  10. 解释 Modify 与 Force 的差异, 以及“仿真里能站住的输入修改, 真机上为什么站不住“。
  11. 说出 #Motor"Motor" 的作用域区别。
  12. (可选硬件线)说出 1214C 三个后缀变体(AG/BG/HG)的供电与输出类型含义, 以及继电器输出与晶体管输出的取舍; 给出二手 CPU 验机要点。

10. 参考链接汇总

官方(权威, 免费注册后可下载):

  • TIA Portal V21 Trial 下载入口(SIOS 109989774): https://support.industry.siemens.com/cs/document/109989774/
  • 许可文档(Trial 21 天 / PLCSIM 无需许可 / WinCC 仿真 1 小时): https://docs.tia.siemens.cloud/r/en-us/v21/installation/licensing/licensing-step-7-and-wincc
  • V21 系统要求(Win11 Home 仅 Basic 等脚注): https://docs.tia.siemens.cloud/r/en-us/v21/installation/system-requirements-for-installation/general-software-and-hardware-requirements
  • 重启循环官方 FAQ 8861819(PendingFileRenameOperations 解法): https://support.industry.siemens.com/cs/document/8861819/
  • PLCSIM 产品对比(标准 vs Advanced, 差异清单依据): https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/cpu-function-manuals/s7-plcsim-advanced/product-overview/s7-plcsim-products
  • PID_Compact 仿真限制(EnEstimation/EnMonitoring): https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/technology-function-manuals/pid-control/using-pid_compact/pid_compact-as-of-v2/simulating-pid_compact-as-of-v2-with-plcsim
  • S7-1200 手册集 - Watch tables and force tables: https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20/programming-concepts/debugging-and-testing-the-program/watch-tables-and-force-tables
  • S7-1200 系统手册(V4.7, SIOS 109977302): https://support.industry.siemens.com/cs/us/en/view/109977302
  • S7-1200 G2 与一代对照(型号变体、V20+ 限制): https://docs.tia.siemens.cloud/r/simatic_s7_1200_g2_manual_collection_enus_20/comparison-to-s7-1200/comparison-to-s7-1200
  • TIA Portal Cloud 试用(SIOS 109772248): https://support.industry.siemens.com/cs/document/109772248/
  • SCE 免费教材(含 SCL 模块, SIOS 109972110): https://support.industry.siemens.com/cs/document/109972110/
  • 中文支持门户(下载中心/找答案/论坛): https://www.ad.siemens.com.cn/home/ 与 https://www.ad.siemens.com.cn/service/answer/

第三方(交叉验证用, 结论已尽量以官方为准):

  • CODESYS IDE(免费)与 SoftPLC 运行时(2 小时 Demo): https://store.codesys.com/en/engineering/ide.html 、https://us.codesys.com/products/runtime/control-sl/
  • OpenPLC: https://www.openplcproject.com/ 、https://github.com/thiagoralves/OpenPLC_v3
  • TIA Portal 强制表使用与 “Test and commissioning function active” 案例: https://industrialmonitordirect.com/blogs/knowledgebase/siemens-tia-portal-force-table-how-to-force-plc-tags 、https://sieportal.siemens.com/en-ww/support/forum/posts/test-commissioning-function-active/113020
  • 路径长度错误(143/260 字符): https://support.industry.siemens.com/cs/document/67184464/

02 PLC 核心概念: 执行模型与程序架构 — 写给 C/Python 开发者

读者画像: 10 年 C/Python 经验, 零 PLC 基础, 学习目标为西门子 S7-1200 + TIA Portal (V16~V20) + SCL。 本章是全套材料里“思维切换“的关键章: 从事件驱动/进程退出模型, 切换到扫描周期/永不退出模型。后面所有 SCL 语法技巧, 都建立在本章概念之上。

本章结构: 1. 本质差异 → 2. 扫描周期与看门狗 → 3. 存储体系 → 4. 数据类型 → 5. 块模型 → 6. 块接口语义 → 7. 调用与执行语义 → 8. 寻址 → 9. PROFINET 与硬件组态 → 10. 诊断体系 → 11. 概念对照总表 → 12. 类比何时失效。

1. PLC 与通用计算机的本质差异

PLC (Programmable Logic Controller) 不是“放在控制柜里的电脑“, 而是一台确定性地、无限重复地执行同一个用户程序的实时控制器。差异不在性能, 而在执行契约:

维度通用计算机 (你熟悉的)S7-1200 PLC
程序生命周期main() 跑完退出; 守护进程靠事件循环用户程序永不退出, 每个扫描周期整体重入
执行触发事件/中断/消息驱动 (回调)时间驱动: 每周期从头到尾全量执行一遍
并发模型多线程/多进程 + 锁单主线 (OB1) + 少量优先级抢占的中断 OB
内存heap + stack, malloc/GC完全静态分配, 无 heap, 无递归
I/O内核驱动抽象, read()/write()过程映像: 每周期一次批量快照
错误处理异常/errno/core dump一般不崩溃: ENO 置位 + 继续 RUN (见 7.3)
实时性尽力而为 (best effort)确定性: 看门狗监督周期上限, 超时两次直接 STOP
状态保持进程死了状态就没了掉电保持 (retentive) 数据跨断电存活

为什么这样设计: 被控对象是电机、阀门、加热器——物理过程连续运行, 控制程序必须“永远在跑“; 同时控制逻辑要求可预测 (一个周期内必须算完), 所以宁可 STOP 也不允许无界延迟。你写过的最接近 PLC 的东西, 可能是 MCU 上的 while(1) 主循环 + 裸机中断, 而不是 Linux 服务。

2. 扫描周期 (scan cycle): PLC 的心跳

2.1 三阶段模型

CPU 的操作系统 (OS) 永远在做同一件事, 用 C 伪代码表达:

// PLC 操作系统的本质 (伪代码)
for (;;) {
    read_physical_inputs_to_input_image();  // 1. 物理输入 -> 输入映像 (process image input, I 区)
    execute_ob1_and_event_obs();            // 2. 执行用户程序 (OB1, 可被高优先级中断 OB 打断)
    write_output_image_to_terminals();      // 3. 输出映像 (Q 区) 一次性写到物理端子
    housekeeping();                         // 4. 诊断、通信、更新周期计时
    watchdog_check();                       // 5. 超过最大循环时间则按规则处置
}

关键语义:

  1. 读输入: 周期开始时, OS 把所有物理输入采样到输入映像表 (I 区)。用户程序读 %I0.0 读的是这份快照, 不是实时电平。
  2. 执行: OB1 从第一行到最后一行完整执行。期间读到的输入永远是本周期开头那份快照; 对 %Q 的赋值只改输出映像, 不立刻驱动端子。
  3. 写输出: 周期结束时 OS 把输出映像一次性刷到端子。即使程序中间对同一个输出赋值 10 次, 端子上只会看到最后一次的值——这是与“每条语句立即生效“的 MCU 直操 GPIO 的关键差异。
  4. 中断 OB (硬件中断、循环中断等) 抢占 OB1 执行, 打断延迟约 175 µs (官方数据)。

2.2 典型周期时长与看门狗 (watchdog / 循环监控时间)

  • 周期时长完全由用户程序规模决定, 小到中型项目通常在 1~10 ms 量级 (经验值, 非官方承诺); 实际值在 CPU 在线属性的 “Cycle time” 页可看到 current/min/max。
  • 最大循环时间 (maximum scan cycle time): 配置范围 1~6000 ms, 默认 150 ms, 恒启用, 配置入口为设备组态 → CPU 属性 → “Cycle”。这就是 PLC 的 watchdog。类比 MCU 的看门狗定时器, 区别: 你不需要手动喂狗——正常情况下一个周期结束 OS 自动重启计时。
  • 只有程序卡死 (死循环、过大的循环体) 才会触发。处置规则: 组态了时间错误 OB 时执行它并保持 RUN; 未组态时, 同一扫描内第一次超时被忽略, CPU 保持 RUN; 第二次超时 → 写诊断缓冲区并转 STOP (不同固件版本手册章节措辞略有差异, 以你的固件手册为准)。
  • RE_TRIGR 指令: 在用户程序里重启监视计时, 相当于“合法喂狗“, 可把本周期上限最多延长到 10 倍 (当前周期 < 10 × 最大值时 ENO=TRUE)。
  • 最小循环时间 (minimum cycle time): 默认禁用; 启用后短周期会被人为拉长, 用于让采样周期恒定 (例如 PID 控制需要稳定 Δt)。

2.3 思维转变: 不是事件驱动, 而是每周期整体重入

main loop + 回调 的对比:

事件驱动 (浏览器/Node/桌面)PLC 扫描模型
代码何时跑事件到达时回调被触发每周期无条件全量执行
“只在变化时处理”框架帮你做 diff你自己写边沿检测 (R_TRIG)
handler 内可以 await/阻塞可以绝对不行, 阻塞 = 拖垮周期 = STOP
注册/注销回调动态无此概念, 调用结构编译期固定

由此推出三条铁律 (C/Python 直觉全部失效的地方):

  1. 程序没有“跑完“这个概念。 每个周期都是一次完整重入。任何需要跨周期的信息 (计数器、状态机当前态、定时器累计值) 必须放在持久位置: M 区、DB、FB 的静态变量——绝不能放 VAR_TEMP (见 6.5)。
  2. “等待“不能靠 sleep。 time.sleep() 会吃掉整个看门狗预算。等待 = 用 TON 定时器描述“条件随时间的变化”, 本周期判一次, 不满足就结束本轮, 下周期再来——本质是状态机, 不是阻塞。
  3. 短事件可能被错过。 按钮按下 100 ms、周期 5 ms, 没问题; 若信号脉宽 < 一个周期, 采样可能漏掉——要么用硬件锁存/硬件中断 (OB40 类), 要么缩短周期。事件驱动框架里“回调永不丢失“的假设在这里不成立。

3. 存储体系: 装载 / 工作 / 保持 + 三个数据区

3.1 三级存储器

存储器易失性存什么类比
装载存储器 (load memory)非易失 (内部 Flash + 可选 SIMATIC Memory Card)完整用户程序、组态、初始值磁盘上的 firmware 镜像
工作存储器 (work memory)易失 (RAM)执行相关的代码与数据 (OS 从装载区复制过来)加载进 RAM 的 .text/.data
保持性存储 (retentive memory)非易失被标记 RETAIN 的工作内存值, 断电/暖启动后恢复MCU 的备份寄存器 / EEPROM
  • 下载 (download) 时块先落入装载存储器, OS 再把执行所需部分复制进工作存储器。S7-1200 内置装载存储器, 通常无需插卡即可运行; SIMATIC Memory Card 用于固件升级、程序交付与扩充 (不同型号/固件行为有差异, 以官方 CPU 手册为准)(待核实)。
  • 保持性存储容量很小, S7-1200 上常引用的规格是 10 KB 量级 (待核实, 以所用 CPU 规格表为准)。RETAIN 是稀缺资源, 别把大数组全设成保持。
  • 类比失效点: 没有“程序文件被 loader 按需换页“这回事——工作存储器装不下就下载失败, 相当于链接期报错而非运行期 OOM; 也没有 heap, 一切内存在编译期定死。

RETAIN 用法 (DB 变量属性):

DATA_BLOCK "Gbl_Persist"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR
      iTotalParts : Int := 0;         // 累计产量
      iShift : Int := 1;              // 班次
      rTempAtPowerFail : Real;        // 掉电瞬间温度
   END_VAR
BEGIN
   iTotalParts := 0;
   iShift := 1;
END_DATA_BLOCK

在 TIA 中把这几个变量的 “Retain” 列勾选 (或在声明中加 RETAIN 关键字, 取决于编辑器), 断电再上电后它们保留最后一次值; 未设 RETAIN 的变量在暖启动 (warm restart) 时被复位为声明时的初始值。启动流程: 上电 → 恢复保持值 → 执行启动 OB (OB100) → 进入 RUN 循环 OB1。

3.2 过程映像 I/Q 与位存储区 M

区域名称写入者类比
I (input)过程映像输入OS 每周期开头采样物理输入影子寄存器 / 每帧同步的输入快照
Q (output)过程映像输出用户程序写, OS 周期末刷到端子双缓冲的输出帧
M (bit memory)位存储区/标志区用户程序自由读写全局变量区
  • M 区就是“带绝对地址的全局变量“: %M100.5 一个 bool, %MB100/%MW100/%MD100 字节/字/双字。M 区可以在 CPU 属性里按范围设置保持性。容量数 KB 量级 (待核实, 随型号)。
  • :P 后缀绕过映像直访外设: "RawAI":P 立即读端子。仅在必须跳过快照语义时用。
  • 内存映射 IO (MMIO) 类比: %I0.0 像 MCU 的寄存器地址, 成立之处在于“固定地址、可直接读写“。失效之处: MMIO 寄存器反映硬件实时状态, 而 I 区是每周期一次的快照——你在 OB1 中段读 %I0.0, 拿到的仍是周期开始那一刻的电平, 中途的真实跳变你看不见。它更像“每帧同步一次的输入结构体“, 不像 volatile 寄存器。

4. 数据类型总览

4.1 基本类型表

类型大小取值范围 / 说明C 近似
BOOL1 位TRUE/FALSEbool
BYTE8 位16#00~16#FF, 按位串解释, 无符号语义uint8_t
WORD16 位16#0000~16#FFFF 位串uint16_t
DWORD32 位位串uint32_t
SINT / USINT8 位-128~127 / 0~255int8_t/uint8_t
INT16 位-32768~32767int16_t
UINT16 位0~65535uint16_t
DINT32 位-2147483648~2147483647int32_t
UDINT32 位0~4294967295uint32_t
REAL32 位IEEE 754 单精度, 约 ±1.18e-38~±3.40e38float
LREAL64 位IEEE 754 双精度double
TIME32 位有符号毫秒: T#-24d_20h_31m_23s_648 ~ T#+24d_20h_31m_23s_647 (约 ±24.8 天溢出)int32_t(毫秒语义)
DATE16 位1990-01-01 ~ 2168-12-31
TIME_OF_DAY (TOD)32 位当日毫秒数 00:00:00.000~23:59:59.999
DTL12 字节1970-01-01 ~ 2262-04-11, 含年月日时分秒毫秒/星期struct tm
CHAR / WCHAR8/16 位ASCII / UCS-2char/char16_t
STRING[n]n+2 字节2 字节头 + 最多 n 字符; 默认 n=254, 上限 254定长 char[] + len 头
WSTRING[n]2n+2 字节最多 65534 字符; 仅优化块可声明 (指令参数表同时接受 STRING/WSTRING, 但 WSTRING 进不了标准 DB 与通信映射, 实际项目以 STRING 为主)
ARRAY[a..b] OF T同质聚合, 支持多维C 数组
STRUCT异质聚合struct

注意: LTIME/LTOD/LDT 等 64 位时间类型是 S7-1500 独占, S7-1200 没有 (官方文档标题即标注 (S7-1500))。

4.2 STRING 的内存布局 (面试级细节)

字节:  [0]最大长度 [1]当前长度 [2..n+1]字符数据
声明 STRING[10] 占 12 字节:  10 | cur | 'a','b',...
  • 头两个字节分别是最大长度、当前长度, 所以 STRING[10] 实占 12 字节, 而非 10。
  • STRING 不能赋给 I/Q 存储区 (M/DB 可以); WSTRING 只能声明在优化块中, 进不了标准 DB 与通信映射表, 实际项目以 STRING 为主 (字符串指令参数表本身同时接受两者)。
  • 与 C 字符串最大的区别: 显式长度而非 \0 结尾; 与 Python str 的区别: 定长、单字节字符、可变而非不可变对象。

4.3 UDT (PLC data type ≈ typedef struct)

TYPE "UDT_MotorCfg"
VERSION : 0.1
   STRUCT
      rAccelRamp : Real := 1.5;      // 加速斜率, 声明即默认值
      tTripDelay : Time := T#3S;     // 跳闸延时
      iMaxTries  : Int := 3;
   END_STRUCT;
END_TYPE

UDT 是纯类型模板, 不占运行内存; 在 DB/FB 接口里 stCfg : "UDT_MotorCfg"; 使用。改 UDT 定义后, 所有使用处右键 “Update data type” 同步布局——相当于改了 struct 后重编所有引用单元。

5. 块模型 (block model): OB / FC / FB / DB / UDT

这是本章核心。PLC “程序“不是一坨源文件, 而是一组块 (block) 的集合, OS 按规则调度它们。

本质类比有状态?
OB (Organization Block)OS 的入口点/调度单元main + 各类 ISR无实例
FC (Function)无状态函数纯函数 (无 static 的 C 函数)否 (Temp 不算)
FB (Function Block)带状态的函数类定义是 (存在背景 DB)
背景 DB (instance DB)FB 的一次实例化对象实例
全局 DB (global DB)全局数据结构全局 struct 变量
UDT (PLC data type)类型定义typedef struct— (纯模板)

5.1 OB (组织块): OS 与用户代码的唯一接缝

OB = “当某事件发生时, OS 调用你写在这个 OB 里的代码”。S7-1200 优先级 1~26, 数字越大优先级越高; 同优先级不互相打断, 按队列/顺序执行。官方默认优先级与 1200 支持度:

事件经典编号 (旧文献)S7-1200 上的编号与支持默认优先级
程序循环OB1OB1, 也可再建多个循环 OB (编号 >=123, 依次执行)1 (最低)
启动OB100OB100 或 >=123, 可多个1
日时间中断 (time-of-day)OB101200 文档中未见 OB10 编号, TIA 对话框新建默认给 >=123; 最多 2 个事件2
延时中断OB20~23OB20-23 或 >=123; 与循环中断合计最多 4 个3~6
循环中断 (cyclic interrupt)OB30~38OB30-38 或 >=123; 时基 1~60000 ms; 与延时合计最多 4 个8~17
硬件中断OB40编号 >=123 (最多 50 个事件挂接)18
诊断错误中断OB82支持; 未组态 OB82 时 CPU 忽略诊断错误保持 RUN5
模块拔/插支持 (编号 >=123)6
机架/站故障OB86支持 (PROFINET 站掉线等)6
时间错误OB80支持22 (可改 22~26)
MC-Servo / MC-Interpolator运动控制 Technology Objects 内部使用25 / 24
编程错误 OB1211200 上不存在, 用 GET_ERROR/GET_ERROR_ID 局部处理
I/O 访问错误 OB1221200 上不存在, 记诊断缓冲保持 RUN

逐个说明:

  • OB1 (主循环): 心脏。每周期从头到尾完整执行一遍你的主逻辑, 可被所有其他事件类型打断 (优先级 1 最低)。
  • OB100 (启动): RUN 之前执行一次, 用于初始化: 恢复通信、设置初始模式、清批次数组。相当于“只跑一次的 setup()“。
  • 时间中断: 在设定的日历时刻/周期 (每分钟、每天 06:00 等) 触发, 用于报表、定时采样。老书上的 OB10 在 1200 上不要依赖手输, 用 TIA 对话框生成 (编号 >=123)。
  • 循环中断 (OB30x 类): 固定周期执行, 与 OB1 当前负载无关, 用于 PID 等需要恒定 Δt 的算法。注意其周期必须远小于其中代码耗时, 否则挤爆 CPU。
  • 硬件中断 (OB40 类): 输入模块检测到边沿等硬件事件立刻触发, 延迟约 175 µs——这才是 PLC 里最接近“回调“的机制。但语义仍是“跑一个短 OB“, 不是注册 handler。
  • 错误 OB: 时间错误 (周期超限等)、诊断错误 (从站诊断, OB82)、拔插、机架故障。1200 的重大差异: OB121/OB122 不存在——编程错误在启用局部错误处理的块里用 GET_ERROR 捕获 (7.3 节), I/O 访问错误只记诊断缓冲并保持 RUN。
  • OB 之间的抢占完全由优先级决定: 任意高优先级 OB 都能打断正在执行的任何低优先级 OB (硬件中断 18 可打断循环中断 8~17, 循环中断可打断 OB1); 同优先级的多个事件只排队依次执行, 不互相打断。不存在“默认不可中断“的组态——设计共享数据时按“任何 OB 都可能被更高优先级打断“来考虑。

OB1 骨架 (SCL 版):

// 组织块: Main [SCL] (OB1)
VAR_TEMP
   // OB 只有临时变量, 没有静态区
END_VAR

BEGIN
// 信息流: 输入(快照) -> 逻辑 -> 输出映像; 以下每周期整体执行
"FB_Station_DB"(bEStop := "EStop_Relay");          // 调用 FB, 单实例 DB
"Gbl_Data".iCycleCount := "Gbl_Data".iCycleCount + 1;  // 全局 DB 累计
END_ORGANIZATION_BLOCK

5.2 FC: 无状态函数

  • 没有“记忆“: 除 VAR_TEMP (不保证初始化的栈) 外无本地存储; 需要跨周期状态就得靠调用方经 VAR_IN_OUT 传入, 或用全局 DB。
  • 无递归: 块调用链编译期固定且禁止循环调用, 这是为了让存储需求可静态计算。
  • 适合: 纯计算 (缩放、限幅、CRC)、把大段逻辑拆小。类似 Python 里不碰全局变量的纯函数。
FUNCTION "FC_Sum1toN" : Int
VERSION : 0.1
   VAR_INPUT
      iN : Int;
   END_VAR
   VAR_TEMP
      i : Int;
      iAcc : Int;      // 陷阱见 6.5: 用前必须先赋值
   END_VAR

BEGIN
   #iAcc := 0;
   FOR #i := 1 TO #iN DO
      #iAcc := #iAcc + #i;
   END_FOR;
   #FC_Sum1toN := #iAcc;   // 函数名即返回值
END_FUNCTION

5.3 FB + 背景 DB: 类与对象

FB = 带接口声明 + 静态存储的“类“; 每次实例化生成一个背景 DB (instance DB), 存放该实例的 Input/Output/Static 副本。状态天然跨周期持久——这是 PLC 对 C 程序员最大的礼物: 你不用手工把状态提到全局区, FB 实例帮你封装。

FUNCTION_BLOCK "FB_Motor"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      bStart : Bool;         // 瞬时启动信号
      bStop  : Bool;         // 瞬时停止信号
   END_VAR
   VAR_OUTPUT
      bRun   : Bool;         // -> 接触器
      bReady : Bool;         // 稳定运行标志
   END_VAR
   VAR
      tonTrip : TON_TIME;    // IEC 定时器多重实例 (16 字节 IEC_Timer 结构)
      iStartCount : Int := 0; // 静态: 跨周期保持, 启动计数
   END_VAR

BEGIN
   // 启保停: 每周期重入, 用 IF/ELSIF 表达"锁存"
   IF #bStop THEN
      #bRun := FALSE;
   ELSIF #bStart THEN
      #bRun := TRUE;
      #iStartCount := #iStartCount + 1;   // 演示: 无边沿检测每周期都会加 (见 R_TRIG 章节)
   END_IF;

   #tonTrip(IN := #bRun, PT := T#3S);     // 定时器状态存在 #tonTrip 里, 跨周期累计
   #bReady := #tonTrip.Q;
END_FUNCTION_BLOCK

单实例调用 (OB1 里, 自动生成实例 DB “FB_Motor_DB”): "FB_Motor_DB"(bStart := "BtnStart", bStop := "BtnStop", bRun => "K1");

5.4 全局 DB: 全局 struct

上面 3.1/2 已示例。要点: 全局 DB 是命名数据容器, HMI、通信、程序共同读写; 优化访问是默认 (见 8.2); 变量可设 RETAIN。

5.5 多重背景 (multi-instance): 成员对象组合

FB 内部声明另一个 FB 类型的静态变量, 该成员实例的数据存在外层 FB 的背景 DB 里, 不产生独立的 DB——等价于 C++ 里 class Station { Motor motorA; Motor motorB; }; 的成员对象, 而不是指针。

FUNCTION_BLOCK "FB_Station"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   VAR_INPUT
      bEStop : Bool;
   END_VAR
   VAR_OUTPUT
      bAnyRunning : Bool;
   END_VAR
   VAR
      fbMotorA : "FB_Motor";    // 多重背景: 组合
      fbMotorB : "FB_Motor";
   END_VAR

BEGIN
   #fbMotorA(bStart := "BtnA_Start", bStop := #bEStop, bRun => "K_A");
   #fbMotorB(bStart := "BtnB_Start", bStop := #bEStop, bRun => "K_B");
   #bAnyRunning := "K_A" OR "K_B";
END_FUNCTION_BLOCK

好处: 20 台电机 = 一个数组化的组合 FB + 一个背景 DB, 而不是 20 个散落的全局 DB。

6. 块接口语义: 五种声明区逐个讲透

声明区语义C 类比传入副本?
VAR_INPUT值传入, 块内可读; 一般不改 (改了也不写回调用者)按值参数
VAR_OUTPUT值传出, 周期结束时写回调用者实参返回值/输出参数是 (写出)
VAR_IN_OUT引用传入, 读写直接作用于调用者的变量float* / C++ float&名义上副本进出, 语义即引用
VAR (仅 FB)静态变量, 存于背景 DB, 跨周期持久类的 private 成员
VAR_TEMP临时局部变量, 栈上, 不保证清零未初始化的栈变量

6.1 VAR_INPUT / VAR_OUTPUT: 值语义

调用 #fbMotorA(bStart := x, bRun => y): bStart 的当前值拷入实例接口; 块执行完, Output 的值拷给 y。Input 写了新值不会影响调用者的 x——除非你显式接回。类比 C 的按值传参 + 输出参数; 类比失效处: FB 的 Input/Output 本身就是背景 DB 里的存储, 上周期的值还在, “未接线的 Output 保留旧值“可能造成误解。

6.2 VAR_IN_OUT: 引用语义

FUNCTION "FC_Scale"
VERSION : 0.1
   VAR_INPUT
      rInMin : Real := 0.0;
      rInMax : Real := 27648.0;   // 模拟量原始满量程
      rOutMin : Real := 0.0;
      rOutMax : Real := 100.0;
   END_VAR
   VAR_IN_OUT
      rValue : Real;              // 读-改-写回调用者的变量 (类比 Real*)
   END_VAR

BEGIN
   IF #rValue < #rInMin THEN
      #rValue := #rInMin;          // 限幅直接写回调用者
   ELSIF #rValue > #rInMax THEN
      #rValue := #rInMax;
   END_IF;
   #rValue := (#rValue - #rInMin) * (#rOutMax - #rOutMin)
              / (#rInMax - #rInMin) + #rOutMin;
END_FUNCTION

调用: #ret := ... 不存在, 直接 FC_Scale(rValue := "Raw"); 之后 "Raw" 已被就地修改。规则: 大数据 (ARRAY/STRUCT/STRING) 尽量走 InOut 避免拷贝开销; InOut 实参必须是可写左值。注意 1200 上没有 POINTER/ANY/REF 引用类型 (那是 S7-1500 的), “传任意类型“用 VARIANT (待深入, 见 8.4)。

6.3 VAR (FB 静态): 成员变量

见 5.3。要点: 初值只在下载/存储复位/暖启动时生效一次, 不是“每次调用“——FB 没有 constructor

6.4 VAR 与全局 DB 的选择

模块自身状态 → FB 静态; 多模块/外部 (HMI、通信) 共享的数据 → 全局 DB。

6.5 VAR_TEMP: 未初始化栈变量, 官方盖章的坑

官方原文: 临时局部数据仅保留一个扫描周期, “必须在想读取它的那个周期内先写入”; 在标准访问的 FC 中使用前必须初始化, 否则值可能是随机的; Temp 不存入实例 DB。

用 C 的话说: VAR_TEMP 等价于 int x; 后直接用——UB。用 Python 的话说: 没有“未定义就报 NameError“这层保护。所以 SCL 惯例是 Temp 先赋值再读; 编译器不会替你清零, 也别依赖上周期残留值做“记忆“ (那该用静态)。

7. 调用与执行语义

7.1 OB1 每周期从头到尾完整执行

调用图编译期固定: OB1 → FB → FB/FC……像一个静态调用树每周期遍历一次。条件调用 (IF cond THEN "FB_x"(...)) 是合法且常用的——但见下条后果。

7.2 FB 未被调用时, 背景 DB 保持上次值

C 对象离开作用域即析构, Python 靠 GC; 背景 DB 永远存在, FB 一旦不被调用, 只是“这个周期没执行它的代码“, 其静态变量与上次的 Input/Output 原样冻结。后果:

  • 特性: 条件调用大算法可以省周期时间, 实例状态无损。
  • 坑 1: FB 内部的 IEC 定时器/计数器不会走, 但也不会复位——“暂停“而非“清零”; R_TRIG 之类的内部边沿记忆同样冻结。
  • 坑 2: 恢复调用的第一个周期, 上次冻结的中间状态继续演进, 与“重新开始“语义不同。需要重入复位就显式给 FB 加 bReset 输入。
  • 坑 3: Output 未接线时, 读到的是冻结的旧值。

7.3 ENO 与 S7-1200 的运行期错误行为

ENO (enable out) 是“上一指令出错“位: 出错时 ENO=FALSE, 后续 ENO 级联的指令被跳过; SCL 里可用 instruction(..., ENO => #ok); 捕获。没有异常机制, 没有 core dump, CPU 一般保持 RUN。S7-1200 具体行为 (官方 ENO 表):

运行期错误S7-1200 行为
整数除零 (IN2=0)结果为 0 (undefined, zero returned), ENO=0, 保持 RUN
REAL/LREAL 非法运算 (NaN/INF)ENO=0, 返回 NaN, 保持 RUN
CONV 转换溢出/NaNENO=0 (OUT 取值不同版本文档表述有差异——以 ENO 为准, 不要依赖 OUT), 保持 RUN
字符串转数值 (S_CONV/STRG_VAL) 遇非法字符/超范围ENO=0, OUT=0 (从首字符转换到首个非法字符为止), 保持 RUN
数组下标越界记诊断缓冲区并保持 RUN (S7-1500 才是转 STOP/OB121); ENO 不会因越界置 FALSE
编程错误 (块内启用局部错误处理)写入 ErrorStruct, 用 GET_ERROR/GET_ERROR_ID 读取

对比 C: 除零在整数下是 UB/崩溃, 在浮点下是 INF; 对比 Python: 抛 ZeroDivisionError。PLC 的哲学是“受控地给出错值并让机器继续转“——所以每条会出错的指令都该检查 ENO, 否则 0 会像真数据一样流进控制逻辑。数组越界不 STOP 这点对 C 程序员尤其反直觉: 坏下标静默读出垃圾值, 务必在代码里自己守住边界。

8. 寻址: 绝对 / 符号 / 优化块 / AT / PEEK-POKE

8.1 绝对寻址与符号寻址

绝对地址含义
%I0.0 / %Q0.0 / %M100.5输入/输出/M 区的位 (字节 0/100, 位 0/5)
%IB0 %QB4 %MB100字节
%IW0 %MW100字 (16 位)
%ID0 %MD100双字 (32 位)
"Gbl_Data".iCycleCount符号寻址: DB 名.变量名

规则: 每个物理点先在硬件组态里分到 I/Q 地址, 再在 PLC tags 表里给 %I0.0 起符号名 ("EStop_Relay"), 代码里一律用符号。注意标识符不区分大小写: MotorMOTOR 是同一个 tag (官方规则); 保留关键字 (Int、指令名等) 不得作 tag 名。

8.2 优化块 vs 标准块 (内存布局与通信映射)

新建 DB/FB 默认 “Optimized block access”。差异全景:

维度优化块 (默认)标准块 (standard)
变量地址只有符号名, 无 % 地址, CPU 自动紧凑排列无空隙符号 + 固定绝对地址 (如 %DB5.DBX30.0)
布局编译器自由重排 (按访问优化)按声明顺序对齐, 可能有填充字节
兼容性S7-1200/1500兼容 S7-300/400 及经典工具/协议
PEEK/POKE 访问 DB不允许允许
AT 覆盖仅保持性 (retentive) 标签任意标签
WSTRING允许声明不允许
按“固定字节偏移“的外部映射不适用 (偏移不承诺稳定)适用

对通信映射的意义: 凡是“第三方按偏移读写你的 DB“的场景——跨 CPU 通信表、Modbus TCP 保持寄存器映射、非西门子 HMI/网关——都需要可承诺的字节布局, 因此要么用标准 DB, 要么用专门的标准“接口 DB“做边界转换。优化块内部随便改不影响外部布局, 这是“信息隐藏“; 标准块是“序列化格式“。两者并存的最佳实践: 内部全优化, 边界放标准 DB。

8.3 AT 视图: 同一块内存的另一种类型 (≈ union 的受控版)

// UDT "UDT_ByteBits" (PLC 数据类型): STRUCT  b0, b1, ... b7 : Bool;  END_STRUCT
//   -- 8 个 Bool 成员在 Struct 内按位打包, 共 1 字节

FUNCTION_BLOCK "FB_UnpackMode"
{ S7_Optimized_Access := 'FALSE' }   // 标准块才能对任意标签做 AT 覆盖
VERSION : 0.1
   VAR_INPUT
      byMode : Byte;                       // 来自通信报文的原始字节
      xBits  : "UDT_ByteBits" AT byMode;   // 同一内存的位视图 (TIA 中 AT 写在"覆盖"列)
   END_VAR

BEGIN
   IF #xBits.b0 THEN        // 读 byMode 的 bit0
      ;
   END_IF;
END_FUNCTION_BLOCK

规则: 覆盖体大小 ≤ 被覆盖体; 不能覆盖 VARIANT; 优化块中只能覆盖保持性标签 (以官方页面为准, 实践建议 AT 只在标准块用)。注意 Array of Bool 的布局与块访问模式有关 (标准块每元素 1 字节, 优化块按位打包)——位视图请用 Bool 成员的 Struct (按位打包) 而不是 Bool 数组。与 C union 的差别: AT 是“给已有标签加视图“, 不额外占内存, 也不允许两个写者打架之外的新风险面。

8.4 间接访问: 数组变址与 PEEK/POKE

S7-1200 没有 C 式指针 (POINTER/ANY/REF_TO 是 S7-1500 的能力)。间接访问三板斧:

  1. 数组变址: #v := "Gbl_Data".aiBuf[#i]; 下标可用 Int/DInt 变量, 支持多维 [i, j]
  2. PEEK/POKE 家族 (仅 SCL), 按区域码+字节偏移读写:
// 区域码: 16#81=I, 16#82=Q, 16#83=M, 16#84=DB (DB 必须是标准块)
#raw := PEEK_WORD(area := 16#83, dbNumber := 0, byteOffset := 100);   // 读 MW100
POKE(area     := 16#84, dbNumber := 5, byteOffset := 20, value := 16#00FF);
POKE_BOOL(area := 16#83, dbNumber := 0, byteOffset := 10, bitOffset := 3, value := TRUE);
  1. VARIANT 形参 + VariantGet/VariantPut/CountOfElements: 传“任意类型“参数并运行期取实数边界 (LOWER_BOUND/UPPER_BOUND), 是 1200 上做泛型库函数的正道; ARRAY[*] 形参需优化块 + 固件 >= V4.2。

另有 slice 访问: "dwFlags".X3 / .B1 / .W1 / .D0 直接取整型变量的位/字节/字片段 (SCL 中需关闭该块的 IEC 检查)。

9. PROFINET 与分布式 IO: 硬件组态是什么

  • PROFINET IO: 建立在标准以太网上的工业实时协议。三个角色: IO Controller (PLC CPU, 主)、IO Device (分布式 IO 站, 如 ET 200SP, 从)、IO Supervisor (工程/诊断工具)。
  • 周期数据交换: Controller 与每个 Device 按组态的更新周期 (毫秒级) 交换输入/输出帧; 远程站的通道在硬件组态里被分配 I/Q 地址后, 在用户程序里与本地 IO 完全同构 (%I2.0 可能来自 30 米外的 ET 200SP 模块)。这正是过程映像抽象的威力: 分布式对程序员透明。
  • 非周期通道 (record data) 用于参数化与诊断, 与周期数据分离。S7-1200 支持 RT 通信等级; 等时同步 IRT 不在 1200 能力内 (待核实)。S7-1200 可同时充当 IO Controller 与 IO Device (智能从站, 组合限制以手册为准)(待核实)。
  • 硬件组态 (device configuration): 在 TIA Portal 里声明“机架上有什么“: CPU 型号、信号模块 (SM) 槽位、PROFINET 站 (设备名 + IP)、各通道地址与参数 (如模拟量 4~20 mA / 0~10 V)。它不是代码, 是一份随项目一起下载到 CPU 的静态描述。最贴切的类比: 嵌入式 Linux 的 devicetree——用声明式数据描述硬件拓扑, 运行时 OS 据此映射寄存器/报文到地址空间。第三方设备用 GSD 文件导入 (≈ 驱动描述文件, 厂商提供设备的能力模板)。
  • 站掉线属于“机架/站故障“事件 (优先级 6), 可组态 OB 处理或靠诊断缓冲发现。

10. 诊断体系: 诊断缓冲区 / LED / 在线模式

  • 诊断缓冲区 (diagnostic buffer): CPU 内的带时间戳环形事件日志: 模式切换 (STOP/RUN 及原因)、断电、模块故障、站掉线、周期超时、数组越界等全部留痕。排障第一站永远是 “Go online → Diagnostics buffer”。类比: 内核日志 ring buffer (dmesg), 但每个条目带硬件时间戳且断电不丢。
  • CPU LED:
LED颜色含义
RUN绿RUN 模式, 循环执行
STOPSTOP 模式
ERROR出错: 闪烁 = 事件级错误 (如站故障); 常亮 = CPU 硬件缺陷
MAINT需要维护 (如组态与实机不一致)
  • 在线模式 (online mode): 下载/监控三件套——监视表/监控表 (watch table) 实时看/改变量; 强制表 (force table) 把变量强制为固定值 (绕过程序, 类比调试器的变量 pin, 但作用于真实端子, 现场危险操作); 程序状态 (status) 在线看 SCL 行级当前值。注意: S7-1200 不支持断点调试 (断点仅 S7-300/400 与固件 >= V2.5 的 S7-1500, 官方明示), 仿真器 PLCSIM 仿 1200 同样无断点。你的调试工作流要从“打断点单步“改成“watch table + status + PLCSIM 仿真 + 诊断缓冲“。

11. PLC ↔ C/Python 概念对照总表

C / PythonS7-1200 (TIA Portal)备注
main() + while(1)OB1 程序循环每周期全量重入, 不退出
中断服务程序 ISR事件 OB (硬件中断/循环中断/时间中断)优先级抢占, 快进快出
setup() / 构造OB100 启动RUN 前跑一次
无状态函数FC无 static, 无递归
类定义FB接口 + 静态数据
对象实例背景 DB (instance DB)生命周期 = 永久, 见 7.2
成员对象组合多重背景 multi-instance嵌在外层背景 DB 里
全局 struct全局 DB可 RETAIN, HMI 可直接绑
typedef structUDT (PLC data type)纯模板
按值参数 / 输出参数VAR_INPUT / VAR_OUTPUT值拷贝进出
T* / T& 传参VAR_IN_OUT引用语义
未初始化局部变量VAR_TEMP不清零, 先写后读
类私有成员FB 的 VAR (静态)跨周期持久
内存映射 IO 寄存器过程映像 I/Q差异: 快照而非实时, 见 3.2
volatile 全局变量M 区 / I / Q每周期被 OS/现场“改写“
看门狗喂狗循环监视 150 ms 默认, 自动重启RE_TRIGR 手动喂, 超时两次 STOP
heap / malloc / GC不存在一切静态分配, 编译期定死
异常 / errnoENO + 诊断缓冲不崩溃, 见 7.3
断点调试1200 无断点; watch/force/statusPLCSIM 也无
devicetree硬件组态 (device config)声明式硬件描述

12. 类比何时失效 (重要: 每条都是真事故来源)

  1. “程序会跑完/会退出“失效。 PLC 程序是无限重入的循环体。把“一次性初始化“写成裸代码而不放进 OB100, 它会每周期执行一遍; 把跨周期状态放 VAR_TEMP, 下周期读到的是随机残留。
  2. 对象生命周期失效。 C++ 栈对象离开作用域析构, Python 靠 GC; 背景 DB 永不析构。“未调用 = 冻结而非销毁”——定时器不走也不清零, 恢复调用时从冻结点继续 (见 7.2)。FB 也没有 constructor, 初值只在下载/重启时生效。
  3. “回调/事件不丢“失效。 没有框架替你做 diff 与排队; 每周期全量执行意味着边沿要自己检 (R_TRIG), 比扫描周期更短的脉冲会丢, 除非上硬件中断。
  4. 异常处理模型失效。 除零不抛异常: 结果 0 + ENO=0 + 继续 RUN; 数组越界在 1200 上不 STOP, 静默读垃圾。不检查 ENO, 错误值会混进 PID 与 HMI。没有 try/catch, 局部错误用 GET_ERROR 结构化捕获。
  5. 指针与动态内存失效。*、无指针算术、无 malloc、无递归; VAR_IN_OUT 只有“引用“语义; 间接访问走数组变址/PEEK/POKE/VARIANT。
  6. “阻塞等待“失效。 sleep/await/长循环都会吃掉看门狗预算, 两次超时即 STOP; 等待必须改写成定时器驱动的状态机。
  7. 多线程直觉部分失效。 没有线程, 但中断 OB 与 OB1 是真实的并发单元: 两者共享的 DB 数据有一致性问题 (读了一半被改), 需要像对待 ISR 共享数据一样设计访问次序。没有 mutex, 靠优先级与结构安排。
  8. “断点单步“失效。 S7-1200 连仿真器都没有断点; 调试 = watch table + status + PLCSIM + 诊断缓冲。习惯 printf 的人转 trace/trace table (若 CPU 支持) 或把状态导出到 HMI。
  9. 内存布局直觉失效。 优化块 (默认) 没有稳定地址, 编译器会重排; 按字节偏移对外映射必须显式用标准块。别拿 C 的 struct 布局/对齐直觉去猜 DB 偏移。
  10. 大小写与命名失效。 tag 名不区分大小写 (Motor == MOTOR), 与 Python/C 的标识符敏感性相反; 保留字 (如 Int) 不能作变量名。
  11. 精度直觉部分失效。 TIME 是 32 位毫秒整数, 约 ±24.8 天回绕——长期累计计时要用 DTL 或自行进位; REAL 是单精度, 别拿 double 的精度预期写控制算法。
  12. 安全责任不类比。 桌面软件崩溃重启即可; PLC 输出直接驱动物理设备, “保持 RUN + 输出错误值“可能比崩溃更危险。急停、联锁必须在硬件层与程序层双重存在——这不是语法问题, 是行业底线。

下一篇 (03) 将进入 SCL 语法细节: 表达式、控制流、定时器/计数器与状态机写法。

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。

04 标准库与常用功能: TIA Portal 指令全景与实战 (S7-1200 + SCL)

面向有 10 年 C/Python 经验、正在学 S7-1200 + TIA Portal + SCL 的工程师。本章把 TIA Portal 的指令体系当作“标准库“来梳理: 怎么查找、怎么在 SCL 里调用、每类常用指令的精确语义, 以及模拟量 / PID / HSC / 运动控制 / 通信 / 诊断这些“应用层大件“的完整走查。所有 SCL 代码均按 TIA Portal V16~V20、S7-1200 (一代, 固件 V4.x) 可用语法书写, 变量声明区与代码区分开。

目录

  1. 指令体系如何组织与查找
  2. SCL 中的调用形态
  3. 基础指令精讲: MOVE 与选择类
  4. 转换指令族
  5. 移位与字逻辑
  6. 模拟量: 27648 的世界与 NORM_X / SCALE_X
  7. PID: PID_Compact 完整走查
  8. 高速计数器 HSC
  9. 运动控制 (PTO 基本定位)
  10. 通信: OUC / Modbus TCP / PROFINET / OPC UA / Web server
  11. 库工程化: 项目库、全局库、第三方库
  12. 诊断类指令与系统功能一览
  13. 收尾: 用库思维写 SCL

1. 指令体系如何组织与查找

TIA Portal 的程序编辑器右侧有一条 Instructions task card(指令任务卡), 指令按三个层级组织:

层级内容类比
Basic instructions位逻辑、定时器、计数器、比较、数学、MOVE、转换、移位、字逻辑C 的 stdlib.h / 运算符
Extended instructions日期时间、中断、诊断、通信 (TSEND_C、MB_CLIENT)、PID、字符串、数据日志等Python 的 batteries included 标准库
Libraries项目库 (Project library)、全局库 (Global library)、西门子自带库 (如 LBC, Library of General Functions)npm 包 / 公司内部 pip index

几点精确说明:

  • Basic / Extended 的区分只是组织方式, 不代表编译产物不同; 两者在 SCL 里都是“函数/功能块调用“或“表达式风格指令“。
  • Technology(工艺)指令单独成组: PID_Compact/PID_Temp 在 Extended > PID control; HSC 的 CTRL_HSC 在 Technology > Counting (High-speed counters); 运动控制 MC_xxx 在 Technology > Motion control。
  • 指令的 CPU 支持范围以在线帮助条目标题为准: 官方文档对 SCL 参考里每条指令都标注 (S7-1200, S7-1500)(S7-1500), 后者即 1200 不可用 (典型: REF 引用类型、POINTER/ANY 参数、遗留 S5 定时器/计数器、TypeOfDB、ARRAY DB、断点调试、LTIME 等)。
  • 查找方式: 在 task card 顶部的搜索框输入指令名 (如 MUX) 或在编辑器内直接键入指令名前几个字母, TIA 会弹出匹配列表 (类似 IDE 的补全); 按 F1 打开该指令的在线帮助页, 这是唯一权威语义来源。

一个重要的“程序员直觉校准“: TIA 的指令不是宏, 而是编译器内建操作 + 预编译库块的混合体。Basic 指令多数编译为内建操作 (无调用开销感), Extended/Technology 指令多数是库 FB/FC, 拖入时会让你创建 instance DB。

2. SCL 中的调用形态

2.1 三种形态

1) 表达式风格 (仅 Basic 类):  #y := SEL(G := #g, IN0 := #a, IN1 := #b);
2) FC 调用 (无状态):          #ok := "LBC".Denoise(...);        // 返回值可接可不接
3) FB 调用 (必须有实例):      "PID_DB"(Setpoint := 100.0, Input := #pv);
  • FC (Function): 无实例、无状态 (VAR_TEMP 生命周期仅当次调用), 相当于纯函数 —— 但注意 S7-1200 的 FC 也允许写全局 DB / Q, 编译器不强制纯度。
  • FB (Function Block): 必须绑定 instance DB (单独实例) 或作为 multi-instance 壌在调用者 FB 的 static 区 (相当于“调用者对象内嵌子对象“)。SCL 中 FB 调用形如 "InstDB"(参数表), 参数表里没出现的引脚沿用实例 DB 中的上次值 —— 这是与 C 最大的差异: FB 调用是“有状态的调用“, 省略参数不等于使用默认值, 而是沿用上次调用的值
  • 从 task card 拖 FB 到 SCL 代码区, TIA 弹出“调用选项“对话框, 让你选 single instance (新建独立 DB) 还是 multi-instance (存在当前 FB 的 static 区)。SCL 亦可在块接口的 Static 区手写 MyInst : TON; 然后 #MyInst(IN := ...)

2.2 EN/ENO 语义 (必懂)

多数指令支持 EN 与 ENO。SCL 中:

  • 块属性 “Set ENO automatically” (自动生成 ENO) 开启时, 运行期错误 (除零、CONV 溢出等) 会把 ENO 置 FALSE 并继续执行下一条语句 —— 类似 Python 的“异常被吞掉但留了 flag“, 而不是 C 的段错误。
  • => 语法接 ENO: "CONV_DB"(..., ENO => #ok); 或对表达式风格指令 #ok := ... 之外的独立检测。
  • 已核实的错误行为 (S7-1200): 整数除零 → 结果置 0、ENO=0、保持 RUN; CONV 溢出 → ENO=0 (OUT 取值不同版本文档表述有差异, 以 ENO 为准); 数组越界 → 记入诊断缓冲、保持 RUN, 但 ENO 不会因此置 FALSE。详见第 12 节与 03 章。

3. 基础指令精讲: MOVE 与选择类

3.1 MOVE

MOVE 把 IN 复制到 OUT (可挂多个输出)。在 SCL 里 := 赋值就是 MOVE, 所以 MOVE 指令的真正用途只剩两个: 需要在 F1 帮助里查 ENO 行为时; 以及需要在 EN=FALSE 时保持“整段复制不发生“的可视化场景。SCL:

// 两种写法等价:
"DB_Data".Cmd := #cmd;                                  // 赋值即 MOVE
MOVE(IN := #cmd, OUT1 => "DB_Data".Cmd, OUT2 => #bak);  // 指令形态: 一次复制两份

注意: MOVE 走的是“符号赋值“语义, 数据类型必须赋值兼容 (隐式转换规则见 4.1); 要按原始字节复制请用 POKE_BLK 或对标准 DB 用 PEEK/POKE 家族 (见 03 章)。

3.2 选择类指令 (SEL/MUX/MIN/MAX/LIMIT)

这一族是 SCL 里最像“三元运算符 / switch / clamp“的东西, 但语义有细节差异, 逐个给出:

指令语义C/Python 类比类比不成立之处
SEL(G, IN0, IN1)G=FALSE 选 IN0, G=TRUE 选 IN1G ? IN1 : IN0注意参数顺序: FALSE 走 IN0, 与 C 三元表达式前后位置相反, 极易写反
MUX(K, IN0..INn, ELSE)整数 K 选第 K 个输入; K 超范围 → ELSE 且 ENO=0数组下标 + defaultMUX 是“位置参数即下标“, 不是数组访问; 超范围不报异常而是走 ELSE
MIN(IN1..) / MAX(IN1..)多输入取最小/最大Python min()/max()结果类型跟随输入类型 (整型家族按声明类型截断/符号处理), 无 Python 的跨类型提升
LIMIT(MN, IN, MX)夹紧到闭区间max(MN, min(IN, MX))MN > MX 时 ENO=0 且行为未定义 (C 里同样 UB, 这点类比成立)

示例 FB —— 一个“限幅三态选择器“, 把四种选择指令串起来用:

// FB 接口:
//   Input:  selMode : Int      // 0=取最小 1=取最大 2=取A 3=取B 4=按K选择
//          valueA  : Int
//          valueB  : Int
//          clampLo : Int
//          clampHi : Int
//          k       : Int       // MUX 的选择号
//   Output: result  : Int
//          valid   : Bool
//   Static: 无
// 代码区:
CASE #selMode OF
    0:  // 两个输入取较小者
        #result := MIN(IN1 := #valueA, IN2 := #valueB);
        #valid := TRUE;
    1:  // 取较大者
        #result := MAX(IN1 := #valueA, IN2 := #valueB);
        #valid := TRUE;
    2:  // 选 A
        #result := SEL(G := FALSE, IN0 := #valueA, IN1 := #valueB);
        #valid := TRUE;
    3:  // 选 B
        #result := SEL(G := TRUE, IN0 := #valueA, IN1 := #valueB);
        #valid := TRUE;
    ELSE  // 4 或其它值: 用 MUX 在 A/B/夹紧后的A 三者中选
        MUX(K := #k,
            IN0 := #valueA,
            IN1 := #valueB,
            IN2 := LIMIT(MN := #clampLo, IN := #valueA, MX := #clampHi),
            ELSE => #result,
            ENO => #valid);      // K 超范围: ELSE 生效, valid=FALSE
END_CASE;

// 最终结果统一限幅; MN>MX 时 ENO=0
LIMIT(MN := #clampLo, IN := #result, MX := #clampHi, OUT => #result, ENO => #valid2);

实践建议: SEL/MUX/LIMIT 的参数必须用命名参数 (G := , IN0 := ) 写, 位置参数在这里几乎是可读性灾难。

4. 转换指令族

4.1 隐式转换 vs 显式 CONV

SCL 赋值/传参时, 整型家族 (SInt..DInt/USInt..UDInt) 与 REAL 之间按“宽度提升“隐式转换; 但有以下硬规则:

  • REAL → 整型必须显式 (ROUND/TRUNC/CEIL/FLOOR), 直接 #i := #r; 不编译 —— 比 C 严格, 类似 Rust 的 as 语义要求显式。
  • String ↔ 数值必须走 S_CONV / STRG_VAL / VAL_STRG (字符串细节见 03 章)。
  • 位串家族 (Byte/Word/DWord) 与整型之间可以 CONV, 但 Bool → Byte 之类的“放大“要用 CONVERT 指令形态。

4.2 指令清单

指令语义备注
CONV显式类型转换溢出: ENO=0 (OUT 取值以 ENO 为准, 不要依赖); 这是“越界不抛异常但留痕“的又一例
ROUNDREAL → 整型, 就近取整.5 边界的舍入方向以在线帮助为准 (待核实)
TRUNCREAL → 整型, 向零截断等价 C 的 (int)x
CEIL / FLOOR向 +∞ / −∞等价 math.ceil/floor
NORM_X / SCALE_X归一化 / 定标见第 6 节, 模拟量核心
S_CONV / STRG_VAL / VAL_STRG字符串 ↔ 数值非法字符/超范围: ENO=0 且 OUT=0 (已核实)
CHARINTCONV 的具体实例#i := CONV(Int, #ch); — 待核实: V16~V20 具体拼写以帮助为准

示例: 把 Word 原始值安全转成工程量 (显式 + 错误检测):

// Input:  rawW  : Word    // 如来自 %IW
// Output: asInt : Int
//         asReal: Real
//         ok    : Bool
CONV(IN := #rawW, OUT => #asInt, ENO => #ok);
IF #ok THEN
    #asReal := TRUNC(IN := CONV(Real, #asInt));  // 先显式 Int->Real, 再截断回 Int? 不: 见下
END_IF;

上面第二行故意写成错的示范 —— TRUNC 的输出是整型, 不能赋给 #asReal。正确写法:

#asReal := CONV(Real, #asInt);   // Int -> REAL 显式转换 (也可隐式, 显式更清晰)

CONV(目标类型, 表达式) 是 V16+ 的函数风格写法; 拖拽指令时 TIA 会生成带 => 的指令形态, 两种都合法。待核实: 函数风格 CONV(Real, x) 的确切括号语法在你所用 TIA 版本的编辑器里可能是 Real#(x) 之外的写法, 以 F1 帮助为准。

5. 移位与字逻辑

5.1 SHL/SHR/ROL/ROR 与 SWAP

指令语义备注
SHL(IN, N)左移, 空位补 0操作数按位模式处理; 有符号整型右移不保留符号位 (待核实: 帮助中 SHR 的空位填充描述为 0)
SHR(IN, N)右移, 空位补 0对 Int 想要算术右移时, 先转无符号再移, 或用除法
ROL / ROR循环左/右移移位数按操作数位宽取模
SWAP(IN)字节序交换 (16/32 位)Word/DWord/Real 通信场景必备, 等价 Python struct.unpack('>H', ...) 前的手工翻转

示例: 从 Word 中按位打包/解包状态 (比 AT 覆盖更直观, 比 PEEK 便宜):

// Input:  statusWord : Word
//         bitNo      : UInt
// Output: bitOut     : Bool
//         highByte   : Byte
// Static: tmp : Word

// 取第 bitNo 位 (0 = LSB)
#tmp := SHR(IN := #statusWord, N := #bitNo);   // 右移使目标位到 bit0
#bitOut := (#tmp MOD 2) = 1;                   // 或用 PEEK_BOOL 亦可

// 取高字节: 右移 8 位后 CONV 成 Byte
#highByte := CONV(Byte, SHR(IN := #statusWord, N := 8) AND 16#00FF);

// 反过来打包: 把 8 个 Bool 装进一个 Byte (用 SHL 累积)
// Input: b : Array[0..7] of Bool   Output: packed : Byte
#packed := 0;
FOR #i := 0 TO 7 DO
    IF #b[#i] THEN
        #packed := #packed OR SHL(IN := CONV(Byte, 1), N := #i);
    END_IF;
END_FOR;

5.2 字逻辑: AND/OR/XOR/INV + DECO/ENCO

  • AND / OR / XOR: 位逻辑, 操作数是位串或整型; SCL 里对 Bool 表达式也用同名关键字 (IF #a AND #b THEN), 语义与 C 的 &/|/^ 对齐。
  • INVNOT: 按位取反; SCL 表达式 #y := NOT #x; 对整型同样按位取反。
  • DECO / ENCO: n 选 1 位译码 / 1-of-n 编码 (K → bit K 置 1; 反向求最高/最低置位位号)。批量指示灯/优先编码时比手写循环清晰。

SWAP 的典型场景 (见 10.3 字节序坑):

#swapped := SWAP(IN := #rawWord);   // 大端 <-> 小端 (按字节对换)

6. 模拟量: 27648 的世界与 NORM_X / SCALE_X

6.1 机制: 为什么是 27648

S7-1200 模拟量输入/输出通道的“额定范围“统一映射到整数 0..27648 (单极性) 或 −27648..27648 (双极性)。27648 不是 2 的幂, 它 = 0x6C00, 是西门子历史选择的“满量程标称值“, 让各种量程有公共的整数标尺。类比: 这是一个定点数格式, 小数点位置由通道量程定义, 27648 即 100.0%。

关键分区表 (以 4..20 mA 电流输入为例, 数值已对照官方手册):

数字值物理电流含义
32767> 约 22.81 mA溢出 / 故障态 (如断线诊断值)
27649..3251120..约 22.81 mA过量程 (overrange): 合法但超出标称
0..276484..20 mA额定范围 (normal range): 正常工作区
−6912..−10..4 mA欠量程 (underrange): 0 mA 恰对应 −6912
≤ −32768断线/短路故障态, 配合通道诊断

注意: 若通道组态为 0..20 mA, 则 0 mA 对应 0 而非 −6912; 上表仅适用于组态为 4..20 mA 时。CPU 本体自带的 AI (0..10 V) 分辨率为 10 位, 信号板/信号模块 (SB/SM 1231 等) 可到 12~16 位 (以模块手册为准)。输出侧对称: AQ 额定 0..27648, 过量程到 32511。

6.2 NORM_X + SCALE_X: 标准两段式

思路与 Python 手写 x/(hi-lo)lo + x*(hi-lo) 相同: 先把任意量纲归一到 0.0..1.0, 再定标到工程单位。

// FB "FB_Ai4to20" 接口:
//   Input:  raw        : Int        // 来自 %IW (Word 读入后 CONV 成 Int, 或直接 Int)
//          eMin, eMax : Real       // 工程量程, 例: 0.0 ~ 100.0 (单位自定)
//   Output: value      : Real       // 工程值
//          quality     : Int        // 0=好 1=过量程 2=欠量程 3=故障(溢出)
//          valid       : Bool       // quality=0 时 TRUE
//   Static: norm01     : Real
// 代码区:
// 第一步: 判质量。参考 6.1 表 (4..20 mA 组态)
IF #raw >= 32767 OR #raw <= -32768 THEN
    #quality := 3;                    // 溢出/断线区, 数值不可信
ELSIF #raw > 32511 THEN
    #quality := 1;                    // 过量程上限之外 (32512..32766)
ELSIF #raw > 27648 THEN
    #quality := 1;                    // 过量程: 合法, 可选择是否使用
ELSIF #raw < -6912 THEN
    #quality := 2;                    // 深度欠量程
ELSIF #raw < 0 THEN
    #quality := 2;                    // 欠量程 (4 mA 以下)
ELSE
    #quality := 0;
END_IF;

// 第二步: NORM_X 把 4..20 mA 的额定区间 (0..27648) 归一到 0.0..1.0
// MIN/MAX 必须与 VALUE 同型 (这里 Int); 输出建议 Real
NORM_X(MIN := 0, VALUE := #raw, MAX := 27648, OUT => #norm01);

// 第三步: SCALE_X 把 0.0..1.0 定标到工程量程
// 注意 SCALE_X 的 VALUE 期望 0.0..1.0, MIN/MAX 是工程单位
SCALE_X(MIN := #eMin, VALUE := #norm01, MAX := #eMax, OUT => #value);

// 第四步: 对带"质量非好"的值做限幅与置无效
IF #quality <> 0 THEN
    // 钳到边界, 避免下游拿到外推值; 断线区保持上一拍工程值也可以是另一种策略
    #value := LIMIT(MN := #eMin, IN := #value, MX := #eMax);
END_IF;
#valid := (#quality = 0);

语义细节 (容易踩):

  • NORM_X(MIN, VALUE, MAX) 允许 MIN > MAX (反向换算), SCALE_X 同理; 但两段必须方向一致, 否则换算翻转。
  • NORM_X/SCALE_X 对区间外输入不报错也不夹紧, 只会线性外推 —— 所以质量判断必须自己做, 这是与“带饱和的库函数“的本质区别。
  • 若模块组态为 0..20 mA 而现场仪表是 4..20 mA (活零点), 把 NORM_X 的 MIN 改为 5529 (4 mA 在 0..20 mA 标尺上的数字值, 27648 × 0.2 ≈ 5529), MAX 仍 27648, 即可让 4 mA 对应 0%。
  • 输出侧同理: 工程值 → NORM_X → SCALE_X 到 0..27648 → CONV 到 Word → %QW。

6.3 滤波与平滑

一阶低通 (PT1) 滤波、滑动平均、以及“坏点剔除 + 保持“的质量机, 在练习章里会以独立练习完整实现 (y += k*(x-y), k 由扫描周期与时间常数折算), 本章不重复展开。要点预告: S7-1200 没有内建的模拟量滤波指令 (部分 SM 模块有硬件滤波, 组态里开), 软件滤波必须自己考虑采样周期不等间隔的问题 (scan cycle 抖动), 所以滤波器要挂循环中断 OB 而不是 OB1。

7. PID: PID_Compact 完整走查

7.1 定位与版本

适用说明
PID_Compact通用过程量 (压力/流量/液位/速度)V1.x 遗留; V2.x 需 S7-1200 固件 V2.0 以上 (待核实: 具体最低固件以帮助 Requirements 为准); 新项目直接用 V2.x
PID_Temp温度专用S7-1200 需固件 V4.0 + STEP 7 V13 SP1 引入; 支持加热+制冷双输出、级联; V2.0 起支持纯制冷

PID_Compact 与 PID_Temp 都是库 FB, 拖入即建实例 DB; 所有组态 (量程、输出限幅、采样时间、PID 参数) 都存在实例 DB 的 Config 结构里, 可在 TIA 的组态界面 (工艺对象组态窗口) 里填, 也可在 SCL 里运行期改 —— 但 Config 属于保持性数据, 下载/上载会覆盖, 自整定结果也写在实例 DB 中。

7.2 从组态到 SCL 调用 (完整流程)

  1. 拖 PID_Compact (Extended > PID control) 到 OB1 的 SCL 代码区, 选 single instance, 命名 PID_DB
  2. 打开组态界面 (双击调用处或项目树 “Technology objects” 下的对应项):
    • Controller basic settings: Input type 选 Input (工程单位 REAL, 推荐: 你自己先做 NORM_X/SCALE_X) 或 Input_PER (直接接 Word 原始值, 块内自动换算); 标定 PV 上下限; Output 类型同理 (Output / Output_PER / Output_PWM)。
    • Advanced settings: 输出限幅、手动值限幅、采样时间 CycleTime (默认由块估计, 见 7.4)。
  3. 在循环中断 OB (如 OB30, 100 ms) 中调用它 —— PID 的采样时间假设“每次调用间隔恒定“, 放 OB1 会被扫描周期抖动破坏。
// OB30 (100 ms 循环中断) 中的调用 —— 变量在 OB30 的接口/全局 DB 中:
//   "PID_DB"       : PID_Compact 的实例 DB
//   "DB_Analog".PV : Real   // 6.2 换算好的工程值
//   "DB_Analog".SP : Real   // 设定值
//   "DB_PID".heat  : Real   // 输出 0..100 (%)

"PID_DB"(Setpoint      := "DB_Analog".SP,
         Input         := "DB_Analog".PV,
         // 手动模式接管 (见 7.5):
         ManualEnable  := "DB_PID".manualMode,
         ManualValue   := "DB_PID".manualOut,
         // 复位/模式切换 (上升沿有效):
         Reset         := "DB_PID".reset,
         ModeActivate  := "DB_PID".modeAct);

"DB_PID".heat    := "PID_DB".Output;      // 0..100.0
"DB_PID".state   := "PID_DB".State;       // 控制器状态 (Int)
"DB_PID".err     := "PID_DB".Error;
"DB_PID".errBits := "PID_DB".ErrorBits;   // DWord, 见下

接口要点 (V2.x):

  • InputInput_PER 二选一 (组态决定哪个生效); Output / Output_PER / Output_PWM 同理。
  • Reset 上升沿: 控制器回到未激活状态, 输出置 0; 再由 ModeActivate 上升沿切换模式。
  • ModeActivate 上升沿: 自动/手动模式切换的“确认键“。上电或 Reset 后控制器处于未激活态, 必须给一次 ModeActivate 边沿才会按当前 ManualEnable 进入自动或手动。
  • State (Int) 指示当前所处阶段 (未激活/预整定/精确整定/自动/手动等), 取值表见在线帮助 (待核实: 各值枚举以帮助为准)。
  • Error/ErrorBits: ErrorBits 是 DWord 位掩码; 已核实的典型值 16#00000800 = 采样时间错误 (PLCSIM 仿真最常见)。

7.3 自整定 (auto-tuning) 流程

  1. 组态好 Input/Output 标定与输出限幅, 接好真实 (或高保真模拟的) 过程。
  2. 控制器先处于自动模式且过程量稳定在设定值附近 (预整定要求过程处于稳态, 否则整定结果差)。
  3. 在调试界面 (工艺对象 > Commissioning) 点 Start pre-tuning + fine-tuning (或分步), 或在 SCL 里对实例 DB 的整定控制位写上升沿 (组态界面里可查到对应位名; 待核实: 位名称随版本变化)。
  4. 整定完成后新的 Gain/Ti/Td 写回实例 DB 的 sPid_Calc 结构, 并提示“是否将参数复制回组态“ (不复制则只存在于实例 DB, 覆盖下载会丢)。

7.4 PLCSIM 仿真的限制 (已核实)

官方明确: PID_Compact V2.x 在一代 S7-1200 上不支持 PLCSIM 仿真; 只有 S7-1500 与 S7-1200 G2 可仿真。此外, 仿真中 PLCSIM 的循环时钟抖动会触发 ErrorBits = 16#00000800 (采样时间错误) 并中止进行中的整定, 官方给出的对策是在实例 DB 中关闭相关检测:

// PID_Compact V2.x (S7-1500 / 1200 G2 仿真时), 在线/离线访问实例 DB:
"PID_DB".CycleTime.EnEstimation := FALSE;   // 关闭采样时间自动估计
"PID_DB".CycleTime.EnMonitoring := FALSE;   // 关闭采样时间监视

V1.x 的对应做法 (官方 V1 页面): 关闭 sb_EnCyclEstimation / sb_EnCyclMonitoring, 手动给 sPid_Calc.r_Cycle 赋采样时间。两代成员名不同, 以你版本的帮助为准。

7.5 手动模式接管示例

运维常见需求: 自动到一定条件切手动开度。注意 ManualEnable 是电平, ModeActivate 是边沿, 组合逻辑如下:

// "DB_PID" 接口: manualMode, manualOut (0..100), reset, modeAct : Bool
// 上升沿检测:
"DB_PID".modeAct := "DB_PID".manualMode AND NOT "DB_PID".manualMode_last;

IF "DB_PID".manualMode THEN
    // 手动: 输出直接跟踪 manualOut (块内部做无扰切换准备)
    ;
ELSE
    // 自动: 正常控制
    ;
END_IF;

"PID_DB"(Setpoint     := "DB_Analog".SP,
         Input        := "DB_Analog".PV,
         ManualEnable := "DB_PID".manualMode,
         ManualValue  := LIMIT(MN := 0.0, IN := "DB_PID".manualOut, MX := 100.0),
         Reset        := "DB_PID".reset,
         ModeActivate := "DB_PID".modeAct);

"DB_PID".manualMode_last := "DB_PID".manualMode;

PID_Temp 与 PID_Compact 的调用形态几乎一致, 差异在: 加热/制冷双输出 (OutputHeat/OutputCool), 加热制冷切换的死区/滞后组态, 以及针对温度过程 (大滞后) 的整定策略。做温控直接选 PID_Temp, 不要拿 PID_Compact 硬凑。

8. 高速计数器 HSC

8.1 组态思路

S7-1200 的 HSC 是 CPU 内建硬件, 不是指令: 在 Device configuration > CPU 属性 > High speed counters (HSC) 里启用并组态。注意 HSC 与 PTO/PWM 共用物理输出/输入资源 (脉冲发生器占用高速输入作反馈等, 具体复用规则以 CPU 手册为准), 组态冲突 TIA 会报错。

组态项: 功能选择 (counting 计数 / frequency 频率测量 / period 周期测量, 依 HSC 编号可用性不同)、计数类型 (单相加/减、加/减双脉冲、A/B 正交 ×1/×2/×4)、初始/比较值、以及中断事件 (如 CV = RV 比较匹配触发硬件中断 OB)。组态完成后, 当前计数值被映射到一个 32 位输入映像区地址 (I 区, 如 %ID1000), 每个扫描周期由操作系统自动刷新 —— 这是最重要的一个事实: 读 HSC 不需要任何指令, 直接读 %ID

8.2 SCL 读取与控制

// 周期性读取当前值 (放在循环中断 OB 中):
"DB_Speed".count := %ID1000;      // 直接读输入映像, DInt

// CTRL_HSC (Technology > Counting): 运行期改变计数参数 (方向/写新计数 CV/写新参考值 RV)
// 典型用途: 回零时写 0, 或改比较值触发下一档中断
// "HSC_Ctrl_DB" : CTRL_HSC 实例, 组态时已关联具体 HSC 硬件对象
IF "DB_Hsc".doWriteZero THEN
    "HSC_Ctrl_DB".CV  := TRUE;    // 允许写新当前值
    "HSC_Ctrl_DB".NewCV := 0;     // 新计数初值 (成员名待核实, 以帮助为准)
    "HSC_Ctrl_DB".REQ  := TRUE;
END_IF;
// REQ 完成后由 Busy=FALSE 收尾, 具体成员名以在线帮助的 CTRL_HSC 参数表为准

上面的 CTRL_HSC 成员名在不同 TIA 版本 (legacy CTRL_HSC 与较新的 CTRL_HSC_EXT) 有差异, 使用前以 F1 参数表核对; 但“读 %ID 不需要指令“这一点在所有版本都成立。

8.3 编码器测速示例思路

两种做法:

  1. 频率测量模式: HSC 组态为 frequency measurement, %ID 直接给出 Hz (每秒脉冲数)。换算: rpm := freq_Hz * 60.0 / pulses_per_rev(pulses_per_rev = 编码器线数 × 倍频)。适合中高速; 低速时刷新周期内脉冲太少导致量化误差大。
  2. 周期测量模式 (T 法, 低速优): HSC 组态为 period measurement, 测量固定脉冲数的时间, %ID 给出周期值, rpm ∝ 1/period
  3. M 法 (通用): 计数模式 + 循环中断固定周期采样 %ID 差分: rpm := DINT_TO_REAL(count_now - count_last) * 60.0 / (pulses_per_rev * sample_s), 差分要用无符号回绕安全的算法 (DInt 减法天然模 2^32 回绕, 直接相减再解释即可, 与 C 的无符号减法同理)。

选型口诀: 高速用 M 法 (频率模式), 低速用 T 法 (周期模式), 全范围用 M/T 切换 —— 与编码器测速的所有嵌入式方案同构。

9. 运动控制 (PTO 基本定位)

S7-1200 的运动控制 = PTO (Pulse Train Output) 脉冲输出 + 工艺对象 (Technology Object, TO) + PLCopen 指令。要求: S7-1200 固件 V4.0 起支持 (对应 TIA V13 时代引入), TO 本身有版本 (V4.0~V7.x, 在指令任务卡 Technology > Motion control 下选版本); 一代 CPU 各型号集成 2~4 路 PTO (1215C 为 4 路, 其余多为 2 路, 以样本手册为准)。步进/伺服驱动器接脉冲+方向, 位置闭环在驱动器内, PLC 侧是“脉冲数即位置“的半闭环。

三步走:

  1. 组态 TO: 项目树 Add new technology object > Positioning axis (TO_PositioningAxis), 组态 PTO 脉冲源、方向输出、加减速、回零方式、软限位、跟随误差。
  2. MC_Power 使能轴 (必须每周期调用):
// "Axis1_Power_DB" 等实例 DB; 轴与 TO 的关联在插入指令时组态 (不是形参)
"Axis1_Power_DB"(Enable := "DB_Mc".powerOn,
                 StopMode := FALSE);          // FALSE=受控停止
  1. MC_MoveJog (点动) / MC_MoveRelative (相对定位):
// 点动: JogForward 按住正转
"Axis1_Jog_DB"(JogForward := "DB_Mc".jogF,
               JogBackward := "DB_Mc".jogB,
               Velocity    := 100.0);        // 单位由 TO 组态决定 (如 mm/s 或 rpm)

// 相对移动: Execute 上升沿启动, 走 500.0 个单位
"Axis1_Rel_DB"(Execute  := "DB_Mc".startRel,
               Distance := 500.0,
               Velocity := 200.0);

// 读取轴状态
"DB_Mc".busy     := "Axis1_Rel_DB".Busy;
"DB_Mc".done     := "Axis1_Rel_DB".Done;
"DB_Mc".axisPos  := "Axis1".ActualPosition;  // TO 的动态数据 (符号访问)

完整指令族一览: MC_Power(使能), MC_Reset(故障复位), MC_Home(回零), MC_Halt(暂停), MC_MoveJog(点动), MC_MoveRelative(相对), MC_MoveAbsolute(绝对), MC_MoveVelocity(速度控制), MC_GroupInterrupt/MC_GroupContinue(组中断/恢复, 版本相关, 待核实 1200 可用性)。共同编程规则: MC_Power/MC_Reset 等“管理类“每周期调用; “执行类”(Move/Home) 用 Execute 上升沿触发, Busy/Done/CommandAborted/Error 报状态 —— 这是一台显式状态机, 和 C 里手写 motion state machine 的差别在于状态机被标准化了。

10. 通信

10.0 PLCSIM 仿真支持度总表 (先看这个再选方案)

功能标准 PLCSIM (V16~V21)说明
TSEND_C/TRCV_C (OUC)部分仅经内部 Softbus 在本机内互通, 无真实 TCP/IP; 与外部 PC 程序通信不可仿真
MB_CLIENT / MB_SERVER (Modbus TCP)不支持需真实 CPU 或 PLCSIM Advanced (Advanced V8+ 才支持 1200 G2, 一代 1200 不支持)
PROFINET IO controller不支持真实 IOPLCSIM 内无 PROFINET IO 设备可挂 (仿真 PN 设备需第三方工具)
OPC UA Server不仿真官方对比表明确无 OPC UA
Web server官方对比表明确

结论: 通信类功能的学习与验证必须上真实 CPU; PLCSIM 只能验证指令调用逻辑与状态机 (还常因时钟问题误报错误)。

10.1 TSEND_C / TRCV_C (开放用户通信, 与 socket 类比)

OUC = CPU 集成网口上的原生 TCP/UDP 套接字通信。类比表:

socket APITIA 对应
socket() + connect()TCON (或 TSEND_C/TRCV_C 内建连接管理)
send()TSEND / TSEND_C
recv()TRCV / TRCV_C
close()TDISCON
bind()+listen()TCON 组态为被动 (服务端) 模式

TSEND_C = “建连 + 发送” 的合体 (带 CONT 参数控制连接保持), TRCV_C = “建连 + 接收” 的合体。连接参数 (对端 IP/port、连接 ID、TCP 还是 UDP) 不在参数表里, 而在指令实例的属性面板 (Configuration > Connection data) 中组态, 存在一个连接 DB (如 TCON_IP_v4) 里。

最小可用示例 —— 每 1 秒发送 8 字节, 异步接收:

// OB1 / 循环中断中调用。变量:
//   "DB_OUC".sendReq  : Bool   // TON 产生的 1s 脉冲
//   "DB_OUC".txBuf    : Array[0..7] of Byte
//   "DB_OUC".rxBuf    : Array[0..63] of Byte
//   "DB_OUC".rcvdLen  : UInt
//   实例: "TS_CDB" (TSEND_C), "TR_CDB" (TRCV_C)

// 发送: REQ 上升沿触发; CONT=TRUE 保持连接
"TS_CDB"(REQ  := "DB_OUC".sendReq,
          CONT := TRUE,
          DATA := "DB_OUC".txBuf,
          LEN  := 8);

// 接收: EN_R=TRUE 常开接收; NDR=TRUE 表示收到新数据
"TR_CDB"(EN_R := TRUE,
          CONT := TRUE,
          DATA := "DB_OUC".rxBuf,
          LEN  := 0,                 // 0 = 接收最多到 DATA 尺寸
          NDR  => "DB_OUC". newData,
          RCVD_LEN => "DB_OUC".rcvdLen,
          ERROR => "DB_OUC".rxErr,
          STATUS => "DB_OUC".rxStatus);

要点:

  • 这些指令是异步的: REQ 发出后要靠 BUSY/DONE/ERROR/STATUS 状态机轮询, 不是阻塞调用 —— 类似非阻塞 socket + 事件标志, 而不是 Python socket.send() 一把梭。
  • STATUS (Word) 在 ERROR=FALSE 时是通知码 (如 1=状态切换), ERROR=TRUE 时是错误码, 必须区分读。
  • 字节序: S7 内存按大端存 16/32 位值, TCP 线上原样发送, 所以 Python 端用 struct.unpack('>h', ...) 即可直接解析, 无需翻转。

配套 Python 测试端 (真机调试用):

# echo 服务器: 与 PLC 的 TSEND_C/TRCV_C 对测 (PLC 侧组态对端 IP=本机, port=2000)
import socket
srv = socket.socket(); srv.bind(('0.0.0.0', 2000)); srv.listen(1)
conn, _ = srv.accept()
while True:
    data = conn.recv(64)
    if not data: break
    print(data.hex())
    conn.send(data)   # 回发, PLC 的 TRCV_C 会收到

10.2 Modbus TCP (MB_CLIENT / MB_SERVER)

S7-1200 既是 Modbus TCP client (MB_CLIENT) 也可作 server (MB_SERVER)。与 OUC 的区别: Modbus TCP 是带标准寄存器模型的应用层协议, 适合与第三方设备 (变频器、仪表、网关) 交互; 同网口可共存。

布局硬性要求 (寄存器 ↔ DB 映射):

  • 数据缓冲 (MB_CLIENT.DATA_PTR / MB_SERVER.MB_HOLD_REG) 必须指向标准 (非优化) DB 或 M 存储区 —— 优化 DB 会报错 (典型 STATUS 8383/8384 类错误)。做法: 新建 DB 时在属性里取消勾选 “Optimized block access”。
  • Modbus 寄存器 = 16 位 Word; DB 里用 Array[0..N] of Word (或连续的 Word/DWord 结构体) 排布, 不要在中间插 Bool/Byte 破坏连续性; Bool 量凑成 16 的倍数打包进 Word。
  • 字节序坑: S7 的 Word 本身是大端 (与 Modbus 线序一致), 单寄存器无问题; 但 32 位值 (DInt/Real) 占两个寄存器, 很多第三方设备 (尤其部分国产表/网关) 是 little-endian 字序 (低字在前), 这时要么设备侧改字序设置, 要么 PLC 侧用 SWAP + 拼接手动换:
// Input: regLo, regHi : Word (两个保持寄存器, 字序存疑时)
// Output: valBE, valLE : DInt
// 大端字序 (高字在前 = regHi 是高 16 位):
#valBE := CONV(DInt, SHL(IN := CONV(DWord, #regHi), N := 16) OR CONV(DWord, #regLo));
// 小端字序 (低字在前): 交换两个字即可
#valLE := CONV(DInt, SHL(IN := CONV(DWord, #regLo), N := 16) OR CONV(DWord, #regHi));
// Real 场景: 同样拼出 DWord 后用 SCAT/位重解释, 或标准 DB 中直接以 Real 声明并整体 SWAP

MB_CLIENT 最小示例 (周期读 4 个保持寄存器, 40001 起):

// 变量:
//   "MBMap" (标准 DB): holdRegs : Array[0..3] of Word
//   "DB_Mb".req : Bool (定时脉冲), .done, .busy, .err : Bool; .status : Word
//   实例 "MB_CL" : MB_CLIENT
//   连接参数在实例属性中组态: 对端 IP、端口 502、连接 ID、MB_UNIT_ID

"MB_CL"(REQ       := "DB_Mb".req,
        MB_ADDR   := 1,           // Modbus 站地址 (unit id), 0..247
        MODE      := 0,           // 0=读 1=写
        DATA_ADDR := 40001,       // 保持寄存器起始 (Modbus 习惯地址; 40001=第一个 4x 寄存器)
        DATA_LEN  := 4,           // 读 4 个寄存器
        DATA_PTR  := "MBMap".holdRegs,
        DONE      => "DB_Mb".done,
        BUSY      => "DB_Mb".busy,
        ERROR     => "DB_Mb".err,
        STATUS    => "DB_Mb".status);

MB_SERVER 最小用法: 组态端口 (默认 502), MB_HOLD_REG 指向标准 DB 的 Word 数组, 客户端 (如 PC 上 pymodbus) 读 40001 起的寄存器即映射到数组 [0]:

"MB_SRV"(DISCONNECT := FALSE,
         MB_HOLD_REG := "MBMap".holdRegs);   // 供客户端读写的寄存器区

注意: MB_SERVER 在 1200 上主要支持保持寄存器 (读写 4x 区); 线圈/离散量 I/O 的支持情况随固件版本演进, 用前查帮助 (待核实)。地址习惯: DATA_ADDR 用 Modbus 习惯地址 (4xxxx), 而非 0 基协议地址 —— 这是与 pymodbus (read_holding_registers(0, ...) 用 0 基) 对接时最常见的差一错误源。

10.3 PROFINET IO controller (概念)

S7-1200 的集成网口默认可充当 PROFINET IO controller: 在 Devices & Networks > Network view 里拖入 IO 设备 (西门子 ET 200SP 远程 IO、带 GSD 文件的第三方设备, 或另一台组态为智能设备 (IO device) 的 CPU), 用网线/拓扑连接并分配 IO 设备名与 IP。之后不需要写任何通信代码: 远程 IO 的输入/输出被操作系统周期性 (PROFINET RT, 默认 1~2 ms 发送时钟, 组态可调) 映射进本地过程映像, SCL 里就像访问本地 I/O 一样 %I0.0 / 符号访问。类比: PROFINET GSD+组态 ≈ Linux device tree + 内核驱动, 你写的是应用层。诊断: 断线等产生机架/站故障 OB86 类事件 (S7-1200 上对应拔插/站故障事件, 优先级 6), 或在组态里为子模块设硬件中断。注意一代 S7-1200 只支持 RT, 不支持 IRT (G2 才支持, 见 02 章硬件对比)。

10.4 OPC UA Server (组态 + 客户端读取)

固件要求 (已核实): 一代 S7-1200 固件 V4.4 起内置 OPC UA Server (V4.5 增加方法调用与结构化数据访问); G2 亦内置 (需较新固件, 第三方资料称 v4.1 起, 待核实)。注意区别于 Web server: OPC UA 是面向 SCADA/IT 的数据接口, 不是网页。

组态步骤:

  1. CPU 属性 > OPC UA Server: 勾选激活; 选地址 (默认 4840); 配 Server interface (新增一个 server interface 块, 把要暴露的变量拖进去, 可分组/设读写权限); 安全策略按需 (最低 None 无加密, 生产建议至少 Basic256Sha256 签名加密并管好证书)。
  2. 编译下载。客户端测试 (Python, asyncua 或 UaExpert 图形客户端):
# pip install asyncua
from asyncua.sync import Client
with Client("opc.tcp://192.168.0.1:4840") as c:
    v = c.get_node("ns=3;s=\"DB_Opc\".\"speed\"")   # 命名空间与节点名以 TIA 生成的地址为准
    print(v.read_value())

限制: 一代 1200 的 UA Server 以读/写变量为主 (V4.5 前无方法与结构化类型); 订阅 (monitored items) 数量与采样率受 CPU 性能限制, 大量高频数据建议走别的路 (如 OUC)。

10.5 内置 Web server

CPU 属性 > Web server: 勾选激活, 配置用户与权限 (只读诊断用户/管理员), 浏览器访问 http://<cpu-ip>。能看到的标准页面 (以 CPU 手册 Web server 章为准):

  • 概览/标识页: CPU 状态、运行模式、IP、固件版本;
  • 诊断缓冲 (diagnostic buffer): 与 TIA 在线看到的一致, 现场 laptop-less 排障第一入口;
  • 模块状态、连接表、PROFINET 拓扑;
  • Data log 页面: 把 SCL 数据日志 (Data log 指令写入的 CSV) 直接下载回 Excel 分析;
  • 用户管理下可远程改 CPU 运行模式 (需相应权限)。

自定义页面: 用西门子 Web2PLS 工具 (SIOS 68011496 应用示例) 把 HTML+JS 处理成 WWW 数据块下载进 CPU, 页面通过 AWP 命令读写 PLC 变量 —— 相当于给 CPU 挂一个只读为主的轻量 dashboard。注意 PLCSIM 无 Web server, 须真机调试。

11. 库工程化: 项目库、全局库、第三方库

11.1 两级库与版本控制 (类比 npm + git)

概念TIA 机制类比
项目库 (Project library)随项目文件保存的私有库项目内 vendor/ 目录或 private registry
全局库 (Global library)独立 .al16/.al17/.al18/... 文件 (扩展名随 TIA 版本), 跨项目共享npm registry / git 子模块
库类型 (Library type)库中的 FB/FC/UDT/全局 DB 常量等, 带版本号, 可随修改递增版本、保留历史版本npm 包的 version + git tag
库副本 (Library copy)从库拖到项目里的一次使用实例; 项目内修改不回写库, 除非“作为类型上传回库“node_modules 里的副本 vs registry 里的包
类型版本调用项目中的调用可钉在旧版本, 或“更新调用“升到新版本package.json 钉版本 vs npm update

工程化要点:

  • 把稳定代码沉到库, 把项目差异留在调用参数: 库类型接口一旦被别的项目引用, 改接口必须升版本而不是原地改语义 —— 和 semver 的破坏性变更同理。
  • 版本更新链路: 修改项目中的库副本 → 右键“上传到库“并选升版本 (如 0.1 → 0.2) → 其他项目“更新调用“。全局库文件本身用 git 管理 (.al18 是二进制, diff 无意义, 但可追踪版本节点); 也有团队用“库项目 (library project)+ 库版本发行“工作流, 官方称 Type/version 一致性检查会在下载时校验版本漂移。
  • 全局库文件向后兼容性差: 高版本 TIA 可打开低版本库 (升级), 反向不行 —— 与 TIA 项目一样只能往上走。

11.2 导出/导入与第三方库

  • 全局库导出/导入即拷贝 .al1x 文件; 老项目块可用 “External source” (导出 .scl/.awl 源) 交换纯代码, UDT/FB/FC 均可 (类似把 C 源码 tar 出来)。
  • 西门子官方库: LBC (Library of General Functions, 通用函数库) 自带于 TIA, 含边沿检测、去抖、限幅、看门狗等通用封装, 新手可直接读它的 SCL 源码学风格。
  • OSCAT (Open Source Community for Automation Technology, oscat.de) 是工业自动化领域最大的开源块库 (数百个 FB/FC: 数学、控制、字符串、网络)。它原生面向 CODESYS, 社区有人移植到 TIA。移植注意:
    1. OSCAT 大量使用 ANY/POINTER/引用与内存映射, 而 S7-1200 接口不支持 POINTER/ANY/REF_TO (1500 才有) —— 这些块必须改写为 VARIANT 或显式数组参数才能在 1200 用;
    2. 数组变址下限: OSCAT 常按 0 基, TIA 数组上下限任意, 逐块核对;
    3. STRING 处理 (前 2 字节头 + 长度语义) 与 CODESYS 不同, 字符串块几乎都要手改;
    4. 优化的访问模型差异: OSCAT 里若用了绝对地址/AT 覆盖, 在优化块上不成立。 结论: OSCAT 在 1200 上“当算法参考实现“价值最大 (读伪代码级 SCL/ST 源码), 整库搬用前逐块审查。

12. 诊断类指令与系统功能一览

12.1 GET_ERROR / GET_ERR_ID

S7-1200 没有 OB121/OB122 (编程错误/I/O 访问错误组织块), 替代机制是块内局部错误处理:

// 在任意块中, 手动读最近一次块内错误 (在块内放置 GET_ERROR/GET_ERR_ID 指令即启用该块的局部错误处理——这不是块属性开关):
//   "DB_Diag".err : ErrorStruct (系统预定义结构体类型)
IF "DB_Diag".checkErr THEN
    GET_ERROR(ERR => "DB_Diag".err);        // 详细: 错误 ID + 块/偏移 + 区域信息
    GET_ERR_ID(ID => "DB_Diag".errId);      // 轻量: 只要 16 位错误码
END_IF;

语义: 这两个指令读走当前块最近发生的编程/访问错误 (读后即清), 相当于 errno + strerror 的组合; 不启用局部处理时, 错误写诊断缓冲且 CPU 保持 RUN。用它做“防御性日志点“比依赖 CPU STOP 更符合 24×7 连续运行的需求。

12.2 RD_SYS_T 与时间类

// 读系统时间 (DTL, 12 字节: 1970-01-01 ~ 2262):
RD_SYS_T(OUT => "DB_Clock".now);
// 写系统时间 (需权限, 慎用): WR_SYS_T(IN := "DB_Clock".set);
// 读本地时间 (按 CPU 时区组态偏移): RD_LOC_T(OUT => "DB_Clock".local);
// 计时基准: 注意 TIME_TCK() 是 S7-1500 专属指令, 在 1200 上编译报错!
// S7-1200 测耗时用 RUNTIME() (返回 LReal 秒; 内部计数器溢出时返回 <= 0, 该次读数应忽略):
"DB_Clock".elapsed := RUNTIME();

TIME 类型是 32 位有符号毫秒, 范围 ±24d20h31m23.647s; S7-1200 不支持 LTIME (1500 专属)。

12.3 常用系统功能一览表

指令类别用途备注
GET_ERROR / GET_ERR_ID诊断读块内错误 (1200 的 OB121 替代)见 12.1
RD_SYS_T / WR_SYS_T / RD_LOC_T时间系统时间读写 (DTL)WR 需相应权限
RUNTIME时间耗时测量, 返回 LReal 秒 (1200 可用)内部计数器溢出时返回 <= 0 应忽略; 注意 TIME_TCK() 为 S7-1500 专属, 1200 编译报错
RE_TRIGR看门狗重启当前扫描的监视计时当前周期须 < 10×最大扫描时间 (默认 150 ms, 1..6000 ms 可配)
STP控制把 CPU 切到 STOP调试/致命错误兜底
ATTACH / DETACH中断运行期把事件 (如硬件中断) 动态挂/摘到指定 OB比静态组态灵活
RDREC / WRREC诊断读写模块数据记录 (细粒度模块诊断/组态)参数复杂, 配合模块手册
RALRM中断在硬件中断 OB 里取中断详情1200 支持 (待核实: 依固件版本)
TSEND/TRCV/TCON/TDISCONOUC底层 socket 族见 10.1
TSEND_C/TRCV_COUC带连接管理的封装见 10.1
MB_CLIENT/MB_SERVERModbusModbus TCP 主/从见 10.2
CTRL_HSCHSC运行期改 HSC 参数读计数值用 %ID 直读
PID_Compact / PID_TempPID见第 7 节
Data log (LOG_GEN 等)数据记录写 CSV 到 CPU/存储卡, 经 Web server 下载PLCSIM 不仿真 Web
RTM / 运行小时计维护操作系统运行时计数器1200 支持情况以 CPU 属性/帮助为准 (待核实)

诊断缓冲的软件侧入口: TIA 在线诊断对话框; Web server 诊断页 (10.5); 或 GetSystemError 类库块 (待核实名称与 1200 可用性) —— 一般前两者已够用。

13. 收尾: 用库思维写 SCL

把本章所有内容压缩成可执行的工程习惯:

  1. 先搜库再写循环: LBC、PID_Compact、MC_xxx、MB_CLIENT 都是“官方 pip 包“; 自研一个通用函数前先 F1 搜指令卡与 LBC。90% 的“我写个工具 FB“在西门子生态里已有带测试的版本。
  2. 每个 FB 都按“库类型“标准写: 参数全命名化、接口一次定稳、语义内聚 (一个 FB 一件事)、不偷写全局 DB —— 今天的项目块就是明天的库类型。接口演进靠版本号, 不靠原地改语义。
  3. 状态机思维而不是阻塞思维: TSEND_C 的 DONE/BUSY/ERROR、MC 指令的 Busy/Done、PID 的 State, 全是显式状态机。写 SCL 时自己的功能也按“输入边沿触发 + 状态字 + 完成位“的模式封装, 调用者用起来就和官方库一致。
  4. 错误处理三件套: ENO/ERROR+STATUS 结构贯穿所有库块, 自己的库块也输出同构的 (done, busy, error, status) 四元组; 1200 上没有异常机制, “留痕不中断“是唯一正解, GET_ERROR 兜底。
  5. 数据接口要“布局自觉“: 模拟量按 0..27648 标尺 + 质量位; 通信缓冲用标准 DB + Word 对齐 + 字节序显式约定 (SWAP 在手); 跨界数据结构写成 UDT 进库, 别散落各 DB。
  6. 能组态的不编程: PID、HSC、运动控制、PROFINET 的参数都在组态/实例 DB 里, 代码只做“调用 + 状态机 + 边沿“; 组态参数变化要纳入版本管理 (项目文件即配置)。
  7. PLCSIM 是逻辑仿真器, 不是控制器仿真器: 时钟敏感 (PID) 与网络功能 (OUC/Modbus/OPC UA/Web) 不仿真或行为失真, 上真机前把这两类验证单列。
  8. F1 是唯一权威: 本章所有事实均可在 TIA 在线帮助 / S7-1200 manual collection 对应页复核; 标注 (待核实) 之处, 使用前请以你手上版本的 F1 为准。

参考来源:

  • S7-1200 手册集合 (TIA Portal V20 在线帮助): https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20
  • SCL 指令参考 (S7-1200/S7-1500, V20): https://docs.tia.siemens.cloud/r/en-us/v20/scl-s7-1200-s7-1500
  • PID_Compact V2.x PLCSIM 仿真: https://docs.tia.siemens.cloud/r/en-us/v20/using-pid_compact-s7-1200-s7-1500/pid_compact-as-of-v2-s7-1200-s7-1500/simulating-pid_compact-as-of-v2-with-plcsim-s7-1200-s7-1500
  • PLCSIM 产品对比 (TCP/Web/OPC UA/Trace): https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/cpu-function-manuals/s7-plcsim-advanced/product-overview/s7-plcsim-products
  • MB_CLIENT 指令 (S7-1200 手册集合): https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20/communication-processor-and-modbus-tcp/modbus-communication/modbus-tcp/modbus-tcp-instructions/mb_client-communicate-using-profinet-as-modbus-tcp-client-instruction
  • CTRL_HSC 指令 (S7-1200 手册集合): https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20/technology-instructions/counting-high-speed-counters/legacy-ctrl_hsc-control-high-speed-counter-instruction/ctrl_hsc-control-high-speed-counter
  • S7-1200 运动控制功能手册 (109773400): https://support.industry.siemens.com/cs/attachments/109773400/s71200_motion_control_function_manual_en-US_en-US.pdf
  • 模拟量表示范围 (S7-1200 G2 手册集合, 数值对照): https://docs.tia.siemens.cloud/r/simatic_s7_1200_g2_manual_collection_zhcn_20/technical-specifications/analog-signal-boards-sbs/measurement-ranges-of-the-analog-inputs-for-voltage-and-current
  • OPC UA for S7-1200/1500 (固件 V4.4 要求): https://docs.tia.siemens.cloud/r/simatic_et_200bl_manual_collection_frfr_20/comprehensive-information/communication-function-manuals/communication/opc-ua-communication/what-you-need-to-know-about-opc-ua/opc-ua-for-s7-1200/s7-1500-cpus
  • 用户自定义 Web 页面应用示例 (SIOS 68011496): https://support.industry.siemens.com/cs/document/68011496/
  • Web server 概览 (S7-1200 手册集合): https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_frfr_20/web-server/web-server-overview
  • PID_Temp 多区控制应用例 (109740463): https://cache.industry.siemens.com/dl/files/463/109740463/att_993000/v1/109740463_PidTemp_MultiZone_DOC_V11_en.pdf
  • MB_CLIENT DATA_PTR 标准库要求 (SIOS 91696622): https://support.industry.siemens.com/cs/mdm/91696622
  • 4-20 mA 换算数值解说 (第三方对照): https://industrialmonitordirect.com/blogs/knowledgebase/siemens-s7-1200-analog-input-4-20ma-scaling-explained

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) < epsmath.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->vali < 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/OBPascalCase + 前缀FB_Conveyor, FC_CalcAvg
全局 DBDB_ 前缀DB_IO, DB_Recipe
UDTPascalCaseMotorConfig
实例 DB块名 + _InstFB_Conveyor_Inst
局部变量/Input/OutputcamelCasecmdStart, speedActual
常量(CONST)UPPER_SNAKEST_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。现实可行的组合:

  1. 块级源码入库: 用 TIA 的外部源(external source)功能把 SCL 块导出为 .scl 文本文件(或用 TIA Openness API 批量导出块/PLC 变量为 XML)纳入 git, 代码评审、diff、 blame 都落在这一层; 二进制 .ap 文件也提交(可配 LFS), 但只当“完整快照“。
  2. 变更对比: TIA 自带项目/块比较工具(离线 vs 在线、项目 vs 项目), 用于人工比对两版本差异, 相当于图形化 diff。
  3. 里程碑用库版本管理: 发布给其他项目的块冻结为库版本, 项目里记录“用的哪个库版本“。
  4. 提交纪律: 二进制无法 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:= 与 =赋值 :=, 比较 =, 不等 <>
2Temp 脏数据先写后读; 跨周期记忆放 Static
3数组越界0 基声明 + LOWER/UPPER_BOUND; 1200 越界不 STOP
4优化 DB默认优化; 需绝对地址/AT/PEEK 才用标准 DB
5REAL 相等容差比较; Int/Int 相除是整除
6边沿R_TRIG 实例化, 无条件调用
7条件调用 FB每周期调用, enable 做输入; 不使能时显式清输出
8WHILE 阻塞延时一律 TON; 大任务分片跨周期
9除零预检分母; 错误是 ENO=0 + 返回 0, 不是异常
10字节序大端; SWAP/移位拼装; DWORD_TO_REAL 是位搬运
11STRING定长 n+2 字节; 默认 254; 赋值截断
12中断并发共享量单字原子 + 序号校验; 临界区放高优先级 OB
13TIME 回绕差值法; 优先 TON
14大小写全局不区分大小写, 勿靠大小写区分名字
15巨型 OB1OB1 只调度; FB 封装; 全局只留 I/O
16大 Temp大数组走 DB + InOut 引用
17调试1200 无断点: Watch/Trace/程序状态/诊断 DB
18FC 非纯函数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)或存配方/存储卡
26OB1=main 类比边界启动 OB 先行; 多循环 OB 按序; 语句级可抢占; 不可重入
27整型静默回绕Int 32767 翻负无告警; 计数用 DInt 并设上限
-平台专属易踩指令TIME_TCK/REF/POINTER/ANY/LTIME/断点均为 1500 独占, 1200 不可用

练习项目阶梯: L0~L3 十三题 (S7-1200 + TIA Portal + SCL)

本篇是纯动手篇: 13 道按 L0~L3 递进的练习, 从 20 行的启保停到包含通信与 HMI 的小型产线。所有题目默认在 PLCSIM + Watch table 中即可完成; 受仿真限制的部分 (3.3/3.4/3.5) 单独标注替代方案。0.1、1.1、1.2 给完整答案代码, 其余给完整变量声明 + 带 TODO 的骨架, 动手部分留给你。每题固定七栏: 目标 / 涉及知识点 / IO 与变量定义 / 提示 / 骨架或完整代码 / 验收标准 / 进阶变体。

0. 使用说明与通用约定

环境: TIA Portal V16~V20 (V21 亦适用); CPU 建议 S7-1200 1214C DC/DC/DC (G1/G2 均可, G2 需 V20+); 仿真用 TIA 内置 S7-PLCSIM (菜单 Online > Simulation, 无需额外许可); 新建工程 Trainer06, OB1 语言直接选 SCL, 每题逻辑封装为一个 FB, OB1 只做调用; 每题一个 Watch table, 命名 wt_xx。 代码约定: TIA 的 SCL 编辑器上半是块接口 (声明区), 下半是代码区。本文代码一律按声明区/代码区分开给出, 可照抄。局部变量带 # 前缀, 全局 tag/DB 名用引号。 与真机的两个关键差异:

  • PLCSIM 里 Watch table 可直接 Modify %I 输入 (仿真的核心便利)。真机上 Modify 输入映像会在下个扫描周期被物理输入覆盖 (“站不住”); 要持续固定 I/O 须用 Force (S7-1200 支持 Watch and force table, 官方手册有专章; 注意 Force 激活期间会阻碍下载, 调试完务必停用)。
  • Watch table 刷新是 PG 通信节奏 (约秒级), 看不到单个 scan cycle 内部的中间状态; 要“看见“扫描, 靠扫描计数器或定时器行为反推 — 0.1 就是练这个。

PLCSIM 能力边界速查 (决定哪些题能仿真, 出处见附录 B):

能力标准 PLCSIM说明
SCL 逻辑、IEC 定时器/计数器、数组/字符串、FB/DBL0~L2 全部题目
Watch table 修改 I/Q/M/DB可 (仅限仿真)见上
PUT/GET、开放用户通信 (TSEND_C/TRCV_C)仅 Softbus, 本机内同一台 PC 的实例间可用, 无真实 TCP/IP
Modbus TCP (MB_CLIENT/MB_SERVER)不可 (社区多方一致结论)3.4 给替代方案
Trace 录制不可 (仅 PLCSIM Advanced)用 Watch table + 变量记录替代
PID_Compact V2.x一代 S7-1200 不可; S7-1500 与 S7-1200 G2 可官方手册, 3.3 引用
Web Server / OPC UA不可官方对比表

题目一览: L0: 0.1 起保停+扫描观察 | L1: 1.1 多地启停+急停, 1.2 TON/TOF+振荡器, 1.3 CTU 装箱, 1.4 单按钮启停 | L2: 2.1 滑动平均, 2.2 SCALE_X+滞回, 2.3 协议解析, 2.4 排序+二分 | L3: 3.1 交通灯, 3.2 电机 FB 多实例, 3.3 PID 温控, 3.4 Modbus TCP, 3.5 HMI+配方, 3.6 小型产线。

L0: 建立 scan cycle 直觉 (1 题)

0.1 起保停: 电机自锁, 并观察扫描周期行为

目标: 用 SCL 实现最经典的电机自锁: 按一下启动, 电机运行并“记住“状态; 按停止立即停。再用 Watch table 验证状态如何在 scan cycle 之间被记住。 涉及知识点: scan cycle (输入采样、逻辑执行、输出写回), process image, 自锁 (self-holding), Watch table 的 Monitor/Modify, 置位/复位优先级。 IO 与变量定义 (PLC tag table):

名称地址类型说明
bStart%I0.0Bool启动按钮 (PLCSIM 中 Modify)
bStop%I0.1Bool停止按钮
bRun%Q0.0Bool电机运行
iScanCnt%MD10DInt扫描计数器 (观察用)

提示:

  • C/Python 类比: 整个程序就是一个永不退出的 while True 循环体, “记住状态“只能靠变量本身 (%Q/%M/DB/static), 没有闭包、没有事件回调。类比到此为止 — 普通程序执行完退出, PLC 程序永远循环, 任何阻塞 (长循环、死循环) 都会拖慢所有逻辑甚至触发 cycle time fault (watchdog)。
  • 自锁的本质: 输出 bRun 既是输出又被读回 (读到上一周期写入的值), 相当于 run = run OR start。两条 IF 的先后顺序决定同时按下时谁赢: 后执行的覆盖先执行的; 这里停止在后 = 停止优先。

完整代码 (OB1 直接写成 SCL, 代码区):

%MD10 := %MD10 + 1;            // 观察用: 每个扫描周期 +1, 证明程序在被周期性执行
IF %I0.0 THEN
    %Q0.0 := TRUE;             // 置位; 启动信号消失后靠下一周期读回保持
END_IF;
IF %I0.1 THEN
    %Q0.0 := FALSE;            // 后执行 → 停止优先
END_IF;

等价的一行布尔方程: %Q0.0 := (%Q0.0 OR %I0.0) AND NOT %I0.1; 语义相同, 同样停止优先。 Watch table 实验: 1) wt_l01 放 4 个量, Online & Monitor, iScanCnt 持续增长 = 你看到的是 N 个扫描周期后的结果; 2) Modify bStart=1 确认 bRun=1, 再改回 0 — bRun 仍为 1, 自锁成立 (状态记在 %Q0.0 的 process image 里); 3) Modify bStop=1, bRun 立即变 0, 改回 0 后保持 0; 4) 选做: 若 PLCSIM 窗口有单次扫描模式 (Single scan / Next scan, 待核实: 以你安装版本界面为准), 逐周期单步观察。 验收标准: 1) bStart 置 1 再回 0 (哪怕只持续一个扫描周期), bRun 保持 1。 2) bStop=1 期间即使 bStart 同时=1, bRun=0 (停止优先)。 3) 重启 PLCSIM 后 bRun=0、iScanCnt=0; 想要跨重启保持计数值该放哪里 (DB 的 retentive 属性或 M 区保持设置)。 4) 能口头回答: bRun 的“记忆“存在哪, 谁每个周期刷新它。 进阶变体: 改成启动优先 (交换两条 IF 顺序); 加热过载 %I0.2, 故障断开并自锁故障灯; 逻辑搬进 FB fbLatch, 状态放 static, OB1 调用 (为后面所有题做准备); 在 Online & Diagnostics > Cycle time 看真实扫描时间。

L1: 基础指令四连 (4 题)

1.1 多地启停 + 急停 (完整答案)

目标: 甲/乙两地都能启停同一台电机; 急停与故障随时切断。 涉及知识点: 多地控制 (启动并联、停止串联), 常闭 (NC) 信号约定, 急停的软件语义, FB + 实例 DB。 IO 与变量定义:

名称地址类型说明
bStartA / bStartB%I0.0 / %I0.1Bool甲/乙地启动, 常开瞬动
bStopA / bStopB%I0.2 / %I0.3Bool甲/乙地停止, 常闭
bEstopNC%I0.4Bool急停, 硬件常闭
bFault%I0.5Bool过载/变频器故障
bRun%Q0.0Bool运行

提示:

  • 现场约定: 停止类信号 (停止按钮、急停、安全门) 一律接常闭。断线时信号变 FALSE, 系统走向安全侧 (停机); 常开接法断线表现为“永远没按过“, 是隐患。多地控制就两条: 启动并联 (OR), 停止串联 (所有常闭健康才允许运行)。
  • 必须写进代码注释的一句话: 软件急停不等于安全回路。真正的急停要靠硬件 (安全继电器/接触器冗余或 F-PLC), 因为 PLC 有扫描延迟与单点故障; 本题只是操作逻辑层的快速停机。官方安全编程指南见附录 B。

完整代码, FB fbMotor2w 声明区:

FUNCTION_BLOCK "fbMotor2w"
VAR_INPUT
    bStartA  : Bool;   // 甲地启动 (常开)
    bStartB  : Bool;   // 乙地启动
    bStopA   : Bool;   // 甲地停止 (常闭: 未按=TRUE)
    bStopB   : Bool;   // 乙地停止 (常闭)
    bEstopNC : Bool;   // 急停 (常闭: 健康=TRUE)
    bFault   : Bool;   // 故障反馈
END_VAR
VAR_OUTPUT
    bRun         : Bool;
    bEstopActive : Bool;   // 急停已触发 (诊断用)
END_VAR
#bEstopActive := NOT #bEstopNC;
IF (NOT #bStopA) OR (NOT #bStopB) OR #bEstopActive OR #bFault THEN
    #bRun := FALSE;              // 停止/急停/故障: 后执行 → 优先级最高
END_IF;
IF (#bStartA OR #bStartB) AND #bStopA AND #bStopB AND #bEstopNC AND NOT #bFault THEN
    #bRun := TRUE;               // 自锁: 按钮松开后靠 bRun 保持
END_IF;

OB1 里用生成的单实例 DB fbMotor2w_DB 调用: "fbMotor2w_DB"(bStartA := %I0.0, bStartB := %I0.1, bStopA := %I0.2, bStopB := %I0.3, bEstopNC := %I0.4, bFault := %I0.5, bRun => %Q0.0); 验收标准: 1) 任一启动按钮脉冲即可启动; 任一停止按钮立即停止。 2) 急停期间任何启动无效; 急停释放 (信号回 TRUE) 后需重新按启动。 3) bFault=1 强制停机, bFault 回 0 后同样需重新启动。 4) 常闭语义正确: 把 bStopA Modify 成 FALSE (模拟按下或断线), 电机停。 进阶变体: 加故障复位按钮与故障锁存; 急停释放后要求 3s 复位延时 (TON, 为 1.2 热身); 启动/停止各扩展为数组的通用件 (为 3.2 铺路)。

1.2 TON/TOF 组合与振荡器 (完整答案)

目标: (a) 延时 3s 启动、命令撤销后延时 5s 停止; (b) 用两个 TON 做占空比可调的闪烁振荡器, 并解释为什么单 TON 自复位版不是方波。 涉及知识点: IEC 定时器 TON (on-delay) / TOF (off-delay) 精确语义, 多实例声明, 自复位振荡器, 占空比。 IO 与变量定义: 两个 FB, 输入输出见声明; 观察用 %M0.0 (bCmd)、%M0.1 (bBlinkEn) 由 wt_l12 Modify 驱动。 提示 (先背语义再写码, 这是面试级事实):

  • TON: IN=TRUE 后开始计时, ET 从 0 增长, ET>=PT 时 Q=TRUE; IN 变 FALSE 时 Q 立即 FALSE 且 ET 清零。TOF: IN=TRUE 时 Q 立即 TRUE; IN 变 FALSE 后 Q 保持, 直到 ET>=PT 才落 FALSE。
  • 定时器实例必须声明在 static 区 (ET 要跨周期保存); TEMP 是每周期复用的草稿区, 装不下实例数据。
  • C 类比: TON 像被主循环轮询驱动的状态机, 不像 sleep — PLC 里没有“等“, 延时只能“设好闹钟, 下个周期再看“。

完整代码, FB fbDelayRun 声明区:

FUNCTION_BLOCK "fbDelayRun"
VAR_INPUT
    bCmd      : Bool;         // 运行命令
    tOnDelay  : Time := T#3S;
    tOffDelay : Time := T#5S;
END_VAR
VAR_OUTPUT
    bRun     : Bool;
    tElapsed : Time;
END_VAR
VAR
    tonStart : TON;    // 多实例
    tofStop  : TOF;
END_VAR
#tonStart(IN := #bCmd, PT := #tOnDelay, ET => #tElapsed);  // 启动延时
#tofStop(IN := #tonStart.Q, PT := #tOffDelay);             // 停止延时
#bRun := #tofStop.Q;

FB fbBlink 声明区:

FUNCTION_BLOCK "fbBlink"
VAR_INPUT
    bEnable : Bool;
    tOn     : Time := T#700MS;
    tOff    : Time := T#300MS;
END_VAR
VAR_OUTPUT
    bLamp : Bool;
END_VAR
VAR
    tonOn  : TON;
    tonOff : TON;
END_VAR
// 灯灭阶段计 tOff, 灯亮阶段计 tOn — 到期翻转
#tonOff(IN := #bEnable AND NOT #bLamp, PT := #tOff);
#tonOn (IN := #bEnable AND #bLamp,     PT := #tOn);
IF #tonOff.Q THEN #bLamp := TRUE;  END_IF;
IF #tonOn.Q  THEN #bLamp := FALSE; END_IF;
IF NOT #bEnable THEN #bLamp := FALSE; END_IF;   // 使能断开: 确定的初始态

单 TON 振荡器陷阱 (记进笔记): #ton1(IN := NOT #ton1.Q, PT := T#1S); — Q 每秒只“亮“一个扫描周期。机理: ET 到 PT 时 Q=TRUE → 下个扫描周期 IN=NOT Q=FALSE → Q 立即回 FALSE、ET 清零; Q 的 TRUE 宽度只有 1 个 scan cycle, wt 几乎看不见, 更点不亮灯。它是“每秒产生一个脉冲沿“的工具, 不是方波。 验收标准: 1) bCmd 置 1, 3s 后 bRun=1; bCmd 置 0, bRun 保持 5s 后落 0; 用 tElapsed 在 wt 上核对时间轴。 2) tOn/tOff 改 T#2S/T#1S, 观察到 2/3 占空比。 3) bBlinkEn 断开瞬间灯立即灭 (而不是停在高电平)。 4) 能解释单 TON 版为什么不是 50% 方波。 进阶变体: 启动延时期间亮“预备“黄灯, 延时停止期间亮“惯性运行“指示; 用 fbBlink 做 1Hz 故障闪烁, 故障上升沿后闪 10 次自动停 (TON + CTU, 为 1.3 热身)。

1.3 CTU 计数装箱 + 复位 (骨架)

目标: 光电每检测一个产品计 1; 计满 12 个则“装满“灯亮、传送带暂停; 复位后继续。 涉及知识点: CTU (up counter) 语义与自带边沿检测, 电平 vs 边沿, 计数联锁, 手动/自动复位。 IO 与变量定义: bPhoto %I0.0 (光电), bReset %I0.1, bBoxReady %Q0.0 (装满), bBelt %Q0.1 (传送带)。FB fbPacker 声明区 (完整可照抄):

FUNCTION_BLOCK "fbPacker"
VAR_INPUT
    bPhoto   : Bool;
    bReset   : Bool;
    bBeltCmd : Bool;        // 上游传送带启动命令
    iBoxSize : Int := 12;
END_VAR
VAR_OUTPUT
    bBelt     : Bool;
    bBoxReady : Bool;
    iCount    : Int;
END_VAR
VAR
    trigPhoto : R_TRIG;     // 手写对照版用
    ctuBox    : CTU;
END_VAR

提示:

  • CTU 语义: CU 的上升沿使 CV+1; R=TRUE 清零 (优先于计数); CV>=PV 时 Q=TRUE; PV/CV 为 Int。CU 自带边沿检测: wt 里把 bPhoto Modify 成 1 并保持, 只会计 1 次 — 长电平只产生一个上升沿。
  • 手写对照: #trigPhoto(CLK := #bPhoto); IF #trigPhoto.Q THEN #iCnt := #iCnt + 1; END_IF; — 写一遍就知道 CTU 内部在做什么。千万别写 IF #bPhoto THEN 计数, 那是每个扫描周期都 +1。联锁语义自己定义并写进注释: 装满立即停带; 复位后自动恢复还是等待 bBeltCmd 重新给命令。
#ctuBox(CU := #bPhoto, R := #bReset, PV := #iBoxSize,
        Q => #bBoxReady, CV => #iCount);
// TODO1: 传送带联锁 — 装满即停, 复位后按你定义的语义恢复
// 参考形状: #bBelt := #bBeltCmd AND NOT #bBoxReady;
// TODO2 (选): 手写 R_TRIG + Int 计数, 验证与 CTU 等价

验收标准: 1) 连续 12 个上升沿后 bBoxReady=1 且 bBelt=0; 2) bReset 脉冲清零并按定义恢复; 3) bPhoto 恒 1 长电平只计 1 次; 4) 计到 11 时挂机 10 分钟计数不丢 (static 语义)。 进阶变体: 计满自动发 500ms 换箱脉冲 (TON/TP) 再自复位; 用 CTUD 实现“满 12 提示、超 14 报警“; 加 DInt 总箱数累计 (为 3.6 铺路)。

1.4 单按钮启停 (骨架, 两种解法)

目标: 一个按钮既启动又停止: 按一次开, 再按一次关 (toggle)。 涉及知识点: 边沿检测两种实现 (R_TRIG 与手写上次值), toggle 写法, FB 的可重入性。 IO 与变量定义: bBtn %I0.0, bRun %Q0.0。FB fbToggle 声明区:

FUNCTION_BLOCK "fbToggle"
VAR_INPUT
    bBtn : Bool;
END_VAR
VAR_OUTPUT
    bRun : Bool;
END_VAR
VAR
    trigBtn  : R_TRIG;
    bBtnLast : Bool;    // 解法 B 用 (static: 跨周期保存)
END_VAR

提示:

  • 解法 A: #trigBtn(CLK := #bBtn); IF #trigBtn.Q THEN #bRun := NOT #bRun; END_IF;
  • 解法 B (手写上次值, bEdge 声明为 TEMP):
#bEdge := #bBtn AND NOT #bBtnLast;   // 本周期=1 且上周期=0
#bBtnLast := #bBtn;                  // 必须在算完边沿之后再更新上次值
IF #bEdge THEN #bRun := NOT #bRun; END_IF;
  • 想想两行赋值交换顺序会怎样 (答案: 边沿永远检测不到, “上次值“总等于当前值)。
  • 坑: 同一 FB 在一个扫描周期内被调用两次, 两种解法都会用同一份实例状态互相踩。与 C 类比失效点: C 里无状态函数天然可重入; FB 状态在实例数据里而不在栈上, 一个实例一处状态, 调两次就是共享。

验收标准: 1) 连按 10 次状态交替且次数正确; 2) 按住不放只翻转一次 (边沿语义, wt 的 Modify 长电平同理); 3) 两种解法行为一致; 4) 能解释原因。 进阶变体: 长按 2s 停机、短按启动 (TON + 边沿); 单按钮三态 (停止/运行/点动)。

L2: 数据处理与算法 (4 题)

2.1 模拟量滑动平均滤波 (骨架)

目标: 对一路模拟量输入做 16 点滑动平均 (定长数组循环缓冲), 并正确处理启动阶段窗口未满。 涉及知识点: circular buffer, Array/Real, FOR 求和与增量求和, 浮点误差, 模拟量在 PLCSIM 中的注入。 IO 与变量定义: iRawAin %IW64 (Int, S7-1200 板载 AI, 地址以硬件组态为准)。FB fbMovAvg 声明区:

FUNCTION_BLOCK "fbMovAvg"
VAR CONSTANT
    cN : Int := 16;
END_VAR
VAR_INPUT
    iRaw : Int;              // 0..27648
END_VAR
VAR_OUTPUT
    rAvg    : Real;          // 滑动平均
    iFilled : Int;           // 已填充点数
END_VAR
VAR
    aBuf : Array[0..15] of Real;
    iIdx : Int;              // 下一个写入位置
    iCnt : Int;              // 有效样本数
END_VAR
VAR_TEMP
    i    : Int;
    rAcc : Real;
END_VAR

提示:

  • 两种实现至少各写一遍: (A) 每周期 FOR 遍历求和再除 — O(N), N=16 无所谓, 先写它; (B) 增量和: 新样本进、被挤出的旧样本出 (需自加 static rSum) — O(1), 但要考虑浮点累计误差与未填满阶段的除数。
  • 循环缓冲与 C 的 ring buffer 完全同构: 写 aBuf[iIdx], #iIdx := (#iIdx + 1) MOD cN; (iIdx 恒非负, MOD 无负数问题)。这一条类比成立。
  • 未填满时 (iCnt < cN) 平均值除以 iCnt, 否则前 15 个周期输出被稀释 16 倍。PLCSIM 注入: wt 直接 Modify %IW64 (仿真允许, 真机不可, 见 0)。
// TODO1: 写入 aBuf[iIdx], 推进 iIdx/iCnt
// TODO2: FOR 求和 (rAcc) → rAvg, 除以实际有效点数
// TODO3 (选): 改增量和 rSum, 与 FOR 版对拍, 差值应在浮点误差内

验收标准: 1) 恒定输入时 rAvg == 输入; 2) 阶跃输入后 rAvg 用 16 个周期收敛 (wt 手动改 16 次); 3) 单点尖峰 (注入一次 27648) 只引起约 1/16 幅度扰动; 4) iFilled 从 1 增长到 16 后封顶。 进阶变体: 3/5 点中值滤波 (复用 2.4 排序); 加权滑动平均; 试着把 cN 提成 VAR_INPUT — 会发现数组声明长度必须编译期常量; 但 S7-1200 支持变长数组形参 Array[] (需优化块 + 固件 >= V4.2; FC 只能声明在 Input/InOut 区, FB 只能 InOut), 把 fbMovAvg 改造成 Array[] + LOWER_BOUND/UPPER_BOUND 的通用版是很好的练习 (见 03 章 9.3)。

2.2 SCALE_X 工程量转换 + 报警滞回 (骨架)

目标: 0..27648 原始值转 0..100.0 工程量, 超过 80.0 报警, 回落到 75.0 以下才解除 (滞回)。 涉及知识点: NORM_X / SCALE_X, 工程量转换, 滞回 (hysteresis), 报警抖动。 IO 与变量定义: iRawAin %IW64。FB fbScaleAlarm 声明区:

FUNCTION_BLOCK "fbScaleAlarm"
VAR_INPUT
    iRaw   : Int;
    rMin   : Real := 0.0;
    rMax   : Real := 100.0;
    rHiLim : Real := 80.0;
    rHyst  : Real := 5.0;
END_VAR
VAR_OUTPUT
    rEng     : Real;
    bHiAlarm : Bool;
END_VAR
VAR_TEMP
    rNorm : Real;
END_VAR

提示:

  • 标准两步 (NORM_X 归一化到 0.0..1.0, 再 SCALE_X 映射): #rNorm := NORM_X(MIN := 0, VALUE := #iRaw, MAX := 27648); #rEng := SCALE_X(MIN := #rMin, VALUE := #rNorm, MAX := #rMax); 若你的版本按带 OUT 的调用形式处理, 写成 NORM_X(MIN := ..., VALUE := ..., MAX := ..., OUT => #rNorm) 即可。27648 是单极性满量程标称值; 溢程/欠程区间 (约 27649..32511 与负值区) 有专门含义, 本题先钳位处理; NORM_X/SCALE_X 不做自动限幅, 溢程输入线性外推 (待核实: 以指令帮助为准)。
  • 滞回永远是两条独立 IF (置位条件/复位条件), 不要写成一条: IF #rEng > #rHiLim THEN #bHiAlarm := TRUE; END_IF; IF #rEng < #rHiLim - #rHyst THEN #bHiAlarm := FALSE; END_IF; 为什么需要滞回: 信号带噪声在 80.0 附近抖动时, 单阈值报警每秒翻转几十次, HMI 与日志被刷爆 — 施密特触发器的软件版。
// TODO1: 原始值钳位 (<0 → 0, >27648 → 27648)
// TODO2: NORM_X + SCALE_X → rEng
// TODO3: 滞回报警 → bHiAlarm

验收标准: 1) raw = 0/13824/27648 → rEng = 0.0/50.0/100.0 (浮点误差内); 2) raw 阶跃跨过 22118 (约 80%) 报警出现; 3) 阈值附近 Modify ±50 码值抖动, 报警状态不抖; 4) 回落到 20736 (约 75%) 以下解除。 进阶变体: 加低报与报警窗口; 报警延时确认 (TON 滤短毛刺); 反向输出: 工程量 → 0..27648 写 %QW (先 NORM_X 再 SCALE_X, 即 4..20mA 输出的套路)。

2.3 简单文本协议解析: SET,1,25.5 (骨架)

目标: 输入一行逗号分隔命令字符串, 解析出命令字/通道号/数值, 非法输入返回错误码 — 3.4 通信与 3.5 配方的解析层基本功。 涉及知识点: SCL String 语义 (1-based、定长缓冲), FIND/MID/LEFT/RIGHT, 字符串比较, S_CONV/STRG_VAL 数值转换, 错误码设计。 IO 与变量定义: FB fbParseCmd 声明区 (eErr 约定: 0=正常 1=格式 2=命令字 3=数值):

FUNCTION_BLOCK "fbParseCmd"
VAR_INPUT
    sRx : String[32];        // 如 'SET,1,25.5'
END_VAR
VAR_OUTPUT
    eErr    : Int;
    iCh     : Int;
    rVal    : Real;
    bSetCmd : Bool;
END_VAR
VAR_TEMP
    iC1  : Int;              // 第一个逗号位置
    iC2  : Int;              // 第二个逗号位置 (绝对)
    sCmd : String[8];
    sCh  : String[4];
    sVal : String[12];
END_VAR

提示:

  • String 是 1-based; LEN 返回实际长度; FIND(IN1 := s, IN2 := ‘,’) 返回首个匹配位置, 找不到返回 0 — 用 0 判错。拆解套路 (SET,1,25.5): iC1 = FIND(sRx, ‘,’); sCmd = LEFT(IN := sRx, L := iC1-1); 第二个逗号: 把首逗号之后的尾巴切出来 (MID/RIGHT), 对尾巴 FIND 得相对位置再换算回绝对位置 — 经典 off-by-one 练习, 先在纸上算好参数。
  • 数值转换: S_CONV(IN := #sVal, OUT => #rVal); 或 STRG_VAL (可指定小数点/指数格式, 格式位含义以指令帮助为准, 待核实)。转换失败的行为 (OUT 保持原值还是置 0) 需要实验确认并写进注释 — 本题隐藏考点。
  • 与 Python 类比: 手写 line.split(‘,’) + float(x) + try/except。类比失效点: SCL 没有 split、正则与异常, 只有 FIND/MID 与显式错误码; String[32] 是定长缓冲 (2 字节头 + 32 字节字符区), 不是动态 str。
  • 解析通过后别忘了范围检查 (如 iCh 1..8, rVal 合理区间)。
#eErr := 0;
#bSetCmd := FALSE;
#iC1 := FIND(IN1 := #sRx, IN2 := ',');
// TODO1: 校验两个逗号都存在且顺序合法, 否则 eErr 置格式错误后 RETURN
// TODO2: 切出 sCmd/sCh/sVal (LEFT/MID/RIGHT + 偏移换算)
// TODO3: IF sCmd = 'SET' THEN 通道号与数值转换+范围检查, bSetCmd := TRUE
//        ELSE eErr 置命令字错误

验收标准: 1) ‘SET,1,25.5’ → eErr=0, iCh=1, rVal=25.5, bSetCmd=TRUE; 2) 空串、‘SET’、‘SET,1’、‘SET,1,25.5,extra’、‘SET,x,25.5’ → 各返回定义的错误码; 3) ‘GET,2’ → 命令字错误或你定义的扩展行为; 4) 转换失败行为有实验记录。 进阶变体: 支持第二条命令 RST,1; 不用 FIND 的逐字符状态机解析; 与 3.4 联动: 从 TRCV 缓冲收字节流, 按 ‘\n’ 断行后交给本 FB。

2.4 插入排序 + 二分查找: 就是普通编程 + TEMP 陷阱 (骨架)

目标: 在 SCL 里对定长 Int 数组实现插入排序与二分查找, 顺手踩一遍 TEMP 的坑。 涉及知识点: 数组参数 (VAR_IN_OUT 引用传递), WHILE/FOR, 布尔求值顺序, TEMP 的真实语义。 IO 与变量定义: FC fcSortIns 与 fcBinSearch 声明区:

FUNCTION "fcSortIns" : Void
VAR_INPUT
    iN : Int;                  // 有效元素个数 (<=16)
END_VAR
VAR_IN_OUT
    a : Array[0..15] of Int;   // 调用方数组, 引用传递
END_VAR
VAR_TEMP
    i    : Int;
    j    : Int;
    iKey : Int;
END_VAR

FUNCTION "fcBinSearch" : Void
VAR_INPUT
    iKey : Int;
    iN   : Int;
END_VAR
VAR_IN_OUT
    a : Array[0..15] of Int;
END_VAR
VAR_OUTPUT
    iPos   : Int;    // 找到: 0..15
    bFound : Bool;   // 未找到: FALSE, iPos := -1
END_VAR
VAR_TEMP
    iLo  : Int;
    iHi  : Int;
    iMid : Int;
END_VAR

提示:

  • 写法与 C 同构, 但有一个致命差异: SCL 的 AND/OR 不提供语言级短路保证 (与 C 的 && 不同)。C 版 WHILE (j >= 0) AND (a[j] > key) 在 j=-1 时若不短路就是负下标运行时错误; 必须改写成不越界的结构 (前置守卫 + 嵌套 IF, 或调整循环变量逻辑) — “普通编程“迁到 SCL 的第一个坑, 也是本题核心考点。
  • 二分查找: 经典 lo/hi/mid 三分; (lo+hi)/2 对正数无取整歧义; 未找到 iPos := -1。
  • TEMP 陷阱 (本题主角): TEMP 是每次调用共用的草稿区, 不清零、跨调用内容不保证。C 类比: 像未初始化的局部变量, 但更隐蔽 — 更像全体函数共用的一块 static scratchpad, 上次留下的值还在, 看似“能记住“, 中间插入别的调用就被改写。实验: 1) FB 里加 VAR_TEMP x : Int, 某次调用赋值, 下次调用不赋值直接读 (拷到 output 观察), 它“记得“上次; 2) 两次调用之间调另一个用大量 TEMP 的块, x 被破坏。结论: 状态一律 static (FB) / DB / M; TEMP 只放“本调用内先写后读“的量。调用方法: 测试 FB 的 static 里建 Int 数组, 乱序赋初值, 调 fcSortIns 后逐元素监控。
FOR #i := 1 TO #iN - 1 DO
    #iKey := #a[#i];
    #j := #i - 1;
    // TODO1: 后移元素, 直到 j<0 或 a[j] <= iKey (不得依赖 AND 短路!)
    // TODO2: 放回 iKey
END_FOR;

验收标准: 1) 16 个逆序元素排成升序, 已有序输入也正确; 2) 二分查找存在/不存在关键字各至少 5 组用例通过; 3) WHILE 在 j=-1 边界无运行时错误 (PLCSIM 实跑); 4) TEMP 两个实验现象亲手复现并写进注释。 进阶变体: FB static 里维护“有序插入“ (新值二分找位后搬移插入); 比较计数器统计最好/最坏情形; 对 Real 数组再写一份 — S7-1200 无泛型, 只能复制改类型, 体会 PLC 生态与软件工程的差距。

L3: 系统集成 (6 题)

L3 开始“像项目“: 状态机、FB 复用、控制算法、通信、HMI、总联调。建议每题先在纸上写清块划分再动手。

3.1 双向交通灯状态机 (骨架)

目标: 十字路口双向红绿灯, 6 相位循环, 时长可配; 任何时刻每个方向有且仅有一盏灯亮, 换向必经全红安全间隔。 涉及知识点: CASE + CONSTANT 命名状态, 相位时长表 (数组), 定时器数组 (包装 FB 手法), 输出解算与互斥自检, “未被调用的实例会冻结”。 相位表与 IO:

相位南北东西默认时长
0 南北绿绿10s
1 南北黄3s
2 全红A1s
3 东西绿绿10s
4 东西黄3s
5 全红B1s

输出 aLamp[0..5] 映射 %Q0.0~%Q0.5, 顺序: nsG/nsY/nsR/ewG/ewY/ewR; 行人请求 %I0.0 (选做)。 小 FB fbPhT (每相位一个定时器的包装, 声明+代码全给):

FUNCTION_BLOCK "fbPhT"
VAR_INPUT
    bRun : Bool;    // 本相位激活时为 TRUE
    tDur : Time;
END_VAR
VAR_OUTPUT
    qDone : Bool;
END_VAR
VAR
    ton1 : TON;
END_VAR
#ton1(IN := #bRun, PT := #tDur);   // IN=FALSE 时自动复位 ET
#qDone := #ton1.Q;

主 FB fbTraffic 声明区:

FUNCTION_BLOCK "fbTraffic"
VAR CONSTANT
    cPhNsG   : Int := 0;
    cPhNsY   : Int := 1;
    cPhAllR1 : Int := 2;
    cPhEwG   : Int := 3;
    cPhEwY   : Int := 4;
    cPhAllR2 : Int := 5;
    cPhCnt   : Int := 6;
END_VAR
VAR_INPUT
    bEnable : Bool;
END_VAR
VAR_OUTPUT
    aLamp : Array[0..5] of Bool;   // nsG nsY nsR ewG ewY ewR
    iPhase : Int;
    bFault : Bool;                 // 互斥自检失败
END_VAR
VAR
    aDur  : Array[0..5] of Time;
    aTon  : Array[0..5] of "fbPhT";
    bInit : Bool;
END_VAR
VAR_TEMP
    i : Int;
END_VAR

提示:

  • 为什么要包装 FB: S7-1200 的 SCL 里直接声明 TON 数组再用变量索引调用 (形如 #toni), 多数版本不支持或行为不可靠 (社区与西门子论坛共识, 待核实: 以你的版本实测为准)。标准做法: 内含单个 TON 的包装 FB → 包装 FB 的数组 → FOR 索引调用。
  • 每个扫描周期用 FOR 把 6 个定时器全部调用一遍, bRun := (#iPhase = #i): 当前相位计时, 其余自动复位。实例不被调用就冻结 (ET/Q 保持) — 若只调用当前相位的定时器, 离开相位时 ET 不清零, 下次进来立刻触发; 踩一次就懂。
  • 相位推进: IF #aTon[#iPhase].qDone THEN #iPhase := (#iPhase + 1) MOD cPhCnt; END_IF;
  • 输出解算用 CASE 写 (读起来像状态说明书), aLamp 每个元素只在对应相位为 TRUE; 末尾加互斥自检: 每方向三灯之和恒 1, 违反则 bFault — “输出不变式“的雏形。
IF NOT #bInit THEN
    #aDur[cPhNsG] := T#10S;  #aDur[cPhNsY] := T#3S;
    // TODO1: 补齐其余 4 个时长
    #bInit := TRUE;
END_IF;
FOR #i := 0 TO cPhCnt - 1 DO
    #aTon[#i](bRun := (#iPhase = #i) AND #bEnable, tDur := #aDur[#i]);
END_FOR;
// TODO2: 相位推进 (MOD 循环)
// TODO3: CASE #iPhase OF ... 输出解算到 aLamp + 互斥自检

验收标准: 1) 完整周期 28s, 相位顺序符合表; 2) 任意时刻每方向恰有一盏灯 (bFault 恒 FALSE); 3) 相位 2/5 的全红间隔真实存在 (输出锁存或趋势证明); 4) bEnable 断开 → 全灭 (或你定义的失败安全态), 重启后从相位 0 开始。 进阶变体: 行人按钮插入“全红+行人绿“相位; 绿末 3s 闪烁 (复用 fbBlink); 夜间黄闪; 相位时长搬进全局 DB 供 HMI 修改 (与 3.5 联动)。

3.2 通用电机设备 FB: 多实例驱动 3 台 + UDT 接口 (骨架)

目标: 一个 FB 封装电机设备全部行为 (启停/故障锁存与复位/运行时长累计/维护提醒), 接口 UDT 化, 多实例驱动 3 台。 涉及知识点: UDT 定义接口, FB 多实例 (multi-instance), OB 无 static → 包装 FB 分层, 时长累计的正确姿势。 UDT 定义 (PLC data types):

类型字段类型初值含义
udtMotorCfgrMaintHoursReal2000.0维护阈值 [h]
udtMotorCfgtFaultOnTimeT#200MS故障确认延时 (滤毛刺)
udtMotorStabRun / bFault / bMaintDueBoolFALSE运行/故障锁存/待维护
udtMotorStarRunHours / iStartCntReal / Int0累计运行时长 [h] / 启动次数

FB fbMotor 声明区:

FUNCTION_BLOCK "fbMotor"
VAR_INPUT
    bStart : Bool;              // 瞬动
    bStop  : Bool;              // 常闭
    bReset : Bool;              // 故障复位 (瞬动)
    bFault : Bool;              // 过载/接触器故障反馈 (电平)
    cfg    : "udtMotorCfg";     // 参数 (存在实例 DB, HMI 可改)
END_VAR
VAR_OUTPUT
    bRun : Bool;
END_VAR
VAR
    sta       : "udtMotorSta";  // 状态 (HMI 经实例 DB 符号路径访问)
    trigStart : R_TRIG;
    trigReset : R_TRIG;
    tonFault  : TON;
    tonHour   : TON;            // 1s 节拍
END_VAR

提示:

  • OB1/OB30 没有 static 区 (OB 无实例 DB), 不能声明多实例。所以建包装 FB fbPlant, 在其 static 里声明 fbMotor 实例数组, OB1 用单实例 DB 调 fbPlant — TIA 标准分层: OB 是事件入口, FB 是组件, 实例是对象。大结构体参数若要按引用传递 (省拷贝) 可用 VAR_IN_OUT; 本题简化用 VAR_INPUT + static, 想清楚两者代价。
  • 运行时长不要用扫描周期累加 (周期抖动大); 用 TON 1s 节拍: #tonHour(IN := #sta.bRun, PT := T#1S); IF #tonHour.Q THEN #sta.rRunHours := #sta.rRunHours + 1.0/3600.0; END_IF;
  • 故障: bFault 经 tonFault 延时确认后锁存并断开 bRun; bReset 用边沿解锁。想清楚: 为什么故障检测用电平、复位用边沿 (提示: 复位若用电平, 按住不放时故障永远无法再报)。维护: rRunHours > cfg.rMaintHours → bMaintDue 锁存, 手动复位; 复位动作包含 rRunHours 清零 (模拟保养完成)。
  • 与 C 类比: fbMotor 约等于 class (数据与逻辑合一), 多实例约等于对象数组; 但没有继承与虚函数, “接口“靠 UDT 约定。类比失效点: 实例不是运行期 new 出来的堆对象, 是编译期分配在实例 DB 里的连续数据; 调用是同步过程调用, 不存在线程或消息循环。
#trigStart(CLK := #bStart AND NOT #sta.bFault);
#trigReset(CLK := #bReset);
#tonFault(IN := #bFault, PT := #cfg.tFaultOn);
// TODO1: 故障锁存 (tonFault.Q 上升即锁存) 与复位
// TODO2: 启保停 (复用 1.1 思路, 停止条件含故障与 bStop 常闭)
// TODO3: 1s 节拍累计 rRunHours; trigStart.Q 计启动次数
// TODO4: bMaintDue 判定与复位清零
#bRun := #sta.bRun;

包装 FB fbPlant (接口: VAR_INPUT aStart/aStop/aFault : Array[1..3] of Bool 与 bReset : Bool; static aMot : Array[1..3] of “fbMotor”; TEMP i : Int), 代码区:

FOR #i := 1 TO 3 DO
    #aMot[#i](bStart := #aStart[#i], bStop := #aStop[#i],
              bFault := #aFault[#i], bReset := #bReset);
END_FOR;

验收标准: 1) 3 台独立启停互不影响; 2) 任一台 bFault 持续 200ms 后锁存停机, bReset 脉冲解锁且需重新启动; 3) 某台 cfg.rMaintHours 改 0.001 并运行数秒后 bMaintDue 置位, 复位后 rRunHours 清零; 4) 时长精度: 对照 PLCSIM 真实时间跑 1 分钟, 误差 < 1%。 进阶变体: 接触器反馈与指令不一致检测 (启动后 2s 无反馈 → 故障); 双阈值 (磨合期/保修期); 改一个常量把 3 台扩到 10 台 (体会多实例复用价值); 给 fbMotor 写最小单元测试 FB。

3.3 PID_Compact 温控 + 一阶惯性对象仿真联调 (骨架)

目标: 闭环控制自写的一阶惯性 (PT1) 温度对象: 设定 60 度, 观察超调与稳定时间; PID_Compact 与自写 PI 各做一遍并对比。 涉及知识点: PID_Compact 调用约束 (cyclic interrupt OB 固定周期), 被控对象离散化, PLCSIM 对 PID 的限制, PI 参数与抗饱和。 仿真可行性 (重要, 官方手册): PID_Compact V2.x 在一代 S7-1200 上不支持 PLCSIM 仿真, 仅 S7-1500 与 S7-1200 G2 可; 且仿真中自整定可能因采样时间误差中断, 需设 CycleTime.EnEstimation = FALSE、EnMonitoring = FALSE。若你的工程是一代 S7-1200 + 标准 PLCSIM, PID_Compact 路线走不通, 用替代法 (自写 PI) 完成 — 它反而更能讲清 PID 本质。 IO 与变量定义: 建 cyclic interrupt OB (如 OB30, 100ms) 作固定采样节拍。被控对象 FB fbPlantPT1 (完整给出, 属基础设施):

FUNCTION_BLOCK "fbPlantPT1"
VAR_INPUT
    rU       : Real;         // 加热功率 0..100 [%]
    rAmbient : Real := 20.0; // 环境温度
    rGain    : Real := 0.8;  // 稳态增益: u=100% 时温升 80 度
    rTau     : Real := 20.0; // 时间常数 [s]
    rTs      : Real := 0.1;  // 采样周期 [s], 必须等于 OB30 周期
END_VAR
VAR_OUTPUT
    rY : Real;               // 对象输出 (温度)
END_VAR
// 一阶欧拉离散: y += Ts/Tau * (K*u + Ambient - y); 稳态 y = Ambient + K*u
#rY := #rY + (#rTs / #rTau) * (#rGain * #rU + #rAmbient - #rY);
IF #rY < #rAmbient THEN #rY := #rAmbient; END_IF;   // 物理下限

包装 FB fbTempLoop 声明区:

FUNCTION_BLOCK "fbTempLoop"
VAR CONSTANT
    cKp : Real := 2.0;
    cTi : Real := 30.0;    // 积分时间 [s]
END_VAR
VAR_INPUT
    rSp            : Real := 60.0;   // 设定值
    bReset         : Bool;
    bUsePidCompact : Bool := FALSE;  // TRUE=PID_Compact, FALSE=自写 PI
END_VAR
VAR_OUTPUT
    rPv  : Real;
    rOut : Real;            // 0..100
END_VAR
VAR
    pidC  : PID_Compact;    // 多实例; 引脚随版本 (V2.x/V3) 有差异, 以指令帮助为准 (待核实)
    plant : "fbPlantPT1";
    rIntg : Real;           // PI 积分项
END_VAR
VAR_TEMP
    rErr : Real;
END_VAR

自写 PI 参考 (fbTempLoop 代码区核心部分, 可直接抄):

#rErr := #rSp - #plant.rY;
#rIntg := #rIntg + #rErr * (0.1 / #cTi);      // 0.1 = 采样周期
IF #rIntg > 100.0 THEN #rIntg := 100.0; END_IF;
IF #rIntg < 0.0 THEN #rIntg := 0.0; END_IF;
#rOut := #cKp * #rErr + #rIntg;
IF #rOut > 100.0 THEN #rOut := 100.0; END_IF;
IF #rOut < 0.0 THEN #rOut := 0.0; END_IF;
#plant(rU := #rOut);   // TODO: bUsePidCompact 分支下改调 PID_Compact
#rPv := #plant.rY;

PID_Compact 分支 (仅当你的 CPU 可仿真时): #pidC(Setpoint := #rSp, Input := #plant.rY, Reset := #bReset); #rOut := #pidC.Output; 手/自动与 PID 组态在实例 DB 的调试 (Commissioning) 界面完成。OB30 代码区每 100ms 调用一次: "fbTempLoop_DB"(rSp := %MD100, bReset := %M10.1); (plant.rTs 必须与此周期一致)。 验收标准: 1) 自写 PI: rSp=60, 稳态误差趋 0 (积分作用), 无持续振荡; 2) 手动扰动 (plant.rAmbient 从 20 改 10), 恢复过程可观测; 3) Kp 增大 10 倍, 记录振荡加剧趋势; 4) 若走 PID_Compact 路线: 完成一次自整定, 与手调参数对比记录在案。 进阶变体: 对象加纯滞后 (环形缓冲延迟 N 步), 看 PI 明显变难; 完整 anti-windup (输出饱和时冻结/回算积分); 加 D 项与一阶滤波; 把 rPv 写入数组供 HMI 趋势 (与 3.5 联动)。

3.4 Modbus TCP 从站数据暴露 (骨架 + 仿真限制说明)

目标: PLC 作为 Modbus TCP server (从站), 把 16 个 holding register 暴露给 PC 主站: 读计数、写设定值。 涉及知识点: MB_SERVER, TCON_IP_v4 连接描述, 非优化 DB, 字节序, 点表 (tag mapping) 文档。 Modbus 数据模型一句话: holding register 是可读可写的 16 位寄存器 (FC03 读, FC06/FC16 写), 对应 PLC 侧一块非优化 DB 里的 Word 数组。 PLCSIM 限制 (决定你的路线): 标准 PLCSIM 无真实 TCP/IP, Modbus 指令不被仿真 (社区多方一致结论; 官方口径: 开放用户通信仅经 Softbus 在本机内工作)。可行路线: (a) PLCSIM Advanced (真实 TCP/IP 走虚拟网卡; 一代 S7-1200 不在其支持列表, 需 S7-1500 或 S7-1200 G2, 待核实: 以 Advanced 版本支持表为准); (b) 真机; (c) 仿真替代题: 两个 PLCSIM 实例 + TSEND_C/TRCV_C (Softbus 内) 互发数据, 练开放用户通信 API, 达到同样的“数据暴露“目的。 IO 与变量定义 (真机/Advanced 路线): 全局 DB dbModbus, 属性取消 Optimized block access (非优化, 按绝对地址访问):

字段类型初值含义
connTCON_IP_v4ConnectionType=16#0B (TCP), ID=1, ActiveEstablished=FALSE (从站被动), LocalPort=502连接描述
hrArray[0..15] of Word0暴露的 holding registers

包装 FB fbModbusSrv 声明区 + 代码骨架:

FUNCTION_BLOCK "fbModbusSrv"
VAR
    mbSrv   : MB_SERVER;    // 多实例; 参数以你版本的库为准 (待核实)
    bErr    : Bool;
    wStatus : Word;
END_VAR
// MB_SERVER 每个扫描周期无条件调用 (非边沿触发), 连接由它自己维护
#mbSrv(DISCONNECT := FALSE, MB_HOLD_REG := "dbModbus".hr, CONNECT := "dbModbus".conn);
#bErr    := #mbSrv.ERROR;
#wStatus := #mbSrv.STATUS;
// TODO1: 生产数据打包进 hr (计数 Int → Word 直接 MOVE; Real 拆两个 Word, 注意 Modbus 大端字节序, 字节交换用 SWAP)
// TODO2: 主站可写的 hr 元素映射回程序内设定值

真机步骤: 1) 硬件组态给 CPU 设固定 IP (如 192.168.0.1/24), PC 同网段; 2) 建 dbModbus (非优化) 并填 conn 初值 (LocalPort 502); 3) OB1 每周期调用 MB_SERVER; 4) PC 端任一 Modbus 主站工具 (开源 CLI: mbpoll; GUI: Modbus Poll、QModMaster) FC03 读地址 0..15、FC06 写单寄存器; 5) Wireshark 抓端口 502 核对 MBAP 头与寄存器值, 异常 STATUS 记成表。 验收标准 (真机/Advanced): 1) 主站读 hr[0] == PLC 内计数; 2) 主站写 hr[1] → PLC 设定值变化且被程序采用; 3) 拔网线再恢复, 连接自动重建; 4) 至少整理 3 个 STATUS 错误码含义。 仿真替代题验收 (标准 PLCSIM 可做): 两个 PLCSIM 实例各跑一个 CPU 工程: A 每 1s 用 TSEND_C 发 8 字节, B 用 TRCV_C 接收并回显; 演示 REQ 边沿重连一次、断开一次, 记录 DONE/BUSY/ERROR/STATUS 行为。 进阶变体: 暴露 Float (字节序全流程); 多连接 (MB_SERVER 多实例, S7-1200 连接数上限查 CPU 手册, 待核实); 反向用 MB_CLIENT 读真实 Modbus 温湿度传感器; hr 映射写成正式点表文档 (SCADA 集成交付物)。

3.5 KTP HMI: 监控画面 + 配方 (可用仿真)

目标: 给 3.2 的三电机系统配一个 KTP 画面: 总览/操作/趋势三屏 + 报警 + 配方切换三套工艺参数。 涉及知识点: HMI 连接与 HMI tag, 画面元素动态化, 离散量报警, 配方 (recipes), HMI 仿真联动 PLCSIM。 仿真可行性: STEP 7 Basic 自带 WinCC Basic, KTP Basic 面板仿真无需额外许可; PC 运行系统 RT Advanced/Professional 仿真无许可每次限 1 小时 (官方)。Basic 面板支持配方但有限制 (无目录层级、记录数量有限; 第三方资料称约 100 条与 256KB, 待核实, 以 WinCC 手册为准)。 步骤清单:

  1. 项目树添加设备 HMI > KTP700 Basic PN; 在 Connections 里把 HMI 的 Ethernet 接口连到 PLC 的 PROFINET 接口 (仿真时同一 PLCSIM 实例)。
  2. HMI tag 表: 建 HMI 变量, 连接指向该 PLC, 地址用符号路径 (如 fbPlant_DB 的 static: aMot[1].sta.bRun — 优化 DB 必须符号访问)。
  3. 画面 1 总览: 3 个电机图形 (圆/矩形), 填充色绑 bRun/bFault/bMaintDue (绿/红/黄)。
  4. 画面 2 操作: 启动按钮用 Set bit while pressed + 释放复位 组合模拟瞬动 (或在 PLC 侧脉冲化), 停止按钮注意常闭语义; I/O 域显示 rRunHours; 画面上若放急停, 必须标注“非安全功能“。
  5. 画面 3 趋势: 实时曲线绑 rRunHours 或 3.3 的 rPv。报警: PLC 侧离散量报警 (bFault/bMaintDue), HMI 报警视图分报警/警告两类, 支持确认。
  6. 配方: 定义配方 (元素 = 3 台电机 cfg 参数), 建 3 条数据记录; 画面放配方视图, 支持从 PLC 读/写到 PLC。
  7. 仿真: 先启动 PLCSIM, 再选中 HMI 画面点 Simulation — HMI 运行系统本机启动并自动连 PLCSIM。

验收标准: 1) HMI 启停任一电机, PLC 状态变化并反馈画面 (1s 内); 2) 趋势曲线时间轴与数值可读; 3) 报警出现、确认、消失全流程; 4) 切换配方数据记录后 3 台参数整组变化且生效; 5) 停 PLCSIM 再恢复, HMI 断线提示并自动重连。 进阶变体: 用户组与权限 (操作工/工程师); 画面模板 + 传参画面 (每电机一个详情页); 报警文本双语; 把 3.6 产线做成 5 画面小项目 (总览/产线/电机详情/趋势/报警)。

3.6 综合小项目: 小型产线 (两段传送带 + 料仓 + 计数 + 班次统计 + 报警)

目标: 把 0.1~3.5 拼成一个小产线控制程序, 20 分钟能讲清架构。 涉及知识点: 架构分层, 顺序启停状态机, 故障传播, 班次统计 (时间处理), 报警首出, 全局数据组织。 工艺描述: 料仓下料到传送带 A, A 末端转 B, B 出料口装箱; 光电计件; 三班倒 (06/14/22 点切换), 每班统计产量; 任一段故障按工艺方向有序停机; 急停立即全停 (软件急停, 非安全回路)。 文字架构图:

              +--------- fbLine (协调 FB) ---------+
              | 产线状态机: 停止/启动中/运行/停止中/故障/急停 |
              +------------------------------------+
 料仓                传送带 A             传送带 B
 +--------+ 下料阀 +------------+ 光电1 +------------+ 光电2
 | fbSilo |------>| fbConveyor A|----->| fbConveyor B|-----> fbPacker 装箱 (1.3)
 +--------+       +------------+       +------------+         |
            (内含 fbMotor)        (内含 fbMotor)              |
                 |                     |                      |
         dbProd (fbShift 班次统计)  dbAlarm (fbAlarm 报警锁存) ← HMI (3.5) / Modbus (3.4)

分块清单 (先建全部空块再逐个填):

类型职责复用
OB1SCL调 fbLine
OB30 (100ms)cyclic interrupt时基/平滑量 (滤波)2.1
OB100startup初始化 (时长表、数组)3.1
fbLineFB产线状态机、顺序启停、联锁CASE 手法 3.1
fbConveyorFB单段皮带: 电机+堵转检测+下游满停内含 fbMotor (3.2)
fbSiloFB料位换算+缺料滞回报警+下料阀时序2.2
fbPackerFB计件装箱1.3
fbShiftFB班次判定+产量环形归档 (8 班)时间处理
fbAlarmFB报警锁存、首出报警、复位2.2 滞回手法
dbProd / dbAlarm全局 DB产量/报警数据, HMI 与 Modbus 从这里取3.4/3.5

关键 FB 骨架, fbLine 声明区:

FUNCTION_BLOCK "fbLine"
VAR CONSTANT
    cStStop    : Int := 0;
    cStStart   : Int := 1;   // 逆序启动: B 先启, 2s 后 A
    cStRun     : Int := 2;
    cStStopSeq : Int := 3;   // 顺向停止: A 先停, 5s 排空后 B
    cStFault   : Int := 4;
    cStEstop   : Int := 5;
END_VAR
VAR_INPUT
    bStart   : Bool;
    bStop    : Bool;
    bEstopNC : Bool;
END_VAR
VAR_OUTPUT
    aBelt  : Array[1..2] of Bool;
    bValve : Bool;
    iState : Int;
END_VAR
VAR
    tonSeq : TON;          // 顺序节拍
    convA  : "fbConveyor";
    convB  : "fbConveyor";
    // silo/packer/shift/alarm 实例同法声明 (见分块清单)
END_VAR

fbLine 代码区 (骨架) 与 fbShift 声明区:

// TODO1: 急停最高优先 (NOT bEstopNC → cStEstop, 清 aBelt/bValve)
// TODO2: CASE iState OF — cStStart: B 先启, tonSeq 2s 后 A → cStRun;
//        cStStopSeq: A 先停, tonSeq 5s (排空) 后停 B → cStStop
// TODO3: 故障传播: 任一 conv 故障 → cStFault, 上游立即停
// TODO4: 产量: packer.iCount 累加进 dbProd, 班次切换时由 fbShift 归档+清零

fbShift 声明要点: 输出 iShift : Int (0/1/2 班) 与 aHist : Array[0..7] of Int (最近 8 班产量, 环形); static 放 dtNow : DTL、iHourIn : Int (可注入的当前小时, 可测性)、iShiftLast : Int、iIdx : Int、bInit : Bool。

提示: 用 RD_LOC_T(OUT => #dtNow); 取本地时间, 按小时判班 (6..13 → 0 班, 14..21 → 1 班, 其余 2 班); 班次号变化瞬间: aHist[iIdx] := 当前班累计, iIdx := (iIdx+1) MOD 8, 累计清零。两点注意: 首扫用 bInit 防误归档; 判班函数把小时做成输入参数 (iHourIn), 测试注入 5/6/13/14/21/22 边界值 — PLCSIM 时钟跟随 PC 时间, 不注入就得在真实时间上等 8 小时。fbAlarm 核心: Array[0..15] of Bool 报警位 + iFirstOut (首出报警) + 复位语义 (报警位随条件消失自动清, 首出须手动复位)。 验收标准: 1) 顺序启动正确 (B 先 A 后, 间隔 2s); 顺序停止正确 (A 先 B 后, 排空 5s); 2) 中段人为置故障: 上游立即停, 下游排空后停; 3) 急停全停且从置位到输出清除不超过一个扫描周期 (输出锁存观察), 注释写明软件急停边界; 4) 班次切换 (注入小时参数测试) 产量正确归档, 旧班数据可查; 5) 报警首出可追溯, 复位语义正确; 6) HMI 上能开停线、看产量与报警 (与 3.5 联动)。 进阶变体: OEE 三要素最小实现 (时间/性能/质量开动率); 料仓自动补料滞回控制; 用 3.4 把 dbProd 暴露给 SCADA; 班次表做成 HMI 可改参数。

附录 A: 每题完成度自查

  • 验收标准逐条通过, 关键代码注释写的是“为什么“而不是“是什么“; L2/L3 每题都自问一遍: 哪些状态放 static, 哪些放 TEMP, 为什么。
  • 每题完成后独立 commit 一次 (TIA V21 起有原生 Git 集成; V18+ 可用 Version Control Interface, 注意 VCI+Git 存不下硬件组态与库类型), 并能在 5 分钟内讲清这道题的块结构与数据流。

附录 B: 事实出处

  • PLCSIM 标准版 vs Advanced 官方对比 (TCP/IP、Trace、Modbus、实例数): https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/cpu-function-manuals/s7-plcsim-advanced/product-overview/s7-plcsim-products
  • PID_Compact V2.x 的 PLCSIM 限制与 EnEstimation/EnMonitoring 设置: https://docs.tia.siemens.cloud/r/simatic_s7_1500_et_200mp_manual_collection_enus_21/comprehensive-information/technology-function-manuals/pid-control/using-pid_compact/pid_compact-as-of-v2/simulating-pid_compact-as-of-v2-with-plcsim
  • 许可 (Trial 21 天、PLCSIM 无需许可、WinCC RT 仿真限 1 小时): https://docs.tia.siemens.cloud/r/en-us/v21/installation/licensing/licensing-step-7-and-wincc
  • TCON_IP_v4 连接描述与 MB_SERVER 的 CONNECT 参数: https://docs.tia.siemens.cloud/r/en-us/v20/modbus-tcp-s7-1200-s7-1500/
  • Basic 面板性能边界 (配方等): https://docs.tia.siemens.cloud/r/en-us/v20/performance-features-basic-panels-panels-comfort-panels-rt-advanced-rt-professional/
  • S7-1200/1500 安全编程指南 (软件急停不等于安全回路的依据): https://assets.new.siemens.com/siemens/assets/api/uuid:1a7ce755-79a1-494e-ae14-5e5f72c29205/safety-programming-guideline-for-simatic-s7-1200-and-1500.pdf

附录 C: 待核实清单汇总

正文标 (待核实) 之处: PLCSIM 单次扫描模式是否随版本提供; NORM_X/SCALE_X 是否不限幅; STRG_VAL 格式位细节; TON 数组变量索引调用在你版本上的实际行为; PID_Compact 具体引脚 (V2.x/V3 差异); Basic 面板配方记录数上限; PLCSIM Advanced 对 S7-1200 G2 的 Modbus 仿真支持。遇到时以你安装版本的指令帮助 (F1) 与官方手册为准。(注: Array[*] 已核实 S7-1200 支持——需优化块 + 固件 >= V4.2, 见 03 章 9.3。)

学习资源清单:S7-1200 + TIA Portal + SCL

本篇是整个学习路线的资料索引,只收录经过核实的条目(核实时间 2026-09),未核实资源一律不收。每条包含:标题(来源/作者)、语言、是否免费、链接、一句话点评(什么阶段用、替代品是什么)。

给程序员的角色定位:你真正需要的是四类东西——系统教材、权威语言参考、问答库、可导入的示例工程。本清单里它们分别对应 SCE 官方模块、TIA Portal Information System、找答案/PLCtalk、官方 Example Blocks。其余条目都是这四个角色的替代品或补充。

目录

    1. 最小核心集
    1. 官方文档(手册与规范)
    1. 工具与门户(检索与下载入口)
    1. 书籍(含免费在线文字教程)
    1. 视频课程
    1. 在线课程与系统培训
    1. 论坛社区
    1. 示例代码
    1. 推荐使用顺序(按学习阶段映射)
    1. 快速答疑地图
    1. 收录说明与待核实项

0. 最小核心集

如果只装四个“依赖“,装这四个(全部免费):

角色资源程序员类比
系统教材SCE TIA Portal Modules(Entry 109972110)官方版免费大学教材,带可打开的工程文件(类比成立;注意它是 PDF 教材而非视频课)
语言参考TIA Portal Information System 的 SCL 分册cppreference(不成立处:它按 TIA 版本分树,查询前先对准自己装的博途版本)
规格书S7-1200 System Manual(Entry 109977302)芯片 Datasheet 加 Reference Manual(类比成立)
问答库找答案(中文)/ PLCtalk(英文)Stack Overflow(不成立处:官方运营,提问需 ONEID 账号,答案按解决状态而非投票排序)

1. 官方文档(手册与规范)

用法:SCL 语法问题一律先查 TIA Portal Information System,硬件与下载问题查 System Manual,代码风格问题查 Styleguide。三者全部免费。

资源语言免费点评(何时用 / 替代品)
S7-1200 System Manual, V4.7 12/2024(Siemens;Entry 109977302;文档号 A5E02486680-AQ)英,附中文 PDF经典 S7-1200 的权威规格书:硬件接线、存储区、Web server、编程支持全在这一本,全程常备;旧条目 109759862/109741593 已标记有更新版,引用时以本条目为准
S7-1200 G2 System Manual, V1.0 01/2025(Siemens;Entry 109972011)2024 年发布、2025 年初上市的新一代 G2 CPU(性能更强,需 TIA Portal V20+)用这本;经典与 G2 两条线手册不能混用;配套官方 Migration Tool(Entry 109986503)。条目已标注有更新版手册(A5E52923928-AA),下载以条目页最新版为准
TIA Portal Information System: SCL for S7-1200/S7-1500(在线文档门户,另有 V21 版本树)SCL 的权威语言参考:语法、指令、数据类型都在这里。西门子已不再出版独立的 SCL PDF 手册,这就是“语言标准库文档“;可整书下载 PDF,写代码时开着随查
Programming Guidelines + Styleguide for S7-1200/S7-1500(Siemens;Entry 81318674;Styleguide PDF 直链西门子官方代码规范(命名、REGION 分区、块结构、可读性),相当于 Google C++ Style Guide;写第一个 FB 之前先通读一遍,从第一天就写工程师风格的 SCL
SCL 编程常见问题大全(TIA Portal)(西门子下载中心官方聚合页)TIA 博途环境下 SCL 的官方中文 FAQ 聚合,语法细节先查这里再看论坛;另有面向 S7-300/400 经典 SCL 的聚合页(materialaggregation_2455.html),经典 STEP 7 与 TIA 的 SCL 有差异,别混用

2. 工具与门户(检索与下载入口)

用法:查官方资料的主入口是 SIOS,核心技能是按 Entry ID 检索(条目 URL 模式 /cs/document/{ID}/{slug});中文资料走 ad.siemens.com.cn 体系。这些门户的直链用脚本访问常被 Akamai 拦 403(反爬,非失效),浏览器访问正常。

资源语言免费点评(何时用 / 替代品)
Siemens Industry Online Support (SIOS)全部官方手册/FAQ/软件下载的官方出处,可按 Entry ID(如 109977302)直达条目;相当于查官方包信息必须会用的 package registry
Siemens SiePortal新一代统一门户(选型+购买+支持),正逐步吸收 Industry Mall 与 SIOS;要认识它,否则旧教程里的链接会越来越对不上;JS 单页应用,需浏览器访问
西门子工业支持中心(中文)总入口中文支持体系主页:下载中心、找答案、技术论坛、1847、培训认证、S7-1200 技术主页都在此导航,技术支持热线 400-810-4288;注意 support.industry.siemens.com/cn 不是中文支持门户的实际形态
西门子下载中心(中文)中文手册/软件/FAQ/操作指南/技术视频下载;S7-1200 EASY PLUS(官方图文 step-by-step 入门参考)在此;S7-1200 资料聚合页在 wap.siemens.com.cn/download/filter?productTypeId=1224,产品入门页在 /productportal/SearchResult.aspx?pid=10

3. 书籍

3.1 中文

资源语言免费点评(何时用 / 替代品)
《西门子S7-1200/1500 PLC SCL语言编程——从入门到精通》(北岛李工;化学工业出版社 2022-01;ISBN 9787122400413;349 页,定价 99 元;豆瓣条目中文世界唯一以 SCL 为主题的专著,15 章三层结构:入门篇(硬件/工作原理/博途/数据类型)、进阶篇(数组指针结构体/高速计数/PWM/PID/运动控制/Modbus/PROFINET)、高级篇(C# 与 SCL 的 TCP/UDP 通信);有 C/Python 背景的读者建议把它当中文主线书,配 81 集 B 站视频形成闭环;作者目录页已迁至 blog.founderchip.com(原 www.founderchip.com 同路径现为迁移提示页)
《S7-1200 PLC编程及应用 第4版》(廖常初;机械工业出版社 2021-07;ISBN 978-7-111-68078-9;定价 69 元;最新印次 2025-06 第 12 次)国内 S7-1200 事实标准教材(西门子重点推荐),第 4 版起新增“SCL语言及其应用实例“章节,配套 50 多个视频、60 多个例程及 V15 SP1 版软件;零基础建立 PLC 体系用;搜索结果显示已有第 5 版(2026-04,ISBN 9787111797647)(待核实),购买前先查机工社页面
《S7-1200/1500 PLC应用技术 第3版》(廖常初;机械工业出版社;ISBN 978-7-111-80642-4;基于 TIA 博途 V18)覆盖面比上一本宽(含 S7-1500、S7-Graph、通信网络、故障诊断),第 5 章为 SCL(含间接寻址与应用举例);作为进阶后的系统参考书

3.2 英文

资源语言免费点评(何时用 / 替代品)
Automating with SIMATIC S7-1200(Hans Berger;Publicis MCD 2018;ISBN 9783895784705;745 页)与目标硬件完全对口的全流程书(硬件组态/PROFINET/编程/诊断,STEP 7 Basic + WinCC Basic);缺点是 TIA 版本停在 2018,界面需与新版博途对照
Automating with SIMATIC S7-1500, 2nd ed(Hans Berger;2017;eText ISBN 978-3-895-78944-1,纸质 9783895784606;基于 STEP 7 Professional V14)英语西门子书的标杆(r/PLC 与 PLCtalk 反复推荐),覆盖 LAD/FBD/STL/SCL;TIA 操作与 SCL 语法对 S7-1200 用户同样适用,且比 S7-1200 版覆盖更新的 TIA 特性
PLC Controls with Structured Text (ST)(Tom Mejer Antonsen;BoD 2019 第 2 版;ISBN 9788743002420;128 页)品牌无关的 IEC 61131-3 ST 实战小书,90 多个代码示例,明确声明适用于 Siemens SCL;讲命名规范、模块划分、LADDER 转 ST;篇幅短,对程序员思维最友好的英文入门书
IEC 61131-3: Programming Industrial Automation Systems(K.-H. John, M. Tiegelkamp;Springer 2010 第 2 版;ISBN 978-3-642-12014-5;390 页)标准语义的学术权威:POU 概念、执行模型、五种语言;想深究 SCL 背后的 IEC 61131-3 标准时读,平时当参考书

3.3 免费在线文字教程(书的低成本替代)

资源语言免费点评(何时用 / 替代品)
西门子SCL编程入门教程连载(知乎)(北岛李工;微信公众号“方正智芯“同步;连载至少 20 篇)与他本人著作同源的免费图文连载(变量/表达式/数组/时钟读写/字符串指令等);买书前先跟连载评估是否需要买书
西门子SCL编程笔记系列(CSDN)(博主 qq_工控_小白;另见《S7-SCL编程指南》汇总帖免费博客补充,适合碎片时间阅读;CSDN 站内搜 “SCL” 有大量单篇实例(如 S7-1200 回原 SCL 代码)可按需检索

4. 视频课程

4.1 Bilibili(中文)

资源(UP主)语言体量免费点评(何时用 / 替代品)
西门子TIA博途SCL语言编程入门到精通全套视频(工控圈)189 集,约 66 小时B 站体量与播放量最高的 SCL 系统课(播放 36.5 万,2024-10 发布);可作主力视频课跟练;搬运性质,需自己配合书筛选重点
《SCL语言编程》配套视频合集(北岛李工)81 集,单集 2-20 分钟与他的书逐章对应,短小精悍带实物演示(含 EPLAN 图纸、步进电机、Modbus-TCP、PROFINET);看书卡住时的可视化补充;注意该链接是合集内单集入口,进入后从合集列表切换到全部 81 集
西门子TIA博途SCL语言编程入门到精通(案例讲解)(电气知识共享)21 集,约 13.5 小时2025-04 新出的案例导向课程,播放 7.7 万且收藏率极高;适合工程师按案例学
SIMATIC S7-1200精英训练营V2.0 SCL编程(押尾光腚朗,官方训练营搬运)27 集,约 6.4 小时按官方训练营大纲快速过一遍 SCL 在 S7-1200 上的知识点;同 UP 主另有 96 集《西门子高级语言SCL培训》(BV1aU4y1Z7iZ,约 53 小时)可作加餐
S7-1200 最全的官网讲解视频(huihui工控,官网视频搬运)98 集,约 10.5 小时西门子官网 S7-1200 讲解视频的系统搬运,覆盖硬件/博途/基础编程(非 SCL 专精);在学 SCL 之前先建立 PLC 与博途整体认知用

4.2 YouTube(英文)

资源(频道)语言免费点评(何时用 / 替代品)
Softwell Automation 频道及其 SCL Step-by-Step 播放列表成体系的免费 SCL 专项播放列表,从零覆盖语法与实例(面向 S7-1200/S7-1500);英文视频起步首选
RealPars 频道英语工控教学口碑最好的频道之一,视频短小精悍;概念建立后查漏补缺用;配套网站有 200 多篇免费文章
TIA PORTAL: Writing your First TIA Code in SCL(Engineering made Easy)单个视频完成从 LAD 思维到 SCL 思维的第一步,时间成本最低的入门材料
SIEMENS TIA Portal SCL Tutorial 播放列表(PLC Tutor)另一条免费 SCL 专题列表(TIA V16、FOR 循环、2D 数组等主题),与 Softwell 列表互补

5. 在线课程与系统培训

先说结论:免费路线用 SCE 模块已足够系统化;付费只建议两类用途——要项目制带教(RealPars/Udemy),或要证书与面授(SITRAIN/SITCC/Coursera)。

资源(机构/讲师)语言免费点评(何时用 / 替代品)
SCE: 000-160 TIA Portal Modules – Learning/Training Documents for Download(Siemens Automation Cooperates with Education;Entry 109972110;总览 Entry 109813970官方免费成体系教材(PDF 教材 + 可直接打开的 TIA 样例工程),从硬件组态到 SCL 模块(052-201 High-Level Language Programming with SCL)循序渐进;零基础系统自学的最重要免费入口,本清单优先级最高的自学资源
西门子 1847 工业学习平台部分免费中文官方视频课最集中的平台,由西门子工程师录制,按产品线组织课程与学习路线,付费会员制;中文快速上手用;系统学 SCL 仍以 SCE 加手册为主
西门子工业培训与认证中心 SITCC官方培训与认证的正规入口(教室课/取证训练营/在线自学课/证书查询/培训伙伴验证);要证书背书(如转岗 PLC)从这里报名,避免第三方机构冒充官方
SITRAIN 课程:SIMATIC S7-1200 系统课程(基于TIA博途),课程号 A7115官方面授系统课的准确课程名与编号,适合预算充足要面授的读者;课程名可直接用作搜索开课计划的检索词;国际对应课程为 “TIA Portal Programming 1”
Siemens PLC Programming Using SCL – Part 1 (TIA Portal)(Udemy,Megha Kumari,2023-06 更新)Udemy 上少数完全以 SCL 为主题的课(FC/FB/DB/OB、数组、字符串,配 Factory I/O 实例);用 S7-1500 演示但 SCL 语法与 S7-1200 相同;评分中等(约 3.7~4.0,随时间漂移),付费前先看试听
Advanced Siemens PLC Programming with TIA Portal masterclass(Udemy)补齐 SCL 之外的 TIA 工程化能力:Arrays/UDT/VARIANT、优化与非优化 DB、OB 与中断、GRAPH、CEM、诊断 Trace、PLCSIM 与 Factory I/O 仿真;适合建立完整工程观
Learn Siemens S7-1200 PLC and HMI via TIA Portal (Advanced)(Coursera,Packt 出品,约 14.5 小时,评分约 4.4)Coursera 上少数直接对口 S7-1200 + TIA 的课(含 “Writing FC in SCL” 章节,覆盖 HMI、RFID、IO-Link、MODBUS、Web Server),有证书;官方 FAQ 说明不可免费旁听,需付费或申请助学金
Siemens Intro to Structured Control Language (SCL) in TIA Portal(RealPars,讲师 Scott Sommer,50 课,评分 4.6)否(7 天免费试用后订阅,约 $50/月)目前唯一针对 SCL 的体系化英文项目制课程(基于 S7-1200 CPU):从语法到阀门反馈程序/CASE 语句/报警/行程计数器完整项目,有结业证书;制作质量高,适合愿为效率付费的读者

6. 论坛社区

中文快问快答去“找答案“,中文长文经验去“技术论坛“,英文社区去 PLCtalk / r/PLC,需要官方口径的英文答案去 SIE Portal 论坛。

资源语言免费点评(何时用 / 替代品)
找答案(西门子官方技术问答社区)中文圈最活跃的西门子官方问答库(已解决问题 25 万+),含 S7-1200 与 S7-1200 G2 独立专区;博途安装/下载/授权/TIS 报错类问题几乎都能搜到现成答案,相当于西门子版 Stack Overflow;提问需 SIEMENS ONEID 账号
西门子技术论坛(中文)S7-1200 版区与找答案互补:论坛偏长文经验分享(大神连载、自写库如经纬度计算日出日落),找答案偏快问快答;站内搜 “S7-1200 SCL” 能找到实战级讨论
中华工控网论坛:西门子版块老牌独立第三方工控社区(下设 S7-1500C、S7-200 SMART 等子版),多年实战帖积累可搜;活跃度低于官方社区,作补充用;老式 GB2312 编码论坛
PLCtalk.net英语世界最老牌 PLC 专业论坛(14 万+主题、99 万+帖);主板块 PLC Questions and Answers 极活跃,Siemens 板块内 SCL/STL 讨论多,搜历史问题基本命中
Reddit r/PLC活跃的英语 PLC 社区,职业视角强(薪资/求职/工具链讨论),适合有软件背景的转岗者;检索技巧:用通用词 Structured Text 搜比 SCL 命中更多
Siemens SIE Portal 官方技术论坛西门子官方英文论坛(原 SIOS 论坛已迁入,11 万+主题);版本迁移/兼容性/报错类问题权威答案多,如 STEP 7 V5.5 SCL 转 TIA Portal 的实例帖;旧链接会重定向

7. 示例代码

读现成 SCL 是学惯用法的捷径,相当于读优秀开源库;官方块集与 GitHub 库都可导入 TIA Portal 编译运行。

资源语言免费点评(何时用 / 替代品)
Example Blocks for WinCC/STEP 7 (TIA Portal) S7-1200/S7-1500 with SCL(官方,Entry 66839614;另有代码片段库 Entry 109747882官方可下载的 SCL 示例块集,可直接导入 TIA Portal 编译运行;片段库含 REGION 结构化等示例;学官方惯用法首选
OttoMeister/Siemens-Tia-Portal-PID-Controller(GitHub,104 stars)OSCAT 风格的 PID/PI 控制器 SCL 源码 + TIA Portal 库文件;西门子 SCL 方向 star 数最高的可导入库之一,代码即教材
LCC-Automation/OpenPID-TIA-SCL(GitHub,17 stars)TIA Portal V19 + SCL 的开源 PID 控制器(含 Ziegler-Nichols 与 Chien-Hrones-Reswick 阶跃自整定),明确面向 S7-1200/S7-1500,结构现代

8. 推荐使用顺序(按学习阶段映射)

假设每天投入 1-2 小时,从零到能独立交付 SCL 工程。每阶段给出主力与辅助资源,阶段间有重叠是正常的。

阶段目标主力资源辅助资源
0. 破冰(1~2 周)建立 PLC、scan cycle、博途界面的整体认知huihui工控 98 集的前几十集 + 廖常初《S7-1200 PLC编程及应用》前几章SCE 基础模块的硬件组态部分
1. SCL 语法(2~3 周)数据类型、表达式、语句、FC/FB/DB 初步北岛李工书入门篇(或其知乎连载)+ TIA Portal Information System 随写随查电气知识共享 21 集(案例讲解)或工控圈 189 集
2. 官方体系化(2 周)按官方教材过一遍并跑通样例工程SCE 模块(含 052-201 High-Level Language Programming with SCL)官方 Example Blocks(Entry 66839614)
3. 工程规范(1 周,此后持续)命名、REGION、块结构、可读性Programming Styleguide(Entry 81318674)Antonsen《PLC Controls with ST》
4. 程序结构与调试(2~3 周)OB/FB/DB 深入、中断、诊断、仿真廖常初《S7-1200/1500 PLC应用技术》第 5 章 + System Manual 诊断章节北岛李工 B 站合集的对应集数
5. 进阶应用(按需)PID、Modbus、PROFINET、上位机通信北岛李工书进阶篇/高级篇 + GitHub OpenPID 源码押尾光腚朗 96 集《西门子高级语言SCL培训》
6. 标准深究(可选)IEC 61131-3 语义、POU、执行模型John & Tiegelkamp(Springer)Berger《Automating with SIMATIC》
全程常备随手查System Manual + TIA Portal Information System找答案 / PLCtalk

两条原则:第一,视频是辅助不是主线——对有 10 年 C/Python 经验的读者,文字材料的信噪比远高于视频,先读后看;第二,每个阶段以“能写出并下载一个可运行的块“为完成标志,而不是以下载了多少资料为标志。

9. 快速答疑地图

遇到问题按症状定位资源,优先级排序:官方文档 > 官方 FAQ > 官方问答社区 > 第三方论坛。

症状 / 问题类型首选备选
某条 SCL 语法、指令、类型转换怎么写TIA Portal Information System(SCL 分册)SCL FAQ 中文聚合页(materialaggregation_1844)
硬件接线、存储卡、LED 诊断含义、Web serverSystem Manual(Entry 109977302)中文下载中心的 EASY PLUS
新到手的是 G2 CPU,行为和教程不一致G2 System Manual(Entry 109972011)+ Migration Tool(109986503)找答案 S7-1200 G2 专区
博途安装、授权、下载报错(如 TIS 功能报错)找答案下载中心 FAQ、SIE Portal 论坛
看不懂报错码或诊断信息SIOS 按报错文本/Entry ID 检索找答案、PLCtalk
命名、REGION、块结构怎么组织Programming Styleguide(Entry 81318674)Antonsen 书、官方 Example Blocks 的实际写法
想找现成 SCL 代码参考官方 Example Blocks(Entry 66839614)GitHub OpenPID、OttoMeister PID
看书卡住,需要看人操作一遍B 站对应章节(北岛李工合集/工控圈)1847、YouTube Softwell 播放列表
要用英文提问或搜英文历史问题PLCtalkr/PLC(关键词改用 Structured Text)
旧程序迁移(STEP 7 V5.x SCL 转 TIA)SIE Portal 论坛实例帖SIOS 条目检索
PID 或控制算法怎么实现GitHub OpenPID + 北岛李工书进阶篇PLCtalk 历史帖
需要证书或系统面授SITCC / SITRAIN(A7115)Coursera(自学型证书)

10. 收录说明与待核实项

  • 本清单全部条目来自 2026-09 完成核实的资源调研;除单独标注外,链接、版本号、统计数字均按核实结果记录。
  • 廖常初《S7-1200 PLC编程及应用》第 5 版(2026-04,ISBN 9787111797647):搜索结果显示存在,机工社页面未直接核实(待核实),购买前先确认。
  • S7-1200 G2 System Manual 条目(109972011)已标注有更新版手册(文档号 A5E52923928-AA);下载时以条目页当前最新版为准。
  • 西门子多个站点(SIOS、SiePortal)直链用脚本访问常被 Akamai 403 拦截,属反爬而非失效,浏览器访问正常。
  • 西门子国际支持体系正从 SIOS 向 SiePortal 迁移,Industry Mall 与 Industry Online Support 也在整合;旧教程链接可能重定向,认准 Entry ID 即可。
  • B 站北岛李工合集入口(BV1Q3411P7jD)是合集内单集,进入后从合集列表切换全部 81 集。
  • Udemy/Coursera/RealPars 的评分与价格随时间漂移,付费前以课程页为准。

全文搜索

Pagefind 提供的静态全文索引(Rust 实现), 支持中文分词, 也索引代码标识符(如 TIME_TCKVAR_IN_OUTNORM_X)。

提示:

  1. 索引由 mdBook 的自定义渲染后端(Rust, 见 pagefind-renderer\)在每次构建时自动生成, 包括 mdbook serve 的热重建 —— 无需手动步骤。
  2. 直接双击打开 book\html\search.html(file:// 协议)会被浏览器拦截索引请求, 请通过 tools\mdbook.exe serve 访问。