
前阵子调一块ZYNQ 7020的板子PS侧的两个UART一个被调试占用另一个被传感器模块抢走想在系统里再加一路串口翻遍原理图发现MIO引脚已经全被安排了。后来把目光放到PL侧用AXI Uartlite这个IP核做了个串口才算是彻底告别串口焦虑。所谓PL串口本质就是在FPGA逻辑里例化一个串口控制器通过AXI总线让PS去读写物理引脚从PL端的任意Bank引出。整个过程不复杂但牵扯到Vivado工程搭建、引脚约束、驱动配置好几层踩过不少坑。这篇就把我从零到一的所有操作记录和排查思路整理出来给正在被ZYNQ串口资源搞得头大的朋友做个参考。如果你手里有块ZYNQ开发板想在PL侧扩展串口或者被PS的MIO复用折磨得不行这篇内容基本可以照抄。不管是裸机跑SDK还是上Linux系统AXI Uartlite都能用它解决的痛点很明确把串口从固定的MIO引脚中解放出来放到任意PL引脚上。1. 为什么选AXI UartlitePS的串口限制与PL串口的解法1.1 PS串口的现实困境ZYNQ-7020的PS侧有两个UART控制器通常对应MIO 14/15和MIO 48/49这组引脚。看起来两个串口够用了但实际项目里往往不够。一个给调试终端一个给通信模块再想接个GPS或者蓝牙模组就完全没地方放。更麻烦的是MIO引脚复用非常复杂同一个引脚可能被SD卡、I2C、SPI、CAN、USB等功能抢占芯片手册里的MIO复用表翻到眼花最后往往只能在外设里取舍。我这次的情况是调试串口占了UART0UART1接了一个需要持续上报数据的传感器。后来客户又说要加一个Debug接口用于远程运维PS这两个UART完全排不开。如果用软件模拟串口bit-bangingCPU占用和实时性都靠不住。用SPI转串口芯片的话又要改动原理图板子已经投出去打样了。这时候把眼光放到PL侧是比较自然的想法FPGA里最不缺的就是逻辑资源例化一个串口IP核几乎不花什么成本。1.2 AXI Uartlite的优势与适用场景AXI Uartlite是Xilinx提供的一个轻量级串口IP核挂在AXI4-Lite总线上PS可以通过寄存器读写来收发数据。和UART16550这类全功能串口IP不同Uartlite去掉了流控、DMA、FIFO扩展这些重量级功能只保留最基本的串口收发能力资源占用极小在ZYNQ里往往只占一百多个LUT查找表单元。它最大的价值在于引脚灵活性。PS的UART只能走固定的MIO引脚而PL串口可以在所有PL引脚里任意指定。只要引脚电平标准匹配信号完整性满足要求你甚至可以把串口放到FPGA的最角落里。板卡布线有问题时可以换个引脚避开不用重新画PCB这在调试阶段特别实用。当然AXI Uartlite也有它的局限性。它不支持硬件流控RTS/CTS信号没有如果需要和带有流控的模块对接得自己在逻辑里实现或者改用其他IP。另外它的波特率是在Vivado配置时固化在IP核里的软件侧没法动态改想要切换波特率必须重新配置和生成比特流。这些限制在选型阶段就应该确认清楚免得做了一半再换方案。1.3 与UART16550、外部转串口芯片的对比我见过不少人在PL侧串口方案里纠结常见的备选还有UART16550 IP核、SPI转串口芯片、USB转串口芯片等。选型的时候重点看几个维度资源占用、功能完整度、引脚灵活性、是否需要额外硬件。方案引脚灵活性资源占用功能特性外部硬件AXI Uartlite任意PL引脚极低无流控波特率固定需要电平转换UART16550任意PL引脚较高支持FIFO和扩展需要电平转换SPI转串口芯片需SPI引脚极低可动态配置但延迟高额外芯片USB转串口芯片需USB引脚无功能完善额外芯片和电路UART16550功能更完整但在ZYNQ环境下它的寄存器接口比较老驱动适配反而麻烦。SPI转串口芯片虽然灵活但通信延迟和吞吐量受SPI时钟限制而且占用了SPI总线。USB转串口芯片还得改硬件涉及USB PHY和时钟源。AXI Uartlite在绝大多数场景下是性价比最高的选择尤其适合嵌入式系统里串口数量不够用的情况。2. Vivado工程搭建与AXI Uartlite配置2.1 新建Block Design与IP例化在Vivado里新建工程选择芯片型号后创建一个Block Design在Diagram窗口里添加ZYNQ7 Processing System IP核。双击PS模块进入配置界面后不需要把所有外设都打开只要保留UART或者MIO相关的约束就够了因为串口这次要跑在PL侧PS的UART都可以先关掉。再添加AXI Uartlite IP核双击打开配置面板。里面参数不多但每个都值得仔细看波特率默认是9600建议根据实际需求设置成115200数据位数可选5到8一般用8位校验位可以不启用但如果是和特定设备通信必须和设备侧的配置保持一致。还有一个是FIFO深度选项默认是16字节可以调大但会多消耗一些BRAM块内存资源。我的建议是串口通信频率本来就不高16字节的FIFO已经够用没必要为了这个浪费BRAM。配置完IP核后Diagram里会多出一个带AXI接口的模块。把PS主口的M_AXI_GP0和Uartlite的S_AXI连接起来时钟也要接上。在ZYNQ里PL侧的时钟一般由PS的FCLK_CLK0提供频率可以设定成100MHz。Uartlite本身没有独立的时钟输入直接用AXI总线的时钟就能工作。2.2 AXI接口连接与地址分配Block Design里连接好AXI总线后Vivado会自动提示运行Connection Automation它会帮你把互联模块和复位模块加好。这个过程看起来是自动的但有几个细节要注意。互联模块会占用额外的LUT和寄存器资源如果只连一个Uartlite可以不用互联直接把PS的M_AXI_GP0接到Uartlite的S_AXI上省掉一层AXI Interconnect。我当时就为了省资源这么干过Bandwidth上完全没压力但如果有多个外设要挂总线上还是老实加上Interconnect。连接完以后要给Uartlite分配地址。在Address Editor里可以看到PS给PL外设分配的地址空间范围。常见的默认基地址是0x42C00000这个地址不是随机的它落在PS可访问PL的地址映射范围内。驱动配置里需要用这个地址来找寄存器后面接Linux设备树时也要保持一致。2.3 引脚约束与硬件设计要点这是整个PL串口方案里最关键也最容易出错的一步。在Block Design里右键Uartlite的uart_tx和uart_rx端口选择Make External它们会变成顶层模块的输入输出。然后就要在XDC约束文件里给它们指定实际的FPGA引脚。约束文件里至少要写两行内容一行是指定引脚位置一行是指定电平标准。举个例子如果把串口放在L15引脚set_property PACKAGE_PIN L15 [get_ports uart_tx] set_property IOSTANDARD LVCMOS33 [get_ports uart_tx] set_property PACKAGE_PIN M15 [get_ports uart_rx] set_property IOSTANDARD LVCMOS33 [get_ports uart_rx]引脚编号要去查看你自己板子原理图。ZYNQ的PL引脚分布在不同Bank里每个Bank的电平标准可以不同3.3V或2.5V等。我的经验是串口这种低速信号对引脚位置没什么敏感度但必须检查目标引脚所在的Bank电压和外部器件电平匹配。比如Bank电压是3.3VIOSTANDARD就写LVCMOS33如果是2.5V就写LVCMOS25。还有一点容易被忽略就是引脚名称必须和Block Design里生成的顶层端口名完全一致。如果你在Make External之后又手动改过端口名比如加了后缀或者统一前缀XDC里的get_ports名称就要对应改。之前就有朋友因为端口名没对上综合后一直报错找不到引脚查了半天才发现是名字里多了一个下划线。2.4 综合、实现与生成比特流约束完成后运行Synthesis然后Implementation最后Generate Bitstream。这个过程本身没什么花活但有一个常见错误值得提如果XDC里忘了加引脚约束Vivado在Implementation阶段会提示有未约束的IO端口然后停下来。解决方案不是直接在所有IO上打补丁而是回到XDC里把引脚补上尤其串口这种要接到外部器件的信号千万不能用虚拟引脚。此外如果你的PS侧配置了DDR、MIO等Vivado会自动生成XDC里的时钟约束和物理约束这些不要去动它。PL串口的约束只添加自己的两个引脚即可两者不会冲突。生成比特流后Export Hardware记得勾选Include bitstream然后Launch SDK或Vitis开始软件侧的工作。3. 驱动配置与软件层实现3.1 SDK裸机下的Uartlite驱动调用在Vitis或SDK中新建应用工程后BSP会自动把Uartlite的驱动包加进来。老版本SDK里驱动函数前缀是XUartLite新版本Vitis里变成了XSysUartlite但核心流程差不多都是初始化、发送、接收三步走。不同版本API名称可能有差异但整体流程一样先初始化Uartlite实例再处理数据。下面是一段典型的初始化流程#include xsysuartlite.h XSysUartlite UartLite; XSysUartlite_Config *UartLiteConfig; int UartLiteInit(void) { UartLiteConfig XSysUartlite_LookupConfig(XPAR_AXI_UARTLITE_0_DEVICE_ID); if (UartLiteConfig NULL) { return XST_FAILURE; } int status XSysUartlite_CfgInitialize(UartLite, UartLiteConfig, UartLiteConfig-RegBaseAddr); if (status ! XST_SUCCESS) { return XST_FAILURE; } return XST_SUCCESS; }初始化之后发送数据只需要调用XSysUartlite_Send接收数据用XSysUartlite_Recv。但在写收发逻辑之前一定要搞懂Uartlite的寄存器状态机。它发送数据时把字节写入发送FIFO即可但写入前必须检查发送FIFO是否满。接收数据时要检查接收FIFO中是否有有效数据否则读出来的可能是垃圾值。对应的状态寄存器位在xuartlite_l.h头文件里有定义分别是TX FIFO Full和RX FIFO Valid。3.2 Linux系统下的设备树配置如果用Linux系统跑ZYNQUartlite的驱动在kernel里对应的是xilinx_uartlite设备树配置不复杂但要写对几个关键属性。首先要给Uartlite分配一个节点compatible属性必须匹配驱动寄存器和中断也要填对。以基地址0x42C00000为例设备树节点可以这样写uartlite0: serial42c00000 { compatible xlnx,xps-uartlite-1.0; reg 0x42c00000 0x10000; interrupts 0 29 4; interrupt-parent intc; clock-frequency 100000000; current-speed 115200; };要点有几个。compatible字符串不要自己发明驱动里匹配的就是xlnx,xps-uartlite-1.0或者xlnx,axi-uartlite-1.0具体要看内核版本。interrupts字段里的中断号不是随便写的需要查ZYNQ的PS中断映射表GPIO中断号要加上SPI偏移一般是28或29。clock-frequency要和硬件实际供给Uartlite的时钟频率一致我这里用的是100MHz。如果这个属性不对波特率算出来会偏表现就是能收到数据但全是乱码。3.3 把Uartlite设为Linux控制台如果想把PL串口作为Linux启动的调试串口还要在设备树里调整aliases和chosen节点。假设你的Uartlite节点标签是uartlite0那么aliases { serial0 uartlite0; }; chosen { stdout-path serial0:115200n8; };这样设置后Linux启动阶段的内核日志会从PL串口输出。但有个前提U-Boot在启动内核之前也要把console指向这个串口否则你只能看到内核启动后半段的日志看不了U-Boot阶段的启动信息。U-Boot环境变量里要设置setenv consolettyUL0,115200 setenv stdoutserial setenv stdinserial saveenv在内核里PL串口对应的设备节点名一般是ttyUL0。注意不是ttyS0ttyS0是标准串口驱动Uartlite是独立的uartlite驱动设备名是ttyUL开头。3.4 Linux驱动框架的简单解读熟悉一点内核驱动的结构对排查问题很有帮助。xilinx_uartlite驱动注册了一个uart_driver核心是uart_ops结构体里的startup、shutdown、start_tx、stop_tx这几个回调。startup函数里会复位Uartlite的FIFO并启用接收中断如果设备树里配了中断start_tx则是把发送缓冲区里的数据逐字节写入发送FIFO。需要注意的一点是Uartlite IP核的发送和接收共用一个状态寄存器部分版本还有Bug例如清中断标志的寄存器位与状态重叠。如果你在内核里看到ttyUL0: FIFO overflow之类的日志多半是硬件中断处理逻辑还有优化空间。实际项目里处理高速数据流时我一般会绕开中断直接在内核线程里轮询FIFO状态反而更稳定。4. 实操中的常见问题与排查实录4.1 回环测试先排除硬件链路硬件和软件都准备好之后别急着接外设先做一个最基础的回环测试。把PL串口的TX引脚和RX引脚用杜邦线短接然后PC端通过USB转串口模块连接PL串口的TX和RX。USB转串口模块的GND必须和ZYNQ板卡的GND共地这是最容易犯的低级错误大多数人第一次调串口没输出就是因为没把地线接在一起。电脑上打开串口调试助手选择对应端口波特率设成115200发送一帧数据。如果回环链路没问题调试助手窗口应该立刻收到相同的数据。如果收不到优先排查USB转串口模块本身是否正常工作可以用模块的TX和RX短接做一次纯硬件回环测试排除模块故障。回环通过后再接上实际的外设模块。外设的TX接FPGA的RX外设的RX接FPGA的TX交叉接线。这时候还出问题就轮到软件层面去查。4.2 典型问题速查表下面是我在实际调试和帮朋友排查时遇到频率比较高的几个问题整理成表格方便对照现象可能原因排查思路PC串口助手无任何数据TX/RX接反、GND未共地、USB转串口模块故障先用杜邦线做硬件回环逐段排除Vivado实现报错缺少引脚约束XDC文件里没有写PACKAGE_PIN约束确认端口名和XDC里的get_ports完全一致波特率正确但全是乱码clock-frequency属性不对或IP核波特率参数不匹配核对设备树里clock-frequency与实际PL时钟Linux下没有ttyUL0设备设备树节点缺失或compatible不匹配确认设备树节点加载成功查看kernel日志SDK发送函数卡死在while循环TX FIFO满但一直在等待FIFO空确认硬件线路连通或减小单次发送数据量能收不能发或能发不能收引脚约束方向错误比如把TX约束到了RX核对原理图和XDC文件检查get_ports方向4.3 引脚“任意”的边界在哪里标题里说PL任意引脚变成串口但这里的“任意”是有边界的。第一个边界是引脚必须存在这个不用说。第二个边界是引脚所在Bank的电平和外部器件必须匹配3.3V器件不能接2.5V电平的引脚。第三个边界是引脚约束还要考虑电流能力串口这种低速信号问题不大但如果外部负载能力差可能会拉低电平导致通信失败。另外ZYNQ的PL引脚并不是所有都可以随便接有一部分引脚可能已经被PS侧的专用功能或者硬核占用了比如MIO引脚和DDR接口引脚。在约束之前打开Vivado的Device视图勾选显示I/O引脚看看目标引脚是否被系统占用。之前我就遇到过一次想把一个空闲引脚强制约束为串口结果实现报告这个引脚已经被DDR控制器占用最后只能换引脚。4.4 多个Uartlite并存时的配置一个Uartlite解决一路串口那想要四路串口怎么办答案很简单例化四个Uartlite IP核每个分配不同的基地址和中断。在Vivado里复制粘贴IP核改一下实例名和Axi地址然后重新布线即可。多路串口并存时要注意地址重叠问题。Vivado的Address Editor会自动分配地址但有时候自动分配的不合理比如两个IP地址范围重叠。建议手动设置让每路串口的地址空间间隔足够大比如0x42C00000、0x42C10000、0x42C20000避免寄存器读写串扰。中断配置是另一个坑。多个Uartlite如果都启用中断要分别映射到PS GIC的不同中断号。ZYNQ的PL中断从68号开始对应SPI中断每个PL外设分配一个中断号。在设备树和BSP里都要对应好。如果中断号冲突会出现一个串口收到数据后另一个串口也跟着触发中断的诡异现象。简单模式下可以让所有Uartlite共用同一个中断号在中断处理函数里判断状态寄存器来区分是哪个串口产生的中断省得要在设备树里每个节点单独配。5. 从调试到量产的一些心得这里分享几个我在实际项目中总结出来的经验和技巧不一定能在芯片手册里找到但能帮你少走不少弯路。串口调试时准备好一个USB转TTL模块和一台带串口调试助手的电脑这是最基础的工具。USB转TTL模块的RX/TX要交叉接ZYNQ的TX/RXGND共地。另外如果板子上有MAX3232之类的RS232电平转换芯片注意FPGA引脚接的是TTL侧还是RS232侧接错位置会出现电平不匹配的情况。在使用Vivado做实现时PL侧加了Uartlite后会消耗少量时钟资源但它和PS的FCLK不在同一个时钟域里。如果Uartlite输入端用的时钟是PS的FCLK那它和PS内部是同步的如果自己另外给了时钟就要考虑跨时钟域的问题。虽然Uartlite内部有同步逻辑但设计中最好保持一致性。说到驱动文件这里再强调一下AXI Uartlite在Linux内核里有标准的驱动不要自己从头写字符设备驱动。标准驱动的好处是稳定、经过社区验证而且Serial核心层会帮你处理波特率设置、termios等细节。如果需要做特殊功能比如自定义流控或特殊数据帧解析先考虑在应用层基于tty设备做二次开发不到万不得已别改内核驱动。如果项目里要用PL串口做高速数据传输Uartlite就不太够了。它的寄存器接口一次只能读写一个字节CPU中断处理和FIFO大小都限制了吞吐量。这时候可以考虑在Uartlite后面再接一个AXI DMA控制器让DMA直接把FIFO里的数据搬到DDR里可以大幅降低CPU负载。但DMA和Uartlite之间的握手逻辑需要仔细调不是简单连连总线就行这部分工作量和收益要提前评估。最后再分享一个测试技巧。串口不出数据时不要急着怀疑FPGA逻辑或者驱动代码先用示波器或者逻辑分析仪看FPGA引脚的TX波形。如果波形正常而PC收不到问题往往在外部电平转换或USB转串口模块如果波形本身就没有问题才在FPGA内部逻辑或软件层。这种从物理层往上层逐段排查的思路比瞎猜代码快得多。