第2章:目标板硬件分析与启动流程
做BSP移植,第一件事不是写代码,而是读文档。我见过太多人上来就对着开发板敲命令,结果连时钟配错了都不知道。这一章,咱们就聊聊怎么把SoC数据手册吃透,把启动流程理清。
2.1 SoC数据手册解读——别被上千页吓住
一块典型的嵌入式SoC数据手册,少说七八百页,多的能到三四千。你不可能全读完,也没必要。我个人的习惯是:先抓三个核心章节。
| 章节 | 为什么重要 | 重点关注 |
|---|---|---|
| Memory Map | 知道外设寄存器在哪个地址 | Boot ROM、SRAM、外设基地址 |
| Clock & Reset | 芯片怎么跑起来的 | PLL配置、时钟源选择、复位序列 |
| Boot Mode | 上电后CPU执行的第一条指令 | 启动设备选择、启动顺序 |
举个例子,我去年移植一个i.MX6ULL的BSP,发现串口死活不输出。查了半天,原来是UART1的基地址我看成了UART2的。嗯,这种低级错误,一张Memory Map就能避免。
2.2 时钟树分析——芯片的“心跳”
时钟树,说白了就是芯片的脉搏。你想想看,CPU要跑、外设要工作、总线要传输,哪个离得开时钟?
典型的ARM SoC时钟树长这样:
时钟配置的坑,我踩过不少。有一次在STM32MP1上做移植,DDR初始化总失败。折腾了两天,最后发现是PLL2的VCO频率超出了手册范围。说白了,就是没仔细看时钟树的约束条件。
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下载模式。
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的“蜕变”
内核启动分两个阶段:
- 汇编阶段:设置页表、使能MMU、切换到C运行环境。这段代码在
arch/arm/kernel/head.S里,我建议你至少读一遍。 - C阶段:start_kernel()函数开始,初始化调度器、内存管理、中断系统、驱动模型……最后挂载根文件系统,启动init进程。
start_kernel()里加early_printk。从打印信息的位置,基本能判断是哪个子系统出了问题。比如卡在setup_arch(),多半是设备树或内存映射的问题。
2.4 启动流程的调试手段
说白了,启动调试就是“看灯”和“看串口”。但具体怎么用,有讲究。
| 调试手段 | 适用阶段 | 优点 | 缺点 |
|---|---|---|---|
| LED闪烁 | ROM Code、SPL | 不需要任何外设初始化 | 信息量有限 |
| 串口打印 | SPL、U-Boot、Kernel | 信息丰富、可定位问题 | 需要初始化UART |
| JTAG调试 | 全阶段 | 单步执行、查看寄存器 | 需要调试器硬件 |
| 逻辑分析仪 | 硬件初始化阶段 | 观察时序、信号完整性 | 设备昂贵、操作复杂 |