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开始「正经干活」了。流程大致如下:

  1. board_init_f():初始化板级外设(定时器、GPIO、串口等)
  2. 重定位:把U-Boot自身从Flash搬到SDRAM的高地址
  3. board_init_r():初始化剩余硬件(网卡、Flash、MMC等)
  4. 进入主循环:等待用户输入命令,或者自动启动内核

为什么第二阶段要重定位?因为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

我个人最常用的是 tftpbootm 组合。开发阶段,内核和文件系统都放在服务器上,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来引导内核。嗯,先把基础打牢,后面才不慌。