资讯动态

自定义uiFDMA IP核:基于AXI协议简化DMA寄存器配置与突发传输

发布时间:2026/10/27 16:27:32 来源:尧图企业网站定制
简介一套面向现场可编程门阵列与片上系统开发者的自定义全双工存储器访问IP核封装文件。该IP核由米联客封装用于处理可编程逻辑与处理器系统之间的高速大容量数据传输尤其适合需要频繁读取处理器端双倍速率内存的网络协议报文搬运与实时数据采集应用。压缩包共6个文件整体仅11KB其中3个可扩展标记语言文件用于IP核组件定义与接口配置2个硬件描述语言文件用于核心逻辑实现另有1个脚本文件用于工程打包与导入模块划分清晰二次开发成本低。目前已有2214人学习下载。借助该工程可以直观理解全双工内存访问控制器的配置方式、突发传输流程、中断与异常处理机制也能将封装好的IP核导入常用FPGA设计环境直接复用从而缩短开发周期并提高网络数据吞吐能力。该资源非常适合作为自定义总线类IP的设计参考模板。1. 自定义uiFDMA IP核不是重复造轮子是给AXI DMA做减法很多人在米联客开发板上第一次看到 uiFDMA 这个名字时第一反应是Xilinx 官方明明有 AXI DMA 和 AXI VDMA为什么还要再封装一个等你真正在 Vivado 里把 VDMA 的寄存器配完或者因为 AXI Stream 的握手时序对不上而调了两天就会明白uiFDMA 不是又一个 IP它是把官方 DMA 最难用的那部分——寄存器配置、帧同步、突发长度计算、中断管理——全部收进一个黑匣子对外只暴露几个简单的 AXI4-Lite 寄存器和一组干净的 AXI4-Stream 接口。这篇笔记想讲的不是怎么去下载一个现成的 uiFDMA 然后用起来而是当你需要在自己的项目里自定义 uiFDMA 时封装思路是什么、RTL 怎么搭、参数怎么定、哪些地方最容易翻车。适合手里有米联客板子、或者正在做图像采集/高速数据搬移被官方 IP 的复杂度折磨过的工程师。2. uiFDMA 的封装思路读通道、写通道和寄存器映射为什么这样设计2.1 官方 AXI DMA/VDMA 的痛点你被寄存器耗尽耐心的那个下午要理解 uiFDMA 的封装逻辑先得回忆一下直接用 Xilinx 官方 VDMA 时遇到的那些体力活。VDMA 的 AXI4-Lite 寄存器有几十个从 S2MM_CTRL0x00、S2MM_DMACTRL0x30到 S2MM_FRMBUF_CNT0x40、S2MM_START_ADDR0x80 起每帧地址都要摆一组更别说 PARK_PTR、IRQ_MASK、IRQ_STAT 这些中断相关的寄存器了。每次上电你要按固定顺序写一轮先复位、再配帧缓冲地址、再配帧大小和行/列参数、最后把 DMACTRL 里的 Run 位置 1。顺序写错一个寄存器DMA 可能直接不干活而且官方 IP 还不会给你任何提示只能用 ILA 去抓 AXI 总线一点一点查。一旦图像分辨率变了帧缓冲地址、行像素数、突发长度要全部重算纯手工配这些字段非常容易配出地址写对但长度写错这种玄学问题。另一个痛点是 VDMA 的 AXI4-Stream 接口带 sof/eof 帧标志但它的握手时序在不同版本上表现不完全一样更新一次 Vivado 版本之后原来跑得好好的设计可能突然丢帧。你很难分辨是 IP 配置问题、时序问题还是版本兼容问题。uiFDMA 的思路就是把这些脏活封装到 IP 内部用户侧不直接面对几十个寄存器而是面对一个简单的控制接口配置项收敛到 5 到 6 个关键寄存器。它保留 AXI4 主接口去访问 DDR把 AXI4-Stream 或者 AXI4-FIFO 作为用户侧数据接口帧同步、突发请求、中断清标志这些逻辑全部内置。2.2 uiFDMA 的三个核心组成写通道、读通道和中断控制从结构上看自定义 uiFDMA 无外乎三个部分写通道用户数据进 DDR、读通道DDR 数据出给用户、中断控制。写通道对应的 AXI 方向是 S2MMStream to Memory-Mapped读通道对应 MM2SMemory-Mapped to Stream。有的封装还会在写通道前面加一个 CDC 异步 FIFO因为用户时钟和 AXI 时钟往往不是一个频率跨时钟域处理如果交给使用者自己做又要引入一堆 FIFO IP不如在 IP 内部统一处理。这里有一个常见设计选择用户侧接口到底用 AXI4-Stream 还是 AXI4-FIFOAXI4-Stream 的好处是标准化任何支持 AXIS 的外设比如 VDMA、AXI-Stream Data FIFO、各种视频处理 IP都能直接对接缺点是你自己要处理 tvalid/tready/tlast 的握手特别是 tlast 不拉高就会死等。AXI4-FIFO 接口的好处是简单读起来像操作一个 FIFO写使能、写数据、满信号、读使能、读数据、空信号时序上不用管 AXIS 那套 valid-ready 反压逻辑对不熟悉 AXI 总线的 FPGA 工程师更友好。米联客封装的 uiFDMA 对不同型号板子给出的接口风格略有差异但设计理念一致把复杂时序关在 IP 里面对外暴露最简单可靠的握手。2.3 关键寄存器映射最少需要哪几个控制位自定义 uiFDMA 时寄存器映射不需要照搬官方 VDMA 那几十个收敛到下面这张表就基本够用。我一般会预留扩展位避免后续加功能时地址空间不够用。偏移地址寄存器名位宽功能描述关键位说明0x00控制寄存器32启动/停止 DMA、软复位、方向选择bit0 DMA 使能bit1 软复位bit2 写通道使能bit3 读通道使能0x04状态寄存器32通道忙碌状态、帧完成标志、错误标志bit0 写通道忙bit1 读通道忙bit2 帧完成bit3 FIFO 上溢/下溢只读0x08写通道基地址32用户数据写入 DDR 的起始地址必须按 4KB 或 64KB 对齐取决于 AXI IP 的地址对齐配置0x0C写通道传输长度32单次突发传输的字节数建议为 AXI 数据位宽字节数的整数倍如 64 位总线则按 8 字节对齐0x10读通道基地址32DDR 数据读出给用户的起始地址对齐要求同写通道0x14读通道传输长度32单次突发传输的字节数同上必须与 FIFO 深度匹配0x18中断使能寄存器32控制哪些中断源能产生中断输出bit0 写完成中断bit1 读完成中断bit2 错误中断0x1C中断状态寄存器32当前中断状态写 1 清 0读返回中断状态写对应位清标志这套映射表的关键在于控制寄存器里把读和写通道分开使能。实际项目中经常有只写不读或者只读不写的场景比如 ADC 采集只往 DDR 写、DAC 回放只从 DDR 读分开使能避免用户不得不写一个空读通道去凑数。状态寄存器里的错误标志位也很重要FIFO 上溢/下溢、AXI 总线错误都应该锁存并且可以通过状态寄存器随时查询。如果你想让这个 IP 挂进 Vivado 的 Block Design 里被 PS 端访问这套寄存器映射完全兼容 AXI4-Lite 从接口的标准读写法不需要额外封装。2.4 为什么突突发长度和地址对齐是 uiFDMA 性能的命门自定义 uiFDMA 时最容易被轻视的两个参数是 AXI 突发长度和地址对齐。AXI4 协议规定突发长度最多 256但具体数值受总线位宽限制如果你把 AXI 数据总线配成 64 位8 字节256 个突发对应 2048 字节如果配成 128 位16 字节256 个突发对应 4096 字节。AXI 突发地址必须按突发字节数对齐比如 64 位总线、突发长度 64那么单次突发传输 512 字节起始地址必须 512 字节对齐。你从软件上层传来一个 DDR 地址如果不做对齐检查直接塞给 AXI 地址总线大概率会触发 AXI 的地址错误甚至 DECERR。更微妙的是 AXI 协议要求突发不能跨越 4KB 边界。哪怕你的地址是对齐的如果最后一个突发从 0x1FF00 开始长度 512 字节正好跨过 0x20000 这个 4KB 边界就必须把它拆成两个突发。这些逻辑在状态机设计时必须显式处理不能假设上层传来的地址和长度都完美。米联客的 uiFDMA 在内部处理了 4KB 边界分割这是它有价值的原因之一但当你自定义自己的 uiFDMA 时千万不要砍掉这个逻辑否则跑大数据量传输时会间歇性出错而且这种错用 ILA 抓波形很难抓到属于典型的不定时翻车。3. 在 Vivado 里自定义 uiFDMA从创建 IP 到写通道路 RTL3.1 创建封装 IP 的正确姿势Create and Package IP 的两种路径在 Vivado 里自定义 uiFDMA第一步不是写 RTL而是决定用哪种封装方式。Vivado 的 Tools - Create and Package IP 提供两种模板一种是创建一个带 AXI4-Lite 从接口和 AXI4 主接口的完整模板Vivado 自动帮你生成 AXI 读写状态机的骨架另一种是从当前工程创建把已经写好的 RTL 模块封装成 IP。对自定义 uiFDMA 来说我推荐第一种——让 Vivado 生成模板然后在此基础上改因为模板里的 AXI4-Lite 从接口处理地址译码、读数据回传、写寄存器时序已经是验证过的你只需要往里面填自己的逻辑。使用第一种方式时在 Vivado 里选 Peripherals - AXI4 Peripheral然后配置接口数量一个 S_AXI32 位 AXI4-Lite 从接口、一个 M_AXIAXI4 主接口用于读写 DDR、一个 M_AXIS 或者用户自定义接口用于数据输出给用户逻辑。Vivado 生成的模板里自带 AXI 读写状态机和 S_AXI 寄存器读写逻辑省掉了最枯燥的部分。关键的改造点在于模板默认的 M_AXI 突发逻辑是单笔传输的你要把它改造成可连续突发、可拆分 4KB 边界的形式并且要把用户数据流Stream/FIFO和 AXI 写数据通道对接起来。3.2 写通道 RTL把用户数据搬进 DDR 的最小状态机下面这段代码是自定义 uiFDMA 写通道的骨架。它实现了最核心的功能从用户侧读数据当满足突发条件时发起一次 AXI 写传输。代码做了简化去掉了跨时钟域 FIFO 和 4KB 边界拆分只保留主干逻辑。module uifdma_wr_channel #( parameter AXI_DATA_WIDTH 64, parameter AXI_ADDR_WIDTH 32, parameter BURST_LEN 16 // beat 数 )( input wire axi_clk, input wire axi_rst_n, // AXI 写地址通道 output wire [AXI_ADDR_WIDTH-1:0] m_axi_awaddr, output wire [7:0] m_axi_awlen, output wire [2:0] m_axi_awsize, output wire m_axi_awvalid, input wire m_axi_awready, // AXI 写数据通道 output wire [AXI_DATA_WIDTH-1:0] m_axi_wdata, output wire [AXI_DATA_WIDTH/8-1:0] m_axi_wstrb, output wire m_axi_wlast, output wire m_axi_wvalid, input wire m_axi_wready, // AXI 写响应通道 input wire [1:0] m_axi_bresp, input wire m_axi_bvalid, output wire m_axi_bready, // 用户侧 FIFO 读接口 input wire [AXI_DATA_WIDTH-1:0] user_fifo_dout, output wire user_fifo_rd_en, input wire user_fifo_empty, // 寄存器配置 input wire [AXI_ADDR_WIDTH-1:0] cfg_base_addr, input wire [31:0] cfg_trans_len, // 字节数 input wire cfg_start, output reg busy ); localparam IDLE 3d0, WAIT_FIFO 3d1, ISSUE_ADDR 3d2; localparam SEND_DATA 3d3, WAIT_RESP 3d4; reg [2:0] state; reg [31:0] byte_cnt; reg [7:0] burst_cnt; reg [AXI_ADDR_WIDTH-1:0] cur_addr; reg [7:0] total_bursts; wire [7:0] bursts_total cfg_trans_len[31:3] / BURST_LEN; // 64 位总线8 字节每 beatburst 总字节数 BURST_LEN * 8 assign m_axi_awsize $clog2(AXI_DATA_WIDTH/8); // 3 for 64-bit assign m_axi_awlen BURST_LEN - 1; always (posedge axi_clk or negedge axi_rst_n) begin if (!axi_rst_n) begin state IDLE; busy 1b0; m_axi_awvalid 1b0; m_axi_wvalid 1b0; m_axi_bready 1b0; user_fifo_rd_en 1b0; cur_addr 32d0; byte_cnt 32d0; burst_cnt 8d0; end else begin case (state) IDLE: begin if (cfg_start) begin busy 1b1; cur_addr cfg_base_addr; byte_cnt 32d0; state WAIT_FIFO; end end WAIT_FIFO: begin // 等待 FIFO 数据积累到足够一个 burst // 实际项目用 FIFO 的 prog_full 或者计数判断 if (!user_fifo_empty) begin m_axi_awvalid 1b1; m_axi_awaddr cur_addr; state ISSUE_ADDR; end end ISSUE_ADDR: begin if (m_axi_awready) begin m_axi_awvalid 1b0; burst_cnt 8d0; state SEND_DATA; end end SEND_DATA: begin // 每个周期从 FIFO 读一个数据同时驱动 wvalid if (!user_fifo_empty) begin user_fifo_rd_en 1b1; m_axi_wvalid 1b1; m_axi_wdata user_fifo_dout; m_axi_wstrb {AXI_DATA_WIDTH/8{1b1}}; if (burst_cnt BURST_LEN - 1) begin m_axi_wlast 1b1; // 最后一个 beat同时拉 wlast end else if (m_axi_wready m_axi_wvalid) begin burst_cnt burst_cnt 1b1; m_axi_wlast 1b0; end // wlast 判断需要提前一拍这里简化展示了思路 end else begin user_fifo_rd_en 1b0; m_axi_wvalid 1b0; end if (m_axi_wready m_axi_wvalid m_axi_wlast) begin m_axi_wvalid 1b0; m_axi_wlast 1b0; user_fifo_rd_en 1b0; state WAIT_RESP; end end WAIT_RESP: begin m_axi_bready 1b1; if (m_axi_bvalid) begin m_axi_bready 1b0; byte_cnt byte_cnt (BURST_LEN * AXI_DATA_WIDTH/8); cur_addr cur_addr (BURST_LEN * AXI_DATA_WIDTH/8); if (byte_cnt cfg_trans_len - (BURST_LEN * AXI_DATA_WIDTH/8)) begin busy 1b0; state IDLE; end else begin state WAIT_FIFO; end end end endcase end end endmodule这段代码的逻辑主干分四步IDLE 等启动信号WAIT_FIFO 等数据SEND_DATA 按 burst 往 AXI 写数据通道灌数据WAIT_RESP 等写响应后更新地址和计数器。有几个参数是自定义 uiFDMA 时必须明确的AXI_DATA_WIDTH 决定每个 beat 多少字节awsize 必须等于 log2(数据位宽/8)awlen 是突发长度减 1。特别注意 SEND_DATA 状态里 wlast 的生成逻辑它必须提前一拍判断当前是否最后一个 beat因为 AXI 协议要求 wlast 与最后一个数据同拍拉高。这里为了展示主干逻辑做了简化实际工程建议用一个专门的前瞻寄存器去生成 wlast避免因为 FIFO 读出一拍延迟导致最后一个数据丢失。这段代码里对 FIFO 空信号的判断是只要有数据就开始一个 burst在实际工程中容易产生碎片化突发效率不高。更稳的做法是用 Xilinx AXI-Stream Data FIFO 的 prog_full 信号来判断等 FIFO 里攒够一个 burst 的数据量再启动 AXI 突发这样 DDR 访问效率会高很多尤其是挂多个 master 争抢带宽的时候。你可以把 prog_full 阈值设为 BURST_LEN然后在 WAIT_FIFO 里等待 prog_full 拉高。3.3 读通道 RTL从 DDR 把数据推给用户侧 FIFO读通道负责发起 AXI 读突发、接收读数据并把数据写入用户侧 FIFO。这里给出与写通道对称的骨架代码。module uifdma_rd_channel #( parameter AXI_DATA_WIDTH 64, parameter AXI_ADDR_WIDTH 32, parameter BURST_LEN 16 )( input wire axi_clk, input wire axi_rst_n, // AXI 读地址通道 output wire [AXI_ADDR_WIDTH-1:0] m_axi_araddr, output wire [7:0] m_axi_arlen, output wire [2:0] m_axi_arsize, output wire m_axi_arvalid, input wire m_axi_arready, // AXI 读数据通道 input wire [AXI_DATA_WIDTH-1:0] m_axi_rdata, input wire [1:0] m_axi_rresp, input wire m_axi_rlast, input wire m_axi_rvalid, output wire m_axi_rready, // 用户侧 FIFO 写接口 output wire [AXI_DATA_WIDTH-1:0] user_fifo_din, output wire user_fifo_wr_en, input wire user_fifo_full, // 寄存器配置 input wire [AXI_ADDR_WIDTH-1:0] cfg_base_addr, input wire [31:0] cfg_trans_len, input wire cfg_start, output reg busy ); localparam IDLE 3d0, ISSUE_ADDR 3d1, READ_DATA 3d2; reg [2:0] state; reg [31:0] byte_cnt; reg [7:0] burst_cnt; reg [AXI_ADDR_WIDTH-1:0] cur_addr; assign m_axi_arsize $clog2(AXI_DATA_WIDTH/8); assign m_axi_arlen BURST_LEN - 1; always (posedge axi_clk or negedge axi_rst_n) begin if (!axi_rst_n) begin state IDLE; busy 1b0; m_axi_arvalid 1b0; m_axi_rready 1b0; user_fifo_wr_en 1b0; cur_addr 32d0; byte_cnt 32d0; burst_cnt 8d0; end else begin case (state) IDLE: begin if (cfg_start) begin busy 1b1; cur_addr cfg_base_addr; byte_cnt 32d0; state ISSUE_ADDR; end end ISSUE_ADDR: begin m_axi_arvalid 1b1; m_axi_araddr cur_addr; if (m_axi_arready) begin m_axi_arvalid 1b0; burst_cnt 8d0; state READ_DATA; end end READ_DATA: begin // 接收 AXI 读数据并写入用户 FIFO if (m_axi_rvalid) begin if (!user_fifo_full) begin user_fifo_din m_axi_rdata; user_fifo_wr_en 1b1; m_axi_rready 1b1; if (m_axi_rlast) begin // 一个突发结束 byte_cnt byte_cnt (BURST_LEN * AXI_DATA_WIDTH/8); cur_addr cur_addr (BURST_LEN * AXI_DATA_WIDTH/8); if (byte_cnt cfg_trans_len - (BURST_LEN * AXI_DATA_WIDTH/8)) begin busy 1b0; user_fifo_wr_en 1b0; m_axi_rready 1b0; state IDLE; end else begin user_fifo_wr_en 1b0; state ISSUE_ADDR; end end end else begin // FIFO 满暂停接收 —— 反压逻辑 m_axi_rready 1b0; user_fifo_wr_en 1b0; end end else begin user_fifo_wr_en 1b0; end end endcase end end endmodule读通道比写通道多一个天然的反压控制点当用户 FIFO 满时可以通过拉低 rready 来暂停接收 AXI 读数据。这符合 AXI 协议——rready 和 rvalid 同时为高时数据才被接收。实际使用中要注意如果用户 FIFO 经常被占满DDR 读带宽会掉下来所以读通道的 FIFO 深度要参考下游消费速率来定。另一个容易忽略的点是 rlast 处理一个突发结束后要立即发起下一个突发中间不要插入额外等待周期否则 DDR 读带宽会明显下降。如果你用 ILA 抓波形发现读通道两个突发之间有超过 10 个周期的空闲大概率是状态机设计里多了一拍空转需要优化。3.4 打包和同一封装内的接口检查这一步决定了你后半夜回不回家RTL 写完后在 Create and Package IP 向导里选择封装当前工程Vivado 会自动分析模块的接口并把所有输入输出 ports 映射成 IP 的接口。这里有一个非常影响体验的细节如果你把 user_fifo_din、user_fifo_wr_en、user_fifo_full 这类信号做成普通端口封装出来的 IP 就不带 AXI-Stream 接口集成到 Block Design 时别人还得手动一个个连非常容易连错线。更符合 uiFDMA 使用习惯的做法是在封装前就把用户侧接口显式定义成 AXI4-Stream 接口利用 Vivado 的 Interface 分组功能——把 tdata/tvalid/tready/tlast 单独拎出来组成一组名为 S_AXIS 的接口这样在 Block Design 里拖出来就直接是一个标准 AXIS 接口能自动连接下游的 AXIS FIFO 或者视频处理 IP。封装时还要注意 IP 版本和 AXI 协议的匹配。Vivado 的 IP Packager 默认生成 IP 是 AXI4 协议规范如果你的总线位宽是 128 位而 m_axi_awsize 参数没跟上地址对齐就全乱了。另外记得把时钟接口命名成 s_axi_aclk 和 s_axi_aresetn这是 Vivado 识别 AXI 外设接口的标准命名命名不对会导致 IP 在 Block Design 里不能被自动连线。做完这些点 Package IP一定打开 IP 的 Example Design 仿真一下确认接口分组和地址空间都正常再发布。很多人在打包时跳过了仿真验证结果导入别处用的时候发现端口名字对不上或者位宽不对非常浪费工时。4. 用自定义 uiFDMA 打通 DDR 读写最小系统搭建与寄存器配置4.1 Block Design 接线最小系统只需要五个组件一个能跑起来的自定义 uiFDMA 最小系统在 Block Design 里只需要五类组件Zynq PS 或者 MicroBlaze提供 AXI4-Lite 配置通道、DDR 控制器MIG 或 Zynq 内部的 DDR 控制器、自定义 uiFDMA IP、AXI Interconnect、以及用户侧的数据源/目的地。如果你用的是纯 FPGA 芯片没有 PS可以用 MicroBlaze 配合 AXI4-Lite 接口来配置寄存器也可以做一个简单的 AXI4-Lite 主机状态机来写寄存器。对于 Zynq 系列最常见的做法是把 uiFDMA 的 S_AXI 挂到 PS 的 AXI_GP 口上M_AXI 经过 AXI Interconnect 挂在 S_AXI_HP 口上访问 DDR。这里有一个带宽考量HP 口的带宽远大于 GP 口所以数据通路一定要走 HP 口配置通路可以走 GP 口分担压力。用户侧接口如何接取决于你的数据场景。图像采集场景通常把 uiFDMA 的写通道接在摄像头采集 IP 后面数据从摄像头进 FIFO再由 uiFDMA 搬进 DDR显示回放场景则把 uiFDMA 的读通道接在显示控制器的 AXI-Stream 接口上。如果用户侧不是流式接口而是带地址的那就需要再加一个地址转换逻辑在 uiFDMA 封装层面往往不做这层适配而是要求用户把数据整理成流——这是 uiFDMA 封装模型的边界要提前想清楚。4.2 寄存器配置顺序先长度后使能最后再启动寄存器配置顺序在自定义 uiFDMA 里是个容易被忽视但又极其关键的环节。推荐配置顺序是这样写通道为例。// 伪代码向 uiFDMA 写通道发起一次传输 // 1. 先写传输长度0x0C再写基地址0x08最后使能启动 void uifdma_write_transfer(uint32_t base_addr, uint32_t byte_len) { // 软复位清掉上一次可能残留的状态 reg_write(UIFDMA_BASE 0x00, 0x02); // 控制寄存器 bit1 软复位 reg_write(UIFDMA_BASE 0x00, 0x00); // 释放复位 // 配置传输参数 reg_write(UIFDMA_BASE 0x0C, byte_len); reg_write(UIFDMA_BASE 0x08, base_addr); // 启动写通道 reg_write(UIFDMA_BASE 0x00, 0x01 | 0x04); // bit0 使能 bit2 写通道使能 }这里有两个细节。第一先配长度再配地址因为在 IP 内部地址和长度寄存器写入时是独立的如果先写地址后写长度内部逻辑虽然不会立即启动但有些粗心的封装会把地址写入当作启动触发信号导致长度还没写就开始传输。所以统一先写长度、后写地址、最后写使能位顺序最安全。第二软复位那两步不是可选的它负责把上一帧的 busy 状态和 FIFO 残余数据清干净。如果你不做软复位就重新启动FIFO 里上一次残留的数据会被发到 DDR造成数据错位而且这种错位在波形上很难看——地址和长度都对就是数据内容不对。读通道的配置顺序类似把写通道基地址换成读通道基地址0x10、长度换成 0x14 即可。如果 udma 同时做读写要注意两个通道的优先级设置取决于 AXI Interconnect 的配置如果你希望写通道优先比如 ADC 采集不能丢数据在 Interconnect 里把写通道对应的 slave 端口 QoS 调高或者在 uiFDMA 封装内部做一个简单的轮询里写通道权重更大。4.3 帧参数怎么设行/列/像素字节对齐这三个值最容易算错很多做图像传输的人在配置 uiFDMA 长度时容易把行、列、像素字节三者关系弄混。假设一幅 1920x1080 的 RGB888 图像一个像素 3 字节。单帧数据字节数 1920 * 1080 * 3 6,220,800 字节。如果你希望按行突发传输每行一个 burst 边界则一次传输长度应该设置为 1920 * 3 5760 字节。但注意AXI 突发传输要求地址对齐5760 字节不能被 8 整除5760 / 8 720其实可以但有些分辨率不满足对齐。比如一个 640x480 的 RGB888一行 1920 字节这可以按 8 对齐但如果是 800x600一行 2400 字节同样可被 8 整除。真正会出问题的是像素宽度非对齐的场景比如 24 位深度的 YUV 422 打包格式如果每行字节数不能被 AXI 总线位宽的字节数整除就需要在行尾做填充padding否则 DMA 会跨行读取。自定义 uiFDMA 的长度寄存器应该设置为本次搬运的总字节数而不是帧数或者像素数。有的封装会额外提供一个 DMA_STRIDE 寄存器专门存在行偏移用于处理图像行对齐。如果你的场景是连续多帧搬运推荐的做法是在软件端把多帧拼接成一个大 buffer一次 DMA 搬运多帧减少启动开销和中断次数。真机上你还会发现DDR 访问效率跟 burst 长度强相关帧总字节数越大平均突发效率越高。做图像采集时常见的坑是你把长度寄存器配成了像素数而不是字节数导致长度偏小 3 倍DMA 搬完一半图像就停下来了而且帧完成中断照常拉高干扰你判断。4.4 仿真和在线验证不用 ILA 也能快速判断通道是否工作自定义 uiFDMA 在跑上板之前至少要在 Vivado 仿真里验证两种情况单次突发正确性和连续突发时的 FSM 状态跳转。建议写一个简单的 testbench用任务函数模拟 AXI 从设备的行为向被测试的 uiFDMA 写通道写入固定递增数据比如 0x00、0x01、0x02...然后在 AXI 总线侧用 monitor 检查写入 DDR 的数据是否符合预期。注意在 testbench 里给 AXI 接口加 RANDOM DELAY用 $urandom 控制 awready/wready 的随机拉低这样能暴露出状态机在反压情况下的设计缺陷。上板验证时如果你不想动用 ILA 去抓内部信号有一个更快捷的办法利用状态寄存器的 busy 位做判断。软件端启动传输后轮询状态寄存器0x04如果 busy 位长时间不拉低要么是 DMA 没有配置正确要么是 AXI 总线出了错误。如果 busy 正常拉低但是数据不对优先怀疑地址对齐或者长度寄存器配错。只有到了这一步查不出来才需要动用 ILA 去抓 M_AXI 的波形看地址通道和数据通道的时序关系。很多人一上来就 ILA 抓总线波形浪费时间。正确的排查顺序是寄存器状态 - 软件端回读 DDR 数据 - ILA 抓波形逐层深入每次只排除一种可能性。5. 自定义 uiFDMA 避坑手册常见报错与排查方向5.1 读通道超时ARM 端读回超时busy 位始终为高现象软件端发起读 DMA 传输后轮询状态寄存器发现 busy 位一直为 1几秒钟后如果是通过 AXI4-Lite 回读数据软件报超时错误。用 ILA 抓 M_AXI 读地址通道发现 arvalid 拉高后 arready 一直没有回。原因这个问题在自定义 uiFDMA 里最常见的来源是地址配置错误。读起始地址超出了 DDR 实际映射范围或者 AXI Interconnect 里没有把地址范围配给对应的 slave导致经过 Interconnect 译码时找不到对应的 slavearready 永远不会拉高。另一种可能是 ddR 控制器没有被正确初始化——MIG 没 training 成功整个 DDR 地址空间都不可用这个时候任何访问 DDR 的 AXI 事务都会挂死。解决先核对基地址是否在 DDR 地址范围内。Zynq 上如果 DDR 起始地址是 0x00100000而你配成了 0x00000000得确认这块是否被其他外设占用。其次是查 AXI Interconnect 的地址映射表确保 uiFDMA 的 M_AXI 接口地址范围能在 Interconnect 里命中 DDR 控制器。最后用最简单的方法确认 DDR 是否活着让 PS 端直接往某一块 DDR 地址写数据再读回能读通说明 MIG 正常再回来查 DMA 配置。5.2 写数据少一拍或多一拍DDR 里前几个字是乱的现象从 DDR 读回 DMA 写进去的数据发现前 4 个字节是上一次传输的残留数据或者最后一个字节丢失。整个数据 pattern 呈现出错位而不是错值。原因用户侧 FIFO 在软复位时没有清空第一次传输前 FIFO 里还留着上一次的残余数据DMA 一启动就把这批老数据先发出去了。另外一个常见原因是 wvalid 和 wdata 的时序不匹配RTL 里在发送数据时把 wdata 打了一拍但 wvalid 没有跟着打拍导致第一个有效数据实际出现在第二个周期而 AXI 总线在第 1 个周期采样到了无效数据。解决在软复位流程里加一步强制把用户 FIFO 的复位信号拉高至少 4 个周期等 FIFO 的 empty 信号拉高后再启动下一帧传输。对于 wdata 和 wvalid 错拍的问题建议在 testbench 里加一个数据完整性校验模块对比写入数据和源数据的 pattern。如果发现错拍修正方法是把 wdata 和 wvalid 用同一个寄存器打拍保证它们同拍变化。这个问题属于典型的时序细节翻车FPGA 工程师俗称时序玄学其实都是设计不规范导致的。5.3 帧完成中断丢了状态寄存器里能看到标志但中断没触发现象软件查询状态寄存器时帧完成标志位是 1但中断控制器那边没有收到中断信号。检查中断使能寄存器发现使能位也配置正确。原因自定义 uiFDMA 的中断输出设计有两个常见缺陷。一个是中断输出采用电平模式如果软件处理中断时是先读状态再清标志而 IP 内部在标志没清掉之前一直拉高中断线这本身没问题但有的封装里中断脉冲宽度只有 1 个时钟周期软件中断控制器没采到就丢了。另一个是中断状态寄存器里写 1 清 0的逻辑没实现好软件清标志后状态没变化导致中断线一直拉着后续中断再也无法触发。解决建议把中断输出设计成可配置电平/脉冲模式通过寄存器 bit 控制。默认用脉冲模式宽度至少 16 个时钟周期保证中断控制器能采到。如果已经流片或者不好改 RTL软件侧可以改成轮询模式绕开硬件中断。脉冲型中断丢失还有一个隐蔽原因中断信号在跨时钟域时没有同步如果你把中断输出直接跨到 PS 端时钟域没有用两级同步器极少概率会漏采。这个要升级 RTL 才能根治同时这也是血泪经验——跨时钟域中断必须同步。5.4 AXI4-Lite 读回寄存器全是 0地址根本不对现象ARM 端读 uiFDMA 的任何寄存器都是 0写寄存器后回读也是 0。仿真里读写逻辑都正确上板后失效。原因这种问题优先怀疑地址没有对齐到 4 字节边界。AXI4-Lite 协议要求所有寄存器访问的地址按 4 字节对齐如果你在 Block Design 里给 uiFDMA 分配的地址基地址不以 0x0 结尾比如分配成 0x43C0_0004访问寄存器偏移量加上基地址后错位读回数据自然不对。另一种可能是 M_AXI 和 S_AXI 的位宽不匹配比如 AXI Interconnect 把 PS 端 32 位 AXI_GP 口和 uiFDMA 的 32 位 S_AXI 对接这个没问题但如果 PS 用 64 位 AXI 口去访问一个 32 位 S_AXIInterconnect 会自动做位宽转换万一配置成 Memory-mapped 到 AXI4-Stream 转换模式行为会非常奇怪。解决检查 Block Design 里 uiFDMA 的地址分配是否按 4KB 对齐。一般习惯把自定义 IP 的地址段分配在 0x43C0_0000 这种固定位置且基地址后 12 位全 0。另一个排查点是确认 S_AXI 接口上有没有把 awready 信号正确回给 Interconnect有的封装在寄存器读写状态机里漏了 awready 的默认拉高导致事务永远挂起软件读到的是超时错误而不是 0。如果回读是 0 而不是超时优先怀疑地址对齐这个可以用 ILA 抓 S_AXI 的 awaddr 来确认。5.5 地址非对齐导致偶发数据传输错误VPF 报错或者 DDR 数据错位现象连续传输大文件或者多帧图像时偶尔出现中间某一段数据错位错位长度固定等于一个字节或者几个字节。不是每次复现加长仿真时间能遇到。原因自定义 uiFDMA 没有做 4KB 边界拆分或者拆分逻辑里地址计算有误。AXI 协议规定一个突发不能跨越 4KB 边界如果基地址 0x1FF0突发长度 512 字节那这次突发从 0x1FF0 到 0x21F0跨越了 0x2000 边界是非法突发。此时不同的 AXI Interconnect 或者 DDR 控制器行为不一致有的直接丢弃数据有的传输错误地址的数据表现就是随机错位。解决根因是 RTL 里必须实现 4KB 边界检测与拆分逻辑。检测条件很简单当前地址的低 12 位 本次突发字节数 4096就说明跨边界了。拆分的做法是把当前突发长度改小到剩余可突发字节数发完后再从 0x2000 边界地址发起下一个突发。这个逻辑必须在写通道和读通道同时实现。拆分的判断建议用判决逻辑而不是计算逻辑否则时序很容易紧张。我见过有人在拆分逻辑里顺序写 if-else 嵌套 8 层综合后频率掉得离谱最后推倒重来改成查表法。所以这里给一个注意提示4KB 边界检测用比较器当前地址低 12 位加上突发字节数比较异或判断不要做一串乘除运算。6. 把自定义 uiFDMA 调稳的最后一招用计数器与标志位做自检当你的 uiFDMA 已经能在仿真里跑通、上板也能搬数据但总感觉心里没底的时候建议给它加一个自检模式。这是一个非常实用的技巧在寄存器映射里预留一个测试模式控制位当置 1 时IP 内部不再从用户 FIFO 读数据而是用一个线性反馈移位寄存器LFSR生成固定伪随机数据写入 DDR 指定地址同时设置一个回读模式把 DDR 里刚写入的数据读回来与 LFSR 生成的数据比对。比对结果存到状态寄存器的一个标志位里一致则标志置 1不一致则置 0 并锁存错误地址。这个功能看起来多花了几百行 RTL但它解决了一个核心问题当你换了一块板子、升级了 Vivado 版本、或者改了 DDR 配置之后如何快速判断 uiFDMA 本身是否还有问题。不用写复杂的软件检查程序不用抓波形软件侧只需要写测试模式控制位然后轮询标志位几分钟就能跑完一遍全地址空间的读写测试。我自己的习惯是每次上板先跑一遍自检确认 IP 的 AXI 通路干净了再去调试用户逻辑。这个自检模式也方便你在给同事或者客户做演示时快速证明 IP 的可靠性。还有一个附带好处自检模式用的 LFSR 数据模式在 ILA 里看起来是伪随机的比固定递增数据更容易暴露数据位粘连问题。另外一个值得养成的习惯是在状态寄存器的预留位里放一个上次错误类型字段。当自检发现数据错误或者 AXI 总线事务错误时把错误类型锁存下来软件端通过一次读操作就能知道到底是地址错误、数据错误还是超时。这比让用户去看 ILA 波形要高效得多尤其是当 uiFDMA 被集成在一个大系统里、错误出现频率很低时这个标志位是唯一的线索。自定义 IP 和官方 IP 相比维护成本在自己手里所以这些调试基础设施值得在一开始就做进去不要等出了问题再去补。加完这些功能你会发现 uiFDMA 从一个黑匣子变成了一个可观测的、可验证的、可维护的自研模块这种掌控感是直接拿来一个 IP 核体会不到的。希望我的这些踩坑经验能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑