第2章:芯片手册精读与启动流程分析:以STM32MP1为例
做BSP移植,第一件事不是写代码,而是读芯片手册。我见过不少工程师上来就翻参考代码,结果遇到启动卡死、外设初始化失败,折腾好几天才发现是时钟配置错了。说白了,手册就是芯片的“使用说明书”,你跳过的每一个细节,后面都会变成坑。
这一章,我们拿STM32MP1这颗典型的多核异构芯片开刀。我会带着你精读Memory Map、Clock Tree和Boot ROM这几个关键章节,然后顺着上电启动的完整链路——从BootROM到SPL再到U-Boot——把整个流程捋清楚。
2.1 Memory Map:芯片的“地址地图”
拿到手册,我习惯先看Memory Map。为什么?因为你要知道CPU能看到哪些地址,哪些是内存、哪些是外设寄存器、哪些是保留区域。搞错了地址,代码跑飞是迟早的事。
STM32MP1是双核架构:一个Cortex-A7(跑Linux)和一个Cortex-M4(跑裸机或RTOS)。两个核看到的地址空间不完全一样,但共享一套外设总线。手册里给出了完整的系统地址映射,我挑几个关键区域说一下。
| 地址范围 | 大小 | 用途 | 备注 |
|---|---|---|---|
| 0x0000_0000 - 0x0FFF_FFFF | 256 MB | Boot ROM / 内部SRAM / 外部NOR Flash映射区 | 上电默认映射,BootROM在这里 |
| 0x1000_0000 - 0x1FFF_FFFF | 256 MB | SYSRAM(内部系统SRAM) | SPL通常在这里运行 |
| 0x2000_0000 - 0x3FFF_FFFF | 512 MB | AXI总线外设(DDR控制器、GPU、DMA等) | DDR初始化后才能访问 |
| 0x4000_0000 - 0x5FFF_FFFF | 512 MB | APB/AHB低速外设(UART、I2C、SPI、GPIO等) | 大部分外设寄存器在这里 |
| 0xC000_0000 - 0xFFFF_FFFF | 1 GB | DDR内存空间(由DDR控制器映射) | U-Boot和Linux内核运行区域 |
这里有个容易踩的坑:STM32MP1的BootROM位于0x0000_0000,但上电后CPU会从那里取指令。而内部SYSRAM在0x1000_0000,两者不是同一个物理区域。我曾经见过有人把SPL的链接地址写成了0x0000_0000,结果SPL死活跑不起来——因为那个地址是只读的BootROM,你写不进去。
2.2 Clock Tree:芯片的“心跳”
时钟树是BSP移植中最容易出问题的部分,没有之一。STM32MP1的时钟树相当复杂,它需要同时满足Cortex-A7、Cortex-M4、DDR、外设总线等多个时钟域的需求。
手册里有一张巨大的时钟树图,我建议你把它拆成三块来看:
- 时钟源:HSE(外部高速晶振,通常是24MHz)、HSI(内部RC振荡器,64MHz)、LSE(外部低速晶振,32.768kHz)、LSI(内部低速RC,约32kHz)
- PLL链:PLL1给A7核用,PLL2给DDR和AXI总线用,PLL3给M4和外设用。每个PLL都有独立的VCO、分频器和后分频
- 时钟门控:每个外设都有自己的时钟使能位,不使能就没法访问寄存器
我个人习惯先确定一个“最小系统时钟方案”:
- 用HSE作为主时钟源(24MHz)
- 配置PLL1输出650MHz给A7
- 配置PLL2输出533MHz给DDR
- 配置PLL3输出200MHz给M4和外设总线
- 确保所有用到的外设时钟门控都已打开
时钟配置的代码通常在SPL的board_init_f()里完成。我贴一段典型的PLL配置流程(伪代码):
void sysclk_init(void)
{
// 1. 使能HSE振荡器
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY));
// 2. 配置PLL1:24MHz输入,倍频到650MHz
RCC->PLL1CFGR = (24 << RCC_PLL1CFGR_DIVM_Pos) | // 预分频
(650 << RCC_PLL1CFGR_DIVN_Pos); // 倍频
RCC->PLL1CFGR |= RCC_PLL1CFGR_PLL1EN;
while (!(RCC->CR & RCC_CR_PLL1RDY));
// 3. 切换系统时钟到PLL1
RCC->CFGR = (RCC->CFGR & ~RCC_CFGR_SW) | RCC_CFGR_SW_PLL1;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL1);
}
关键点:每次切换时钟源之前,必须等待目标时钟源稳定(RDY标志位置1)。跳过这个等待,系统会直接死机。我在调试时习惯用逻辑分析仪抓CLK输出脚,肉眼确认频率对不对。
2.3 Boot ROM:芯片的“第一口饭”
Boot ROM是芯片出厂时固化的一段只读代码,上电后CPU第一条指令就从这里开始。STM32MP1的Boot ROM大约128KB,主要做三件事:
- 初始化最基本的硬件(CPU、堆栈、时钟源)
- 检测启动模式引脚(BOOT0、BOOT1)
- 从选定的启动设备加载SPL到SYSRAM,然后跳转
启动模式的选择逻辑是这样的:
| BOOT0 | BOOT1 | 启动设备 | 说明 |
|---|---|---|---|
| 0 | 0 | NOR Flash(FMC接口) | 并行NOR,常用于调试 |
| 0 | 1 | NAND Flash(FMC接口) | 大容量存储,但坏块管理复杂 |
| 1 | 0 | SD卡 / eMMC(SDMMC接口) | 最常用的启动方式 |
| 1 | 1 | USB / UART(串行下载) | 用于烧录或恢复 |
Boot ROM加载SPL时,会按照固定的扇区位置去读。以SD卡为例:
- 扇区0:主引导记录(MBR)或GPT头
- 扇区1-16:FSBL(即SPL),最大128KB
- 扇区17-32:SSBL(即U-Boot),最大128KB
2.4 上电启动流程:从BootROM到U-Boot
好了,现在我们把前面三块知识串起来,看看芯片上电后到底发生了什么。我画了一张流程图,帮你理清这个链路。
这个流程看起来简单,但每个环节都有讲究。我拆开来说。
2.4.1 BootROM阶段
上电后,Cortex-A7核直接从0x0000_0000开始执行。BootROM会先设置CPU为SVC模式、关中断、初始化堆栈指针。然后读取BOOT0和BOOT1引脚的电平,决定从哪个设备加载SPL。
这里有个细节:BootROM会尝试从启动设备的前几个扇区读取“头部信息”(TOC结构),里面包含了SPL的偏移、大小和校验值。如果头部校验失败,BootROM会尝试下一个启动设备(如果配置了fallback)。
2.4.2 SPL阶段
SPL被加载到SYSRAM后,BootROM会跳转到SPL的入口地址。SPL的任务很明确:
- 初始化时钟树(PLL1、PLL2、PLL3)
- 初始化DDR控制器(配置时序、刷新率等)
- 初始化启动设备(SD卡、NAND等)
- 从启动设备加载U-Boot到DDR
- 跳转到U-Boot的入口
2.4.3 U-Boot阶段
U-Boot被加载到DDR后,SPL会跳转过去。U-Boot会做更完整的初始化:
- 重新配置时钟树(可以切换到更高性能的模式)
- 初始化更多外设(网络、USB、显示等)
- 解析设备树(DTB),根据硬件配置驱动
- 加载Linux内核(从存储设备或网络)
- 跳转到内核入口
嗯,这里要注意:U-Boot的链接地址必须和DDR的映射地址一致。如果链接脚本里写的地址是0xC000_0000,但DDR实际映射到了0xC100_0000,那U-Boot一运行就崩。我在第一次移植时犯过这个错,调试串口打印了几行乱码就死机了,后来用JTAG单步跟踪才发现是地址没对上。
2.5 本章小结
这一章我们干了三件事:读Memory Map、啃Clock Tree、理清启动流程。说白了,BSP移植的第一步就是“看懂芯片怎么活过来”。从BootROM到SPL再到U-Boot,每一级都有明确的职责和约束。你不需要记住所有寄存器,但必须知道每个阶段该做什么、不该做什么。
我个人觉得,启动流程是BSP移植中最容易出bug的地方,也是调试最困难的地方——因为串口还没初始化,你连printf都用不了。所以,我的建议是:在写代码之前,先把手册里的启动时序图看懂,把每个阶段的地址、时钟、外设依赖关系画出来。磨刀不误砍柴工,这句话在BSP开发里尤其适用。