
简介ZYNQ 7020 UART0串口通信全工程文件基于Vivado 2018.2环境搭建面向嵌入式开发者和ZYNQ SoC初学者提供可直接使用的完整工程。工程涵盖PS与PL协同设计从MIO14/15引脚映射、xil_uartlite IP核配置到驱动初始化、收发函数实现均有完整代码并附带可运行的测试程序帮助使用者跳过重复的环境搭建与调试环节直接验证串口通信功能。资源共计544个文件压缩包约5.56MB以C/H源码、Vivado工程文件xpr、bd、xci、硬件定义与配置脚本hdf、xdc、tcl以及.o/.elf等编译输出为主结构完整方便用户按需查看或移植。已有3770人浏览学习适合需要在ZYNQ 7020上快速跑通UART0、或参考其FPGAARM联合开发风格的开发者。 拿到一块ZYNQ 7020的板子很多人真正上手遇到的第一个坎就是串口。标题里这个“ZYNQ 7020 UART0串口程序全工程文件可以直接使用”听起来像是老手丢出来的现成模板但如果你自己从头搭一遍会发现MIO引脚分配、BSP里的stdin/stdout设置、printf重定向、烧写方式每一步都有能卡住人的细节。这篇文章就把这套流程完整拆开包括为什么选UART0而不是UART1、Block Design里到底要放什么、SDK侧怎么配置才算“拿到就能用”以及我实际调试中踩过的坑。新手照着做三十分钟内能让串口跑起来老手也可以拿这个当工程模板改一改。1. UART0这条串口到底该用PS还是PL1.1 标题里的UART0指的就是PS端硬核ZYNQ 7020本质上是双核ARM Cortex-A9处理器加一片Artix-7级FPGA组合成的异构SoCPS端自带两个UART控制器分别叫UART0和UART1。这两个控制器嵌在ARM处理系统内部通过MIO引脚引到芯片外部不占用PL资源。标题里明确写UART0等于把方案限定死了用的是PS端UART控制器而不是在PL里例化AXI UART Lite或者自己写Verilog串口模块再绑引脚。这个区别非常关键。我见过不少刚接触ZYNQ的工程师一听说要调串口第一反应是去写FPGA逻辑结果绕了一大圈其实PS端的UART控制器就是现成的硬件外设驱动也由Xilinx官方SDK自动生成。用PS端UART的好处就是调试方便、可靠性高收发FIFO深度64字节支持中断和DMA完全能满足日常调试和一般通信需求。只有一种情况我建议用PL串口串口数量不够PS端两个UART都用完了才考虑在PL里加IP扩展。1.2 为什么是UART0而不是UART1很多开发板出厂时会把UART1默认接到板载USB转串口芯片上比如MIO14和MIO15所以大多数例程都喜欢用UART1。UART0对应的MIO引脚常见是MIO8和MIO9存在感没那么强但实际用起来有它独特的优势。我习惯优先把UART0作为调试串口。首先是地址和中断的“默认待遇”在SDK生成的BSP里UART0的驱动实例和中断ID都会提前准备好配置路径很直接。其次是资源规划上的考虑一块板子上往往还需要外接蓝牙、GPS、RS485模块UART1被复用的情况非常多如果把UART0固定给调试输出后面扩展其他串口设备时就不会面临引脚冲突。关键是确认你自己板子的原理图MIO8/MIO9是否确实接到了USB转串口芯片。有的板子会反着来把UART0引到调试口也有的板子UART0并不引出这时候硬按标题里的UART0来做就会白忙一场。1.3 一个“全工程文件”应该长什么样判断一份ZYNQ工程是不是“全工程”不要只看有没有.xpr文件。我在工作里比较看重这几样东西Vivado工程文件.xpr工程、Block Design源文件以及生成bitstream用的全部IP配置。硬件导出文件Vivado 2019.1及以上版本导出的是.xsa文件早期版本是.hdf文件SDK/Vitis靠它来识别硬件信息。平台工程和应用工程SDK里的platform app或者Vitis里的platform工程和application工程。约束文件纯PS串口不需要引脚约束但如果有PL逻辑必须带上.xdc。README或使用说明写明使用的Vivado/SDK版本、板卡型号、烧写步骤。换台电脑能不能直接打开、打开后能不能直接生成eclipse工程并下载这才是“可以直接使用”的真正标准。我收到别人的工程第一件事就是看版本号版本不一致时很多IP需要重新生成这是后面第4章要细说的坑。2. 建工程的完整一套流程从Vivado到Block Design2.1 Vivado侧新建工程和PS配置打开Vivado创建RTL工程在器件选择界面务必选对具体型号。ZYNQ 7020有两个常用封装xc7z020clg400-1和xc7z020clg484-1选错会导致综合报错或者后期没法对板。这一步看起来基础却是全工程能否“直接用”的前提。创建完成后在IP INTEGRATOR里新建Block Design添加ZYNQ7 Processing System IP。双击IP打开配置界面重点看两个页面第一个是PS-PL Configuration下的Peripheral I/O Pins。在这里勾选UART0同时取消UART1。注意界面左侧的树形菜单里I/O Peripherals展开后能找到UART0和UART1的勾选项勾选UART0后下面的MIO引脚会自动分配为MIO8和MIO9。如果你的板子原理图把UART0接到了MIO10/MIO11可以直接在MIO配置里改成对应的引脚编号并不复杂。第二个是Clock Configuration。PS端输入时钟频率必须以板载晶振为准常见是33.33MHz或50MHz。如果这个频率填错串口波特率会整体偏移出现“能发送但全是乱码”的典型症状。CPU主频我一般设置到650MHz或666MHz串口工作时钟保持默认即可不影响这部分功能。2.2 最小可用Block Design该放哪些IP如果只做PS端UART0Block Design可以极度精简一个ZYNQ7 Processing System核双击进入配置勾选完UART0后不做任何PL扩展直接Validate Design生成顶层hdl wrapper。但作为“全工程”我个人建议至少加一个AXI GPIO引出两路LED。原因很简单串口调试最大的困惑是“程序到底跑没跑”如果出现完全无输出的情况你很难判断是PS根本没启动还是串口配置有问题。加两路GPIO控制LED闪烁能快速确认ARM侧程序是否正常运行排查效率高很多。添加AXI GPIO后需要运行Connection Automation它会自动把GPIO的AXI接口连到PS端的GP Master端口并分配地址。这样Block Design里就同时有了PS外设和PL外设更贴近真实项目后面写中断、加DMA时也不用重建工程。2.3 约束文件和你想的可能不一样我反复跟同事强调一件事PS端UART0这种MIO引出的外设不需要在.xdc里写引脚约束。MIO引脚是PS侧硬件固定分配的用户能改的是MIO编号选择而不是像PL引脚那样在约束文件里指定package pin。很多从纯FPGA转过来的工程师会有惯性思维以为串口一定要写set_property PACKAGE_PIN这是一个认知误区。所以纯PS UART0工程里.xdc文件完全可以不添加任何约束或者只有一些时钟相关配置。如果Block Design里加了AXI GPIO这类PL外设才需要在约束文件里给GPIO的emio引脚绑定物理管脚比如set_property PACKAGE_PIN M14 [get_ports led_tri_o[0]]之类的写法。生成bitstream的步骤和普通FPGA工程一样Generate Output Products、Generate Bitstream顺利的话几十秒就能完成。3. SDK侧UART0裸机驱动与printf重定向3.1 BSP生成时最关键的一步stdin/stdoutVivado侧完成之后File → Export Hardware勾选Include bitstream生成.xsa文件。注意2019.1以上的Vivado导出后缀是.xsa旧版是.hdf这个文件包含了PS配置信息、时钟配置、DDR地址映射等SDK/Vitis全靠它识别硬件结构。启动Xilinx SDK或Vitis后第一步是新建Platform工程选择刚才导出的.xsa文件。BSP设置里有一个特别关键的选项在Board Support Package Settings里找到standalone库的配置项把stdin和stdout都改成uart0。这个步骤我排在新手易错榜第一位。很多人的ZYNQ串口程序明明编译下载都正常但就是不出打印信息查了半天最后发现BSP默认把stdin/stdout配到了uart1。程序里printf的内容全部都去了UART1而调试线接的却是UART0自然什么都看不到。改完BSP后记得保存并重新生成BSP再用Rebuild All编译平台工程。3.2 主程序里到底怎么写才算“直接能用”新建Application工程时模板选Empty Application不要选Hello World模板因为那个模板会带入一些干扰性的示例代码。在主程序里其实只需要做两件事初始化UART0然后循环收发。初始化UART0的代码极其简洁用SDK自动生成的驱动即可#include xuartps.h #include xparameters.h #include xil_printf.h #define UART0_DEVICE_ID XPAR_XUARTPS_0_DEVICE_ID static XUartPs Uart0Inst; int Uart0_Init(void) { XUartPs_Config *Config; Config XUartPs_LookupConfig(UART0_DEVICE_ID); if (Config NULL) { return XST_FAILURE; } XUartPs_CfgInitialize(Uart0Inst, Config, Config-BaseAddress); XUartPs_SetBaudRate(Uart0Inst, 115200); return XST_SUCCESS; } int main(void) { u8 RxBuf[64]; u8 TxBuf[64]; int RecvCount; Uart0_Init(); xil_printf(UART0 ready, baudrate115200\r\n); while (1) { RecvCount XUartPs_Recv(Uart0Inst, RxBuf, sizeof(RxBuf)); if (RecvCount 0) { XUartPs_Send(Uart0Inst, TxBuf, RecvCount); } } return 0; }注意我特意用了xil_printf而不是标准printf。xil_printf是Xilinx提供的精简版printf占用内存更小在裸机环境下不会因为堆栈配置问题而崩溃。如果你确实要用标准printf需要在BSP里启用MicroLib或者增加堆大小否则第一次调用printf就可能跳进硬件异常。接收回环的逻辑看起来简单但有一个容易被忽略的点XUartPs_Recv在实际没有数据时如果配置成阻塞模式会在while循环里卡住造成主流程动不了。所以在循环里使用XUartPs_Recv时可以配置为非阻塞或者直接依赖中断方式做接收这个我会在下一小节展开。3.3 波特率、校验位和中断怎么取舍波特率这块ZYNQ PS端UART的时钟来自PS的IOU时钟域XUartPs_SetBaudRate内部会根据时钟频率自动计算分频系数。调试场景下115200-8-N-1是默认选择不建议为了省事用9600波特率越低对单端信号线反而更容易引入干扰。如果是板间长距离通信优先考虑RS485或者降低电平光降波特率并不能根治问题。校验位方面全工程默认无校验就够了串口调试工具里也要保持一致。如果两边配置不统一通信会出现偶发乱码和丢字节。中断方面纯printf完全不需要开中断阻塞发送就行。但一旦你需要在程序里响应上位机指令做不定长接收就必须用FIFO中断。UART0的收发FIFO都是64字节这个容量比STM32的串口FIFO大不少。用中断方式时设置好接收FIFO触发阈值比如收到8个字节触发一次中断然后在中断回调函数里把数据搬进自己的环形缓冲区。这样即使主循环里在做其他事串口数据也不会丢。做一个接收中断的初始化流程大概是XUartPs_SetInterruptMask(Uart0Inst, XUARTPS_IXR_RXOVR | XUARTPS_IXR_RXFULL); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XUartPs_InterruptHandler, Uart0Inst); Xil_ExceptionInit(); Xil_ExceptionEnable();不过这里要注意XUartPs_InterruptHandler只是一个分发函数具体回调函数要通过XUartPs_SetHandler注册在回调里根据中断状态寄存器判断是接收事件还是发送事件。4. 常见问题排查与避坑记录4.1 串口一个字符都不出该从哪里查这类问题占了我帮同事调串口故障的一半以上。正常排查顺序是先看板子有没有上电DDR、CPU电源轨是否正常然后看设备管理器里USB转串口芯片是否识别COM口号是多少接着确认串口助手选对COM口、波特率是115200且无校验再检查UART0的MIO引脚和板载转接芯片是否一致这一点只能对着原理图确认。排除硬件问题后回到软件。可以先在main函数第一行加一个GPIO翻转逻辑如果LED变亮说明程序确实跑起来了问题只出在串口配置上。这时候重点查BSP的stdin/stdout是不是uart0以及XUartPs_CfgInitialize有没有成功返回。我见过有人把XPAR_XUARTPS_0_DEVICE_ID写错查找配置时拿到NULL指针直接异常后面所有代码自然都不执行。4.2 乱码、首字符丢失是波特率还是时序问题乱码的排查优先级最高的是波特率两边是否一致。但有个隐蔽问题如果Vivado里PS输入时钟频率和板载晶振不一致SDK里设置115200实际产生的波特率可能是110000不到表现就是偶尔能收到几个正确字符大部分时间是乱码。这种情况去查Clock Configuration里的输入频率确保和晶振丝印一致即可。首字符丢失则是另一个常见现象。上电瞬间PS端复位还未完全释放串口控制器没有完成初始化此时上位机发出的第一个字节就会丢失。解决方法是把串口助手的发送时机往后延或者在主程序初始化后加一个500ms的延时再进入接收循环。另外还要注意如果使用外部USB转串口芯片它的DTR/RTS信号在上电时会拉低对端电平也可能导致首字符异常绕开办法是连接后先手动触发一次DTR。4.3 换电脑就打不开工程版本兼容性怎么处理这是“全工程文件”最尴尬的地方。Vivado工程升级是单行道2018.3打开2017.4的工程可以反过来不行。新版本打开旧工程时会提示Upgrade IP一定要点备份选项不然IP配置可能会在升级过程中被改动。升级完重新生成bitstream后还要重新导出.xsa并在SDK里重新生成BSP这一串操作做完工程才真正在新版本环境下可用。这里有个经验我习惯在Block Design里右键生成一个tcl脚本定时备份。tcl脚本本质上是Block Design的文字化记录以后不管Vivado升级到什么版本都能用Source tcl脚本的方式重建整个Block Design比直接发.xpr文件可靠得多。全工程里如果带了这条tcl脚本就算用户手里的版本和你不同也能大概率重建出可用工程。4.4 下载和烧写全工程能不能落到Flash里SDK里Program FPGA只是把bitstream配置到FPGA里断电就丢。要让工程真正“直接用”一般要生成BOOT.bin把FSBL、bitstream和应用程序打包烧到QSPI Flash里。在SDK里创建Boot Image启动分区按顺序添加FSBL、bitstream、app.elf生成BOOT.bin。用Vivado的Hardware Manager连接JTAG在QSPI Flash模式下烧录BOOT.bin之后板子就可以上电自启动了。不过这一套步骤里常见问题是启动模式跳线没拨对明明是QSPI启动拨码却拨到了JTAG或者SD卡模式。烧写成功但不启动的时候优先检查跳线。为了让拿到全工程的人少踩坑我把几个关键检查点整理成了表格配置项检查内容常见错误器件型号xc7z020clg400-1或实际型号选错封装导致综合后无法对板PS输入时钟与板载晶振一致填错导致波特率偏移、乱码MIO引脚UART0对应的MIO8/MIO9是否接对引脚和板载USB转串口芯片不一致BSP stdout必须指向uart0默认uart1printf无输出波特率115200/8/N/1两边一致9600与115200混淆乱码启动模式QSPI/SD/JTAG跳线与烧写方式匹配烧写BOOT.bin后不启动工程版本Vivado/SDK版本一致性高版本工程低版本打不开写在最后在我自己的项目里ZYNQ 7020的UART0调试模板已经固化成了一套标准文件Vivado工程带tcl重建脚本SDK工程分platform和app两层主程序里把UART0初始化和回环收发做成了独立函数每次开新项目直接把这份模板复制过去只改MIO引脚和板级外设相关配置。串口这个东西看起来是ZYNQ开发里最简单的一环但它恰恰是整个调试链路的生命线。没有一条稳定可用的串口DDR初始化、SD卡读写、网络通信、DMA搬运出了问题都没法快速定位。把UART0这套基础链路一次做扎实后面复杂功能调试起来会顺畅非常多。本文还有配套的精品资源点击获取