
事情发生在一次模型评审会的前一天晚上。我对着改了一个月的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 路线AMATLAB Report Generator官方方案MATLAB官方提供Report Generator工具箱里面的mlreportgen.dom.Document类可以直接生成PDF支持标题、段落、表格、图片、页眉页脚。这是生成PDF最稳的一条路。它的优势在于输出结构可控一个Document对象里什么都能塞而且生成的是矢量文本不是简单图片。缺点也很明显需要单独的Report Generator许可证。很多公司买了MATLAB和Simulink但不一定买了这个工具箱。我当时先确认了手头这台机器的许可证情况发现没有装所以没把它作为首选。如果你正好有RG许可证强烈建议优先用它代码路径更短跨版本兼容性也更好。2.2 路线BHTML中转再交给浏览器或工具转PDF不依赖Report Generator的做法是先用MATLAB把模型信息和图片生成一个HTML文件然后用Chrome或Edge打开该文件CtrlP打印为PDF或者用wkhtmltopdf这类命令行工具一键转换。这条路的优势是在任何MATLAB许可证下都能跑样式完全用CSS控制表格分页、表头重复这些功能写起来比DOM方便多了中文字体处理也简单一段CSS就能解决。缺点是多了一个HTML中转目录路径处理要细心。我把HTML转PDF的自动化搭起来时第一次执行wkhtmltopdf命令就遇到“无法将wkhtmltopdf识别为cmdlet、函数、脚本文件或可运行程序的名称”原因是程序没有加入系统PATH。这类环境问题看着小实际排查起来很花时间后面我会专门展开。2.3 路线C全量截图拼PDF如果只是想“看图”最简单的方式是把模型所有层级截图导成一个PDF。代码量最少但只能看到外观模块参数、信号连接关系还是要回到模型里看。它适合临时给同事确认模型结构不适合作为正式设计文档。我实际测试过一个8MB的模型全量截出来的PDF很快就超过40MB而且很多截图角度不对阅读体验很差。2.4 我最终怎么选的我做了个对比表供你根据自己的环境判断对比维度Report GeneratorHTML中转全量截图许可证要求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{end1} subImgPath; %#okSAGROW 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:005.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模型又需要频繁出文档建议找一个周五下午先把最需要的参数表和截图跑通再逐步加上层级遍历、模板化和版本联动。搭完这套之后你会发现原来用于“截图粘贴”的时间终于可以花在真正需要脑子的设计讨论上了。