自编AXI4从核实现PCIe数据交互

AI助手已提取文章相关产品:

基于XDMA核和AXI4协议实现PCIe数据读写(二):自编AXI4 Slave核

在现代高速数据采集与边缘计算系统中,FPGA作为连接主机处理器与专用硬件逻辑的桥梁,承担着越来越关键的角色。尤其是在使用Xilinx平台构建PCIe接口系统时,XDMA(Xilinx Direct Memory Access)IP核已成为实现CPU与FPGA间高效通信的标准方案。然而,仅仅部署XDMA还远远不够——要真正让主机“控制”或“访问”FPGA内部功能模块,必须设计一个符合规范、稳定可靠的 AXI4从设备(Slave)

这正是问题的核心:XDMA作为主控端(Master),需要有明确的目标地址空间去读写。而这个目标,往往就是我们自己编写的AXI4 Slave外设。它可能是配置寄存器组、状态反馈单元,也可能是数据缓冲区的入口。能否正确理解并实现这一接口,直接决定了整个系统的可用性与性能上限。


为什么不能只用现成IP?

你可能会问:“Vivado不是提供了AXI4-Lite Slave模板吗?为什么不直接用?”
答案是:够用但不够灵活。

标准IP适合简单场景,比如仅需几个寄存器进行配置。但在实际项目中,我们常遇到以下需求:
- 支持突发传输(Burst Transfer),而非单次访问;
- 实现连续地址映射的大块内存区域;
- 精确控制响应时机,避免握手死锁;
- 与用户逻辑深度耦合,例如触发ADC采样、启动DMA搬运等动作。

这些都要求我们跳出黑盒IP,亲手编写一个 可综合、可验证、行为可控的AXI4 Full Slave模块 。本文将带你一步步完成这样一个轻量级但完整的AXI4 Slave设计,并与XDMA协同工作,打通PCIe链路中的“最后一公里”。


AXI4协议的本质:分离通道 + 握手机制

AXI4之所以能胜任高带宽、低延迟的应用,关键在于其独特的架构设计。它不像传统总线那样共用地址/数据线,而是将读写操作完全拆解为独立通道:

  • AW/W/B :写地址、写数据、写响应
  • AR/R :读地址、读数据

每个通道都采用 valid / ready 双向握手机制。只有当双方同时拉高信号时,才算完成一次有效传输。这种机制允许流水线化处理、跨时钟域桥接,也为乱序传输提供了基础。

更重要的是,AXI4支持 突发传输(Burst Transfer) ,即一次地址请求后可连续传输多个数据拍(beat)。类型包括:
- INCR :地址递增,最常用;
- FIXED :固定地址,如FIFO写入;
- WRAP :回绕地址,用于Cache对齐。

我们在设计Slave时,至少要完整支持 INCR 类型,才能满足XDMA的高性能数据交互需求。

此外,还需注意一些细节:
- 地址需按数据宽度对齐(32位宽则4字节对齐);
- 每个beat通过 WSTRB 指示哪些字节有效;
- 必须返回 BRESP RRESP 响应信号,正常为 OKAY ,错误时返回 SLVERR
- LAST 信号标识burst最后一个beat,不可遗漏。

这些看似琐碎的规定,一旦出错就会导致主机端超时甚至驱动崩溃。因此,我们的Verilog实现必须严格遵循协议时序。


自定义AXI4 Slave的设计思路

我们的目标很明确:构建一个既能被XDMA识别,又能服务于用户逻辑的小型外设。具体功能包括:

  • 映射一段物理地址空间(如 0xA000_0000 开始的4KB区域);
  • 提供若干控制/状态寄存器供主机配置;
  • 设立一块数据缓冲区用于批量读写;
  • 支持最多16-beat的INCR突发传输;
  • 返回正确的响应,确保握手不卡死。

为了达成这一目标,模块内部划分为几个关键部分协同运作:

地址译码单元

这是所有访问的第一道关卡。输入地址经过基地址比对和偏移提取,判断其属于哪个功能区。例如:

assign addr_valid = (s_axi_awaddr[31:OPT_MEM_ADDR_BITS] == 12'hA00);

上面这行代码表示:只有高地址位匹配 0xA00 才认为是本设备的有效请求。后续再根据低位索引具体寄存器或内存位置。

写通道处理机

写操作涉及三个阶段:地址、数据、响应。

首先是AW通道接收地址信息。我们用状态标志 w_ready_flag 表示已准备好接收数据。一旦 awvalid && awready 成立,就锁存起始地址和burst长度:

if (s_axi_awvalid && s_axi_awready) begin
    aw_addr_reg   <= s_axi_awaddr[OPT_MEM_ADDR_BITS-1:ADDR_LSB];
    burst_len_reg <= s_axi_awlen;
    w_ready_flag  <= 1'b1;
end

接着W通道逐拍传入数据。每收到一拍且 wvalid && wready 成立,就根据当前地址更新对应寄存器或内存,并递增地址指针。遇到 wlast 则置位 write_done ,通知响应通道可以发送ACK。

最后B通道发出响应。这里要注意: 必须等到主机接收了响应(bready为高)之后,才能清除 bvalid ,否则会丢失响应。典型做法如下:

if (write_done && !s_axi_bvalid)
    s_axi_bvalid <= 1;
else if (s_axi_bready && s_axi_bvalid)
    s_axi_bvalid <= 0;
读通道处理机

读操作稍复杂些,因为R通道要在arvalid之后才开始输出数据。

当AR通道握手成功时,记录起始地址和burst长度,并立即启动第一拍数据输出( rvalid=1 )。此时 rlast 根据burst长度决定是否为最后一拍。

随后在每次 rvalid && rready 成立时,递增地址、输出下一拍数据,直到计数达到 arlen 。最终一拍需拉高 rlast 并在下周期关闭 rvalid

特别提醒:不要试图在一个always块里处理全部逻辑。建议将读写通道分开管理,各自维护自己的地址、计数器和状态标志,避免相互干扰。

寄存器文件与数据缓冲

我们定义一组简单的内部寄存器:

reg [31:0] ctrl_reg;
reg [31:0] status_reg;
reg [31:0] data_mem [0:255]; // 1KB buffer

其中前两个用于控制和状态交互,后者模拟一片共享内存区域。地址译码时通过判断 aw_addr_reg 的值来选择写入目标。

对于 byte-enable 支持,可通过 WSTRB 信号实现按字节更新。虽然示例中未展开,但在实际工程中强烈建议加入,以兼容不同大小的数据访问。


关键代码片段解析

以下是精简后的核心逻辑结构,保留了可综合性和基本功能性:

module axi_slave #(
    parameter C_S_AXI_ADDR_WIDTH = 12,
    parameter C_S_AXI_DATA_WIDTH = 32
)(
    input wire s_axi_aclk,
    input wire s_axi_aresetn,

    // Write Address Channel
    input wire [C_S_AXI_ADDR_WIDTH-1:0] s_axi_awaddr,
    input wire [7:0]                    s_axi_awlen,
    input wire [2:0]                    s_axi_awsize,
    input wire [1:0]                    s_axi_awburst,
    input wire                          s_axi_awvalid,
    output reg                          s_axi_awready,

    // Write Data Channel
    input wire [C_S_AXI_DATA_WIDTH-1:0] s_axi_wdata,
    input wire [(C_S_AXI_DATA_WIDTH/8)-1:0] s_axi_wstrb,
    input wire                          s_axi_wlast,
    input wire                          s_axi_wvalid,
    output reg                          s_axi_wready,

    // Write Response Channel
    output reg [1:0]                    s_axi_bresp,
    output reg                          s_axi_bvalid,
    input wire                          s_axi_bready,

    // Read Address Channel
    input wire [C_S_AXI_ADDR_WIDTH-1:0] s_axi_araddr,
    input wire [7:0]                    s_axi_arlen,
    input wire [2:0]                    s_axi_arsize,
    input wire [1:0]                    s_axi_arburst,
    input wire                          s_axi_arvalid,
    output reg                          s_axi_arready,

    // Read Data Channel
    output reg [C_S_AXI_DATA_WIDTH-1:0] s_axi_rdata,
    output reg [1:0]                    s_axi_rresp,
    output reg                          s_axi_rlast,
    output reg                          s_axi_rvalid,
    input wire                          s_axi_rready
);

localparam integer ADDR_LSB          = (C_S_AXI_DATA_WIDTH/32) + 2;
localparam integer OPT_MEM_ADDR_BITS = C_S_AXI_ADDR_WIDTH - ADDR_LSB;

reg [OPT_MEM_ADDR_BITS-1:0] aw_addr_reg;
reg [7:0]                   burst_len_reg;
reg                         w_ready_flag;
reg                         write_done;

// 用户寄存器
reg [31:0] ctrl_reg;
reg [31:0] status_reg;
reg [31:0] data_mem [0:255];

// 地址有效性检查
assign addr_valid = (s_axi_awaddr[31:OPT_MEM_ADDR_BITS] == 12'hA00);

// AW通道准备就绪
always @(posedge s_axi_aclk or negedge s_axi_aresetn)
begin
    if (!s_axi_aresetn)
        s_axi_awready <= 1'b0;
    else
        s_axi_awready <= (s_axi_awvalid && !s_axi_awready) ? 1'b1 : 
                         (s_axi_awready && s_axi_awvalid && s_axi_wready) ? 1'b0 : s_axi_awready;
end

// 锁存AW信息
always @(posedge s_axi_aclk)
begin
    if (s_axi_awvalid && s_axi_awready)
    begin
        aw_addr_reg   <= s_axi_awaddr[OPT_MEM_ADDR_BITS-1:ADDR_LSB];
        burst_len_reg <= s_axi_awlen;
        w_ready_flag  <= 1'b1;
    end
end

// W通道就绪控制
always @(posedge s_axi_aclk or negedge s_axi_aresetn)
begin
    if (!s_axi_aresetn)
        s_axi_wready <= 1'b0;
    else
        s_axi_wready <= w_ready_flag;
end

// 数据写入与地址递增
always @(posedge s_axi_aclk)
begin
    if (s_axi_wvalid && s_axi_wready)
    begin
        case (aw_addr_reg)
            10'h000: ctrl_reg <= s_axi_wdata;
            10'h001: status_reg <= s_axi_wdata;
            default: 
                if (aw_addr_reg >= 10'h100 && aw_addr_reg < 10'h200)
                    data_mem[aw_addr_reg - 10'h100] <= s_axi_wdata;
        endcase
        aw_addr_reg <= aw_addr_reg + 1;

        if (s_axi_wlast)
        begin
            write_done <= 1;
            w_ready_flag <= 0;
        end
    end
end

// B响应生成
always @(posedge s_axi_aclk or negedge s_axi_aresetn)
begin
    if (!s_axi_aresetn)
    begin
        s_axi_bvalid <= 0;
        s_axi_bresp  <= 2'b00;
    end
    else if (write_done && !s_axi_bvalid)
    begin
        s_axi_bvalid <= 1;
        s_axi_bresp  <= 2'b00; // OKAY
    end
    else if (s_axi_bready && s_axi_bvalid)
    begin
        s_axi_bvalid <= 0;
    end
end

其余读通道逻辑类似,此处不再重复列出。完整版本可在GitHub仓库中获取。


典型应用场景:主机控制+FPGA执行

设想一个典型的高速采集系统:

[PC Host]
    ↓ PCIe x8
[XDMA IP] ←→ [Custom AXI4 Slave]
                     ↓
             [ADC Sampling Logic]
                     ↓
                 [Data FIFO]

工作流程如下:

  1. 主机通过 mmap 访问映射地址:
    c uint32_t *base = (uint32_t*)mmap(...); base[0] = 1; // 向 offset 0x000 写入,触发 ctrl_reg 更新
  2. XDMA 将该写操作转换为 AXI4 写事务,发送至自定义Slave;
  3. Slave 解析地址,将数据写入 ctrl_reg
  4. FPGA内部逻辑检测到 ctrl_reg[0] 被置位,启动ADC采样;
  5. 采集数据存入BRAM或FIFO;
  6. 主机发起 burst read 操作读取 data_mem 区域;
  7. Slave 按地址顺序返回数据,完成传输。

整个过程无需CPU干预数据搬运,极大提升了效率。


常见问题及调试建议

即便逻辑看似正确,仍可能因时序或握手机制不当导致失败。以下是一些典型问题及其解决方案:

问题现象 可能原因 解决方法
主机写操作超时 B通道未返回响应 检查 write_done 是否正确置位, bvalid 是否保持直到 bready
读数据错乱或少一拍 rvalid 提前撤销 确保 rvalid rready 后才降为低,且 rlast 准确标记最后一拍
突发传输中断 未支持 arlen > 0 必须在读通道中跟踪 beat 数并持续输出,直到完成所有 beats
驱动加载失败 地址未在Block Design中分配 在Vivado中为Slave分配固定地址范围并生成约束

调试手段推荐:
- 使用 ILA(Integrated Logic Analyzer) 抓取 awvalid , wdata , rdata , bvalid 等关键信号;
- 添加 AXI VIP 进行仿真验证,模拟异常情况如延迟ready、early valid等;
- 编写简单C程序测试基本读写,逐步扩展到burst模式。


设计优化与进阶方向

虽然上述实现已能满足大多数应用,但在更高要求场景下仍有改进空间:

性能优化
  • 引入 AXI Interconnect 支持多Slave仲裁;
  • 使用 BRAM 而非分布式RAM 存储大数据块,节省LUT资源;
  • 加入 FIFO缓冲 ,解耦XDMA与用户逻辑速率差异。
功能增强
  • 实现 MSI/MSI-X中断上报 ,替代轮询方式提升响应速度;
  • 集成 AXI4-Stream输出接口 ,用于实时流式数据转发;
  • 支持 Cache一致性(ACE) ,适用于Zynq UltraScale+ MPSoC平台。
可维护性提升
  • 将寄存器定义抽取为 .h 文件,供软硬件团队统一使用;
  • 使用 IP Integrator封装 ,便于复用;
  • 添加 parameterized参数配置 ,适配不同数据/地址宽度。

结语

掌握AXI4 Slave的自主开发能力,意味着你不再受限于标准IP的功能边界。无论是构建定制外设、实现高性能数据交互,还是为AI加速器提供前端接口,这项技能都能让你在FPGA系统设计中游刃有余。

更重要的是,通过亲手实现协议层逻辑,你会对“主从交互”、“握手机制”、“突发传输”等概念有更深刻的理解——这种底层认知,远比调用一个黑盒IP来得珍贵。

未来的技术演进中,PCIe带宽将持续提升(Gen4/Gen5普及),AXI总线也将向更复杂的Coherent互联发展。而今天你写的每一行Verilog,都是迈向高性能异构计算架构的坚实一步。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

您可能感兴趣的与本文相关内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值