☰
Simulink MIL测试实战:从环境搭建到覆盖率分析
2026/10/3 6:45:51 网站建设 项目流程

做MIL测试这件事,在Simulink里到底是在干嘛?很多刚接触基于模型开发(MBD)的人,看到“MIL”这个词就头皮发麻,总觉得是个特别玄乎的测试名词。其实说白了,MIL(Model in the Loop,模型在环)就是把你辛辛苦苦搭好的控制算法模型,放到一个模拟环境里去“跑起来”,看看它的逻辑对不对、输出符不符合预期。什么真实硬件、什么代码生成,统统不需要,只在纯软件的模型世界里做验证。

我一直跟团队里的新人反复强调一个观点:MIL是整个V模型开发流程里“性价比”最高的测试环节,没有之一。原因特别简单——你在MIL阶段发现一个逻辑错误,改的可能只是一条连线的工夫;但这个问题如果漏到了代码阶段、再漏到实车阶段,那排查成本可能就是几十倍甚至上百倍的差距。所以MIL测试从来不是走形式,它是在跟时间和成本赛跑。

这篇文章我就从自己在实际项目中做MIL测试的经验出发,把Simulink里怎么搭测试环境、怎么写测试用例、怎么分析覆盖率、怎么跟SIL/PIL/HIL衔接这些事儿掰开揉碎讲一遍。不管你是刚开始接触MBD的新人,还是已经被集成测试折磨得焦头烂额的工程师,这篇文章都能给你一个可以直接拿来用的完整思路。

1. MIL测试到底是什么:从一段代码到模型世界的平移

1.1 一个容易误会的前提:MIL测试不是“仿真”

很多教程喜欢把MIL测试跟普通Simulink仿真混在一起讲,这其实是个挺大的误区。普通仿真是什么?是你搭了个Plant模型(被控对象模型),又搭了个Controller模型(控制算法模型),两个一拼,跑出来的波形就是仿真结果,主要用来验证算法原理对不对。

MIL测试不一样。MIL测试的核心对象是那个Controller模型,Plant模型在这里其实是充当“测试环境”的配角。你真正要做的事情是:设定好输入条件(包括正常工况、极限工况、故障工况),让Controller模型在Simulink的软件环境里跑起来,然后去检查它的输出行为是否满足需求。

所以一个完整的MIL测试环境,至少要包含三块内容:

  • 被测对象:控制算法模型,也就是你真正要交付的那部分逻辑。
  • 测试输入:来自需求文档的测试用例,可能是信号的时序组合、数值边界、故障注入等。
  • 测试评估:判断输出是否符合预期的机制,可以是信号对比、断言、容差检查,也可以是覆盖率分析。

这三块缺一不可。很多人做MIL测试只做了前两步,跑完波形看一眼“感觉差不多”就算完事,这其实是不合格的。没有自动化的结果评估,你的测试用例就没有“通过/失败”的判断标准,也就不具备回归的价值。

1.2 MIL在V模型流程里的具体位置

V模型开发是汽车电子、航空航天这些功能安全领域绕不开的一套流程。左边是开发流程:需求定义、系统设计、软件设计、代码实现;右边是测试流程:单元测试、集成测试、系统测试、实车验收。MIL测试就横跨在“软件设计”和“代码实现”之间,是模型验证的第一道关卡。

从工程角度看,MIL测试承担着一个特殊的双重角色。一方面,它是对控制模型本身的功能验证,你要证明模型的行为跟需求文档描述的是一致的;另一方面,它又是后续代码生成的“准入门槛”,如果模型在MIL阶段就有逻辑漏洞,那你生成的嵌入式代码大概率也有同样的漏洞,而且更难查。

我在实际项目里经常用这么一句话跟测试工程师解释MIL的意义:MIL测试是给模型“照X光”,它解决的不是“代码写错没有”的问题,而是“逻辑本身长歪没有”的问题。这个阶段不检查,后面越走越偏,返工的代价是灾难性的。

1.3 MIL测试的优缺点,说点不爱听的实话

MIL测试的优点网上到处都是,比如成本低、可复用、覆盖高、灵活方便,这些都是真的,我不否认。但作为实操过的人来说,我得说说它的局限。

最大局限是“模型误用风险”。Simulink模型本身是一种高度抽象的表示,里面很多信号是double类型,采样时间也是连续可变或者固定周期设置的,可一旦你生成了嵌入式C代码,就会面对定点数、固定步长、内存限制这些现实问题。MIL阶段验证通过的逻辑,在SIL/PIL阶段可能就翻车了。很多人以为MIL跑完就万事大吉了,这是个很危险的认知。

第二个局限是“成本陷阱”。做MIL测试同样需要投入很多精力去设计测试用例、搭建测试框架、维护测试脚本。如果项目周期短、模型规模大,MIL测试的时间成本可能并不比直接做快速原型测试低多少。关键是要根据项目场景做好取舍。

2. 在Simulink里做MIL测试:环境搭建与核心操作

2.1 准备一个可测的模型

说句实在话,MIL测试的第一步不是搭测试环境,而是把被测模型整理成“可测”的状态。什么意思?就是你的模型结构必须分层清晰、接口定义明确,不能是那种一个大子系统里塞了二三百个模块的“意大利面模型”。

在开始MIL测试之前,我建议先做三件整理工作:

第一,把所有的Inport(输入端口)和Outport(输出端口)改成有意义的名字,并且给每个端口加上单位、范围和描述。别小看这一步,后面写测试用例的时候全靠这些信息来理解信号含义。

第二,尽量对你的控制模型做接口封装。也就是说,Controller模型应该是一个单独的模型或者单独的子系统,和Plant模型分开。这样你做MIL测试的时候,可以单独加载Controller,再另行添加测试激励模块,而不是在一个大模型中纠缠不清。

第三,对模型中的数据对象进行规范化管理。这是很多人容易忽略的。在Simulink里,信号的数据类型、初始值、存储类别这些属性往往是模型级别的设置,如果东一个西一个地硬编码,后面做SIL/PIL时接口就会对不上。建议用Base Workspace或者Data Dictionary统一管理这些对象。

2.2 测试激励的注入方式

MIL测试中,最核心的实操就是“给模型喂数据”。喂数据的方式五花八门,我按从小到大排列,说几个常用的手段。

第一种,直接用Signal Builder或者Signal Editor模块。这个适合输入信号比较规则、用例数量不多的场景。比如你要测试一个温度控制器的逻辑,需要给定一个从常温升到高温的斜坡信号,Signal Builder拖进来画两条折线,配置好时间轴,这事儿就完成了。优点是上手快,缺点是信号多了以后配置界面会变得非常混乱,后期维护成本高。

第二种,用From Workspace或者Inport配合MATLAB脚本。这是我自己最喜欢的方式,因为数据源可以放在MATLAB工作区甚至Excel文件里,测试脚本可控性非常强。你可以用脚本生成任意复杂的时序信号,包括正弦叠加、阶跃组合、随机噪声、特定故障模式,然后一次性批量运行所有用例。实际项目里,我通常会用Excel维护一份测试用例清单,每一行记录用例ID、输入信号值域、运行时长、预期结果,然后用脚本读取这个表去自动跑仿真。

第三种,用Simulink Test模块。这是MathWorks官方提供的测试管理工具包,专门为MIL/SIL/PIL自动化测试设计的。它支持创建Test Harness(测试隔离环境)、Test Case(测试用例)、Test Assessment(结果评估)、Test Suite(测试套件),还能直接生成测试报告。如果公司项目要求过ASPICE流程,Simulink Test基本是标配。

2.3 结果观测与自动对拍

测试输入只是前半场,真正让MIL测试变得有价值的是自动化的结果评估机制。

在Simulink里做结果评估,一种简单粗暴的方式是让仿真跑完以后,人工去看Scope波形,用眼睛比对曲线跟你预期是否一致。这种做法我实话实说,在初期开发探索阶段可以接受,但你不可能靠人眼去做回归测试。一套模型迭代十版以后,你靠人眼能看出第三版和第四版之间的细微行为差异吗?肯定不能。

正确的做法是在模型里嵌入断言(Assertion)模块,或者在外部用MATLAB脚本对输出信号做检查。具体来说有两种落地方式。

一种是被测模型内部嵌入检查逻辑。比如我用Signal Processing这块的Check Static Range模块,可以直接判断某个信号是否超出预设范围,一旦越限就会在仿真报错或者记录一个标志。这种方法的优点是实时性好,缺点是检查逻辑跟被测模型耦在一起,侵入性太强,不太适合做第三方独立验证。

另一种更推荐的做法是把评估逻辑写在仿真后处理脚本里。仿真跑完,用ModelOutputs = simOut.get(‘logsout’)把输出信号取出来,然后写代码跟期望值做对比,判断容差是否在允许范围内。这样你的测试用例和被测模型是彻底解耦的,可以单独维护,互不干扰。

2.4 一个关键认知:仿真步长与数据类型的影响

做MIL测试时,一个非常基础但很容易踩坑的配置就是仿真求解器的设置。

很多做算法开发的人在MIL阶段习惯用默认的变步长求解器(如ode45),跑得快、曲线光滑、看起来效果棒极了。但这里有个隐患:你最终生成代码的时候,目标硬件上跑的一定是Fixed-Step(固定步长)离散求解器。MIL阶段用变步长得到的仿真结果,跟SIL阶段固定步长代码运行的结果之间,必然存在数值误差。

所以我个人的实操习惯是:从MIL阶段开始就直接用Fixed-Step Discrete求解器,步长设置跟后续代码生成的步长保持一致。比如你的算法运行周期是10ms,那你在MIL里就把Fixed-step size设为0.01。这虽然会让仿真的速度稍微慢一点,但换来的是MIL和SIL结果的高一致性,后期联调时能省掉大量排查时间。

还有一个容易被忽视的坑是数据类型。Simulink里很多模块默认是double类型,可实际嵌入式代码里用的可能是single、int16、uint8。如果你在MIL阶段没有注意数据类型的配置,模型跑得再顺也说明不了问题。建议在模型里显示信号的数据类型(View->Signal Information),并且定期检查。

3. 一个完整的MIL测试用例:从设计到执行

3.1 需求拆解与用例设计

说了那么多概念,该实操了。我从实际项目里抽取一个最简单的例子来演示:一个电池过温保护逻辑。需求描述是:当电池温度持续3秒超过60℃时,输出报警信号;当温度回落到55℃以下持续5秒时,报警解除。

拿到这个需求,做MIL测试的第一个人就是拆解测试条件。很多人一上来就只会写一个“给温度输入65℃,看报警是不是1”,这是典型的最不合格的用例设计。作为一个专业测试工程师,你需要想的远不止这些:

  • 初始状态是什么?温度从0开始缓慢升到65℃,还是直接从65℃开始?
  • 超温持续时间的边界怎么测?持续2.9秒不报警,持续3.0秒报警?持续3.1秒呢?
  • 迟滞效果的验证:温度降到56℃时报警是否保持?降到54.9℃时是否解除?
  • 故障场景:温度传感器信号出现跳变、NaN、满量程时算法如何表现?
  • 时序场景:温度反复穿越阈值时,是否出现报警抖动的现象?

把这些分析清楚,最终整理的用例表大概是这样的。

用例编号用例名称输入条件预期输出优先级
TC-01正常超温报警0-70℃斜坡,上升速率1℃/s达到60℃后3s输出报警高
TC-02未到超温时间不报警输入超温持续2.5秒后回落不输出报警高
TC-03超温时间边界验证输入超温持续3.1秒输出报警中
TC-04报警解除阈值验证65℃下降至54℃并保持6s报警解除高
TC-05迟滞保护验证65℃下降但未低于55℃报警保持中
TC-06传感器断线故障注入输入信号=NaN输出报警或安全状态高
TC-07输入抖动脉冲干扰脉冲串间歇穿越阈值报警状态稳定不抖动低

3.2 模型侧搭建测试框架

用例设计完了,回到Simulink里搭测试环境。我习惯的做法是新建一个专门用于测试的模型,比如叫“BMS_Overtemp_MIL_Harness.slx”,在这个模型里只做三件事:加载被测模型、注入测试输入、采集测试输出。

以TC-01为例,我在Harness里放了一个Signal Editor模块,配置温度信号从0秒开始按每秒1℃的速率从0升到70℃,仿真时长设置为80秒。然后用一个Inport连接到被测模型的温度输入端口,被测模型的输出接到日志模块中,通过Signal Logging的方式把报警信号记录下来。

这里有个需要注意的细节:如果你在Harness里直接引用外部模型,建议用Model Reference的方式挂在子系统里,而不是Copy-Paste一份进来。Model Reference的好处是版本统一,修改被测模型后Harness自动更新,避免了“明明改了模型但测试还是旧结果”这种低级错误。

3.3 用断言模块做自动判断

光把波形记录下来还远远不够。对于TC-01这个用例,预期的行为是在温度达到60℃后的第3秒(也就是大概t=63秒)报警输出变为1。我需要让仿真自动判断这一点,而不是人肉去看曲线。

我通常会加一个Assessment子系统,里面用几个关系运算模块(>=、==)、一个延时模块和一个Assertion模块组成判断链条。判断链条的逻辑是:报警信号从0变为1的时刻,用Clock模块记录时间戳,然后判断该时间戳是否在62.8秒到63.2秒的容差窗口内。如果超出窗口,Assertion模块直接报错,仿真失败。

这个时间容差窗口的设置是有讲究的。如果需求里写的是“持续3秒报警”,那理论上报警精确发生在第63秒。可由于Simulink仿真步长的限制,信号从0变成1的那个时刻脚本是有量化误差的。步长10ms就可能有10ms的误差。所以容差窗口不是越小越好,我一般设置为仿真步长的5~10倍。

3.4 批量跑用例的数据管理

单个用例跑通以后,正式项目中肯定是有几十上百个用例要执行。这时候再靠手动切换Signal Editor里的信号配置,效率太低且容易出错。

我的做法是在MATLAB脚本里维护一个测试用例结构体数组,每个用例包含输入信号的描述、仿真参数的设置以及期望输出的阈值。脚本按顺序执行仿真,每次仿真后提取报警信号的跳变时间点,然后跟结构体里的期望值做比对,汇总为一份表格或者Excel报告。

用到的核心API其实不多,核心几个就是new_system、add_block、sim和Simulink.SimulationInput。特别是Simulink.SimulationInput这个类,它可以非常方便地批量修改模型参数和外部输入,在循环里创建多个实例后并行仿真,速度快很多。比如我用parsim代替sim跑批量用例时,4核机器差不多能提速3倍左右,极大缓解了测试等待时间。

4. 覆盖率分析:你测够了没有

4.1 覆盖率不是越高越好

说到覆盖率,很多人的第一反应是“覆盖率要做到100%”。做MIL测试的时候,这句话其实是个陷阱。

Simulink的覆盖率分析(Coverage Analyzer)可以统计语句覆盖、分支覆盖、条件覆盖、MCDC覆盖等指标。理论上,当然覆盖率越高说明测试越充分。但实际项目中,为了把覆盖率从90%追到95%,可能要多设计几十个专门“凑覆盖”的用例,投入产出比极低。

工程上一个更务实的做法是:根据项目的功能安全等级(比如ISO 26262中的ASIL等级)来确定覆盖率目标。ASIL-D级别可能要求MCDC覆盖率达到100%,而ASIL-A可能只要语句覆盖率达到100%就够了。这个目标需要开发、测试、项目经理几方提前拉齐,而不是每个人各自为战。

4.2 在Simulink里打开覆盖率分析

激活覆盖率分析其实非常方便,直接在模型的仿真设置里勾选“Coverage”标签页,然后选择你要统计的覆盖率类型,在模型上右键选择“Coverage Analysis”。跑完仿真后,在结果窗口里就能看到每一个模块、每一个子系统、每一条分支的覆盖情况。

用覆盖率报告去反推测试缺失是个好习惯。比如我发现某个子系统的输出端口“真”的覆盖率只有50%,说明所有测试用例中这个端口从未输出过“真”值。这往往意味着有一类工况完全没覆盖到,需要倒回去补充用例。

4.3 从覆盖率结果倒推用例补充

给你一个我自己项目的真实案例。之前做发动机冷却液温度控制器的MIL测试,覆盖率报告跑完以后发现一个很蹊跷的结果:整个模型的STATUS覆盖已经92%,可某个局部子系统的MCDC只有60%。排查下来发现,这个子系统实现了一个“过热降额”策略,但降额条件里有一个“冷却液温度>110℃且持续5秒”的分支条件在现有测试用例里从未真正满足过。

原因很简单:测试用例里设计过110℃的瞬时高温,但没有设计过能让温度持续保持在110℃以上的运行场景。后来我补充了对应长时高温用例,覆盖率一下从60%蹦到了85%。这就说明,覆盖率分析不只是用来“交差”的,它真的是指导新用例生成的有效工具。

5. MIL和SIL/PIL/HIL怎么配合:工程链路上的角色分工

5.1 四个“在环”的对比矩阵

很多新人都会问:MIL跑得好好的,为什么非要搞SIL、PIL、HIL那么麻烦?我做个简洁的对比表格,你一看就明白了。

测试阶段运行环境被测对象主要验证目的成本实时性
MILSimulink软件环境控制算法模型逻辑功能正确性最低无
SIL宿主机软件环境自动生成的代码代码与模型行为一致性低无
PIL目标处理器目标硬件上的代码代码在真实CPU上的行为中有
HIL实时机+真实I/O嵌入式控制器控制器与实物/仿真被控对象集成最高高

从表格能看出来,MIL是整个测试链条的“地基”。它解决的问题是“算法的逻辑到底对不对”,SIL和PIL解决的是“生成的代码有没有把算法给写歪了”,而HIL解决的是“控制器被塞进真实电气环环境中还能不能正常工作”。这四层测试是层层递进、各有分工的,谁也替代不了谁。

5.2 工程实践中的选择策略

实际做项目的时候,有些人会觉得四层测试必须全做,一步都不能少。但我个人的经验是,测试策略必须结合变更范围和开发阶段动态调整。

举个例子,如果这次迭代只是改了一个查表模块的标定参数,数值范围变化不影响逻辑分支走向,那MIL跑一遍回归就够了,SIL和PIL可以省略。如果这次迭代是新增了一个状态机,可能影响全局模式切换逻辑,那就必须MIL+SIL+PIL三连跑。如果涉及硬件的IO映射、通讯协议栈变更,那HIL测试就是必须的了。

我自己维护过的一套MIL回归用例库,大概是400多个用例,跑完整套差不多需要40分钟。这个库几乎每轮迭代都要执行。而SIL的用例库跟MIL共用一套测试用例,只是执行时配置不一样。这样既保证了测试的复用性,也大幅节省了用例维护成本。

5.3 MIL与SIL差异比对:一个典型的返工案例

再分享一个很典型的案例,能很好地说明MIL和SIL之间存在差异的原因。

之前有个项目,控制算法里用了一个积分器,在Simulink里默认是连续时间积分。MIL阶段测试全绿,但代码生成后SIL测试中发现输出有明显漂移,误差一路累积,最后触发了保护逻辑。排查再三,发现就是连续积分和离散积分之间的数值差异导致的。

后来我们的措施就是在早期MIL阶段就把积分器模块明确修改为离散形式,并且在MIL用例中特意加入了“长时仿真漂移”类测试来捕捉这类问题。一旦你的MIL环境跟目标运行环境越接近,后面暴露的意外就越少。这也是我前面反复强调固定步长和数据类型一致性的原因。

6. 高频问题与排查技巧实录

6.1 仿真结果和期望值差得离谱

这种情况见过太多次了,十有八九是信号值域和单位没对齐导致的。比如你测试开环增益为2的控制模型,喂进去的油门信号是0到1之间的标幺值,但是模型内部期望的是0到100的百分比,出来的结果就会差两个数量级。

排查技巧很简单:在关键的信号线上接Display模块或Scope模块,在特定时刻暂停仿真,查看中间值。如果你能看到哪一级的信号突变异常,问题自然就定位了。要记住,MIL阶段是模型级的验证,模型内部信号可见是最大的优势,不要靠猜。

6.2 断言一直报违规但波形看着没问题

这个问题的常见根因是容差设置得太严格,或者断言判断的时间点不对。比如报警信号的跳变需要在第3秒整发生,但是仿真步长是10ms的倍数,信号真正改变可能发生在2.998秒,恰好落在容差窗口外。

解决办法是不要用极小的容差窗口,也不要在信号变化沿上用严格相等判断,换成上升沿检测加死区时间再利用。最省力的做法是在脚本里用find函数获取信号第一个跳变索引,再乘以步长换算成时间,这样可以消除步长带来的量化误差。

6.3 批量仿真时内存爆掉或者异常退出

批量跑用例时碰到这个问题,大概率是日志数据积累太多。每跑完一个用例,Simulink默认会把全部输出信号都缓存到内存里,用例多了以后内存自然不够。

我的做法是每执行完一个用例立即清理日志数据,只保存关键指标。可以用simOut = sim(‘model’,‘StopTime’,'80')这种方式,然后运行simOut.get(‘logsout’),只提取需要的那个信号,再用clear变量释放内存。如果还要更快,可以用parsim的ShowProgress选项监控进度,并在每个worker上控制数据缓存。

6.4 从Excel表驱动用例的脚本骨架

最后分享一个最常用的测试管理脚本骨架,你用这个做底子,稍微改改就能适配你的项目。

%% 从Excel读取测试用例 caseData = readtable('mil_test_cases.xlsx'); nCases = height(caseData); %% 准备批量仿真输入 simIn(1:nCases) = Simulink.SimulationInput('BMS_Overtemp_MIL_Harness'); for i = 1:nCases tempProfile = eval(caseData.tempProfile{i}); simIn(i) = simIn(i).setVariable('TempSet', tempProfile); simIn(i) = simIn(i).setVariable('StopTime', caseData.stopTime(i)); end %% 执行批量仿真 simOut = parsim(simIn, 'ShowProgress', 'on'); %% 提取结果并判断 for i = 1:nCases logsout = simOut(i).get('logsout'); alarmSignal = logsout.get('Alarm').Values.Data; alarmTime = logsout.get('Alarm').Values.Time; idx = find(alarmSignal > 0.5, 1); if isempty(idx) result(i) = false; else actualTime = alarmTime(idx); result(i) = abs(actualTime - caseData.expectTime(i)) < 0.1; end end

这段脚本的逻辑两分钟就能看懂:读取用例表、把每个用例的输入变量注入仿真对象、批量跑仿真、提取结果做自动判定。实际项目上你可以再加一层,比如把判定结果写回Excel,生成一条超链接指向具体的信号曲线,从而形成闭环的测试报告。这套流程搭好以后,日常的MIL回归测试基本能做到“一键全跑”。

6.5 一个提升仿真速度的小坑

最后说个小坑。很多人批量跑MIL时发现速度特别慢,看CPU每个核都是绿的,但仿真没快起来。这往往是模型里的开的并行worker数超过了物理核数,导致内存带宽成为瓶颈。

查一下parsim默认开的worker数量,通常跟逻辑核数相等,比如16线程的CPU就开16个worker。可这么多仿真同时跑,内存瞬间吃满,硬盘疯狂swap,速度反而更慢。我实测下来,对于中型模型,把worker数限制为物理核数的一半,整体吞吐反而更快。调整方式是在parsim调用前设置parpool('local', 4)之类的并行池大小。

做MIL测试这件事,说到底就是在把对控制逻辑的信任拆散、再重建。拆散,是因为你得把需求文里每一句话都变成可执行、可验证的测试用例;重建,是因为当几百个用例全都跑绿的时候,你才对那个模型建立起真正的信心。这个信心不是靠眼睛看几帧波形来的,而是靠一套可以自动、可回归、可追溯的测试机制垒起来的。我个人在实际项目里的体会是,MIL测试环境一旦搭好,它带来的长期收益会远超前期搭建的那点成本。所以别嫌麻烦,尽早把自动化那套东西配齐,你会感谢当初自己这个决定的。

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

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

立即咨询