集成實踐:Simulink模型自動化驗證流水線搭建指南)
做了幾年基于模型的開發(fā)MBD我早年的工作方式其實挺原始打開Simulink手改模型手動拖測試用例跑完看波形再花半小時整理報告。直到有一次一個改動很小的分支在主干上引起了回歸而我發(fā)現(xiàn)問題的時間已經(jīng)比出錯的那天晚了整整三天。那一次之后我開始認真思考一個問題Simulink模型能不能像C代碼一樣每次提交都自動接受一套固定的檢查、仿真和代碼生成驗證答案是可以的而且一旦跑通整個團隊的交付節(jié)奏會是另一種狀態(tài)。這篇文章是我在一款實際控制器軟件項目中把Simulink模型接入持續(xù)集成流水線的完整記錄。我會從工具鏈選型、流水線分級、關(guān)鍵環(huán)節(jié)實現(xiàn)到常見坑位排查盡量把能直接“抄作業(yè)”的東西都寫出來。適合正在做MBD的建模工程師、對CI有需求的嵌入式軟件開發(fā)團隊以及要和功能安全打交道的同學(xué)參考。1. MBD場景下持續(xù)集成到底在解決什么問題1.1 模型不是代碼傳統(tǒng)CI無法直接套用持續(xù)集成的老套路大家都熟代碼提交后自動拉分支、跑編譯、跑單測、出報告。這背后依賴的是“文本可diff、編譯可重復(fù)、單測門檻清晰”這三個基礎(chǔ)??梢坏ο髶Q成Simulink的.slx模型這三個基礎(chǔ)都會有點松動。首先是diff問題。文本代碼改一行g(shù)it里清清楚楚.slx本質(zhì)上是壓縮包里面是XML、二進制數(shù)據(jù)和圖片資源的組合大部分團隊根本沒法對兩個模型文件做精細的三方合并。和一個同事同時改了同一個模型的不同子系統(tǒng)最后誰后保存誰就覆蓋了誰沖突提示還看不懂。這是MBD協(xié)作里最讓人頭疼的一點不是工具不夠好而是對象本身的格式?jīng)Q定了它沒法像代碼那樣被輕易比較。其次是環(huán)境依賴。一個模型能跑出正確結(jié)果依賴的可不只是“保存的框圖”。求解器類型、步長、采樣時間、工作區(qū)變量、數(shù)據(jù)字典、回調(diào)函數(shù)、依賴的自定義庫任何一個環(huán)節(jié)不對仿真結(jié)果就完全不一樣。這些依賴常常不會被人在提交信息里主動說明你拉下來一跑模型能打開但仿真結(jié)果和提交人說好的完全對不上。再就是每次跑批的成本。代碼項目編譯一次可能幾十秒到幾分鐘Simulink要啟動一個完整的MATLAB進程模型加載、編譯圖、仿真動輒十分鐘以上。這種成本放在傳統(tǒng)CI里是難以想象的但反過來也說明了一個事實正因為模型驗證成本高、手動容易漏才更需要把能機器驗證的環(huán)節(jié)全部自動化讓它成為每天回歸的底線。MBD的CI不是要把人替換掉而是把“確定性的、重復(fù)的、容易漏的”檢查變成機器的職責讓人的精力集中在模型結(jié)構(gòu)設(shè)計、算法邏輯和評審這些真正需要判斷力的事情上。1.2 一條MBD CI流水線應(yīng)該覆蓋的五個環(huán)節(jié)在我的經(jīng)驗里一條完整的MBD CI流水線至少要覆蓋下面五個環(huán)節(jié)。這五環(huán)不一定要全部塞進每次提交的門禁但要確保它們在主干上一定會被執(zhí)行。環(huán)節(jié)常用手段主要目的模型靜態(tài)檢查Model Advisor、Simulink Check、MAAB規(guī)范、自定義命名規(guī)則攔截模型結(jié)構(gòu)、命名、代數(shù)環(huán)等規(guī)范問題模型在環(huán)測試MILSimulink Test 測試用例驗證算法邏輯在模型層面的正確性自動代碼生成Embedded Coder、ERT目標、slbuild驗證模型可生成、可編譯為C代碼軟件在環(huán)測試SIL生成代碼編譯為獨立程序后重跑用例驗證模型與生成代碼行為一致覆蓋率與報告歸檔Simulink Coverage、測試報告導(dǎo)出功能安全審計、質(zhì)量門禁、回歸分析這五個環(huán)節(jié)是環(huán)環(huán)遞進的。靜態(tài)檢查保證“模型長得標準”MIL保證“模型邏輯對”代碼生成保證“模型能落地”SIL保證“落地的代碼和模型沒跑偏”覆蓋率保證“測試沒有漏關(guān)鍵路徑”。任何一環(huán)崩了都說明“這次改動不該合進主干”。但要注意這五個環(huán)節(jié)的執(zhí)行成本差異極大。靜態(tài)檢查最快可能1到2分鐘MIL測試要看用例量快則幾分鐘慢則幾十分鐘代碼生成加SIL又是十幾分鐘起步。如果無視成本全部塞進一個門禁MR要等待的時間會非常長最后大家很容易選擇繞過流水線。所以我?guī)ы椖繒r會把流水線按觸發(fā)時機分成三層來設(shè)計這個后面專門說。2. 工具鏈與環(huán)境搭建讓MATLAB在服務(wù)器里“無頭”奔跑2.1 許可證與運行方式選擇在CI里跑MATLAB繞不開的第一個坎就是許可證。MATLAB是商業(yè)授權(quán)軟件不可能像開源編譯鏈那樣隨便并發(fā)。我們的做法是使用網(wǎng)絡(luò)浮動許可也就是常說的license server。具體來說CI節(jié)點上的MATLAB啟動時會通過環(huán)境變量去服務(wù)器租界一個可用授權(quán)export MLM_LICENSE_FILE27000172.16.10.20job跑完后授權(quán)自動釋放。聽起來簡單實際有幾點很關(guān)鍵一是授權(quán)數(shù)量直接決定CI并發(fā)上限。團隊辦公坐席本來就要占用一部分授權(quán)如果CI任務(wù)再去搶兩邊都受影響。我們當時專門單獨申請了一個CI專用授權(quán)池只有3個并發(fā)額度所以流水線并發(fā)數(shù)被刻意壓得很低用排隊換穩(wěn)定。二是Matlab進程一旦“死而不退”授權(quán)就會被長時間占住。這種情況我在Windows agent上遇到過多次最常見的肇事者是腳本里有uiwait、waitforbuttonpress這類GUI阻塞函數(shù)或者異常分支沒有顯式退出。后文生命周期管理部分會再提。三是不要圖省事把CI做成每個模型一個job。MATLAB啟動就需要幾十秒甚至幾分鐘job拆得太碎每個都在重復(fù)啟動、加載、釋放許可證成本非常難看。寧可一個大job里按順序跑多階段任務(wù)也不要一個階段一個job。2.2 用Docker跑MATLAB去掉環(huán)境相關(guān)的大量破事如果說許可證是“能不能跑起來”的問題那運行環(huán)境就是“跑起來是不是每次都一樣”的問題。我在這上面吃過虧團隊有的人筆記本上裝R2021bCI agent上裝了R2022a模型一打開就是一堆模型升級提示偏偏升級后的參數(shù)差一點仿真結(jié)果就壓著容差邊界過。所以強烈建議直接使用MATLAB官方提供的Docker鏡像按版本打tag比如matlab:r2023b。鏡像里包含基礎(chǔ)的MATLAB和Simulink以及若干常用工具箱啟動之后就是一個干凈、確定的MATLAB環(huán)境。在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這個環(huán)境變量值得單獨點一下。它在CI里能禁用MATLAB崩潰時的彈窗和對話框避免構(gòu)建卡在無人點擊的窗口上直接等到超時。很多團隊第一次跑CI就莫名其妙掛在某一步一查就是這個。還有個現(xiàn)實細節(jié)一些公司內(nèi)外網(wǎng)隔離CI runner訪問不了外網(wǎng)鏡像倉庫。我的做法是把官方鏡像拉到有網(wǎng)絡(luò)權(quán)限的機器上再推送到內(nèi)網(wǎng)私有鏡像倉庫CI job里直接用內(nèi)網(wǎng)鏡像地址。別嫌麻煩鏡像一次性導(dǎo)入后續(xù)版本升級才會再折騰一次。鏡像解決了環(huán)境一致性問題但里面沒有許可證。所以容器跑起來的時候必須把MLM_LICENSE_FILE傳進去這就是上面YAML里變量區(qū)那一行的作用。2.3 命令行之必須matlab -batch 的正確姿勢MATLAB從R2019b開始支持-batch模式這基本就是為CI場景設(shè)計的。老式的寫法是matlab -nodisplay -nosplash -r run(ci/run_all.m)這種寫法有兩個老坑一是-r執(zhí)行完后MATLAB不會自動退出必須在腳本最后補exit漏一次CI就掛到超時二是一旦腳本中途拋異常進程返回狀態(tài)和日志容易讓人看不明白到底掛沒掛成。-batch模式就好很多自動啟動MATLAB不顯示桌面運行指定命令或腳本腳本執(zhí)行完后自動退出如果腳本拋異常MATLAB返回非零退出碼CI系統(tǒng)天然能識別失敗一個典型用法matlab -batch run(ci/pipeline.m) -logfile matlab_ci.log注意-logfile參數(shù)MATLAB會把控制臺輸出同時寫到文件里這樣CI網(wǎng)頁上既能看輸出也能在下游把日志作為artifact存起來。遇到大數(shù)據(jù)量的輸出還能避免頁面渲染卡頓。我也習(xí)慣在pipeline.m最外層做一次try/catch把關(guān)鍵階段耗時和最終狀態(tài)匯總打出來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模式下漏了這個也能返回非零碼但加上這個結(jié)構(gòu)以后日志一眼就能定位是哪一步掛的后面排查會省很多事。提示如果出現(xiàn)“模型能打開但仿真報錯”多數(shù)問題出在工具箱許可缺失或模型依賴路徑不對。鏡像里裝了哪些工具箱一定程度上要靠前期梳理建議先在本機把項目需要的工具箱列表列清楚再定制鏡像。3. 流水線詳細設(shè)計從MR門禁到主干回歸的分層策略3.1 為什么要分三層本地/MR/主干很多MBD團隊第一次搭CI最容易犯的錯誤是“用力過猛”。靜態(tài)檢查、全量MIL、代碼生成、SIL、覆蓋率全塞在一個MR流水線里跑一次四十分鐘以上一天有二十次提交流水線永遠在排隊開發(fā)者急著合代碼又過不了門禁最后都開始“繞過規(guī)則”。我在項目里采用的是三層結(jié)構(gòu)層級觸發(fā)時機內(nèi)容時長目標本地自測開發(fā)者提交前手動跑模型打開檢查、關(guān)鍵用例仿真30分鐘以內(nèi)MR門禁每次發(fā)起Merge Request靜態(tài)檢查、命名規(guī)范、快速MIL回歸10分鐘左右主干流水線合并到主干后全量MIL、代碼生成、SIL、覆蓋率、報告歸檔1小時左右核心邏輯是反饋越快的門禁放越便宜的檢查反饋越慢的門禁放越重的保障。MR門禁是給開發(fā)者“快糙猛”的體檢主干流水線才是“全面體檢檔案留存”。有人在MR門禁里也想跑全量用例理由是“不跑全量合進去怕出事”。但主干流水線還在啊MR階段漏掉的主干會兜底。而且主干流水線失敗后可以自動回滾合并、自動通知作者這套機制比把MR時間拖到天荒地老要健康得多。3.2 MR門禁里放什么靜態(tài)檢查與快速MIL測試MR門禁的第一關(guān)是靜態(tài)檢查執(zhí)行速度最快、問題最可解釋。Simulink里的Model Advisor是一個規(guī)則引擎本質(zhì)上是對模型圖拓撲做一次“代碼體檢”能查代數(shù)環(huán)、信號屬性沖突、死分支、可讀性隱患等一大堆問題。團隊還可以把MAAB規(guī)范配進去變成自動門禁而不是靠評審時人眼去盯命名。常見的調(diào)用方式是這樣的mdl VehicleController; md ModelAdvisor.getModelAdvisor(mdl); md.TaskFilters {MAAB}; res md.RunTasks(); md.saveAllResults();細節(jié)不同版本會有差異但邏輯不變加載模型執(zhí)行指定規(guī)則組收集結(jié)果并導(dǎo)出。CI里只要發(fā)現(xiàn)有“需修復(fù)”或“高?!奔墑e的違規(guī)就讓job失敗普通提示級別的違規(guī)不阻斷但寫進報告供評審人查看。Model Advisor之外我還習(xí)慣加一組自定義命名檢查腳本。比如模塊名必須用in_/out_前綴、Goto和From標簽必須一一配對、不允許未連線端口之類。這些規(guī)則用MATLAB腳本遍歷模型對象就能實現(xiàn)% 偽代碼示例檢查未連線端口 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門禁的第二關(guān)是快速MIL回歸。全量用例不動只跑一個提前劃好的“快速集”。這個快速集怎么選是有講究的不是隨便挑幾個用例就算完。我們的做法是按影響面動態(tài)選擇先用git diff拿本次變更的模型文件清單映射到對應(yīng)子系統(tǒng)再選擇覆蓋這些子系統(tǒng)的關(guān)鍵用例。如果映射不到子系統(tǒng)就退回到最常用的冒煙用例集。這套邏輯用MATLAB腳本配合git diff命令就能實現(xiàn)[~, out] system(git diff --name-only HEAD~1 HEAD); changedFiles strsplit(strtrim(out));當然在MR門禁階段不可能對全模型做窮盡驗證但“改動相關(guān)的關(guān)鍵用例能跑通”已經(jīng)能攔住大部分低級回歸。全量的事情交給主干。3.3 主干回歸怎么跑代碼生成、覆蓋率、SIL對照驗證主干流水線是MBD CI里最精華的部分因為它真正把“模型是源產(chǎn)物”這個概念落實了。模型不只要仿真通過還要能生成代碼、生成出來的代碼還要能通過同樣的驗證。代碼生成這一步我用的是Embedded Coder配合ERT目標。命令行里一句slbuild就能完成slbuild(VehicleController);這一步會做兩件事一是從模型生成C代碼二是調(diào)用配置好的編譯工具鏈生成目標產(chǎn)物。生成目錄下會有對應(yīng)的makefileCI里可以進一步手動編譯和鏈接(cd VehicleController_ert_rtw make -f VehicleController.mk)如果代碼生成這一步都過不了說明模型里有Simulink無法落地的構(gòu)造這種問題留在模型階段銷毀是最便宜的。SIL測試的核心目的是驗證“模型仿真結(jié)果”和“生成代碼跑出來的結(jié)果”是否一致。我把MIL階段跑過的測試用例用Simulink Test在SIL模式下重新執(zhí)行再把兩組仿真輸出信號做對齊比較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這個容差要按信號性質(zhì)去設(shè)我們項目里用的是絕對誤差1e-4配合允許的采樣時間偏移判斷起來不慢也不誤殺。覆蓋率這塊Simulink Coverage提供決策覆蓋率、條件覆蓋率、MCDC等指標。對功能安全要求高的項目MCDC覆蓋率會被作為強門禁。我常見的做法是把覆蓋率配置寫在測試管理器里跑完全量后讀出覆蓋率結(jié)果低于閾值直接失敗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這種需要接真實硬件、負載箱和故障注入單元的測試我一般不放進常規(guī)CI里。一個原因是硬件資源有限并發(fā)跑不起來另一個原因是CI要的是快速穩(wěn)定而HIL對環(huán)境依賴太大適合作為獨立執(zhí)行的周更任務(wù)或發(fā)布前驗證。SIL才是常規(guī)流水線里該有的“準硬件驗證”它足夠快又能覆蓋絕大多數(shù)模型到代碼的落地問題。4. 常見問題與排查技巧實錄4.1 模型合并沖突與Review策略這是MBD協(xié)作里繞不開的痛。前面說過.slx文件不是純文本git默認情況下不能做細粒度合并。我們的應(yīng)對方案是組合拳。第一招把大模型按功能拆成多個引用模型Model Reference。每個子系統(tǒng)獨立成一個.slx文件大家改動盡量落在不同文件里從源頭減少沖突。一個控制器模型拆出十幾個引用模型多人并行時體驗會好非常多而且每個子模型還能單獨構(gòu)建、緩存主模型編譯時直接復(fù)用速度也快不少。第二招開啟gitattributes讓模型文件在diff時導(dǎo)出為XML雖然不能直接做三方合并但至少能在評審時看到變量級變化*.slx diffslxmerge第三招模型間比較用Simulink自帶的模型比較工具命令行里可以直接打開visdiff(VehicleController_old.slx, VehicleController_new.slx);它會把兩個模型的差異按層級列出來評審人能看到新增了哪個block、改了哪個參數(shù)而不只是“二進制文件變了”。評審流程上我們還有個約定每次MR描述里必須寫明接口變更說明比如“新增輸入信號BrakePressure類型uint16刪除輸出TorqueLimit原邏輯遷移至Xxx子系統(tǒng)”。這個說明對代碼評審來說可能顯得笨拙但對模型評審來說特別關(guān)鍵能有效防止“看起來沒沖突實際上總線接口已經(jīng)變了”的隱藏雷。4.2 許可證排隊與并發(fā)問題CI里最典型的許可證病況是job卡在啟動階段日志里出現(xiàn)License Manager相關(guān)的錯誤或長時間無響應(yīng)。排查時我會先看授權(quán)服務(wù)器側(cè)的狀態(tài)lmstat -a -c 27000172.16.10.20能直接看到當前授權(quán)被哪些主機占用以及剩余額度。只要看到CI agent的條目長期掛著但對應(yīng)的MATLAB進程早沒了基本可以斷定是license沒能釋放。這種情況的根因八成是腳本還沒完整退出或者前一個job異常后沒有正確地結(jié)束MATLAB進程。我們的對策有三個一是給CI單獨劃分授權(quán)池不讓CI和辦公坐席搶授權(quán)。數(shù)量再少都行關(guān)鍵是互不影響。二是調(diào)整并發(fā)策略將CI并發(fā)上限控制在授權(quán)池可用的60%到70%。留一點余量給瞬時啟動和壓測任務(wù)避免多job同時搶授權(quán)造成“雪花崩”。三是把單個job的任務(wù)盡量串行減少同時啟動多個MATLAB進程。自動化測試用例多的時候可以考慮parfor做用例級并行但前提是授權(quán)池夠用否則反而會因為爭搶授權(quán)而整體變慢。4.3 慢測試、超時與flaky測試的處理Simulink測試的慢是有名的一個用例跑幾分鐘很常見全量跑下來一小時也不意外。為了不讓流水線被“長尾用例”拖垮我在測試配置里強制給每個用例設(shè)置超時。副作用是如果用例超時了流水線會攔下一份超時報告而不是整個job掛到平臺級超時。這個差異經(jīng)歷過的人都會懂。清零和固定隨機源這兩件事是我在每個測試腳本開頭必做的bdclose(all); sltest.testmanager.clear(); clear classes; rng(default);作用分別是關(guān)閉所有已打開的模型避免模型鎖沖突清空測試管理器確保上次遺留用例不影響這次清掉類定義防止被改動的.m腳本繼續(xù)用舊版本運行固定隨機數(shù)序列保證可復(fù)現(xiàn)。flaky測試里我踩得最深的一個坑是曾經(jīng)有個用例單獨跑永遠通過但跟著前面的用例一起跑就必掛。查了一天根因是前面一個用例在同一個MATLAB進程里修改了一個模型回調(diào)函數(shù)引用的.m文件我的用例加載模型時拿到了舊緩存。清理腳本一加問題再沒出現(xiàn)過。模型引用組件的增量構(gòu)建也值得單獨說。主干流水線第一次全量構(gòu)建可能很慢但開起引用的增量構(gòu)建后只有改動過的子模型會重新構(gòu)建和編譯其他部分走緩存整個流水線耗時能降到原來的三分之一左右。這個優(yōu)化在模型量大的時候非常明顯是把MBD CI跑通的關(guān)鍵開關(guān)之一。4.4 模塊版本兼容問題MATLAB基本上每年一個大版本模型格式并不是全向前兼容的低版本打不開高版本的模型是常態(tài)。我們團隊的做法是把建模版本統(tǒng)一鎖定比如全team固定R2023bCI鏡像也固定用R2023b。git提交說明里強制標注模型版本合并時檢查是否有人手滑用別的版本保存過。真需要升級工具版本時不要邊開發(fā)邊升級也不要在下班前啟動升級。我的習(xí)慣是新建一條單獨的工具升級流水線先讓舊版模型跑到全綠再用新版本MATLAB跑靜態(tài)檢查和全量MIL回歸對比兩次的仿真波形和覆蓋率全部通過后才允許新版本模型合入主干。模型里還有一些塊在不同版本下默認參數(shù)會變比如求解器或采樣時間升級后用slupdate批量升級一下再配合gitattributes導(dǎo)出XML做diff能把它干掉。說實話版本問題聽著不太“技術(shù)”但它才是MBD CI里最讓你半夜被喊起來的類型。一次不小心混用了版本矩陣計算接口變了波形對不上第二天全組都在排查環(huán)境問題。提前定死版本是成本最低的防呆策略。最后再分享一點個人體會。我見過不少團隊一上來就照著最佳實踐把五道門禁全加到MR上結(jié)果跑一次四十分鐘開發(fā)反饋極其難受后來流水線就被悄悄繞過了。正確的路徑是先從兩個動作開始——一條靜態(tài)檢查門禁加一條關(guān)鍵用例回歸跑兩周等大家習(xí)慣看到綠燈和紅燈再把代碼生成、SIL、覆蓋率逐步加進去。不要高估團隊對慢流水線的容忍度也不要低估一條穩(wěn)定流水線對交付節(jié)奏的改變。還有一個小技巧是把流水線每次跑完的模型快照、測試報告和覆蓋率記錄按版本歸檔。功能安全審計的時候這些記錄能讓你少熬很多夜。模型改了什么、什么叫通過全在報告里。MBD和持續(xù)集成這事最怕的不是起步晚而是方向錯了還拼命往前跑。