一.概述
在系统上电时需要先执行一段引导程序,也称bootloader程序,来完成对系统运行环境的初始化工作。就和 PC 上的 BIOS程序一样,bootloader 就相当于 BIOS。
基于ARM架构的处理器其内核启动流程大同小异,本文就基于cortex-M4内核的MCU以及基于cortex-A7内核的MPU,对其启动流程做简要分析。
二.cortex-M4内核启动流程
在我们进行单片机编程的时候,通常没有考虑过自己写bootloader程序,直接编写main函数就能运行。那是因为半导体厂商给我们写好了bootloader,通常不需要我们修改。以STM32F4系列MCU为例,可以看到在我们的工程中存在startup_stm32f40_41xxx.s这个文件,这个文件是使用汇编写的,也就是bootloader程序。
在单片机上电时,首先指向的就是startup_stm32f40_41xxx.s这个文件,因为在这个文件中完成了对系统的初始化工作。下面具体看看做了哪些事情。在这个文件上方给出了这样一段描述:
This module performs:
;* - Set the initial SP //初始化SP指针
;* - Set the initial PC == Reset_Handler //设置PC指针指向Reset_Handler
;* - Set the vector table entries with the exceptions ISR address //设置中断向量表
;* - Configure the system clock and the external SRAM mounted on //调用SystemInit函数进行系统初始化,包括时钟、偏移地址等,且执行SystemInit函数后跳转回来
;* STM324xG-EVAL board to be used as data memory (optional,
;* to be enabled by user)
;* - Branches to __main in the C library (which eventually //最终跳转到main函数执行,且不再跳转回来,因此汇编初始化只执行一次,一旦跳出就不会回来。
;* calls main()).
;* After Reset the CortexM4 processor is in Thread mode,
;* priority is Privileged, and the Stack is set to Main.
从上面的描述中可以大概看出初始化工作做了哪些事情:包括堆栈初始化、SP初始化、PC初始化、系统时钟初始化并最终跳转到main函数。
1.在代码最开始进行定义中断向量表
__Vectors DCD __initial_sp ; Top of Stack //初始化SP指针指向栈顶,栈顶地址:0X08000000
DCD Reset_Handler ; Reset Handler //复位中断向量(中断向量表起始地址):0X08000004
DCD NMI_Handler ; NMI Handler //非可屏蔽中断向量:0X08000008
DCD HardFault_Handler ; Hard Fault Handler ..................
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler ..................
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved ..................
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts //外部中断
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
DCD RTC_WKUP_IRQHandler ; RTC Wakeup through the EXTI line
DCD FLASH_IRQHandler ; FLASH
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line0
DCD EXTI1_IRQHandler ; EXTI Line1
DCD EXTI2_IRQHandler ; EXTI Line2
..................
..................
..................
2.编写中断服务函数(重点分析Reset_Handler复位中断函数)
; Reset handler
Reset_Handler PROC //执行Reset_Handler中断服务函数
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit //输入SystemInit
IMPORT __main //输入__main
LDR R0, =SystemInit //将SystemInit函数的地址放在R0寄存器中
BLX R0 //跳转到SystemInit函数执行,并将返回地址放在LR寄存器中,执行完返回!!!
LDR R0, =__main //将main函数的地址放在R0寄存器中
BX R0 //跳转到main函数执行,并且不会返回!!!
ENDP
可以看出,在Reset handler中断服务函数中主要完成了两件事:
(1)调用SystemInit进行系统初始化
(2)跳转到mian处执行并不再跳回(因为BX R0并未保存返回地址至LR)
3.分析SystemInit()函数
SystemInit()系统初始化函数主要做了一下几件事情:
(1)设置是否开启FPU
(2)进行系统时钟初始化配置
(3)设置中断向量表偏移地址(ARM处理器默认中断向量表地址为:0x00000000,这里FLASH起始地址是0x08000000,因此设置偏移地址为0x08000000)
void SystemInit(void)
{
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* Reset the RCC clock configuration to the default reset state ------------*/
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset CFGR register */
RCC->CFGR = 0x00000000;
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* Reset PLLCFGR register */
RCC->PLLCFGR = 0x24003010;
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Disable all interrupts */
RCC->CIR = 0x00000000;
#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
/* Configure the System clock source, PLL Multiplier and Divider factors,
AHB/APBx prescalers and Flash settings ----------------------------------*/
SetSysClock();
/* Configure the Vector Table location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
}
4. 启动过程总结
<1>初始化堆栈、SP指针、令PC指向Reset_Handler(也就是最先执行复位中断服务函数)
<2>定义中断向量表(后面在中断发送时会根据中断向量表找到中断服务函数的入口地址)
<3>由于最开始PC指针指向了Reset_Handler中断,因此先执行Reset_Handler中断服务函数(进行初始化工作:包括初始化时钟,FPU,设置VTOR中断向量表偏移地址等)
<4>在Reset_Handler服务函数中SystemInit执行完返回并跳转到mian函数(采用BX跳转,不会返回了),进入用户程序执行,至此,系统启动,boot程序作用终结。
<5>通常main函数是while死循环执行的方式,那么当中断来的时候怎样执行呢?别忘了我们在bootloader程序的一开始就定义了中断向量表,将各中断都注册了(包括外部中断和内部中断),并且有系统或用户实现其中断服务函数。当有中断来临时系统会自动跳转到中断向量表,并根据中断号查找中断向量表获得中断服务子程序的入口地址,并跳转指向,中断指向完成后跳回main函数继续指向。
三.cortex-A7内核启动流程
通常ARM的A系列处理器会基于linux系统做开发,由专门的bootloader程序做引导。常用的例如U-BOOT。这里我们不分析U-BOOT,只是按照上面的MCU裸机开发的方式分析cortex-A7内核启动流程。具体过程和上面大同小异。
1.定义中断向量表
_start: //此处为定义中断向量表,后面需写其执行部分
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
可以看出Cortex-A中断向量表有8个中断,比上面的M4内核中断少了很多,而且我们常见的中断都没有了,这是为什么?这是因为Cortex-A内核有9种运行模式,其中断系统也更加复杂,因此中间又封装了一层,将所有中断分为了以上几类,我们常用的一些中断都在IRQ_Handler这一大类里面。在上面的表中我们重点关注Reset_Handler和IRQ_Handler。因为Reset_Handler完成了系统初始化,IRQ_Handler是用户级中断。
2.Reset_Handler中断服务函数
/******编写复位中断服务函数Reset_Handler,内容如下:*******/
Reset_Handler:
/*1)关闭I,D Cache和MMU。
CP15寄存器:
MRC将CP15协处理器中的寄存器数据读到ARM寄存器中。
MRC就是读CP15寄存器,MCR就是写CP15寄存器,MCR指令格式如下:*/
MCR{cond} p15,
,