本文研究Embedded Coder中的Optimization配置,通过一些模型示例和代码直观地比较配置对代码生成的影响。由于配置选项很多,本文会长期更新。
文章目录
1 Optimization配置
Optimization配置中包含了代码生成的优化选项。在Simulink配置窗口的Code Generation下可以找到Optimization配置。
后文会研究Optimization配置中的一些常用选项。
2 Default parameter behavior
2.1 描述
该配置直译过来是“默认参数行为”,其含义是,生成代码时常量参数的形式。
该配置中包含两个选项:Tunable和Inlined。如果选Tunable,意为可调式,生成的代码就会以StorageClass中的Auto类来表现常数参数。如果选Inlined,意为内联式,会把参数“内联”到代码中,表现为直接的数值。
配置中默认为Inlined。
2.2 模型示例
打开Simulink,建立一个简单的带有Gain模块的模型。
将Gain模块的增益系数写为3.观察后续这个系数在代码中的表现方式。
2.3 生成代码
1)当Default parameter behavior选为Tunable时,生成的代码如下:
可以看出,输入变量乘以一个系数demo_P.Gain_Gain。这个变量在demo_data.c中定义。
2)当Default parameter behavior选为Inlined时,生成的代码如下:
这里可以看出,系数直接写成了3.0F这个浮点数。也就是说,把Gain参数“内联”到代码中了。
2.4 分析与思考
比较两种代码生成的方式,显然是Inlined更好。这是因为将参数生成全局变量会占据芯片的RAM资源。
在企业级项目中,每个模型都可能有数十个这种常量,广泛地存在于Constant,Gain等模块中。如果这些常量占据了大量的RAM资源,就可能会导致链接过程中出现资源溢出。
3 Pass reusable subsystem outputs as
3.1 描述
这个配置项影响了可复用子系统的输出生成的代码。
下拉框包含两个选项。默认的是Individual arguments,指的是输出为局部变量。还有一个选项是Structure reference,他会生成一个全局结构体变量,然后可复用子系统对应的函数会调用这个结构体的指针。
3.2 模型示例
建立如下图模型,将两个完全相同的子系统配置为原子子系统以及可复用函数。这样模型就符合这个配置项的场景了。
原子子系统内随便什么模型都可以,这里用了个比较简单的Gain模块。
3.3 生成代码
1)当Pass reusable subsystem outputs as这个配置选为Individual arguments时,代码生成如下:
可以看出,在只有一个返回值的情况下,子系统所对应的函数直接把计算结果返回。在step函数中直接把结果赋值给Out1或者Out2。
因为模型比较简单,在step函数中把局部变量优化掉了。如果比较复杂的模型会把demo_Subsystem的结果先赋值给一个局部变量,然后参与后续的运算。
2)当Pass reusable subsystem outputs as这个配置选为Structure reference时,代码生成如下:
可以看出函数复杂了一些。首先是生成了一个结构体的全局变量demo_B,这里面存的是子系统的输出值。然后子系统对应的函数,用了个结构体指针获取返回值。
再看看下图中的Step函数:
step函数是先用demo_Subsystem这个函数更新了全局变量结构体里的成员,然后再把成员变量的值赋值给Out1或Out2。这样,在这个过程中就没有局部变量了,而是多了全局变量。
3.4 分析与思考
正所谓鱼和熊掌不可兼得,这两种方式就是对局部变量和全局变量的取舍。多生成一些全局变量,就用Structure reference,多生产一些局部变量,就用Individual arguments。
博主根据个人经验认为,汽车ECU软件一般已经用了很多全局变量,包括模型输入输出接口和观测量等都是全局变量。所以全局变量占据的RAM资源是比较稀缺和紧张的,这里按照Simulink默认的Individual arguments配置比较好,也就是多生成些局部变量。
而且,从代码复杂度的角度来说,也是Individual arguments配置更优。
4 Remove root level I/O zero initialization
4.1 描述
这个配置决定了生成的代码在initialize函数中是否有对根路径输入输出的零初始化。
4.2 模型示例
模型其实很简单,只要根路径下有个输入输出的模块就行。
4.3 生成代码
当不勾选Remove root level I/O zero initialization的时候,会在初始化函数中将输入输出模块的变量初始化为0.
当勾选Remove root level I/O zero initialization的时候,初始化函数中不会有对输入输出的零初始化。
4.4 分析与思考
个人认为这一项可以勾选,也就是说不在初始化函数中将输入输出初始化为零。
因为在ECU中,我们生成的函数不断地被OS调度,每个采样周期都会赋给新的数值,而大多数时候不会关注他们的初始值。对于需要定义初始值的信号,一般也会在模型中做好初始化的子系统,以保证某个信号生成代码的时候赋给特定的初值而不是零。
所以没有必要生成零初始化,这样也能减少一些Flash的占用,提高代码生成效率。虽然微不足道。
除了一种特殊的情况,如果控制器因为某种原因,复位后没有将RAM中的数值清零。这时候就需要这部分的初始化函数。
5 Remove internal data zero initialization
5.1 描述
这个配置决定了生成的代码在initialize函数中是否对内部变量进行零初始化。
5.2 模型示例
内部变量的类型有很多种,这里用一个Unit Delay为例。由于Unit Delay实现的是一个延时效果,所以生成的代码会通过一个全局变量来存储上一个周期的数值。这个全局变量就是一种内部变量。建一个简单的模型如下:
5.3 生成代码
当不勾选Remove internal data zero initialization的时候,会在初始化函数中将Unit Delay模块内部的全局变量初始化为0。下图中用了一个memset函数。
当勾选了Remove internal data zero initialization的时候,初始化函数中就不会把Unit Delay模块内部的全局变量初始化为0。
5.4 分析与思考
同4.4一样,博主也认为这条可以勾选。但是还是要根据实际项目需求来看,因为博主也只是基于自身项目团队的经验所得出的结论。
6 Use memcpy for vector assignment
6.1 描述
该选项控制生成代码时,是否用memcpy函数代替for循环对数组赋值。
6.2 模型示例
在模型中构建出Vector信号,在生成的代码中就会有数组的赋值操作。搭建如下示例模型:
1)在空白的Simulink中搭建一个Inport模块、一个Selector模块和一个Outport模块。
2)将Inport模块的Dimension配置为[1X100],Output DataType配置为int32。
3)将Selector配置如下,表示取出输入信号的前50个元素。
4)仿真模型后,显示出Vector信号的长度以及数据类型。
6.3 生成代码
1)当勾选上Use memcpy for vector assignment时,生成的代码如下:
很容易看出,step函数中用了memcpy函数对输出Out1的数组进行赋值。
2)当取消勾选Use memcpy for vector assignment时,生成的代码如下:
可以看出,step函数中用了一个for循环对输出Out1逐个赋值。
6.4 分析与思考
通过memcpy对数组赋值会比用for循环的方式执行速度更快,所以勾选这一项比较合理。