文章目录
- [1 STM32MP1启动详解](#1 STM32MP1启动详解)
-
- [1.1 STM32MP1启动模式](#1.1 STM32MP1启动模式)
- [1.2 STM32MP1启动流程详解](#1.2 STM32MP1启动流程详解)
-
- [1.2.1 内部ROM代码](#1.2.1 内部ROM代码)
- [1.2.2 安全启动](#1.2.2 安全启动)
- [1.2.3 串行启动](#1.2.3 串行启动)
-
- [1.2.3.1 USB启动](#1.2.3.1 USB启动)
- [1.2.3.2 UART启动](#1.2.3.2 UART启动)
- [1.3 Flash设备启动要求](#1.3 Flash设备启动要求)
-
- [1.3.1 从NAND启动](#1.3.1 从NAND启动)
- [1.3.2 从EMMC启动](#1.3.2 从EMMC启动)
- [1.3.3 从SD卡启动](#1.3.3 从SD卡启动)
- [1.4 STM32MP1二进制头部信息](#1.4 STM32MP1二进制头部信息)
- [2 STM32MP1 Linux系统启动过程](#2 STM32MP1 Linux系统启动过程)
1 STM32MP1启动详解
STM32单片机是直接将程序下载到内部Flash中,上电以后直接运行内部Flash中的程序。STM32MP157内部没有供用户使用的Flash,系统都是存放在外部Flash里面的,比如EMMC、NAND等,因此STM32MP157上电以后需要从外部Flash加载程序到内存中。STM32MP157支持多种启动方式,这些启动方式都是怎么运行的?
1.1 STM32MP1启动模式
STM32MP1支持从多种设备启动,比如EMMC、SD、NAND、NOR、USB、UART等。STM32MP1内部有一段ROM来存放ST自己编写的程序,这段ROM空间是不开放给用户使用的,仅供ST存放自己的ROM代码,ROM空间如下图所示:

上图中CA7是Cortex-A7的缩写,可以看出A7内核有128KB的ROM空间,起始地址为0X00000000,STM32MP1上电以后会先运行这段ROM代码。STM32MP157有三个BOOT引脚:BOOT0~BOOT2,这三个BOOT引脚通过拉高/拉低来设置从哪种设备启动,正点原子STM32MP157开发板上的拨码开关就是控制这三个BOOT引脚的,如下所示:

从上图可以看出,当BOOT0~BOOT1拨到"ON"的时候就会接到3.3V上,此时就是逻辑1,拨到OFF的时候BOOT0~BOOT1就悬空(也可以外接下拉电阻),此时就是逻辑0(悬空或接地都为逻辑0),三个引脚不同电平对应的启动模式,如表所示:

1.2 STM32MP1启动流程详解
STM32MP1内部有一段ST自己编写的ROM代码,这段ROM代码上电以后就会自动运行,ROM代码会读取BOOT0~BOOT2这三个引脚电平,获取启动模式信息,比如读取到是从EMMC启动的,那么ROM代码就会从EMMC中读取相关程序。
1.2.1 内部ROM代码
内部ROM代码支持如下功能:
- Secure boot(安全启动),不管是串行启动还是从Flash设备启动;
- Engineeringboot(工程启动??),当BOOT2~BOOT0设置为100的时候,我们就可以通过STLINK访问A7或者M4内核。一般是通过此方法来调试M4内核代码;
- Secondarycoreboot(第二个内核启动),复位以后STM32MP157的每个A7内核都会启动,并且运行相同的指令。内部ROM代码会分离执行流,只有Core0才会运行ROM代码,另外一个内核会处于一个死循环状态,等待应用程序发送信号来进行下一步操作。这个信号是由SGI(软中断)和另外两个BACKUP寄存器:MAGIC_NUMBER、BRANCH_ADDRESS组成的。如果要启动Core1,运行在Core0的应用程序需要:
- 将跳转地址写入BRANCH_ADDRESS寄存器;
- 将0xCA7FACE1这个值写入到MAGIC_NUMBER寄存器;
- 向Core1和发送SGI中断;
- 总结一下,只要不开启第二个核,那么由于内部ROM代码的作用,此时STM32MP157就相当于单核A7;这样有利于我们编写的STM32MP157的A7裸机例程,因为无需考虑多核情况;
- RMAboot,RMA是Return Material Authorization的缩写;
- 低功耗唤醒;
- 提供安全相关服务。
内核ROM启动流程如下图所示:

红色部分就是最常用的启动流程,也就是上电或复位以后运行流程:
- 首先检查当前是不是CPU0运行,如果不是的话就启动CPU1,正常上电肯定是CPU0在运行;
- 如果是CPU0在运行,检查复位原因;
- 检查是否为退出Standby而导致的复位,如果不是的话就进入RMA检查;
- 检查是否为RMA启动,不是的话是否为ENGI启动;
- 如果不是ENGI启动的话直接进去冷启动;
- 进入冷启动以后就从flash中加载系统,并且进行鉴权,如果鉴权成功的话就运行系统。
1.2.2 安全启动
FSBL全称为 First stage boot loader,也就是**第一阶段启动文件**。
SSBL全称为 Second stage boot loader,也就是**第二阶段启动文件**。
当设置好BOOT2~BOOT0,选择从外部Flash,比如EMMC、NAND或NOR等启动的时候就会进入安全启动流程。STM32MP157的安全启动流程比较复杂,简单了解一下安全启动的基本流程:
- 首先ROM代码从选定的Flash设备中加载FSBL镜像文件,FSBL镜像就是ROM加载的第一个用户编写的可执行程序,一般是TF-A镜像,但是可以换成用户编写的程序,比如A7裸机代码。FSBL镜像是有要求的,不是简单的把bin文件丢过来就可以,而是需要在bin文件前面添加一个[[#^4d6a3d|头部信息]],否则内部ROM代码不知道如何处理这个bin文件;
- FSBL镜像加载以后需要对其进行鉴权;
- 如果鉴权成功,那么就会跳转到FSBL镜像入口地址,开始运行FSBL固件。
内部ROM首先从选定的Flash设备中读取FSBL镜像文件并运行,但是此时DDR还没有初始化,那么FSBL镜像在哪里运行呢?ST32MP1内部有256KB的SYSRAM,如图所示:

分析上图,SYSRAM地址范围为:0X2FFC0000~0X2FFFFFFF,一共是256KB。ROM代码会将FSBL镜像拷贝到0X2FFC2400地址,但是要注意,FSBL镜像的起始地址不是0X2FFC2400,因为FSBL镜像前面还有一个256(0X100)字节的头部信息,因此FSBL镜像的真正起始地址为0X2FFC2400+0X100=0X2FFC2500。将FSBL镜像换成A7裸机例程,编译A7裸机例程的时候要指定链接起始地址,这个链接起始地址就是0X2FFC2500。由此可以计算出整个FSBL镜像大小不能超过0X30000000 - 0X2FFC2500 = 252672B = 246.75KB,编写裸机程序大小也不能超过246.75KB。
FSBL镜像鉴权成功以后,ROM代码会boot上下文的起始地址保存到R0寄存器,然后跳转到FSBL镜像的入口地址,这个入口地址会定义到头部里面,其实就是0X2FFC2500。
1.2.3 串行启动
当设置BOOT2~BOOT0为串行启动,也就是从USB或UART启动的时候就会进入此模式。当选择串行启动以后ROM代码就会并行扫描所有可以启动的UART以及USB OTG接口。当扫描到某个活动的串行接口以后,ROM代码就会使用此串行接口,并且忽略掉其他的串行接口。
1.2.3.1 USB启动
内部ROM代码支持USB OTG启动,使用STM32CubeProgrammer软件通过USB OTG接口来向STM32MP1烧写系统。USB OTG需要一个48M和60M的时钟,这两个时钟由HSE生成。ROM代码支持的HSE时钟值如下:
tex
8,10,12,14,16,20,24,25,26,28,32,36,40,48MHz
正点原子STM32MP157开发板使用24M有源晶振作为HSE时钟源。可以通过设置OTP来更改ROM代码的HSE晶振大小。但是不建议修改,可能会导致芯片报废。
1.2.3.2 UART启动
如果要选择UART启动,也就是通过UART烧写系统,那么只能使用USART2、USART3、UART4、UART5、USART6、UART7或UART8,此时串口工作模式为1位起始位、8位数据位、偶校验、1位停止位、波特率115200。由于STM32的IO复用功能,1个串口可能有多个IO可以使用,比如UART4的RX(接收)可以使用PI10、PH14、PA1、PA11、PB2、PB8、PC11、PD0或PD2,一共9个IO可以用作UART4_RX引脚,但是ROM代码里面的UART4_RX引脚肯定只会使用这个9个里面的其中一个,所以板子的串口引脚要和ROM代码里面的一致,否则就无法使用串口启动。ROM代码里面串口使用的引脚如表5.2.3.2所示:
| 串口 | 串口引脚 | 所使用的IO | 复用编号(AF) |
|---|---|---|---|
| USART2 | USART2_RX | PA3 | AF07 |
| USART2_TX | PA2 | AF07 | |
| USART3 | USART3_RX | PB12 | AF08 |
| USART3_TX | PB10 | AF07 | |
| UART4 | UART4_RX | PB2 | AF08 |
| UART4_TX | PG11 | AF06 | |
| UART5 | UART5_RX | PB5 | AF12 |
| UART5_TX | PB13 | AF14 | |
| USART6 | USART6_RX | PC7 | AF07 |
| USART6_TX | PC6 | AF07 | |
| UART7 | UART7_RX | PF6 | AF07 |
| UART7_TX | PF7 | AF07 | |
| UART8 | UART8_RX | PE0 | AF08 |
| UART8_TX | PE1 | AF08 |
1.3 Flash设备启动要求
STM32MP1支持从SD、EMMC、NAND或NOR等Flash设备启动,但是不同的Flash设备在启动的时候有不同的要求。linux系统不像单片机那样,就一个bin文件,烧写进去就可以启动并运行,linux系统自身编译出来就是一个镜像文件,但是这个镜像文件要运行是需要一大堆的工具来辅助。比如需要uboot来启动,启动以后还需要根文件系统(rootfs),传统的嵌入式linux有三巨头:uboot、kernel和rootfs,但是对于STM32MP1而言,又多了几个工具,比如TF-A、TEE、vendorfs等,所有这一大堆构成了最终的系统镜像。系统镜像是要烧写到Flash设备中的,这些不同的文件肯定要按照一定的要求,分门别类的烧写,一个萝卜一个坑,TF-A应该放到哪里、uboot应该放到哪里等等。
针对Flash设备,可以通过创建不同的分区来存放不同的文件,ST针对STM32MP1系列给出了官方分区建议,这些建议包含了Flash分区数量、分区最小空间、分区存放的内容等,如表所示:
| 尺寸 | 分区 | 描述 |
|---|---|---|
| 256KB~512KB | fsbl | 第一阶段启动代码,此分区存放TF-A或者uboot的SPL部分,如果写A7裸机例程的话此分区也用来存放裸机代码 |
| 2MB | ssbl | 第二阶段启动代码,一般是uboot,如果uboot使用设备树的话,设备树添加到后面 |
| 64MB | bootfs | boot文件分区,可以存放如下内容: - init ram文件系统,可以将此文件系统拷贝到RAM中,在linux内核挂载正式根文件系统之前可以使用init ram文件系统; - linux内核设备树; - linux内核; - uboot显示的启动界面; - uboot发行配置文件extlinux.conf。 |
| 16MB | vendorfs | 此分区存放第三方的版权信息,确保他们不会受到任何开源许可的污染,比如GPL V3 |
| 768MB | rootfs | linux根文件系统 |
| 剩余空间 | userfs | 用户自行使用的剩余空间 |
1.3.1 从NAND启动
NAND前几个块(block)里面包含了多份FSBL,ROM代码会从第一个块开始扫描,并且加载第一个有效块里面的FSBL。ROM代码支持并行NAND和串行NAND,并行NAND连接到FMC总线上,串行NAND连接到QSPI上。
ROM代码支持的并行NAND要求如下:
| 块大小(KB) | 页大小(KB) | 数据宽度 | ECC(bit数和编码) |
|---|---|---|---|
| 128 | 2 | 8,16 | 4(bch),8(bch),1(hamming) |
| 256 | 4 | 8,16 | 4(bch),8(bch),1(hamming) |
| 512 | 4 | 8,16 | 4(bch),8(bch),1(hamming) |
| 512 | 8 | 8,16 | 4(bch),8(bch),1(hamming) |
ROM代码支持串行NAND的要求如表所示:
| 块大小(KB) | 页大小(KB) |
|---|---|
| 128 | 2 |
| 256 | 4 |
| 512 | 4 |
| 512 | 8 |
1.3.2 从EMMC启动
EMMC在物理结构上有boot1、boot2、RPMB(Replay Protected Memory Block)、GPP(General Purpose Partitions,GPP最多4个分区)以及UDA(User Data Area)这5种分区,比如三星的KLM系列EMMC5.1的分区结构如图所示:

常用的就是UDA分区,也就是用户数据区域,很少会关心boot1、boot2这样的分区。boot1、boot2、RPMB这三个分区大小是固定的,用户不能修改,boot1、boot2分区存在的意义就是用于引导系统。正点原子STM32MP157开发板所使用的EMMC型号为KLM8G1GETF,这是三星的一颗8GB EMMC5.1芯片,boot1、boot2和RPMB分区大小如图所示:

从上图可以看出,对于三星的8GB的EMMC而言,boot1和boot2分区默认大小为4096KB,RPMB为512KB。
ST会使用EMMC的boot1和boot2这两个分区作为FSBL,但是同一时间只有一个有效,ROM代码会加载有效的哪个FSBL。ROM代码使用单bit模式来操作EMMC,默认情况下ROM代码使用连接到SDMMC2上的EMMC。
1.3.3 从SD卡启动
SD卡也包含两个FSBL,但是SD卡没有boot1和boot2这样的物理分区。ROM代码默认尝试加载第一个FSBL,如果第一个FSBL加载失败,那么ROM代码就会加载第二个FSBL。
ROM代码首先在SD卡上查找GPT分区,如果找到的话就查找名字以"fsbl"开始的两个FSBL分区。如果没有找到GPT分区的话就直接根据物理地址查找两个FSBL,第一个FSBL的起始偏移地址为LBA34,地址为34 * 512 = 17408 = 0X4400,所以第一个FSBL的起始地址为0X4400。第二个FSBL的起始偏移地址为LBA546,地址为546 * 512 = 279552 = 0X44400,所以第二个FSBL的起始地址为0X44400。
ROM代码默认也是使用单bit模式操作SD卡,并且默认使用连接到SDMMC1接口上的SD卡。
1.4 STM32MP1二进制头部信息
^4d6a3d
STM32MP1内部的ROM代码会先读取FSBL代码,一般是TF-A或者Uboot的SPL,也可以是A7裸机代码。比如TF-A编译生成二进制bin文件,但是这个bin文件不能直接拿来用,需要在前面添加一段头部信息,这段头部信息也包含了鉴权内容。加入头部信息以后的FSBL代码结构如图所示:

| Name | Length | Byte Offset | Description |
|---|---|---|---|
| Magic number | 32 bits | 0 | 4字节,大端序: 'S', 'T', 'M', 0x32 = 0x53544D32 |
| Image signature | 512 bits | 4 | ECDSA签名,用于镜像鉴权 |
| Image checksum | 32 bits | 68 | 镜像校验和 |
| Header version | 32 bits | 72 | 头部版本信息,V1.0的话为0X00010000,含义: Byte0:保留 Byte1:主版本号为0X01 Byte2:次版本号为0X00 Byte3:保留 |
| Image length | 32 bits | 76 | 镜像长度,不包含头部,单位为字节 |
| Image entry Point | 32 bits | 80 | 镜像入口地址 |
| Reserved1 | 32 bits | 84 | 保留 |
| Load address | 32 bits | 88 | 镜像加载地址,ROM代码不使用此地址 |
| Reserved2 | 32 bits | 92 | 保留 |
| Version number | 32 bits | 96 | 镜像版本信息 |
| Option flags | 32 bits | 100 | 可选字段,b0=1 的话表示不需要验证签名 |
| ECDSA algorithm | 32 bits | 104 | ECDSA算法,1:P-256NIST;2:brainpool 256 |
| ECDSA public key | 512 bits | 108 | ECDSA公共密钥,签名的时候使用 |
| Padding | 83 Bytes | 172 | 保留的填充区域,必须全部为0 |
| Binary type | 1 Byte | 255 | 二进制文件类型: 0X00:U-Boot 0X10-0X1F:TF-A 0X20-0X2F:OPTEE 0X30:Copro |
| 头部信息不需要手动添加,在编译官方提供的TF-A或Uboot的时候会自动添加。 |
2 STM32MP1 Linux系统启动过程
STM32MP1是面向Linux领域的,因此所以的这些启动过程都是为了启动Linux内核。STM32MP1启动Linux内核的流程如图所示:

上图中,STM32MP1启动Linux内核共分为5个步骤:
- ROM代码:ST自己编写的代码,在STM32MP1出厂的时候就已经烧写进去的,不能被修改的。ROM代码因为保存在STM32内部ROM里面,因此也就直接简单明了的叫做"ROM代码"了。处理器上电以后首先执行的程序,ROM代码的主要工作就是读取STM32MP1的BOOT引脚电平,然后根据电平判断当前启动设备,最后从选定的启动设备里面读取FSBL代码,并将FSBL代码放到对应的RAM空间。现在很多产品对设备上运行的应用都提出了安全要求,上图中,STM32MP1启动Linux内核的过程是一个链式结构:ROMCode→FSBL→SSBL→Linuxkernel→rootfs,系统启动的过程中要保证整个链式结构都是安全的。ROM代码作为第一链,首先要对FSBL代码进行鉴权,同样的,FSBL以及后面的每一链都要对下一个阶段的镜像进行鉴权,直到设备系统正确启动;
- FSBL:FSBL代码初始化时钟树、初始化外部RAM控制器,也就是DDR。最终FSBL将SSBL加载到DDR里面并运行SSBL代码。一般FSBL代码是TF-A或者Uboot的SPL代码,也可以将FSBL换成A内核裸机代码;
- SSBL:由于SSBL代码运行在DDR里面,无需担心空间不够,因此SSBL代码的功能就可以做的很全面,比如使能USB、网络、显示等等。这样就可以在SSBL中灵活的加载linux内核,比如从Flash设备上读取,或者通过网络下载等,用户使用起来也非常的友好。SSBL一般是Uboot,用来启动Linux内核;
- Linux内核:SSBL部分的Uboot就一个使命,启动Linux内核,Uboot会将Linux内核加载到DDR上并运行。Linux内核启动过程中会初始化板子上的各种外设;
- Linux用户空间:系统启动的时候会通过init进程切换到用户空间,在这个过程中会初始化根文件系统里面的各种框架以及服务。