任何一个ECU供应商去拿项目的时候,客户需求中一般都会明确提出CPU Load不能超过百分之多少多少(70%~80%)。OEM提这么个要求是有他的考虑的,因为在项目的前期,OEM自己也不甚确定将来是否有功能的扩展,而随着项目的进展,对零件理解的深入,他们则往往会有这样或那样的新的要求提出来。下面的这个漫画的故事,我相信在不少公司都上演过。
因此,怎样让软件运行的更高效一些,降低CPU Load,是软件工程师要一直记在心头的一个话题。笔者认为,软件运行效率可以从如下几个方面进行改进:
1 合理的Task Scheduling
嵌入式软件是有大大小小很多个任务组合起来的,需要合理的进行任务的调度,以降低CPU时间。
-
比如EPS软件需要其助力特性曲线模块每1ms计算一次助力力矩,其助力特性曲线是根据车速查表得到,车速信号每10ms更新一次,我们就没有必要把查表的模块也放到1ms的任务中。
-
软件中有1ms任务和10ms任务甚至更多,务必将10ms任务在分成10个小的任务10.1,10.2……10.10,且保证每个小任务的运行时间比较类似。避免出现在某一个10ms之内任务比较集中的现象。
扫描二维码关注公众号,回复: 7590914 查看本文章
2 定点化软件中合理的定标
2.1 在定点化软件中,定标往往是比较头疼的一个问题,数据长度选择16位的话,在涉及到传递函数相关的模型里,中间变量往往会出现精度不够的导致失真严重,因此很多同学往往就是一股脑将所有数据长度选为32位,所有变量的长度与输出一直,这样做一版来说不会有大的问题,但是32位数据的运算时间远远高于16位,会占用更多的CPU资源。笔者的做法是首先分析好输入变量的值的范围和频率范围,输出信号的精度要求之后,设定10%左右的Tolerance后用Matlab的Fix Point Tool进行参数Scaling的初步设定后,再检查一遍对个别变量的scaling进行调整。
2.2 在软件中,尽可能使用2^-x作为scaling,不要使用10^-x作为scaling。因为用二进制的scaling,软件可以简单的用移位进行scaling的转换,用十进制则要用到乘除法。
3 LookupTable与PreLookUpTable与DirectLookupTable
LookUpTable太占用运行时间,不建议使用,可以用PreLookUpTable代替,如果表的数据量很大,建议直接使用DirectTable,省去interpolation算法的时间。
4 尽量减少对除法的使用
除法要占用的CPU load要远远的超过乘法,一般来说16位整数乘法在1个到几个机器周期之内可以完成,而除法则要几十个机器周期。如果是变量除以常量,那么在实现的时候可以用变量乘以该常量的倒数来代替除法运算。如果是变量之间的相除,则在算法上尽量合并同类项,减少对除法的使用,例如将b/a + c/a 简化为(1/a)*(b+c),原来的表达式中有两个除法和一个加法,合并同类项以后一个除法、一个乘法、一个加法,能够节省不少运行时间。
5 自加自检运算符的使用
能够自加自减的地方不要用+1或-1,因为二者的汇编一般来说是有区别的,前者的效率更高。
使用增量操作符a++;得到的汇编:
incra ;a加1
使用数学运算a=a+1;得到的汇编:
move A,a;把a从内存取出存入累加器A
add A,1;累加器A加1
store a;把新值存回a
孰优孰劣一目了然。但是切记滥用自加自减,一个表达式中最多出现一个自加自减,虽然诸如x=i+++++j这样的表达式也是合理的,但太过晦涩,不要使用。
6 对于需要经常需要频繁调用的功能,尽量使用宏函数而不是函数
比如实现两个数中求较小者,
使用宏函数:#define min(X, Y) ((X) < (Y) ? (X) : (Y))
使用函数函数:
Int16 min(x,y)
{ return (((x) < (y) ? (x) : (y));}
使用宏函数就比函数的效率来的要高一些,因为宏函数仅仅只是将预先写好的代码替换,而在调用函数的时候,CPU需要保存和恢复当前的现场,进行压栈和出栈的操作,因此相对而言宏函数会占用更小的CPU Load。
但是宏函数用的不恰当,也会产生很多很多的坑,以下事项需注意:
-
不论如何,宏函数最外面一定一定一定要加一个括号(重要的事情说三遍)
-
宏函数的参数里不要调用函数
-
宏参数不要自身递归
-
两个不同的宏函数之间不要有交叉引用
7 变量的内存模式定义
-
Near与Far空间:对16位单片机而言,near空间是指能够通过16bit进行快速寻址的内存空间,Far空间是整个RAM空间。Near空间的访问速度要高于Far空间的访问速度。一般来说,不加特别的定义,所有的全局变量都会优先定义到near空间中。因此在进行软件开发时,应尽量将频繁访问的数据放到near空间,如每次运行周期内都会使用到的变量;将低概率事件触发的变量、数组放到Far空间。
-
PSRAM、DSRAM和DPRAM
PSRAM虽然名字为程序RAM,其实也可以存储数据用。三者的访问速度为:PSRAM<DSRAM<DPRAM.
DPRAM(Dual Port Ram)访问速度极快,但成本又比较高,一般的单片机中只有极少的DPRAM,一般用来配置为堆栈、全局寄存器的镜像、和一些特别需要快速访问速度的变量(如单片机内的快速MAC运算单元)。
8 使用DMA/PEC
DMA(Direct Memory Access直接内存存取)和PEC(Peripheral Event Controller外围时间控制器)是不同的公司对同一个功能的叫法。其作用注要是让外设和存储器之间实现直接的数据交互而不需要CPU的过多干预,减少中断的次数,从而降低CPU的负载率。比较典型的应用是高速的AD采样、对一串协议信号的解析(如SENT信号等)。
9 逻辑运算的执行顺序
软件对a&b&c的执行顺序为必须a,b,c都为1,该指令才会全部被执行;只要a为0,该指令的执行结果直接返回为0,软件也不会去检查b和c的取值到底是多少。因此对&运算,尽量把结果为0发生频率高的条件放在前面。
反之,对于或运算,只要有一个条件为1,那么返回值就会为1,尽量把结果为1发生频率高的条件放在前面。
10 if条件/switch分支的执行顺序
Switch语句是一个普通的编程技术,编译器会产生if-else-if的嵌套代码,并按照顺序进行比较,发现匹配时,就跳转到满足条件的语句执行。每一个条件/分支实现的跳转仅仅是为了决定下一步要做什么,为了提高速度,把最可能发生的情况放在第一位,最不可能的情况放在最后可以提高程序的运行效率。 另外,还可以将各种分支进行分组,在switch中嵌套switch。
11 For循环的嵌套
当for循环发生嵌套时,尽量将大循环放在里面,小循环放在外面,减少CPU跨切循环的次数。下面这篇文章讲得非常透彻,有兴趣的同学可以去看一下。
http://www.cnblogs.com/tolimit/p/4276844.html
12 Static的使用
对在函数中经常需要使用到的“不变量”,定义为Static变量;对于需要经常访问的函数,定义为static函数。提前分配好内存,节省软件开销。
13 iCache的使用
我们都知道程序是存储在Flash中的,需要用到该程序指令时,CPU才会去Flash进行取指,而访问Flash的速度明显是低于Ram的。iCache就是把Flash中的某一段程序代码搬到RAM中,直接在RAM中运行这段代码(就跟直接在PC机内存里面装一个Windows操作系统是一个道理)。使用iCache后,该段代码的运行效率一般能提高30%以上。但iCache的内存也是极少的,一般的应用是被频繁调用的程序放到iCache中。比如无刷电机的矢量控制一般都要求其运行周期为几十到几百个us,使用iCache就能很大程度的提高软件运行效率。
14 合理利用软件流水线
现在的单片机大多都使用了指令流水线技术,以提高CPU的运行速度。
以如下代码为例:
for (i = 0; i < 100; i = i + 1)
{
a[i] = 10 * b[i];
b[i] = 10 * c[i];
c[i] = 10 * d[i];
}
循环体内的三条指令具有相互依存的关系(a[i]=10*b[i]计算完成之前,不能对b[i]进行取指),因此编译器不能利用指令流水线进行优化。如果我们改成如下一种方式:
for (i = 0; i < 100; i = i + 4)
{
a[i] = 10 * b[i];
a[i+1] = 10 * b[i+1];
a[i+2] = 10 * b[i+2];
a[i+3] = 10 * b[i+3];
b[i] = 10 * c[i];
b[i+1] = 10 * c[i+1];
b[i+2] = 10 * c[i+2];
b[i+3] = 10 * c[i+3];
c[i] = 10 * d[i];
c[i+1] = 10 * d[i+1];
c[i+2] = 10 * d[i+2];
c[i+3] = 10 * d[i+3];
}
则编译器能够更好的利用指令流水线,节省运行时间。