资讯详情

Simulink MIL测试实战:从环境搭建到覆盖率分析

📅 2026/10/3 6:45:58 | 华诺云谱 👁 阅读
Simulink MIL测试实战:从环境搭建到覆盖率分析
做MIL测试这件事在Simulink里到底是在干嘛很多刚接触基于模型开发MBD的人看到“MIL”这个词就头皮发麻总觉得是个特别玄乎的测试名词。其实说白了MILModel 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秒也就是大概t63秒报警输出变为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可以省略。如果这次迭代是新增了一个状态机可能影响全局模式切换逻辑那就必须MILSILPIL三连跑。如果涉及硬件的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测试环境一旦搭好它带来的长期收益会远超前期搭建的那点成本。所以别嫌麻烦尽早把自动化那套东西配齐你会感谢当初自己这个决定的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑