1. 软件在环仿真(SIL)是什么,为什么需要它
在基于模型的设计(MBD)开发流程中,从算法模型到产品代码,中间有一个关键过渡点:你写的 Simulink 模型最终要变成 C 代码,烧录到控制器里。这个过渡往往最怕两件事:一是模型能跑但代码行为不对,二是生成代码与模型仿真结果不一致。软件在环仿真(Software-in-the-Loop,SIL)就是专门用来验证“模型到代码”这个环节可靠性的手段。
通俗理解,SIL 就是把“控制器算法”从 Simulink 模型形态,替换成“已经编译好的软件代码形态”,然后在普通电脑上继续跑仿真。原来的被控对象模型可以保留在 Simulink 中,不需要真实的硬件。通过这种方式,我们可以在没有 ECU、没有单片机的情况下,提前验证目标代码的行为是否符合算法设计。
与大家熟悉的 MIL(Model-in-the-Loop,模型在环)相比,SIL 最大的变化是被测对象发生了变化。MIL 阶段测的是模型本身,算法是否正确;SIL 阶段测的是代码本身,代码是否忠实地实现了模型逻辑。如果只做 MIL 不做 SIL,很多代码生成时引入的问题要到 PIL 或者 HIL 阶段才能暴露,那时候排错代价会明显增加。
SIL 的常见应用场景包括:
- 验证自动生成的代码与原始模型行为是否一致。
- 在没有目标硬件的情况下,提前开展集成测试和回归测试。
- 验证手写代码与模型代码混合集成后的正确性。
- 进行大批量自动化测试,例如参数扫描、随机输入测试、边界条件测试。
- 在 V 型开发流程中,作为 MIL 与 PIL/HIL 之间的过渡验证环节。
在汽车电子、航空航天、轨道交通等长周期控制器开发项目中,SIL 几乎是“标配”。因为控制器软件复杂度很高,如果直接跳到硬件测试,很多软件逻辑问题不好定位。而 SIL 可以在普通电脑上快速跑完大量测试用例,帮助开发者在进入硬件在环测试之前,先把软件级问题清干净。
要注意的是,SIL 不能完全替代 HIL。SIL 只验证软件代码逻辑,不验证目标芯片时钟、外设、中断优先级、驱动接口等硬件相关问题。它更像是“软件层面的第一道关口”,后续还需要通过 PIL 或 HIL 解决硬件相关风险。
2. MIL、SIL、PIL、HIL 的区别与联系
很多刚开始学习软件在环仿真的人,容易把 SIL、PIL、HIL 混为一谈。这里用一个表格帮大家梳理清楚。
| 仿真模式 | 控制器算法形态 | 被控对象运行环境 | 控制器运行平台 | 主要验证目标 |
|---|---|---|---|---|
| MIL(模型在环) | Simulink 模型 | Simulink 模型 | 开发电脑 | 算法逻辑正确性 |
| SIL(软件在环) | 编译后的软件代码 | Simulink 模型或外部模型 | 开发电脑通用处理器 | 代码与模型一致性、软件逻辑 |
| PIL(处理器在环) | 编译后的软件代码 | Simulink 模型或外部模型 | 目标处理器/开发板 | 代码在目标处理器上的正确性 |
| HIL(硬件在环) | 真实控制器(ECU/MCU) | 实时仿真机 | 真实控制器 | 控制器软硬件整体行为、时序、故障注入 |
从中可以看出,SIL 和 PIL 的区别不在于代码,而在于代码运行在哪里:
- SIL 的代码运行在开发电脑的处理器上,速度较快,方便调试。
- PIL 的代码运行在目标处理器上,虽然平台更接近实际,但编译下载和部署成本更高。
所以在流程上,多数团队会先做 SIL 再做 PIL。SIL 能提前发现算法实现问题,PIL 再确认目标平台兼容性问题,HIL 最后做闭环系统级验证。
这里也顺带解释一个容易混淆的概念:SIL 中的“软件”并不一定专指 Simulink 自动生成的代码。如果你的项目有手写 C 代码,同样可以把手写代码编译后接入 Simulink 仿真环境,通过 SIL 模式验证它与模型算法、被控对象模型之间的协同是否正确。本文后面主要以 Simulink 自动生成代码的场景为例展开。
3. SIL 仿真的核心工作流程与原理
在深入实操之前,先了解 SIL 在 Simulink 内部是怎么工作的,这样遇到问题时更容易定位。
3.1 SIL 的基本流程
Simulink 中做软件在环仿真,本质上分为三个步骤:
- 根据模型生成 C/C++ 代码。
- 将生成的代码编译成可执行模块,通常以 S-Function 或共享库的形式加载到 Simulink 中。
- 运行仿真。此时模型中原来的控制器算法被替换成这段编译好的代码,被控对象模型仍由 Simulink 正常计算。
从用户角度看,仿真还是那个仿真,示波器还能看波形,数据还能导出到工作区。但真正参与计算的核心控制器部分,已经从“模型解释执行”变成了“目标代码执行”。
3.2 SIL 与普通仿真的差异
普通仿真模式下,Simulink 以解释执行的方式运行模型,重点在于验证算法逻辑,并不会生成可供嵌入式控制器使用的代码。
加速器模式和快速加速器模式虽然也会生成代码,但它们的目的是“加速计算”,生成代码的路径和产品级代码不是一回事。例如,加速器生成的代码不追求运行效率,也不会按照目标芯片架构做优化。
SIL 模式则不同。使用 SIL 时,代码生成配置会走产品代码生成路径,例如使用ert.tlc系统目标文件,生成适合嵌入式系统的代码。然后这段代码以软件在环的形式参与仿真。正因为代码生成路径与真实产品一致,SIL 的验证结果才更有说服力。
3.3 SIL 的三种常见接入方式
在实际项目中,SIL 可以通过三种方式接入:
| 接入方式 | 操作位置 | 适用场景 |
|---|---|---|
| 顶层模型 SIL | 模型设置 → 代码生成 → 验证 → 仿真模式 | 对整个模型做验证,适合快速验证完整算法 |
| 模型引用块 SIL | 被引用模型对应的 Model 块 → 块参数 → 仿真模式 | 对控制器子系统做 SIL,被控对象仍保持模型仿真 |
| 外部代码 SIL | 将手写 C 代码通过 S-Function 或其他接口集成 | 验证手写代码与模型的协同逻辑 |
其中第二种“模型引用块 SIL”是最符合实际工程习惯的。因为一个完整系统里面,往往包含控制器、被控对象、传感器、执行器等模块。真正需要生成代码的是控制器部分,被控对象在仿真中通常只是一个被模拟的环境。因此,把控制器单独建到一个模型里,再以模型引用的方式接入顶层系统,然后在模型引用块上打开 SIL,是最清晰的验证结构。
4. 软件在环仿真环境准备
做 SIL 仿真前,需要先确认环境和工具箱是否齐全。
4.1 必需工具箱
- Simulink:建模和仿真主环境。
- Simulink Coder:负责从模型生成 C/C++ 代码。
- Embedded Coder:生成适合嵌入式控制器的优化代码,属于进阶选项。如果只是学习 SIL,有 Simulink Coder 基本够用;如果目标是做产品级代码生成,建议安装 Embedded Coder。
- MATLAB Coder:如果你的算法通过 MATLAB Function 块编写,可能需要用到它。
部分 SIL 测试还会用到 Simulink Test、Simulink Coverage,前者用于自动化测试管理,后者用于代码覆盖率分析。初学者第一阶段不强制安装,后面做项目规范化时可以再补充。
4.2 C/C++ 编译器
SIL 模式需要把生成的 C 代码编译成可执行模块,所以电脑上必须安装可用的 C/C++ 编译器。在 Windows 环境中,常用的编译器是 Microsoft Visual C++ 或者 MinGW-w64。建议尽量安装官方支持的编译器版本。
打开 MATLAB,在命令行窗口执行以下命令确认编译器可用:
mex -setup如果返回结果列出了可用的编译器,说明 C 编译器已经配置好;如果提示找不到编译器,需要先安装对应版本的 Visual Studio 或者 MinGW-w64。
4.3 版本说明
不同 MATLAB 版本的菜单项和默认配置可能略有差异。本文示例以较新的 MATLAB 版本为主,例如 R2021b 及以后版本。如果你使用的是旧版本,菜单名称可能稍有不同,但核心逻辑一致:找到“代码生成”配置页,将“仿真模式”设置为“软件在环”,再生成代码运行仿真即可。
5. 完整实战:从模型搭建到 SIL 仿真
下面用一个简单但完整的闭环控制系统,演示如何从零开始完成软件在环仿真。这个例子包含一个离散 PID 控制器和一个一阶被控对象,在工程中非常有代表性。
5.1 搭建系统模型
假设我们的控制目标是一个简单被控对象,传递函数为:
G(s) = 1 / (s + 1)控制器采用离散 PID 算法,采样时间为 0.01 秒。
打开 Simulink,新建一个模型,命名为sil_demo_top。在模型中添加以下模块:
| 模块 | 模块库 | 说明 |
|---|---|---|
| Step | Simulink/Sources | 输入阶跃信号,例如幅值为 1 |
| Sum | Simulink/Math Operations | 计算误差 e = r - y |
| Discrete PID Controller | Simulink/Continuous 或按名称搜索 | 控制器,采样时间设为 0.01 |
| Transfer Fcn | Simulink/Continuous | 被控对象,设为 1/(s+1) |
| Scope | Simulink/Sinks | 看输出波形 |
| To Workspace | Simulink/Sinks | 导出仿真数据到工作区,用于 MIL/SIL 对比 |
为了更像工程结构,建议把控制器单独封装成子系统。操作方式:选中 Discrete PID Controller 模块,右键 → Create Subsystem from Selection。这样生成一个名为Subsystem的子系统,后续可以把它单独抽出来作为控制器模型。
模型连接关系如下:
Step ──► Sum(+) ──► PID Controller ──► Transfer Fcn ──► Scope ▲ │ └─────────────────────────────────┘在 To Workspace 模块中,设置变量名为yout,保存格式选择Timeseries,这样后续方便用脚本分析。
5.2 设置求解器
由于控制器是离散的,而被控对象是连续系统,仿真需要采用固定步长求解器。打开“模型设置”,进入“求解器”页面,配置如下:
类型:固定步长 求解器:ode4(Runge-Kutta) 固定步长:0.01 停止时间:10为什么必须用固定步长?因为代码生成之后,目标代码的采样周期必须是确定的。如果使用变步长求解器,Simulink 会按误差自动调整仿真步长,生成代码后这种行为无法在目标控制器上复现,导致 SIL 结果失去参考意义。
5.3 将控制器封装成模型引用
为了让 SIL 验证更符合工程习惯,我们需要把控制器独立成一个模型,然后在顶层模型中通过“模型引用”方式调用它。
步骤一:右键控制器子系统,选择“Cut”。然后在新建模型中粘贴,并给这个新建模型添加两个端口:
- Inport 表示误差输入。
- Outport 表示控制量输出。
将新建模型保存为sil_demo_controller.slx。
步骤二:回到sil_demo_top顶层模型,把原来的子系统删除,从模块库中拖入一个 Model 块:
Simulink / Ports & Subsystems / Model双击 Model 块,选择sil_demo_controller。此时顶层模型会根据模型引用的端口自动生成输入输出连接。如果连接不对,重新连线即可。
最后模型结构如下:
sim_demo_top ├── Model: sil_demo_controller └── Plant: Transfer Fcn + Step + Scope + To Workspace5.4 配置控制器模型的代码生成
在生成 SIL 之前,需要给控制器模型配置代码生成环境。
打开sil_demo_controller.slx,进入“模型设置”,找到“代码生成”页面:
代码生成 └─ 系统目标文件:ert.tlc这里选择ert.tlc是因为我们要模拟嵌入式控制器代码。如果只是验证功能,grt.tlc也可以,但嵌入式目标更贴近真实产品。
然后进入“代码生成 → 验证”页面,将“仿真模式”设置为“软件在环”:
代码生成 └─ 验证 └─ 仿真模式:Software-in-the-loop部分版本中,还需要勾选“生成代码报告”,这样代码生成完成后会自动打开 HTML 报告,方便查看代码结构和变量映射。
5.5 在模型引用块上启用 SIL
回到sil_demo_top.slx,右键点击控制器对应的 Model 块,选择“块参数”,找到“仿真模式”下拉框:
仿真模式:软件在环(Software-in-the-loop)如果没有这个选项,说明代码生成配置尚未生效,请先回到第 5.4 步确认系统目标文件和验证选项已经设置。
这里也给出命令行方式,便于后续脚本化自动化测试:
% 切换控制器模型引用块为 MIL 模式 set_param('sil_demo_top/Controller', 'SimulationMode', 'normal'); % 切换控制器模型引用块为 SIL 模式 set_param('sil_demo_top/Controller', 'SimulationMode', 'software-in-the-loop');需要注意,sil_demo_top/Controller是模型引用块在顶层模型中的路径,实际名称以你的模型为准。
5.6 运行 MIL 仿真并保存结果
为了对比,先运行一次 MIL 仿真,也就是让控制器以普通模型模式运行。
设置好模型引用块为normal后,在命令行执行:
simOut_mil = sim('sil_demo_top', 'StopTime', '10'); y_mil = simOut_mil.yout{1}.Values.Data; t_mil = simOut_mil.tout;仿真结束后,把 MIL 结果保存下来:
save('mil_result.mat', 't_mil', 'y_mil');此时可以打开 Scope 查看波形,正常的闭环阶跃响应应该先上升再稳定到 1,超调量不大。
5.7 运行 SIL 仿真并对比结果
接下来把控制器模型引用块切换为 SIL 模式:
set_param('sil_demo_top/Controller', 'SimulationMode', 'software-in-the-loop');然后再次运行仿真:
simOut_sil = sim('sil_demo_top', 'StopTime', '10'); y_sil = simOut_sil.yout{1}.Values.Data; t_sil = simOut_sil.tout;首次运行 SIL 时,Simulink 会先生成代码,再调用编译器编译。命令行会输出类似下面的构建信息:
### Starting build procedure for model: sil_demo_controller ### Successful completion of build procedure for model: sil_demo_controller构建完成后,仿真会自动继续。SIL 仿真通常比普通仿真慢一些,因为每个采样步长都要通过 S-Function 调用生成的代码。
对比两种模式的结果:
figure; plot(t_mil, y_mil, 'b-', 'LineWidth', 1.5); hold on; plot(t_sil, y_sil, 'r--', 'LineWidth', 1.5); grid on; legend('MIL', 'SIL'); xlabel('时间 t/s'); ylabel('输出 y'); title('MIL 与 SIL 仿真结果对比');正常情况下,两条曲线会基本重合。如果两条曲线有明显差异,说明控制器代码生成过程中存在数据精度、采样周期或初始化问题,需要进一步排查。
5.8 在构建日志中定位生成代码
SIL 运行完成后,工作目录下会生成slprj文件夹,里面存放代码生成中间产物。如果项目配置了代码生成报告,也可以直接在报告里查看控制器代码。对于简单的 PID 控制器,代码量不大,可以重点看以下几个文件:
sil_demo_controller.c sil_demo_controller.h sil_demo_controller_private.h对比生成代码里的 PID 系数和 Simulink 模型中的离散 PID 参数,确认系数是否一致。这一步是 SIL 验证中非常实用的细节,很多项目都是在这里发现参数定标或变量名映射问题。
6. 常见问题与排查思路
SIL 仿真在第一次使用时,容易遇到一些配置和编译问题。这里整理最常见的几种情况。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 模型引用块仿真模式里没有“软件在环”选项 | 控制器模型未正确配置代码生成目标 | 打开控制器模型,设置系统目标文件ert.tlc,保存后再回到顶层模型 |
| 构建时报错找不到 System Target File | 未安装 Simulink Coder 或 Embedded Coder | 检查工具箱列表,确认两个工具箱已安装且许可有效 |
| 编译报错找不到 C 编译器 | 未安装编译器或 MATLAB 未识别 | 运行mex -setup,选择正确的编译环境 |
| SIL 仿真结果与 MIL 差异较大 | 数据类型不一致、求解器步长不合适、PID 初始化状态不一致 | 检查模型数据类型,设置相同固定步长,对比初始化条件 |
| 仿真运行非常慢 | SIL 每次调用都要经过代码生成、编译和外部接口调用 | 避免在模型内部频繁使用不必要的数据转换;大批量测试用脚本批处理 |
| 生成代码文件特别多,找不到关键文件 | 项目目录未整理,代码生成中间文件混杂 | 使用 Simulink 项目管理,将代码生成路径独立到slprj或指定工作文件夹 |
| 切换 SIL 后模型报 “Simulation mode cannot be changed” | 模型正在运行或代码生成未完成 | 先停止仿真,清理生成代码,重新构建 |
| 信号日志丢失,无法获取输出 | 信号没有标记保存,或优化将信号优化掉了 | 使用 Outport 导出信号,或者在模型设置中开启信号日志 |
如果你遇到的是比较模糊的编译错误,建议优先清理以下内容:
% 关闭当前模型 bdclose('sil_demo_top'); % 删除代码生成中间目录 if exist('slprj', 'dir') rmdir('slprj', 's'); end % 重新打开模型 open_system('sil_demo_top');这种“清缓存、重新生成代码、重新运行”的操作,能解决大部分奇怪的构建问题。
7. 软件在环仿真的最佳实践与工程建议
SIL 不是一个简单的开关,用好了能显著提升代码质量,用不好会变成重复劳动。下面这些建议来自实际项目经验,供参考。
7.1 为不同验证阶段建立独立配置集
在一个模型中,可能同时存在 MIL、SIL、PIL 三种模式的配置需求。如果每次手动改求解器和代码生成参数,很容易出错。建议使用 Simulink 的“配置引用”功能,为不同仿真阶段创建独立配置集。
在模型设置中:
Simulation → Model Configuration Parameters → Active configuration或者使用脚本:
% 创建配置引用 configRef = Simulink.ConfigSetRef; configRef.Name = 'SIL_Config';实际项目中,通常会有MIL_Config、SIL_Config、PIL_Config三套配置。这样切换验证模式时,只需激活对应配置集,不需要逐项核对参数。
7.2 用数据字典管理参数
SIL 验证过程中,经常需要修改控制器参数、采样时间、运行时长。如果这些参数分散在模型内部,维护成本很高。推荐把参数集中到 Simulink 数据字典中,例如controllerParams.sldd。
在数据字典中定义:
Ts = 0.01; Kp = 1.2; Ki = 0.5; Kd = 0.1;模型中的 PID 控制器参数直接引用这些变量。这样在自动化测试时,只需要修改字典中的数值,就能批量跑不同参数组合下的 SIL 仿真。
7.3 用脚本实现 SIL 自动化测试
SIL 适合大批量测试,但手工点击“运行”显然太低效。可以用脚本驱动仿真和结果对比。
这里给出一个简单的自动化框架思路:
% 定义测试参数 paramList = [1.0, 0.2, 0.05; 1.2, 0.5, 0.10; 0.8, 0.1, 0.01]; for i = 1:size(paramList, 1) % 更新控制器参数 Kp = paramList(i, 1); Ki = paramList(i, 2); Kd = paramList(i, 3); % 运行 MIL set_param('sil_demo_top/Controller', 'SimulationMode', 'normal'); simOutMIL = sim('sil_demo_top', 'StopTime', '10'); % 运行 SIL set_param('sil_demo_top/Controller', 'SimulationMode', 'software-in-the-loop'); simOutSIL = sim('sil_demo_top', 'StopTime', '10'); % 对比 yMIL = simOutMIL.yout{1}.Values.Data; ySIL = simOutSIL.yout{1}.Values.Data; diffMax = max(abs(yMIL - ySIL)); fprintf('测试用例 %d:最大误差 = %.6f\n', i, diffMax); end这种方式虽然简单,但已经具备仿真测试自动化雏形。如果项目规模更大,建议使用 Simulink Test 管理测试用例、预期结果和覆盖率数据。
7.4 数据类型与步长一致性是重点
SIL 结果和 MIL 结果不一致,最常见的根因就是数据类型和采样步长。
- 确认控制器模型输入输出都是
double,如果使用定点数,要检查定标方式。 - 确认仿真求解器是固定步长,且步长与控制器采样时间存在合理关系。
- 确认模型中的单位延迟、PID 初始状态在两次仿真中一致。
- 如果模型中存在 MATLAB Function 块,建议用
codegen提前检查类型推导结果。
7.5 SIL 不是越早越好,也不是越多越好
SIL 验证是有成本的,尤其是代码生成和编译阶段比较耗时。建议在以下时间点做 SIL:
- 算法模型稳定、MIL 测试通过之后。
- 控制器接口定义完成之后。
- 每次修改控制器代码生成相关配置时。
- 每次修改控制器算法逻辑时。
不建议在算法还在频繁调整的阶段就反复跑 SIL,那样只会增加编译等待时间。建议先在 MIL 阶段把算法迭代成熟,再进入 SIL 阶段做代码级验证。
7.6 保留构建日志和测试记录
在 V 型开发流程中,SIL 测试结果往往需要作为交付物提交给质量团队或配合安全标准评审。因此建议每次 SIL 测试保留以下信息:
- MATALB 和 Simulink 版本号。
- 系统目标文件名称。
- 求解器和步长设置。
- 测试用例列表。
- MIL 与 SIL 对比误差。
- 生成的代码报告。
这些记录不仅方便追溯问题,也是很多行业标准评审时的必要材料。
8. 总结与下一步学习建议
本文围绕 Simulink 软件在环仿真,讲解了 SIL 的基本概念、与 MIL/PIL/HIL 的关系、环境准备、核心配置、完整实战流程以及常见问题排查方法。通过一个离散 PID 控制器的闭环系统,演示了如何从普通模型仿真切换到 SIL 仿真,并对比验证结果。
掌握 SIL 之后,下一步可以继续学习:
- PIL 处理器在环仿真,看看代码在目标处理器上的行为差异。
- HIL 硬件在环仿真,结合实时仿真机验证真实控制器。
- Simulink Test 自动化测试,把 SIL 变成可持续的回归测试体系。
- 代码覆盖率分析,了解测试用例对代码的覆盖情况。
建议你先找一个小模型,比如本文的 PID 闭环系统,把 MIL 到 SIL 的流程完整跑一遍。第一次跑通可能花一些时间,但跑通之后,后续再做复杂控制器的软件验证,就会顺很多。如果你在实际操作中遇到其他问题,欢迎在评论区留言交流。