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后,会做两件事:
- 解析自身需要的硬件信息:比如串口地址、时钟配置
- 传递给内核:在跳转到内核前,把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在板子上打印出第一行日志时,那种成就感,嗯,值得期待。