4. 启动流程与Bootloader:嵌入式系统启动流程、Bootloader的作用、U-Boot分析与移植、启动参数传递
嵌入式系统上电后,第一行代码从哪里开始?CPU怎么知道该执行什么?
很多人一上来就啃Linux内核源码,却忽略了启动流程这个地基。我见过不少工程师,内核配置玩得飞起,但板子就是起不来——最后发现是Bootloader传参传错了。说白了,启动流程没搞透,后面全是盲人摸象。
4.1 嵌入式系统启动流程全景
一个典型的嵌入式Linux系统,从上电到用户态程序运行,大致经历这么几个阶段:
- ROM Boot(片内引导):CPU上电后,从预设地址(通常是片内ROM)执行固化代码。
- SPL/第一阶段Bootloader:初始化最小硬件(时钟、DDR、串口),加载第二阶段。
- U-Boot完整版:初始化更多外设,加载内核镜像到内存。
- Linux内核启动:解压、初始化子系统、挂载根文件系统。
- 用户空间初始化:init进程启动,系统进入可用状态。
核心要点:整个流程就像接力赛,每一棒只做自己该做的事,然后把控制权交给下一棒。任何一棒掉链子,系统就卡死。
我当年调试一块i.MX6的板子,卡在SPL阶段整整三天。最后发现是DDR时序参数配错了,SPL初始化内存时直接挂掉。嗯,从那以后我养成了一个习惯——先确认DDR能跑通,再往下走。
4.2 Bootloader的核心作用
Bootloader到底在干什么?说白了就三件事:
- 硬件初始化:把CPU、内存、串口、存储等基础硬件从“原始状态”带到“可用状态”。
- 加载内核:从Flash、SD卡、网络等介质把内核镜像读到内存。
- 传递启动参数:告诉内核“你是谁”、“你在哪”、“该怎么跑”。
| 功能 | 说明 | 常见坑点 |
|---|---|---|
| 硬件初始化 | 时钟、DDR、GPIO、串口等 | DDR时序参数错误,板子直接死 |
| 加载内核 | 从存储介质读取镜像到内存 | 分区表不对,读错位置 |
| 传递参数 | 通过设备树或ATAG传递硬件信息 | 参数与内核不匹配,外设无法识别 |
| 提供交互界面 | 命令行接口,方便调试和烧写 | 环境变量配置错误,启动流程异常 |
个人经验:我习惯在Bootloader阶段加一个“心跳灯”——通过GPIO控制LED闪烁。这样一眼就能看出Bootloader有没有跑起来,比看串口日志快多了。
4.3 U-Boot深度分析
U-Boot是目前嵌入式领域最主流的Bootloader。为什么选它?因为支持架构多、社区活跃、功能强大。你想想看,从ARM到RISC-V,从MIPS到x86,U-Boot几乎全覆盖。
4.3.1 U-Boot的启动流程
U-Boot的启动分两个阶段:
- Stage 1(汇编阶段):设置异常向量表、初始化CPU模式、关闭中断、初始化DDR、重定位自身到DDR。
- Stage 2(C语言阶段):初始化板级外设、解析环境变量、进入命令行或自动启动内核。
这里有个关键点——重定位。U-Boot一开始在Flash或ROM里跑,但Flash太慢了。所以它把自己复制到DDR里,然后跳过去继续执行。我刚开始做移植时,重定位地址算错了,U-Boot一跑就飞。后来老老实实对着数据手册算偏移,再也没出过问题。
4.3.2 U-Boot的目录结构
U-Boot源码目录看着大,其实核心就几个:
u-boot/
├── arch/ # 架构相关代码(arm、riscv、x86等)
├── board/ # 板级支持代码
├── common/ # 通用功能(命令行、环境变量等)
├── drivers/ # 驱动(串口、网卡、存储等)
├── dts/ # 设备树源文件
├── include/ # 头文件
├── net/ # 网络协议栈
├── fs/ # 文件系统支持
└── cmd/ # 命令行命令实现
移植U-Boot时,你主要关注board/和arch/这两个目录。板级差异基本都在这里。
4.4 U-Boot移植实战
移植U-Boot到一块新板子,我一般按这个步骤来:
- 选择参考板:找一块CPU相同、硬件配置接近的板子作为起点。
- 配置编译:
make <参考板>_defconfig,先确保能编译通过。 - 修改DDR参数:这是最关键的。DDR时序参数必须和硬件设计一致。
- 调整时钟配置:CPU频率、总线频率、外设时钟。
- 配置串口:确保串口能输出,这是调试的基础。
- 调整存储分区:告诉U-Boot内核和设备树放在哪里。
- 烧录测试:烧到板子上,看串口输出。
注意:移植U-Boot时,DDR参数是最大的坑。我曾经因为一个时序参数差了0.5ns,板子在高温下就随机死机。后来用示波器量了波形才发现问题。所以,DDR参数一定要和硬件工程师反复确认,最好用DDR厂商提供的计算工具生成。
4.5 启动参数传递
Bootloader和内核之间怎么通信?两种方式:
- ATAG(老方式):用标签列表传递参数,ARM平台早期使用。
- 设备树DTB(新方式):用二进制设备树描述硬件,现在的主流。
我个人强烈建议用设备树。为什么?因为ATAG把硬件信息硬编码在Bootloader里,换一块板子就得改Bootloader。设备树把硬件描述和内核代码分离,同一份内核镜像可以跑在不同板子上,只要换设备树就行。
U-Boot加载设备树的典型命令:
# 从SD卡读取设备树到内存
fatload mmc 0:1 0x88000000 board.dtb
# 设置fdt地址
fdt addr 0x88000000
# 启动内核
bootz 0x80000000 - 0x88000000
这里0x80000000是内核镜像地址,0x88000000是设备树地址。地址不能重叠,否则内核启动时会踩掉设备树。
避坑指南:我曾经在调试时,内核和设备树的地址只差了1MB。结果内核解压后直接覆盖了设备树,系统启动到一半就崩了。后来我养成了习惯——内核和设备树之间至少留8MB的间隔。
4.6 知识结构图
下面这张图总结了嵌入式系统从上电到内核启动的完整流程:
4.7 常见问题与调试技巧
启动流程中遇到的问题,我总结了几类:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 串口无输出 | 时钟配置错误、串口引脚未初始化 | 检查时钟树、GPIO复用配置 |
| U-Boot启动后重启 | DDR参数错误、看门狗未关闭 | 核对DDR时序、关闭WDT |
| 内核无法启动 | 内核地址错误、设备树不匹配 | 确认加载地址、检查DTB内容 |
| 文件系统挂载失败 | 分区表错误、驱动未编译 | 检查分区偏移、确认驱动配置 |
调试技巧:我习惯在U-Boot里加几个调试打印点——SPL入口、DDR初始化完成、加载内核前。这样出问题时,看打印到哪一步就能快速定位。另外,串口波特率别设太高,115200最稳,460800在干扰大的环境下容易丢数据。
启动流程是整个嵌入式系统的基石。Bootloader虽然看起来只是“加载内核”这么简单,但里面的门道不少。把这一块吃透了,后面调试内核、驱动、应用都会顺手很多。