写在前面
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 建模
Simulink 搭建二阶传递函数模型
先来个简单的,搭建一个二阶传递函数模型;
使用 simulink 完成绘图
-
[必要] 打开 Matlab R2020a,切换 matlab 工作目录;
工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;
-
点击顶部的 Simulink 图标;
或者在 matlab 命令行窗口输入
simulink
进行启动; -
创建一个空白模型;
-
从菜单栏 “SIMULATION” –> “library browser” 中拖拽模型到绘图窗口:
- “library browser” –> “Simulink/Sources” 找到 “阶跃信号”、“矩形脉冲”、“锯齿波”、“正弦波”;
- “library browser” –> “Simulink/Signal Routing” 找到 “手动开关”(双击切换状态);
- “library browser” –> “Simulink/Continuous” 找到 “传递函数”;
- “library browser” –> “Simulink/Sinks” 找到 “Out1”、“示波器”(鼠标放上面可以新增显示通道);
配置输入信号和传递函数
-
配置阶跃信号:在第 1s 时产生一个值为 1 的信号
-
配置矩形脉冲:周期 2s,占空比 50%,峰值为 1;
-
配置锯齿波:周期 2s,峰值为 1;
-
配置正弦波:周期 2*3.14,峰值为 1;
-
配置传递函数:
1/(s^2+2*s+1)
;
最终得到下图的模型:
查看输入输出波形
结束时间均设置为 10s,黄线是输入,蓝线是输出;
输入信号 | 仿真结果(双击示波器图标可看波形) |
---|---|
阶跃信号 | ![]() |
矩形脉冲 | ![]() |
锯齿波 | ![]() |
正弦波 | ![]() |
Simulink 搭建 PID 模型
使用 simulink 完成绘图
-
[必要] 打开 Matlab R2020a,切换 matlab 工作目录;
工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;
-
点击顶部的 Simulink 图标;
或者在 matlab 命令行窗口输入
simulink
进行启动; -
创建一个空白模型;
-
从菜单栏 “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 参数
-
配置阶跃信号:在第 10s 时产生一个值为 1 的信号
-
配置矩形脉冲:周期 20s,占空比 50%,峰值为 1;
-
配置 PID:
- 连续模型,P 系数为 1,I 系数为 1,D 系数为 1;
-
配置传递函数:
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 搭建状态机模型
使用 simulink 完成绘图
-
[必要] 打开 Matlab R2020a,切换 matlab 工作目录;
工作目录不能是 matlab 初始安装目录,否则仿真运行时会报错;
-
点击顶部的 Simulink 图标;
或者在 matlab 命令行窗口输入
simulink
进行启动; -
创建一个空白模型;
-
从菜单栏 “SIMULATION” –> “library browser” 中拖拽模型到绘图窗口:
先不要连线,配置好 Chart 的输入输出再进行连线;
- “library browser” –> “Simulink/Sources” 找到 “正弦波”;
- “library browser” –> “Stateflow” 找到 “Chart”;
- “library browser” –> “Simulink/Sinks” 找到 “示波器”(鼠标放上面可以新增显示通道);
-
双击 Chart 进行配置;
-
首先展开菜单栏 ”SIMULATION“ –> ”PREPARE“ –> ”Symbols Pane“ 展开符号面板;
-
在符号面板新增输入 “input”、输出 “output”;
按照创建 data、更改 type、输入 name 的顺序进行创建;
-
从左侧工具栏拖入两个 State(圆角矩形)到绘图区;
-
第一个 State 填入代码;
1 2 3
Name1 entry:output=1;
-
第二个 State 填入代码;
1 2 3
Name2 entry:output=-1;
-
画上两个 State 相互转换的箭头;
- 从左到右条件:
[input>=0]
; - 从右到左调节:
[input<0]
;
- 从左到右条件:
-
-
-
回到顶层绘图页,进行连线:
-
仿真运行结果:
单步调试的方法
双击 Chart 进入流程图编辑界面,按下菜单栏 ”SIMULATION“ –> ”Step Forward“ 按钮进行单步调试;
Matlab 生成代码
.m 脚本生成 C、CPP 代码
生成代码
-
[必要] 切换 matlab 工作目录;
最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;
-
新建一个 foo.m 文件并保存,内容如下;
%#codegen
可以防止代码生成出现警告;1 2 3 4
function c = foo(a, b) %#codegen %Thisfunction muliplies a and b c = a * b;
-
在 matlab 命令窗口输入
mex -setup
选择一个存在的编译器;如果只搜索到一个编译器,那么会默认选择它;
-
在 matlab 命令窗口输入
coder
或菜单栏 “APP” –> “MATLAB Coder” 打开代码生成引导窗口; -
选择第 1 步创建的 foo.m 脚本;
-
定义传参类型;
这里 a、b 都定义为 int32 的 1 维矩阵;
-
[可跳过] 输入表达式对函数进行验证;
注意传参类型要和第 5 步指定的一致;
-
选择生成配置;
注意:这里生成的 for host 版本对 matlab 安装目录下的头文件也有依赖;
-
生成的文件和函数如图:
左下角是生成的所有文件;
进行验证
-
使用 VS2019 创建一个空白 CMake 工程 demo1;
-
将 matlab 生成的如下文件拷贝到工程:
- foo.cpp
- foo.h
- foo_types.h
- rtwtypes.h
-
修改子目录中的 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")
-
修改 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; }
结果如下:
simulink 模型生成 C、CPP 代码
生成代码
-
[必要] 切换 matlab 工作目录;
最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;
-
打开 simulink 新建一个 tf1 模型并保存,绘图如下;
注意:连续 simulink 模型生成代码前需要进行离散处理。如果不进行离散处理,虽然也能生成代码,但是结果会很迷,和仿真结果会不同;
-
在 simulink 菜单栏 “APPS” –> “Model Discretizer” 打开离散化工具 TAB;
接着选择要离散化的传递函数模型进行离散化(步长 1.0);
最终的模型图会变成下面这样:
-
在 simulink 菜单栏 “APPS” –> “Simulink Coder” 打开代码生成器 TAB;
注意:这里生成的会是 for host 版本,对 matlab 安装目录下的头文件也有依赖;
如果是生成给嵌入式产品使用,那就选择 ”Embedded Coder“,生成出的代码和 for host 在依赖上会有差异;
-
接着在 “C CODE” TAB 上进行配置;
- 图标 1 选择生成 C 还是 CPP 代码;
- 图标 2 启动代码生成的引导窗口;
-
引导窗口选择生成的实例:
-
引导窗口选择优化方式:
-
最终生成的文件和函数如下:
进行验证
-
使用 VS2019 创建一个空白 CMake 工程 demo2;
-
将 matlab 生成的如下文件拷贝到工程:
- tf1.c
- tf1.h
- tf1_private.h
- tf1_types.h
- multiword_types.h
- rtwtypes.h
-
修改子目录中的 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")
-
修改 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 代码
生成代码
-
[必要] 切换 matlab 工作目录;
最终生成的代码会放到工作目录,工作目录如果是 matlab 初始安装目录那么生成代码时会报错;
-
打开 simulink,在上面文章建立的 Stateflow 模型的基础上修改下输入输出并保存为 chart1,绘图如下:
-
在 simulink 菜单栏 “APPS” –> “Simulink Coder” 打开代码生成器 TAB;
注意:这里生成的会是 for host 版本,对 matlab 安装目录下的头文件也有依赖;
如果是生成给嵌入式产品使用,那就选择 ”Embedded Coder“,下一节有使用示例;
-
接着在 “C CODE” TAB 上进行配置;
- 图标 1 选择生成 C 还是 CPP 代码;
- 图标 2 启动代码生成的引导窗口;
-
引导窗口选择生成的实例:
-
引导窗口选择优化方式:
-
最终生成的文件和函数如下:
进行验证
-
使用 VS2019 创建一个空白 CMake 工程 demo3;
-
将 matlab 生成的如下文件拷贝到工程:
- chart1.c
- chart1.h
- chart1_private.h
- chart1_types.h
- multiword_types.h
- rtwtypes.h
-
修改子目录中的 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")
-
修改 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 模型作为示例;
生成代码
-
菜单栏 ”APPS“ –> ”Embedded Coder“;
-
菜单栏 ”C CODE“ –> ”Quick Start“ 启动向导;
-
选择用于生成代码的模型;
-
选择输出;
-
选择处理器,进而配置字大小;
-
选择优化方式;
-
最终生成的文件和函数如下:
可以看到生成的文件数量只有 4 个(其中 ert_main.c 告诉我们调用的方法,故实际不需要用到它);
进行验证
-
使用 VS2019 创建一个空白 CMake 工程 demo4;
-
将 matlab 生成的如下文件拷贝到工程:
- chart1.c
- chart1.h
- rtwtypes.h
-
修改子目录中的 CMakeLists.txt
1 2 3
cmake_minimum_required (VERSION 3.8) add_executable (demo4 "demo4.cpp" "demo4.h" "chart1.c" "chart1.h" "rtwtypes.h")
-
修改 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; }
运行结果如下: