1. 为什么要把SIMULINK模型变成动态链接库?

如果你用过SIMULINK,肯定知道它是个“画图”做仿真的神器。拖拖拽拽,连几条线,一个控制算法或者信号处理模型就搭好了,跑起来看波形,非常直观。但问题来了,咱们做工程,尤其是做嵌入式开发、机器人(比如ROS)或者跨平台应用,最终是要把模型“塞”进实实在在的硬件里跑的,不能总在MATLAB的仿真环境里转悠。

这时候,你就得考虑“代码生成”了。SIMULINK自带这个强大功能,能把你的框图模型变成C/C++代码。听起来很美,对吧?但我猜你可能踩过坑:自动生成的那一堆代码文件,看着就头大。一个简单的加法器,生成了十来个.c、.h、.cpp文件,里面还有各种初始化和接口代码。如果你的模型稍微复杂点,文件数量直接爆炸,管理起来非常麻烦。更头疼的是,你想在另一个程序里,比如用C++写的一个主控程序里,调用这个加法功能,难道要把这几十个文件都拷贝过去一起编译吗?这显然不现实,耦合度太高,移植性也差。

所以,咱们得找个更优雅的办法——把这一大坨生成的代码,打包编译成一个.so动态链接库文件。你可以把它想象成一个功能“胶囊”。你的主程序不需要知道胶囊里具体是什么药(也就是那些复杂的模型代码),只需要知道胶囊的名字(函数名)和服用方法(函数接口),就能直接调用它的功能。这样做的好处太多了:

  • 极度干净的解耦:你的模型算法和主程序彻底分开。模型迭代更新了?只需要重新生成、编译一下.so文件替换掉旧的,主程序代码一行都不用动。
  • 跨语言调用:生成的标准C接口.so库,不仅C/C++能调,Python、Java等语言通过一些机制也能调用,灵活性大增。
  • 便于团队协作:算法工程师专心搞SIMULINK建模和生成库,软件工程师只管调用接口,分工明确。
  • 保护知识产权:分发.so库文件比分发源代码要安全一些。

这次,我就带你走通这条实战路线:从SIMULINK里一个简单的模型开始,生成代码,然后在Ubuntu系统下,亲手把它编译成.so动态链接库,最后写个小程序验证调用。整个过程我会把每一步的细节、容易踩的坑都掰开揉碎讲清楚,保证你跟着做一遍就能掌握。

2. 从模型到代码:SIMULINK配置的关键一步

万事开头难,而转代码这个“开头”,配置不对,后面全白费。很多新手在这里栽跟头,生成一堆根本编译不过的代码。咱们一步步来。

首先,打开SIMULINK,建一个最简单的模型用于测试。我就建一个加法器:两个输入端口(In1, In2),一个求和模块(Add),一个输出端口(Out1)。为了后续代码清晰,我习惯把输入输出端口的名字改得有含义些,比如In1改成a1,In2改成a2,Out1改成o。这样生成的代码里,变量名看着也舒服。

模型建好能仿真运行后,重点来了:配置代码生成参数。在模型界面,按 Ctrl+E 打开“模型配置参数”窗口。这里面的选项很多,但咱们聚焦几个最关键的。

第一步,选择系统目标文件。 这是最重要的设置,决定了生成代码的框架。在 Code Generation 页面,找到 System target file。对于嵌入式实时应用,我强烈推荐选择 ert.tlc (Embedded Coder)。它生成的代码结构非常紧凑、高效,没有太多仿真用的冗余部分,更适合拿来编译成库。相比之下,grt.tlc 生成的目标更偏向于通用实时仿真,会多一些我们不需要的东西。

第二步,设置代码生成目标。 在 Hardware Implementation 页面,需要告诉SIMULINK我们的目标硬件环境。虽然我们最终在Ubuntu的g++下编译,但这里选择 Device vendor 为 Intel,Device type 为 x86-64 (Linux 64),或者更通用的 Generic->Unspecified (32-bit) 也可以。这个设置主要影响一些数据类型(比如int是32位还是64位)和对齐方式,选对了能避免一些底层兼容性问题。

第三步,处理浮点模型。 如果你的模型里用了浮点数(比如double类型的Gain),务必在 Solver 页面,将 Solver 类型选为 Fixed-step,并且 Fixed-step size 要设置一个合理的值(比如0.01)。对于要生成代码的模型,固定步长是必须的。同时,在 Code Generation -> Interface 里,把 Support floating-point numbers 勾上。

第四步,配置函数接口。 我们希望生成的代码有一个清晰的入口函数。在 Code Generation -> Interface 里,找到 Code replacement library,可以选择 None 或者 GNU99。更关键的是下面,把 Generate an example main program 这个选项取消勾选。因为我们是要生成库,不需要它那个示例主程序,那个ert_main.c文件内容太杂,我们后面会自己写一个精简的接口。

配置好这些,点击“应用”然后“确定”。回到模型界面,点击菜单栏的 CODE -> C/C++ Code -> Build Model。稍等片刻,如果配置正确,MATLAB会在当前目录下生成一个以模型名命名的文件夹,里面就是所有的源代码。

打开这个文件夹,你可能会有点惊讶:一个加法器,居然生成了addd.c, addd.h, addd_types.h, addd_private.h, rtwtypes.h,可能还有addd_data.c等等。文件虽多,但别慌,它们的结构是有规律的。addd.h里包含了模型的主类声明和公共接口,addd.c是核心的实现。我们下一步就要对这些“原材料”进行加工。

3. 加工生成的代码:打造清晰的C接口

自动生成的代码是“毛坯房”,功能齐全但不好直接入住(调用)。我们需要给它封装一个简洁的“大门”(接口函数)。

生成代码的核心是一个以模型名命名的C++类(如果选择了ERT目标),比如我的模型叫addd,那么类名就是adddModelClass。这个类有initialize(), step(), terminate() 这几个关键方法。step()就是执行一次模型计算。我们的任务就是写一个C风格的函数,内部创建这个类的对象,调用它的方法,并传递输入输出参数。

我通常会新建一个文件,比如叫model_interface.cpp(或者直接修改生成的ert_main.cpp)。来看看我是怎么封装的:

// model_interface.cpp
#include “addd.h” // 这是生成代码的主头文件
#include <stddef.h>

// 声明一个全局的模型实例(或者静态全局,根据需求)
static adddModelClass model_instance;

// 封装接口函数
extern “C” double add_model(double input_a1, double input_a2) {
    // 1. 初始化模型(对于多次调用,可能需要判断是否已初始化)
    model_instance.initialize();

    // 2. 设置输入
    model_instance.addd_U.a1 = input_a1; // `addd_U` 是生成的输入结构体
    model_instance.addd_U.a2 = input_a2;

    // 3. 执行一步模型计算
    model_instance.step();

    // 4. 获取输出
    double result = model_instance.addd_Y.o; // `addd_Y` 是生成的输出结构体

    // 5. 终止(如果确定只调用一次,可以调用。对于库函数,常省略或在调用结束时处理)
    // model_instance.terminate();

    return result;
}

这里有几个关键点:

  1. extern “C”:这个修饰符至关重要!它告诉C++编译器,这个函数要用C语言的链接规范来编译。这样编译出来的动态库,函数名就不会被C++的“名字修饰”机制搞得面目全非,其他C程序甚至Python的ctypes才能正确地按名字找到它。
  2. 输入输出结构体:addd_U 和 addd_Y 是生成代码中自动定义的结构体,分别包含了所有输入端口和输出端口的变量。你需要去addd.h里确认一下它们的成员具体叫什么名字。
  3. 初始化和终止:对于简单的、无状态的函数式模型,每次调用都初始化再执行是可以的。但如果模型有内部状态(比如积分器、延迟单元),你可能需要将model_instance设为静态全局,只在第一次调用时初始化,然后反复调用step(),最后在程序退出前再terminate()。这取决于你的模型特性。

这个add_model函数,就是我们未来动态链接库对外暴露的唯一接口,干净利落。

4. 在Ubuntu上编译动态链接库

现在,我们把加工好的代码(包括生成的所有.c/.cpp文件和我们的model_interface.cpp)拷贝到Ubuntu系统里。假设所有文件都在当前目录下。

打开终端,进入这个目录。编译动态链接库分两步走:

第一步,将每个源文件编译成位置无关的目标文件。 这是生成.so文件的前提条件。

g++ -c -fPIC -I. addd.cpp model_interface.cpp
  • -c:表示只编译不链接,生成.o文件。
  • -fPIC:关键参数! 意思是生成位置无关代码。动态库会被加载到内存的任意地址,所以它的代码必须能在任何位置运行,这就需要PIC。
  • -I.:告诉编译器在当前目录寻找头文件(比如addd.h)。

执行后,你会得到addd.o和model_interface.o。

第二步,将这些目标文件链接成共享库。

g++ -shared -fPIC -o libmodel_add.so addd.o model_interface.o
  • -shared:告诉链接器我们要生成一个共享库。
  • -fPIC:这里再次指定,确保链接阶段也使用PIC。
  • -o libmodel_add.so:指定输出文件名。动态链接库的命名惯例是lib开头,.so结尾。model_add是我自己取的名字,和模型功能相关。

如果一切顺利,当前目录下就会诞生一个libmodel_add.so文件。你可以用file命令验证一下:

file libmodel_add.so

输出应该包含 ELF 64-bit LSB shared object 字样。

5. 部署与调用:让主程序找到你的库

生成了.so文件,怎么用呢?这里有个常见的“坑”:库文件的存放位置和系统的查找路径。

方法一:安装到系统库目录(推荐用于正式部署) 像系统自带的库一样,把我们的库放到标准目录,比如/usr/local/lib。

sudo cp libmodel_add.so /usr/local/lib
sudo ldconfig # 更新动态链接器运行时绑定

ldconfig命令会刷新系统共享库的缓存,让新安装的库立即可被找到。这样做之后,你在编译程序时就可以直接用-l选项了。

方法二:指定当前目录(适合开发测试) 在开发阶段,我们不想污染系统目录。可以在编译主程序时,通过-L指定库的搜索路径。

g++ -o test_app test_main.cpp -L. -lmodel_add
  • -L.:告诉链接器在当前目录(.)寻找库文件。
  • -lmodel_add:链接名为model_add的库(链接器会自动加上lib前缀和.so后缀)。

但这样编译出的程序,运行时还需要告诉系统去哪里找这个库。有两种方式:

  1. 将库所在路径添加到环境变量LD_LIBRARY_PATH中:
    export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
    ./test_app
    
  2. 或者在编译时,通过-Wl,-rpath选项将路径嵌入可执行文件:
    g++ -o test_app test_main.cpp -L. -lmodel_add -Wl,-rpath=.
    
    这样编译出的test_app,运行时就会自动在当前目录找库。

编写测试程序 现在,我们来写一个简单的C++程序test_main.cpp调用它:

// test_main.cpp
#include <iostream>

// 声明我们库里的函数。注意,名字和 model_interface.cpp 里的一致。
extern “C” double add_model(double, double);

int main() {
    double x, y;
    std::cout << “Enter first number: “;
    std::cin >> x;
    std::cout << “Enter second number: “;
    std::cin >> y;

    double sum = add_model(x, y);
    std::cout << “The sum is: “ << sum << std::endl;

    return 0;
}

注意,这里只有函数声明,没有定义。函数的实现在我们编译的libmodel_add.so里。

编译并运行 使用我们上面讨论的方法二(开发测试)来编译:

g++ -o test_app test_main.cpp -L. -lmodel_add -Wl,-rpath=.

然后直接运行:

./test_app

如果看到程序能正确输出两数之和,那么恭喜你,整个流程彻底跑通了!从SIMULINK的图形化模型,到Ubuntu上一个独立的、可被任意C/C++程序调用的动态链接库,你已经掌握了这条高效的生产力链条。

6. 进阶探讨与避坑指南

走通基本流程只是开始,在实际项目中你会遇到更复杂的情况。这里分享几个我踩过坑后总结的经验。

复杂模型的处理 如果你的SIMULINK模型包含了S函数、Stateflow状态机、或者引用了自定义的MATLAB函数,代码生成配置会更复杂。对于S函数,你需要确保有对应的TLC目标语言编译器文件,或者将其转换为Embedded MATLAB Function。Stateflow生成代码一般没问题,但要检查生成代码的可读性。自定义MATLAB函数需要确保其支持代码生成(使用coder.screener检查),或者将其功能用Simulink基础模块重新实现。

多采样率与模型引用 对于多速率系统,ERT生成的代码会包含多个step函数,例如model_step()和model_step_fast()。封装接口时,你需要根据主程序的节奏来调用对应的step函数。如果使用了模型引用,生成代码时会为每个子模型生成独立的文件和函数,你需要分别封装它们,或者配置生成“单模型”代码。

数据类型与内存对齐 SIMULINK和C/C++的数据类型映射要小心。double和single对应real_T和real32_T。特别是布尔类型,在Simulink里是boolean,生成代码可能是uint8_T。在跨语言调用(如Python)时,这点尤其重要。另外,生成代码中的结构体可能会有特定的内存对齐要求,在封装时如果自己定义结构体传递数据,需要确保对齐方式一致,否则可能导致数据错乱或性能下降。可以查看rtwtypes.h里的定义。

性能与优化 默认生成的代码为了可读性和安全性,可能未做极致优化。在确定功能正确后,可以尝试在SIMULINK配置中开启优化选项,如 Code Generation -> Optimization 下的 Remove root-level I/O zero initialization(移除根级I/O零初始化)、Remove internal data zero initialization(移除内部数据零初始化)等,可以减小代码体积并提升速度。在g++编译时,也可以加上-O2或-O3优化等级。

调试技巧 如果调用库时程序崩溃(段错误),调试起来比普通程序麻烦。首先,确保你的库和主程序都是用-g选项编译的,保留调试信息。可以使用gdb进行调试:

gdb ./test_app

在gdb中,用run启动程序,崩溃后用bt查看调用栈。如果问题在库内部,可能需要查看库的源代码。另外,一个很实用的工具是ldd,用来查看可执行文件依赖哪些动态库,以及它们是否都被找到:

ldd ./test_app

如果看到libmodel_add.so => not found,就说明运行时库路径没设置对。

把SIMULINK模型变成动态链接库,本质上是在搭建一座连接快速原型设计(Simulink)与严肃软件工程(C/C++项目)的桥梁。这座桥搭好了,算法迭代和系统集成就能并行不悖,大大提升开发效率。希望这篇详细的实战指南,能帮你把这座桥搭得又稳又好。

更多推荐