☰
Simulink调用DLL实战:从GenerateDynamicLib.rar到S-Function与C Caller
2026/10/15 1:13:30 网站建设 项目流程

简介:本资源面向需要在Simulink环境中生成并调用动态链接库的工程师与研究人员,围绕模型代码生成、DLL封装及外部集成这一常见任务,提供可复用的工程示例与配套说明。压缩包共127个文件,约400KB,涵盖34个html与20个js构成的报告页面、18个mat与2个slx模型数据、7个h头文件与5个c源文件、2个dll与2个lib库文件,以及mk、bat、rsp等编译脚本和mexw64、obj等中间产物,完整呈现从模型配置到库文件产出的目录结构。资源中涉及代码生成接口设置、S-Function接口编写、外部函数链接与输入输出类型匹配等关键环节,并附有错误处理与跨平台ABI注意事项,便于读者对照复现编译流程、排查链接问题。目前已有799人学习下载,适合具备一定Simulink与C语言基础、希望实现模块化设计与非Simulink代码集成的开发者参考。

1. 从一份只有名字的压缩包说起:Simulink 里那个 DLL 到底怎么接

手头拿到一个叫GenerateDynamicLib.rar的包,没有说明文档,没有 README,解压出来大概率是一堆.c、.h、.mk或者.bat,外加一个已经编好的.dll。很多人第一反应是「这玩意儿能干嘛」,第二反应是「Simulink 里怎么把它挂上去」。这两个问题其实是一件事:Simulink 调用动态链接库(DLL),本质上是让仿真模型在运行时去加载一个外部编译好的二进制模块,通过约定的函数入口做数据交换。

它解决的是纯 Simulink 模块搞不定的场景:你有一段用 C 写好的控制算法、一个第三方提供的硬件驱动、或者一个不想暴露源码的加密模块,需要塞进 Simulink 的仿真回路里跑。适合谁?做电机控制、车载 ECU 原型、信号处理链路验证的工程师,尤其是那种「算法在 C 里已经跑通了,现在要放进 Simulink 做系统级联调」的场合。这份资源的价值不在于它多复杂,而在于它把「生成 DLL」和「在 Simulink 里调用」这两步之间的空白填上了——很多人卡就卡在编译出来的 DLL 符号名不对、调用约定不匹配、或者 Simulink 根本找不到入口点。

2. Simulink 加载 DLL 的三种路径:S-Function、Legacy Code Tool 与 C Caller

2.1 先搞清楚你的 DLL 是给谁用的

Simulink 调 DLL 不是只有一条路。选错路径,后面全是玄学问题。常见做法是看你的 DLL 导出的是什么函数:如果是标准的 C 函数,参数是基本类型(double、int、指针),优先考虑C Caller 模块;如果 DLL 里封装的是一个带状态的系统,需要mdlInitializeSizes、mdlOutputs这类回调,那得走S-Function或者Legacy Code Tool包装。

GenerateDynamicLib.rar这个命名暗示它偏向「生成」侧,也就是它可能包含一套模板或脚本,帮你把 C 源码编译成符合 Simulink 调用规范的 DLL。我一般会先解压看目录结构:有.tlc文件的基本是 Legacy Code Tool 路线;有sfun_前缀或者mdl开头的函数,就是手写 S-Function;如果只有干净的extern "C"导出函数,那 C Caller 最省事。

三条路径的取舍可以用一张表说清楚:

路径适用场景需要改 C 代码编译依赖调试难度
C Caller无状态函数、纯计算不需要仅需 DLL 和头文件低
Legacy Code Tool有状态、需生成 S-Function少量,加包装声明需要 .tlc 和编译器中
手写 S-Function复杂接口、自定义采样需要,实现回调需要 mex 编译高

选型理由很直接:能不动 C 代码就不动。C Caller 是 MathWorks 后来推的方式,配置最少,但要求函数必须是可重入的、不能有全局状态。如果你的算法里有静态变量或者需要保持上一次的输出,C Caller 会翻车,这时候必须上 Legacy Code Tool 或者手写 S-Function。

2.2 用 Legacy Code Tool 把 C 源码包成 S-Function

假设GenerateDynamicLib.rar里有一个myfilter.c和myfilter.h,导出了一个void myfilter_step(double u, double *y)函数。用 Legacy Code Tool 的典型流程是在 MATLAB 命令窗里写一段定义脚本:

% 定义 Legacy Code Tool 规格 def = legacy_code('initialize'); def.SFunctionName = 'sfun_myfilter'; % 生成的 S-Function 名 def.HeaderFiles = {'myfilter.h'}; % 头文件路径 def.SourceFiles = {'myfilter.c'}; % 源文件路径 def.IncPaths = {pwd}; % 包含路径 def.SrcPaths = {pwd}; % 源文件路径 def.Options.language = 'C'; % 语言选 C,别选 C++ def.StartFcnSpec = 'void myfilter_init()'; % 初始化函数,没有就留空 def.OutputFcnSpec = 'void myfilter_step(double u1, double y1[1])'; % 输出回调 def.SampleTime = '-1'; % 继承采样时间 % 生成 S-Function 源文件 legacy_code('generate_for_sim', def); % 编译成 mex 文件 legacy_code('compile', def); % 生成 masked block,方便拖进模型 legacy_code('slblock_generate', def);

这段脚本的逻辑是:先告诉工具你的 C 函数长什么样,OutputFcnSpec里的u1和y1对应 Simulink 端口的输入输出,y1[1]表示输出是一个长度为 1 的数组。SampleTime设为-1表示继承上游模块的采样时间,如果你的算法有固定步长需求,改成具体数值比如0.001。编译成功后会在当前目录生成sfun_myfilter.mexw64(Windows)或.mexa64(Linux),以及一个.slx里可用的 masked block。

参数说明:StartFcnSpec不是必须的,但如果你的 C 代码里有初始化逻辑(比如分配内存、清零状态),必须在这里声明,否则第一次运行输出是随机值。IncPaths和SrcPaths如果文件不在当前目录,要写绝对路径,相对路径在 Simulink 的编译上下文里经常找不到,这是血泪经验。

2.3 C Caller 模块的配置与限制

如果 DLL 导出的是干净的无状态函数,C Caller 更直接。在 Simulink 库里搜C Caller,拖进模型,双击配置:

  • Library file:选你的.dll文件
  • Header file:选对应的.h
  • Function:下拉框会自动列出 DLL 里导出的函数
  • Port specification:根据函数签名自动映射输入输出端口

但 C Caller 有几个硬限制:不支持可变参数函数、不支持返回结构体、不支持函数指针作为参数。如果你的 DLL 函数签名里有void*或者回调指针,C Caller 会直接报错,这时候只能退回 Legacy Code Tool 或者手写 S-Function 包装层。

提示:C Caller 在仿真模式下能跑,但生成代码(Embedded Coder)时对 DLL 的链接方式有额外要求,如果目标是嵌入式部署,一开始就选 Legacy Code Tool 更稳妥。

3. 从 .rar 到可调用 DLL:编译链路与符号导出排查

3.1 解压后先看什么:文件清单与编译脚本

拿到GenerateDynamicLib.rar,解压后别急着编译。先列目录,重点看这几类文件:

  • .c/.cpp:算法实现,确认有没有__declspec(dllexport)或者.def文件
  • .h:函数声明,看参数类型和调用约定(__cdecl还是__stdcall)
  • .bat/.sh/Makefile:编译脚本,里面藏着编译器选项和输出路径
  • .tlc:Legacy Code Tool 的目标语言编译器文件,有它说明作者已经准备好了 Simulink 集成
  • .dll:预编译好的二进制,如果源码编译不过,可以先拿它验证调用链路

常见做法是先用dumpbin /exports your.dll(Windows)或者nm -D your.so(Linux)看导出符号表。如果函数名被 C++ 编译器 name mangling 了,比如变成?myfilter_step@@YAXNPEAN@Z,那 Simulink 端根本找不到。解决办法是在头文件里加extern "C",或者编译时用.def文件显式指定导出名。

3.2 编译命令与调用约定匹配

假设包里有一个build.bat,内容大概是:

@echo off REM 用 MSVC 编译成 DLL,注意 /LD 生成动态库 cl /LD /Fe:myfilter.dll myfilter.c /I"C:\Program Files\MATLAB\R2023b\extern\include" REM 如果要用 Legacy Code Tool,还需要 mex 编译 S-Function mex -largeArrayDims sfun_myfilter.c myfilter.c

这里的关键参数:/LD告诉编译器生成 DLL 而不是 EXE;/I指定 MATLAB 的头文件路径,因为 S-Function 需要mex.h和simstruc.h。如果你的 MATLAB 版本是 R2020a 之前,-largeArrayDims可能不需要,但新版本默认就是大数组 API,加上更保险。

调用约定是另一个翻车高发区。Simulink 的 S-Function 默认用__cdecl,如果你的 DLL 是用__stdcall编译的(Windows API 常见),链接时会报unresolved external symbol。解决办法是在函数声明前统一加__cdecl,或者在 Legacy Code Tool 的Options里指定def.Options.useStdCall = false。

3.3 验证 DLL 能否被 MATLAB 加载

在把 DLL 拖进 Simulink 之前,先在 MATLAB 命令窗里用loadlibrary试一下:

% 加载 DLL,检查是否报错 if not(libisloaded('myfilter')) loadlibrary('myfilter.dll', 'myfilter.h', 'alias', 'myfilter'); end % 查看导出的函数 libfunctions('myfilter'); % 调用一次测试函数 y = calllib('myfilter', 'myfilter_step', 1.0, 0); disp(y); % 卸载 unloadlibrary('myfilter');

如果loadlibrary报The specified module could not be found,通常是 DLL 依赖的其他库不在PATH里,用Dependency Walker或者ldd查依赖链。如果报Undefined function,说明符号名不对,回去检查extern "C"和.def文件。这一步过了,再进 Simulink 配置,能省掉大量「模型报错但不知道哪错」的时间。

4. 避坑与排查:DLL 在 Simulink 里跑不起来的五个典型场景

4.1 现象:模型编译通过,仿真一跑就崩溃

原因:DLL 里的函数有全局状态,但 Simulink 在仿真步进时可能多次调用同一个函数,状态没重置。或者 DLL 内部有静态缓冲区,多线程仿真时被并发访问。

解决:在 Legacy Code Tool 的StartFcnSpec里加初始化调用,确保每次仿真开始前状态清零。如果 DLL 不是线程安全的,在 Simulink 的求解器设置里把「允许任务并发」关掉,或者改用单线程求解器。

4.2 现象:输出一直是零,或者第一次有值后面全是零

原因:OutputFcnSpec里的参数映射写错了。比如 C 函数是void step(double u, double *y),但你在 spec 里写成了void step(double u1, double y1),少写了[1],Simulink 会把y1当成标量传值而不是指针,函数内部写不回去。

解决:输出参数必须写成y1[1]或者y1[n],表示这是一个数组指针。输入参数如果是数组,也要写u1[1]。标量输入可以写u1,但为了统一,我一般全写成数组形式。

4.3 现象:Legacy Code Tool 编译报错undefined reference to 'mexFunction'

原因:你试图用mex命令直接编译一个纯 C 文件,但那个文件里没有mexFunction入口。Legacy Code Tool 生成的 S-Function 包装文件才有mexFunction,纯算法文件不需要单独 mex 编译。

解决:只对sfun_*.c文件执行mex,算法文件通过def.SourceFiles一起链接进去。如果手动编译,确保命令行里同时包含 S-Function 包装文件和算法文件。

4.4 现象:C Caller 模块下拉框里找不到函数

原因:DLL 导出的是 C++ 修饰名,或者函数被static修饰了,或者头文件里的声明和 DLL 实际导出的签名不一致。

解决:用dumpbin /exports确认导出名,如果是修饰名,在头文件里加extern "C"重新编译。如果是static函数,去掉static或者换一个非 static 的包装函数导出。

4.5 现象:仿真能跑,但生成代码时链接失败

原因:Simulink 仿真时通过 MATLAB 的loadlibrary动态加载 DLL,但代码生成时需要静态链接或者显式指定库路径。Embedded Coder 不知道你的 DLL 在哪。

解决:在模型配置的「代码生成 > 自定义代码」里,把 DLL 对应的.lib文件(Windows)或.a文件(Linux)加到「附加库」列表,头文件路径加到「附加包含目录」。如果只有 DLL 没有 LIB,需要用dumpbin /exports生成一个.def再lib /def生成导入库。

注意:32 位和 64 位不匹配是另一个隐形杀手。MATLAB 是 64 位的,你的 DLL 也必须是 64 位,否则loadlibrary直接报「不是有效的 Win32 应用程序」。编译时确认目标平台。

5. 进阶:用 DLL 做硬件在环与参数在线调优的一个小技巧

前面讲的都是「让 DLL 跑起来」,但实际项目里更常见的是「跑起来之后还要能调」。如果你的 DLL 里算法参数是写死的,每次改参数都要重新编译 DLL、重启仿真,效率极低。一个实用的技巧是在 DLL 里预留一个参数设置函数,通过 Simulink 的set_param或者直接调用calllib在仿真过程中动态修改。

具体做法:在 C 代码里加一个void set_gain(double g)函数,把内部增益存到一个全局变量里。然后在 Simulink 模型里加一个MATLAB Function模块,里面调用calllib('myfilter', 'set_gain', new_gain)。这样仿真运行时,你可以通过滑块或者脚本实时改参数,不用重新编译。

% 在 MATLAB Function 模块里动态设置 DLL 内部参数 function set_dll_gain(g) coder.extrinsic('calllib'); if libisloaded('myfilter') calllib('myfilter', 'set_gain', g); end end

注意coder.extrinsic声明,否则代码生成时会报错。这个方式在硬件在环(HIL)场景下特别有用:真实控制器跑在目标机上,Simulink 里的被控对象模型通过 DLL 加载,你可以一边跑一边调被控对象参数,观察控制器响应。

验证方法:跑一个阶跃响应,在仿真过程中把增益从 1 改到 2,看输出幅值是否跟着变。如果没变,检查libisloaded是否返回 true,以及set_gain是否真的修改了 DLL 内部的全局变量(有些编译器会把全局变量优化掉,加volatile修饰)。

从那以后我每次拿到一个带 DLL 的 Simulink 集成包,都强制走一遍「dumpbin 看符号 → loadlibrary 测调用 → Legacy Code Tool 生成 S-Function → 仿真验证 → 代码生成验证」这五步,少一步后面就可能翻车。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询