
从近似0基础开始FPGA开发 -- part.5 Vitis工程搭建Uart及GPIO前几篇我们把Vivado里的硬件工程搭了起来Block Design里也把Zynq PS核配好并且导出了xsa硬件描述文件。但说实话到这一步很多刚开始接触Zynq的朋友都会有点懵Vivado里看着那一堆block就已经晕头转向了接下来那个叫Vitis的玩意儿又是干嘛的我到底在哪儿写C代码这一篇我不打算绕弯子直接动手把Vitis工程从零到一建起来然后把UART串口和GPIO这两个最基础、但也是最常用的外设跑通。我尽量按我实际调试时一步步来的顺序写你跟着操作基本上一个晚上就能把这套流程吃透。先说几个前提条件免得你卡在环境上Vivado版本2020.1及以上我这边实测用的是2020.2和2021.1都没问题如果你用的版本太老Vitis的界面和操作路径会有差异。硬件环境是Zynq-7000系列或者Zynq UltraScale都行本文以Zynq-7000比如xc7z020为例ZU系列流程完全一样只是PS侧配置略有区别。你已经按照我part.4的内容在Vivado里完成Block Design的创建、PS侧MIO/UART/GPIO的配置并且成功生成比特流、导出xsa文件。如果你还没做这一步先回去补一下。好开始。1. 整体设计与思路拆解1.1 为什么用Vitis而不是老旧的SDK这是个老生常谈的问题但还是要说清楚。Xilinx从2019.2版本开始把原来Vivado内置的SDK工具彻底拆出来独立成了Vitis统一软件平台。你如果之前用过2018.3、2019.1版本的SDK会发现Vitis基本就是换了个皮但底层改了不少东西编译工具链从原来的arm-none-eabi-换成了通用的aarch64-linux-gnu或arm-linux-gnueabihf对不同操作系统的适配更好。不再区分SDK和Vivado的版本匹配问题只要Vitis版本匹配Vivado版本即可。支持多核开发、AI引擎开发、嵌入式Linux开发、HLS开发等多种模式说白了就是从MPSoC到RFSoC全覆盖。站在我个人的角度Vitis最大的感受是它对CMake的支持更好了而且独立的workspace管理比原来嵌在Vivado里那个SDK要清爽很多。当然也有人吐槽Vitis启动慢、内存占用高但用习惯了其实还好。1.2 打通UART和GPIO的完整链路不管你是做传统嵌入式还是FPGA开发UART串口永远是调试的第一选择。FPGA片上逻辑出了bug你总不能老用JTAG去抓信号吧通过串口打印log是最原始也最有效的手段。而GPIO就更基础了——点灯。可以说GPIO的工程跑通了你的Zynq开发环境就是真正能用的了后面什么DMA、中断、自定义IP核都是建立在这套流程之上的。梳理一下我们这一篇要搞定的完整链路Vivado硬件工程导出xsa ↓ Vitis新建platform工程用xsa生成 ↓ Vitis新建application工程baremetal裸机 ↓ SSH/串口工具连接开发板比如MobaXterm、SecureCRT ↓ 下载bitstream到PS/PL运行程序 ↓ 串口打印UART测试信息和GPIO测试信息这个流程图你看一眼心里就有底了后面每一个环节我都会展开说。1.3 选型定版MIO还是EMIOGPIO这块很多人会在MIO和EMIO之间纠结。直接说我的经验如果你只是做最简单的点灯验证优先用MIO如果你的PL逻辑FPGA可编程逻辑部分也需要读写这个GPIO或者你想用PL侧的引脚走线那就得用EMIO。MIO是PS侧专用的引脚一共54个直接连到Zynq的PS管脚上不是通过PL逻辑的所以它不需要占PL资源也不需要在原理图上做FPGA管脚约束。而EMIO是PS和PL之间的一个桥接接口它把PS的GPIO控制信号引到了PL侧占PL引脚你需要在管脚约束里重新分配脚位。说白了MCU级别的GPIO读写功能用MIO就够了如果你将来要做高速并行接口、或者希望用PL侧的引脚来扩展GPIO口再考虑EMIO。这篇我以MIO为主EMIO会在后面的中断、自定义IP章节再单独提。2. 核心配置Vivado端导出xsa和Vitis工程创建全流程2.1 从Vivado导出xsa前的最后一个检查点很多人在这一步踩坑我特意把它单独拉出来说。你在Vivado里打开Block Design双击Zynq PS核的配置界面确认这几项没问题Peripheral I/O Pins里UART1要勾上或者你本来用的UART0也行并且记下它的MIO编号。一般Zynq开发板默认用UART1MIO引脚编号可能是MIO48/MIO49具体看你板卡的原理图。GPIO这一栏里MIO GPIO要勾上EMIO GPIO如果有需要也可以勾但本文不推荐用EMIO做主测试。检查时钟配置PS的CPU主频通常给666.667MHz或650MHzDDR时钟按你板卡颗粒实际参数来。另外提醒一点如果你要在Vivado里生成Bitstream记得Block Design里的地址映射别冲突保证PS侧的UART地址、GPIO地址都没有被PL侧的IP核占用。这一步虽然简单但真有人在这里把UART地址搞错了后面Vitis里printf全打到空气上。确认完这些之后正常走Generate Bitstream。等跑完File-Export Hardware勾上Include bitstream导出得到.xsa文件。注意你导出xsa之后Vivado工程本身还可以继续改但如果硬件配置变了比如新增IP核、修改了PS配置一定要重新生成bitstream并且重新导出xsa否则Vitis那边用的还是旧的硬件描述。2.2 第一次打开Vitis工作区怎么建打开Vitis以后第一个弹窗就是让你选workspace目录。我建议你单独建一个文件夹不要和Vivado工程混在一起比如我常用的目录结构project/ ├── vivado/ # Vivado工程目录 ├── vitis/ # Vitis工作区目录 ├── docs/选择workspace定位到vitis目录点Launch。进入界面后File-New-Platform Project这里新建的就是平台工程。输入工程名比如platform_uart_gpio点Next。关键一步在Hardware Specification这里选择我们刚才导出的xsa文件。如果你这里没看见xsa选项可以点Browse手动定位。下方Operating System选择standalone裸机Processor选择ps7_cortexa9如果是Zynq-7000的PS侧。然后直接Finish。Vitis会自动开始生成platform工程这一步会编译所有驱动库、BSPBoard Support Package、链接脚本等耗时一二十秒正常现象。2.3 platform工程产生完之后application工程怎么建平台工程只是地基真正的用户代码在application工程里。右键platform工程或者Left面板的Platform标签下选择New-Application Project。输入应用工程名比如uart_gpio_test点Next。这里会让你选择关联哪个platform选我们刚建的platform_uart_gpio。继续Next在模板那一堆例程里我强烈建议你选Empty Application而不是那些带DDR测试、Hello World的模板。为啥选空的因为Hello World模板虽然能直接跑通串口但它默认帮你初始化好了很多东西你看不到背后的过程反而容易学个寂寞。空工程让你自己敲每一行代码出问题知道往哪儿找对新手最友好。当然如果你这次纯粹只为了验证环境串口通不通选Hello World也行。2.4 Vitis工程目录结构长什么样谁是谁的先搞清楚工程生成之后左侧Project Explorer会看到这个结构uart_gpio_test/ # 应用工程 ├── src/ # 你的源代码放这里 │ └── helloworld.c # 默认模板生成的示例 ├── src/ ├── vitisconfig/ │ └── ... └── ... platform_uart_gpio/ # 平台工程 ├── bsp/ # Board Support Package板级支持包 │ └── ps7_cortexa9_0/ │ ├── include/ # 驱动头文件 │ └── lib/ # 编译好的库 ├── hw/ # 硬件描述相关 └── ...对初学者来说你只需要关心两个地方应用工程里的src目录放你的main.c和其它源文件。平台工程里的bsp目录里面是所有Xilinx官方驱动库比如xuartps.hUART驱动、xgpiops.hPS端GPIO驱动。你include的头文件就是从这里来的。一般来说你不用去手改bsp里面的内容但如果你需要自定义链接脚本可以去bsp/ps7_cortexa9_0/libsrc/standalone_vX_X/src/里找lscript.ld按需修改。3. 核心细节解析UART和GPIO的寄存器与逻辑3.1 UART为何要从寄存器层面理解在Zynq里UART是可以直接通过挂在APB总线上的寄存器来控制的。你现在用Vitis写的是C代码但最终编译出来执行的就是对一组寄存器的读写操作。很多人会问“不就是调用Xil_Out32、Xil_In32这类函数嘛我直接查驱动API不就行了”没错API调用是最终的目的但你懂一点硬件底层逻辑真的遇到“printf打印出来是乱码”、“串口没反应”这类问题时排查思路就完全不一样了。Zynq的UART控制器是兼容16550的增强版本主要寄存器包括Control Register控制寄存器地址偏移0x00负责UART的使能、收发使能、波特率分频设置。Mode Register模式寄存器偏移0x04选择数据位、停止位、奇偶校验。Interrupt Enable Register中断使能偏移0x08中断开关。Channel Sts Register通道状态寄存器偏移0x2C里面能看到FIFO是否为满、是否为空。Receive/Transmit Holding Register偏移0x30往它写数据就是发一个字节从它读数据就是收一个字节。你在Vitis的bsp里看到的XUartPs_Send、XUartPs_Recv这些函数本质上就是帮你把这些寄存器读写封装好了。比如XUartPs_Send最核心的一步就是把数据循环写入XUARTPS_FIFO_OFFSET这个偏移对应的寄存器。这也是为什么理解寄存器映射这么重要你在芯片手册上查寄存器地址然后用Xil_In32/Xil_Out32直接操作这其实就是写驱动最原生的方式。3.2 UART波特率背后的数学串口通信双方要约定同一个波特率Zynq的UART控制器内部有一个波特率发生器。它的核心公式是实际波特率 输入时钟频率 / (CD × (BDIV 1))其中CDClock Divisor和BDIVBaud Rate Divisor是两个分频寄存器你只需要在Vitis的bsp或者代码里给出目标波特率驱动会自动计算合适的CD和BDIV。但如果你非要自己手动算给你一个参考计算过程假设输入时钟为100MHz这个值取决于你在Vivado里给UART配置的时钟Zynq标准情况下UART时钟是50MHz或100MHz目标波特率9600其实大多数情况下我们根本不需要手动去算这些分频数Xilinx的驱动库已经帮我们算好了。但我们得知道一个关键点波特率能不能精确分频取决于输入时钟频率能否被目标波特率整除。如果误差太大比如高于2%通信就会出错乱码。所以如果你在调试某些非标准波特率比如76800、250000发现串口数据偶尔错位先从配置里确认UART时钟然后计算一下分频误差往往就能解释通。我在实际项目中用的比较多的是115200和921600。115200在50MHz时钟下误差很小921600稍大一点但只要在容差范围内日常调试完全没问题。3.3 GPIO的MIO和EMIO再展开一点Zynq的GPIO控制器官方称呼是“ps7_gpio”它其实是一个挂在APB总线上的通用寄存器组管理着MIO和EMIO两类引脚。MIO一共54个引脚MIO0~MIO53直接作为PS的IO引脚使用可以配置为输入、输出、开漏等模式。EMIO则是把GPIO控制信号接到了PL侧最多64个EMIO0~EMIO63不直接连接到物理引脚而是通过PL逻辑再连接到FPGA引脚上。这里有一个非常关键的差异MIO引脚是直接挂到PS的I/O bank上的它们的电平标准是固定的一般3.3V或1.8V由板卡硬件决定而EMIO引脚的电平呢取决于PL侧的bank电压这就意味着如果你要做不同电平标准的外设用EMIO更灵活。在代码层面MIO和EMIO的操作方式是一样的都是通过GPIO寄存器的DATA、DIRM、OEN等寄存器控制方向和数据。区别只在引脚编号MIO的编号直接是0~53。EMIO在软件里被映射为编号54~117也就是你如果操作EMIO0对应软件里的pin号就是54。这解释了为什么很多例程里看到的GPIO引脚号是54、55之类的——那多半是在操作EMIO。4. 实操过程完整UART与GPIO工程的编写与验证4.1 初始化一下串口背下来这几个函数在Vitis里写裸机程序第一步永远是初始化外设。UART的初始化分为几个层次你不一定每次都手动接触底层寄存器但核心的几个函数得心里有数#include xparameters.h // 硬件参数定义满是外设基地址 #include xuartps.h // UART驱动头文件 #include xil_printf.h // Xilinx轻量级printf static XUartPs UartInst; int uart_init(void) { int Status; XUartPs_Config *Config; // 1. 查找UART1的配置具体哪个UART要对应你的Vivado设置 Config XUartPs_LookupConfig(XPAR_XUARTPS_0_DEVICE_ID); if (Config NULL) { return XST_FAILURE; } // 2. 初始化UART驱动实例 Status XUartPs_CfgInitialize(UartInst, Config, Config-BaseAddress); if (Status ! XST_SUCCESS) { return XST_FAILURE; } // 3. 设置波特率可选默认115200 Status XUartPs_SetBaudRate(UartInst, 115200); if (Status ! XST_SUCCESS) { return XST_FAILURE; } // 4. 设置UART工作模式8位数据位、无校验、1个停止位 XUartPs_SetFormat(UartInst, XUARTPS_FORMAT_8_BITS | XUARTPS_FORMAT_NO_PARITY); XUartPs_SetOperMode(UartInst, XUARTPS_OPER_MODE_NORMAL); return XST_SUCCESS; }几个注意点XPAR_XUARTPS_0_DEVICE_ID这个宏在xparameters.h里定义你不需要记忆具体数值只需要知道它对应的是Vivado里配置的UART实例编号。波特率如果不设置驱动初始化时默认也会设置为115200但显式设置可以避免你后面改代码时忘了。XUartPs_SetFormat调用后uart逻辑会复位如果你在中断里发送数据顺序要格外注意。4.2 GPIO初始化方向、输出、读取一个不少GPIO的操作比UART要简单直接。但正因为简单很多人反而容易忽略方向设置的顺序问题。看代码#include xgpiops.h static XGpioPs GpioInst; #define MIO_PIN_NUM 7 // 以MIO7为例实际用哪个看你板卡 int gpio_init(void) { XGpioPs_Config *Config; int Status; // 1. 查找GPIO配置 Config XGpioPs_LookupConfig(XPAR_XGPIOPS_0_DEVICE_ID); if (Config NULL) { return XST_FAILURE; } // 2. 初始化GPIO控制器 Status XGpioPs_CfgInitialize(GpioInst, Config, Config-BaseAddr); if (Status ! XST_SUCCESS) { return XST_FAILURE; } // 3. 设置MIO7为输出方向 XGpioPs_SetDirectionPin(GpioInst, MIO_PIN_NUM, 1); // 4. 使能MIO7输出这一步很多新手会漏 XGpioPs_SetOutputEnablePin(GpioInst, MIO_PIN_NUM, 1); return XST_SUCCESS; }看到区别没有SetDirectionPin设置方向SetOutputEnablePin使能输出两步缺一不可。这算是Xilinx GPIO驱动的一个特点方向寄存器管方向输出使能寄存器管输出状态两步分开控制给硬件设计留了不少灵活性。如果你要读取输入方向设为0即可不需要调用SetOutputEnablePin。然后通过XGpioPs_ReadPin读回来。// 设置MIO8为输入 XGpioPs_SetDirectionPin(GpioInst, 8, 0); // 读取MIO8的电平状态 u32 PinValue XGpioPs_ReadPin(GpioInst, 8);4.3 发送和接收阻塞式与中断式的差别裸机环境下最常用的发送方式是轮询/阻塞式。核心就是往FIFO丢数据丢一个检查一下FIFO是否满了满了就等。int uart_send_string(const char *str) { while (*str ! \0) { // 等待发送FIFO不满 while (XUartPs_IsTransmitFull(UartInst)) { // 空转等待 } XUartPs_Send(UartInst, (void *)str, 1); str; } return XST_SUCCESS; }接收端类似理论上可以轮询接收FIFO是否有数据然后读出来。但实际项目里串口接收更多是用中断方式不然CPU空转等数据浪费太高。中断方式的使用我放在后面常见问题部分讲因为新手做验证阶段轮询收发已经完全够用先把中断理解成为“串口收到数据后自动跳进一个回调函数”就对了。4.4 把UART和GPIO合到一起LED点灯和串口回环有了上面的初始化函数合到一起写个完整的main.c就很轻松了。这里我设计一个最简单的验证流程上电初始化UART和GPIO。串口打印一段欢迎信息。循环里翻转LEDMIO7控制一个LED或直接测量电平变化每次翻转后串口打印当前状态。同时做串口回环测试收到什么发送什么方便验证收发通路。int main(void) { int Status; // 初始化UART Status uart_init(); if (Status ! XST_SUCCESS) { return XST_FAILURE; } // 初始化GPIO Status gpio_init(); if (Status ! XST_SUCCESS) { return XST_FAILURE; } xil_printf(UART and GPIO test started...\r\n); u32 LedState 0; while (1) { // 翻转LED XGpioPs_WritePin(GpioInst, MIO_PIN_NUM, LedState); LedState ~LedState; xil_printf(LED state: %d\r\n, LedState); // 简单回环收到一个字节发送一个字节 u8 RxBuffer; if (XUartPs_IsReceiveData(UartInst)) { XUartPs_Recv(UartInst, RxBuffer, 1); XUartPs_Send(UartInst, RxBuffer, 1); } // 延时不然LED翻转太快肉眼看不出来 for (volatile int i 0; i 10000000; i); } return 0; }这里有一个非常典型的细节xil_printf不是标准的printf它是Xilinx裁剪过的一个轻量级打印函数不支持浮点数打印默认情况下但支持%d、%s、%x、%c这些基本的。好处是占内存小坏处是如果你要打印浮点数或者用%f你会得到错误结果或者编译报警。如果你在裸机工程里特别喜欢用标准printf需要在编译器里把标准库链接进来并且在BSP设置里把stdout重定向到UART。操作方法是右键platform工程Board Support Package Settings找到stdin和stdout把它们都设置为ps7_uart_1即可。4.5 下载运行bitstream先下还是程序先跑这个顺序问题几乎每次带新人都会遇到。Zynq这种PSPL一体的芯片上电后首先是PS侧启动如果你想让PL逻辑比如自定义的IP核、Verilog模块也能工作就需要先把bitstream下载到PL的配置寄存器里去。在Vitis里运行应用工程之前有两种方式把bitstream配置进PL方式一在Xilinx菜单的Program FPGA里选择你的bitstream文件在platform_uart_gpio/hw/目录下能找到点击Program。方式二直接选择应用工程右键Run As-Launch Hardware。Vitis通常会提示你选择是否Program FPGA。有时候你会发现不下载bitstreamUART和GPIOMIO也能工作这是因为MIO是属于PS侧的它不依赖PL配置。但如果你用了EMIO或者PL逻辑模块不下载bitstreamPL侧的活动就统统不生效。所以建议你养成习惯每次下载运行前先Program FPGA一次。5. 常见问题与排查技巧实录5.1 串口完全没有输出可能不只是代码问题这是新手遇到最多的问题。我直接按优先级列排查清单检查串口工具参数波特率、数据位、停止位、校验位必须和代码里一致。如果设置了硬件流控RTS/CTSZynq的UART1默认引脚不一定是全功能很容易卡死。检查USB转串口模块的接线TXD接RXD、RXD接TXD、GND共地。很多人第一次接反了查半天。检查串口工具“打开串口”了没你以为打印出来了结果工具没开或者被其它进程占用了Windows下串口被多个工具占用是常见问题。在Vitis里看Run控制台确认程序已经Downloaded成功并且开始运行了。如果程序卡死在某个初始化阶段串口同样没输出。用示波器/逻辑分析仪测UART TX引脚的波形。如果你能看到串口数据波形但电脑收不到说明是电平转换或USB转串口模块的问题如果你压根看不到波形问题就在Zynq侧代码或引脚配置。有次我一个朋友遇到串口没输出查了两个小时结果发现是USB转串口模块是坏的换个模块马上就好。从此我学到一个教训不要过度相信硬件排查问题时先用替换法把链路每一环都验证一遍。5.2 打印出来是乱码波特率谁的锅一串乱码第一反应就是波特率不对。这倒不一定是代码里配置的波特率不对还可能是串口助手的波特率和你代码里的不匹配。检查代码里XUartPs_SetBaudRate那行以及串口助手的设置。如果代码里没设置波特率驱动默认值不一定是115200部分版本的bsp默认确实是9600或115200但你必须确认。晶振频率不对。Zynq上UART时钟来源于PS侧的IO PLL或者某个可编程时钟默认都是正常配置的但如果你在Vivado里置了奇怪的时钟频率实际波特率就会偏离理论值。你的USB转串口模块质量太差在921600这类高波特率下收发不稳定换回115200试试就知道。另外xil_printf打印%d的时候偶尔也会有乱码感但那种乱码通常是格式不对比如打印了无符号当有符号和波特率乱码的“整体错位”还是能区分的。5.3 GPIO引脚没反应先查物理连接再查软件号GPIO点灯没反应排查思路要分硬件和软件两个层面。硬件层面确认你操作的那个MIO引脚确实引到了板卡上的LED或者测试点。很多开发板的MIO引脚并不是全部引出可能被DDR、FLASH、SD卡等占用了。确认板卡原理图上LED的极性。如果LED是低电平点亮你写1它反而不亮写0才亮。确认没有和别的外设共用一个引脚导致电平冲突。软件层面确认你用的引脚号对上了。MIO编号为0~53EMIO映射为54~117。误把EMIO当MIO或者反过来是最常见的错法。确认初始化代码顺序先设方向再使能输出顺序反了偶尔会出怪问题。检查XPAR_XGPIOPS_0_DEVICE_ID到底对应哪个GPIO控制器的设备ID如果板卡上GPIO控制器例化不止一个别选错。遇到这类问题最快的办法是用万用表测一下引脚电平看它有没有在高低电平之间翻转。只要电平在跳问题八成出在你和板卡LED之间的物理连接如果电平根本没反应就去查软件配置。5.4 中断方式的UART接收初始化顺序有个坑虽然本篇重点是轮询但迈向真实项目一定绕不开中断收发。给你一个最小可用的中断初始化参考int uart_interrupt_init(void) { // 把UART的中断handler挂到GIC中断控制器上 XScuGic_Config *IntcConfig; XScuGic IntcInst; IntcConfig XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(IntcInst, IntcConfig, IntcConfig-CpuBaseAddress); // 设置UART中断的回调函数 XUartPs_SetHandler(UartInst, (XUartPs_Handler)Uart_Intr_Handler, UartInst); // 连接中断到GIC XScuGic_Connect(IntcInst, XPAR_XUARTPS_0_INTR, (Xil_ExceptionHandler)XUartPs_InterruptHandler, UartInst); XScuGic_Enable(IntcInst, XPAR_XUARTPS_0_INTR); // 开启UART接收FIFO中断 XUartPs_SetInterruptMask(UartInst, XUARTPS_IXR_RXOVR | XUARTPS_IXR_RXEMPTY); return XST_SUCCESS; }踩坑点在于XUartPs_SetHandler设置的回调函数其调用时机是在中断服务程序里但中断服务程序本身要挂到GIC上顺序不能反。如果你先开GIC使能再设回调就可能出现中断已经来了但handler还没有被注册导致前程未卜的跑飞。另外XUARTPS_IXR_RXEMPTY这个中断条件指的是接收FIFO为空时触发在实际使用中通常配合其它条件。真正常用的接收中断触发条件是XUARTPS_IXR_RXOVR接收溢出或者XUARTPS_IXR_RXFULLFIFO满具体用哪个取决于你的FIFO阈值设置。我建议先用RXFULL配合读取FIFO深度逻辑更直观。5.5 Vitis编译报错但警告不显示试试CleanVitis偶尔会抽风代码逻辑没问题编译却报警告或错误而且错误信息指向了一些奇怪的文件。遇到这种情况先别慌着改代码右键工程Clean Project然后重新Build。八成是增量编译的缓存出了问题。如果Clean后还是报错再看是不是BSP生成有问题。这时候可以在platform上右键Rebuild Platform让BSP重新生成一遍。通常这么一轮下来伪报错就消失了。还有一种极端情况xparameters.h里面的设备ID和你verilog/IP里的不完全一致导致驱动的LookupConfig返回NULL。这种情况下程序编译能过运行时却卡死。你要打开bsp/ps7_cortexa9_0/include/xparameters.h手动确认里面的UART设备ID和基地址和你Vivado配置的一致。6. 经验总结与踩坑记录6.1 我的UART初始化黄金顺序我经过大量的调试最终总结出一个最稳的初始化顺序在新项目里我都是直接套用LookupConfig获取配置结构体。CfgInitialize初始化驱动实例。SetOperMode设置为Normal模式确保之前如果跑过别的模式先复位到正常状态。SetBaudRate设置波特率。SetFormat设置帧格式先设置格式再使能发送接收。如果开中断在这里设置Handler并且使能中断。核心逻辑是先让控制器回到一个干净的初始状态然后再按“波特率→帧格式→收发使能”的顺序向下配置。其中SetOperMode最好在SetBaudRate之前因为它里面涉及到一个软件复位。我见过有人反过来写结果波特率怎么设都不对其实就是因为没先复位。6.2 想少踩GPIO的坑硬件先确认三件事引脚有没有被别的功能复用Zynq同一个MIO引脚往往有多种复用功能查看数据手册里的Table of Pin Definitions看清楚哪个功能优先占用。电平标准MIO的电平由硬件确定一般3.3V。如果要驱动5V的逻辑中间必须加电平转换不能直接怼。上拉/下拉很多开发板的LED、按键电路自带电阻如果你的输出模式和外围电路不匹配读到的电平可能和你写的不一致。如果你在做按键输入检测外部没有上拉电阻的话记得在片内使能弱上拉。Zynq的GPIO控制器里可以配置上拉/下拉对应的API是XGpioPs_SetPullType这在按键悬空时很有用。6.3 从工程管理的角度建议你养成两个习惯第一个习惯是把xsa文件当成交付物来管理。每次硬件配置有变化就重新导出xsa并且覆盖旧的同时在git提交记录里注明改动内容。后期如果出现三四个硬件版本你就会发现这个习惯能救命。第二个习惯是application工程的src目录只放自己的源码别把编译生成的中间文件塞进去。Vitis的构建目录默认在工程内部的build/下如果git提交时把build目录也提交了会特别乱。在.gitignore里把build/和.metadata/忽略掉能省去很多麻烦。7. 把这两个外设吃透之后下一步该学什么我一直跟新人说UART和GPIO是Zynq开发的两条腿把这两条腿站稳了后面走路才不歪。你花一晚上跟着这篇文章跑通了UART回环和GPIO点灯恭喜你Zynq的开发大门已经打开了。接下来你可以按这个路线往下走给UART加上中断收发做一个带简易命令行的串口调试工具。把GPIO扩展成按键中断配合定时器做一个周期性任务调度。去了解一下Zynq的DDR内存和DMA这是性能和复杂逻辑的起点。考虑通过EMIO把PS侧的GPIO连到PL侧和你的Verilog逻辑联动。实话说Zynq这套系统比纯MCU复杂比纯FPGA又多了个CPU但它的魅力也正在于此软件和硬件在你手里同一个工具链里打通了。你既可以写C来控制外设又可以用HDL来实现并行逻辑这种“软硬通吃”的体验是单独学STM32或单独学Verilog都很难感受到的。我这边还留了几个扩展方向比如通过UART给PL下发配置参数、用GPIO模拟I2C/SPI时序等等后面如果你们有兴趣我再写具体篇目。先到这有问题评论区交流。最后说个亲测有用的小技巧如果你在验证UART收发的时候手里没有逻辑分析仪可以把TX和RX直接短接然后在程序里发一个字节看看能不能收到——能收到就说明UART的收发通路是完好的排除法排查起来特别快。