本章节讲述如何使用FPGA(纯verilog语言)实现NVME SSD硬盘的读写控制。
参考: 三、实战篇-NVME SSD控制之FPGA实现(一)-CSDN博客
三、实战篇-NVME SSD控制之FPGA实现(二)-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(四)_fpga实现ssd的控制-CSDN博客
二、实战篇-NVME SSD控制之ZYNQ实现(五)-CSDN博客

前面两节分别介绍了FPGA和NVME SSD的PCIE CFG空间的配置 以及 NVME SSD的寄存器配置。通过 NVME SSD的寄存器配置将ADMIN提交、完成队列基地址以及长度给定义好了,本节通过ADMIN命令读取NVME SSD硬盘基本信息、创建IO提交、完成队列。通过IO队列发送指定命令就可以进行NVME SSD存储空间的访问。
前面讲过队列提交、完成流程,这里就只放一张流程图在这里:

Admin命令集(Admin Command Set)如下


这里先简单介绍一下提交、完成队列在FPGA中实现的流程:
①先将提交队列数据写入RAM中;
②然后写门铃寄存器通知NVME SSD硬盘;
③NVME SSD通过M_AXI总线读取提交数据;
④执行完命令后,NVME SSD通过M_AXI总线发送完成数据。
BD工程中连线如下:


地址分配:

这里输入axi_smartconnect输入分别为AXI_PCIE IP的M_AXI总线和FDMA的M_AXI总线。输出连接DDR3 IP和M_AXI_SLAVE模块
这里的FDMA使用的米联客的FDMA IP,用于控制DDR3。主要负责DDR3数据读写:写NVME数据时,先将数据写入DDR3中,然后再通知NVME来读取数据;读NVME数据时,先通知NVME将数据写入DDR3中,再通过FDMA读取数据验证。
M_AXI_SLAVE(axi4_full_slave)模块主要讲AXI_PCIE IP的M_AXI总线转换为本地读写信号,方便数据读写。
M_AXI_SLAVE(axi4_full_slave)模块接口定义如下:
module axi4_full_slave #(
parameter C_M_AXI_ADDR_WIDTH = 32,
parameter C_M_AXI_DATA_WIDTH = 128,
parameter STRB_WIDTH = C_M_AXI_DATA_WIDTH / 8
)(
// AXI 全局时钟复位
input wire aclk,
input wire aresetn,
//———- AXI 写地址通道 AW ———-
input wire [C_M_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 wire s_axi_awready,
//———- AXI 写数据通道 W ———-
input wire [C_M_AXI_DATA_WIDTH-1:0] s_axi_wdata,
input wire [STRB_WIDTH-1:0] s_axi_wstrb,
input wire s_axi_wlast,
input wire s_axi_wvalid,
output wire s_axi_wready,
//———- AXI 写响应通道 B ———-
output reg [1:0] s_axi_bresp,
output reg s_axi_bvalid,
input wire s_axi_bready,
//———- AXI 读地址通道 AR ———-
input wire [C_M_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 wire s_axi_arready,
//———- AXI 读数据通道 R ———-
output reg [C_M_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,
//==================== 本地读写接口 ====================
// 写通路
output reg local_wr_en,
output reg [C_M_AXI_ADDR_WIDTH-1:0] local_wr_addr,
output reg [C_M_AXI_DATA_WIDTH-1:0] local_wr_data,
output reg [STRB_WIDTH-1:0] local_wr_strb, // 新增:字节写使能,对应 AXI wstrb
output reg local_wr_done,
// 读通路
output reg local_rd_en,
output reg [C_M_AXI_ADDR_WIDTH-1:0] local_rd_addr,
input wire [C_M_AXI_DATA_WIDTH-1:0] local_rd_data,
input wire local_rd_valid
);
ADMIN提交队列模块:

完成队列模块:

下面介绍ADMIN的提交和完成队列
提交模块信号定义:
module Sub_ram#(
parameter ADDR_WIDTH =32,
parameter DATA_WIDTH =128,
parameter IO_Q_SIZE =16'd8,//io 队列深度
parameter ADMIN_SQ_SIZE =16'd16,//admin 队列深度
parameter IDENTIFY_CTRL_BASE=32'h1020_0000,
parameter IDENTIFY_NS_BASE=32'h1020_1000,
parameter ADMIN_SQ_BASE=32'h1010_0000,
parameter ADMIN_CQ_BASE=32'h1010_1000,
parameter IO_WR_SQ_BASE=32'h1010_2000,
parameter IO_WR_CQ_BASE=32'h1010_3000,
parameter IO_RD_SQ_BASE=32'h1010_4000,
parameter IO_RD_CQ_BASE=32'h1010_5000
)(
input wire i_clk,
input wire i_rst_n,
inputi_sq_start,
outputo_sq_done,
input[15:0]i_nvme_io_deep,
input wire i_local_rd_en,
input wire [ADDR_WIDTH-1:0] i_local_rd_addr,
output [DATA_WIDTH-1:0] o_local_rd_data,
output o_local_rd_valid,
output[15:0]o_sq_Head,
outputo_sq_door_req,
inputi_cq_done,
inputi_sq_door_done
);
提交模块将数据写入RAM,然后等待M_AXI总线进行读取,不会存在写数据操作,所以这里只将local_rd_*信号接入进来。
主要流程(状态机)如下:
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
cstate<=0;
end else begin
cstate<=nstate;
end
end
always@(*)begin
case(cstate)
0:beginif(start_pdg)nstate=1;
elsenstate=0;end
//获取NvmeId_Ctr
1:beginif(bram_wr_cnt>=3) nstate=2;
elsenstate=1;end//wr_ram
2:beginnstate=3;end//door req
3:begin if(i_sq_door_done)nstate=50;
elsenstate=3;end
//等待完成
50:begin if(i_cq_done)nstate=4;
elsenstate=50;end
//获取NvmeId_Namespace
4:begin if(bram_wr_cnt>=3) nstate=5;
elsenstate=4;end//wr_ram
5:beginnstate=6;end//door req
6:begin if(i_sq_door_done)nstate=51;
elsenstate=6;end
//等待完成
51:begin if(i_cq_done)nstate=7;
elsenstate=51;end
//NvmeSetIoQueueNum 设置IO队列数量
7:begin if(bram_wr_cnt>=3) nstate=8;
elsenstate=7;end//wr_ram
8:beginnstate=9;end//door req
9:begin if(i_sq_door_done)nstate=52;
elsenstate=9;end
//等待完成
52:begin if(i_cq_done)nstate=10;
elsenstate=52;end
//NvmeCreateIoCq 创建IO队列—写完成
10:begin if(bram_wr_cnt>=3) nstate=11;
elsenstate=10;end//wr_ram
11:beginnstate=12;end//door req
12:begin if(i_sq_door_done)nstate=53;
elsenstate=12;end
//等待完成
53:begin if(i_cq_done)nstate=13;
elsenstate=53;end
//NvmeCreateIoCq 创建IO队列—读完成
13:begin if(bram_wr_cnt>=3) nstate=14;
elsenstate=13;end//wr_ram
14:beginnstate=15;end//door req
15:begin if(i_sq_door_done)nstate=54;
elsenstate=15;end
//等待完成
54:begin if(i_cq_done)nstate=16;
elsenstate=54;end
//NvmeCreateIoCq 创建IO队列—写请求
16:begin if(bram_wr_cnt>=3) nstate=17;
elsenstate=16;end//wr_ram
17:beginnstate=18;end//door req
18:begin if(i_sq_door_done)nstate=55;
elsenstate=18;end
//等待完成
55:begin if(i_cq_done)nstate=19;
elsenstate=55;end
//NvmeCreateIoCq 创建IO队列—读请求
19:begin if(bram_wr_cnt>=3)nstate=20;
elsenstate=19;end//wr_ram
20:beginnstate=21;end//door req
21:begin if(i_sq_door_done)nstate=56;
elsenstate=21;end
//等待完成
56:begin if(i_cq_done)nstate=22;
elsenstate=56;end
22:begin if(delay_cnt>=10)nstate=0;
elsenstate=22;end
endcase
end
接收到开始信号后,按照流程进行ADMIN命令配置。ADMIN命令提交顺序代码中进行了注释
调用一个双口RAM,A通道用于数据写入,B通道用于数据读取:
//A :WR B:RD
BRAM_128W_256D u_admin_bram (
.clka(i_clk),
.ena(BRAM_A_en),
.wea(BRAM_A_we),
.addra(BRAM_A_addr),
.dina(BRAM_A_din),
.douta(),
.clkb(i_clk),
.enb(BRAM_B_en),
.web(0),
.addrb(BRAM_B_addr),
.dinb(0),
.doutb(BRAM_B_dout)
);
写RAM代码如下:
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
BRAM_A_temp<=0;
end else begin
case(cstate)
0:BRAM_A_temp<=NvmeId_Ctr;
50:BRAM_A_temp<=NvmeId_Namespace;
51:BRAM_A_temp<=NvmeSetIoQueueNum;
52:BRAM_A_temp<=NvmeCreateIo_WRCq;
53:BRAM_A_temp<=NvmeCreateIo_RDCq;
54:BRAM_A_temp<=NvmeCreateIo_WRSq;
55:BRAM_A_temp<=NvmeCreateIo_RDSq;
default:BRAM_A_temp<=BRAM_A_temp;
endcase
end
end
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
BRAM_A_addr<=0;
end else begin
if(BRAM_A_en)begin
if(BRAM_A_addr>=MAX_ADDR-1)
BRAM_A_addr<=0;
else
BRAM_A_addr<=BRAM_A_addr+1;
end else begin
BRAM_A_addr<=BRAM_A_addr;
end
end
end
always@(*)begin
if(!i_rst_n)begin
BRAM_A_en<=0;
BRAM_A_we<=0;
end else begin
if(cstate==1||cstate==4||cstate==7||cstate==10||cstate==13||cstate==16||cstate==19)begin
BRAM_A_en<=1;
BRAM_A_we<=1;
end else begin
BRAM_A_en<=0;
BRAM_A_we<=0;
end
end
end
always@(*)begin
case(bram_wr_cnt)
0:begin BRAM_A_din<=BRAM_A_temp[127:0];end
1:begin BRAM_A_din<=BRAM_A_temp[255:128];end
2:begin BRAM_A_din<=BRAM_A_temp[383:256];end
3:begin BRAM_A_din<=BRAM_A_temp[511:384];end
default:begin BRAM_A_din<=BRAM_A_temp[127:0];end
endcase
end
提交命令请求和提交队列头:
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
sq_door_req<=0;
end else begin
if(cstate==2||cstate==5||cstate==8||cstate==11||cstate==14||cstate==17||cstate==20)
sq_door_req<=1;
else if(i_sq_door_done)
sq_door_req<=0;
end
end
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
sq_Head<=0;
end else begin
if(BRAM_A_en && (bram_wr_cnt==3))begin
if(sq_Head>=ADMIN_SQ_SIZE-1)begin
sq_Head<=0;
end else begin
sq_Head<=sq_Head+1;
end
end
end
end
提交门铃申请最终发送到nvme_reg_cfg模块进行门铃寄存器写:


这里需要注意axi4_full_slave模块将0x0-0x7fff_ffff地址都进行了映射,所以每个使用local_wr和 local_rd的模块都需要进行地址判断,只有地址一致的读写操作才执行![]()

admin提交队列模块中:

admin完成队列模块中:
![]()
ADMIN完成提交队列模块:
module Com_Queue#(
parameter ADDR_WIDTH = 32,
parameter DATA_WIDTH = 128,
parameter CQ_SIZE = 16'd8,
parameter CQ_BASE_ADDR= 32'h1010_1000
)(
input wire i_clk,
input wire i_rst_n,
input[15:0]i_nvme_io_deep,
//========== 来自AXI桥接模块:本地写通路 ==========
input wire i_local_wr_en,
input wire [ADDR_WIDTH-1:0] i_local_wr_addr,
input wire [DATA_WIDTH-1:0] i_local_wr_data,
input wire i_local_wr_done,
output[15:0]o_Cq_Head,
outputo_cq_tx_door_req,
inputi_clear_flag,
output[31:0]o_rx_cq_num,//接收到cq个数
inputi_tx_door_done
);
完成队列中主要进行完成状态判断,判断命令是否成功执行
assignlocal_wr_en=i_local_wr_en && (i_local_wr_addr[31:12]==CQ_BASE_ADDR[31:12]);
assigncq_error=(Status_Field==0) ? 1'b0:1'b1;
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
CDW0<=0;
SQID<=0;
SQHD<=0;
CID<=0;
P<=0;
Status_Field<=0;
end else begin
if(local_wr_en)begin
CDW0<=i_local_wr_data[31:0];
SQID<=i_local_wr_data[95:80];
SQHD<=i_local_wr_data[79:64];
CID<=i_local_wr_data[111:96];
P<=i_local_wr_data[112];
Status_Field<=i_local_wr_data[127:113];
end
end
end
接收到完成数据后,也需要发起完成队列门铃
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
cq_tx_door_req<=0;
end else begin
if(i_tx_door_done)begin
cq_tx_door_req<=0;
end else if(local_wr_en)begin
cq_tx_door_req<=1;
end
end
end
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
Cq_Head<=0;
end else begin
if(local_wr_en)begin
//if(Cq_Head>=CQ_SIZE-1)
if(Cq_Head>=i_nvme_io_deep-1)
Cq_Head<=0;
else
Cq_Head<=Cq_Head+1;
end
end
end
NvmeIdentify模块用于接收NVME 发送的ID等信息,并进行解析
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2026/06/30 11:29:50
// Design Name:
// Module Name: NvmeIdentify
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 – File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module NvmeIdentify#(
parameter ADDR_WIDTH = 32,
parameter DATA_WIDTH = 128,
parameter IDENTIFY_CTRL_BASE=32'h1020_0000,
parameter IDENTIFY_NS_BASE=32'h1020_1000
)(
input wire i_clk,
input wire i_rst_n,
//========== 来自AXI桥接模块:本地写通路 ==========
input wire i_local_wr_en,
input wire [ADDR_WIDTH-1:0] i_local_wr_addr,
input wire [DATA_WIDTH-1:0] i_local_wr_data,
input wire i_local_wr_done
);
reg[15:0]VID;//2BYTE
reg[15:0]SSVID;//2BYTE
reg[7:0]MDTS;//1BYTE
reg[31:0]VER;//4BYTE
reg[63:0]NSZE;
reg[63:0]NCAP;
reg[63:0]NUSE;
reg[7:0]NSFEAT;
reg[7:0]NLBAF;
reg[7:0]FLBAS;
always@(posedgei_clk or negedge i_rst_n)begin
if(!i_rst_n)begin
VID<=0;
SSVID<=0;
MDTS<=0;
VER<=0;
end else begin
if(i_local_wr_en)begin
if(i_local_wr_addr[31:12]==IDENTIFY_CTRL_BASE[31:12])begin
if(i_local_wr_addr[11:4]==8'h000)begin
VID<=i_local_wr_data[15:0];
SSVID<=i_local_wr_data[32:16];
end else if(i_local_wr_addr[11:4]==8'h004)begin
MDTS<=i_local_wr_data[111:104];
end else if(i_local_wr_addr[11:4]==8'h005)begin
VER<=i_local_wr_data[31:0];
end
end else if(i_local_wr_addr[31:12]==IDENTIFY_NS_BASE[31:12])begin
if(i_local_wr_addr[11:4]==8'h000)begin
NSZE<=i_local_wr_data[63:0];
NCAP<=i_local_wr_data[127:64];
end else if(i_local_wr_addr[11:4]==8'h004)begin
NUSE<=i_local_wr_data[63:0];
NSFEAT<=i_local_wr_data[71:64];
NLBAF<=i_local_wr_data[79:72];
FLBAS<=i_local_wr_data[87:80];
end
end
end
end
end
endmodule
本节内容就讲到这,主要需要理解NVME 的提交、完成队列流程与机制,理解完后写代码就没那么困难了。





