分类: 嵌入式
2010-04-22 22:04:31
ARM中断异常处理的返回
作者:孙晓明,讲师。
举个小例子,下面是一段ARM汇编代码:
地址 |
指令 |
0x3000 |
BL add |
0x3004 |
MOV r0,#0 |
0x3008 |
MOV r1,#1 |
0x300C |
MOV r2,#2 |
AREA test,CODE,READONLY
ENTRY
Start
MOV r0,#1
MOV r1,#1
BL add
MOV r0,#0
MOV r1,#1
Add
ADD r0,r0,r1
MOV r0,r0,r1
END
当0x3000处的BL指令执行时,会把PC(=0x3008)保存到LR寄存器里面,也就是LR=0x3008。接下来处理器会立即对LR进行一个自动的更新动作:LR=LR-0x4,这样,LR里面的地址为0x3008 – 0x4 = 03004,它是指令”MOV r0,#0”的地址,所以当从子程序add返回时,LR里面正好是正确的返回地址。既是下一条要执行的指令的地址。
(2)中断异常处理函数调用
异常就是正在执行的指令,由于各种软件或硬件故障被打断,比如,在读数据或指令时,访问存储器失败、产生了一个外部硬件中断等。当这些情况发生时,在ARM系统里,由异常和中断处理程序做出相应的处理,当处理完成后,要返回到被中止的指令,使被中止的指令能够继续正常执行下去。因此,确定异常和中断处理程序的返回地址是一个非常重要的问题。
1、中断处理
当外部中断IRQ和FIQ(Fast Interrpt Request,快速中断请求)发生时,ARM核完成一部分工作。当然,这些工作是任何异常发生时都必须要做的,所以ARM处理器就会自动带我们完成。 其它重要的工作,必须由程序员来完成。ARM处理器处理的事包括从用户模式切换到IRQ模式、状态寄存器值的变化及跳转。比如说,处理器自动跳转到从0x0地址开始的异常中断向量表的0x18处,在向量表的0x18处,最简单的指令为”B HandlerIRQ”。
那程序员所要关心的就是实现具体的异常处理程序(HandlerIRQ)。当用ARM汇编语言实现HandlerIRQ函数的时候,如何确定HandlerIRQ函数正确地返回地址,使被中止的指令能够继续正常执行下去。
比较常用的中断处理程序结构如下:
HandlerIRQ ;中断响应,从向量表直接跳来
SUB r14,r14,#4 ;计算返回地址
STMFD r13,{r0-r3,r14} ;保护现场,一般只需要保护{r0-r3,lr}
BL irqHandler ;跳到具体的异常处理函数
LDMFD r13,{r0-r3,pc}^ ;恢复现场
有程序可以看出,通过”SUB R14,R14,#4”计算中断函数的返回地址。那有人一定会问,为什么计算返回地址的时候要减去4呢?
地址 |
指令 |
0x3000 |
BL add |
0x3004 |
MOV r0,#0 |
0x3008 |
MOV r1,#1 |
0x300C |
MOV r2,#2 |
我们看上个表,比如在执行地址为0x3004的move指令时,突然来了一个IRQ中断,这个中断打断了move指令的执行,这个时候就要去跳转到异常处理函数,之后还要返回0x3004地址重新执行move指令。当中断发生时,LR里面保存了用户模式下PC的值,那么当执行地址为0x3004的move指令时,PC的值应该是0x300C,前面介绍过,当发生跳转时,处理器会对LR进行一个自动的更新动作:LR=LR-0x4,这样LR里面的地址是0x300C-0x04=0x3008。但是0x3008并不是我们要的地址,因为中断发生在地址为0x3004的move指令执行的时候,所以中断处理完后应该返回这个地址。 这就是在计算返回地址的时候LR减去4的原因。对于FIQ中断和预取指中止异常,计算返回地址方法和IRQ相同。
The interrupt handler that wishes to store its return link on the stack might use the instructions of the following form at its entry point.
SUB R14, R14, #4
STMFD SP!, {
why the statement SUB R14, R14, #4 is used?
The address stored in R14/LR on entry to an interrupt handler is the instruction address of the last instruction to complete plus 8 (two instructions in ARM); this is (conceptually at least), because the current PC for the instruction in execute is the address of the instruction in fetch, with decode being 4 bytes ahead of fetch, and execute being 4 bytes ahead of that.
To return to the instruction immediately after the last one to complete, you would execute:
SUBS PC,LR,#4
To repeat the instruction that was interrupted, you could execute:
SUBS PC,LR,#8
In order to free up more registers, to allow interrupt re-entrancy / nesting, or to make a function call, it is necessary to preserve R14/LR. One could write the code:
STMFD SP!,{...,LR}
....
LDMFD SP!,{...,LR}
SUBS PC,LR,#4
Or, one could write:
SUB LR,LR,#4
STMFD SP!,{...,LR}
....
LDMFD SP!,{...,PC}^
The later has the advantage that the exception return instruction only need to return and not worry about where it is returning to (on the basis that data-aborts and other exceptions result in differing desired R14/LR offsets), and may be higher performance on some processors.
Note that on the M-Profile cores Cortex-M1/M3, the Return PC is automatically adjusted by the hardware.
ARM处理器流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅,连续,能提供0.9MIPS/MHZ的指令执行速度。
PC代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:1.取指(从存储器装载一条指令);2.译码(识别将要被执行的指令);3.执行(处理指令并将结果写回寄存器)。而R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的指令或正在“译码”的指令。一般来说,人们习惯性约定将“正在执行的指令作为参考点”,称之为当前第一条指令,因此PC总是指向第三条指令。当ARM状态时,每条指令为4字节长,所以PC始终指向该指令地址加8字节的地址,即:PC值=当前程序执行位置+8;
周期1 周期2 周期3 周期4 周期5 周期6
PC-8 取指 译码 执行
PC-4 取指 译码 执行
PC 取指 译码 执行
2.................................................................
如果ARM7的主频是60M的话,实际代码运行速度是60*0.9=54MIPS,对于ARM9则是60*1.1=66MIPS吗?
另外对于3级的ARM,一条指令从取指到执行完毕,实际时间也是3周期吧?还有如此算的话一周期执行一条指令,那60M的主频,实际代码运行速度为什么不是60MIPS,而要乘0.9呢?是不是把一些特殊指令,还有分支任务时指令延时也算了,才乘个系数0.9?还有ARM9的1.1MIPS/Hz是怎么实现的?
为分支和循环操作的存在,使得在执行这些指令时需要清空流水线,导致ARM7的指令执行效率降低,所以ARM7的指令性能是0.9MIPS/MHz。
而ARM9相对于ARM7,由于是harvard结构,取指令和取数的效率更高,即执行某些指令的效率更高,所以其指令性能达到1.1MIPS/MHz。但这并不意味着,ARM9在1个指令周期内可以执行超过1条指令。
3..............................................................
归根结底:就是——
取指
译码
执行
主要在循环程序的设计上可以优化程序
流水线技术通过多个功能部件并行工作来缩短程序执行时间,提高处理器核的效率和吞吐率,从而成为微处理器设计中最为重要的技术之一。ARM7处理器核使用了典型三级流水线的冯·诺伊曼结构,ARM9系列则采用了基于五级流水线的哈佛结构。通过增加流水线级数简化了流水线各级的逻辑,进一步提高了处理器的性能。
ARM7的三级流水线在执行单元完成了大量的工作,包括与操作数相关的寄存器和存储器读写操作、ALU操作以及相关器件之间的数据传输。执行单元的工作往往占用多个时钟周期,从而成为系统性能的瓶颈。ARM9采用了更为高效的五级流水线设计,增加了2个功能部件分别访问存储器并写回结果,且将读寄存器的操作转移到译码部件上,使流水线各部件在功能上更平衡;同时其哈佛架构避免了数据访问和取指的总线冲突。
然而不论是三级流水线还是五级流水线,当出现多周期指令、跳转分支指令和中断发生的时候,流水线都会发生阻塞,而且相邻指令之间也可能因为寄存器冲突导致流水线阻塞,降低流水线的效率。本文在对流水线原理及运行情况详细分析的基础上,研究通过调整指令执行序列来提高流水线运行性能的方法。
1 ARM7/ARM9流水线技术
1.1 ARM7流水线技术
ARM7系列处理器中每条指令分取指、译码、执行三个阶段,分别在不同的功能部件上依次独立完成。取指部件完成从存储器装载一条指令,通过译码部件产生下一周期数据路径需要的控制信号,完成寄存器的解码,再送到执行单元完成寄存器的读取、ALU运算及运算结果的写回,需要访问存储器的指令完成存储器的访问。流水线上虽然一条指令仍需3个时钟周期来完成,但通过多个部件并行,使得处理器的吞吐率约为每个周期一条指令,提高了流式指令的处理速度,从而可达到O.9 MIPS/MHz的指令执行速度。
在三级流水线下,通过R15访问PC(程序计数器)时会出现取指位置和执行位置不同的现象。这须结合流水线的执行情况考虑,取指部件根据PC取指,取指完成后PC+4送到PC,并把取到的指令传递给译码部件,然后取指部件根据新的PC取指。因为每条指令4字节,故PC值等于当前程序执行位置+8。
1.2 ARM9流水线技术
ARM9系列处理器的流水线分为取指、译码、执行、访存、回写。取指部件完成从指令存储器取指;译码部件读取寄存器操作数,与三级流水线中不占有数据路径区别很大;执行部件产生ALU运算结果或产生存储器地址(对于存储器访问指令来讲);访存部件访问数据存储器;回写部件完成执行结果写回寄存器。把三级流水线中的执行单元进一步细化,减少了在每个时钟周期内必须完成的工作量,进而允许使用较高的时钟频率,且具有分开的指令和数据存储器,减少了冲突的发生,每条指令的平均周期数明显减少。
2 三级流水线运行情况分析
三级流水线在处理简单的寄存器操作指令时,吞吐率为平均每个时钟周期一条指令;但是在存在存储器访问指令、跳转指令的情况下会出现流水线阻断情况,导致流水线的性能下降。图1给出了流水线的最佳运行情况,图中的MOV、ADD、SUB指令为单周期指令。从T1开始,用3个时钟周期执行了3条指令,指令平均周期数(CPI)等于1个时钟周期。
点击看原图
流水线中阻断现象也十分普遍,下面就各种阻断情况下的流水线性能进行详细分析。
2.1 带有存储器访问指令的流水线
对存储器的访问指令LDR就是非单周期指令,如图2所示。这类指令在执行阶段,首先要进行存储器的地址计算,占用控制信号线,而译码的过程同样需要占用控制信号线,所以下一条指令(第一个SUB)的译码被阻断,并且由于LDR访问存储器和回写寄存器的过程中需要继续占用执行单元,所以下一条(第一个SUB)的执行也被阻断。由于采用冯·诺伊曼体系结构,不能够同时访问数据存储器和指令存储器,当LDR处于访存周期的过程中时,MOV指令的取指被阻断。因此处理器用8个时钟周期执行了6条指令,指令平均周期数(CPI)=1.3个时钟周期。
点击看原图
2.2 带有分支指令的流水线
当指令序列中含有具有分支功能的指令(如BL等)时,流水线也会被阻断,如图3所示。分支指令在执行时,其后第1条指令被译码,其后第2条指令进行取指,但是这两步操作的指令并不被执行。因为分支指令执行完毕后,程序应该转到跳转的目标地址处执行,因此在流水线上需要丢弃这两条指令,同时程序计数器就会转移到新的位置接着进行取指、译码和执行。此外还有一些特殊的转移指令需要在跳转完成的同时进行写链接寄存器、程序计数寄存器,如BL执行过程中包括两个附加操作——写链接寄存器和调整程序指针。这两个操作仍然占用执行单元,这时处于译码和取指的流水线被阻断了。
2.3 中断流水线
处理器中断的发生具有不确定性,与当前所执行的指令没有任何关系。在中断发生时,处理器总是会执行完当前正被执行的指令,然后去响应中断。如图4所示,在Ox90000处的指令ADD执行期间IRQ中断发生,这时要等待ADD指令执行完毕,IRQ才获得执行单元,处理器开始处理IRQ中断,保存程序返回地址并调整程序指针指向Oxl8内存单元。在Oxl8处有IRO中断向量(也就是跳向IRQ中断服务的指令),接下来执行跳转指令转向中断服务程序,流水线又被阻断,执行0x18处指令的过程同带有分支指令的流水线。
点击看原图
3 五级流水线技术
五级流水线技术在多种RISC处理器中被广泛使用,被认为是经典的处理器设计方式。五级流水线中的存储器访问部件(访存)和寄存器回写部件,解决了三级流水线中存储器访问指令在指令执行阶段的延迟问题。图5为五级流水线的运行情况(五级流水线也存在阻断)。
点击看原图
点击看原图
3.1 五级流水线互锁分析
五级流水线只存在一种互锁,即寄存器冲突。读寄存器是在译码阶段,写寄存器是在回写阶段。如果当前指令(A)的目的操作数寄存器和下一条指令(B)的源操作数寄存器一致,B指令就需要等A回写之后才能译码。这就是五级流水线中的寄存器冲突。如图6所示,LDR指令写R9是在回写阶段,而MOV中需要用到的R9正是LDR在回写阶段将会重新写入的寄存器值,MOV译码需要等待,直到LDR指令的寄存器回写操作完成。(注:现在处理器设计中,可以通过寄存器旁路技术对流水线进行优化,解决流水线的寄存器冲突问题。)
点击看原图
虽然流水线互锁会增加代码执行时间,但是为初期的设计者提供了巨大的方便,可以不必考虑使用的寄存器会不会造成冲突;而且编译器以及汇编程序员可以通过重新设计代码的顺序或者其他方法来减少互锁的数量。另外分支指令和中断的发生仍然会阻断五级流水线。
3.2 五级流水线优化
采用重新设计代码顺序在很多情况下可以很好地减少流水线的阻塞,使流水线的运行流畅。下面详细分析代码优化对流水线的优化和效率的提高。
要实现把内存地址0x1000和Ox2000处的数据分别拷贝到0x8000和0x9000处。
Oxl000处的内容:1,2,3,4,5,6,7,8,9,10
Ox2000处的内容:H,e,l,l,o,W,o,r,l,d
实现第一个拷贝过程的程序代码及指令的执行时空图如图7所示。
点击看原图
全部拷贝过程由两个结构相同的循环各自独立完成,分别实现两块数据的拷贝,并且两个拷贝过程极为类似,分析其中一个即可。
T1~T3是3个单独的时钟周期;T4~T11是一个循环,在时空图中描述了第一次循环的执行情况。在T12的时候写LR的同时,开始对循环的第一条语句进行取指,所以总的流水线周期数为3+10×10+2×9=121。整个拷贝过程需要121×2+2=244个时钟周期完成。
考虑到通过减少流水线的冲突可以提高流水线的执行效率,而流水线的冲突主要来自寄存器冲突和分支指令,因此对代码作如下两方面调整:
①将两个循环合并成一个循环能够充分减少循环跳转的次数,减少跳转带来的流水线停滞;
②调整代码的顺序,将带有与临近指令不相关的寄存器插到带有相关寄存器的指令之间,能够充分地避免寄存器冲突导致的流水线阻塞。
对代码调整和流水线的时空图如图8所示。
点击看原图
调整之后,T1~T5是5个单独的时钟周期,T6~T13是一个循环,同样在T14的时候BNE指令在写LR的同时,循环的第一条指令开始取指,所以总的指令周期数为5+10×10+2×9+2=125。
通过两段代码的比较可看出:调整之前整个拷贝过程总共使用了244个时钟周期,调整了循环内指令的顺序后,总共使用了125个时钟周期就完成了同样的工作,时钟周期减少了119个,缩短了119/244=48.8%,效率提升十分明显。
代码优化前后执行周期数对比的情况如表1所列。
点击看原图
因此流水线的优化问题主要应从两方面考虑:
①通过合并循环等方式减少分支指令的个数,从而减少流水线的浪费;
②通过交换指令的顺序,避免寄存器冲突造成的流水线停滞。
4 结 论
流水线技术提高了处理器的并行性,与串行CPU相比大大提高了处理器性能。通过调节指令序列的方法又能够有效地避免流水线冲突的发生,从而提高了流水线的执行效率。因此如何采用智能算法进行指令序列的自动调节以提高流水线的效率和进一步提高处理器的并行性将是以后研究的主要方向。