ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AXI Uartlite IP核详解:FPGA串口调试与Zynq嵌入式应用实践

AXI Uartlite IP核详解:FPGA串口调试与Zynq嵌入式应用实践 1. 先搞清楚 Uartlite 是个什么东西早些年我第一次在 Vivado 里看到 AXI Uartlite 这个 IP 核时心里其实挺不屑的——串口嘛自己写个收发模块不就完事了何必绕一圈去挂 AXI 总线后来被项目时间逼着用了一回才发现这东西在 Zynq 和 FPGA 混合系统里简直就是“基础设施”级别的存在。现在凡是遇到需要调试、日志输出、参数配置的场合我第一反应就是把它挂上。AXI Uartlite 是 Xilinx 提供的一个轻量级串口控制器 IP 核对外接口是 AXI4-Lite对内部逻辑则通过简单的发送 FIFO、接收 FIFO 和中断引脚来交互。和完整版的 AXI UART16550 相比它去掉了 modem 控制信号、FIFO 深度也更浅默认 16 字节但好处是逻辑极其简单、资源占用小、时序收敛容易非常适合做调试串口、低速通信、配置通道这类工作。它解决的核心问题其实有两个第一省去了自己写 UART 收发状态机的功夫也避免了不同工程师写的串口模块风格各异、复用困难的问题第二在 PS 和 PL 协同工作的系统里它天然就是 AXI 总线上的一个从设备ARM 端直接操作寄存器就能收发数据不用在 PL 里再堆一组 GPIO 模拟时序的逻辑。这套路适合谁适合用 Zynq、Zynq UltraScale 做嵌入式开发的人也适合纯 FPGA 工程里想快速跑通串口通信、把精力放在业务逻辑上的人。哪怕是刚开始接触 Vivado 的新手照着这篇把模块搭起来再配合 ILA 看一眼波形对 AXI 总线和串口协议的理解都会上一个台阶。2. Vivado 里的关键配置选错一个参数都要返工2.1 创建 IP 和基础参数设置打开 Vivado在 IP Catalog 里搜索 Uartlite双击 AXI Uartlite 进入配置界面。第一个要决定的是Board Interface如果工程是基于官方开发板创建的比如 ZedBoard、Ultra96 这类Vivado 会自动关联板级接口串口的管脚约束都帮你填好了。如果是自己画的板子这里选 Manual 就行后面到约束文件里自己写管脚。接下来是UART 协议参数这部分看似简单但实际工程里最容易翻车Baud Rate默认 9600很多新手不留意就直接用了结果上位机那边忘记改波特率调了半天发现全是乱码。我一般习惯设成 115200这是调试工具默认支持最好的速率之一也有设 921600 的但前提是时钟精度够否则误码率会明显上升。Data Bits默认 8没有特殊需求不要改。Parity通常选 No Parity。选了奇偶校验之后发送和接收的数据格式都得对应而且一旦出现校验错误接收 FIFO 的数据是否继续保留、中断如何触发不同版本的 IP 行为还有细微差别没必要给自己添堵。Stop Bits默认 1 位停位够用。还有一个容易被忽略但很关键的选项Frequency (MHz)。这个参数填的是 Uartlite 模块所挂时钟的实际频率IP 内部靠它计算波特率分频系数。如果填错了后果就是实际波特率偏离设定值低速时可能还能勉强通信速率一高就是乱码。我见过有人把 100MHz 的时钟填成 50MHz表面上 Vivado 不会报错但逻辑仿真和上板测都会出问题。2.2 FIFO 深度和中断配置Uartlite 的 FIFO 深度默认是 16 字节这意味着接收端如果长时间不读数据超过 16 字节之后新来的数据就会被丢弃。有些应用里数据是突发到达的比如一帧几百字节从传感器端发过来接收端 CPU 被其他中断占着没法及时处理这种情况下 16 字节的 FIFO 就显得很紧张。IP 配置界面里有 Receive FIFO depth 和 Transmit FIFO depth 两个参数可选范围是 16 到 64KB但没有必要追求大。FIFO 太大有两个问题一是 BRAM 资源消耗增加二是接收数据的实时性反而变差——数据在 FIFO 里待的时间长了控制回路响应不过来。我看到的大部分实际项目里要么用默认 16要么升级到 64够用就好。中断相关参数里_Enable Interrupts打开之后IP 会输出一个 uart_interrupt 信号接收 FIFO 非空、发送 FIFO 空、接收超时、溢出错误都会触发中断。这里的“接收超时”是指接收 FIFO 非空但一段时间内没有新数据到达通常用来判断一帧数据接收完毕很适合包协议的场景。注意Uartlite 的中断输出是一个带极性选择的单信号不像 UART16550 那样有多个独立中断源。在驱动里必须读中断状态寄存器来区分具体是哪种事件触发了中断。2.3 时钟和复位域的坑Uartlite 模块只有一个时钟输入 s_axi_aclk发送和接收逻辑都跑在这个时钟上所以不存在跨时钟域的问题。但这并不意味着随便接一个时钟就行。第一这个频率就是前面说的 Frequency 参数填的值两者必须一致。第二如果系统里用 MMCM 或 PLL 产生了多个时钟Uartlite 的时钟和其他 AXI 外设的时钟最好来自同一个 MMCM避免异步时钟域之间做 AXI 跨时钟桥接虽然 Vivado 会自动插入同步逻辑但 Protocal 错误的风险会增加。复位信号 s_axi_aresetn 是低有效这点要特别提醒。有人习惯用高有效的复位接到这里忘了取反结果 IP 一直处于复位状态读写寄存器全部返回 0现象非常隐蔽。另外复位信号的释放需要和时钟同步如果你的系统复位是异步复位同步释放的那没问题如果是纯异步复位直接撤掉有概率导致 AXI 从机内部状态机跑飞实测中这种问题偶发且极难排查。3. 硬件连接与地址分配Block Design 里的三件事3.1 连接 AXI 和中断引脚在 Block Design 里使用 Uartlite 很简单从 IP Catalog 拖进来然后做三件事第一把 S_AXI 接口连到 Zynq 的 M_AXI_GP 端口或者连到 AXI Interconnect 的从机接口上。PS 访问 PL 外设一般走 M_AXI_GP带宽不高但连串口绰绰有余。如果纯 PL 工程里要用微处理器软核比如 MicroBlaze那就连到 MicroBlaze 的 M_AXI_DC 或 M_AXI_IC 上或者走 AXI Interconnect 统一管理。第二把 s_axi_aclk 和 s_axi_aresetn 接到系统时钟和复位的网络上。这里有个细节如果 Uartlite 挂在 AXI Interconnect 的某个时钟域下它的时钟必须和 Interconnect 的时钟一致否则 AXI 协议握手时序会乱。第三把 interrupt 引脚接到 PS 的 IRQ 控制器上。Zynq-7000 的 PL 中断通过 IRQ_F2P 端口接入在 Block Design 里会自动生成只需要把 Uartlite 的中断输出连到这个端口上就可以PS 端的中断号在 xparameters.h 里会对应到延展 ID。注意多个外设共用 IRQ_F2P 时需要接一个 concat IP 将多路中断汇聚成一路。3.2 地址分配和地址冲突给 IP 分配地址是 Block Design 里必不可少的一步。右键点击 Uartlite 的地址段选择 Assign AddressVivado 会自动在 AXI 主设备的地址空间里给它划一块区域。我习惯在自动分配后手动改成规整的地址比如 0x42C00000方便记忆也方便在驱动代码里直接硬编码。实际项目里还遇到过一个问题Block Design 里生成了两个一模一样的 Uartlite 实例一个用于调试、一个用于通信地址分配时一个设成 0x42C00000另一个设成 0x42C10000看起来没什么问题但地址总线宽度默认只有 12 位也就是 4KB 空间。如果地址跨越了 4KB 边界访问第二个外设时地址会被 AXI 解码截断导致读写行为异常。这个坑出现的概率不高但一旦出现排查时往往以为是自己驱动的问题实际上就是地址段没对齐。3.3 管脚约束的常见错误Block Design 生成好之后Uartlite 的 UART 引脚uart_txd、uart_rxd需要手动约束到 FPGA 的物理管脚上。这一步本身不复杂但经常有人遇到 Vivado 综合时报错“IO placement failed to meet constraints”其实就是管脚约束和 Bank 电压不匹配或者管脚被其他外设占用了。另外需要注意电平标准。开发板上串口芯片一般是 3.3V 电平约束文件里通常会写成 LVCMOS33。如果板子的串口引脚在 HR Bank可以用 LVCMOS33如果在 HP Bank可能只能用 LVCMOS18 或其他电平标准这取决于板卡设计。最靠谱的方法是直接抄开发板提供的 XDC 文件而不是自己凭经验猜。还有一个容易忽略的点Uartlite 的 uart_txd 是从 FPGA 输出到外部串口芯片的发送数据线uart_rxd 是接收外部串口芯片送过来的数据。有些板子的丝印上写着 RX、TX对应的方向容易搞反接反之后的现象是“能发不能收”或者“能收不能发”上位机里也能看到数据但内容永远不对。我每次布完板或者接完调试线第一件事就是用串口助手发个 0x55二进制 01010101看回显和波形是否正常。4. 软件驱动从轮询到中断两条路怎么选4.1 裸机环境下访问 Uartlite 寄存器Uartlite 的寄存器映射非常简单一共几个 32 位寄存器访问地址基地址加上偏移即可。核心的两个是发送保持寄存器偏移 0x00和接收缓冲寄存器偏移 0x04写发送寄存器启动发送读接收寄存器取回数据。然后是状态寄存器偏移 0x0C几个关键位分别是发送 FIFO 满、发送 FIFO 空、接收 FIFO 空、接收 FIFO 有效数据、溢出错误、帧错误、奇偶校验错误。接收数据的典型流程是先读状态寄存器确认“接收 FIFO 非空”再读接收缓冲寄存器避免读到空数据。用轮询方式发送一串数据时标准写法是while (*pReg ! 0) // 伪代码更严谨的裸机写法其实是#define UARTLITE_HRR 0x00 #define UARTLITE_RBR 0x04 #define UARTLITE_SR 0x0C void uart_send_byte(uint32_t base, uint8_t data) { volatile uint32_t *sr (volatile uint32_t *)(base UARTLITE_SR); volatile uint32_t *hrr (volatile uint32_t *)(base UARTLITE_HRR); while ((*sr 0x08) ! 0) // 发送 FIFO 满则等待 ; *hrr data; } uint8_t uart_recv_byte(uint32_t base) { volatile uint32_t *sr (volatile uint32_t *)(base UARTLITE_SR); volatile uint32_t *rbr (volatile uint32_t *)(base UARTLITE_RBR); while ((*sr 0x01) ! 0x01) // 接收 FIFO 空则等待 ; return (uint8_t)(*rbr); }状态寄存器需要先读后写写 1 清除错误标志位。但这里有个细节状态寄存器的 FIFO 状态位是“读时清零、写时更新”的也就是说如果你在读状态寄存器之后立刻写状态寄存器来清错误标志那么新写入的 1 会把当时 FIFO 的状态覆盖掉。正确做法是先清错误再读数据。4.2 Xilinx 官方库函数XUartLite 的用法如果用 SDK 或 Vitis 开发不用自己操作寄存器直接用 Xilinx 提供的驱动库函数就行。初始化流程是XUartLite UartLiteInstance; XUartLite_Config *UartLiteConfig; UartLiteConfig XUartLite_LookupConfig(XPAR_AXI_UARTLITE_0_DEVICE_ID); XUartLite_CfgInitialize(UartLiteInstance, UartLiteConfig, UartLiteConfig-RegBaseAddr);初始化完成后发送数据用XUartLite_Send接收数据用XUartLite_Recv。这两个函数都支持指定数据长度但实际发送时不会保证一次性发完所有字节需要检查返回值确认实际发送了多少剩余部分要手动补发。如果系统里同时有多个 Uartlite比如一个调试串口、一个通信串口查找配置时一定要确认 Device ID 和实际寄存器基地址匹配。我在 Vitis 里遇到过 Device ID 和 RegBaseAddr 对不上的情况原因是生成的 xparameters.h 是旧工程的没有被正确更新。解决方法是右键 BSP 工程选择 Regenerate BSP Sources然后再重新编译。4.3 中断接收防止丢数据的正确姿势轮询接收的缺点是 CPU 被完全占用而且如果串口数据到达时 CPU 正在执行其他任务数据会被 FIFO 溢出而丢掉。所以实际项目里只要数据不是几字节、而是持续流量我都建议用中断方式。中断接收的流程分四步第一在初始化阶段注册接收回调函数XUartLite_SetRecvHandler(UartLiteInstance, UartLiteRecvHandler, UartLiteInstance);第二前使能接收中断XUartLite_EnableInterrupt(UartLiteInstance);第三在回调函数里处理数据。注意回调运行在中断上下文绝对不能在里面做耗时操作比如 printf 打印、延时、复杂协议处理。正确做法是把数据拷贝到环形缓冲区然后置一个标志位主循环检测到标志位后再去解析。void UartLiteRecvHandler(void *CallBackRef, unsigned int EventData) { XUartLite *InstancePtr (XUartLite *)CallBackRef; uint8_t byte; while (XUartLite_IsReceiveData(InstancePtr-RegBaseAddress)) { byte XUartLite_ReadReg(InstancePtr-RegBaseAddress, XUL_RBR_OFFSET); ring_buffer_push(rx_ring, byte); } }第四发送时如果需要等发送完成可以注册发送回调或者直接在发送循环里检查XUartLite_IsTransmitFull。实测下来发送 FIFO 一旦满了之后继续写数据会直接丢掉并且没有错误标志所以发送侧必须做好流控。提示Uartlite 的接收触发方式默认是“接收 FIFO 非空就触发”所以回调里要循环读直到 FIFO 被读空。如果只读一个字节就退出很快 FIFO 就会溢出然后触发溢出错误中断。4.4 轮询和中断的选择标准这两种方式没有绝对优劣按场景选场景推荐方式原因系统启动时打印日志轮询此时中断系统还没完全初始化轮询最可靠短帧、低频率上报轮询简单不占中断资源持续数据流、大文件传输中断防止 CPU 被占死降低丢数率多外设并发中断避免串口长时间占用 CPU5. 数据通路与调试手段信号正确性如何保证5.1 环路测试的方法IP 配置完、上板能收发之后第一件事是做一个环路测试。所谓环路测试就是让 Uartlite 自己把发送的数据通过外部短路回收到接收端验证链路是否完整。具体做法在上位机串口助手里开启“发送新行”向板子发送一串测试数据。板子端的裸机程序里将接收到的数据原封不动通过同一个 Uartlite 发送回上位机。如果上位机能收到发送内容的完整回显说明 UART 物理链路、IP 配置、驱动收发都正常。如果回显内容有错位、多字节、丢字节就要用 ILA 抓到 IP 内部的 AXI 信号检查状态机跳转和握手时序。开 ILA 时采样深度不用很大1024 就够触发条件可以设置为发送 FIFO 状态变化或者 AXI 写通道上的 WVALID 拉高。5.2 波特率误差的计算与验证波特率误差是串口通信最隐蔽的坑。如果 IP 配置的时钟频率和实际运行频率不一致波特率会偏离标准值。以 115200 波特率为例IP 内部会计算分频系数如果时钟源有 100ppm 的偏差长时间收发后数据就会出错。实际验证波特率的方式有两种。一种是逻辑分析仪或示波器直接看 TX 引脚波形测量单个 bit 的宽度算出实际波特率。另一种是用 ILA 观察接收端采样窗口看采样点是否落在 bit 中心附近。如果采样点在 bit 边界附近跳动说明波特率偏差大需要调整时钟或波特率值。我在调试时遇到过一种情况从 PLL 输出给串口的时钟是 96MHz但配置界面的频率写的却是 100MHz结果串口通信时而正常、时而乱码。当时还以为是上位机问题用示波器一测才发现实际 bit 宽度比标准值宽了 4% 左右。所以说配置界面里 Frequency 的参数值必须和最终进入 IP 的时钟频率完全一致。5.3 AXI-Lite 读写的验证思路如果串口没有数据惯性的怀疑是引脚接错、波特率不对但还有一个可能AXI 总线本身有问题。判断的方法是直接在 SDK 里写一段简单的寄存器读写测试代码往发送寄存器写一个字节然后读状态寄存器看发送 FIFO 空标志是否发生变化。如果写寄存器没有反应、状态寄存器读出来全是零问题大概率在上游 AXI 路径上。此时看这几个地方Block Design 里 Interconnect 的时钟和复位是否连接正确PS 端 M_AXI_GP 的地址空间映射是否覆盖了 Uartlite 的基地址地址线位宽是否足够。这些都是我在实际项目里排查过的问题任何一个隐蔽的接线问题最终都需要靠“从寄存器层面逐级往上查”的方式定位千万不要直接跳过 AXI 验证去怀疑物理层。6. Linux 环境下使用 Uartlite 的补充6.1 设备树节点在嵌入式 Linux 系统里使用 Uartlite需要在设备树里增加节点。以常见的基于 device tree 启动的内核为例节点写法如下axi_uartlite_0: serial42C00000 { compatible xlnx,xps-uartlite-1.00.a; reg 0x42C00000 0x10000; interrupts 0 29 4; // 中断号需根据实际连接修改 interrupt-parent intc; clock-frequency 100000000; };注意 compatible 字符串在不同版本内核里有差异老内核是 xlnx,opb-uartlite-1.00.b新内核是 xlnx,xps-uartlite-1.00.a。如果 compatible 不匹配内核驱动不会绑定这个设备表现为 /dev/ttyUL0 设备节点不存在。6.2 内核配置与终端获取需要在 kernel config 里打开 UARTLITE 驱动支持一般是CONFIG_SERIAL_UARTLITEy。打开之后启动过程中会有多个串口设备需要确认哪个是 Uartlite 对应的 ttyUL 节点。如果要把这个串口作为默认控制台在 bootargs 里指定consolettyUL0,115200这里有个容易忽略的点ttyUL 的波特率初始值由设备树里的 clock-frequency 和波特率共同决定。设备树里的 clock-frequency 必须和 Uartlite IP 配置界面里填的值一致否则内核驱动的波特率计算会出错开机后控制台乱码。7. 常见问题与排查技巧实录7.1 问题速查表现象可能原因排查方向完全无数据TX 引脚无波形引脚约束错误、时钟未接、复位一直有效检查 XDC、检查时钟复位连接数据乱码波特率不匹配、Frequency 配置错误示波器测 bit 宽度核对时钟频率只能发不能收RX 引脚约束错误、外部芯片方向接反检查引脚方向检查电路图接收丢字节FIFO 溢出、中断处理不及时增大 FIFO 深度、优化中断处理写寄存器无反应AXI 地址映射错误、IP 未初始化检查地址分配、检查寄存器偏移发送时数据卡死发送 FIFO 满时未做等待、状态寄存器误读检查发送循环逻辑7.2 ILA 抓不到信号先检查这几个地方用 ILA 调试 Uartlite 时如果始终抓不到期望的信号先检查三件事第一ILA 的采样时钟是不是和 Uartlite 连的是同一个时钟。如果 ILA 用了另一个时钟域采样窗口可能完全错开信号的变化。第二触发条件的设置是否合理。比如要抓接收数据就可以用接收 FIFO 非空作为触发条件确保触发时刻数据已经到达。第三ILA 在综合时是否会插入到目标信号路径上。某些情况下如果信号被优化掉了比如寄存器输出直接接到引脚、中间没有组合逻辑ILA 采样点选不到正确的寄存器就看不到变化。这时候可以在信号的 RTL 代码里加(* keep true *)属性避免综合工具把它优化掉。7.3 乱码问题的高级排查思路乱码问题除了波特率不匹配还有几个容易被忽略的原因。第一个是数据位校验位格式不一致。上位机配置为 8N1但 IP 里被改成 7E1两边看到的每一位含义完全不同乱码也就顺理成章了。所以排查乱码时不要只盯着波特率把数据格式也核对一遍。第二个是接地问题。如果 FPGA 板子和上位机的 USB 转串口模块共地不良逻辑电平判断会不稳定低速可能没事高速就会出现随机错位。解决方法是检查共地或者换一个隔离型 USB 转串口模块。第三个是电平标准不匹配。FPGA 引脚输出 3.3V但外部串口芯片是 1.8V 电平这时候寄存器配置可能都能工作正常但物理层信号会处于非标准电平区域导致误码。这类问题示波器看波形最直观——看高电平是否接近预期电压。7.4 一个困扰我两天的实战案例最后分享一个真实的排查经历。某个项目里需要 FPGA 在启动时通过 Uartlite 上报版本信息上位机偶尔能收到偶尔收不到完全没有规律。一开始怀疑是复位时序问题把复位时间延长后现象依然存在。然后用 ILA 抓 AXI 写通道发现写发送寄存器的 WVALID 和 WREADY 都正常数据也写进去了但发送 FIFO 空标志一直不拉低。再往深处查发现 Uartlite 的时钟和 AXI 互连的时钟不是同一个 MMCM 输出的两者之间有固定相位偏移导致 IP 内部状态机有时能正常处理 AXI 写请求有时在跨时钟域握手时采样失败。最终把时钟统一到同一个 MMCM 输出问题彻底消失。这个案例再次说明Uartlite 虽然本身很简单但它挂在 AXI 总线上整个链路里任何一个环节的时序问题最后都可能表现为“串口数据偶尔丢字节”这种看似无关的现象。8. 结尾一点个人的操作体会用过 Uartlite 几年下来我觉得它最大的价值不是省去了写串口模块的时间而是提供了一个稳定的、可预测的通信入口让调试工作从“先确保串口能通”直接跳到“专注业务逻辑”。但如果配置不仔细这几个参数——时钟频率、FIFO 深度、地址映射——任何一项没有对齐排查起来都要花掉比写代码多得多的时间。最后再分享一个小技巧在工程初期哪怕只有一块裸板、上面只挂了 Uartlite 一个外设也要把它的 AXI 接口当作核心逻辑来认真设计不要因为它简单就随意连接。端口规范、时钟干净、复位同步、地址固定这几条做到位后续项目哪怕是换到 UltraScale 或者 Versal 平台这套经验可以直接平移几乎不需要重新摸索。
返回列表