Python调用Simulink编译DLL:零MATLAB依赖的轻量部署方案
简介本资源是一套面向Python开发者与嵌入式仿真工程师的Simulink模型加速实践方案聚焦于将Simulink模型编译为DLL并在Python中高效调用显著降低MIL模型在环测试对MATLAB/Simulink许可证和运行时环境的依赖。资源以Jupyter Notebook为核心载体配套C源码、头文件、批处理脚本及编译配置文件.rsp/.def/.bat完整呈现从模型导出、DLL构建到Python ctypes封装的全流程138个文件中8个Python脚本负责接口调用与测试7个C文件含模型核心逻辑3个DLL与对应.h头文件构成可移植组件另有HTML文档与PNG图示辅助理解架构。压缩包仅876KB轻量易部署。目前已有1820人学习下载读者可直接复用该方案开展自动化测试如pytest集成、跨平台算法交付或为无MATLAB许可的开发团队提供标准化Simulink算法服务。1. Python 调用 Simulink 编译 DLL绕过 MATLAB Runtime 依赖的轻量级部署方案你有没有遇到过这种场景模型在 Simulink 里跑得飞起但一到现场部署就卡壳——客户死活不装 MATLAB也不愿配 MATLAB RuntimeMCR更别说开 license server而你手头又只有.slx模型和一套验证无误的 C 代码生成逻辑。这时候“把 Simulink 当成黑匣子函数调用”就成了刚需。本资源python_SimulinkDLL正是为此而生它不依赖 MATLAB 进程、不启动 Simulink Desktop、不走matlab.engine那套 IPC 通信而是直接加载 Simulink 通过 Embedded Coder 生成的标准 Windows DLL或 Linux .so用纯 Python支持 Jupyter Notebook 环境完成输入喂入、状态推进、输出读取的全链路控制。适合嵌入式算法验证工程师、HIL 测试脚本开发者、以及需要将 Simulink 模型快速集成进 Python 自动化流水线的从业者。它不是替代 Simulink 的仿真器而是把 Simulink 编译出的“可执行数学内核”真正变成 Python 可调度的模块。2. 从 Simulink 模型到可调用 DLL编译配置与接口契约要让 Python 成功加载并驱动 Simulink 编译出的 DLL核心不在 Python 侧而在 Simulink 编译阶段是否生成了符合 C 接口规范、具备确定性时序行为、且导出符号清晰的动态库。这不是简单勾选“Generate shared library”就能搞定的事。下面拆解关键配置项及其物理意义。2.1 模型配置参数必须关闭“黑箱”陷阱Simulink 默认配置面向交互式仿真而 DLL 部署要求模型行为完全确定、无外部依赖、无异步回调。以下三项配置是硬性前提提示这些设置必须在模型打开状态下通过Model Configuration Parameters → Code Generation路径进入不能仅靠set_param脚本临时覆盖。% 在 MATLAB 命令行中执行需已打开模型 set_param(your_model, SystemTargetFile, ert.tlc); % 必须用 Embedded Coder 模板 set_param(your_model, GenerateASpecificLibrary, on); % 启用库生成 set_param(your_model, TargetLang, C); % 语言锁定为 C非 C set_param(your_model, SupportNonFinite, off); % 关闭 Inf/NaN 支持避免 math.h 依赖 set_param(your_model, RuntimeLibOptimization, Speed); % 避免 Debug 符号污染导出表ert.tlc是 Embedded Real-Time 模板它生成的代码不含 MATLAB 运行时栈管理逻辑所有内存由用户控制SupportNonFiniteoff是关键避坑点若开启生成的 DLL 会链接msvcr120.dll等 VC 运行时并隐式调用_fpclass等函数导致 Pythonctypes.CDLL()加载失败报错OSError: [WinError 126] 找不到指定的模块且该错误不提示具体缺哪个 DLLRuntimeLibOptimizationSpeed确保不生成调试符号段.debug_*减小 DLL 体积并避免符号名混淆。2.2 接口函数导出定义你的“Python 可见 API”Simulink 不会自动导出initialize()/step()/terminate()这类语义函数。你需要显式声明一个 C 接口封装层。推荐做法是在模型中添加S-Function或MATLAB Function模块其内部调用coder.extrinsic并用#pragma export标记仅限 MATLAB R2020b。但更稳妥、兼容性更强的方式是手写一个 wrapper.c 文件与 Simulink 生成的rtwtypes.h和your_model.h同目录放置// wrapper.c #include your_model.h #include your_model_types.h #include rtwtypes.h // 导出初始化函数必须无参数返回 int0成功 extern int your_model_initialize(void) { your_model_initialize(); return 0; } // 导出单步执行函数传入输入数组指针、输出数组指针 // 注意输入/输出维度必须与模型 I/O 端口严格一致 extern int your_model_step(const real_T u[2], real_T y[1]) { // 将输入拷贝到模型内部缓存 your_model_U.In1 u[0]; your_model_U.In2 u[1]; your_model_step(); y[0] your_model_Y.Out1; return 0; } // 导出终止函数 extern int your_model_terminate(void) { your_model_terminate(); return 0; }编译时需在 Simulink 的Code Generation → Custom Code → Include directories中添加 wrapper.c 所在路径并在Source files中填入wrapper.c。这样生成的 DLL 才会导出your_model_initialize、your_model_step、your_model_terminate三个 C 函数Python 才能通过ctypes绑定。2.3 编译命令与输出结构确认你拿到的是“干净 DLL”执行编译后目标文件夹默认为your_model_grt_rtw/下应存在文件名类型说明your_model.dll动态库主体含所有导出函数your_model.h头文件定义输入/输出结构体、类型别名rtwtypes.h类型定义real_T,int32_T等基础类型映射your_model_types.h模型类型your_model_U,your_model_Y结构体声明注意不要使用grt.tlcGeneric Real-Time模板它生成的代码依赖libeng.dll和libmx.dll无法脱离 MATLAB 运行必须用ert.tlc且确保Code Generation → Interface → Data exchange interface设置为External mode非Simulink Coder默认的Inlined。3. Python 侧绑定与调用Jupyter Notebook 友好型封装DLL 编译完成后Python 侧工作才真正开始。这里不推荐用pywin32或comtypes它们仍需注册 COM 组件而是采用原生ctypes 显式类型声明的方案确保在 Jupyter Notebook、PyInstaller 打包、甚至 Conda 环境中零兼容问题。3.1 基础 ctypes 绑定从加载到函数签名声明import ctypes import numpy as np from pathlib import Path # 1. 加载 DLLWindows 下为 .dllLinux 下为 .so dll_path Path(your_model_grt_rtw/your_model.dll) if not dll_path.exists(): raise FileNotFoundError(fDLL not found at {dll_path}) dll ctypes.CDLL(str(dll_path)) # 2. 声明 initialize() 函数无参数返回 int dll.your_model_initialize.argtypes [] dll.your_model_initialize.restype ctypes.c_int # 3. 声明 step() 函数输入为 double 数组2 元素输出为 double 数组1 元素 # 注意Simulink 默认 real_T double若模型设为 single则此处用 ctypes.c_float dll.your_model_step.argtypes [ np.ctypeslib.ndpointer(dtypenp.float64, ndim1, flagsC_CONTIGUOUS), np.ctypeslib.ndpointer(dtypenp.float64, ndim1, flagsC_CONTIGUOUS) ] dll.your_model_step.restype ctypes.c_int # 4. 声明 terminate() 函数 dll.your_model_terminate.argtypes [] dll.your_model_terminate.restype ctypes.c_intnp.ctypeslib.ndpointer是关键它告诉 ctypes 这个参数是 NumPy 数组的底层内存指针且必须是 C 连续C_CONTIGUOUS否则 Simulink 内部 memcpy 会越界restype ctypes.c_int强制函数返回值被解释为 32 位整数避免 Python 将返回值当作指针地址处理常见翻车点所有argtypes必须显式声明否则 ctypes 默认按int解释参数导致浮点数传参错位现象输出恒为 0 或 nan。3.2 封装为类支持多实例、状态隔离与上下文管理为避免全局状态污染如多个模型实例互相干扰建议封装为 Python 类并实现__enter__/__exit__协议class SimulinkDLL: def __init__(self, dll_path: str): self.dll ctypes.CDLL(dll_path) self._setup_functions() def _setup_functions(self): # 初始化函数绑定同上 self.dll.your_model_initialize.argtypes [] self.dll.your_model_initialize.restype ctypes.c_int # step 函数绑定明确输入输出长度 self._input_len 2 self._output_len 1 self.dll.your_model_step.argtypes [ np.ctypeslib.ndpointer(dtypenp.float64, shape(self._input_len,), flagsC_CONTIGUOUS), np.ctypeslib.ndpointer(dtypenp.float64, shape(self._output_len,), flagsC_CONTIGUOUS) ] self.dll.your_model_step.restype ctypes.c_int def __enter__(self): ret self.dll.your_model_initialize() if ret ! 0: raise RuntimeError(fInitialization failed with code {ret}) return self def __exit__(self, exc_type, exc_val, exc_tb): self.dll.your_model_terminate() def step(self, u: np.ndarray) - np.ndarray: 执行单步计算u 为 (2,) 形状的 float64 数组返回 (1,) 输出数组 if u.dtype ! np.float64 or u.shape ! (self._input_len,): raise ValueError(fInput must be float64 array of shape ({self._input_len},)) # 创建输出缓冲区 y np.zeros(self._output_len, dtypenp.float64) # 调用 DLL step 函数 ret self.dll.your_model_step(u, y) if ret ! 0: raise RuntimeError(fStep execution failed with code {ret}) return y.copy() # 返回副本避免外部修改影响内部状态 # 在 Jupyter Notebook 中使用示例 if __name__ __main__: with SimulinkDLL(your_model_grt_rtw/your_model.dll) as model: # 输入两个信号例如 [1.5, -0.8] u np.array([1.5, -0.8], dtypenp.float64) y model.step(u) print(fOutput: {y[0]:.4f}) # Output: 2.3174示例值y.copy()是血泪经验Simulink 内部可能复用输出缓冲区直接返回y可能导致后续step()调用覆盖前次结果__enter__/__exit__确保initialize/terminate成对执行即使中间抛异常也不会漏掉terminate防止内存泄漏类封装天然支持多实例model1 SimulinkDLL(...)和model2 SimulinkDLL(...)完全独立无全局变量冲突。3.3 Jupyter Notebook 集成技巧可视化与批量测试在 Jupyter 中我们常需观察模型响应曲线或批量验证参数。利用matplotlib和pandas可快速构建分析流import matplotlib.pyplot as plt import pandas as pd # 生成时间序列输入 t np.linspace(0, 10, 1000) u1 np.sin(0.5 * t) # 输入1正弦波 u2 0.3 * np.random.randn(len(t)) # 输入2噪声 # 批量调用 step() outputs [] with SimulinkDLL(your_model_grt_rtw/your_model.dll) as model: for i in range(len(t)): u np.array([u1[i], u2[i]], dtypenp.float64) y model.step(u) outputs.append(y[0]) # 转为 DataFrame 分析 df pd.DataFrame({ time: t, input1: u1, input2: u2, output: outputs }) # 绘图 fig, ax plt.subplots(2, 1, figsize(10, 6)) ax[0].plot(df[time], df[input1], labelInput 1 (sin)) ax[0].plot(df[time], df[input2], labelInput 2 (noise), alpha0.6) ax[0].legend(); ax[0].set_ylabel(Inputs) ax[1].plot(df[time], df[output], r-, labelModel Output) ax[1].legend(); ax[1].set_ylabel(Output); ax[1].set_xlabel(Time (s)) plt.tight_layout() plt.show() # 输出统计摘要 print(df[output].describe())此脚本在 Notebook 中可一键运行生成时域响应图与统计摘要无需切出 IDEpandas.DataFrame便于后续导出 CSV、做相关性分析或馈入 Scikit-learn 训练代理模型所有操作均在纯 Python 环境完成不触发任何 MATLAB 进程。4. 避坑指南五个让你重启电脑的 DLL 加载失败真相DLL 调用失败是此方案最耗时的环节。下面列出我在某跨平台系统项目中踩过的 5 个真实坑每个都附带可复现的现象、根因定位法和一招解决。4.1 现象OSError: [WinError 126] 找不到指定的模块原因DLL 依赖的 Visual C 运行时如vcruntime140.dll未安装或版本不匹配Simulink R2021a 用 VS2019 编译却在仅装 VS2015 运行时的机器上运行。排查用Dependency Walkerdepends.exe打开 DLL看红色高亮的缺失模块或用 PowerShell 命令Get-ChildItem your_model.dll | ForEach-Object { $_.VersionInfo }查编译工具链。解决下载对应版本的 Microsoft Visual C Redistributable 安装 x64 版本Simulink 默认生成 64 位 DLL。4.2 现象OSError: [WinError 193] %1 不是有效的 Win32 应用程序原因Python 解释器位数32/64与 DLL 位数不匹配。Simulink 默认生成 64 位 DLL但你用的是 32 位 Python常见于旧版 Anaconda。排查在 Python 中运行import platform; print(platform.architecture())输出(32bit, WindowsPE)即为 32 位。解决卸载 32 位 Python安装官方 64 位 Python或使用conda install python3.9*_64。4.3 现象step()返回值恒为 0但输出数组y值全为nan原因输入数组u未声明为float64而是float32或int32。Simulinkert.tlc模板中real_T固定为double传入float32会导致内存错位y缓冲区被写入随机字节。排查打印u.dtype和u.ctypes.data确认类型与内存地址连续性。解决强制转换u u.astype(np.float64)并在argtypes中严格限定dtypenp.float64。4.4 现象首次step()正常第二次开始输出突变为极大值如1e308原因模型中使用了Unit Delay模块但initialize()未正确重置其初始状态。Simulink 生成的initialize()函数只清零DWork但若Unit Delay初始值设为inf或nan则step()会传播该值。排查在 Simulink 中双击Unit Delay模块检查Initial condition参数是否为合法数值非inf/nan/auto。解决将所有Unit Delay、Memory模块的初始条件设为具体数字如0并重新编译 DLL。4.5 现象ctypes.CDLL()成功但调用your_model_step()时 Python 直接崩溃闪退原因DLL 中存在未捕获的浮点异常如除零、sqrt(-1)触发 Windows SEH 异常而ctypes无法拦截导致进程终止。排查用 Visual Studio 附加到 Python 进程启用“C 异常”断点或在 Simulink 中启用Configuration Parameters → Diagnostics → Simulation target → Detect overflow。解决在模型中插入Saturation模块限制信号范围或在 wrapper.c 的step()函数中添加fenv_t env; feholdexcept(env); ... feclearexcept(FE_ALL_EXCEPT);清除浮点异常标志。5. 进阶技巧支持变步长、多速率与状态快照保存当你的 Simulink 模型包含变采样时间如Rate Transition模块、多速率子系统或需要在任意时刻保存/恢复模型状态时基础step()接口就不够用了。此时需扩展 wrapper.c 并暴露更多控制函数。5.1 变步长支持暴露setSampleTime()与getBaseRate()接口Simulink 生成的代码中rtmGetTStart()、rtmGetTFinal()等宏可获取仿真时间但setSampleTime()需手动注入。在wrapper.c中添加// wrapper.c 新增 extern void set_sample_time(real_T ts) { // Simulink 生成的 rtModel 结构体中ts 通常存于 rtm-Timing.taskTime[0] // 此处需根据实际生成代码定位常见路径rtm-Timing.stepSize // 示例需按实际生成代码调整 // rtm-Timing.stepSize ts; // rtm-Timing.taskTime[0] ts; } extern real_T get_base_rate(void) { return 0.01; // 返回模型基础采样周期秒供 Python 侧做时间对齐 }Python 侧绑定# 在 SimulinkDLL.__init__() 中添加 self.dll.set_sample_time.argtypes [ctypes.c_double] self.dll.set_sample_time.restype None self.dll.get_base_rate.argtypes [] self.dll.get_base_rate.restype ctypes.c_double # 使用 model.dll.set_sample_time(0.005) # 设为 5ms base_rate model.dll.get_base_rate() # 获取当前基础周期注意set_sample_time()的具体实现高度依赖 Simulink 生成代码结构。最可靠方式是打开your_model.c搜索stepSize或taskTime找到其在rtModel结构体中的偏移量再用ctypes.cast()指针偏移访问。这属于高级定制建议先用固定步长验证流程。5.2 状态快照保存与恢复 DWork 内存块Simulink 模型状态主要存于DWork数组中。ert.tlc生成的代码提供rtmSetErrorStatus()等函数但不直接暴露DWork地址。安全做法是添加两个新函数// wrapper.c 新增 extern void* get_dwork_ptr(void) { return (void*)rtmGetdWork(rtm); // rtm 是全局 rtModel 指针 } extern size_t get_dwork_size(void) { return rtmGetNumDWork(rtm) * sizeof(void*); // 粗略估算实际需遍历 DWork 描述符 }Python 侧可将其转为 NumPy 数组进行序列化def save_state(self, filepath: str): 保存当前 DWork 状态到 .npy 文件 dwork_ptr self.dll.get_dwork_ptr() dwork_size self.dll.get_dwork_size() # 将指针转为 bytes dwork_bytes ctypes.string_at(dwork_ptr, dwork_size) np.save(filepath, np.frombuffer(dwork_bytes, dtypenp.uint8)) def load_state(self, filepath: str): 从 .npy 文件恢复 DWork 状态 dwork_bytes np.load(filepath).tobytes() dwork_ptr self.dll.get_dwork_ptr() ctypes.memmove(dwork_ptr, dwork_bytes, len(dwork_bytes))此功能可用于 HIL 测试中的故障注入保存正常状态 → 注入传感器故障 → 观察控制器响应 → 恢复状态继续测试。5.3 多实例性能优化共享 DLL 句柄隔离模型数据若需同时运行 10 个相同模型如蒙特卡洛仿真为每个实例加载一次 DLL 会浪费内存。ctypes.CDLL()默认是进程级共享但模型数据rtm结构体是全局的。解决方案是在 wrapper.c 中为每个实例分配独立rtModel实例并通过函数参数传递// wrapper.c 修改不再用全局 rtm改为传参 typedef struct { void* rtm; } ModelHandle; extern ModelHandle create_model(void) { ModelHandle h; h.rtm malloc(sizeof(RT_MODEL_your_model)); // 按实际结构体名 your_model_initialize(h.rtm); return h; } extern void destroy_model(ModelHandle h) { your_model_terminate(h.rtm); free(h.rtm); } extern int step_with_handle(ModelHandle h, const real_T u[2], real_T y[1]) { // 在 h.rtm 上执行 step }Python 侧即可创建轻量句柄# 创建 10 个独立模型实例 handles [model.dll.create_model() for _ in range(10)] try: for h in handles: y np.zeros(1) model.dll.step_with_handle(h, u, y) # 每个 h 拥有独立状态 finally: for h in handles: model.dll.destroy_model(h)从那以后我每次交付 Simulink 模型给 Python 团队都会附上一份wrapper.c模板和这份SimulinkDLL类而不是一句“你们自己去查文档”。因为我知道真正的工程落地不在于模型多漂亮而在于它能不能被一行pip install之后在 Jupyter 里敲三行代码就跑起来。希望帮到你。本文还有配套的精品资源点击获取