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.js | SCL 语法定义(注册进 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-环境搭建.md | TIA 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-常见陷阱与最佳实践.md | 27 个陷阱(现象/根因/正确写法/C 对照) + 命名/UDT/状态机骨架/版本管理/下载语义 | 第 4 周先通读一遍, 之后每次踩坑回查 |
| 06-练习项目阶梯.md | L0~L3 共 13 题: 起保停 → 定时/计数 → 滤波/解析/排序 → 交通灯/电机FB/PID/Modbus/HMI/小产线 | 与各阶段同步做, 是主线 |
| 07-学习资源清单.md | 45 条已核实资源: 官方文档/书/视频/课程/论坛/示例代码 + 使用顺序 + 答疑地图 | 随时查 |
学习路线图
阶段 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)。
十条速记(反直觉事实, 现在就记住)
- 赋值是
:=, 比较是=——与 C 相反; 没有==。 - 看门狗默认 150 ms 恒启用(1~6000 ms 可配): 阻塞循环两次超时直接 STOP, PLC 没有 sleep。
- S7-1200 没有断点调试(断点是 300/400/1500 的能力); 调试 = Watch table + 程序状态 + Trace + 诊断缓冲。
TIME_TCK()、REF 引用、POINTER/ANY、LTIME、SIMATIC 遗留定时器都是 S7-1500 专属, 1200 编译报错; 测耗时用RUNTIME()。Array[*]变长形参 1200 支持, 但要优化块 + 固件 ≥ V4.2(FC 仅 Input/InOut 区)。- 除零不炸: 结果 0 + ENO=0 + 继续 RUN; 数组越界在 1200 上也只记诊断缓冲——错误哲学是“留痕不中断“。
- SCL 布尔表达式不短路:
i <= n AND a[i] > 0的守卫无效, 必须拆嵌套 IF。 - TIA 新建 DB 默认优化访问: 没有 % 地址、PEEK/POKE/AT/通信映射都要改用标准 DB。
- PLCSIM(标准版)不仿真 Modbus TCP/真实 TCP/OPC UA/Web server/Trace; PID_Compact V2.x 不能在一代 1200 上仿真; PLCSIM Advanced 不支持一代 1200。
- 标识符大小写不敏感:
Motor与MOTOR是同一个变量(重名报错)。
学习心法(给程序员的)
- 文字优先, 视频辅助: 对你而言官方手册与教材的信噪比远高于视频; 卡在“看不见的操作“时再看对应视频(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 Portal | Visual Studio / PyCharm(IDE + 工具链一体) | 基本成立, 但 TIA 还兼任硬件组态(HMI、网络、模块) |
| SCL 语言 | C / Python(文本语言) | 成立; SCL 属 IEC 61131-3 Structured Text 方言 |
| S7-PLCSIM 仿真器 | QEMU / 模拟器 | 部分成立: PLCSIM 是指令级仿真, 非周期精确, 高速硬件不仿真 |
| OB / FB / DB | main() / class / object | 粗略成立, 细节差异后文逐条指出 |
| scan cycle | while(1) { read_input(); run(); write_output(); } | 成立, 这是最重要的一条心智模型 |
| CPU watchdog | 看门狗定时器 | 成立: 单周期超时即停机, 不像 PC 程序可以慢慢算 |
目录
- 软件选型: STEP 7 Basic 够不够
- 授权与获取: 正版 / 21 天试用 / 教育渠道
- 安装实操与常见坑
- 仿真: S7-PLCSIM 能做什么、不能做什么
- 无官方环境时的替代: CODESYS / OpenPLC
- 可选硬件: CPU 选型、供电与接线常识
- 第一个项目分步走查(精确到菜单名)
- 调试工具速览
- 验收 checklist
- 参考链接汇总
1. 软件选型: STEP 7 Basic 够不够
1.1 Basic vs Professional
TIA Portal 里的编程软件叫 STEP 7, 分两个档位。官方定义: STEP 7 Basic 是 STEP 7 Professional 的 “price-optimized subset”(价格优化子集)。
| 能力 | STEP 7 Basic | STEP 7 Professional |
|---|---|---|
| S7-1200 编程(含 SCL) | 支持 | 支持 |
| S7-1500 编程 | 不支持 | 支持 |
| S7-300/400(经典系列) | 不支持 | 支持(注: V20 起迁移经典工程改用独立迁移工具) |
| WinCC Basic(组态 KTP Basic 触摸屏) | 自带 | 自带 |
| WinCC Advanced/Professional(PC 级 HMI) | 不含 | 含 |
| 仿真对象 | 仅 S7-1200 | S7-1200 + S7-1500 等 |
结论: 只学 S7-1200 的 SCL, Basic 完全够用, 而且试用版(Trial)直接给的是全家桶 Professional, 学习期内你甚至感知不到差别。
1.2 TIA Portal 版本选择
一年一个大版本, 近年发布节奏(V17 之前同理):
| 版本 | 发布时间 | 备注 |
|---|---|---|
| V17 | 2021 | 大量中文教材基于 V15~V17 |
| V18 | 2022 | |
| V19 | 2023-11 | |
| V20 | 2024-11 | 首个支持 S7-1200 G2 的版本; PLCSIM Basic 更名 S7-PLCSIM(Standard)并换新 UI |
| V21 | 2025-11/12 | 2026-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 数字(官方全文核实):
| 项 | 最低 | 推荐 |
|---|---|---|
| CPU | Intel Core i3-6100U | Core i5-12600HE 级别 |
| 内存 | 8 GB | 16 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, 全家桶。
- 获取步骤:
- 注册免费的 Siemens Industry Online Support(SIOS)账号。
- 打开 SIOS Entry 109989774 “SIMATIC STEP 7 incl. Safety, S7-PLCSIM and WinCC V21 TRIAL Download”。
- 下载(体量大, 十 GB 级, 留足磁盘与时间)。
- 安装后首次启动时按提示激活 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
- 确认 Windows 版本在目标 TIA 版本支持列表(见 1.3), Home 版注意只能装 Basic。
- 磁盘留 50 GB 以上(最低要求 20 GB, 全家桶装完 40 GB+ 很常见), 装在 SSD。
- 关闭杀毒软件实时防护(或把安装目录加白名单): 杀软扫描会显著拖慢甚至中断安装。
- Windows 更新做完并重启一次; 暂停其他后台安装任务。
- 用户名、安装路径、后续项目路径全部用英文短路径, 例如
C:\TIA、C:\Projects。Windows 用户名含中文/特殊字符是高频安装失败原因。 - 用管理员权限运行
setup.exe。 - 笔记本接电源, 避免安装中断电。
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 ST | TIA 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 / REFERENCE | S7-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:
- 把 GVL 拆成 PLC tag table(管 IO 地址)与 Global DB(管数据)。
- FB 调用点全部改写为 instance DB 形式, 补建 instance DB。
%IX0.0风格地址改为%I0.0; 位/字节序访问方式核对。- 删掉 CODESYS 特有(AT 覆盖、继承、指针、REF_TO)代码, 用 TIA 等价物重写。
- 逐块编译, 优先处理 Optimized access 与非优化访问差异。
6. 可选硬件: 想买也没问题
纯学习不买硬件完全可行(本文主线即仿真)。要买, 看本节。
6.1 CPU 选型对比(经典一代, DC/DC/DC 型号)
| CPU | 板载 DI/DO | 板载 AI/AO | PRONET 口 | 定位 | 新品价格区间(2026-09 检索, 波动大) |
|---|---|---|---|---|---|
| 1211C | 6/4 | 2/0 | 1 | 最小实验 | 约 US$178-251(第三方/eBay) |
| 1212C | 8/6 | 2/0 | 1 | 入门 | G2 版约 138 欧元净价 |
| 1214C | 14/10 | 2/0 | 1 | 学习/小项目主力(教程事实标准) | 欧洲 366-478 欧; 美国 $350-378; 国行渠道约 5084 元(未税) |
| 1215C | 14/10 | 2/2 | 2(内置交换机) | 中型, 自带 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 二手注意
- 固件版本: 设备组态里目标固件要匹配 CPU 实际固件; 太旧的 CPU(如 V2.x 硬件)无法升级到 V4 特性, 老 TIA 工程也打不开新特性。先问卖家固件版本。
- 密码与 know-how 保护: 被 CPU 级密码保护或块加密的二手 CPU 很难恢复(整体复位出厂设置可去密码, 但块保护仍可能在源工程里, 你拿不到工程)。
- 检查: PROFINET 口是否完好、端子排螺丝是否滑丝、外壳挡板是否缺失、是否进过水/粉尘。
- 一代停产传闻若属实, 二手一代件会更充裕, 价格更低, 学完再出掉损失不大。
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 仿真设备“条目。
- Portal 视图点 Devices & Networks > Add new device。
- 点 PLC 按钮, 展开 SIMATIC S7-1200 > CPU 1214C DC/DC/DC (6ES7214-1AG40-0XB0), 固件选 V4.4, 勾选 Open device view, 点 OK。
- 项目树出现
PLC_1。
7.3 新建 SCL 语言的 FB
- 项目树展开 PLC_1 > Program blocks, 双击 Add new block。
- 选 Function block (FB), Name 填
FB_Motor, Language 选 SCL, 点 OK。生成FB_Motor [FB1]并打开编辑器。
接口声明(编辑器上方的接口表, 相当于 class 的成员声明区; C 类比: Input=const 形参, Output=出参, InOut=指针形参, Static=私有成员, Temp=局部变量):
| Name | Data type | 区(Interface) | Comment |
|---|---|---|---|
| Start | Bool | Input | 启动按钮(点动) |
| Stop | Bool | Input | 停止按钮(点动) |
| Motor | Bool | Output | 电机输出 |
代码区:
// 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, 双击打开, 添加三行:
| Name | Data type | Address |
|---|---|---|
| Start | Bool | %I0.0 |
| Stop | Bool | %I0.1 |
| Motor | Bool | %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); 点击块引脚上的 ??.?.?? 占位符, 依次输入 Start、Stop、Motor 并回车绑定。
做法 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
- 工具栏 Download to device(或菜单 Online > Download to device)。
- 弹出 Load preview, 列出将下载的块, 点 Load。
- 若提示模块启动状态, 选 Start modules / RUN, 点 Finish。
- PLCSIM 窗口确认 CPU 处于 RUN。
下载失败先查: PG/PC 接口是否指向 PLCSIM(通常自动); CPU 是否被前一次在线会话占用(关闭旧的在线连接)。
7.9 Watch table: 改输入、看输出
- 项目树 PLC_1 > Watch tables > Add new watch table, 双击打开。
- Name 列依次输入
Start、Stop、Motor(自动补全 tag)。 - 菜单/工具栏 Go online 切到在线; 点 Monitor all(眼镜图标)开始监视。
- 在
Start行的 Modify value 列输入1(TRUE), 点工具栏 Modify all(一次性写入); 观察 Motor 变 1。 - 把
Start改回0: Motor 保持 1(自保持生效 —— 状态在 instance DB 里, 不依赖输入保持)。 - 把
Stop置1: 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 buffer | CPU 级事件日志: 停机原因、块错误、上电记录 | 类比 dmesg; CPU 停机后第一件事看这里 |
9. 验收 checklist
学完本章, 以下每条都应能独立完成(不看本文):
- 说出 STEP 7 Basic 与 Professional 对学 S7-1200 的实际差别(结论: 无差别)。
- 从 SIOS 下载 Trial 并完成安装, 知道 21 天一次性、不可延长的规则。
- 独立排掉重启循环安装故障(PendingFileRenameOperations 注册表解法)。
- 新建项目(短英文路径)、添加 CPU 1214C、新建 SCL FB、在 OB1 以 instance DB 方式调用、编译到 0 error —— 全程 30 分钟内。
- 启动 PLCSIM, 下载, 切 RUN。
- 用 Watch table 的 Modify 驱动起保停的全部三种状态(启动/保持/停止), 并解释为什么 Start 松开后 Motor 还是 1。
- 在 SCL 编辑器里打开程序状态监视并读值。
- 打开诊断缓冲区并读懂一条事件。
- 列出标准 PLCSIM 不可用的功能(Trace、HSC/PWM、真实 TCP/IP、Modbus TCP、一代 1200 的 PID_Compact 仿真)。
- 解释 Modify 与 Force 的差异, 以及“仿真里能站住的输入修改, 真机上为什么站不住“。
- 说出
#Motor与"Motor"的作用域区别。 - (可选硬件线)说出 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. 超过最大循环时间则按规则处置
}
关键语义:
- 读输入: 周期开始时, OS 把所有物理输入采样到输入映像表 (I 区)。用户程序读
%I0.0读的是这份快照, 不是实时电平。 - 执行: OB1 从第一行到最后一行完整执行。期间读到的输入永远是本周期开头那份快照; 对
%Q的赋值只改输出映像, 不立刻驱动端子。 - 写输出: 周期结束时 OS 把输出映像一次性刷到端子。即使程序中间对同一个输出赋值 10 次, 端子上只会看到最后一次的值——这是与“每条语句立即生效“的 MCU 直操 GPIO 的关键差异。
- 中断 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 直觉全部失效的地方):
- 程序没有“跑完“这个概念。 每个周期都是一次完整重入。任何需要跨周期的信息 (计数器、状态机当前态、定时器累计值) 必须放在持久位置: M 区、DB、FB 的静态变量——绝不能放
VAR_TEMP(见 6.5)。 - “等待“不能靠 sleep。
time.sleep()会吃掉整个看门狗预算。等待 = 用 TON 定时器描述“条件随时间的变化”, 本周期判一次, 不满足就结束本轮, 下周期再来——本质是状态机, 不是阻塞。 - 短事件可能被错过。 按钮按下 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 近似 |
|---|---|---|---|
BOOL | 1 位 | TRUE/FALSE | bool |
BYTE | 8 位 | 16#00~16#FF, 按位串解释, 无符号语义 | uint8_t |
WORD | 16 位 | 16#0000~16#FFFF 位串 | uint16_t |
DWORD | 32 位 | 位串 | uint32_t |
SINT / USINT | 8 位 | -128~127 / 0~255 | int8_t/uint8_t |
INT | 16 位 | -32768~32767 | int16_t |
UINT | 16 位 | 0~65535 | uint16_t |
DINT | 32 位 | -2147483648~2147483647 | int32_t |
UDINT | 32 位 | 0~4294967295 | uint32_t |
REAL | 32 位 | IEEE 754 单精度, 约 ±1.18e-38~±3.40e38 | float |
LREAL | 64 位 | IEEE 754 双精度 | double |
TIME | 32 位 | 有符号毫秒: T#-24d_20h_31m_23s_648 ~ T#+24d_20h_31m_23s_647 (约 ±24.8 天溢出) | int32_t(毫秒语义) |
DATE | 16 位 | 1990-01-01 ~ 2168-12-31 | — |
TIME_OF_DAY (TOD) | 32 位 | 当日毫秒数 00:00:00.000~23:59:59.999 | — |
DTL | 12 字节 | 1970-01-01 ~ 2262-04-11, 含年月日时分秒毫秒/星期 | struct tm |
CHAR / WCHAR | 8/16 位 | ASCII / UCS-2 | char/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结尾; 与 Pythonstr的区别: 定长、单字节字符、可变而非不可变对象。
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 上的编号与支持 | 默认优先级 |
|---|---|---|---|
| 程序循环 | OB1 | OB1, 也可再建多个循环 OB (编号 >=123, 依次执行) | 1 (最低) |
| 启动 | OB100 | OB100 或 >=123, 可多个 | 1 |
| 日时间中断 (time-of-day) | OB10 | 1200 文档中未见 OB10 编号, TIA 对话框新建默认给 >=123; 最多 2 个事件 | 2 |
| 延时中断 | OB20~23 | OB20-23 或 >=123; 与循环中断合计最多 4 个 | 3~6 |
| 循环中断 (cyclic interrupt) | OB30~38 | OB30-38 或 >=123; 时基 1~60000 ms; 与延时合计最多 4 个 | 8~17 |
| 硬件中断 | OB40 | 编号 >=123 (最多 50 个事件挂接) | 18 |
| 诊断错误中断 | OB82 | 支持; 未组态 OB82 时 CPU 忽略诊断错误保持 RUN | 5 |
| 模块拔/插 | — | 支持 (编号 >=123) | 6 |
| 机架/站故障 | OB86 | 支持 (PROFINET 站掉线等) | 6 |
| 时间错误 | OB80 | 支持 | 22 (可改 22~26) |
| MC-Servo / MC-Interpolator | — | 运动控制 Technology Objects 内部使用 | 25 / 24 |
| 编程错误 OB121 | — | 1200 上不存在, 用 GET_ERROR/GET_ERROR_ID 局部处理 | — |
| I/O 访问错误 OB122 | — | 1200 上不存在, 记诊断缓冲保持 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 转换溢出/NaN | ENO=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"), 代码里一律用符号。注意标识符不区分大小写: Motor 与 MOTOR 是同一个 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 的能力)。间接访问三板斧:
- 数组变址:
#v := "Gbl_Data".aiBuf[#i];下标可用 Int/DInt 变量, 支持多维[i, j]。 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);
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 模式, 循环执行 |
| STOP | 黄 | STOP 模式 |
| 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 / Python | S7-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 struct | UDT (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 | 不存在 | 一切静态分配, 编译期定死 |
| 异常 / errno | ENO + 诊断缓冲 | 不崩溃, 见 7.3 |
| 断点调试 | 1200 无断点; watch/force/status | PLCSIM 也无 |
| devicetree | 硬件组态 (device config) | 声明式硬件描述 |
12. 类比何时失效 (重要: 每条都是真事故来源)
- “程序会跑完/会退出“失效。 PLC 程序是无限重入的循环体。把“一次性初始化“写成裸代码而不放进 OB100, 它会每周期执行一遍; 把跨周期状态放
VAR_TEMP, 下周期读到的是随机残留。 - 对象生命周期失效。 C++ 栈对象离开作用域析构, Python 靠 GC; 背景 DB 永不析构。“未调用 = 冻结而非销毁”——定时器不走也不清零, 恢复调用时从冻结点继续 (见 7.2)。FB 也没有 constructor, 初值只在下载/重启时生效。
- “回调/事件不丢“失效。 没有框架替你做 diff 与排队; 每周期全量执行意味着边沿要自己检 (
R_TRIG), 比扫描周期更短的脉冲会丢, 除非上硬件中断。 - 异常处理模型失效。 除零不抛异常: 结果 0 + ENO=0 + 继续 RUN; 数组越界在 1200 上不 STOP, 静默读垃圾。不检查 ENO, 错误值会混进 PID 与 HMI。没有 try/catch, 局部错误用 GET_ERROR 结构化捕获。
- 指针与动态内存失效。 无
*、无指针算术、无 malloc、无递归;VAR_IN_OUT只有“引用“语义; 间接访问走数组变址/PEEK/POKE/VARIANT。 - “阻塞等待“失效。
sleep/await/长循环都会吃掉看门狗预算, 两次超时即 STOP; 等待必须改写成定时器驱动的状态机。 - 多线程直觉部分失效。 没有线程, 但中断 OB 与 OB1 是真实的并发单元: 两者共享的 DB 数据有一致性问题 (读了一半被改), 需要像对待 ISR 共享数据一样设计访问次序。没有 mutex, 靠优先级与结构安排。
- “断点单步“失效。 S7-1200 连仿真器都没有断点; 调试 = watch table + status + PLCSIM + 诊断缓冲。习惯 printf 的人转 trace/trace table (若 CPU 支持) 或把状态导出到 HMI。
- 内存布局直觉失效。 优化块 (默认) 没有稳定地址, 编译器会重排; 按字节偏移对外映射必须显式用标准块。别拿 C 的 struct 布局/对齐直觉去猜 DB 偏移。
- 大小写与命名失效。 tag 名不区分大小写 (
Motor==MOTOR), 与 Python/C 的标识符敏感性相反; 保留字 (如Int) 不能作变量名。 - 精度直觉部分失效。
TIME是 32 位毫秒整数, 约 ±24.8 天回绕——长期累计计时要用 DTL 或自行进位;REAL是单精度, 别拿 double 的精度预期写控制算法。 - 安全责任不类比。 桌面软件崩溃重启即可; 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 的基本分工。本文只关注“语言本身“。
目录速览
- SCL 是什么: 定位与心法
- 词法与字面量
- 变量声明: 接口区、初始化、ARRAY/STRUCT/STRING
- 运算符与表达式 (含类型转换)
- 控制流
- FC 与返回值
- FB 与调用
- 定时器与计数器 (重点)
- 高级类型与机制: VARIANT / ARRAY[*] / PEEK/POKE / AT / 位访问
- 字符串实战
- 时间与日期
- 运行时错误与错误处理
- 字节序与位操作
- 附录A: C/Python ↔ SCL 语法速查大表
- 附录B: 同一状态机的 C 与 SCL 双版本
- 附录C: 常见编译报错与解决方法
1. SCL 是什么: 定位与心法
1.1 语言血统: IEC 61131-3 ST, Pascal 家族
SCL 是 IEC 61131-3 标准中 ST (Structured Text) 语言的西门子方言。ST 的语法血统是 Pascal 而不是 C:
- 语句用关键字块封闭 (
IF ... END_IF), 不用花括号; - 赋值是
:=, 相等比较是=(Pascal 传统); - 标识符不区分大小写;
- 函数声明与变量声明分区书写, 声明区和语句区严格分离。
对 C/Python 程序员来说, SCL 读起来像“去掉了指针、加上 PLC 数据类型的 Pascal“。它是完全编译型语言: 编译成 S7 CPU 的本地机器码, 没有解释器、没有虚拟机、没有运行时异常机制 (try/catch 不存在, 见第 12 节)。
1.2 大小写不敏感 (官方行为)
标识符、关键字全部不区分大小写: Motor、MOTOR、motor 是同一个变量; if、IF、If 都是合法的 IF 关键字。官方文档明确: PLC 变量名的唯一性检查不区分大小写。因此 Motor 和 MOTOR 不能同时声明, 编译器按重复标识符报错。
工程实践: 团队内统一一种风格 (本文用关键字全大写、变量首字母小写驼峰), 不要靠大小写区分含义 —— 那在 SCL 里是编译错误。
1.3 C 程序员 60 秒上手对照表
| 你想做的事 | C 写法 | SCL 写法 |
|---|---|---|
| 赋值 | x = y; | x := y; |
| 相等比较 | x == y | x = y |
| 不等比较 | x != y | x <> y |
| 分支 | if (c) { } else if () { } else { } | IF c THEN ; ELSIF THEN ; ELSE ; END_IF; |
| 多分支 | switch (x) { case 1: ... break; } | CASE x OF 1: ... END_CASE; (无 fallthrough) |
| 计数循环 | for (i=0; i<n; i++) { } | FOR i := 0 TO n-1 BY 1 DO ; END_FOR; |
| 条件循环 | while (c) { } | WHILE c DO ; END_WHILE; |
| 后测试循环 | do { } while (!c); | REPEAT ; UNTIL c END_REPEAT; |
| 跳出/下一轮 | break; / continue; | EXIT; / CONTINUE; |
| 返回 | return v; | 块名 := v; RETURN; |
| 逻辑与/或/非 | && || ! | AND OR NOT (AND 也可写作 &) |
| 位与/异或/取反 | & ^ ~ | AND XOR NOT (同一套关键字, 按操作数类型自动按位) |
| 取模 | a % b | a MOD b |
| 幂 | pow(a,b) | a ** b |
| 行注释/块注释 | // /* */ | // (* *) |
| 空语句 | ; | ; |
| 类型前缀字面量 | 1.0f、0x1FUL | REAL#1.0、WORD#16#00FF |
1.4 类比边界: 哪些 C/Python 直觉不能用
类比是学习加速器, 但以下边界必须现在划清:
- FB ≈ class, 实例 DB ≈ object: 类比大体成立 (见第 7 节), 但 FB 没有构造函数; 更重要的是“不调用 FB“是合法且常见的控制手段, 此时其内部状态冻结在最后一次调用的样子 (C++ 对象不存在“不调用“的概念)。
- VAR_TEMP ≈ 未初始化的 C 栈变量: 类比成立且被官方文档强化 —— 官方明确警告临时变量“使用前必须初始化, 否则值可能是随机的“。
- 没有短路求值:
A AND B不保证先算 A、B 为副作用表达式时不会省略 (SCL 里赋值根本不是表达式, 但函数调用是, 两个都会被求值)。C 的if (p != NULL && p->x)模式在 SCL 无对应物。 - 没有 ++/–、没有三元 ?:、赋值不是表达式:
x := y := 0;非法;a = c ? b : d要写成 IF/ELSE 或独立 FC。 - 除法语义同 C99, 不同于 Python:
7 / 2 = 3(整除截断),-7 / 2 = -3(Python 的//向下取整得 -4);MOD结果符号同被除数,-7 MOD 3 = -1(C 同, Python 得 2)。 - 没有堆: 所有内存在编译期确定, 无 malloc/new/GC; “动态数据结构“只能用定长数组 + 计数器模拟。
- 程序不会“结束“: OB1 每个 scan cycle 被操作系统重新调用一次, 更像 Arduino 的
loop(); 顶层代码不是一次性的main。
2. 词法与字面量
2.1 标识符规则
- 组成: 字母、数字、下划线; 不得以数字开头。建议只用 ASCII。
- 保留字 (关键字如
IF、FOR、MOD、指令名如TON、类型名如INT) 不得用作变量名。 - 含空格/特殊字符/中文的标识符必须整体用双引号包裹 (如
"我的变量#1"), 这是为 HMI 兼容保留的逃生门, 日常不要用。 - 大小写不敏感 (见 1.2)。
- TIA 的 SCL 编辑器会自动为块内变量加
#前缀、为全局符号加双引号; 手写外部源 (External source) 时要自己写对。
2.2 注释
// 单行注释: 到行尾结束
(* 块注释: 可跨多行,
且 (* 可以嵌套 *) 像这样 *)
块注释 (* ... *) 支持嵌套, 这点与 C 的 /* */ (不可嵌套) 不同。// 与 (* *) 可以互相包裹。
2.3 数值字面量
| 写法 | 含义 | 十进制值 |
|---|---|---|
42 | 十进制整型 (类型由上下文推断) | 42 |
-42 | 负十进制 | -42 |
2#1010_1010 | 二进制, _ 仅作可读性分隔 | 170 |
8#777 | 八进制 | 511 |
16#FF | 十六进制 | 255 |
16#DEAD_BEEF | 十六进制 + 分隔符 | 3735928559 |
1_000_000 | 十进制 + 分隔符 | 1000000 |
3.14 | REAL (必须有小数点) | 3.14 |
INT#300 | 带类型前缀的 INT | 300 |
DINT#100000 | 带类型前缀的 DINT | 100000 |
REAL#1.5 | 带类型前缀的 REAL | 1.5 |
WORD#16#00FF | 类型前缀 + 进制前缀组合 | 255 |
BOOL#TRUE / TRUE | 布尔 | 真 |
两个高频坑:
- 未定型字面量的推断:
#w := 16#FFFF;若#w是 WORD 没问题; 但#i := 16#FFFF;(#i为 INT) 会因超出 INT 范围报错。写位模式请养成用类型前缀的习惯 (WORD#16#FFFF), 这相当于 C 的(uint16_t)0xFFFF之于裸0xFFFF。 - 进制前缀是
基数#, 不是 C 的0x/0b。
2.4 TIME 与日期时间字面量
// TIME: 32 位有符号毫秒数 (本质是 DINT 的语义包装)
T#5ms // 5 毫秒
T#2s500ms // 单位必须从大到小排列: d > h > m(分钟!) > s > ms
T#1h30m15s // 1 小时 30 分 15 秒
TIME#10s // T# 与 TIME# 等价
T#25d // 接近上限; 合法范围约 ±24d20h31m23s647ms
// 注意 1m 是 1 分钟 (不是 1 米), 与 C 的 m 后缀习惯不同
// 负时长: 不要写 T#-5s (编译器拒绝; 个别版本行为待核实),
// 负值由运算产生: #t := 0 - T#5s; TIME 值域本身包含负数
// 日期/时间
D#2026-09-05 // DATE (16 位, 1990~2168 年)
DATE#2026-09-05 // 等价
TOD#13:45:30.123 // TIME_OF_DAY, 时:分:秒.毫秒
TIME_OF_DAY#06:00:00 // 等价写法
DTL#2026-09-05-13:45:30 // DTL 字面量, 注意年月日之间用 '-' 连成一段 (小数秒位数写法以编译器接受为准, 待核实)
2.5 字符与字符串字面量
'C' // CHAR 字面量, 单引号
'Hello' // STRING 字面量, 同样是单引号 (不是 C 的双引号!)
'It''s' // 串内的单引号写两遍转义 (相当于 C 的 \')
没有 C 风格的 \n、\t 转义序列; 需要控制字符时用字符码转换或 CHAR 变量运算。STRING 的内存布局见第 10 节。
2.6 三种限定符: #、"名字"、%地址
SCL 里访问一个变量, 由前缀说明它“住在哪里“:
| 前缀/形式 | 含义 | 例子 |
|---|---|---|
#名字 | 当前块内声明的变量 (接口区/临时/常量) | #counter, #tonDelay.Q |
"名字" | 全局作用域: 全局 DB 及其成员、块名、UDT 名 | "DataDB".setpoint, "MyFC"(...), "MotorCtrl" |
%I../%Q../%M.. | 绝对地址 (过程映像输入/输出/位存储区) | %I0.0, %QW4, %MD100 |
绝对地址的完整写法:
%I0.0 // 输入位 (等价 %IX0.0); %IB0 字节, %IW0 字, %ID0 双字
%Q0.3 // 输出位
%M0.0 // 位存储区位; %MB0, %MW0, %MD0
组合访问路径可以串起来, 类似 C 的结构链式取成员:
"ProductionDB".batches[3].weight // DB 内数组的结构成员
"ProductionDB".batches[3].flags.X2 // 再加位片访问 (见 9.6)
绝对地址是向后兼容的遗产机制 (来自 S5/S7-300 时代): 新项目应优先用符号名 + 全局 DB; %M 区仅在对接老程序或 HMI 强制要求时使用。
3. 变量声明
3.1 块骨架: 声明区与代码区严格分离
SCL 每个代码块 = 声明区 (interface) + BEGIN + 语句区。三种块的骨架:
// ---------- FB: 有静态变量 (状态), 需要实例 ----------
FUNCTION_BLOCK "MotorCtrl"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
running : BOOL;
END_VAR
VAR // 静态变量: 状态, 存入实例 DB
tonDelay : TON; // 系统类型实例声明 (见 3.10)
stepCnt : INT;
END_VAR
VAR_TEMP
i : INT;
END_VAR
VAR CONSTANT
MAX_RETRY : INT := 3;
END_VAR
BEGIN
// 语句区
#tonDelay(IN := #cmd, PT := T#3s);
#running := #tonDelay.Q;
END_FUNCTION_BLOCK
// ---------- FC: 无状态, 可有返回值 ----------
FUNCTION "Clamp" : INT
VAR_INPUT
v, mn, mx : INT;
END_VAR
VAR_TEMP
r : INT;
END_VAR
BEGIN
IF #v < #mn THEN #r := #mn;
ELSIF #v > #mx THEN #r := #mx;
ELSE #r := #v;
END_IF;
"Clamp" := #r; // 函数名当左值 → 返回值 (见第 6 节)
END_FUNCTION
// ---------- OB: 程序入口 (由 PLC 操作系统调用) ----------
ORGANIZATION_BLOCK "Main" // OB1
VAR_TEMP
x : REAL;
END_VAR
BEGIN
;
END_ORGANIZATION_BLOCK
TIA 生成代码时会插入
{ S7_Optimized_Access := 'TRUE' }之类的块属性行, 属于工程配置不是语言逻辑, 本文示例省略。
3.2 接口区总表: 哪种块能声明什么
| 声明区 | FB | FC | OB | 存放位置 | 类比 |
|---|---|---|---|---|---|
VAR_INPUT | 是 | 是 | 是 (仅事件信息) | 调用时的实参副本 | 函数形参 (值语义) |
VAR_OUTPUT | 是 | 是 | 否 | 实例 DB / 调用方 | 输出形参 (引用回写) |
VAR_IN_OUT | 是 | 是 | 否 | 调用方变量本体 | C 的 T* 指针形参 |
VAR (静态) | 是 | 否 | 否 | 实例 DB | 类的成员变量 |
VAR_TEMP | 是 | 是 | 是 | L 栈 (不保存) | 未初始化的栈变量 |
VAR CONSTANT | 是 | 是 | 是 | 编译期常量 | const/constexpr |
| Return (返回值) | 否 | 是 | 否 | - | 函数返回值 |
关键结论: FC 和 OB 没有静态变量, 因此不能拥有状态; 想要“带状态的函数“必须用 FB。OB 的 VAR_INPUT 由系统填入事件信息 (如启动 OB 的保持性丢失标志), 用户不能在调用时传参 —— 因为调用者是操作系统。
3.3 VAR CONSTANT
VAR CONSTANT
BUF_SIZE : INT := 64;
GAP_TOL : REAL := 0.01;
READY : INT := 0; // 状态机状态码, 充当 "枚举"
RUNNING : INT := 1;
FAULT : INT := 2;
END_VAR
常量必须在声明处用 := 赋值, 编译期内联, 不占运行时存储。SCL 没有真正的 enum 类型, “INT 常量集“是标准替代品 (可在 CASE 标签处直接使用常量名)。
3.4 保持性: TIA 里没有 VAR RETAIN 关键字
IEC 61131-3 文本标准里有 VAR RETAIN 区, 但 TIA Portal 不用这个语法: 保持性是接口编辑器里每个变量上的 Retain 复选框 (FB 静态变量、全局 DB 变量逐个勾选)。SCL 文本中看不到任何 RETAIN 字样。
- 断电后: Retain 变量保持最后值, 非 Retain 变量恢复为声明初值。
- M 存储区的保持字节数在 CPU 设备组态里设置。
- 编程初始化 (如“上电必须复位“的逻辑) 放启动 OB (OB100) 里做, 不要依赖初值语义。
3.5 声明时初始化 :=
VAR
count : INT := 0;
gain : REAL := 1.0;
mode : STRING[10] := 'AUTO';
table : ARRAY[1..3] OF INT := 10, 20, 30; // 数组按序赋初值
pos : ARRAY[1..2, 1..2] OF REAL := 1.0, 0.0, 0.0, 1.0;
END_VAR
初值在下载组态、CPU 暖启动 (warm restart) 时生效。例外: VAR_TEMP 不提供运行时初始化 —— 即使某版本编辑器允许填写初值, 也不要依赖 (待核实), 见下节。
3.6 VAR_TEMP 警告: 读前必写
官方文档的原文警告, 翻译过来: 临时局部数据只保留一个扫描周期 (对 FC/FB 而言是一次调用), 读取前必须先写入, 否则值可能是随机的。这就是 C 的“读未初始化栈变量“的 UB, SCL 侧同样真实存在。
VAR_TEMP
sum : INT; // 每次进入块, 内容不可预知
buf : STRING[32]; // 尤其字符串: 头部的"当前长度"是垃圾 → LEN() 返回垃圾
END_VAR
BEGIN
sum := 0; // 正确: 先写
buf := ''; // 正确: 空串初始化 (当前长度置 0)
...
规则: 每个 VAR_TEMP 变量在被读之前必须有明确赋值路径, 包括 STRING (先 := '')、结构体、数组 (循环清零或整体赋值)。Temp 不存入实例 DB, 不消耗保持性/掉电存储。
3.7 ARRAY: 下界可以是任意整数 (不是 0 基!)
这是 C/Python 程序员最容易栽的声明差异: S7 数组上下界由你指定, 下界不必是 0 也不必是 1。
VAR
a : ARRAY[0..9] OF INT; // C 风格 0 基, 完全合法
b : ARRAY[1..10] OF REAL; // 传统 PLC 风格 1 基
c : ARRAY[-5..5] OF INT; // 负下界合法, 11 个元素
d : ARRAY[2020..2030] OF WORD; // 年份当下标, 合法
m : ARRAY[1..3, 0..5] OF BOOL; // 二维
END_VAR
// 访问: 多维用逗号, 不是 a[i][j]
#x := #a[#i] + #d[2026];
#flag := #m[2, 4];
- 元素个数 = 上界 - 下界 + 1 (每维); 越界行为见第 12 节 (1200 上不停机!)。
- 下标可以是常量或整型变量 (最长 32 位整型); DB 中的数组用
"DB名".arr[#i]变址访问, 这就是 1200 上最常用的“指针替身“。 - 数组名作为整体可以赋值/传参 (类似 Python 的列表切片语义? 不, 类似 C 的 struct 拷贝, 整块复制)。
ARRAY[*]变长数组形参是另一套机制, 见 9.2。
3.8 STRING[n] 与默认长度
VAR
s1 : STRING; // 未写长度 → 默认最大 254 字符, 占 256 字节!
s2 : STRING[20]; // 最大 20 字符, 占 22 字节 (头 2 字节, 见第 10 节)
END_VAR
n上限 254; 总占用 = n + 2 字节。- 声明不写
[n]会静默占用 256 字节 —— 在 DB 里是实打实的存储, 多个字符串时务必显式给长度。 - STRING 不能赋给 I/Q (过程映像) 存储区的变量。
- WSTRING 只能在“优化访问“的块中声明; 字符串指令参数表本身同时接受 STRING 与 WSTRING, 但 WSTRING 进不了标准 DB 与通信映射表, 实际项目以 STRING 为主。
3.9 STRUCT 与 UDT 嵌套
匿名结构 (内联) 与命名结构 (UDT, TIA 里叫 PLC data type, 独立于 SCL 块存在):
// UDT "RecipeStep" (单独的 PLC 数据类型对象):
// STRUCT
// setpoint : REAL;
// holdTime : TIME;
// END_STRUCT
FUNCTION_BLOCK "Batch"
VAR
// 匿名嵌套结构
pos : STRUCT
x : REAL;
y : REAL;
END_STRUCT;
// UDT 类型的数组与标量
steps : ARRAY[1..10] OF "RecipeStep";
active : "RecipeStep";
END_VAR
BEGIN
#pos.x := 1.5;
#steps[3].setpoint := 80.0; // 逐级点号访问
#active.holdTime := T#30s;
END_FUNCTION_BLOCK
嵌套深度没有实际限制; UDT 相当于 C 的 typedef struct, 定义一次、处处可用, 且改 UDT 会同步改所有使用者。
3.10 系统类型实例声明 (定时器/计数器)
IEC 定时器/计数器是系统 FB 类型, 声明一个变量就是声明一个实例 (多重背景, multi-instance):
FUNCTION_BLOCK "Pump"
VAR
tonDelay : TON; // 通电延时定时器实例 (16 字节 IEC_TIMER 结构)
tofStop : TOF; // 断电延时
cntParts : CTU; // 加计数器
END_VAR
也可以把实例放在全局 DB 里 ("TimerDB".t1 : TON;), 供 OB 直接使用。实例不能放在 VAR_TEMP (Temp 无状态语义)。详细用法见第 8 节。
3.11 优化块 vs 标准块 (影响一串语法能力)
TIA 新建 DB/FB 默认是 optimized block access (优化访问: 无固定地址、按符号紧凑存放)。两种模式的能力差异 (官方):
| 能力 | 优化块 | 标准块 |
|---|---|---|
| PEEK/POKE 按 DB 区读写 | 不可 | 可 |
| AT 覆盖 | 仅保持性变量可 | 任意变量可 |
ARRAY[*] 形参 (1200, 固件 >= V4.2) | 需要优化块 | 不可 |
| WSTRING 声明 | 可 | 不可 |
| 与 S7-300/400、经典工具/HMI 老驱动兼容 | 弱 | 强 |
结论: 默认用优化块; 只有需要 PEEK/POKE 直捣 DB 或对接老系统时才用标准块。
4. 运算符与表达式
4.1 := 是赋值, = 是比较 (与 C 相反)
高危警示: 方向与 C 相反。
#x := 1;是赋值语句 (C 的x = 1;)#x = 1是比较表达式 (C 的x == 1;)- SCL 不存在
==、也不存在把赋值当表达式。因此 C 的两大经典事故在这里形态不同:
- C 的
if (x = 1)(编译通过、逻辑错误) 在 SCL 写成IF x := 1 THEN会直接编译报错 —— 赋值不是表达式, 这是 SCL 比 C 安全的地方;- 但反过来, 顺手写
#x == 1;或条件里写:=之外, 还要注意IF #x = 1 THEN的=是“等于“, 不是赋值 —— 肌肉记忆必须重训。
<> 是不等于 (Pascal 传统), 没有 tampoco !=。
4.2 算术运算符
| 运算符 | 含义 | 示例 | 备注 |
|---|---|---|---|
+ - | 加减 | #a - #b | 数值与 TIME 均可 |
* | 乘 | #a * #b | |
/ | 除 | 7 / 2 = 3 | 两个整型 → 整除, 向零截断 (同 C99, 异 Python) |
MOD | 取模 | -7 MOD 3 = -1 | 结果符号同被除数 (同 C, Python 得 2) |
** | 幂 | 2.0 ** 10 | REAL 运算; 两个整型操作数会先转 REAL, 结果为 REAL (待核实) |
整型运算先按操作数类型计算: INT + INT 的结果若溢出 INT 范围, ENO=FALSE 且结果无效 (第 12 节)。不同宽度的整型混算会按隐式转换规则先对齐 (见 4.7)。
TIME 之间可直接 + - 和比较 (本质是毫秒数运算):
VAR t1, t2, dt : TIME; END_VAR
#dt := #t2 - #t1; // TIME - TIME = TIME
IF #dt > T#500ms THEN ... END_IF;
4.3 逻辑与位运算: 同一套关键字
AND OR XOR NOT 对 BOOL 是逻辑运算, 对位串类型 (BYTE/WORD/DWORD, 以及整型) 是按位运算, 由操作数类型决定:
// BOOL 逻辑
#ok := #ready AND NOT #fault;
#go := #auto OR #manual;
// 位串按位 (类似 C 的 & | ^ ~)
VAR
mask : WORD := WORD#16#0F0F;
val : WORD := WORD#16#FF00;
END_VAR
#y := #val AND #mask; // WORD#16#0F00
#z := #val XOR WORD#16#FFFF; // 按位取反也可 NOT #val
AND 可以写成 &。没有短路求值 (见 1.4)。
4.4 比较运算符
= <> < <= > >=, 全部返回 BOOL。操作数类型要兼容 (数值间可隐式转换后比较)。SCL 没有 C 的链式写法 a < b < c (会按 (a<b) < c 把 BOOL 和数值比较, 编译报错), 也没有 Python 的 a < b < c 链式语义, 必须写 #a < #b AND #b < #c。
4.5 完整优先级表 (从高到低)
| 优先级 | 运算 | C 对应 | 与 C 的差异说明 |
|---|---|---|---|
| 1 (最高) | ( ) 括号 | 同 | |
| 2 | 函数调用求值 f(x) | 同 | |
| 3 | ** 幂 | pow() 是函数 | |
| 4 | 一元负 -, NOT | -x, !x | |
| 5 | * / MOD | * / % | |
| 6 | + - | 同 | |
| 7 | < <= > >= | 同 | |
| 8 | = <> | == != | 注意符号 |
| 9 | AND (&) | && 与 & | SCL 的 AND 比 = 更低, 与 C 的 && 一致, 但修复了 C 的 & 低于 == 的坑 |
| 10 | XOR | ^ | SCL 中 XOR 介于 AND/OR 之间, 同 C |
| 11 (最低) | OR | || |
实用结论: 优先级结构与 C 的逻辑层基本同构, 最需要记的是 NOT a = b 解析为 (NOT a) = b (NOT 优先级高于比较), 与 C 的 !a == b 相同 —— 都要小心, 习惯加括号。
4.6 求值语义: 无副作用表达式、无短路
- 赋值是语句不是表达式; 没有
++/--; 逗号表达式不存在。 - 表达式中的函数调用不保证求值顺序、不保证省略 (无短路), 不要在表达式里放“有副作用“的调用 —— SCL 里能改变状态的语句其实很少 (主要是 FC 输出参数), 但要意识到这点。
4.7 类型转换: 隐式规则与显式函数
隐式转换规则 (官方): 当目标类型的值域完全覆盖源类型值域时, 编译器自动转换。可隐式的方向 (S7-1200):
- 整型加宽:
SINT→INT→DINT,USINT→UINT→UDINT, 以及一切“窄→宽且值域覆盖“的组合 (如BYTE→WORD,UINT→DINT)。 - 整型→浮点:
INT→REAL,DINT→REAL(官方确认 DINT→REAL 可隐式; 但 REAL 只有 24 位尾数, 大 DINT 会丢精度, 编译器不报警)。 REAL→LREAL。- 字符:
CHAR→STRING赋值。
不可隐式 (必须显式转换):
- 一切“窄化“:
REAL→INT,DINT→INT,WORD→BYTE,UINT→INT(值域 65535 > 32767 覆盖不了)。 - TIME 与整型互转、数值与 STRING 互转。
显式转换函数族是 TO_<类型>:
#i := TO_INT(#r); // REAL → INT (截断小数? 见下表区别)
#d := TO_DINT(#i);
#r := TO_REAL(#i);
#w := TO_WORD(#b);
#s := TO_STRING(#i); // 数 → STRING (非格式化)
REAL/LREAL 取整有四个语义不同的指令, SCL 中默认返回 DINT (可显式指定其他整型):
| 指令 | 语义 | TO/C 类比 | 示例 (输入 REAL) |
|---|---|---|---|
ROUND(#r) | 四舍五入到最近整数 | C 的 round() | 2.5→3, -2.5→-3 |
TRUNC(#r) | 向零截断 | C 强转 (int) | 2.9→2, -2.9→-2 |
CEIL(#r) | 向上取整 | C 的 ceil() | 2.1→3, -2.9→-2 |
FLOOR(#r) | 向下取整 | C 的 floor()、Python 的 // 结果方向 | 2.9→2, -2.1→-3 |
陷阱清单:
TO_INT(#r)与TRUNC的区别容易混: 对 REAL 输入,TO_INT等价截断转换; 想要四舍五入必须显式用ROUND。- 转换结果超出目标值域 (如
TO_INT(1.0E9)) → ENO=0, 输出值无效 (OUT 取值不同版本文档表述有差异——以 ENO 为准, 不要依赖 OUT), CPU 保持 RUN (第 12 节)。 - STRING↔数值用
S_CONV/STRG_VAL/VAL_STRG(见 10.4), 非法输入 → ENO=0、OUT=0。
4.8 REAL 精度问题
REAL = IEEE 754 单精度 (24 位有效尾数, 约 7 位十进制有效数字); LREAL = 双精度。经典事实:
// 0.1 无法被二进制精确表示
#r := 0.1 + 0.2; // 结果 ≠ 0.3, 打印为 0.30000001192...
// 2^24 以上整数间隔 2, 加 1 会丢失
#a := 16777216.0;
#a := #a + 1.0; // #a 仍是 16777216.0
规则: 不要对 REAL 用 = 直接判等, 用容差:
IF ABS(#a - #b) < 1.0e-6 THEN ... END_IF;
累加大量数值时用 DINT/LREAL 或补偿算法; 标定/计量场合优先 LREAL。
4.9 内置数学函数
ABS MIN MAX LIMIT SQRT EXP LN (还有 EXP_D? 以指令帮助为准) 以及三角函数 SIN COS TAN (弧度制!)、反三角 ASIN ACOS ATAN。
#y := LIMIT(MN := 0.0, IN := #x, MX := 100.0); // 三参钳位, 相当于 clamp()
#m := MIN(#a, #b); // 注意: 参数名是 IN1/IN2? 直接位置传参最稳妥: MIN(#a, #b)
5. 控制流
5.1 IF / ELSIF / ELSE
// C: if (t > 80) hot=1; else if (t > 40) warm=1; else cold=1;
IF #t > 80 THEN
#hot := TRUE;
ELSIF #t > 40 THEN
#warm := TRUE;
ELSE
#cold := TRUE;
END_IF;
条件必须是 BOOL; 写 IF #count THEN (整型当条件, C 习气) 是编译错误。关键字块必须以 END_IF; 收尾 (带分号)。
5.2 CASE: 无 fallthrough
CASE #state OF
0: // 单值
#y := 1;
1, 3, 5: // 多个离散值并列
#y := 2;
10..20: // 闭区间范围 (含两端)
#y := 3;
ELSE // 可选; 相当于 C 的 default
#y := 0;
END_CASE;
与 C switch 的差异表:
| 特性 | C switch | SCL CASE |
|---|---|---|
| fallthrough | 有 (忘 break 出事故) | 没有, 每个分支天然独立结束 |
| 多值并列 | case 1: case 3: 靠 fallthrough | 1, 3: 直接写 |
| 值范围 | 无 | 10..20 语法内建 |
| 标签类型 | 整型常量 | 整型常量 / 命名常量 (VAR CONSTANT); 不允许变量 |
| 选择器类型 | 整型/字符/枚举 | 整型 (INT/DINT/BYTE 等) |
| default | default: | ELSE |
5.3 FOR: 边界含端点
VAR_TEMP
i : INT;
sum : INT;
END_VAR
// C: for (i = 1; i <= 10; i++) sum += a[i];
sum := 0;
FOR #i := 1 TO 10 BY 1 DO // BY 1 可省略
sum := sum + #arr[#i];
END_FOR;
// 倒数: BY -1
FOR #i := 10 TO 1 BY -1 DO
;
END_FOR;
精确语义 (与 C 的差异逐条列出):
- 边界含端点:
TO 10包含 10 (C 的i <= 10, 不是i < 10)。 - 循环变量必须是本块声明的整型临时变量; 循环体内不允许给循环变量赋值 (编译报错) —— 不能像 C 一样用
i++跳出。 - 起止表达式在进入循环时求值一次; 若初值已越过终值 (按步进方向), 循环体一次都不执行。
EXIT= C 的break(跳出整个循环);CONTINUE= C 的continue(进入下一轮)。- 正常结束后循环变量的值是“最后一个被执行的值 + 步长“ (IEC 语义, 类似 C 循环变量出界值; 不要依赖它)。
- 循环体内不要调用耗时指令或死等外设 —— 整个循环在一个 scan cycle 里跑完, 循环太长会喂狗失败 (第 12.5 节)。
5.4 WHILE: 先测试
// C: while (buf[n] <> 0) n++;
WHILE #buf[#n] <> 0 DO
#n := #n + 1;
END_WHILE;
注意: 1200 的 WHILE 循环仍是“单扫描周期内跑完“的忙循环, 不是跨扫描周期的等待。想跨周期等条件, 用“状态 + 下一周期再判断“的模式 (PLC 的常态写法)。
5.5 REPEAT / UNTIL: 等价 do..while(取反)
// C: do { n++; } while (arr[n] <> 0);
REPEAT
#n := #n + 1;
UNTIL #arr[#n] = 0 // 注意: UNTIL 是"退出条件", 相当于 C while 里的条件取反
END_REPEAT;
至少执行一次。UNTIL c 为真时退出 —— 写成 C 的循环条件会逻辑颠倒, 这是移植代码时的高发 bug。
5.6 RETURN、空语句与 REGION
// RETURN: 立即结束当前块 (相当于 C 的 return; 但 FC 返回值仍是"函数名 := 值")
IF #dataInvalid THEN
RETURN;
END_IF;
// 空语句: 单独一个分号是合法语句, 可作占位
;
// REGION: 仅影响编辑器代码折叠, 无编译语义, 可嵌套
REGION 初始化_如需中文名请实测
#x := 0;
END_REGION
GOTO 标签名; + 标签名: ... 属于经典 STEP 7 SCL 的遗留能力; TIA Portal 各版本 SCL 对它的支持不一致——以你所用版本的编译器实测为准 (待核实)。即使可用, 新代码也不要用它: SCL 的结构化控制流 (IF/CASE/循环 + EXIT/RETURN) 足以表达一切。
6. FC 与返回值
FC 的返回值通过函数名当左值赋值 (Pascal 传统):
FUNCTION "Scale" : REAL
VAR_INPUT
raw, inMin, inMax, outMin, outMax : REAL;
END_VAR
VAR_TEMP
r : REAL;
END_VAR
BEGIN
IF #inMax = #inMin THEN
"Scale" := 0.0; // ← 给函数名赋值 = 设定返回值
RETURN; // ← 提前返回
END_IF;
r := (#raw - #inMin) / (#inMax - #inMin);
"Scale" := r * (#outMax - #outMin) + #outMin;
END_FUNCTION
调用 (返回值可当表达式用):
#v := "Scale"(raw := #ai0, inMin := 0.0, inMax := 27648.0,
outMin := 0.0, outMax := 100.0);
要点与惯例:
- 忘了给函数名赋值就走到 END_FUNCTION, 返回值是未定义的 (TIA 可能给编译警告, 但不保证拦截)。每条路径都要赋值, 类似 C 的“所有路径 return“。
FUNCTION "X" : VOID声明无返回值的过程。- 行业惯例: 多输出参数优于返回值。返回值一次只能带出一个数; 把结果放
VAR_OUTPUT(可多个) 更清晰, 也方便扩展:
FUNCTION "ParseCmd" : VOID
VAR_INPUT cmd : STRING[32]; END_VAR
VAR_OUTPUT ok : BOOL; code : INT; value : REAL; END_VAR
...
- FC 无静态变量, 调用之间不留状态; 需要状态就用 FB。
7. FB 与调用
7.1 FB 需要实例 (背景 DB 或多重背景)
FB 的静态变量必须落在实例存储区里, 两种落地方式:
- 单实例 (single instance): 为 FB 生成一个专属 DB (调用时由 TIA 提示创建), DB 与 FB 同构。
- 多重背景 (multi-instance): 在调用者 FB 的静态区里声明实例变量 (见 3.10), 状态存在调用者的实例 DB 里, 不产生独立 DB。这是首选方式: 一个“设备 FB“声明十个泵实例, DB 树状收纳。
FUNCTION_BLOCK "Pump"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
running : BOOL;
END_VAR
VAR
tonDelay : TON;
END_VAR
BEGIN
#tonDelay(IN := #cmd, PT := T#2s);
#running := #tonDelay.Q;
END_FUNCTION_BLOCK
7.2 调用语法: 按名传参, := 输入、=> 输出
// ---- 在另一个 FB 里: 多重背景实例 pump1 声明于本 FB 的 VAR 区 ----
#pump1(cmd := #startCmd, running => #pumpRun);
// 输入参数用 :=, 输出参数用 =>, InOut 用 := (双向)
// 尾部参数可省略; 被省略的输出不会被写入
// 输出也可以不接 =>, 调用后直接读实例的成员:
#pump1(cmd := #startCmd);
#pumpRun := #pump1.running; // 从实例读输出, 同样合法且常见
// ---- 在 OB 里: 单实例 (DB 名直接当实例名) ----
"PumpDB"(cmd := "bStartPump", running => "bPumpRun");
注意 FB 调用是语句不是表达式, 不能 #x := #pump1(...) (除非… 不行, FB 无返回值; 要结果就接输出参数或读实例成员)。
7.3 条件调用语义: 不调用 = 状态冻结
IF #enable THEN
#pump1(cmd := #startCmd);
END_IF;
被跳过的 FB 既不执行也不复位: 其定时器停在半路、输出保持最后一次的值。这与 C 对象根本不同 (C 里方法不调用这个动作本身不存在)。常见用法与事故:
- 用途: 调试旁路、模式切换时“挂起“某设备逻辑。
- 事故: 把定时器调用放进
IF里, 导致 IN 上升沿丢失或 ET 行为不可预期。定时器/计数器实例应当每个扫描周期无条件调用, 用输入参数控制, 而不是用调用本身控制 (第 8 节的所有示例都遵守这一点)。
8. 定时器与计数器 (程序员最容易懵的一节)
8.1 心智模型: 定时器是“有状态对象“, 不是“函数“
C/Python 里的 sleep(1000) 是阻塞等待; SCL 里没有 sleep —— scan cycle 必须短。IEC 定时器是一个每次调用让它推进一次的状态机: 你每个周期调用它, 告诉它当前 IN 和 PT, 它根据“内部记录的已运行时间“刷新 Q/ET。状态 (剩余时间/已计时) 保存在实例里, 跨扫描周期存活。
S7-1200 提供 (SCL/LAD 通用): TON (通电延时)、TOF (断电延时)、TP (脉冲生成)、TONR (累计定时, 带 R 复位输入); 另有 SCL 专用的 RESET_TIMER、PRESET_TIMER。每个实例是 16 字节 IEC_Timer 结构。(SIMATIC 遗留定时器 S_PULSE/S_ODT/S_OFFDT 等整族是 S7-1500 独占, 1200 没有, 一句带过不再展开。)
四个定时器的行为表 (IN=输入, PT=预设时长, Q=输出, ET=已计时):
| 定时器 | Q 何时为 TRUE | ET 行为 | 一句话 |
|---|---|---|---|
TON | IN 为 TRUE 且持续满 PT 之后 | IN 变 FALSE 立即清零 | 延时启动 (on-delay) |
TOF | IN 为 TRUE 期间, 以及 IN 变 FALSE 后再保持 PT | IN 下降后开始累计到 PT | 延时停止 (off-delay) |
TP | IN 上升沿触发起, 恰好 PT (期间 IN 再变化不影响; 不可重触发) | 脉冲期间累计到 PT | 定宽脉冲 |
TONR | 累计通电时长达到 PT (IN 断开不丢已计时, R 复位) | 断开保持, R 清零 | 累计计时器 |
8.2 通电延时 (TON): 延时启动
FUNCTION_BLOCK "MotorStart"
VAR_INPUT
cmd : BOOL; // 启动命令
END_VAR
VAR_OUTPUT
contactor : BOOL; // 接触器
END_VAR
VAR
tonOn : TON; // ← 状态在实例里
END_VAR
BEGIN
// 每个周期无条件调用; 参数按名传递
#tonOn(IN := #cmd, PT := T#3s);
#contactor := #tonOn.Q; // cmd=TRUE 满 3s 后 Q 才为 TRUE
END_FUNCTION_BLOCK
ET (已计时) 也可以读: #elapsed := #tonOn.ET; (#elapsed : TIME)。
8.3 断电延时 (TOF): 延时停止 + 组合示例
// 需求: 停止命令后风扇再吹 5s 散热
FUNCTION_BLOCK "FanControl"
VAR_INPUT
cmd : BOOL;
END_VAR
VAR_OUTPUT
fan : BOOL;
END_VAR
VAR
tofCool : TOF;
END_VAR
BEGIN
#tofCool(IN := #cmd, PT := T#5s);
#fan := #tofCool.Q; // cmd 掉电后 Q 继续保持 5s
END_FUNCTION_BLOCK
启动延时 + 停止延时组合 (TON 后接 TOF):
VAR
tOn : TON;
tOff : TOF;
END_VAR
// 启动: cmd=TRUE 2s 后输出; 停止: cmd=FALSE 5s 后才真停
#tOn(IN := #cmd, PT := T#2s);
#tOff(IN := #tOn.Q, PT := T#5s);
#output := #tOff.Q;
8.4 振荡器 (闪烁): 两个 TON 交叉耦合
经典振荡器 (占空比可调的方波), 不依赖任何系统时钟函数:
FUNCTION_BLOCK "Blinker"
VAR_OUTPUT
heartbeat : BOOL;
END_VAR
VAR
tOn : TON;
tOff : TON;
END_VAR
BEGIN
// 每个 TON 的输入 = 另一个 TON 输出的反相
#tOn(IN := NOT #tOff.Q, PT := T#600ms);
#tOff(IN := #tOn.Q, PT := T#400ms);
#heartbeat := #tOn.Q; // 周期 1s, 高电平 600ms
END_FUNCTION_BLOCK
推导一遍 (上电首周期两 Q 均 FALSE): tOn 开始计时 → 600ms 后 tOn.Q=TRUE → tOff 开始计时 → 400ms 后 tOff.Q=TRUE → tOn 的 IN 变 FALSE → tOn.Q 立即 FALSE → tOff 的 IN 变 FALSE → tOff.Q 立即 FALSE → 回到起点。周期 = 600+400ms。
单定时器自复位脉冲 (Q 每 PT 产生一个“一个扫描周期宽“的脉冲):
#pulseT(IN := #enable AND NOT #pulseT.Q, PT := T#200ms);
// enable=TRUE 时: 每 200ms, Q 高电平恰好一个 scan cycle
// 注意: 脉冲宽度 = 扫描周期, 若消费方在同一周期内没看到它就丢了
8.5 TONR 与 RESET_TIMER / PRESET_TIMER
VAR
tAcc : TONR;
END_VAR
// IN 断开不清零, 断续累计; R 输入复位
#tAcc(IN := #sensor, R := #manualReset, PT := T#10s);
IF #tAcc.Q THEN
#tooLong := TRUE; // 累计通电满 10s
END_IF;
// SCL 专用指令 (LAD 里对应 RT/PT 线圈):
RESET_TIMER(T := #tAcc); // 程序化硬复位
PRESET_TIMER(T := #tOn, PT := T#9s); // 程序化改 PT
TP 用法同理: #tp(IN := #trigger, PT := T#500ms); #lamp := #tp.Q; —— 触发后亮固定 500ms, 期间再触发无效。
8.6 IEC 计数器: CTU / CTD / CTUD
FUNCTION_BLOCK "Batcher"
VAR_INPUT
partPassed : BOOL; // 光电信号 (每件产品一个上升沿)
manualReset : BOOL;
END_VAR
VAR_OUTPUT
batchDone : BOOL;
END_VAR
VAR
cntParts : CTU; // 加计数器实例 (IEC_Counter 结构)
cntStock : CTUD; // 双向计数器
END_VAR
BEGIN
// CU 检测的是输入信号的上升沿 (内部边沿检测), 直接接布尔即可
#cntParts(CU := #partPassed, R := #manualReset, PV := 100);
#batchDone := #cntParts.Q; // Q: CV >= PV 时为 TRUE
// CV (当前值) 也可直接读:
// #count := #cntParts.CV;
// CTUD: CU 加 / CD 减 / R 复位 / LD 置初值 (=PV)
#cntStock(CU := #partIn, CD := #partOut, R := #manualReset,
LD := #loadInit, PV := 0);
#n := #cntStock.CV; // QU=到达 PV, QD=减到 0
END_FUNCTION_BLOCK
PV (预设值) 与 CV 是整型 (SINT..UDINT 可选, 常用 INT/DINT)。减计数器 CTD 单独存在 (CD, LD, PV → Q, CV)。SIMATIC 遗留计数器 S_CU/S_CD/S_CUD 同样是 S7-1500 独占, 1200 忽略。
8.7 TIME 的加减运算与回绕
TIME + TIME = TIME,TIME - TIME = TIME, 可与T#字面量直接运算和比较 (见 4.2)。- TIME 是 32 位有符号毫秒: 范围约 -24d20h31m23s648ms ~ +24d20h31m23s647ms。加法累计约 24.8 天后溢出回绕 —— 长周期累计要用 DINT 天数+TIME 毫秒拆分, 或用 DTL (第 11 节)。
- 两个时刻求差: 若两个时刻都是 DTL, 用扩展指令
T_DIFF得 TIME; 若都是“某 TON 的 ET“, 相减即可。 - TIME 转毫秒数 DINT 没有隐式路径, 用
T_CONV指令 (待核实具体签名)。
9. 高级类型与机制 (逐项标注 1200 支持度)
9.1 总表
| 机制 | S7-1200 支持 | 说明 |
|---|---|---|
VARIANT 通用形参 + VariantGet/VariantPut/CountOfElements | 支持 | 传“任意类型“的正解 |
TypeOf / TypeOfElements / IS_ARRAY | 支持 | 运行时类型检查 |
TypeOfDB | 不支持 (1500 独占) | |
ARRAY[*] 变长数组形参 | 支持 (需优化块 + 固件 >= V4.2) | FC 仅 Input/InOut 区可声明; FB 只能 InOut (Output/Return 不可) |
PEEK/POKE 家族 | 支持 | 仅 SCL; DB 区必须标准 DB; 外设输入区 16#1 是 1500 独占 |
AT 覆盖 | 支持 (受限) | 标准块任意变量 / 优化块仅保持性变量 |
Slice 访问 .X/.B/.W/.D | 支持 | SCL 对整型 slice 需关闭 IEC 检查 |
REF/引用类型 REF_TO | 不支持 (1500 独占) | 传引用语义用 InOut/VARIANT 替代 |
POINTER/ANY 参数类型 | 不支持 (1500 独占; ANY 在 1500 也仅限标准块 Temp) | |
DB_ANY 间接 DB 访问 | 部分支持 (官方间接寻址页含 1200; 作接口形参类型标注 1500, 待核实) | 1200 上更常用变址数组 |
| LTIME/LTOD/LDT、ARRAY DB、遗留 S 定时器/计数器、断点调试 | 不支持 (1500 独占) | 1200 在线调试只有监视/强制/trace |
9.2 VARIANT: 通用形参 (1200 可用)
VARIANT 只能作形参 (Input/InOut/Temp) 且调用时必须连接实参, 运行时携带实参的类型与地址信息。典型“泛型 FC“:
// FC "AvgRealArr": 对任意长度/任意边界的 REAL 数组求均值
FUNCTION "AvgRealArr" : REAL
VAR_INPUT
arr : VARIANT; // 调用时接任意 ARRAY OF REAL
END_VAR
VAR_TEMP
n, i : DINT;
sum : REAL;
tmp : ARRAY[1..64] OF REAL; // 中转缓冲 (演示上限 64)
END_VAR
BEGIN
sum := 0.0;
#tmp := []; // 不可行, 数组无此初始化; 见下——逐个不用, VariantGet 整体复制
n := CountOfElements(#arr); // 实参数组的元素个数
IF n > 64 THEN n := 64; END_IF; // 演示用途的硬上限
VariantGet(SRC := #arr, DST => #tmp); // 复制到具体类型 (类型不符会失败, ENO 可查)
FOR #i := 1 TO #n DO
sum := sum + #tmp[#i];
END_FOR;
IF #n > 0 THEN
"AvgRealArr" := sum / TO_REAL(#n);
END_IF;
END_FUNCTION
// 调用 (实参是 DB 里的任意 REAL 数组):
#avg := "AvgRealArr"(arr := "DataDB".measurements);
配套: VariantPut (向 VARIANT 指向的目标写入)、IS_ARRAY(#v)、TypeOf(#v) 与 TypeOf(#otherVar) 比较。参数名 (SRC/DST 等) 以编辑器 IntelliSense 为准。注意上面 #tmp := [] 那行是错误示范 (SCL 数组没有这种清零语法), 实际靠 VariantGet 整体覆盖。
9.3 ARRAY[*]: 变长数组形参 (需优化块 + FW >= 4.2)
比 VARIANT 更轻的“任意边界数组“方案, 搭配 LOWER_BOUND / UPPER_BOUND:
FUNCTION "SumArr" : DINT
VAR_IN_OUT
a : ARRAY[*] OF DINT; // FC 中任意声明区可用; FB 中仅 InOut
END_VAR
VAR_TEMP
i : DINT;
s : DINT := 0; // Temp 初值不要依赖, 这里演示; 正式代码仍应在体内赋 0
END_VAR
BEGIN
s := 0;
FOR #i := LOWER_BOUND(a, 1) TO UPPER_BOUND(a, 1) DO // (数组, 维号)
s := s + a[#i];
END_FOR;
"SumArr" := s;
END_FUNCTION
实参可以是任意 [下界..上界] 的 DINT 数组, 循环自动适配边界 —— 修复了“数组边界不一致“这个 PLC 经典耦合问题。
9.4 PEEK / POKE: 按地址读写 (1200 的“指针“)
经典 S7-1200 没有 POINTER 类型的指针间接寻址; SCL 提供按存储区/偏移读写的指令族 (仅 SCL 可用):
| 指令 | 作用 | 返回/参数 |
|---|---|---|
PEEK(area, dbNumber, byteOffset) | 读 1 字节 | 返回 BYTE |
PEEK_WORD(area, dbNumber, byteOffset) | 读 2 字节 | WORD |
PEEK_DWORD(area, dbNumber, byteOffset) | 读 4 字节 | DWORD |
PEEK_BOOL(area, dbNumber, byteOffset, bitOffset) | 读 1 位 | BOOL |
POKE(area, dbNumber, byteOffset, value) | 写字节/字/双字 | |
POKE_BOOL(area, dbNumber, byteOffset, bitOffset, value) | 写 1 位 | |
POKE_BLK(area_src, dbNumber_src, byteOffset_src, area_dst, dbNumber_dst, byteOffset_dst, length) | 块复制 (类似 memcpy) |
区域码: 16#81 = I (过程映像输入), 16#82 = Q, 16#83 = M, 16#84 = DB (外设输入区 16#1 为 1500 独占)。DB 区必须用标准 (非优化) DB。所有变体支持 ENO => #ok 检测访问错误。
VAR
b : BYTE;
w : WORD;
flag : BOOL;
END_VAR
// 读 M100.0
#flag := PEEK_BOOL(area := 16#83, dbNumber := 0, byteOffset := 100, bitOffset := 0);
// 读标准 DB (假设 DB1) 偏移 10 处的 WORD
#w := PEEK_WORD(area := 16#84, dbNumber := 1, byteOffset := 10);
// 把 M 区 0..9 共 10 字节复制到 DB1 偏移 20 处
POKE_BLK(area_src := 16#83, dbNumber_src := 0, byteOffset_src := 0,
area_dst := 16#84, dbNumber_dst := 1, byteOffset_dst := 20,
length := 10, ENO => #ok);
这是与通信报文、第三方协议打交道的最后手段; 正常逻辑优先用符号名 + 数组变址。
9.5 AT: 同一内存多视图 (受限的 union/reinterpret_cast)
// UDT "UDT_ByteBits" (PLC 数据类型): STRUCT b0, b1, ... b7 : BOOL; END_STRUCT
// -- 8 个 Bool 成员在 Struct 内按位打包, 共 1 字节
FUNCTION_BLOCK "RawFrame" // 建议: AT 示例使用标准访问块 (非优化)
VAR
frame : ARRAY[1..4] OF BYTE; // 通信原始帧 (4 字节)
asDWord : DWORD AT frame; // 同一内存按 DWORD 看 (大小相等, 合法)
modeByte : BYTE;
modeBits : "UDT_ByteBits" AT modeByte; // 字节按位拆开 (TIA 中 AT 写在声明的"覆盖"列)
END_VAR
BEGIN
IF #modeBits.b0 THEN
;
END_IF;
#crc := #asDWord; // 4 字节一读
END_FUNCTION_BLOCK
限制 (官方): 不能覆盖 VARIANT; 覆盖体大小不得超过被覆盖变量; 优化块中只能覆盖保持性变量 (以官方页面 “Accessing a tag with an AT overlay” 为准), 实践建议 AT 只在标准块用。注意 Array of Bool 的存储布局与块访问模式有关 (标准块中每元素 1 字节; 优化块中按位打包)——拿它做位视图前先确认布局, 更稳的位视图是 Bool 成员的 Struct 或 slice 访问 (9.6)。AT 是 reinterpret_cast, 所有类型双关 (type punning) 的注意义务归你。
9.6 布尔数组与位访问汇总
SCL 没有 C 的 bits.field 位域, 但有四种位级手段:
ARRAY OF BOOL: 存储布局与块访问模式有关——标准块中每元素 1 字节 (8 个元素占 8 字节), 优化块中按位打包。拿它做位级视图前先确认实际布局; 位视图更稳的写法是 Bool 成员的 Struct (按位打包) 或 slice 访问。- Slice 访问: 对 (D)WORD/整型变量用
.X<位>.B<字节>.W<字>.D<双字>切片读写:
VAR
statusWord : WORD;
big : DWORD;
END_VAR
// 注意: SCL 中对整型做 slice 需在块属性里关闭 IEC 检查; STRUCT/常量/AT 覆盖变量不可 slice
IF #statusWord.X7 THEN ... END_IF; // X0 是最低位
#statusWord.X7 := TRUE; // 也可写
#nibble := #big.B0; // B0 最低字节 (大端布局下注意, 见第 13 节)
- 绝对位地址:
%M0.0、%I0.0(2.6 节)。 PEEK_BOOL/POKE_BOOL(9.4 节)。
10. 字符串实战
10.1 内存布局: 头 2 字节 + 数据
STRING[n] 占 n+2 字节 (官方):
偏移 0: 最大长度 (声明时的 n)
偏移 1: 当前长度 (0..n)
偏移 2..n+1: 字符数据 (ASCII, 未用位置内容不定)
'AB' 存进 STRING[10]: [10][2]['A']['B'][?][?]...。所有函数的下标从 1 开始 (不是 0), MID(s, 1, 1) 是第一个字符。
10.2 空串与未初始化
- 空串 = 当前长度 0 (
''), 与“全零字节“不同, 但 DB 里 STRING 的默认初值就是''。 - VAR_TEMP 里的 STRING 未初始化时, 头部“当前长度“是随机值 →
LEN()返回垃圾 → 所有函数跟着出错。永远先#s := '';(3.6 节)。 - 赋值超出目标容量的串: 运行时截断且 ENO=0 (编译不报错), CPU 继续 RUN。
- 有效串要求
0 < 最大长度 < 255且当前长度 <= 最大长度。
10.3 内置函数全清单
下表 s/s1/s2 为 STRING, n/l/p 为 INT, 下标 1 基, 全部返回 STRING (除注明):
| 函数 | 语义 | 示例 (结果) |
|---|---|---|
LEN(IN := s) → INT | 当前长度 | LEN('abc') = 3 |
LEFT(IN := s, L := l) | 前 l 个字符 | LEFT('PLC123', 3) = 'PLC' |
RIGHT(IN := s, L := l) | 后 l 个字符 | RIGHT('PLC123', 3) = '123' |
MID(IN := s, L := l, P := p) | 从位置 p 起 l 个字符 | MID('PLC123', 3, 2) = 'LC1' |
CONCAT(IN1 := s1, IN2 := s2) | 拼接 | CONCAT('a','b') = 'ab' |
DELETE(IN := s, L := l, P := p) | 删除从 p 起 l 个字符 | DELETE('ABCDEF',2,3) = 'ABEF' |
INSERT(IN1 := s1, IN2 := s2, P := p) | 把 s2 插到 s1 的 p 位置前 | INSERT('ADEF','BC',2) = 'ABCDEF' |
REPLACE(IN1 := s1, IN2 := s2, L := l, P := p) | s1 从 p 起 l 个字符换成 s2 | REPLACE('ABCDEF','XY',2,3) = 'ABXYEF' |
FIND(IN1 := s1, IN2 := s2) → INT | s2 在 s1 中首次出现位置; 找不到 0 | FIND('ABCABC','CA') = 3 |
EQ_STRNG(IN1, IN2) / NE_STRNG | 串相等/不等 (LAD 用; SCL 直接写 = <> 即可) | 'a' = 'a' 为 TRUE |
S_CONV(IN := x, OUT => y) | 串与数互转 (方向由参数类型定) | 见 10.4 |
STRG_VAL(...) | STRING → 数值, 带 P/Q 格式参数 | 见 10.4 |
VAL_STRG(...) | 数值 → STRING, 带 SIZE/P/Q 格式参数 | 见 10.4 |
注意: SCL 对 STRING 的比较运算只有 = 和 <> (没有 >/< 的字典序比较)。
10.4 串数互转
VAR
s : STRING[16];
v : INT;
r : REAL;
END_VAR
// 数 → 串: S_CONV 最简单; 需要定宽/小数位时用 VAL_STRG (SIZE 总宽, P 整数位, Q 小数位, 具体语义以帮助为准)
S_CONV(IN := 123, OUT => #s); // #s = '123'
// 串 → 数: S_CONV 从首字符起转换, 遇非法字符/超范围 → ENO=0 且输出 0
S_CONV(IN := '42', OUT => #v); // #v = 42
// STRG_VAL/VAL_STRG 提供符号/小数/宽度控制, 处理 '3.14'、'-5' 等:
STRG_VAL(IN := #s, OUT => #r); // 参数 P/Q 省略时按默认格式
10.5 FOR 逐字符处理: 转大写 (SCL 没有 s[i] 下标与 ORD())
SCL 不支持 #s[i] 直接下标到字符, 也没有 ORD/CHR。逐字符的标准姿势: MID 取出“1 字符的 STRING“, 用 FIND 在字母表里定位, 再从另一张表取对应字符:
FUNCTION "ToUpper" : VOID
VAR_IN_OUT
s : STRING[64];
END_VAR
VAR CONSTANT
lo : STRING[26] := 'abcdefghijklmnopqrstuvwxyz';
up : STRING[26] := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
END_VAR
VAR_TEMP
i, n, pos : INT;
one : STRING[1]; // MID 结果截断为 1 字符
out : STRING[64];
END_VAR
BEGIN
out := ''; // 初始化!
n := LEN(IN := #s);
FOR #i := 1 TO #n DO
one := MID(IN := #s, L := 1, P := #i); // 第 i 个字符
pos := FIND(IN1 := #lo, IN2 := #one); // 是小写字母?
IF #pos > 0 THEN
out := CONCAT(IN1 := out, IN2 := MID(IN := #up, L := 1, P := #pos));
ELSE
out := CONCAT(IN1 := out, IN2 := #one); // 非字母原样保留
END_IF;
END_FOR;
#s := out;
END_FUNCTION
逐字符解析数字 (不使用 STRG_VAL 的手写版, 展示模式):
FUNCTION "ParseInt" : INT
VAR_INPUT s : STRING[10]; END_VAR
VAR CONSTANT digits : STRING[10] := '0123456789'; END_VAR
VAR_TEMP i, n, pos, val : INT; one : STRING[1]; END_VAR
BEGIN
n := LEN(IN := #s); val := 0;
IF n = 0 THEN
"ParseInt" := -1; RETURN;
END_IF;
FOR #i := 1 TO #n DO
one := MID(IN := #s, L := 1, P := #i);
pos := FIND(IN1 := #digits, IN2 := #one);
IF #pos <= 0 THEN // 非数字字符
"ParseInt" := -1; RETURN;
END_IF;
val := val * 10 + (#pos - 1); // FIND 的位置恰好 = 数值 + 1
END_FOR;
"ParseInt" := val;
END_FUNCTION
性能提示: 逐字符 CONCAT 是 O(n^2); 大量文本处理应攒进定长缓冲或干脆在 HMI/上位机做。
11. 时间与日期
11.1 类型总表
| 类型 | 宽度 | 语义/范围 | 备注 |
|---|---|---|---|
TIME | 32 位 | 有符号毫秒, 约 ±24d20h31m23s647ms | 唯一能直接加减的时长类型; 会回绕 (8.7) |
DATE | 16 位 | 日历日, 1990-01-01 ~ 2168 年 | 字面量 D#2026-09-05 |
TIME_OF_DAY (TOD) | 32 位 | 毫秒精度的日内时间 | 字面量 TOD#13:45:30.123 |
DTL | 12 字节 | 1970-01-01 ~ 2262-04-11, 结构体 | 1200 上读系统时间用它 |
DT (DATE_AND_TIME) | 8 字节 | 兼容旧系统 | 1200 上优先 DTL |
LTIME/LDT/LTOD | - | 纳秒级/长时型 | S7-1500 独占, 1200 无 |
11.2 DTL: 结构体字段访问
VAR
now : DTL;
END_VAR
// 字段: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND (USINT), NANOSECOND (UDINT)
#now.MONTH := 9;
IF #now.HOUR >= 22 OR #now.HOUR < 6 THEN
#nightMode := TRUE;
END_IF;
11.3 读系统时间: RD_SYS_T / RD_LOC_T
VAR_TEMP
now : DTL;
w : WORD;
END_VAR
RD_SYS_T(OUT => #now); // 系统时间 (UTC 相关); 有可选返回值/RET_VAL, 以帮助为准
// RD_LOC_T(OUT => #now); // 本地时间 (含组态时区/夏令时)
毫秒级测耗时的稳妥做法: 不是解析 DTL, 而是用 TON.ET 或 TIME 差值 (8.7); 或读两次 #now 转 TIME 累计 (要自己处理字段进位, 不推荐)。系统时间断电是否保持取决于 CPU 供电 (电池/电容), 不要假设。
12. 运行时错误与错误处理
12.1 先立世界观: 没有 SIGSEGV, 没有 exception
C 程序员需要重建的预期: S7-1200 的设计哲学是错误尽量不挂机。大多数“非法操作“的结果是 ENO=0 或诊断缓冲记录 + CPU 继续 RUN。真正会转 STOP 的主要是看门狗超时 (12.5) 等少数情况。
12.2 三大典型错误在 S7-1200 上的真实后果 (官方)
| 错误 | C 里的直觉 | S7-1200 实际行为 | S7-1500 对照 |
|---|---|---|---|
数组越界 (#a[99], 声明 [1..10]) | 段错误/未定义行为 | 记入诊断缓冲, CPU 保持 RUN; 访问被放弃; ENO 不会被置 FALSE (勿想靠 ENO 捕获) | 转 STOP, 可用 OB121 或局部错误处理拦截 |
整数除零 (#a / 0) | SIGFPE 崩溃 | ENO=0, 返回值置 0 (结果未定义), CPU 保持 RUN, 无错误 OB | 类似 |
| REAL 非法 (0.0/0.0 → NaN, 溢出) | inf/NaN 静默传播 | ENO=0, 返回 NaN/INF 语义值, 保持 RUN | 类似 |
CONV 溢出 (TO_INT(1.0E9)) | 截断 | ENO=0 (OUT 取值以 ENO 为准、不要依赖), 保持 RUN | 类似 |
串转数非法 ('12a') | - | ENO=0, OUT=0 (从首字符转到第一个非法字符为止), 保持 RUN | 类似 |
整型算术结果溢出 (32767+1 on INT) | 回绕 | ENO=0, OUT 结果无效 | 类似 |
// 除零防护的正确写法 (自防, 别指望运行时):
IF #divisor <> 0 THEN
#q := #a / #divisor;
ELSE
#q := 0;
#mathErr := TRUE;
END_IF;
12.3 OB121/OB122 在 1200 上不存在; 用 GET_ERROR
S7-1500/300/400 的“编程错误 OB121 / I/O 访问错误 OB122“机制 在 S7-1200 上不存在。1200 的对应物:
- 编程错误: 启用块内局部错误处理后, 错误写入错误结构, 用
GET_ERROR/GET_ERROR_ID读取; 启用全局错误处理时写诊断缓冲并保持 RUN。 - I/O 访问错误: 首次发生记诊断缓冲, 保持 RUN。
VAR_TEMP
err : ErrorStruct; // 系统类型: 含 ErrorID 等
id : WORD;
bad : BOOL;
END_VAR
// 在疑似出错的语句之后:
GET_ERROR(ERROR => #err); // 取最近一次本块错误详情 (ENO=TRUE 表示确实发生了错误)
// id := GET_ERROR_ID(); // 只取 16 位错误码 (调用签名以指令帮助为准)
IF #err.ErrorID <> WORD#16#0000 THEN
#bad := TRUE;
END_IF;
ErrorStruct 的高字节是错误类别、低字节是具体原因; 完整字段表查指令帮助。诊断缓冲 (diagnostics buffer) 在 TIA 在线视图 & CPU 诊断里可读, 是事后排障的第一入口。
12.4 能让 S7-1200 转 STOP 的事 (短清单)
- 同一扫描周期内看门狗二次超时 (见下);
- 某些不可恢复的硬件/系统故障;
- (1500 上会 STOP 的越界/编程错误在 1200 上不STOP —— 两代产品策略不同, 迁移代码时务必重验。)
12.5 看门狗 (watchdog) 与扫描时间
- 最大扫描循环时间: 组态范围 1~6000 ms, 默认 150 ms, 恒启用 (设备组态 → CPU 属性 → Cycle)。
- 超时触发“时间错误“事件: 编程了对应的错误 OB 时执行它并保持 RUN; 未编程时: 第一次超时被容忍、保持 RUN; 同一扫描周期内第二次超时 (周期耗时达 2 倍最大循环时间) → 记诊断缓冲并 STOP (不同固件版本的手册章节措辞略有差异, 以你的固件手册为准)。
- 主动续命:
RE_TRIGR();重启监视计时 (当前周期 < 10 倍最大值时成功, ENO=TRUE)。放在长循环体内:
FOR #i := 1 TO 100000 DO
// ...重活...
IF #i MOD 1000 = 0 THEN
RE_TRIGR(); // 喂狗, 防止长循环触发时间错误
END_IF;
END_FOR;
- 更好的做法不是喂狗而是拆任务: 用状态机把长工作切片到多个扫描周期 (PLC 正道)。
13. 字节序与位操作
13.1 S7 是大端 (big-endian)
一个 WORD 在内存/寄存器中: 低地址放高字节。%MW0 由 %MB0(高) + %MB1(低) 组成; %MD0 = MB0..MB3, MB0 是最高字节。位编号: .X0 = 最低有效位。与 x86 (小端) 相反, 与网络字节序相同。
WORD 值 16#ABCD 存于 MW0:
MB0 = 16#AB (高字节, 低地址)
MB1 = 16#CD (低字节, 高地址)
Modbus、很多传感器协议是小端 —— 通信数据经常要换序, 这是 SWAP 存在的理由。
13.2 位移/循环移位/字节交换指令
| 指令 | 语义 | C 对应 |
|---|---|---|
SHL(IN := x, N := n) | 左移 (补 0) | x << n |
SHR(IN := x, N := n) | 右移 | x >> n (无符号语义) |
ROL(IN := x, N := n) | 循环左移 | 无 (要手写) |
ROR(IN := x, N := n) | 循环右移 | 无 |
SWAP(IN := x) | 字节序交换 (WORD/DWORD) | 无内置 |
VAR
bHi, bLo : BYTE;
w : WORD;
END_VAR
// 两个 BYTE 拼一个 WORD (协议解析常见):
#w := SHL(IN := TO_WORD(#bHi), N := 8) OR #w#... // 写法见下, 一步到位:
w := SHL(IN := TO_WORD(#bHi), N := 8) OR TO_WORD(#bLo);
// 拆开:
#bHi := TO_BYTE(SHR(IN := #w, N := 8));
#bLo := TO_BYTE(#w);
// 字节交换 (大小端转换):
#netOrder := SWAP(IN := #hostOrder);
// 循环移位流水灯:
#lights := ROL(IN := #lights, N := 1);
以上 #w := ... 与 w := ... 是同一变量 (大小写不敏感), 示例保留笔迹提醒读者注意。
13.3 位操作速查
- 取某位:
IF #statusWord.X7 THEN(slice, 需关 IEC 检查) 或PEEK_BOOL。 - 置/清位:
#statusWord.X7 := TRUE;或掩码:#statusWord := #statusWord OR WORD#16#0080;清位:#statusWord := #statusWord AND (NOT WORD#16#0080); - 测多位掩码:
IF #status AND WORD#16#0F0F = WORD#16#0F0F THEN(注意加括号:(#status AND WORD#16#0F0F) = WORD#16#0F0F, 虽然 AND 优先级低于比较, 显式括号可读性更好)。
14. 附录A: C/Python ↔ SCL 语法速查大表
| # | 语义 | C | Python | SCL |
|---|---|---|---|---|
| 1 | 块注释 | /* */ | 无 | (* *) (可嵌套) |
| 2 | 行注释 | // | # | // |
| 3 | 赋值 | x = y; | x = y | x := y; |
| 4 | 相等 / 不等 | == / != | == / != | = / <> |
| 5 | if / else | if () {} else {} | if: else: | IF THEN ELSE END_IF; |
| 6 | else-if | else if | elif | ELSIF |
| 7 | switch/case | switch+case+break | match (3.10) | CASE x OF ... ELSE ... END_CASE; |
| 8 | case 范围 | 无 | case 10|20 | 10..20: |
| 9 | for 计数循环 | for(i=0;i<n;i++) | for i in range(n) | FOR i := 0 TO n-1 BY 1 DO |
| 10 | while 先测 | while (c) | while c: | WHILE c DO |
| 11 | do-while 后测 | do {} while(!c) | 无 | REPEAT UNTIL c END_REPEAT |
| 12 | break / continue | break / continue | break / continue | EXIT / CONTINUE |
| 13 | 返回值 | return v; | return v | 块名 := v; RETURN; |
| 14 | 三元表达式 | c ? a : b | a if c else b | 无 (IF/ELSE 语句或 FC) |
| 15 | 逻辑与/或/非 | && || ! | and or not | AND OR NOT (无短路) |
| 16 | 位与/或/异或/反 | & | ^ ~ | & | ^ ~ | AND OR XOR NOT |
| 17 | 移位 | << >> | << >> | SHL() SHR() |
| 18 | 循环移位 | 手写 | 手写 | ROL() ROR() |
| 19 | 取模 | a % b (符号同被除数) | a % b (符号同除数) | a MOD b (符号同被除数, 同 C) |
| 20 | 幂 | pow(a,b) | a ** b | a ** b |
| 21 | 整除 | 7/2=3 (截断) | 7//2=3, -7//2=-4 | 7/2=3 (截断, 同 C) |
| 22 | 布尔字面量 | 1/0 | True/False | TRUE/FALSE |
| 23 | 十六进制 | 0xFF | 0xFF | 16#FF / WORD#16#00FF |
| 24 | 二进制 | 0b1010 (C23) | 0b1010 | 2#1010 |
| 25 | 数字分隔符 | ' (C14) | _ | _ (1_000_000) |
| 26 | 字符字面量 | 'a' | 'a' | 'a' (CHAR) |
| 27 | 字符串字面量 | "abc" | "abc" | 'abc' (单引号) |
| 28 | 串内引号转义 | "a\"b" | 'a"b' | 'It''s' (双写) |
| 29 | 字符串拼接 | strcat() | a + b | CONCAT(IN1:=a, IN2:=b) |
| 30 | 字符串长度 | strlen() | len(s) | LEN(IN := s) |
| 31 | 子串 | strncpy | s[a:b] | LEFT/RIGHT/MID |
| 32 | 查找 | strstr | s.find(t) | FIND(IN1, IN2) (1 基, 0=未找到) |
| 33 | 数组声明 | int a[10]; | a = [0]*10 | a : ARRAY[1..10] OF INT; (任意下界) |
| 34 | 二维数组 | int m[3][4] | 列表套列表 | ARRAY[1..3, 1..4] OF INT, 访问 m[2,3] |
| 35 | 数组长度 | sizeof(a)/sizeof(a[0]) | len(a) | UPPER_BOUND(a,1) (需 ARRAY[*]) 或常量 |
| 36 | 结构体 | struct {..} | 无 (类/dataclass) | STRUCT .. END_STRUCT / UDT |
| 37 | 成员访问 | s.f / p->f | s.f | s.f (无指针) |
| 38 | 枚举 | enum | Enum | 无; 用 INT 常量集 |
| 39 | 常量 | const/#define | 大写约定 | VAR CONSTANT x : T := v; END_VAR |
| 40 | 函数定义 | int f(int x){..} | def f(x): | FUNCTION "f" : INT + 接口区 |
| 41 | 无返回函数 | void f() | (返回 None) | FUNCTION "f" : VOID |
| 42 | 多返回值 | 指针参数/结构体 | 元组 | VAR_OUTPUT 多个输出参数 |
| 43 | 引用传参 | int *x / T& | 可变对象 | VAR_IN_OUT |
| 44 | 全局变量 | 文件级变量 | 模块级变量 | 全局 DB 变量 / PLC tags |
| 45 | 静态局部变量 | static | 函数属性/闭包 | (FB 的 VAR 静态区) |
| 46 | 空语句 | ; | pass | ; |
| 47 | NULL/None | NULL | None | 无 (无指针); 可用“有效标志“ BOOL |
| 48 | 类型转换 | (int)x | int(x) | TO_INT(x) 等 TO_* 族 |
| 49 | 舍入取整 | (int)x 截断 / round | int() / round() | TRUNC / ROUND / CEIL / FLOOR |
| 50 | 绝对值/钳位 | abs 手写 | abs min max | ABS MIN MAX LIMIT |
15. 附录B: 同一状态机的 C 与 SCL 双版本
需求: 电机序列控制。IDLE –start–> STARTING (开接触器等反馈, 2s 超时判故障) –> RUNNING (开阀) –stop–> STOPPING (500ms 冷却) –> IDLE。
15.1 C 版本 (裸机轮询风格, dt 毫秒由调度器喂入)
#include <stdbool.h>
#include <stdint.h>
typedef enum { IDLE = 0, STARTING = 1, RUNNING = 2, STOPPING = 3 } MotorState;
typedef struct {
MotorState state;
uint32_t t_in_state_ms; // 进入当前状态后累计时间
} MotorSm;
void motor_sm_step(MotorSm* m, bool start, bool stop, bool feedback,
uint32_t dt_ms,
bool* contactor, bool* valve, bool* fault)
{
*contactor = false; *valve = false; *fault = false;
m->t_in_state_ms += dt_ms; // 时间由外部推进
switch (m->state) {
case IDLE:
if (start) { m->state = STARTING; m->t_in_state_ms = 0; }
break;
case STARTING:
*contactor = true;
if (feedback) { m->state = RUNNING; m->t_in_state_ms = 0; }
else if (m->t_in_state_ms > 2000)
{ m->state = STOPPING; m->t_in_state_ms = 0; *fault = true; }
break;
case RUNNING:
*contactor = true; *valve = true;
if (stop) { m->state = STOPPING; m->t_in_state_ms = 0; }
break;
case STOPPING:
if (m->t_in_state_ms > 500) { m->state = IDLE; m->t_in_state_ms = 0; }
break;
}
}
15.2 SCL 版本 (时间交给 TON, 不需要 dt 管道)
FUNCTION_BLOCK "MotorSm"
VAR_INPUT
start : BOOL; // 启动按钮
stopCmd : BOOL; // 停止按钮
feedback : BOOL; // 接触器反馈 (运行确认)
END_VAR
VAR_OUTPUT
contactor : BOOL; // 接触器命令
valve : BOOL; // 工艺阀
fault : BOOL; // 启动超时故障 (单周期脉冲)
stateNum : INT; // 当前状态码 (供 HMI/监视)
END_VAR
VAR
state : INT := 0; // 0=IDLE 1=STARTING 2=RUNNING 3=STOPPING
tState : TON; // "进入当前状态多久了"计时器, IN 恒真, 转移时复位
END_VAR
VAR CONSTANT
T_START : TIME := T#2s;
T_COOL : TIME := T#500ms;
END_VAR
BEGIN
// 状态计时器: 每周期无条件调用; ET 在达到 PT 后不再增长,
// 因此 PT 要设得比所有阈值大
#tState(IN := TRUE, PT := T#10s);
// 输出默认值 (对应 C 版函数开头的清零)
#contactor := FALSE;
#valve := FALSE;
#fault := FALSE;
CASE #state OF
0: // IDLE
IF #start THEN
#state := 1;
RESET_TIMER(T := #tState); // 对应 C 的 t_in_state_ms = 0
END_IF;
1: // STARTING: 等反馈, 2s 超时 → 故障
#contactor := TRUE;
IF #feedback THEN
#state := 2;
RESET_TIMER(T := #tState);
ELSIF #tState.ET >= #T_START THEN
#fault := TRUE;
#state := 3;
RESET_TIMER(T := #tState);
END_IF;
2: // RUNNING
#contactor := TRUE;
#valve := TRUE;
IF #stopCmd THEN
#state := 3;
RESET_TIMER(T := #tState);
END_IF;
3: // STOPPING: 500ms 冷却
IF #tState.ET >= #T_COOL THEN
#state := 0;
RESET_TIMER(T := #tState);
END_IF;
END_CASE;
#stateNum := #state;
END_FUNCTION_BLOCK
移植对照要点:
- C 版自带
t_in_state_ms += dt_ms的时间推进管道; SCL 版把时间委托给 TON (PLC 的惯用法), 转移时RESET_TIMER对应清零。 - C 的
switch无初值概念; SCL 的state : INT := 0用声明初值定义上电状态 (实例 DB 的初值在下载/暖启动时生效)。 - C 版枚举是类型; SCL 用 INT + 注释/常量,
stateNum输出给 HMI 显示。 - 两版的
fault都只保持一个周期 (对应 C 版每轮重算), 要锁存就加faultLatch。
16. 附录C: 常见编译报错与解决方法
报错文案随 TIA 版本变化, 下列为典型措辞与根因 (按出现频率排序):
| # | 典型报错 (英, 措辞近似) | 根因 | 解决 |
|---|---|---|---|
| 1 | The operand '#x' is not defined / identifier not found | 变量未声明; 或全局名没加双引号; 或大小写拼错 | 声明/补 # (块内) 或 "名" (全局); 记住大小写不敏感, 重名冲突也报这附近 |
| 2 | Duplicate identifier 'MOTOR' | 声明了 motor 又声明 MOTOR | 改名; 大小写不敏感 |
| 3 | The data types of the operands are not compatible / cannot be converted implicitly | 窄化赋值 (REAL→INT、DINT→INT、WORD→BYTE) 或 INT 与 STRING 混算 | 显式 TO_INT() 等; 见 4.7 隐式规则 |
| 4 | Value out of range / constant out of value range | #i := 16#FFFF; (#i 是 INT) 或 #i := 65535; | 用类型前缀 WORD#16#FFFF 或换宽类型 |
| 5 | 语法错在 ==、!=、&&、++ | C 运算符带进来了 | 改 =、<>、AND、+1 |
| 6 | Syntax error 在 IF #x := 1 THEN | 把比较写成了赋值 | IF #x = 1 THEN; 反方向: 语句里把赋值写成 = 同理报错 |
| 7 | Cannot convert ... to BOOL / condition not BOOL | IF #count THEN 整型当条件 | 写全 IF #count > 0 THEN |
| 8 | Keyword 'xxx' is reserved / keyword cannot be used | 变量名撞关键字/指令名/类型名 (FOR、TON、INT…) | 改名 |
| 9 | A function block call requires an instance DB / no instance specified | 直接调 FB 没带实例 | 用实例 DB 名或块内声明的多重背景实例调用; 见第 7 节 |
| 10 | The parameter 'IN' is unknown (调用内置函数/指令时) | 参数名写错 (如 LEN(S := ..)) | 查函数表参数名: LEN 用 IN, MID 用 IN/L/P; 见 10.3 |
| 11 | The index ... is outside the limits of the array | 常量下标越界 (编译期可查的部分) | 检查边界; 注意声明是 [1..10] 不是 [0..9] 时首元素是 1 |
| 12 | Invalid TIME literal 'T#30s1h' | TIME 单位未从大到小排列/重复 | T#1h30m; 负值用 0 - T#30s |
| 13 | Invalid string length / length must be 0..254 | STRING[300] | 上限 254; 记得 [n] 不写就是 254 |
| 14 | Constants expected (CASE 标签处) | CASE 标签写了变量或表达式 | 标签只能是常量/命名常量 |
| 15 | The control variable of the FOR loop ... (赋值被拒) | 循环体内给循环变量赋值 | 用 EXIT/CONTINUE 控制流, 不要动循环变量 |
| 16 | 警告: function value not set / 未赋值路径 | FC 某路径没给函数名赋值 | 每条路径 块名 := 值; (返回值未定义是运行时垃圾) |
| 17 | POINTER / ANY / REF_TO ... not supported for this CPU | 用了 1500 独占特性 | 1200 替代: VARIANT / ARRAY[*] / InOut / PEEK-POKE; 见第 9 节 |
| 18 | 警告: IEC check 未关导致 .X0 slice 报错 | 对整型做位片访问 | 块属性关闭 IEC 检查, 或改用掩码运算 |
运行时 (不报编译错但行为怪) 三巨头, 一并挂在这里备忘: 数组运行期越界 (1200 不 STOP, 见 12.2)、Temp 未初始化 (3.6)、优化 DB 上 PEEK/POKE (9.4)。
17. 参考出处 (事实主源)
- SIMATIC S7-1200 Manual Collection (TIA Portal V20 在线文档): OB 优先级与队列、循环时间监视 (150 ms 默认)、数组越界/除零/转换错误行为、GET_ERROR、PEEK/POKE、AT 覆盖、STRING 布局、数据类型表。 https://docs.tia.siemens.cloud/r/simatic_s7_1200_manual_collection_enus_20
- SCL 指令参考 (S7-1200, S7-1500), V20 (各条目标题即标注 CPU 支持): https://docs.tia.siemens.cloud/r/en-us/v20/scl-s7-1200-s7-1500
- 块接口合法数据类型 (V20, VARIANT/ANY/POINTER/REF_TO/ARRAY[*] 差异): https://docs.tia.siemens.cloud/r/en-us/v20/declaring-the-block-interface/rules-for-declaring-the-block-interface/valid-data-types-in-the-block-interface-s7-1200-s7-1500
- S7-1200 数据类型转换总览 (隐式/显式): https://docs.tia.siemens.cloud/r/en-us/v20/data-types/data-type-conversion-for-s7-1200-s7-1200/overview-of-data-type-conversion-s7-1200
- ROUND/TRUNC/CEIL/FLOOR (SCL 默认返回 DINT): S7-1200 Manual Collection, Conversion operations 章节。
本文所有示例代码均按 S7-1200 + TIA Portal V16~V20 SCL 语法书写 (变量声明区与语句区分离,
BEGIN/END_*完整闭合), 可直接粘贴到外部源或块编辑器中编译 (个别演示全局 DB/UDT 需先建)。发现与官方手册冲突之处, 以手册为准并欢迎在本仓库提 issue。
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) 可用语法书写, 变量声明区与代码区分开。
目录
- 指令体系如何组织与查找
- SCL 中的调用形态
- 基础指令精讲: MOVE 与选择类
- 转换指令族
- 移位与字逻辑
- 模拟量: 27648 的世界与 NORM_X / SCALE_X
- PID: PID_Compact 完整走查
- 高速计数器 HSC
- 运动控制 (PTO 基本定位)
- 通信: OUC / Modbus TCP / PROFINET / OPC UA / Web server
- 库工程化: 项目库、全局库、第三方库
- 诊断类指令与系统功能一览
- 收尾: 用库思维写 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 选 IN1 | G ? IN1 : IN0 | 注意参数顺序: FALSE 走 IN0, 与 C 三元表达式前后位置相反, 极易写反 |
MUX(K, IN0..INn, ELSE) | 整数 K 选第 K 个输入; K 超范围 → ELSE 且 ENO=0 | 数组下标 + default | MUX 是“位置参数即下标“, 不是数组访问; 超范围不报异常而是走 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 为准, 不要依赖); 这是“越界不抛异常但留痕“的又一例 |
ROUND | REAL → 整型, 就近取整 | .5 边界的舍入方向以在线帮助为准 (待核实) |
TRUNC | REAL → 整型, 向零截断 | 等价 C 的 (int)x |
CEIL / FLOOR | 向 +∞ / −∞ | 等价 math.ceil/floor |
NORM_X / SCALE_X | 归一化 / 定标 | 见第 6 节, 模拟量核心 |
S_CONV / STRG_VAL / VAL_STRG | 字符串 ↔ 数值 | 非法字符/超范围: ENO=0 且 OUT=0 (已核实) |
CHAR ↔ INT 等 | CONV 的具体实例 | #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 的&/|/^对齐。INV或NOT: 按位取反; 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..32511 | 20..约 22.81 mA | 过量程 (overrange): 合法但超出标称 |
| 0..27648 | 4..20 mA | 额定范围 (normal range): 正常工作区 |
| −6912..−1 | 0..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 调用 (完整流程)
- 拖 PID_Compact (Extended > PID control) 到 OB1 的 SCL 代码区, 选 single instance, 命名
PID_DB。 - 打开组态界面 (双击调用处或项目树 “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)。
- 在循环中断 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):
Input与Input_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) 流程
- 组态好 Input/Output 标定与输出限幅, 接好真实 (或高保真模拟的) 过程。
- 控制器先处于自动模式且过程量稳定在设定值附近 (预整定要求过程处于稳态, 否则整定结果差)。
- 在调试界面 (工艺对象 > Commissioning) 点 Start pre-tuning + fine-tuning (或分步), 或在 SCL 里对实例 DB 的整定控制位写上升沿 (组态界面里可查到对应位名; 待核实: 位名称随版本变化)。
- 整定完成后新的 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 编码器测速示例思路
两种做法:
- 频率测量模式: HSC 组态为 frequency measurement, %ID 直接给出 Hz (每秒脉冲数)。换算:
rpm := freq_Hz * 60.0 / pulses_per_rev(pulses_per_rev = 编码器线数 × 倍频)。适合中高速; 低速时刷新周期内脉冲太少导致量化误差大。 - 周期测量模式 (T 法, 低速优): HSC 组态为 period measurement, 测量固定脉冲数的时间,
%ID给出周期值,rpm ∝ 1/period。 - 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 侧是“脉冲数即位置“的半闭环。
三步走:
- 组态 TO: 项目树 Add new technology object > Positioning axis (TO_PositioningAxis), 组态 PTO 脉冲源、方向输出、加减速、回零方式、软限位、跟随误差。
- MC_Power 使能轴 (必须每周期调用):
// "Axis1_Power_DB" 等实例 DB; 轴与 TO 的关联在插入指令时组态 (不是形参)
"Axis1_Power_DB"(Enable := "DB_Mc".powerOn,
StopMode := FALSE); // FALSE=受控停止
- 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 | 不支持真实 IO | PLCSIM 内无 PROFINET IO 设备可挂 (仿真 PN 设备需第三方工具) |
| OPC UA Server | 不仿真 | 官方对比表明确无 OPC UA |
| Web server | 无 | 官方对比表明确 |
结论: 通信类功能的学习与验证必须上真实 CPU; PLCSIM 只能验证指令调用逻辑与状态机 (还常因时钟问题误报错误)。
10.1 TSEND_C / TRCV_C (开放用户通信, 与 socket 类比)
OUC = CPU 集成网口上的原生 TCP/UDP 套接字通信。类比表:
| socket API | TIA 对应 |
|---|---|
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 的数据接口, 不是网页。
组态步骤:
- CPU 属性 > OPC UA Server: 勾选激活; 选地址 (默认 4840); 配 Server interface (新增一个 server interface 块, 把要暴露的变量拖进去, 可分组/设读写权限); 安全策略按需 (最低 None 无加密, 生产建议至少 Basic256Sha256 签名加密并管好证书)。
- 编译下载。客户端测试 (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。移植注意:
- OSCAT 大量使用 ANY/POINTER/引用与内存映射, 而 S7-1200 接口不支持 POINTER/ANY/REF_TO (1500 才有) —— 这些块必须改写为 VARIANT 或显式数组参数才能在 1200 用;
- 数组变址下限: OSCAT 常按 0 基, TIA 数组上下限任意, 逐块核对;
- STRING 处理 (前 2 字节头 + 长度语义) 与 CODESYS 不同, 字符串块几乎都要手改;
- 优化的访问模型差异: 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/TDISCON | OUC | 底层 socket 族 | 见 10.1 |
TSEND_C/TRCV_C | OUC | 带连接管理的封装 | 见 10.1 |
MB_CLIENT/MB_SERVER | Modbus | Modbus TCP 主/从 | 见 10.2 |
CTRL_HSC | HSC | 运行期改 HSC 参数 | 读计数值用 %ID 直读 |
PID_Compact / PID_Temp | PID | 见第 7 节 | |
Data log (LOG_GEN 等) | 数据记录 | 写 CSV 到 CPU/存储卡, 经 Web server 下载 | PLCSIM 不仿真 Web |
RTM / 运行小时计 | 维护 | 操作系统运行时计数器 | 1200 支持情况以 CPU 属性/帮助为准 (待核实) |
诊断缓冲的软件侧入口: TIA 在线诊断对话框; Web server 诊断页 (10.5); 或 GetSystemError 类库块 (待核实名称与 1200 可用性) —— 一般前两者已够用。
13. 收尾: 用库思维写 SCL
把本章所有内容压缩成可执行的工程习惯:
- 先搜库再写循环: LBC、PID_Compact、MC_xxx、MB_CLIENT 都是“官方 pip 包“; 自研一个通用函数前先 F1 搜指令卡与 LBC。90% 的“我写个工具 FB“在西门子生态里已有带测试的版本。
- 每个 FB 都按“库类型“标准写: 参数全命名化、接口一次定稳、语义内聚 (一个 FB 一件事)、不偷写全局 DB —— 今天的项目块就是明天的库类型。接口演进靠版本号, 不靠原地改语义。
- 状态机思维而不是阻塞思维: TSEND_C 的 DONE/BUSY/ERROR、MC 指令的 Busy/Done、PID 的 State, 全是显式状态机。写 SCL 时自己的功能也按“输入边沿触发 + 状态字 + 完成位“的模式封装, 调用者用起来就和官方库一致。
- 错误处理三件套: ENO/ERROR+STATUS 结构贯穿所有库块, 自己的库块也输出同构的 (done, busy, error, status) 四元组; 1200 上没有异常机制, “留痕不中断“是唯一正解, GET_ERROR 兜底。
- 数据接口要“布局自觉“: 模拟量按 0..27648 标尺 + 质量位; 通信缓冲用标准 DB + Word 对齐 + 字节序显式约定 (SWAP 在手); 跨界数据结构写成 UDT 进库, 别散落各 DB。
- 能组态的不编程: PID、HSC、运动控制、PROFINET 的参数都在组态/实例 DB 里, 代码只做“调用 + 状态机 + 边沿“; 组态参数变化要纳入版本管理 (项目文件即配置)。
- PLCSIM 是逻辑仿真器, 不是控制器仿真器: 时钟敏感 (PID) 与网络功能 (OUC/Modbus/OPC UA/Web) 不仿真或行为失真, 上真机前把这两类验证单列。
- 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) < eps、math.isclose 一一对应, 类比完全成立。差异点: SCL 里 REAL 是 32 位, 精度只有约 7 位十进制有效数字——比 Python 默认的 64 位 double 差得多(Python 里 sum([0.1]*10) 得 0.9999999999999999, SCL 的 REAL 得 1.0000001, 都是错, 错法不同), 误差更早暴露; 需要高精度用 LREAL(1200 支持 LREAL)。
陷阱 6: 边沿检测: R_TRIG/F_TRIG 是 FB, 必须有实例
现象: 想在按钮按下瞬间动作一次, 结果写成 IF #btn THEN ..., 按住一秒动作了几百次(每个 scan cycle 一次); 或者把 R_TRIG 的调用放在 IF 里, 边沿时灵时不灵。
根因: PLC 的输入是“电平“, 每个 cycle 都会被重新求值, 没有“事件回调“。边沿检测本质是“本周期值 vs 上周期值“的比较, 需要一块跨周期记忆的存储——这正是 FB + 实例 DB。R_TRIG/F_TRIG 就是这样的 FB, 调用时必须指定实例(单实例 DB 或多重实例)。
正确写法(多重实例, 推荐):
// FB 接口
// Input: btn : Bool
// Output: pulse : Bool
// Static: trigBtn : R_TRIG // 多重实例, 放 Static; 无条件每周期调用
#trigBtn(CLK := #btn); // 无条件调用, 让它每周期都刷新记忆
#pulse := #trigBtn.Q;
IF #pulse THEN
#count := #count + 1; // 只在 0->1 的那个周期执行一次
END_IF;
手写等价物(理解原理用):
// Static: btnLast : Bool
#pulse := #btn AND NOT #btnLast; // 上升沿
#btnLast := #btn; // 每周期末尾保存, 供下一周期比较
C/Python 对照: 就是 C 的 edge = cur && !last; last = cur; 或 Python 边沿检测库里的 was_pressed()。R_TRIG 的实例 DB 相当于那个 static bool last。类比的边界: 实例 DB 是持久状态, 掉电后按保持性设置决定是否恢复; C 的 static 变量重启即丢——保持性(retentive)是 PLC 独有概念。另见陷阱 7: 把 R_TRIG 放进条件分支调用会破坏 last 的连续性。
陷阱 7: 条件调用 FB: 不调用的周期输出保持旧值
现象: IF #mode = 2 THEN #motorFB(...); END_IF;——切到别的模式后, #motorFB 的输出(速度、运行反馈、故障字)冻结在最后一次调用时的值, HMI 上显示一个不存在的旧转速。
根因: FB 的输出与内部状态全部存在实例 DB 里。FB 不被调用的周期, 实例 DB 原样保留, 输出自然不动。没有任何机制帮你“清零“。
正确写法: 二选一。方案 A(推荐): FB 每周期无条件调用, “使能“作为 FB 的一个输入, 由 FB 内部决定输出:
// FB "FB_Heater" 接口
// Input: enable, fault, cmd
// Output: power : Real
// Static: state : Int
// 调用侧 (每周期一次):
// #heater(enable := #modeOk, cmd := #cmd, fault := #fault);
// FB 内部: 不使能时显式给输出赋安全值
IF NOT #enable THEN
#power := 0.0;
#state := 0;
RETURN; // 直接返回, 后面逻辑不执行
END_IF;
方案 B: 调用侧在跳过分支手工清理输出/内部状态。
C/Python 对照: 相当于一个长生命周期的对象, 不调用 update() 它的字段就停在旧值——C 里全局 struct、Python 里的单例都一样。类比成立之处: 状态持久; 不成立之处: PLC 里“跳过调用“是常见控制流(按模式切换), 而软件里通常不会整轮跳过 update, 危险性更高。附带提醒: FB 内部的 IEC 定时器基于时间累计, FB 长时间不调用后重新调用, 其 TON 可能直接判为已到期(待核实, 取决于固件对 IEC_Timer 结构的时间戳处理), 跨周期的长延迟逻辑要显式设计。
陷阱 8: WHILE 阻塞扫描: 没有 sleep, 定时必须 TON 跨周期
现象: 想等 500 ms 再动作, 写了 WHILE 循环空转; 或者在 OB1 里处理一大批数据耗时 300 ms——CPU 一会儿就 STOP, 诊断缓冲显示 cycle time 超时。
根因: PLC 程序不是常驻进程, 而是 scan cycle 模型: 读输入 -> 逐条执行 OB1 -> 写输出 -> 内务 -> 下一轮。OB1 必须在最大扫描循环时间(S7-1200 范围 1~6000 ms, 默认 150 ms, 恒启用)内完成。超时触发“时间错误“事件: 若编程了时间错误 OB(默认优先级 22, 可改 22~26)则执行它并保持 RUN; 未编程时按系统手册(不同章节措辞略有差异, 建议以你固件版本手册为准): 同一扫描周期内第一次超时通常被容忍、保持 RUN, 第二次超时(周期耗时达 2 倍最大循环时间)记入诊断缓冲并 STOP。没有 sleep/delay 函数, 空转 WHILE 就是烧掉整个周期预算; 且阻塞期间所有输出刷新、低优先级任务全部停摆。
正确写法: 延时用 TON, 让“等待“分布在多个扫描周期里:
// FB 接口
// Input: cmd : Bool
// Static: tOn : TON_TIME // 多重实例
// state : Int
// Output: done : Bool
// 代码区
#tOn(IN := (#state = 1), PT := T#500MS); // 无条件调用, IN 由状态驱动
IF #cmd AND (#state = 0) THEN
#state := 1;
ELSIF #state = 1 AND #tOn.Q THEN // 500 ms 后跨周期推进
#state := 2;
#done := TRUE;
END_IF;
大批量数据处理同样拆分: 每周期处理 N 条, 用 Static 游标记住进度。确有一段逻辑必须长时间运行时, 可用 RE_TRIGR 重启周期监视(当前周期 < 10 倍最大值时 ENO=TRUE), 但这是最后手段。
C/Python 对照: 相当于裸机嵌入式的主循环 + 看门狗: while(1) { read_inputs(); step(); write_outputs(); feed_watchdog(); }——你在 step() 里 sleep(1) 整个系统就死。time.sleep() 在 PLC 世界没有对应物, “经过时间“只能靠定时器状态机这种协作式调度表达。类比完全成立, 只是 PLC 的看门狗超时默认直接 STOP 整台 CPU, 比多数嵌入式更狠。
陷阱 9: 除零/数学错误的真实行为: 不崩溃, 静默返回 0
现象: 除数为 0, 程序“没事“继续跑, 结果悄悄变成 0, 控制量跳变; 浮点非法运算得到 NaN 往下游传播。
根因: S7-1200 官方行为表: 整数除零(IN2=0)时“结果未定义并返回 0“, ENO=0, CPU 保持 RUN, 不触发任何错误 OB; REAL/LREAL 的 NaN/INF 非法运算同样 ENO=0 返回 NaN。而且 S7-1200 上不存在编程错误 OB(OB121)/I/O 访问错误 OB(OB122)——这类事件在 1200 上要么走诊断缓冲保持 RUN, 要么在启用了“局部错误处理“的块里用 GET_ERROR/GET_ERROR_ID 读取。CONV 转换溢出: ENO=0(OUT 的具体取值不同版本文档表述有差异——不要依赖 OUT, 以 ENO 为准); 字符串转数值(STRG_VAL/S_CONV)遇非法字符或超范围: ENO=0, OUT=0。全部保持 RUN。
正确写法: 除法前显式判零, 数学指令的错误要么预检要么事后校验:
// 声明区 (FB Static)
// denom : Real, numer : Real, ratio : Real, ratioOk : Bool
IF ABS(#denom) < 1.0E-9 THEN // 预检, 别指望运行时拦截
#ratio := 0.0;
#ratioOk := FALSE;
ELSE
#ratio := #numer / #denom;
#ratioOk := TRUE;
END_IF;
在块内放置 GET_ERROR/GET_ERROR_ID 指令, 即启用该块的局部错误处理(注意: 这不是块属性开关, 而是“块里有没有这对指令“), 这是 1200 上替代 OB121 的机制。
C/Python 对照: C 整数除零是未定义行为(通常 SIGFPE 崩溃), Python 抛 ZeroDivisionError——你习惯了“除零必炸“。S7-1200 恰恰相反: 静默给 0 且继续 RUN。类比不成立之处必须记住: 这里的错误哲学是“带 ENO 的容错“, 与其等异常, 不如把守卫写在前面。
陷阱 10: 通信字节序(大端)与 SWAP
现象: 从 Modbus TCP / 第三方设备收到的 INT 值“怪怪的“: 0x1234 变成 0x3412, 4660 变成 13330; REAL 完全是乱码。
根因: S7 CPU 的数据存储与协议字节序是大端(big-endian, 高字节在低地址), 与 x86/ARM 上 C 的小端相反。两个寄存器拼 DINT/REAL 时还有“高低字顺序“问题(不同厂商对 32 位值的两个字先后排列不统一)。Python 的 struct.pack/unpack 默认按本机小端, 同样要显式 > 才是大端。
正确写法:
// 声明区 (FB Static)
// hiB, loB : Byte; wRaw, wFix : Word; vInt : Int;
// wHi, wLo : Word; dw : DWord; vReal : Real;
// 两字节 -> INT (大端帧): 高字节左移 8 位
#wRaw := SHL(IN := BYTE_TO_WORD(#hiB), N := 8) OR BYTE_TO_WORD(#loB);
#vInt := WORD_TO_INT(#wRaw);
// 字节序颠倒时的修复: SWAP 交换 WORD 内两字节
#wFix := SWAP(IN := #wRaw); // 16#3412 -> 16#1234
// 两字 -> DINT/REAL (确认高字在前):
#dw := SHL(IN := WORD_TO_DWORD(#wHi), N := 16) OR WORD_TO_DWORD(#wLo);
#vReal := DWORD_TO_REAL(#dw); // DWORD->REAL 按位搬运, 不做数值转换
调试手段: 用 PEEK_WORD/PEEK_DWORD 对标准 DB 或 I 区按字节偏移检查原始帧(区域码 16#81=I, 16#82=Q, 16#83=M, 16#84=DB; DB 区必须标准 DB)。
C/Python 对照: 等价于 C 的 ntohs/ntohl 与手动移位拼包, 等价于 Python struct.unpack('>h', data)。类比成立。额外注意: DWORD_TO_REAL 这类等宽转换是“位模式搬运“(类似 C 的 memcpy / 指针重解释), 而 DINT_TO_REAL 是数值转换(类似 (float)i)——用错语义就出鬼数据。
陷阱 11: STRING 定长、截断与形参传递
现象: 赋值后字符串尾部莫名丢失; 一个“字符串变量“占了 256 字节; 传参后长度变化; 拼接结果变短。
根因: String 是带 2 字节头的定长缓冲: 第 1 字节最大长度, 第 2 字节当前长度, 后跟字符, 总占用 = 最大长度 + 2。声明 myStr : String 不写 [n] 时默认 String[254], 即占 256 字节。赋给更短的 String 时从右侧截断到目标最大长度。形参也有自己的声明长度, 实参过长同样截断。String 不能赋给 I/Q 存储区。比较用 =/<>(按字符比较)。子串/查找等操作靠 LEN、LEFT、RIGHT、MID、CONCAT、DELETE、INSERT、REPLACE、FIND 等 FC。
正确写法:
// 声明区
// name : String[16] // 占 18 字节, 按需声明, 别默认 254
// line : String[32]
// n : Int
#name := 'PUMP-01'; // 当前长度 7, 实际存 7 个字符
#line := CONCAT(IN1 := #name, IN2 := ' OK'); // 超过 32 截断
#n := LEN(IN := #name); // 7
IF #name = 'PUMP-01' THEN ... END_IF;
// 传参: 形参声明 String[16]; 传入 String[254] 的实参会被截到 16
C/Python 对照: String 既不是 C 的 char*(无指针、无 ‘\0’ 结尾, 长度显式存在头部), 也不是 Python str(非不可变对象, 长度上限编译期固定)。最接近的类比是 C 的 struct { char max, len; char buf[N]; } 定长字段。注意 WString(UTF-16)在 1200 上只能声明在优化块中; 字符串指令(LEN/CONCAT/FIND 等)的参数表本身同时接受 String 与 WString 操作数, 但 WString 进不了标准 DB 与通信映射表, 实际项目仍以 String 为主。
陷阱 12: 多 OB 中断并发访问共享数据的一致性
现象: OB1 里算出的流量偶尔跳变一个离谱值; 累计值偶尔少加一次; 状态机偶发“跳状态“。
根因: S7-1200 的程序循环 OB(OB1, 优先级 1, 最低)“可被所有其它事件类型中断”——循环中断 OB30~38(默认优先级 8~17)、硬件中断(优先级 18)、延时中断 OB20~23 等随时打断 OB1(中断延迟约 175 us)。共享的全局 DB 数据没有任何锁; OB1 里“读-改-写“序列(如 #total := #total + #x;)或读取多字段结构的过程中断, 就会出现撕裂。抢占规则完全由优先级决定: 任意高优先级 OB 都能打断正在执行的任何低优先级 OB——硬件中断(默认优先级 18)可打断循环中断(OB30~38, 默认 8~17), 循环中断可打断 OB1; 同优先级的多个事件只排队(先来先服务), 不互相打断。所以风险不只是“打断 OB1“: 任意两个不同优先级的 OB 之间, 只要共享数据就可能撕裂。
正确写法:
// 硬件中断 OB (>=123): 只做最小工作, 原子地更新快照
"DB_Shared".snapshot := "DB_Shared".rawValue; // 单个 Real 赋值, 一条指令完成
"DB_Shared".seq := "DB_Shared".seq + 1; // 序号, 供 OB1 校验
// OB1: 用"序号未变则不取"的模式读一致快照
#seq1 := "DB_Shared".seq;
#val := "DB_Shared".snapshot;
#seq2 := "DB_Shared".seq;
IF #seq1 = #seq2 THEN // 序号相同 => 读取期间未被中断改写
#use := #val;
END_IF;
原则: (1) 共享量控制在单个对齐的字/双字内(单条指令搬运, 天然原子); (2) 多字段一致性用双缓冲 + 序号; (3) 能在中断 OB 里完成的算术就别放 OB1; (4) 绝不在两个不同优先级 OB 里做同一变量的读-改-写。
C/Python 对照: 中断 OB 就是 signal handler / 硬件 ISR——共享变量没有锁, data race 与 torn read 的模型完全一致, volatile/memory barrier 那套直觉可以迁移。不成立之处: 这里的“线程“是按优先级严格抢占且不可阻塞, 也没有 mutex 原语可用, 只能靠“缩短临界区 + 数据布局“解决, 类似无锁编程的约束。
陷阱 13: TIME 回绕(约 24.8 天)
现象: 设备连续运行 24.8 天后, 基于“自上电自由计时“做差值比较的逻辑集体异常: 延时瞬间触发或永不触发。
根因: TIME 是 32 位有符号毫秒, 范围 T#-24d_20h_31m_23s_648ms ~ T#+24d_20h_31m_23s_647ms(即 -2147483648 ~ +2147483647 ms), 任何自由运行计时到约 24.8 天即回绕; 直接比较 now >= deadline 在回绕点附近必然出错。平台大坑: 教程里常见的 TIME_TCK() 是 S7-1500 专属指令, 在 S7-1200 上编译直接报错(“Block TIME_TCK is not supported by the CPU”)。1200 上测耗时用 RUNTIME()(返回 LReal 秒, 基于内部高频计数器, 溢出时会返回 <= 0, 该次读数应忽略)、RTM 运行时间计量, 或 RD_SYS_T 读日历时间做差。
正确写法: 差值比较(对计数回绕安全的惯用法):
// 声明区 (FB Static)
// lastT : LReal, interval : LReal := 1.0 // 秒
#now := RUNTIME(); // S7-1200 可用; LReal 秒
IF #now > 0.0 THEN // 丢弃溢出时的无效读数(<=0)
IF (#now - #lastT) >= #interval THEN // 差值法, 对回绕安全
#lastT := #now;
#fire := TRUE;
ELSE
#fire := FALSE;
END_IF;
END_IF;
一般的延时逻辑优先用 TON(IEC 定时器自己处理时间累计), 只有自研调度/秒表才碰自由计时; 对 TIME 类型做算术时, “有符号差值 >= 间隔“的补码回绕惯用法依然成立且推荐。1200 上无 LTIME(1500 独占), DTL 可用于日历时刻(1970~2262)。
C/Python 对照: 与 Arduino 的 millis() 回绕、Linux jiffies 完全同构, (uint32_t)(now - last) >= dt 这个惯用法原样迁移(这里用 DInt 有符号数, 补码回绕行为等效)。类比成立; 额外坑是 TIME 字面量写法 T#24d_20h 与类型检查, 混用 TIME/DInt 需显式 TIME_TO_DINT。
陷阱 14: 符号大小写不敏感撞名
现象: 块里已声明 motor, 再加 Motor 直接报“标识符已使用“; 两个 FB 一个叫 fbPump 一个叫 FbPump, 在跨块引用/库导入时报重名冲突。
根因: TIA 对 PLC 标签/块名的唯一性检查不区分大小写——Motor 与 MOTOR 视为同名。另外保留关键字(指令名、IEC 函数名、数据类型名如 Int、Time)不得用作标识符; 含空格/特殊字符的名字要加双引号。
正确写法: 同一作用域内不要用大小写区分两个名字; 命名规范里显式规定风格(见最佳实践), 并避免:
// 编译失败示例:
// Input: cmd : Bool
// Static: Cmd : Int // 与 cmd 同名 (大小写不敏感)
// 正确: 语义不同就换词
// Input: cmd : Bool
// Static: cmdCount : Int
C/Python 对照: C 与 Python 都大小写敏感, Total/total 是两个变量——这个直觉在 SCL 失效。可以理解为“所有标识符都被 lower-case 归一化后再查重“, 类似某些配置系统对环境变量名的处理。团队协作时特别危险: 你觉得是两个人定的不同名字, 编译器认为是重名。
陷阱 15: 巨型 OB1 与全局变量滥用
现象: OB1 写到上千行, 几十个 FB 直接连着几十个全局 DB, 逻辑互相穿插; 改一处, 三台设备行为都变; 没法在 PLCSIM 里单测任何一段。
根因: C/Python 里“一个 main + 一堆全局变量“的反模式在 PLC 界同样致命, 但更隐蔽——因为全局 DB 在 TIA 里用起来毫无摩擦(双引号即达), 且早期 LAD 习惯就是把所有东西放 M 区/全局 DB。后果: 命名冲突面大(全局且大小写不敏感)、隐式耦合(FB 内部直接读全局 DB, 签名上看不出依赖)、下载影响面失控、无法复用到下一台机器。
正确写法: OB1 只当调度器; 每个工艺对象一个 FB + 实例 DB; 全局只留物理 I/O 映射和真正的共享资源:
// OB1 (SCL) — 只做调度
// 全局 DB "DB_IO": 仅物理量 (sensorRaw, cmdPulse ...) 与 HMI 共享量
// FB "FB_Conveyor": 封装整条输送机逻辑; FB "FB_Pump": 封装泵逻辑
"FB_Conveyor_DB"(sensor := "DB_IO".convSensor,
motorCmd => "DB_IO".convMotor);
"FB_Pump_DB"(enable := "FB_Conveyor_DB".pumpReq,
feedback := "DB_IO".pumpFb,
cmd => "DB_IO".pumpCmd);
判断标准: FB 的所有输入输出都出现在接口里、不读写全局 DB 的内部状态, 才能整体拷贝到下一个项目。
C/Python 对照: OB1 = main(), FB + 实例 DB = 类 + 实例, 全局 DB = 全局单例, UDT = struct 定义。整个重构直觉(mains should be thin, 依赖走参数)可以直接迁移。不成立之处: FB 的“实例“是数据块, 与代码分离——更像“一个函数 + 一块专属内存“, 没有方法表与继承。
陷阱 16: 临时区大数组吃 L 区
现象: FC 里声明 Temp: buf : Array[1..4096] of DInt 或十几个 String(默认各 256 字节), 编译过但运行行为异常, 或深调用链上某块直接出错——L 栈耗尽。
根因: VAR_TEMP 分配在局部数据栈(L 区)上, 每层调用都占一份, 大小按 CPU 型号有限(典型仅若干 KB, 具体查 CPU 规格表)(待核实: 各型 1200 的 L 区精确上限)。S7 用户程序也不支持递归调用, 深层嵌套 + 大 Temp 是最常见的爆栈方式。另外 Temp 不初始化(陷阱 2), 大缓冲读脏数据的概率更高。
正确写法: 大缓冲放 DB(全局 DB 或 FB 的 Static/InOut), 通过引用传递:
// FC "FC_Filter" 接口 — 大数组走 InOut (按引用传递, 不占 L 栈)
// InOut: buf : Array[0..4095] of DInt
// Input: len : DInt
// Return: avg : DInt
#sum := 0;
FOR #i := 0 TO #len - 1 DO
#sum := #sum + #buf[#i];
END_FOR;
IF #len > 0 THEN #avg := #sum / #len; ELSE #avg := 0; END_IF;
C/Python 对照: 与“不要在栈上放 16 KB 数组, 改用堆/memcpy 传指针“一致; InOut 传参相当于传指针(C 的 int buf[] 参数 / Python 传列表引用)。类比成立之处: 栈容量有限、按值拷贝昂贵; 不成立之处: PLC 没有 malloc/堆, “堆“的角色由 DB 承担, 且 DB 同时是持久存储。
陷阱 17: 调试方式差异: 没有断点, 没有 print
现象: 想 printf 一个中间值发现没有控制台; 想打断点单步, TIA 界面上的断点按钮是灰的。
根因: 官方明确: 断点测试仅支持 STL 与 SCL 代码, 但支持的 CPU 只有 S7-300/400 和固件 >= V2.5 的 S7-1500——S7-1200 不支持断点(包括 PLCSIM 仿真 1200 的场景)。也没有任何 print/控制台: CPU 上没有 stdio。
正确替代手段(按用途选):
| 需求 | 1200 上的做法 |
|---|---|
| 看变量实时值 | Program status(在线监视 SCL 代码行旁的值) |
| 盯一组关键量 | Watch table(监视表), 可在线修改/强制(force) |
| 看时序波形 | Trace(示波器式采集; 1200 固件 V4.0 起支持, 需真实 CPU——PLCSIM 不支持录制) |
| “打印“日志 | 写入专用诊断 DB(环形缓冲 + 时间戳), HMI 或监控端读取 |
| 事后分析 | CPU 诊断缓冲(在线与诊断 -> 诊断缓冲) |
| 仿真调试 | PLCSIM + 上述手段(注意 PID_Compact V2.x 不支持对经典 1200 仿真) |
调试用诊断 DB 的最小实现:
// 全局 DB "DB_Diag"
// idx : DInt
// log : Array[0..99] of String[40]
// 需要记录处:
"DB_Diag".idx := ("DB_Diag".idx + 1) MOD 100;
"DB_Diag".log["DB_Diag".idx] := CONCAT(IN1 := 'E', IN2 := INT_TO_STRING(#errorCode));
C/Python 对照: 心智模型从 gdb/IDE 断点 + print 切换到“永远在线的仪表“: Watch table 像调试器的 watch 面板(但常驻、免断点), Trace 像 logic analyzer/示波器, 诊断 DB 像结构化日志。关键差异: PLC 的“调试“不打断被控过程——设备在转, 你不能暂停真实世界, 这也是断点在低危 CPU 上被砍掉的根本原因。
陷阱 18: FC 不是纯函数——它有副作用, 形参也会“脏“
现象: 把 FC 当无副作用的纯函数用, 后来发现它悄悄改了全局 DB; 标准访问的 FC 里读到的输入形参值有时不对。
根因: FC 只是没有自己的持久存储, 并不妨碍它读写全局 DB 与 M 区——“FC=纯函数“是错觉。且标准访问的 FC 中, 未连接/未写入的形参与 Temp 同源, 值不确定。
正确写法: FC 内部不直接读写全局 DB(依赖全走形参); 形参先赋值再读; 需要跨周期记忆就换 FB。
C/Python 对照: C 函数也能改全局变量, 这半条类比成立; 但 C 没有“未初始化形参“, 形参那半条是 PLC 特有。
陷阱 19: 优化块的 Temp 会被预置默认值——坏习惯被掩盖
现象: 在(默认的)优化块里 Temp 不初始化也“没事“; 把代码搬到标准块(为做 AT/PEEK/通信表)后, 同一段代码静默读出脏数据。
根因: S7-1200 固件 >= V4(及 S7-1500 全系)的优化块中, Temp 每次调用前被预置默认值(数值 0); 标准访问块没有这层保护。
正确写法: 始终按陷阱 2 的“Temp 必脏“纪律写代码, 不因当前块恰是优化块而偷懒——代码是会被搬家的。
C/Python 对照: 相当于“Debug 构建清零、Release 构建不清零“的经典差异: 行为依环境而变, 唯一安全假设是“未初始化即未知“。
陷阱 20: 布尔表达式不短路求值
现象: IF #i <= #n AND #a[#i] > 0 THEN 在 #i 越界时照样触发访问错误——守卫没起作用。
根因: SCL 的 AND/OR 对两个操作数都求值, 没有短路语义(不同于 C 的 && 与 Python 的 and)。
正确写法: 守卫拆成嵌套 IF:
IF #i <= #n THEN
IF #a[#i] > 0 THEN
// 安全: 只有第一层成立才求值第二层
END_IF;
END_IF;
C/Python 对照: 这是 Pascal 系语言的经典差异; 所有依赖短路的条件表达式(ptr && ptr->val、i < n and a[i])都必须重写成嵌套 IF, 无一例外。
陷阱 21: 同一输出多处赋值——最后写者胜, 且周期末才刷新
现象: 两个 FB 都驱动同一个 Q, 输出“偶发不对“, 无任何编译告警; 写了 Q 后立即读回, 读到的是旧值。
根因: 一个扫描周期内对同一地址多次赋值, 只有最后一次生效; 物理输出在周期末统一刷新; 读 %Q 读到的是输出映像而非端子电平。
正确写法: 一个输出只有一个“属主“代码路径; 模式切换用显式优先级 IF 或 SEL/MUX 收敛到一次赋值。确需本周期立即生效时写 %Q:P(直写外设, 慎用)。
C/Python 对照: 像多写者竞争, 但没有竞态——执行顺序确定(自上而下), 只是反直觉: “赋值即生效“的事件驱动直觉不迁移。
陷阱 22: 输入映像每周期只采样一次——窄脉冲整体消失
现象: 按钮/传感器的短脉冲偶尔“没收到“, 以为是程序 bug。
根因: %I 在每个扫描周期开始时快照一次; 比扫描周期短的电平变化(或恰好落在两次采样之间的脉冲)完全不可见。
正确写法: 短脉冲/高频信号用硬件中断 OB(编号 >= 123)、HSC 高速计数器, 或 %I:P 直读外设; 普通按钮无需处理(按压时长远大于周期)。
C/Python 对照: 采样率不足的 ADC——Nyquist 直觉直接迁移: 扫描周期就是你的输入采样周期(典型 1~10 ms)。
陷阱 23: 两次 FB 调用共用一个实例 DB——状态串扰
现象: 两台“同型号设备“互相影响: 一台复位, 另一台的定时器/状态也乱了; 偶发性与陷阱 7 相似, 极难排查。
根因: FB 的全部状态在实例 DB 里; 同一实例接两台设备 = 两个“对象“共享同一块内存。
正确写法: 每个“对象“一个实例——多重实例(在父 FB 的 Static 里声明)或独立实例 DB; 排查偶发状态异常时, 第一步检查实例 DB 是否被两处复用。
C/Python 对照: 误共享 static/单例的经典事故, 完全同构。
陷阱 24: 在线改值会被用户程序在下一个周期改回去
现象: Watch table 给变量写了新值, “没反应“或一闪而过; 以为通信或程序坏了。
根因: 只要程序每周期给该变量赋值, 手写的值立即被覆盖——与 gdb set var + continue 的预期完全不同。
正确写法: 调试用 force(仅 I/Q 可强制); 或程序里预留 manual/auto 使能分支; 或临时禁用写它的逻辑(记得恢复)。
C/Python 对照: 调试器改值后被主循环覆盖; PLC 里这是常态而非异常, 设计阶段就要给“人工干预“留门。
陷阱 25: 非保持数据掉电即回初始值
现象: HMI 上改好的设定值, 一次停电后“自己变回去“; 现场投诉高发。
根因: DB/M 区数据默认不保持: 暖启动后回到组态初始值; 只有显式设置保持性(retentive, 容量有上限)或写入存储卡/配方机制的数据才掉电保留。
正确写法: 运行期可写参数在 DB 声明中设保持性(注意保持性存储容量上限); 大批量参数用配方/存储卡; 关键累计量显式设计掉电策略。
C/Python 对照: “所有变量都是易失的, 除非你显式放进 EEPROM”——PLC 把这个选择交给程序员, 默认易失。
陷阱 26: “OB1 = main()“类比的边界
现象: 以为整个程序只有一个入口; 多 OB 的执行时机与预期不符。
根因(类比必须标注的边界): 启动 OB(100 或 >= 123)在进入循环前执行一次(≈ init/全局构造); 多个程序循环 OB(同为优先级 1)每周期按编号依次执行, 不是并行; 任何 OB 执行中都可能在语句级被更高优先级 OB 抢占(见陷阱 12); OB 不可重入。
正确写法: 初始化放启动 OB; 程序循环只留 OB1, 不同频率的任务用循环中断 OB(OB30~38)而不是多个循环 OB。
C/Python 对照: 更接近“固定优先级抢占式 RTOS 的任务集“而不是单线程 main: 每个 OB 是一个任务, 优先级表就是调度器; main 只对应了其中一个任务。
陷阱 27: 整型运算静默回绕——Python 背景毫无防备
现象: Int 计数器到 32767 后变成 -32768; 大数乘法/累加结果莫名变负; 无任何告警。
根因: SCL 整型就是 C 的定宽补码整数: 溢出回绕, 编译器与运行时都不报。Python 的 int 无限精度, 从 Python 过来毫无防备(C 背景至少有肌肉记忆)。
正确写法: 计数/累计/大数组下标一律 DInt/UDInt; 关键累加处显式判上限:
IF #count < #COUNT_MAX THEN
#count := #count + 1;
END_IF;
C/Python 对照: 与 C 完全一致(细节上 C 的有符号溢出是 UB, SCL 是定义良好的回绕, 工程上同样不能依赖); 与 Python 完全不同。
最佳实践
1. 命名规范
原则: 全项目统一; 不靠大小写区分语义(陷阱 14); 避开保留字。推荐一套可直接用的:
| 对象 | 风格 | 示例 |
|---|---|---|
| FB/FC/OB | PascalCase + 前缀 | FB_Conveyor, FC_CalcAvg |
| 全局 DB | DB_ 前缀 | DB_IO, DB_Recipe |
| UDT | PascalCase | MotorConfig |
| 实例 DB | 块名 + _Inst | FB_Conveyor_Inst |
| 局部变量/Input/Output | camelCase | cmdStart, speedActual |
| 常量(CONST) | UPPER_SNAKE | ST_RUNNING, CMD_TIMEOUT_MS |
| I/O tag | 物理功能名 | ixStart(i=输入 x=Bool)之类按需定 |
变量名里就编码类型/方向是团队自选项, 但常量用 UPPER_SNAKE 与状态名前缀 ST_ 强烈推荐——它们是 SCL 里枚举的替身(见下)。
2. UDT 优先
凡“同一结构出现在多处“(每台电机一组参数、每段配方一组字段), 先定义 UDT(用户数据类型), 再在 FB 接口、全局 DB、实例 DB 中复用:
TYPE "MotorConfig" :
STRUCT
maxSpeed : Real;
accelTime : Time;
invert : Bool;
END_STRUCT;
END_TYPE
// FB 接口: Input: cfg : "MotorConfig"
// 全局 DB: motors : Array[1..8] of "MotorConfig"
等于 C 的共享 struct 头文件: 改一处定义, 全部使用点编译期强制同步; 且 SCL 的 Array of UDT 天然替代“多台同型设备“的复制粘贴。类比成立, 差异是 UDT 改定义后所有引用块需重新编译下载。
3. 状态机惯用法: CONSTANT + INT 的枚举替身(完整骨架)
SCL 没有枚举类型。惯用法: 块内 CONST 定义状态码, Static 里存 state : Int, CASE 驱动。完整可抄的骨架(含延时、互锁、故障、HMI 状态码):
// ============ FB "FB_Pump" 接口(声明区) ============
// CONST
// ST_IDLE : Int := 0;
// ST_STARTING : Int := 1;
// ST_RUNNING : Int := 2;
// ST_STOPPING : Int := 3;
// ST_FAULT : Int := 99;
// END_CONST
// Input:
// cmdStart : Bool; // 电平
// cmdStop : Bool;
// cmdReset : Bool;
// faultSig : Bool; // 外部故障(电平)
// startDelay : Time := T#3S;
// stopDelay : Time := T#5S;
// Output:
// runCmd : Bool; // 接触器命令
// stateId : Int; // 给 HMI 显示
// ready : Bool;
// Static:
// state : Int;
// faultLatch : Bool;
// tmrStart : TON_TIME; // 多重实例
// tmrStop : TON_TIME;
// ============ 代码区 ============
// 1) 故障处理最优先: 任意状态 -> ST_FAULT
IF #faultSig THEN
#faultLatch := TRUE;
END_IF;
IF #faultLatch AND #cmdReset AND NOT #faultSig THEN
#faultLatch := FALSE;
END_IF;
// 2) 定时器集中无条件调用(避免陷阱 6/7), IN 由状态译码
#tmrStart(IN := (#state = ST_STARTING), PT := #startDelay);
#tmrStop (IN := (#state = ST_STOPPING), PT := #stopDelay);
// 3) 状态转移与输出
CASE #state OF
ST_IDLE:
#runCmd := FALSE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #cmdStart AND NOT #cmdStop THEN
#state := ST_STARTING;
END_IF;
ST_STARTING:
#runCmd := TRUE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #tmrStart.Q THEN
#state := ST_RUNNING;
END_IF;
ST_RUNNING:
#runCmd := TRUE;
IF #faultLatch THEN
#state := ST_FAULT;
ELSIF #cmdStop OR NOT #cmdStart THEN
#state := ST_STOPPING;
END_IF;
ST_STOPPING:
#runCmd := FALSE;
IF #tmrStop.Q THEN
#state := ST_IDLE;
END_IF;
ST_FAULT:
#runCmd := FALSE;
IF NOT #faultLatch THEN
#state := ST_IDLE;
END_IF;
ELSE // 非法状态(如下载后 DB 脏值)兜底回 IDLE
#state := ST_IDLE;
END_CASE;
// 4) 集中导出对外状态
#stateId := #state;
#ready := (#state = ST_IDLE) AND NOT #faultLatch;
要点: 定时器实例的 IN 用状态译码驱动、调用永不进分支; 状态码 0 给初始态(Static 默认 0, 下载初始化即回 IDLE); ELSE 分支兜底; HMI 只读 stateId, 不读内部 flag。C/Python 对照: 这就是函数指针表/字典分发的 switch 状态机, CONST 相当于 enum { ST_IDLE = 0, ... }; 差异是没有真正的枚举类型——状态码本质是裸 Int, 类型系统不防你把 stateId 赋成 47, 所以不要复用状态变量存任意数值。
4. FB 单一职责
一个 FB 只管一个工艺对象(一台泵、一段输送机、一个配方槽)。判据: 名字能用一句话说清; 接口不超过约 15 个信号; 不读写自己的全局 DB; 内部不调用“隔壁设备“的 FB(设备间协同放到上层 FB 或 OB1 编排)。层层嵌套 FB 是允许且推荐的(顶层 FB 用多重实例持有子 FB), 与 C 的模块分层同构。
5. 库化复用
把验证过的 FB/FC/UDT/全局 DB 组拖入 TIA 的 project library / global library, 从库里拖出使用而非复制粘贴; 库支持版本化(库版本 = 你的 release)。修改流程: 改库内对象 -> 升库版本 -> 各项目更新引用。配合 UDT, “第三台产线“的搭建方式是“从库拖件 + 改参数”, 不是“复制整个项目改名字“。类比: global library = 内部包仓库(nuget/pypi), 库版本 = 语义化发布的依赖。注意跨项目复制 FB 时其依赖(UDT、被调 FB)必须一起带走, TIA 的“组复制“(copy group)就是为此设计。
6. TIA 项目的版本管理思路
TIA 项目文件(.ap17/.ap18/…)是二进制容器, git 无法做有意义的 diff/merge。现实可行的组合:
- 块级源码入库: 用 TIA 的外部源(external source)功能把 SCL 块导出为 .scl 文本文件(或用 TIA Openness API 批量导出块/PLC 变量为 XML)纳入 git, 代码评审、diff、 blame 都落在这一层; 二进制 .ap 文件也提交(可配 LFS), 但只当“完整快照“。
- 变更对比: TIA 自带项目/块比较工具(离线 vs 在线、项目 vs 项目), 用于人工比对两版本差异, 相当于图形化 diff。
- 里程碑用库版本管理: 发布给其他项目的块冻结为库版本, 项目里记录“用的哪个库版本“。
- 提交纪律: 二进制无法 diff, 所以 commit message 必须写清改了哪些块、为什么——相当于强制性的 CHANGELOG。
Openness API 是官方脚本化接口(可编程导出/导入、自动编译下载), 把“导出源码 + 编译验证“做成 CI 步骤是成熟做法(具体 API 版本随 TIA 版本变化, 使用前查对应版本文档)。
7. 在线下载对 DB 值的初始化影响
下载不是无害操作。关键事实与策略:
- 下载改动过的 DB 时, 默认会用组态的初始值重新初始化当前值——运行时累计的计数值、当前状态码会丢。关键事实(系统手册 15.16): 下载不会清除保持性存储器中的现值(想清除只能恢复出厂设置); S7-1200(固件 V4.x)支持“不重新初始化下载“(Download without reinitialization), 但仅限优化块内新增标签且不超过内存预留(新建块默认预留 100 字节, 可在块属性中调整; 新增保持性标签需另配保持性内存预留); 修改既有标签的名称/数据类型/数组长度/保持性, 仍会强制重新初始化(当前值复位为初始值)。
- 策略 1: FB 的 Static 里, 所有“可从外部重建“的初值写进声明默认值(state := 0 就是设计的一部分, 见状态机骨架)。
- 策略 2: 关键初始化逻辑放启动 OB(S7-1200 上启动 OB 编号 100 或 >=123, CPU 每次暖启动都会执行它), 不依赖下载时的默认值行为——这等于 C 程序的
init()显式初始化, 比“相信加载器清零“可靠。 - 策略 3: 改 DB 结构前先想清楚哪些是“运行时数据“(计数值、批次号), 把它们隔离到独立 DB, 与“参数/组态“分开, 下载影响面就可控。
- 策略 4: 生产设备上的下载遵守流程: PLCSIM 验证 -> 停机窗口 -> 下载 -> 核对初始化效果。RUN 模式下载仅对兼容改动可用, 不兼容改动会触发 CPU 停止/重启。
附: 陷阱速查表
| # | 陷阱 | 一句话防御 |
|---|---|---|
| 1 | := 与 = | 赋值 :=, 比较 =, 不等 <> |
| 2 | Temp 脏数据 | 先写后读; 跨周期记忆放 Static |
| 3 | 数组越界 | 0 基声明 + LOWER/UPPER_BOUND; 1200 越界不 STOP |
| 4 | 优化 DB | 默认优化; 需绝对地址/AT/PEEK 才用标准 DB |
| 5 | REAL 相等 | 容差比较; Int/Int 相除是整除 |
| 6 | 边沿 | R_TRIG 实例化, 无条件调用 |
| 7 | 条件调用 FB | 每周期调用, enable 做输入; 不使能时显式清输出 |
| 8 | WHILE 阻塞 | 延时一律 TON; 大任务分片跨周期 |
| 9 | 除零 | 预检分母; 错误是 ENO=0 + 返回 0, 不是异常 |
| 10 | 字节序 | 大端; SWAP/移位拼装; DWORD_TO_REAL 是位搬运 |
| 11 | STRING | 定长 n+2 字节; 默认 254; 赋值截断 |
| 12 | 中断并发 | 共享量单字原子 + 序号校验; 临界区放高优先级 OB |
| 13 | TIME 回绕 | 差值法; 优先 TON |
| 14 | 大小写 | 全局不区分大小写, 勿靠大小写区分名字 |
| 15 | 巨型 OB1 | OB1 只调度; FB 封装; 全局只留 I/O |
| 16 | 大 Temp | 大数组走 DB + InOut 引用 |
| 17 | 调试 | 1200 无断点: Watch/Trace/程序状态/诊断 DB |
| 18 | FC 非纯函数 | FC 可写全局 DB/M 区; 形参先赋值再读; 要记忆用 FB |
| 19 | 优化块 Temp 预置默认值 | 始终按“Temp 必脏“写代码, 搬到标准块才不炸 |
| 20 | 布尔不短路 | AND/OR 全操作数求值, 守卫拆嵌套 IF |
| 21 | 多处驱动同一输出 | last-write-wins; 立即生效写 %Q:P(慎用) |
| 22 | 窄脉冲丢失 | 输入映像每周期采样一次; 用硬件中断/HSC/%I:P |
| 23 | 共用实例 DB | 一个对象一个实例, 严禁两处共用 |
| 24 | 在线改值被覆盖 | 周期程序会改回去; 用 force 或 manual 分支 |
| 25 | 掉电丢设定值 | 可写参数设保持性(retentive)或存配方/存储卡 |
| 26 | OB1=main 类比边界 | 启动 OB 先行; 多循环 OB 按序; 语句级可抢占; 不可重入 |
| 27 | 整型静默回绕 | Int 32767 翻负无告警; 计数用 DInt 并设上限 |
| - | 平台专属易踩指令 | TIME_TCK/REF/POINTER/ANY/LTIME/断点均为 1500 独占, 1200 不可用 |
练习项目阶梯: 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/DB | 可 | L0~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.0 | Bool | 启动按钮 (PLCSIM 中 Modify) |
| bStop | %I0.1 | Bool | 停止按钮 |
| bRun | %Q0.0 | Bool | 电机运行 |
| iScanCnt | %MD10 | DInt | 扫描计数器 (观察用) |
提示:
- 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.1 | Bool | 甲/乙地启动, 常开瞬动 |
| bStopA / bStopB | %I0.2 / %I0.3 | Bool | 甲/乙地停止, 常闭 |
| bEstopNC | %I0.4 | Bool | 急停, 硬件常闭 |
| bFault | %I0.5 | Bool | 过载/变频器故障 |
| bRun | %Q0.0 | Bool | 运行 |
提示:
- 现场约定: 停止类信号 (停止按钮、急停、安全门) 一律接常闭。断线时信号变 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 全红A | 红 | 红 | 1s |
| 3 东西绿 | 红 | 绿 | 10s |
| 4 东西黄 | 红 | 黄 | 3s |
| 5 全红B | 红 | 红 | 1s |
输出 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):
| 类型 | 字段 | 类型 | 初值 | 含义 |
|---|---|---|---|---|
| udtMotorCfg | rMaintHours | Real | 2000.0 | 维护阈值 [h] |
| udtMotorCfg | tFaultOn | Time | T#200MS | 故障确认延时 (滤毛刺) |
| udtMotorSta | bRun / bFault / bMaintDue | Bool | FALSE | 运行/故障锁存/待维护 |
| udtMotorSta | rRunHours / iStartCnt | Real / Int | 0 | 累计运行时长 [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 (非优化, 按绝对地址访问):
| 字段 | 类型 | 初值 | 含义 |
|---|---|---|---|
| conn | TCON_IP_v4 | ConnectionType=16#0B (TCP), ID=1, ActiveEstablished=FALSE (从站被动), LocalPort=502 | 连接描述 |
| hr | Array[0..15] of Word | 0 | 暴露的 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 手册为准)。 步骤清单:
- 项目树添加设备 HMI > KTP700 Basic PN; 在 Connections 里把 HMI 的 Ethernet 接口连到 PLC 的 PROFINET 接口 (仿真时同一 PLCSIM 实例)。
- HMI tag 表: 建 HMI 变量, 连接指向该 PLC, 地址用符号路径 (如 fbPlant_DB 的 static: aMot[1].sta.bRun — 优化 DB 必须符号访问)。
- 画面 1 总览: 3 个电机图形 (圆/矩形), 填充色绑 bRun/bFault/bMaintDue (绿/红/黄)。
- 画面 2 操作: 启动按钮用 Set bit while pressed + 释放复位 组合模拟瞬动 (或在 PLC 侧脉冲化), 停止按钮注意常闭语义; I/O 域显示 rRunHours; 画面上若放急停, 必须标注“非安全功能“。
- 画面 3 趋势: 实时曲线绑 rRunHours 或 3.3 的 rPv。报警: PLC 侧离散量报警 (bFault/bMaintDue), HMI 报警视图分报警/警告两类, 支持确认。
- 配方: 定义配方 (元素 = 3 台电机 cfg 参数), 建 3 条数据记录; 画面放配方视图, 支持从 PLC 读/写到 PLC。
- 仿真: 先启动 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)
分块清单 (先建全部空块再逐个填):
| 块 | 类型 | 职责 | 复用 |
|---|---|---|---|
| OB1 | SCL | 调 fbLine | |
| OB30 (100ms) | cyclic interrupt | 时基/平滑量 (滤波) | 2.1 |
| OB100 | startup | 初始化 (时长表、数组) | 3.1 |
| fbLine | FB | 产线状态机、顺序启停、联锁 | CASE 手法 3.1 |
| fbConveyor | FB | 单段皮带: 电机+堵转检测+下游满停 | 内含 fbMotor (3.2) |
| fbSilo | FB | 料位换算+缺料滞回报警+下料阀时序 | 2.2 |
| fbPacker | FB | 计件装箱 | 1.3 |
| fbShift | FB | 班次判定+产量环形归档 (8 班) | 时间处理 |
| fbAlarm | FB | 报警锁存、首出报警、复位 | 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。其余条目都是这四个角色的替代品或补充。
目录
-
- 最小核心集
-
- 官方文档(手册与规范)
-
- 工具与门户(检索与下载入口)
-
- 书籍(含免费在线文字教程)
-
- 视频课程
-
- 在线课程与系统培训
-
- 论坛社区
-
- 示例代码
-
- 推荐使用顺序(按学习阶段映射)
-
- 快速答疑地图
-
- 收录说明与待核实项
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 server | System 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 播放列表 |
| 要用英文提问或搜英文历史问题 | PLCtalk | r/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_TCK、VAR_IN_OUT、NORM_X)。
提示:
- 索引由 mdBook 的自定义渲染后端(Rust, 见
pagefind-renderer\)在每次构建时自动生成, 包括mdbook serve的热重建 —— 无需手动步骤。- 直接双击打开
book\html\search.html(file:// 协议)会被浏览器拦截索引请求, 请通过tools\mdbook.exe serve访问。