U-Boot移植实战:从PowerPC HPC II平台硬件初始化到Linux内核引导
1. 项目概述与核心挑战
在嵌入式系统的世界里,引导加载程序(Bootloader)是那个在幕后默默无闻,却又至关重要的“幕后英雄”。它是在系统上电后第一个跑起来的软件,负责唤醒沉睡的硬件,为操作系统的登场铺平道路。而U-Boot,作为这个领域的“瑞士军刀”,以其开源、强大和高度可移植的特性,成为了无数嵌入式开发者的首选。今天,我想和你深入聊聊,如何将U-Boot这把“军刀”,精准地打磨并适配到一块特定的“战场”上——Freescale(现NXP)的HPC II评估平台。
HPC II平台的核心是一颗PowerPC MPC7447A/7448处理器,搭配Tsi108高性能桥接芯片。这套组合在当年是高性能嵌入式计算的代表,但同时也带来了复杂的初始化流程。U-Boot的默认配置显然无法直接在这套硬件上运行,这就是移植工作的起点。移植的核心目标,是让U-Boot能够正确识别并初始化这块板子上的所有关键硬件:从SDRAM控制器、Flash存储器,到PCI总线、网络控制器(包括备用的RTL8139和集成的Tsi108 GigE),最后顺利地将Linux内核加载并启动起来。
这个过程远不止是改几个配置文件那么简单。它要求开发者深入理解硬件手册,精确地配置每一个内存映射地址、每一个控制寄存器位域,并处理好从ROM启动到RAM运行这个“乾坤大挪移”般的重定位过程。输入材料中那些看似晦涩的
out32
寄存器写操作、
CFG_TSI108_CSR_BASE
宏定义,以及
CFG_CMD_FLASH
等编译选项,正是这场“硬件对话”的具体语言。接下来,我将结合我多年的嵌入式踩坑经验,为你拆解这其中的每一个关键步骤、背后的原理,以及那些手册上不会写的实操技巧。
2. 硬件平台深度解析与U-Boot启动框架
2.1 HPC II平台硬件架构总览
要成功移植,必须先吃透硬件。HPC II平台可以看作一个以PowerPC CPU为核心,通过Tsi108桥接芯片扩展出的一个微型计算机系统。
CPU与内存子系统
:MPC7447A/7448是高性能的PowerPC G4处理器。U-Boot需要正确初始化其L1/L2缓存、MMU(虽然初期可能禁用),但最紧要的是配置好内存控制器,让CPU能访问SDRAM。在HPC II上,内存控制器集成在Tsi108芯片内部。输入代码中
board_early_init_r()
函数里对
HLP_Bx_CTRL0
和
HLP_Bx_CTRL1
的配置,正是在设置Tsi108的Host Local Port(HLP)控制器,也就是CPU访问本地设备(如Flash、Boot ROM)的窗口。例如,值
0x7FFC44C2
并非随意设置,它定义了该Bank的基地址、大小、位宽(最后一位
2
表示32位)和访问时序。理解这些十六进制数字背后的位域定义,是调试硬件不工作的第一步。
Tsi108桥接芯片的关键角色
:Tsi108远不止是一个简单的桥,它集成了SDRAM控制器、PCI主机控制器、DMA引擎、两个GbE MAC等。对U-Boot而言,需要初始化其内部多个功能模块的寄存器空间。这些寄存器都被映射到CPU的物理地址空间,例如
CFG_TSI108_CSR_BASE
(通常是
0xC0000000
)就是Tsi108配置空间的基地址。
PCI_PFAB_BAR0
、
PB_OCN_BAR1
等寄存器的设置,就是在划分PCI内存空间、I/O空间以及定义外部设备的地址映射。
存储与启动设备
:板载的Flash(如AMD AM29LV641MH)是最终存放U-Boot和内核的地方,但在开发初期,我们通常通过PROMjet这类硬件仿真器将U-Boot直接下载到板载SRAM或Flash的某个地址运行。这就引出了“BOOT位”的概念。如材料所述,在启动初期,Tsi108的
PB_OCN_BAR1
寄存器的BOOT位(bit 30)为1,CPU只能看到很小一块Boot区域(连接到HLP0)。在
board_early_init_r()
中将其清零后,完整的Flash地址空间(如
0xFE000000
)才变得可见。这个地址切换逻辑是理解后续Flash编程和调试的基础。
2.2 U-Boot启动流程与移植切入点
U-Boot的启动是分阶段的,理解这个流程才能知道我们的代码该插在哪里。
第一阶段:汇编启动代码
(
start.S
)。这是上电后最先执行的代码,用汇编编写,与CPU架构强相关。对于PowerPC,它负责:设置异常向量表、初始化关键寄存器、禁用中断和缓存、设置初始堆栈指针。对于HPC II,这部分通常已经由CPU相关的通用代码完成,我们一般无需修改,除非有特殊的早期硬件初始化需求。
第二阶段:C语言环境初始化与板级早期初始化
。当堆栈设置好,可以跳转到C代码时,就进入了
board_init_f()
函数。它会进行一系列非常基础的初始化,然后调用
relocate_code()
将U-Boot自身从Flash(或ROM)复制到SDRAM的高端地址运行,这就是所谓的“重定位”。重定位后,会跳转到
board_init_r()
,这是主初始化函数。
我们的主战场——
board_early_init_r()
:在输入材料中,大量硬件初始化代码都位于
board/Freescale/freeserve2/tsi108_init.c
文件的
board_early_init_r()
函数里。这个函数通常由
board_init_r()
或相关初始化序列调用。它是我们进行
板级特定硬件初始化
的核心位置。在这里,我们按顺序初始化:
- GPIO和系统时钟 :配置引脚复用和基本时钟。
-
内存控制器
:配置Tsi108的HLP和SDRAM控制器寄存器,让CPU能正确访问内存和Flash。这就是代码中设置
HLP_Bx_CTRL0/1的部分。 -
地址映射
:配置
PB_OCN_BAR1等寄存器,定义PCI、本地总线等地址空间。例如out32(CFG_TSI108_CSR_BASE + TSI108_PB_REG_OFFSET + PB_OCN_BAR1, 0xE0000011);,其中0xE0000011表示基地址为0xE0000000,并使能该区域(bit 0=1),清除BOOT位(bit 1=1?这里需查手册确认,材料描述为“boot bit is cleared”,通常使能和BOOT位是独立的位)。 - PCI子系统 :初始化PCI控制器,配置空间、I/O空间和内存空间的基地址。
- 网络设备 :初始化RTL8139 PCI网卡或Tsi108内置GbE控制器。
- 环境变量 :初始化存储环境变量的介质(如Flash的某个扇区)。
注意 :
board_early_init_r()的执行时机可能在重定位前,也可能在重定位后,取决于具体移植。如果初始化代码需要访问SDRAM(此时SDRAM可能还未初始化),或者代码本身还在Flash中运行,那么对地址的访问要格外小心。例如,在重定位前,代码中使用的CFG_FLASH_BASE地址可能是物理地址;重定位后,如果启用了MMU,则可能是虚拟地址。HPC II的案例中,早期初始化在Flash中运行,配置的是物理地址。
3. 关键外设驱动配置与代码剖析
3.1 Flash存储器驱动适配
Flash是U-Boot和内核的“家”,驱动必须稳定可靠。输入材料显示,HPC II板载的是一块16MB的AMD Flash芯片,使用CFI(通用Flash接口)标准。
配置与使能
:在
include/configs/FS2.h
中,关键配置如下:
#define CFG_MAX_FLASH_BANKS 1 /* 只有一个Flash芯片 */
#define PHYS_FLASH_SIZE 0x01000000 /* 物理大小16MB */
#define CFG_MAX_FLASH_SECT (128) /* 128个扇区 */
#define CFG_FLASH_BASE 0xfe000000 /* Flash在CPU地址空间的基地址 */
#define CFG_FLASH_CFI /* 启用CFI驱动框架 */
#define CFG_FS2_FLASH_CFI_DRIVER /* 启用板级特定的CFI驱动代码 */
CFG_FS2_FLASH_CFI_DRIVER
这个宏是关键,它告诉U-Boot的通用CFI驱动,要去调用位于
board/Freescale/freeserve2/cfi_flash.c
中的板级特定初始化函数。这是U-Boot驱动模型的常见做法:通用框架+板级钩子。
驱动初始化逻辑
:在
cfi_flash.c
中,我们需要实现Flash的探测和初始化。材料中给出的代码片段非常经典:
info->portwidth = FLASH_CFI_32BIT; // Flash数据总线位宽为32位
info->chipwidth = FLASH_CFI_BY16; // 芯片内部是16位组织(由两片16位芯片并联)
这段代码设置了Flash的位宽。更重要的逻辑是判断当前运行位置:
// 向Flash发送CFI查询命令
flash_write_cmd(info, 0, FLASH_OFFSET_CFI, FLASH_CMD_CFI);
// 读取CFI标识字符串“QRY”
if (!(flash_isequal(info, cptr1, 'Q') && ...)) {
printf("Started u-boot from FLASH. Not using Promjet\n");
base = 0xFF000000; // 如果读不到CFI信息,说明可能从Promjet运行,调整基地址
info->start[0] = base;
}
这个判断逻辑是基于一个经验:如果U-Boot是从Flash中直接启动的,那么发送CFI命令后应该能读到正确的响应。如果读不到,说明当前代码可能是在Promjet中运行,访问Flash的地址可能经过了映射(比如BOOT位的影响),因此需要调整操作的基地址。这是一种增强驱动鲁棒性的常见技巧。
实操心得 :Flash驱动调试是最容易卡住的地方。如果
flinfo命令无法显示Flash信息,请按以下顺序排查:
- 确认
CFG_FLASH_BASE地址是否正确。用md命令查看该地址,在未初始化时可能全是0或ff,初始化后应能看到CFI信息或已编程的数据。- 检查
board_early_init_r()中HLP控制器的配置,特别是位宽(HLP_Bx_CTRL0的最后几位)和时序参数是否与Flash芯片手册匹配。- 确认是否正确定义了
CFG_FS2_FLASH_CFI_DRIVER,并实现了必要的板级函数(如flash_init)。- 使用仿真器(如PROMjet)单步跟踪Flash的CFI查询命令序列,用逻辑分析仪或示波器抓取Flash芯片引脚上的波形,确认片选、读/写、地址和数据线信号是否正确。
3.2 网络控制器初始化
网络是后续通过TFTP下载内核的“生命线”。HPC II提供了两种选择:PCI接口的RTL8139网卡和Tsi108内置的GbE控制器。
RTL8139 PCI网卡(备用方案)
:
在
include/configs/FS2.h
中定义
CONFIG_RTL8139
即可启用。但材料中提到需要修改驱动源码
drivers/rtl8139.c
:
static unsigned char tx_buffer[TX_BUF_SIZE] __attribute__((aligned(32)));
static unsigned char rx_ring[RX_BUF_LEN+16] __attribute__((aligned(32)));
这里将DMA缓冲区的对齐从默认值改为32字节。这是因为某些PCI主控制器(如Tsi108)对DMA缓冲区的起始地址有更严格的对齐要求,不满足会导致数据传输失败。 这是一个非常典型的、因硬件差异而需要修改通用驱动的案例。
Tsi108内置GbE控制器(主要方案) : 启用它需要:
-
在
include/configs/FS2.h中定义CONFIG_TSI108_ETH。 -
确保板级驱动文件
board/Freescale/freeserve2/tsi108_eth.c存在并正确实现了初始化函数。 -
在
net/eth.c的eth_initialize()函数中,通过#ifdef CONFIG_TSI108_ETH来调用tsi108_eth_initialize()。
材料中提到,在U-Boot开发初期,可以先使用RTL8139,因为它的驱动更成熟、调试更简单。等主要功能稳定后,再切换到性能更好的内置GbE。这是一种务实的开发策略。
3.3 命令集与功能裁剪
U-Boot功能强大,但资源有限,需要根据板子需求裁剪命令。这是通过
include/configs/FS2.h
中的
CONFIG_COMMANDS
宏进行位或操作实现的:
#define CONFIG_COMMANDS ( (CONFIG_CMD_DFL \
| CFG_CMD_ASKENV \
| CFG_CMD_CACHE \
... \
| CFG_CMD_FLASH \
| CFG_CMD_ENV \
| CFG_CMD_PING) )
-
CFG_CMD_FLASH:启用flinfo、erase、protect、cp等Flash操作命令,这是烧写系统的必备工具。 -
CFG_CMD_ENV:启用环境变量相关命令printenv、setenv、saveenv。它需要配合CFG_ENV_IS_IN_NVRAM(或CFG_ENV_IS_IN_FLASH)来指定环境变量的存储位置。HPC II材料中提到了NVRAM。 -
CFG_CMD_PING:启用网络连通性测试命令。
注意事项 :不要盲目添加所有命令。每个命令都会增加U-Boot镜像的大小。务必根据存储空间(Flash大小)和实际需求(调试、生产)来裁剪。生产版本可能只需要最基本的
bootm和网络命令,而开发版本则需要完整的调试命令集。
4. 构建、调试与Flash烧写实战
4.1 U-Boot镜像构建流程
构建是针对特定板卡的“定制化”过程。
make distclean # 彻底清理,避免旧配置干扰
make FS2_config # 应用HPC II(FS2)的板级配置
make CROSS_COMPILE=powerpc-linux-gnu- # 指定交叉编译工具链进行编译
-
make FS2_config:这个命令会根据boards.cfg或旧式的Makefile,将include/configs/FS2.h这个板级头文件与编译系统关联起来,并生成特定的链接脚本。 -
编译后生成两个关键文件:
-
u-boot:ELF格式文件,包含符号信息,用于调试。 -
u-boot.bin:纯二进制镜像,是最终烧写到Flash中的内容。
-
-
生成反汇编文件用于调试:
powerpc-linux-gnu-objdump -D u-boot > u-boot.dis。这个文件在结合PROMjet或BDM调试器时至关重要,因为调试器显示的地址需要与反汇编文件中的地址对应起来。
4.2 上电调试与地址空间迷思
将
u-boot.bin
通过PROMjet下载到板子并启动后,串口会输出启动信息。如果卡住,调试就开始了。
理解地址重定位 :这是U-Boot调试的核心概念。如材料所述,U-Boot启动分为两阶段:
-
ROM/Flash运行阶段
:代码从Flash(或通过PROMjet映射)的物理地址(如
0xFFF00000)开始执行。此时,调试器(COP)显示的PC地址与u-boot.dis文件中的地址是 直接对应 的。 -
RAM运行阶段
:U-Boot将自己拷贝到SDRAM的高端(如
0x01FCB000),然后跳转过去。此后,所有代码都在RAM中运行。此时,调试器显示的PC地址(如0x01FD0200)对应的是 运行时地址 。要找到它在u-boot.dis中对应的代码,需要减去重定位的偏移量(0x01FCB000),得到 链接时地址 (0x00005200),然后去反汇编文件中查找这个地址附近的代码。
Flash地址访问的“陷阱”
:材料中特别强调了Tsi108的BOOT位带来的地址映射变化。在
board_early_init_r()
清除BOOT位前,CPU通过一个“窗口”访问Flash。清除后,完整的Flash空间才线性映射。这导致了一个地址计算问题:在U-Boot命令行中,访问Flash的地址需要是
清除BOOT位后的映射地址
(如
0xFE000000
),而不是芯片本身的物理地址或早期的映射地址。命令
md ff000000
和
md fe000000
访问的可能是同一块物理Flash的不同映射窗口,理解这一点对编程和调试至关重要。
4.3 Flash烧写完整流程解析
材料中给出了一个从PROMjet烧写U-Boot到Flash的完美范例。我们来拆解其每一步的意图和原理:
-
查看Flash信息
(
flinfo):确认Flash型号、大小、扇区布局,并查看保护状态(显示RO表示只读,受保护)。 -
解除写保护
(
protect off all):Flash的每个扇区默认可能有硬件或软件写保护,此命令发送特定解锁序列,解除保护以便擦写。 -
擦除目标区域
(
erase all):Flash写入前必须先擦除(变为全0xFF)。这是一个相对耗时的操作,需要等待。 -
验证源数据
(
md ff000000):查看PROMjet中U-Boot镜像的起始内容,确认镜像正确。 -
复制镜像
(
cp.w ff000000 fe000000 b100):这是最关键的一步。cp.w是按字(Word)复制。b100是十六进制,表示复制0xB100个字,即45248个字,约180KB,需大于实际U-Boot.bin的大小。这里复制的是 代码的二进制内容 。 -
验证写入
(
md fe000000):读取Flash起始地址,与步骤4的结果对比,确保数据一致。 -
重新上电并从Flash启动
:改变板上的启动开关设置,让CPU从Flash的
0xFE000000地址启动,而非PROMjet。复位后,如果串口出现熟悉的U-Boot启动信息,则大功告成。
避坑指南 :
- 擦除失败 :检查
HLP_Bx_CTRL1寄存器中是否正确使能了Flash写操作(WP位?需查手册)。确认Flash芯片的Vpp编程电压是否正常。- 写入失败/校验错误 :最常见的原因是 时序不匹配 。仔细核对
HLP_Bx_CTRL0/1寄存器中关于读/写访问周期、建立/保持时间的设置,与Flash数据手册的要求是否一致。可尝试稍微放宽时序(增加周期数)。- 复制后无法启动 :确认
cp.w复制的数据量足够,覆盖了U-Boot的整个镜像(包括向量表)。使用tftp下载u-boot.bin到内存,再用crc32命令计算其校验和,与烧写到Flash后的校验和对比。
5. 引导Linux内核与高级调试技巧
5.1 配置与引导Linux内核
U-Boot的终极任务是启动Linux。这需要正确设置环境变量并传递参数。
环境变量设置 :
=> setenv serverip 192.168.1.1 # TFTP服务器IP
=> setenv ipaddr 192.168.1.200 # 开发板IP
=> setenv bootfile zImage.initrd.elf # 默认下载的内核文件名
=> setenv loadaddr 0x200000 # 内核加载到内存的地址
=> saveenv # 保存到永久存储
loadaddr
是U-Boot将内核镜像(通过
tftp
)加载到内存的地址。它必须是一个
空闲的、不会覆盖U-Boot自身代码和数据
的RAM地址。
内核引导步骤 :
-
编译内核
:在主机上,使用交叉编译工具链为PowerPC架构编译Linux内核。通常需要先配置(
make taiga_defconfig),然后编译生成zImage或带初始RAM磁盘的zImage.initrd.elf。 -
放置内核
:将编译好的内核镜像(如
arch/ppc/boot/images/zImage.initrd.elf)复制到主机的TFTP服务器目录(如/tftpboot)。 -
下载内核
:在U-Boot命令行中,执行
tftp ${loadaddr} ${bootfile}。U-Boot会通过TFTP协议将文件下载到loadaddr指定的内存地址。 -
启动内核
:执行
go ${loadaddr}或更常用的bootm ${loadaddr}。bootm命令会解析uImage格式的头部信息(包含加载地址、入口点、校验和等),然后跳转到内核入口点执行。
内核参数传递
:U-Boot通过
struct bd_t
(板级信息结构体)向Linux内核传递参数,如内存起始地址和大小、波特率等。
bdinfo
命令可以查看这些信息。对于更复杂的参数(如根文件系统位置
root=
、控制台
console=
),需要通过
bootargs
环境变量设置:
=> setenv bootargs root=/dev/ram console=ttyS0,115200
=> bootm 0x200000
5.2 内存与寄存器操作实战
U-Boot提供了强大的内存操作命令,是底层调试的利器。
查看与修改内存/寄存器 :
-
md c0004000:显示从0xC0004000(Tsi108 SDRAM控制器寄存器组)开始的内存内容。由于外设寄存器是内存映射的,这等同于查看寄存器值。 -
mm 100000:进入交互式内存修改模式,从地址0x100000开始。可以依次输入新的十六进制值。这对于动态打补丁或测试小段代码非常有用。
运行自定义代码片段 :材料中展示了一个极佳的例子——在U-Boot中动态编写并运行一小段PowerPC汇编代码。
-
使用
mm命令,将汇编指令对应的机器码写入一块空闲内存(如0x100000)。 -
使用
go 100000跳转到该地址执行。 -
代码执行
blr(分支到链接寄存器)指令返回U-Boot。
这演示了如何在不重新编译的情况下,测试一个硬件操作(如写某个寄存器)或验证一个算法逻辑。 注意 :这种操作风险很高,如果代码错误(如修改了关键数据或陷入死循环),可能导致系统崩溃。务必在已知的、空闲的内存区域进行。
5.3 常见问题排查与解决思路
在移植和调试过程中,你会遇到各种各样的问题。这里总结一个排查清单:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 上电无任何串口输出 |
1. 时钟未初始化
2. 串口UART控制器未配置 3. 代码未正确加载到启动地址 |
1. 用调试器检查最早期的汇编代码(
start.S
)是否运行。
2. 检查CPU和Tsi108的时钟配置寄存器。 3. 检查UART引脚复用和波特率设置。 |
卡在
board_init_f()
或
relocate_code()
|
1. SDRAM初始化失败
2. 重定位地址计算错误 3. 代码/数据段拷贝出错 |
1. 仔细检查Tsi108 SDRAM控制器寄存器(
md c0004000
)配置,特别是时序参数。
2. 检查链接脚本
u-boot.lds
中
_start
、
_end
等符号的地址定义。
3. 在重定位代码前后加入串口打印,定位具体出错位置。 |
flinfo
命令不显示Flash信息
|
1. Flash基地址
CFG_FLASH_BASE
错误
2. HLP控制器配置错误(位宽、时序) 3. Flash驱动未正确初始化或探测失败 |
1. 用
md
命令查看
CFG_FLASH_BASE
地址是否有数据。
2. 核对
HLP_Bx_CTRL0/1
寄存器值与Flash手册。
3. 单步跟踪
flash_init()
函数,看CFI查询命令是否发出并收到响应。
|
网络
ping
不通
|
1. 网络PHY未复位或初始化
2. MAC地址未设置 3. 缓冲区对齐问题(如RTL8139) 4. 交换机/网线问题 |
1. 检查网络控制器复位引脚和软件初始化序列。
2. 检查环境变量
ethaddr
是否设置。
3. 对于RTL8139,确认DMA缓冲区对齐修改已生效。 4. 用
mii
命令(如果支持)查看PHY链路状态。
|
tftp
下载失败
|
1. 服务器IP、本机IP、网关设置错误
2. 服务器防火墙阻止TFTP(端口69) 3. 文件名或路径错误 |
1. 用
printenv
确认网络环境变量。
2. 在主机用
tcpdump
抓包,看是否有TFTP请求发出。
3. 确保TFTP服务器目录有正确权限,且文件名大小写匹配。 |
bootm
启动内核时重启或挂起
|
1. 内核镜像格式不对或损坏
2. 内核加载地址
loadaddr
与内核编译地址不匹配
3. 设备树(DTB)未传递或错误 4. 内核启动参数(
bootargs
)错误
|
1. 用
iminfo ${loadaddr}
检查uImage头部信息是否有效。
2. 确认内核编译时指定的加载地址(
LOADADDR
)与U-Boot的
loadaddr
一致。
3. 对于使用设备树的内核,确保通过
bootm <内核地址> - <设备树地址>
正确传递DTB。
4. 检查
bootargs
中的根设备、控制台参数是否正确。
|
最后的建议
:移植U-Boot是一个系统工程,耐心和细致的记录至关重要。为每一个关键的寄存器配置、每一次代码修改都做好注释。善用调试器的内存查看、断点、单步功能,并结合串口打印信息(
printf
),逐步缩小问题范围。当你看到“
## Starting kernel ...
”这行输出,并且Linux内核开始解压运行时,那一刻的成就感,是对所有努力最好的回报。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)