Simulink模型生成DLL全流程:从配置到外部调用的实战指南

发布时间:2026/10/19 8:59:00
Simulink模型生成DLL全流程:从配置到外部调用的实战指南
简介在Simulink中封装算法并生成动态链接库DLL是将模型功能复用至外部应用或实现模块化集成的高频需求。面向有Simulink基础、希望掌握C/C代码集成与DLL调用技巧的开发者这份资料完整收录了从模型配置到DLL生成、再到S-Function调用的流程说明。压缩包共127个文件大小仅400KB其中包含Simulink模型文件、C源码与头文件、构建批处理、编译产物DLL/LIB/EXP/OBJ、MAT数据文件以及HTML报告覆盖了生成与调用DLL所需的关键材料。借助包内内容可以梳理代码生成接口设置、目标语言选择、库输出类型配置以及S-Function中头文件包含、外部函数声明、链接路径指定等实现细节同时也能参考输入输出类型匹配、数据转换等注意事项。已有799人学习下载对需要将仿真控制算法封装为独立模块、并与非Simulink程序协同应用的工程师很有帮助。1. 项目定位从 Simulink 到 DLL一个工程化封装很多做算法仿真的同事都会遇到同一个翻车场景Simulink 里调好的控制模型要集成到 C 写的上位机或设备程序里还得让人家那套代码直接调用我们的算法逻辑。手动去配置代码生成、导出模型文件、再把输入输出口径理清楚每次少则半天多则两天而且生成出来的 DLL 到底怎么调、头文件里那些函数签名是什么意思基本是个黑匣子。GenerateDynamicLib 这个资源包就是来收拾这个局面的。它把“从 Simulink 模型生成动态链接库”这件事做成了固定套路模型侧参数怎么设、生成脚本怎么写、生成的 DLL 如何被外部 C 程序调用以及常见的报错怎么排查全部打包在一起。适合用 Simulink 做算法原型、又需要把算法部署到非 Simulink 环境里的工程师尤其是刚接触代码生成的人照着流程走能少踩一半的坑。2. 资源拆解目录结构、依赖环境与关键技术选型拿到这个 rar 包之后第一件事不是解压就跑而是把目录结构看清楚。这套工程和我们平时随意丢几个 .slx 文件不一样它把模型、脚本、模板和文档分得清清楚楚按固定目录组织。我拆完之后发现它本质上是一个“最小可用的自动化工程模板”而不是单个脚本。2.1 目录结构逐层拆解解压后你会看到这样一个目录骨架GenerateDynamicLib/ ├── models/ │ └── add_sample.slx ├── scripts/ │ ├── generate_dll.m │ ├── check_environment.m │ └── batch_generate.m ├── templates/ │ ├── dll_call_template.c │ └── build_bridge_template.c ├── docs/ │ ├── 使用说明.md │ └── 接口定义.md └── README.mdmodels 目录里放的是一个加法示例模型 add_sample.slx输入是两个 double 类型的信号输出是它们的和。别小看这个模型它故意选得足够简单方便你把整个代码生成链路跑通后再替换成自己的算法。scripts 目录是核心check_environment.m 负责验证当前环境是否具备生成 DLL 的条件generate_dll.m 是真正干活的脚本batch_generate.m 则是给后面批量生成用的。templates 目录里的模板代码是给你做二次开发时用的参照docs 目录则写了接口定义和配置细节。这个目录结构的设计其实很有讲究模型、脚本、模板、文档分离意味着你可以不动脚本只换 models 下的模型文件或者不动模型只改脚本里的配置参数。更重要的是它把“环境检查”和“实际生成”拆成两个脚本这个习惯我在后来自己写自动化工具时也一直沿用因为生成失败有八成是环境问题提前检查能省掉大量重复报错。2.2 技术选型与版本选择这套流程依赖的核心技术栈是三样Simulink 负责建模Simulink Coder 负责把模型转换成 C 代码然后通过 C 编译器把 C 代码编译成 DLL。资源包里没有写死版本号但从脚本的写法可以判断它针对的是 R2018 之后使用 Simulink Coder 的常规做法。这里有一个关键点容易让新手懵生成 DLL 并不是 Simulink 直接输出一个 .dll 文件而是先由 Simulink Coder 生成一堆 C 源文件和头文件再用编译器把这堆文件编成动态链接库。所以你的机器上必须有一个可用的 C 编译器MATLAB 里通常用 MinGW-w64 或者 Visual Studio具体取决于你的操作系统和 MATLAB 版本。为什么选择生成 C 代码再编译而不是直接用 MATLAB Compiler 做 DLL因为 Simulink Coder 生成的 C 代码是纯算法逻辑不依赖 MATLAB 运行时外部程序只需要拿到头文件和 DLL 就能调用集成成本低。而且 ERTEmbedded Real-Time目标文件生成的代码风格固定接口函数命名有规律二次开发时很容易做脚本化处理。3. 核心链路模型配置、脚本生成与接口分析这一章是整个资源包的精华部分。我们要看清楚一个 Simulink 模型是怎么一步步变成 DLL 的。我把核心链路拆成三段模型侧配置、脚本触发构建、以及最终生成的 C 接口长什么样。任何一段出了问题后面生成的 DLL 都可能只能看不能用。3.1 模型侧的设置这一步错后面全白搭很多人第一次用 Simulink Coder 都会忽略模型配置里的求解器设置。资源包里的 check_environment.m 脚本会先帮你确认一堆模型参数其中最关键的是求解器类型、代码生成语言和目标文件系统。在模型窗口的 Code Generation 面板里你至少要保证这么几个参数是合理的% 配置模型为固定步长求解 set_param(add_sample, SolverType, Fixed-step); set_param(add_sample, FixedStep, 0.01); % 代码生成语言用 C set_param(add_sample, TargetLang, C); % 目标文件系统使用 ERT而不是默认的 GRT set_param(add_sample, SystemTargetFile, ert.tlc); % 设置为仅生成代码不直接编译 set_param(add_sample, GenCodeOnly, on);这里每个参数都是有实际意义的。SolverType 必须设为 Fixed-step因为生成 DLL 是一个周期调用的算法模块外部程序会在自己的循环里按固定周期调用它如果模型里用变步长求解器生成出来的代码在外部实时环境下无法保证稳定运行。SystemTargetFile 选择 ert.tlc 很关键它生成的代码专门针对嵌入式实时场景接口更干净内存占用也更小比默认的 GRT 更适合做 DLL。GenCodeOnly 设为 on 的意思是只生成 C 源码不让 Simulink 自动调用编译器。为什么不在这一步直接编译因为当你把生成和编译分开后才能灵活控制编译选项比如指定输出目录、决定是否生成独立 DLL、或者把多个模型编到一个库里。资源包里的脚本正是这么设计的。3.2 生成脚本的核心实现从模型到产物接下来看 generate_dll.m 脚本它是整个工程的引擎。脚本的逻辑可以归纳为清理上一次的生成结果、设置代码生成目录、把模型配置参数统一改到位、最后调用 Simulink Coder 的构建命令。function dllInfo generate_dll(modelName) % 指定代码生成输出目录避免污染原始模型路径 workFolder fullfile(pwd, build, modelName); if exist(workFolder, dir) rmdir(workFolder, s); end mkdir(workFolder); % 将生成目录指向 workFolder Simulink.fileGenControl(set, CodeGenFolder, workFolder, ... CacheFolder, workFolder, CreateDir, true); % 统一模型配置 set_param(modelName, SolverType, Fixed-step); set_param(modelName, FixedStep, 0.01); set_param(modelName, TargetLang, C); set_param(modelName, SystemTargetFile, ert.tlc); set_param(modelName, GenCodeOnly, off); set_param(modelName, RTWVerbose, off); % 触发代码生成与编译 slbuild(modelName); % 返回生成产物路径 dllInfo.dllPath fullfile(workFolder, modelName .dll); dllInfo.hdrPath fullfile(workFolder, modelName .h); end这段脚本有几个设计值得借鉴。首先是 workFolder 的清理动作代码生成是一个高度依赖中间文件的过程上一次的缓存文件会直接影响下一次构建结果所以先删掉再重建是血泪经验换来的习惯。Simulink.fileGenControl 把生成目录隔离到 build 子目录下这样原始模型路径不会被自动生成的一大堆代码污染。注意我把 GenCodeOnly 设成了 off因为这里我们不仅要代码还要 DLL。这个参数和前面模型配置里的 on 并不矛盾模型里只负责把状态存下来脚本里再根据实际需要覆盖它最终以脚本执行为准。RTWVerbose 设为 off 是让构建日志更清爽否则屏幕上会刷出几百行编译流水账不利于排查问题。slbuild 执行完后DLL 和头文件都会出现在 workFolder 里。如果你的模型名和 DLL 名不一致比如想生成 add_lib.dll 而模型叫 add_sample就得在脚本里再设置输出文件名这一点在后面的二次开发小节里我会给出具体做法。3.3 接口分析生成的 DLL 里到底有什么生成完的 DLL 不是拿到就能胡乱调用的你得知道头文件里暴露了哪些函数。用 ERT 目标生成的代码每个模型会暴露三个核心函数初始化函数、单步执行函数、终止函数。以 add_sample 模型为例生成的 add_sample.h 中会声明类似下面的接口/* 初始化模型 */ void add_sample_initialize(void); /* 单步执行每个采样周期调用一次 */ void add_sample_step(double a, double b, double *y); /* 释放模型资源 */ void add_sample_terminate(void);这种接口设计的背后逻辑很清晰。initialize 负责初始化模型内部状态step 函数对应 Simulink 模型里的一个采样周期计算a 和 b 对应模型的两个输入端口y 是指针形式的输出参数对应模型的输出端口。terminate 在程序退出时调用释放内部申请的缓存。从调用者的视角来看DLL 里真正要关心的只有 step 函数的签名。这里的输入输出类型必须和你在 Simulink 模型里定义的端口数据类型一致。如果模型输入是 single那这里就是 float如果是 int16那这里就是 short。接口定义文档 docs/接口定义.md 里就写清楚了这些对应关系避免你去猜。还有一个容易被忽略的点ERT 生成的代码可能有标准头文件依赖比如 stdint.h 或 stddef.h外部程序引入头文件时需要把编译环境的 include 路径指到生成目录。资源包里的模板 C 文件已经帮你处理了 include 路径的写法后面调用验证一章我会展示完整例子。4. 实例运行从模型到 DLL 的完整落地步骤前面的原理分析可能让你觉得“大概懂了”但真正动手时你会发现每一步都有细节。这一章我按实际操作顺序把从解压到 DLL 能被外部程序调用的全过程走一遍。强烈建议你不要跳步尤其是环境校验这一步很多人就是跳过它才一路翻车。4.1 环境准备与参数校验第一步不是打开 MATLAB而是先确认机器上有没有可用的 C 编译器。在 MATLAB 命令行执行mex -setup如果弹出窗口让你选择编译器说明系统里至少有一个编译器可用。如果提示找不到支持的编译器就需要先去装 MinGW-w64 或者 Visual Studio注意选择与 MATLAB 版本兼容的编译器工具链。这一步是生成 DLL 最大的拦路虎我见过太多同事卡在这里大半天。环境确认之后把当前目录切到资源包解压目录然后运行check_environment这个脚本会检查三件事模型文件是否存在、MATLAB 版本是否支持 Simulink Coder、以及代码生成目录是否可写。它还会输出一段环境摘要告诉你哪些参数会在下一步被脚本自动覆盖。如果这段摘要里出现红色警告一定要先解决不要硬往下走。4.2 三步生成 DLL环境没问题后生成过程其实只有三步但每一步都要看懂它在做什么。第一步打开示例模型确认它能够正常仿真。直接在 MATLAB 命令行运行open_system(add_sample);双击模型点击运行按钮观察输出结果是否为两个输入之和。这一步很多人觉得多余但模型实际上必须能够仿真通过才能进入代码生成阶段否则 Simulink Coder 会因为模型内部有错误直接终止构建。第二步运行生成脚本generate_dll(add_sample)脚本执行过程中MATLAB 状态栏会显示构建进度。第一次构建通常会持续一两分钟因为背后要执行编译器的完整编译和链接过程。这个过程中不要中断操作也不要切换目录否则会造成缓存锁冲突。第三步去 build/add_sample 目录下检查生成结果。正常情况下你会看到 add_sample.h、add_sample.c、add_sample.dll以及一大堆 ert_main.c、add_sample_data.c 之类的支撑文件。这些支撑文件不需要你手动管理编译器在链接 DLL 时已经把它们打包进去了。你真正要关心的只有 .dll 和 .h。4.3 在外部 C 程序中调用验证拿到 DLL 后别急着在 MATLAB 里验证我们要在纯 C 环境里调用它才能确认它是可以被外部程序使用的独立产物。资源包 templates 目录里有一个现成的模板核心代码是这样的#include add_sample.h #include stdio.h int main(void) { double x1 1.5; double x2 2.5; double y 0.0; /* 初始化算法模块 */ add_sample_initialize(); /* 调用一次单步计算 */ add_sample_step(x1, x2, y); printf(Result: %f\n, y); /* 释放资源 */ add_sample_terminate(); return 0; }编译这个程序时需要把 add_sample.h 所在的 build/add_sample 目录加入头文件搜索路径并把 add_sample.dll 放在当前工作目录或者系统路径下。在 Windows 下用 MinGW 编译的命令大概是gcc main.c -I build/add_sample -L build/add_sample -ladd_sample -o test_alg.exe这里 -ladd_sample 告诉链接器去链接 add_sample.dll 的导入库如果是 Visual Studio 环境则对应加 .lib 文件。-I 和 -L 分别指定头文件和库文件的搜索路径这是很多第一次接触 DLL 调用的开发者最容易忽略的两个参数。编译成功并运行 test_alg.exe 后屏幕上会输出 Result: 4.000000整个链路就算真正跑通了。5. 避坑排查生成 DLL 过程中最常见的六个问题这一章是我拆这个资源包时最想写的内容。代码生成这个领域报错信息往往不太说人话很多问题看起来是模型问题实际是配置问题看起来是代码问题实际是环境问题。我把最常见的问题按现象、原因、解决三段式总结出来算是一份排雷清单。5.1 构建报错“编译器未找到”现象是 slbuild 执行后立刻报错错误信息类似 “No supported compiler detected”但之前mex -setup又能正常选择编译器。原因是 Simulink Coder 的代码生成环境与 mex 的编译器配置并不完全共享Simulink Coder 需要额外识别TMW_COMPILER环境变量或配置信息。解决方法是先运行mex -setup C指定一次 C 编译器然后重启 MATLAB再执行一次check_environment确认环境摘要中没有红色警告。记住每次换编译器之后都要重启 MATLAB这个顺序不能变。5.2 生成了代码但没有 DLL现象是 build 目录下只有 C 文件和 .h 文件却没有 .dll 文件整个过程看起来成功了但没有产物。原因是GenCodeOnly参数被设为了 on或者目标文件系统配置成了不支持生成本地可执行文件的模式。解决方法是检查 generate_dll.m 里的配置把GenCodeOnly显式设为 off同时确认 SystemTargetFile 是 ert.tlc。如果你是从代码生成面板手动操作的需要到 Configuration Parameters 里找到 “Generate code only” 复选框确保它是未勾选状态。5.3 外部程序链接时报“符号未定义”现象是编译调用程序时链接器报错找不到add_sample_step等符号。原因通常有两个要么是编译器没有正确链接 DLL 的导入库要么是 DLL 和调用程序的位数不一致比如 64 位 MATLAB 生成的 DLL被 32 位的调用程序链接符号表完全对不上。解决方法是首先检查调用方程序的位数必须与 MATLAB 安装版本位数一致其次确认链接命令里的库路径确实指向了生成的 .lib 或 .dll最后用nm命令或者 Visual Studio 的 dumpbin 查看 DLL 导出符号表确认函数名称没有被修饰成 C 风格。如果函数名被修饰了就让 DLL 编译方改成纯 C 接口或增加 extern C 声明。5.4 DLL 被上位机加载后崩溃现象是程序启动时能加载 DLL但第一次调用 step 函数就崩溃。这个问题往往不在生成过程而在模型设计。如果 Simulink 模型里使用了连续状态变量或者变步长采样生成的 step 函数会在内部动态分配内存每次调用都会产生额外开销并且在外部程序退出时如果没有正确调用 terminate内存就会泄漏。解决方法是给你的算法模块在模型里显式配置为离散系统设置固定采样周期并确保模型里没有异步驱动模块。另外外部程序里一定要配对调用 initialize 和 terminate这也是模板代码那么写的原因。5.5 输入输出参数类型对不上现象是很久没有报错但调用结果明显不对比如传了一个 1 进去出来的是疯值。原因在于模型端口的数据类型是 int16 或者 single而外部程序里按 double 传参。Simulink Coder 生成的接口里step 函数的参数类型是严格与模型端口类型绑定的它不会强制做类型转换。解决办法是在模型里把输入端口的 Output Data Type 设置为 double或者在外部程序里按照头文件里的真实类型去定义变量。这里头文件就是你最好的参考依据不要凭经验猜测打开 add_sample.h 看函数声明才是正解。5.6 构建目录缓存导致的诡异错误现象是某次修改模型配置之后重新生成报错里夹杂着旧模型的文件路径甚至会出现找不到某个头文件的错误。原因是 Simulink Coder 的构建缓存和中间文件残留上一次生成时遗留下的.cache文件干扰了本次构建。这种问题非常玄学但解决办法很单一清空 build 目录手动删除slprj文件夹然后重启 MATLAB 再重新生成。资源包里的 generate_dll.m 之所以开头就做目录清理就是提前帮你规避这个问题。6. 二次开发把固定流程改造成自己的自动化工具前面的内容已经把固定流程跑通但真实项目里你不会只生成一个 DLL。你可能有十几个模型要生成还要输出到不同目录、起不同的名字。这一章我讲一下怎么在这个资源包的基础上做二次开发把它变成一个趁手的批量生成工具。6.1 参数化改造把 generate_dll.m 的参数从固定值变成函数参数是最基础的一步。比如把输出 DLL 名拆出来支持传一个自定义的名字进去function dllInfo generate_dll(modelName, dllName) if nargin 2 dllName modelName; end % 输出文件名覆盖逻辑 set_param(modelName, CodeGenDirectory, ... fullfile(pwd, build, modelName)); % ... 原有配置逻辑 ... slbuild(modelName); % 将生成的 dll 改名 dllPath fullfile(workFolder, dllName .dll); copyfile(fullfile(workFolder, modelName .dll), dllPath); end这样生成 add_sample 模型时可以指定生成 add_core.dll然后在外部程序里用 add_core.h 里的接口函数。这个改法让我在集成多个模型时省了很多手工改名的时间。注意 copyfile 之后原文件也还在不影响任何一方编译。6.2 批量生成的脚本配合配合资源包里的 batch_generate.m可以一次性处理多个模型。我在实际使用中会维护一个模型清单列表脚本逐个调用参数化后的生成函数models {add_sample, pi_controller, filter_lowpass}; for i 1:length(models) try generate_dll(models{i}, [models{i}, _lib]); fprintf(模型 %s 生成成功\n, models{i}); catch err fprintf(模型 %s 生成失败: %s\n, models{i}, err.message); end end这段脚本的容错处理非常关键。生产中如果某个模型生成失败不能让整个循环中断而是记录错误继续处理下一个模型。日志里可以看到哪些模型成功、哪些失败方便统一排查。这里还体现了一个开发习惯把所有模型和 DLL 名的映射关系放在脚本开头集中维护而不是散落在各个代码块里。6.3 用命令行模式接入持续集成如果你想把这套生成流程放进 CI 流水线资源包里的 MATLAB 脚本已经可以支持命令行免交互运行。在系统命令行直接执行matlab -batch generate_dll(add_sample, add_core)-batch模式会自动启动 MATLAB、执行代码、然后退出全程不需要人工打开 MATLAB 界面。输出日志可以直接重定向到流水线的文本输出里配合上面的 try-catch 逻辑每个模型的成败一目了然。我第一次在实际项目里做大规模 DLL 生成时没有提前设计好目录隔离结果所有模型的生成文件全部叠加在同一个 slprj 目录下导致多次出现 5.6 里的缓存错误浪费了整整一天去清缓存。从那以后我每次设计自动生成工具时都会强制在脚本开头做“清理目录、检查环境、逐个构建”这三步再也没出现过那种莫名其妙的构建失败。希望这份拆解也能帮你在 Simulink DLL 生成这条路上少踩几个坑。本文还有配套的精品资源点击获取