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上。

整个启动链条是这样的:

  1. ROM代码:芯片上电后,内部ROM先跑。它检查启动模式引脚(BOOT_MODE),决定从eMMC、SD卡还是USB启动。
  2. SCU固件:系统控制单元(SCU)的固件先加载。它负责电源管理、时钟、复位等底层操作。
  3. SPL(Secondary Program Loader):u-boot的第一阶段。它初始化DDR、时钟、串口,然后加载完整的u-boot。
  4. u-boot proper:完整的u-boot。它加载设备树、内核、ramdisk,然后跳转到内核。

为什么要有SPL?说白了,就是ROM太小了,装不下完整的u-boot。SPL就像个“瘦身版”的u-boot,只做最必要的事。

调试小技巧:SPL阶段如果串口没输出,别急着怀疑驱动。先检查SCU固件有没有加载成功。我曾经有一次,SCU固件烧错了版本,结果SPL死活不跑,串口一点动静都没有。后来用JTAG单步跟踪才发现是SCU没起来。

下面我用一张图来展示这个流程:

i.MX8QM 启动流程 1. ROM代码 2. SCU固件 3. SPL 4. u-boot proper 5. Linux内核 检查启动模式 电源/时钟/复位 DDR/串口初始化 加载内核/设备树 启动Android

2.3 DDR初始化与时钟配置

DDR初始化,是Bootloader移植中最容易出问题的地方。你想想看,DDR没起来,代码都没地方跑。SPL必须在一开始就把DDR搞定。

i.MX8QM的DDR控制器是DDR4/3L兼容的,支持32位和64位总线。初始化流程大致如下:

  1. 时钟配置:先给DDR控制器提供正确的时钟频率。i.MX8QM的DDR时钟来自PLL,典型频率是1600MHz(DDR4-3200)。
  2. PHY初始化:DDR PHY负责物理层信号。需要配置阻抗、驱动强度、ODT等参数。
  3. 时序训练:这是最关键的一步。控制器会发送训练序列,调整读写延迟,确保信号眼图合格。
  4. 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。移植时主要做三件事:

  1. 配置引脚复用:在设备树或板级代码中,把UART的TX/RX引脚配置为UART功能。
  2. 设置时钟:给UART模块提供正确的时钟源。i.MX8QM的UART时钟通常来自OSC 24MHz。
  3. 初始化寄存器:设置波特率、数据位、停止位等参数。

设备树中串口节点的典型配置:

&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烧写到板子上。


公众号:蓝海资料掘金营,微信deep3321