☰
ZYNQ串口扩展:用AXI Uartlite将任意PL引脚变成串口
2026/10/6 3:29:17 网站建设 项目流程

做ZYNQ开发的时间久了,你会发现一个特别真实的焦虑来源:需要用串口的地方,永远比PS端可用的UART多。调试要一个、日志要一个、跟外部MCU通信又要一个,偏偏ZYNQ的PS固定就那么几个UART,多一个都没有。后来我把主意打到了PL上——用AXI Uartlite IP核,直接把任意普通PL引脚变成串口。这个方案让我从“串口不够用”的泥潭里彻底解脱出来,而且整个过程不复杂,从Vivado里的IP配置、引脚约束,到SDK里的驱动调用,一整套链路我都踩过一遍,今天把完整流程和坑都整理出来。

这篇内容适合已经被PS串口数量卡住、或者想让ZYNQ的串口走PL侧自定义引脚的工程师。不需要你有多深的FPGA功底,只要用过Vivado、会看基本原理图,照着做就能把串口扩展出来。我尽量把每一步为什么这么做也讲清楚,毕竟只抄作业不弄懂原理,换个板子换个时钟,你照样会翻车。

1. 为什么要把串口放到PL侧

1.1 PS自带串口的两点现实局限

ZYNQ的PS端UART,从硬件资源上说不算少,比如ZYNQ-7020有2个UART,ZYNQ UltraScale+的型号会更多一些。但真实项目里,这两个串口的处境非常尴尬。

第一,PS端的MIO引脚是固定的。每个UART对应的MIO引脚位置是芯片出厂就固定死的,你想换也换不了。如果板级设计的时候MIO引脚已经被分配给了其他功能,比如SD卡、EMMC、ENET,那某个UART可能根本引不出来,或者只能通过EMIO借道PL再往外接。

第二,PS串口往往承担着系统调试的重任。Boot ROM打印、U-Boot信息、内核启动日志,全都默认走PS端的UART0或UART1。一旦你的调试串口被业务占用,系统启动阶段出了异常,你连个看日志的地方都没有。这时候PL侧顶一个串口出来,专门用于和业务设备通信,调试串口原封不动留给系统,这是最舒服的分工。

1.2 PL侧UART能带来的自由度

PL侧做串口,最吸引人的一点就是引脚不受MIO限制。只要满足bank电压和IO标准要求,理论上PL的任意普通IO都能作为UART的TX和RX引脚。

这意味着什么呢?你可以在PCB设计阶段,把串口信号就近拉到你方便走线、方便接插件的引脚上,而不是被MIO固定位置逼着绕一圈。对已经做好的板子,如果发现某个FPGA引脚空着没用,也可以临时把它配置成串口,不用改硬件,只改约束和逻辑,这灵活性是PS串口给不了的。

不过要泼一盆冷水:PL引出的串口,是逻辑层信号,不是直接的电平转换后的RS232。要对接PC的DB9或者USB转串口模块,外部电路还是需要一个电平转换芯片,比如MAX3232或者CH340G模块,这个跟PS串口在板级上的要求是一样的。

2. AXI Uartlite IP核是这样工作的

2.1 从IP核名字拆解它的定位

“AXI Uartlite”这个名字其实已经把它的特点说得很直白了。“AXI”说明它是挂在AXI总线上的外设,ZYNQ的PS通过AXI接口就能访问它;“Uart”说明它实现的是UART协议;“Lite”则是说它在功能上是轻量版。

和Xilinx另一个常用的UART IP核AXI UART16550相比,Uartlite的寄存器数量更少,FIFO深度也更浅,没有16550那种兼容PC老式串口的复杂模式。Uartlite内部结构非常精简:发送寄存器、接收寄存器、状态寄存器、控制寄存器,外加一个可选的波特率发生器,总共就这几个东西。

别小看这个精简设计。对于FPGA里面做嵌入式控制,我们大部分场景只关心“把字节发出去”和“把字节收进来”,那些调制解调器控制信号、DMA流控、多种帧格式,日常根本用不上。Uartlite在资源占用上非常省,在ZYNQ上跑起来也就消耗几百个LUT,对PL资源几乎谈不上压力。

2.2 波特率发生器的原理和计算

Uartlite支持两种波特率产生方式。一种是不勾选“Baud Rate Generator Enable”,完全由外部逻辑提供一个特定频率的信号,对应波特率是固定的;另一种是勾选“Baud Rate Generator Enable”,IP核内部通过分频的方式,把输入时钟按照你设定的波特率重新分频得到串口时钟。

在Vivado的IP配置界面,你只需要填两个参数:外部输入时钟频率和期望波特率。工具会自动算出分频系数。手动计算也不难,UART Lite的分频公式是:

分频系数 = 期望波特率对应的分频基准 = 输入时钟频率 / (2 × 波特率)

以100MHz外部时钟、115200bps为例:

100000000 / (2 × 115200) = 434.0278

分频系数要取整数,434。那么实际波特率是:

100000000 / (2 × 434) = 115207.37

误差只有0.006%,完全在UART通信允许的±2%误差范围之内,稳得很。

提示:这里尤其要注意,IP核配置界面里填的外部时钟频率,必须跟实际接到IP核上的时钟频率严格一致。很多人的串口乱码不是电路问题,而是这里拍脑袋填了一个数,跟实际不一致,结果波特率实际值跑偏到天边去了。

2.3 IP核配置界面里那些关键选项

在Vivado IP Catalog里搜索“Uartlite”或者“AXI UART Lite”就能看到这个核。双击进入配置界面,真正需要关心的选项其实就那么几个:

  • Baud Rate:目标波特率,常用9600、115200、57600都行
  • Baud Rate Generator Enable:勾上,让IP核内部自己分频;不勾的话,波特率由外部输入时钟直接决定
  • Data Bits:数据位宽,常规选8位
  • Parity:校验方式,None、Even、Odd可选
  • Stop Bits:停止位,1位或2位
  • FIFO:收发FIFO的深度,可选16字节或32字节,够用就行

这个IP核还有一个General Purpose选项,通常是“UART_LITE”功能模式,不需要动。配置界面的Interrupt选项,建议把“Enable Interrupt”勾上。虽然裸机下轮询也能用,但中断方式在系统的实时性上会好很多,尤其是接收外部不定长数据的时候,轮询很容易丢字节。

3. Vivado实操:从添加IP到生成比特流

3.1 创建工程并添加IP核

新建RTL工程,这一步大家都会。器件型号选中你自己的ZYNQ型号,比如XC7Z020CLG484。在原理图设计界面点击添加IP,搜索“AXI Uartlite”,双击添加进去。

添加之后,IP核会自动出现在Block Design里。接着把ZYNQ Processing System也添加进去,运行Block Automation,让Vivado自动把PS和PL之间的AXI总线连好。这里有一个容易忽略的点:Block Automation的选项里,会有一个“Apply Board Preset”的复选框,除非你知道自己在做什么,否则不要勾选预设,让工具使用默认配置,免得自动生成的引脚约束跟你后续手动约束冲突。

3.2 连线方式与地址分配细节

AXI Uartlite作为一个AXI从设备,要接到PS的AXI主端口上。在ZYNQ-7020这种器件上,PS的M_AXI_GP0口是最常用的连接点,它可以挂载多个AXI外设,Uartlite作为其中一个从机。

在Block Design里选中Uartlite的S_AXI端口,右键点击“Make Connection”,连接到M_AXI_GP0。工具会自动给你分配一个地址空间,但建议手动指定,方便后续写设备树和驱动。常见做法是分配一个独立的地址块,比如0x42C00000,地址范围默认给64KB,Uartlite的寄存器只有十几个字节,64KB对它来说绰绰有余,系统里预留大一点反而避免跟其他IP地址重叠。

连线的时候还要特别注意时钟和复位。Uartlite需要一个时钟输入,通常直接用PL的FCLK_CLK0,也就是PS生成并输出到PL的时钟。复位信号也要连上,可以是PS的复位输出,也可以是自己做的复位逻辑。

3.3 关键约束:怎么把UART信号绑定到任意引脚

这一步就是标题里说到的“把任意引脚变成串口”的关键动作。在Block Design里,Uartlite的UART接口是一组端口,名字默认叫uart_tx和uart_rx。要让它们变成FPGA的真实物理引脚,得在顶层模块里把它们引出来,然后在XDC约束文件里指定位置。

在顶层Verilog里,把uart_tx和uart_rx声明为输出和输入,连接到总线的例化端口。然后在XDC文件里写:

set_property PACKAGE_PIN AB10 [get_ports uart_tx] set_property PACKAGE_PIN AB11 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart_rx]

AB10和AB11只是示例,实际引脚号要从你板子的原理图或者FPGA封装文件里查。要强调的一点是:这里的引脚必须是PL侧的普通IO(HR或者HP bank),不能是PS专用引脚。只要bank电压等级和IOSTANDARD匹配,TX和RX放到哪两个脚完全由你说了算。

这里有个经验:优先选择靠近电路板边缘、便于接插件走线的引脚,同时避免占用跟DDR、Flash等硬核功能相关的专用引脚。另外,同一个bank里的引脚IOSTANDARD必须统一,不要把3.3V和1.8V混在同一个bank里用,否则时序和电平都会出问题。

3.4 生成比特流并导出硬件描述

约束写好后,先跑综合,再跑实现,最后生成比特流。这个过程如果遇到时序违例,优先看是不是引脚约束的IOSTANDARD写错了,或者bank电压不匹配。UART是低速外设,理论上走线不会成为时序瓶颈,大多数报错还是电平标准配置问题。

比特流生成完毕,记得在Vivado里执行File > Export Hardware,勾选“Include bitstream”,导出XSA文件。这个XSA会作为后续SDK或者Vitis工程的硬件描述输入。到这一步,硬件部分的配置工作就完成了,接下来才是从逻辑世界走进驱动世界。

4. 驱动层:裸机和Linux下的两种调用路径

4.1 裸机驱动:SDK/Vitis中生成并调用

在Vitis(老版本叫SDK)里,基于刚才导出的XSA文件创建应用工程。新建平台工程时,Vitis会自动解析XSA里的硬件信息,为AXI Uartlite生成驱动源码。面向单个应用时,常用的驱动API有这么几个:

  • XUartLite_Initialize:初始化UART实例,参数是设备ID或者基地址
  • XUartLite_Send:发送指定长度的数据
  • XUartLite_Recv:接收指定长度的数据
  • XUartLite_SetRecvHandler:设置接收中断回调函数
  • XUartLite_IsReceiveDataEmpty:查询接收FIFO是否为空

裸机下最简单也最稳定的流程是:初始化之后,发送用轮询,接收用中断。轮询发送的好处是逻辑简单,不会出现数据覆盖;接收用中断,是因为外部设备什么时候发数据是完全不可预测的,轮询接收在实时性上根本没有保障。

中断处理有一点要提醒:Uartlite的中断是电平敏感的,回调函数里要尽快把FIFO里的数据读出来,否则中断一直挂起,系统会被打断到崩溃边缘。我在项目里一般是在回调里把数据搬进一个环形缓冲区,中断处理函数本身只做“搬运工”,绝不做复杂解析,这样系统响应又快又稳。

4.2 Linux驱动:设备树与uartlite模块配置

Linux环境下,Xilinx提供了现成的uartlite驱动,内核配置里开启CONFIG_SERIAL_UARTLITE以后,驱动模块就会在系统启动时自动探测设备树里描述的Uartlite节点。

设备树里,Uartlite节点的写法大同小异,核心内容如下:

serial@42c00000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x0 0x42c00000 0x0 0x1000>; interrupts = <0 23 4>; interrupt-parent = <&intc>; clock = <&clkc 22>; };

这里几个字段要解释一下。reg里的地址要和Vivado里分配的地址完全对应,否则驱动探测不到设备;interrupts是中断号,这个数值不是随便写的,它由PS的IRQ编号和PL外设连接到PS中断控制器的具体位置决定。具体数值怎么查?Vitis的xparameters.h里有宏定义,比如XPAR_AXI_UARTLITE_0_INTC_TYPE,照着搬就行。

设备树配好以后,内核起来应该能在/dev下看到ttyUL0、ttyUL1这样的设备节点。ttyUL是uartlite专用的命名,用它就行。

在Linux下调用跟普通串口没有任何区别。打开设备节点、配置termios参数、调用read/write,这套标准操作完全适用。我的习惯是用stty工具配置波特率:

stty -F /dev/ttyUL0 115200 raw

配置好之后,直接echo test > /dev/ttyUL0就能把数据发出去。

5. 测试环节与踩坑记录

5.1 验收功能前先做回环测试

无论裸机还是Linux系统,拿到一个新串口,我的习惯是先把TX和RX短接,做一次硬件回环测试。把TX和RX用一根杜邦线直接连起来,然后在代码里发送一串数据,如果接收端能原样收到,说明UART通路是通的。

这个测试在硬件调试阶段最有效。因为如果回环都收不到数据,问题百分之百出在引脚约束、电平标准或者时钟配置上,跟后面业务逻辑一点关系都没有。先把这个最简单的链路打通,再去接外部设备,排查问题的时候范围能缩小一大半。

裸机下做回环测试的伪代码很好写:

XUartLite_Initialize(&UartLite, 0); XUartLite_Send(&UartLite, test_buf, buf_len); while (!XUartLite_IsReceiveDataEmpty(&UartLite)) { XUartLite_Recv(&UartLite, &recv_ch, 1); }

如果收回来的数据跟发出去的完全一致,恭喜,硬件链路没问题。

5.2 最容易翻车的几个坑

我在这套流程里踩过的坑,按出现频率排序大概是这几个。

第一个是波特率对不上,表现是数据全是乱码。排查时先回头看IP核配置界面的外部时钟频率,跟实际给的时钟是不是一致。我之前有一次是从FCLK_CLK1取的100MHz时钟,配置界面里却填了50MHz,结果波特率偏了一倍,排了半天才发现是这种低级的粗心错误。

第二个是引脚约束报错。Vivado在综合的时候报“端口不存在”或者“引脚是专用输入引脚”之类的问题,通常是因为顶层模块端口名和XDC里get_ports的名字不匹配。这个名字必须跟顶层RTL里的端口名完全一致,大小写都不能错。

第三个是Linux下设备节点没生成。大概率是设备树地址和Vivado分配的地址对不上。建议直接去Vitis生成的xparameters.h里复制基地址,粘贴进设备树,不要手动输入,能省去对齐地址的麻烦。

第四个是中断不生效。裸机下如果设置了接收中断但一直不进回调,先查中断控制器的连接有没有使能,再看Uartlite的中断输出有没有接到PS的中断输入端口。用户自定义的逻辑里,这一步经常因为忘记在Block Design里连线而白忙活。

5.3 排查思路速查表

现象最可能的原因排查方向
完全无数据输出引脚约束错误或外部电平转换芯片故障先测IO电平,再查XDC和bank电压
输出乱码波特率不匹配核对IP时钟频率与预期波特率
发送正常收不到RX引脚约束错误或外部设备未接GND测量RX引脚静态电平,回环测试验证
Linux下没有ttyUL节点设备树地址错误或驱动未编译核对reg地址,检查内核config
中断回调不触发中断连线遗漏或中断号错误检查Block Design中断连接和xparameters宏定义
收发一段时间死机接收FIFO溢出中断回调搬数据要快,使用环形缓冲

这套排查思路其实不局限在Uartlite这个IP上。任何PL外设出问题,我都会先按“硬件链路 → 寄存器配置 → 驱动配置”这三层来排查。硬件层面的问题用示波器看波形,寄存器层面的问题用AXI调试工具读寄存器值,驱动层面的问题看内核日志和系统调用返回,每一层都有对应的高效工具,善用它们能省下大把调试时间。

6. 走完一遍流程后的实用建议

6.1 扩展应用与资源规划

如果你发现一个Uartlite不够用,可以瞬间再例化第二个、第三个,只要PL资源够用。它们彼此完全独立,互不干扰,每个都只需要一条AXI地址空间和一个中断通道。在Block Design里多拖几个IP核,再调整一下地址和中断,一次性扩展出好几个串口不是梦。

但别太放飞自我。每个Uartlite在PS上都要占一个中断号,中断号资源是有限的,尤其是PL到PS的中断只有固定的几个通道。串口数量太多,中断分配会变得非常局促。另一个要注意的就是AXI总线的带宽,低速串口虽然吃不了多少带宽,但多个外设同时高频率访问时,总线资源还是有竞争,最好估算一下峰值流量。

6.2 一句真心话

从我个人的实际经验看,AXI Uartlite是ZYNQ开发里“性价比”极高的一个IP核。学习成本低、硬件实现快、驱动也成熟,再加上PL引脚自由度带来的硬件设计弹性,几乎是一本万利的选择。比起去抠那些复杂总线协议和高速接口,把这类小IP用好、用透,能解决很多项目里的实际问题。如果你正被串口数量困扰,不妨花一个下午把这个方案落地,大概率会回来感谢现在的自己。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询