做了几年基于模型的开发MBD我早年的工作方式其实挺原始打开Simulink手改模型手动拖测试用例跑完看波形再花半小时整理报告。直到有一次一个改动很小的分支在主干上引起了回归而我发现问题的时间已经比出错的那天晚了整整三天。那一次之后我开始认真思考一个问题Simulink模型能不能像C代码一样每次提交都自动接受一套固定的检查、仿真和代码生成验证答案是可以的而且一旦跑通整个团队的交付节奏会是另一种状态。这篇文章是我在一款实际控制器软件项目中把Simulink模型接入持续集成流水线的完整记录。我会从工具链选型、流水线分级、关键环节实现到常见坑位排查尽量把能直接“抄作业”的东西都写出来。适合正在做MBD的建模工程师、对CI有需求的嵌入式软件开发团队以及要和功能安全打交道的同学参考。1. MBD场景下持续集成到底在解决什么问题1.1 模型不是代码传统CI无法直接套用持续集成的老套路大家都熟代码提交后自动拉分支、跑编译、跑单测、出报告。这背后依赖的是“文本可diff、编译可重复、单测门槛清晰”这三个基础。可一旦对象换成Simulink的.slx模型这三个基础都会有点松动。首先是diff问题。文本代码改一行git里清清楚楚.slx本质上是压缩包里面是XML、二进制数据和图片资源的组合大部分团队根本没法对两个模型文件做精细的三方合并。和一个同事同时改了同一个模型的不同子系统最后谁后保存谁就覆盖了谁冲突提示还看不懂。这是MBD协作里最让人头疼的一点不是工具不够好而是对象本身的格式决定了它没法像代码那样被轻易比较。其次是环境依赖。一个模型能跑出正确结果依赖的可不只是“保存的框图”。求解器类型、步长、采样时间、工作区变量、数据字典、回调函数、依赖的自定义库任何一个环节不对仿真结果就完全不一样。这些依赖常常不会被人在提交信息里主动说明你拉下来一跑模型能打开但仿真结果和提交人说好的完全对不上。再就是每次跑批的成本。代码项目编译一次可能几十秒到几分钟Simulink要启动一个完整的MATLAB进程模型加载、编译图、仿真动辄十分钟以上。这种成本放在传统CI里是难以想象的但反过来也说明了一个事实正因为模型验证成本高、手动容易漏才更需要把能机器验证的环节全部自动化让它成为每天回归的底线。MBD的CI不是要把人替换掉而是把“确定性的、重复的、容易漏的”检查变成机器的职责让人的精力集中在模型结构设计、算法逻辑和评审这些真正需要判断力的事情上。1.2 一条MBD CI流水线应该覆盖的五个环节在我的经验里一条完整的MBD CI流水线至少要覆盖下面五个环节。这五环不一定要全部塞进每次提交的门禁但要确保它们在主干上一定会被执行。环节常用手段主要目的模型静态检查Model Advisor、Simulink Check、MAAB规范、自定义命名规则拦截模型结构、命名、代数环等规范问题模型在环测试MILSimulink Test 测试用例验证算法逻辑在模型层面的正确性自动代码生成Embedded Coder、ERT目标、slbuild验证模型可生成、可编译为C代码软件在环测试SIL生成代码编译为独立程序后重跑用例验证模型与生成代码行为一致覆盖率与报告归档Simulink Coverage、测试报告导出功能安全审计、质量门禁、回归分析这五个环节是环环递进的。静态检查保证“模型长得标准”MIL保证“模型逻辑对”代码生成保证“模型能落地”SIL保证“落地的代码和模型没跑偏”覆盖率保证“测试没有漏关键路径”。任何一环崩了都说明“这次改动不该合进主干”。但要注意这五个环节的执行成本差异极大。静态检查最快可能1到2分钟MIL测试要看用例量快则几分钟慢则几十分钟代码生成加SIL又是十几分钟起步。如果无视成本全部塞进一个门禁MR要等待的时间会非常长最后大家很容易选择绕过流水线。所以我带项目时会把流水线按触发时机分成三层来设计这个后面专门说。2. 工具链与环境搭建让MATLAB在服务器里“无头”奔跑2.1 许可证与运行方式选择在CI里跑MATLAB绕不开的第一个坎就是许可证。MATLAB是商业授权软件不可能像开源编译链那样随便并发。我们的做法是使用网络浮动许可也就是常说的license server。具体来说CI节点上的MATLAB启动时会通过环境变量去服务器租界一个可用授权export MLM_LICENSE_FILE27000172.16.10.20job跑完后授权自动释放。听起来简单实际有几点很关键一是授权数量直接决定CI并发上限。团队办公坐席本来就要占用一部分授权如果CI任务再去抢两边都受影响。我们当时专门单独申请了一个CI专用授权池只有3个并发额度所以流水线并发数被刻意压得很低用排队换稳定。二是Matlab进程一旦“死而不退”授权就会被长时间占住。这种情况我在Windows agent上遇到过多次最常见的肇事者是脚本里有uiwait、waitforbuttonpress这类GUI阻塞函数或者异常分支没有显式退出。后文生命周期管理部分会再提。三是不要图省事把CI做成每个模型一个job。MATLAB启动就需要几十秒甚至几分钟job拆得太碎每个都在重复启动、加载、释放许可证成本非常难看。宁可一个大job里按顺序跑多阶段任务也不要一个阶段一个job。2.2 用Docker跑MATLAB去掉环境相关的大量破事如果说许可证是“能不能跑起来”的问题那运行环境就是“跑起来是不是每次都一样”的问题。我在这上面吃过亏团队有的人笔记本上装R2021bCI agent上装了R2022a模型一打开就是一堆模型升级提示偏偏升级后的参数差一点仿真结果就压着容差边界过。所以强烈建议直接使用MATLAB官方提供的Docker镜像按版本打tag比如matlab:r2023b。镜像里包含基础的MATLAB和Simulink以及若干常用工具箱启动之后就是一个干净、确定的MATLAB环境。在GitLab CI里整个job可以写成这样image: matlab:r2023b variables: MLM_LICENSE_FILE: 27000172.16.10.20 MW_CRASH_MODE: 1 mci-static-and-mil: stage: test script: - matlab -batch run(ci/pipeline.m) -logfile matlab_ci.log artifacts: paths: - reports/ - matlab_ci.log when: alwaysMW_CRASH_MODE1这个环境变量值得单独点一下。它在CI里能禁用MATLAB崩溃时的弹窗和对话框避免构建卡在无人点击的窗口上直接等到超时。很多团队第一次跑CI就莫名其妙挂在某一步一查就是这个。还有个现实细节一些公司内外网隔离CI runner访问不了外网镜像仓库。我的做法是把官方镜像拉到有网络权限的机器上再推送到内网私有镜像仓库CI job里直接用内网镜像地址。别嫌麻烦镜像一次性导入后续版本升级才会再折腾一次。镜像解决了环境一致性问题但里面没有许可证。所以容器跑起来的时候必须把MLM_LICENSE_FILE传进去这就是上面YAML里变量区那一行的作用。2.3 命令行之必须matlab -batch 的正确姿势MATLAB从R2019b开始支持-batch模式这基本就是为CI场景设计的。老式的写法是matlab -nodisplay -nosplash -r run(ci/run_all.m)这种写法有两个老坑一是-r执行完后MATLAB不会自动退出必须在脚本最后补exit漏一次CI就挂到超时二是一旦脚本中途抛异常进程返回状态和日志容易让人看不明白到底挂没挂成。-batch模式就好很多自动启动MATLAB不显示桌面运行指定命令或脚本脚本执行完后自动退出如果脚本抛异常MATLAB返回非零退出码CI系统天然能识别失败一个典型用法matlab -batch run(ci/pipeline.m) -logfile matlab_ci.log注意-logfile参数MATLAB会把控制台输出同时写到文件里这样CI网页上既能看输出也能在下游把日志作为artifact存起来。遇到大数据量的输出还能避免页面渲染卡顿。我也习惯在pipeline.m最外层做一次try/catch把关键阶段耗时和最终状态汇总打出来function pipeline() totalStart tic; try run(ci/static_checks.m); run(ci/mil_tests.m); run(ci/codegen_build.m); fprintf(PIPELINE_OK, total %.1f s\n, toc(totalStart)); catch e fprintf(2, PIPELINE_FAILED: %s\n, e.message); for k 1:numel(e.stack) fprintf(2, at %s line %d\n, e.stack(k).name, e.stack(k).line); end exit(1); end end这里用fprintf(2, ...)把错误打到stderrCI日志里能直接看到。exit(1)放在catch里确保失败时返回码明确。说句实话-batch模式下漏了这个也能返回非零码但加上这个结构以后日志一眼就能定位是哪一步挂的后面排查会省很多事。提示如果出现“模型能打开但仿真报错”多数问题出在工具箱许可缺失或模型依赖路径不对。镜像里装了哪些工具箱一定程度上要靠前期梳理建议先在本机把项目需要的工具箱列表列清楚再定制镜像。3. 流水线详细设计从MR门禁到主干回归的分层策略3.1 为什么要分三层本地/MR/主干很多MBD团队第一次搭CI最容易犯的错误是“用力过猛”。静态检查、全量MIL、代码生成、SIL、覆盖率全塞在一个MR流水线里跑一次四十分钟以上一天有二十次提交流水线永远在排队开发者急着合代码又过不了门禁最后都开始“绕过规则”。我在项目里采用的是三层结构层级触发时机内容时长目标本地自测开发者提交前手动跑模型打开检查、关键用例仿真30分钟以内MR门禁每次发起Merge Request静态检查、命名规范、快速MIL回归10分钟左右主干流水线合并到主干后全量MIL、代码生成、SIL、覆盖率、报告归档1小时左右核心逻辑是反馈越快的门禁放越便宜的检查反馈越慢的门禁放越重的保障。MR门禁是给开发者“快糙猛”的体检主干流水线才是“全面体检档案留存”。有人在MR门禁里也想跑全量用例理由是“不跑全量合进去怕出事”。但主干流水线还在啊MR阶段漏掉的主干会兜底。而且主干流水线失败后可以自动回滚合并、自动通知作者这套机制比把MR时间拖到天荒地老要健康得多。3.2 MR门禁里放什么静态检查与快速MIL测试MR门禁的第一关是静态检查执行速度最快、问题最可解释。Simulink里的Model Advisor是一个规则引擎本质上是对模型图拓扑做一次“代码体检”能查代数环、信号属性冲突、死分支、可读性隐患等一大堆问题。团队还可以把MAAB规范配进去变成自动门禁而不是靠评审时人眼去盯命名。常见的调用方式是这样的mdl VehicleController; md ModelAdvisor.getModelAdvisor(mdl); md.TaskFilters {MAAB}; res md.RunTasks(); md.saveAllResults();细节不同版本会有差异但逻辑不变加载模型执行指定规则组收集结果并导出。CI里只要发现有“需修复”或“高危”级别的违规就让job失败普通提示级别的违规不阻断但写进报告供评审人查看。Model Advisor之外我还习惯加一组自定义命名检查脚本。比如模块名必须用in_/out_前缀、Goto和From标签必须一一配对、不允许未连线端口之类。这些规则用MATLAB脚本遍历模型对象就能实现% 伪代码示例检查未连线端口 blks find_system(mdl, SearchDepth, 2, BlockType, Inport); for k 1:numel(blks) port get_param(blks{k}, PortHandles); if isempty(port.Outport(1).Line) error(Inport %s has no connected line, blks{k}); end endMR门禁的第二关是快速MIL回归。全量用例不动只跑一个提前划好的“快速集”。这个快速集怎么选是有讲究的不是随便挑几个用例就算完。我们的做法是按影响面动态选择先用git diff拿本次变更的模型文件清单映射到对应子系统再选择覆盖这些子系统的关键用例。如果映射不到子系统就退回到最常用的冒烟用例集。这套逻辑用MATLAB脚本配合git diff命令就能实现[~, out] system(git diff --name-only HEAD~1 HEAD); changedFiles strsplit(strtrim(out));当然在MR门禁阶段不可能对全模型做穷尽验证但“改动相关的关键用例能跑通”已经能拦住大部分低级回归。全量的事情交给主干。3.3 主干回归怎么跑代码生成、覆盖率、SIL对照验证主干流水线是MBD CI里最精华的部分因为它真正把“模型是源产物”这个概念落实了。模型不只要仿真通过还要能生成代码、生成出来的代码还要能通过同样的验证。代码生成这一步我用的是Embedded Coder配合ERT目标。命令行里一句slbuild就能完成slbuild(VehicleController);这一步会做两件事一是从模型生成C代码二是调用配置好的编译工具链生成目标产物。生成目录下会有对应的makefileCI里可以进一步手动编译和链接(cd VehicleController_ert_rtw make -f VehicleController.mk)如果代码生成这一步都过不了说明模型里有Simulink无法落地的构造这种问题留在模型阶段销毁是最便宜的。SIL测试的核心目的是验证“模型仿真结果”和“生成代码跑出来的结果”是否一致。我把MIL阶段跑过的测试用例用Simulink Test在SIL模式下重新执行再把两组仿真输出信号做对齐比较m load(mil_result.mat); s load(sil_result.mat); diffMax max(abs(m.signal - s.signal)); if diffMax 1e-4 error(MIL/SIL mismatch, max diff %.6f, diffMax); end这个容差要按信号性质去设我们项目里用的是绝对误差1e-4配合允许的采样时间偏移判断起来不慢也不误杀。覆盖率这块Simulink Coverage提供决策覆盖率、条件覆盖率、MCDC等指标。对功能安全要求高的项目MCDC覆盖率会被作为强门禁。我常见的做法是把覆盖率配置写在测试管理器里跑完全量后读出覆盖率结果低于阈值直接失败results sltest.testmanager.run; cov results.getCoverageResults(); dc cov.DecisionCoverage.Executed / cov.DecisionCoverage.Total; if dc 0.99 error(Decision coverage too low: %.2f%%, dc * 100); endHIL这种需要接真实硬件、负载箱和故障注入单元的测试我一般不放进常规CI里。一个原因是硬件资源有限并发跑不起来另一个原因是CI要的是快速稳定而HIL对环境依赖太大适合作为独立执行的周更任务或发布前验证。SIL才是常规流水线里该有的“准硬件验证”它足够快又能覆盖绝大多数模型到代码的落地问题。4. 常见问题与排查技巧实录4.1 模型合并冲突与Review策略这是MBD协作里绕不开的痛。前面说过.slx文件不是纯文本git默认情况下不能做细粒度合并。我们的应对方案是组合拳。第一招把大模型按功能拆成多个引用模型Model Reference。每个子系统独立成一个.slx文件大家改动尽量落在不同文件里从源头减少冲突。一个控制器模型拆出十几个引用模型多人并行时体验会好非常多而且每个子模型还能单独构建、缓存主模型编译时直接复用速度也快不少。第二招开启gitattributes让模型文件在diff时导出为XML虽然不能直接做三方合并但至少能在评审时看到变量级变化*.slx diffslxmerge第三招模型间比较用Simulink自带的模型比较工具命令行里可以直接打开visdiff(VehicleController_old.slx, VehicleController_new.slx);它会把两个模型的差异按层级列出来评审人能看到新增了哪个block、改了哪个参数而不只是“二进制文件变了”。评审流程上我们还有个约定每次MR描述里必须写明接口变更说明比如“新增输入信号BrakePressure类型uint16删除输出TorqueLimit原逻辑迁移至Xxx子系统”。这个说明对代码评审来说可能显得笨拙但对模型评审来说特别关键能有效防止“看起来没冲突实际上总线接口已经变了”的隐藏雷。4.2 许可证排队与并发问题CI里最典型的许可证病况是job卡在启动阶段日志里出现License Manager相关的错误或长时间无响应。排查时我会先看授权服务器侧的状态lmstat -a -c 27000172.16.10.20能直接看到当前授权被哪些主机占用以及剩余额度。只要看到CI agent的条目长期挂着但对应的MATLAB进程早没了基本可以断定是license没能释放。这种情况的根因八成是脚本还没完整退出或者前一个job异常后没有正确地结束MATLAB进程。我们的对策有三个一是给CI单独划分授权池不让CI和办公坐席抢授权。数量再少都行关键是互不影响。二是调整并发策略将CI并发上限控制在授权池可用的60%到70%。留一点余量给瞬时启动和压测任务避免多job同时抢授权造成“雪花崩”。三是把单个job的任务尽量串行减少同时启动多个MATLAB进程。自动化测试用例多的时候可以考虑parfor做用例级并行但前提是授权池够用否则反而会因为争抢授权而整体变慢。4.3 慢测试、超时与flaky测试的处理Simulink测试的慢是有名的一个用例跑几分钟很常见全量跑下来一小时也不意外。为了不让流水线被“长尾用例”拖垮我在测试配置里强制给每个用例设置超时。副作用是如果用例超时了流水线会拦下一份超时报告而不是整个job挂到平台级超时。这个差异经历过的人都会懂。清零和固定随机源这两件事是我在每个测试脚本开头必做的bdclose(all); sltest.testmanager.clear(); clear classes; rng(default);作用分别是关闭所有已打开的模型避免模型锁冲突清空测试管理器确保上次遗留用例不影响这次清掉类定义防止被改动的.m脚本继续用旧版本运行固定随机数序列保证可复现。flaky测试里我踩得最深的一个坑是曾经有个用例单独跑永远通过但跟着前面的用例一起跑就必挂。查了一天根因是前面一个用例在同一个MATLAB进程里修改了一个模型回调函数引用的.m文件我的用例加载模型时拿到了旧缓存。清理脚本一加问题再没出现过。模型引用组件的增量构建也值得单独说。主干流水线第一次全量构建可能很慢但开起引用的增量构建后只有改动过的子模型会重新构建和编译其他部分走缓存整个流水线耗时能降到原来的三分之一左右。这个优化在模型量大的时候非常明显是把MBD CI跑通的关键开关之一。4.4 模块版本兼容问题MATLAB基本上每年一个大版本模型格式并不是全向前兼容的低版本打不开高版本的模型是常态。我们团队的做法是把建模版本统一锁定比如全team固定R2023bCI镜像也固定用R2023b。git提交说明里强制标注模型版本合并时检查是否有人手滑用别的版本保存过。真需要升级工具版本时不要边开发边升级也不要在下班前启动升级。我的习惯是新建一条单独的工具升级流水线先让旧版模型跑到全绿再用新版本MATLAB跑静态检查和全量MIL回归对比两次的仿真波形和覆盖率全部通过后才允许新版本模型合入主干。模型里还有一些块在不同版本下默认参数会变比如求解器或采样时间升级后用slupdate批量升级一下再配合gitattributes导出XML做diff能把它干掉。说实话版本问题听着不太“技术”但它才是MBD CI里最让你半夜被喊起来的类型。一次不小心混用了版本矩阵计算接口变了波形对不上第二天全组都在排查环境问题。提前定死版本是成本最低的防呆策略。最后再分享一点个人体会。我见过不少团队一上来就照着最佳实践把五道门禁全加到MR上结果跑一次四十分钟开发反馈极其难受后来流水线就被悄悄绕过了。正确的路径是先从两个动作开始——一条静态检查门禁加一条关键用例回归跑两周等大家习惯看到绿灯和红灯再把代码生成、SIL、覆盖率逐步加进去。不要高估团队对慢流水线的容忍度也不要低估一条稳定流水线对交付节奏的改变。还有一个小技巧是把流水线每次跑完的模型快照、测试报告和覆盖率记录按版本归档。功能安全审计的时候这些记录能让你少熬很多夜。模型改了什么、什么叫通过全在报告里。MBD和持续集成这事最怕的不是起步晚而是方向错了还拼命往前跑。
阅读完成 · 觉得有帮助?