Zynq Vitis入门实操:GPIO点亮LED与UART回环详解

发布时间:2026/9/4 10:54:14
Zynq Vitis入门实操:GPIO点亮LED与UART回环详解 很多朋友玩FPGA一开始都是冲着Verilog逻辑设计去的跑通一个LED流水灯就觉得入门了。但一旦涉及真正的项目比如要和上位机通信、要调试寄存器、要控制外部设备你就绕不开一个东西嵌入式软核处理器。Zynq系列之所以受欢迎就是因为ARM处理器和FPGA在同一个芯片里既能跑软件又能做硬件逻辑。而Vitis就是Xilinx现在是AMD官方用来开发Zynq ARM端软件的工具。Part.5我来聊聊怎么在Vitis里搭建一个最基础的工程目标有两个用GPIO点亮LED再用UART实现回环echo。这两个功能通了基本就说明你的软硬件链路是完整的后面再上复杂应用心里才有底。1. 内容整体设计与思路拆解1.1 为什么是Vitis它不是个新东西好多教程现在还习惯叫“Vivado SDK”搞得新手一脸懵。这里先理清版本线Vivado 2019.2之后原来的SDK正式更名为Vitis底层还是Eclipse那套但集成了更多功能尤其是对AI推理和软件加速的支持。不过对我们做基础嵌入式开发来说Vitis和SDK用起来几乎一模一样就是换个名字、换个启动图标而已。这个系列的前几部分如果用的是Vivado 2019.2以上版本那Part.5跟着用Vitis就对了。如果你还在用老的2018.3界面可能有点差异但核心流程——导出硬件、创建platform、写应用、跑调试——是通用的不用慌。1.2 硬件工程导出的关键一步在进入Vitis之前你必须在Vivado里完成三件事创建Block Design加入Zynq PS核配置好UART一般是MIO 14/15对应板载USB转串口配置好GPIO这里用的是MIO外接的LED/按键或者EMIO都行看你的板子然后在Vivado里跑完综合、实现、生成比特流最后执行 File - Export Hardware注意勾选Include bitstream。这一步产生的.xsa文件就是Vitis工程的基石。很多人会在这个环节踩坑只导出了硬件描述文件没勾选包含比特流结果Vitis里Program FPGA时找不到bitstream。虽然可以在Vitis里重新指定但多一事不如少一事勾上最省心。1.3 Vitis工程的两个核心部件打开Vitis后你的整个工作区其实围绕两个东西转Platform和Application。Platform相当于硬件平台的定义它告诉编译器你的ARM核主频多少、有什么外设、DDR怎么分配。Application就是你的裸机程序或者Linux应用但这里先讲裸机它跑在Platform之上通过BSP提供的库函数操作硬件。这个分层逻辑很像我平时调STM32的方式Platform对应HAL库的底层配置Application就是你的main函数。好处是换硬件时只要重新生成Platform上层软件基本不用动。2. 核心细节解析与实操要点2.1 Zynq的GPIO到底有几个别搞混了Zynq的GPIO分两部分这是新手最容易懵的地方。第一部分是MIOMultiplexed I/O一共54个引脚直接挂在PS端ARM处理器上。它们不经过FPGA逻辑所以响应快用起来像单片机。MIO的功能是复用式的除了当GPIO还能配置成UART、SPI、I2C等各种外设引脚。第二部分是EMIOExtended MIO一共64个引脚走的是FPGA逻辑侧。你在Vivado Block Design里把GPIO引脚数拉到64以上多出来的就是EMIO。EMIO需要经过FPGA的引脚约束才能到外部所以延时大一些但好处是引脚位置灵活可以随便分配。我Part.5的板子用的是MIO接的LED和按键所以只需要在Vivado里勾选GPIO MIO Bits就可以了。如果你的板子LED接在FPGA侧就选EMIO然后在约束文件里指定物理引脚。2.2 UART的波特率与时钟树关系Zynq的UART控制器是挂在一个可配置时钟域上的默认情况下UART参考时钟通常是100MHz来自IO PLL或者ARM PLL分频。Vivado会根据你的波特率设置自动计算分频系数。这里要提醒一个细节在Vivado里配置UART时波特率字段有个Override选项如果你选了标准波特率比如115200但实际上板载USB转串口芯片比如FT232或CP2102的时钟精度不高长时间通信可能出现偶发乱码。这种情况不如在代码里用XUartPs_SetBaudRate动态设置一次驱动会自动重新计算分频器比依赖Vivado的静态配置更稳。我自己实测下来PS端的UART处理器在115200下非常稳除非你把参考时钟改得很离谱。所以这个坑主要出现在时钟配置混乱的项目里Part.5用默认配置就行。2.3 中断还是轮询第一次别贪多UART和GPIO的驱动方式都有三种轮询Polling、中断Interrupt、DMA。很多教程一上来就教你配中断一配就是全套中断控制器初始化、中断回调注册、使能中断源。对零基础的朋友来说这信息量有点大。而且一旦中断服务函数里出了bug查起来比轮询难一个量级。我的建议是Part.5先全部用轮询方式GPIO读按键直接XGpio_DiscreteRead管它准不准先读出来再说UART收数据用XUartPs_Recv加超时判断收到什么发什么这样代码逻辑是线性的出了问题分段打串口日志就能定位。等这套流程跑顺了Part.6再引入中断你会更容易理解中断到底帮你省了什么资源而不是稀里糊涂地把一堆寄存器配完就完事。2.4 为什么要同时做GPIO和UART把这两个外设放在同一个工程里是有讲究的。很多人觉得单独跑个LED闪烁叫点亮单独打通串口叫通信。但实际项目里GPIO和UART通常是联动的。比如你想知道某个传感器数据什么时候更新习惯做法是传感器数据来了GPIO拉高通知处理器处理器通过UART把这个事件发到上位机。或者反过来上位机通过UART发送控制指令处理器解析后通过GPIO输出电平控制继电器。这种“一个输入一个输出、一个控制一个反馈”的组合才是嵌入式开发的基本盘。所以Part.5看似是两个独立外设的实验其实是在给你搭建一套完整的外设协同处理框架。3. 实操过程与核心环节实现3.1 第一步Vivado侧完整导出假设你已经在Vivado里建好了带Zynq PS的Block Design接下来是最容易漏掉的流程。首先打开Block Design双击Zynq PS核进入配置界面。在Peripheral IO Pin和MIO Configurations面板里确认UART1的TX/RX勾选在MIO 14/15上这是最常见的板载串口映射。确认GPIO里MIO相关位被勾上具体哪几位能用要看你的板卡原理图。然后回到Vivado主界面在Sources窗口右键你的Block Design选择Generate Output Products等待完成。接下来在左侧Flow Navigator点击Run Synthesis、Run Implementation最后Generate Bitstream。烧完bitstream后执行File - Export Hardware勾选Include bitstream导出的文件保存为design_1_wrapper.xsa。实际操作里我会顺手检查一下.xsa文件的大小如果只有几KB很可能没包含bitstream正常情况应该有几十KB以上虽然这不是绝对标准但可以辅助判断。3.2 第二步创建Vitis平台工程打开Vitis选择一个空的工作目录菜单栏File - New - Platform Project名字叫zynq_platform。在弹窗里点击Browse选择刚才导出的design_1_wrapper.xsa然后点击Finish。Vitis会自动解析这个.xsa文件生成platform。这一步完成后左侧Project Explorer里会出现zynq_platform展开可以看到ps7_cortexa9_0等子项这就是你的ARM核硬件抽象层。第一次生成platform会比较慢因为要编译BSP库。如果中途报错多半是.xsa文件本身有问题或者Vitis版本和Vivado版本不匹配。官方要求大版本一致比如都是2020.2或2021.1混用版本经常会出一些奇怪问题。3.3 第三步创建Application应用工程选择菜单File - New - Application Project名字叫hello_uart_gpio在弹出的界面里选择Use existing platform指定刚才的zynq_platform。接下来选择模板这里不要选Hello World我们选Empty Application(C)从零开始写。Application创建完成后右键工程名 - Build Project先把整个工程编译一遍。此时虽然没有任何源文件但这个步骤能验证platform和编译器工具链是否正常。然后右键hello_uart_gpio下的src目录选择New - Source File创建一个main.c把下面的代码写进去。3.4 第四步核心代码实现与解析先说代码框架Part.5需要三个文件main.c主逻辑、gpio_driver.cGPIO封装、uart_driver.cUART封装。工程虽小但分文件是习惯问题为以后代码膨胀打基础。main.c里最核心的逻辑#include xparameters.h #include xgpio.h #include xuartps.h #include xil_printf.h #define GPIO_DEVICE_ID XPAR_XGPIO_0_DEVICE_ID #define UART_DEVICE_ID XPAR_XUARTPS_0_DEVICE_ID #define LED_CHANNEL 1 #define BTN_CHANNEL 2 #define LED_MIO_PIN 0x01 #define BTN_MIO_PIN 0x01 static XGpio gpio_inst; static XUartPs uart_inst; static u8 recv_buffer[64]; int main(void) { int status; u32 led_state 0; status XGpio_Initialize(gpio_inst, GPIO_DEVICE_ID); if (status ! XST_SUCCESS) { xil_printf(GPIO init failed\r\n); return -1; } XGpio_SetDataDirection(gpio_inst, LED_CHANNEL, 0x0); // 输出 XGpio_SetDataDirection(gpio_inst, BTN_CHANNEL, 0x1); // 输入 status XUartPs_LookupConfig(UART_DEVICE_ID); if (status NULL) { xil_printf(UART config lookup failed\r\n); return -1; } XUartPs_CfgInitialize(uart_inst, status, UART_DEVICE_ID); XUartPs_SetBaudRate(uart_inst, 115200); xil_printf(UART GPIO Test Start\r\n); while (1) { u32 btn_val XGpio_DiscreteRead(gpio_inst, BTN_CHANNEL); if (btn_val BTN_MIO_PIN) { led_state ^ 0x01; XGpio_DiscreteWrite(gpio_inst, LED_CHANNEL, led_state); xil_printf(Button pressed, LED toggled\r\n); } int bytes XUartPs_Recv(uart_inst, recv_buffer, sizeof(recv_buffer)); if (bytes 0) { XUartPs_Send(uart_inst, recv_buffer, bytes); } usleep(50000); // 50ms防抖 } return 0; }这段代码有几个细节我要说一下第一XGpio_SetDataDirection第二个参数传的是通道号。XGpio驱动用通道区分不同bank对应Vivado里的GPIO位宽配置。一般通道1对应MIO [0:31]通道2对应MIO [32:53]如果你的配置不同需要对照xparameters.h里的宏定义。第二XUartPs_Recv的返回值是实际接收到的字节数不是寄存器状态。如果你发现回环总是漏数据多半是缓冲区满之前上位机已经发了超过64字节的数据。Part.5的测试规模下64字节完全够用。第三防抖我用的是最简单的usleep。这个方法对教学够了但实际项目里做按键检测应该用定时器中断加状态机来实现避免阻塞主循环。这里先不展开后面系列里专门讲。gpio_driver.c的封装#include gpio_driver.h static XGpio s_gpio; int gpio_init(u16 device_id, u32 out_mask, u32 in_mask) { int status XGpio_Initialize(s_gpio, device_id); if (status ! XST_SUCCESS) { return status; } XGpio_SetDataDirection(s_gpio, 1, out_mask); XGpio_SetDataDirection(s_gpio, 2, in_mask); return XST_SUCCESS; } void gpio_write(u32 value) { XGpio_DiscreteWrite(s_gpio, 1, value); } u32 gpio_read(void) { return XGpio_DiscreteRead(s_gpio, 2); }这里把LED通道固定为通道1、按键通道固定为通道2在板子固定的情况下这种简化是合理的。如果你是通用模块建议把通道号也做成参数传进去。uart_driver.c的封装#include uart_driver.h static XUartPs s_uart; int uart_init(u16 device_id, u32 baud_rate) { XUartPs_Config *cfg XUartPs_LookupConfig(device_id); if (cfg NULL) { return XST_FAILURE; } int status XUartPs_CfgInitialize(s_uart, cfg, device_id); if (status ! XST_SUCCESS) { return status; } XUartPs_SetBaudRate(s_uart, baud_rate); return XST_SUCCESS; } int uart_send(u8 *buffer, u32 length) { return XUartPs_Send(s_uart, buffer, length); } int uart_recv(u8 *buffer, u32 length) { return XUartPs_Recv(s_uart, buffer, length); }封装的好处是main函数里看不到一堆驱动对象逻辑会非常清爽。你写复杂应用时这些基础函数改都不用改直接在业务层调用就行。3.5 第五步硬件验证流程编译工程无误后点击菜单Xilinx - Program FPGA这时候会弹出硬件配置文件选择框建议选上design_1_wrapper.bit。如果你的JTAG链上还有其他设备这里可能显示多个FPGA器件选对型号就行。Program完成后右键hello_uart_gpio工程选择Run As - Launch on Hardware (Single Application Debug)。Vitis会自动下载elf文件到DDR里并运行。这时串口终端应该输出UART GPIO Test Start。然后做两个实验按一下按键LED状态翻转串口同时打印Button pressed, LED toggled在串口终端里输入任意内容比如hello FPGA发出去终端里会原样收到一遍这两个实验都通过恭喜你Zynq的软硬件链路已经彻底打通了。3.6 关于Vitis里运行Linux的后续思路如果你最终目标是vitis 运行linux也就是在Zynq上跑PetaLinux或者手工构建的Linux镜像那Part.5这套裸机代码其实没白写。Linux设备树里UART和GPIO的节点描述本质上就是在告诉内核硬件资源在哪、中断号是多少跟你在Vivado里配置外设的思维模式是一致的。裸机程序调试熟了你对PMU、DDR、MIO这些硬件的理解会直接迁移到Linux开发里。我见过很多直接上手PetaLinux的初学者连devicetree.dts里的status okay是什么意思都搞不明白就是因为缺了裸机这一步。4. 常见问题与排查技巧实录4.1 程序下载后串口没有任何输出大概率不是程序问题是你板子的USB转串口芯片驱动没装对。Zynq开发板用得最多的三款芯片是FT232、CP2102和CH340对应驱动分别是FTDI VCP、Silicon Labs CP210x和沁恒官方Driver。检查方法很简单打开设备管理器看端口下面有没有带感叹号的设备。如果是FT232驱动装好后会识别成USB Serial PortCP2102会识别成Silicon Labs CP210x USB to UART Bridge。要是这些都没出现可能需要手动指定驱动路径。另外一个容易忽视的是接线。虽然MIO 14/15已经被Vivado配置成UART但板子上通常有个跳线帽或者拨码开关控制USB口跟PS UART的连接方向。拿万用表量一下或者翻一下板子原理图确认USB转串口芯片的RXD接到了PS的TXMIO15TXD接到了PS的RXMIO14。4.2 LED不亮的三种可能第一方向配置错了。XGpio_SetDataDirection(gpio_inst, LED_CHANNEL, 0x0)是全0输出如果你不小心写成0x1那只有bit0是输入、其余全输出如果LED接在bit0上就亮不了。第二物理极性反了。开发板的LED大多是高电平点亮但也有一些是灌电流接法高电平反而灭。这时候调试方法是在代码里先写死一个值比如XGpio_DiscreteWrite(gpio_inst, LED_CHANNEL, 0xFFFFFFFF)看LED亮不亮。亮了说明极性正常不亮就把值改成0x0再试。第三你用的管脚被其他外设占用了。Zynq的MIO有些引脚默认会被配置成Quad SPI Flash或者SD卡控制器。你在Vivado里如果没有显式将这些引脚设为GPIO系统可能默认保留给其他控制器。这时候去Zynq配置界面的MIO Configurations里逐项检查确保对应引脚属于GPIO。4.3 UART接收乱码或丢字节新手上路最常见的UART问题就是乱码。如果终端显示的是乱七八糟的符号先检查波特率。Vivado里默认如果是115200你要确保串口工具也是115200、8、N、1。这种低级错误我见过太多次了。排除波特率因素后再看时钟。Zynq的UART参考时钟如果有地方被改动了跟Vivado导出硬件时的配置不一致会直接导致波特率偏移。建议在代码里加一句校验u32 baud_actual XUartPs_GetBaudRate(uart_inst); xil_printf(Actual baud rate: %lu\r\n, baud_actual);输出如果是115200那硬件链路基本没问题问题大概率出在线材或干扰上。丢字节则通常是上位机发送太快、下位机来不及接收。轮询方式下XUartPs_Recv每次最多只能收满指定的缓冲长度剩余字节会留在FIFO里等下一次调用。如果上位机一次性发几百字节轮询代码一次取64字节要好几轮才能取完中间如果有其他耗时操作就会丢。Part.5工程你不用担心这个但后续做实际项目时建议直接换中断或DMA方式。4.4 Vitis连不上开发板的快速排查点Program FPGA时弹出No hardware target之类的话先别急着重装驱动。第一步检查JTAG线缆连接Zynq开发板一般用内置USB-JTAG不需要外接下载器。第二步在Vitis的菜单Xilinx - XSCT Console里输入connect看能不能发现设备。如果提示connect: timeout大概率是JTAG链路被其他程序占了关掉Vivado Hardware Manager再试。我遇到过一种很隐蔽的情况第一次Program FPGA成功第二次就报ERROR: [Vitis-60] No hardware target is available。后面发现是Vivado的Hardware Manager还在后台开着跟Vitis抢JTAG资源。把Vivado那边的硬件连接断开问题立刻解决。5. 进阶玩法与实际项目联想5.1 用这个框架做FMCC通信验证很多工业项目是stm32h743和fpga实现fmc通信Zynq想验证类似的并行总线通信思路也差不多。你可以把Zynq的GPIO配置成16位或者32位并行数据总线再拿几个GPIO当读/写/片选控制信号连接到一个简单的外设。在代码里写一个软件模拟的寄存器读写函数上位机通过UART发送寄存器地址和值Zynq解析后控制GPIO时序就像在操作FMC总线一样。这样做的好处是先把总线的时序逻辑在软件里调通等以后接到真正的高速外设时你已经对建立时间、保持时间、片选时序有概念了再迁移到FPGA侧的时序逻辑会顺手很多。5.2 跑卡尔曼滤波做传感器融合在Zynq上跑卡尔曼滤波 fpga这个方向属于PSPL协同工作的典型例子。通常PS端采集传感器数据、做预处理PL端实现高吞吐的滤波或者Hardware Accelerator。Part.5的UART和GPIO框架可以作为这个方向的骨架GPIO模拟读取传感器中断信号UART上报滤波结果。后面你需要做的就是在ARM上调通卡尔曼矩阵运算再决定要不要把核心计算挪到PL端的AXI接口IP里去。5.3 尝试高云FPGA或国产FPGA的迁移如果你工作环境用的是高云FPGA这类国产器件Vitis这套流程就不适用了因为高云有自己的IDE叫Tang Dynasty或者高云云源软件流程完全不同。但从思维模型上看仍是“软核/硬核处理器 可编程逻辑”这套组合。我在帮一个朋友评估国产FPGA方案时发现很多国产器件的GPIO和串口设计逻辑跟Xilinx差距没有想象中大。只要在Xilinx平台把外设驱动的分层思想学明白换平台无非是重新学一遍寄存器和编译工具上手周期比从零开始短很多。所以我一直建议新手先用Xilinx建立整体概念再去接触国产工具链兼容颗粒度完全不同效果会更好。6. 写在最后的几个心得做完Part.5这个工程我自己最大的体会是软件开发不能拖着硬件问题一起调试。很多人喜欢把UART、GPIO、中断、DMA全塞进一个工程里结果出了问题你根本不知道找软件还是找硬件。我现在的习惯是每个外设先单独验证验证通过后再组合。Part.5的GPIO和UART其实也是两个独立实验只是放进了一个工程文件里组合价值体现在交互逻辑上。另外一点是版本管理。Vitis工程的.xsa文件我建议和Vivado工程一起放到Git仓库里虽然二进制文件diff起来不方便但至少能保证换电脑或者回溯版本时不至于找不到硬件描述文件。我见过不少同事半年后重新打开老项目xsa文件找不到了Vivado工程里又改过配置导出后行为跟当初完全不一样排查起来极其痛苦。Vitis本身的生成文件确实很大每次编译都会产生几十MB到上百MB的中间产物。我在实际项目里的做法是workspace下面只提交src目录和platform目录的描述性文件其余generated文件夹全部加到.gitignore里。如果你的协作伙伴用同一个版本的Vivado和Vitis他拿到src和platform工程也能完整编译。Part.5的代码量虽然不大但覆盖了嵌入式开发里最能打的两个外设。把这两个玩透后续的UART协议解析、GPIO模拟I2C/SPI、甚至操作系统移植你都会觉得顺理成章。下一期如果继续我可能会讲Zynq的中断系统或者直接上新版Vitis里的Linux流程。