Simulink中的SIL软件在环仿真:模型与代码一致性验证指南
2026/8/31 7:01:50 网站建设 项目流程

SIL(Software-in-the-Loop,软件在环仿真)是Simulink模型开发流程里,从模型走向控制器代码时绕不开的一道验证关卡。这个系列到了第十七讲,我不打算再翻模块库,而是直接讲一个很多人会卡住的问题:模型在电脑里跑得好好的,生成C代码之后,行为还和模型一致吗?SIL就是用来回答这个问题的。这一轮的主角不再是Simulink模型本身,而是控制器算法经过代码生成后得到的C/C++代码。

适合看这篇的,是那些已经能搭Simulink模型、会用Scope和Data Inspector、现在要开始做代码生成和模型验证的工程师,尤其是汽车电控、电机控制、电源和电池管理方向。建议水平在“入门以上、量产未满”这个阶段,正好能衔接上。这篇最值得关注的信息不是SIL按钮在哪,而是SIL结果怎么判断、模型结构怎么划分才能让SIL顺利跑起来,以及从SIL往PIL和HIL走的时候要注意什么。

1. 先搞清楚SIL到底解决了什么问题

1.1 SIL在V开发流程中的位置

SIL通常和MIL、PIL、HIL放在一起讨论。这四个缩写分别对应不同的验证层次:

类型运行对象运行环境主要验证目标
MIL纯Simulink模型桌面MATLAB环境算法逻辑和控制策略是否正确
SIL由模型生成的C/C++代码宿主机PC,编译成本地动态库生成的代码行为和模型逻辑是否一致
PIL生成的C/C++代码目标处理器(MCU/MPU)代码在真实处理器上能否运行、时间性能是否满足
HIL被控对象模型 + 真实控制器硬件实时仿真机系统级闭环、信号链路、故障注入

SIL在中间这个位置,非常重要但又容易被误解。

具体做法是:把控制器模型生成C代码,然后在你当前这台电脑上编译成动态库,再把这个动态库包装回Simulink环境里,和剩下的被控对象模型做闭环仿真。也就是说,控制器部分跑的是代码,被控对象部分跑的还是模型。

这样做有一个很直接的好处:不需要任何目标硬件,就能验证“代码生成这一步有没有把算法逻辑改坏”。

1.2 常见误解和适用边界

我见过不少朋友一开始会这样理解SIL:

误解一:SIL只是把模型重新跑一遍。不对。SIL必须经过代码生成,没有代码生成就没有“软件在环”里的“软件”。

误解二:SIL结果应该和MIL完全一致。不对。模型仿真和代码执行之间存在数值差异,这个差异后面会细讲,但你需要提前接受它,而不是强迫它们逐bit相等。

误解三:SIL能验证硬件时序和中断响应。不对。SIL跑在宿主机PC上,不涉及真实处理器、外设和I/O。要验证时序,至少要到PIL阶段。

误解四:SIL可以替代HIL。不对。SIL只是逻辑验证,不能替代系统级的硬件在环测试。

这个阶段最常用的场景包括新能源电池管理算法、电机控制、四旋翼飞控、车辆动力学联合仿真等。只要你的控制器模型后续要生成嵌入式代码,SIL基本都是必经步骤。

2. 准备SIL环境:版本、编译器、模型划分三件事

2.1 版本与组件准备

先说版本。不同MATLAB版本里,SIL相关菜单的位置和叫法会有差异,但核心原理一致。你不需要为了SIL专门去买最新版,只要能满足你的代码生成许可即可。

关键是要确认你的Licence里包含代码生成相关组件。SIL需要至少Simulink Coder;如果要用ERT配置做更贴近量产风格的代码,通常会用到Embedded Coder。很多人装的是完整版或学校套装,那一般没什么问题,但要检查一下。

我这里给一个稳妥的验证方式:打开MATLAB,在命令行输入:

license('test', 'Simulink_Coder')

如果返回1,说明有Simulink Coder许可。如果返回0,可能需要确认许可配置或者换一套包含代码生成能力的安装。

注意,原始材料没有给出具体版本要求,落地时先以你自己机器上的版本为准。

2.2 Windows编译器和代码生成配置

SIL要把生成的C代码编译成动态库,所以系统里必须有一个可用的C编译器。

Windows下比较常见的选择是MinGW-w64或Microsoft Visual Studio(MSVC)。很多用Simulink的人会在MATLAB的Add-On里直接安装MinGW-w64,这个方式最省事。装好后用以下命令确认MATLAB能找到编译器:

mex -setup C

如果显示编译器已经配置好,SIL构建就能往后走。如果没有,先把编译器配置好再继续。Linux下通常直接用系统GCC,macOS下用Xcode Command Line Tools。

然后看代码生成配置。模型配置参数里有一项“System Target File”,常见的有grt.tlc和ert.tlc。对于SIL学习阶段,可以先不管GRT和ERT的区别,先用默认目标配置把SIL流程跑通。等到真正要做量产代码,再往ERT方向优化。

这里还有一个容易踩坑的点:不要一上来就调整各种高级代码生成优化选项,比如局部块减少、表达式折叠、流水线优化这些。SIL阶段最重要的是验证逻辑,默认配置已经够用。你把优化开得越激进,MIL和SIL的数值差异往往越大,反而不利于判断问题。

2.3 模型划分与原子子系统设置

SIL不是把整个模型都生成代码。通常情况下,你只想对控制器算法生成代码,被控对象模型继续留在Simulink环境里跑。

这就要求模型结构划分得很清晰。我的建议是:

  • 控制器算法单独放进一个子系统。
  • 这个子系统要设置为原子子系统(Atomic Subsystem)。
  • 被控对象部分不要参与代码生成。
  • 模型的输入输出接口尽量用普通Inport/Outport,不要搞复杂的总线或者变长信号,SIL阶段越简单越好。

为什么要原子子系统?因为原子子系统的代码生成边界非常明确。生成SIL模块时,它会被当成一个独立函数来编译和调用。如果只是普通Subsystem,代码生成时可能被内联、被优化掉,或者整个模型打包成一个巨大的函数,SIL替换边界就不清晰了。

还有一点要提前处理:模型文件和工程目录的路径不要带中文、不要带空格。这个看起来是小事,但在代码生成和编译器调用阶段经常变成莫名其妙的报错源。我自己习惯用纯英文文件夹,比如D:\Workspace\sil_demo,没有特殊字符,编译器不会闹脾气。

3. 从一个最小模型开始:MIL到SIL的标准迁移流程

3.1 搭一个PID控制最小模型

不建议拿已经做了几千行的复杂工程来第一次试SIL。先从最小模型开始,跑通流程,再把它套回正式工程。

我一般会搭这样一个模型:

  • 输入:Step阶跃信号。
  • 控制器:Discrete PID Controller。
  • 被控对象:一阶惯性环节,比如1/(s+1),再加一个饱和限幅。
  • 输出:连接Scope和信号记录器。
  • 求解器:固定步长,步长0.01秒。

固定步长很关键。SIL的代码是离散执行逻辑,用固定步长才能让MIL和SIL的对比条件一致。如果你用变步长,模型和代码之间会因为采样时刻不同产生额外差异,排查时容易误判。

模型名称简单一点,比如就叫sil_demo

3.2 第一步:先跑MIL基线

在生成SIL之前,必须先跑一遍MIL,把结果存下来当基线。没有基线,后面就没有对比对象。

这一步不需要任何代码生成配置,直接运行模型即可:

model = 'sil_demo'; outMIL = sim(model, 'StopTime', '10');

跑完后,在Simulink Data Inspector里看波形,或者把outMIL保存到工作区。我习惯把关键输出信号以数据集形式保存,方便后面和SIL结果逐条对比。

这一步的验证标准是:MIL结果符合控制器设计预期,比如阶跃响应上升时间、超调量、稳态误差在合理范围。如果MIL阶段就不正常,那后面SIL肯定也不会正常,先去改模型,不要浪费时间继续往后走。

3.3 第二步:生成SIL模块

生成SIL模块的方式在不同版本里略有差异,但思路相同。我先说流程,具体菜单你对照自己的MATLAB版本找。

常见做法有两种:

方式一,按整个模型启用SIL仿真。在Configuration Parameters里找到Code Generation -> Verification,勾选SIL/PIL simulation选项,然后回到模型窗口,把仿真模式从Normal切换为SIL。这种方式的优点是操作简单,适合整个模型都生成代码的情况。缺点是它会对整个模型生成代码,如果被控对象也包含不可代码生成的模块,会卡住。

方式二,针对子系统生成SIL模块并替换。在原子子系统上右键,找到C/C++ Code相关菜单,选择Build This Subsystem或者Generate S-Function之类。构建完成后,用生成的SIL模块替换原来的控制器子系统。

我建议学习时优先用方式二,因为它更贴近实际工程里的做法:只对要上芯片的算法生成代码,被控对象模型仍然保持Simulink形式。这样既符合SIL的定义,也更容易排查问题。

构建过程中Simulink会调用编译器,生成动态库。这一步正常完成后,你会看到模型里多了一个新的模块,名字通常带有sil字样,它的图标看起来更像一个代码片段,双击进去看到的不是普通模块图,而是代码包装信息。

3.4 第三步:运行SIL仿真并对比

SIL模型就绪后,直接运行仿真:

modelSIL = 'sil_demo_sil'; % 具体名称以实际生成的模块为准 outSIL = sim(modelSIL, 'StopTime', '10');

跑完后,把outMIL和outSIL放在一起对比。

判断标准有三条:

  • 控制量曲线趋势一致。
  • 被控量输出曲线趋势一致。
  • 误差没有随时间持续增大。

如果这三条满足,SIL就算跑通了。如果曲线差异很大,不要急着改参数,先回到配置和模型结构上检查。

注意:SIL模块不能被当成普通Simulink模块随意编辑。它代表的是已生成的代码,任何算法修改都要回到原模型去做,然后重新生成SIL模块。

4. 结果怎么对比:数值容差不是越小越好

4.1 SIL结果为什么会和模型结果有差异

很多人第一次跑SIL,看到输出和MIL不是完全重合,就开始担心是不是代码生成出了问题。其实不用慌,SIL结果和MIL结果存在一定数值差异是正常的,原因主要有这几个:

第一,模型仿真会用到求解器对连续对象做数值积分,而控制器生成的代码是在离散时间点执行,采样和计算时序不一定完全和求解器步进逻辑一致。

第二,代码生成过程中,编译器可能会对浮点运算进行调整。比如表达式重组、常量折叠、寄存器分配,都会影响浮点运算的舍入顺序,从而产生微小误差。

第三,如果模型里用了定点数和浮点数的混用,类型转换、饱和、取整逻辑也会带来差异。

所以,SIL的一个核心判断标准不是“完全一致”,而是“行为一致”。也就是动态特性一致、控制效果一致、关键状态量一致。

4.2 如何设置可接受的容差

那容差怎么设?不同模型差异很大,但我可以给一个经验值参考。

如果主要是浮点运算顺序带来的误差,相对误差通常在1e-4以下,甚至更小。如果误差到了1e-2量级,就要开始怀疑算法实现差异,而不是单纯浮点问题。如果误差超过1e-1,基本可以认为SIL和MIL不等价,需要排查数据范围、类型转换、状态初始化或者代码生成配置。

判断时不要只看单一信号,我建议这样:

  • 先看控制量,再看被控量。
  • 用绝对误差和相对误差一起判断,避免信号很小时相对误差虚高。
  • 观察误差是否随时间累积。如果误差在零附近波动,没问题;如果单调增大,说明可能存在状态漂移或者初始化不一致。

在Simulink Data Inspector里,可以直接叠加两组波形,也可以给信号配置公差。如果需要更系统的自动化对比,可以借助Simulink.sdi.compareRuns这类接口,或者用Simulink Test里的等价性判断功能。

4.3 从工程指标维度判断

除了逐点对比,工程上更关心的往往是系统指标:

  • 上升时间。
  • 超调量。
  • 稳态误差。
  • 控制量超调。
  • 在某些边界工况下是否发散。

如果这些指标在SIL和MIL之间都满足设计范围,那就没有必要纠结每个采样点的微小差异。

我自己做这套对比时,会先把两条曲线叠在同一个图上,先看视觉趋势。趋势对,再看数值。如果趋势都不对,就是大问题,不是调容差能解决的。

5. 多场景回归:用脚本和Simulink Test自动化跑SIL

5.1 为什么要自动化SIL回归

单个用例跑通SIL只是开始。实际项目里,一个控制器要覆盖很多工况:阶跃、斜坡、正弦、负载突变、饱和边界、限幅触发、异常输入等。

如果每个工况都手动切换输入、手动看曲线、手动记录结果,非常浪费时间,而且容易漏掉细节。SIL的价值就在于,代码生成一次之后,可以反复编译好的动态库去跑很多用例,不需要每次重新生成。这就是批量回归的核心优势。

5.2 用脚本批量执行

最简单的自动化方式是用MATLAB脚本循环跑。下面是一个示意流程:

% 加载SIL模型 load_system('sil_demo_sil'); % 假设有多个测试场景 scenarios = {'step', 'ramp', 'sine', 'saturation'}; for i = 1:length(scenarios) % 根据场景设置输入信号,这部分需要根据自己的模型实现 configureScenario(scenarios{i}); % 运行SIL仿真 out = sim('sil_demo_sil', 'StopTime', '10'); % 保存结果 saveResult(out, scenarios{i}); end

这里configureScenario和saveResult需要你根据模型自己封装。思路是:每个场景跑完,保存独立的输出文件和运行信息,比如运行时间、模型版本、生成代码的构建编号。

有一点要提醒:批量跑SIL时,不要在每个循环里都重新构建SIL模块。构建一次,后续反复运行仿真即可。如果模型算法没有变化,重新构建只会浪费时间,还可能因为覆盖文件导致并发冲突。

5.3 用Simulink Test做正式测试用例管理

如果需要更高程度的规范化,可以用Simulink Test工具。在Test Manager里可以创建测试用例,把MIL和SIL的结果做等价性比较,设置容差,然后批量运行。

这种方式的好处是:

  • 测试用例和结果有结构化管理。
  • 可以自动统计通过失败。
  • 多人协作时,每个人都可以复用同一套用例。
  • 回归测试时可以一键运行。

在功能安全要求比较高的项目里,Simulink Test用的比较多。因为它能保留测试记录、结果文件和判断逻辑,便于追溯。

5.4 覆盖率检查

如果项目对覆盖率有要求,SIL阶段也可以打开覆盖率统计。Simulink Coverage能统计语句覆盖率、分支覆盖率、条件覆盖率等。

SIL模式下统计的覆盖率,是基于生成的C代码执行的,比MIL里的模型覆盖率更接近实际代码运行情况。不过注意,覆盖率不是越高越好。很多分支在正常工况下永远不会走到,比如极端保护分支,需要用专门的用例去触发。覆盖率的价值是告诉你哪些代码没有被执行过,而不是给你一个“100%就安全”的结论。

6. 常见报错和排查顺序

6.1 编译和构建报错

SIL阶段最常见的报错是构建失败。

第一条要确认的是编译器没有配置好。直接跑一下:

mex -setup C

如果这里报错,说明系统找不到C编译器。安装MinGW-w64或者匹配版本的Visual Studio,再设置一次。

第二条是路径问题。模型或工程目录里如果有中文、空格、奇怪符号,构建过程很可能失败。把整个工程放到纯英文路径下,再试一次。

第三条是模型里有模块不支持代码生成。有些显示模块、动画模块、特定工具箱模块,只用于仿真,不能生成代码。处理办法是把它从被测子系统中移出去,放到外围模型部分。

第四条是某些自定义TLC文件冲突。SIL学习阶段不需要处理器目标包,也不需要自定义TLC。如果你在目标文件里选了处理器相关配置,先换回默认目标,跑通SIL流程再考虑定制。

6.2 仿真运行报错

构建成功不等于运行成功。运行SIL仿真时常见问题有:

SIL模块初始化失败。这种情况先检查构建后是否移动了文件夹。动态库是绝对路径还是相对路径加载,不同版本处理不一样。把SIL模块和构建产物放在原目录,别乱移动。

模型中定义了Simulink.Parameter或者信号对象,但没有正确初始化。代码生成后这些对象会变成全局变量或宏定义,如果初始化缺失,运行时会出错。排查时到Model Explorer里检查数据对象是否完整。

SIL模型输出一片空白。检查SIL模块输入输出连线是否断开,或者被控对象和控制器之间的采样率是否匹配。有时候模型看起来在运行,但实际数据没有进入到记录器。

6.3 结果异常

如果MIL和SIL都能跑,但结果对不上,按这个顺序排查:

先看数值类型。模型里是不是有整型溢出?float和double混用?定点数范围没设置好?这些都会显著改变结果。

再看初始化。SIL代码执行时,状态变量是否按照模型里Initial Condition初始化。如果模型里有状态依赖上次运行结束值,而SIL每次重新运行从零开始,那结果对不上很正常。

再看采样时间。控制器代码的采样时间是否和原模型一致。如果模型里是连续PID,代码里被转换成固定采样步长,那离散化的等效性和原模型肯定有差异,这时需要回到模型里把控制器改成离散模块。

最后看代码生成优化。如果优化级别过高,比如启用了表达式折叠、函数内联、重新关联浮点运算,数值差异会变大。SIL阶段可以先降低优化级别,把逻辑验证清楚,再做性能优化。

6.4 通用排查顺序

遇到SIL相关的问题,我习惯按这个顺序查:

顺序检查点常见观察
1现象编译失败、运行失败、结果异常、速度异常慢
2输入信号幅值、参数范围、初始值设置
3环境编译器、路径、权限、依赖库
4参数步长、求解器、代码生成选项
5工具MATLAB版本、许可、目标配置

不要一上来就怀疑模型算法或者调代码生成优化参数,先看日志和报错信息,再改配置。

7. 进阶方向:从SIL到PIL再到HIL

7.1 SIL的边界在哪里

SIL的最大优势是快、便宜、无需硬件。但它有一个本质边界:它跑在宿主机PC上,不是目标处理器上。

这意味着,SIL无法验证以下内容:

  • 目标处理器的指令集差异。
  • 中断响应、任务调度时序。
  • 栈使用、内存布局、外设寄存器。
  • 真实I/O延迟和通信协议时序。

所以SIL适合在代码生成后、目标板到手前,做高频次的逻辑回归。

7.2 什么情况下转到PIL

当你拿到目标处理器开发板,并且希望验证生成的代码能不能真实跑起来时,就要从SIL转到PIL。

PIL会把编译好的代码下载到目标处理器上执行,宿主PC端通过调试器或通信接口与被测目标交换数据。Simulink环境仍然负责被控对象仿真和结果记录,而控制器算法在真正的MCU上运行。

PIL能暴露的问题包括:

  • 处理器不支持某些编译选项。
  • 代码在目标上执行时间超过任务周期。
  • 内存占用过大,栈溢出。
  • 浮点运算结果与宿主PC不一致。

如果在PIL阶段发现问题,比如运行时间不够,一般需要回头优化算法、调整代码生成配置、或者换更高效的采样策略。外部模式在线调参也经常在PIL阶段配合使用,方便实时查看目标上的内部变量。

7.3 HIL和最终落地建议

再进一步是HIL。HIL把控制器代码放在真实控制器硬件中,被控对象放在实时仿真机里,通过真实I/O、总线、传感器和执行器通道连接起来,做系统级闭环测试。

HIL的成本和复杂度明显高于SIL和PIL,但它是量产前很重要的验证手段。很多故障注入、边界情况、通信异常测试,必须在HIL环境里做。

我给的建议是:不要一上来就奔着HIL去。先把SIL跑稳,把代码生成和逻辑验证做扎实,再根据硬件条件逐步升级。很多团队在SIL阶段就能发现大量逻辑问题,这些问题如果拖到HIL甚至实车阶段,定位成本会高很多。

最后回到这篇的核心:SIL验证的不是“模型能不能跑”,而是“代码和模型是不是讲同一套逻辑”。先把最小模型跑通,再扩展到完整工程;先把单工况跑通,再自动化回归;先接受数值差异,再学会用工程指标判断差异。这条路走顺了,后续PIL和HIL就会省心很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询