从零搞定Vitis下自定义板卡的驱动适配:一次真实的Bring-up实战
最近接手了一个基于Zynq-7000的工业控制项目,客户给了块自己画的板子,没有现成BSP,连启动都卡在“Starting kernel…”不动。翻遍Xilinx官网文档,发现大多数教程都在讲ZCU102怎么跑Hello World,对自定义硬件的支持却语焉不详。
这其实是个很典型的痛点:我们花了几天装好Vitis、配置好环境变量,结果一导入工程才发现——根本跑不起来。问题出在哪?不是工具链不行,而是很多人误解了“vitis安装”的意义:它只是搭了个舞台,真正让系统动起来的是后续那一套软硬件协同的“编排逻辑”。
今天我就带大家从头走一遍,如何在一个全新的自定义板卡上,完成从Vivado设计到Linux系统正常启动、外设驱动加载的全过程。不讲虚的,全是踩过坑后总结下来的实操路径。
别再以为“装完Vitis就万事大吉”——真正关键的是平台构建
先说一个残酷的事实:你装的Vitis本身并不认识你的板子。
无论你是用KC705还是自家飞线焊出来的最小系统,只要不是Xilinx官方认证的开发板,Vitis就不会自带支持包(BSP)。这意味着:
✅ Vitis能编译代码
❌ 但它不知道你的DDR接了多少、UART连到哪个管脚、QSPI是x1还是x4模式
所以,“vitis安装”这件事,充其量只是准备好了厨房和灶台,而你要做的,是亲手把食材(硬件描述)做成一顿饭(可运行镜像)。
整个流程的核心枢纽,是一个叫XSA(Xilinx Synthesis Archive)的文件。你可以把它理解为FPGA世界的“硬件快照”——它打包了你在Vivado里做的所有事:PS配置、PL IP连接、地址分配、时钟树、引脚约束……然后这个文件会被Vitis或PetaLinux读取,用来生成BOOT.BIN、设备树、内核驱动等关键组件。
简单来说:
Vivado → 导出 XSA → PetaLinux/Vitis → 构建平台 → 生成 Linux 镜像如果你跳过这一步,直接在Vitis里新建Application Project,大概率会遇到“Platform not found”或者“Device tree missing”的报错。
第一步:导出正确的XSA文件——让软件“看见”你的硬件
很多初学者在这里就栽了跟头:明明Vivado工程能综合成功,导出XSA也没报错,为什么PetaLinux构建时报“invalid hardware description”?
原因往往出在两个地方:导出命令不对或硬件设计未固化。
正确的TCL命令长这样:
write_hw_platform -fixed -force -include_bit -file ./output/custom_z7.xsa几个参数得记牢:
--fixed:表示这是一个物理固定的平台(适用于真实板卡),别用-flexible;
--include_bit:一定要包含比特流!否则烧写时无法自动下载bitstream;
--force:覆盖已有文件,适合自动化流程。
我曾经因为忘了加-include_bit,导致每次调试都要手动加载.bit,浪费了整整半天时间。
小技巧:用脚本批量导出,避免手误
我把导出过程封装成一个TCL脚本,放在项目的scripts/目录下,每次硬件有变更就跑一次:
# export_xsa.tcl set proj_name "custom_zynq" set output_dir "./build/hw" if {![file exists $output_dir]} { file mkdir $output_dir } open_project ./${proj_name}.xpr write_hw_platform -fixed -include_bit -force -file ${output_dir}/${proj_name}.xsa close_project puts "✅ XSA generated at ${output_dir}/${proj_name}.xsa"配合CI/CD工具,还能实现“硬件一改,自动同步XSA”,极大减少人为失误。
第二步:创建PetaLinux工程——开始搭建软件平台
拿到XSA之后,下一步就是让它“活”起来。推荐使用PetaLinux来构建完整Linux系统,因为它比纯Vitis更灵活,尤其适合需要定制内核、裁剪根文件系统的场景。
创建工程三连击:
petalinux-create -t project -n custom_ctrl --template zynq cd custom_ctrl petalinux-config --get-hw-description=../build/hw重点来了:petalinux-config这一步会弹出图形化菜单,必须仔细检查以下几项:
-Subsystem AUTO Hardware Settings → Serial Port:确认你用的是ps7_uart_0还是ps7_uart_1;
-Image Packaging Configuration → Boot Image Generation:选择QSPI还是SD启动;
-DTG Settings → Kernel DTS:确保能正确解析XSA中的IP节点。
如果这里选错了串口,后面连dmesg都看不到,只能靠猜。
第三步:设备树不是摆设——它是驱动能否加载的关键
很多人觉得设备树是“自动生成”的,改都不用改。错!DTG(Device Tree Generator)确实能根据XSA生成基础.dts,但默认配置往往不能满足实际需求。
举个例子:我在PL端加了个AXI GPIO用来监测急停按钮,Vivado里已经连上了中断,但Linux启动后/dev/gpiochip*死活不出现在sysfs里。
查了一圈才发现,缺了两样东西:
1. 设备树里没声明这个IP;
2. 内核没启用GPIO中断支持。
手动添加AXI GPIO节点
编辑project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi:
&axi_gpio_0 { compatible = "xlnx,xps-gpio-1.00.a"; reg = <0x41200000 0x10000>; interrupts = <0 69 4>; // 对应Fabric IRQ ID 69 xlnx,all-inputs = <1>; xlnx,external-intr = <1>; status = "okay"; };注意这里的interrupts = <0 69 4>:
- 第一个0代表GIC SPI中断;
- 69是该IP在中断控制器中的编号(可在Vivado Address Editor查看);
- 4表示上升沿触发。
保存后重新构建:petalinux-build,重启目标板,终于看到内核日志输出:
xilinx_gpio e000a000.gpio: Added GPIO char device gpiochip_find_base: found new base at 298如果你还想响应中断,模块也得跟上
光有设备树还不够,还得写个简单的内核模块来注册中断处理函数:
// gpio_irq_demo.c #include <linux/module.h> #include <linux/interrupt.h> #include <linux/of.h> static int irq_num; static irqreturn_t emergency_btn_handler(int irq, void *dev_id) { pr_info("🚨 Emergency stop button pressed!\n"); return IRQ_HANDLED; } static int __init demo_init(void) { struct device_node *np; np = of_find_compatible_node(NULL, NULL, "xlnx,xps-gpio-1.00.a"); if (!np) return -ENODEV; irq_num = irq_of_parse_and_map(np, 0); if (irq_num == 0) return -EINVAL; if (request_irq(irq_num, emergency_btn_handler, IRQF_TRIGGER_RISING, "emerg_btn", NULL)) { return -EBUSY; } pr_info("✅ IRQ %d registered for emergency button\n", irq_num); return 0; } static void __exit demo_exit(void) { free_irq(irq_num, NULL); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE("GPL");编译进内核或作为ko加载,就能实时捕获外部事件了。
实战避坑指南:那些让你怀疑人生的常见问题
下面这几个问题,我都亲自踩过,每一个都能让你卡住一整天。
🔴 问题1:卡在“Starting kernel…”,再也下不去
最常见的原因是bootargs错了。打开project-spec/meta-user/recipes-bsp/u-boot/files/platform-top.h,检查:
#define CONFIG_BOOTARGS \ "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait"重点关注:
-ttyPS0是否对应你实际使用的UART?
-mmcblk0p2是SD卡第二个分区吗?如果是eMMC,可能是mmcblk1p2;
- 是否漏了rootwait?没有它,内核可能在存储准备好前就尝试挂载根文件系统。
加上earlycon和loglevel=8可以看到更多启动细节:
"console=ttyPS0,115200 earlycon loglevel=8 ..."🔴 问题2:I2C设备扫描不到(i2cdetect无反应)
现象:板上接了个温湿度传感器AT24C32,地址0x50,但i2cdetect -y -r 0显示UU或全空。
排查顺序:
1. 看设备树是否使能I2C0:dts &i2c0 { status = "okay"; clock-frequency = <100000>; // 别太高,走线长容易出错 };
2. 用示波器测SCL/SDA有没有波形;
3. 检查电源和上拉电阻是否到位;
4. 查内核配置是否启用了I2C_CHARDEV(CONFIG_I2C_CHARDEV=y)。
有一次我发现是因为PCB上拉电阻焊成了10kΩ而不是标准的4.7kΩ,通信速率一高就丢包。
🔴 问题3:QSPI启动失败,JTAG又能跑
最可能的原因是BOOT.BIN没正确打包比特流。
正确做法是使用petalinux-package命令,并显式指定FPGA bitstream:
petalinux-package --boot \ --fsbl ./images/linux/zynq_fsbl.elf \ --fpga ./hardware/project.bit \ --u-boot \ --force如果省略--fpga参数,生成的BOOT.BIN不会包含bitstream,QSPI启动时PL就是空的,自然没法工作。
另外注意硬件拨码开关要设置为QSPI模式(通常是拨码SW1=off, off, on, on),不然SOC根本不会去读Flash。
经验总结:高效适配自定义板卡的五个关键点
经过多个项目的锤炼,我总结出一套行之有效的方法论:
1.硬件冻结后再动软件
别一边改DDR布线一边调Linux,版本混乱会让你分不清问题是出在硬件还是软件。建议:硬件定型 → 导出XSA → 打标签(如hw_v1.2)→ 启动软件开发。
2.保持工具链版本一致
Vivado 2023.1 + Vitis 2023.1 + PetaLinux 2023.1 必须严格匹配。混用不同版本可能导致XSA解析失败或DTG崩溃。
3.善用日志追踪
开启以下选项:
-earlyprintk
-dynamic_debug
-CONFIG_DEVTMPFS_MOUNT=y
这些能在早期启动阶段暴露问题,避免盲目猜测。
4.设备树即接口文档
把system-user.dtsi当成硬件接口说明书来维护。每个新增外设都应在此留下记录,方便团队协作和后期维护。
5.生产环境务必考虑安全启动
调试阶段可以裸奔,但产品化必须上加密+签名验证。利用Zynq的BBRAM或eFUSE实现一次性烧录密钥,防止固件被篡改。
如果你也在做自定义FPGA板卡的Bring-up,欢迎留言交流你遇到的奇葩问题。毕竟在这个领域,没人能靠手册活着走出来,都是踩着前人的坑前进的。