简介:围绕飞腾FT2004/FT2000C平台uboot移植、合成与下载的PDF实战文档,面向嵌入式Linux驱动、BSP开发及bootloader调试的初中级工程师,也可供需要完成板卡适配与固件烧录任务的技术人员参考。压缩包仅含1个PDF文件,大小约5.03MB,以图文并茂的实操记录形式呈现,便于离线查阅与对照复现。内容覆盖GCC-ARM V8.2交叉编译环境搭建、飞腾官方uboot源码定制(FT2004C.h、FT2004C_defconfig及PEU1 X16配置)、编译脚本编写、image_fix打包工具配置PCIE与DDR控制器、串口调试,以及GZUT_EZP_XPro烧录flash等完整链路,所有截图与测试结果均来自实测。目前已有504人学习,可作为FT2004系列uboot移植的流程参考与排错依据,帮助读者理解从源码修改到可启动镜像落地的关键节点。
1. FT2004 板子上那条从源码到点亮的链路
拿到一块 FT2004 的板子,能用的资料往往只有一页原理图、一个调试串口和几个名字各异的二进制文件:bl31.bin、u-boot.bin、fip.bin、.itb,谁先谁后、各自烧到哪个地址,没有一份能直接照抄的说明。FT2004 是 ARMv8-A 的四核处理器,上电后由片内 BootROM 起步,经固件层(PBF 或 ARM Trusted Firmware 的 BL31)把控制权交给 U-Boot,U-Boot 再去拉内核。这条链路上任何一环的加载地址、对齐方式或者设备树节点写错,串口就是一行字都不吐。
所谓移植,是让 U-Boot 认识这块板的 DDR、串口、时钟、存储和网口;所谓合成,是把 BL31、U-Boot 和 DTB 按固件约定的偏移拼成一个可烧录镜像;所谓下载,是通过串口、网口或者烧录器把镜像送进 QSPI Flash、eMMC、SD 卡或直接加载到内存里跑起来。这三件事在 FT2004 上互相咬合,单独做对一件没有意义。下面按准备工具链、板级移植、镜像合成、下载固化、调优排错的顺序,把每一步能复现的命令和参数写清楚。
2. FT2004 的 U-Boot 源码与 aarch64 交叉工具链怎么准备
动手改代码之前,先把编译环境和源码布局理顺。FT2004 上跑的都是 64 位 ARM 代码,宿主机上装一套能用的 aarch64 工具链是底线。这一步做不扎实,后面所有报错都会指向错误的方向。
2.1 交叉工具链的选型与自检
常见做法是用gcc-aarch64-linux-gnu,Debian/Ubuntu 下直接apt install gcc-aarch64-linux-gnu,也可以用厂商 SDK 里自带的裸机工具链。两者都能编 U-Boot,区别在于前者带 glibc 头文件,后者更干净,编出来的镜像体积略小。BL31 和 PBF 一般用同一套工具链即可,除非固件方明确要求某个版本。
装完先自检,别等到编译到一半才发现路径不对:
# 确认工具链可用,输出里要有 aarch64 前缀 aarch64-linux-gnu-gcc -v # 写进环境变量,注意结尾那个短横线不能丢 export ARCH=arm export CROSS_COMPILE=aarch64-linux-gnu- export PATH=$PATH:/opt/toolchain/bin # 快速验证能否产出目标文件 aarch64-linux-gnu-gcc -c -o /tmp/t.o -xc - <<'EOF' int main(void){return 0;} EOFARCH在 U-Boot 里填arm而不是arm64,这是很多人第一次编 64 位 U-Boot 时踩的坑;CROSS_COMPILE结尾的短横线是 U-Boot Makefile 拼接工具名前缀用的,漏掉会报aarch64-linux-gnu-gcc: not found。验证用的那段代码只是确认工具链能出目标文件,输出物在 /tmp 下,不影响仓库。
SDK 目录建议按职责分开,避免 U-Boot 的编译产物和固件产物混在一起:
sdk/ ├── boot/ │ ├── pbf/ # 片内固件,通常是二进制形式交付 │ ├── atf/ # BL31 源码,产物 bl31.bin │ └── uboot/ # 移植主战场 ├── images/ # 合成后的可烧录镜像 └── tools/ # mkimage、fiptool、打包脚本2.2 编译前先跑通一次原厂配置
在改任何一行代码之前,先确认这套源码本身能编出产物。这一步的价值在于把「源码问题」和「我的改动问题」分开。用厂商提供的板级配置名跑一遍(配置名以源码configs/目录下实际存在的为准):
cd sdk/boot/uboot make distclean make ft2004_defconfig # 板级配置名按 defconfig 实际文件名替换 make -j$(nproc) 2>&1 | tee build.logdistclean清掉上一次编译留下的.config和中间文件,避免旧配置残留;-j$(nproc)按 CPU 核数并行编译;tee build.log把日志同时落到文件,便于后面grep -i error定位。如果这一步就失败,先解决工具链和源码完整性问题,别急着动设备树。
编译通过后,make menuconfig可以进去核对几个关键项:CONFIG_SYS_TEXT_BASE(U-Boot 自身被加载的地址)、CONFIG_BAUDRATE(调试串口波特率)、CONFIG_DEFAULT_DEVICE_TREE(默认设备树文件名)、CONFIG_SYS_LOAD_ADDR(tftpboot这类命令的默认加载地址)。这四个值的取值范围必须落在板子的 DDR 可用区间内,抄别的板子的值十有八九起不来。
2.3 产物清单与各自用途
一次完整编译会吐出好几个文件,名字相近,用途完全不同。合成镜像时拿错文件是常见事故,先对照这张表:
| 产物文件 | 内容 | 用途 |
|---|---|---|
u-boot.bin | 纯二进制,不含设备树 | 单独烧写或作为 fip 的 nt-fw |
u-boot-nodtb.bin | 去掉 DTB 的 U-Boot | 与外部 DTB 拼 FIT 时用 |
u-boot.dtb | U-Boot 自己用的设备树 | 拼 FIT、确认节点是否生效 |
u-boot.itb | 打开 FIT 后自动生成的组合镜像 | 部分平台可直接烧写 |
u-boot | ELF 格式,带符号表 | 用 gdb 或 addr2line 定位崩溃 |
System.map | 符号地址表 | 分析启动卡死的现场地址 |
判断某个配置项有没有真正生效,最直接的办法是去.config里查,而不是看menuconfig界面上的勾。看出u-boot.dtb的修改是否生效,用fdtdump u-boot.dtb | grep -A3 uart之类的命令确认节点确实编进去了,比反复烧板子快得多。
提示:
u-boot.bin和设备树是两份独立数据,烧写时如果只更新了其中一份,会出现「驱动认得到、地址却不对」的诡异现象,改动 dts 后一定要两份一起更新。
3. 板级移植:让 FT2004 跑出第一行串口输出
移植的第一目标不是让 U-Boot 完整启动,而是让串口先吐出第一行字。只要有一行输出,就说明 DDR、时钟和串口三件事里至少有两条是对的,后面的调试才有反馈渠道。这一章围绕设备树、板级配置和外设驱动三块展开。
3.1 设备树里必须先对上的几个节点
FT2004 平台的 U-Boot 使用设备树描述硬件,根节点的compatible会和板级文件的匹配表比对,对不上就走不到初始化流程。控制台输出靠chosen节点里的stdout-path指定,别名则让命令行能用serial0这种短名字引用。
/ { model = "FT2004 EVB"; compatible = "phytium,ft2004", "phytium,e2000"; chosen { stdout-path = &uart0; /* 决定控制台往哪个串口打印 */ }; aliases { serial0 = &uart0; /* 让命令行可以用 serial0 引用 */ }; }; &uart0 { status = "okay"; clock-frequency = <100000000>; /* 必须与实际输入时钟一致,否则波特率全错 */ };compatible的第一项要和板级文件的of_match表一致;clock-frequency写错会出现「有输出但全是乱码」,因为分频算出来的波特率偏离了 115200;stdout-path指向的节点status必须是okay,标成disabled就没输出。检查设备树有没有编对,用dtc -I dtb -O dts u-boot.dtb反编译出来比对着看。
3.2 defconfig 与板级开关的对应关系
板级配置项在configs/下的 defconfig 里,每一条都影响最终镜像行为。改的时候按用途分组,别一股脑往里加。
| 配置项 | 含义 | 常见错误 |
|---|---|---|
CONFIG_TARGET_xxx | 选择板级目录和初始化代码 | 名字和board/下目录不一致 |
CONFIG_DEFAULT_DEVICE_TREE | 默认加载的 dts 名字 | 与实际 dts 文件名差一个后缀 |
CONFIG_SYS_TEXT_BASE | U-Boot 重定位前的运行地址 | 落在 DDR 未初始化区间 |
CONFIG_SYS_LOAD_ADDR | 下载命令的默认落点 | 与内核加载区重叠 |
CONFIG_BAUDRATE | 调试串口波特率 | 与上位机终端不一致 |
修改后执行make olddefconfig让新增项补齐默认值,再重新编译。想看某个配置是否真的被选中,直接grep CONFIG_TARGET_xxx .config,比在图形界面里翻菜单可靠。
3.3 从有输出到能敲命令:串口与存储的衔接
串口有输出只是起点,接下来要让命令行可用、让存储设备能被识别。U-Boot 进命令行前会初始化board_init、dram_init、misc_init_r等钩子,DDR 容量报错会导致后续内存操作全部失败,典型的症状是bdinfo里 DRAM 容量显示为 0 或明显偏小。
# 板上执行,确认 DDR 和启动参数 FT2004> bdinfo FT2004> dm tree FT2004> mmc list FT2004> sf probe 0bdinfo看 DDR 起始地址和容量,dm tree看驱动模型里各设备有没有绑定成功,mmc list和sf probe 0分别验证 eMMC/SD 和 SPI Flash 能不能被探测到。sf probe失败通常是 SPI 控制器节点的reg或片选引脚配错,先回到设备树核对,别急着怀疑硬件。
注意:DDR 初始化参数(时序、容量、位宽)属于最不该凭经验猜的部分,务必用固件方给出的配置或参数表,写错会表现为随机的偶发死机,极难定位。
4. 把 BL31、U-Boot 与 DTB 合成为可烧录镜像
合成这一步决定了板子能不能从存储介质里被正确加载。核心是三件事:各部分放在镜像的什么偏移、用什么工具拼、拼完怎么校验。FT2004 平台上常见的合成方式有 FIT 镜像和 fip 两种,选哪种取决于固件层怎么解析。
4.1 合成前的镜像布局
动手拼之前,先把布局画清楚。固件层加载时按固定偏移去取 BL31、U-Boot 和 DTB,偏移对不上就加载到一片空白。下面是一种常见的布局示意,具体数值必须以所用固件的说明为准:
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x0 | 引导头/预留区 | 部分平台留出头部空间 |
| 0x1000 | bl31.bin | 通常要求 4KB 对齐 |
| 0x40000 | u-boot.bin | 与 BL31 之间留足空间 |
| 0x100000 | u-boot.dtb | 或直接编进 FIT |
预留空间宁可留大不留小,BL31 后续升级体积变大时不用重排整个布局。对齐要求来自固件层的解析逻辑,常见的对齐粒度是 4KB 或 64KB。
4.2 用 mkimage 合成 FIT 镜像
FIT 的好处是把多份镜像和它们的加载地址、入口地址写在一份 its 描述文件里,加载时由 U-Boot 自己解析,不需要固件层知道固定偏移。
/dts-v1/; / { description = "FT2004 firmware"; #address-cells = <1>; images { bl31 { description = "ARM Trusted Firmware BL31"; data = /incbin/("bl31.bin"); /* 相对 its 文件的路径 */ type = "firmware"; os = "arm-trusted-firmware"; arch = "arm64"; compression = "none"; load = <0x80000000>; /* 按实际内存布局填写 */ entry = <0x80000000>; }; uboot { description = "U-Boot"; data = /incbin/("u-boot-nodtb.bin"); type = "firmware"; os = "u-boot"; arch = "arm64"; compression = "none"; load = <0x80200000>; entry = <0x80200000>; }; fdt { description = "FT2004 device tree"; data = /incbin/("u-boot.dtb"); type = "flat_dt"; arch = "arm64"; compression = "none"; }; }; configurations { default = "conf"; conf { description = "FT2004 boot"; firmware = "bl31"; /* 先被加载并跳转的镜像 */ loadables = "uboot"; /* 由前一级转交控制权 */ fdt = "fdt"; }; }; };生成与检查:
mkimage -f ft2004.its firmware.itb mkimage -l firmware.itb # 列出各镜像的地址与校验信息firmware字段指定第一级被加载的镜像,loadables是后续交接的对象,两者顺序颠倒会直接卡在第一级;load/entry必须落在有效 DDR 区间内且不能互相覆盖;mkimage -l能列出每份子镜像的哈希和地址,合成后必查一次,比烧到板子上再猜要省事。
4.3 fip 方式与直接拼接的差异
如果固件层不解析 FIT,就要用 fip 或者裸拼。fiptool 是 ATF 自带工具,优点是会自动校验各段对齐:
fiptool create \ --soc-fw bl31.bin \ --nt-fw u-boot.bin \ fip.bin # 不打包、只按偏移直接拼时,用 dd 预留空间再覆盖写入 dd if=/dev/zero of=fw.bin bs=1M count=16 dd if=bl31.bin of=fw.bin bs=1K seek=4 conv=notrunc dd if=u-boot.bin of=fw.bin bs=1K seek=256 conv=notruncseek的单位由bs决定,上面用bs=1K,seek=4就是偏移 4KB,写错单位是最常见的低级错误;conv=notrunc保证不把已写入的内容截断。合成完做一次完整性校验:
sha256sum fw.bin cmp -n $(stat -c%s bl31.bin) bl31.bin <(dd if=fw.bin bs=1K skip=4 count=64 2>/dev/null)cmp确认镜像里对应区间的内容和源文件逐字节一致,通过之后再进入烧写环节。
5. 下载与烧写:从串口 XMODEM 到 QSPI/eMMC
镜像合成了,接下来要把它送进板子。FT2004 平台上常用的路径有三条:串口传文件到内存、网口 TFTP 到内存、以及把内存里的内容固化进 Flash。三条路径适用场景不同,速度差得很远。
5.1 内存下载:XMODEM、YMODEM 与 TFTP
串口下载不依赖网络,只要串口通就能用,代价是慢。U-Boot 侧执行接收命令,宿主机侧用sx/sb或 kermit 发送:
# 宿主机:把文件通过串口发给板子,设备名按实际替换 sx -b firmware.itb < /dev/ttyUSB0 > /dev/ttyUSB0 # 或者用 kermit,参数更可控 kermit -i -l /dev/ttyUSB0 -b 115200 -s firmware.itbFT2004> loadx ${loadaddr} # XMODEM 接收,配合宿主机 sx FT2004> loady ${loadaddr} # YMODEM 接收,配合 sb,速度更快 FT2004> md ${loadaddr} 0x20 # 看接收到的头几个字,确认不是空的loadx用 128 字节块,loady用 1KB 块,同样波特率下后者快将近八倍。传输完一定要用md看内存内容,镜像没传完整时前几个字往往就是全 0 或全 F。
有网络的情况下走 TFTP 更快:
FT2004> setenv ipaddr 192.168.1.100 FT2004> setenv serverip 192.168.1.10 FT2004> tftpboot ${loadaddr} firmware.itbipaddr是板子自己的地址,serverip是宿主机上 TFTP 服务的地址,两者必须在同一网段。tftpboot成功后filesize环境变量会自动更新为收到的字节数,后面的写 Flash 命令直接引用它,比自己算长度可靠。
5.2 固化到 QSPI Flash / eMMC / SD
内存里跑通之后才轮到固化。三种介质的命令和坑各不相同:
FT2004> sf probe 0 # 探测 SPI Flash FT2004> sf erase 0x0 0x200000 # 擦除 2MB,地址和长度都是十六进制 FT2004> sf write ${loadaddr} 0x0 ${filesize} # 从内存写入 FlashFT2004> mmc dev 0 # 选中设备 0 FT2004> mmc write ${loadaddr} 0x0 0x1000 # 第三个参数是块数,不是字节数sf erase的第二个参数是长度,单位字节;mmc write的第三个参数是从哪个块开始写多少块,一块 512 字节,写 2MB 要传0x1000而不是0x200000。这两个命令的参数单位不一致,是烧写失败中最常见的来源之一。写之前务必先擦除,Flash 只能把 1 写成 0,不擦除直接写会得到一堆不可预期的数据。
| 方式 | 典型速度 | 依赖 | 适用场景 |
|---|---|---|---|
| 串口 XMODEM/YMODEM | 慢 | 只需串口 | 首板点亮、网络未通 |
| TFTP | 快 | 网口和 TFTP 服务 | 日常迭代 |
| 烧录器 | 最快 | 专用工具 | 量产、批量刷写 |
5.3 上电验证与环境变量落盘
烧完不要立刻断电,先在当前会话里验证能不能从烧写位置正常加载:
FT2004> sf probe 0; sf read ${loadaddr} 0x0 0x200000 FT2004> bootm ${loadaddr}sf read把刚写进去的内容读回内存,bootm尝试启动,能走到内核或 U-Boot 二次加载界面说明写入位置和长度都对。随后把启动命令固化下来:
FT2004> setenv bootcmd 'sf probe 0; sf read ${loadaddr} 0x0 0x200000; bootm ${loadaddr}' FT2004> saveenvbootcmd是上电后自动执行的命令序列,saveenv把它写进环境变量存储区。如果saveenv报Cannot save environment,先确认环境变量分区在配置里指向了可写介质,而不是默认的nowhere。
6. 把 FT2004 的 U-Boot 调稳的几个技巧
前几章走完,板子基本能启动了,剩下的问题往往更零碎:网口时通时不通、偶发卡死、换个批次的核心板行为不一样。这一章集中处理这类疑难。
6.1 网口不通的排查顺序
千兆网口在移植阶段是最容易出问题的外设,同类平台上的经验基本通用。按这个顺序查,能覆盖大部分情况:
| 症状 | 优先排查点 |
|---|---|
mii info读不到 PHY | PHY 地址、MDIO 时钟、复位 GPIO 时序 |
| 能链路但 ping 不通 | RGMII 收发延时、时钟方向、PHY 模式配置 |
| 大包丢、小包通 | DMA 描述符数量与缓冲区对齐 |
FT2004> mii info # 逐个地址扫 PHY FT2004> mii read 0 0 # 读寄存器 0,确认 PHY 是否响应PHY 复位时序是最容易被忽略的一环:复位脚拉低后需要保持一段时间再释放,很多板子因为释放太快导致 PHY 内部寄存器还没准备好,表现为「第一次上电通、热重启不通」。这类问题在设备树里加一个复位延时节点往往就能解决。
6.2 启动卡死的定位手段
串口完全没有输出时,先把早期调试串口打开,让打印从 U-Boot 重定位之前就开始:
CONFIG_DEBUG_UART=y CONFIG_DEBUG_UART_BASE=0x... CONFIG_DEBUG_UART_CLOCK=100000000 CONFIG_DEBUG_UART_ANNOUNCE=y这四个配置项让debug_uart_init在 SPL 或早期阶段就初始化串口,输出的是不走驱动模型的裸打印,能在 DDR 初始化之前就给出反馈。如果这样还是没有任何字符,问题就在 DDR 初始化或更前面的固件层,不在 U-Boot 本身,此时应该换一个已知能启动的镜像回来对比。
进了命令行之后,bootstage report能列出各阶段耗时,卡在哪一段一目了然;dm tree看驱动绑定情况;bdinfo看内存布局。偶发死机这类问题,把System.map里的地址和串口打印出的 PC 值对照,用addr2line -e u-boot -f -i <地址>反查函数名,比反复改代码试要快得多。
本文还有配套的精品资源,点击获取