4、Bootloader基础:U-Boot概述、三星平台U-Boot启动流程、板级配置与设备树基础

好,咱们进入第四章。这一章聊的是Bootloader,具体说就是U-Boot。很多做Linux应用开发的兄弟,可能一辈子都不碰Bootloader。但做BSP的,这是基本功。说白了,Bootloader就是硬件的“闹钟”,它得把硬件叫醒,再把系统内核请进来。

我个人习惯把U-Boot比作一个“微型操作系统”。它有自己的驱动、命令行、文件系统支持。但它的使命很单纯——为启动Linux做准备。嗯,这里要注意,千万别把U-Boot想得太复杂,也别想得太简单。

4.1 U-Boot概述

U-Boot,全称是Das U-Boot,德国人搞的。它支持几乎所有的嵌入式CPU架构:ARM、MIPS、RISC-V、x86等等。三星平台用的就是ARM架构,所以U-Boot是标配。

U-Boot能干什么?我列几个核心功能:

  • 硬件初始化:设置时钟、配置DDR、初始化串口
  • 加载内核:从Flash、SD卡、网络加载Linux内核镜像
  • 传递启动参数:把硬件信息告诉内核
  • 提供命令行:调试时可以直接操作硬件寄存器

核心观点:U-Boot不是操作系统,它是操作系统的“搬运工”。

我在项目中遇到过一件事。有次客户说板子启动不了,我一看,U-Boot根本没跑起来。查了半天,发现是DDR初始化参数配错了。你想想看,DDR都没起来,代码往哪跑?所以U-Boot的DDR配置,是重中之重。

4.2 三星平台U-Boot启动流程

三星平台的U-Boot启动流程,跟其他ARM平台大同小异,但有几个关键点不一样。我把它拆成几个阶段来讲。

4.2.1 BL0:芯片内部ROM

芯片上电后,CPU从内部的ROM开始执行。这段代码是三星出厂固化的,我们改不了。它负责:

  • 初始化最基本的时钟和SRAM
  • 从启动设备(SD卡、eMMC、SPI Flash)读取BL1
  • 校验BL1的签名(如果开启了安全启动)

嗯,这里要注意。BL0只认固定的启动设备顺序。我记得Exynos 4412的启动顺序是:SD卡 -> eMMC -> SPI Flash。如果SD卡没插,它自动跳到下一个。

4.2.2 BL1:一级Bootloader

BL1是U-Boot的第一部分,存放在启动设备的固定位置(比如SD卡的扇区1)。它很小,一般只有16KB左右。为什么这么小?因为芯片内部的SRAM只有那么大。

BL1的任务:

  • 初始化DRAM控制器(让DDR能用)
  • 从启动设备加载BL2到DDR
  • 跳转到BL2执行

避坑指南:我曾经在BL1阶段卡了三天。原因是DDR的时序参数配错了,导致BL2加载到DDR后,一执行就崩溃。后来用示波器量了DQS信号,才发现是PCB走线长度没匹配。嗯,硬件问题有时候比软件更坑。

4.2.3 BL2:二级Bootloader

BL2就是完整的U-Boot了。它被加载到DDR中运行,这时候内存管够。BL2会做:

  • 完整的板级初始化(时钟、GPIO、网络等)
  • 解析设备树
  • 加载Linux内核和设备树到内存
  • 跳转到内核入口

你可能会问,为什么分BL1和BL2?直接一个U-Boot不行吗?原因很简单:芯片内部的SRAM太小,装不下完整的U-Boot。所以先搞个小块头把DDR初始化了,再把大块头请进来。

4.3 板级配置与设备树基础

这部分是BSP开发的核心。说白了,就是告诉U-Boot:“你的板子长什么样,有哪些外设,内存多大。”

4.3.1 板级配置

在U-Boot源码中,每个板子都有自己的配置文件。以三星Exynos为例,配置文件在:

include/configs/exynos5-common.h
include/configs/smdk5250.h

这些文件里定义了:

配置项 说明
CONFIG_SYS_TEXT_BASE U-Boot在内存中的加载地址
CONFIG_SYS_CLK_FREQ 系统时钟频率
CONFIG_NR_DRAM_BANKS DDR的bank数量
CONFIG_BOOTCOMMAND 默认的启动命令

我个人习惯,拿到一块新板子,先看配置文件。这里能看出板子的“骨架”。比如DDR是512MB还是1GB,网卡用的是哪个PHY,都在这里定义。

4.3.2 设备树基础

设备树(Device Tree)是Linux内核和U-Boot之间传递硬件信息的“信使”。以前的做法是把硬件信息硬编码在代码里,改一个引脚就要重新编译。有了设备树,硬件描述和驱动代码分开了。

设备树文件的后缀是.dts(源文件)和.dtb(编译后的二进制)。U-Boot启动时,会把dtb加载到内存,然后传给内核。

一个简单的设备树节点示例:

/ {
    model = "Samsung SMDK5250 board";
    compatible = "samsung,smdk5250", "samsung,exynos5250";

    memory {
        device_type = "memory";
        reg = <0x40000000 0x80000000>;  // 2GB DDR
    };

    serial@12C00000 {
        compatible = "samsung,exynos4210-uart";
        reg = <0x12C00000 0x100>;
        interrupts = <0 52 4>;
    };
};

嗯,这里要注意几个关键点:

  • compatible:驱动匹配的关键字,内核靠它找到对应的驱动
  • reg:寄存器地址和大小
  • interrupts:中断号、触发方式

小技巧:调试设备树时,我常用U-Boot的fdt命令。比如fdt list /可以查看根节点,fdt set可以临时修改属性。不用重新编译,直接改内存里的dtb,调试效率高很多。

4.3.3 U-Boot中的设备树处理

U-Boot本身也使用设备树。它从启动设备加载dtb后,会做两件事:

  1. 解析自身需要的硬件信息:比如串口地址、时钟配置
  2. 传递给内核:在跳转到内核前,把dtb的地址放在寄存器r2中

我曾经踩过一个坑。有次我修改了设备树,增加了一个GPIO按键。结果U-Boot启动时直接挂掉了。查了半天,发现是设备树语法写错了——少了一个分号。嗯,设备树的语法检查很严格,一个标点符号都不能错。

4.4 实战要点总结

好,这一章的内容就这些。我总结几个实战中要注意的点:

  • DDR配置是第一步:U-Boot跑不起来,90%是DDR的问题
  • 启动设备顺序要搞清楚:SD卡、eMMC、SPI Flash,优先级不能搞反
  • 设备树语法要严谨:一个分号、一个逗号都不能错
  • 多用U-Boot命令行调试md读内存,mw写内存,fdt操作设备树

下一章,我们会真正动手,从零开始为一块三星板子移植U-Boot。到时候这些理论知识都会用上。你想想看,当你的U-Boot在板子上打印出第一行日志时,那种成就感,嗯,值得期待。