简介:面向赛灵思ZYNQ 7020平台的UART0串口通信完整工程,专注处理器系统(PS)端外设的配置与调试,适合FPGA与嵌入式开发入门及进阶者直接使用。压缩包共544个文件、5.56MB,包含Vivado工程设计文件(工程文件、块设计、硬件定义)、C语言与头文件源码、串口驱动、软件开发套件工程及可执行文件等编译产物,同时保留自动化构建脚本、仿真文件和详尽的说明文档,便于完整导入工程并理解软硬件协同流程。目前已有3770人学习下载,被验证为可直接运行的全流程方案。资源基于Vivado 2018.2环境,涵盖串口IP核添加、MIO14与MIO15引脚约束、处理器初始化代码及串口收发测试程序,并附带底层库文件与配置脚本,可帮助使用者从零搭建设计环境,免去繁琐的项目配置,快速实现UART0数据收发。此外,工程内文件组织清晰,按模块划分明确,对学习ZYNQ异构计算、掌握PS与PL协同开发具有很好的参考价值。 如果你刚拿到一块ZYNQ 7020开发板,我猜你第一件事大概率是把UART0串口跑通——只有看到串口终端里蹦出字符,这块板子才算真正“活”了。从最早用Zynq-7000系列做项目到现在,我被问得最多的就是“有没有能直接用的UART0全工程文件”。网上不少教程要么停在旧版本Vivado,要么只丢一段main.c、缺了Block Design配置,新手照着做,卡在MIO引脚那一步就放弃了。
这篇文章就干一件事:把ZYNQ 7020(XC7Z020-CLG484)上UART0串口全工程文件的完整链路拆开讲透。从Vivado里的PS配置、MIO选型,到SDK/Vitis里的裸机驱动代码、收发验证方法,再到我实际踩过的一堆坑,全部给完整方案,照着做就能复现一个“可以直接使用”的串口工程。文章适合刚接触ZYNQ的硬件工程师、嵌入式软件开发者,也适合想拿现成工程做二次改造的老手。
1. 先聊为什么起点放UART0,而不是UART1或EMIO
1.1 ZYNQ 7020的串口资源全景
Zynq-7000系列的PS(Processing System)部分集成了两个完全一致的UART控制器:UART0和UART1。它们的内核逻辑一模一样,都是ARM PrimeCell PL011的授权实现,支持可编程波特率、5到8位数据位、奇偶校验、1或2个停止位,还有64字节的发送FIFO和64字节的接收FIFO。两个控制器都支持中断触发和DMA触发,也都能在轮询、中断、DMA这三种工作模式之间切换。
但这里有个新手最容易忽略的点:UART0和UART1的物理引脚映射完全不同,而且都走MIO(Multi-use I/O)通道,不占PL逻辑资源。UART0最常见的引脚是MIO 14(TXD)和MIO 15(RXD),UART1则默认落在MIO 48/49一带。绝大多数ZYNQ 7020开发板,比如正点原子、米联客、黑金这些主流板卡,板载USB转串口芯片都是直接连到UART0的MIO 14/15。所以选UART0做起点,本质上是顺着绝大多数硬件设计走。
1.2 UART0 / UART1 / EMIO 怎么选
经常有人问我:既然UART0和UART1功能一样,那用UART1行不行?当然行,但没必要给自己加戏。我自己做项目时的选择逻辑很简单:
| 对比维度 | UART0(MIO 14/15) | UART1(MIO 48/49等) | EMIO扩展串口 |
|---|---|---|---|
| 是否占用PL逻辑 | 否 | 否 | 是,要消耗LUT引脚 |
| 开发板默认支持度 | 极高,主流板卡默认 | 较低,需查原理图 | 需自己分配引脚和电平转换 |
| 启动早期打印 | FSBL阶段即可输出 | 同左 | 要等PL配置完成后才可用 |
| 驱动支持 | XUartPs驱动,BSP默认生成 | 同左 | 本质还是同一个驱动,但引脚下沉到PL |
| 使用复杂度 | 最低 | 低 | 较高,涉及引脚约束、PL时钟 |
建议很直接:除非你的MIO 14/15被其他外设占了,或者你有特殊的多串口需求,否则第一块板子跑串口,一律选UART0。就连FSBL和U-Boot的默认调试串口也是UART0,把它作为“系统调试锚点”,后续无论是跑Linux还是调试PL逻辑,都能复用同一套串口基础设施。
1.3 MIO复用冲突
MIO不是一个“哪个空就插哪个”的概念。每个MIO引脚都有多种功能,比如MIO 14/15除了UART0,还可能被SDIO、SPI或CAN复用。实际工程里,如果Vivado的Block Design里同时勾选了SDIO和UART0,而两者争抢同一组MIO,工具会直接报错或者强制你重映射。
所以拿到任何一块新板子,第一件事不是打开Vivado,而是打开原理图,确认UART0电路实际接到了哪个MIO。绝大多数板卡是MIO 14/15,但我也见过个别板子放在MIO 10/11上。这个习惯能帮你省掉后面一整天的排查时间。
2. 全工程文件地图:Vivado与SDK两侧各有什么
2.1 Vivado侧的关键文件
一份能直接用的UART0全工程文件,本质上包含两套东西:一套是Vivado硬件工程,一套是SDK(新一代叫Vitis)软件工程。硬件工程的核心不是那些综合报告,而是Block Design。
一个路径大概是这样的:
vivado/ ├── zynq_uart0.xpr # Vivado工程入口 ├── srcs/ │ ├── bd/ │ │ └── system/ │ │ ├── system.bd # Block Design,核心中的核心 │ │ └── hdl/ │ │ └── system_wrapper.v # 顶层HDL包装 │ └── constrs/ │ └── system.xdc # 约束文件,PS-only设计基本用不到 └── zynq_uart0.runs/ # 综合、实现、比特流结果system.bd这个文件里存放了ZYNQ7 Processing System IP的完整配置:UART0引脚、DDR颗粒参数、时钟频率、启动模式。你双击这个IP看到的所有配置,都存在这里面。如果工程文件能在你的Vivado版本里正常打开,那么所有关键配置都会自动恢复。
这里特别提一下DDR配置。很多人以为跑个串口程序用不到DDR,但其实SDK裸机程序默认就链接到DDR地址空间,PS启动第一步就要完成DDR初始化。如果system.bd里的DDR颗粒型号、位宽、速度等级和你的板卡对不上,程序大概率卡死在启动早期,串口根本来不及打印。所以看到全工程文件时,先检查DDR配置是否匹配板子,比急着看main.c重要得多。
2.2 SDK/Vitis侧的关键文件
硬件工程导出的.hdf文件是连接Vivado和软件开发的桥梁。在SDK里导入硬件平台后,会自动生成BSP(板级支持包),里面最核心的文件包括:
| 文件/目录 | 作用 |
|---|---|
| .hdf | 硬件平台描述,包含PS配置与比特流 |
| uart0_bsp/ | 板级支持包,内含xuartps驱动 |
| uart0_bsp/ps7_cortexa9_0/include/xparameters.h | 外设基地址、设备ID、时钟频率宏定义 |
| uart0_app/src/helloworld.c | 应用程序入口,串口收发逻辑所在 |
| uart0_app/src/lscript.ld | 链接脚本,决定代码跑在DDR还是OCM |
xparameters.h是排查问题的第一站。你会在里面看到类似这样的定义:
#define XPAR_XUARTPS_0_DEVICE_ID 0 #define XPAR_XUARTPS_0_BASEADDR 0xE0000000 #define XPAR_XUARTPS_0_CLOCK_HZ 50000000BASEADDR是UART0控制器在PS地址空间的基地址,CLOCK_HZ是UART参考时钟频率。如果这两个值和Vivado里的实际配置不一致,波特率计算就会出问题。看到全工程后,先打开这个文件核对一遍。
2.3 拿到工程后先做什么
我最反感那种拿到工程包就一顿“Next”下载完不去看的操作方式。全工程文件虽然可以直接用,但前提是硬件环境匹配。我的建议流程分三步:
- 检查版本:低版本Vivado打不开高版本工程文件,高版本打开低版本有报错风险。确认工程是由哪个版本创建的。
- 检查器件:确认工程器件型号是xc7z020clg484-1,而不是zcu104或者别的。
- 检查DDR和UART0引脚:对照板卡原理图,确认DDR颗粒型号和UART0实际MIO引脚是否一致。
这三步只要有一个不匹配,直接下载大概率翻车,而翻车时串口通常“安静如鸡”,没有任何报错可看。
3. 波特率生成与初始化代码:从时钟树到FIFO的完整链路
3.1 时钟到底怎么来的
很多人把波特率配置当成一个黑盒:调用XUartPs_SetBaudRate就完事了。但真出问题时,你还是要懂底层的来龙去脉。
ZYNQ 7020的PS_CLK通常接33.333MHz晶振,这个时钟进到PS内部的PLL(锁相环)后倍频,再分频出各个外设的参考时钟。UART的参考时钟在Vivado的ZYNQ7 PS配置项里可以单独设,常见的默认值是50MHz。这个值会跟随硬件导出到SDK,最后体现在xparameters.h的CLOCK_HZ宏里。
而ARM PL011的波特率生成公式是:
Baud = RefClk / (16 × Divisor)其中Divisor由整数分频IBRD和小数分频FBRD组合而成。以RefClk=50MHz、Baud=115200为例:
Divisor = 50000000 / (16 × 115200) ≈ 27.1267 IBRD = 27 FBRD = round(0.1267 × 64) = 8 实际波特率 = 50000000 / (16 × (27 + 8/64)) ≈ 115207.4 误差 ≈ 0.006%这个误差完全在UART的容忍范围内,所以115200这个常用波特率在50MHz参考时钟下很稳。反过来,如果UART参考时钟是33.333MHz或者别的频率,误差就可能是另一个故事了。排查乱码问题时,第一步就该算一下参考时钟和波特率是否匹配。
3.2 初始化代码逐段拆解
全工程里main.c的初始化部分大概长这样,我加上了注释:
#include "xuartps.h" #include "xparameters.h" #include "xil_printf.h" #define UART_DEVICE_ID XPAR_XUARTPS_0_DEVICE_ID static XUartPs UartInst; int UartInit(void) { XUartPs_Config *Config; int Status; /* 1. 从配置表里找到UART0的配置信息 */ Config = XUartPs_LookupConfig(UART_DEVICE_ID); if (Config == NULL) { return XST_FAILURE; } /* 2. 把硬件配置写入驱动实例,完成初始化 */ Status = XUartPs_CfgInitialize(&UartInst, Config, Config->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } /* 3. 设置波特率,内部会计算IBRD/FBRD并写寄存器 */ Status = XUartPs_SetBaudRate(&UartInst, 115200); if (Status != XST_SUCCESS) { return XST_FAILURE; } /* 4. 设置正常工作模式,不带流控 */ XUartPs_SetOperMode(&UartInst, XUARTPS_OPER_MODE_NORMAL); return XST_SUCCESS; }这三步顺序不能乱:LookupConfig从静态配置表里拿地址和设备ID,CfgInitialize把控制器复位并配置基本参数,SetBaudRate才真正修改波特率寄存器。如果你先调SetBaudRate再CfgInitialize,后者的初始化动作会把你刚配好的波特率寄存器重写一遍。
3.3 收发模式怎么选:轮询、中断还是DMA
全工程默认用轮询模式,这是最不容易出错的方案。发送用XUartPs_Send,接收用XUartPs_IsReceiveData判断FIFO是否非空,再用XUartPs_ReadReg从FIFO读数据。
带一个可靠发送的发送函数,我用的是这种写法:
void UartSendString(const char *str) { int len = strlen(str); int sent = 0; while (sent < len) { sent += XUartPs_Send(&UartInst, (u8 *)str + sent, len - sent); } }XUartPs_Send内部是把数据往TX FIFO里塞,FIFO满时会等待。我这个循环写法是保险起见,如果驱动因为异常只发送了一部分,剩余数据还能继续补发。
接收轮询也很简单:
int UartPollReceive(u8 *buf, int max_len) { int count = 0; while (count < max_len && XUartPs_IsReceiveData(&UartInst)) { u8 ch = XUartPs_ReadReg(UartInst.Config.BaseAddress, XUARTPS_FIFO_OFFSET); buf[count++] = ch; } return count; }轮询的缺点是CPU全程都在“等”,不适合大数据流。如果后续要做高吞吐或者实时响应,再改中断模式。中断模式下UART0的SPI中断号是82,需要配合XScuGic中断控制器使用,在事件回调函数里处理XUARTPS_EVENT_RECV_DATA和XUARTPS_EVENT_RECV_TOUT。全工程文件里我保留了中断模式的基础代码,但默认不启用,目的是让新手先跑通轮询链路,再逐步增加复杂度。
4. 实测验证:回环测试、收发确认与“假通”的识别
4.1 硬件准备与连接
在把工程下载到板子上之前,先做好硬件准备。你需要一根USB转串口线,或者直接用开发板自带的USB转串口接口。大多数板卡的Micro USB口附近会印有UART或者UART0丝印,插上后电脑会枚举出一个COM口。
第一板跑通前,建议做一个最简单的回环测试:用杜邦线或跳线帽把UART0的TXD和RXD短接。这样板子发出的数据会直接从自己的RX脚收回来,通过回环是否成功可以快速判断UART0控制器本身工作是否正常,还能排除外部串口线和电平转换芯片的干扰。
4.2 上位机软件怎么配
串口助手的选择很多,SecureCRT、MobaXterm、Putty,或者国产的友善串口助手都可以。关键是参数不要配错:
- 波特率:115200
- 数据位:8
- 停止位:1
- 校验位:None
- 流控:关闭(这一步极其关键,很多板子翻车都是因为默认勾选了RTS/CTS)
打开串口前,确认COM口号没有被其他软件占用。Windows设备管理器里如果出现黄色感叹号,先装USB转串口芯片的驱动,主流板卡常用CP210x或FT232。
4.3 回环测试代码与判定标准
全工程里加了个隐藏彩蛋:默认跑的不是Hello World,而是一段回环测试。程序上电后先通过UART0发送一行ASCII字符,然后进入回环等待状态,把收到的每个字节原样回发。
核心代码就是上面那个轮询循环,主函数里长这样:
#include "sleep.h" int main(void) { UartInit(); UartSendString("ZYNQ7020_UART0_LOOPBACK\r\n"); while (1) { u8 rx_buf[64]; int recv_count = UartPollReceive(rx_buf, 64); if (recv_count > 0) { UartSendString("\r\n[ECHO] "); XUartPs_Send(&UartInst, rx_buf, recv_count); UartSendString("\r\n"); } } return 0; }测试流程是这样的:
- 先什么都不动,上电,看串口终端有没有打印
ZYNQ7020_UART0_LOOPBACK。 - 有打印,说明UART0初始化、发送链路、上位机接收这个闭环是通的。
- 然后手动发一条字符串,比如
hello,看能不能收到[ECHO] hello回显。 - 如果发送和回显都正常,说明接收链路也通。
如果上电后完全没有打印,不要急着改代码。先用万用表或示波器量一下板卡上UART0 TXD引脚的静态电平,如果一直是高电平(3.3V),说明UART空闲状态正常;如果有波形,恭喜你,硬件其实在工作,问题大概率在MIO配置或波特率。
4.4 “假通”怎么分辨
我在帮人排错时经常遇到一种情况:串口能打开、能显示,甚至能看到板子发的数据,但内容全是乱码。这种“假通”最迷惑人。
乱码的排查优先级是:
- 波特率不对(最常见,尤其上位机和板子一个设115200、一个设9600)。
- UART参考时钟和BSP配置不一致。
- 电平不匹配,比如板子TXD是1.8V电平,转串口芯片不支持。
- 接地问题,USB转串口线和板子没有共地。
用示波器看TXD引脚输出波形,是区分这些原因最快的手段。空闲高电平、起始位低电平、8个数据位、停止位高电平,对着ASCII表一比对,就知道波特率和数据格式对不对了。
5. 排坑记录:五个让串口“哑火”的细节,我都踩过
5.1 MIO配成了EMIO,串口静悄悄的
第一次踩这个坑是在帮朋友调一块定制板,SDK程序编译下载一次过,但串口就是没输出。查了半天,发现Vivado的Block Design里,UART0的引脚选项被改成了EMIO,而不是MIO 14/15。EMIO意思是引脚从PS引到PL再通过PL引脚出去,但PL侧的引脚约束和电平转换电路根本没做,数据自然出不来。
排查方法很简单:双击Vivado里的ZYNQ7 PS IP,打开Peripheral I/O Pins页面,看UART0那一栏选的是MIO几还是EMIO。选成MIO 14/15,重新生成比特流、
本文还有配套的精品资源,点击获取