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的启动流程大致分两步:
- SPL(Secondary Program Loader)阶段:这是U-Boot的迷你版,负责初始化DRAM、时钟、串口等。因为BROM加载的U-Boot可能太大,SRAM装不下,所以SPL先跑起来,把完整的U-Boot从存储介质搬到DRAM里。
- 完整的U-Boot阶段:这时候你就能在串口看到熟悉的启动信息了。它会解析环境变量、加载设备树、初始化更多外设,最后根据
bootcmd环境变量来决定怎么启动内核。
你想想看,U-Boot其实就是一个微型操作系统。它有命令行、文件系统支持、网络协议栈。我经常在U-Boot里用tftp命令通过网络下载内核镜像,调试起来特别方便。
我的习惯:在U-Boot里设置一个bootdelay环境变量,设为3秒。这样每次启动时我都有机会按任意键进入U-Boot命令行,进行调试或修改启动参数。量产时再把这个值设为0,加快启动速度。
3.3 第三棒:Linux内核启动
U-Boot最终会执行bootm或bootz命令,把内核镜像加载到内存指定地址,然后跳转过去。这时候,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-frequency、hactive、vactive等属性,然后重新编译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表示显示所有调试信息 |
修改启动参数有两种方式:
- 在U-Boot环境变量中修改:进入U-Boot命令行,执行
setenv bootargs '你的参数',然后saveenv保存。 - 在DTS中指定:在
chosen节点里设置bootargs属性。这样U-Boot会优先使用DTS里的参数。
注意:如果你在U-Boot环境变量和DTS中都设置了bootargs,U-Boot环境变量的优先级更高。我建议统一在U-Boot里管理,因为DTS编译后就不方便改了。
调试时我经常把loglevel设成8,这样能看到内核所有的打印信息。量产时再改回3或4,减少不必要的输出,也能稍微提升启动速度。
嗯,关于启动流程就聊这么多。下一章我们会深入内核的驱动模型,看看设备树里的节点是怎么和驱动代码一一对应的。到时候你会更理解设备树的威力。