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启动的流程大致分步

  1. 芯片上电,CORE读取并执行ROM程序
  2. 读取FLASH文件信息
  3. 根据文件信息把FLASH文件中的应用程序搬移到RAM上
  4. 跳转PC到RAM上应用程序的起始地址
  5. 开始执行应用程序的第一条指令

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用来当堆栈;


做完计算就可以得到下面这个设计结果啦。它指导着我们明确三件事:

  1. 应保证应用程序编译生成的的大小不超过60KB
  2. 上电复位后,应把堆栈指针寄存器(SP)初始值写为指向堆栈的最高地址0x8000FFFC(四字节对齐)
  3. 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_addrpc_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, #0x90000000LDR r1, [r0]MOV r2, #0x1AND r2, r2, r0CMP    r2, #0BEQ   debug_bootB smcflash_boot

首先把0x90000000这个地址上的值读到通用寄存器r1中,

 LDR r0, #0x90000000LDR r1, [r0]MOV r2, #0x1

然后把读出来的值和0x1做与运算,结果存到r2

 MOV r2, #0x1AND r2, r2, r0

如果值是0,就选择debug启动模式,否则选择smcflash启动模式

 CMP r2, #0BEQ   debug_bootB 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)
  • Github仓库 WaterCutter/pickArm

怎样编写裸片启动程序-ARMv8的Boot Code和ROM程序相关推荐

  1. spring boot基础教程:入门程序Hello World的编写

    前言 随着各种开源的第三方的组件爆发式增长,java的开发显得越来越笨重:繁多的配置,低下的开发效率,复杂的部署流程以及第三方技术集成难度大. 在上述环境下,Spring Boot应运而生.它使用&q ...

  2. 让小程序在自有App中启动的技术来了:mPaaS小程序架构深度解析

    简介:mPaaS 小程序框架作为一款 App 通用框架,帮助开发者面向自身的 App 实现小程序投放.不止如此,小程序代码仅需撰写一次,便可多端投放至自有 App.支付宝.钉钉甚至其他小程序开放平台. ...

  3. linux编写运行shell程序,Linux的Shell编程运行Shell程序的方法有哪些呢?

    用户可以用任何编辑程序来编写Shell程序.因为Shell程序是解释执行的,所以不需要编译成目的程序.按照Shell编程的惯例,以 bash为例,程序的第一行一般为"#!/bin/bash& ...

  4. 我的第一个安卓应用程序_今天,我启动了我的第一个移动应用程序。 这是我学到的...

    我的第一个安卓应用程序 by Harshita Arora 通过Harshita Arora 今天,我启动了我的第一个移动应用程序. 这是我学到的 (Today I launched my first ...

  5. VS2008环境下开发的某些程序在其他机器运行提示“由于应用程序配置不正确,应用程序未能启动”的问题(IIS)...

    比较全的有关vs2008部署问题集(1): http://blog.csdn.net/buhuizheyangba/article/details/7220598 比较全的有关vs2008部署问题集( ...

  6. 由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题

    我们用 VS 2005 编写非托管的程序, 在一台未安装 .net 开发环境的机器上运行会出现 "由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题". ...

  7. ARM Cortex-M底层技术(四)编写自己的启动代码

    编写自己的启动代码 上一篇扯了一些关于启动代码的应用方面的内容,列举了4种我自己遇到过的常见的启动代码应用,当然实际的应用肯定不止上一篇文章中提到的那几种,关键是大家懂了原理后根据实际的需求添加自己的 ...

  8. 解决“由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题”...

    [VS2005]解决"由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题" 今天在准备发布用VS2005写的那个程序时,拷贝到我同事机器上,双击突然出现 ...

  9. 解决“由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题”(转贴)...

    [VS2005]解决"由于应用程序的配置不正确,应用程序未能启动,重新安装应用程序可能会纠正这个问题" 今天在准备发布用VS2005写的那个程序时,拷贝到我同事机器上,双击突然出现 ...

最新文章

  1. Boost:bind绑定的unique_ptr测试程序
  2. VOC和COCO数据集标注格式的介绍
  3. appium启动APP配置参数:
  4. 山东大学网络教育计算机基础考试题,山东大学网络教育计算机网络基础期末考试复习...
  5. 开源日志系统log4cplus(三)
  6. 致远OA ajaxAction formulaManager 文件上传漏洞
  7. java实现萤石云截图保存上传到ftp功能
  8. 睡眠革命——摘抄总结大纲
  9. 【观察】戴尔:为核心数据“保驾护航”,为数字化转型“拨云见日”
  10. 青出于蓝而胜于蓝——揭秘全新的 HWSQL
  11. DhtmlxGrid第一次接触,jsp页面
  12. 灰色简约大学生小组作业展示PPT模板
  13. 首次「机器学习」挑战赛下周开始,内含知识点剧透
  14. 四川省全国计算机考试一年可以考几次,四川网络教育统考科目没通过怎么办,一年可以考几次?...
  15. matlab心电信号处理,基于Matlab的心电信号自动处理系统的设计与开发 毕业论文设计.doc...
  16. Android截取人物头像,Android 图片截取人物头像(仿逗拍)
  17. qma7981 源码 驱动_高品质PCB板配单报价,QMA7981-TR
  18. 计算机体检查杀病毒,如何去深度查杀电脑病毒
  19. 天眼探空经济发展_前沿|“天眼”探空惊艳全球
  20. Android网页浏览器开发详解(一)

热门文章

  1. Stateless Network Functions
  2. 23教资报名今日正式开始!这些资料填写一定要注意
  3. 分治算法——二分查找
  4. linux栈溢出检测原理,操作系统栈溢出检测之ucosII篇
  5. 【FFMPEG】AVERROR(EAGAIN)含义
  6. mysql删除表编码字段_mysql 修改 添加 删除 表字段
  7. python银行定期存款利率_银行员工说漏嘴:一年之中,这些时间段去存钱,银行给的利息最高...
  8. IOC和DI 的区别
  9. php合并数组,不使用函数array_merge()
  10. c语言 二维坐标,AutoCAD的二维坐标可分为() 。A.直角坐标B.极坐标C.UCS坐标D.三维坐标...