ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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.md

models 目录里放的是一个加法示例模型 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 就能调用,集成成本低。而且 ERT(Embedded 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 生成这条路上少踩几个坑。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进