FPGA嵌入式实战:AXI Uartlite IP核配置、驱动与调试全攻略
发布时间:2026/10/5 2:57:49 锦皓数字建站

做FPGA嵌入式这几年串口通信接口里用得最多、也最容易出幺蛾子的就是UART。而Xilinx官方提供的AXI Uartlite这款IP核是我在Zynq和MicroBlaze平台里最常挂在总线上用的串口控制器。这篇文章不打算讲多高深的理论就把我从头到尾配置、驱动、调通AXI Uartlite的完整过程以及踩过的那些坑整理成一份可以直接照着做的经验记录。无论是刚接触Vivado的新手还是被串口调试折磨过的老手应该都能从这里找到点有用的东西。1. 为什么我最终选择了AXI Uartlite这个IP核1.1 它到底解决什么问题做FPGA项目时尤其用Zynq或MicroBlaze搭建片上系统UART几乎是一个躲不开的外设。无论是调试打印、引导日志输出还是和外部MCU、传感器、上位机做低速数据交互一个稳定好用的UART控制器都是刚需。Xilinx官方在Vivado里提供了一套现成的串口解决方案AXI Uartlite就是这个家族里的轻量级选手。它在AXI4-Lite总线上暴露出几个简单的寄存器通过读写这些寄存器就能完成波特率设置、数据收发、状态查询和中断控制。相比自己用Verilog手写一套波特率发生器、移位寄存器和FIFOAXI Uartlite的接入成本低得惊人图形界面配置不用写一行RTL代码自动生成AXI接口适配AXI4-Lite总线读写自带接收FIFO和发送FIFO数据缓冲不用自己操心官方SDK驱动成熟上电后调用库函数即可完成收发对于工程师来说选择这个IP核的原始动机通常很朴素省时间。串口本身技术含量不算高但细节很多用手写RTL实现一个兼容性好、波特率误差小、能扛住总线持续读写的外设至少得折腾几天。直接用官方IP核把时间留给业务逻辑是性价比最高的方案。1.2 与UART16550、自定义UART相比它有哪些取舍Vivado里除了AXI Uartlite还提供了AXI UART16550。两者很容易混淆我刚开始也纠结过很久。16550是PC时代就存在的经典串口控制器寄存器布局和PC标准兼容FIFO深度可以配得更大而且可以通过DMA方式搬运数据适合高吞吐场景。Uartlite则更轻FIFO通常只有16字节不支持DMA但占用逻辑资源小驱动简单用在调试输出和低速通信上绰绰有余。我个人的选型经验是这样的只是打印日志、和外部低速设备通信选AXI Uartlite需要兼容PC软件、使用标准COM口寄存器、或者通信数据量较大选UART16550如果系统里连AXI总线都没有只是一个纯FPGA逻辑模块那不如直接在Verilog里调用一个开源UART模块省去总线开销这里要明确一点AXI Uartlite不是万能的。它的名字里带着“lite”定位就是轻量。不要指望它做大数据量的透传也不要用它去对接需要高实时性的协议。选型之前先搞清楚需求边界后面才不会返工。2. Vivado中的IP配置与硬件搭建2.1 创建并配置AXI Uartlite IP打开Vivado在IP Catalog里搜索“Uartlite”就能看到AXI Uartlite的条目。双击进入配置界面关键参数不算多但每一个都有讲究。先看“C_S_AXI_ACLK_FREQ_HZ”这个参数是AXI总线时钟频率必须填准确。这个值会被IP核用来生成波特率分频填错直接导致串口乱码。我遇到过不止一次有人把100MHz填成50MHz实际波特率直接翻倍收出来的数据全是错位的。再看“C_BAUDRATE”这是目标波特率常见的有9600、115200、460800。这个IP核的波特率生成基于整数分频通常只能设置标准波特率。如果要跑非标准波特率一定要先确认实际分频误差是否在可接受范围内。UART通信双方允许的波特率误差一般在±2%以内超过这个范围就容易出误码。“C_DATA_BITS”可配置5到8位一般选8。“C_PARITY”可选无校验、奇校验或偶校验必须和对端设备保持一致。“C_USE_PARITY”如果不勾选校验相关状态位不会生效。“C_ODD_PARITY”则决定奇校验还是偶校验。这些参数组合起来基本能覆盖绝大多数串口设备的协议要求。配置完成后点击OKIP核会在Block Design里生成一个带AXI4-Lite从接口、UART收发引脚和中断引脚的模块。它的引脚不算多核心就四个s_axi_aclk、s_axi_aresetn、uart_txd、uart_rxd外加一个interrupt输出。这种简洁的接口即使第一次用也能很快上手。2.2 在Block Design里完成总线与中断连接将IP核加入到Block Design后需要给它接上AXI总线。在Zynq平台上一般挂在Processing System的M_AXI_GP0或GP1上在MicroBlaze平台上通过AXI Interconnect连接。连接时有一个细节值得注意AXI Uartlite的时钟域和复位域必须和AXI Interconnect保持一致。如果你用了Clocking Wizard给不同模块提供不同频率的时钟务必确认Uartlite的s_axi_aclk和总线的时钟是同一个否则跨时钟域会出现偶发读写失败。另一个关键点是中断引脚。AXI Uartlite有一个interrupt输出当接收FIFO中的数据量达到触发深度或者发送FIFO空时会拉高中断。在Zynq里这个引脚要接到GIC的PL到PS中断线上例如IRQ0或IRQ1。在MicroBlaze里则通常先接在Concat IP上聚合后再通向中断控制器。这一步是我最常看到新手出错的地方中断引脚悬空。结果就是轮询能收到数据中断模式下却永远进不了中断处理函数。别问我怎么知道的排查过太多次了。硬件上接好中断线后还要在PS侧配置里勾选对应的中断通道Vivado生成的平台文件才会把中断ID暴露给SDK。2.3 时钟与波特率的计算关系很多文章会把UART通信的波特率配置说成是“软件里写个寄存器就行”但AXI Uartlite的波特率来源不是这样。波特率的基准来自AXI总线时钟S_AXI_ACLKIP内部用下面的关系做分频实际波特率 S_AXI_ACLK频率 / 分频系数这里的“分频系数”是IP核根据C_BAUDRATE和总线时钟频率自动计算并固化的并不会在软件运行期被动态修改。也就是说如果你在Block Design里填的时钟频率和实际运行的时钟频率不一致串口就会以错误的速率工作。举个例子总线时钟配置成50MHz目标波特率115200实际分频系数就是434换算出来的实际波特率约115207误差很小。但如果总线时钟实际是100MHz配置却填了50MHz实际波特率就会变成约230400直接翻倍双方必然乱码。有些情况下你可以通过驱动里的XUartLite_SetBaud函数在软件运行期重新设置波特率但前提条件是IP核内部的时基已经由硬件配置正确。换句话说软件可以改目标值但硬件时钟基准必须对。为了防止这类低级问题我后来养成了一个习惯生成比特流之前花10秒钟确认Block Design里时钟频率和IP配置页面的总线时钟频率一致。看似浪费时间实际能省下几个小时的调试时间。2.4 用ILA观测AXI读写时该看什么调试Uartlite过程中如果怀疑是AXI总线层面的问题我会上ILA核挂到总线接口上。很多人问ILA的采样频率是不是有范围限制确实有。ILA的采样时钟一般直接用s_axi_aclk采样深度根据实际需要设置数据位宽则根据要观测的信号数量决定。采样时钟频率太高或采样深度太大都会显著增加资源占用严重时甚至导致布局布线失败。观测AXI4-Lite读写时重点看三个信号AWVALID和AWREADY的握手是否完成、WVALID和WREADY握手是否完成、BVALID和BREADY响应是否正常。AXI协议里的valid和ready必须是同时有效才叫一次握手只有valid拉高但ready一直为低说明从设备还在忙这就是所谓的背压。Uartlite作为从设备它不会主动背压AXI读写事务所以通常一瞬间就能完成握手。如果ILA里看到WREADY长时间为低很可能总线频率或复位有问题。不过说实话如果只是验证Uartlite寄存器读写用SDK的Xil_In32和Xil_Out32直接读写基地址更直观ILA更适合排查复杂总线互连问题。3. 软件端从轮询到中断的完整用法3.1 最小发送/接收程序建立SDK或者Vitis工程后BSP里会自动生成uartlite的驱动文件。以Zynq平台为例分配好基地址后在BSP设置里可以看到UartLite对应的驱动是uartlite版本号一般与IP核配套。API非常简单。初始化部分通常是这样#include xuartlite.h #include xuartlite_l.h XUartLite UartInst; XUartLite_Config *UartConfig; UartConfig XUartLite_LookupConfig(XPAR_UARTLITE_0_DEVICE_ID); XUartLite_CfgInitialize(UartInst, UartConfig, UartConfig-RegBaseAddr); XUartLite_ResetFifos(UartInst); XUartLite_SetBaud(UartInst, 115200);发送数据时调用int sent XUartLite_Send(UartInst, buffer, length);这个函数是阻塞发送会一直等到所有字节写入发送FIFO。因为Uartlite的发送FIFO只有16字节如果要发送超过这个容量的数据它会自动分批写入。接收数据时int received XUartLite_Recv(UartInst, buffer, length);注意这个接收函数不会阻塞。如果FIFO里数据不足它最多返回当前FIFO里已有的字节数。所以实际应用里经常会看到一个循环轮询接收的模式while (1) { received XUartLite_Recv(UartInst, recv_buf, sizeof(recv_buf)); if (received 0) { // 处理接收到的数据 } }但轮询有个明显问题CPU会被一直占用而且在有更高优先级任务时接收时机不可控容易丢数据。这时候就该考虑中断模式了。3.2 中断模式配置步骤在Zynq上启用Uartlite中断步骤比较固定我把过程拆开来说。第一步硬件确认IP核的interrupt引脚接到PS的中断控制器。在Vivado里双击Zynq Processing System展开Interrupts配置勾选PL-PS Interrupts中的IRQ0或IRQ1然后把Uartlite中断引脚连上去。硬件重新生成并导出到SDK后BSP里会自动生成XPAR_FABRIC_UARTLITE_0_INTR_INTR这样的中断ID宏。第二步软件里初始化并安装中断处理函数#include xscugic.h static XScuGic Intc; static XUartLite UartInst; void UartIntrHandler(void *CallBackRef) { XUartLite *UartPtr (XUartLite *)CallBackRef; u8 buffer[64]; int received XUartLite_Recv(UartPtr, buffer, sizeof(buffer)); // 这里处理接收到的数据 } void setup_uart_intr(void) { XScuGic_Config *IntcConfig; IntcConfig XScuGic_LookupConfig(XPAR_SCUGIC_SINGLE_DEVICE_ID); XScuGic_CfgInitialize(Intc, IntcConfig, IntcConfig-CpuBaseAddress); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, Intc); Xil_ExceptionEnable(); XUartLite_Config *UartConfig XUartLite_LookupConfig(XPAR_UARTLITE_0_DEVICE_ID); XUartLite_CfgInitialize(UartInst, UartConfig, UartConfig-RegBaseAddr); XUartLite_ResetFifos(UartInst); XScuGic_Connect(Intc, XPAR_FABRIC_UARTLITE_0_INTR_INTR, (Xil_ExceptionHandler)UartIntrHandler, UartInst); XScuGic_Enable(Intc, XPAR_FABRIC_UARTLITE_0_INTR_INTR); XUartLite_SetInterruptHandler(UartInst, UartIntrHandler, UartInst); XUartLite_EnableInterrupt(UartInst, XUARTLITE_IXR_RX_FIFO_TRIGGER_MASK); }第三步在中断处理函数中一般只需要处理接收中断发送可以继续用轮询方式。因为发送本来就是主动行为中断接收反而会加大代码复杂度。这个过程中有个常见的疑问到底是进InterruptHandler后只读一个字节还是读完整个FIFO。我的建议是读完整个FIFO虽然Uartlite的接收触发深度是1字节但在高波特率下突发数据很容易连续涌入中断处理函数里多读几次不会增加多少开销反而避免反复进出中断的上下文损耗。3.3 使用SDK驱动需要注意的坑官方驱动的代码质量没问题但接口行为和很多人预期不一样。我总结几个长期实践中遇到的规律。第一个坑XUartLite_Send是阻塞且可能长时间占用CPU。它本质上是循环写发送FIFO如果发送FIFO满它就在软件里等待TX FIFO空标志。如果在中断处理函数里调用发送大块数据很容易造成中断处理时间过长严重时触发看门狗。我一般把发送数据都放到主循环或独立任务里做。第二个坑默认情况下Uartlite不把帧错误和校验错误当“异常”处理。状态寄存器里虽然能看到错误标志但如果没有主动去读中断状态寄存器错误是静默的。我在实际项目中踩过一次外部设备波特率从115200变成了9600导致大量帧错误但系统没有任何提示数据全是0x00和0xFF交替出现排查了很久才发现是外部设备配置变了。第三个坑中断触发深度固定为1。也就是说只要FIFO里有一个字节就会触发接收中断。这本来是好事但如果对端一次性发几百个字节CPU会被多次中断占用。应对办法就是上面提到的收到中断后把FIFO里的数据全部读完而不是只读一个字节就走。还有一个和平台相关的点BSP里的UartLite驱动在Zynq和MicroBlaze上行为略有差异。Zynq上如果同时存在PS自带的UART和PL里的Uartlite要区分清楚XPAR_UARTLITE_0_DEVICE_ID和XPAR_PS7_UART_0_DEVICE_ID这两个不是同一个设备。很多人把printf重定向到Uartlite时用错设备ID怎么调都不通。4. 调试实录乱码、丢字节、中断不进的排查过程4.1 回环测试为什么是第一步每次配好一个新的Uartlite实例我第一件事就是做回环验证不直接连外部设备。在软件里把发送引脚和接收引脚短接或者在FPGA内部把TX输出接到RX输入然后用一个已知字符串做发送判断能不能原样收回来。这里有个细节如果是FPGA内部回环要在顶层把uart_tx_out和uart_rx_in直接连线不经过外部引脚约束这样能避免杜邦线接触不良或噪声干扰带来的假故障。回环测试跑通的标志是发送和接收的字节完全一致且没有帧错误标志。这一步过了说明硬件配置、时钟、寄存器地址、软件驱动基础都正确。接下来再接外部设备如果还有问题就能把排查范围缩小到电平匹配或对端设备配置上。我自己的回环测试代码非常简单char test_buf[] Hello Uartlite \r\n; char recv_buf[32] {0}; XUartLite_Send(UartInst, (u8 *)test_buf, strlen(test_buf)); usleep(10000); int n XUartLite_Recv(UartInst, (u8 *)recv_buf, 31); if (n 0 memcmp(test_buf, recv_buf, n) 0) { xil_printf(Loopback test passed.\r\n); } else { xil_printf(Loopback test failed: received %d bytes.\r\n, n); }这里要注意usleep的时间要给发送FIFO和接收链路足够的时间。对于115200波特率1个字节大约86微秒10毫秒的延时足够覆盖16字节FIFO的收发过程。4.2 常见故障速查表与寄存器快照技巧我把这些年遇到的Uartlite故障整理成了一张表方便大家对照排查。现象可能原因排查思路完全无输出发送FIFO未复位或使能位未置1检查控制寄存器复位位确认使能位已置1输出乱码波特率分频不匹配核对Block Design时钟频率和IP核配置能收不能发引脚约束错误或发送FIFO一直满检查TX引脚IO标准、电平、位置中断不触发中断线未连接或中断ID错误检查IP连接和XScuGic_Connect参数间歇性丢字节触发深度过大或轮询不及时改用中断接收读完整个FIFO偶发帧错误对端波特率漂移或线路干扰用示波器看波形测量实际波特率接收全FFRX引脚浮空或对端未使能检查外部设备供电和TX引脚输出排查工具方面可以借助ILA挂到AXI总线上看总线的读写时序是否正常。但说实话如果只是验证Uartlite寄存器读写用Xil_In32和Xil_Out32直接读写基地址更直观。我分享一个自己常用的寄存器快照函数void uart_dump_regs(UINTPTR base_addr) { u32 sr Xil_In32(base_addr 0x08); u32 cr Xil_In32(base_addr 0x0C); u32 isr Xil_In32(base_addr 0x10); xil_printf(SR0x%08X, CR0x%08X, ISR0x%08X\r\n, sr, cr, isr); // SR bit0: RX FIFO有数据 bit1: RX FIFO满 // SR bit2: TX FIFO空 bit3: TX FIFO满 // SR bit9: 帧错误 bit10: 校验错误 }通过寄存器快照能快速判断是发送侧阻塞还是接收侧异常比反复用串口助手试要快得多。比如ISR里出现0x00000100就代表帧错误标志被置位这时候不用怀疑代码逻辑先去查波特率和对端设备配置。4.3 中断不进的深度排查中断不触发是大家问得最多的问题我再单独多写几句。首先确认硬件连接正确然后用测试代码检查GIC是否已经工作。可以先注册一个空的定时器中断跑通再验证Uartlite。这种方法能帮助区分是GIC初始化问题还是Uartlite中断配置问题。其次查看XPAR_FABRIC_UARTLITE_0_INTR_INTR这个宏的值它应该对应你在Vivado里连接的中断ID通常是61或者62对应IRQ0或IRQ1。如果宏的值为-1说明BSP没有正确识别硬件连接需要清理BSP重新生成。最后确认XUartLite_EnableInterrupt里的中断屏蔽字是否正确。驱动里接收中断对应的掩码是XUARTLITE_IXR_RX_FIFO_TRIGGER_MASK这个值通常等于0x10。如果你误用了其他宏中断自然不会触发。5. 工程化选型与个人心得5.1 什么时候该换UART16550或上DMA项目里如果有大量数据要通过串口传输比如几十KB级别的固件升级包用16字节FIFO的Uartlite会非常吃力。CPU要不停循环发送期间无法响应其他任务很容易触发看门狗。这种场景下我通常建议换成AXI UART16550并配合AXI DMA使用。16550的寄存器接口兼容PC标准很多上位机软件直接按COM口方式访问兼容性更好。FIFO深度也可以配置到更大的值配合中断和DMACPU负载能大幅下降。另外有一个正面的经验是如果数据量不大但延迟要求高Uartlite的轮询模式反而比中断模式更稳定因为省去了中断上下文切换的时间。中断适合突发和不定长数据轮询适合固定周期小数据量。这个选择要和项目需求挂钩。5.2 个人积累的几个使用心得最后分享几个长期实践中总结的小技巧。第一个BSP里默认的stdout打印如果接在Uartlite上printf会变得非常慢。原因很简单库函数对Uartlite发送是逐字节等待如果波特率是9600每秒才约960字节调试打印会拖慢整个系统。我一般把主调试打印放在PS自带的UART上Uartlite只用于特定外设通信。第二个多核平台要注意中断亲核性。Zynq如果跑双核AMPUartlite中断默认是CPU0的GIC如果CPU1的任务想用这个串口需要处理跨核中断或共享内存否则会很难调试。第三个也是我最想强调的一点文档要细读。Xilinx给AXI Uartlite配套的PG142文档虽然页数不多但寄存器表是最权威的参考。很多人遇到问题时喜欢网上搜但网上的结论很多是别人项目里的特例反而容易带偏方向。遇到Uartlite行为不对先打开PG142把状态寄存器和控制寄存器的每个位对照一遍80%的问题都能自己定位。第四个关于工程目录管理一个常用实践是把Vivado工程和SDK工程分开放SDK工程放到独立目录这样Vivado升级后SDK工程还可以复用。如果遇到Vivado工程无法生成比特流优先检查是否有IP核没有升级到当前版本AXI Uartlite这种老牌IP在版本升级时经常会提示需要升级ip的版本。我在很多项目里的体会是AXI Uartlite不是一个需要研究很深的技术点但它系统里最基础、最不可缺的一环。把这个IP用好能省下大量调试时间。如果你正准备在新的FPGA项目里加入串口通信建议直接从这篇文章里的步骤开始做先跑通回环再扩展中断、接外部设备然后再根据具体需求调整。串口这东西配置一次、调通一次后面就是十几年都不会忘的技能。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。