
去年我拿到一块 iTOP-4412 开发板第一件事不是跑 Linux而是先把 U-Boot 调起来。很多刚接触嵌入式的朋友觉得“uboot 移植”是个很高大上的事其实当你把整个流程走一遍就会发现U-Boot 移植更像是“配置适配板级适配”而不是从零写 BootLoader。这篇文章我就用自己的实战经历把 uboot 移植的基础流程、关键参数、串口调试和排坑方法完整拆给你看适合手里刚拿到一块新板子、想从 U-Boot 开始入手的同学参考。U-Boot 是什么简单说它是嵌入式 Linux 系统上电后运行的第一段用户程序。它负责初始化 DDR、时钟、串口、存储介质然后把 Linux 内核加载到内存里启动。你买的开发板、量产的路由器、机顶盒、工控机绝大多数跑 Linux 的设备BootLoader 都是 U-Boot 或者它的变种。所以理解 U-Boot 的移植流程等于掌握了嵌入式底层启动的第一课。2. 移植前先搞清楚U-Boot 到底是什么角色2.1 一条启动链路里的 U-Boot 位置拿我手里的 Exynos 4412 四核板子举例完整启动流程大致是这样芯片内部固化的一小段 ROM 代码iROM先运行它从 SD 卡或 eMMC 的固定位置读入一小段引导代码 BL1BL1 初始化 DDR 内存和部分时钟然后加载 BL2BL2 再加载真正的 U-Boot 主体到 DDR 里把控制权交给 U-Boot最后 U-Boot 加载 Linux 内核和设备树dtb跳转执行。这条链路的每一级都像俄罗斯套娃上一步只做最小必要的事为下一步铺路。为什么要分这么多级因为芯片上电那一刻DDR 还没初始化CPU 只能从片内 SRAM比如 64KB~256KB 的 iRAM里执行代码而 U-Boot 完整功能太大放不下所以必须有一个很小的引导阶段把内存初始化好再加载大块头。不同芯片厂对这种“分段”的叫法不同三星叫 BL0/BL1/BL2全志叫 SPL/TPLTI 的 OMAP 系列也是 SPL。U-Boot 自身从 2016 年以后把 SPL 机制标准化了你可以把 SPL 当成“迷你 U-Boot”它的唯一使命就是把完整 U-Boot 从存储介质搬到内存里。理解了这条链条你后面定位“卡在哪一步”会非常快这是 U-Boot 移植最重要的思维框架。2.2 移植的本质配置适配而不是重写很多新手一听“移植”两个字本能地觉得要把源码从头读懂、从哪里开始改。真不是这样。U-Boot 的设计目标就是“一套源码跑遍所有板子”它靠的是极度彻底的配置化。厂商把你的板子和原厂开发板之间的差异通过宏定义、配置文件、设备树描述出来编译时选择对应的 defconfig 和 dts就能生成适配的镜像。你可以把 U-Boot 想象成一个大型定制服装厂芯片型号SoC是“身高体重”板级配置是“肩宽臂长”外设配置是“口袋位置”。裁缝默认按一个标准模特裁剪你要做的是告诉他你的具体尺寸而不是重新发明一台缝纫机。所以移植 U-Boot 的核心动作有三件事第一找出你的板子和官方支持的某块参考板之间的差异第二把这些差异用配置项和 dts 节点表达出来第三编译烧录后通过串口日志反复验证修正。整个过程是迭代式的我最快一次调通一个新板子花了两个多小时大部分时间不是写代码而是改参数、看日志、查手册。2.3 启动介质和镜像布局U-Boot 可以放在 SD 卡、eMMC、NOR Flash、NAND Flash、网络TFTP等很多介质里。不同的介质决定了烧录方式完全不同。以 SD 卡启动为例三星 Exynos 4412 的 iROM 要求 BL1 存放在 SD 卡物理块的第 1 块block 1注意不是第 0 块BL2 放在后面固定偏移U-Boot 主体再往后放。这个偏移不是拍脑袋定的而是芯片 datasheet 规定死的你必须照做。烧录的时候一般是先给 SD 卡分区然后用 dd 命令把镜像写到对应扇区偏移或者用厂商的烧录工具一键完成。NAND/NOR 启动的偏移又不一样而且还要处理 ECC纠错码问题。我的经验是刚接触移植的时候尽量选 SD 卡或 eMMC 启动的板子来练手烧录简单、反复刷写不容易把板子弄报废等流程熟了再碰 NAND。3. 环境搭建与源码准备别在第一步就翻车3.1 交叉编译工具链和版本雷区U-Boot 是用 C 写的但它不是跑在你电脑上的是跑在 ARM或 MIPS、RISC-V 等处理器上的所以必须用交叉编译器。32 位 ARM 用 arm-linux-gnueabihf- 或 arm-none-eabi-64 位 ARM 用 aarch64-linux-gnu-。我一般在 Ubuntu 18.04/20.04 上用 Linaro 提供的 GCC 7.x 系列稳定性很好。工具链版本这个坑特别值得说。老的 U-Boot比如 2018.01用新版的 GCC 12 编译很容易报“unknown type name ‘u64’”、“dereferencing pointer to incomplete type”这类错误。不是你的代码有问题是编译器变严了、U-Boot 源码写法跟不上了。遇到这种问题别硬着头皮改源码优先换回工具链版本或者用 docker 跑一个合适的环境。安装工具链后先验证一下能不能用arm-linux-gnueabihf-gcc -v能正常打印版本信息说明工具链可用然后再开始编译 U-Boot。千万别跳过这一步我见过有人环境没配对编译报错半小时最后发现是 PATH 没生效。3.2 U-Boot 源码结构哪些目录必须看去 U-Boot 官网或 GitHub 拉一份源码比如 2018.01 版本解开后先别急着 grep你需要对目录结构有个整体认识arch/和 CPU 架构相关的代码比如 arch/arm/cpu/armv7/ 对应 32 位 ARMv7 处理器。移植时要改的主要是这个目录下的 start.S、低速初始化、以及板级 start 代码。board/各厂商的板级目录比如 board/samsung/smdk4412/、board/ti/am335x/。这里面通常放 DDR 初始化参数、板级 GPIO 配置、板级复位逻辑。include/configs/老式板级头文件里面躺着一大堆 CONFIG_ 宏定义。2014 年以后新板子逐步转向 Kconfig很多板子的宏定义散到各个 defconfig 里了但老板子还是靠这种头文件。configs/存放每个板子的 defconfig 文件比如 exynos4412_defconfig。编译时用 make xxx_defconfig 就是解析这个文件。arch/arm/dts/设备树源文件描述板子上的外设、内存、引脚复用。新版 U-Boot 的设备树格式和 Linux 内核的设备树基本一致。我建议你把 board/ 和 arch/arm/dts/ 当成重点这两个目录里的文件就是移植时的“战场”。3.3 U-Boot 的配置体系defconfig、Kconfig、头文件三件套老式 U-Boot 的配置全靠 include/configs/ 底下的头文件里面是一堆 #define CONFIG_xxx。现在主流做法是defconfigmake 时的入口配置 Kconfig交互菜单配置 dts设备树描述。三者分工不一样别搞混。编译的基本流程是这样make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- exynos4412_defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8第一步是生成 .config第二步根据 .config 编译整个工程。如果要调整功能项可以make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfigmenuconfig 会弹出一个图形化菜单类似 Linux 内核配置界面你可以勾选需要的驱动、命令、功能。改完以后配置会写回 .config再重新编译即可。我第一次接触这套体系时最不适应的是“为什么配置改了没生效”后来才知道 U-Boot 默认不会重新生成 .config你需要 make mrproper 清一次或者手动 diff 确认。建议养成好习惯改配置前先cp .config .config.bak方便回滚。4. 板级移植实操从参考板到你的板子4.1 创建自己的板级目录以 Exynos 4412 为例官方支持的是三星的 smdk4412 板我手上的 iTOP-4412 开发板硬件布局和它有差异所以我先复制一份板级目录作为起点不改原厂文件这样版本管理清晰、出问题也好对比cp -r board/samsung/smdk4412 board/samsung/myboard然后把 myboard 目录下所有文件名里的 smdk4412 换成 myboard改里面的 Makefile、Kconfig、MAINTAINERS、以及头文件 include/configs/ 里对应文件mv smdk4412.c myboard.c mv smdk4412.h myboard.h再在 configs/ 下创建自己的 defconfig。如果你的参考板有现成的 defconfig可以直接复制然后修改名字cp configs/exynos4412_defconfig configs/myboard_defconfigdefconfig 里面就是CONFIG_TARGET_MYBOARDy这类选项还要确认CONFIG_DEFAULT_DEVICE_TREE指向自己的 dts 文件名。做完这些编译时就能看到你自己的板子选项了。4.2 三个关键配置链接地址、DDR 参数、时钟链接地址CONFIG_SYS_TEXT_BASE这决定了 U-Boot 主体被加载到内存的哪个地址运行。内存物理基址加上偏移量就是链接地址必须落在 DDR 有效范围内。比如 Exynos 4412 的 DDR 基址是 0x40000000U-Boot 主体通常放在 0x43E00000 之类的位置这样不会和内核加载地址冲突。链接地址设错了代码一运行要么跑飞要么覆盖自己表现就是串口上电后没有输出或者打印几个乱码就死掉。DDR 参数DDR 初始化是移植最头大的环节。DDR 的时序参数非常多tRCD、tRP、tCL、tWR、刷新周期每一家在板上的 DDR 颗粒型号可能都不一样。U-Boot 里是通过配置结构体类似 dmc_phy_settings[]描述这些参数的参考板的值只能保证参考板能用你换了颗粒就必须重新计算或拷贝正确配置。强烈建议从开发板厂商提供的 U-Boot 源码里扒 DDR 参数比自己瞎调靠谱一万倍。时钟系统时钟、串口时钟、DDR 时钟都必须正确。常见做法是在板级代码里根据外部晶振频率比如 24MHz、12MHz设置 PLL 倍频系数。时钟配错会导致串口波特率对不上、DDR 频率异常、整板发热。我的原则是DDR 和时钟这类底层配置有官方参考值就绝不自己发明能少改就少改。4.3 设备树要怎么准备2016 年以后的 U-Boot 已经强制使用设备树来抽象板级信息。你需要确保 arch/arm/dts/ 下有自己的板子 dts 文件。dts 里描述的东西包括内存大小device_type memory、串口控制器节点、MMC 控制器、网卡 PHY、GPIO 引脚复用等。看一段典型的 dts 片段/ { model MyExynos4412 Board; compatible samsung,exynos4412; memory40000000 { device_type memory; reg 0x40000000 0x40000000; /* 1GB RAM */ }; serial13800000 { compatible samsung,exynos4210-uart; reg 0x13800000 0x100; }; };这里compatible字符串很关键U-Boot 会靠它匹配对应的驱动。你把寄存器地址写错了驱动访问的就不是串口控制器而是某个不存在的地址轻则无输出重则触发异常。如果你从 Linux 内核的 dts 抄节点注意 U-Boot 用的 dts 里有些属性比如 bootargs、别名语法上不完全一样需要做减法。我每次都是先让串口工作再逐步加 MMC、网络、显示一次只动一个外设调试起来非常清爽。4.4 常用外设串口先行、MMC 和网络跟上板上外设的优先级是有讲究的。第一步一定是串口因为你看不到 BootLoader 跑到了哪里就只有串口日志这一双眼睛。串口配置包括UART 控制器基地址、波特率、时钟源、引脚复用。Exynos 4412 的 UART 基址是 0x13800000、0x13810000 等波特率 115200 最常用。串口通了你的移植工作就有了“显示器”。MMC需要配置 MMC 控制器基地址、SD 检测引脚、总线宽度。如果 MMC 不工作U-Boot 就读不到烧录在 SD/eMMC 里的内核镜像后面 Linux 启动就无从谈起。网络网卡驱动比如 DM9000、KSZ9031、RTL8211F需要配置 PHY 地址、MAC 地址、MDIO 引脚。网络通了以后调试效率会大幅提升因为可以直接用 tftp 下载内核和设备树不用反复插拔 SD 卡烧录。我建议的调试路径是串口 → MMC → 网络 → 其他外设。每打通一个保存一份对应的 env 配置和固件备份。5. 编译、烧录与首次上电调试5.1 编译产物解读执行完编译命令以后顶层目录会生成一堆文件别光盯着 u-boot.bin它们各有各的用途u-boot.bin完整的 U-Boot 镜像常用来烧录到存储介质。u-boot-dtb.binU-Boot 和 dtb 打包在一起的镜像适合单镜像烧录。u-boot.img给 SPL 加载用的镜像格式文件头带长度和校验信息。SPL/SPL 子目录生成迷你引导程序比如 SPL/spl-u-boot.bin。u-boot.map链接映射文件记录了每个符号的最终链接地址。查“为什么 U-Boot 在 0x43E00000 跑飞”这类问题它就是最重要的依据。make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8编译完成后我习惯先看 u-boot.map 里 start.S 和 board_init_r 的地址确认链接地址符合预期然后再烧录。这个习惯帮我提前发现过两次链接地址配置错误。5.2 建立 TTL 串口调试通道TTL 串口调试是嵌入式开发的基本功U-Boot 移植绝对绕不开它。准备一根 USB 转 TTL 模块CH340、CP2102 都比较常见接线有讲究板子上的 TX 接模块的 RX板子的 RX 接模块的 TXGND 必须共地。千万别直接接 5V 的模块很多开发板的串口电平是 3.3V接错了轻则烧串口重则挂掉整块板子。我第一块板子就是因为图省事把 5V 接上去把 UART 芯片干废了。电脑端我一般用 minicom 或 puttyminicom -D /dev/ttyUSB0 -b 115200参数设置115200 波特率、8 数据位、无校验、1 停止位。上电瞬间就要盯着串口窗口因为 U-Boot 有 autoboot 倒计时几秒内不打断就会自动加载内核。打断方式通常是按任意键部分板子是空格键或回车进入 U-Boot 命令行。如果串口完全没输出先检查接线是否接反、波特率对不对、有没有共地、模块驱动装没装。我遇到过最隐蔽的问题是把 RX 和 TX 接反了结果串口软件一片空白还以为是代码问题折腾了大半天才发现是线的问题。建议用万用表先量一下模块的 TX 引脚正常空闲时应为高电平3.3V 左右能帮你快速判断模块好坏。5.3 烧录到 SD 卡和首启验证以 SD 卡启动的 Exynos 4412 为例先给 SD 卡分区留出前面的保留区因为 BL1 要从第 1 块开始放然后依次写入 BL1、BL2、U-Boot 主体。具体命令大致如下偏移量以你手里的 BSP 文档为准sudo dd ifbl1.bin of/dev/sdb seek1 sudo dd ifbl2.bin of/dev/sdb seek17 sudo dd ifu-boot.bin of/dev/sdb seek49 sync这里的 seek 单位是 512 字节的扇区。写入前先用lsblk确认你的 SD 卡设备名别把电脑硬盘写废了。每次写完执行 sync否则缓存没落盘就拔卡图像会不完整。烧录完成以后插卡上电串口应该打印 U-Boot 的版本信息、CPU 型号、DRAM 容量、启动介质信息最后出现Hit any key to stop autoboot的倒计时。看到这些说明 U-Boot 主体已经跑起来了移植最艰难的部分已经过去。5.4 通过启动日志判断问题启动日志永远是你排错的第一依据。我整理了一个判断流程一点输出都没有问题在 BL1 之前可能是烧录偏移错、SD 卡没插好、串口接线。有输出但卡在 BL1/BL2DDR 初始化没过查 DDR 或者时钟配置。卡在 U-Boot 版本信息后面可能是链接地址问题CONFIG_SYS_TEXT_BASE 不对。U-Boot 起来但加载不了内核环境变量 bootcmd、bootargs 配置问题或 dtb 不匹配。6. 常见问题与排查技巧实录我把这几年做 U-Boot 移植踩过最有代表性的问题整理了一个速查表现象常见原因排查思路串口无输出接线错误、波特率不对、UART 控制器未初始化先量 TX/RX 电平再检查板级串口配置最后看波特率串口乱码波特率漂移、时钟配置不对检查外部晶振频率和 PLL 配置115200 对不上就试 9600卡在“DDR Initialize”前后DDR 参数错误、供电不足核对 DDR 颗粒型号从官方源码抄参数检查电源烧录后完全没反应偏移错误、镜像格式不对检查 dd 的 seek 值确认烧的是 bl1 还是 u-boot.bin能启动但保存不了环境变量MMC 分区偏移和 env 存储区冲突查 CONFIG_ENV_IS_IN_MMC 和 CONFIG_ENV_OFFSET网络不通PHY 地址不对、复位引脚配置缺失用 mii info 查看 PHY 状态核对 dts 里 PHY 节点加载内核秒死dtb 和内核不匹配、bootargs 没传 console用 tftp 单独下载 dtb 验证检查 bootargs 里的 console 参数识别出来的内存只有一半DDR rank 配置不全、bank 数设错查 dts 里的 reg 属性以及 DDR 配置结构体的 bank/row/col再分享一个我压箱底的经验每次解决一个问题立刻把能启动的镜像备份一份按日期命名存放。比如u-boot-20180101-working.bin。你永远不知道下一次改动会不会把板子弄成砖有一个已知能启动的好镜像配合串口和烧录再难的问题都能救回来。7. 进阶视角SPL 机制与后续扩展如果你把基础移植流程走通了下一步值得深入了解 SPL。SPL 本质是一个裁剪版的 U-Boot很多平台用 SPL 替代了厂商的 BL1/BL2因为它的源码是开放可控的。SPL 通常只有几十 KB配置项在spl/目录和 Kconfig 里有专门分组你可以通过CONFIG_SPL_xxx控制它支持哪些功能。SPL 的好处在于完全开源、和 U-Boot 共用配置、调试方便。我曾经把一个板子的闭源 BL1 换成 SPL整个启动链路都变得透明起来定位问题快了很多。还有一个高级玩法叫 Falcon Mode或者叫 Boot From SPL 直接跳内核。它的思路是 SPL 初始化完硬件后不加载完整 U-Boot而是直接加载 Linux 内核并跳转可以把启动时间从两三秒压缩到几百毫秒。工业设备、车载设备对启动时间敏感这个优化很常见。但你别急着上这个什么时候把所有基础流程跑熟了再碰它不迟。另外关于 U-Boot 版本选择的问题我想多说两句。很多人喜欢直接拉最新主线觉得新功能多但主线代码改动大、配套的板级文件很可能和你手上的硬件对不上。做移植和做开发不一样稳定压倒一切。我个人的习惯是先看开发板厂商提供的是哪个版本的 U-Boot尽量在同版本或相近版本比如 2016~2018基础上做适配。等到你这套流程完全跑通再考虑把改动 rebase 到新版本或往主线提交。最后再说说设备树驱动模型dmDriver Model。新版 U-Boot 的外设驱动逐渐统一到 dm 框架下所有设备都是通过设备树描述、由驱动模型统一管理。以前那种#define CONFIG_xxx满天飞、一个板子一个写法的时代慢慢过去了。理解 dm 以后你会发现移植一个新板子时大部分工作就是“写一个 dts 描述板子”C 代码的改动量越来越少。这也是 U-Boot 演进的大方向——硬件细节下沉到设备树代码逻辑尽量通用。在我实际做移植的这段时间里最大的体会是别怕问题多就怕你没有一套可靠的调试手段。串口、能启动的好镜像、完整的源码 diff 记录这三样东西是我每次移植必守的防线。把基础流程走通之后你会对整个嵌入式启动链路有一个质变级的理解后面再去调 Linux 内核启动、根文件系统都会顺手不少。