与编译器开发商密切合作优化微控制器开发
扫描二维码
随时随地手机看文章
微控制器开发团队与编译器开发人员的合作成果是生成的代码效率更高,性能更好。本文介绍的是为了使ATMEL AVR微控制器系列更适合C编译器,开发者在编译器开发阶段对微控制器架构和指令集所进行的调整。
AVR架构的核心是一个可快速访问RISC寄存器文件。该文件由32个8位通用寄存器构成。微控制器可在一个单时钟周期内加载该文件中的任意两个寄存器到算术逻辑单元(Arithmetic Logical Unit, ALU),完成所要求的操作,将结果写回到任意一个寄存器。ALU支持寄存器间或某一寄存器与一个常数之间的运算和逻辑功能,单寄存器操作也是在ALU中执行的。微控制器使用一个哈佛(Harvard)架构,在该架构中,程序存储器空间与数据存储器空间是相互隔离的。程序存储器采用单级管道访问技术,当一条指令被执行的同时,下一条指令已从程序存储器中被预先提取。由于其算术和逻辑操作都真正地在单周期内完成,因此AVR微控制器的性能达到每MHz一个MIPS。
图1 AVR 架构
细调微控制器
采用高级语言(High Level Languange, HLL)代替汇编语言来开发微控制器应用程序有许多优势,但一直都有一个重大缺点,即代码量不断增加。我们在开发AVR微控制器时考虑了使用C语言来开发应用,使得我们有可能为器件构建出一个高效C编译器。为了进一步提升这项特性,我们在AVR的架构和指令集未完成前就开始着手C编译器的开发。我们先让瑞典 IAR Systems的编译器专业开发人员对我们的AVR架构和指令集进行评测,最后开发出非常适合运行C编译器生成代码的微控制器。
寻址模式
为让编译器生成高效的代码,重要的是让寻址模式匹配C语言的需要。AVR架构原来配了两个指针寄存器(Pointer Register)。这两个指针可用于间接寻址、算后增量(post increment)间接寻址、算前减量(pre-decrement)间接寻址,以及带位移(displacement)的间接寻址,能够很好地支持指针操作。此外,还有一个用于访问数据存储器中变量的页面直接寻址模式。
指针位移
带位移的间接寻址是一种非常有用的寻址模式,即使从C编译器的角度亦如此。例如,将指针指向某一结构(struct)的第一个成员,就可以访问该结构内位移量所允许的其他任何位置,无须变更16位指针。带位移的间接寻址模式也常常用于访问软件堆栈上的变量。函数参数和autos常常放在软件堆栈上,这样,不用变更指针就可进行读写操作。位移寻址在定位数组成员(addressing elements in an array)时也非常有用。
尽管位移模式在许多情况下非常有用,但仍存在一个位移受限的问题。位移原本被限制在16个位置以内,而实际应用往往超出该数量。这样,在位移模式无法访问的位置,就必须加载一个新的指针。为扩展位移模式的访问范围,我们不得不改变指令集的其他部分,以获得足够的编码空间。同时,我们还得知,C编译器很难使用页面直接寻址模式。于是,取消了页面直接寻址模式,使用腾出的空间将位移模式扩展为64个位置,足以满足大多数间接寻址的要求。原来的页面直接寻址模式变成一个两个字长的非页面直接寻址模式。
存储器指针数
AVR微控制器原来配置了两个16位存储器指针。如要采用C编译器,那么其中一个指针必须专门用作软件堆栈,这样,就只剩一个存储器指针。在许多情况下,需要将存储器从一个区域复制到另一个区域。但由于只有一个指针,需要读1字节,设置指针,确定写入目标位置,写入这字节,然后再将指针设回数据源位置。如果增加第三个存储器指针(精简功能),完成存储器区域复制就不需要设置指针。如下例所示,只要使用算后增量间接寻址模式就可构建非常高效的存储器读写循环(假设:将指针Z指向源的第一字节,X指向目标的第一字节):
LDI R16,0x60 ;Load byte count
loop: LD R17,Z+ ;Load byte,
increment pointer
ST X+,R17 ;Store
byte, increment pointer
SUBI R16,1 ;Decrement
counter
BRNE loop ; Branch
if more bytes
具备指针算后增量(post increment)、算前减量(pre-decrement) (+1、-1)操作的可能性对于实现堆栈也非常有效。这当然也可用于软件运行时间堆栈。
直接寻址
如在指针位移一节所述, AVR原来有一个页面直接寻址模式,但是,对于编译器该模式难于使用,且效率较低。由于我们需要更多的编码空间来增加位移量,因此取消了页面直接寻址模式。不过,如果完全没有直接寻址模式,代码效率也会降低,因为在有些情况下需要访问存放在数据存储器中的变量。尤其是处理静态字符时,代码开销将会很大(达到50%),因为静态变量必须保存在数据存储器中,不能自动放置到寄存器中。为了克服代码效率低下的问题,我们占用一个16位地址来增加一些非页面直接寻址指令。这样,就可用一条指令来完成64KB数据空间的寻址。要访问如此大的一个存储器区域,访问指令必须是两个16位字。
如果访问的字节数少(例如读取一个字符),使用这种寻址方式的效率高于指针方式。对于较大的区域,可能仍然是使用间接寻址比较有效(参见下面的示例)。
Loading of a character:
Indirect addressing (6 Bytes): Direct addressing (4 Bytes):
LDI R30,LOW(CHARVAR) LDS R16,CHARVAR
LDI R31,HIGH(CHARVAR)
LD R16, Z
Loading of a long integer:
Indirect addressing (12 Bytes) Direct addressing (16 Bytes)
LDI R30,LOW(LONGVAR) LDS R0,LONGVAR
LDI R31,HIGH(LONGVAR) LDS R1,LONGVAR+1
LDD R0,Z LDS R2,LONGVAR+2
LDD R1,Z+1 LDS R3,LONGVAR+3
LDD R2,Z+2
LDD R3,Z+3
零标志传播
为实现条件转移,需要使用一些指令来操作由一些标志(flag)构成的AVR状态寄存器。跟在这类指令之后的条件转移指令(conditional branch instruction)是否执行转移,取决于这些标志的设置。使用运算指令操作这些标志,就可检查一个数A与另一个数B之间的大小关系。当被检查的数为8位的数时,不存在什么问题,因为所有标志都依赖一条指令设置的标志值。当被检查的数为16位或32位的数时(这在C语言中是常有的情况),问题就有点棘手了,例如一个32位减法操作就相当于要连续进行4个8位减法操作,而每做一次8位减法,就会产生一组新的标志。
为传播进位标志,大多数处理器都包含一些能处理进位标志先前设置值的指令。例如,带进位的减法(SBC)指令;执行SBC A,B语句就相当于将A变成进位位。但要正确完成所有的条件转移操作,这里还有另一个标志需要传播,即零标志。
示例:
A=R3:R2:R1:R0,
B=R7:R6:R5:R4
我们打算从A中减去B,并如果A=B就跳转到一个指定位置。如果这个零标志只依赖于最后的运算指令,那么下面的指令将不会执行:
SUB R0,R4
SBC R1,R5
SBC R2,R6
SBC R3,R7 ; R3=R7
=> Zero flag set
BREQ destination
这是因为BREQ指令使用的标志值只取决于最后的SBC指令设置的标志值。如果大多数高位直接相等,即便32位数不相等,零标志也将被置位,而转移也会被执行。这种问题也会出现在其他的条件转移上。
有两种办法可以解决这个问题。一是保存每个指令产生的标志,然后在第四个减法完成后检查所有的零标志。另一个更精细的方法是在进位指令中传播零标志(参见下面方式):
Znew =Not(R7) AND
Not(R6) AND
...
Not(R0) AND
Zold
使用这种方式传播零标志,所有条件转移在最后一个减法操作完成后都会被执行,因为参与标志(溢出和正数标志)所剩部分只取决于最高位字节。