启动流程第二阶段:SPL/MLO加载、DDR初始化、时钟与PLL配置、串口调试输出

好,咱们接着往下聊。上一章我们把ROM Code那点事儿讲清楚了,说白了就是BootROM把SPL从SD卡或者NAND里搬到了内部SRAM里。那接下来呢?SPL开始干活了。

这一阶段,我个人认为是整个Bootloader里最"刺激"的一段。为什么?因为你要在DDR还没初始化的情况下,用那点可怜的SRAM空间,完成时钟配置、PLL锁定、DDR训练……任何一个环节出问题,板子就是一块砖。我在项目中遇到过好几次,DDR参数配错了,串口打印到一半就卡死,连个错误信息都看不到。

SPL/MLO到底是什么?

SPL,全称是Secondary Program Loader。在TI的平台上,它也叫MLO。名字不同,干的活一样——它是ROM Code和U-Boot之间的"桥梁"。

你想想看,ROM Code能做的事情非常有限。它不认识DDR,不认识复杂的文件系统,甚至不认识U-Boot的镜像格式。它只认识一种东西:经过特殊签名的、固定格式的二进制文件。这个文件就是SPL。

SPL的大小通常被限制在几十KB到一百多KB。为什么?因为内部SRAM就那么点大。以我常用的AM335x为例,SRAM只有64KB,扣掉ROM Code占用的部分,留给SPL的空间大概在109KB左右。嗯,就是这么抠门。

关键点:SPL的使命就是"初始化最小系统,然后加载U-Boot到DDR"。它不会做多余的事,比如网络启动、复杂的命令行交互——那些是U-Boot的事。

DDR初始化——最头疼的一步

DDR初始化,说白了就是让CPU和DDR芯片"对上话"。这一步要是搞不定,后面全白搭。

DDR初始化的流程大致是这样的:

  1. 配置DDR控制器——告诉控制器DDR的类型、位宽、频率
  2. 设置时序参数——比如tRCD、tCL、tRP这些值
  3. 执行DDR训练——调整读写延迟,确保信号质量
  4. 启动DDR自刷新——让DDR进入正常工作状态

我给你们看一段典型的DDR初始化代码,这是我在AM335x上实际用过的:

/* DDR3时序配置 - 以MT41J128M16为例 */
void ddr_init(void)
{
    /* 1. 使能DDR时钟 */
    writel(0x00000002, &cm_device_cm->clkstctrl);
    
    /* 2. 配置DDR控制器寄存器 */
    writel(0x00000800, &ddr_ctrl->sdram_config);  /* 32位数据总线 */
    writel(0x00000001, &ddr_ctrl->sdram_tim1);    /* tRCD = 1 */
    writel(0x00000001, &ddr_ctrl->sdram_tim2);    /* tRP = 1 */
    writel(0x00000001, &ddr_ctrl->sdram_tim3);    /* tWR = 1 */
    
    /* 3. 执行DDR2/DDR3初始化序列 */
    writel(0x00000000, &ddr_ctrl->sdram_init);
    udelay(200);  /* 等待200us */
    
    /* 4. 发送ZQ校准命令 */
    writel(0x00000001, &ddr_ctrl->sdram_init);
    udelay(1000); /* 等待ZQ校准完成 */
    
    /* 5. 检查DDR是否就绪 */
    while(!(readl(&ddr_ctrl->sdram_sts) & 0x01));
    
    printf("DDR初始化完成!\n");
}

注意:DDR时序参数不能随便抄。不同厂商、不同型号的DDR芯片,时序参数可能完全不同。我曾经因为抄了参考设计里的参数,结果板子在高温下频繁死机。后来老老实实按照DDR芯片的数据手册重新配了一遍,问题才解决。

时钟与PLL配置——心脏起搏

时钟系统,就是整个SoC的心脏。PLL(锁相环)负责把外部晶振的低频信号,倍频成CPU、DDR、外设需要的高频时钟。

以AM335x为例,外部晶振通常是24MHz或25MHz。通过PLL,我们可以得到:

时钟域 目标频率 PLL倍频系数
MPU (CPU) 600MHz ~ 1GHz 25 ~ 40
DDR 303MHz ~ 400MHz 12 ~ 16
L3/L4总线 100MHz ~ 200MHz 4 ~ 8

配置PLL的步骤其实不复杂:

  1. 先把PLL置于旁路模式(Bypass)
  2. 设置倍频系数(M、N、M2)
  3. 等待PLL锁定(Lock)
  4. 切换到PLL输出模式

我给你们看一段PLL配置的代码:

/* 配置MPU PLL,目标频率600MHz */
void pll_init(void)
{
    /* 1. 进入旁路模式 */
    writel(0x00000000, &cm_mpu_pll->clkmode);
    
    /* 2. 设置倍频系数 */
    /* Fdco = 2 * M * Fin / (N + 1) */
    /* 假设Fin = 24MHz, M = 25, N = 1 */
    /* Fdco = 2 * 25 * 24 / 2 = 600MHz */
    writel(0x00000019, &cm_mpu_pll->clksel_m);   /* M = 25 */
    writel(0x00000001, &cm_mpu_pll->clksel_n);   /* N = 1 */
    writel(0x00000001, &cm_mpu_pll->clksel_m2);  /* M2 = 1 */
    
    /* 3. 等待PLL锁定 */
    writel(0x00000001, &cm_mpu_pll->clkmode);    /* 进入锁定模式 */
    while(!(readl(&cm_mpu_pll->clkmode) & 0x01)); /* 等待锁定 */
    
    /* 4. 切换到PLL输出 */
    writel(0x00000002, &cm_mpu_pll->clkmode);
    
    printf("MPU PLL已锁定,频率:%d MHz\n", 600);
}

小技巧:配置PLL时,建议先配置DDR PLL,再配置MPU PLL。因为DDR PLL锁定后,DDR才能工作,而串口打印需要DDR吗?不需要。但后续的U-Boot加载需要DDR。所以顺序很重要。

串口调试输出——你的"眼睛"

串口调试输出,说白了就是Bootloader的"眼睛"。没有它,你根本不知道代码跑到哪一步了,也不知道哪里出了问题。

串口初始化的流程很简单:

  1. 配置GPIO复用为UART功能
  2. 设置波特率(通常是115200)
  3. 配置数据位、停止位、校验位
  4. 使能发送和接收

我给你们看一段串口初始化的代码:

/* 串口初始化 - 以UART0为例 */
void uart_init(void)
{
    /* 1. 使能UART0时钟 */
    writel(0x00000002, &cm_per->clkstctrl_uart0);
    
    /* 2. 配置GPIO复用为UART功能 */
    writel(0x00000001, &control_module->conf_uart0_rxd);
    writel(0x00000001, &control_module->conf_uart0_txd);
    
    /* 3. 设置波特率115200 */
    /* 假设UART时钟为48MHz */
    /* 分频系数 = 48000000 / (16 * 115200) = 26.04 */
    writel(0x0000001A, &uart0->divisor_latch_low);   /* 26 */
    writel(0x00000000, &uart0->divisor_latch_high);  /* 0 */
    
    /* 4. 配置数据格式:8位数据,1位停止,无校验 */
    writel(0x00000003, &uart0->line_control);
    
    /* 5. 使能FIFO */
    writel(0x00000001, &uart0->fifo_control);
    
    printf("串口初始化完成!\n");
}

经验之谈:串口输出是调试Bootloader最有效的手段。我建议你在每个关键步骤后都加一句打印信息。比如:

  • "PLL配置开始..."
  • "PLL锁定成功!"
  • "DDR初始化开始..."
  • "DDR初始化完成!"
  • "正在加载U-Boot..."

这样,如果板子卡住了,你一眼就能看出是哪个环节出了问题。

避坑指南

做Bootloader开发,坑是真的多。我给你们总结几个我踩过的:

  • DDR时序参数别乱抄——不同DDR芯片的参数不一样,一定要看数据手册
  • PLL锁定时间要留够——有些PLL需要几百微秒才能锁定,别急着往下走
  • 串口波特率要算准——分频系数算错了,打印出来的全是乱码
  • SRAM空间要省着用——SPL就那么点空间,别把全局变量定义太多

嗯,这一章的内容就到这里。下一章我们会讲U-Boot的加载和启动,那才是真正的大戏。到时候我会详细讲讲U-Boot是怎么从DDR里把自己解压出来的,以及它又是怎么把内核加载到内存里的。

记住一句话:Bootloader的每一行代码,都是在和硬件"对话"。你写对了,它就听你的;你写错了,它就给你脸色看。