
通过 AXI Bridge 让 FPGA 主动访问主机内存一、功能总览二、基础知识2.1 AXI4‑Lite 协议速览2.2 `simple_ctrl` 寄存器映射2.3 触发写机制2.4 VIP(Verification IP)简介三、复现步骤3.1 AXI4‑Lite 转发控制器的实现与 VIP 仿真3.1.1、DUT 设计:`simple_ctrl.v`3.1.2、仿真验证平台 `tb_simple_ctrl_vip.sv`3.1.3、Vivado 工程 Tcl 脚本 `run_axi_vip_xsim.tcl`3.1.4、运行测试3.2 生成 Bitstream3.2.1、创建Block Design3.2.2、全自动 Bitstream 生成脚本3.2.3、运行全自动脚本,生成bitstream3.3 主机(Host)侧测试3.3.1、创建目录3.3.2、生成驱动代码3.3.3、驱动源码解析3.3.4、编译并运行3.3.5、加载驱动四、总结五、参考链接本文将带你从零开始,搭建一个完整的 FPGA‑to‑Host 数据通路。即使你刚接触 PCIe 与 AXI,也能跟着一步步理解原理、仿真验证,并最终在真实板卡上看到结果。一、功能总览在很多 FPGA 加速场景中,我们希望FPGA 主动把数据写入主机内存,而不是让主机 CPU 一次次去读 FPGA。例如:高速数据采集卡持续将采样数据推送到主机内存;FPGA 做预处理后,把结果直接放入应用程序的缓冲区。本项目的目标就是实现这一链路,并尽量保持设计极简,方便快速生成 Bitstream 和验证。硬件设计只包含两个部分:XDMA IP(DMA/Bridge Subsystem for PCI Express)负责 PCIe 链路与 DMA 传输,本身内置 AXI Bridge 功能,可以把 FPGA 侧的 AXI 事务转换为 PCIe 事务,从而访问主机内存。自研 AXI4‑Lite 转发控制器(simple_ctrl)一个极简的寄存器配置 + 写触发模块。对外提供一个AXI4‑Lite Slave 接口,挂接在 CPU 的配置空间(BAR0)上,软件可以读写其寄存器。对内提供一个AXI4‑Lite Master 接口,当软件配置好目标地址、写入数据并置起“触发位”后,该模块会自动发起一次 Master 写事务,把数据写到预设地址,然后自动清零触发位。工作流程:主机驱动分配一块连续的 DMA 内存,并计算出其在 PCIe 地址空间中的映射地址。驱动通过 BAR0 配置 AXI Bridge 的地址转换寄存器,使得 FPGA 用到的 AXI 地址能被正确路由到那一段主机内存。驱动通过 BAR0 向simple_ctrl的 Slave 接口写入:目标 AXI 地址、要写入的数据、触发位。simple_ctrl发起 AXI Master 写事务,经过 XDMA 转换成 PCIe 写 TLP,最终修改主机内存的值。主机驱动读出内存,确认数据已被 FPGA 成功修改。整篇文章会分三步带你复现这个流程:开发simple_ctrl并用 VIP 仿真验证生成 FPGA Bitstream(全自动脚本)开发主机驱动,上板测试二、基础知识开始动手前,我们先快速补一下需要用到的基础知识。2.1 AXI4‑Lite 协议速览AXI4‑Lite 是 ARM AMBA 协议族中的轻量级存储映射协议,专门用于简单的寄存器读写和外设控制,没有复杂的突发传输。它使用 5 个独立的单向通道:写地址通道(AW):主机发送目标地址和控制信息。写数据通道(W):主机发送要写入的数据。写响应通道(B):从机返回写事务状态(成功或错误)。读地址通道(AR):主机发送要读取的地址。读数据通道(R):从机返回数据和状态。每个通道都使用VALID/READY 握手机制:只有发送方把 VALID 拉高、接收方把 READY 拉高的同一拍,数据才会真正传输。这种机制让双方可以以各自的速度工作,避免了严格时序约束。本项目中,simple_ctrl的 Slave 接口用于接收 CPU 的配置,Master 接口用于对外发起一次 AXI4‑Lite 写事务。2.2simple_ctrl寄存器映射simple_ctrl的 Slave 接口仅映射了 3 个 32 位寄存器:偏移地址寄存器名读写属性描述0x00addr_regRWMaster 写事务的目标 AXI 地址(由 FPGA 发出)0x04ctrl_regRW控制寄存器:bit[0] 写 1 触发 Master 写;事务完成后硬件自动清 00x08data_regRWMaster 写事务要写入的 32 位数据如果访问这三个寄存器之外的地址,模块会返回SLVERR(从机错误)响应,方便调试。2.3 触发写机制我们设计的触发流程非常直接:软件通过 Slave 接口先写好addr_reg和data_reg。然后向ctrl_reg的 bit[0] 写入 1。硬件检测到ctrl_reg[0]有效后,立即在 Master 接口上发起一次 AXI4‑Lite 写事务:在 AW 通道上给出addr_reg的值;在 W 通道上给出data_reg的值(字节选通置为全有效,即 4 字节全部更新);等待 B 通道返回响应。一旦收到写响应,硬件自动将ctrl_reg[0]清零,并回到空闲状态,等待下一次触发。这种“软件置位、硬件自动清零”的方式可以避免软件轮询复杂的完成标志,也防止同一个触发被多次执行。2.4 VIP(Verification IP)简介VIP是仿真中用到的“验证 IP”,它可以模拟 AXI 主机或从机的行为,并提供丰富的激励生成和结果检查功能,让我们在没有真实硬件的情况下就能全面测试设计。Xilinx 提供了图形化界面和 Tcl 脚本来配置 AXI VIP。本文用到两个实例:axi_cfg_mst_vip:配置为AXI4‑Lite Master,模拟 CPU 读写simple_ctrl的寄存器。axi_target_slv_vip:配置为AXI4‑Lite Slave,模拟 FPGA 要写入的“目标外设”,接收并检查 Master 写事务的内容。通过这两个 VIP,我们能够检查寄存器复位值、写入正确性、触发写行为以及地址映射是否完全符合预期。三、复现步骤3.1 AXI4‑Lite 转发控制器的实现与 VIP 仿真我们先设计simple_ctrl模块,然后用 SystemVerilog 搭建仿真平台,在 Vivado 中一键运行仿真,确保逻辑完全正确。3.1.1、DUT 设计:simple_ctrl.v下面是完整的 Verilog 实现。设计采用经典的三段式风格:Slave 写逻辑、Slave 读逻辑、Master 写状态机,代码中已包含详细注释。cat simple_ctrl.v 'EOF' // ---------------------------------------------------------------------- // Module: simple_ctrl // Description: // - AXI4-Lite Slave for register access: // offset 0x00 : addr_reg [31:0] (RW) target address // offset 0x04 : ctrl_reg [0] (RW) start trigger (bit0) // offset 0x08 : data_reg [31:0] (RW) data to be written // - When ctrl_reg[0] is written as 1, the module performs one // AXI4-Lite Master write transaction to addr_reg with data_reg. // - After completion, ctrl_reg[0] is automatically cleared. // ---------------------------------------------------------------------- module simple_ctrl ( // System signals input wire clk, input wire rst_n, // AXI4-Lite Slave interface (for register configuration) input wire [31:0] s_awaddr, input wire [2:0] s_awprot, input wire s_awvalid, output reg s_awready, input wire [31:0] s_wdata, input wire [3:0] s_wstrb, input wire s_wvalid, output reg s_wready, output reg [1:0] s_bresp, output reg s_bvalid, input wire s_bready, input wire [31:0] s_araddr, input wire [2:0] s_arprot, input wire s_arvalid, output reg s_arready, output reg [31:0] s_rdata, output reg [1:0] s_rresp, output reg s_rvalid, input wire s_rready, // AXI4-Lite Master interface (for writing data_reg) output reg [31:0] m_awaddr, output reg [2:0] m_awprot, output reg m_awvalid, input wire m_awready, output reg [31:0] m_wdata, output reg [3:0] m_wstrb, output reg m_wvalid, input wire m_wready, input wire [1:0] m_bresp, input wire m_bvalid, output reg m_bready ); localparam [1:0] AXI_RESP_OKAY = 2'b00; localparam [1:0] AXI_RESP_SLVERR = 2'b10; localparam [1:0] MW_IDLE = 2'd0, MW_REQ = 2'd1, MW_RESP = 2'd2; reg [31:0] addr_reg; reg [31:0] ctrl_reg; reg [31:0] data_reg; // Decode the local offset within the AXI segment. The interconnect can // forward the absolute AXI address, e.g. 0x20000 for BAR0+0x20000. reg [14:0] wr_addr_word; reg [31:0] wr_data_reg; reg [3:0] wr_strb_reg; reg wr_addr_captured; reg wr_data_captured; reg [14:0] rd_addr_word; reg rd_addr_captured; reg [1:0] mw_state; reg start_seen; reg ctrl_auto_clear; function automatic [31:0] apply_wstrb( input [31:0] prior_data, input [31:0] new_data, input [3:0] byte_strobe ); begin apply_wstrb[7:0] = byte_strobe[0] ? new_data[7:0] : prior_data[7:0]; apply_wstrb[15:8] = byte_strobe[1] ? new_data[15:8] : prior_data[15:8]; apply_wstrb[23:16] = byte_strobe[2] ? new_data[23:16] : prior_data[23:16]; apply_wstrb[31:24] = byte_strobe[3] ? new_data[31:24] : prior_data[31:24]; end endfunction // ---------------------------------------------------------------- // AXI4-Lite Slave write logic // ---------------------------------------------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin s_awready = 1'b0; s_wready = 1'b0; s_bresp = AXI_RESP_OKAY; s_bvalid = 1'b0; addr_reg = 32'h0000_0000; ctrl_reg = 32'h0000_0000; data_reg = 32'h0000_0000; wr_addr_word = 15'd0; wr_data_reg = 32'h0000_0000; wr_strb_reg = 4'h0; wr_addr_captured = 1'b0; wr_data_captured = 1'b0; end else begin if (ctrl_auto_clear) begin ctrl_reg[0] = 1'b0; end if (s_bvalid) begin s_awready = 1'b0; s_wready = 1'b0; end else begin s_awready = !wr_addr_captured; s_wready = !wr_data_captured; end if (s_awready s_awvalid) begin wr_addr_word = s_awaddr[16:2]; wr_addr_captured = 1'b1; s_awready = 1'b0; end if (s_wready s_wvalid) begin wr_data_reg = s_wdata; wr_strb_reg = s_wstrb; wr_data_captured = 1'b1; s_wready = 1'b0; end if (!s_bvalid wr_addr_captured wr_data_captured) begin s_bresp = AXI_RESP_OKAY; case (wr_addr_word) 15'd0: addr_reg = apply_wstrb(addr_reg, wr_data_reg, wr_strb_reg); 15'd1: ctrl_reg = apply_wstrb(ctrl_reg, wr_data_reg, wr_strb_reg); 15'd2: data_reg = apply_wstrb(data_reg, wr_data_reg, wr_strb_reg); default: s_bresp = AXI_RESP_SLVERR; endcase s_bvalid = 1'b1; wr_addr_captured = 1'b0; wr_data_captured = 1'b0; end if (s_bvalid s_bready) begin s_bvalid = 1'b0; end end end // ---------------------------------------------------------------- // AXI4-Lite Slave read logic // ---------------------------------------------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin s_arready = 1'b0; s_rdata = 32'h0000_0000; s_rresp = AXI_RESP_OKAY; s_rvalid = 1'b0; rd_addr_word = 15'd0; rd_addr_captured = 1'b0; end else begin if (s_rvalid) begin s_arready = 1'b0; end else begin s_arready = !rd_addr_captured; end if (s_arready s_arvalid) begin rd_addr_word = s_araddr[16:2]; rd_addr_captured = 1'b1; s_arready = 1'b0; end if (!s_rvalid rd_addr_captured) begin s_rresp = AXI_RESP_OKAY; case (rd_addr_word) 15'd0: s_rdata = addr_reg; 15'd1: s_rdata = ctrl_reg; 15'd2: s_rdata = data_reg; default: begin s_rdata = 32'h0000_0000; s_rresp = AXI_RESP_SLVERR; end endcase s_rvalid = 1'b1; rd_addr_captured = 1'b0; end if (s_rvalid s_rready) begin s_rvalid = 1'b0; end end end // ---------------------------------------------------------------- // AXI4-Lite Master write logic // ---------------------------------------------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin m_awaddr = 32'h0000_0000; m_awprot = 3'b000; m_awvalid = 1'b0; m_wdata = 32'h0000_0000; m_wstrb = 4'hF; m_wvalid = 1'b0; m_bready = 1'b0; mw_state = MW_IDLE; start_seen = 1'b0; ctrl_auto_clear = 1'b0; end else begin ctrl_auto_clear = 1'b0; if (!ctrl_reg[0]) begin start_seen = 1'b0; end case (mw_state) MW_IDLE: begin m_awvalid = 1'b0; m_wvalid = 1'b0; m_bready = 1'b0; if (ctrl_reg[0] !start_seen) begin m_awaddr = addr_reg; m_awprot = 3'b000; m_wdata = data_reg; m_wstrb = 4'hF; m_awvalid = 1'b1; m_wvalid = 1'b1; start_seen = 1'b1; mw_state = MW_REQ; end end MW_REQ: begin if (m_awvalid m_awready) begin m_awvalid = 1'b0; end if (m_wvalid m_wready) begin m_wvalid = 1'b0; end if (((m_awvalid m_awready) || !m_awvalid) ((m_wvalid m_wready) || !m_wvalid)) begin m_bready = 1'b1; mw_state = MW_RESP; end end MW_RESP: begin if (m_bvalid m_bready) begin m_bready = 1'b0; ctrl_auto_clear = 1'b1; mw_state = MW_IDLE; end end default: begin mw_state = MW_IDLE; end endcase end end // Keep the input referenced so lint does not flag it as unused. wire _unused_ok = {1'b0, s_awprot, s_arprot, m_bresp}; endmodule EOF解释DUT 模块simple_ctrl采用典型的三段式状态机实现:Slave 写逻辑:通过wr_addr_word捕获地址,wr_data_reg和wr_strb_reg捕获数据,利用apply_wstrb函数实现部分写(字节选通)。当地址和数据均就绪后,按照解码的寄存器地址更新addr_reg、ctrl_reg或data_reg。非法地址返回SLVERR。Slave 读逻辑:地址捕获后组合逻辑读取对应寄存器,同样处理非法地址。Master 写状态机(MW_IDLE→MW_REQ→MW_RESP):在空闲状态检测ctrl_reg[0]且未发生过此次触发 (start_seen为 0),同时置起m_awvalid和m_wvalid,进入请求态。等待 AW 和 W 通道握手完成后,转为响应态,等待m_bvalid握手,返回后自动清除ctrl_reg[0]并回到空闲。该设计可综合,且完全遵循 AXI4-Lite 握手规范。3.1.2、仿真验证平台tb_simple_ctrl_vip.svTestbench 使用 SystemVerilog 并例化了两个 AXI VIP,同时包含了一系列自动检查任务。cat tb_simple_ctrl_vip.sv 'EOF' `timescale 1ns/1ps import axi_vip_pkg::*; import axi_cfg_mst_vip_pkg::*; import axi_target_slv_vip_pkg::*; module tb_simple_ctrl_vip; reg clk; reg rst_n; wire [31:0] s_awaddr; wire [2:0] s_awprot; wire s_awvalid; wire s_awready; wire [31:0] s_wdata; wire [3:0] s_wstrb; wire s_wvalid; wire s_wready; wire [1:0] s_bresp; wire s_bvalid; wire s_bready; wire [31:0] s_araddr; wire [2:0] s_arprot; wire s_arvalid; wire s_arready; wire [31:0] s_rdata; wire [1:0] s_rresp; wire s_rvalid; wire s_rready; wire [31:0] m_awaddr; wire [2:0] m_awprot; wire m_awvalid; wire m_awready; wire [31:0] m_wdata; wire [3:0] m_wstrb; wire m_wvalid; wire m_wready; wire [1:0] m_bresp; wire m_bvalid; wire m_bready; axi_cfg_mst_vip_mst_t cfg_mst_agent; axi_target_slv_vip_slv_t target_slv_agent; integer cycle_count; integer target_write_count; reg [31:0] last_target_addr; reg [31:0] last_target_data; reg [3:0] last_target_strb; reg [2:0] last_target_prot; reg [31:0] readback; always #5 clk = ~clk; simple_ctrl dut ( .clk (clk), .rst_n (rst_n), .s_awaddr (s_awaddr), .s_awprot (s_awprot), .s_awvalid(s_awvalid), .s_awready(s_awready), .s_wdata (s_wdata), .s_wstrb (s_wstrb), .s_wvalid (s_wvalid), .s_wready (s_wready), .s_bresp (s_bresp), .s_bvalid (s_bvalid), .s_bready (s_bready), .s_araddr (s_araddr), .s_arprot (s_arprot), .s_arvalid(s_arvalid), .s_arready(s_arready), .s_rdata (s_rdata), .s_rresp (s_rresp), .s_r