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() 或相关初始化序列调用。它是我们进行 板级特定硬件初始化 的核心位置。在这里,我们按顺序初始化:

  1. GPIO和系统时钟 :配置引脚复用和基本时钟。
  2. 内存控制器 :配置Tsi108的HLP和SDRAM控制器寄存器,让CPU能正确访问内存和Flash。这就是代码中设置 HLP_Bx_CTRL0/1 的部分。
  3. 地址映射 :配置 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位是独立的位)。
  4. PCI子系统 :初始化PCI控制器,配置空间、I/O空间和内存空间的基地址。
  5. 网络设备 :初始化RTL8139 PCI网卡或Tsi108内置GbE控制器。
  6. 环境变量 :初始化存储环境变量的介质(如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信息,请按以下顺序排查:

  1. 确认 CFG_FLASH_BASE 地址是否正确。用 md 命令查看该地址,在未初始化时可能全是 0 或 ff ,初始化后应能看到CFI信息或已编程的数据。
  2. 检查 board_early_init_r() 中HLP控制器的配置,特别是位宽( HLP_Bx_CTRL0 的最后几位)和时序参数是否与Flash芯片手册匹配。
  3. 确认是否正确定义了 CFG_FS2_FLASH_CFI_DRIVER ,并实现了必要的板级函数(如 flash_init )。
  4. 使用仿真器(如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控制器(主要方案) : 启用它需要:

  1. 在 include/configs/FS2.h 中定义 CONFIG_TSI108_ETH 。
  2. 确保板级驱动文件 board/Freescale/freeserve2/tsi108_eth.c 存在并正确实现了初始化函数。
  3. 在 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启动分为两阶段:

  1. ROM/Flash运行阶段 :代码从Flash(或通过PROMjet映射)的物理地址(如 0xFFF00000 )开始执行。此时,调试器(COP)显示的PC地址与 u-boot.dis 文件中的地址是 直接对应 的。
  2. 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的完美范例。我们来拆解其每一步的意图和原理:

  1. 查看Flash信息 ( flinfo ):确认Flash型号、大小、扇区布局,并查看保护状态(显示 RO 表示只读,受保护)。
  2. 解除写保护 ( protect off all ):Flash的每个扇区默认可能有硬件或软件写保护,此命令发送特定解锁序列,解除保护以便擦写。
  3. 擦除目标区域 ( erase all ):Flash写入前必须先擦除(变为全 0xFF )。这是一个相对耗时的操作,需要等待。
  4. 验证源数据 ( md ff000000 ):查看PROMjet中U-Boot镜像的起始内容,确认镜像正确。
  5. 复制镜像 ( cp.w ff000000 fe000000 b100 ):这是最关键的一步。 cp.w 是按字(Word)复制。 b100 是十六进制,表示复制 0xB100 个字,即 45248 个字,约 180KB ,需大于实际U-Boot.bin的大小。这里复制的是 代码的二进制内容 。
  6. 验证写入 ( md fe000000 ):读取Flash起始地址,与步骤4的结果对比,确保数据一致。
  7. 重新上电并从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地址。

内核引导步骤 :

  1. 编译内核 :在主机上,使用交叉编译工具链为PowerPC架构编译Linux内核。通常需要先配置( make taiga_defconfig ),然后编译生成 zImage 或带初始RAM磁盘的 zImage.initrd.elf 。
  2. 放置内核 :将编译好的内核镜像(如 arch/ppc/boot/images/zImage.initrd.elf )复制到主机的TFTP服务器目录(如 /tftpboot )。
  3. 下载内核 :在U-Boot命令行中,执行 tftp ${loadaddr} ${bootfile} 。U-Boot会通过TFTP协议将文件下载到 loadaddr 指定的内存地址。
  4. 启动内核 :执行 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汇编代码。

  1. 使用 mm 命令,将汇编指令对应的机器码写入一块空闲内存(如 0x100000 )。
  2. 使用 go 100000 跳转到该地址执行。
  3. 代码执行 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内核开始解压运行时,那一刻的成就感,是对所有努力最好的回报。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐