第 1 节 概述
由于工作上的一些需要,需要将 Simulink 部署到服务器上,通过 Restful API 调用,传入参数进行仿真。
Simulink 是 Matlab 的一个模块,属于公司 MathWorks,是一款商业软件。服务的部署有多种方式可选,但是选择的方法不恰当,可能会面临授权和收费的问题。MathWorks 的收费向来很多,显然,通过付费来解决问题并不是我们想要的。
第 2 节 部署方式
Matlab 主程序提供了一些部署方法,这些部署方法更加适合对.m文件进行部署。
「代码生成」中的 Matlab Coder 可以将 Matlab 代码编译成原生的 C 语言程序,从而不依赖 Matlab 环境运行,但是其不支持 Simulink 的编译
「应用程序部署」那一类,都需要 Matlab 运行环境,其实现的是使用对应编程语言调用 Matlab 运行环境的接口。
Simulink 子程序中也提供了一些部署方法,这些部署方法是专门给 Simulink 适配的。
其中的 Simulink Coder 可以将 Simulink 模块编译成原生的 C语言程序,从而不依赖 Matlab 环境运行。
2.1 Matlab Production/Web App Server
首先,MathWorks 公司肯定是料想到了它的客户需要将 Matlab 相关服务部署到服务器上运行,于是,它推出了两款产品。
分别是:Matlab Production Server 和 Matlab Web App Server ,这两款产品的区别在于,Matlab Production Server 提供的是 Restful API,而 Matlab Web App Server 可以通过可视化的方式在浏览器上运行 Matlab 应用程序。
编译器的位置如下:
Matlab Production Server
Matlab Production Server 可以直接将 .m文件及其他关联附属文件打包。
打包的过程中使用依赖关系分析器,并将依赖关系用图列举出来:
最终,相关的依赖模块会以各种Addon的形式体现在buildresult.json文件中:
于是在执行相应函数时,只会加载对应的 Addon。
Matlab Web App Server
Matlab Web App Server 不同于 Matlab Production Server,它需要先创建一个 Matlab 的 App 应用,并在其中设计好相应的前端 UI 并绑定函数功能,如下:
由于本人并不打算使用这种方式构建,因而不再进行进一步的功能探索。
2.2 MATLAB Engine API
MATLAB Engine API 是 MathWorks 提供的官方工具,允许在 Python 中调用 MATLAB 函数、脚本和数据。
找到如下路径:[MATLAB安装路径]/extern/engines/python
使用命令安装:
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 包编译器
Python 包编译器可以直接将 .m文件及其他关联附属文件打包,如下:
安装一下即可:
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函数时会报错,因而最终也没有采用这种方式。
2.4 Simulink Coder
使用 Simulink Coder 前需要安装一款合适的 C 编译器,如下:
在「APP」中选择 Simulink Coder,然后在弹出的 C 代码标签页上点击「编译」即可:
这样,便会生成如下的 C Make 项目:
打开文件夹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直接安装,方法如下:
Windows ARM64 的编译器无法直接通过homebrew安装,可以在 Github 上下载llvm-mingw的最新版本,然后解压到任意指定位置即可。
可以看到压缩包也包含了Windows X86_64 的编译器,所以也可以直接用这个,homebrew会同时配置好环境变量,更加方便一些。
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/目录下,也就是在项目的根目录下有如下内容:
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