
1. 项目缘起为什么要在AUP ZU3上折腾GPIO最近在做一个基于Xilinx Zynq UltraScale MPSoC的嵌入式项目板卡用的是AUP ZU3。项目里有个需求需要通过Linux系统实时控制几个外部设备的状态说白了就是得用上GPIO。一开始我以为在PetaLinux 2022.1这种官方支持的系统里配置几个GPIO引脚应该是分分钟的事就像在树莓派上那样简单。但真正上手才发现从Zynq UltraScale的硬件设计到PetaLinux的设备树配置再到用户空间的驱动访问这一整套流程里藏着不少“坑”。如果你也在用类似的平台比如ZCU102、ZCU106或者其他基于ZU系列的板卡并且需要在PetaLinux下控制GPIO那么我踩过的这些坑或许能帮你省下大把的调试时间。这篇文章不是泛泛而谈的理论而是我基于AUP ZU3这块具体板卡在PetaLinux 2022.1环境下从硬件引脚分配、设备树编写、内核驱动加载到用户空间C程序测试的全流程实战记录。我会重点讲清楚每个步骤背后的“为什么”而不仅仅是“怎么做”。2. 硬件基础与设计决策理解ZU的GPIO架构在开始写一行代码之前我们必须先搞清楚Zynq UltraScale MPSoC的GPIO子系统是怎么工作的。这和传统的微控制器或者简单的SoC比如树莓派用的博通芯片有很大不同。2.1 Zynq UltraScale的GPIO控制器分布ZU芯片的GPIO并不只有一个统一的控制器。它主要分为两大块PSProcessing System侧GPIO这部分GPIO控制器是硬核集成在ARM Cortex-A53/A72的应用处理器子系统里。在AUP ZU3上PS MIOMultiplexed I/O引脚是直接连接到芯片引脚上的通常用于连接低速外设如UART、I2C、SPI当然也可以配置为通用GPIO。PS GPIO的驱动是Linux内核原生支持的稳定性最高。PLProgrammable Logic侧GPIO这部分是通过FPGA逻辑编程实现的。你可以在Vivado里用AXI GPIO IP核创建一个或多个GPIO控制器通过AXI总线挂载到PS上。它的灵活性极高数量、分组Bank、电气标准LVCMOS, LVDS等都可以自定义但需要额外的硬件设计步骤。我的选择与理由 对于我的项目需要控制的设备信号电平是3.3V且对实时性要求不是纳秒级但引脚数量需求超过了PS MIO的富余数量。因此我决定使用PL侧的AXI GPIO。理由如下灵活性可以自由定义引脚数量我只需要8个输出用1个8位的AXI GPIO IP核正好不浪费资源。引脚分配自由PL侧的引脚可以通过Vivado约束文件分配到板卡上任何支持的用户IO引脚不受PS MIO固定映射的限制。独立性不占用宝贵的PS MIO资源这些资源可以留给更重要的系统功能如以太网PHY的MDIO、QSPI Flash等。注意如果你的GPIO需求很少1-2个且对实时性没要求可以优先考虑使用PS GPIO配置起来更简单无需动硬件设计。但AUP ZU3的PS MIO大多已用于核心系统功能所剩无几所以PL GPIO是更实际的选择。2.2 Vivado中的硬件设计创建AXI GPIO IP核这一步是在Vivado 2022.1与PetaLinux版本对齐中完成的。过程不复杂但有几个关键点容易出错。创建Block Design在Vivado中为你的项目创建Block Design。添加Zynq UltraScale IP从IP Catalog中添加“ZYNQ UltraScale MPSoC” IP核。双击进行配置。关键是在“PS-PL Configuration” - “GP Master AXI Interface”中确保至少使能了一个M_AXI_HPM0_FPD接口。这个AXI主端口将用于PS访问我们即将创建的PL侧外设AXI GPIO。默认通常是使能的但务必检查。添加AXI GPIO IP从IP Catalog添加“AXI GPIO” IP核。双击它进行配置。All Outputs因为我的需求全是输出所以这里我勾选了“All Outputs”这样IP核就只提供输出通道。GPIO Width设置为8对应我需要控制的8个信号。其余保持默认双通道、中断这些功能我暂时不需要。连接与自动化将ZYNQ UltraScaleIP的M_AXI_HPM0_FPD接口连接到AXI GPIO的S_AXI接口。将ZYNQ UltraScale的pl_clk0通常为100MHz连接到AXI GPIO的s_axi_aclk和ZYNQIP的m_axi_hpm0_fpd_aclk。时钟必须连接正确否则AXI总线无法工作。使用“Run Block Automation”和“Run Connection Automation”让Vivado自动完成复位信号、中断控制器如果用了的连接以及地址分配。关键一步查看地址分配在Address Editor标签页中记录下系统为axi_gpio_0分配的Base Address。例如我这里的地址是0x8000_0000。这个地址至关重要它将是Linux内核驱动和用户空间程序访问这个GPIO控制器的“门牌号”。生成输出产品与创建HDL Wrapper在Block Design上右键依次选择“Generate Output Products”和“Create HDL Wrapper”。让Vivado生成最终的网表文件。引脚约束打开“I/O Planning”或直接编辑XDC约束文件。找到axi_gpio_0对应的输出端口例如gpio_io_o[7:0]将它们分配到AUP ZU3板卡上你计划使用的具体物理引脚上并设置正确的I/O标准如LVCMOS33。例如set_property PACKAGE_PIN AG11 [get_ports {gpio_io_o[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {gpio_io_o[0]}] # ... 为gpio_io_o[1]到[7]重复类似约束生成比特流完成综合、实现并生成最终的.bit比特流文件。这个文件包含了你的PL侧GPIO硬件设计。3. PetaLinux工程配置搭建软件环境硬件设计冻结后软件工作正式开始。我们需要在PetaLinux中配置内核以识别并驱动我们刚刚创建的AXI GPIO硬件。3.1 创建与配置PetaLinux工程# 1. 设置PetaLinux环境变量具体路径根据你的安装位置调整 source /opt/pkg/petalinux/2022.1/settings.sh # 2. 创建PetaLinux工程并指定之前Vivado导出的硬件描述文件(.xsa) petalinux-create -t project -n aup-zu3-gpio --template zynqMP cd aup-zu3-gpio petalinux-config --get-hw-description/path/to/your/vivado/project/aup_zu3.xsa执行最后一条命令时会进入PetaLinux的配置菜单。这里我们主要关注两个地方Subsystem AUTO Hardware Settings-Advanced bootable images storage Settings-kernel image settings确保内核启动参数bootargs中的root设备指向你的根文件系统位置如/dev/mmcblk0p2。其他设置可以先保持默认直接保存退出。3.2 内核配置确保GPIO驱动就绪PetaLinux 2022.1使用的内核版本较高AXI GPIO驱动通常是内置的但我们仍需确认。petalinux-config -c kernel在内核配置菜单中导航到Device Drivers-GPIO Support- 确保Xilinx GPIO support被选中通常标记为[*]即编译进内核而不是模块[M]。同样在GPIO Support下可以检查/sys/class/gpio/... (sysfs interface)是否被选中这是通过sysfs在用户空间操作GPIO的传统方式但对我们通过设备树定义的AXI GPIO主要使用更规范的/sys/class/gpio/gpiochipX接口。保存并退出内核配置。3.3 设备树定制连接硬件与驱动这是最核心也最容易出错的一步。设备树Device Tree告诉Linux内核在地址0x8000_0000处存在一个兼容于“xlnx,xps-gpio-1.00.a”的GPIO控制器。PetaLinux工程中系统级的设备树源文件在project-spec/meta-user/recipes-bsp/device-tree/files/目录下。我们需要修改或创建system-user.dtsi文件。vim project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi添加以下内容/include/ system-conf.dtsi / { /* 定义在Vivado中创建的AXI GPIO控制器 */ axi_gpio_0: gpio80000000 { #gpio-cells 2; compatible xlnx,xps-gpio-1.00.a; gpio-controller; reg 0x0 0x80000000 0x0 0x10000; /* 地址和高位长度 */ xlnx,all-inputs 0x0; xlnx,all-outputs 0x1; xlnx,gpio-width 0x8; xlnx,interrupt-present 0x0; status okay; }; };关键参数解析compatible xlnx,xps-gpio-1.00.a;这个字符串必须与内核驱动drivers/gpio/gpio-xilinx.c中定义的of_match_table匹配。这是驱动识别设备的关键。reg 0x0 0x80000000 0x0 0x10000;这是地址映射。前两个0x0 0x80000000表示基地址的高32位和低32位。对于ZU的地址空间0x8000_0000通常位于PS的地址映射内所以高32位为0。后两个0x0 0x10000表示地址空间的长度64KB。这个长度是AXI GPIO IP核的地址范围在Vivado中可以看到通常默认就是0x10000。xlnx,all-outputs 0x1;和xlnx,gpio-width 0x8;这些属性必须与你在Vivado中配置AXI GPIO IP核时的参数严格一致。all-outputs1表示全输出gpio-width8表示8位宽。gpio-controller;和#gpio-cells 2;这两个属性声明这是一个GPIO控制器并且每个GPIO描述符需要2个cell通常用于指定引脚号和标志位。3.4 编译PetaLinux镜像配置完成后开始编译整个系统镜像。petalinux-build这个过程会编译uboot、内核、设备树以及根文件系统。如果设备树语法有误通常会在编译过程中报错。编译成功后在images/linux/目录下会生成我们需要的关键文件image.ub包含内核、设备树和初始RAM磁盘的FIT镜像。BOOT.BIN包含FSBL、PMU Firmware、ARM Trusted Firmware、U-Boot的启动镜像。rootfs.tar.gz根文件系统。将image.ub和BOOT.BIN拷贝到SD卡的FAT32分区将rootfs.tar.gz解压到SD卡的ext4分区假设你的启动方式是SD卡。4. 系统启动与驱动加载验证给AUP ZU3板卡上电从SD卡启动。通过串口终端登录系统后我们需要验证AXI GPIO驱动是否成功加载。4.1 检查设备树与平台设备首先检查设备树节点是否被正确识别# 查看/sys/firmware/devicetree/base下是否存在我们定义的节点 ls /sys/firmware/devicetree/base/ | grep gpio # 或者用更强大的ofdump工具如果已安装 dtc -I fs -O dts /sys/firmware/devicetree/base | grep -A 20 axi_gpio_0然后检查内核是否成功创建了平台设备# 查看平台设备寻找与axi_gpio或xlnx,gpio相关的设备 cat /sys/devices/platform/soc/80000000.gpio/uevent 2/dev/null # 或者用dmesg查看启动日志 dmesg | grep -i gpio你应该能看到类似gpio-xilinx 80000000.gpio: GPIO chip registered的日志这表明驱动成功探测到了我们的硬件。4.2 确认GPIO控制器在sysfs中的呈现GPIO驱动会在/sys/class/gpio/下创建gpiochipX目录其中X是控制器编号。ls -la /sys/class/gpio/你可能会看到多个gpiochipX例如gpiochip0对应PS GPIOgpiochip504编号是动态分配的可能就对应我们的AXI GPIO。如何确认哪个是我们的查看每个gpiochipX目录下的label文件cat /sys/class/gpio/gpiochip504/label # 如果输出是“80000000.gpio”那就对了这个标签对应设备树中的节点名。同时查看ngpio文件它应该显示为8对应我们设计的8位GPIO宽度。cat /sys/class/gpio/gpiochip504/ngpio5. 用户空间控制实战从命令行到C程序驱动加载成功GPIO控制器也已就位接下来就是在用户空间实际控制它们了。5.1 使用Sysfs接口进行快速测试Sysfs接口简单直观适合快速验证。假设我们的控制器是gpiochip504其管理的GPIO编号范围是504到511因为base是504ngpio是8。操作单个GPIO例如控制器上的第0个引脚对应系统GPIO编号504导出GPIO让该GPIO在用户空间可见。echo 504 /sys/class/gpio/export执行后会出现/sys/class/gpio/gpio504目录。设置为输出方向echo out /sys/class/gpio/gpio504/direction设置输出电平echo 1 /sys/class/gpio/gpio504/value # 输出高电平 echo 0 /sys/class/gpio/gpio504/value # 输出低电平取消导出完成后echo 504 /sys/class/gpio/unexport使用Shell脚本进行批量控制 你可以写一个简单的bash脚本一次性控制所有8个GPIO。但注意频繁的echo操作到sysfs性能较低不适合高频操作。5.2 编写C程序进行高效控制对于需要实时性、复杂逻辑或高频操作的应用直接使用sysfs的echo/cat命令就不合适了。我们需要在C程序中通过mmap直接映射GPIO控制器的内存地址进行操作。这才是嵌入式Linux下操作外设的“专业”方式。原理AXI GPIO IP核的寄存器映射非常简单。主要关注两个寄存器偏移量相对于基地址0x8000_0000数据寄存器Data Register偏移0x0。写入此寄存器可以控制输出引脚的电平。读此寄存器可以获取输出值对于输出模式或输入值对于输入模式。方向寄存器Tri-state Register偏移0x4。控制每个引脚的方向。1表示输入0表示输出。在我们的全输出配置中这个寄存器默认应该为0。下面是一个完整的C程序示例axi_gpio_test.c#include stdio.h #include stdlib.h #include stdint.h #include fcntl.h #include sys/mman.h #include unistd.h // AXI GPIO的基地址来自设备树 reg 属性 #define GPIO_BASE_ADDR 0x80000000 // 内存页大小 #define PAGE_SIZE (4*1024) #define GPIO_MAP_SIZE PAGE_SIZE // 寄存器偏移量 #define GPIO_DATA_OFFSET 0x00 #define GPIO_TRI_OFFSET 0x04 int main() { int fd; void *gpio_map; volatile uint32_t *gpio_data; volatile uint32_t *gpio_tri; // 1. 打开 /dev/mem它代表了整个物理内存空间 fd open(/dev/mem, O_RDWR | O_SYNC); if (fd -1) { perror(Failed to open /dev/mem); return EXIT_FAILURE; } // 2. 将物理地址映射到进程的虚拟地址空间 gpio_map mmap( NULL, // 让内核自动选择映射的起始地址 GPIO_MAP_SIZE, // 映射的大小一页足够 PROT_READ | PROT_WRITE, // 可读可写 MAP_SHARED, // 共享映射 fd, // /dev/mem 的文件描述符 GPIO_BASE_ADDR // AXI GPIO的物理基地址 ); if (gpio_map MAP_FAILED) { perror(Failed to mmap GPIO); close(fd); return EXIT_FAILURE; } // 3. 计算寄存器指针 gpio_data (volatile uint32_t *)((char *)gpio_map GPIO_DATA_OFFSET); gpio_tri (volatile uint32_t *)((char *)gpio_map GPIO_TRI_OFFSET); printf(GPIO Controller mapped at virtual address: %p\n, gpio_map); printf(Data register address: %p\n, gpio_data); printf(Tri-state register address: %p\n, gpio_tri); // 4. 可选确认方向寄存器为输出模式。全输出IP核应默认为0。 printf(Current Tri-state (Direction) register value: 0x%08X\n, *gpio_tri); // 如果需要可以强制设置为0全输出 // *gpio_tri 0x00000000; // 5. 控制GPIO输出 printf(\nTesting GPIO outputs...\n); for (int i 0; i 8; i) { uint32_t val (1 i); // 依次将第i位置1其余为0 *gpio_data val; printf(Set output pattern: 0x%02X (binary: , val 0xFF); for (int j 7; j 0; j--) { printf(%d, (val j) 1); } printf()\n); usleep(500000); // 延时500ms方便观察LED或示波器 } // 全部置零 *gpio_data 0x00; printf(All GPIOs set to LOW.\n); // 6. 清理工作取消映射关闭文件 if (munmap(gpio_map, GPIO_MAP_SIZE) -1) { perror(Failed to munmap); } close(fd); return EXIT_SUCCESS; }编译与运行 在AUP ZU3的PetaLinux系统上使用交叉编译工具链或直接在板卡上编译如果已安装gcc。# 在板卡上直接编译 gcc -o axi_gpio_test axi_gpio_test.c # 运行需要root权限因为要访问/dev/mem sudo ./axi_gpio_test程序逻辑解析通过open(“/dev/mem”)打开物理内存设备文件。使用mmap将AXI GPIO的物理地址0x8000_0000映射到用户空间的虚拟地址。O_SYNC标志确保读写操作直接作用于设备不经过缓存这对于硬件寄存器操作是必须的。通过计算偏移量得到数据寄存器gpio_data的指针。向*gpio_data写入数据即可控制8位GPIO的输出电平。写入0x01二进制00000001即让第0位输出高电平其他为低。程序依次点亮每一位然后全部关闭。重要提示直接操作/dev/mem需要root权限且映射的物理地址必须准确否则可能导致系统崩溃。在生产环境中可以考虑编写一个内核模块来提供更安全的字符设备接口或者使用libgpiod这样的用户空间库如果内核和根文件系统支持。6. 调试与排错我踩过的那些坑整个过程并非一帆风顺以下是几个典型问题及解决方法。6.1 设备树节点未被识别sysfs中无对应gpiochip现象/sys/class/gpio/下没有新的gpiochipdmesg | grep gpio也没有相关日志。排查检查设备树源文件确保system-user.dtsi被正确包含并编译。检查compatible字符串是否完全正确一个字母都不能错。检查reg地址是否与Vivado中的分配一致。检查内核配置确认CONFIG_GPIO_XILINXy已设置。可以查看/proc/config.gz如果启用或内核的.config文件。检查编译产物查看最终生成的设备树二进制文件device-tree.dtb是否包含了你的节点。可以使用dtc -I dtb -O dts images/linux/system.dtb | grep -A 10 “axi_gpio”来反编译确认。解决最可能的原因是compatible字符串错误或地址reg格式错误。仔细核对Vivado中的IP配置和地址编辑器中的信息。6.2 GPIO可以导出但设置方向或值时报错“Device or resource busy”现象echo 504 /sys/class/gpio/export成功但echo out direction或echo 1 value时失败。原因该GPIO引脚可能被内核中的其他驱动占用了。在ZU平台上PL侧的引脚如果也被用于其他IP核如I2C、SPI的引脚复用或者在设备树中被其他节点引用就可能发生冲突。排查检查Vivado中的引脚分配确保这8个GPIO引脚没有分配给其他IP核。检查设备树确保没有其他节点如axi_iic_0使用了相同的引脚。6.3 C程序运行时报“Segmentation fault”或“Bus error”现象编译的C程序一运行就崩溃。原因地址映射失败mmap返回MAP_FAILED。可能是/dev/mem打开失败权限不足或者物理地址GPIO_BASE_ADDR不对。地址访问越界指针计算错误访问了未映射的内存区域。排查检查程序是否以root运行。在mmap后立即检查返回值并打印错误信息perror(“mmap”)。仔细核对GPIO_BASE_ADDR。确保它是完整的64位地址并且在reg属性中正确指定。在64位系统上有时需要处理高低32位。使用printf打印映射后的虚拟地址和计算出的寄存器指针确保它们在合理的范围内。6.4 写入值后硬件引脚上没有电平变化现象软件操作一切正常但用万用表或示波器测量物理引脚电平没有变化。排查硬件连接首先确认你的测量点是否正确板卡是否上电引脚是否连接正确。Vivado约束回头检查Vivado中的XDC约束文件确认gpio_io_o[*]信号被正确地分配到了你期望的物理引脚上并且I/O标准如LVCMOS33设置正确。一个常见的疏忽是约束文件没有在综合实现过程中被正确应用。比特流文件确认你烧写到板卡上的.bit文件是最新生成的包含了正确的GPIO设计和引脚约束。有时需要重新生成比特流并重新加载。方向寄存器在C程序中读取并打印方向寄存器*gpio_tri的值。如果是全输出IP它应该是0。如果非0手动将其写为0再尝试。电平标准与驱动能力检查板卡原理图确认该引脚没有外部上拉/下拉电阻导致电平固定。确认FPGA的Bank电压VCCIO是否正确设置为3.3V。7. 进阶思考与优化建议当基本的输出功能实现后可以考虑以下方向来完善和优化你的GPIO控制方案。7.1 实现输入功能与中断如果你的应用需要读取开关状态或检测上升/下降沿就需要将AXI GPIO配置为输入或双向模式。硬件修改在Vivado中重新配置AXI GPIO IP核取消“All Outputs”并根据需要设置输入/输出位宽。设备树修改更新system-user.dtsi中的xlnx,all-inputs和xlnx,all-outputs属性。对于双向引脚这两个属性可能都需要设置为0并通过方向寄存器动态控制。软件操作轮询在C程序中定期读取*gpio_data寄存器的值。中断在Vivado中使能AXI GPIO的中断功能并在设备树中添加interrupt-parent gic;和interrupts ...;属性。在驱动中GPIO子系统会将中断映射为标准的Linux GPIO中断用户空间可以通过poll()或epoll()监听/sys/class/gpio/gpioX/edge和value文件或者在内核编写驱动处理。7.2 使用libgpiod库对于新的项目Xilinx推荐使用libgpiod库来代替传统的sysfs接口。它提供了更清晰、更稳定、性能更好的API。优点不依赖全局的GPIO编号如504而是通过芯片标签如“80000000.gpio”和偏移量0-7来寻址。避免了sysfs接口的一些竞态条件和已弃用的问题。在PetaLinux中集成在petalinux-config -c rootfs中可以在Filesystem Packages-libs-libgpiod下选择安装该库及其工具。使用安装后可以使用gpiodetect、gpioinfo、gpioset、gpioget等命令行工具或者调用libgpiod的C API进行编程。7.3 性能考量与实时性Sysfs速度最慢适合不频繁的操作如初始化、配置。内存映射mmap如本文C程序所示速度极快延迟在微秒级适合需要快速翻转GPIO的场景。内核模块如果对实时性要求极高硬实时可能需要编写一个内核模块并可能结合Xenomai或PREEMPT_RT实时内核补丁。对于大多数应用mmap方式已完全足够。7.4 管理与维护设备树覆盖在开发后期可以考虑使用设备树覆盖Device Tree Overlay来动态加载GPIO配置而无需重新编译整个内核镜像。系统服务化将GPIO控制功能封装成一个系统守护进程daemon通过DBus、Socket或共享内存等方式提供API给其他应用调用实现集中管理和资源安全访问。整个流程走下来从Vivado画图到PetaLinux配置再到最终用C程序控制引脚高低电平虽然步骤繁多但每一步都有其明确的意义。最关键的是理解硬件AXI GPIO IP与软件设备树、内核驱动、内存映射之间的桥梁关系。一旦这个通道打通在ZU平台上操作PL侧外设的思路就基本通了后续操作其他自定义IP核如PWM、定时器也将是类似的流程。