简介:ZYNQ7010的UART串口程序是一套基于Vivado 2017.1开发环境、面向Zynq-7010 SoC的完整串口通信工程,适合嵌入式开发者和FPGA学习者在PS与PL协同开发中理解UART中断收发机制。压缩包共1146个文件、约21.08MB,以h/c源代码、v/vhd硬件描述文件为核心,辅以xdc约束、bit比特流、tcl脚本、makefile构建文件以及xci IP核配置等,覆盖从硬件工程到软件驱动的完整开发链。工程中CH09_Interrupt_Uart部分重点展示了中断处理在UART收发中的实现方式,涉及波特率配置、收发FIFO、中断服务例程编写等关键环节,可帮助读者掌握Vivado下的IP集成、HDL综合、SDK调试等完整流程,在实际嵌入式项目中具有直接参考价值。资源已有3031人学习下载,适合需要参考完整工程结构、对照源码理解Zynq UART底层原理的开发者。 做ZYNQ7010开发,我拿到板子的第一件事就是把UART串口程序跑通。不是说它有多难,而是ZYNQ7010这个芯片有点特殊——它内部有ARM核也有FPGA逻辑,调试手段比普通单片机复杂,UART反而成了最快、最直接的反馈通道。很多新手一上来就在ZYNQ7010的UART上卡住,不一定是代码写错,而是没想清楚到底走PS端还是PL端,电平对不对,上位机驱动装没装好。这篇文章我就把调ZYNQ7010 UART串口的完整思路、代码和踩坑记录整理一遍,适合刚拿到开发板不知道从哪起步的同学,也适合回头排查问题的朋友。
1. 为什么ZYNQ7010的UART值得单独拿出来讲
1.1 UART在ZYNQ开发中的特殊地位
ZYNQ7010属于Xilinx Zynq-7000系列,PS端是双核Cortex-A9,PL端是Artix-7架构的FPGA。不少人买了板子就急着跑Linux、搞高速接口,结果第一步就卡在串口上。因为无论你跑裸机程序还是Linux,系统启动日志、调试打印、命令行交互,全都依赖UART。它虽然老,却是整个系统里最可靠的“眼睛”。
说得直白一点,UART在ZYNQ7010开发里的价值有三个:第一,代码量最少,适合作为PL/PS协同工作的入门例程;第二,调试反馈直接,逻辑有问题看串口输出就能定位一半;第三,所有开发板都会把UART引出来,不需要额外接硬件。所以我把UART当成ZYNQ开发的第一课,比点灯有意义得多。
1.2 PL还是PS,先想清楚再动手
这是所有新手第一个纠结的问题。ZYNQ7010的UART其实有两条完全不同的实现路线:
| 路线 | 实现位置 | 特点 | 适用场景 |
|---|---|---|---|
| PS端UART | 芯片内部UART控制器,通过MIO或EMIO引出 | 不需要自己写协议逻辑,驱动成熟,Linux也能用 | 跑系统、裸机调试主CPU |
| PL端UART | FPGA内部用Verilog手写,或使用AXI UART Lite IP | 可自定义波特率、帧格式,与PS无关 | 学习串口协议、纯FPGA逻辑设计 |
很多教程默认用PS端,因为对应用开发更友好。但如果你主要写FPGA逻辑,有时候PL端更需要一个独立的串口来打印状态。我的建议是:如果你只是想验证开发板能不能用,直接走PS端;如果你正在学FPGA或者PL端有独立通信需求,再考虑用Verilog实现。两条路不是对立的,我后面分别讲。
2. UART协议的关键点:帧格式、波特率与接口对比
2.1 串口时序到底长什么样
UART是异步串行通信,收发双方没有单独时钟线,靠的是事先约定好的波特率和对起始位的检测。标准帧结构可以描述成一段电平序列:
- 空闲状态:TX引脚保持高电平;
- 起始位:发送方把一个bit时间长度的低电平放到线上;
- 数据位:通常8个bit,从最低位LSB开始发送;
- 校验位:可选,常见的是偶校验或奇校验;
- 停止位:至少一个bit时间的高电平,表示一帧结束。
有个容易忽略的细节:停止位之后线路必须回复到高电平,才能开始下一帧。很多自制模块在停止位处理上偷懒,导致接收端识别不了下一帧的起始位。用逻辑分析仪抓波形时,你会看到起始位低电平、数据位乱序、停止位高电平这个特征非常明显。写Verilog时一定要把状态机按这个顺序做全。
2.2 波特率与时钟分频计算
波特率就是每秒传输的bit数,常用值是9600、115200。在PL端手写UART时,波特率由系统时钟分频得到。举个例子:假设系统时钟50MHz,目标波特率115200,每个bit周期需要的时钟个数是:
50_000_000 / 115200 = 434.027...
我一般取整数434,实际波特率误差约为0.007%,完全能接受。如果系统时钟是33.333MHz这种非整数值,误差可能会变大,需要适当调整分频值或者选一个能被整除的波特率。PS端的UART控制器会自动处理分频,一般只需要设置波特率值,不用管底层细节。
2.3 UART和USART、SPI、I2C、CAN到底有什么区别
这个问题在搜索UART相关信息时经常出现。我把几个常见协议放到一张表里对比:
| 协议 | 线数 | 同步/异步 | 应用场景 | 特点 |
|---|---|---|---|---|
| UART | TX/RX,共地 | 异步 | 调试、模块间低速通信 | 简单,全双工,无时钟线 |
| USART | 可配 | 同步/异步 | 单片机外设 | 比UART多了同步时钟输出能力 |
| SPI | SCLK/MOSI/MISO/CS | 同步 | Flash、高速传感器 | 速度快,一主多从,线多 |
| I2C | SDA/SCL | 同步 | EEPROM、低速配置 | 总线式,可挂多设备,速率相对低 |
| CAN | CANH/CANL差分 | 异步 | 汽车、工业现场 | 抗干扰强,带仲裁和错误处理 |
ZYNQ7010开发中,UART主要负责和人交互,SPI/I2C主要用于连接传感器和存储器,工业场景才会额外接RS485/CAN。不要觉得协议多很难,它们本质都是把数据变成线上高低电平,只是“约定”不同而已。
3. PL端实现UART:一个能用的Verilog例子
3.1 发送模块代码与关键逻辑
如果采用PL端实现,最核心的就是波特率发生器和发送状态机。我给出一个最简可用的发送模块,系统时钟50MHz,波特率115200:
module uart_tx( input clk, // 50MHz input rst_n, input tx_start, input [7:0] tx_data, output reg txd ); localparam BAUD_CNT = 434; // 50MHz / 115200 reg [8:0] cnt; reg [3:0] bit_cnt; reg sending; reg [7:0] shift_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin txd <= 1'b1; cnt <= 0; bit_cnt <= 0; sending <= 0; end else begin if (!sending) begin txd <= 1'b1; if (tx_start) begin sending <= 1'b1; shift_reg <= tx_data; cnt <= 0; bit_cnt <= 0; txd <= 1'b0; // 拉低,产生起始位 end end else begin if (cnt < BAUD_CNT - 1) begin cnt <= cnt + 1; end else begin cnt <= 0; if (bit_cnt == 0) begin // 起始位结束,开始发数据位 txd <= shift_reg[0]; shift_reg <= {1'b0, shift_reg[7:1]}; bit_cnt <= bit_cnt + 1; end else if (bit_cnt <= 8) begin txd <= shift_reg[0]; shift_reg <= {1'b0, shift_reg[7:1]}; bit_cnt <= bit_cnt + 1; end else begin // 停止位 txd <= 1'b1; bit_cnt <= 0; sending <= 0; end end end end end endmodule这段代码里我用了移位寄存器,每到一个bit周期就把最低位送到txd,然后右移一位。注意bit_cnt从0计数到9,其中0对应起始位,1到8对应8个数据位,9是停止位。如果你需要偶校验,可以在数据位结束后插入一个校验bit,但状态计数要相应加一,新手容易在这里踩坑。
3.2 接收端为什么难写,以及电平转换问题
接收模块比发送复杂,核心是检测起始位下降沿,然后在数据位中间时刻采样。很多人写接收失败,就是因为采样点太靠近跳变沿,稍微有点抖动就采错。我的建议是:如果只是应付功能验证,别自己写接收,直接用Vivado里的AXI UART Lite IP;如果想学协议,可以去GitHub找开源UART接收模块,然后逐步读懂。
另外,PL端UART的引脚通常是3.3V TTL电平。如果你的开发板PS端MIO配置成1.8V,或者外接的传感器模块是1.8V逻辑,就必须加电平转换电路。常见的方案有TXS0108双向电平转换芯片,或者用MOS管搭单向转换。真要用RS485做工业通信,还需要在UART的TX/RX后面接MAX3485这类收发器,并把方向控制脚接对。RS485是半双工,方向控制弄反就变成只能发不能收,这是硬件上最容易忽略的坑。
4. PS端UART:Vivado/SDK配置流程
4.1 在Vivado里把UART外设配出来
如果你不想在PL端折腾Verilog,PS端的UART会省心很多。我常用的配置步骤是:
- 新建Vivado工程,添加ZYNQ7 Processing System IP;
- 双击IP,在PS-PL Configuration的MIO Configuration里勾选UART1(或者UART0,取决于板卡原理图);
- 确认UART引脚对应的MIO编号,很多开发板默认是MIO14/MIO15,你的板子不一定一样,一定要查原理图;
- 保存Block Design,生成HDL wrapper,跑综合和实现,最后Generate Bitstream;
- 导出硬件时勾选Include bitstream,打开SDK;
- 在SDK中新建Application Project,选择模板Hello World,SDK会自动生成通过UART打印的裸机代码。
这里有个很关键的点:ZYNQ的PS端UART波特率在SDK里默认是9600还是115200,取决于BSP和模板设置。很多人下载后串口没输出,其实是SDK Serial Terminal的波特率和工程不匹配。
4.2 SDK里写UART收发程序
Hello World模板默认使用XUartPs驱动,核心初始化代码大致如下:
#include "xparameters.h" #include "xuartps.h" #include "xil_printf.h" static XUartPs UartInst; int uart_init(void) { XUartPs_Config *Config; Config = XUartPs_LookupConfig(XPAR_XUARTPS_0_DEVICE_ID); if (Config == NULL) return -1; XUartPs_CfgInitialize(&UartInst, Config, Config->BaseAddress); XUartPs_SetBaudRate(&UartInst, 115200); XUartPs_SetDataFormat(&UartInst, XUARTPS_FORMAT_8N1); return 0; }发送可以轮询:
void uart_send_str(char *s) { while (*s) { XUartPs_Send(&UartInst, (u8 *)s, 1); while (XUartPs_IsTransmitFull(&UartInst)); s++; } }接收如果要做到中断方式,需要初始化FIFO中断阈值并注册回调函数,代码量会多一些。我先给一个用于验证接收通路的循环轮询写法,在功能验证阶段完全够用:
u8 buf; while (1) { int n = XUartPs_Recv(&UartInst, &buf, 1); if (n == 1) { XUartPs_Send(&UartInst, &buf, 1); } }这段代码会让CPU一直占用,但它能直接证明接收通路是通的。
4.3 FT232R/FT231X USB转串口驱动
热搜里反复出现FT232R和FT231X驱动,不是没原因的。市面上很多ZYNQ开发板用FTDI芯片做USB转串口,Win10系统一般免驱,但精简版系统或者特殊版本芯片经常认不出。设备管理器里如果出现“USB Serial Port (COMx)”,说明驱动正常;如果显示“USB Serial Converter”或者黄色感叹号,就需要去FTDI官网下载VCP驱动手动安装。
还有个实用经验:装完驱动后一定要拔插一次USB线,然后去设备管理器确认端口号变了没有。很多调试失败都错在串口助手选错了COM口,明明硬件是好的,却怀疑代码有bug。
5. 调试问题与排查技巧
5.1 串口完全无输出
遇到这个问题,我一般按下面顺序排查:
- 板卡供电是否正常,电源LED是否亮;
- 下载程序后SDK控制台有没有报错;
- TX和RX是否接反。使用外部USB转串口模块时,板子TX要接模块RX,板子RX要接模块TX;
- Vivado里配置的UART引脚和板卡原理图是否一致;
- 波特率是否匹配;
- 是否忘了复位,或者复位脚被拉到了错误电平。
不要小看这些点,我至少有两次是电源线接触不良导致完全没有输出,排查了半天硬件才发现毛病。
5.2 输出乱码
乱码是UART调试最头疼的问题。主要原因通常是:波特率分频误差太大、电平不匹配、收发双方没有共地、USB转串口芯片本身不稳定。
其中“没共地”最隐蔽。如果板子和外部模块各自用独立电源,地线没接在一起,RX/TX上的电压参考点不一致,收到的数据必然是乱码甚至全是0xFF。我这里特别强调:凡是外接模块,第一件事就是确认地线连接。
5.3 回环测试三步走
不管PL还是PS,先做回环能省一半时间:
- 把板子的TX和RX用杜邦线短接,串口助手上发什么就回什么,说明物理通路没问题;
- PS端可以用上面的轮询接收代码做硬件回环验证;
- PL端手写的UART如果回环不正常,用逻辑分析仪抓TX波形,看起始位和停止位是否正确。
有条件的建议直接上逻辑分析仪,采样率设1M以上,能看到每一位的电平变化,比示波器好用很多。
5.4 用逻辑分析仪抓波形
调PL端UART时,我几乎每次都用逻辑分析仪。20多块钱的8通道逻辑分析仪就能搞定,软件里选UART协议解析,自动解码出十六进制数据。你可以把抓到的波形和发送字节对照,很快定位是发送状态机的问题还是波特率误差的问题。
6. 一个实战复盘:PS UART跑通全流程
6.1 核心板上的MIO配置经历
我之前调试一块ZYNQ7010核心板,原理图标注PS UART1用MIO14、MIO15,板载FT231X。我在Vivado里配好UART1,SDK里编译Hello World,下载后Serial Terminal什么反应都没有。我查了电源、下载、波特率都没问题,最后才发现SDK Serial Terminal里选的是COM8,设备管理器里的实际端口是COM7。改过来立刻出现启动打印,这种低级错误以后一定要先检查端口。
6.2 接收回环验证实例
以为发送通了就行?不行,还得验证接收。我在SDK里写了轮询回环程序,用串口助手发送55 AA 01 02 03,然后观察返回。注意XUartPs的接收FIFO默认可能没开中断,裸机轮询模式下要保证每次读一个字节后及时处理,否则数据多时会丢。如果回环返回的数据和发送完全一致,就可以放心进入下一步开发了。
7. 高频坑清单和个人心得
7.1 我把高频坑整理成一张清单
- VIVADO里UART0和UART1选错,引脚对不上;
- 原理图看错MIO编号,TX/RX位置反了;
- 外接模块忘记共地,导致数据全乱;
- 串口助手端口选错,或者被其他软件占用;
- 波特率设置不一致,比如程序里115200,串口助手却用9600;
- 使用PL自制UART时,采样点靠近跳变沿导致误码;
- SDK Serial Terminal和串口助手同时打开同一个端口,互相抢资源。
这些坑单独看都不难,但碰上一次就能耗掉半天。先收藏,出了问题再逐条对照。
7.2 我在实际项目中的习惯
我个人经验是,如果只是验证板卡,优先用PS端UART;如果是为了学协议,再手写PL端UART。两者是先后关系而不是二选一。实际项目中我经常在PL端挂AXI UART Lite IP做业务串口,PS端UART留给Linux console,两个口各司其职。这样系统日志和数据流分开,调试起来不会互相干扰。
最后再分享一个小技巧:调试时在串口助手里发送带固定帧头的测试数据(比如AA 55开头),然后在代码里加个简单的帧解析,比对帧头是否正确。这比看一长串原始数据直观很多,尤其是数据量大时。UART本身不难,但把它用顺了,ZYNQ7010开发的整个节奏都会快起来。
本文还有配套的精品资源,点击获取