3、Bootloader与内核启动流程:分析A133的启动流程,从BROM到U-Boot再到Linux内核,理解设备树(DTS)的作用,修改内核启动参数。

好,咱们今天聊聊A133的启动流程。说实话,这块内容我当年刚接触时也觉得挺绕的,但搞明白了之后,你会发现整个系统的脉络一下子就清晰了。

从你按下电源键到屏幕上出现Android桌面,这中间到底发生了什么?说白了,就是一场接力赛。BROM先跑,然后叫醒U-Boot,U-Boot再拉起Linux内核,内核最后启动Android系统。每一棒都有自己的任务,谁也不能掉链子。

3.1 第一棒:BROM(Boot ROM)

芯片一上电,CPU首先执行的是内部固化的BROM代码。这段代码是写死的,你改不了,我也改不了。它的任务很简单:初始化最基本的硬件(比如时钟、SRAM),然后检测启动介质。

A133的BROM会按照预设的顺序去检查启动设备。我记得默认顺序是:eMMC → NAND Flash → SD卡 → USB下载模式。它会去读取每个设备的第一个扇区,看看有没有合法的签名或头部信息。

关键点:BROM只认它自己的启动头格式。如果你把U-Boot直接烧到eMMC的0偏移处,BROM是不认的。它需要前面有一个特定的结构体,包含魔术字、校验和、加载地址等信息。

我在项目中遇到过一个问题:板子死活起不来,串口也没输出。查了半天,发现是eMMC的前几个字节被擦除了,BROM找不到合法的启动头,直接跳到了USB下载模式。嗯,这种坑踩过一次就记住了。

3.2 第二棒:U-Boot

BROM把U-Boot加载到SRAM(或者DRAM的特定位置)后,就把控制权交给了U-Boot。U-Boot是个大家伙,它要做的事情比BROM多得多。

U-Boot的启动流程大致分两步:

  1. SPL(Secondary Program Loader)阶段:这是U-Boot的迷你版,负责初始化DRAM、时钟、串口等。因为BROM加载的U-Boot可能太大,SRAM装不下,所以SPL先跑起来,把完整的U-Boot从存储介质搬到DRAM里。
  2. 完整的U-Boot阶段:这时候你就能在串口看到熟悉的启动信息了。它会解析环境变量、加载设备树、初始化更多外设,最后根据bootcmd环境变量来决定怎么启动内核。

你想想看,U-Boot其实就是一个微型操作系统。它有命令行、文件系统支持、网络协议栈。我经常在U-Boot里用tftp命令通过网络下载内核镜像,调试起来特别方便。

我的习惯:在U-Boot里设置一个bootdelay环境变量,设为3秒。这样每次启动时我都有机会按任意键进入U-Boot命令行,进行调试或修改启动参数。量产时再把这个值设为0,加快启动速度。

3.3 第三棒:Linux内核启动

U-Boot最终会执行bootmbootz命令,把内核镜像加载到内存指定地址,然后跳转过去。这时候,Linux内核开始接管一切。

内核启动过程我简单梳理一下:

  • 解压内核:如果内核是压缩的(zImage),先自解压。
  • 架构相关初始化:设置页表、使能MMU、初始化中断控制器等。
  • start_kernel():这是C语言入口,后面的事情就都是C代码了。它会初始化调度器、内存管理、文件系统、驱动模型等。
  • 挂载根文件系统:最后一步,挂载rootfs,然后执行/init进程,Android系统就正式启动了。

这里有个细节:内核在启动早期需要知道硬件平台的信息。比如CPU类型、内存大小、外设地址等。这些信息从哪里来?从设备树(DTS)来。

3.4 设备树(DTS)的作用

设备树,说白了就是一份硬件描述文件。它用树形结构描述了板子上所有的硬件资源:CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器……

以前的内核(比如2.6时代),这些信息都是硬编码在C代码里的。换一块板子就得重新编译内核,非常麻烦。有了设备树之后,内核和板级硬件信息解耦了。同一份内核镜像,搭配不同的设备树文件,就能跑在不同的板子上。

A133的设备树文件通常放在内核源码的arch/arm64/boot/dts/allwinner/目录下。你会看到类似sun50i-a133.dtsi(芯片级)和sun50i-a133-pinephone.dts(板级)这样的文件。

举个例子:如果你想修改LCD的时序参数,不需要改内核代码。只需要在DTS文件里找到对应的display节点,修改clock-frequencyhactivevactive等属性,然后重新编译DTS并烧录即可。

我曾经犯过一个低级错误:修改了DTS后忘了重新编译,直接烧了旧的内核镜像。结果屏幕死活不亮,我还以为是硬件坏了。折腾了两天才发现是DTS没更新……

3.5 修改内核启动参数

内核启动参数(bootargs)是U-Boot传给内核的命令行参数。它告诉内核根文件系统在哪里、控制台用哪个串口、内存大小等等。

常见的启动参数示例:

console=ttyS0,115200 root=/dev/mmcblk0p5 rootwait rw loglevel=8

各参数含义:

参数 含义
console=ttyS0,115200 使用串口0作为控制台,波特率115200
root=/dev/mmcblk0p5 根文件系统在eMMC的第5个分区
rootwait 等待根设备就绪(对于SD卡/eMMC很重要)
rw 以读写方式挂载根文件系统
loglevel=8 内核日志级别,8表示显示所有调试信息

修改启动参数有两种方式:

  1. 在U-Boot环境变量中修改:进入U-Boot命令行,执行setenv bootargs '你的参数',然后saveenv保存。
  2. 在DTS中指定:在chosen节点里设置bootargs属性。这样U-Boot会优先使用DTS里的参数。

注意:如果你在U-Boot环境变量和DTS中都设置了bootargs,U-Boot环境变量的优先级更高。我建议统一在U-Boot里管理,因为DTS编译后就不方便改了。

调试时我经常把loglevel设成8,这样能看到内核所有的打印信息。量产时再改回3或4,减少不必要的输出,也能稍微提升启动速度。

嗯,关于启动流程就聊这么多。下一章我们会深入内核的驱动模型,看看设备树里的节点是怎么和驱动代码一一对应的。到时候你会更理解设备树的威力。