从零构建 Zynq-7000 Linux 启动系统:基于 Vivado 2018.3 的实战手记
最近接手了一个老项目——在一块基于Zynq-7020的定制板上移植嵌入式 Linux。客户明确要求使用Vivado 2018.3工具链,不许用 PetaLinux,必须手动搭建整个启动流程。
说实话,现在都 2025 年了,还用这么“复古”的方式干活,一开始我也有点抵触。但真动手做完一遍才发现:这不仅是技术复盘,更是一次对嵌入式底层机制的深度理解之旅。今天就把我踩过的坑、总结的经验,毫无保留地分享出来。
为什么选择 Vivado + 手动构建?而不是直接上 PetaLinux?
PetaLinux 确实方便,三行命令就能生成一个完整的镜像。但它像一个黑箱——你知其然,不知其所以然。
而当我们面对的是非标准硬件、需要极致裁剪、或要搞软硬协同加速时,只有亲手走完 FSBL → U-Boot → Kernel → Rootfs 这条完整路径,才能真正掌控系统每一环的行为。
更何况,很多工业现场的老项目还在维护,你躲不开这些“古早”工具链。掌握它,不是怀旧,是生存技能。
第一步:用 Vivado 搭建 PS 系统 —— 别小看这个“配置向导”
打开 Vivado 2018.3,新建工程,添加ZYNQ7 Processing SystemIP,进入配置界面。别急着点“OK”,这里有太多细节决定成败。
关键设置清单(以常见 Zynq-7020 板卡为例)
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| PS Clocks | CPU_6OR4X_CLK = 666.66MHz | ARM 核运行在 667MHz |
FCLK_CLK0 = 100MHz | 给 PL 提供时钟,常用于 QSPI 或 GPIO 中断 | |
| DDR Configuration | DDR3, 800MHz (400MHz x2) | 必须和你的物理内存颗粒匹配 |
| MIO Selection | 启用 UART0, SD0, GigE, QSPI | 常用外设 |
| 禁用未使用的 MIO 引脚 | 减少干扰 | |
| USB Reset Polarity | Active Low | 很多开发板 USB 芯片复位是低有效,手册没写清楚! |
| SDIO 0 CD/PD | Connected to MIO[8]/MIO[9] | 千万别忘了插卡检测脚,否则 SD 卡热插拔会出问题 |
🛠️血泪教训:有一次我忽略了
FCLK_CLK0的使能,结果 QSPI Flash 死活读不出来。查了一整天才发现 PL 没有时钟驱动 AXI_QSPI 控制器!
输出什么?.hdf还是.xsa?
在 Vivado 2018.3 中,默认导出的是.hdf文件(Hardware Description File),这是 SDK 能识别的格式。到了后来版本才主推.xsa。
记住一句话:
.hdf是给 Xilinx SDK 用的“硬件说明书”,没有它,FSBL 和 U-Boot 就不知道你的 DDR 地址在哪、串口连了哪个 MIO。
最后记得生成Bitstream并导出到 SDK,哪怕你 PL 部分暂时为空,也得走完这一步。
第二步:编译 FSBL —— 第一阶段引导程序的本质
FSBL(First Stage Boot Loader)听起来高大上,其实它的任务非常简单:
- 被 ROM Code 从 Flash 加载进 OCM(0xFFFF0000)
- 初始化 DDR
- 把 U-Boot 从 Flash 搬到 DDR
- 跳过去执行
就这么四步。但它必须跑赢时间——因为 OCM 只有 256KB,代码不能膨胀。
如何生成 FSBL 工程?
在 Xilinx SDK 中:
- 新建 Application Project
- 选择模板:Zynq FSBL
- 导入前面生成的.hdf
SDK 会自动从embeddedsw目录复制源码并创建工程。
调试技巧:让 FSBL “说话”
默认情况下 FSBL 是静默的。一旦卡住,你就只能“盲调”。解决办法很简单:
// 在 fsbl_debug.h 中取消注释 #define FSBL_DEBUG_INFO然后重新编译。你会在串口看到类似输出:
Starting FSBL... Initializing DDR... Copying Linux Image to DDR... Jumping to U-Boot at 0x3000000...如果只打印到“Initializing DDR”就停了?那基本可以锁定是DDR 参数配错了,回去检查 MIG 设置。
第三步:交叉编译 U-Boot —— 自己动手,丰衣足食
U-Boot 是整个系统的“指挥官”。它负责加载内核、解析设备树、提供命令行接口,甚至可以通过 TFTP 实现远程更新。
获取正确的源码版本
别随便 git clone 一个 u-boot master 分支!你要找的是:
git clone https://github.com/Xilinx/u-boot-xlnx.git git checkout xilinx-v2018.1为什么是 v2018.1?因为Vivado 2018.3 工具链配套的就是这个版本。混用新旧版本可能导致设备树兼容性问题。
编译流程
make ARCH=arm distclean make ARCH=arm xilinx_zynq_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8最终你会得到几个关键文件:
-u-boot: ELF 格式,可用于 JTAG 调试
-u-boot.bin: 二进制镜像,烧写进 Flash 使用
让 U-Boot 自动启动 Linux
我们当然不想每次上电都手动敲命令。编辑include/configs/zynq-common.h或通过环境变量设置自动启动脚本:
setenv bootcmd 'fatload mmc 0:1 0x3000000 uImage; fatload mmc 0:1 0x2A00000 system.dtb; bootm 0x3000000 - 0x2A00000' setenv bootargs 'console=ttyPS0,115200 root=/dev/mmcblk0p2 rw rootwait' saveenv解释一下:
-fatload mmc 0:1 xxxxx: 从 SD 卡第一个 FAT32 分区读取文件
-uImage: 内核镜像(zImage 经 mkimage 封装)
-system.dtb: 设备树二进制
-bootm: 启动内核,-表示无 initrd
保存后,U-Boot 会在倒计时结束时自动执行bootcmd。
第四步:定制设备树 —— 描述你的硬件真相
很多人觉得设备树神秘,其实它就是一份硬件说明书 JSON 化。Linux 内核靠它知道:“哦,原来 UART0 接在 MIO14/15 上,而且有个 AXI GPIO 在 0x41200000。”
设备树结构拆解
Zynq 的设备树通常由两部分组成:
- 基础描述文件:
zynq-7000.dtsi(由 Xilinx 提供,定义 PS 内部资源) - 板级描述文件:
my_board.dts(你写的,补充 PL 外设和引脚映射)
编译命令:
dtc -I dts -O dtb -o system.dtb my_board.dts或者集成进内核编译体系:
make ARCH=arm zynq_my_board.dtb实战案例:把 PL 端的 AXI GPIO 写进设备树
假设你在 Vivado 里加了个 AXI GPIO IP,地址分配为0x41200000,想让它在 Linux 下可用。
/ { amba_pl: amba_pl { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; ranges; axi_gpio_led: gpio@41200000 { compatible = "xlnx,axi-gpio-2.0"; reg = <0x41200000 0x10000>; xlnx,all-inputs = <0>; xlnx,all-outputs = <1>; xlnx,signal-width = <8>; gpio-controller; #gpio-cells = <2>; }; }; };编译后放进 SD 卡,启动 Linux,就可以用 sysfs 控制 LED 了:
echo 8 > /sys/class/gpio/export echo out > /sys/class/gpio/gpio8/direction echo 1 > /sys/class/gpio/gpio8/value # 点亮⚠️ 注意事项:
- 地址必须与 Vivado Address Editor 完全一致
- 如果 PL 没有实现该 IP,却留在设备树中,内核可能卡死
- 修改设备树后一定要重新编译.dtb,否则无效!
最终系统是如何一步步启动起来的?
让我们把所有环节串起来,看看通电瞬间发生了什么:
上电复位
- Zynq ROM Code 开始执行(固化在芯片内部)
- 检测启动模式(QSPI / SD / JTAG)加载 FSBL
- 从 Flash(如 QSPI)读取 FSBL 到 OCM(0xFFFF0000)
- 执行 FSBLFSBL 初始化 DDR
- 配置 MIO、时钟、DDR 控制器
- 将 U-Boot.bin 从 Flash 拷贝至 DDR(比如 0x3000000)跳转到 U-Boot
- FSBL 跳转至0x3000000
- U-Boot 初始化外设、恢复环境变量U-Boot 加载内核
- 从 SD 卡读取uImage和system.dtb
- 放入指定内存地址
- 执行bootm启动内核Linux 内核接管
- 解析设备树,初始化驱动
- 挂载根文件系统(EXT4)
- 启动init,进入用户空间
整个过程就像接力赛,每一棒都不能掉链子。
常见问题与调试秘籍
❌ 问题1:串口无输出
- ✅ 检查 MIO 是否启用了 UART0?
- ✅ 波特率是不是 115200?
- ✅ 电源是否稳定?尤其是 VCCPINT
- ✅ 使用 JTAG 调试,查看 PC 指针停在哪
❌ 问题2:卡在“DDR init”阶段
- ✅ 回去检查 MIG 配置,频率、CL 值是否匹配实际颗粒
- ✅ 供电电压是否达标(DDR3 通常是 1.5V ±0.075V)
- ✅ PCB 走线等长控制如何?差太远会导致采样失败
❌ 问题3:U-Boot 能启动,但内核不起来
- ✅ 检查
bootargs中的root=参数是否正确(mmcblk0p2vsmmcblk1p2) - ✅
.dtb是否包含正确的根文件系统分区信息? - ✅ uImage 是否真的存在?可以用
ls mmc 0:1查看 SD 卡内容
🔧 调试利器推荐
| 工具 | 用途 |
|---|---|
| 串口线 + minicom | 最基础也是最重要的日志来源 |
| JTAG + Xilinx SDK Debugger | 查寄存器、看堆栈、设断点 |
| TFTP + NFS | 免烧卡快速测试内核和根文件系统 |
| 逻辑分析仪 | 抓 QSPI/SPI 时序,确认 Flash 通信正常 |
写在最后:这套方法还有价值吗?
有人可能会问:现在都有 Vitis、PetaLinux、Yocto,谁还这么原始地一个个编译?
我的回答是:正因为高级工具太智能,我们才更需要理解底层原理。
当你遇到以下情况时,这种“手工派”技能就会救命:
- 客户只要最小系统,连根文件系统都不想要
- 需要在启动早期做安全验证(比如验签 U-Boot)
- PL 要和 PS 做超低延迟通信,必须精确控制启动顺序
- 老项目维护,文档缺失,只能逆向分析.bit和.bin
掌握这套流程,意味着你不再是一个“点按钮工程师”,而是真正理解 Zynq 启动机制的开发者。
如果你正在学习嵌入式 Linux 移植,不妨放下 PetaLinux,亲手试一次从 Vivado 到 Shell 的全过程。相信我,那种“终于看到 login prompt”的成就感,无与伦比。
💬互动时间:你在移植 Zynq Linux 时遇到过哪些奇葩问题?欢迎留言分享,我们一起排坑!