资讯动态

Python调用Simulink模型DLL:从配置到ctypes绑定的完整指南

发布时间:2026/9/7 13:24:47 来源:尧图企业网站定制
简介python_SimulinkDLL是一套面向Simulink开发者和自动化测试工程师的示例工程展示如何将Simulink模型编译为DLL再通过Python调用并复用其算法。资源共138个文件大小仅876KB虽然精简但覆盖了核心链路c/h源文件与ert_main.c等生成代码、slx/mdl模型文件、m脚本、py调用示例、ipynb交互式说明以及构建所需的dll/so、bat与txt文档便于对照理解从模型配置到Python包装的完整流程。资源包含bouncing_ball、discrete_tf、dllModel等多个demo读者可借此掌握模型封装、接口生成、Python调用以及循环中测试MIL的关键步骤。适合希望在测试环境中减少Matlab/Simulink开销或将算法分发给无许可证同事的团队。已有1815人学习下载。 做控制算法的人基本都会遇到这个阶段模型在 Simulink 里跑得挺好可一旦要脱离 MATLAB 环境批量做参数扫描、算法对比或者把模型直接集成到自己的测试工具链里就开始头疼了。我最近在搭一套离线参数标定平台核心思路就是把 Simulink 模型编译成 DLL然后用 Python 去加载这个 DLL 来跑仿真。实践证明这套方案跑通之后一个控制模型的执行速度比 Simulink 软实时快一个量级而且整个流程完全不依赖 MATLAB 运行时测试机上装个 Python 就能用。今天就把这套“Python Simulink DLL”的完整链路整理出来从模型配置、代码生成、DLL 编译到 ctypes 绑定全部走一遍附上我踩过的坑。这套方案的价值不在于把模型“导出来”这么简单。它解决的是工程上很常见的一个矛盾Simulink 建模和调参效率高但真实项目里的数据分析、优化算法、自动化测试大部分是 Python 生态的。让两边各自干自己擅长的事比强迫团队统一技术栈要实际得多。文章面向的是有一定 Simulink 基础、想打通 Python 和模型仿真边界的工程师不需要你精通 C 语言只要能看懂函数调用就行。1. 项目背景为什么非要把模型变成 DLL1.1 传统做法的三个痛点我最早也是用 MATLAB 引擎matlab.engine在 Python 里跑 Simulink 模型的这种方式最直接但痛感也最明显。第一是速度每次仿真从启动引擎到sim()返回结果光进程开销就有好几秒做参数优化时跑几千组仿真时间直接爆炸。第二是环境依赖测试同事的机器上必须装完整 MATLAB光 license 和安装体积就劝退不少人。第三是版本管理MATLAB 一个大版本升级后原来能跑的脚本很可能因为 Simulink 内部接口变化而报错连带 Python 侧代码一起动维护成本相当高。把模型编译成 DLL 后这三个问题基本消失。DLL 是标准的动态链接库任何语言都能调Python 用 ctypes 或者 cffi 就能加载不需要额外装任何 MATLAB 组件。而且生成的 C 代码是模型编译后的产物不存在 MATLAB 版本兼容问题只要有编译器放哪儿都能编。1.2 为什么调用方偏偏选 Python有人会问既然都编译成 DLL 了直接用 C/C 调用不是更快吗确实更快但现实是现在做算法验证、数据分析、自动化测试的主流脚本语言就是 Python。NumPy 处理数组、pandas 管理数据、matplotlib 出图、scipy 做优化这一套组合拳打下来写业务逻辑的效率比 C/C 高太多了。我这边实际项目里Python 端负责三件事生成输入激励信号正弦扫频、阶跃、随机噪声把输出数据整理成 DataFrame 存库然后用 scipy 的优化器自动调模型参数。这三件事如果用 C 写工作量至少翻三倍。Python 做“调度和数据处理”DLL 做“核心计算”这个分工非常合理。1.3 这个方案的边界不是所有模型都适合走这条路。我的经验是离散定步长模型最适合连续模型也能做但要特殊处理。模型里如果有手写 S-Function 依赖特定 MATLAB 环境或者用了只有 Simulink 运行时才能提供的模块某些通信库、状态流的高级画布特性编译成 DLL 的时候就会比较痛苦。实时性要求极高、必须跑在嵌入式目标上的场景比如硬件在环也不适合用 Python 这种非实时语言去调应该直接部署到控制器里。2. 整体方案设计技术链路和工具选型2.1 从模型到 Python 调用的完整链路整个流程分四步先在 Simulink 里把模型配置成适合代码生成的状态然后生成 C 代码接着用编译器把 C 代码编成 DLL最后在 Python 里通过 ctypes 加载 DLL 并调用。听起来简单但每一环都有决定成败的细节。我用一个非常容易复现的例子来讲在 Simulink 里搭一个一阶低通滤波模块采样时间设成 0.01 秒模型命名lp_filter外部只暴露一个输入端口和一个输出端口。这个模型简单到十几秒就能搭完但它完整走一遍这套流程后你就能把方法平移到任何更复杂的模型上比如车辆动力学模型、电机控制模型或者电池等效电路模型。2.2 编译器以及绑定方式的取舍编译 DLL 在 Windows 上首选 Visual Studio 的 cl.exe。原因是 MATLAB 生成的代码大量使用了 Microsoft 的扩展语法和宏定义MSVC 兼容性最好。MinGW-w64 的 gcc 也能编但对于新手来说VS 的环境变量、SDK 路径、库路径都是配好的踩坑少很多。Python 绑定侧我用 ctypes而不是 pybind11 或者 Cython。原因是 ctypes 是 Python 标准库不需要额外编译任何 C 扩展写个结构体映射就能调链路最短对模型开发者最友好。2.3 三个替代方案的真实对比我可以负责任地说在动手前我至少评估过三条替代路线。一是 MATLAB Compiler SDK 直接生成 Python 包限制少但必须要对应的 MATLAB Compiler license而且生成的包依赖 MATLAB Runtime体积几百 MB太重。二是 FMUFunctional Mock-up Unit导出标准统一但需要额外安装 FMU 支持包而且很多 Python FMU 库性能一般。三是直接把 Simulink 模型手动翻译成 Python等值换算费时费力还得反复验证数值一致性模型一旦更新翻译工作全部重来完全不可维护。相比之下生成 DLL ctypes 是成本最低、效果最可控的方案。3. Simulink 模型代码生成实操3.1 模型侧必须改的三个关键配置生成 DLL 之前模型的配置是成败的关键。第一求解器必须改成定步长Fixed-step离散。我见过太多人拿默认的变步长连续求解器去生成代码结果生成的 step 函数里根本没有积分逻辑跑出来的输出完全不对。定步长离散模式下每次调用 step 函数就是推进一个采样周期行为最可预测。第二系统目标文件System target file要选ert.tlc也就是 Embedded Coder。这里有个前提你需要有 Embedded Coder 许可证。ERT 目标生成的代码非常干净几乎零运行时依赖独立编译成 DLL 最省事。如果只有 Simulink Coder 许可证那就只能选grt.tlc代码也能生成但里面依赖rt_sim.c等一套运行时文件手动编译 DLL 时要额外包含进去步骤繁琐很多我建议条件允许就直接用 ert.tlc。第三在代码生成选项里把 “Generate code only” 勾上避免 Simulink 在生成时自作主张帮你编译。另外如果希望生成的代码把输入输出结构体lp_filter_U、lp_filter_Y作为全局变量暴露出来需要在 Code Generation Interface 里勾选 “Generate C API from: Model data structures” 和 “Generate C API from: Model functions”。这一步很多人会漏掉漏了之后 wrapper 里访问不到这两个结构体DLL 就没法和 Python 交换数据了。3.2 生成代码后拿到了哪些文件点击 Build 之后工作目录下会生成一个lp_filter_ert_rtw文件夹里面就是全部代码。需要重点关注的文件有五个lp_filter.c实现模型核心算法lp_filter.h声明接口lp_filter_data.c存放参数和信号数据结构体lp_filter_types.h定义结构体类型rtwtypes.h定义real_T等数据类型。还有一个lp_filter.mk是自动生成的 Makefile 模板可以作为参考但手动编 DLL 时一般直接写个简单的编译脚本。3.3 把 C 代码编成 DLL 的两种编译方式第一种方式是在 Visual Studio 的 “x64 Native Tools Command Prompt” 里手动编译。进入lp_filter_ert_rtw目录用 cl 命令把lp_filter.c、lp_filter_data.c和自写的wrapper.c一起编成 DLL然后指定两个 include 路径一个是 MATLAB 安装目录下的rtw\c\src另一个是其extern\include。命令大致是cl /LD /I C:\Program Files\MATLAB\R2023a\rtw\c\src /I C:\Program Files\MATLAB\R2023a\extern\include /I . lp_filter.c lp_filter_data.c wrapper.c /Fe:lp_filter.dll第二种方式适合想要一个可复用构建工程的团队写一个 CMakeLists.txt用file(GLOB)把模型源文件收集起来再add_library(lp_filter SHARED ...)目标和 include 目录和上面命令一致。CMake 的好处是以后模型更新了重新跑一遍 configure 就能编出新的 DLL不用记那串又长又容易抄错的手动命令。无论哪种方式编译成功后都会得到一个独立的lp_filter.dll这个文件就是你要交付给 Python 侧的产物。4. Python 调用 DLL 的完整实现4.1 用 wrapper 封装对外接口最直接的做法是让 Python 直接访问 DLL 里导出的全局结构体变量lp_filter_U和lp_filter_Y。ctypes 完全可以做到但缺点是你得在 Python 侧重新定义和 Simulink 生成的 C 结构体内存布局一模一样的 structure模型端口一变Python 代码就要跟着改。开发中期模型频繁加输入这种写法非常累。我的做法是额外写一个 wrapper.c把对结构体的读写全部封装成几个简单的导出函数。这样 Python 端面对的接口非常稳定初始化函数、单步计算函数、参数设置函数不管 Simulink 模型内部怎么变只要输入输出端口不变Python 侧代码一行都不用动。wrapper.c 的完整内容大致如下#include lp_filter.h __declspec(dllexport) void lp_filter_init(void) { lp_filter_initialize(); } __declspec(dllexport) void lp_filter_do_step(const double *u, double *y) { lp_filter_U.In1 u[0]; lp_filter_step(); y[0] lp_filter_Y.Out1; }这里假设模型有一个输入端口和一个输出端口都是 double 类型。如果你的模型是多输入多输出就把u和y当成数组根据端口宽度循环赋值原理一模一样。4.2 ctypes 加载 DLL 和数组传参Python 端我直接用 ctypes 标准库。加载 DLL 用ctypes.CDLL然后必须显式声明lp_filter_do_step的参数类型不然默认会把所有参数当 32 位整数处理64 位的 double 数组指针传进去直接崩。输入输出数组我用 NumPy 创建指定dtypenp.float64再用arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double))拿到指针传给 DLL。这样做的好处是如果你想做批量仿真NumPy 数组既可以用起来像 C 数组又天然支持向量化运算生成不同工况的激励信号特别方便。核心绑定代码是import ctypes import numpy as np lib ctypes.CDLL(rD:\work\model_build\lp_filter.dll) lib.lp_filter_init() lib.lp_filter_do_step.argtypes [ ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double), ] lib.lp_filter_do_step.restype None注意如果你的 Python 是 64 位的DLL 也必须是 64 位编译出来的否则加载的时候会报[WinError 193]。这个坑很常见后面专门讲。4.3 批量仿真循环与结果回收有了上面的绑定写一个仿真循环非常自然。下面这段代码模拟的是 1 Hz 正弦信号通过低通滤波器步长 0.01 秒共 1000 步输出结果存到列表里并画出来u np.zeros((1,), dtypenp.float64) y np.zeros((1,), dtypenp.float64) u_p u.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) y_p y.ctypes.data_as(ctypes.POINTER(ctypes.c_double)) outs [] for i in range(1000): u[0] np.sin(2 * np.pi * 1.0 * i * 0.01) lib.lp_filter_do_step(u_p, y_p) outs.append(y[0]) import matplotlib.pyplot as plt plt.plot(outs) plt.show()这段代码的可扩展性在于你可以在循环里动态改变u[0]的值从而自由定义任意激励信号也可以在每次 step 之后读取outs[-1]做在线判断或存储。做参数扫描优化时通常会有两层循环外层用 scipy 的优化器生成一组候选参数内层跑完整个仿真序列返回一个代价函数值DLL 在每次内层仿真启动前重新初始化一次保证模型状态是干净的。4.4 两个进阶玩法实时控制和在线调参这套链路不仅能做离线仿真还能做“软实时”控制。如果模型的采样时间是 10 ms你可以在 Python 循环里调用lp_filter_do_step后用time.sleep(0.01)撑住节奏配合外部采集设备的数据输入就变成了一个简易的实时控制原型。当然这不是硬实时但用来验证控制逻辑完全足够了。另一个很实用的玩法是在线调参。Simulink 模型里的参数比如一阶低通滤波器的时间常数生成代码后会放在lp_filter_P结构体里。我封装 wrapper 时会加一个set_param(int param_id, double value)函数在 wrapper 内部把参数写回对应的结构体字段。这样 Python 端就具备了和 Simulink 外部模式类似的能力仿真中途改变模型的某个参数观察系统响应的实时变化。如果你们团队在做标定工作这个功能非常香。5. 常见问题与排查技巧实录5.1 编译阶段的典型报错我在编译阶段遇到的第一个问题是cannot open include file rtwtypes.h。原因很简单rtwtypes.h在rtw\c\src目录里不在模型代码目录里l 命令需要把 MATLAB 对应路径加到 /I 后面。第二个问题是直接用普通 cmd 窗口运行 cl结果提示cl 不是内部或外部命令。这个是因为 cl 依赖 Visual Studio 的环境变量必须从 Developer Command Prompt 启动或者用 vcvars64.bat 初始化环境。第三个问题更隐蔽模型没有生成lp_filter_data.c文件这是因为模型里没有任何参数或常量被标定为可调编译器优化掉了这个文件的生成。解决办法是在模型中任意一个模块参数上勾选 tunable或者直接忽略这个文件不参与编译也行。5.2 Python 加载 DLL 时的错误OSError: [WinError 193] %1 不是有效的 Win32 应用程序是这阶段最高频的错误几乎全是位数不匹配导致的。Python 是 64 位DLL 是 32 位或者反过来都会报这个错。排查方式很简单编译 DLL 时明确选择 x64 或 x86 的编译环境然后和 Python 位数对齐。还有一种错误是DLL load failed while importing ...此时 DLL 本身位数没错但依赖的 Microsoft Visual C 运行库缺失装上对应的 VC Redistributable 就能解决。另外DLL 路径里尽量不要有中文和空格避免一些不必要的编码问题。5.3 模型运行结果和 Simulink 对不上这是最让人抓狂的一类问题。明明代码编译成功Python 也跑起来了但输出曲线和 Simulink 里完全不一样。我的排查经验是优先检查模型求解器是不是定步长离散。如果是连续模型生成代码后的 step 函数里没有积分器输出自然不对。解决办法是把连续模块替换成对应的离散版本比如 Transfer Fcn 换成 Discrete Transfer Fcn然后重新生成。第二个常见原因是调用了 step 函数但没有先调用 init 函数模型的初始状态是内存垃圾前几步的输出完全随机。第三个原因是多速率模型。Simulink 里不同的模块采样时间不一样时生成的代码会有多个 step 函数比如lp_filter_step0和lp_filter_step1Python 侧必须按照实际速率关系去调度不能只调用其中一个。以下是一个简易的排查顺序表我每次遇到问题都会按这个流程过一遍现象首选排查方向解决办法编译报找不到头文件include 路径不全加上rtw\c\src和extern\includecl 命令不可用环境变量未初始化使用 VS Native Tools 命令行Python 报 WinError 193DLL 和 Python 位数不同统一编译目标位数和 Python 位数DLL 加载后提示缺运行库缺少 VC 运行时安装对应版本的 vc_redist仿真曲线和 Simulink 不一致模型用了连续求解器换成定步长离散求解器并验证多速率模型输出异常step 函数不止一个按采样速率调度多个 step 函数6. 几条个人经验总结这套方案我在三个项目里完整落过地踩了这么多坑之后有几句实在话想分享。第一不要在小模型上验证通过就以为万事大吉。我建议验证阶段专门用一个多输入多输出、多个采样速率的小模型走一遍全流程把最棘手的问题在最小范围内暴露出来比直接拿大模型调试省心得多。第二wrapper 层值得多花一点时间设计。输出接口一旦定义好模型再怎么更新Python 端都稳定如山。反过来如果图省事让 Python 直接碰结构体后面每次模型加输入输出都是一次联动改动非常磨人。第三如果团队里有人不愿意用 Python不太会配环境可以考虑把这些逻辑包成一个简单的命令行工具用 argparse 读取输入文件路径和参数输出结果写文件再用任何语言调用思路和 DLL 方案完全一致只是多了一层文件 IO。这个方案牺牲了一点性能但换来了极大的协作便利。最后再分享一个小技巧。DLL 文件放好后用 Python 也能快速检查这个 DLL 导出了哪些函数避免在 ctypes 里调用一个不存在的符号。用dumpbin /exports lp_filter.dllVisual Studio 自带或者 Python 里遍历lib._name_to_index就能看到。这个检查在 wrapper 接口改动后特别有用几秒钟就能确认导出接口是否符合预期。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价