第2章:目标板硬件分析与启动流程

做BSP移植,第一件事不是写代码,而是读文档。我见过太多人上来就对着开发板敲命令,结果连时钟配错了都不知道。这一章,咱们就聊聊怎么把SoC数据手册吃透,把启动流程理清。

2.1 SoC数据手册解读——别被上千页吓住

一块典型的嵌入式SoC数据手册,少说七八百页,多的能到三四千。你不可能全读完,也没必要。我个人的习惯是:先抓三个核心章节。

章节 为什么重要 重点关注
Memory Map 知道外设寄存器在哪个地址 Boot ROM、SRAM、外设基地址
Clock & Reset 芯片怎么跑起来的 PLL配置、时钟源选择、复位序列
Boot Mode 上电后CPU执行的第一条指令 启动设备选择、启动顺序
我的小技巧: 拿到手册先翻到Memory Map那页,截图保存。调试时90%的问题都能在这张图上找到线索。

举个例子,我去年移植一个i.MX6ULL的BSP,发现串口死活不输出。查了半天,原来是UART1的基地址我看成了UART2的。嗯,这种低级错误,一张Memory Map就能避免。

2.2 时钟树分析——芯片的“心跳”

时钟树,说白了就是芯片的脉搏。你想想看,CPU要跑、外设要工作、总线要传输,哪个离得开时钟?

典型的ARM SoC时钟树长这样:

典型ARM SoC时钟树结构 外部晶振 (24MHz) PLL (倍频至~1GHz) CPU Core (1GHz) AHB/APB总线 (200MHz) 外设时钟 (UART/SPI/I2C) 分频器 (/1, /2, /4...) 分频器 (/1, /2, /4...) 💡 调试要点: • 先确认晶振起振,再用示波器测PLL输出 • 分频系数配错会导致外设无法工作,这是最常见的坑 • 我习惯在boot阶段把每个时钟源都打印出来,方便定位

时钟配置的坑,我踩过不少。有一次在STM32MP1上做移植,DDR初始化总失败。折腾了两天,最后发现是PLL2的VCO频率超出了手册范围。说白了,就是没仔细看时钟树的约束条件。

⚠️ 避坑指南: 配置PLL时,一定要算好VCO范围、分频系数、输出频率三者关系。我曾经因为少看了一个“/2”的分频器,导致CPU跑在标称频率的两倍,板子直接冒烟。

2.3 典型ARM/Linux启动流程

ARM Linux的启动,说白了就是一场接力赛。从ROM代码到Bootloader,再到内核,最后到根文件系统,每一棒都不能掉。

2.3.1 启动阶段总览

阶段 执行位置 主要任务 典型耗时
ROM Code 芯片内部ROM 初始化时钟、检测启动模式、加载SPL ~50ms
SPL (Secondary Program Loader) 内部SRAM 初始化DDR、加载U-Boot ~100ms
U-Boot DDR 初始化外设、加载内核和设备树 ~500ms
Kernel DDR 解压、初始化子系统、挂载根文件系统 ~1-3s
Init进程 DDR 启动用户空间服务 ~1-5s

2.3.2 ROM Code阶段——芯片的“本能”

上电瞬间,CPU从复位向量取第一条指令。这个地址通常是0x00000000或者0xFFFF0000,具体看芯片设计。ROM Code是芯片出厂固化的,你改不了,但必须知道它干了什么。

我个人的经验是:先查Boot Mode引脚的电平配置。很多芯片通过几个GPIO的上下拉来决定从哪启动——SD卡、NAND Flash、SPI NOR,还是USB下载模式。

关键点: ROM Code会初始化最基本的时钟(通常是内部RC振荡器),然后根据Boot Mode配置,从启动设备读取SPL到内部SRAM。SRAM一般只有几十到几百KB,所以SPL必须足够小。

2.3.3 SPL阶段——从“小”到“大”的跳板

SPL存在的意义很简单:内部SRAM太小,装不下完整的U-Boot。所以SPL只做两件事:初始化DDR,然后把U-Boot从启动设备拷贝到DDR。

这里有个容易忽略的细节:DDR初始化参数。每个板子的DDR型号、布线长度、时序参数都不一样。我遇到过最头疼的问题,就是SPL加载U-Boot时CRC校验失败。查到最后,发现是DDR的刷新周期配错了,导致数据在拷贝过程中出错。

// SPL中DDR初始化的典型流程(伪代码)
void spl_ddr_init(void) {
    // 1. 配置DDR控制器时钟
    ddr_set_clock(DDR_CLK_400MHZ);
    
    // 2. 设置时序参数(来自DDR芯片数据手册)
    ddr_set_timing(T_RCD, 12);   // RAS到CAS延迟
    ddr_set_timing(T_RP, 12);    // 预充电时间
    ddr_set_timing(T_RAS, 28);   // 行有效时间
    
    // 3. 校准DDR(ZQ校准)
    ddr_zq_calibration();
    
    // 4. 使能DDR控制器
    ddr_enable();
}

2.3.4 U-Boot阶段——真正的“管家”

U-Boot启动后,会做几件关键的事:

  • 初始化串口——让你能看到打印信息。没有这一步,调试就是瞎子摸象。
  • 加载设备树——告诉内核硬件长什么样。
  • 加载内核镜像——从Flash、网络或SD卡读到DDR。
  • 传递启动参数——比如根文件系统在哪、控制台用哪个串口。

嗯,这里要特别提一下设备树。我刚开始做移植时,总觉得设备树就是个配置文件,随便改改就行。直到有一次,我把GPIO的中断控制器配错了,内核启动到一半直接panic。从那以后,我每改一个设备树节点,都会用dtc工具反编译检查一遍。

2.3.5 Kernel启动——从汇编到C的“蜕变”

内核启动分两个阶段:

  1. 汇编阶段:设置页表、使能MMU、切换到C运行环境。这段代码在arch/arm/kernel/head.S里,我建议你至少读一遍。
  2. C阶段:start_kernel()函数开始,初始化调度器、内存管理、中断系统、驱动模型……最后挂载根文件系统,启动init进程。
调试心得: 内核启动卡住时,我习惯在start_kernel()里加early_printk。从打印信息的位置,基本能判断是哪个子系统出了问题。比如卡在setup_arch(),多半是设备树或内存映射的问题。

2.4 启动流程的调试手段

说白了,启动调试就是“看灯”和“看串口”。但具体怎么用,有讲究。

调试手段 适用阶段 优点 缺点
LED闪烁 ROM Code、SPL 不需要任何外设初始化 信息量有限
串口打印 SPL、U-Boot、Kernel 信息丰富、可定位问题 需要初始化UART
JTAG调试 全阶段 单步执行、查看寄存器 需要调试器硬件
逻辑分析仪 硬件初始化阶段 观察时序、信号完整性 设备昂贵、操作复杂
⚠️ 我曾经犯过的错: 在SPL阶段用串口打印调试信息,结果串口初始化本身就有bug,导致打印出来的全是乱码。后来我改用GPIO拉高拉低的方式,用示波器看波形,才定位到问题。所以,调试手段要分阶段选,别一根筋。