一. 启动过程
1.前奏
1.1 用C语言作为开发语言后,变量/函数的地址便由编译器在编译时自行分配。
1.2 启动文件:无论是keil uvision4还是IAR EWARM开发环境,ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。
== 相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况:
1、 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
2、 通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为0x8000000,同时复位后PC指针位于0x8000000处(最常用的方式);
3、 通过boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;
而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断向量表的位置而起始地址是可变化的。==
1.3 STM32的FLASH地址起始于0x08000000,RAM地址:0x2000000。
图片: ![)
二. 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,<opc1>,<Rt>,<CRn>,<CRm>,<opc2>
MRC p15,0,r0,c0,c0,0
/*现在要关闭I,D Cache和MMU,打开Cortex-A7参考手册到105页,找到SCTLR寄存器。也就是系统控制寄存器,此寄存器bit0用于打开和关闭MMU,bit1 对其控制位,bit2控制D Cache的打开与关闭。bit11用于控制分支预测,bit12用于控制I Cache。*/
/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除r0寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除r0寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除r0寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除r0寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除r0寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */
/*2)设置中断向量表偏移
将新的中断向量表首地址写入到CP15协处理器的VBAR寄存器。*/
MCR{cond} p15,<opc1>,<Rt>,<CRn>,<CRm>,<opc2>
MRC p15,0,r0,c12,c0,0 //读CP15寄存器
MCR p15,0,r0,c12,c0,0 //写CP15寄存器
/* 汇编版本设置中断向量表偏移,一定要在中断发生之前设置! */
//注:此处屏蔽掉是因为在C语言进行中断初始化中进行了中断向量表的偏移,因此汇编和C只需初始化一个即可
ldr r0, =0X87800000
dsb /* 数据同步指令 */
isb /* 指令同步指令 */
mcr p15, 0, r0, c12, c0, 0 /* 将r0寄存器中的值写入到CP15的C12寄存器中,设置VBAR寄存器=0x87800000*/
dsb /* 数据同步指令 */
isb /* 指令据同步指令 */
/*3)设置处理器9种工作模式对应的SP指针。要使用中断那么必须设置IRQ模式下的SP指针。索性直接设置所有模式下的SP指针。*/
/* 设置各个模式下的栈指针(SP指针),
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x12,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x1f,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
/*这里只设置了三种模式的栈指针,至于其余六种模式可自行设置!*/
/*4)跳到c函数,也就是main函数。*/
b main /* 跳转到 main 函数 */
3.IRQ_Handler中断服务函数
/************************1.功能:保护现场!!!***********************************/
IRQ_Handler:
111 push {lr} /* 保存 lr 地址 */
112 push {r0-r3, r12} /* 保存r0-r3,r12寄存器 ,触发中断的机制会自动保存其他寄存器,对于不能自动保存的需要手动保存*/
113
114 mrs r0, spsr /* 读取 spsr 寄存器 */
115 push {r0} /* 保存 spsr 寄存器 */
/************************2.功能:获取当前IRQ中断中特定中断的中断号,并保存在R0寄存器中,作为C语言调用的一个参数!!!***************/
117 mrc p15, 4, r1, c15, c0, 0 /* 将 CP15 的 C0 内的值到 R1 寄存器中,读取CP15的CBAR寄存器。CBAR寄存器保存了GIC控制器的寄存器组首地址。
118 * 参考文档 ARM Cortex-A(armV7)编程手册 V4.0.pdf P49
119 * Cortex-A7 Technical ReferenceManua.pdf P68 P138
120 */
121 add r1, r1, #0X2000 /* GIC 基地址加 0X2000,得到 CPU 接口端基地址 */
122 ldr r0, [r1, #0XC] /* CPU 接口端基地址加 0X0C 就是 GICC_IAR 寄存器,
123 * GICC_IAR 保存着当前发生中断的中断号,我们要根据
I.MX6U 嵌入式 Linux 驱动开发指南
124 * 这个中断号来绝对调用哪个中断服务函数
125 */
126 push {r0, r1} /* 保存 r0,r1 */
/*此时r0,r1分别保存了当前发生中断的中断号,CPU 接口端基地址*/
128 cps #0x13 /* 进入 SVC 模式,允许其他中断再次进去 */
129
130 push {lr} /* 保存 SVC 模式的 lr 寄存器 */
/************************3.功能:跳转到C语言处去执行具体的中断服务函数,执行完返回!!!***********************************/
131 ldr r2, =system_irqhandler /* 加载 C 语言中断处理函数到 r2 寄存器中*/
132 blx r2 /* 运行 C 语言中断处理函数,带有一个参数:中断号R0 ,在跳转到C语言函数执行时,自动的将RO作为参数传递给c语言函数*/
/************************4.功能:执行完C语言中断服务函数各寄存器出栈恢复现场,重新返回到曾经被中断打断的地方运行!!!****************/
134 pop {lr} /* 执行完 C 语言中断服务函数,lr 出栈 */
135 cps #0x12 /* 进入 IRQ 模式 */
136 pop {r0, r1}
137 str r0, [r1, #0X10] /* 中断执行完成,写 EOIR */
138
139 pop {r0}
140 msr spsr_cxsf, r0 /* 恢复 spsr */
141
142 pop {r0-r3, r12} /* r0-r3,r12 出栈 */
143 pop {lr} /* lr 出栈 */
144 subs pc, lr, #4 /* 将 lr-4 赋给 pc */
可以看出IRQ_Handler中断服务函数总体上完成了三件事,在发送通用级用户中断时,系统会在中断向量表中找到IRQ_Handler中断服务函数的入口地址并指向上述代码:上述代码时是汇编写的,大概流程是:1.保护现场----->2.获取更具体的中断号------>3.调用更具体的中断服务函数system_irqhandler,这个函数是有参数的,就是中断号,保存在R0寄存器中---->4.指定具体的中断服务程序,完成后返回------>5.恢复现场,返回main继续执行。
三.总结
总体来看cortex-M4与cortex-A7的启动流程基本一致:都是定义中断向量表---->执行复位中断服务Reset_Handler并在此中完成系统初始化----->跳转至main函数。
对于后面中断的处理过程也基本一致:都是查找中断向量表获取中断服务函数入口地址---->执行中断服务函数(保护现场、具体指向、恢复现场)------>指向完返回main。M4与A7的唯一不同可能就是对于系统中断的封装上,M4直接将所有中断都列在了中断向量表中。而A7将所有中断分为了两级,其中我们常见的中断都放在IRQ_Handler中,中断发生时先执行IRQ_Handler,在IRQ_Handler中再具体指向中断处理。
3.应用A7linux启动
待补充。
原文链接:https://blog.csdn.net/weixin_42700740/article/details/109703541
链接: link