1.5 软件低功耗设计
软件可以区分为“系统软件”、“实时操作系统”和“应用软件”。软件对于一个低功耗系统的重要性常常被人们忽略。一个重要的原因是,软件上与功耗设计有关的缺陷并不像硬件那样容易发现,同时软件的低功耗特性也没有一个严格的标准来判断。而对于一个低功耗系统设计,设计者必须注意软件的低功耗设计方法,尽可能避免那些“看不见”的功耗损失。
1.5.1 编译优化
在微处理器执行的程序中,每一条指令都将激活微处理器中的某些硬件部件。因此,可以认为每一条指令都有一个固定的功率消耗量,正确选择指令可以降低微处理器的功耗。通过建立特定处理器架构下指令集的功耗信息,采用“减少跳转的指令重排序”等方法,可以有效地优化软件的低功耗设计。
编译器的作用是将由高级语言(如C/C++等)编写的程序,翻译成能够在目标机上执行的程序。编译器为高级语言程序员提供了一个抽象层,使得程序员能够不用汇编或机器语言,而直接采用高级语言代码编写解决实际问题的程序。同时,编译器也使得程序的可读性和可维护性得到保证,可以提高软件开发的效率。另外,当需要将程序移植到新的目标机时,也只需要采用相应的编译器对程序进行重新编译,而不必重新编写程序。
但在某些情况下,编译器的一些做法是以牺牲程序的执行能力为代价的,即需要增加执行的指令数。因此,通过对编译器的优化,生成效率更高的代码,可以有效地降低微处理器的功耗。
1.5.2 指令排序
对于一个微处理器,执行某一特定程序的功耗为
E=P×t (1.8)
式中,P=I×VDD(I为平均电流,VDD为微处理器的电源电压);t为程序的执行时间(t=N×T,T为指令周期,即为主频的倒数,N为程序执行的周期数)。
因此有
E=(I×VDD)×(N×T) (1.9)
由式(1.9)可见,当VDD和T都是已知量时,程序消耗的电能E与电流I和程序周期数N的乘积成正比。
可以通过建立一定的模型来测量并估计执行每条指令所需要的电流I。在一个嵌入式系统中,可以利用嵌入式微处理器中的多数据存储区域的特性,实现数据的并行处理,通过对指令的排序,减少指令的执行周期,从而达到降低功耗的目的。
现假设需要完成图1.15(a)所示的运算,图1.15(b)所示是其相应的汇编代码。图1.15(c)所示为每个节点带有两个权值的数据依赖图(DataDependenceGraph,DDG),第一个权值表示节点在DDG中的深度,如V10的第一个权值为1,V0的第一个权值为6。假设这个权值越大,表示其优先级越高,如图1.15(c)中V0和V1具有最高的优先级。
指令排序前节点的执行顺序见表1.9。注意,表中V2(ADD)、V6(ADD)和V9(MPY)的指令与其他指令(MOVE)不同,ADD和MPY指令需要用到系统的ALU部件。在同一指令周期中,可以同时执行ALU运算及MOVE操作,但是不可以同时执行两个ALU操作。
图1.15 运算要求、汇编代码与数据依赖图
表1.9 指令排序前节点的执行顺序
节点的第二个权值,表示相关寄存器的生命周期。指令排序前的状态如图1.16所示,V0所依赖的寄存器是r0,它的生命周期为1到3,即为2。从图1.16中可以得出以下结论:此段程序总共需要11个指令周期和最少同时使用2个寄存器。
基于排序算法,将指令重新排序后的情况如图1.17所示,程序总的执行周期变为6,但是所占用的寄存器个数增加到3。由此也可以看到,程序的执行周期与寄存器的个数之间也是一个折中权衡的结果。
图1.16 指令排序前的状态
图1.17 基于排序算法的重新排序
可以通过建立一定的模型来测量并估计执行每条指令所需要的电流I。在该示例中,程序执行时所需要的总电流I=780mA。如图1.16所示,在不使用任何算法的情况,即I总=N×I=11×780(mA)=8580(mA)。在图1.17中,总的执行周期数为N=6,因此电路消耗的电流为I总=N×I=6×780(mA)=4680(mA)。由此可见,通过使用基于排序算法,减少了程序的执行周期,程序的执行性能得到提高。同时,由E=(I×VDD)×(N×T)可知,N的减少,也大大降低了执行程序的功耗。
优化的算法描述,主要是以减少程序的执行周期为目的,同时考虑到使用尽量少的寄存器。从优化系统的功耗层面上来看,算法的优化也是非常重要的。
1.5.3 常用的降低软件功耗的方法
采用高效率的算法可以有效地降低功耗,一些常用的方法如下:
(1)用查表的方法代替实时的计算,尽量减少CPU的运算量。特别是在没有硬件浮点处理单元的MCU进行浮点处理时,直接用MCU进行浮点处理将会消耗大量的时间。将一些运算的结果预先算好,放在Flash存储器中,用查表的方法替代实时的计算,减少CPU的运算工作量,可以有效地降低CPU的功耗。很多微处理器都有快速有效的查表指令和寻址方式,用于优化查表算法。这种处理方法在离散余弦变换和A/D数据采集中能够带来可观的效率提升。
(2)对于不可避免的实时计算,应注意计算的精度,算到精度够了就应立即结束,避免“过度”的计算。在精度允许的情况下,使用简单函数代替复杂函数作近似运算,也是减少功耗的有效方法。
(3)尽量使用短的数据类型,如尽量使用字符型的8位数据替代16位的整型数据。
(4)尽量使用分数运算而避免浮点数运算等。
(5)用移位运算代替乘除法运算。采用MCU计算乘除法也是非常耗时的,如果采用左移和右移的办法来实现乘除法运算,将会减少运算时间。注意,除法的移位计算只能针对除数比较特殊的情况。
(6)采用快速算法。在搜索算法中,使用二分搜索算法和分段查找算法的效率是不同的。从理论上可以估算,在1024个测量值的查找中,二分搜索最坏情况下10次可以查找到结果,顺序搜索最坏可能需要1024次。这在测量数值更多的情况下更为突出,一个高效率的查找算法有助于减小程序运行功耗。
(7)数字信号处理中的运算,采用FFT和快速卷积等,可以节省大量运算时间。
(8)一个程序使用中断方式还是查询方式,对于很多应用来说并不那么重要,但在软件低功耗设计特性上却相差甚远。例如,ADC在采集少量的数据时,MCU读取A/D转换数据可以采用查询方式或中断方式。查询方式和中断方式的低功耗特性相差甚远。使用中断方式,MCU可以什么都不做,甚至可以进入待机或停止模式。而采用查询方式,MCU必须不停地读取I/O端口寄存器,需要消耗很多额外的功耗。
(9)采用定时器。在程序中可以采用软件延时。但是,如果系统的定时器资源充裕,在需要定时的场合,最好采用硬件定时器,当定时器到了定时时间后,向MCU发出中断请求信号,这样可以减少MCU的工作时间,进而可以降低功耗。
(10)用宏代替子程序。在程序执行的过程中,读RAM需要比读Flash更大的功耗。宏是在编译器预处理阶段进行替代,而在子程序的调用中MCU需要进行现场保护。在一次子程序调用中,因为CPU进入子程序时会首先将当前CPU寄存器推入堆栈(RAM),在离开时又将CPU寄存器弹出堆栈,这样至少对RAM有两次操作。对于程序设计来说,调用一个子程序还是一个宏,在程序写法上并没有什么不同,但宏会在编译时展开,CPU只是顺序执行指令,避免了调用子程序。唯一的问题是增加了代码的长度(代码量)。目前,MCU片内的Flash空间越来越大,对于一些不在乎程序代码量大一些的应用,用宏代替子程序无疑可以降低系统的功耗。