☰
Python调用Simulink编译DLL:零MATLAB依赖的轻量部署方案
2026/10/15 18:19:07 网站建设 项目流程

简介:本资源是一套面向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 Runtime(MCR),更别说开 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 运行时栈管理逻辑,所有内存由用户控制;
  • SupportNonFinite=off是关键避坑点:若开启,生成的 DLL 会链接msvcr120.dll等 VC 运行时,并隐式调用_fpclass等函数,导致 Pythonctypes.CDLL()加载失败(报错OSError: [WinError 126] 找不到指定的模块),且该错误不提示具体缺哪个 DLL;
  • RuntimeLibOptimization=Speed确保不生成调试符号段(.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" // 导出初始化函数:必须无参数,返回 int(0=成功) 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.tlc(Generic 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. 加载 DLL(Windows 下为 .dll,Linux 下为 .so) dll_path = Path("your_model_grt_rtw/your_model.dll") if not dll_path.exists(): raise FileNotFoundError(f"DLL 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(dtype=np.float64, ndim=1, flags='C_CONTIGUOUS'), np.ctypeslib.ndpointer(dtype=np.float64, ndim=1, flags='C_CONTIGUOUS') ] dll.your_model_step.restype = ctypes.c_int # 4. 声明 terminate() 函数 dll.your_model_terminate.argtypes = [] dll.your_model_terminate.restype = ctypes.c_int
  • np.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(dtype=np.float64, shape=(self._input_len,), flags='C_CONTIGUOUS'), np.ctypeslib.ndpointer(dtype=np.float64, shape=(self._output_len,), flags='C_CONTIGUOUS') ] self.dll.your_model_step.restype = ctypes.c_int def __enter__(self): ret = self.dll.your_model_initialize() if ret != 0: raise RuntimeError(f"Initialization 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(f"Input must be float64 array of shape ({self._input_len},)") # 创建输出缓冲区 y = np.zeros(self._output_len, dtype=np.float64) # 调用 DLL step 函数 ret = self.dll.your_model_step(u, y) if ret != 0: raise RuntimeError(f"Step 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], dtype=np.float64) y = model.step(u) print(f"Output: {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]], dtype=np.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'], label='Input 1 (sin)') ax[0].plot(df['time'], df['input2'], label='Input 2 (noise)', alpha=0.6) ax[0].legend(); ax[0].set_ylabel('Inputs') ax[1].plot(df['time'], df['output'], 'r-', label='Model 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 中可一键运行,生成时域响应图与统计摘要,无需切出 IDE;
  • pandas.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 Walker(depends.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 python=3.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中严格限定dtype=np.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, dtype=np.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 里敲三行代码就跑起来。希望帮到你。

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

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

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

立即咨询