Model Based Design实践过程中的问题

前言

MBD学习和实践也有一年了,逐渐领悟到了一些MBD的思想以及其与传统“粗放型”、“随心所欲型”的手写代码开发方式的不同,特此总结一下,不断更新中。

MBD实践过程中的问题

  • 由于现在在做的项目是多年前开始通过手写代码的形式一步步形成的(Simulink模型只有矢量控制部分),因此在做MBD时就自然而然地以能够自动生成和现有代码相同的代码为目标。在实际过程中发现,就算是一个稍复杂些的子模型(不含Model reference)想逐行相同也几乎是不可能的,后来只能把目标转为实现相同代码功能。原因主要有:

    • 代码生成

      自动代码生成都是以Simulink模型中的子模块为基本单元来生成的,模块生成代码形式固定,已经按照最优形式生成,不可随意更改,而手写代码并不都是最优的。例如手写y=(a+b)*c/d,模型是用Add、Product、Divide组成的,生成代码就会有一个中间临时变量,一步步计算,但其实MCU运行情况是完全相同的,即使有时候手写代码效率相同甚至更高,但两者并不会有质的区别,因此不需太过在意。

    • 有返回值的函数

      Atomic subsystem在封装成函数时,无法产生有返回值的函数,返回值只能以指针形式被函数调用,但是二者效果相同,也不需太过在意。生成有返回值的函数的方法有两种,一是整个Model的入口函数,在设置中的Interface可以进行配置;二是手写tlc文件,tlc文件可以根据用户的需求任意生成想要的函数形式,自由度很大,但是tlc语言比较复杂,实际操作中可通过s-function builder生成一个无返回值的tlc文件,然后在其基础上进行修改,工作量会小很多,不过同样的,这个方法的收益不大,因为只是生成代码形式的不同,对于功能、效率等并没有影响,性价比极低,MBD的重点要放在Model上,而不是Code。

    • 变量初始化

      变量初始化不会在定义时赋值,而是先定义,在初始化函数中赋值,有些非零量会再定义一个const存放该变量的初始值。一开始对于这个问题我也很是纠结,但是现在回想起来,这应该也是更优的处理形式。因为手写代码时,对于定义时赋非零值的情况,编译器也是会把该值存放在ROM里,程序运行之前将ROM里的值拷贝到RAM中。(我记得曾经遇到过定义时对结构体赋初值发生错误的情况)本质上和通过初始化函数赋值是一样的,这样生成代码也更符合实际编译过程,减少意外错误发生的情况。在代码集成时注意要添加初始化函数。

    • 变量名称

      手写时每个变量,无论是全局变量还是局部变量,都是自己命名的,因此会特别注意命名方式,从名称上就能看出来变量的意义和作用。然而自动代码生成时,如果未定义某一条线(Signal)的名称或者定义了名称而Storage class是Auto的话,系统会自动起一个例如Modelname_B/Modelname_BW/Modelname_U/Modelname_Y这样的名字,并且都是结构体形式,在一开始会很不适应。现在回头看来,这同样也是MBD理念的范畴。既然用户没有规定变量的Storage class为Imported/Exported,也就代表这个变量既不输出也不被输入,即只在模型内部作为中间变量存在,用户不会去观测该变量,那么用户就没有必要了解其名称,系统就按照一定规则自动生成的。

    • 位域结构体

      手写代码时为了节省RAM会经常定义位域结构体,将一些boolean类型变量放在结构体中的1bit,再将另一个4状态的状态机放在这个结构体中的2bit等。然而Simulink中只能定义1bit的位域结构体(bus)。在搭建模型时就不得不尽量减少不同长度成员结构体的使用,像刚才说的4状态的状态机就用单独一个uint8/uint16来存放。浪费的这几个bits在当前几百KB甚至上MB的MCU上都是可以忽略的。

    • union数据类型

      Simulink不支持union,这个确实会造成某些代码效率下降。用union时我可以通过位域来对成员赋值,还可以以整体进行读取和逻辑操作。

    • 状态机

      Stateflow代码生成时是switch形式,并且每一个state的名称是系统自动生成,会有一些前缀,不利于观测,因此还需要用户自定义状态机的全局变量,这会影响一点RAM和代码效率,但基本可以忽略。手写过状态机的人应该都明白Stateflow的开发效率是高得多的。

    • 代码和变量存放位置

      手写代码时需要很清楚得定义每个软件模块的c文件、h文件,在h文件中声明变量和函数,在c文件中定义变量和函数,只要能够实现想要的功能并且编译通过,其实代码和变量的位置是无所谓的。这也就造成了我们在手写代码时很容易随着项目复杂度的增加,代码和变量的位置会越来越混乱。例如最早在a.h中定义了一个结构体类型,a.c中定义了该类型的变量structA,这个变量在a.c中被赋值,在b.c中被读取,那么我还需要声明一下该变量是外部全局变量,b.h中要包含相应的h文件;后来由于功能需要我又增加了这个结构体类型中的成员structA.B,但是structA.B只在b.c中被赋值和使用。这种情况在模型中就实现不了,首先由于变量要在运行中被赋值和读取,就必须用data store memory,而data store read/write的作用范围只在data store memory之下,如果a.c和b.c是两个软件模块,即两个模型的话,必须在每个模型内添加一个叫做structA的data store memory,显然两个structA是不共用同一片存储区的。

    这种情况如何解决?一是将structA.B从结构体中拿出来,定义在b.c中,道理很简单,因为他只在b.c中被使用,和a.c没任何关系,为什么要定义在A中呢?二是将structA作为outport传递到B模型中,但是B模型中只能读取不能对其赋值。
    说完了解决方案,我们再回头看一下这个问题的本质。出现这种问题的原因是手写代码时的不规范,对变量定义位置没有清晰的规则去约束。原则上一个变量的赋值操作应该都在一个软件模块(即c文件)中进行,那么该变量就应该被定义在这个模块内,其他模块可以读取,但不允许对其赋值。如果出现了一个变量被多个软件模块赋值的情况(很容易造成软件和逻辑错误),那就需要想一想软件模块的划分是否存在问题了。而基于模型的设计中,我们直接操作的是模型,模型与模型之间只能通过输入输出接口来传递参数,这样模型间的关系才会很清晰。

你可能感兴趣的:(MBD)