资讯动态

iTOP-4412 U-Boot重定位与UART串口移植调试实战

发布时间:2026/10/15 20:03:03 来源:尧图企业网站定制
1. 从链接脚本到代码搬运重定位到底在解决什么问题移植 u-boot 到一块新板子最让人心里没底的就是重定位这一段。前面把时钟、DDR 初始化跑通之后串口能打印出第一行字符很多人就以为大功告成了结果一开重定位要么直接跑飞要么打印到一半卡死。我在 iTOP-4412 这块板子上前后折腾了好几轮才把这段逻辑彻底理顺。先说清楚重定位要解决的核心矛盾。u-boot 编译出来的时候链接地址也就是CONFIG_SYS_TEXT_BASE通常设成一个固定的值比如 0x43e00000 或者 0x40008000 这类。但板子上电后代码实际运行的物理位置和这个链接地址往往对不上。以 iTOP-4412 为例它用的是三星 Exynos 4412 这颗四核 Cortex-A9上电后先跑的是芯片内部固化的 BL1然后 BL2 把 u-boot 的 SPL 或者完整镜像加载到 DDR 里的某个位置。如果加载位置和链接地址不一致代码里所有用绝对地址寻址的跳转、全局变量访问、函数指针调用全部会指向错误的地方。重定位干的事情说白了就是把 u-boot 从当前实际所在的位置整体搬到链接时约定的位置然后修正运行环境让 CPU 从新位置继续执行。这个过程听起来简单但涉及几个关键点搬多少、搬到哪、搬完之后怎么跳过去、跳过去之前要准备什么。在 Exynos 4412 这种 ARMv7 架构上重定位的实现和 ARMv8 或者 RISC-V 有本质区别。ARMv7 没有像 ARMv8 那样丰富的重定位类型u-boot 2020.07 里针对 ARM 的重定位主要靠relocate_code这个汇编函数完成。它做的事情是把__image_copy_start到__image_copy_end之间的内容整块拷贝到目标地址然后处理 GOT 表如果开启了CONFIG_SYS_RELOC_GD_ENV_ADDR之类的选项最后通过计算偏移量跳转到新地址的board_init_r。这里有个容易踩的坑很多人以为重定位就是把整个 u-boot 镜像搬过去实际上链接脚本里__image_copy_start和__image_copy_end之间的内容才是需要搬运的。.bss段、堆栈区域、以及一些标记为NOLOAD的段是不搬的搬过去反而会出问题。我在第一次改链接脚本的时候手一抖把.bss也圈进了拷贝范围结果重定位之后全局变量全变成随机值串口打印出来的东西驴唇不对马嘴查了两天才发现是这里的问题。还有一个关键概念是gdglobal data指针。u-boot 里大量使用gd-来访问全局数据这个指针在重定位前后必须正确更新。在 ARMv7 上gd通常放在寄存器 r9 里取决于编译选项重定位代码里会重新计算它的位置。如果你在board_init_f里往gd里塞了东西重定位之后发现读出来是乱码八成是gd指针没更新对。理解重定位的本质可以类比成搬家你的家具代码和数据原来堆在一个临时仓库当前加载地址现在要搬进新房子链接地址。搬家之前你得知道哪些家具要搬拷贝范围新房子门牌号是多少目标地址搬完之后怎么找到新家的门跳转地址以及搬家过程中哪些东西不能碰比如正在使用的堆栈。把这几个问题想清楚重定位的代码就不难理解了。2. iTOP-4412 上重定位的地址规划与链接脚本改造iTOP-4412 这块板子的内存布局有它自己的特点不能照搬其他 Exynos 板子的配置。Exynos 4412 的 DRAM 起始地址是 0x40000000iTOP-4412 板载 1GB DDR3所以可用范围大致是 0x40000000 到 0x80000000。u-boot 的链接地址和加载地址怎么定直接决定了重定位能不能顺利跑通。先看链接地址。在include/configs/itop_4412.h或者类似的板级配置头文件里CONFIG_SYS_TEXT_BASE决定了 u-boot 链接时假设自己住在哪。常见的做法是把它设成 0x43e00000这个地址在 DDR 范围内且留出了足够的空间给前面的 BL1、BL2 以及可能存在的 TrustZone 固件。为什么选 0x43e00000 而不是 0x40008000因为 Exynos 4412 上电后BL1 会把自己搬到 IRAM内部 SRAM里运行BL2 则可能加载到 DDR 的低地址区域。如果 u-boot 链接在 0x40008000很容易和 BL2 或者 ATF 之类的固件打架。0x43e00000 这个地址距离 DRAM 起始有将近 62MB 的偏移避开了低端区域相对安全。但链接地址是一回事实际加载地址又是另一回事。iTOP-4412 上u-boot 的镜像通常由 BL2 从存储介质eMMC 或 SD 卡读取并放到 DDR 的某个位置。这个位置可能是 0x40008000也可能是 0x43e00000 本身。如果加载地址恰好等于链接地址那重定位其实可以跳过但 u-boot 的流程里通常还是会走一遍重定位逻辑只是拷贝的源和目的相同相当于原地不动。如果加载地址和链接地址不同重定位就必须正确执行。链接脚本arch/arm/cpu/u-boot.lds里定义了__image_copy_start和__image_copy_end这两个符号它们标记了需要搬运的代码和数据范围。在 u-boot 2020.07 里这个范围通常包括.text、.rodata、.data、.got等段但不包括.bss和堆栈。我在 iTOP-4412 上改链接脚本的时候特意检查了.got段的位置。如果开启了CONFIG_SYS_RELOC_GD_ENV_ADDR或者使用了位置无关代码相关的选项GOT 表必须在拷贝范围内否则重定位后访问全局偏移表会出错。地址规划里还有一个隐藏的坑DDR 的可用范围。iTOP-4412 虽然标称 1GB但实际可用范围受限于芯片的地址映射。Exynos 4412 的 DRAM 控制器可能把某些地址段映射给其他外设或者保留。我在配置CONFIG_SYS_SDRAM_BASE和CONFIG_SYS_SDRAM_SIZE的时候一开始照抄了参考板的 0x40000000 和 0x400000001GB结果重定位到高端地址时访问异常。后来查了芯片手册才发现iTOP-4412 的 DDR 实际可用范围是 0x40000000 到 0x7FFFFFFF但中间有些地址段被保留给显示控制器或者安全固件。把CONFIG_SYS_SDRAM_SIZE改成 0x3FFFFFFF 之后才稳定。链接脚本改造的另一个重点是栈指针的设置。在board_init_f阶段u-boot 使用的是临时栈通常放在 IRAM 或者 DDR 的低地址区域。重定位之后栈指针要切换到新的位置通常是在 DDR 的高端地址往下留出一段空间。iTOP-4412 上我把栈顶设在 0x7FE00000 附近留出 2MB 给栈空间避免和 u-boot 镜像本身重叠。这个地址不是随便定的要确保它在 DDR 可用范围内且不会和重定位后的 u-boot 镜像、malloc 区域冲突。配置项典型值说明CONFIG_SYS_TEXT_BASE0x43e00000u-boot 链接地址CONFIG_SYS_SDRAM_BASE0x40000000DDR 起始地址CONFIG_SYS_SDRAM_SIZE0x3FFFFFFF实际可用 DDR 大小栈顶地址0x7FE00000重定位后栈指针初始值加载地址由 BL2 决定通常为 0x40008000 或 0x43e00000改完链接脚本和配置头文件之后编译出来的 u-boot 镜像里__image_copy_start和__image_copy_end的地址可以通过nm命令查看。我习惯在编译后跑一遍arm-none-eabi-nm u-boot | grep image_copy确认这两个符号的地址差值和预期一致。如果差值明显偏大可能是某些不该被拷贝的段被圈进来了如果偏小可能是.got或者.data没被正确包含。3. relocate_code 的执行链路与调试手段relocate_code这个函数在arch/arm/lib/relocate.S里是重定位的核心。它的调用链路大致是board_init_f里计算好目标地址和拷贝范围然后调用relocate_code传入目标地址、源地址、以及gd指针的偏移信息。在 ARMv7 上这个函数用汇编写成因为 C 语言没法直接操作 PC 和栈指针的切换。函数一开始会保存一些寄存器然后根据传入的参数计算拷贝的源和目的。拷贝过程通常用ldmia和stmia指令批量搬运效率比逐字节拷贝高得多。拷贝完成后它会处理 GOT 表如果配置了的话然后计算跳转地址。跳转地址的计算方式是目标地址加上board_init_r相对于__image_copy_start的偏移。这个偏移在链接时就已经确定了所以重定位代码只需要做一次加法。调试重定位最有效的手段是串口打印。但这里有个鸡生蛋蛋生鸡的问题重定位之前串口可能还没初始化重定位之后串口寄存器地址可能变了。我的做法是在board_init_f里先把串口初始化好确保重定位前后都能打印。iTOP-4412 的串口是 Exynos 4412 内置的 UART寄存器基地址在 0x13800000 附近。这个地址是物理地址不随重定位变化所以串口在重定位前后都能正常工作。在relocate_code的关键位置插入打印语句可以清楚地看到拷贝的源地址、目的地址、拷贝长度以及跳转前的寄存器状态。我通常会在拷贝开始前打印一行 Relocating from 0x... to 0x..., size 0x...拷贝完成后再打印一行 Relocation done, jumping to 0x...。如果串口在第二行打印之后就没了输出说明跳转目标地址算错了或者新地址处的代码有问题。另一个调试手段是点灯。iTOP-4412 板子上有可用的 GPIO LED在重定位前后翻转 GPIO 电平用示波器或者肉眼观察 LED 状态可以判断代码执行到了哪一步。这种方法不依赖串口在串口还没调通的时候特别有用。我在第一次调重定位的时候串口死活不出字符就是靠 LED 判断出代码卡在了拷贝循环里——后来发现是拷贝长度算错了多拷贝了一大块把 DDR 里其他数据覆盖了。还有一个容易被忽略的点缓存和 MMU。在重定位之前u-boot 通常还没开启 MMU 和 D-Cache代码运行在物理地址上。重定位之后如果开启了 MMU虚拟地址和物理地址的映射关系会变化跳转地址需要做相应转换。iTOP-4412 上我一开始没开 MMU重定位直接跑物理地址简单可靠。后来为了性能开了 MMU结果重定位后跳转到了错误的虚拟地址查了半天才发现是页表映射没覆盖到新的代码位置。提示在重定位调试阶段建议先关闭 MMU 和 D-Cache用纯物理地址跑通流程再逐步开启高级特性。这样能把问题范围缩小到重定位逻辑本身而不是被缓存一致性问题干扰。relocate_code执行完成后会跳转到board_init_r。这个函数运行在新地址上负责后续的外设初始化、环境变量加载、命令解析等。如果重定位成功但board_init_r里出问题通常表现为串口打印了一部分初始化信息后卡死或者进入命令行后执行命令异常。这类问题多半是gd指针没更新对或者某些全局变量的地址在重定位后没有正确修正。4. UART 初始化的时钟依赖与寄存器配置串口是移植过程中最重要的调试工具没有之一。iTOP-4412 用的是 Exynos 4412 内置的 UART 控制器支持多个串口通道通常把 UART2 或者 UART3 作为调试串口引出。要让串口正常工作核心是三件事时钟使能、引脚复用、波特率配置。时钟使能是第一步。Exynos 4412 的外设时钟由 CMUClock Management Unit管理UART 的时钟源通常来自 PLL 或者外部晶振。在board_init_f阶段时钟树可能还没完全初始化所以串口初始化要放在时钟配置之后。我在 iTOP-4412 上把串口初始化放在clock_init之后、board_init_f的早期阶段确保后续所有打印都能正常输出。如果时钟没使能就去写 UART 寄存器写进去的值读出来全是 0串口自然没反应。引脚复用是第二步。Exynos 4412 的引脚功能是复用的UART 的 TX、RX 引脚需要配置成对应的 AFAlternate Function模式。在 u-boot 里这通常通过gpio或者pinctrl驱动完成。iTOP-4412 的调试串口引脚在 GPA0 或者 GPA1 组具体要看原理图。我一开始没配引脚复用串口寄存器配置全对但就是不出字符后来查了原理图才发现引脚还处于默认的 GPIO 输入模式。波特率配置是第三步也是最容易出错的一步。Exynos 4412 的 UART 波特率计算公式是UBRDIV UFRACVAL/16 时钟源频率 / (波特率 * 16) - 1。这里时钟源频率取决于前面时钟树里给 UART 分频的结果。如果时钟源是 100MHz目标波特率是 115200那么UBRDIV大约是 53UFRACVAL根据小数部分计算。我在第一次配的时候时钟源频率算错了导致波特率偏差太大串口输出全是乱码。后来用示波器量了 TX 引脚的实际波形才反推出时钟源频率不对。参数值说明时钟源频率100MHzUART 模块输入时钟目标波特率115200调试串口常用波特率UBRDIV53整数分频值UFRACVAL1小数分频值数据位8标准配置停止位1标准配置校验位无标准配置u-boot 2020.07 里串口驱动通常放在drivers/serial/serial_s5p.c或者类似的文件里。这个驱动实现了serial_setbrg、serial_putc、serial_getc等标准接口。移植的时候需要根据 iTOP-4412 的实际寄存器地址和时钟配置修改驱动里的基地址定义和分频计算逻辑。我习惯在驱动里加一些调试打印把计算出来的UBRDIV和UFRACVAL打出来和手册里的推荐值对比确认无误后再关掉调试输出。还有一个细节FIFO 模式。Exynos 4412 的 UART 支持 FIFO开启后可以减少中断次数提高吞吐量。但在调试阶段我建议先关掉 FIFO用最简单的轮询模式发送和接收。FIFO 模式下一旦配置有误数据会卡在 FIFO 里出不来反而增加调试难度。等基本收发跑通之后再开启 FIFO 做性能优化。5. 重定位与 UART 联调时踩过的坑重定位和 UART 单独调的时候都挺顺但放在一起联调问题就来了。我遇到的最诡异的一个现象是重定位之前串口打印正常重定位之后串口输出变成乱码但代码逻辑看起来完全正确。查了很久才发现重定位过程中拷贝数据的时候把 UART 的寄存器配置区域覆盖了。原因是链接脚本里某个段的地址范围和 UART 寄存器地址重叠了——虽然 UART 寄存器是物理地址不应该被链接脚本影响但我在配置CONFIG_SYS_NS16550_COM1之类的宏时不小心把基地址写成了 DDR 范围内的地址导致编译器把串口结构体放到了 DDR 里重定位拷贝时正好覆盖了它。这个坑的教训是外设寄存器的基地址必须是物理地址且不能落在 u-boot 镜像的拷贝范围内。iTOP-4412 的 UART 基地址在 0x13800000 附近这个地址远低于 DDR 起始地址 0x40000000正常情况下不会冲突。但如果配置宏写错了编译器可能会把一些数据结构分配到错误的位置。第二个坑是重定位后的栈溢出。重定位之后栈指针切换到了 DDR 高端地址但我在board_init_r里调用了一些递归函数或者局部变量很大的函数把栈给撑爆了。栈溢出会覆盖相邻的内存区域表现出的症状千奇百怪可能是串口突然不输出也可能是某个全局变量莫名其妙变了值。我的解决办法是在栈顶和栈底各放一个魔术字定期检查这两个字有没有被改写一旦发现被改写就说明栈溢出及时调整栈大小或者优化函数调用深度。第三个坑和缓存有关。重定位之后如果开启了 D-Cache串口驱动的寄存器访问需要确保是强序的不能被缓存或者乱序执行影响。ARMv7 上可以用__raw_writel和__raw_readl来访问寄存器这两个函数默认不做缓存处理。但如果用了writel和readl在某些配置下可能会走缓存导致寄存器写入没有真正到达硬件。我在 iTOP-4412 上就遇到过这个问题串口配置写进去了但实际寄存器没变原因是写操作被缓存了。后来统一改用__raw_writel才解决。注意重定位和 UART 联调时建议在重定位前后各打印一次 UART 的关键寄存器值比如控制寄存器、波特率分频寄存器对比两次打印是否一致。如果不一致说明重定位过程中有东西改动了 UART 配置需要排查拷贝范围或者内存映射。第四个坑是重定位目标地址和 UART 初始化代码的位置冲突。u-boot 里有些代码是位置相关的比如异常向量表。ARMv7 的异常向量表可以放在低地址或者高地址取决于CONFIG_SYS_TEXT_BASE和VBAR寄存器的设置。如果重定位后异常向量表没有跟着搬过去一旦发生中断或者异常CPU 会跳到错误的地址表现为死机或者跑飞。iTOP-4412 上我一开始没注意这个重定位后串口正常打印但一按键盘就死机后来发现是中断向量表没重定位。6. 验证重定位与串口稳定性的实操方法重定位和串口都调通之后怎么确认它们真的稳定了我总结了一套验证方法在 iTOP-4412 上跑过多轮比较靠谱。第一步是地址校验。在重定位完成后打印__image_copy_start、__image_copy_end、board_init_r的实际运行地址和链接时nm命令看到的地址对比。如果运行地址和链接地址一致说明重定位成功。如果不一致说明重定位的目标地址算错了或者跳转逻辑有问题。这个校验可以放在board_init_r的开头用串口打印出来。第二步是内存校验。在重定位前后分别对 DDR 的某个区域写入已知模式比如 0xAA55AA55然后读回来对比。如果重定位过程中这个区域被意外覆盖读回来的值会变。这个方法可以发现拷贝范围过大或者目标地址重叠的问题。我在 iTOP-4412 上把校验区域设在 0x50000000 附近这个地址在 DDR 范围内但远离 u-boot 镜像和栈区域。第三步是串口压力测试。让串口连续发送大量数据比如循环打印 1MB 的字符观察是否有丢字符或者乱码。这个测试可以发现波特率偏差、FIFO 溢出、中断处理不及时等问题。iTOP-4412 上我用 115200 波特率连续打印了 10MB 数据没有丢字符说明串口驱动基本稳定。如果换成 921600 或者更高波特率可能需要调整时钟分频或者开启 DMA。第四步是重定位后的功能验证。重定位不只是把代码搬过去还要确保搬过去之后所有功能正常。我会在重定位后依次测试串口输入输出、定时器、GPIO、环境变量读写、网络如果有的话。这些测试不需要很复杂能跑通基本流程就行。比如串口输入按一个键能在终端看到回显定时器用md命令读系统 tick 计数确认在递增GPIO用gpio命令翻转 LED肉眼确认。验证项方法预期结果地址校验打印运行地址与链接地址对比完全一致内存校验写入模式后读回对比数据不变串口压力连续打印 10MB 数据无丢字符、无乱码串口输入按键回显字符正确显示定时器读 tick 计数数值递增GPIO翻转 LED肉眼可见变化第五步是异常测试。故意触发一个异常比如访问非法地址看 u-boot 能不能正确打印异常信息并进入命令行。这个测试可以验证异常向量表是否重定位正确以及异常处理流程是否完整。iTOP-4412 上我通常用md命令读一个未映射的地址触发数据 abort观察串口输出。如果异常向量表没搬对这一步会直接死机没有任何输出。这套验证方法跑下来基本能覆盖重定位和串口的主要风险点。我在实际项目中每次改完链接脚本或者串口配置都会把这套流程跑一遍虽然花点时间但能避免很多后期莫名其妙的问题。尤其是重定位这种底层机制一旦出问题表现往往很隐蔽提前验证比事后排查划算得多。7. 从 iTOP-4412 延伸到其他 Exynos 板子的移植思路iTOP-4412 上把重定位和 UART 调通之后再移植到其他 Exynos 4412 板子或者同系列的 Exynos 4412 衍生型号思路基本可以复用。核心差异在于 DDR 的地址范围、UART 的引脚复用、以及时钟树的配置。这三块摸清楚剩下的就是改改配置宏和链接脚本。DDR 地址范围方面不同板子用的 DDR 颗粒容量和位宽可能不同导致可用地址范围有差异。比如有的板子只焊了 512MB DDR那CONFIG_SYS_SDRAM_SIZE就要相应改小栈顶地址也要往下调。我在一块 512MB 的衍生板子上移植时忘了改栈顶地址结果栈指针指到了不存在的物理内存一访问就死机。后来把栈顶从 0x7FE00000 调到 0x5FE00000 才正常。UART 引脚复用方面不同板子引出的调试串口可能不同。iTOP-4412 用的是 UART2有的板子用 UART0 或者 UART3。引脚所在的 GPIO 组和 AF 编号也不一样。移植的时候要对着原理图和芯片手册把引脚配置改对。我一般会在board_init_f里加一段引脚配置的打印把配置前后的寄存器值都打出来确认 AF 设置生效。时钟树配置方面Exynos 4412 的 CMU 寄存器很多不同板子的晶振频率可能不同常见的有 24MHz 和 26MHz。晶振频率变了PLL 的配置参数就要跟着变UART 的时钟源频率也会变波特率分频值需要重新计算。我在一块用 26MHz 晶振的板子上移植时直接照搬了 24MHz 的配置结果串口波特率偏差了将近 8%打印出来的字符勉强能认但很不稳定。后来重新算了 PLL 和 UART 分频参数才解决。移植过程中我习惯把 iTOP-4412 的配置作为基准新建一个板级配置目录然后逐项对比修改。u-boot 2020.07 的板级配置结构比较清晰board/目录下每个板子一个文件夹configs/目录下每个板子一个 defconfig 文件。新建板子的时候复制一份 iTOP-4412 的配置改掉名字和关键参数编译报错的地方就是需要适配的地方。这种方法比从零开始写配置快得多也不容易漏掉关键选项。重定位和 UART 这两个模块在 Exynos 4412 系列上具有很强的共性。把 iTOP-4412 上的实现理解透彻其他板子基本就是改改地址和引脚。真正需要重新思考的是那些架构差异较大的平台比如从 ARMv7 换到 ARMv8或者从 Exynos 换到其他厂商的芯片那时候重定位的实现方式和 UART 驱动框架都会变需要重新学习。但在同一颗芯片的不同板子之间经验复用率很高这也是为什么值得花时间把 iTOP-4412 这块板子吃透。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑