☰
Simulink Test Sequence实战:基于Excel用例的自动化MIL测试
2026/10/2 1:11:02 网站建设 项目流程

做模型在环(MIL)测试的工程师,尤其是跟汽车控制器、电源管理、机器人逻辑打过交道的,应该都有过这种体验:被测逻辑一多,模型里铺满If/Else模块和一大堆Signal Builder信号源,改一个分支条件要顺着线找半天,测完还得靠人工拉Scope看波形猜结果。后来我换了思路,把所有复杂逻辑测试都放到Simulink Test Sequence里跑,配合一张Excel模板做用例管理,效率和可维护性直接上了一个台阶。这篇实战文章,我会从测试用例怎么在Excel里设计、Test Sequence怎么一步步搭起来、底层运行机制是怎么回事,到常见坑和排查技巧,完整讲一遍。适用对象是已经在用Simulink做仿真、想提升测试规范度和自动化程度的工程师,哪怕之前完全没用过Test Sequence,照着这5步走也能跑通。

1. 为什么复杂逻辑测试要选Test Sequence而不是一堆If模块

1.1 传统测试方式的痛点

以前我在一家做新能源BMS控制器的项目里,碰到过一套高压上下电逻辑:上电要求挡位OK、绝缘检测OK、接触器反馈OK,三个条件同时满足才允许闭合主继电器,而且从发出命令到收到反馈超过2秒就要报超时故障。这种时序逻辑,如果用传统方式做测试,先得用Signal Builder或者Signal Editor去搭三路输入的波形,还得在合适的时刻让某一路信号变化,一团乱麻。更麻烦的是结果判定,大多数人只能拉Scope去肉眼对比,一个分支覆盖没覆盖到,完全凭感觉。

这套做法有几个典型问题:

  • 激励信号和时间轴强耦合,换一组参数就得重新拉波形,维护成本极高。
  • 逻辑分支多时,没有任何层次感,测试意图看不出来。
  • 没有自动断言机制,Pass还是Fail靠人看,回归测试基本是做样子。
  • 测试用例和需求没有关联,评审的时候你拿不出一张清晰的表来解释"我到底测了什么"。

1.2 Test Sequence到底解决了什么问题

Test Sequence这个块,本质上是用一种类状态机的Step方式,让你把"激励序列、时序条件、期望结果"用文本描述出来。每个Step可以写Condition(跳转条件)和Action(进入该步骤时执行的动作),步骤之间用Transition连接,表达力非常强。

它最能打的地方有三个:

第一,天然适合描述时序逻辑。什么时候开始等待,等待多久,哪个信号变化后触发下一步,写起来非常直观。比去拉波形不知道高到哪里去了。

第二,内置Pass/Fail判定能力。你可以在Test Sequence里定义failed信号,当某个Condition不满足时就置位,然后把这个failed输出接到Data Store或者日志里。配合Simulink Test的Assessment块,还能做断言验证。

第三,和Excel、脚本、Test Manager联动方便。测试用例可以在Excel里维护,通过MATLAB脚本批量生成参数变化,跑完还能自动写回结果。这是后面要讲的重头戏。

1.3 前置条件与运行环境准备

开始之前,先确认软件环境:

  • MATLAB R2019b以上版本最好,旧版本Test Sequence功能也能用,但编辑器体验差一些。
  • 必须装了Simulink和Stateflow,因为Test Sequence块是Stateflow框架里的东西,缺了它库浏览器里根本没有这个块。
  • 建议装Simulink Test工具箱,虽然不装也能用Test Sequence,但做了自动化测试报告、需求追溯这些高级功能还是要靠它。
  • 电脑内存建议16G以上,后面批量仿真会舒服很多。

2. 测试用例设计:Excel才是逻辑测试的真正源头

2.1 为什么从Excel开始

很多工程师上来就打开Simulink,在Test Sequence里写步骤,写完之后发现问题一堆。我的习惯正好反过来:先在Excel里把测试用例想清楚,再落到Test Sequence里执行。理由特别朴素。

需求评审会、测试评审会上,产品经理和项目经理不会打开你的Simulink模型,但所有人都会打开Excel。一张清晰的用例表,能让干系人快速确认你的测试覆盖了哪些需求、边界条件是什么、预期结果是否合理。而且Excel天然支持版本管理,发一个v1.2的用例表给同事,谁改了什么都清清楚楚。

2.2 Excel模板字段如何设计

以我刚才说的BMS上电逻辑为例,我常用的用例表字段是这样的:

字段说明示例
用例ID全局唯一TC_PWR_001
优先级高/中/低高
前置条件运行前模型状态系统下电,无故障
激励步骤对输入信号的具体操作GearOK=1, InsulOK=1
时序条件等待时间或信号变化after(1,sec) 且 RelayFb==0
期望输出模型输出应满足的条件RelayCmd==1
关联需求需求条款号SYS_REQ_102
备注补充说明正常上电路径

这里有个很重要的细节:Excel里的条件表达式,最好直接按Test Sequence的语法来写。比如期望输出列写RelayCmd == 1,真到了搭建模型那一天,你直接复制粘贴就能用。如果写成"继电器命令应该为闭合",到模型里你还得再翻译一遍,既容易错又浪费时间。

2.3 模板填写的三个铁律

先提醒一个Excel本身的大坑:如果单元格里写的是1,但你想表达的是文本"1",Excel会自动转成数字,更麻烦的是以=开通的字符串会被当成公式。处理办法是填表前先把这一列设置成文本格式,或者写的时候在内容前面加一个单引号。

填用例表,还有三个铁律:

  1. 激励和期望严格分离。激励列只管输入信号,期望列只管被测对象的输出,不要混在一起。混在一起的结果就是后面Test Sequence里写Action和断言的时候会打架。
  2. 一个步骤只做一件原子事。比如"把挡位置OK"是一步,"把绝缘检测置OK"是另一步,不要写成"挡位OK且绝缘OK"一步到位。只有拆细,时序错误才能定位到具体某一步。
  3. 每个用例都必须能追溯需求。写不出来关联需求的要求,说明这个用例还没有想清楚,不要进模板。

2.4 从Excel到Test Sequence的三种导入路径

不同项目规模,从Excel到测试工程的落地方式不一样,我给你列一下常见做法:

小项目/快速验证:人工照着Excel在Test Sequence里写Step。优点是快捷,缺点是Excel可能和模型脱节。适合一次性验证。

中规模项目:用MATLAB脚本把Excel读进来,结构体或者.mat文件丢进Model Workspace,Test Sequence的Condition和Action直接引用变量。比如Excel里配了所有测试阈值,脚本往基础工作区写一个param = struct(),Test Sequence里写mode == param.modeON。这样Excel改阈值,模型和测试逻辑都不用动。

大规模项目/合规要求高的项目:用Simulink Test Manager自带的Excel导入接口,按它的标准模板批量生成测试用例,再配合Test Sequence跑。这个路径最规范,能自动生成测试报告、覆盖率和需求追溯矩阵,但前期模板设计花费也大。

3. 五步搞定Test Sequence实战流程

3.1 第一步:搭一个被测模型,先明确接口和采样时间

为了演示,我们搭一个简化的"启动联锁"逻辑模型:输入有三路信号,StartReq(请求启动)、ReadyOK(系统就绪)、Fault(故障);模型内部做一个简单组合逻辑,输出RunCmd(运行指令)、DiagCode(诊断码)。这个模型你可以用基础模块搭,也可以直接用Stateflow写。关键是被测模型的输入输出端口要清晰。

搭模型的时候,有一个比端口更重要的坑必须提前排掉:求解器设置。建完模型第一件事,把求解器改成离散定步长,比如固定步长0.01秒。Test Sequence的时序逻辑在定步长下行为最可控,如果跑连续求解器,事件触发和采样时刻各种漂移,你会被整疯。

3.2 第二步:把Test Sequence块拖进模型并连接端口

在Simulink库浏览器的搜索框里直接搜"Test Sequence",把它拖到模型里。双击块打开编辑器,左侧Symbols窗格里能看到系统自动生成的输入输出端口,手动添加端口的话,点一下"Create Input"或者"Create Output",给它命名。

我见过一个特别普通的错误:把Test Sequence的输出端口类型设成了1x1的数组,被测模型的输入是标量,连接的时候报错。解决方法是配置端口时直接把Attributes里的Size改成1,或者让Test Sequence直接继承被连接信号的类型。

端口连完之后,从被测模型的输出拉两根线回到Test Sequence的新输入端,这样Test Sequence就能实时看到被测模型的输出了,这是后面做闭环断言的基础。

3.3 第三步:写第一个Step,理解Condition和Action

打开Test Sequence编辑器,默认会有一个Step,叫Step 1。我先把它改名成Init,然后在你想要的位置新建Step。每个Step都有两个关键区域:Condition和Action。

  • Condition:当前Step下持续评估的跳转条件,满足后离开当前Step,进入下一个连着的Step。
  • Action:在进入当前Step的那一个时刻执行一次的动作。默认情况下,它不是持续执行的,除非你手动把它配置成持续行为。

这个区分太重要了。网上很多人写Test Sequence翻车,多半都是把Action当成每个仿真步都会执行。比如Action里写runCmd = 1,你以为这个输出在整个Step期间都保持1,其实它只在进入Step那一下被赋值为1。如果后面没有新的赋值,输出会保持住,这个还好;但如果去了别的Step又回来,你得重新赋值。

初始化Step的Action可以这样写:

runCmd = 0; diagCode = uint8(0);

Condition区域写:

StartReq == 1 && ReadyOK == 1 && Fault == 0

意思很明确:初始化完成后,一旦启动请求、系统就绪、且无故障这三个条件同时满足,就进入下一个Step。

3.4 第四步:用Transition实现复杂分支与时序控制

Test Sequence里的步骤跳转是通过Transition箭头实现的。你可以在Step上右键创建Transition,拉到一个新的Step上。这里核心的语法是时序函数,最常用的有这几个:

函数含义示例
after(n, tick/sec)n个周期/秒之后才允许离开after(5, tick)表示5个采样周期后
before(n, tick/sec)必须在n个周期/秒之内离开,否则视为失败before(100, tick)超时就报错
duration(n, tick/sec)条件需要持续满足n个周期/秒才有效duration(2, sec)防抖
hasChanged(x)信号x在上一周期发生了变化hasChanged(Fault)

还是用BMS那个例子。启动请求发出后,我们需要等接触器反馈回来,但如果2秒内没等到反馈,必须报超时故障。这个逻辑在Test Sequence里这样写:

Step名字:WaitFb,Action里先发出闭合命令:

RelayCmd = 1;

Condition区域写两行:

  • 第一行:RelayFb == 1 && before(2, sec)— 反馈正常,且还没超时,跳去Run步骤。
  • 第二行:after(2, sec)— 已经过了2秒,直接跳去Fault步骤。

注意它们是有优先级的,从上往下评估。这样就把"正常路径"和"超时故障路径"分开了。这种写法比状态机还直观,因为它把人脑里的判断逻辑直接搬过来了。

3.5 第五步:接入Excel数据,把自动化跑起来

到了这一步,模型里已经能用Test Sequence跑通一条用例。但真正的价值在于:把Excel里的用例批量导进来,自动跑,自动判结果。

我用得最顺的方式是这样的:Excel里的"期望输出"列,每跑一条用例,把仿真输出结果和期望值对比,然后把Pass/Fail写回Excel。参考脚本如下:

% 读取Excel用例表 T = readtable('test_cases.xlsx', 'Sheet', '上电时序用例'); mdl = 'startup_interlock'; % 打开模型 load_system(mdl); % 批量仿真的输入容器 in(1:height(T)) = Simulink.SimulationInput(mdl); % 循环给每条用例设置输入参数 for k = 1:height(T) in(k) = in(k).setVariable('StartReq', T.StartReq(k)); in(k) = in(k).setVariable('ReadyOK', T.ReadyOK(k)); in(k) = in(k).setVariable('Fault', T.Fault(k)); end % 并行或串行跑仿真 out = parsim(in, 'ShowProgress', 'on'); % 提取结果,和期望做对比 for k = 1:height(T) logs = out(k).logsout; % 从logsout里取RunCmd最终值 runCmdFinal = logs.get('RunCmd').Values.Data(end); % 和Excel期望对比 if runCmdFinal == T.ExpectRunCmd(k) T.Result{k} = 'PASS'; else T.Result{k} = 'FAIL'; end end % 把结果写回Excel writetable(T, 'test_results.xlsx');

注意我用setVariable设置的是工作区变量,所以被测模型里那些输入得用基本工作区或数据字典里的变量,而不是硬编码常数。这个习惯从搭模型的时候就该养成——所有输入参数都用变量,不要用Constant模块写死。否则Excel根本没法驱动你的仿真。

关于结果判定,还有一条进阶路径:不需要在脚本里手动对比。可以直接在Test Sequence里内置断言,用failed输出表示失败,然后用out(k).logsout去读这个failed信号。这样脚本只管跑,判定的逻辑留在Test Sequence里,更符合"测试逻辑跟着测试走"的工程原则。

3.6 操作过程中的几个高效细节

用Test Sequence编辑器多了,有几个体验细节值得提一下:

  • 右侧Symbols窗格里,右键本地变量可以方便地转成输入、输出或者参数,在表达式中引用时也会自动补全,写多了手指能飞起来。
  • 写完逻辑后,按Ctrl+D做模型一致性检查,Minimal mismatch的提示能挡住不少低级错误。
  • 新建Step时,回车键可以直接在当前Step下方加新Step;在Step上右键加Transition也非常顺手。
  • 所有Condition和Action都支持MATLAB语法,包括&&、||、uint8()这类类型转换。这个特性建议好好利用,比如诊断码用枚举或整数类型,就能在Test Sequence里直接做类型安全的判断。

4. 解析原理:Test Sequence运行机制和几个关键概念

4.1 不是状态机,但有状态机的心

Test Sequence底层的执行机制和Stateflow非常像,每个Step相当于状态,Transition相当于状态迁移。你说它是状态机吧,它又专门为测试做了优化,比如你可以很方便地在任意Step里记录时间、生成外部激励、甚至通过get_param去干预模型。

但核心认知必须纠正过来:Step本身不主动干活,它只是在特定时刻帮你执行Action,并且不断评估Condition,决定下一步往哪走。所以写测试序列的思路是"我应该在哪个时刻做什么动作、期待什么现象",而不是"我要让模型进入什么状态"。

4.2 时序函数怎么组合才不出错

在Test Sequence里,时间相关的函数是最容易用错的。很多人写after(5, tick)以为是从模型开始仿真算5个周期,其实是当前Step被激活开始,等待5个周期。如果你在上一个Step里等了很久,跳到这里面来,after是从进入这个新Step的时刻才开始计时的。

这个"相对时间"的语义,做复杂时序的时候特别需要心知肚明。我见过一个同事写电梯门超时保护逻辑,他把开门等待的Step放在前面,后面又写了个after(30, sec)的关门超时,结果测试一直失败,就是因为他没意识到after是从新Step的激活时刻开始算的,而不是从模型启动开始算。

再比如duration的使用场景:接触器反馈信号可能存在抖动,你希望信号稳定1秒后才认为有效。此时完整的写法是:

duration(1, sec) && RelayFb == 1

看清楚了,信号条件必须和duration同时成立,它表示"满足1秒"的意思,这个才叫防抖。

4.3 数据类型的匹配和广播问题

Test Sequence的输入输出端口可以是标量、向量、矩阵,甚至总线。端口数据维度必须和被连接的信号匹配。一个经常出问题的场景是:Test Sequence输出了一个列向量1x3,被测模型输入期望的是3x1行向量,连接后Simulink会直接报维度不匹配。

还有一个隐藏陷阱:如果Test Sequence连的是总线信号,你必须在Symbols窗格里把端口类型设为对应的总线类型,否则编译都过不去。遇到总线问题时,最快的排查方向就是先查端口的Attributes配置。

4.4 为什么不要把所有常量写死在Test Sequence里

Test Sequence里写数字越少越好。比如阈值0.5、超时时间2秒这种,最好都定义成parameter(在Symbols窗格里把数据类型改成Parameter),这样做的直接原因是:

  • 需求变更时,改一个参数比在十几个Step里搜数字靠谱得多。
  • 模型数据字典(Data Dictionary)管理参数之后,Excel里的测试参数可以直接映射到参数对象,批量赋值极其方便。
  • 代码评审时,参数名比魔法数字有信息量得多。timeout == 2和timeout == param.contactFbTimeout,哪个好一目了然。

我甚至见过更极端的方案:整个测试序列的Condition和Action里面全用参数变量,Step结构里几乎看不到数字,所有数值都来自Excel。这种方案初期搭建慢,但后期维护成本极低,非常适合需要长期回归测试的控制器项目。

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

5.1 高频问题速查表

用Test Sequence跑了三四年,我把踩过的坑和别人的翻车现场整理成了一张表,可以先收藏再慢慢品:

现象典型原因处理方式
Step一直停在原地不跳转Transition条件不满足,或写成了after但等待时间没到在Condition加hasChanged日志输出,或者临时把条件改成after(1,tick)试跳转
Action里的赋值没生效把Action当持续执行用了,但实际只在进入Step时执行确认是否需要把该赋值写到Condition区的side-effect里,或者新建一个持续赋值的Step
测试假PASS,一堆用例绿得发亮期望值写成恒真,比如`req==0
仿真速度极慢定步长太小,例如0.0001,或者开了很多界面动画把步长放到0.01级别,关闭动画显示,用parsim并行加速
输出端口类型不匹配数组和标量混用,总线类型不对在Symbols窗格里把Attributes配置成匹配类型,再不行就加Data Type Conversion块
Excel里的数字读不到用例表某一列被Excel格式化成了日期/文本在Excel里把该列设为数字或文本,别让它自作主张变日期
Test Sequence块无法编译依赖Stateflow许可或版本不支持某些API确认有没有Stateflow工具箱,换上R2019b以上版本

5.2 最致命的假阳性问题

用了Test Sequence之后,自动化程度高了,出现另一种更隐蔽的风险:测试结果是绿的,但不是因为逻辑对了,而是因为你的期望条件根本就是个恒真命题。

有一次系统联调,我看整个测试报告全PASS,当时就警惕起来了。后来排查才发现,Test Sequence里有个条件写成了Fault == 0 || Fault == 1,等于没判断。这个教训让我养成了一个"反证习惯":每写完一条测试用例,我会故意把模型里的逻辑改反一次,跑一遍看它会不会FAIL。如果怎么改都是PASS,那就是期望条件写废了,得赶紧修正。这一步非常推荐,成本低、效果奇好。

5.3 跑大批量用例时的性能优化

当你的Excel用例表有几百条记录,一次跑完的时间就成了大问题。我实际项目里用过的优化组合拳:

  • 定步长从0.001放宽到0.01,如果被测逻辑对仿真精度不敏感,速度能提升近10倍。
  • 打开Fast Restart,避免重复初始化模型。
  • 用parsim并行跑,把每个用例分到不同的worker上。
  • 关闭Simulink的动画效果,把诊断级别降到只报错误,省掉一堆信息刷屏。

注意parsim虽然好用,但前提是每个Simulink.SimulationInput之间不能有共享变量冲突。所以我一般不建议在Test Sequence里直接写evalin('base','x=1')这类代码,会让并行仿真很难做。规规矩矩用setVariable传递参数,才能安全并行。

5.4 从Test Sequence迈向自动化测试工程

Test Sequence单块玩得再溜,不跟工程体系结合,价值也有限。我的建议是往这三个方向扩展:

  1. 需求追溯:把Excel用例表里关联需求那一列接上Simulink Requirements工具箱,跑完测试直接生成需求覆盖矩阵。这对写测试报告和过评审非常有说服力。
  2. 回归测试:每次模型改动后,用脚本一键跑全量用例,跟上次结果做对比。Test Sequence条件稳定的话,回归几乎不花人工成本。
  3. 公司内部的测试规范:把Excel模板定为需求分析阶段必须交付的工件,Test Sequence作为执行载体。这样测试的源头是文档化的Excel,而不是某个人脑子里的逻辑。

在我个人看来,学Test Sequence最忌讳把目光锁死在编辑器里那几行步骤上。真正值钱的地方在于,搞清楚测试数据和测试逻辑怎么分离、怎么批量跑、怎么让别人也能看懂你的测试。我踩过无数次测试用例和模型脱节的坑之后,现在的做法永远是:Excel管用例,Test Sequence管执行,Simulink Test管判定和报告,脚本管批量和回归。三条线各干各的,再用一个统一模板串起来。

最后分享一个小技巧:哪怕是小项目,也值得预留一个"用脚本批量跑结果写回Excel"的口子,哪怕一开始只跑三条用例。等哪天真要回归测试的时候,你会发现当初只花了十几分钟做的这个接口,帮你省下了一整天的痛苦。

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

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

立即咨询