尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Simulink MIL测试实战:从模型在环到自动化回归的完整指南

Simulink MIL测试实战:从模型在环到自动化回归的完整指南 1. 为什么一上来就要谈MIL模型仿真测试的真正价值1.1 MIL到底是什么一套用来问模型对不对的系统化方法MILModel-in-the-Loop模型在环测试很多刚接触Simulink的人以为它就是在Simulink里跑一下仿真、看看波形其实没那么简单。它是一套把被测控制模型放到仿真环境里用设计好的输入激励去驱动它再检查输出是否符合预期的系统化验证方法。我一直觉得MIL测试的核心不是仿真而是测试方法——仿真只是它的载体它的灵魂是那套你写好的用例、判据和覆盖度分析。在Simulink里做MIL你面对的被测对象是纯粹的算法模型比如一个电机控制器的PMSM矢量控制模型、一个电池管理系统的SOC估算模型或者一个汽车ESP标定逻辑。这些模型还没被编译成C代码也没有跑在真实硬件上所以MIL测试更像是在图纸阶段就把方案论证清楚输入端给定转速阶跃输出端是不是在预期时间内到了目标值、超调量在不在范围内、状态转换是否符合状态机定义。有人会问我拿Signal Builder加个阶跃信号跑一下仿真再看Scope里的曲线这不就是MIL测试了吗严格来说这叫仿真验证不叫测试。MIL测试更强调可重复、可量化、可追溯每条用例对应哪条需求通过/失败按什么判据判定覆盖率项次是否跑满今天跑了三个月后改了一版模型能不能一条脚本全量回归这才是MIL测试真正要做的事。1.2 从V模型看懂MIL的位置越早发现问题成本越低你随便找一本做汽车电子或航空软件开发的参考书都会看到V模型。左边从上到下是需求定义、系统设计、软件设计、代码实现右边从下往上是单元测试、集成测试、系统测试、实车验收。MIL测试的位置就在软件详细设计完成之后、代码生成之前也就是模型既已经搭出来了、但还没转成产品代码的那个阶段。这个阶段在整个开发链条里极其关键。按行业内流传的一个成本经验需求阶段发现一个缺陷修复成本可能是1到了模型阶段变成10到了代码阶段是50到了实车阶段可能就是200甚至更高。为什么差距这么大因为模型阶段改的是一根连线、一个判断条件、一个查表数据编译器都不需要重新跑到了代码阶段你还要改代码、重新生成、重新配置集成环境到了实车阶段问题往往隐藏在所有系统交互之中必须用标定工具、CAN报文、录制数据一路排查才能找到根因。所以我在项目里一直坚持一个原则模型冻结之前MIL回归必须全部通过。模型一旦冻结之后每次修改都要问一个问题——这次改动动了哪个函数影响了哪些需求对应的MIL用例是哪几条这是在用流程成本换测试成本非常划算。1.3 哪些项目真正需要MIL不是所有Simulink模型都值得做我得说句实话不是所有Simulink模型都值得搭一整套MIL测试环境。如果你只是课堂上做个demo或者给论文跑个算法流程图那直接仿真就够了。MIL测试真正发挥价值的场景有三个特征。第一模型会被用于自动代码生成。也就是说这个Simulink模型最终要经过Embedded Coder之类工具生成产品级C代码。这种情况下模型本身不只是算法验证工具它等于源代码本身。代码级的Bug有很大概率在模型阶段就有对应表现如果不在模型层查清楚等代码生成出来再查代价高得多。第二需求数量多且变更频繁。像车身控制器、域控制器这类项目需求动辄几百条而且每周都在变。如果没有一套自动化的MIL回归用例光靠人眼看波形改一处就复查全校能把人累死。第三功能安全相关。ISO 26262、DO-178C这类标准都明确要求模型层的验证活动。测试覆盖率、需求可追溯性、用例执行记录这些不是可选项是审厂、认证时必须拿得出来的证据。反过来那种纯做前期算法预研、搭完模型只是用来出效果图的MIL测试确实可以做得轻一点几条关键边界用例跑一遍就够了。我建议你先问自己一个问题这个模型后面要变成什么想清楚这个再决定投入多少精力搭测试环境。2. 在Simulink里搭一套可复用的MIL测试环境2.1 被测模型规范化接口、采样与命名是跑通基线的前提做一个MIL测试环境不是新建一个Simulink模型然后把被测模型拖进去那么简单。你首先要处理的就是接口规范化问题。很多时候我们拿到手的模型它的输入端口上直接怼了一个Constant或者Step模块根本没有留出外部输入接口或者输出端口直接接了Scope信号没命名。这样的模型根本没法作为被测对象因为你很难把测试激励和判据自动地接到它身上。所以第一步是先把被测模型改造一遍。我个人的做法是把被测模型封装成Subsystem所有输入、输出都用Inport/Outport模块引出并在外部再用Goto/From或者Signal Label把信号名定好。输入输出端口的名字和需求文档里的信号名保持一致别嫌麻烦。比如一个电机控制器的转矩控制模型输入端口就该是Trq_Cmd、Motor_Speed、Bus_Voltage输出是Cur_D、Cur_Q、SVPWM_Duty这样后面写判据的时候信号名一写就知道在测什么。采样时间也要统一。Simulink模型里常见的问题是混合了连续模块和离散模块有人直接用了VariableStep求解器步长还设成了自动。MIL测试环境下我建议统一用定步长离散求解器比如FixedStepDiscrete或FixedStepAuto步长按控制周期的整数倍来设。如果是做电气那类快速环路的仿真步长可能只有1e-5秒做整车控制逻辑的通常10ms就够了。测控制逻辑的MIL用例没必要把步长缩到比控制任务本身还小几十倍白浪费时间。还有一个容易被忽略的规范是模型内部不能有奇怪的初始化依赖。比如有些模型用了Initialize Function模块里面根据某个全局变量做了条件初始化。MIL测试里每次用例运行时这个状态能不能正确重置直接决定了回归结果可不可信。我在前面踩过这个坑后面会专门讲。2.2 测试激励生成Signal Builder、Signal Editor与脚本三选一Simulink里生成激励信号常用的办法有三类老式的Signal Builder、新版信号编辑器和用MATLAB脚本直接给输入端口赋值。我推荐优先用Signal Editor或者脚本因为Signal Builder在较新的MATLAB版本里虽然还能用但功能上已经边缘化建模和批量管理都不如Signal Editor方便。Signal Editor适合那种一段段手动拉出来的波形给转速信号加一个从0升到4000rpm的斜坡保持2秒再突然阶跃到8000rpm然后再下来。你能直观地看到波形长什么样适合单条用例调试。但它有一个毛病就是多组用例之间切来切去比较麻烦而且你没法用代码很容易地控制它运行哪一组、期望输出是什么。所以我大多数项目用的是脚本方式。做法很简单用Simulink.SimulationInput对象生成多个仿真输入每条用例把输入端口的值直接赋成自己生成的数据序列。比如官方库里的test用例很多内部测试就是这么干的先用timeseries对象定义端口信号再放在Simulink.SimulationInput里最后用sim批量跑。这样做的好处是你的测试用例本质上是MATLAB代码和数据文件天然支持版本管理、批量执行和自动比对。我举个例子。假设被测模型是VMC_Controller想测踩油门踏板从30%突然到80%时输出扭矩是否在50ms内到位。你可以先做两层数据一是输入数组比如踏板开度从0到1秒保持30%1秒时阶跃到80%持续2秒二是期望结果数组在仿真后根据模型输出计算到达时间、稳态误差等指标。然后用脚本批量跑最后把结果指标和阈值一比对生成一份pass/fail报告。2.3 自动判据与需求追溯用Simulink Test让用例可重复执行说到这儿就得介绍Simulink Test工具箱了。它是目前在Simulink里做MIL、SIL测试最主流、最工程化的环境。你把各种输入信号整理到.mat或Excel文件里用Simulink Test把一组输入和一组判定模板组合成一个TestCase比如TC_001_TrqCmd_StepUp_CheckOvershootAndSettleTime。这个Test Case里有三个关键部分输入部分加载外部输入数据可以是Signal Editor里的信号组也可以是从Excel读入的信号表。仿真参数求解器类型、步长、仿真时长、初始状态。判定部分用Assessments逻辑块或Simulink Test自带的验证语句定义通过条件。我特别喜欢它的一个机制是需求追溯。每个Test Case可以关联到需求条目的唯一标识比如把需求ID填到Requirement标签里。跑完一遍测试生成的测试报告中会自动列出每条需求对应跑了哪些用例、结果是通过还是失败。这对过功能安全评审简直是救命稻草审厂的人就喜欢看这种表。哪怕你不搞认证想象一下半年后你接手一个自己都快忘掉的模型有这么一份追溯表在恢复上下文的速度能快一倍。判据的定义是整个MIL测试环境里最能体现功力的一环。太严容易误报比如期望输出1.0就不允许超过1.001结果仿真噪声大一点就红了太松呢模型都错得很离谱了判据还说pass。我的经验是凡是涉及连续物理量的不要用等于要用绝对误差带或相对误差带凡是涉及时序的不要把时间点卡死要给一个合理的窗口。比如电机类模型判据写成0.5秒内达到指令值的95%稳态误差小于2%而不是第0.5秒时值必须等于499.7rpm。3. 从跑通模型到测得全面测试用例设计的思路与坑3.1 测试用例来源从需求条目反推而不是对着模型临场编很多人拿到一个模型第一反应是打开Simulink开始想几个输入信号试试。这种临场发挥式的方法测出来的用例要么跟需求没关系要么重复测同一个功能点遗漏那些真正容易出问题的场景。做MIL测试用例来源必须是需求文档你每写一条用例都要能说出我这是在验证需求第几条。我常用的拆解方法是按需求类型分类。对于功能需求比如当车速大于120km/h时激活限速报警那就至少要拆出三类用例车速从119升到121报警是否激活车速从121降到119报警是否解除车速一直在120附近抖动报警是否产生频繁抖振。这三条对应的是正常激活、正常退出、滞回判断每一个都针对需求表达里可能隐含的逻辑漏洞。对于性能需求比如最大稳态误差不超过±1%那就要考虑输入在全量程范围内扫过时哪个工作点是误差最大的。这类用例往往不是一两个激励就能覆盖的需要做参数扫描。Simulink里可以用Simulink.SimulationInput批量改模型参数把控制目标值从10%跑到100%步长5%跑完把所有稳态误差收集起来生成散点图一眼就能看出模型在哪个工作区间的边角出了问题。还有一个很重要但容易被忽略的来源是异常和边界输入比如信号丢线NaN或Inf、传感器值越界负的转速、突然断连信号值跳到0或保持上一拍。这些用例在功能需求里往往不会写但恰恰是模型在实车上最容易出Bug的场景。MIL阶段因为仿真成本低非常适合把这些极端case跑一遍。3.2 边界值与时序测试看一个真实的电机控制MIL用例怎么写我拿以前做过的一个PMSM电机控制器的MIL测试来举例。被测模型包含电流环、速度环、SVPWM调制和过流保护逻辑。需求里有这么一条当母线电压低于260V时系统在10ms内进入降额状态最大允许扭矩下降50%。当时我把这条需求拆成了五条用例母线电压从270V平缓降到260V系统必须触发降额。母线电压从260V突然跳到250V模拟瞬间掉电降额必须在10ms内生效。母线电压从255V回升到265V系统需要解除降额并且解除时不能产生扭矩跳变。母线电压在260V附近以1V幅值、5Hz频率振荡看降额标志是不是跟着高频抖振。母线电压缺失NaN信号时系统按最保守策略进入降额并闭锁。你可能注意到了我把降额生效时间这种时序要求单独立了一条用例。为什么因为很多时候模型逻辑是正确的但响应延时超标。比如一个低通滤波器把母线电压信号滤得太平滑了260V跌到255V时滤波器输出还没到阈值等到输出低于阈值时已经过了50ms。这种问题是只看稳态波形看不出来的必须设计专门的时序用例。在Simulink Test里写这种时序判据很直接给降额标志信号加一个从下降到触发的事件检测再用follow逻辑看触发时间和电压跌落时间之间的差值。跑下来确实发现过一版模型因为滤波器时间常数配得太大降额触发延时到了25ms远超需求的10ms。后来把滤波器截止频率提高重新跑这条用例才通过。这就是MIL测试的价值——所有问题都发生在模型还只是一堆方块连线的阶段改起来成本极低。3.3 我在实际项目中遇到的MIL翻车现场很多坑不是模型算法本身不对而是测试环境、测试数据或测试方法出了问题。我在这里分享三个印象最深的现场。第一个是初始化残留问题。当时测一个状态机模型它进去之后会记住当前处于自动模式还是手动模式。前一版用例跑到自动模式结束我没有显式重置模型初始状态下一版用例一跑显示初始状态被设定成了自动模式结果第二版用例在手动模式下允许操作的判断上直接误报失败。排查了半天最后发现是Simulink缓存的初始状态没清。从那以后我所有MIL回归脚本第一行必然先执行clear all级别的工作区清空并且在每个SimulationInput里显式指定InitialState。第二个是用了连续模块导致判据忽红忽绿。被测模型里有一个RC滤波环节是连续时间模块我用的是变步长连续求解器。问题在于变步长在不同的用例里自适应步长不同导致滤波输出在高频信号段有微小差异而判据又把阈值写死了。最后统一改成离散滤波器并把求解器设成定步长问题才消失。要记住MIL测试环境的求解器配置必须全局统一否则同一套判据在不同用例间的数值可比性很差。第三个是信号类型不一致导致的静默错误。有一次模型接收的是single类型信号测试激励里给的是double类型Simulink里自动做了类型转换也没报错但转换时出现了精度损失导致一个边界值用例在阈值附近忽长忽短跑十次能有两种结果。后来我在测试脚本里加了类型检查凡是激励端口数据类和被测端口不一致就直接报警不再静默转换。这些坑单看都不是什么高深问题但在实际项目中它们会一遍遍地消耗你的调试时间。我把它们写出来就是希望你搭MIL环境时直接把这三件事做在前面——模型初始状态统一重置、求解器和采样方式全局固定、数据类型显式检查能帮你省下好几周的踩坑时间。4. MIL与SIL、HIL三层测试到底怎么配合4.1 各查各的问题模型、代码、硬件三层职责划分做嵌入式控制开发的人应该都听过MIL、SIL、HIL这三个词。很多人把它们当成三种不同的测试工具选一个用就行。其实它们解决的问题完全不同是一套组合拳。MIL查的是算法模型本身对不对它验证的是你在Simulink里画的那些逻辑块、状态机、查表模块组合在一起是不是符合需求。SILSoftware-in-the-Loop查的是生成的代码和模型是否一致被测对象从模型变成了Embedded Coder生成的C代码它验证的是代码生成、编译、配置过程有没有把模型语义改歪。HILHardware-in-the-Loop查的是代码跑到目标芯片上、和外部传感器/执行器交互时是否正常被测对象是真实ECU或控制器硬件它验证的是驱动、中断、总线通信、I/O映射这些模型和代码层面碰不到的问题。举一个更具体的例子电机控制中一个死区补偿的逻辑。MIL测试确认了逻辑本身正确期望电压加上死区时间对应的补偿量输出脉宽符合预期。SIL测试确认了生成的C代码执行同样的计算逻辑没有出现整型溢出、定标误差。HIL测试确认了在真实MCU上中断周期稳定PWM模块配置出来的波形和软件里算出来的占空比一致。三个测试分别在论文阶段、软件阶段和硬件阶段堵住了不同类型的问题缺一不可。4.2 从MIL到SIL的过渡步长、初值、类型转换三座山要搬掉做SIL测试最常见的方法是用Embedded Coder把模型生成C代码然后放到一个SIL仿真模式下运行输入输出和MIL保持相同。听起来很简单但实际上从MIL到SIL会遇到三个非常经典的问题。第一个是仿真步长和求解器类型变化。很多模型在MIL阶段用的是定步长离散求解器进了代码生成默认会保留这个配置。但如果你模型里混有连续模块生成代码后这些连续模块会被转换为等价的数值积分函数积分步长和精度行为会和Simulink仿真有微妙差异。比如一个PID控制器的微分项在Simulink里用的是较高精度的积分算法编译到嵌入式环境后代码生成的离散化积分器会退化成最简单的欧拉法PID的微分响应可能就不一样了。所以做MIL测试时尽量让模型直接采用代码生成支持的离散化架构别留太多只在仿真环境下能跑的连续模块。第二个是初始状态的传递。MIL测试里你可以在SimulationInput里随便设置初始状态但SIL测试时这些初始状态能不能传到生成的代码结构体里又是另一回事。我通常的做法是做一个函数级初始化封装模型提供一个Init函数所有工作区变量和状态都在这个函数里赋初值。MIL和SIL测试都显式调用这个Init函数从源头上保证两边初始状态一致。第三个是数据类和精度差异。模型里某条总线信号在MIL中是double生成代码后在结构体里可能变成single甚至uint16取决于你设置的数据字典。有些模型在Simulink里double精度下一切正常到了single精度下数值误差就开始累积了。我在SIL对比测试中就遇到过累积误差超过判据上限的情况最后把模型中积分环节的精度从single改成double才过关。这个类型问题在MIL阶段就要提前扫描一遍别拖到SIL再查。4.3 MIL回归在开发中的节奏不是跑一次就万事大吉MIL测试不是一次性工作。一个靠谱的开发流程里MIL测试至少会在三个节点各跑一轮。第一轮是模型初版完成后叫作基线回归。这时候的目的是验证模型功能和需求一致用例要覆盖全部功能需求这轮通过后模型才算能拿得出手。第二轮是每次模型变更后叫作增量回归。改动一个参数、加一个条件、改一个状态都要把受影响的用例重新跑一遍。这里有个经验法则每一条模型变更记录对应一条或一小组回归用例不要每次都全量跑也不要直觉猜这个改动不影响其他功能就不跑。我在实际中经常看到改了一个查表的一端数据结果阶梯函数在另一端产生了映射错误。所以增量回归的范围宁可大一点也不要漏测。第三轮是在代码生成之前叫作冻结前全量回归。所有用例、全部覆盖度指标全部跑一遍并留存报告。这是MIL测试工作的收官之后模型进入代码生成阶段原则上不允许再对算法逻辑做任何修改。如果逼不得已必须改那就要重新走需求-用例-用例更新-全量回归的完整流程很多项目为了这一个改动可能要付出两天的代价。这也是为什么我一直强调做MIL测试时用例一定要写得和数据、脚本分离让更新用例本身变得足够快。5. 覆盖率、自动化和团队协作MIL测试落地的最后一公里5.1 覆盖率度量别只盯着100%要盯着哪些没测到MIL测试做完了需求也都验过了但总觉得少了点什么。这时候需要看覆盖率。Simulink里自带的覆盖率分析工具能统计模型里的决策覆盖率、条件覆盖率、MCDC覆盖率等指标。对功能安全要求高的项目一般要求决策覆盖率达到100%MCDC覆盖率按不同ASIL等级要90%到100%不等。我刚开始做MIL时傻傻地盯着决策覆盖率100%这个目标跑去结果发现就算把需求相关的测试用例全跑完覆盖率也到不了100%因为模型里有一些防御性逻辑比如输入幅值大于10000就报错这种分支需求文档里根本不会写自然没有对应测试用例。硬凑100%只能被迫加一堆莫名其妙的用例测试价值很低。后来我调整了思路。覆盖率在MIL测试里不是目标而是探索工具。我先跑完需求用例看一眼覆盖率报告重点去分析那些没有被覆盖到的分支。如果某个分支是因为条件不可能发生比如母线电压1000V才会触发的保护那是合理的可以标记为不可达并在报告里说明理由。但如果某个分支理论上应该能触发却始终没触发那很可能意味着需求理解错了或者测试激励设计得不充分这才是我真正需要花时间的地方。5.2 通过脚本实现批量回归把MIL测试从手工活变成流水线当你把测试用例增加到几十条、上百条时在Simulink Test界面里一条条点Run是完全不现实的。正确做法是写一个回归脚本让它能做三件事加载测试信息、批量执行仿真、生成汇总报告。我用的是经典的Simulink.SimulationInput迭代方式。先把所有用例的输入数据、期望结果存成独立的MAT文件或Excel表格脚本遍历这些数据文件每一条都构建一个SimulationInput设置仿真参数然后调用sim执行。执行完的结果自动和期望结果对比生成一个数据结构最后用publish或自写的报告生成函数输出HTML或PDF格式的测试报告。整个回归流程不需要人手动打开Simulink界面在MATLAB命令行跑一条主函数就能完成。这种做法还有一个额外的好处就是能集成到CI流水线里。我们团队甚至用MATLAB Compiler把回归脚本编译成了可执行文件每天夜里自动跑一遍全量回归早上来公司先看测试报告有红的话再定位问题。这样模型的每次变更都留有自动化测试的结果记录几个月后翻出来还能知道哪天改坏了什么。5.3 团队协作中MIL测试落地的几个细节经验最后聊聊团队层面的事。MIL测试在技术层面不难难的是让一整个团队都愿意按这个流程来。我在落地过程中总结了三个关键细节。第一测试用例和数据文件必须纳入版本管理而且和模型文件分开管理。模型目录放模型测试目录放测试脚本、测试数据、报告模板。这样当两条需求同时变更时模型和测试用例可以分别在不同的分支上改合并时冲突也少。别把测试数据放在模型同一目录的slprj里那一堆自动生成的文件追版本能追到崩溃。第二看门狗一样守护测试基线。一旦某条用例在模型变更后失败了不要直接动手改模型或改用例先看变更前后差异确认是模型行为变化还是用例期望值需要更新。如果确认是需求变更导致预期结果必须变那么用例修改记录里要写上对应的需求变更编号这样审查时可以追查每一个用例调整的理由。第三Excel表格和脚本数据文件要规范化命名。我见过团队用新建文档(2).xlsx这种文件名管理测试数据一个月后没人敢动它。我的惯例是每条用例一个名字包含用例编号、被测功能、激励类型和目标结果关键词比如TC_042_TrqCmd_ABSActive_RampUp_SpeedChange_ExpectTqFallback.mat。文件一看就大概知道在测什么即使过了半年再回来接手也不需要问前同事。MIL测试做到这个程度才算真正在团队里站稳了脚跟。它不是某个人单独做的一个验证活动而是整个开发过程的一把尺子每一次模型改动都要被它量一量。我私下觉得做MIL测试最重要的品质倒不是多熟悉Simulink操作而是愿意把每一个应该没问题的时刻都转化成一个可验证的自动化用例。模型这次没出问题不等于下次不出问题但如果你已经为这个下次备好了回归脚本和判据那么问题一露头就能被抓住。这也是我一直坚持在项目里保留一套MIL自动化回归流程的根本原因。
返回列表