3. Bootloader原理与移植:U-Boot启动流程分析、U-Boot常用命令、在PDA开发板上移植U-Boot
好,咱们今天聊聊Bootloader。说白了,它就是嵌入式系统的「开门人」。你按下电源键,CPU刚醒来,第一件事就是跑Bootloader。没有它,Linux内核根本进不去。
我个人习惯把Bootloader比作「PC的BIOS + 引导程序」的合体。但嵌入式里没BIOS,所以U-Boot得自己搞定硬件初始化,再把内核请进内存。嗯,这里要注意——U-Boot不是操作系统,它只是个「搬运工」。
3.1 U-Boot启动流程分析
U-Boot的启动流程,我拆成两个阶段来讲。为什么分两段?因为早期嵌入式CPU的RAM太小,放不下完整的U-Boot,所以先跑一小段「瘦身版」代码,初始化好内存,再把完整版搬进去。
第一阶段:汇编代码(SPL / 前导部分)
这一阶段通常写在 start.S 里。CPU一上电,从复位向量地址开始执行。做过PDA项目的朋友都知道,这时候CPU还处于「裸奔」状态——时钟没配、内存没开、栈都没设。
我遇到过最坑的事:有次移植U-Boot,板子死活不跑。查了两天,发现是CPU的时钟配置写错了,导致串口波特率对不上。你想想看,连打印信息都看不到,调试起来多痛苦。
第一阶段主要干这几件事:
- 设置CPU为SVC模式(超级管理员模式)
- 关闭看门狗、中断
- 初始化系统时钟和PLL
- 初始化SDRAM控制器
- 设置栈指针(SP)
- 将第二阶段代码从Flash拷贝到SDRAM
- 跳转到C语言入口
board_init_f()
关键点:第一阶段代码必须用位置无关码(PIC)编写。因为这时候代码还在Flash里跑,地址和链接地址可能不一样。我刚开始做时不懂这个,跳转指令写死了绝对地址,结果一跑就飞。
第二阶段:C语言主导
进入C代码后,U-Boot开始「正经干活」了。流程大致如下:
board_init_f():初始化板级外设(定时器、GPIO、串口等)- 重定位:把U-Boot自身从Flash搬到SDRAM的高地址
board_init_r():初始化剩余硬件(网卡、Flash、MMC等)- 进入主循环:等待用户输入命令,或者自动启动内核
为什么第二阶段要重定位?因为Flash读取速度慢,而且U-Boot运行时需要频繁修改全局变量,这些变量必须放在可写的RAM里。说白了,就是给自己找个「舒服的家」。
我的经验:调试第二阶段时,建议先让串口能打印信息。哪怕只打印一个字符,都说明CPU和时钟没问题了。我曾经在PDA上调试,串口死活没输出,最后发现是GPIO复用配置错了——嗯,这种低级错误犯过一次就够了。
3.2 U-Boot常用命令
U-Boot的命令行,说白了就是个「瑞士军刀」。你可以在它上面做各种操作:读内存、写Flash、传文件、启动内核。我整理几个最常用的:
| 命令 | 功能 | 示例 |
|---|---|---|
printenv |
打印所有环境变量 | printenv bootargs |
setenv |
设置环境变量 | setenv bootdelay 3 |
saveenv |
保存环境变量到Flash | saveenv |
tftp |
通过网络下载文件 | tftp 0x30000000 uImage |
bootm |
启动内核镜像 | bootm 0x30000000 |
md |
显示内存内容 | md.b 0x30000000 20 |
mw |
写内存 | mw.l 0x30000000 0xdeadbeef |
flinfo |
查看Flash信息 | flinfo |
erase |
擦除Flash扇区 | erase 0x100000 +0x40000 |
我个人最常用的是 tftp 和 bootm 组合。开发阶段,内核和文件系统都放在服务器上,U-Boot通过网线下载到内存,直接启动。这样不用反复烧写Flash,效率高很多。
避坑指南:我曾经在PDA上调试,用 tftp 下载内核,结果每次都卡在99%。查了半天,发现是网卡驱动里的超时时间设得太短。解决办法很简单:在U-Boot配置里把 CONFIG_NET_RETRY_COUNT 改大一点。
3.3 在PDA开发板上移植U-Boot
移植U-Boot,说白了就是让U-Boot认识你的板子。每个板子的CPU型号、内存大小、Flash布局、外设地址都不一样,U-Boot必须「因板制宜」。
我一般按以下步骤来:
第一步:选择参考板
U-Boot源码里已经支持几百种开发板。找到和你PDA板子最接近的,比如CPU相同、内存配置类似的。我习惯用 make menuconfig 先看看有哪些可选配置。
第二步:修改板级文件
在 board/ 目录下,复制一份参考板的文件夹,改成你的板子名。主要修改这几个文件:
Kconfig:添加板子配置选项Makefile:添加编译目标board.c:板级初始化代码(时钟、GPIO、内存时序)lowlevel_init.S:底层硬件初始化(SDRAM参数)
核心难点:SDRAM时序参数。这个配错了,板子直接死给你看。我当年在PDA上移植,SDRAM的CAS延迟写错了一个值,结果内存读写偶尔出错,内核启动到一半就崩溃。调试这种问题,只能用 md 命令反复读写同一地址,看数据对不对。
第三步:配置环境变量
PDA开发板通常有固定的Flash分区。我习惯这样设置:
setenv bootargs console=ttyS0,115200 root=/dev/mtdblock3 rw
setenv bootcmd tftp 0x30000000 uImage \; bootm 0x30000000
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.1
saveenv
这里 bootargs 是传给内核的参数,告诉内核用哪个串口、根文件系统在哪。bootcmd 是自动启动时执行的命令。嗯,注意那个反斜杠和分号——U-Boot的命令行语法有点怪,多个命令要用 ; 分隔,但前面得加反斜杠转义。
第四步:编译与烧写
配置好之后,编译命令很简单:
make distclean
make your_board_defconfig
make -j4
编译出来的 u-boot.bin 就是最终镜像。烧写方式取决于你的PDA板子——有的用JTAG,有的用SD卡,有的用串口Xmodem协议。我个人推荐先用JTAG烧一次,后面就可以用U-Boot自己更新自己了。
我的小技巧:第一次烧写U-Boot时,别急着烧完整的。先烧一个最小版本,只包含串口和内存初始化。能打印出U-Boot版本号,就说明硬件通路通了。然后再逐步添加网卡、Flash等驱动。这叫「分步验证」,能省很多调试时间。
第五步:调试与验证
移植完成后,至少验证这几项:
- 串口输出是否正常
- 内存读写是否稳定(跑一遍
mtest命令) - Flash擦写是否正常
- 网络能否ping通
- 能否通过tftp下载并启动内核
你想想看,如果这些基本功能都正常,那U-Boot移植就算成功了。剩下的就是优化——比如缩短启动时间、添加自定义命令等。
最后提醒:U-Boot的环境变量默认保存在Flash里。如果你改了环境变量但没 saveenv,一重启就恢复原样。我曾经在PDA上调试了一整天,每次重启都要重新设IP地址,后来才发现是忘了保存——这种低级错误,犯一次就长记性了。
好了,U-Boot的原理和移植就聊到这儿。下一章咱们讲Linux内核的编译与配置,到时候会用到今天移植好的U-Boot来引导内核。嗯,先把基础打牢,后面才不慌。