基于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]
工作流程如下:
-
主机通过 mmap 访问映射地址:
c uint32_t *base = (uint32_t*)mmap(...); base[0] = 1; // 向 offset 0x000 写入,触发 ctrl_reg 更新 - XDMA 将该写操作转换为 AXI4 写事务,发送至自定义Slave;
-
Slave 解析地址,将数据写入
ctrl_reg; -
FPGA内部逻辑检测到
ctrl_reg[0]被置位,启动ADC采样; - 采集数据存入BRAM或FIFO;
-
主机发起 burst read 操作读取
data_mem区域; - 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),仅供参考
6703

被折叠的 条评论
为什么被折叠?



