MBD的Simulink使用技巧:详解代码生成中的模型与代码(2)

电子说

1.2w人已加入

描述

1 Simulink代码生成的基本概念(续)

1.1 上一期补充内容

上一篇文章中提到,生成嵌入式代码,必须选择定步长求解器。实际中,生成嵌入式代码几乎不会使用Simulink模型库中的连续模型,往往需要通过最简单的离散模块来实现算法模型。

所以要强调一点:生成嵌入式代码,要学会 使用和适应离散模型的搭建方式 。在建模的一开始就要充分考虑离散模型的特点和需求。

一个模型是允许多个离散步长的,这涉及到多任务相关的内容,后续再作详细介绍。建议读者在建模的时候,将离散步长(其倒数即为采样频率)显示出来,方便辨别不同模块的实际步长。显示离散步长的方法如下:

simulink仿真

显示离散步长 - From autoMBD

同时也建议读者将端口数据类型显示出来,因为不同模块之间的数据传递,要保证数据类型相同(后续将介绍数据类型的相关内容)。显示端口数据类型的方法如下所示:

simulink仿真

显示端口数据类型 - From autoMBD

1.2 Embedded Coder的使用

Embedded Coder工具专门为嵌入式软件生成代码而设计,集成了MATLAB Coder和Simulink Coder,可以将m脚本和模型生成C代码。Embedded Coder可以在下图位置找到:

simulink仿真

Embedded Coder位置 - From autoMBD

单击“Embedded Coder”便可以进入到Code Perspective窗口。在这个窗口下可以看到四个主要功能区域:

  • 工具栏
  • 模型区域
  • 代码面板
  • 代码映射面板

如下图所示:

simulink仿真

Code Perspective**四个功能区域 - From autoMBD

Tips :MATLAB/Simulink的版本不同,上述界面可能会有差异,截图为版本为2020b。

在工具栏中,可以找到大部分关于代码生成的选项工具或配置入口。模型区域用于编辑和设计模型,如果设置了定步长求解器和ert系统目标文件,单击工具栏中的“Generate Code”按钮便可以生成代码了。

代码面板用于查看生成的代码。在代码面板中,点击生成的代码,代码对应的模型会高亮显示。这样方便用户追踪模型生成的代码。

代码映射面板有很多功能,涉及到的概念和知识点很多,在后续的内容中会逐步讲解。

读者可以自行探索和体验这四个功能区域的作用和使用方法。

2 详解模型与代码之间的联系

为了便于展示此部分内容,我制作了一个简易的PI控制模型作为示例。读者可以在autoMBD资源库的“临时资源分享”文件夹中找到该模型(资源序号 tA22 )。资源库链接的获取可以在《autoMBD原创技术文章合集》中找到(见文章开头)。

2.1 模型生成的代码

打开PI控制器示例模型,可以看到模型如下图所示:

simulink仿真

PI控制器示例模型 - From autoMBD

该示例模型已经配置好,点击“Generate Code”生成代码。可以发现一共生成了六个文件:

simulink仿真

PI控制器示例模型生成的代码 - From autoMBD

生成的文件位于模型同级目录下“** 模型名 _ert_rtw**”文件夹内,这六个文件的作用分别是:

  • ert_main.c :该文件是一个样例文件,向用户展示主程序如何调用模型代码,在代码集成时可以参考该文件;
  • 模型名.c:该文件包含模型的全部实现方法,包含变量的声明和定义,Step函数、初始化函数、终止函数等的定义和实现,即“模型的本身”;
  • 模型名.h:该文件包含模型所依赖的数据结构、数据类型的定义和声明,以及模型变量、模型算法函数的外部声明;
  • 模型名_private.h:包含模型本地的宏和数据类型定义,有定义才会生成相关内容;
  • 模型名_types.h:该文件包含模块参数(Parameters)和模型数据(Model Data)的数据结构的向前声明,在一些可重用函数中可能会被使用;
  • rtwtypes.h :定义了基本的数据类型和宏,大部分的生成代码可能会依赖该文件;
  • 模型名_data.c:上图中没有生成,但在某些情况下会生成该文件,其中包含模型中的模块参数(Parameters)、常数模块和I/O的数据结构的定义和声明。

对于代码集成来说,用户只需要在主函数代码中,添加下面这个语句,即可使用模型生成的代码,实现相关的算法和功能:

#inlcude "模型名.h"

读者可以自行阅读生成的代码,初步接触生成代码的风格和特点。接下来会详细介绍模型和代码之间的对应关系。

2.2 代码之母——模型

作为MBD的核心,怎么强调对****模型的理解的重要性都不为过。

在嵌入式代码中,数据是代码的重要组成部分。 代码中的数据和模型中的数据是怎么联系起来的 ,便是今天讨论的重点。对于模型,有四个与生成代码紧密关联的 模型数据形式

  • 信号(Signals)
  • 参数(Parameters)
  • 状态(States)
  • 模型数据(Model Data)

模型中的这四种数据形式,生成的代码各不相同。通过控制这些模型数据的配置,即可控制生成代码的数据类型、存储位置和数据形式。

在开始介绍前,给出模型数据形式的思维导图:

simulink仿真

模型数据形式的思维导图 - From autoMBD

2.2.1 信号

信号(Signals), 即模型中不同模块之间的数据传递线,有两种:外部信号内部信号 。其中外部信号又分为外部输入信号和外部 输出信号

以我构建的PI控制器模型为例,模型包含的信号如下图所示:

simulink仿真

PI控制器模型的信号 - From autoMBD

上图中,Req_Ctrl和Feedback与输入端口连接,属于外部输入信号;PI_Ctrl与输出端口连接,属于外部输出信号;Err、P_value和I_value没有与任何端口连接,属于内部信号。

Tips :上图并没有标注全部的内部信号,一般只标注有重要、意义的内部信号即可。

保持默认设置情况下生成代码, 外部信号会生成全局变量 ,其中输入变量为“ ** 模型名 _U** ”,而输出变量为“ ** 模型名 _Y** ”。

以本文的示例工程为例,输入输出信号生成的全局变量以及相关的数据类型声明和定义如下所示:

/* 外部输入数据类型定义 代码生成于"模型名.h" */
/* External inputs (root inport signals with default storage) */
typedef struct {
  real_T Req_Ctrl;                     /* '< Root >/Req_Ctrl' */
  real_T Feedback;                     /* '< Root >/Feedback' */
} ExtU_autoMBD_example_PI_noSub_T;
/* 外部输出数据类型定义 代码生成于"模型名.h" */
/* External outputs (root outports fed by signals with default storage) */
typedef struct {
  real_T PI_Ctrl;                      /* '< Root >/PI_Ctrl' */
} ExtY_autoMBD_example_PI_noSub_T;
/* 外部输入变量定义 代码生成于"模型名.c" */
/* External inputs (root inport signals with default storage) */
ExtU_autoMBD_example_PI_noSub_T autoMBD_example_PI_noSubs_U;
/* 外部输出变量定义 代码生成于"模型名.c" */
/* External outputs (root outports fed by signals with default storage) */
ExtY_autoMBD_example_PI_noSub_T autoMBD_example_PI_noSubs_Y;

保持默认设置生成代码时,对于内部信号, 只有具有分叉点的信号线会生成局部变量 ,变量名为“ rtb_信号名 ”。由于是局部变量,它会随着Step函数的出栈而被释放。

其他不具备分叉点的内部信号,不会生成任何变量,而是 隐含在算法的运算过程当中

以示例工程的内部信号Err为例,模型中仅该信号具有分叉点,它生成的局部变量如下所示:

/* 代码生成于"模型名.c" */
/* Model step function */
void autoMBD_example_PI_noSubs_step(void)
{
  real_T rtb_Err; /* 内部信号Err 变量定义*/


  /* Sum: '< Root >/Sum2' incorporates:
   *  Inport: '< Root >/Feedback'
   *  Inport: '< Root >/Req_Ctrl'
   */
  rtb_Err = autoMBD_example_PI_noSubs_U.Req_Ctrl -
    autoMBD_example_PI_noSubs_U.Feedback; /* 内部信号Err 变量计算*/


  /* Outport: '< Root >/PI_Ctrl' incorporates:
   *  DiscreteIntegrator: '< Root >/Discrete-Time Integrator'
   *  Gain: '< Root >/Kp'
   *  Sum: '< Root >/Sum1'
   */
  autoMBD_example_PI_noSubs_Y.PI_Ctrl = 2.0 * rtb_Err +
    autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE; /* 内部信号Err 变量使用*/


  /* Update for DiscreteIntegrator: '< Root >/Discrete-Time Integrator' incorporates:
   *  Gain: '< Root >/Ki'
   */
  autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE += 3.0 * rtb_Err *
    0.001; /* 内部信号Err 变量使用*/
}

关于如何修改信号的代码生成模块信号(对应生成变量为“ 模型名_B ”),以及其他关于信号的配置选项,在后续的文章中进行介绍。

2.2.2 参数

参数 (Parameters)指的是模块的参数,例如:本文PI控制器模型中的PI增益模块,它们的参数分别实现Kp和Ki,模型中的Discrete-Time Integrator模块也有两个惨数,具体为采样时间参数、初始值参数。

保持默认设置情况下, 模块的参数会作为数值常数,直接用于算法的运算过程 。如下所示PI控制器生成的代码中,Kp、Ki和采样时间直接使用其数值2、3和0.001参与算法的运算。

/* 代码生成于"模型名.c" */
/* Model step function */
void autoMBD_example_PI_noSubs_step(void)
{
  real_T rtb_Err;


  /* Sum: '< Root >/Sum2' incorporates:
   *  Inport: '< Root >/Feedback'
   *  Inport: '< Root >/Req_Ctrl'
   */
  rtb_Err = autoMBD_example_PI_noSubs_U.Req_Ctrl -
    autoMBD_example_PI_noSubs_U.Feedback;


  /* Outport: '< Root >/PI_Ctrl' incorporates:
   *  DiscreteIntegrator: '< Root >/Discrete-Time Integrator'
   *  Gain: '< Root >/Kp'
   *  Sum: '< Root >/Sum1'
   */
  /* 直接使用比例增益的数值常量2.0参与计算 */
  autoMBD_example_PI_noSubs_Y.PI_Ctrl = 2.0 * rtb_Err +
    autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE;


  /* Update for DiscreteIntegrator: '< Root >/Discrete-Time Integrator' incorporates:
   *  Gain: '< Root >/Ki'
   */
  /* 直接使用积分增益的数值常量3.0和离散步长数值常量0.001参与计算 */
  autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE += 3.0 * rtb_Err *
    0.001;
}

有的时候,我们不希望模块参数是一个数值常量,而是一个可以修改的变量。例如对Kp和Ki参数进行在线标定时,需要有两个变量来保存Kp和Ki参数。

关于如何修改模块参数的代码生成方式,使其成为一个变量而不是数值常量,可以按照下图所示步骤进行:

simulink仿真

修改模块参数的生成方式 - From autoMBD

简单来说,就是让模型参数是可调(Tunable)的,这样便会生成一个新的变量来保存模型中所有模块的参数。

修改模块参数的生成方式后,重新生成代码。可以看到,在“ 模型名 .h”中会生成一个新的数据结构体,包含了所有模块的全部可调参数:

/* 模块参数数据结构体定义 代码生成于"模型名.h" */
/* Parameters (default storage) */
struct P_autoMBD_example_PI_noSubs_T_ {
  real_T Kp_Gain;                      /* Expression: 2
                                        * Referenced by: '< Root >/Kp'
                                        */
  real_T DiscreteTimeIntegrator_gainval;
                           /* Computed Parameter: DiscreteTimeIntegrator_gainval
                            * Referenced by: '< Root >/Discrete-Time Integrator'
                            */
  real_T DiscreteTimeIntegrator_IC;    /* Expression: 0
                                        * Referenced by: '< Root >/Discrete-Time Integrator'
                                        */
  real_T Ki_Gain;                      /* Expression: 3
                                        * Referenced by: '< Root >/Ki'
                                        */
};

在“ 模型名 *types.h”中会对参数结构体声明新的数据类型,并在“ 模型名 * data.c”(新生成的文件,默认配置无该文件)中定义一个该数据类型的变量,同时幅初值,如下所示:

/* 模块参数数据类型定义 代码生成于"模型名_types.h" */
/* Parameters (default storage) */
typedef struct P_autoMBD_example_PI_noSubs_T_ P_autoMBD_example_PI_noSubs_T;
/* 模块参数变量定义和赋初值 代码生成于"模型名_data.c "*/
/* Block parameters (default storage) */
P_autoMBD_example_PI_noSubs_T autoMBD_example_PI_noSubs_P = {
  /* Expression: 2
   * Referenced by: '<  Root  >/Kp'
   */
  2.0,


  /* Computed Parameter: DiscreteTimeIntegrator_gainval
   * Referenced by: '<  Root  >/Discrete-Time Integrator'
   */
  0.001,


  /* Expression: 0
   * Referenced by: '<  Root  >/Discrete-Time Integrator'
   */
  0.0,


  /* Expression: 3
   * Referenced by: '<  Root  >/Ki'
   */
  3.0
};

可以看到,用于存储模块参数的变量名为“ 模型名_P ”。重新查看生成的Step函数,可以发现PI控制器算法的计算,不再直接使用数值常量,而是使用变量“ 模型名 _P”进行的运算。

/* 代码生成于"模型名.c" */
/* Model step function */
void autoMBD_example_PI_noSubs_step(void)
{
  real_T rtb_Err;


  /* Sum: '< Root >/Sum2' incorporates:
   *  Inport: '< Root >/Feedback'
   *  Inport: '< Root >/Req_Ctrl'
   */
  rtb_Err = autoMBD_example_PI_noSubs_U.Req_Ctrl -
    autoMBD_example_PI_noSubs_U.Feedback;


  /* Outport: '< Root >/PI_Ctrl' incorporates:
   *  DiscreteIntegrator: '< Root >/Discrete-Time Integrator'
   *  Gain: '< Root >/Kp'
   *  Sum: '< Root >/Sum1'
   */
  /* 使用"模型名_P.Kp_Gain"参与计算 */
  autoMBD_example_PI_noSubs_Y.PI_Ctrl = autoMBD_example_PI_noSubs_P.Kp_Gain *
    rtb_Err + autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE;


  /* Update for DiscreteIntegrator: '< Root >/Discrete-Time Integrator' incorporates:
   *  Gain: '< Root >/Ki'
   */
  /* 使用"模型名_P.Ki_Gain""模型名_P.DiscreteTimeIntegrator_gainval"参与计算 */
  autoMBD_example_PI_noSubs_DW.DiscreteTimeIntegrator_DSTATE +=
    autoMBD_example_PI_noSubs_P.Ki_Gain * rtb_Err *
    autoMBD_example_PI_noSubs_P.DiscreteTimeIntegrator_gainval;
}

更多关于模块参数的代码生成配置方法,将在后续的文章中介绍。

2.2.3 状态

状态 (States)是离散系统运算过程中必不可少的元素。

我们知道,离散系统是在每一个离散的时间点上, 运行一次Step函数。某一时刻运行一次Step函数,除了需要输入数据(通过外部输入信号输入)以外, 往往还需要上一个时刻的运算结果 ,甚至之前连续几个时刻的运算结果。

在嵌入式系统中,这些结果需要一个变量来存储,这个变量即为 状态变量

在Simulink模型库中,凡是包含离散因子“ z ”的模块,全部具有状态变量。这些模块在生成代码时,都会生成一个名为“ 模型名_DW ”的变量来保存状态变量。

具体而已,在本文PI控制器示例工程中,包含一个离散积分模块,该模块具有状态变量,生成的代码如下:

/* 数据类型定义 位于"模型名.h"中*/
/* Block states (default storage) for system '< Root >' */
typedef struct {
  real_T DiscreteTimeIntegrator_DSTATE;/* '< Root >/Discrete-Time Integrator' */
} DW_autoMBD_example_PI_noSubs_T;
/* 状态变量定义 位于"模型名.c"中 */
/* Block states (default storage) */
DW_autoMBD_example_PI_noSubs_T autoMBD_example_PI_noSubs_DW;

用户还可以定义自己的“状态变量”,通过Data Store Memory模块即可实现。 Data Store Memory模块位于Simulink库中的下图这个位置:

simulink仿真

Data Store Memory - From autoMBD

Data Store Memory模块与离散模块一样,被当作状态变量,生成在变量“ 模型名 _DW”当中。

虽然我们称它为状态变量,但对于Data Store Memory模块,把它当作普通的变量来使用也是可以的。

更多关于状态变量的代码生成,将在后续的文章中介绍。

2.2.4 模型数据

模型数据是Simulink为模型定义的一个数据类型,它保存了模型的部分信息。在代码生成中,它的数据类型和定义如下图所示:

/* 模型数据结构体定义 位于"模型名.h"中 */
/* Real-time Model Data Structure */
struct tag_RTM_autoMBD_example_PI_no_T {
  const char_T * volatile errorStatus;
};
/* 模型数据类型声明 位于"模型名.h"中 */
/* Forward declaration for rtModel */
typedef struct tag_RTM_autoMBD_example_PI_no_T RT_MODEL_autoMBD_example_PI_n_T;
/* 模型数据变量外部声明 位于"模型名.h"中 */
/* Real-time Model object */
extern RT_MODEL_autoMBD_example_PI_n_T *const autoMBD_example_PI_noSubs_M;
/* 模型数据变量定义 位于"模型名.c"中 */
/* Real-time model */
static RT_MODEL_autoMBD_example_PI_n_T autoMBD_example_PI_noSubs_M_;
RT_MODEL_autoMBD_example_PI_n_T *const autoMBD_example_PI_noSubs_M = &autoMBD_example_PI_noSubs_M_;

可以看到,默认情况下,模型数据只包含了一个表示错误状态的字符串,他的变量名为“ 模型名_M ”。在实际中,很少会使用Simulink默认的模型数据定义。

更多关于模型数据的代码生成,将在后续的文章中介绍。

3 规范建模过程

从上文可以看到,模型的数据形式直接关系到代码的生成,所以模型的好坏直接影响代码的可读性。

为了生成好的代码,规范建模过程是非常重要的。MathWorks官方发布了建模指南《MathWorks® Advisory Board Control Algorithm Modeling Guidelines》(可以在autoMBD资源库“临时资源分享”文件夹中找到该指南的PDF文档,资源序号 tA23 ,资源库链接可以在文章合集中找到,文章合集的获取见文章开头)。

该建模指南在模型的命名、模块基本配置、模型的框架层级涉及、模型的复用等等多个方面,指导用户建模。但该文档内容太多,五百多页,不是职业工作要求,一般人也很难看完并坚持执行。

但我依然建议读者保持一个良好的建模习惯,我认为可以从以下几个方面来做:

  • 为端口、信号线、子系统、模块等命有意义的名字,而不是空着,或命名无意义;
  • 建模时,遵循信号从上到下、从左到右的传递顺序,而不是杂乱无章的到处飞线,尽量避免信号逆向传递,无法避免时尽量少而清晰。
  • 合理使用子系统模块、参考子系统模块、参考模型,构建合理的模型框架和层级;
  • 一个功能的模型不要太复杂,复杂的模型可以考虑分层简化,子系统模型不要嵌套太多层;
  • 建模的过程,要考虑模型的可重用性、模型的独立性;
  • 可以使用脚本来定义模型数据,这样可以扩展模型的功能。

以上是我的一些建模经验,规范建模不是一朝一夕的事情,是平时的积累和习惯养成。虽然这会花一些精力,但好处是明显的。为了更好的生成可读性高的代码,特别是模型复杂到一定程度时,我提倡保持一个好的建模习惯。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分