ROM程序就是固化在芯片的ROM里面,把应用程序从存储器里加载/搬移到RAM中并使处理器开始执行应用程序的一段程序
1 Boot Code和ROM程序
从多普通单核MCU(如STM32)的使用者的角度来看,只需要把编译好的hex文件烧写到片上Flash中,再复位一下就能让程序在芯片上跑起来。
但此类开发者往往忘记了一个重要的问题——Flash里的程序是怎么被加载到MCU的RAM中的,或者说,被烧写到Flash中的程序是怎么跑起来的?程序明明是在RAM上执行,怎么把程序烧写到FLASH中就行了呢?
这个问题,实际上就是在问:裸片是如何Boot(启动)起来的?是的,你可以从本文的标题中获得这个问题的答案——Flash中的程序是由ROM程序加载到芯片的RAM中的。此外,在执行此前存储在Flash中的那个程序前,芯片可能还执行了一段被称为Boot Code的程序,用以设定芯片的初始状态。
2 芯片从Flash启动的流程
上边又抛出两个问题,ROM程序是啥?ROM程序执行的流程是怎样的?这一节就来讨论这两个问题。
如上图,MCU内含CORE(处理核)、ROM、RAM和FLASH这几个组件。芯片上电以后,CORE会从ROM固定地址读取程序,用以加载芯片使用者烧写到FLASH中的内容。这段存储在ROM中,芯片上电后首先执行的程序,就是我们说的ROM程序。
FLASH的开头位置通常会放一些FLASH文件的信息,比如程序有多大,搬移到什么地方去,搬移完成后从哪里开始执行。这些问题都是我们在编写ROM程序时需要考虑的,这部分放到第三节再详细讨论。
ROM程序首先会读取FLASH的文件信息,确认搬移长度和目的地址,这个地址一般是RAM的起始地址。然后根据读取到的信息把FLASH文件搬移到RAM上。搬移完成后,芯片或者MCU的RAM中就有了我们烧写到FLASH里的应用程序。这时只要把PC修改到程序的起始地址,就可以让我们的应用程序在芯片上跑起来了。
所以芯片从Flash启动的流程大致分步
- 芯片上电,CORE读取并执行ROM程序
- 读取FLASH文件信息
- 根据文件信息把FLASH文件中的应用程序搬移到RAM上
- 跳转PC到RAM上应用程序的起始地址
- 开始执行应用程序的第一条指令
3 如何设计ROM程序
3.1 考虑片上资源使用方式
这里提到的片上资源主要指RAM,它是我们搬移的目的地和执行程序的大本营。对于多核SoC而言,RAM需要被划分成几个区域供给不同的处理器作RAM使用。同时还应当保留一些空间用作共享内存(用来做数据交换和核间通信)。比如像下边这样。
值得注意的是,咱在RAM末尾预留了一部分空间用作堆栈,需要指定堆栈空间也是ROM程序和一般应用程序的不同之处。毕竟此时整片RAM都由我们规划,堆栈尽可能放到末尾能够方便我们划分其他空间的用途。
对于单核处理器而言,物理RAM的起始地址就可以是处理器的RAM空间起始地址,整片RAM的划分情况可以如下图所示,只有RAM和堆栈两个区域。为了方便叙述和读者理解,咱后面的设计都基于单核SoC这个前提。
比如假设某个MCU(比如STM32)有64K的RAM,就可以拿4K做堆栈,其余的60K全部用来放应用程序。
有了大致的划分方案,就要开始做计算了哈。
假设RAM起始地址为0x80000000,
64K = 601024 = 65536 = 0x10000*
60K = 601024 = 61440 = 0x0F000*
所以,RAM的地址范围是0x80000000-0x80010000;
其中,0x80000000-0x8000EFFF用来存放程序;
0x8000F000-0x8000FFFF用来当堆栈;
做完计算就可以得到下面这个设计结果啦。它指导着我们明确三件事:
- 应保证应用程序编译生成的的大小不超过60KB
- 上电复位后,应把堆栈指针寄存器(SP)初始值写为指向堆栈的最高地址0x8000FFFC(四字节对齐)
- ROM程序搬移FLASH文件的的目地址和搬移完成后的跳转地址都是0x80000000
3.2 设计存储FLASH文件信息数据结构
要把一段代码从FLASH搬到RAM,至少得知道这段代码在FLASH的什么位置吧,所以代码存放的起始地址和结束地址就得记录在前一节提到的文件信息段里。
知道从哪儿搬出来,还得知道搬到那个地址去。不过因为这个地址通常是固定的(比如本文介绍的这个启动程序固定搬到RAM的起始地址0x80000000去),所以就不用记录在FLASH文件信息段里啦。
搬移完成后需要一个跳转地址,用于设置PC,所以这个地址也得记录在信息段里边。
综上所述,我们的FLASH文件信息数据结构可以设计成下面这样。
typedef struct flash_info_seg{
unsigned int program_file_offset; //应用程序代码起始位置在flash里的偏移地址
unsigned int ram_start_addr; //搬移到ram时对应的起始地址
unsigned int ram_end_addr; //搬移到ram时对应的结束地址
unsigned int pc_branch_addr; //搬移完成后pc的跳转地址
}flash_info_seg_def;
假如你这样设计FLASH文件信息数据结构的话,下图里【FLASH文件信息】这个区域就应该放着一个flash_info_seg_def
。相应的,由于flash_info_seg_def
的大小为4*4Bytes = 16Bytes,所以【FLASH文件内容】这个区域的偏移地址B也就等于A + sizeof(flash_info_seg_def
),即A +16。
所以第一个参数program_file_offset
应赋值为B。
ram_start_addr
和pc_branch_addr
都应该赋值为RAM的起始地址0x80000000。
第三个参数ram_end_addr
的值跟你的应用程序的实际长度有关,假设你编译得到的二进制文件长度为0x100, 那么ram_end_addr
就应该赋值为ram_start_addr
+ 0x100。
3.3 设计启动方式选择组件
这部分可能大伙感到很陌生,但是至少知道开发板上通常有那么几个用于选择启动方式的跳帽或者拨码开关。芯片上电以后我们需要首先用几句汇编读一下这些引脚的状态,选择启动方式。
比如假设只有两种启动方式debug_boot和smcflash_boot,引脚的状态存储在0x90000000这个地址上,可以用下面的汇编程序实现启动模式的选择。
/* choose boot method option */
option_jump:
/* read boot mode pins status */
LDR r0, #0x90000000
LDR r1, [r0]
MOV r2, #0x1
AND r2, r2, r0
CMP r2, #0
BEQ debug_boot
B smcflash_boot
首先把0x90000000这个地址上的值读到通用寄存器r1中,
LDR r0, #0x90000000
LDR r1, [r0]
MOV r2, #0x1
然后把读出来的值和0x1做与运算,结果存到r2中
MOV r2, #0x1
AND r2, r2, r0
如果值是0,就选择debug启动模式,否则选择smcflash启动模式
CMP r2, #0
BEQ debug_boot
B smcflash_boot
3.4 考虑核间通信的需求
本文讨论的是单核处理器的启动程序,但设计方法也适用于多核处理器的启动。多核处理器启动时可能会遇到FLASH不能同时被多核读取的问题,也就要考虑多核的启动顺序。
关于这个,可以参考我这篇文章哈:在多核异构SoC平台上进行软件开发。
4 什么是Boot Code
可能你已经发现,前面咱们一直在讨论如何启动,但没有涉及硬件初始化的内容。所以这里说的Boot Code,特指芯片启动时堆芯片进行初始化的代码。
的这部分代码要做的工作包括以下这些:
- 异常初始化(Initializing exceptions)
- 寄存器初始化(Initializing registers)
- 配置MMU和Cache(Configuring the MMU and caches)
- 使能NEON和浮点加速器(Enabling NEON and Floating Point)
- 切换异常处理级别(Changing Exception levels)
ARMv8架构的处理器支持AArch32和AArch64两种运行模式,本文只介绍AArch32模式,AArch64模式的Boot Code可以参考ARM官方裸片启动程序示例手册《Bare-metal Boot Code for ARMv8-A Processors》
5 如何编写Boot Code
5.1 异常初始化
异常初始化需要设置向量表并启用异步异常。
在AArch32模式下启动处理器时,SCTLR的值。v设置复位向量的位置:
- 当SCTLR.V为0,处理器从地址0x00000000开始执行。
- 当SCTLR.V为1,处理器从地址0xFFFF0000开始执行。可以使用硬件输入VINITHI来设置SCTLR.V的复位值。
对于复位以外的异常,处理器查找向量表,通过对向量基址寄存器进行编程,可以将向量表放置在定制的位置。最多有四个向量表。相应的向量基址寄存器有:
- 向量基址寄存器(VBAR)(安全)
- 监控向量基址寄存器(MVBAR)
- 向量基址寄存器(HVBAR)
- VBAR(非安全)
.balign 0x20
vector_table_base_address:
B reset_handler
B undefined_handler
B svc_handler
B prefetch_handler
B data_handler
NOP
B IRQ_handler
// You can place the FIQ handler code here.
在使用向量表之前,必须初始化四个向量表,并对向量表基址寄存器进行编程。向量表的基址必须32字节对齐。
然后用下面的代码在复位后初始化VBAR和MVBAR
LDR R1, =secure_vector_table_base_address
MCR P15, 0, R1, C12, C0, 0 // Initialize VBAR (Secure).
LDR R1, =monitor_vector_table_base_address
MCR P15, 0, R1, C12, C0, 1 // Initialize MVBAR.
异步异常包括异步中止、IRQ和FIQ。它们可以由CPSR的{A,I,F}寄存器位屏蔽。因此,如果要使用异步中止(asynchronous aborts),IRQ和FIQ,CPSR.{A,I,F}必须清零。
// Enable asynchronous aborts, interrupts, and fast interrupts.
CPSIE aif
要使能中断,还必须初始化外部中断控制器,以便将中断传送给处理器,但本文不讨论这一个。
5.2 寄存器初始化
寄存器初始化包括初始化以下寄存器:
- 通用寄存器
- 堆栈指针寄存器
- 系统控制寄存器
5.2.1 初始化系通用寄存器
通对于用寄存器,没有特殊需求的话,全写0吧
5.2.2 初始化堆栈指针寄存器
堆栈指针寄存器(r13)在一些指令中隐式使用,例如push
和pop。在使用它之前,必须用一个合适的值初始化它。
在MPCore系统中,不同的堆栈指针(sp)必须指向不同的内存地址,以避免覆盖堆栈区域。如果需要在不同模式下使用sp,那就必须初始化所有sp。
下面代码是为一种模式初始化SP。SP指向的堆栈位于stack_top,堆栈大小为CPU_STACK_SIZE字节。
// Initialize the stack pointer.
LDR R13, =stack_top
ADD R13, R13, #4
MRC P15, 0, R0, C0, C0, 5 // Read MPIDR.
AND R0, R0, #0xFF // R0 == core number.
MOV R2, #CPU_STACK_SIZE
MUL R1, R0, R2 // Create separate stack spaces
SUB R13, R13, R1 // for each processor.
5.2.3 初始化系统控制寄存器
对于一些系统控制寄存器,例如保存的程序状态寄存器(SPSR)和异常链接寄存器hyp模式(ELR_hyp),该架构没有为它们定义复位值。因此,在使用寄存器之前,必须对其进行初始化。
下面是在监控(Monitor)模式下初始化SPSR和ELR_hyp
// Initialize SPSR in all modes.
MOV R0, #0
MSR SPSR, R0
MSR SPSR_svc, R0
MSR SPSR_und, R0
MSR SPSR_hyp, R0
MSR SPSR_abt, R0
MSR SPSR_irq, R0
MSR SPSR_fiq, R0
// Initialize ELR_hyp.
MOV R0, #0
MSR ELR_hyp, R0
理论上,所有没有架构上定义的复位值的系统寄存器都必须手动初始化。
我们前面说可以把所有通用寄存器写成0,但一些寄存器可以具有实现定义的复位值,这部分内容可以参考参见ARM架构参考手册 《ARM® Architecture Reference Manual ARMv8, for
ARMv8-A architecture profile》里面的通用寄存器(General system control registers)章节。
5.3 配置MMU和Cache
MMU和Cache配置涉及以下操作:
• 清除(Cleaning)和关闭(Invalidating)Cache
• 配置 MMU
• 使能 MMU 和 Caches
5.3.1 清理缓存并使其失效
缓存RAM中的内容在重置后无效,因此必须执行无效操作来初始化处理器中的所有缓存。
在某些ARMv7-A处理器中,如Cortex-A9处理器,必须使用软件来使所有Cache ram无效。在ARMv8-A处理器和大多数ARMv7-A处理器中,就不必这样做,因为硬件会在复位后自动使所有Cache ram失效。但是,在某些情况下,您必须使用软件来清理数据缓存并使其无效,例如核心断电过程。
下面是多次使用DCCISW指令来清理L1数据缓存并使其无效。其他级别缓存或其他缓存操作也可以参考这个代码。
// Disable L1 Caches.
MRC P15, 0, R1, C1, C0, 0 // Read SCTLR.
BIC R1, R1, #(0x1 << 2) // Disable D Cache.
MCR P15, 0, R1, C1, C0, 0 // Write SCTLR.
// Invalidate Data cache to create general-purpose code. Calculate the
// cache size first and loop through each set + way.
MOV R0, #0x0 // R0 = 0x0 for L1 dcache 0x2 for L2 dcache.
MCR P15, 2, R0, C0, C0, 0 // CSSELR Cache Size Selection Register.
MRC P15, 1, R4, C0, C0, 0 // CCSIDR read Cache Size.
AND R1, R4, #0x7
ADD R1, R1, #0x4 // R1 = Cache Line Size.
LDR R3, =0x7FFF
AND R2, R3, R4, LSR #13 // R2 = Cache Set Number – 1.
LDR R3, =0x3FF
AND R3, R3, R4, LSR #3 // R3 = Cache Associativity Number – 1.
CLZ R4, R3 // R4 = way position in CISW instruction.
MOV R5, #0 // R5 = way loop counter.
way_loop:
MOV R6, #0 // R6 = set loop counter.
set_loop:
ORR R7, R0, R5, LSL R4 // Set way.
ORR R7, R7, R6, LSL R1 // Set set.
MCR P15, 0, R7, C7, C6, 2 // DCCISW R7.
ADD R6, R6, #1 // Increment set counter.
CMP R6, R2 // Last set reached yet?
BLE set_loop // If not, iterate set_loop,
ADD R5, R5, #1 // else, next way.
CMP R5, R3 // Last way reached yet?
BLE way_loop // if not, iterate way_loop.
5.3.2 配置MMU
ARMv8-A处理器使用VMSAv8-32在AArch32中执行以下操作:
- 将物理地址转换成虚拟地址。
- 确定内存属性并检查访问权限。
地址转换由转换表定义,并由内存管理单元(MMU)管理。启用MMU之前,必须设置映射表(Translation table)和映射表遍历规则。
每个特权级(PL)都有专用的映射表和控制寄存器。在使用之前,必须设置所有映射表和控制寄存器。
有关ARMv8-A架构配置文件的详细信息,可以参考ARM架构参考手册ARMv8中关于VMSAv8-32的部分。
AArch32支持两种转换表格式:
- VMSAv8-32短描述符格式。
- VMSAv8-32长描述符格式。
在ARMv8-A中,安全状态下的软件执行权限等级由异常级别(el)定义。有关PLs和ELs之间的关系,可以参考《ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile》中的 Execution privilege, Exception levels, and AArch32 Privilege levels章节,这里就不赘述了。
5.3.3 使能MMU和Cache
在启用MMU和缓存之前,必须对它们进行初始化。在使能所有ARMv8-A处理器的MMU和缓存之前,必须设置SMPEN位,以支持硬件一致性。
下面是设置SMPEN位以及启用MMU和缓存。
// SMP is implemented in the CPUECTLR register.
MRRC P15, 1, R0, R1, C15 // Read CPUECTLR.
ORR R0, R0, #(0x1 << 6) // Set SMPEN.
MCRR P15, 1, R0, R1, C15 // Write CPUECTLR.
// Enable caches and the MMU.
MRC P15, 0, R1, C1, C0, 0 // Read SCTLR.
ORR R1, R1, #(0x1 << 2) // The C bit (data cache).
ORR R1, R1, #(0x1 << 12) // The I bit (instruction cache).
ORR R1, R1, #0x1 // The M bit (MMU).
MCR P15, 0, R1, C1, C0, 0 // Write SCTLR.
DSB
ISB
5.4 使能NEON和浮点加速器
在AArch32模式下,默认禁用NEON技术和FP,因此必须手动使能。
// Enable access to NEON/FP by enabling access to Coprocessors 10 and 11.
// Enable Full Access in both privileged and non-privileged modes.
MOV R0, #(0xF << 20) // Enable CP10 & CP11 function
MCR P15, 0, R0, C1, C0, 2 // Write the Coprocessor Access Control
ISB // Register (CPACR).
// Switch on the FP and NEON hardware.
MOV R1, #(0x1 << 30)
VMSR FPEXC, R1
这部分可以参考《ARM® Architecture Reference Manual ARMv8, for ARMv8-A architecture profile》中的Enabling Advanced SIMD and floating-point support章节。
5.5 切换模式
AArch32有九种处理器模式,分别是:
- USR
- SYS
- FIQ
- IRQ
- SVC
- ABT
- UND
- HYP
- MNT
在AArch32模式下启动时,处理器在复位后进入SVC模式。
通常,处理器接受或返回异常以改变到其他模式。也可以直接改变CPSR.m实现模式切换,如下。
.equ Mode_USR, 0x10
.equ Mode_FIQ, 0x11
.equ Mode_IRQ, 0x12
.equ Mode_SVC, 0x13
.equ Mode_MNT, 0x16
.equ Mode_ABT, 0x17
.equ Mode_HYP, 0x1A
.equ Mode_UND, 0x1B
.equ Mode_SYS, 0x1F
// When a processor is in Monitor, System, FIQ, IRQ, Supervisor, Abort
// or Undefined mode, use the CPS instruction to change modes.
CPS #Mode_FIQ
USR模式不能直接改写寄存器切换模式。所以当处于USR模式时,可以通过如下命令切换到SVC模式。
// When processors are in User mode, use SVC to change from User mode
// to SVC mode. Make sure that VBAR is initialized before executing SVC.
SVC #0
6 参考
- Bare-metal Boot Code for ARMv8-A Processors Version 1.0
- ARM® Architecture Reference Manual ARMv8, for ARMv8-A architecture profile (ARM DDI 0487)
- ARM® Cortex™-A Series Programmer’s Guide for ARMv7-A (ARM DEN 0013)
- ARM® Cortex®-A Series Programmer’s Guide for ARMv8-A (ARM DEN0024)