1. 前言
如图1所示,R14是连接寄存器(Link Register),在汇编指令中通常也写为LR,用于存储函数调用和异常等的返回信息,复位时,默认值为0xFFFFFFFF;
图1 Core register
R15是程序计数器(PC,Program Counter),复位后初始值为Vector Table(中断向量表)的首地址加上0x04(Reset向量),其bit[0的值为必须1,并会加载到EPSR(Execution Program Status Register)的T字段(Thumb state bit),表示处于Thumb状态。ARM-v7只支持在Thumb下执行指令,在T字段为0的情况下,执行任何指令都将导致错误或锁定。
如下所示,向量表的首地址存放的是MSP的初始地址,偏移四字节后即是Reset_Handler,也就是说代码复位后是从Reset_Handler开始运行的。
const pFunc __VECTOR_TABLE[256] __VECTOR_TABLE_ATTRIBUTE = {
(pFunc)(&__INITIAL_SP), /* Initial Stack Pointer */
Reset_Handler, /* Reset Handler */
NMI_Handler, /* -14 NMI Handler */
HardFault_Handler, /* -13 Hard Fault Handler */
MemManage_Handler, /* -12 MPU Fault Handler */
BusFault_Handler, /* -11 Bus Fault Handler */
UsageFault_Handler, /* -10 Usage Fault Handler */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
0, /* Reserved */
SVC_Handler, /* -5 SVCall Handler */
DebugMon_Handler, /* -4 Debug Monitor Handler */
0, /* Reserved */
PendSV_Handler, /* -2 PendSV Handler */
SysTick_Handler, /* -1 SysTick Handler */
/* Interrupts */
Interrupt0_Handler, /* 0 Interrupt 0 */
Interrupt1_Handler, /* 1 Interrupt 1 */
Interrupt2_Handler, /* 2 Interrupt 2 */
Interrupt3_Handler, /* 3 Interrupt 3 */
Interrupt4_Handler, /* 4 Interrupt 4 */
Interrupt5_Handler, /* 5 Interrupt 5 */
Interrupt6_Handler, /* 6 Interrupt 6 */
Interrupt7_Handler, /* 7 Interrupt 7 */
Interrupt8_Handler, /* 8 Interrupt 8 */
Interrupt9_Handler /* 9 Interrupt 9 */
/* Interrupts 10 .. 255 are left out */
};
对于PC来说,其相关的表达式或标签(label)指示着一条指令或数据的地址(目标位置),如果PC当前位置到目标位置的偏移量大的过分,编译器会报错。由于ARM-v7采用了指令流水线技术,所以读PC的返回值是当前指令地址+4,且返回值的LSB为0(Thumb指令至少半字对齐),例如:
0x1000: MOV R0, PC ; R0 = 0x1004
具体来说:
①对于B、BL、CBNZ和CBZ指令,PC的值是当前指令地址加上4字节;
②对于其他使用label的指令来说,PC的值是当前指令地址加上4字节,且指令执行后PC的值的bit[1]会被强制清零,以保证其值按字长(word)对齐。
此外,向PC中写数据,就会引起一次程序分支(不更新LR寄存器),但无论是直接写PC还是使用分支转移指令,都必须保证加载到PC的值的LSB为1,即bit[0]为1,用以表明是在Thumb状态下执行;
2.相关汇编指令
2.1 PUSH/POP
PUSH和POP指令适用于寄存器的压栈和出栈,且必须是满减栈(full descending stack):
PUSH <condition> {reglist} | reglist中不可包含PC(独一无二的PC不允许有影子的存在,说一不二) |
POP <condition> {reglist} | reglist中不可同时包含PC和LR(既生瑜何生亮) |
其中:
①conditon为条件码,可选;
②reglist为非空寄存器列表,列表元素可以是寄存器或寄存器子列表(range,如"R0-R2"表示R0,R1,R2),如果包含多个寄存器或寄存器子列表,则以逗号分隔;
③reglist不可包含SP(医者不能自医啊);
④当 reglist中存在PC时,则在POP指令完成时就会跳转到PC所对应的地址执行(该地址必须半字对齐);同时,PC对应出栈值的bit[0]会用来更新APSR的T字段(T-bit),且该bit的值必须为1,以指示Thumb状态;此外,如果该POP指令带有条件码,则必须是IT指令块的最后一条指令。
通常来说,PUSH和POP会成对使用,且 在PUSH和POP的过程中,SP的值会按堆栈的使用规则自动调整。例如,如满减栈情况下,PUSH的同时SP自减,POP的同时SP递增;
注意:在寄存器列表中,不管寄存器序列如何,汇编器都将把它们升序排序,优先 PUSH序号大的寄存器,优先 POP序号小的寄存器,例如:
PUSH {R0,R4-R7} ; Push R0,R4,R5,R6,R7 onto the stack
PUSH {R2,LR} ; Push R2 and the link-register onto the stack
POP {R0,R6,PC} ; Pop r0,r6 and PC from the stack, then branch to the new PC.
这样就意味着,R0最后入栈,最先出栈,这应该也利于R0的频繁使用吧。
值得一提的是,STMDB和LDMIA在以R13(SP)为目的寄存器时,可以达到与PUSH/POP相同的效果:
STMDB SP!, {R0-R3, LR} ;等效于 PUSH {R0-R3, LR}
LDMIA SP!, {R0-R3, PC} ;等效于 PUSH {R0-R3, PC}
2.2 分支(branch)指令
指令 | 跳转范围 | 说明 |
B label | -16MB ~ +16MB | 立即跳转(通过立即数或表达式) |
B<cond> lable (IT指令块外) | -1MB ~ +1MB | 立即跳转 |
B<cond> lable (IT指令块内) | -16MB ~ +16MB | 立即跳转 |
BL{cond} label | -16MB ~ +16MB | 立即跳转,同时将返回地址存储到LR |
BX{cond} Rm | Rm中的任意值 | 通过寄存器间接跳转 |
BLX{cond} Rm | Rm中的任意值 | 通过寄存器间接跳转,同时将返回地址存储到LR |
其中:
①由于PC的值为当前指令地址+4,着也就意味着向前跳转的范围多了4个字节;
②label是一个PC相关的表达式,表示要跳转到的地址;
③ BX 和 BLX中,Rm寄存器的值为跳转的目的地址,bit[0]指示跳转后CPU要进入的状态,且如前文所述,该值的bit[0]必须为1,生成地址时会忽略该bit(置0),如果BL和BLX指令中Rm的bit[0]不为1,则会产生一个用法错误异常(UsageFault exception);
④BL和BLX指令会将当前下一条指令的地址存储到LR中,以提供返回信息;
⑤B<cond> lable是唯一在IT指令块内外都可以使用的条件分支指令,对于其余的分支指令,在IT指令块内部必须是带条件的(IT指令块内部的指令都是条件指令),在IT指令块外则必须是无条件的;
⑥在IT指令块内部使用分支指令时,则该分支指令必须时IT指令块的最后一条指令;
⑦BLX指令中不可使用PC;
⑧使用 .W后缀可以拓展分支跳转范围;
3.通过PC控制程序执行
3.1通过 MOV指令
MOV PC, Rn ; branch to the address indicated in Rn
当使用MOV指令将Rn中存储的值赋给PC时,该值的bit[0]将会被忽略,并跳转到Rn给出的地址中。此外,使用MOV指令对PC进行赋值时,MOV后不可使用S条件后缀,且Rn必须是一个没有移位的寄存器。虽然MOV指令可实现分支跳转,但BX或BLX指令更专业,移植性也更好。
3.2 通过分支指令
B loopA ; Branch to loopA
BLE ng ; Conditionally branch to label ng
B.W target ; Branch to target within 16MB range
BEQ target ; Conditionally branch to target
BEQ.W target; Conditionally branch to target within 1MB
BL funC ; Branch with link (Call) to function funC, return address stored in LR
BX LR ; Return from function call
BXNE R0 ; Conditionally branch to address stored in R0
BLX R0 ; Branch with link and exchange (Call) to a address stored in R0.
3.3 通过LDR指令
LDR PC, [Rn] ;转移地址存储在 Rn 所指向的存储器中
3.4 通过POP指令
既然LR在子程序调用过程中的唯一用处就是存储返回地址,那就直接绕过LR,将返回地址传给PC,返回子程序调用处,例如:
push {r0-r3, lr} ;子程序入口
pop {r0-r3, pc} ;子程序出口
4. PC跳转的应用
4.1 程序加载后跳转到resetHandler
/**************************************************************************************************
Local Functions
**************************************************************************************************/
uint32 resetHandlerAddr;
void Device_Deinit(void);
status_t bootUpCurrentCore(uint32_t entryPoint)
{
/* entryPoint为中断向量表(vectortable)的首地址, vectortable[1]的地址为resetHandlerAddr */
resetHandlerAddr =*((uint32_t*)entryPoint+1);
Device_Deinit();
S32_SysTick->CSRr = S32_SysTick_CSR_ENABLE(0u);
__asm("ldr r0, =resetHandlerAddr");
__asm("ldr r1, [r0]"); /* r1 = *resetHandlerAddr; 即r1 = resetHandler */
__asm("mov pc, r1"); /* pc = resetHandler, 即跳转到resetHandler函数 */
return STATUS_SUCCESS;
}
4.2 Reset_Handler函数完成系统初始化
/*----------------------------------------------------------------------------
Reset Handler called on controller reset
*----------------------------------------------------------------------------*/
void __attribute__((naked,__noreturn__)) Reset_Handler(void)
{
__EARLY_INIT();
/* Stack pointer initialisation */
__set_CONTROL(0); /* MSP with privilege mode*/
__set_PSP(0);
__set_BASEPRI(0);
__set_MSP((uint32_t)&__INITIAL_SP);
SystemInit(); /* CMSIS System Initialization */
__PROGRAM_START(); /* Enter PreMain (C library entry point) */
}