资料准备
ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf
stm32MP135裸机示例工程
0 前言
众所周知,在裸机程序在进入main函数之前都需要一份启动代码,这份启动代码基本都是汇编语言编写,为跳转到C语言运行环境做好准备工作。本文以基于Cortex-A7内核的stm32MP135为例,分析其裸机启动代码。
1 stm32MP135第一个执行的函数Reset_Handler
查看stm32MP135的裸机链接脚本,可以看到如下语句:
/* Entry Point */
ENTRY(Reset_Handler)
ENTRY(symbol)定义了应用程序执行的一条指令,symbol为所在的符号,也就意味着stm32MP135启动的第一条指令是Reset_Handler符号所在的地址
接着我们看看stm32MP135定义Reset_Handler的地址:
可以看到Reset_Handler函数被定义在内存最开始的位置。
查看.list链接文件可以看到Reset_Handler被定义在0x2ffe0000位置:
这个位置就是stm32MP135的SYSRAM的首地址:
这样一来,当stm32MP135的内部bootloader将PC指向SYSRAM首地址时,便会进入Reset_Handler函数。
2 stm32MP135启动代码分析:Reset_Handler函数
Reset_Handler函数是在.c文件中的内嵌汇编代码,源码如下。__asm__表示后面的代码为内嵌汇编,"__volatile__告诉编译器不要优化这块内嵌汇编代码。
/*----------------------------------------------------------------------------
Reset Handler called on controller reset
*----------------------------------------------------------------------------*/
void Reset_Handler(void) {
__asm__ volatile(
".code 32 \n"
/* Mask interrupts */
"CPSID if \n"
/* Put any cores other than 0 to sleep */
"MRC p15, 0, R0, c0, c0, 5 \n" /* Read MPIDR */
"ANDS R0, R0, #3 \n"
"goToSleep: \n"
"ITT NE \n" /* Needed when in Thumb mode for following WFINE instruction */
"WFINE \n"
"BNE goToSleep \n"
/* Reset SCTLR Settings */
"MRC p15, 0, R0, c1, c0, 0 \n" /* Read CP15 System Control register */
"BIC R0, R0, #(0x1 << 12) \n" /* Clear I bit 12 to disable I Cache */
"BIC R0, R0, #(0x1 << 2) \n" /* Clear C bit 2 to disable D Cache */
"BIC R0, R0, #0x1 \n" /* Clear M bit 0 to disable MMU */
"BIC R0, R0, #(0x1 << 11) \n" /* Clear Z bit 11 to disable branch prediction */
"BIC R0, R0, #(0x1 << 13) \n" /* Clear V bit 13 to disable hivecs */
"BIC R0, R0, #(0x1 << 29) \n" /* Clear AFE bit 29 to enable the full range of access permissions */
"ORR R0, R0, #(0x1 << 30) \n" /* Set TE bit to take exceptions in Thumb mode */
"MCR p15, 0, R0, c1, c0, 0 \n" /* Write value back to CP15 System Control register */
"ISB \n"
/* Configure ACTLR */
"MRC p15, 0, r0, c1, c0, 1 \n" /* Read CP15 Auxiliary Control Register */
"ORR r0, r0, #(1 << 1) \n" /* Enable L2 prefetch hint (UNK/WI since r4p1) */
"MCR p15, 0, r0, c1, c0, 1 \n" /* Write CP15 Auxiliary Control Register */
/* Set Vector Base Address Register (VBAR) to point to this application's vector table */
"LDR R0, =Vectors \n"
"MCR p15, 0, R0, c12, c0, 0 \n"
"ISB \n"
/* Setup Stack for each exceptional mode */
"CPS %[fiq_mode] \n"
"LDR SP, =FIQ_STACK \n"
"CPS %[irq_mode] \n"
"LDR SP, =IRQ_STACK \n"
"CPS %[svc_mode] \n"
"LDR SP, =SVC_STACK \n"
"CPS %[abt_mode] \n"
"LDR SP, =ABT_STACK \n"
"CPS %[und_mode] \n"
"LDR SP, =UND_STACK \n"
"CPS %[sys_mode] \n"
"LDR SP, =SYS_STACK \n"
/* Call SystemInit */
"BL SystemInit \n"
/* Unmask interrupts */
"CPSIE if \n"
/* Initialize libc */
"BL __libc_init_array \n"
/* Call __main */
"BL main \n"
::[usr_mode] "M" (USR_MODE),
[fiq_mode] "M" (FIQ_MODE),
[irq_mode] "M" (IRQ_MODE),
[svc_mode] "M" (SVC_MODE),
[abt_mode] "M" (ABT_MODE),
[und_mode] "M" (UND_MODE),
[sys_mode] "M" (SYS_MODE));
}
源码分析如下:
(1)指示后面指令均为ARM指令集
".code 32 \n"
表示后面的指令全部用ARM指令集。
(2)关闭IRQ和FIQ中断
"CPSID if \n"
CPSID指令用来改变处理器状态,具体来说就是失能中断。这里的if指的是IRQ和FIQ,意思就是失能IRQ和FIQ中断。
(3)将除0之外的所有核心置于睡眠状态
/* Put any cores other than 0 to sleep */
"MRC p15, 0, R0, c0, c0, 5 \n" /* Read MPIDR */
"ANDS R0, R0, #3 \n"
"goToSleep: \n"
"ITT NE \n" /* Needed when in Thumb mode for following WFINE instruction */
"WFINE \n"
"BNE goToSleep \n"
Cortex-A内核支持多核心,stm32MP135是单核SoC,这里将其它没有用上的核心全部置于睡眠状态。
逐行分析:
(3.1)将MPIDR寄存器(多处理器关联寄存器)的值写入到寄存器R0
"MRC p15, 0, R0, c0, c0, 5 \n" /* Read MPIDR */
MRC指令的语法如下所示:
MRC{cond} coproc, opc1, Rt, CRn, CRm{, opc2}
cond为条件码。
coproc为协处理器名称,CP0~CP15协处理器分别对应名称p0~p15。
opc1为协处理器要执行的操作码,取指范围为0~7。
Rt为ARM通用寄存器,用于存储读取到的协处理器寄存器数据。
CRn为协处理器寄存器,对于CP15协处理器来说,CRn取值范围为c0~c15。
CRm为协处理器寄存器,对于CP15协处理器来说,通过CRm和opc2一起来确定CRn对应的具体寄存器。
opc2为可选的协处理器执行操作码,取指范围为0~7,当不需要的时候要设置为0。
(3.2)将MPIDR寄存器的值和0x3进行逻辑与,将结果保存到寄存器R0
"ANDS R0, R0, #3 \n"
ANDS是一个ARM指令,与and类似,但对两个操作数进行按位与运算,并将结果存储到第一个操作数中。例如:ands r1, r2, #0xff 表示将寄存器r2中的值和0xff进行按位与运算,然后将结果存储到寄存器r1中。ands影响z比特位,Z标志位用于表示两个操作数进行逻辑与操作后的结果是否为零,如果ANDS指令的操作数进行逻辑与操作后的结果为零,则Z标志位被设置为1,表示两个操作数相等。
(3.3)定义goToSleep函数
在汇编语言中,地址标号:用于标识内存单元的地址,它们在程序中的作用类似于高级语言的函数名或变量名。地址标号可以分为两种主要类型:地址标号和数据标号。
"ITT NE \n"
其实ITT指令的结构就是“IF-Then-Then”,语法都是由字母“T”和“E”构成。IT块中的每条指令必须指定相同或逻辑相反的条件后缀。意思是,如果使用NE,那么前两个指令必须有相同的后缀。 NE表示not equal(不相等)。这里判断MPIDR的值是否为0x3,如果不为0x3则会执行接下来的2行汇编代码。
"WFINE \n"
WFI指令是ARM架构中用于将处理器置于等待中断状态的指令,主要用于低功耗模式。 当处理器执行WFI指令时,它会停止执行当前程序,进入等待状态,直到接收到中断信号为止。
"BNE goToSleep \n"
跳转到goToSleep标号(函数)处。
(4)重设系统控制寄存器(SCTLR)
/* Reset SCTLR Settings */
"MRC p15, 0, R0, c1, c0, 0 \n" /* Read CP15 System Control register */
"BIC R0, R0, #(0x1 << 12) \n" /* Clear I bit 12 to disable I Cache */
"BIC R0, R0, #(0x1 << 2) \n" /* Clear C bit 2 to disable D Cache */
"BIC R0, R0, #0x1 \n" /* Clear M bit 0 to disable MMU */
"BIC R0, R0, #(0x1 << 11) \n" /* Clear Z bit 11 to disable branch prediction */
"BIC R0, R0, #(0x1 << 13) \n" /* Clear V bit 13 to disable hivecs */
"BIC R0, R0, #(0x1 << 29) \n" /* Clear AFE bit 29 to enable the full range of access permissions */
"ORR R0, R0, #(0x1 << 30) \n" /* Set TE bit to take exceptions in Thumb mode */
"MCR p15, 0, R0, c1, c0, 0 \n" /* Write value back to CP15 System Control register */
"ISB \n"
通过设置CPSR寄存器,将ICache、DCache、MMU、分支预测、向量基址设置为0x0、页表支持全部访问权限、在Thumb模式下处理异常/中断,最后使用指令同步屏障,保证所有操作执行完再执行下一条汇编代码。
有关CPSR寄存器的描述如下:
(5)配置辅助控制寄存器(ACTLR)
/* Configure ACTLR */
"MRC p15, 0, r0, c1, c0, 1 \n" /* Read CP15 Auxiliary Control Register */
"ORR r0, r0, #(1 << 1) \n" /* Enable L2 prefetch hint (UNK/WI since r4p1) */
"MCR p15, 0, r0, c1, c0, 1 \n" /* Write CP15 Auxiliary Control Register */
通过设置ACTLR寄存器,使能L2 prefetcher(预取)。打开L2 prefetcher功能后,当访问大片内存,同时Dcache中没有缓存时,可以提高访问速率。这个寄存器是IMPLEMENTATION DEFINED(简称 IMP DEF,由处理器设计厂商定义)寄存器,因此在ARMv7的参考手册并没有该寄存器描述。
(6)设置向量表基地址
/* Set Vector Base Address Register (VBAR) to point to this application's vector table */
"LDR R0, =Vectors \n"
"MCR p15, 0, R0, c12, c0, 0 \n"
"ISB \n"
首先将向量表基地址Vectors写入寄存器R0,然后使用MCR指令,将向量表基地址写VBAR(向量基址寄存器),VBAR寄存器描述如下:
注意:
向量基址32字节对齐的。
(7)为不同工作模式设置堆栈指针
#define USR_MODE 0x10 /* User mode */
#define FIQ_MODE 0x11 /* Fast Interrupt Request mode */
#define IRQ_MODE 0x12 /* Interrupt Request mode */
#define SVC_MODE 0x13 /* Supervisor mode */
#define ABT_MODE 0x17 /* Abort mode */
#define UND_MODE 0x1B /* Undefined Instruction mode */
#define SYS_MODE 0x1F /* System mode */
/* Setup Stack for each exceptional mode */
"CPS %[fiq_mode] \n"
"LDR SP, =FIQ_STACK \n"
"CPS %[irq_mode] \n"
"LDR SP, =IRQ_STACK \n"
"CPS %[svc_mode] \n"
"LDR SP, =SVC_STACK \n"
"CPS %[abt_mode] \n"
"LDR SP, =ABT_STACK \n"
"CPS %[und_mode] \n"
"LDR SP, =UND_STACK \n"
"CPS %[sys_mode] \n"
"LDR SP, =SYS_STACK \n"
CPS指令用于切换当前CPU工作模式,切换到某种模式后,将对应的堆栈首地址写入SP堆栈指针,最后切换到的模式是系统模式。
在链接脚本内有所有模式栈底地址的定义:
在初始化了堆栈指针后我们便可以进入C语言的世界了。
(8)系统初始化
"BL SystemInit \n"
跳转到SystemInit函数,这个函数使用C语言编写。主要工作如下:将.bss段全部设置为0(不需要对.data段操作,因为本例的.data段数据已经被GCC直接加载到RAM中。如果是使用ARM编译器,则在执行__main中的__scatterload会初始化data段、bss段,GCC编译器可以使用__libc_init_array对data段和bss段初始化)、初始化页表、清空所有中断/异常、初始化分支预测、初始化ICache、初始化DCache,如果定义了MMU_USE宏,则创建页表并使能MMU。如果定义了CACHE_USE宏则使能ICache、DCache。最后初始化GIC,设置中断向量。
有关GCC是怎样将data段放到镜像中(最终被stm32MP135内部bootloader加载到RAM)的可以参考下图:
(10)使能IRQ和FIQ中断
"CPSIE if \n"
CPSIE指令用来改变处理器状态,具体来说就是使能中断。这里的if指的是IRQ和FIQ,意思就是使能IRQ和FIQ中断。
(11)跳转到main函数
"BL main \n"
最后便是进入mian函数,执行用户代码。
3 总结
(1)Cortex-A7裸机启动代码负责清理环境,营造一个纯净的运行环境(失能中断、关闭Cache等),这在BOOT跳转到APP时非常有用。
(2)Cortex-A7裸机启动代码负责初始化各个工作模式下的堆栈指针,最后切换到系统模式。
(3)Cortex-A7裸机启动代码负责系统初始化,如Cache初始化、页表初始化及创建等。