
简介针对ZYNQ平台PS端仅两个串口、其中一个用作控制台的困境这套资料给出基于PL逻辑扩展串口的完整实现方案。工程基于Vivado/Vitis 2020采用AXI UARTLITE IP核为PS增加串口并搭配AXI TIMER实现类似串口空闲中断的检测机制该思路在FPGA、单片机及软件开发中均可复用。压缩包共2000个文件以C/H驱动源码、Vivado工程配置、VHDL/Verilog硬件描述及.bd/.xsa等工程文件为主整体约39.26MB便于直接查看或移植。目前已有831人学习下载适合有一定ZYNQ基础、希望快速扩充串口资源并优化中断处理的开发者。资源内包含Vivado配置步骤、uartlite与timer代码示例以及空闲中断的实现逻辑可帮助读者缩短工程搭建时间并理解AXI外设集成方法。 几周前调一块Zynq板子PS端的UART1被调试终端占着另一路UART0又给了某个外设模块系统日志和业务数据抢同一个串口调起来别提多别扭。后来干脆在PL侧加了一个AXI UART Lite IP核用AXI总线给PS扩展出第三路串口问题一下解决了。这个方案在Zynq开发里其实很常用但不少刚接触的人会在Block Design连线、中断配置、Linux设备树这些环节卡住我把自己完整趟通的过程和踩过的坑整理出来希望能帮你少走弯路。1. 为什么不用PS自带UART非要到PL侧扩展先聊清楚需求。Zynq-7000系列的PS端一般内置两个UART控制器硬件上确实只有这么多。实际项目里这两个串口往往早就分配好了——一个接调试终端看log一个接RS485总线或者蓝牙模块等做到后面想再加一个GPS模块或者第二路调试口硬件上已经没得选了。有人会说那我换个引脚复用的方式把UART从MIO挪到EMIO行不行这个思路有点绕。PS的UART控制器本身还是那两路EMIO只是把引脚引到PL侧去做电平转换控制器数量不会变多照样不够用。也有人想过外接USB转串口的芯片但那占的是一路UART本质还是没扩容。AXI UART Lite的思路完全不同。它在PL侧例化一个独立的UART控制器IP通过AXI4-Lite总线挂到PS的GP口上PS端用内存映射的方式读写寄存器就能控制收发。也就是说每加一个IP核就多一路独立串口想扩几路扩几路只要PL资源够用就行。它和PS自带UART的区别在于前者是挂在AXI总线上的外设地址空间在0x40000000以上后者是PS内部硬核地址在0xE0000000附近逻辑上互不干扰。选型上还有个小分支Xilinx提供两种串口IP一个是AXI UART Lite一个是AXI UART 16550。前者寄存器少、逻辑简单适合做基础收发后者兼容PC标准16550带FIFO深一些适合高吞吐场景。扩展常规串口用Lite版就够了资源占用更小代码也更直观。2. Block Design里搭建AXI UART Lite的完整过程2.1 添加IP核并配置参数在Vivado里打开Block Design点击加号搜AXI UART Lite双击添加。配置界面里有两个关键参数需要提前想清楚一个是波特率默认9600实际项目里我一般改成115200这个要和后续接的外设保持一致改完Vivado会自动根据输入时钟算出分频系数另一个是FIFO深度默认16字节收发普通AT指令和日志完全够用如果怕突发数据丢失可以选64。有一个细节容易被忽略IP核名字默认叫axi_uartlite_0后面Linux设备树里会用到这个名字对应的地址最好在开始就改成有含义的名字比如uart_rs485省得后面翻地址映射表的时候对不上号。2.2 AXI接口和时钟的连接双击Zynq PS核打开配置界面在PS-PL Configuration页面里把S AXI GP0 interface勾上这是PS访问PL外设的通用AXI从接口。如果你的扩展串口数据量不大GP0够用不用去碰HP口。回到Block Design运行自动连接Vivado会自动把AXI UART Lite的S_AXI接到GP0上也会帮忙连好时钟和复位。这个自动连接有一个坑它连的时钟可能是默认的FCLK_CLK0如果你的设计里同时存在多个时钟域需要手动检查S_AXI_ACLK到底挂在哪个时钟上。这里要特别强调一个我亲手踩过的错误。某次改设计我把FCLK_CLK0从100MHz改成了50MHz结果Block Design里AXI UART Lite的S_AXI_ACLK还连着一根改名前遗留的时钟信号Validation时报了这样一个错误[BD 41-968] AXI interface port /axi_uartlite_0/S_AXI is not associated to any clock报错信息虽然直接但一开始我没看明白以为IP核和PS之间的连接断了重新跑了好几遍自动连接都没解决。后来才发现是时钟源改了名字旧时钟信号悬空AXI接口没关联到有效时钟。把S_AXI_ACLK重新连到新的FCLK_CLK0上问题消失。遇到这个错误码先检查S_AXI_ACLK连接比重连AXI接口有效得多。2.3 地址分配和中断连接自动连接完成后Address Editor里会多出一段地址映射默认可能是0x44A00000这一段就是PS访问这个UART的基地址。如果还有其他PL外设注意地址不要重叠Vivado一般会自动分配但Interconnect多了以后偶尔会分配出同一个地址段建议看一眼确认。中断连接是另外一个关键点。AXI UART Lite的中断输出是高电平有效、电平触发这一点和很多PL IP的中断类型不太一样。在Block Design里把interrupt输出接到PS的IRQ_F2P端口上然后在Zynq PS配置里勾选对应中断ID。PS端中断号是从61开始的私有时钟但那是另一个话题在Block Design里只需要保证连线正确即可。全部连完后跑Validation通过后Generate Output Products再Create HDL Wrapper然后综合、实现、生成bitstream。这个过程比较常规重点就是Validation要过尤其是那个41-968错误不要留到后面。3. 裸机驱动验证轮询和中断两种方式bitstream生成后导出硬件到SDK。新建一个Application Project用空模板。扩展串口的寄存器映射和PS自带UART完全不同我们需要直接操作AXI UART Lite的寄存器来验证功能。3.1 寄存器和初始化AXI UART Lite的寄存器偏移量是0x00到0x1C一共8个但真正日常用到的就几个偏移名称作用0x00RBR接收缓冲寄存器读操作从这里拿数据0x04THR发送保持寄存器写操作往这里丢数据0x08IER中断使能寄存器0x0CISR中断状态寄存器读中断源0x10FCRFIFO控制寄存器复位FIFO用0x14LSR线状态寄存器判断收发状态初始化很简单往FCR写0x07复位收发FIFO往IER写0就可以先关闭中断用轮询方式验证收发。Xilinx官方驱动XUartLite_Initialize内部干的就是这些事如果你不想直接搬驱动手写这几行寄存器操作也能跑通。3.2 轮询收发一小段#include xil_io.h #include xparameters.h #define UARTLITE_BASEADDR XPAR_AXI_UARTLITE_0_BASEADDR #define UARTLITE_RBR 0x00 #define UARTLITE_THR 0x04 #define UARTLITE_LSR 0x14 #define LSR_TX_FIFO_EMPTY 0x20 #define LSR_RX_FIFO_VALID 0x01 void uartlite_send_char(char c) { while ((Xil_In32(UARTLITE_BASEADDR UARTLITE_LSR) LSR_TX_FIFO_EMPTY) 0); Xil_Out32(UARTLITE_BASEADDR UARTLITE_THR, c); } char uartlite_recv_char(void) { while ((Xil_In32(UARTLITE_BASEADDR UARTLITE_LSR) LSR_RX_FIFO_VALID) 0); return (char)(Xil_In32(UARTLITE_BASEADDR UARTLITE_RBR) 0xFF); } int main(void) { char buf[] AXI UART Lite Test\r\n; int i; for (i 0; i sizeof(buf) - 1; i) { uartlite_send_char(buf[i]); } while (1) { uartlite_send_char(uartlite_recv_char()); } return 0; }这段代码的逻辑不复杂发送前查LSR的bit5接收前查LSR的bit0。XPAR_AXI_UARTLITE_0_BASEADDR这个宏在xparameters.h里自动生成不用手动填地址。需要注意Xil_In32读RBR和THR时虽然寄存器只有8位有效但地址是32位对齐的返回整型后手动掩掉高24位更稳妥。3.3 中断方式收发轮询方式在验证时很好用但生产环境里接收方向必须用中断否则CPU会一直死等在接收寄存器上啥也干不了。AXI UART Lite接到PS的IRQ_F2P后中断控制器用GIC。SDK里的初始化流程分三步先注册中断处理函数再使能PS端GIC中断最后使能UART自身中断。#include xscugic.h #include xuartlite.h #include xparameters.h static XUartLite UartLite; static XScuGic Intc; void uart_handler(void *CallBackRef, u32 Event, unsigned int ByteCount) { if (Event XUARTLITE_RECV_FIFO) { u8 buf[16]; u32 len XUartLite_Recv(UartLite, buf, ByteCount); XUartLite_Send(UartLite, buf, len); } }这个回调里没有处理FIFO被读空之后寄存器状态恢复的细节实际项目中需要根据XUARTLITE_RECV_FIFO事件里返回的ByteCount做循环接收因为一次中断可能有多字节到达。另一个经验是回调函数里不要做耗时操作直接把数据搬到环形缓冲区主循环再统一处理否则高波特率下容易丢字节。裸机验证通过后起码说明硬件链路是通的PS能通过AXI总线访问PL侧的UART寄存器收发功能正常。接下来才是把这个串口接到Linux里用起来。4. Linux下把扩展串口变成ttyUL4.1 设备树节点的写法Zynq上跑LinuxPL外设的使用依赖设备树描述。Xilinx官方设备树生成脚本会把AXI UART Lite自动转换成一个节点通常长这样uartlite_0: serial44A00000 { compatible xlnx,xps-uartlite-1.00.a; reg 0x44A00000 0x10000; interrupts 0 29 4; clock-frequency 100000000; current-speed 115200; };这里几个字段要格外认真核对。reg里的地址要和Vivado Address Editor分配的一致0x10000是地址空间长度一般保持默认。interrupts里三个数字的含义分别是SPI中断Zynq里PL外设基本都是SPI、中断号29、触发类型4高电平触发。**这个中断号29不是随便猜的它和Block Design里IRQ_F2P的引脚编号有关需要在Vivado的Address Editor配合中断映射表算出来或者从Xilinx设备树生成工具的输出里直接抄。**实际项目里我看到很多人栽在中断号对不上导致串口注册成功但永远收不到数据。clock-frequency必须和S_AXI_ACLK的实际频率一致比如连的FCLK_CLK0是100MHz就写100000000。这个值直接影响波特率分频写错最典型的症状是收发的字符全部乱码。current-speed是驱动用来设置初始波特率用的和Vivado里配置的保持一致就行。4.2 内核驱动的匹配Linux内核里对应AXI UART Lite的驱动是drivers/tty/serial/uartlite.c它识别设备的compatible字段是xlnx,xps-uartlite-1.00.a。只要设备树节点存在且地址能访问驱动加载后就会注册成一个串口设备设备名是ttyULn。第一路扩展串口对应ttyUL0第二路是ttyUL1以此类推和PS自带串口的ttyPS0、ttyPS1区分开。在文件系统里查一下ls /dev/ttyUL*能看到ttyUL0说明驱动绑定成功。用echo和cat做回环测试echo hello uartlite /dev/ttyUL0 cat /dev/ttyUL0如果没有接外部设备做回环可以把TX和RX短接cat进程会显示刚才echo的内容说明收发路径完全打通。4.3 动态加载FPGA时的特殊处理现在Zynq项目里很流行Linux跑起来以后再动态加载bitstream也就是热搜词里那个zynq linux动态加载fpga。这种方式下扩展串口有个坑内核启动时设备树已经描述了serial44A00000节点但那时FPGA还没加载访问0x44A00000这段地址会挂死总线或者读到全F。最佳实践是把PL相关外设的设备和驱动做成模块不在内核启动时自动初始化。或者干脆设备树里先屏蔽这个节点statusdisabled等FPGA加载完成后再通过sysfs把节点改成okay。不过这个交互比较麻烦更常见的做法是把FPGA加载放到uboot阶段完成这样内核起来时硬件已经就绪。如果坚持要在运行时加载也可以用一种简化方案设备树节点保留但让uartlite驱动不自动启动应用层手动打开设备节点时再去确认FPGA加载完成。实测下来这种方案可用但要注意驱动probe时机如果驱动在FPGA加载前probe访问寄存器可能直接导致系统异常。5. 调试中常见的四个问题和排查链路我在调这个扩展串口时前后遇到过四个典型问题按出现的频率排个序分享一下。第一个就是乱码。这个问题的排查链路很明确先量S_AXI_ACLK的实际频率确认和设备树里clock-frequency一致再确认Vivado IP配置里的波特率和实际外设一致最后用示波器量TX引脚的波形算一下一位的时长对不对。三步排查下来90%乱码问题都出在clock-frequency。曾有一个caseFCLK_CLK0被改成125MHz但设备树里还写着100000000怎么调波特率都乱码改完就好了。第二个是收不到数据但发送正常。这个问题在中断模式下面比较隐蔽。先绕开中断用轮询方式读LSR看RX_FIFO有没有数据如果轮询能收到但中断收不到问题基本出在IRQ_F2P中断映射或者GIC配置上。一个常见原因是Block Design里IRQ_F2P的中断号和系统里实际注册的中断号不一致。可以在Linux里查/proc/interrupts看对应中断号有没有统计计数如果中断触发但没计数说明中断没有正确路由到CPU。第三个是设备树里没有串口节点。如果用的是老版本内核或者自定义设备树AXI UART Lite节点可能压根没被编译进去。遇到这种情况确认设备树源文件里有没有serial44A00000这一段没有就手动加。加了以后也要注意同一个地址在设备树里不能出现两处描述否则内核启动时会报地址冲突设备直接注册失败。第四个问题是和PS自带UART的地址混淆。这个主要用于裸机开发。PS的UART地址在0xE0000000段很多参考代码默认操作的是这个地址如果直接套到AXI UART Lite上写的寄存器的确是PS UART的寄存器但硬件上根本没这个功能。用xparameters.h里的宏别名去引用地址别自己写死可以有效避免这种错误。6. 一点使用上的体会AXI UART Lite这个IP在Zynq项目里看起来不起眼但它解决的是硬件资源不足的刚需问题。整套流程走下来最花时间的不是Block Design的连线反而是时钟关联错误和中断号映射这类看起来不起眼的细节。尤其那个41-968错误查一次记住了以后做其他AXI外设扩展遇到类似报错能直接秒杀。如果你只是想在现有板子上快速多一路串口不求速率AXI UART Lite是最轻的方案资源占用小驱动成熟Linux和裸机都有现成支持。如果你的数据量很大比如每秒几十KBLite版的16字节FIFO可能会丢数据那就换成AXI UART 16550配置思路完全一致只是寄存器多了几个。最后再提一个建议不管用哪种扩展方式PCB设计时一定把扩展串口的TX/RX引脚留到板边排针上方便调试时飞线不然等板子贴好才发现没引出来就只能用万用表在BGA背面碰运气了。本文还有配套的精品资源点击获取