ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FPGA软硬协同开发入门:Vitis平台搭建Zynq UART与GPIO完整实操

2026/9/5 10:19:17 拓冰建站 浏览量
FPGA软硬协同开发入门:Vitis平台搭建Zynq UART与GPIO完整实操 写这个系列的时候我经常被问到一个问题前面四篇都在写Verilog、写仿真怎么做到第五篇突然画风一转要聊Vitis了对不少从近似零基础一路看过来的朋友来说FPGA开发基本上等于写代码、看波形突然冒出一个和C语言、和软件工程强相关的环境确实容易懵。先放下这种困惑我们理清一件事FPGA能干的事分两类。一类是纯硬件逻辑用Verilog或VHDL描述电路另一类是软硬协同在FPGA内部放一个处理器软核比如MicroBlaze或者直接用芯片里的硬核ARM比如Zynq系列然后在这个处理器上写C代码通过总线去控制FPGA里的逻辑和外部外设。到了后者你就绕不开Vitis。它是Xilinx现在归AMD提供的统一软件开发平台用来编译、调试、部署在处理器上跑的裸机程序或Linux系统。我这次就以最常见的Zynq-7000系列为例完整走一遍在Vitis里搭建工程、跑通UART串口回显和GPIO控制LED的全过程。目标读者和前面几篇保持一致——你不需要有C语言开发经验只需要跟着操作理解每一步在干什么。1. 为什么必须学会Vitis软硬协同不再是可选项1.1 从纯逻辑到带“大脑”的FPGA转变前几篇我们做的都是纯组合逻辑、时序逻辑用LED、数码管、按键这些外设验证电路是否正确。那种模式下所有行为都由硬件描述语言固化一旦烧录功能就固定了。但实际项目里经常需要动态控制上电后先初始化然后根据串口收到的指令改变逻辑行为甚至要把数据往内存里搬。这种场景如果全部用Verilog写状态机也能做但复杂度会爆炸。举个例子你想让FPGA通过串口接收不同命令命令里面包含滤波参数、增益系数然后动态改变信号处理链路。用Verilog写一个解析模块、再写一个参数配置寄存器、还要考虑时序同步调试起来非常痛苦。而在Zynq上ARM核里跑C代码一条printf就能看到状态一个XGpio_WriteReg就能控制逻辑开发效率完全不同。1.2 Vitis和Vivado的分工一个是工地一个是施工图很多刚入门的朋友会把Vivado和Vitis搞混。简单说Vivado负责硬件设计也就是搭建FPGA内部的电路结构包括把ARM核配置好、挂上各种外设IP、然后生成一个叫xsaXilinx Support Archive的硬件描述文件。Vitis负责软件设计它读到这个xsa文件后知道芯片里有哪些外设、内存多大、中断怎么接然后在这个基础上编译你的C代码。打个比方Vivado是建筑施工队把房子盖好各种插座、水管位置都预留好Vitis是装修队拿到房子的户型图决定怎么布置家具、走电线。两者缺一不可。前几年Vitis还没出来的时候Xilinx用的是SDKSoftware Development Kit当时很多人吐槽它和Vivado切换到卡顿、代码补全弱。Vitis算是SDK的全面升级加入了多线程编译、更好的调试器还支持AI推理库但核心的裸机开发流程其实变化不大。1.3 我们这次要搭建的最小系统长什么样为了不在一开始就把人绕晕我建议你先在脑海里记下这个最小系统图Zynq的ARM核PS端通过AXI总线连接一个UART控制器和一个GPIO控制器。UART控制器把数据送到芯片外部的串口引脚GPIO控制器把电平送到LED灯引脚。你在Vitis里写的C代码就是控制这两个控制器干活。就这么简单。全程不碰任何逻辑门。2. Vitis它爹Vivado先搭一个带UART和GPIO的硬件平台Vitis不是凭空就能用的你手里必须有一个xsa硬件描述文件。这个文件从哪来从Vivado的Block Design里来。很多人一开始就在这一步卡壳因为习惯了纯Verilog开发突然要画图形化连线感觉像是在操作一个陌生软件。别怕图形化操作其实比写代码更直观。2.1 创建Vivado工程并选择Zynq器件打开Vivado我这里是2021.2版本Vitis版本必须和Vivado匹配选择Create Project输入工程名比如zynq_uart_gpio。在Part这一步选择你手头开发板对应的器件型号。如果你用的是常见的Zynq-7010或7020开发板一般要选xc7z010clg400-1或者xc7z020clg484-1。实在找不到直接搜索器件型号或者选择Boards标签页里官方预设的开发板。选对器件型号很关键后面Vitis要依据这个来匹配编译工具链。万一选错了也不是完全不能用但板子上一些外设的引脚定义会和你导入的xsa不匹配到时候报错会很烦。2.2 在Block Design里添加Zynq PS处理器创建完工程后左侧Flow Navigator里点Create Block Design名字随意比如system。在Diagram窗口空白处右键选Add IP搜索zynq找到Zynq 7000双击添加。添加之后会有一个Zynq处理器的框图出现在画布上但这个框还需要配置。双击它进入Re-customize IP界面。这里配置项很多我们只动两个最关键的。第一是PS-PL Configuration里的General把GPIO和UART相关的接口打开。在Zynq-7000里PS端本身自带一个UARTUART1和一组GPIOMIO。我这里配置UART1使用MIO 48-49速率默认115200GPIO MIO没有特殊需求保持默认。第二是Peripheral I/O Pins确认UART1、GPIO对应的MIO引脚没有被别的功能占用。配置完点击OK回到Diagram你会看到PS处理器的框上多出了外设引脚。2.3 关键一步把UART和GPIO映射到内部连接其实Zynq PS端自带的UART和GPIO默认就已经连接到PS内部了不需要额外连线。但在Block Design里你还需要做一件事让Vivado知道这些外设不需要外部逻辑介入。有一个很常见的操作在Diagram窗口上方的工具栏里有一个Run Block Automation按钮点它会自动帮你完成一些连接。这时候Vivado会问你PS的UART和GPIO要不要引出到外部或挂到PL端我们保持默认让它们留在PS内即可。另外还要点一下Run Connection Automation它会把PS的时钟、复位、内存这些连接自动搭好省去手动连线的麻烦。建好的Block Design里应该能清楚看到PS核右侧有M_AXI_GP0接口这是PS访问PL端的通道但本工程用不到PL逻辑所以不连也行。2.4 生成比特流并导出硬件这是最容易忽略的一步。很多人以为只要Block Design画完就能直接进Vitis了其实不行必须先生成比特流bitstream和导出硬件工程。在Sources窗口右键你的Block Design先选择Generate Output Products让它把IP核的约束和网表生成出来。然后点击左侧Program and Debug里的Generate Bitstream。这个过程可能需要几分钟第一次跑还会弹出需要综合和实现的提示一路Yes就行。等待比特流生成完毕后点菜单File - Export Hardware。注意在弹出的对话框里务必勾选Include bitstream这样导出的xsa文件里不仅包含硬件描述还包含了你刚刚综合出来的比特流。Vitis在烧写程序的时候会把比特流一起下载到FPGA里如果你不勾选这个选项后面在Vitis里直接运行就会报“找不到比特流”的错误。导出之后会生成一个system_wrapper.xsa文件。这个文件就是Vitis的地基。2.5 补充为什么这次不用MicroBlaze软核有读者可能会想既然Vitis也支持MicroBlaze为什么我们要用Zynq的ARM硬核主要原因是Zynq的PS端处理能力强得多UART和GPIO外设也都是现成的不需要在PL里例化额外IP。MicroBlaze虽然灵活但每次都要自己配置定时器、中断、可用的内存流程更复杂。对于零基础入门ARM硬核的体验更接近单片机开发容易建立信心。3. Vitis工程搭建实操从xsa文件到第一个裸机可执行程序硬件导出成功接下来才真正进入Vitis环节。如果你是第一次打开Vitis会有种进入另一个世界的感觉——这里没有原理图没有Verilog只有C代码和编译配置。别慌跟着步骤走。3.1 打开Vitis并创建工作空间从Vivado菜单里直接选Launch Vitis或者单独启动Vitis。它会要求选择一个工作空间路径workspace建议专门建个目录比如D:/fpga_vitis_workspace。这里有一个很多新手常踩的坑工作空间路径不能有中文也不能有空格。Xilinx的编译工具链对路径非常敏感一旦路径里出现中文后面编译时可能出现莫名其妙的“无法打开文件”或者“找不到头文件”错误。我最初在“桌面/我的工程”这种路径下建工作空间折腾了一晚上没编过后来把路径改成纯英文一次通过。3.2 创建平台工程Platform Project进入Vitis主界面后先看到一个Welcome窗口关掉即可。然后点菜单File - New - Platform Project这里就是导入xsa文件的地方。输入平台工程名比如platform_uart_gpio。点击Next在Hardware Specification这里点击Browse选择你刚刚导出的system_wrapper.xsa文件。下方有一个Operating System选项选择standalone裸机模式Processor会自动识别为ps7_cortexa9_0这是Zynq的ARM双核中的一个核。这里解释一下standalone是什么意思。它就是一个最小的C运行环境没有操作系统提供硬件底层驱动库和标准库的一部分。你用printf能往UART输出就是因为standalone把串口驱动和printf重定向做好了。创建完平台工程后Vitis会自动扫描xsa文件生成一堆驱动库文件包括xuartps.hUART驱动和xgpiops.hGPIO驱动等。这些库就是Xilinx提供给我们的“官方SDK”。3.3 创建应用工程Application Project右键平台工程选择New - Application Project输入应用工程名比如hello_uart_gpio。然后选择你刚创建的平台工程作为目标平台。接下来选择模板。Vitis提供了一些示范模板比如“Hello World”、“Empty Application”。对于我们的需求直接选Empty Application因为我要给你手写代码让你理解每一步。选Hello World虽然能快速跑通串口但背后隐藏了太多细节对学习不利。点击Finish工程就创建好了。这时候左侧Project Explorer里应该能看到一个src文件夹你的C代码就放在这里。3.4 BSP配置里的两个开关在Vitis里平台工程下有一个BSPBoard Support Package文件双击打开可以看到驱动库列表。对于UART默认是开启的GPIO驱动也是默认开启的。但有一个容易忽略的配置standalone库里的stdin/stdout设置。默认情况下stdin和stdout都指向我们配置的UART1。如果你的系统里有多个串口或USB转串口可能就要手动调整。检查一下Board Support Package Settings里的standalone选项卡确保stdout和stdin都设置成了ps7_uart_1。另外一个开关是按位缓存Cache。Zynq的ARM核有L1缓存默认是开着的。如果涉及DMA和FPGA共享内存缓存一致性问题会很麻烦但本工程只用UART和GPIO开缓存没影响保持默认即可。4. 让串口说话UART回显代码与GPIO点灯一起跑起来现在开始写C代码。这是最让人兴奋的一部分因为你马上能在屏幕上看到来自FPGA开发板的消息了。4.1 编写UART回显程序一个最简单的串口Demo右键src文件夹选择New - File命名为main.c。然后输入以下代码代码是Xilinx库的标准用法#include xparameters.h #include xuartps.h #include xgpiops.h #define UART_DEVICE_ID XPAR_XUARTPS_0_DEVICE_ID #define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID #define LED_PIN 7 // 这里用PS端MIO的某个引脚具体看你的硬件设计 static XUartPs Uart; static XGpioPs Gpio; int main() { XUartPs_Config *UartConfig; XGpioPs_Config *GpioConfig; u8 RecvBuf; u32 LedValue 0; // 初始化UART UartConfig XUartPs_LookupConfig(UART_DEVICE_ID); XUartPs_CfgInitialize(Uart, UartConfig, UartConfig-BaseAddress); XUartPs_SetBaudRate(Uart, 115200); // 初始化GPIO GpioConfig XGpioPs_LookupConfig(GPIO_DEVICE_ID); XGpioPs_CfgInitialize(Gpio, GpioConfig, GpioConfig-BaseAddr); XGpioPs_SetDirectionPin(Gpio, LED_PIN, 1); XGpioPs_SetOutputEnablePin(Gpio, LED_PIN, 1); // 写一行欢迎信息 char msg[] UART OK! Please input.\r\n; XUartPs_SendBuffer(Uart, (u8*)msg, sizeof(msg)-1); while(1) { // 串口收一个字节再回显一个字节 if (XUartPs_IsReceiveData(Uart)) { RecvBuf XUartPs_ReadReg(Uart, XUARTPS_FIFO_OFFSET); XUartPs_SendBuffer(Uart, RecvBuf, 1); // 顺便翻转LED状态看着直观 LedValue ^ 1; XGpioPs_WritePin(Gpio, LED_PIN, LedValue); } } return 0; }别急着编译先理解这段代码在干什么。头文件xparameters.h是所有外设基地址的集合xuartps.h和xgpiops.h是UART和GPIO的驱动函数声明。XUartPs_LookupConfig和XUartPs_CfgInitialize是初始化接口几乎每个裸机外设都有这么一套。XUartPs_SetBaudRate设置波特率115200全兼容。GPIO部分用XGpioPs_SetDirectionPin把MIO7设为输出然后循环里每秒翻转一次电平。4.2 把printf重定向到串口上面代码你没看到printf但实际开发中肯定想用。Xilinx在standalone库中已经做了printf重定向前提是你正确初始化了UART并且stdout指向了这个UART。多写一个初始化函数就行#include stdio.h // 在XUartPs_CfgInitialize之后调用 // 需要包含 xil_printf.h 来获得轻量级printf可以改成直接用xil_printf它是Xilinx的精简实现只支持基本格式符用于嵌入式场景足够。在main.c中加上#include xil_printf.h然后把上面代码里的XUartPs_SendBuffer换成xil_printf(UART OK! Please input.\r\n);再在while循环中直接对接收到的字符调用xil_printf(%c, RecvBuf);。这个函数比标准printf占用空间小得多对零基础更友好。4.3 生成启动文件.elf和可执行文件写完代码后右键应用工程选Build Project。Vitis会调用arm-none-eabi-gcc编译链把main.c编译链接生成.elf文件这个文件就是我们最终要下载到Zynq上的可执行程序。第一次编译可能需要几分钟因为要链接库文件。看到Build Finished和Finished building target就说明成功了。如果编译报错最多的情况是xparameters.h找不到先检查平台工程是否已经Build过。右键平台工程选Build Project等它生成BSP库然后再编译应用工程。4.4 连接开发板与运行用USB线连接开发板的UART口到电脑安装好USB转串口驱动。我用的开发板是在电脑设备管理器里能看到COM端口。打开串口调试助手或Vitis自带的Terminal设置波特率115200数据位8停止位1无奇偶校验。接下来在Vitis中右键应用工程选Run As - Launch on Hardware。Vitis会自动完成三件事下载比特流到PL端、把.elf加载到ARM核、复位运行。如果一切顺利串口调试助手里应该会立刻出现UART OK! Please input.。随便在发送框输入一串字符比如abc123你会看到同样的内容从串口助手收到。同时那盏接在MIO7上的LED灯会随着你每次输入字符而翻转状态。那一刻我真的挺感动从Verilog写状态机到C代码控制外设中间看似跨越很大实际只隔了一个Vitis。5. 实测中的那些坑串口乱码、引脚没输出、下载失败排查笔记这部分我专门把实操过程中遇到过的几个典型问题拉出来讲每个都是真实经历对应的解决思路是通用的。5.1 串口输出乱码或者根本看不到字符先排查波特率。如果Vitis里BSP设置是115200而你的串口调试助手用了9600屏幕上肯定是乱码。但更隐蔽的问题是UART引脚映射错误。在Vivado配置PS时UART用的是MIO 48/49但开发板上USB转串口芯片接的却是MIO 14/15这就需要你修改配置把UART分配到正确引脚上。怎么确认查开发板的原理图。你会发现很多Zynq开发板出厂把UART1连到MIO 48/49但也有用UART0的。Vivado里选中PS核看外设IO配置里面能勾选UART0或UART1并显示对应的引脚。务必和原理图对上号。还有一种是USB转串口芯片供电问题FT232R或CP2102如果供电不稳也会收不到数据。可以先检查板子上对应芯片的电源指示灯。5.2 GPIO引脚没输出LED死活不亮这个问题最常见的原因是引脚方向和输出使能没设对。Zynq的GPIO库函数有两个概念方向和输出使能。方向为1代表是输出输出使能为1代表引脚真正输出信号。两个都要设置缺一不可。XGpioPs_SetDirectionPin(Gpio, LED_PIN, 1); XGpioPs_SetOutputEnablePin(Gpio, LED_PIN, 1);有些初学者只设了方向忘了使能结果LED纹丝不动。另外确认LED接在哪个MIO引脚上。如果接在MIO9你代码里写LED_PIN 7自然点不亮。建议先用万用表量一下引脚电压若一直低电平多半是方向或使能问题。5.3 下载失败找不到器件或DONE信号未拉高在Vitis点击Launch on Hardware时有时候会报错提示could not open port或者JTAG cable。先检查电源然后检查下载器驱动。很多板载JTAG使用FTDI芯片需要安装驱动才能被Vivado识别。打开Vivado的Hardware Manager如果能扫描到器件才说明连接正常。如果下载时卡在Programming FPGA很久最后报错DONE failed可能是脉冲DDR训练没通过。对Zynq来说如果DDR配置不正确比如内存颗粒型号选错虽然UART和GPIO程序用不到DDR但PS初始化和比特流下载过程仍然会卡在DDR自检上。解决办法是回到Vivado在DDR配置里选择正确的型号和总线宽度。5.4 修改硬件配置后没有重新导出xsa这个坑特别隐蔽。如果你在第一次导出后又回到Vivado改了配置比如换了串口引脚、调整了DDR但没有重新Generate Bitstream和Export Hardware那么Vitis里用的还是旧的xsa文件。即使你新建了平台工程也还是旧参数。我后来养成了习惯每次改硬件都务必完整执行两遍——Generate Bitstream和Export Hardware并且把新生成的xsa文件覆盖到原位置或者给平台工程重新Specify。5.5 用printf去调试小心堆栈默认情况下Zynq的ARM核从DDR启动需要初始化DDR控制器。如果DDR没配置好而你又用了标准printf会直接跑飞。一个保险的做法是使用xil_printf它不依赖堆栈分配。6. 从这一个Demo我们能延伸出什么UART回显和GPIO点灯看起来微不足道但它验证的是一条完整的软硬件链路Vivado创建硬件平台Vitis加载xsa编写裸机C程序下载运行串口交互。这条路一旦打通后面很多事情都是一马平川。你可以在这个基础上扩展无限可能。比如把GPIO换成PWM输出就可以控制电机转速把UART换成SPI或I2C就可以挂传感器把代码移植到FreeRTOS或者Linux就会进入更复杂的嵌入式软件世界。我个人在实际操作中的体会是很多人卡在Vitis这一步不是因为技术难而是因为过去习惯了纯硬件思维。一旦你接受了“处理器 外设寄存器 驱动库”这种软件思维就能同时打通FPGA开发和嵌入式开发两条线。这个Demo的价值不在功能而在帮你完成视角的转变。最后再分享一个小技巧在Vitis里的Debug Configuration也可以直接用单步调试观察寄存器值。我经常用Debug模式跑这个程序设置断点看XUartPs_SendBuffer执行后UART的FIFO寄存器变化比串口助手定位问题更准。学有余力的朋友可以试一试。