☰
基于MATLAB Simulink的MIL与SIL单元测试实战指南
2026/10/14 23:48:55 网站建设 项目流程

1. 项目概述:为什么要做MIL和SIL单元测试

搞过嵌入式控制开发的朋友应该都有体会,模型越来越大、逻辑越来越复杂之后,光靠眼睛看和手点仿真已经撑不住了。尤其做汽车电控、无人机飞控、机器人控制这类对安全性要求高的项目,一个不起眼的逻辑分支错误,等烧录到硬件上再发现,排错成本会翻好几倍。我在实际项目中切身体会过这种痛:模型几千个模块,改动一个状态机分支,跑一遍闭环仿真看着挺正常,结果上了台架实验,某条边界路径直接跑飞,查了两天才发现是模型里一个比较符号写反了。

这正是MIL(Model in the Loop,模型在环)和SIL(Software in the Loop,软件在环)测试要解决的问题。简单说,MIL是在Simulink模型层面验证你写的控制逻辑对不对,SIL是把模型生成的C代码拿来做等价性验证,看代码跑出来的结果跟模型是不是一致。再配上单元测试的方法论,从最小粒度的功能模块开始逐层验证,把bug尽量留在开发早期。

这篇文章我将带你完整走一遍基于MATLAB Simulink的MIL与SIL单元测试流程,从测试环境搭建、测试用例编写、自动化执行到结果分析和缺陷定位,全程给出可直接落地的步骤和踩坑记录。适合正在做模型开发或者刚接触V流程验证的工程师,无论你是写模型的、测模型的还是管质量的,这套流程都能直接拿来用。

2. 核心概念拆解:MIL、SIL和单元测试到底是什么关系

2.1 三种测试层级的分工定位

很多刚接触基于模型开发的人容易把MIL、SIL、PIL这几个词混在一起。我习惯用一张递进关系图来理解它们:

  • MIL:控制策略还在Simulink模型阶段,虚拟信号激励模型,验证的是算法逻辑。
  • SIL:自动生成C代码,在PC上编译运行,代码的输入输出跟MIL对比,验证代码生成是否正确。
  • PIL:代码烧到目标处理器上跑,验证编译器行为、实时性、定点数效应等硬件相关差异。

三者的共同点是:同一套测试用例、同样的输入激励、同样的参考输出,只是被测对象不同。这也是整个测试思路的核心——可复用。你在MIL阶段设计好的测试用例,换个被测对象就能做回归。

单元测试这个维度可以叠加在MIL和SIL之上。单元测试关注的是模型内部的单个功能模块,比如一个PI控制器、一个状态机、一个故障诊断逻辑。跟系统级测试比,单元测试要控制依赖、隔离外围,等于把一块电路板拆成一个个芯片来单独验证。

2.2 为什么需要两套测试并行

简单说,MIL测的是"想法对不对",SIL测的是"翻译对不对"。生成代码的工具有时候会因为配置不当,导致代码行为和模型不一致。常见的比如:除法运算的舍入模式变了、向量索引的边界处理、数据类型转换时溢出保护丢失。这些差异MIL阶段根本看不到,必须让同样的测试用例在SIL跑一遍来对比。

我在一个项目中遇到过真实案例:模型里一个查表模块在MIL仿真中输出完全正常,代码生成后因为查表索引的数据类型从uint16被优化成了uint8,当表索引超过255时输出就跳变,SIL测试一跑差异立刻就暴露了。这种问题如果不上SIL对比测试,可能要等到硬件在环或者实车阶段才能发现。

3. 测试环境准备与前置检查

3.1 软件栈选型与版本匹配

测试环境的搭建其实比写测试用例更容易踩坑,因为版本兼容性问题往往在你最忙的时候突然冒出来。我这里给出一个经反复验证的组合建议:

  • MATLAB版本:如果团队规模不大,统一用R2020a以上版本比较好,2020版本之后Simulink Test模块的功能成熟度已经够做自动化单元测试。新版本在代码覆盖率和测试管理界面上的改进非常明显。
  • 嵌入式编码器:做SIL测试必须装Embedded Coder。只装Simulink Coder生成不了针对嵌入式目标优化过的代码。
  • Simulink Test:做测试用例管理、测试执行和结果评审的利器,强烈建议装,比纯手工用脚本管理用例省心太多。
  • Simulink Coverage:如果你需要统计语句覆盖率、分支覆盖率、MC/DC覆盖率,这个工具箱是标配。汽车电子功能安全标准(ISO 26262) 虽然没有出现在标题里,但覆盖率数据本身就是测试完整性的有力论据。

几个版本号之间注意一个细节:Simulink Test的用例文件格式(.mldatx)在R2020a和R2022b之间是可以互读的,但如果跨大版本(比如从R2019a直接升到R2022b),用例文件会提示升级,升级后旧版就打不开了。所以我建议在项目开始前锁定版本,中间不要随意升级。

3.2 被测模型代码规范预检

测试环境搭好之后,比写测试用例更紧迫的工作是先给你的被测模型做一次"体检"。这不是普通的代码生成检查,而是针对可测试性的专项审查,重点看三件事:

模型内部是否存在耦合在子系统外部的全局变量。Simulink里可以用Data Store Memory或者全局WorkSpace变量传数据,但从单元测试的角度看,这类全局依赖就像是隐藏的雷,测试时很难隔离,故障定位时也很难追踪。我强烈建议在写测试之前先做一轮模型重构,把全局数据改成显式的输入输出端口。实测下来这个重构工作量不大,但对后续测试的收益非常明显。

其次,模型的采样时间设置要理清楚。连续模块和离散模块混用,在MIL测试里会引入代数环或者仿真步长的选取问题。单元测试阶段最好被测模块内部统一用离散采样,这样每个测试用例的步长可以固定。

第三件事是信号线的命名。很多模型的信号线是系统自动生成的名称(比如Goto/From的tag),这在测试结果分析时是噩梦。建议在模型规范里就强制要求所有跨层级的信号必须有语义清晰的命名,比如Ctrl_Err_Voltage,而不是Sig_003。这么做不是为了好看,而是因为Simulink Test的Signal Tracking功能需要依赖信号名来绑定数据。

4. MIL单元测试的完整实操流程

4.1 设计测试用例的准备工作

写MIL单元测试用例之前,先想清楚一个问题:你测的功能模块,它的功能规格是什么?很多团队测试效果差,问题恰恰出在这里——用例是根据模型实现反推出来的,而不是根据需求文档导出的。这样测出来的用例只能证明模型没写错,不能证明模型符合需求。

正确的做法是,在写用例前先固话模块的输入输出规格。比如一个查表模块,你要明确:输入是角度,范围为0到360度;输出是扭矩修正系数,范围在0.8到1.2之间;边界条件包括0度、360度、90度等关键点。把这些规格整理成一张Excel表格,每个测试用例对应一行:"用例编号、测试目的、输入值、预期输出、容差"。

测试用例的设计方法上,我用的是三条腿走路:功能用例覆盖正常工况和标定场景;边界用例卡范围极限、切换点、复位条件;异常用例打非法数据、超出范围、断线故障。三个方向覆盖完,单个模块一般能有20到50个用例。你如果剛开始做,不需要一次追求全,先把功能用例做扎实,边界和异常在第二轮迭代中补上,这样效率会更高。

4.2 在Simulink Test中创建测试工程

打开Simulink Test管理器(在App页签下),创建一个新的测试工程。测试工程里可以建多个测试套件(Test Suite),每个套件对应一个被测模块。创建测试用例时,选择"Baseline Test"。这种类型的用例适合做MIL单元测试,因为它可以直接定义输入信号的时间序列,再设定输出信号的预期值范围。

绑定被测模块的时候有一个重要选项:你可以选择"Test Harness"模式。测试工具会自动在系统模型中剥离出被测模块,创建一个独立的测试脚手架,带好输入输出端口。好处很明显,测试用例可以只关注模块本身,不关心外围逻辑,也方便并行执行多个模块的测试。我一般直接选择自动生成Test Harness,省得自己搭建。

但注意一点:Test Harness的输入端口数据类型必须与被测模块的输入端口类型完全一致。有一次测试某模块的枚举输入,我在Harness里给的输入源是uint8数据,结果仿真一跑直接端口类型报错,又是一顿排查。所以建完Harness后,先花几分钟把各端口的数据类型和采样时间对着模型检查一遍,再开始设计用例内容。

4.3 基于仿真数据创建测试用例

如果被测模块已经有一个可行性仿真的基础场景,最快的方法是直接基于仿真数据来生成测试用例。在Simulink Test用例编辑器的"External Input"里,可以选择"From Workspace"或者"From File"引用一组已有的输入信号数据。当初这个功能帮我省了很多时间——我把系统调试阶段采集的典型工况数据,直接切成模块输入,马上就生成了一组跟实际运行有关联的测试用例。

更常用的是直接在用例编辑器里面手动定义信号。我以"PI控制器饱和限幅逻辑"为例,给你演示一套典型的用例定义流程:

先在用例的Inputs页签下,添加一个信号,命名为err_sig,Source设置为Input Port,数据类型设置为double,采样时间设为0.001秒。然后在下方的Signal Editor表格里,填入这一列数据:0、1、2、3、2、1、0……这一个序列模拟的是阶跃递增再递减的误差信号。

再去Outputs页签下,添加预期输出信号。在这里定义格式是"时间、上限、下限、容差"组成,需要特别注意:预期值本身就是判断通过与否的边界。假设控制器的输出应该是先线性增长,在2000这个值处触顶限幅,那么在Outputs表里你需要填入:在0.5秒之后预期输出值保持在2000加减2的范围以内。这样定义完成后,运行测试,Simulink Test会自动比对实际仿真输出和预期值的偏差。

定义一个用例不难,难的是把预期输出"猜"对。这里有个技巧:第一次跑用例的时候,先把预期输出设为宽范围(比如上下限离开50%),让仿真跑过一遍,再基于实际输出曲线来确定窄范围预期值。这么做不是为了偷懒,而是因为很多功能模块内部包含状态量,初始状态不同会导致输出曲线整体平移,先把曲线形状确认了,再卡边界就顺手多了。

4.4 执行MIL测试与覆盖率分析

用例编写完成后,点击Run按钮执行整个Test Suite。Simulink Test会依次运行每个用例,并在结果管理器中生成每个用例的通过/失败状态。如果你的模型里包含Random Number之类的非确定性模块,建议在测试仿真配置里固定随机数种子,否则每次运行结果可能不同,导致"上次过、这次挂"的随机性失败。

跑完测试后,让我特别推荐你去关注覆盖率数据。在Simulink Test的结果窗口里,右键可以选择"Coverage Results"。这里能看到的指标包括语句覆盖率、分支覆盖率、条件覆盖率、MC/DC覆盖率。MC/DC覆盖率是嵌入式行业认证常用的功能安全指标,要求每个条件的取值独立影响判定结果。如果你的项目目标客户是车厂或者航空领域,MC/DC基本是硬性要求。单元测试阶段把MC/DC做达标,比到系统集成阶段再补要轻松得多。

覆盖率不达标时,说明测试用例本身挖得不够深。你可以点开覆盖率的详细报告,逐条看未覆盖的分支。比如某个逻辑表达式a > 5 && b < 10,覆盖率报告会告诉你b < 10为真这条路径没跑到,那你就针对性地补一个能触发这条路径的输入组合。循环这个过程,覆盖率就会逐步爬上去。我一般给自己设的标准是:语句覆盖率不低于90%,核心分支覆盖率不低于85%,MC/DC作为一个建议项,尽量多做,但单元测试阶段不强求100%。

5. 从MIL到SIL:搭建软件在环测试环境

5.1 SIL测试的原理理解与准备工作

MIL测试全部通过后,接下来进入SIL测试阶段。SIL要解决的核心问题是:同一套测试用例,换一个被测对象,结果是否还能保持一致。

SIL的被测对象是模型生成的C代码,运行平台依然是PC。Simulink在调用SIL仿真时,本质上是在你本机把生成的C代码编译成一个可执行的动态库,然后用Simulink引擎加载它来替换模型模块。对用户来说,仿真界面看起来跟原来的MIL仿真几乎一样,但实际计算发生在C代码里。

所以我个人认为SIL测试是"性价比"最高的测试阶段——不花钱买硬件,不做实时系统搭环境,但能有效捕获代码生成带来的差异。它比代码评审查得准,又比硬件在环测得快。

开始SIL测试之前有两件事要做。第一件事,在Simulink的配置参数中打开代码生成:求解器选择离散求解器,固定步长,代码生成的语言设定为C语言。如果你连代码生成这步都没有配置过,可以先在模型窗口按Ctrl+B生成一次代码,确认能顺利跑通编译,再去做SIL测试。

第二件事,你要理解SIL测试在Simulink Test中的触发机制。Simulink Test的测试用例本身不区分MIL和SIL。同一个用例,在"Test Harness"模式下,最右侧有一个下拉列表,默认是Normal Mode(即MIL仿真),你可以切换成Software-in-the-Loop (SIL)模式。切换后,Simulink会在后台生成代码、编译、链接,然后加载运行。这一整个过程就是SIL测试的执行方式。

5.2 SIL测试的步骤细节

在Test Harness里切到SIL模式并点击运行,系统会进入代码生成阶段。不要小看这一步,整个代码生成时间通常比仿真时间长得多,在大型模型里可能要等好几分钟。建议在运行前先手动生成一次代码,把编译错误提前排除掉,别等到测试执行到一半才发现代码生成报错,白白浪费时间。

SIL仿真运行结束后,在测试结果管理器里观察结果。如果一切正常,结果应显示通过。此时关键的操作来了:打开"Signal Compare"或者"Simulation Data Inspector",将SIL仿真的输出信号和MIL仿真的输出信号画在一张图里对比。数值完全一致代表代码生成未改变算法行为;有偏差则需要分析是正常的数值误差,还是逻辑性差异。

你可能遇到的一个棘手场景是:正常情况下SIL和MIL输出应完全一致,但某些模块(比如Lookup Table、Math函数)中不同的代码生成配置,会让输出产生微小差异。例如查表模块在线性插值时的浮点数累加顺序差异,可能导致数值在第6位小数上不一样。这是可接受的,但为了测试能顺利通过,在用例的预期输出定义里,要给定足够的容差。我实际项目里用的容差经验值:浮点运算模块0.01(1%),查表和滤波模块0.001到0.005,逻辑判断和状态机模块必须完全相等,不做任何容差。

另外要特别关注数据类型的隐式转换问题。模型里当一个uint8类型信号和double类型信号相加时,Simulink会提示数据类型转换;但在生成代码里,转换规则可能被自动优化或改变。SIL测试的对比分析阶段要重点盯这些数据转换点。怎么盯?在覆盖率报告或者Signal Compare里分别看转换点的输入和期望输出,建议在模型里强制把类型转换块显式化,不要依赖隐式转换规则。

5.3 实时更新MIL/SIL回归测试

经历过一次完整测试流程后,你就会发现真正的日常工作是回归测试。每改动一次模型,都需要重新跑一遍全部用例,确认没有影响旧功能。如果项目有20个模块、每个模块40个用例,一次全量回归就是800次仿真。手工一个个跑,既枯燥又容易出错。

所以到了这个阶段,必须引入自动化脚本。Simulink Test本身就支持命令行运行,我用的一行核心脚本是这样写的:

testResult = sltest.testmanager.run('testRoot')

前提是你有一个已经建好的测试文件(.mldatx),其中包含所有测试套件。脚本执行完,结果会保存在testResult对象里,你可以用几行代码提取通过率:

resultObj = testResult.getResult; passedCount = resultObj.numPassed; failedCount = resultObj.numFailed; disp(['Passed: ', num2str(passedCount), ', Failed: ', num2str(failedCount)]);

版本里更精细的逐用例汇总和失败原因提取,可以通过导出到PDF或者Excel来完成。在命令行模式下,指定输出结果为XML也是可以实现的,方便和CI系统对接。

自动化跑回归的注意事项,这里总结三点:第一,测试前确保工作目录干净,不要有旧的生成代码缓存文件。第二,脚本里要设置Simulink.BlockDiagram.internal.setSimulationTimeout之类的超时保护,避免仿真卡死导致整个回归流程挂起。第三,回归结果的可追溯性很重要,建议把每次运行的测试结果自动保存到以日期命名的文件夹里,同时导出带时间戳的PDF报告。出了现场问题,你要能回头查阅当时那版代码的测试记录,这个习惯在项目交付阶段尤其重要。

6. 测试文档与结果评审标准

6.1 测试步骤文档应该包含什么

标题中的"测试步骤文档"很多人理解太窄,以为只是把用例和结果贴出来。我这里建议你按照功能安全的标准来写,这样评审会难挑毛病,项目交付也更站得住脚。一份完整的单元测试文档应该包含:

  • 测试环境描述:MATLAB版本、工具箱版本、操作系统、代码生成配置参数。
  • 被测对象描述:模型版本号、Simulink模块的路径、接口定义表格(信号名、数据类型、范围、单位)。
  • 测试用例清单:用例编号、关联的需求条目、输入设置、预期输出、容差设置、覆盖目标。
  • 测试执行记录:每次执行的时间、执行人、执行结果、通过率汇总。
  • 结果分析记录:差异列表、原因分析、处理措施、复测结果。

文档里最关键的是需求追踪矩阵。把你的每条测试用例映射到需求编号上,评审专家一看就知道每个需求都有对应的验证,每条测试用例背后也都有需求支撑。这个矩阵在项目交付时几乎是必须的。

6.2 测试度量与通过标准

我实际项目中常用的度量口径,方便你定自己的基线:

  • 测试用例通过率:正常交付标准为100%。如果有失败的用例,必须有明确的分析结论和修改记录,不允许带失败状态交付。
  • 用例与需求的覆盖比:每条需求至少对应一条正向用例和一条反向用例。
  • 覆盖率数据:语句覆盖和分支覆盖不低于90%。条件覆盖尽量做到100%,如果做不到,要有解释说明证明未覆盖的部分没有逻辑风险。
  • 缺陷逃逸率:这个指标需要跨多个项目周期来看,如果单元测试阶段修掉的bug多但集成阶段Bug仍很多,说明测试的有效性存在问题,要回头审视用例设计质量,"有没有照抄模型实现而不是基于需求设计"。

6.3 评审流程建议

测试完成后组织一次技术评审,叫上模型开发人员、测试人员和项目负责人。评审的重点不是看用例跑完没有,而是看失败用例的根因分析深度。如果发现失败率超过15%,建议先停下新增用例的编写,集中精力排查共性根因——这通常不是某个偶然的数值问题,而是测试环境或者被测对象存在系统性问题。我经历过一次连续三天的回归全挂,最后发现是工作区间的一个常量被其他的初始化脚本污染了。这种问题靠单个用例分析根本看不出来,必须从整体视角去审视。

7. 常见问题与排查技巧实录

7.1 测试用例在MIL下正常,SIL下结果不一致

这应该是SIL测试阶段出现频率最高的困扰。不要急着怀疑代码生成器有bug,先按三步排查:

第一步,对比MIL和SIL两次仿真中关键中间信号的值。在Simulink Test的对比视图里,逐帧看波形差异出现在哪个时间点。如果从开始就偏差,优先查初始化逻辑或者常量参数在代码生成时的处理。如果是在某个特定时刻开始偏差,重点查该时刻是否有状态跳转或表索引切换。

第二步,检查代码生成配置中的"Default parameter behavior"设置。如果这个参数设置为Tunable,生成的代码里会有参数结构体;如果是Inline,参数会被编译为常量。两者在某些边界数值处理上会产生细微差异,尤其在浮点常量被编译器优化时。

第三步,逐项排除代码生成选项。在代码生成界面的"Optimization"页签下,尝试关闭"Signal reuse"、"Local block outputs"等优化项,再重新生成代码做对比测试。注意:你做这个操作只是为了定位问题,不要用了关闭优化后的配置就交付。找到根因后,要回到模型的实现层面去修正,而不是靠关闭优化来掩盖问题。

7.2 测试执行时间过长,如何加速

模型大到一定程度,MIL测试单个用例如仿真10秒,40个用例跑下来就要近7分钟,如果一次回归跑800个用例就太慢了。加速策略按效果排序:

  • 并行执行。Simulink Test支持使用Parallel Computing Toolbox做多worker并行。在测试管理器的Parallel设置里,开启"Run tests in parallel"。我实测4核并行大概能提速3.2倍左右。注意并行模式下每个worker要独立的工作目录,避免文件写冲突。
  • 拆分测试工程。将一个大的Test Suite拆成多个小Suite,每个Suite单独开启MATLAB进程执行,相当于手动并行。这种方式更可控,尤其适合在服务器上的CI环境跑。
  • 降低仿真精度需求。如果模块是离散系统,可以把Solver类型设置为Discrete且不勾选"Zero-cross detection",仿真速度有明显提升。这里的代价是可能会漏掉某些过零事件,所以只建议在回归筛选阶段临时使用,正式交付的测试还是要用标准配置跑一次。

7.3 覆盖率始终卡在某个值上不去

覆盖率不达标往往是测试用例没有真正覆盖到模块的"逻辑死角"。我常用一个技巧:打开覆盖率报告,点击未覆盖的分支,Simulink会自动定位到模型中的对应模块,同时高亮显示这个分支上的所有前置输入条件。从报告返回模型看条件,再回过来补测试数据,这个过程可能要多重复几轮。

另一种情况是,覆盖率报告里显示某个条件被覆盖了,但MC/DC是假阴性。这意味着该条件的取值没有独立地影响判定输出。这种场景多数出现在逻辑表达式中存在短路(short-circuit)结构。要对这种问题进行排查,重点检查使用的逻辑运算符是不是在代码生成时采用了短路的实现方式。如果确认代码用于生产环境,MC/DC在单元测试阶段不强求全覆盖,可以标注为"分析通过、留待集成阶段补充"。关键是不要为了追求覆盖率而在用例里堆垃圾数据——那只会让覆盖率虚高,降低数据的说服力。

7.4 测试环境不稳定导致的随机失败

排除模型自身的逻辑问题以外,环境不稳定是造成测试结论可信度下降的另一大因素。我在实践中总结过常见问题:

  • 工作空间变量污染:测试脚本中定义了与模型同名的变量,影响参数解析。每个测试用例执行前,记得用clear all清理工作空间,或者使用测试工程自带的Setup脚本统一初始化。
  • 随机数种子未固定:模型里如果有随机信号源,每个用例运行得到的结果不同,容易产生虚假失败。在模型的回调函数里用rng(0)固定种子。
  • 文件路径缓存:不要用中文路径或者带空格的长路径存放测试文件。老版本MATLAB在代码生成时会因为路径解析异常导致编译失败。直接放到C:\work\sltest_project\之类的路径下最省心。

7.5 快速排查速查表

现象可能原因排查操作
MIL正常,SIL结果全偏代码生成配置参数被改动对比配置字典,确认求解器、数据类型、内联参数设置
用例偶发性失败随机源未固定种子检查随机模块,在回调加rng(0)
覆盖率偏低用例设计缺少边界或异常路径打开覆盖率报告,逐条定位未覆盖分支,补输入组合
编译报错包含"unresolved symbol"测试脚手架中缺少外部函数库在Test Harness的初始化回调中引路径库;或者检查模型引用的外部代码没有正确设置为源码文件
仿真时间极慢模型中存在连续求解器或过零检测改为离散求解器,关闭过零检测(仅测试模式允许)
参数被意外修改MATLAB脚本竞争执行测试前统一设置独立工作区并备份基本参数

我个人在实际项目中使用这套流程最深的体会是:MIL和SIL单元测试的价值不体现在一次执行上,而体现在每次迭代的回归里。真正提高效率和质量的,是你已经把用例设计成了一套可复用的资产,模型一改,测试就能跟上,而你心里有数这次改动到底影响到了哪些逻辑。刚开始搭建这套体系的那几天确实辛苦——先把模型重构得可测,再逐模块补用例,中间还会被各类环境问题分走不少精力。但如果你能坚持把一个中等复杂度的模块完整走通一遍从MIL到SIL的闭环,后面就是复制的功夫了。后续可以考虑加入PIL测试和硬件在环,结合代码覆盖率和静态分析工具,整套开发验证链就会更完整。最后留一个额外建议:给每条测试用例加上稳定的标识符,之后无论是CI集成、缺陷追踪还是客户审计,追溯起来都方便得多。

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

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

立即咨询