使用 Python 调用 Simulink 进行仿真

第 1 节 概述

由于工作上的一些需要,需要将 Simulink 部署到服务器上,通过 Restful API 调用,传入参数进行仿真。

Simulink 是 Matlab 的一个模块,属于公司 MathWorks,是一款商业软件。服务的部署有多种方式可选,但是选择的方法不恰当,可能会面临授权和收费的问题。MathWorks 的收费向来很多,显然,通过付费来解决问题并不是我们想要的。

第 2 节 部署方式

Matlab 主程序提供了一些部署方法,这些部署方法更加适合对.m文件进行部署。

  • 「代码生成」中的 Matlab Coder 可以将 Matlab 代码编译成原生的 C 语言程序,从而不依赖 Matlab 环境运行,但是其不支持 Simulink 的编译
  • 「应用程序部署」那一类,都需要 Matlab 运行环境,其实现的是使用对应编程语言调用 Matlab 运行环境的接口。

https://img.papergate.top:5000/i/2025/09/68d4af57d6051.webp

Simulink 子程序中也提供了一些部署方法,这些部署方法是专门给 Simulink 适配的。

https://img.papergate.top:5000/i/2025/09/68d4b260dd9a0.webp

其中的 Simulink Coder 可以将 Simulink 模块编译成原生的 C语言程序,从而不依赖 Matlab 环境运行。

2.1 Matlab Production/Web App Server

首先,MathWorks 公司肯定是料想到了它的客户需要将 Matlab 相关服务部署到服务器上运行,于是,它推出了两款产品。

分别是:Matlab Production ServerMatlab Web App Server,这两款产品的区别在于,Matlab Production Server 提供的是 Restful API,而 Matlab Web App Server 可以通过可视化的方式在浏览器上运行 Matlab 应用程序。

信息

这两款软件都是商业软件,所以都是要收费的。

编译器的位置如下:

https://img.papergate.top:5000/i/2025/09/68d389225bed7.webp

Matlab Production Server

Matlab Production Server 可以直接将 .m文件及其他关联附属文件打包。

https://img.papergate.top:5000/i/2025/09/68d38a017dd29.webp

打包的过程中使用依赖关系分析器,并将依赖关系用图列举出来:

https://img.papergate.top:5000/i/2025/09/68d38b0e83219.webp

最终,相关的依赖模块会以各种Addon的形式体现在buildresult.json文件中:

于是在执行相应函数时,只会加载对应的 Addon。

Matlab Web App Server

Matlab Web App Server 不同于 Matlab Production Server,它需要先创建一个 Matlab 的 App 应用,并在其中设计好相应的前端 UI 并绑定函数功能,如下:

https://img.papergate.top:5000/i/2025/09/68d38d04e944c.webp

https://img.papergate.top:5000/i/2025/09/68d38cd0831db.webp

由于本人并不打算使用这种方式构建,因而不再进行进一步的功能探索。

2.2 MATLAB Engine API

MATLAB Engine API 是 MathWorks 提供的官方工具,允许在 Python 中调用 MATLAB 函数、脚本和数据。

找到如下路径:[MATLAB安装路径]/extern/engines/python

https://img.papergate.top:5000/i/2025/09/68d38815a727e.webp

使用命令安装:

1
python setup.py install

MATLAB Engine API 就相当于在后台打开了一个 matlab 程序,使用 python 执行的命令会原封不动的转变成执行 matlab 命令:

1
2
3
4
5
6
7
8
# 导入库
import matlab.engine
# 在后台打开 matlab 程序
eng = matlab.engine.start_matlab()
# 添加 matlab 项目的目录
eng.addpath('path')
# 执行目录下的 xxx.m 文件定义的函数
result = eng.xxx(1,2)

这种方式只是在 matlab 外套了一层 python 的包装,怎样使用 matlab 就怎样使用 python,不会出现任何的意料之外的报错。

也正因为这样,这种方式需要在服务器上安装完整的 matlab 程序并授权,并且每次服务启动前需要先启动 matlab,执行 simulink 仿真前也会加载相应模块,每次运行前会有好几秒的加载时间,需要进行优化并预先加载资源,所以这种方式不适合部署在服务器上。

2.3 Python 包编译器

https://img.papergate.top:5000/i/2025/09/68d394cb51102.webp

Python 包编译器可以直接将 .m文件及其他关联附属文件打包,如下:

https://img.papergate.top:5000/i/2025/09/68d3954bd5389.webp

安装一下即可:

1
python setup.py install
1
2
3
4
5
6
7
# 导入包
import RPC_ABSmin  

# 初始化包
handle = RPC_ABSmin.initialize()
# 执行包中定义的函数
result = handle.run_RPC_ABSmin(1,2)

这种方式不需要完整安装 matlab 应用,只需要安装 matlab 运行环境,执行函数时只加载依赖的模块,所以速度上是要快上不少的,并且 matlab 的运行环境是不收费且无需授权的,所以这是一种可以考虑的部署方式。

警告

不过,本人在使用这种方式运行 Simulink 仿真的时候遇到了未知的错误,执行sim函数时会报错,因而最终也没有采用这种方式。

使用 Simulink Coder 前需要安装一款合适的 C 编译器,如下:

https://img.papergate.top:5000/i/2025/09/68d4b9f4316ac.webp

在「APP」中选择 Simulink Coder,然后在弹出的 C 代码标签页上点击「编译」即可:

https://img.papergate.top:5000/i/2025/09/68d4babb288a7.webp

这样,便会生成如下的 C Make 项目:

https://img.papergate.top:5000/i/2025/09/68d4bbab1ecf8.webp

打开文件夹cmake-build-debug,可以看到,matlab 已经为我们编译好了可执行文件。

信息

这种方式不依赖于 Matlab 运行环境,并且因为是原生的 C 语言环境,程序的运行速度也很快,所以最后本人便采用这种方式来编译 Simulink。

第 3 节 编译共享链接库

使用 Simulink Coder 编译时,默认生成的是可执行文件,这有一定的局限性。

  • 一方面,执行可执行文件时,不好指定输入,输出也不好获取,对于 Simulink 这样依赖输入输出的仿真应用,非常不方便。
  • 另一方面,可执行文件的中间运算过程是固定的,不方便在仿真的中途修改数据。

所以,需要修改C Make的编译方式,生成共享链接库(Windows 中是 dll 文件,Mac 中是dylib 文件,linux 中是 so 文件)。

找到CMakeLists.txt文件,其中有一行用于生成可执行文件:

1
add_executable(「xxx」  ${MATLAB_ROOT}/rtw/c/src/common/rt_main.c)

将其替换成以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 将原来的add_executable改为add_library并指定为SHARED库  
add_library(「xxx」 SHARED ${MATLAB_ROOT}/rtw/c/src/common/rt_main.c)  
  
# 设置库的属性  
set_target_properties(「xxx」 PROPERTIES PREFIX ""  
        BUILD_WITH_INSTALL_RPATH TRUE  
        INSTALL_RPATH_USE_LINK_PATH TRUE  
        SOVERSION 1)  # 添加版本号  
  
# 修改安装规则,将RUNTIME改为LIBRARY  
install(TARGETS 「xxx」  
        LIBRARY DESTINATION "lib"  # 修改为LIBRARY并指定安装目录  
        ARCHIVE DESTINATION "lib"  # 添加静态库安装目录  
        RUNTIME DESTINATION "bin"  # 可执行文件目录(如果有的话)  
        OPTIONAL)

重新编译即可获得共享链接库。

第 4 节 查看 C 代码

因为后续使用 Python 调用共享链接库时,需要执行库中的 C 函数,因而需要了解源码中 C 函数的变量名。

xxx.h里面,定义了模型的输入和输出,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct {  
  real_T CV11;                         /* '<Root>/CV11' */  
  real_T CV12;                         /* '<Root>/CV12' */  
  real_T CV21;                         /* '<Root>/CV21' */  
  real_T CV22;                         /* '<Root>/CV22' */  
  boolean_T Reduce1;                   /* '<Root>/Reduce1' */  
  boolean_T Reduce2;                   /* '<Root>/Reduce2' */  
  real_T Power;                        /* '<Root>/Power' */  
} ExtU_RPC_ABSmin_T;
typedef struct {  
  real_T Number;                       /* '<Root>/Number' */  
} ExtY_RPC_ABSmin_T;

定义了两个结构体,ExtU_xxx_T是输入,ExtY_xxx_T是输出。

xxx.c里面,定义了模型的运算过程,如下:

1
2
3
void RPC_ABSmin_initialize(void)  {...}
void RPC_ABSmin_step(void)  {...}
void RPC_ABSmin_terminate(void)  {...}

RPC_ABSmin_initialize是构造函数,RPC_ABSmin_step是迭代函数,而RPC_ABSmin_terminate是析构函数。

如果在模型中指定了仿真的步长和时间,就会在构造函数中看到如下内容:

1
2
3
4
# 步长 0.01 s
RPC_ABSmin_M->Timing.stepSize0 = 0.01;
# 时间 1s
rtmSetTFinal(RPC_ABSmin_M, 1.0);  

迭代函数里面是每个步长中需要执行的运算。

信息

可见 Simulink 就是定义了初始状态和每一步的运算,然后反复迭代的过程。

第 5 节 编写 Python 代码

有了上面 C 代码的铺垫,可以编写 Python 代码如下:

首先,定义模型的输入输出形式,对应.h文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import ctypes  
  
class ExtU_RPC_ABSmin_T(ctypes.Structure):  
    _fields_ = [  
        ("CV11", ctypes.c_double),   
        ("CV12", ctypes.c_double),   
        ("CV21", ctypes.c_double),  
        ("CV22", ctypes.c_double),   
        ("Reduce1", ctypes.c_uint8),   
        ("Reduce2", ctypes.c_uint8),   
        ("Power", ctypes.c_double)
    ]  
  
class ExtY_RPC_ABSmin_T(ctypes.Structure):  
    _fields_ = [  
        ("Number", ctypes.c_double)  
    ]

然后,读取共享链接库,并绑定数据:

1
2
3
4
lib = ctypes.CDLL("RPC_ABSmin.dylib")  

RPC_ABSmin_U = ExtU_RPC_ABSmin_T.in_dll(lib, "RPC_ABSmin_U")  
RPC_ABSmin_Y = ExtY_RPC_ABSmin_T.in_dll(lib, "RPC_ABSmin_Y")

最后,执行仿真,对应.c文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
lib.RPC_ABSmin_initialize()  

RPC_ABSmin_U.CV11 = 1.0  
RPC_ABSmin_U.CV12 = 1.0  
RPC_ABSmin_U.CV21 = 1.0  
RPC_ABSmin_U.CV22 = 1.0  
RPC_ABSmin_U.Reduce1 = 0  
RPC_ABSmin_U.Reduce2 = 0  
RPC_ABSmin_U.Power = 2000.0  

for i in range(100):  
    lib.RPC_ABSmin_step()  
print("模型输出 Number =", RPC_ABSmin_Y.Number)  

lib.RPC_ABSmin_terminate()

第 6 节 移植到其他平台

在完成上述工作以后,需要将项目移植到其他平台,常见的服务器有 Windows 服务器和 Linux 服务器,而架构有 X86 架构和 ARM 架构,需要按照服务器的系统和架构编译对应版本的共享链接库。

由于 CMake 可以跨平台交叉编译,所以只需要进行一定的设置,就可以一次生成全平台的编译结果,方便我们进行移植。

6.1 安装编译器

本人安装过程中主要使用homebrew进行安装。

Windows X86_64 的编译器可以通过homebrew直接安装,方法如下:

1
brew install mingw-w64

Windows ARM64 的编译器无法直接通过homebrew安装,可以在 Github 上下载llvm-mingw的最新版本,然后解压到任意指定位置即可。

可以看到压缩包也包含了Windows X86_64 的编译器,所以也可以直接用这个,homebrew会同时配置好环境变量,更加方便一些。

https://img.papergate.top:5000/i/2025/11/691ab5ac2a816.webp

Linux 的编译器可以通过homebrew直接安装,方法如下:

1
brew install FiloSottile/musl-cross/musl-cross

6.2 编写 CMakePresets.json

这个文件中定义了多种编译方式,以及各种编译方式的编译器和相关设置所在的位置:

以上是本人使用的编译方式,从上到下定义了 MacOS、Linux 和 Windows 的多种编译:

  • MacOS的编译:使用的是本地的编译器,位置在/usr/bin/clang/usr/bin/clang++,可能会因为安装方式的不同而发生变化,本人的操作系统在如下位置。
  • Linux和Windows的编译:这里使用了工具链(ToolChain)进行编译,可以看到工具链放在了${sourceDir}/cmake/toolchains/目录下,也就是在项目的根目录下有如下内容:

https://img.papergate.top:5000/i/2025/11/691aae80d278b.webp

linux-x86_64.cmake

1
2
3
4
5
6
7
8
set(CMAKE_SYSTEM_NAME Linux)  
set(CMAKE_SYSTEM_PROCESSOR x86_64)  
set(CMAKE_C_COMPILER /opt/homebrew/bin/x86_64-linux-musl-gcc)  
set(CMAKE_CXX_COMPILER /opt/homebrew/bin/x86_64-linux-musl-g++)  
set(CMAKE_SYSROOT /opt/homebrew)  
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

linux-arm64.cmake

1
2
3
4
5
6
7
8
set(CMAKE_SYSTEM_NAME Linux)  
set(CMAKE_SYSTEM_PROCESSOR aarch64)  
set(CMAKE_C_COMPILER /opt/homebrew/bin/aarch64-linux-musl-gcc)  
set(CMAKE_CXX_COMPILER /opt/homebrew/bin/aarch64-linux-musl-g++)  
set(CMAKE_SYSROOT /opt/homebrew)  
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

windows-x86_64.cmake

1
2
3
4
5
6
7
8
set(CMAKE_SYSTEM_NAME Windows)  
set(CMAKE_SYSTEM_PROCESSOR x86_64)  
set(CMAKE_C_COMPILER /opt/homebrew/bin/x86_64-w64-mingw32-gcc)  
set(CMAKE_CXX_COMPILER /opt/homebrew/bin/x86_64-w64-mingw32-g++)  
set(CMAKE_SYSROOT /opt/homebrew)  
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

windows-arm64.cmake

1
2
3
4
5
6
7
8
set(CMAKE_SYSTEM_NAME Windows)  
set(CMAKE_SYSTEM_PROCESSOR ARM64)  
set(CMAKE_C_COMPILER   /xxx/llvm-mingw/bin/aarch64-w64-mingw32-clang)  
set(CMAKE_CXX_COMPILER /xxx/llvm-mingw/bin/aarch64-w64-mingw32-clang++)  
set(CMAKE_SYSROOT /xxx/llvm-mingw/aarch64-w64-mingw32)  
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)  
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)  
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

这里llvm-mingw的位置按照安装位置设定。

6.3 执行编译

执行编译的步骤如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# MacOS-x86_64
cmake --preset macos-x86_64
cmake --build build/macos-x86_64
# MacOS-arm64
cmake --preset macos-arm64
cmake --build build/macos-arm64
# Windows-x86_64
cmake --preset windows-x86_64
cmake --build build/windows-x86_64
# Windows-arm64
cmake --preset windows-arm64
cmake --build build/windows-arm64
# Linux-x86_64
cmake --preset linux-x86_64
cmake --build build/linux-x86_64
# Linux-arm64
cmake --preset linux-arm64
cmake --build build/linux-arm64