第1章:Bootloader移植——U-Boot启动流程分析

做嵌入式Linux开发,Bootloader是第一道坎。

我记得刚入行那会儿,带我的师傅就说:「搞不定U-Boot,后面全是空中楼阁。」当时不以为然,直到自己亲手把一块板子搞成砖头……嗯,那次经历让我老老实实把U-Boot从头啃了一遍。

今天咱们就聊聊U-Boot的启动流程、板级配置怎么改,以及DDR、Timer、串口这些底层驱动怎么移植。说白了,就是让你拿到一块新板子,能快速让U-Boot跑起来。

1.1 U-Boot启动流程:从复位到命令行

U-Boot的启动,分两个阶段。

第一阶段是汇编写的,跟CPU架构强相关。第二阶段是C语言,可移植性好。我习惯把这两个阶段叫做「硬起」和「软起」。

第一阶段:汇编启动(SPL/TPL)

芯片上电后,CPU从复位向量地址开始执行。通常是0x00000000或者0xFFFF0000。这时候DDR还没初始化,代码只能在SRAM或者内部ROM里跑。

主要干这几件事:

  • 设置CPU为SVC模式,关中断
  • 初始化时钟、关MMU和Cache
  • 初始化串口(方便打印调试信息)
  • 初始化DDR控制器
  • 把U-Boot主体从Flash拷贝到DDR
  • 跳转到第二阶段入口

关键点:SPL(Secondary Program Loader)就是第一阶段的产物。很多新式芯片(比如i.MX6、AM335x)都要求用SPL,因为内部SRAM太小,装不下完整U-Boot。

代码入口一般在 arch/arm/cpu/armv7/start.S。我贴一段关键流程:

reset:
    /* 设置SVC模式,关中断 */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0xd3
    msr cpsr, r0

    /* 关MMU和Cache */
    mrc p15, 0, r0, c1, c0, 0
    bic r0, r0, #0x0005
    mcr p15, 0, r0, c1, c0, 0

    /* 初始化板级基本配置 */
    bl board_init_f

这里有个坑——board_init_f 执行时,DDR还没好。所以这个函数里不能定义全局变量,也不能用栈太深。我曾经在这上面栽过跟头,全局变量一多,直接死机。

第二阶段:C语言启动

DDR初始化完成后,U-Boot把自己从Flash搬到DDR里。然后跳转到 board_init_r,这才是真正的C语言世界。

第二阶段做的事:

  1. 初始化剩余外设(网卡、I2C、SPI、MMC等)
  2. 设置环境变量(bootargs、bootcmd)
  3. 初始化命令行终端
  4. 进入主循环,等待用户输入

你想想看,从复位到出现「U-Boot>」提示符,中间经历了多少步骤?每一步出错,板子就变砖。所以调试U-Boot,串口打印是命根子。

1.2 板级配置修改:让你的板子被U-Boot认识

拿到一块新板子,U-Boot不认识它。你得告诉U-Boot:你的CPU是什么、DDR多大、时钟多少、GPIO怎么接。

这些信息放在哪里?在 board/include/configs/ 目录下。

1.2.1 创建板级目录

我一般这么干:

# 复制参考板配置
cp -r board/vendor/common board/vendor/myboard
# 修改目录名和文件
mv board/vendor/common board/vendor/myboard
# 修改Makefile,把目标名改成myboard

然后修改 include/configs/myboard.h。这个头文件是板级配置的核心。里面定义了:

  • DDR大小和时序
  • 时钟频率
  • 串口基地址
  • 环境变量分区位置
  • 网络配置

我的习惯:先找一个跟你的板子最接近的参考板。比如用i.MX6ULL,就参考 mx6ull_14x14_evk。改配置比从头写快10倍。

1.2.2 关键配置项

配置宏 说明 典型值
CONFIG_SYS_TEXT_BASE U-Boot在DDR中的加载地址 0x87800000
CONFIG_SYS_SDRAM_BASE DDR基地址 0x80000000
CONFIG_SYS_INIT_SP_OFFSET 栈指针偏移 0x400000
CONFIG_CONS_INDEX 调试串口号 1(UART1)

改完配置后,执行 make myboard_defconfig 生成 .config 文件。然后 make 编译。如果编译通过,恭喜你,至少语法没问题。

1.3 DDR驱动移植:最头疼的一步

DDR初始化是Bootloader移植里最坑的环节。没有之一。

为什么?因为DDR时序参数一错,板子直接死。而且死得悄无声息——串口都没初始化,你连错误信息都看不到。

1.3.1 DDR初始化流程

  1. 配置DDR控制器(比如i.MX6的MMDC)
  2. 设置时序参数(CAS、RAS、tRCD、tRP等)
  3. 校准DDR(写电平、读DQS门控)
  4. 启动DDR自刷新

代码一般在 board/vendor/myboard/ddr.c 里。我拿一个实际例子:

void ddr_init(void)
{
    struct mmdc_p_regs *mmdc = (struct mmdc_p_regs *)MMDC_P0_BASE;

    /* 1. 复位DDR控制器 */
    writel(0x0, &mmdc->mdctl);

    /* 2. 配置DDR时序 */
    writel(0x0F, &mmdc->mdotc);  /* 片选时序 */
    writel(0x3333, &mmdc->mdcfg0); /* 配置参数0 */
    writel(0x0000, &mmdc->mdcfg1); /* 配置参数1 */

    /* 3. 启动DDR */
    writel(0x04000000, &mmdc->mdctl);

    /* 4. 等待DDR就绪 */
    while (!(readl(&mmdc->mdctl) & 0x00010000));
}

注意:DDR时序参数必须跟你的DDR芯片数据手册一致。我曾经因为tRCD差了1个时钟周期,板子在高温下频繁死机。查了三天才找到原因。

1.3.2 调试DDR的技巧

我个人经验:

  • 先用低速时钟初始化DDR,跑通了再提频
  • 用读写测试验证DDR稳定性(U-Boot自带 mtest 命令)
  • 如果板子有多个DDR颗粒,逐个测试

嗯,这里要注意:DDR校准参数跟PCB布线长度强相关。同一批板子,不同批次可能都要微调。量产时一定要留余量。

1.4 Timer驱动移植:系统的心跳

Timer是U-Boot的脉搏。延时、超时、看门狗都靠它。

U-Boot里Timer的接口很简单:

  • timer_init() — 初始化定时器
  • get_timer() — 获取当前时间(毫秒)
  • udelay() — 微秒级延时

移植时,你需要实现这三个函数。一般用芯片内部的通用定时器(GPT)或者系统节拍定时器(SysTick)。

以i.MX6为例:

#define GPT_BASE      0x02098000
#define GPT_CR        (GPT_BASE + 0x00)
#define GPT_PR        (GPT_BASE + 0x04)
#define GPT_SR        (GPT_BASE + 0x08)
#define GPT_CNT       (GPT_BASE + 0x24)

void timer_init(void)
{
    /* 使能GPT时钟 */
    writel(0x00000041, GPT_CR);  /* 使能+24分频 */
    writel(0x00, GPT_PR);        /* 不分频 */
}

ulong get_timer(ulong base)
{
    return readl(GPT_CNT) / 1000 - base;
}

避坑指南:我曾经遇到过Timer计数溢出的问题。如果你的Timer是32位,主频100MHz,那每43秒就溢出一次。记得在 get_timer() 里处理溢出,或者用64位计数器。

1.5 串口驱动移植:调试的生命线

串口是嵌入式开发者的眼睛。没有串口,你连U-Boot死在哪都不知道。

U-Boot的串口驱动在 drivers/serial/ 目录下。移植时主要实现:

  • serial_init() — 初始化UART控制器
  • serial_putc() — 发送一个字符
  • serial_getc() — 接收一个字符
  • serial_tstc() — 检查是否有数据

以16550兼容UART为例:

#define UART_BASE   0x02020000
#define UART_RBR    (UART_BASE + 0x00)
#define UART_THR    (UART_BASE + 0x00)
#define UART_LSR    (UART_BASE + 0x14)

void serial_init(void)
{
    /* 设置波特率115200,8N1 */
    writel(0x00, UART_BASE + 0x08);  /* 关中断 */
    writel(0x83, UART_BASE + 0x0C);  /* 设置DLAB */
    writel(0x01, UART_BASE + 0x00);  /* 分频系数 */
    writel(0x00, UART_BASE + 0x04);
    writel(0x03, UART_BASE + 0x0C);  /* 8位数据,1位停止 */
}

void serial_putc(char c)
{
    while (!(readl(UART_LSR) & 0x20));  /* 等待发送缓冲区空 */
    writel(c, UART_THR);
}

重要:串口波特率必须跟你的终端软件一致。我见过有人把波特率设成115200,终端却配了9600,结果打印全是乱码。排查了半天才发现是这种低级错误。

1.6 实战经验总结

说了这么多,最后分享几点我的实战心得:

  1. 先让串口说话 — 其他驱动可以慢慢调,串口必须第一个跑通。没有打印信息,你就像瞎子摸象。
  2. DDR参数要保守 — 先按最慢的时序配,跑起来再优化。别一上来就追求极致性能。
  3. 用好U-Boot的命令行bdinfo 看板级信息,md 读内存,mw 写内存,mtest 测DDR。这些命令能帮你快速定位问题。
  4. 保留一个能用的参考板 — 我每次移植新板子,都会在旁边放一块开发板。出问题了,对比一下配置差异,往往能找到原因。

U-Boot移植说难也难,说简单也简单。难在DDR和时钟这些底层参数,简单在框架已经很成熟了。你只要照着参考板改,大部分工作就是复制粘贴+微调。

下一章咱们聊聊Linux内核的启动流程和移植要点。到时候见。