搞过ZYNQ开发的朋友,多多少少都经历过这么一遭:板子上PS端的串口不够用,或者被其他外设占了MIO引脚,调了半天硬是要把串口挪到PL侧。这时候AXI Uartlite IP核就是一张很好的底牌。不少人觉得这东西不复杂,但真正动手时会发现,从Vivado搭建、引脚约束到驱动文件,每一步都有不少容易被忽略的细节。这篇文章就以“把PL任意引脚变成串口”为目标,把AXI Uartlite从原理、配置、约束到驱动的完整链路梳理一遍,顺便把我实测过程中踩过的坑和验证方法一并写出来。
1. PS端串口不够用的真实场景:MIO固定映射带来的设计困境
1.1 为什么ZYNQ的UART引脚不能随便换:MIO的固定映射规则
开始之前,先说清楚一个很多人容易混淆的问题:ZYNQ里面的PS串口(UART0和UART1)并不是你想映射到哪个引脚就能映射到哪个引脚。它们走的是MIO(Multiplexed I/O)通道,而MIO引脚分配由芯片内部硬件固定,属于“配置选通”而不是“任意映射”。ZYNQ-7000器件上MIO引脚分散在Bank 500和Bank 501,每个Bank的引脚功能复用关系在数据手册里都有明确表格,你只能在这些表格允许的组合里做选择。
举个例子,MIO 10到MIO 13可以选UART1的TXD、RXD等信号,但它们同时也能复用为SDIO、SPI、GPIO、CAN等功能。你一旦把某个MIO引脚配成SDIO的数据线,它就没法同时作为串口引脚使用。如果板子的SD卡、以太网、QSPI Flash把MIO占得七七八八,留给UART的选择可能就剩下一两个默认引脚位。更麻烦的是,很多外设(比如SDIO)默认的主复用功能占了MIO 40到MIO 47,这部分和你想要的UART引脚往往存在重叠。所以有时候不是“不会配”,而是硬件上确实没有空闲的MIO资源供PS串口使用。
另外,还有一层比较隐蔽的限制:PS UART的引脚选择在FSBL或者BootRom阶段就要确定,属于系统启动早期的一部分。虽然你可以通过修改MIO配置把UART0/1放到不同位置,但每次改完都要重新生成FSBL、重新做启动镜像,不像PL侧的引脚约束那样改完XDC重新综合就行。这个流程上的成本,只有真正改过MIO的人才有体会。
1.2 什么时候必须用PL引脚做串口:EMIO扩展的现实需求
那什么时候需要用PL引脚来做串口?我在实际项目里遇到的主要是这么几种情况:
第一种是引脚物理位置不合适。PS的MIO引脚位置是固定的,PCB布线时要绕很远才能接到连接器或电平转换芯片。而PL侧引脚分布非常灵活,完全可以选一个挨着目标器件方向的引脚,布线短、信号干净。
第二种是MIO资源确实不够。板子上已经有以太网、SD卡、NAND Flash、USB等外设,MIO的每个引脚都有主用对象,硬挤串口会牺牲其他功能。这时候把一个或多个串口放到PL侧,是最省事的做法。
第三种是需要的串口数量超过2个。ZYNQ的PS本身只有两个UART控制器,但很多项目需要3个、4个甚至更多的串口。PL里例化多个AXI Uartlite(或者UART 16550 IP核),就能轻松扩展出多个独立串口,各自有独立的中断号和地址空间。
从实现路径上讲,让PL引脚承担串口功能有两种思路:第一种是把PS的UART信号通过EMIO接口引到PL引脚。EMIO是PS到PL的一组可配置接口,PS外设可以通过EMIO把信号路由到PL的任意引脚。但这种方式本质上还是使用PS内部的UART控制器,只是在引脚分配上做了延长,EMIO引脚数量和分配规则还有额外限制。第二种是直接在PL里例化UART控制器IP核,由PL内部逻辑完成串口协议处理,然后通过AXI总线和PS交互。这种方式在引脚选择上最自由,几乎可以接到PL侧任意一个支持所需电平标准的引脚。
1.3 三条路线对比:怎么选更合理
下面这张表我从实操角度把MIO串口、EMIO串口、AXI Uartlite的差异整理了一下,方便大家按项目情况对号入座。
| 对比项 | PS UART(MIO) | PS UART(EMIO) | AXI Uartlite |
|---|---|---|---|
| 引脚位置 | 固定MIO引脚 | PL任意引脚 | PL任意引脚 |
| 控制器资源 | PS内部硬件 | PS内部硬件 | PL内IP核 |
| FIFO深度 | 64字节(每个方向) | 64字节(每个方向) | 默认16字节,可配置 |
| 波特率范围 | 最高可达1Mbps以上 | 最高可达1Mbps以上 | 和时钟频率相关,115200完全够用 |
| 中断开销 | PS内部中断 | PS内部中断 | 经过AXI互联上报PL中断 |
| 驱动复杂度 | 最低,BSP自带 | 低,BSP自带 | 中等,需要配置IP和地址 |
| 适用场景 | 常规串口,资源充足 | MIO被占用但仍想用PS控制器 | 多串口、引脚位置自由、PL侧扩展 |
选型逻辑其实很朴素:如果只是MIO引脚位置不合适,且MIO数量还够,用EMIO最省事;如果是为了扩展串口数量,或者希望完全绕开MIO分组限制,AXI Uartlite更适合。
2. AXI Uartlite的工作机制拆解:从AXI总线到引脚电平的完整链路
2.1 IP内部到底有什么:寄存器、FIFO和状态机
把AXI Uartlite理解成“一个挂在AXI总线上的串口协议处理器”就够了。当你从软件层向某个地址写入一个字节,数据会先经过AXI-Lite总线进入IP核内部的发送FIFO,然后由状态机按串口时序一位一位地把数据从TXD引脚送出去。接收侧的逻辑则相反,RXD引脚上的电平被采样后,经过起始位检测、数据位采样、停止位校验等流程,最终把字节写入接收FIFO,等待软件读取。
IP核内部核心逻辑包含几大部分:
- AXI-Lite从接口:负责和PS的AXI互联对接,软件通过读写寄存器来操作IP。
- 控制寄存器(Control Register):控制发送/接收的使能、中断使能等。
- 状态寄存器(Status Register):反映发送FIFO是否为空、接收FIFO是否有数据、是否溢出等。
- TX FIFO / RX FIFO:每个方向默认深度16字节(在Vivado里可以配置,最多能到64或更大)。
- 波特率发生器:根据输入的时钟频率和波特率分频参数,产生串口采样时钟。
- 发送/接收状态机:完成UART协议帧的组帧和解帧。
有一点必须在设计之前了解清楚:AXI Uartlite的接收路径没有“超时中断”机制。也就是说,如果对方发了一帧数据,但字节之间的间隔超过了某个阈值,IP核不会主动通知你“数据接收可能不完整”。你只能靠状态寄存器里的RX FIFO非空标志去判断有没有新数据。这在轮询方式下问题不大,但如果依赖中断处理高频数据流,可能会遇到“一帧数据被拆成多次中断”的情况,后面实测部分会详细说。
2.2 波特率是怎么算出来的:分频公式与误差
AXI Uartlite内部波特率发生器的实现并不复杂,它通过计数分频产生采样时钟。理想情况下,我们要的波特率是115200bps,则每一位的周期是1/115200 ≈ 8.68微秒。IP核根据输入时钟s_axi_aclk的频率,计算需要分频的系数,使采样时钟尽可能接近目标波特率。
实际使用时,这个分频系数是被取整的,所以总会存在一定的波特率误差。误差太小可能无所谓,但误差一旦超过百分之一二,长时间通信时就会出现误码。我在配置IP时习惯的做法是:
- 先确定s_axi_aclk的时钟频率。比如设计里全局时钟是100MHz,IP核的时钟就接100MHz。
- 在Vivado的IP配置界面里把波特率填上,同时核对实际生成的采样时钟频率。
- 重点关注波特率误差,一般小于1%问题不大,工程上建议控制在0.5%以内。
以100MHz时钟、115200波特率为例,分频系数大约是868.06,取868后实际波特率约为115207,误差不到0.01%,非常理想。但如果你的时钟用了33.33MHz这种比较奇怪的频率,分频后的误差可能就会偏大。所以在Block Design里给Uartlite选时钟时,尽量选一个能整除波特率的频率,能省去很多后续麻烦。
2.3 数据路径追踪:一帧数据从软件到引脚的完整旅程
为了后面调试方便,这里把数据走的路径完整过一遍。假设软件往Uartlite的发送FIFO写入一个字节0x55,也就是二进制01010101,接下来会发生这些事:
- PS的CPU执行写操作,把地址和写数据放到AXI总线上。
- AXI互联把这次写事务路由到对应地址的Uartlite IP。
- IP核的AXI-Lite接口解析写地址和写数据,把字节写入TX FIFO。
- 发送状态机检测到TX FIFO非空,取出一个字节,先发送起始位(低电平)。
- 随后按LSB first的顺序逐位输出8个数据位,对应0x55就是1、0、1、0、1、0、1、0。
- 最后发送停止位(高电平)。
接收侧的过程是逆过来的:RXD引脚先检测到下降沿(起始位),然后在每一位的中心点采样电平,凑满8位后连同停止位一起校验,最终把数据字节写入RX FIFO,置位状态寄存器里的非空标志。
这个流程听起来简单,但它意味着你在调试时可以把每一段链路单独拆开验证:软件层看寄存器能不能正确读写,IP层看FIFO有没有数据在流动,物理层用示波器看引脚波形。想明白这条链路,后面遇到问题就不至于一头雾水。
3. Vivado Block Design里的完整搭建过程:IP配置、地址分配与引脚约束
3.1 添加并配置IP:时钟和中断这两个选项最容易被忽略
在Vivado里新建Block Design之后,添加ZYNQ PS核是最基础的步骤。跑完Block Automation之后,PS核默认会带出M_AXI_GP0接口,这个接口就是用来连接PL侧AXI外设的。接下来添加AXI Uartlite IP核,双击打开配置界面,有几个选项值得仔细斟酌。
第一个是component name,保持默认就行,不影响功能。第二个是波特率设置,按上文的误差分析来定。第三个是FIFO深度。默认是16字节,如果串口通信的数据量不大、且以交互式命令为主,16字节完全够用;如果要做连续大批量传输,建议把FIFO加深到32或64,减少CPU频繁进中断的次数。
另外还有一个经常被忽略的选项:是否使用中断。AXI Uartlite的interrupt端口是可选的。如果你希望串口收到数据时主动通知PS,就需要把IP的中断输出接到PS核的IRQ_F2P端口上。对于简单轮询应用,可以完全不接中断,节省一个PL-PS中断通道。但如果业务上不允许CPU长时间占用等待串口,那么中断几乎是必须的。
这里要特别提醒一下时钟的设置。AXI Uartlite有两个时钟端口:一个是s_axi_aclk,也就是AXI总线时钟,另一个是s_axi_aresetn,用于复位。s_axi_aclk通常和AXI互联的时钟保持一致,在Block Design里一般会自动连接。关键是它的频率要和你预期的波特率匹配,我的建议是给Uartlite单独分配一个100MHz时钟,而不要随便用一个什么外设的派生时钟。时钟频率不稳,波特率误差就会变大。
3.2 Block Automation与地址分配:AXI地址如何落到0x42xxxxxx
IP添加完成后,Vivado会提示你运行Block Automation。这一步会自动完成AXI互联的连接,并给Uartlite分配一个地址段。ZYNQ PS核的M_AXI_GP0接口默认地址范围是0x40000000到0x7FFFFFFF,所以Uartlite的地址一般会落在0x42xxxxxx附近。
运行Run Connection Automation后,Vivado会自动插入AXI Interconnect。如果你只挂了这一个外设,互联层就是简单的一对一连接。但如果你以后要多挂几个PL侧IP,可以手动指定每个IP的地址范围,注意不要重叠。
地址分配完成后,把IP的中断(如果有)连到PS核的IRQ_F2P,把Uartlite的uart_txd和uart_rxd这两个端口做成外部端口,后面在XDC里约束引脚。还有一个细节:Uartlite默认没有把uart_ctsn和uart_rtsn例化出来,因为AXI Uartlite本身不做硬件流控,这两个信号是可选的。如果不需要流控,保持默认不上拉就行。
3.3 XDC引脚约束写法:类型、电平标准与IO Bank规则
接下来是文章标题里提到的重点——把PL任意引脚变成串口的物理基础。XDC约束其实分成两部分:IP的引脚约束和时钟约束。
IP新增的uart_txd和uart_rxd端口,在XDC里要明确指定物理引脚和电平标准。比如:
set_property PACKAGE_PIN L14 [get_ports uart_txd] set_property IOSTANDARD LVCMOS33 [get_ports uart_txd] set_property PACKAGE_PIN M15 [get_ports uart_rxd] set_property IOSTANDARD LVCMOS33 [get_ports uart_rxd]这里有两件事需要严格注意。
第一,PACKAGE_PIN必须是你选择的PL引脚的FPGA封装编号,这个编号在外设原理图设计时就要确定下来,不能随手写。第二,IOSTANDARD要和对应的Bank电压一致。绝大多数ZYNQ开发板上HR Bank的VCCO是3.3V,所以用LVCMOS33没错。但如果某个Bank的VCCO被接成了1.8V,那这里就应该是LVCMOS18,否则编译会报错,或者即使跑通了信号电平也不对。
另外,引脚方向也很关键。uart_txd是输出,应该选一个在原理图上没有被占用、且驱动能力足够的引脚;uart_rxd是输入,引脚选择相对自由。最好避开DDR、GTP、配置相关的专用引脚,同时关注引脚是否支持你需要的IOSTANDARD。在Vivado里可以通过IO Planning视图直观地查看每个Bank的电平和可选引脚,操作起来比对着封装文件翻方便很多。
如果你想让Uartlite的中断生效,XDC里不需要专门约束中断引脚,因为IRQ_F2P走的是PS和PL之间的专用接口,不做物理引脚约束。
3.4 生成比特流和导出硬件:把设计落到可调试状态
Block Design配置完毕、XDC约束写好后,先做综合和实现。这一步经常遇到的问题就是引脚约束冲突:某个引脚已经被其他IP占用、或者电平标准不匹配。Vivado会在布局布线阶段给出明确错误信息,不需要盲猜,按报错改XDC就行。
实现通过后,Generate Bitstream生成比特流。导出硬件时有两个选项:一种是只导出硬件配置,适合后面用PetaLinux做系统镜像;另一种是导出同时启动SDK,适合裸机开发。无论哪种方式,导出的hdf文件里都已经包含了Uartlite的地址信息、中断号和外设配置,这是后面写裸机或Linux驱动的基础。
有一个我在工程里反复遇到的问题:导出硬件后,如果修改了Block Design里的地址分配或IP配置,务必重新Generate Bitstream并重新导出hdf,否则驱动里用的地址和实际硬件不一致,会出现“寄存器能写但串口没反应”的怪现象。这个坑看着低级,但工程繁忙时特别容易犯。
4. 驱动文件从哪来、怎么改:裸机驱动与Linux驱动的落地点
4.1 裸机驱动:Vivado SDK自动生成的xuartlite核心函数
硬件导出到SDK后,BSP会自动生成Uartlite的驱动代码,路径一般在bsp/ps7_cortexa9_0/libsrc/xuartlite_vX_X/src/下。如果只是想在裸机上快速验证串口能不能通,可以直接使用现成API,不需要自己写寄存器操作。
初始化接口是XUartLite_Initialize,传入设备ID和基地址。设备ID在xparameters.h里可以查到,一般形如XPAR_AXI_UARTLITE_0_DEVICE_ID,基地址则是XPAR_AXI_UARTLITE_0_BASEADDR。初始化之后,发送用XUartLite_Send,接收用XUartLite_Recv。
实际使用时要注意一个细节:XUartLite_Send是按FIFO发送的,如果发送的数据量大于FIFO深度,函数只把前几个字节推入FIFO就返回了。裸机例程里经常配合XUartLite_IsTransmitFull(或者直接用状态寄存器判断)来轮询等待发送FIFO空,再继续发送剩余字节。很多人上来直接丢一大包数据给XUartLite_Send,结果收到的数据缺胳膊少腿,就是因为忽略了FIFO深度限制。
接收侧同样容易踩坑。XUartLite_Recv如果指定接收字节数大于当前FIFO内的数据量,它可能会一直阻塞等待,这在轮询模式下风险很大。我建议在裸机代码里先读状态寄存器,判断RX FIFO非空,再取一个字节处理,不要一次性等待固定字节数。
4.2 Linux驱动:内核里的uartlite.ko与设备树节点怎么写
到了Linux下,情况又不一样。ZYNQ的PL侧Uartlite在Linux内核里的驱动是drivers/tty/serial/uartlite.c,它的设备树匹配字符串是xlnx,xps-uartlite-1.00.a。这个驱动在内核配置中的开关是CONFIG_SERIAL_UARTLITE,编译选项名可能随内核版本略有变化,但基本上都要同时勾选它作为模块或直接编入内核。
设备树节点才是真正和硬件绑定的一环。一个典型节点长这样:
axi_uartlite_0: serial@42c00000 { compatible = "xlnx,xps-uartlite-1.00.a"; reg = <0x42c00000 0x10000>; interrupts = <0 29 4>; interrupt-parent = <&intc>; clock-names = "s_axi_aclk"; clocks = <&clkc 15>; port-number = <2>; };逐个字段解释一下:
- compatible:必须和内核驱动匹配,否则不会绑定到这个节点。
- reg:基地址和地址空间大小,来源就是Block Design里的地址分配。这个例子里0x42c00000是实际分配的地址。
- interrupts:中断号。这里要非常小心,PL侧IP的中断经过PS中断控制器后,中断号会有一个偏移。ZYNQ PS的GIC是SPI中断,Uartlite接在IRQ_F2P上时,设备树里写的中断号基本是在F2P编号基础上加32左右。具体数值可以在Block Design里看到,也可以查
xparameters.h里的XPAR_FABRIC_INTERRUPT_...宏。 - clocks:时钟源。通常接PS的FCLK_CLK0,具体时钟索引要在设备树头文件里查。
- port-number:可选字段。如果不写,Linux会按探测顺序自动分配tty编号,写上的话可以固定成比如ttyUL2。
有一点要尤其提醒:Uartlite在Linux下创建的设备节点名不是ttyS*,而是ttyUL*。如果某个应用脚本里写死了/dev/ttyS1,但实际设备是/dev/ttyUL1,就会出现“服务起来了但数据不出去”的诡异现象。检查的时候一定要先ls /dev/ttyUL*确认设备节点存在。
如果设备树里没写interrupts字段,驱动会退化为轮询模式,CPU占用会很高,而且在数据密集时容易丢字节。所以中断字段建议一定配上。
4.3 PetaLinux下的整合步骤:从配置到启动镜像
如果整个系统用PetaLinux构建,整合Uartlite的流程要更自动化一些。在PetaLinux工程里先导入从Vivado导出的xsa文件,然后运行petalinux-build。构建系统会根据xsa里的硬件信息自动生成设备树源文件,一般位于components/plnx_workspace/device-tree/device-tree/下,plnx_author生成的pl.dtsi会包含Uartlite节点。
很多情况下自动生成的节点已经可以正常工作,但你仍然要检查两处:
- 是否有
port-number属性,是否需要固定tty编号。 - 中断号的偏移是否正确匹配。
如果想用这个串口作为系统的启动控制台(console),还需要在U-Boot或内核启动参数里指定console=ttyUL0,115200。注意ZYNQ默认的console通常绑定PS端UART1的ttyPS0,不要让两个console指向同一个物理串口。PetaLinux的配置界面里有一个“Serial console”设置,改成你的Uartlite对应设备即可。
如果使用较新版本内核,xlnx,xps-uartlite-1.00.a这个兼容字符串有可能发生变化或增加变体。编译之后用lsmod或/proc/devices确认驱动有没有注册成功,再用dmesg | grep uartlite看内核日志,比反复怀疑设备树写法高效得多。
5. 实测验证与信号质量评估:回环测试、波特率误差与常见怪问题
5.1 先把硬件链路分成三段排查
实测是检验设计的唯一标准。拿到一个调试不通过的Uartlite系统,我的排查顺序是把链路分成三段:PS到IP寄存器、IP到引脚、引脚到外部器件。
第一段验证PS能不能正确读写Uartlite寄存器。在裸机程序里直接往发送FIFO写几个字节,读回状态寄存器,看看FIFO空标志有没有变化。也可以用调试器直接看地址0x42c00000附近的内容。这段能通,说明AXI互联和地址分配没问题。
第二段验证IP到引脚。写一个最简单的循环,持续向外发送0x55或0xAA这样的固定字节,用示波器看引脚有没有对应波形。如果波形没有,检查引脚约束和方向,再检查IP的时钟有没有真正连上。经常有人把s_axi_aclk挂在了一个没有起振的时钟上,结果发送侧完全没动静。
第三段验证外部硬件。很多板子上Uartlite的TXD/RXD还经过电平转换芯片、排针、杜邦线等,任何一环接触不良都会导致通信失败。把TXD和RXD短接做回环测试,是排除外部链路问题最快的方法。
5.2 回环测试的完整实操:短接TXD/RXD看串口助手
回环测试的做法很简单:在引脚附近的排针或测试点上,用杜邦线把Uartlite的TXD和RXD短接在一起,然后从软件侧发送一个字节,看能不能在接收FIFO里读到同一个字节。
具体步骤可以这样:
- 在Vivado里确认uart_txd和uart_rxd都约束正确,生成比特流并烧录。
- 用短线把TXD和RXD短接。尽量靠近引脚端短接,不要跨越长距离的PCB走线。
- 在裸机程序里先初始化Uartlite,然后发送0x55,再读取接收FIFO。如果读到的值是0x55,说明从发送FIFO、移位寄存器、引脚驱动、回环到采样接收、接收FIFO这一整条PL内链路都通了。
- 如果发送后接收FIFO一直为空,用示波器在引脚处看是否有波形的转换。有波形但软件读不到,多半是时钟采样或FIFO读时序问题;连波形都没有,问题大概率在IP配置或引脚约束。
回环测试通过之后,才能放心地把TXD/RXD接到外部串口芯片或另一块板子,否则互连调试时你会分不清是这一侧的问题还是对侧的问题。
5.3 抓到的波形和波特率误差:为什么115200时误差接近0
回环通过后,用示波器或逻辑分析仪抓一下TXD引脚的波形。以115200波特率、发送0x55为例,示波器上应该能看到一帧完整的UART波形:先是低电平的起始位,然后是8个数据位(0x55对应01010101),最后是高电平的停止位。
实际测量时,可以用示波器的光标功能量一下一位的宽度。115200bps时一位的理论宽度是8.68微秒,如果测量结果是8.69微秒左右,说明波特率误差在可接受范围。绝大多数情况下AXI Uartlite配合整数分频的时钟,115200的误差都非常小,这一点比某些软件模拟串口要可靠得多。
但如果你用了非标准波特率(比如250000),或者时钟频率选择不合理,误差就可能放大。我之前在某个设计里为了省时钟资源,给Uartlite挂了一个33.333MHz的时钟,波特率配成115200后实测误差在0.5%以上。短距离调试看不出问题,但接上一根十几米的线,或者对端的串口芯片要求严格,就会出现随机乱码。后来把时钟换到100MHz,一切恢复正常。这件事给我的教训是:波特率误差不能只在纸面上算,最好还是用示波器实测一帧波形。
5.4 常见怪问题:发送阻塞、丢字符、中断风暴
调试过程中大家最常遇到的几个问题,我按踩坑频率从高到低排一下:
发送阻塞是最常见的。现象是软件写发送FIFO后,程序卡在某个循环里出不来。原因通常是发送FIFO满时驱动代码一直在轮询等待,而外部时钟或复位没有配置好,状态机没有正常运行。排查时先看复位信号是不是正确释放,再看时钟有没有供应。
丢字符也挺常见。尤其是接收侧不使能中断、纯轮询读FIFO时,如果下一帧数据到达时上一帧还没读完,新数据会覆盖或触发溢出标志。Uartlite的RX FIFO只有16字节深度,一旦CPU响应不及时,丢数据几乎是必然的。解决办法是加深FIFO、开中断、或者在应用层做流控。
中断风暴这个坑比较隐蔽。如果没有处理好Uartlite的中断状态清除逻辑,IP会在中断服务函数退出后立即再次触发同一中断,导致系统看起来像卡死了一样。裸机编程里务必在ISR中读取状态寄存器,明确清除对应中断标志;Linux下一般由内核驱动处理,但如果你自己改了驱动,也需要注意同样的逻辑。
6. 扩展方案的边界与替代思路:FIFO深度、中断开销与何时该换方案
6.1 AXI Uartlite的硬限制:FIFO默认16、无硬件流控、无超时中断
AXI Uartlite不是万能的,它的定位本来就是“轻量级UART控制器”。在做关键选型之前,它的三个硬限制必须先想清楚:
第一,FIFO深度最大也就几十字节,而且这是在Vivado里可配置的上限。如果要做高速、大批量的数据透传,这个缓冲深度很容易造成溢出。第二,IP核没有硬件流控引脚(CTS/RTS是可选但带不来流控协议),也就是说它不提供自动的RTS/CTS握手逻辑。你要做流控的话,只能靠GPIO和软件模拟。第三,接收侧没有超时中断。很多串口协议是一帧多字节、字节间有间隔的,如果依赖Uartlite判断“一帧结束”,你会发现它很难区分一次完整接收和多次零星接收。
这些限制在简单应用里不算什么,但如果你要做RS485通信、Modbus从站、或需要自动方向控制的场景,AXI Uartlite会显得力不从心。
6.2 中断开销对比:Uartlite vs PS UART
有的团队会问:既然PS本身有UART控制器,为什么非要为了PL引脚灵活性去承担一个PL侧IP的中断开销?答案取决于你的中断实时性要求。
ZYNQ的PL-PS中断路径经过IRQ_F2P和GIC,中断延迟比PS内部外设的中断多了一些周期,但对绝大多数串口应用来说,这个额外的几微秒延迟完全没有体感。实测下来,115200波特率下,每字节间隔大约87微秒,CPU有充足的时间在两次中断之间完成数据搬运。真正的瓶颈不是中断延迟,而是中断頻次和CPU负载。如果每字节都来一次中断、每字节又只处理一个字节,那么再快的中断也有开销。这种情况下的优化方向是加深FIFO或使用DMA,而不是纠结PL还是PS控制器。
如果你真要追求极限吞吐,可以考虑改用AXI UART 16550 IP核,它的FIFO可以配到64字节,且寄存器模型和PC上的16550兼容,驱动生态更成熟。但代价是IP配置项更多、误配概率更高。
6.3 哪些场景该换方案:高速收发、RS485需要DIR、多串口
根据我的实际项目经验,下面这些场景建议慎用甚至不用AXI Uartlite:
- 需要持续高速收发大量数据(比如1Mbps以上连续传输),FIFO深度不够,Uartlite的轮询和中断模式都容易忙不过来。
- 需要RS485自动方向控制,Uartlite没有专门的RTS信号且不带方向切换逻辑,需要外加GPIO控制收发芯片的DE/RE引脚,时序上还得小心处理,不然方向切换瞬间会丢字节。
- 需要多个串口而且每个串口都有较高吞吐,此时每个Uartlite都会占用一条PL中断线,中断通道不够用不说,CPU的中断处理压力也会比较大。
如果遇到这些情况,我的建议是:高速透传用AXI DMA配合FIFO加深的Uartlite,或者直接换PL里更完整的UART控制器;RS485场景用GPIO模拟方向控制,同时在驱动里增加发送完成等待;多串口场景优先保证每个串口的中断独立,避免共享中断时无法快速定位来源。
6.4 我的选型建议
在我自己的板卡设计里,AXI Uartlite用得最多的地方是调试串口、低速配置通道、以及需要把串口分布到PCB不同位置的场景。这类应用波特率通常不超过115200,数据量小,对延迟不敏感,Uartlite的轻量特性反而是优点:资源占用小、逻辑简单、不容易出错。
如果项目一开始就预期有高吞吐或复杂串口协议,我不会把Uartlite作为首选。ZYNQ上可选的串口方案其实不少,从PS自带UART、AXI Uartlite、AXI UART 16550到完全自研的UART模块,各有各的适用边界。选型时先把需求里的吞吐量、FIFO要求、流控需求、协议复杂度逐项列出来,再决定用哪一档,比拿到需求就直接拖一个IP进Block Design要靠谱得多。
再分享一个小习惯:每次做完Uartlite设计,我都会把IP配置截图、XDC约束段、设备树节点和驱动文件名一起存档。别小看这个动作,串口这种“不起眼”的外设,往往是最容易在项目后期被反复翻出来调试的部分。有了一份完整的配置快照,再遇到问题就能快速定位,不至于重新翻Vivado工程逐项核对。