2. Bootloader移植(上):u-boot源码结构分析、i.MX8QM启动流程解析、DDR初始化与时钟配置、串口驱动移植与调试
各位同学,大家好。今天我们正式开始Bootloader移植的上半部分。说实话,Bootloader是整个系统启动的“第一棒”,它要是跑不起来,后面Linux内核、Android系统全是空谈。我个人习惯把Bootloader比作“看门大爷”——它得先确认硬件环境没问题,再把大门(内核)打开。
这一节内容比较多,我把它拆成四个核心块:u-boot源码结构、i.MX8QM的启动流程、DDR和时钟配置、串口驱动移植。咱们一个一个啃。
2.1 u-boot源码结构分析
u-boot的源码,说白了就是一个大型的C语言项目。我第一次看u-boot源码时,感觉像进了迷宫。后来摸清了套路,发现它其实很有章法。
顶层目录结构大致如下:
u-boot/
├── arch/ # 架构相关代码(ARM、x86、RISC-V等)
├── board/ # 板级支持包(你的板子在这里)
├── common/ # 通用功能(命令行、环境变量等)
├── drivers/ # 驱动(串口、网卡、MMC等)
├── dts/ # 设备树源文件
├── include/ # 头文件
├── lib/ # 库函数
├── net/ # 网络协议栈
├── scripts/ # 编译脚本
├── tools/ # 工具链
└── configs/ # 默认配置文件
嗯,这里要注意:arch/arm/mach-imx/ 是我们重点关注的地方。i.MX8QM的SoC初始化代码基本都在这里。而 board/freescale/imx8qm_mek/ 则是官方开发板的板级代码,我们移植时通常以它为蓝本。
核心思路:移植Bootloader,不是从零写代码,而是“改配置、调参数、加驱动”。90%的工作量在 board/ 和 configs/ 目录下。
我个人建议,拿到一个芯片的BSP包后,先别急着编译。花半小时把 arch/arm/mach-imx/ 下的Kconfig和Makefile扫一遍,搞清楚哪些文件是必选的。我在项目中遇到过,有人直接复制了别的板子的配置,结果编译出来DDR初始化跑飞了,查了两天才发现是时钟树配置不对。
2.2 i.MX8QM启动流程解析
i.MX8QM的启动流程,比我想象中要复杂一些。它内部有多个处理器核心:4个Cortex-A72、2个Cortex-A53、还有M4和DSP。但Bootloader主要跑在A72上。
整个启动链条是这样的:
- ROM代码:芯片上电后,内部ROM先跑。它检查启动模式引脚(BOOT_MODE),决定从eMMC、SD卡还是USB启动。
- SCU固件:系统控制单元(SCU)的固件先加载。它负责电源管理、时钟、复位等底层操作。
- SPL(Secondary Program Loader):u-boot的第一阶段。它初始化DDR、时钟、串口,然后加载完整的u-boot。
- u-boot proper:完整的u-boot。它加载设备树、内核、ramdisk,然后跳转到内核。
为什么要有SPL?说白了,就是ROM太小了,装不下完整的u-boot。SPL就像个“瘦身版”的u-boot,只做最必要的事。
调试小技巧:SPL阶段如果串口没输出,别急着怀疑驱动。先检查SCU固件有没有加载成功。我曾经有一次,SCU固件烧错了版本,结果SPL死活不跑,串口一点动静都没有。后来用JTAG单步跟踪才发现是SCU没起来。
下面我用一张图来展示这个流程:
2.3 DDR初始化与时钟配置
DDR初始化,是Bootloader移植中最容易出问题的地方。你想想看,DDR没起来,代码都没地方跑。SPL必须在一开始就把DDR搞定。
i.MX8QM的DDR控制器是DDR4/3L兼容的,支持32位和64位总线。初始化流程大致如下:
- 时钟配置:先给DDR控制器提供正确的时钟频率。i.MX8QM的DDR时钟来自PLL,典型频率是1600MHz(DDR4-3200)。
- PHY初始化:DDR PHY负责物理层信号。需要配置阻抗、驱动强度、ODT等参数。
- 时序训练:这是最关键的一步。控制器会发送训练序列,调整读写延迟,确保信号眼图合格。
- MRC(Memory Reference Code):NXP提供了一套DDR初始化代码,放在 board/freescale/common/ddr/ 下。我们一般直接调用。
注意:DDR参数和PCB布局强相关。同样的芯片,换了PCB板厂,DDR时序参数可能都要重新调。我曾经遇到过,同一套代码在两块板子上,一块跑得稳,另一块随机死机。最后发现是PCB走线长度差了几毫米,导致信号时序偏移。
时钟配置方面,i.MX8QM的时钟树比较复杂。它有三个主要PLL:ARM PLL、DDR PLL、GPU PLL。Bootloader里我们主要关心DDR PLL和系统总线时钟。
配置代码通常在 arch/arm/mach-imx/imx8m/clock.c 中。一个典型的时钟配置片段如下:
/* 设置DDR PLL为1600MHz */
void init_ddr_pll(void)
{
/* 1. 旁路PLL */
writel(0x1, &ccm->analog->pll_ddr->ctrl);
/* 2. 设置分频系数 */
writel(0x64, &ccm->analog->pll_ddr->num); /* 100 */
writel(0x1, &ccm->analog->pll_ddr->denom); /* 1 */
/* 3. 锁定PLL */
writel(0x0, &ccm->analog->pll_ddr->ctrl);
while (!(readl(&ccm->analog->pll_ddr->ctrl) & 0x1));
printf("DDR PLL locked at 1600MHz\n");
}
嗯,这里要注意:不同版本的BSP,寄存器地址可能不一样。我建议你对照芯片参考手册(RM)来确认偏移量。别完全相信网上抄来的代码。
2.4 串口驱动移植与调试
串口是Bootloader的“生命线”。没有串口输出,你根本不知道代码跑到哪了。所以移植的第一步,往往是让串口能打印东西。
i.MX8QM的串口是UART IP,兼容16550。驱动代码在 drivers/serial/ns16550.c。移植时主要做三件事:
- 配置引脚复用:在设备树或板级代码中,把UART的TX/RX引脚配置为UART功能。
- 设置时钟:给UART模块提供正确的时钟源。i.MX8QM的UART时钟通常来自OSC 24MHz。
- 初始化寄存器:设置波特率、数据位、停止位等参数。
设备树中串口节点的典型配置:
&uart1 {
compatible = "fsl,imx8qm-uart", "ns16550";
reg = <0x5a060000 0x1000>;
interrupts = <0 26 4>;
clocks = <&clk IMX8QM_UART1_CLK>;
status = "okay";
};
调试经验:如果串口没输出,先别怀疑驱动代码。用示波器量一下TX引脚的波形。如果有方波,说明硬件在发数据,可能是终端软件配置不对(波特率、校验位)。如果没波形,再查时钟和引脚复用。我遇到过最奇葩的一次,是板子上TX和RX焊反了,折腾了半天。
串口输出正常后,你会在终端看到类似这样的信息:
U-Boot SPL 2020.04 (Jan 12 2025 - 10:30:00 +0800)
Normal Boot
Trying to boot from MMC1
看到这个输出,说明SPL已经跑起来了。接下来就可以继续移植其他外设了。
小结
这一节我们覆盖了u-boot源码结构、i.MX8QM的启动流程、DDR和时钟配置、串口驱动移植。说白了,Bootloader移植的核心就是“让芯片先跑起来,再让串口能说话”。
我个人觉得,串口调试是最值得花时间的地方。它就像你的眼睛,没有它,后面的工作根本没法做。所以,如果你在移植过程中遇到问题,先从串口入手,确保它能打印出信息。
好了,这一节就到这里。下一节我们会继续深入Bootloader移植的下半部分,包括eMMC驱动、网络驱动、以及如何把u-boot烧写到板子上。