资讯动态

FPGA自编AXI4 Slave核:从XDMA到自定义IP的实战指南

发布时间:2026/10/16 10:37:27 来源:尧图企业网站定制
简介这份资源面向FPGA与PCIe接口开发的中高级工程师聚焦XDMA核与AXI4协议数据读写链路中的关键一环——自编AXI4 Slave核的设计与实现解决AXI4事务与AXI4-Stream数据流之间协议转换、宽度匹配、流控与错误响应等实际问题。压缩包共15个文件约25KB以tcl脚本、Verilog源码、C与头文件为主另含mdd、makefile、sv及xml等覆盖IP打包、综合仿真、驱动与示例设计等用途目录按bd、hdl、drivers、xgui等模块组织便于快速集成到Vivado工程。目前已有5190人学习下载读者可从中获得可复用的Slave核RTL实现、IP封装脚本与测试平台理解FIFO缓冲、事务同步及流开始结束信号生成思路为高性能低延迟PCIe系统开发提供参考。1. 从 XDMA 到自编 AXI4 Slave为什么现成 IP 不够用手里有一块 FPGA 开发板PC 端要跑大数据吞吐XDMA 核一挂上AXI4 总线上立刻多出一堆读写请求。这时候你会发现一个尴尬的事XDMA 的 AXI4 Master 接口是现成的但你要挂的那个从设备——比如一块自定义的片上缓存、一组寄存器阵列、或者一个流式数据搬运通道——市面上根本没有刚好合适的 IP。Vivado 里倒是有 AXI4 的 Slave 模板但那个模板是给寄存器读写用的突发长度、Outstanding 能力、数据位宽全被写死稍微改一点就要动它的状态机改到最后还不如自己写一个。这就是这篇要聊的事在 XDMA 核已经跑通 PCIe 链路的前提下自己用 Verilog 写一个 AXI4 Slave 核接在 XDMA 的 M_AXI 接口上实现 PC 端到 FPGA 内部存储的稳定读写。适合两类人一类是已经调通了 XDMA 基础回环、想往里面塞自定义逻辑的另一类是被 AXI4 协议时序折腾过、想找一个能直接改的 Slave 参考实现。AXI4 和 AXI-Stream 的区别这里不展开但记住一点XDMA 的 M_AXI 是完整 AXI4带地址通道不是 Stream别搞混。2. AXI4 Slave 核的接口拆解与状态机设计2.1 五个通道到底谁先谁后AXI4 的五个通道——AW写地址、W写数据、B写响应、AR读地址、R读数据——协议里说它们相互独立但实际做 Slave 的时候读写通道之间的优先级和握手顺序直接决定会不会死锁。我一般把写通道和读通道做成两套独立的状态机写通道内部再分 AW 和 W 的会合逻辑。写通道的核心问题是AW 和 W 谁先到协议允许任意顺序所以 Slave 必须能缓存先到的那个。常见做法是用两个小 FIFO一个存 AW 的地址和突发信息一个存 W 的数据然后状态机从两个 FIFO 里同时取凑齐一笔就写一次存储。读通道相对简单AR 到了之后直接按突发长度连续吐 R 数据就行但要注意 R 的 LAST 信号必须跟 AR 的突发长度严格对齐差一个周期 PC 端就报错。注意XDMA 的 M_AXI 接口默认 Outstanding 能力不低如果你的 Slave 没有反压机制读请求堆多了会把 R 通道堵死。建议在 AR 通道加一个深度 4 到 8 的 FIFO 做缓冲。2.2 状态机怎么写才不翻车下面是我常用的写通道状态机骨架用三段式写法状态编码用独热码方便时序收敛。这段代码可以直接放进 Vivado 里综合接口信号名跟 XDMA 的 M_AXI 对得上。// 写通道状态机IDLE - GET_AW - GET_W - WRITE - RESP // 假设存储侧是一个简单双口 RAM 接口 localparam S_IDLE 4b0001; localparam S_GET_AW 4b0010; localparam S_GET_W 4b0100; localparam S_WRITE 4b1000; localparam S_RESP 4b10000; // 实际用 5 位这里示意 reg [4:0] cur_state, nxt_state; reg [31:0] aw_addr_reg; reg [7:0] aw_len_reg; reg [2:0] aw_size_reg; reg [1:0] aw_burst_reg; reg aw_valid_reg; reg [31:0] w_data_reg; reg [3:0] w_strb_reg; reg w_last_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) cur_state S_IDLE; else cur_state nxt_state; end always (*) begin nxt_state cur_state; case (cur_state) S_IDLE: begin if (s_axi_awvalid s_axi_wvalid) nxt_state S_WRITE; // 地址和数据同时到直接写 else if (s_axi_awvalid) nxt_state S_GET_AW; // 先到地址 else if (s_axi_wvalid) nxt_state S_GET_W; // 先到数据 end S_GET_AW: begin if (s_axi_wvalid) nxt_state S_WRITE; end S_GET_W: begin if (s_axi_awvalid) nxt_state S_WRITE; end S_WRITE: begin if (w_last_reg) nxt_state S_RESP; end S_RESP: begin if (s_axi_bready) nxt_state S_IDLE; end default: nxt_state S_IDLE; endcase end这段状态机的逻辑说明IDLE 状态同时检测 AW 和 W 的 valid谁先到就进对应的缓存状态等另一个也到了再进 WRITE。WRITE 状态里每写一个数据就判断 w_last如果是最后一拍就跳 RESP 发 B 响应。参数方面aw_len_reg 存的是突发长度减一AXI4 的 INCR 模式下实际传输拍数是 len1这个减一操作在接收 AW 的时候就要做掉不然后面地址计算会多算一拍。2.3 地址对齐和 4K 边界AXI4 协议里有一条硬规矩一笔突发不能跨 4KB 边界。XDMA 作为 Master 一般会遵守但你自己写 Slave 的时候如果地址计算逻辑有 bug比如把 aw_addr_reg 直接累加而不检查低位就可能出现跨边界访问。我的做法是在 WRITE 状态里每拍检查当前地址的低 12 位如果加上传输宽度之后超过 0xFFF就提前拉高 B 响应的错误标志——虽然 XDMA 不一定理你但至少仿真波形里能一眼看出来。地址对齐还有一层aw_size_reg 表示每拍传输的字节数2b10 是 4 字节2b11 是 8 字节。如果你的存储侧位宽是 64 位而 AXI 数据位宽是 32 位就要做位宽转换这个转换逻辑放在 WRITE 状态里做不要放到状态机外面否则时序会很难看。3. 把 Slave 核挂到 XDMA 上地址映射与位宽匹配3.1 地址窗口怎么划XDMA 核的 M_AXI 接口出来之后地址空间是 PC 端 BAR 映射过来的。假设你在 Vivado 里给 XDMA 配了 1MB 的 BAR 空间那 Slave 核能看到的地址范围就是 0x0000_0000 到 0x000F_FFFF。我一般把这段空间再切一刀低 512KB 给寄存器组高 512KB 给数据缓存。寄存器组用一个小型的 AXI4-Lite Slave 处理数据缓存用上面那个完整 AXI4 Slave 处理。地址译码的逻辑很简单在 AW 通道的 valid 握手时判断地址高位// 地址译码根据 aw_addr_reg 的 bit19 区分寄存器区和数据区 wire sel_reg (aw_addr_reg[19] 1b0); wire sel_mem (aw_addr_reg[19] 1b1); // 只有选中对应区域时才拉高该区域的 awready assign reg_awready sel_reg ? s_axi_awready : 1b0; assign mem_awready sel_mem ? s_axi_awready : 1b0;这里有个坑两个 Slave 的 awready 不能同时拉高否则 XDMA 会收到两个响应行为未定义。用地址位做互斥是最稳妥的别用优先级编码器那个在高速时序下容易出毛刺。3.2 位宽不匹配怎么办XDMA 的 M_AXI 数据位宽常见的是 64 位或 128 位而你的内部存储可能是 32 位。位宽转换有两种做法一种是写一个专门的位宽转换模块把 64 位拆成两个 32 位另一种是在 Slave 状态机里直接处理用 w_strb 信号控制哪些字节有效。我倾向于第二种因为少一层模块少一层时序压力。具体做法是在 WRITE 状态里判断 w_strb如果当前拍只有低 32 位有效就只写低 32 位如果高 32 位也有效就分两拍写。但这样会拉长写周期如果 XDMA 的写吞吐要求高还是老老实实加一个异步 FIFO 做位宽转换。提示位宽转换模块的 FIFO 深度建议至少 16因为 XDMA 的突发长度可能到 256深度太小会频繁反压反而降低吞吐。3.3 读写通道的仲裁当读通道和写通道同时有请求时存储侧只有一个端口必须仲裁。我的做法是给写通道更高优先级因为写响应 B 如果迟迟不发PC 端的写事务会堆积最终导致驱动层超时。读通道可以稍微等一等R 数据的延迟对 PC 端来说通常比写响应更宽容。仲裁逻辑用一个简单的优先级选择器// 存储侧端口仲裁写优先 assign mem_wr_en (cur_state S_WRITE); assign mem_rd_en (cur_state S_RESP) ? 1b0 : ar_valid_reg; // 当写状态机活跃时读请求被挂起这段逻辑的意思是只要写状态机不在 IDLE读请求就不放行。实际项目中如果读写比例接近 1:1这种策略会导致读吞吐下降这时候就要改成轮询仲裁。轮询仲裁的实现稍微复杂一点需要一个 last_grant 寄存器记录上次谁被服务。4. 仿真验证与上板调试从波形到眼图4.1 Testbench 怎么搭才像真的自己写 Slave 核Testbench 不能只发几笔单次读写就完事。XDMA 的行为特点是突发长度不固定、Outstanding 深度大、读写交织。我一般用 SystemVerilog 写一个简单的 AXI4 Master BFM随机生成突发长度1 到 16 拍、随机地址4KB 对齐、随机读写比例。// 简化的 AXI4 Master 激励随机突发 task automatic send_write(input [31:0] addr, input [7:0] len); // 拉高 awvalid等待 awready s_axi_awaddr addr; s_axi_awlen len; s_axi_awvalid 1b1; wait (s_axi_awready); (posedge clk); s_axi_awvalid 1b0; // 发数据 for (int i 0; i len; i) begin s_axi_wdata $random; s_axi_wstrb 8hFF; s_axi_wlast (i len); s_axi_wvalid 1b1; wait (s_axi_wready); (posedge clk); end s_axi_wvalid 1b0; // 等 B 响应 wait (s_axi_bvalid); (posedge clk); s_axi_bready 1b1; (posedge clk); s_axi_bready 1b0; endtask这个 task 的关键参数是 len它决定突发拍数。仿真的时候把 len 从 0 到 15 都跑一遍重点看 wlast 和 bvalid 的时序关系。如果 bvalid 在 wlast 之前就拉高了说明状态机跳转条件写错了。4.2 ILA 抓什么信号上板之后Vivado 的 ILA 是唯一的黑匣子。我一般抓这几组信号s_axi_awvalid/awready、s_axi_wvalid/wready/wlast、s_axi_bvalid/bready、s_axi_arvalid/arready、s_axi_rvalid/rready/rlast。触发条件设在 awvalid 和 wvalid 同时为高的那拍这样能抓到一笔完整的写事务。抓到的波形重点看三个地方第一awready 和 wready 有没有同时拉高导致数据错位第二bvalid 的持续时间够不够长XDMA 的 bready 有时候会延迟几个周期才来第三rlast 和 arlen 是否匹配差一拍就会导致 PC 端读到的数据错位。注意ILA 的采样深度有限如果抓 256 拍突发的读事务深度至少设 4096否则看不到 rlast。4.3 性能瓶颈在哪上板跑通之后用 PC 端的测试程序跑一个 1GB 的连续读写看实际带宽。如果带宽远低于 XDMA 的理论值先查 Slave 的反压频率。反压太频繁说明存储侧带宽不够要么加 FIFO 深度要么提高存储时钟频率。另一个常见瓶颈是位宽转换64 位转 32 位如果每拍都转吞吐直接减半这时候要考虑用位宽转换 FIFO 做批量转换。5. 避坑与排查那些让我重写过三次的细节5.1 坑一awready 和 wready 同时拉高导致数据丢失现象仿真波形里看到 awready 和 wready 在同一拍拉高但存储侧只写进去了一拍数据后面几拍全丢了。原因状态机在 IDLE 状态同时判断 awvalid 和 wvalid如果两个同时为高就直接跳 WRITE但跳转的那一拍没有把 aw 的地址信息锁存下来导致 WRITE 状态里用的地址是上一笔的残留值。解决在 IDLE 跳 WRITE 的那一拍用组合逻辑把 awaddr 和 wdata 同时打入寄存器或者改成先跳 GET_AW 再跳 WRITE多一个周期但逻辑清晰。我后来统一改成后者宁可多一拍也不冒这个险。5.2 坑二B 响应提前拉高导致 PC 端报错现象PC 端驱动打印 unexpected completionILA 波形显示 bvalid 在 wlast 之前就拉高了。原因状态机在 WRITE 状态里判断 wlast 的条件写成了 wvalid wready但 wready 是存储侧给的如果存储侧提前拉高 wreadywlast 还没到就误判了。解决wlast 的判断必须用 wvalid wready s_axi_wlast 三个条件与缺一不可。另外 B 响应的 bresp 要固定给 2b00OKAY除非你真的要报错否则 XDMA 收到 SLVERR 会直接断链路。5.3 坑三读通道 rlast 与 arlen 不匹配现象PC 端读回来的数据整体偏移了 4 个字节或者最后几个字节是 0。原因AR 通道的 arlen 是突发长度减一但 R 通道的 rlast 是在第 arlen1 拍拉高。如果状态机里用 arlen 直接计数就会少发一拍或者多发一拍。解决在 AR 握手时把 arlen 锁存到 arlen_regR 状态里用一个计数器从 0 计到 arlen_reg计数器等于 arlen_reg 时拉高 rlast。计数器位宽要够arlen 最大 255所以计数器至少 8 位。5.4 坑四跨时钟域导致数据错位现象XDMA 的时钟和存储侧时钟不同频上板后偶尔读到错误数据仿真却没问题。原因AW/W/AR/R 通道的信号没有做跨时钟域同步亚稳态导致握手失败。解决在 XDMA 和 Slave 之间加异步 FIFO或者用双寄存器同步器处理 valid/ready 信号。注意 ready 信号跨时钟域要用脉冲同步器不能简单打两拍。5.5 坑五4KB 边界检查漏掉了写通道现象PC 端连续写 8KB 数据前 4KB 正常后 4KB 全错。原因地址译码只检查了读通道的 4KB 边界写通道的地址累加逻辑没有做边界判断导致第二笔突发跨了 4KB 边界但 Slave 没有报错数据写到了错误的存储位置。解决在 WRITE 状态里每拍检查 aw_addr_reg[11:0] 传输字节数是否超过 0x1000超过就拉高 bresp 的 SLVERR 并提前结束突发。虽然 XDMA 不一定会重试但至少不会静默写错。6. 进阶技巧用 AXI-Stream 做数据旁路Slave 核跑通之后如果数据不需要落到存储而是直接转发给另一个模块处理可以在 Slave 内部加一个 AXI-Stream 旁路通道。具体做法是在 WRITE 状态里判断地址是否落在旁路窗口如果是就不写存储直接把 wdata 推到 m_axis_tdata 上同时拉高 m_axis_tvalid。读通道同理从 s_axis_tdata 取数据填到 rdata 上。这个旁路通道的握手逻辑要注意AXI-Stream 的 tready 是反压信号如果下游模块没准备好Slave 必须把 wready 拉低否则数据就丢了。我一般用一个深度 8 的 FIFO 做缓冲FIFO 满的时候拉低 wready这样既能吸收突发又不会丢数据。验证旁路通道是否正常最简单的办法是在 PC 端发一笔写事务地址落在旁路窗口然后在 ILA 里看 m_axis_tvalid 和 m_axis_tdata 是否跟 wdata 一致。如果 tvalid 拉高了但 tdata 是旧值说明 FIFO 的读使能时序有问题检查 fifo_rd_en 是不是在 tvalid 之前就拉高了。从那以后我每次写完 Slave 核都会先用一个 1KB 的随机数据做回环测试PC 端写进去再读出来逐字节比对。这个习惯帮我省掉了至少三次上板后才发现数据错位的返工。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑