事情发生在一次模型评审会的前一天晚上。我对着改了一个月的Simulink模型,把顶层框图和七层子系统的截图一张张导出,再打开Word逐个粘贴、写说明、更新参数表,一直折腾到凌晨还剩一半没做完。第二天评审会上,同事指着一个参数问:“你文档里写的是10,但模型里明明是12。”我当时愣住了,翻回去对了半天才发现,模型早就改过,文档却一直没跟上。
那次之后我就下决心,必须用脚本把“Simulink模型生成PDF文档”这条链路彻底自动化:让脚本自己遍历模型层级,把模块清单、参数表、子系统截图全部抓出来,最后拼装成一份带版本信息、目录和图表的设计文档PDF。适合那些需要频繁给模型出文档、做版本交接、写仿真项目说明书、甚至在客户验收时提交附件的工程师。下面我就把完整做法拆开讲清楚,包括方案怎么选、代码怎么写、坑在哪里,以及最后怎么从一段脚本长成一个团队可用的文档小工具。
1. 为什么我盯上了这份PDF:被文档折磨出来的需求
1.1 手动导出文档的真实成本
我先算一笔账。以一个80个模块、5层子系统的中等规模模型为例,手动出一份完整设计文档,要做的事情至少有这些:逐层展开子系统,把模型调整到适合截图的角度;用截图工具截顶层图和每个子系统图,通常要10到15张;打开Word或WPS,逐张贴图、写标题、编号;对每个Mask封装的模块,打开参数对话框手动抄参数;涉及工作区变量时,还得去MATLAB工作区找对应值;最后核对模型版本号、最近修改时间,再导出PDF,人工检查至少半小时。
这一整套坐下来,快的话两个小时,慢的话半天。我在那次评审会前算得很清楚:从下午四点开始做,到晚上十点半,一共只完成了模型截图、参数表初稿和前三章文字,后面还有一大半内容没写。更让人崩溃的是,文档里的参数和模型实际值不一致这个问题,它不会在写文档的时候暴露,只会在评审、审计、现场联调的时候突然跳出来,打得你措手不及。
| 任务类型 | 手动耗时(中等模型) | 出错风险 |
|---|---|---|
| 模型截图与贴图 | 40~60分钟 | 截图与当前模型不一致 |
| 参数表整理 | 30~45分钟 | 参数值抄错或漏抄 |
| 版本信息核对 | 5~10分钟 | 版本号对不上 |
| 排版与编号 | 20~30分钟 | 编号错乱、缺少更新 |
| 总耗时 | 1.5~2.5小时 | 高 |
1.2 自动生成PDF到底解决了什么
自动化的核心价值不是“省两次手动操作”,而是让文档从“人抄模型”变成“模型直接打印自己”。脚本通过MATLAB API直接读模型内部数据,参数表一定是当前模型里的值,截图也是当前模型最新状态。只要模型保存过,重新跑一遍脚本,PDF就是一份全新的文档。
另外,脚本生成PDF天然可重复、可追溯。我在生成的PDF首页固定放模型文件名、模型版本号、脚本生成时间和MATLAB版本,评审时打开就知道这一份文档对应的是哪一次模型状态。版本交接时,拿着PDF就能快速判断文档和模型是否匹配。后来我给客户做阶段验收,对方要求提供带版本号的系统设计文档,我没再手工整理过一次,全部由脚本输出。这个转变带来的直接收益是:评审被问到某个参数时,我敢当场打开PDF说“这就是当前模型的值”,心里有底。
2. 三条技术路线,哪条最省心
2.1 路线A:MATLAB Report Generator官方方案
MATLAB官方提供Report Generator工具箱,里面的mlreportgen.dom.Document类可以直接生成PDF,支持标题、段落、表格、图片、页眉页脚。这是生成PDF最稳的一条路。
它的优势在于输出结构可控,一个Document对象里什么都能塞,而且生成的是矢量文本,不是简单图片。缺点也很明显:需要单独的Report Generator许可证。很多公司买了MATLAB和Simulink,但不一定买了这个工具箱。我当时先确认了手头这台机器的许可证情况,发现没有装,所以没把它作为首选。如果你正好有RG许可证,强烈建议优先用它,代码路径更短,跨版本兼容性也更好。
2.2 路线B:HTML中转,再交给浏览器或工具转PDF
不依赖Report Generator的做法是:先用MATLAB把模型信息和图片生成一个HTML文件,然后用Chrome或Edge打开该文件,Ctrl+P打印为PDF,或者用wkhtmltopdf这类命令行工具一键转换。
这条路的优势是:在任何MATLAB许可证下都能跑;样式完全用CSS控制,表格分页、表头重复这些功能写起来比DOM方便多了;中文字体处理也简单,一段CSS就能解决。缺点是多了一个HTML中转目录,路径处理要细心。我把HTML转PDF的自动化搭起来时,第一次执行wkhtmltopdf命令就遇到“无法将wkhtmltopdf识别为cmdlet、函数、脚本文件或可运行程序的名称”,原因是程序没有加入系统PATH。这类环境问题看着小,实际排查起来很花时间,后面我会专门展开。
2.3 路线C:全量截图拼PDF
如果只是想“看图”,最简单的方式是把模型所有层级截图导成一个PDF。代码量最少,但只能看到外观,模块参数、信号连接关系还是要回到模型里看。它适合临时给同事确认模型结构,不适合作为正式设计文档。我实际测试过,一个8MB的模型,全量截出来的PDF很快就超过40MB,而且很多截图角度不对,阅读体验很差。
2.4 我最终怎么选的
我做了个对比表,供你根据自己的环境判断:
| 对比维度 | Report Generator | HTML中转 | 全量截图 |
|---|---|---|---|
| 许可证要求 | RG独立许可证 | 无 | 无 |
| 表格/分页 | 较强,但需自定义 | 强,浏览器自带分页 | 不支持 |
| 自定义样式 | 一般 | 好 | 差 |
| 中文字体 | 偶尔有坑 | 稳定 | 好 |
| 自动化深度 | 高 | 高 | 低 |
| 输出体积 | 小 | 中等 | 大 |
我的经验是:有RG许可证就用RG,没有就走HTML中转。核心逻辑完全一样,截图和参数抓取不管用哪种方式都要做,差别只在于最后一步的出口。
3. 核心脚本拆解:信息抓取、截图与PDF组装
3.1 从模型里抓什么
一份完整的模型PDF文档,我建议至少包含五块内容:模型概述、版本信息、模块统计、完整模块列表、关键参数表。前两块是静态文字,后三块需要动态获取。
关键API是这几个:
- 用
Simulink.MDLInfo('myModel')读取模型的版本、修改时间 - 用
find_system('myModel', 'LookUnderMasks', 'all', 'FollowLinks', 'on', 'Type', 'Block')拿到所有模块路径 - 用
get_param(blockPath, 'MaskValues')读取Mask封装参数 - 用
Simulink.findVars('myModel')列模型引用的工作区变量
下面是一段核心抓取代码:
mdlName = 'myModel'; load_system(mdlName); % 读取模型信息 mdlInfo = Simulink.MDLInfo(mdlName); fprintf('Model: %s\n', mdlName); fprintf('Version: %s\n', char(mdlInfo.ModelVersion)); fprintf('Last Modified: %s\n', char(mdlInfo.LastModifiedDate)); % 获取所有模块 allBlocks = find_system(mdlName, ... 'LookUnderMasks', 'all', ... 'FollowLinks', 'on', ... 'Type', 'Block'); blockCount = numel(allBlocks); % 统计子系统数量 subsysCount = 0; for i = 1:blockCount if strcmp(get_param(allBlocks{i}, 'BlockType'), 'SubSystem') subsysCount = subsysCount + 1; end end这里要注意LookUnderMasks和FollowLinks两个参数。不加这两个参数时,find_system默认只扫表面层级的非封装模块,很多子系统里的内容会被漏掉。加完之后,扫出来的模块数量可能翻一倍,这正是我们想要的“全量”清单。
3.2 把模型“拍”进文档
截图这部分,我用的是open_system加exportgraphics组合。为什么不用print或saveas?因为exportgraphics在R2020a之后支持设置分辨率、背景色,而且导出的尺寸和Simulink画布显示一致,不会有多余边距。
open_system(mdlName); set_param(mdlName, 'ScreenColor', 'white'); imgPath = fullfile(outDir, 'top_model.png'); exportgraphics(get_param(mdlName, 'Handle'), imgPath, ... 'Resolution', 200, 'BackgroundColor', 'white'); % 遍历子系统逐层截图 allSubs = find_system(mdlName, 'BlockType', 'SubSystem'); subImgPaths = {}; for i = 1:numel(allSubs) subPath = allSubs{i}; % 跳过空子系统 innerBlocks = find_system(subPath, 'SearchDepth', 1, 'Type', 'Block'); if numel(innerBlocks) <= 1 continue; end fileName = sanitizeName(strrep(subPath, '/', '_')); subImgPath = fullfile(outDir, [fileName, '.png']); exportgraphics(get_param(subPath, 'Handle'), subImgPath, ... 'Resolution', 200, 'BackgroundColor', 'white'); subImgPaths{end+1} = subImgPath; %#ok<SAGROW> endset_param(mdlName, 'ScreenColor', 'white')这一步很关键。Simulink默认的模型背景是白色,但有些模板会改成灰色或带网格,不设置成白底,导出的图片放到正式文档里会显得很不干净。
3.3 DOM组装PDF的完整示例
如果你的环境有Report Generator许可证,最省心的做法是用DOM API直接把内容拼成PDF。代码大概是这样的:
import mlreportgen.dom.*; d = Document(fullfile(outDir, 'ModelDocument'), 'pdf'); open(d); % 标题 h = Heading(1, sprintf('%s 模型设计文档', mdlName)); h.Style = {Bold(true), FontSize('22pt'), Color('black')}; append(d, h); % 版本信息 p = Paragraph(sprintf('模型版本:%s,最后修改:%s', ... char(mdlInfo.ModelVersion), char(mdlInfo.LastModifiedDate))); p.Style = {FontSize('11pt')}; append(d, p); % 统计表 tableData = { '模块总数', num2str(blockCount); '子系统数', num2str(subsysCount); '更新时间', char(mdlInfo.LastModifiedDate); }; tbl = Table(tableData); tbl.Style = {Border('solid'), ColSep('solid'), RowSep('solid')}; append(d, tbl); % 插入顶层截图 imgObj = Image(imgPath); imgObj.Style = {Width('6.5in'), Height('4.5in')}; append(d, imgObj); close(d);这里有三个容易忽略的细节。第一,close(d)才会真正写盘,忘记close会导致生成的PDF是0字节。第二,图片宽度不要超过页面可用宽度,A4纸默认边距下我用的是6.5英寸,超过会被裁剪。第三,Document默认会在内存缓存,如果脚本在运行中途报错退出,必须主动close(d),否则下次用同一个文件名生成会失败。
3.4 没有Report Generator时的替代写法
没有RG许可证,我建议走HTML中转。生成HTML的活儿MATLAB干最合适,因为所有表格和图片都是现成的。核心步骤是先把数据拼成HTML字符串,再写出完整HTML文件,最后调wkhtmltopdf转PDF。
wkhtmltopdf --enable-local-file-access -s A4 -T 10mm -B 10mm -L 15mm -R 15mm model.html model.pdf我第一次跑这个命令时,直接报“无法将wkhtmltopdf识别为cmdlet、函数、脚本文件或可运行程序的名称”,一看就是PATH没配好。解决方法是把wkhtmltopdf的安装目录加入系统的Path环境变量,然后重启MATLAB或命令终端。这个问题本身很简单,但它提醒了我一个道理:任何自动化脚本,只要依赖外部命令行工具,第一步就该检查环境变量,不然后面排查会很痛苦。--enable-local-file-access这个参数也是实测出来的,新版wkhtmltopdf默认禁止访问本地文件,不加它,HTML里的本地图片全都会变成空白。
4. 实测跑通全程后踩过的坑
4.1 模型没加载、工作路径不对
第一次跑脚本,最典型的报错是Cannot load model 'myModel'。很多脚本刚写完时,模型没有打开,或者脚本的工作目录不在模型目录,find_system就找不到。
我的排查链路是这样的:
- 先确认模型文件确实存在:用
exist(fullfile(modelDir, [mdlName '.slx']), 'file')检查 - 再用
addpath(modelDir)把模型目录加入MATLAB路径 - 最后
load_system(mdlName)把模型载入内存,再执行后续操作
这三步顺序不能乱。很多人习惯直接open_system,但open_system会弹出图形界面,在无人值守的定时任务里不合适。load_system只加载不到界面,更轻量。你写脚本时一定要用load_system而不是open_system,否则脚本挂在GUI上,自动化就失败了一半。
4.2 截图一片黑、分辨率低、字体消失
截图问题是最折腾人的。我遇到三种情况:
第一种,背景不对。模型模板里可能开了网格、用了灰色背景,甚至某些公司模板会把注解浮层打开。统一处理方式是在截图前加一段配置:set_param(mdlName, 'ScreenColor', 'white'),然后手动关掉网格显示,再把注解层隐藏。这样导出的图才算干净。
第二种,分辨率太低。默认分辨率导出的图片在电脑上看还行,投到评审会议室大屏就发虚。我建议至少200dpi。但分辨率上调之后,PDF体积会暴涨。我试过300dpi全量截图,一个中等模型出来60多MB,投屏没觉得清晰多少,反而发邮件都超附件限制。最后稳定在200dpi,只截重点视图,文档体积能控制在20MB以内。
第三种,中文和特殊字体丢失。Simulink模块名如果是中文,导出图片时字体缺失会变成方框。这个问题比较难根治,最稳妥的做法是在模型里统一用英文模块名,文档里再用参数表说明中文含义。如果你一定要保留中文模块名,建议先升级显卡驱动和MATLAB版本,有时候旧版本对HiDPI屏幕的字形渲染是有bug的。
4.3 中文字体和跨页表格
走HTML中转时,CSS里如果不给body设置中文字体,很容易出现乱码或者方块。我用的CSS片段是:
body { font-family: 'Microsoft YaHei', 'SimSun', sans-serif; }段落文本和表格正文都继承这个字体,基本不会出错。还有一个问题:表格跨页时,默认不重复表头,翻页之后读者不知道那一列是什么参数。解决方式是HTML里把表头包在<thead>里,wkhtmltopdf在分页时会自动重复thead,比手动指定分页规则稳得多。我强烈建议你把所有长表格都套上thead,这个习惯救了我很多次。
4.4 文件命名和特殊字符
模型里的模块名可能是“PID Controller (1)”,如果脚本直接拿模块名当文件名,在Windows上就会因为括号、空格出现各种问题。我的习惯是写一个sanitizeName函数,把所有非字母数字字符统一替换成下划线:
function safeName = sanitizeName(name) safeName = regexprep(name, '[^a-zA-Z0-9_.]', '_'); end另外,模型中如果有Outport、Inport这类端口,它在find_system里也算Block,统计和截图时会得到一些意外结果。遍历时我会加一个判断,跳过BlockType为'Outport'和'Inport'的项。这个问题看起来小,但不处理的话,PDF里会混进一堆没有实际意义的端口截图,显得很不专业。
4.5 调用外部工具时PATH和权限的坑
脚本化生成PDF还有一个特别容易被忽略的环节:外部工具能不能被正常调用。前面说的wkhtmltopdf只是其中之一。如果你的脚本里还调用了其他命令行程序,比如图片压缩工具、PDF合并工具,一定要在脚本启动时打印一份环境检查日志,把所有外部命令的版本号输出来。
我在一次定时任务里发现,白天跑得好好的脚本,晚上通过Windows任务计划程序跑就失败,报错信息正是“无法将wkhtmltopdf识别为cmdlet”。原因很典型:定时任务运行时的用户环境跟我在交互终端里不一样,PATH没有包含wkhtmltopdf目录。所以后来我在脚本里统一用了完整路径:
wkhtmltopdfPath = 'C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe'; [status, cmdout] = system(['"' wkhtmltopdfPath '" --enable-local-file-access ...']);这个问题经验值很高。任何自动化脚本,只要依赖外部程序,都别指望PATH一定正确,直接写绝对路径最稳。
5. 从脚本进化成“小工具”:模板化、版本联动与批量处理
5.1 自动抓取版本和修改时间
Simulink模型文件本身带元数据,不需要去“模型属性”里手动抄。Simulink.MDLInfo可以直接拿到ModelVersion和LastModifiedDate。我把这些信息写进PDF封面页,还额外从get_param(mdlName, 'Created')读创建时间。这样文档和模型的对应关系一目了然。
我用的封面信息模板大致是:
| 字段 | 内容 |
|---|---|
| 模型名称 | myModel |
| 模型版本 | 1.6 |
| 创建时间 | 2024-03-12 09:30:00 |
| 最后修改 | 2024-11-28 16:42:00 |
| 文档生成时间 | 2024-11-29 10:00:00 |
5.2 做一套可复用的HTML模板
因为最终主力方案是HTML中转,所以我把整个PDF的样式做成了一个独立的HTML模板文件。模板里有固定的封面区、版本信息区、章节标题样式、表格样式、图片展示样式。脚本只负责生成“可变内容”,把图片路径和参数数据填进模板,模板负责排版。
这样做的好处很明显:以后想换配色、想加页脚、想调整字体大小,直接改模板就行,根本不用动脚本。我后来还做了两个风格的模板,一个是项目内部使用的浅色调,一个是给客户交付用的正式深色封面,只是切换模板文件就完成了整套风格迁移。如果你不做模板化,每换一次样式就要回头翻代码找字符串拼接逻辑,会越改越乱。
5.3 与版本管理联动
Simulink的.slx文件是压缩包,Git里直接diff基本不现实。我现在的做法是:每次Git提交后,由一条命令自动跑脚本生成最新PDF,存储到docs目录,再人工review。这样团队看到的PDF永远是当前分支匹配的,不会再出现“文档是上个月的,模型是今天改的”这种问题。
如果不想接Git,也可以用系统级定时任务来跑。Windows下用“任务计划程序”配一个批处理,晚上定时执行:
matlab -batch "generateModelDoc('myModel')"第二天早上就能拿到最新文档。团队里只要有人改动模型并推送,文档就会跟着自动更新。
5.4 批量处理一批模型
一个项目往往有好几个模型,控制器模型、被控对象模型、整车模型各一个。脚本写成接受模型名为参数的函数后,再写一个外层循环就能批量生成:
mdlList = {'EngineModel', 'VehicleModel', 'ControllerModel'}; for i = 1:numel(mdlList) try generateModelDoc(mdlList{i}); catch ME fprintf('生成 %s 失败: %s\n', mdlList{i}, ME.message); end end这样开评审会前,就能一键把所有子系统或整车模型的文档全部重新生成一遍,不用一个模型一个模型去操作。我在实际用的时候,把模型名列表也放到外部配置文件里,这样加模型或者删模型都只需要改配置,不用改脚本。
6. 脚本该在哪里放手,又该在哪里管住手
6.1 适合自动化的场景
根据我这段时间的使用体验,下面几类场景特别适合自动化出文档:
- 模型结构和参数评审材料:模块清单、参数表、结构图,正好是脚本最擅长抓的部分,能确保数据和模型完全一致
- 新同事入职学习:一份自动生成的PDF,比在模型里点来点去看目录高效得多,至少能先把整体结构过一遍
- 版本交接:交接时附一份带版本号的PDF,比口头交代更可靠
- 客户验收附件:非保密的系统框图、参数表,直接由脚本输出,避免人工抄错
6.2 不建议完全交给脚本的场景
但我也得实话实说:脚本生成的是“信息快照”,不是“设计解释”。某个参数为什么取这个值、某个信号为什么走这条路径、某个模块为什么设计成这个样子,这些逻辑脚本永远写不出来。
在安全关键功能、控制策略设计、异常场景说明这类文档里,自动生成的PDF只能当底稿,必须有人工评审环节把真正的设计意图写进去。我的经验是,自动生成PDF负责“全”,人工补写负责“透”。只靠脚本生成的PDF去交付安全关键文档,风险太高,不建议。
6.3 我目前的工作流
经历了多次迭代之后,我在团队里推的工作流是这样:
- 模型冻结到一个节点,打上版本标签
- 脚本一键生成带版本号的PDF初稿,5分钟以内完成
- 设计负责人对照模型人工review,在PDF上加批注
- 批注过的PDF归档到受控文档目录
这套流程既没有让人去做重复劳动,又保证了文档不是没有灵魂的导出物。脚本负责把信息整整齐齐摆出来,人负责把思考写进去。
最后再说一个实战小经验。第一次把脚本跑通、看到PDF自动生成的时候,说实话挺兴奋的,那种感觉就像以前每周手动对账,突然发现Excel公式把账做完了。直到现在,我每次生成新文档还会保持一个习惯:生成后打开PDF,从头到尾快速扫一遍,重点看封面版本号、那一堆截图有没有乱序、参数表有没有出现明显异常值。别小看这几分钟,它能拦掉八成因为脚本本身写错而导致的文档问题。
如果你也在维护Simulink模型又需要频繁出文档,建议找一个周五下午,先把最需要的参数表和截图跑通,再逐步加上层级遍历、模板化和版本联动。搭完这套之后,你会发现原来用于“截图粘贴”的时间,终于可以花在真正需要脑子的设计讨论上了。