第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内核运行区域
我的习惯:拿到新芯片,我会把Memory Map打印出来贴在工位上。写链接脚本、配置MMU、调试DMA时,随时扫一眼,能省下大量翻手册的时间。

这里有个容易踩的坑: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、分频器和后分频
  • 时钟门控:每个外设都有自己的时钟使能位,不使能就没法访问寄存器

我个人习惯先确定一个“最小系统时钟方案”:

  1. 用HSE作为主时钟源(24MHz)
  2. 配置PLL1输出650MHz给A7
  3. 配置PLL2输出533MHz给DDR
  4. 配置PLL3输出200MHz给M4和外设总线
  5. 确保所有用到的外设时钟门控都已打开
注意:STM32MP1的DDR初始化必须在PLL2稳定之后才能进行。如果你在SPL里先配DDR再配PLL,DDR控制器会直接挂掉。这个顺序我在项目里踩过,板子直接黑屏,查了两天才找到原因。

时钟配置的代码通常在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
避坑指南:我曾经在SD卡启动时遇到SPL加载失败,查了半天发现是SD卡的分区表把扇区0占用了。Boot ROM读扇区1时发现数据不对,直接卡死。解决方案是:把SPL放在扇区1-16,并且确保扇区0是合法的MBR(即使不用也要占位)。

2.4 上电启动流程:从BootROM到U-Boot

好了,现在我们把前面三块知识串起来,看看芯片上电后到底发生了什么。我画了一张流程图,帮你理清这个链路。

STM32MP1 上电启动流程 上电复位 BootROM:初始化CPU & 时钟 检测BOOT0/BOOT1引脚 从启动设备加载SPL SPL:初始化DDR & 时钟 从启动设备加载U-Boot U-Boot:加载Linux内核 说明:SPL运行在SYSRAM(0x1000_0000),U-Boot运行在DDR(0xC000_0000)

这个流程看起来简单,但每个环节都有讲究。我拆开来说。

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的入口
注意:SPL本身不能太大。STM32MP1的SYSRAM只有256KB,SPL代码加上堆栈、全局变量,必须控制在200KB以内。我一般会开启编译优化(-Os),并且把不必要的驱动(比如网络、USB)都去掉,只保留串口、时钟、DDR和存储设备驱动。

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开发里尤其适用。