基于模型的设计概念
基于模型的设计(Design Based on Model),可简称为MBD,以MATLAB等软件为平台,让工程师在可视化开发环境中,一边进行需求分析、算法研究、模型与需求分析的双向跟踪、模型验证与优化;另一边进行自动生成C代码的软件在环测试、处理器在环测试、代码的有效性分析、代码与模型的双向跟踪、代码优化、硬件测试等,让算法到嵌入式实时C代码的生成一步到位、一次成功,避免传统开发MCU器件,前期投入大、开发周期长、一般需要重复多次才能成功的弊端。
这一程序设计方法,又被称为快速控制原型(Rapid Controller Prototyping,RCP),把计算机仿真(纯软件)和实时控制(硬件在回路)有机结合起来,用户可把仿真结果直接用于实时控制,极大提高控制系统的设计效率。
目前,此类设计方法中,具有代表性的方案有dSPACE等。dSPACE实时仿真系统是由德国dSPACE公司开发的一套基于MATLAB/Simulink的控制系统开发及半实物仿真的软硬件工作平台,实现了和MATLAB/Simulink/RTW的完全无缝连接。dSPACE实时系统拥有实时性强,可靠性高,扩充性好等优点。dSPACE硬件系统中的处理器具有高速的计算能力,并配备了丰富的I/O支持,用户可以根据需要进行组合;软件环境的功能强大且使用方便,包括实现代码自动生成/下载和试验/调试的整套工具。
matlab工具箱的安装
由于MATLAB2013a之后的版本中已经将c2000系列DSP移除,所以需要独立安装。
操作步骤:
1.建立一个Mathwork的账号https://cn.mathworks.com
2.以2014b版本为例,主页->资源->附加功能->Get Hardware Support Package/获取硬件支持包
3.找到C2000支持包,选择安装即可
装完可通过管理附加功能来配置
代码生成MBD的考虑
首先,代码生成中,算法和驱动层代码都适合使用代码生成吗?
其次,手工整合代码/混合编程是否等于要手写驱动层代码?
最后,以一个通信模块的设计为例,说明如何利用现有的c程序结合simulink模块,混合编程快速完成代码生成,并且这一过程如何不需要自己再写代码。
底层驱动是否适合代码生成
自动代码生成,有两方面的内容,一方面是应用层代码,如控制策略等核心算法的代码,一方面是硬件驱动层代码,然而在当前的资料也表明,手工代码的底层驱动+代码生成的核心算法是一种常见的开发方式。
需要注意的是,底层驱动,不应当简单视作初始化配置某个外设的寄存器(简单的配置外设其实很简单),还应该考虑它所对接具体模块时,需要的代码,如通信驱动程序,处理配置sci外,还需要通信协议的解析部分;eeprom的驱动程序,除了配置下spi、iic外,还有根据模块编写的按照一定时序的解析程序。
直接利用matlab生成底层驱动代码,确实简化了代码生成的步骤,但是也存在着较多的问题,无论是普通的嵌入式开发,还是汽车行业中的应用,都考虑底层驱动用matlab生成存在问题:
因此, matlab的代码生成方式,手工代码+算法的整合,即硬件驱动自己编写,而算法代码由matlab生成,且通过一些模块将之与matlab算法整合。
另外,从目前的代码生成来看,更注重的是模型验证的效果
手工整合代码/混合编程=必须手写代码?
利用厂商的工具来获得c代码
底层驱动代码,不用matlab的方式,也并意味着需要自己编写c代码
底层代码生成工具,厂商针对性制作的是matlab以外的平台,如意法半导体为自家stm32的cubemx,而目前的趋势是,厂商将其代码生成工具集成于它们过去的开发工具,Microchip的MPLAB IDE,赛灵思的Vivado,英飞凌的DAVE, stm32也有了新的开发工具STM32CubeIDE。
stm32的cubemx代码生成:
英飞凌的dave:
可见,多数厂商都为自家芯片推出了代码生成工具,这些工具不依赖于任何软件,即可生成驱动代码,并且通常可以支持多种开发环境(IDE)。
TI的则同时提供了simulink、plecs、psim等第三方平台的驱动模块。
此外,有更多的厂商既没有在simulink中推出驱动模块,也没有设计自己的代码生成模块,如国产的stc单片机,但是提供了许多驱动程序代码。
TI的controlsuit、c2000ware也为DSP提供了算法库函数
Matlab为C代码嵌入做的工作
针对这些没有驱动模块的厂商,可以采用这几个模块,将已有的c语言代码嵌入工程中,从而可以让任意芯片都能产生代码工程。
这样代码生成,适用性将不再局限于目前已经提供simulink模块的几个厂商。
而且从前面来看,在代码生成的过程中,我们基本也不用自己编写代码,只需要利用已有的模块或者c语言程序,混合调用,快速实现代码生成。
TI的simulink模块-sci为例
更新速度
Simulink的代码生成功能,也是靠这些厂商提供的,但是相比于厂商自家的开发环境,Simulink的模块更新慢,且功能少:
Matlab2018b:
从功能角度看,厂商为matlab等第三方平台增加的代码生成功能,主要在于增加器件支持,功能上增加实质很少。
以TI通信模块,接收模块为例
在18b之中,可以看到这一模块,在此方面没有增加任何新功能:
到了2021b
假设由于多年的积累,功能模块无需增加。
对模块进行研究,这里,接收帧数据的方法是通过包头和包尾判断,或者是利用FIFO中断,收满固定字节数后,进入FIFO中断来处理帧数据。
TI的SCI模块缺失的功能
这里,我们在通信中,如果使用的是串口的RXBKIN和TXINT中断,则没有提供这两个中断的使能位。
如果翻看代码来看,目前代码生成时,SCICTL2对应的几个位都没有被纳入模块
struct SCICTL2_BITS { // bit description
Uint16 TXINTENA:1; // 0 Transmit interrupt enable
Uint16 RXBKINTENA:1; // 1 Receiver-buffer break enable
Uint16 rsvd:4; // 5:2 reserved
Uint16 TXEMPTY:1; // 6 Transmitter empty flag
Uint16 TXRDY:1; // 7 Transmitter ready flag
Uint16 rsvd1:8; // 15:8 reserved
};
union SCICTL2_REG {
Uint16 all;
struct SCICTL2_BITS bit;
};
与现有协议的冲突
标准modbus协议中,与这里存在矛盾,以常用的03H指令为例
主机发送: 01 03 00 00 00 01 84 0A
从机回复: 01 03 02 12 34 B5 33
/发送数据解析/
01-地址
03-功能码,代表查询功能,其他功能后面再说
00 00-代表查询的起始寄存器地址.说明从0x0000开始查询.
(这里需要说明以下,Modbus把数据存放在寄存器中,通过查询寄存器来得到不同变量的值,一个寄存器地址对应2字节数据;)
00 01-代表查询了一个寄存器.结合前面的00 00,意思就是查询从0开始的1个寄存器值;
84 0A-循环冗余校验,是modbus的校验公式,从首个字节开始到84前面为止;
(这个校验就是保证数据传输过程没有错误的一种手段,不同的协议这种校验公式不一样)
/回复解析/
01-地址
03-功能码
02-代表后面数据的字节数,因为上面说到,一个寄存器有2个字节,所以后面的字节数肯定是2*查询的寄存器个数;
12 34-寄存器的值是12 34,结合发送的数据看出,01这个寄存器的值为12 34
B5 33-循环冗余校验
对应的基本流程:
发送:地址正确+我要查的寄存器个数+校验
回复:从机的地址+数据的字节数+数据+校验
因此,帧数据是不定的
而实际上,帧数据识别的实现方式,是借助定时器+串口检测,即串口中断接收数据,接收一个指令/一个小包(考虑CAN等通信一次传输的单位是多个字节)计时,计时超过一定时间后即认为帧数据传输结束。
纯模块编程
手工代码整合/混合编程
进行代码整合,核心在于整合二字,即利用matlab等第三方软件产生的代码、已经验证过的c语言代码,整合得到相应的DSP、单片机、FPGA等的工程。这样,既能够充分利用matlab等第三方软件的优势,又可以利用现有的许多高质量c语言代码,包括但不限于各类驱动程序。
整合时,驱动层的设计也应考虑模块,即一块区域的代码单独封装处理,最后由matlab等第三方软件将其进行整合
以通信中封装modbus通信模块为例:
这里的模块封装时,要考虑初始化部分、连续运行部分程序,此外,嵌入的代码如果缺失了部分内容,增补的代码也应被考虑写入。
这里的每一个模块都可以写代码进去,使用的是c语言,但是在手工代码整合中,这些模块主要应该执行的操作是调用已有的c语言函数,而非从头写起,除了部分初始化、缺失的代码(后期应该移入对应的c语言模块)外,这里执行的主要操作是函数调用。
system initialize模块
首先,system initialize模块,这里嵌入的任何代码,都将会被转入main函数中的XX_initialize函数中,XX为simulink的模块名。
第一部分写变量或函数声明的语句
第二部分写进入该函数时的执行语句
第三部分写离开该函数时的执行语句
/* Model initialize function */
void communication_FE_initialize(void)
{
/* Registration code */
/* initialize error status */
rtmSetErrorStatus(communication_FE_M, (NULL));
/* user code (Initialize function Trailer) */
/* System '<Root>' */
paramet[0]=;
paramet[1]=0;
SCIBOVCUN = 10;
scib_init();
enable_interruptsSCIB_RX();
}
值得注意的是,第一段的declaration code包含函数声明和变量声明都可以放在里面,但是,这里的声明不是全局的,作用域仅局限于该函数之中,示例:
生成的a只是该函数的局部变量
因此这一模块的定位是初始化,调用各类的初始化函数。
system outputs模块
outputs函数,在代码生成中,默认将会被转换到XX_step函数中,即原本的运行在timer0、adc等的时基中断中。
如果该模块放在子系统中,代码生成时会成为子系统调用的一个子函数。
与initialize模块一样
第一部分写变量或函数声明的语句,且作用域在该函数内
第二部分写进入该函数时的执行语句
第三部分写离开该函数时的执行语句
考虑执行的位置,这里的程序可以执行一些定时运行的程序,如中断的解析函数等。
同时,还可以执行判断帧数据是否接收完成,通过计数,一旦到了计数到了某一数值,意味着前后两次串口的接收数据间隔超过了时间n,此时可认为一帧数据接收完成。
Model source
明面上这一模块的作用是写各类基础代码,但是该模块的声明部分不同前面的两个模块,这里的声明位置,作用域是整个文件,即代码生成的函数
top of source可以加包含的头文件,包含的各种.c文件,函数声明等,作用域位于整个生成的c文件。
bottom 部分则可添加缺失的代码,这里是特地写了一些,但实际上模块设计时预先考虑的话,这一块可以不写。
模块和代码间的内存读取
通讯部分一旦完成后,将数据写入指定的数据paramet,但是其他模块想利用时,需要借助的是memery copy模块,将此内容与其他模块对接。
source界面,主要设置的数据输入
copy from选取数据源,
input port会引出一个口,直接输入数据
specificed address则是指定地址,即给出变量地址,类似指针取地址,而我们通常难以直接预知变量地址,但是可以知道变量的名,因此,第三种方式
specificed source code symbol,利用变量名直接获取数据
data type指定的是输入类型
data length选择的是数据长度
Stride允许您指定读取输入的间距。 默认情况下,stride值为1,这意味着生成的代码按顺序读取输入数据。 当您添加一个不等于1的stride值时,块不是按顺序读取输入数据元素,而是跳过源地址中等于stride的空格。 Stride必须为正整数。
接下来的两幅图有助于解释跨步概念。 在第一个图中,您可以看到数据没有一步地复制。 在该图之后,第二个图显示了当块将输入复制到输出位置时,应用于读取输入的stride值为2。 您可以在Destination窗格上使用参数stride为输出指定步幅值。 比较步幅和偏移量,看看差别。
其余的界面类似。