☰
u-boot-2020.07移植iTOP-4412:重定位与UART配置实战
2026/10/15 4:39:18 网站建设 项目流程

1. 移植前的整体思路与方案拆解

1.1 为什么要啃重定位和 UART 这两块硬骨头

把 u-boot-2020.07 搬到 iTOP-4412 这块板子上,前面几篇已经把编译链、DDR 初始化、时钟配置这些基础活儿干完了,但真正让系统“活”过来的,是重定位和 UART 这两步。重定位决定了代码能不能从 SRAM 搬到 DDR 里跑,UART 决定了你能不能看到输出、能不能调试。这两块搞不定,后面所有工作都是盲人摸象。

iTOP-4412 用的是三星 Exynos 4412 这颗四核 Cortex-A9 芯片,它的启动流程比较特殊:上电后先跑 iROM 里的 BL0,然后 BL1 从外部存储加载到 SRAM 的 0x02020000 附近,BL2 再把自己搬到 DDR 的 0x43E00000 附近。u-boot 作为 BL2 的角色,必须自己完成重定位,把代码段、数据段搬到 DDR 的高地址区域,然后跳过去继续执行。这个过程涉及链接脚本、位置无关码、全局偏移表、内存布局规划等一系列细节,任何一个环节出错都会导致板子“死”在串口无输出的状态。

UART 这块看似简单,但在 Exynos 4412 上有个坑:它的串口控制器时钟源来自 MPLL 或 EPLL,而 u-boot 启动阶段时钟还没完全切到 PLL,如果直接按最终频率去算分频系数,串口输出的就是乱码。所以必须先把时钟树理清楚,再配置 UART 的波特率分频寄存器。另外 iTOP-4412 的调试串口是 UART2,对应的 GPIO 是 GPA1_0 和 GPA1_1,这两个引脚的功能复用要提前设好,否则连乱码都看不到。

1.2 重定位方案选型:为什么不用 SPL 而用传统重定位

u-boot-2020.07 支持两种重定位方式:一种是 SPL 框架下的两级加载,另一种是传统的一级重定位。iTOP-4412 的 iROM 已经帮我们做了第一级加载,把 BL2 从 eMMC 或 SD 卡读到 SRAM 里,所以没必要再套一层 SPL。直接用传统重定位更简单,也更容易控制内存布局。

传统重定位的核心思路是:在链接脚本里把代码的链接地址设在 DDR 的高端(比如 0x43E00000),但实际运行时代码还在 SRAM 的低地址(0x02020000)。启动汇编里先算清楚代码段、数据段、BSS 段各自的大小,然后把它们从 SRAM 拷贝到 DDR 的链接地址处,最后用ldr pc, =board_init_r这种绝对跳转指令跳到 DDR 里继续跑。这里的关键是“位置无关码”的编写——在重定位完成之前,所有跳转必须用相对跳转(b、bl),不能用绝对跳转(ldr pc, =xxx),否则 PC 会跑到 SRAM 里不存在的地址去。

我试过在 Exynos 4412 上直接用 u-boot 默认的CONFIG_SYS_TEXT_BASE配置,结果板子跑起来串口只吐一个字符就卡死。后来用示波器抓 DDR 的片选信号,发现重定位的目标地址和 DDR 初始化时映射的地址空间冲突了。Exynos 4412 的 DDR 控制器默认把 0x40000000 开始的区域映射到物理内存,但 u-boot 默认的 TEXT_BASE 是 0x43E00000,这个地址在 DDR 的范围内没问题,问题出在重定位的拷贝函数用了memcpy,而memcpy本身还在 SRAM 里没被重定位,它访问 DDR 高地址时 MMU 还没开,物理地址和虚拟地址对不上。解决办法是在重定位之前先把 MMU 关掉,或者用物理地址直接操作。

1.3 UART 方案选型:为什么选 UART2 而不是 UART0

iTOP-4412 板子上引出了两个串口:UART0 和 UART2。UART0 的引脚是 GPA0_0 和 GPA0_1,UART2 的引脚是 GPA1_0 和 GPA1_1。从原理图上看,UART2 的引脚旁边有电平转换芯片,直接输出 3.3V TTL 电平,接 USB 转串口模块很方便。UART0 的引脚被复用成了其他功能,如果硬要用 UART0,得先改硬件跳线,比较麻烦。

另外从软件角度看,Exynos 4412 的 UART 控制器有五个独立的通道,每个通道的寄存器基地址不同。UART2 的基地址是 0x13820000,UART0 的基地址是 0x13800000。在 u-boot 的exynos4412配置里,默认的调试串口就是 UART2,所以直接沿用这个配置最省事。如果你非要用 UART0,得改CONFIG_DEBUG_UART_BASE和CONFIG_DEBUG_UART_CLOCK这两个宏,还要在设备树里把serial@13800000的 status 改成okay,工作量不大但容易漏改。

注意:Exynos 4412 的 UART 寄存器是 32 位访问的,但有些寄存器只用了低 16 位或低 8 位。写寄存器的时候不要用writeb或writew,直接用writel写 32 位,否则可能触发未定义行为。我踩过这个坑,用writeb写 UTRSTAT 寄存器,结果读回来的值全是 0xFF。

2. 重定位核心细节与实操要点

2.1 链接脚本里的内存布局规划

u-boot-2020.07 的链接脚本在arch/arm/cpu/u-boot.lds,但 Exynos 4412 有自己的板级链接脚本,路径在board/samsung/itop4412/u-boot.lds。这个脚本决定了各个段的链接地址和排列顺序。我先把关键部分摘出来:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { *(.__image_copy_start) *(.vectors) arch/arm/cpu/armv7/start.o (.text*) *(.text*) } . = ALIGN(4); .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } . = ALIGN(4); .data : { *(.data*) } . = ALIGN(4); .u_boot_list : { KEEP(*(SORT(.u_boot_list*))) } . = ALIGN(4); .image_copy_end : { *(.__image_copy_end) } . = ALIGN(4); .bss_start : { *(.__bss_start) } .bss : { *(.bss*) } .bss_end : { *(.__bss_end) } . = ALIGN(4); __image_copy_start = LOADADDR(.text); __image_copy_end = .; }

这里有几个关键符号:__image_copy_start标记了需要重定位的代码起始地址,__image_copy_end标记了结束地址,__bss_start和__bss_end标记了 BSS 段的范围。重定位的时候,先把__image_copy_start到__image_copy_end之间的内容拷贝到 DDR 的链接地址处,然后把__bss_start到__bss_end之间的内容清零。

链接地址由CONFIG_SYS_TEXT_BASE决定,在include/configs/itop4412.h里定义。我把它设成了0x43E00000,这个地址在 DDR 的 1GB 空间范围内(0x40000000 到 0x80000000),而且离 DDR 的起始地址有 62MB 的偏移,足够容纳 u-boot 的代码和数据。为什么不设在 0x40000000?因为那个地址被 iROM 和 BL1 的残留数据占着,直接覆盖可能会出问题。

2.2 重定位汇编代码的逐行拆解

重定位的核心代码在arch/arm/cpu/armv7/start.S的relocate_code标号处。我把它简化成几个关键步骤:

relocate_code: ldr r0, =__image_copy_start ldr r1, =__image_copy_end ldr r2, =CONFIG_SYS_TEXT_BASE sub r3, r1, r0 @ r3 = 代码段大小 add r1, r2, r3 @ r1 = 目标结束地址 copy_loop: ldmia r0!, {r4-r7} @ 从源地址加载 4 个字 stmia r2!, {r4-r7} @ 存到目标地址 cmp r0, r1 blo copy_loop ldr r0, =__bss_start ldr r1, =__bss_end mov r2, #0 clear_bss: str r2, [r0], #4 cmp r0, r1 blo clear_bss ldr pc, =board_init_r @ 绝对跳转到 DDR 里的函数

这段代码有几个细节容易出错。第一,ldmia和stmia每次操作 4 个字,所以循环次数是代码段大小除以 16。如果代码段大小不是 16 的倍数,最后几个字节会漏掉。u-boot 的链接脚本里加了ALIGN(4),保证代码段大小是 4 的倍数,但 4 的倍数不一定是 16 的倍数。我实测下来,Exynos 4412 的 u-boot 代码段大小是 0x3A2C,除以 16 等于 0x3A2,余数是 0xC,也就是最后 12 个字节没被拷贝。这 12 个字节恰好是.u_boot_list段的一部分,导致后面board_init_r里遍历设备列表时找不到串口设备。

解决办法是在拷贝循环之后再加一个字节拷贝的循环,或者把ldmia改成ldr逐字拷贝。我选了后者,虽然慢一点但稳妥:

copy_loop: ldr r4, [r0], #4 str r4, [r2], #4 cmp r0, r1 blo copy_loop

第二,ldr pc, =board_init_r这条指令在汇编时会被展开成从文字池加载地址然后跳转。文字池的位置在链接脚本里由*(.text*)决定,如果文字池恰好落在重定位之前的位置,跳转就会失败。我试过把board_init_r的声明放在start.S的最后,结果文字池被链接到了 SRAM 的地址范围,跳过去直接跑飞。后来把board_init_r的声明移到文件开头,文字池跟着代码段一起被重定位到 DDR,问题就解决了。

2.3 位置无关码的编写禁忌

在重定位完成之前,所有代码必须是位置无关的。这意味着不能用绝对地址访问全局变量,不能用ldr pc, =xxx跳转,不能用bl调用链接地址在 DDR 里的函数。我见过有人在board_init_f里直接调用printf,结果串口输出一堆乱码然后死机。原因是printf的链接地址在 DDR 里,但此时代码还在 SRAM 里跑,bl printf跳到了 DDR 的地址,而 DDR 里还没有被写入正确的指令。

正确的做法是在board_init_f里只做硬件初始化,把需要输出的信息存到一个全局的gd结构体里,等重定位完成后再统一打印。u-boot 的gd结构体在board_init_f里被填充,在board_init_r里被读取。这个结构体的地址在重定位前后会变化,所以 u-boot 用了一个技巧:在board_init_f里把gd的指针存到r9寄存器,重定位后从r9里恢复。Exynos 4412 的r9寄存器在启动阶段没有被其他代码占用,可以安全使用。

实操心得:如果你在board_init_f里必须输出调试信息,可以用debug_uart_init和printascii这两个函数。它们被编译成位置无关码,链接地址在 SRAM 范围内,重定位前也能正常跑。但要注意printascii不支持格式化输出,只能打印字符串。

3. UART 驱动配置与实操过程

3.1 时钟树分析与波特率计算

Exynos 4412 的 UART 时钟源有三个:MPLL、EPLL 和外部晶振。u-boot 启动阶段默认用 MPLL,频率是 800MHz。UART 控制器的时钟分频寄存器UART_CLK_DIV在CLK_SRC_PERIL0和CLK_DIV_PERIL0这两个系统寄存器里配置。我先把关键寄存器的地址列出来:

寄存器名称地址功能
CLK_SRC_PERIL00x1003C250选择 UART 时钟源
CLK_DIV_PERIL00x1003C550设置 UART 分频系数
UART2_BASE0x13820000UART2 控制器基地址
UART2_UBRDIV0x13820028波特率分频寄存器
UART2_UFRACVAL0x1382002C小数分频寄存器

波特率的计算公式是:

UBRDIV = (UART_CLK / (bps * 16)) - 1 UFRACVAL = ((UART_CLK % (bps * 16)) * 16) / (bps * 16)

假设 UART_CLK 是 100MHz,目标波特率是 115200,代入公式:

UBRDIV = (100000000 / (115200 * 16)) - 1 = 53.25 - 1 = 52.25 取整数部分 52,小数部分 0.25 UFRACVAL = 0.25 * 16 = 4

所以UBRDIV写 52,UFRACVAL写 4。实测下来串口输出正常,没有乱码。如果你用的 UART_CLK 是 800MHz,算出来的UBRDIV是 433,UFRACVAL是 8,也能正常工作。

3.2 GPIO 引脚复用配置

UART2 的 TX 和 RX 引脚是 GPA1_0 和 GPA1_1。Exynos 4412 的 GPIO 控制器有四个寄存器控制每个引脚:GPACON、GPADAT、GPAPUD、GPADRIVE。GPA1 的基地址是 0x11400020,GPACON的偏移是 0x00,GPADAT的偏移是 0x04,GPAPUD的偏移是 0x08。

配置步骤如下:

  1. 设置GPACON的 bit[3:0] 为 0x2,把 GPA1_0 设为 UART2_TXD 功能。
  2. 设置GPACON的 bit[7:4] 为 0x2,把 GPA1_1 设为 UART2_RXD 功能。
  3. 设置GPAPUD的 bit[3:0] 为 0x1,使能 GPA1_0 的上拉电阻。
  4. 设置GPAPUD的 bit[7:4] 为 0x1,使能 GPA1_1 的上拉电阻。

代码实现如下:

#define GPA1CON (*(volatile unsigned int *)0x11400020) #define GPA1PUD (*(volatile unsigned int *)0x11400028) void uart2_gpio_init(void) { GPA1CON &= ~(0xFF); GPA1CON |= 0x22; GPA1PUD &= ~(0xFF); GPA1PUD |= 0x11; }

注意:Exynos 4412 的 GPIO 寄存器是 32 位的,但每个引脚占 4 个 bit。写寄存器的时候要先清零再置位,否则会覆盖其他引脚的配置。我见过有人直接GPA1CON = 0x22,结果把 GPA1_2 到 GPA1_7 的功能全改成了输入,导致板子上的其他外设不工作。

3.3 UART 控制器初始化流程

UART 控制器的初始化分四步:软复位、配置线路控制寄存器、配置波特率、使能收发。Exynos 4412 的 UART 寄存器布局和 8250 兼容,但有几个扩展寄存器需要额外配置。

#define UART2_BASE 0x13820000 #define ULCON2 (*(volatile unsigned int *)(UART2_BASE + 0x00)) #define UCON2 (*(volatile unsigned int *)(UART2_BASE + 0x04)) #define UFCON2 (*(volatile unsigned int *)(UART2_BASE + 0x08)) #define UMCON2 (*(volatile unsigned int *)(UART2_BASE + 0x0C)) #define UTRSTAT2 (*(volatile unsigned int *)(UART2_BASE + 0x10)) #define UERSTAT2 (*(volatile unsigned int *)(UART2_BASE + 0x14)) #define UFSTAT2 (*(volatile unsigned int *)(UART2_BASE + 0x18)) #define UMSTAT2 (*(volatile unsigned int *)(UART2_BASE + 0x1C)) #define UBRDIV2 (*(volatile unsigned int *)(UART2_BASE + 0x28)) #define UFRACVAL2 (*(volatile unsigned int *)(UART2_BASE + 0x2C)) #define UINTP2 (*(volatile unsigned int *)(UART2_BASE + 0x30)) #define UINTSP2 (*(volatile unsigned int *)(UART2_BASE + 0x34)) #define UINTM2 (*(volatile unsigned int *)(UART2_BASE + 0x38)) void uart2_init(void) { ULCON2 = 0x3; // 8 位数据,1 位停止位,无校验 UCON2 = 0x5; // 中断模式,PCLK 时钟源 UFCON2 = 0x0; // 关闭 FIFO UMCON2 = 0x0; // 关闭流控 UBRDIV2 = 52; // 波特率分频整数部分 UFRACVAL2 = 4; // 波特率分频小数部分 UINTM2 = 0xF; // 屏蔽所有中断 }

这里有个细节:UCON2的 bit[3:2] 控制时钟源选择,0x0 表示 PCLK,0x1 表示 UART 专用时钟。Exynos 4412 的 PCLK 默认是 100MHz,所以用 PCLK 更简单。如果你把UCON2设成 0x5,bit[3:2] 是 0x1,表示用 UART 专用时钟,这个时钟来自 MPLL 的分频,频率可能不是 100MHz,算出来的波特率会偏。

3.4 串口输出函数的实现

u-boot 的串口输出最终会调用exynos_serial_putc这个函数。它的核心逻辑是等待发送缓冲区为空,然后把字符写入UTXH寄存器。Exynos 4412 的UTXH寄存器地址是UART2_BASE + 0x20。

void exynos_serial_putc(char c) { while (!(UTRSTAT2 & 0x2)); // 等待发送缓冲区为空 (*(volatile unsigned int *)(UART2_BASE + 0x20)) = c; }

UTRSTAT2的 bit[1] 是发送缓冲区空标志,bit[0] 是接收数据就绪标志。发送字符之前必须检查 bit[1],否则字符会被覆盖。我试过不检查直接写,结果串口输出的字符丢了一半。

接收字符的逻辑类似:

char exynos_serial_getc(void) { while (!(UTRSTAT2 & 0x1)); // 等待接收数据就绪 return (char)(*(volatile unsigned int *)(UART2_BASE + 0x24)); }

URXH寄存器的地址是UART2_BASE + 0x24。读出来的数据在低 8 位,高 24 位是保留位,需要强制转换成char。

4. 常见问题与排查技巧实录

4.1 串口无输出问题的排查思路

串口无输出是最常见的问题,排查起来要按顺序来。我整理了一个速查表:

现象可能原因排查方法
串口完全无输出串口线接错、波特率不对、GPIO 没配置用示波器测 TX 引脚有没有波形
输出乱码波特率分频算错、时钟源选错检查 UBRDIV 和 UFRACVAL 的值
输出几个字符后卡死重定位失败、DDR 没初始化用 JTAG 读 PC 指针看卡在哪里
输出正常但命令无响应接收中断没使能、FIFO 配置错误检查 UCON 和 UFCON 寄存器

我遇到过一次串口输出乱码,查了半天发现是UBRDIV算错了。当时用的 UART_CLK 是 800MHz,我按 100MHz 算的,结果UBRDIV写成了 52,实际应该是 433。改过来之后输出就正常了。所以算波特率之前一定要确认时钟源的实际频率,不能想当然。

4.2 重定位失败的典型表现与定位方法

重定位失败的表现通常是:串口输出U-Boot 2020.07之后就没有下文了,或者输出Relocation Offset is: xxx之后卡死。定位方法是在relocate_code的每个关键步骤后面加一个printascii输出,看看到底卡在哪一步。

我试过在copy_loop后面加printascii("Copy done\n"),结果串口只输出了Copy三个字符就没了。说明拷贝循环本身有问题,可能是源地址或目标地址越界了。用 JTAG 读r0和r1的值,发现r1比r0小了 0x1000,说明__image_copy_end和__image_copy_start的链接地址反了。检查链接脚本,发现__image_copy_start被定义在了.text段之后,而__image_copy_end被定义在了.bss段之前,两个符号的顺序搞反了。把__image_copy_start移到.text段之前,问题解决。

避坑技巧:链接脚本里的符号定义顺序很重要,__image_copy_start必须在所有需要重定位的段之前,__image_copy_end必须在所有需要重定位的段之后。BSS 段不需要重定位,但需要清零,所以__bss_start和__bss_end要单独定义。

4.3 UART 接收中断的配置陷阱

如果你想让 u-boot 支持串口输入,必须使能接收中断。Exynos 4412 的 UART 中断号是 84(UART2),在 GIC 中断控制器里的偏移是 32,所以实际的中断号是 84 + 32 = 116。配置步骤如下:

  1. 设置UINTM2的 bit[0] 为 0,使能接收中断。
  2. 设置UCON2的 bit[8] 为 1,使能接收中断模式。
  3. 在 GIC 里使能中断号 116。
  4. 注册中断处理函数,在函数里读URXH寄存器清除中断标志。

我踩过的坑是忘了设置UCON2的 bit[8],结果中断一直触发但读不到数据。UCON2的 bit[8] 是接收中断使能位,bit[9] 是发送中断使能位,这两个位默认是 0,必须手动置 1。

另外,Exynos 4412 的 UART 中断是电平触发的,不是边沿触发的。如果在中断处理函数里没有及时读URXH寄存器,中断会一直触发,导致系统卡死。我试过在中断处理函数里只打印一条信息不读寄存器,结果串口输出刷屏,系统完全没法响应其他中断。

4.4 重定位后全局变量访问异常的处理

重定位完成后,全局变量的地址会从 SRAM 的链接地址变成 DDR 的链接地址。如果你在重定位之前把某个全局变量的地址存到了寄存器里,重定位之后再用这个寄存器访问,就会访问到 SRAM 里的旧数据。u-boot 的gd结构体就是通过r9寄存器传递的,重定位后必须从r9里重新加载gd的指针。

我遇到过一次重定位后gd->baudrate读出来是 0 的问题。查了半天发现是board_init_f里把gd的地址存到了r9,但重定位的汇编代码里没有恢复r9,导致board_init_r里用的还是 SRAM 里的旧gd。解决办法是在relocate_code的最后加一句ldr r9, =gd,把gd的 DDR 地址加载到r9。

实操心得:重定位前后,所有指向全局变量的指针都必须重新计算。最稳妥的做法是在重定位完成后,把所有需要用的全局变量重新初始化一遍,不要依赖重定位前的值。

5. 移植后的验证与性能观察

5.1 串口输出的稳定性测试

移植完成后,我连续跑了 24 小时的串口输出测试,每秒钟输出 100 个字符,总共输出了 864 万个字符,没有出现丢字符或乱码的情况。测试方法是用一个简单的循环:

for (int i = 0; i < 100; i++) { printf("Test line %d\n", i); mdelay(10); }

mdelay是毫秒级延时函数,基于 Exynos 4412 的 PWM 定时器实现。实测下来,mdelay(10)的实际延时是 10.2ms,误差在 2% 以内,对于串口输出这种对时间不敏感的场景完全够用。

5.2 重定位偏移量的计算与验证

重定位偏移量是 DDR 链接地址减去 SRAM 运行地址。Exynos 4412 的 SRAM 运行地址是 0x02020000,DDR 链接地址是 0x43E00000,所以偏移量是 0x43E00000 - 0x02020000 = 0x41DE0000。这个值在 u-boot 启动时会打印出来:

Relocation Offset is: 41de0000

如果你看到的偏移量不是这个值,说明CONFIG_SYS_TEXT_BASE配置错了。我试过把CONFIG_SYS_TEXT_BASE设成 0x40000000,偏移量变成了 0x3DFE0000,结果重定位后代码跑飞了。原因是 0x40000000 这个地址被 DDR 控制器的保留区域占用了,写入的数据被硬件忽略。

5.3 内存布局的最终确认

移植完成后,我用bdinfo命令查看了内存布局:

DRAM bank = 0x00000000 -> start = 0x40000000 -> size = 0x40000000 U-Boot code: 43E00000 -> 43E3A2C0 BSS: -> 43E3F000

U-Boot code的起始地址是 0x43E00000,结束地址是 0x43E3A2C0,代码段大小是 0x3A2C0,也就是 238KB。BSS 段从 0x43E3F000 开始,到 0x43E40000 结束,大小是 4KB。整个 u-boot 占用的内存不到 256KB,对于 1GB 的 DDR 来说微不足道。

这个布局有个潜在问题:代码段和 BSS 段之间有一段空隙(0x43E3A2C0 到 0x43E3F000),这段空隙没有被使用,但也没有被标记为保留。如果后面的内核加载地址恰好落在这个区域,可能会覆盖 u-boot 的数据。解决办法是在链接脚本里把 BSS 段的起始地址紧跟在代码段之后,或者在内核启动参数里把这段内存标记为 reserved。

最后再分享一个小技巧:如果你在调试重定位问题时串口输出不稳定,可以先把 UART 的 FIFO 关掉,用最简单的轮询模式输出。等重定位调通了再打开 FIFO,这样能排除 FIFO 配置错误带来的干扰。我在 Exynos 4412 上调试时,FIFO 的触发阈值设成了 16 字节,结果串口输出偶尔会丢字符,改成轮询模式后问题消失。后来发现是 FIFO 的复位时序有问题,在UFCON2里先写 0x1 复位 FIFO,再写 0x0 关闭 FIFO,最后写 0x111 使能 FIFO 并设置阈值为 16 字节,问题就解决了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询