Matlab 建模和代码生成

Simulink 绘图,Coder 生成代码

Posted by Jerry Chen on November 13, 2021

写在前面

Matlab 需要安装的组件

组件的介绍:MATLAB & Simulink

  • MATLAB 本体;
  • Simulink 本体;
  • 状态机:Stateflow;
  • 代码生成:
    • MATLAB Coder:.m 脚本转 C、C++ 代码;
    • Simulink Coder:simulink、stateflow 模型转 C、C++ 代码;
    • Embedded Coder:扩展了 MATLAB Coder、Simulink Coder 的功能,以便于生成适合嵌入式 MCU 的代码;
    • [不需要] HDL Coder:.m 脚本、simulink、stateflow 模型转 VHDL、Verilog 代码;
  • Control Systems Toolbox:simulink 上的连续模型离散化工具;

值得注意的是 MATLAB Coder、Simulink Coder 需要外部编译器支持(如 VS2019、mingw-w64 + windows-10-sdk),详见支持的编译器

我的电脑已经提前装好了 VS2019;

Matlab 添加 mexopts 适配 VS2022

VS 更新后,Matlab 配置好的编译器可能会不受支持(在主页命令行窗口输入 mex -setup -v 进行设置和查看信息);

在 Matlab 的安装目录下,我的在 D:\Program Files\Matlab R2020a\bin\win64\mexopts 会有一系列可用的编译器配置文件。Matlab R2020a 是不支持 VS2022 的,所以我们把 msvc2019.xml 和 msvcpp2019.xml 拷贝一份变成 msvc2022.xml 和 msvcpp2022.xml,内容上做如下修改:

  • 将如下信息:

    1
    2
    3
    4
    
    Name="Microsoft Visual C++ 2019"
    ShortName="MSVCPP160"
    Manufacturer="Microsoft"
    Version="16.0"
    

    改为:

    1
    2
    3
    4
    
    Name="Microsoft Visual C++ 2022"
    ShortName="MSVCPP170"
    Manufacturer="Microsoft"
    Version="17.0"
    
  • 全局替换 [16.0,17.0)[17.0,18.0)

PID 是什么

先看一个模型

模型来自 bilibili up 主 “机器人工坊”;

目标:让圆柱物体尽可能停在横杆中点;

控制方式:通过舵机转动带动连杆,从而使横杆调整高低,最终使物体左右滚动;

控制模型解析

该系统的控制使用了 PID 模型:

下面所有的思考请结合图中的公式分析;

  • P 让偏离目标的物体动起来;

    • 本例偏离目标距离越大,调节力度越大;

    • 本例偏离目标距离越小,调节力度越小;偏差距离为 0 时调节效果为 0,即最终作用是让物体停下来;

      思考一下:只有 P 就可以让物体停下来,干嘛还要 D?

      性能原因,左右摇摆过冲距离大,停止时间长;

  • D 让目标快速停下来,也就是阻尼(误差的微分,当前示例中是距离的微分,即速度);

    • 本例速度越大,调节力度越大;

    • 本例速度越小,调节力度越小;速度为 0 时调节效果为 0,即最终作用是让物体停下来;

      思考一下:D 就可以让物体停下来,只有 D 没有 P 行不行?

      不行,假设来了一个扰动使物体在偏离目标点的地方停止了一段时间,这段时间速度为 0,系统就不会进行调节;

      思考一下:只有 D、P 的情况下,时间拉到无限长,认为物体已经停止,静态(稳态)误差 e 是否为 0?

      只有 D、P 时,从数学公式的角度看稳态误差不可能为 0。静止情况下 D 微分为 0,如果 P 中误差 e 为 0,则最终公式左边 u 为 0,和非零目标冲突;

  • I 决定停在哪里,用于调整静态误差(当前示例中让停下来的位置调整为中点);

    • 稳态是 D、P 效果为 0,I 产生的效果等于目标;

      思考一下:为什么 I 是积分运算?

      因为完美稳态下 P 效果为 0(误差 e 为 0),D 效果为 0(微分为 0),而积分产生的效果可以非 0,故适合做稳态补偿;

Matlab 建模

先来个简单的,搭建一个二阶传递函数模型;

  1. [必要] 打开 Matlab R2020a,切换 matlab 工作目录;

    工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;

  2. 点击顶部的 Simulink 图标;

    或者在 matlab 命令行窗口输入 simulink 进行启动;

  3. 创建一个空白模型;

  4. 从菜单栏 “SIMULATION” –> “library browser” 中拖拽模型到绘图窗口:

    • “library browser” –> “Simulink/Sources” 找到 “阶跃信号”、“矩形脉冲”、“锯齿波”、“正弦波”;
    • “library browser” –> “Simulink/Signal Routing” 找到 “手动开关”(双击切换状态);
    • “library browser” –> “Simulink/Continuous” 找到 “传递函数”;
    • “library browser” –> “Simulink/Sinks” 找到 “Out1”、“示波器”(鼠标放上面可以新增显示通道);

配置输入信号和传递函数

  1. 配置阶跃信号:在第 1s 时产生一个值为 1 的信号

  2. 配置矩形脉冲:周期 2s,占空比 50%,峰值为 1;

  3. 配置锯齿波:周期 2s,峰值为 1;

  4. 配置正弦波:周期 2*3.14,峰值为 1;

  5. 配置传递函数:1/(s^2+2*s+1)

最终得到下图的模型:

查看输入输出波形

结束时间均设置为 10s,黄线是输入,蓝线是输出;

输入信号 仿真结果(双击示波器图标可看波形)
阶跃信号
矩形脉冲
锯齿波
正弦波
  1. [必要] 打开 Matlab R2020a,切换 matlab 工作目录;

    工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;

  2. 点击顶部的 Simulink 图标;

    或者在 matlab 命令行窗口输入 simulink 进行启动;

  3. 创建一个空白模型;

  4. 从菜单栏 “SIMULATION” –> “library browser” 中拖拽模型到绘图窗口:

    • “library browser” –> “Simulink/Sources” 找到 “阶跃信号”、“矩形脉冲”;
    • “library browser” –> “Simulink/Signal Routing” 找到 “手动开关”(双击切换状态);
    • “library browser” –> “Simulink/Math Operations” 找到 “加法器”;
    • “library browser” –> “Simulink/Continuous” 找到 “PID Controller”;
    • “library browser” –> “Simulink/Continuous” 找到 “传递函数”;
    • “library browser” –> “Simulink/Sinks” 找到 “示波器”(鼠标放上面可以新增显示通道);

配置输入信号和 PID 参数

  1. 配置阶跃信号:在第 10s 时产生一个值为 1 的信号

  2. 配置矩形脉冲:周期 20s,占空比 50%,峰值为 1;

  3. 配置 PID:

    • 连续模型,P 系数为 1,I 系数为 1,D 系数为 1;

  4. 配置传递函数:1/(s^2+2*s+1)

查看输入输出波形

结束时间均设置为 100s,黄线是输入,蓝线是输出;

从下面的结果可以看出给现实黑盒(任意模型)加上前置 PID 模型后的输出信号可以有效拟合目标输入波形;

输出波形拟合输入波形有什么用?

假设输出波形的采样信号是温度。通过计算机很容易给出一条参考曲线 (0, 15), (1, 16), (2, 19), (3, 18),加上 PID 模块后系统输出的温度将拟合给出的曲线;

不同目标波形下的输出拟合 函数 1/(s+1) 模拟现实黑盒 函数 1/(s^2+2*s+1) 模拟现实黑盒
阶跃信号
矩形脉冲

Stateflow 搭建状态机模型

  1. [必要] 打开 Matlab R2020a,切换 matlab 工作目录;

    工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;

  2. 点击顶部的 Simulink 图标;

    或者在 matlab 命令行窗口输入 simulink 进行启动;

  3. 创建一个空白模型;

  4. 从菜单栏 “SIMULATION” –> “library browser” 中拖拽模型到绘图窗口:

    先不要连线,配置好 Chart 的输入输出再进行连线;

    • “library browser” –> “Simulink/Sources” 找到 “正弦波”;
    • “library browser” –> “Stateflow” 找到 “Chart”;
    • “library browser” –> “Simulink/Sinks” 找到 “示波器”(鼠标放上面可以新增显示通道);

  5. 双击 Chart 进行配置;

    1. 首先展开菜单栏 ”SIMULATION“ –> ”PREPARE“ –> ”Symbols Pane“ 展开符号面板;

    2. 在符号面板新增输入 “input”、输出 “output”;

      按照创建 data、更改 type、输入 name 的顺序进行创建;

    3. 从左侧工具栏拖入两个 State(圆角矩形)到绘图区;

      • 第一个 State 填入代码;

        1
        2
        3
        
        Name1
                
        entry:output=1;
        
      • 第二个 State 填入代码;

        1
        2
        3
        
        Name2
                
        entry:output=-1;
        
      • 画上两个 State 相互转换的箭头;

        • 从左到右条件:[input>=0]
        • 从右到左调节:[input<0]
  6. 回到顶层绘图页,进行连线:

  7. 仿真运行结果:

单步调试的方法

双击 Chart 进入流程图编辑界面,按下菜单栏 ”SIMULATION“ –> ”Step Forward“ 按钮进行单步调试;

Matlab 生成代码

.m 脚本生成 C、CPP 代码

生成代码

  1. [必要] 切换 matlab 工作目录;

    最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;

  2. 新建一个 foo.m 文件并保存,内容如下;

    %#codegen 可以防止代码生成出现警告;

    1
    2
    3
    4
    
    function c = foo(a, b)	%#codegen
       
    %Thisfunction muliplies a and b
    c = a * b;
    
  3. 在 matlab 命令窗口输入 mex -setup 选择一个存在的编译器;

    如果只搜索到一个编译器,那么会默认选择它;

  4. 在 matlab 命令窗口输入 coder 或菜单栏 “APP” –> “MATLAB Coder” 打开代码生成引导窗口;

  5. 选择第 1 步创建的 foo.m 脚本;

  6. 定义传参类型;

    这里 a、b 都定义为 int32 的 1 维矩阵;

  7. [可跳过] 输入表达式对函数进行验证;

    注意传参类型要和第 5 步指定的一致;

  8. 选择生成配置;

    注意:这里生成的 for host 版本对 matlab 安装目录下的头文件也有依赖;

  9. 生成的文件和函数如图:

    左下角是生成的所有文件;

进行验证

  1. 使用 VS2019 创建一个空白 CMake 工程 demo1;

  2. 将 matlab 生成的如下文件拷贝到工程:

    • foo.cpp
    • foo.h
    • foo_types.h
    • rtwtypes.h

  3. 修改子目录中的 CMakeLists.txt

    注意更改你的 matlab 头文件目录;

    1
    2
    3
    4
    5
    
    cmake_minimum_required (VERSION 3.8)
       
    include_directories("D:\\Program Files\\Matlab R2020a\\extern\\include")
       
    add_executable (demo1 "demo1.cpp" "demo1.h" "foo.c" "foo.h" "foo_types.h" "rtwtypes.h")
    
  4. 修改 demo1.cpp 调用 foo 函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #include "demo1.h"
    extern "C" {
      #include "foo.h"
    }
       
    using namespace std;
       
    int main() {
    	cout << "c=" << foo(3, 4) << endl;
    	return 0;
    }
    

    结果如下:

生成代码

  1. [必要] 切换 matlab 工作目录;

    最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;

  2. 打开 simulink 新建一个 tf1 模型并保存,绘图如下;

    注意:连续 simulink 模型生成代码前需要进行离散处理。如果不进行离散处理,虽然也能生成代码,但是结果会很迷,和仿真结果会不同;

  3. 在 simulink 菜单栏 “APPS” –> “Model Discretizer” 打开离散化工具 TAB;

    接着选择要离散化的传递函数模型进行离散化(步长 1.0);

    最终的模型图会变成下面这样:

  4. 在 simulink 菜单栏 “APPS” –> “Simulink Coder” 打开代码生成器 TAB;

    注意:这里生成的会是 for host 版本,对 matlab 安装目录下的头文件也有依赖;

    如果是生成给嵌入式产品使用,那就选择 ”Embedded Coder“,生成出的代码和 for host 在依赖上会有差异;

  5. 接着在 “C CODE” TAB 上进行配置;

    • 图标 1 选择生成 C 还是 CPP 代码;
    • 图标 2 启动代码生成的引导窗口;

  6. 引导窗口选择生成的实例:

  7. 引导窗口选择优化方式:

  8. 最终生成的文件和函数如下:

进行验证

  1. 使用 VS2019 创建一个空白 CMake 工程 demo2;

  2. 将 matlab 生成的如下文件拷贝到工程:

    • tf1.c
    • tf1.h
    • tf1_private.h
    • tf1_types.h
    • multiword_types.h
    • rtwtypes.h

  3. 修改子目录中的 CMakeLists.txt

    注意更改你的 matlab 头文件目录、simulink 头文件目录;

    1
    2
    3
    4
    5
    6
    
    cmake_minimum_required (VERSION 3.8)
       
    include_directories("D:\\Program Files\\Matlab R2020a\\extern\\include")
    include_directories("D:\\Program Files\\Matlab R2020a\\simulink\\include")
       
    add_executable (demo2 "demo2.cpp" "demo2.h" "tf1.c" "tf1.h" "tf1_private.h" "tf1_types.h" "multiword_types.h" "rtwtypes.h")
    
  4. 修改 demo2.cpp 调用 tf1 模型的相关函数:

    细心的小伙伴会发现循环调用 tf1_step() 时没有进行延时,因为每次调用 tf1_step() 就相当于 matlab 过了 1 个单位时间;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #include "demo2.h"
    extern "C" {
      #include "tf1.h"
    }
       
    using namespace std;
       
    int main() {
    	tf1_initialize();
    	for (int i = 0; i <= 20; i++) {
    		/* 更改输入信号(目前是阶跃信号) */
    		if (i >= 1) {
    			tf1_U.In1 = 1;
    		} else {
    			tf1_U.In1 = 0;
    		}
    		tf1_step();
    		/* 打印输出信号 */
    		cout << "[out] " << i << ":" << tf1_Y.Out1 << endl;
    	}
    	return 0;
    }
    

    和 matlab 仿真结果进行对比,结果完全一致:

    右边的仿真图可以看到步长为 1.0 个单位时间,符合离散化时的设置;

    离散调用 tf1_step() 经过 20 个单位时间的结果

    tf1_step() 是由 simulink coder 生成的模型
    Matlab 仿真 20 个单位时间的结果

Stateflow 模型生成 C、CPP 代码

生成代码

  1. [必要] 切换 matlab 工作目录;

    最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;

  2. 打开 simulink,在上面文章建立的 Stateflow 模型的基础上修改下输入输出并保存为 chart1,绘图如下:

  3. 在 simulink 菜单栏 “APPS” –> “Simulink Coder” 打开代码生成器 TAB;

    注意:这里生成的会是 for host 版本,对 matlab 安装目录下的头文件也有依赖;

    如果是生成给嵌入式产品使用,那就选择 ”Embedded Coder“,下一节有使用示例;

  4. 接着在 “C CODE” TAB 上进行配置;

    • 图标 1 选择生成 C 还是 CPP 代码;
    • 图标 2 启动代码生成的引导窗口;

  5. 引导窗口选择生成的实例:

  6. 引导窗口选择优化方式:

  7. 最终生成的文件和函数如下:

进行验证

  1. 使用 VS2019 创建一个空白 CMake 工程 demo3;

  2. 将 matlab 生成的如下文件拷贝到工程:

    • chart1.c
    • chart1.h
    • chart1_private.h
    • chart1_types.h
    • multiword_types.h
    • rtwtypes.h

  3. 修改子目录中的 CMakeLists.txt

    注意更改你的 matlab 头文件目录、simulink 头文件目录;

    1
    2
    3
    4
    5
    6
    
    cmake_minimum_required (VERSION 3.8)
       
    include_directories("D:\\Program Files\\Matlab R2020a\\extern\\include")
    include_directories("D:\\Program Files\\Matlab R2020a\\simulink\\include")
       
    add_executable (demo3 "demo3.cpp" "demo3.h" "chart1.c" "chart1.h" "chart1_private.h" "chart1_types.h" "multiword_types.h" "rtwtypes.h")
    
  4. 修改 demo3.cpp 调用 chart1 模型的相关函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    #include "demo3.h"
    extern "C" {
      #include "chart1.h"
    }
       
    using namespace std;
       
    int main() {
    	chart1_initialize();
    	for (int i = 0; i <= 20; i++) {
    		/* 更改输入信号 */
    		if (i % 5) {
    			chart1_U.input = 1;
    		}
    		else {
    			chart1_U.input = -1;
    		}
    		chart1_step();
    		/* 打印输出信号 */
    		cout << "[out] " << i << ":" << chart1_Y.output << endl;
    	}
    	return 0;
    }
    

    运行结果如下:

利用 Embedded Coder 生成不依赖 Windows 的代码

以上一步的 Stateflow 模型作为示例;

流程文档:Generate Code by Using the Quick Start Tool

生成代码

  1. 菜单栏 ”APPS“ –> ”Embedded Coder“;

  2. 菜单栏 ”C CODE“ –> ”Quick Start“ 启动向导;

  3. 选择用于生成代码的模型;

  4. 选择输出;

  5. 选择处理器,进而配置字大小;

  6. 选择优化方式;

  7. 最终生成的文件和函数如下:

    可以看到生成的文件数量只有 4 个(其中 ert_main.c 告诉我们调用的方法,故实际不需要用到它);

进行验证

  1. 使用 VS2019 创建一个空白 CMake 工程 demo4;

  2. 将 matlab 生成的如下文件拷贝到工程:

    • chart1.c
    • chart1.h
    • rtwtypes.h

  3. 修改子目录中的 CMakeLists.txt

    1
    2
    3
    
    cmake_minimum_required (VERSION 3.8)
       
    add_executable (demo4 "demo4.cpp" "demo4.h" "chart1.c" "chart1.h" "rtwtypes.h")
    
  4. 修改 demo4.cpp 调用 chart1 模型的相关函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    #include "demo4.h"
    extern "C" {
      #include "chart1.h"
    }
       
    using namespace std;
       
    int main() {
    	chart1_initialize();
    	for (int i = 0; i <= 20; i++) {
    		/* 更改输入信号 */
    		if (i % 5) {
    			rtU.input = 1;
    		}
    		else {
    			rtU.input = -1;
    		}
    		chart1_step();
    		/* 打印输出信号 */
    		cout << "[out] " << i << ":" << rtY.output << endl;
    	}
    	return 0;
    }
    

    运行结果如下: