做控制的兄弟对这套流程应该不陌生:Simulink 里模型跑得飞起,曲线调得漂漂亮亮,到了要上 PLC 的那一步,就成了在博图(TIA Portal)里对着 SCL 硬啃。我见过同事手动翻译一套状态机算法,翻了两周,仿真与实机一对比还发现“翻译错了一行”,连夜排查到头秃。后来我开始用 Simulink PLC Coder 把模型直接转成 PLC 代码,再导进博图跑,效果比手动翻译稳定得多。这篇不准备铺开讲理论,就按我实际走过一遍的流程,把最关键的几个要点拎出来。适配博图的链路里,60% 的问题其实出在建模习惯和配置选项上,真正卡导入的反而少。适合正在做控制算法落地的工程师、调试人员,也适合刚接触“模型转 PLC”的人当一张避坑地图。
1. 为什么要把 Simulink 模型“翻译”成博图能认的语言
1.1 三条路线,我为什么最后选了直出 SCL
先说方案选型。把 Simulink 算法弄到 PLC 里跑,市面上常见的有三条路线:手写 SCL、转 C 代码再想办法接进去、直接用 Simulink PLC Coder 生成结构化文本。
手写 SCL 是最传统也最容易出问题的路。模型里的逻辑一旦复杂起来,比如状态机嵌套、多速率多中断,手写代码量会成倍增长,而且仿真模型和 PLC 代码就成了两份需要同步维护的东西。更麻烦的是,人的翻译永远有出错概率——前面提到的把 if 条件顺序写反只是个缩影,边界处理、溢出、初值不一致这些坑更隐蔽。转 C 代码那一路通常也不适合标准 PLC 环境。普通中小型 PLC 原生执行不了 C 代码,要么外加工控机,要么通过 OPC 之类的通讯中转,实时性和成本都不划算。对产线上那类“拍一下按钮几毫秒内要有响应”的场景,这条路基本走不通。
所以我最后选了 Simulink PLC Coder 直出 IEC 61131-3 结构化文本(ST),博图的 SCL 就是这一标准的方言。模型里验证过的逻辑直接变成函数块代码,改模型重新生成一份就完事,不用维护两套东西。这条路的价值核心是:仿真和实机跑的是同一份“逻辑来源”,一致性有保证。
1.2 博图为什么认 SCL,二者怎么搭上线
很多人一听“代码生成”就以为要复制粘贴几百行文本,其实博图提供了现成的导入通道。TIA Portal 左侧项目树里有一个叫“外部源文件”的区域,右键添加一个新的外部文件,选择生成的.scl后缀文件,再右键“从外部源文件生成块”,博图就能把它编译成一个函数块(FB)或函数(FC),然后拖进 OB1 调用,整个过程不算复杂。
SCL 本身是博图对 IEC 61131-3 文本语言的具体实现,而 PLC Coder 生成的就是这个标准下相当“干净”的 ST 代码,二者天然兼容。打个不严谨的比方,模型是设计图纸,SCL 是施工说明,博图就是照说明干活的施工队。只要图纸画得规范,施工队基本不用额外发挥。实际导入时偶尔需要微调声明区,大部分情况下生成代码的兼容性已经足够好,这也是我敢把它写进常规工作流的原因。
1.3 什么时候这条路不值得走
不是所有模型都适合转码。如果控制逻辑很简单,比如一个启停联锁、一堆互锁条件,手写 SCL 五分钟能写完,没必要为了“自动化”而自动化。模型里如果塞了大量自定义 MATLAB Function、第三方工具箱模块,或者严重依赖连续域求解,生成代码的改造成本可能比重新手写还高。另外还要看团队条件,没有对应工具授权、没人用过,硬推这套工作流,光是踩配置选项的坑就能耗掉一星期。
我的判断标准很简单:算法是否有状态、是否有复杂时序逻辑、是否需要在仿真里反复调参验证。满足其中两三条,转代码就值。否则就老实手写。
2. 建模阶段的硬性门槛,改不好后面全白搭
2.1 求解器、采样时间与 PLC 扫描周期怎么对齐
这一步决定整个转换能不能走通。模型配置里的 Solver 必须改成固定步长(Fixed-step),求解器选discrete (no continuous states),也就是纯离散求解。原因很直白:PLC 的执行方式是循环扫描,每个扫描周期跑一遍相同的代码,没有求解器去迭代解连续方程。你模型里如果有连续积分器,生成代码时会被直接拒绝,或者生成出逻辑上没法跑的东西。
步长设置不是随便填的。工程上一般直接把模型步长设成 PLC 主循环 OB1 的周期,比如你打算让 OB1 每 10ms 调用一次逻辑,模型步长就设 0.01。这样生成代码的采样行为和 PLC 实际执行节奏严格对齐,仿真和实机输出才有可比性。如果模型里有多个采样时间,比如传感器信号是 5ms 采集、控制输出是 10ms 计算,那中间必须加Rate Transition模块,并且让各个采样时间是基础步长的整数倍。多速率如果不同步,生成代码在 PLC 里会产生异步任务,调试时根本分不清信号到底来自哪一拍。
我自己习惯把模型里的连续积分器、连续传递函数全部替换成离散版本。比如 PID 控制器,Simulink 的 PID 模块可以直接选离散公式(如后向欧拉),传递函数用 z 域的离散传递函数重构。这块改造越彻底,后续生成越省事。
2.2 数据类型和命名:提前定好“翻译规则”
数据类型映射是生成代码后出现怪问题的高发区。Simulink 里默认的 double 类型,PLC Coder 通常映射成 LREAL,也就是 64 位浮点数。博图里 LREAL 能用,但很多主流中小型 PLC 对 64 位浮点运算的支持和效率都不如 32 位 REAL。模型里几百个浮点运算,全部按 LREAL 算,扫描周期可能直接被拖垮。所以我的做法是:模型内部尽量把信号设成 single,并在生成配置里把 double 映射成 REAL。这一点是我踩过几次坑之后总结的,配置里一个选项就能避开后面所有性能问题。
整数类型也要心里有数,常见的映射关系大概是:
| Simulink 类型 | PLC 类型 |
|---|---|
| double | LREAL(默认)或 REAL |
| single | REAL |
| boolean | BOOL |
| int8 | SINT |
| uint8 | USINT |
| int16 | INT |
| uint16 | UINT |
| int32 | DINT |
| uint32 | UDINT |
命名规范同样重要。Simulink 允许信号名带中文、带空格,但生成的 SCL 进博图后,变量名只要带空格,声明区直接报语法错误;中文名碰到编码问题更是两眼一抹黑。我现在的习惯是模型里所有端口、子系统、信号线、Data Store 全部用英文字母加下划线,开头必须是字母,长度控制在 30 个字符以内。别觉得这限制太死,等你在博图里看报错行号的时候就会感谢自己当初命名干净。
2.3 连续环节与代数环:必须在建模期“消灭”
代数环是模型转 PLC 里最让我头疼的问题。Simulink 仿真时遇到代数环还能用数值迭代硬解,但生成到 PLC 后没有求解器,代码里出现“这一个周期的输出依赖同一周期的另一个输出”就是死循环,轻则生成失败,重则生成出逻辑错误的代码。所以我会在模型诊断配置里把 Algebraic Loop 的诊断级别直接设为 error,让问题在仿真阶段就爆出来,而不是等到生成代码时一脸懵。解决代数环的办法通常是打断回路,插入一个Unit Delay或离散滤波,让某一路信号用上一拍的值,这样数据流就闭环了。
连续状态模块也要清理干净。Integrator、连续Transfer Fcn、Derivative这类模块在 PLC Coder 面前基本都是拦路虎。替代方案很多:积分器换成离散积分器,微分环节换成离散差分加滤波,传递函数用离散化公式重写。说白了,建模的时候就要把它当成一段将要周期执行的代码来审视,而不是一个可以随便调用的数学表达式。每条反馈回路都问自己一句话:这一个计算周期里,数据流能不能在有限步骤里完整走完?不能的话,就得改。
3. 生成 PLC 代码的配置细节
3.1 生成前配置清单:60% 的问题出在这里
我估计很多人第一次用 PLC Coder 生成代码,都是点开一个窗口就按生成。但实际情况是,配置项没设对,生成出来的代码要么没法导入博图,要么导入后行为跟仿真差一截。我现在每次生成前会过一遍配置清单,基本能省掉后面的返工:
第一,Code Generation 里的 System Target File 要选 PLC Coder 对应的目标,装好工具后在选项中能找到 PLC 相关条目。第二,生成语言选结构化文本(ST/SCL),而不是 C。第三,IDE 选项如果有 SIMATIC S7 / TIA Portal 之类的条目,直接选它;没有就选 Generic,生成的代码也能手导进博图。第四,数据类型替换配置要认真看,把前面说的 double 映射成 REAL 落实到位。第五,确认顶层模型或子系统是要生成 FB 还是 FC,一般带内部状态的模型选 FB。第六,代码可读性选项在调试期打开,交付阶段再切紧凑模式。
生成按钮点下去之后,代码通常落在slprj目录下的某个子目录里,找到.scl文件它就是最终产物。我每次生成完会顺手用文本编辑器打开看一眼文件大小和开头声明,如果连文件都是空的那多半是模型里还有不支持的模块,早发现早处理。
3.2 读一读生成的 SCL:结构、声明和初始化逻辑
很多人把 SCL 文件导入博图就算完事了,从来不看里面到底写了什么。我建议至少要读一遍,不需要逐行懂,但要搞清楚三个东西:块的外在接口、内部状态变量、初始化逻辑。
生成出来的函数块通常长这样:
FUNCTION_BLOCK FB_Controller VAR_INPUT Setpoint : REAL; Feedback : REAL; END_VAR VAR_OUTPUT ControlOut : REAL; END_VAR VAR state1 : REAL; initDone : BOOL; END_VAR输入输出变量对应模型里的 Import 和 Outport。模型里的 Memory、Unit Delay、积分器状态会落到 FB 的静态变量区或背景 DB 里。初始化逻辑在多数生成代码里是用一个initDone之类的标志位实现的,第一次调用时给内部状态赋初值,之后置位不再重置。有的版本会生成单独 INIT 操作段或独立调用块,要看生成代码里的注释约定。
这一步理解之后,你在博图里调背景 DB 的保持属性、找中间变量、判断掉电恢复行为都会清晰很多。千万不要把生成代码当成黑盒,后面排查问题全靠对这些结构的熟悉程度。
3.3 浮点、状态保持和 IO 承接
生成代码里如果出现浮点相等判断,要格外小心。模型里a == b在连续仿真里也许成立,但 PLC 上两个浮点变量经过不同计算路径得到的结果很难严格相等。这类比较在建模阶段就该改成容差范围比较,比如ABS(a - b) < 0.001。
状态保持是工程上容易忽略的点。如果控制器要求掉电后保留累计量、当前位置等历史状态,生成 FB 的背景 DB 里对应变量要设置保持属性(Retain)。更稳妥的办法是在建模阶段就把需要保持的数据用 Data Store Memory 管理,并映射到外部全局 DB,再把那个 DB 整体设为保持性。别等项目上线了才发现一个掉电重启,控制器状态全部归零,那场面相当尴尬。
关于 IO 承接,要明白模型里的 Import 和 Outport 只是算法逻辑接口,不是硬件地址。模拟量输入模块采集的原始值、输出模块要写的工程百分比,这些工作应该在博图侧完成。算法块只负责“接收到工程值、输出控制量”,外围的量程转换、滤波、故障安全由接口程序负责。这个分工想明白了,很多后续联调问题都能提前避免。
4. 导入博图与挂接实操
4.1 把 SCL 文件“装”进 TIA Portal 的标准动作
导入流程本身不复杂,但细节决定成败。新建博图项目,选一个主流 CPU 机型(不同型号对 SCL 支持度有差异,常见中小型 PLC 基本都支持)。左侧项目树找到“外部源文件”,右键选择“添加新的外部文件”,定位到生成的.scl文件。接着右键这个外部文件,选择“从外部源文件生成块”,确认块名称和类型就可以生成。
这个流程里我最常遇到的问题不是文件本身,而是编码。生成文件如果是 UTF-8 带 BOM,博图一般认;如果碰到乱码,尤其是模型里带中文注释时,导入时很可能报语法错误。所以我导入之前会先用文本编辑器打开.scl看一眼,确认注释没乱码、声明区格式正常。如果生成块失败,TIA Portal 会给出错误行号,多数情况可以顺着行号从声明区开始排查。
4.2 OB1 调用、背景 DB 与 I/Q 地址接线
生成的 FB 生成之后不会自动跑,必须在 OB1 里调用。把 FB 拖拽到 OB1,博图会弹出调用选项让你分配一个背景 DB(Instance DB),名字建议起得和 FB 对应,比如Inst_Controller。背景 DB 里保存着这个实例的输入输出和静态数据,如果现场有两台设备都用同一个算法,那就创建两个背景 DB,互不干扰。
输入输出的连接是算法块和现场信号的“最后一公里”。在博图变量表里定义符号变量,比如模拟量输入原始值对应的符号,然后在调用 FB 的时候把输入引脚连上这个符号。如果模型输入期望的是工程值(0~100℃),而模拟量模块给你的是原始整数(0~27648),那就需要在接口处做一次量程转换,可以写一小段 SCL 放在 FB 调用前,也可以用标准的缩放函数。核心思想是:算法块里不掺 IO 杂活,IO 杂活都在调用环境里处理好。
4.3 PLCSIM 对照验证:别急着下载到真机
每次导入成功后,我都建议先用 PLCSIM(虚拟 PLC)把整个逻辑跑一遍,再考虑下载真机。验证方式很直接:构造一组相同的输入序列,分别喂给 Simulink 模型和 PLC 里的函数块,然后对比输出曲线。比如把输入存成数组,在 PLC 里逐周期循环执行并记录输出,最后导出来和 Simulink 仿真结果比较。浮点计算差异会有,但一般误差在千分之一以内可以接受,如果偏差大就要检查采样时间是否对齐、IO 刷新时序和信号噪声。
PLCSIM 阶段还要顺手看一眼 CPU 扫描时间。生成的代码计算量如果很大,OB1 扫描时间可能逼近甚至超过设定周期。我会把扫描时间的监控阈值设在 80% 以上报警,提前暴露性能问题。这块不查,真机上线再出问题,排查成本就完全不是一个量级了。
5. 常见坑位速查与细节修整
5.1 常见报错/异响速查表
下面这张表是我做这个工作流以来遇到次数最多的几类问题,按现象、原因、处理办法列出来,基本可以当排查索引看待:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 生成报错:continuous sample time 不支持 | 模型里残留连续模块 | 积分器、传递函数、微分环节全部换离散版本 |
| 生成报错:algebraic loop | 模型里有代数环 | 诊断配置改为 error 提前暴露,插入 Unit Delay 打散回路 |
| 生成的 SCL 导入博图后声明区报错 | 变量名含中文/空格,或类型不支持 | 统一英文命名,按数据类型映射表调整声明 |
| 运行结果与 Simulink 仿真偏差大 | 采样步长和 OB1 周期没对齐,IO 无滤波 | 对齐采样时间,信号链路加滤波或迟滞 |
| 背景 DB 里状态每次上电都被清零 | 变量没有保持属性 | DB 编辑器中把保持变量勾选 Retain |
| 在线监控看不到 FB 内部状态 | 博图开启优化块访问,内部变量不可见 | 关键中间量引出为 Outport,或关闭块的优化访问 |
| CPU 扫描超时报警 | 大量 64 位浮点运算、复杂数学函数 | 映射为 REAL,精简模型中的超越函数 |
5.2 几个反直觉但能救命的细节
第一,初始化逻辑不能每周期触发。生成代码通常自带首次调用判断,但如果你在博图里手动写了初始化信号,一定要确保它只在 OB100 或首次标志位时执行一次。我曾经在一台设备上看到积分器状态永远起不来,排查半天发现初值被放在每次扫描都调用的程序段里,等于每隔几毫秒就把状态清零一次,输出自然上不去。
第二,保持型 DB 和普通 DB 要提前区分。建模阶段就要想清楚哪些状态是掉电必须保持的,把它们放在独立的数据区里,生成后设置 Retain 属性。别等真机调试时才发现累计量一掉电就归零,那种低级错误很影响项目节奏。
第三,模型中间量一定要主动留监控口。生成到 FB 内部的变量在博图在线监控时不太好抓,尤其块访问优化开启后。我现在的习惯是:凡是调试时要观察的关键中间量,都专门引出成模型 Outport,或者用 Data Store Memory 映射成全局 DB 变量。这样在线监控直接拖到监控表里就能看到,省去一堆翻背景 DB 的功夫。
第四,注释强度要控制。模型里塞满中文注释,生成到 SCL 后可能出现编码问题,注释不比代码更“安全”。我现在只保留必要的关键注释,且用英文写,导入博图的成功率明显高一大截。
这条路走顺之后,我最大的体会是:转 PLC 代码不是一键生成就完事,真正的门槛在建模侧的“翻译友好度”和导入后的确认验证。你花一天把模型改到能生成,比花两周手写 SCL 然后对仿真一天要划算得多。动手之前把模型当成将要在 PLC 里周期执行的代码来设计,后面几乎每一步都会顺。