FPGA控制NVME开发流程

📅 2026/7/10 7:55:25 👁️ 阅读次数 📝 编程学习
FPGA控制NVME开发流程

基于XILINX IP-AXI Memory Mapped to PCIe Gen2

一、使用S_AXI_CTL总线进行配置

S_AXI_CTL总线主要用于配置PCIE的配置CFG空间寄存器。

1、读取RC端(FPGA)的PCIE配置空间

RC设备使用TYPE1类型配置空间:

先进行读操作可以验证S_AXI_CTL总线通信是否正常,使用S_AXI_CTL总线读取0x0, 0x4, 0x8地址,读取数据如下

供应商ID,设备ID值:0x712410EE

命令,状态寄存器值为:0x00100000

ClassCode ID,版本ID值为:0x06040000

2、配置RC端(FPGA)的PCIE配置空间

①配置命令CMD寄存器(0x04)

开启BUSM_EN 、MEM_EN、IO_EN、PARITY、SERR_EN.

②配置设备枚举范围寄存器(0x18

将寄存器配置为0x00070100,代表:RC 主总线 Bus0; 直接下游总线起点 Bus1; 系统最多支持 7 级总线(Bus1~Bus7);

3、读取EP端(NVME SSD)的PCIE配置空间

当 AXI 映射 PCIe 核配置为Root Port(RC)时,通过ECAM 增强配置访问机制生成配置事务;该功能仅 RC 模式可用。对一段特定内存窗口(ECAM 窗口)的 AXI 读写,会自动转换成 PCIe 标准配置读 / 写 TLP。

访问Primary Bus(默认 Bus0):生成 Type 1 桥配置头事务,访问 FPGA 内置 RC 本地配置空间;

访问Secondary Bus(次总线,如 Bus1):生成 Type 0 EP 配置事务(用于 NVMe 等终端设备);

访问区间 Secondary Bus < Bus ≤ Subordinate Bus(多级桥下游总线):生成 Type1 桥配置事务;软件枚举阶段,必须将主 / 次 / 从属总线号写入 RC 内置 Type1 配置头(偏移 0x18 寄存器)。

ECAM的基地址为:ECAM_OFFSET=Bus<<20 ∣ Dev <<15 ∣ Func<<12。只有一块NVME SSD时默认Bus=1,Dev=0,Func=0。基地址ECAM_OFFSET=0x100000。

NVME SSD配置空间使用type0类型:

读取0x100000,0x100004,0x100008地址查看ID等信息:

供应商ID,设备ID值:0x10011BC0

命令,状态寄存器值为:0x00100000

ClassCode ID,版本ID值为:0x01080201

4、配置EP端(NVME SSD)的PCIE配置空间

①配置命令CMD寄存器(0x100004)

开启BUSM_EN 、MEM_EN。

②配置BAR空间地址寄存器(0x100010、0x100014

0x100010写入低32位地址:0x80000004(对应IP核中配置地址)

0x100014写入高32位地址:0x00000000

5、配置IP核寄存器

配置根端口状态 / 控制寄存器(寄存器偏移地址:0x148

开启Bridge Enable 桥使能。

Bridge Enable:该位置 1 时,AXI BAR 空间的读写流量可以转发到 PCIe 总线。软件完成 PCIe 枚举流程后,必须向该位写 1 开启桥转发;

读取0x148寄存器值后,BIT0置1后再写入。

二、使用S_AXI总线进行配置

S_AXI总线主要用于配置NVME SSD内部寄存器。

NVME SSD内部寄存器位于BAR空间中,完成地址映射后就可以进行访问了。

1、读取寄存器

获取CAP,VS,CC寄存器数据,对应地址分别为0x80000000CAP低32位),0x80000004(CAP高32位)0x80000008,0x80000014。

acpl is FF0100FF

acph is 00000020

VS is 00010300

CC is 00000000

2、配置寄存器(初始化)

①等待CSTS寄存器(0x8000001C)ready位为0

②配置CC寄存器(0x80000014)为0x00460000 :配置Admin提交队列单条大小为64byte, Admin完成队列单条大小为16byte。

③配置AQA寄存器(0x80000024)为0x00100010:配置Admin提交、完成队列深度为16。

④配置ASQ寄存器(0x80000028、0x8000002C)分别为0x10100000,0x00000000:配置Admin提交队列基地址高32位为0,低32位为0x10100000。

⑤配置ACQ寄存器(0x80000030、0x80000034)分别为0x10101000,0x00000000:配置Admin完成队列基地址高32位为0,低32位为0x10101000。

⑥配置CC寄存器(设置EN)地址0x80000014写0x00460001

⑦等待CSTS寄存器(0x8000001C)ready位为1

3、提交和完成队列流程(Admin和IO通用)

流程如下:

提交队列:

①指令写入内部DDR提交队列地址中

这一步不会向NVME SSD发送数据,只将指令存入指定内部空间,等待NVME读取

②发起提交门铃

向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

完成队列:

①判断是否接收到完成命令

根据P位相位翻转判断是否接受到新的完成指令。状态Status不为0表示出错。

②发起完成门铃

向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

4、读取Identify中Controller信息

Identify信息共4094字节。主要读取NVME SSD硬盘的ID、MDTS信息等

①定义Identify存放地址,如:

#define IDENTIFY_CTRL_BASE 0x10200000

②提交队列信息

sqe.cdw0 = 0x06;

sqe.nsid = 0;

sqe.dptr[0] = 0x10200000; //数据缓冲区地址

sqe.dptr[1] = 0;

sqe.cdw10 = 1;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

③等待完成队列

向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

获取MDTS计算出单次最大传输数据大小。

控制器最小页 CAP.MPSMIN = 4KB (4096B)

5、设置IO队列数量

设置IO队列数量使用设置特征命令

DW11如上,实际写入值为:队列数量-1,创建2个队列是,NCQR和NSQR都为1

①提交队列信息

sqe.cdw0 = 0x09;

sqe.cdw10 = 0x07

sqe.cdw11 = 0x00010001;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

③等待完成队列

向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

6、创建IO队列

①定义IO队列基地址,如:

#define IO_WR_SQ_BASE 0x10102000

#define IO_WR_CQ_BASE 0x10103000

#define IO_RD_SQ_BASE 0x10104000

#define IO_RD_CQ_BASE 0x10105000

②创建写完成队列

sqe.cdw0 = 0x03000005;

sqe.nsid = 0;

sqe.rsvd1 = 0;

sqe.mptr = 0;

sqe.dptr[0] = 0x10103000; // PRP1:CQ物理基地址

sqe.dptr[1] = 0;

sqe.cdw10 = 0x00070001;

sqe.cdw11 = 0x00000001;

sqe.cdw12 = 0;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

等待完成队列,判断是否创建成功。向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

③创建读完成队列

sqe.cdw0 = 0x03010005;

sqe.nsid = 0;

sqe.rsvd1 = 0;

sqe.mptr = 0;

sqe.dptr[0] = 0x10105000; // PRP1:CQ物理基地址

sqe.dptr[1] = 0;

sqe.cdw10 = 0x00070002;

sqe.cdw11 = 0x00000001;

sqe.cdw12 = 0;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

等待完成队列,判断是否创建成功。向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

④创建写提交队列

sqe.cdw0 = 0x03020001;

sqe.nsid = 0;

sqe.rsvd1 = 0;

sqe.mptr = 0;

sqe.dptr[0] = 0x10102000; // PRP1:CQ物理基地址

sqe.dptr[1] = 0;

sqe.cdw10 = 0x00070001;

sqe.cdw11 = 0x00010001;

sqe.cdw12 = 0;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

等待完成队列,判断是否创建成功。向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

⑤创建读提交队列

sqe.cdw0 = 0x03030001;

sqe.nsid = 0;

sqe.rsvd1 = 0;

sqe.mptr = 0;

sqe.dptr[0] = 0x10104000; // PRP1:CQ物理基地址

sqe.dptr[1] = 0;

sqe.cdw10 = 0x00070002;

sqe.cdw11 = 0x00020001;

sqe.cdw12 = 0;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

将指令写入Admin提交队列中;

发起提交门铃,向提交门铃寄存器(Admin为0x80001000)写提交队列尾指针。

等待完成队列,判断是否创建成功。向完成门铃寄存器(Admin为0x80001004)写完成队列头指针。

7、写数据到NVME SSD

①定义写数据基地址及链表地址

#define WRITE_DATA_BUF 0x20000000

#define PRP_LIST_BUF 0x10400000UL

链表结构,单条数据长度64B.

typedef struct {

u64 prp_entry[256];

} PrpList;

②提交队列信息

单个LBA大小为512B. Nlb为写入LBA的个数,SLBA为第一逻辑块地址

// DW0: CID + Write操作码

sqe.cdw0 = (((u32)cid) << 16) | 0x01;

sqe.nsid = nsid;

sqe.rsvd1 = 0;

sqe.mptr = 0;

// DW10/DW11:64位起始LBA

sqe.cdw10 = (u32)lba;

sqe.cdw11 = (u32)(lba >> 32);

// DW12:NLB =nlb- 1 (NVMe标准)

sqe.cdw12 = (nlb - 1) & 0xFFFF;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

// dptr表示访问的内存空间地址,向nvme中写数据只用提交队列告诉NVME数据存放在哪个地址,nvme自行进行dma取数据,

sqe.dptr[0] = cur_addr; //这里为0x20000000

//仅1页(4KB),PRP2置0

sqe->dptr[1] = 0;

//多页情况:

//剩余页数填充PRP链表

sqe->dptr[1] = PRP_LIST_BUF; // PRP2指向PRP列表首地址,这里为0x10400000

//在链表中填入需要写入的页地址

while(remain > 0)//剩余需要写入的数据大于0

{

prp_list->prp_entry[entry_idx] = cur_addr;//实际地址填入链表,0x20000000地址基础上进行4K累加后的地址

cur_addr += 4096; //地址按4K累加

if(remain > 4096)

remain -= 4096;

else

remain = 0;

entry_idx++;

}

// PRP链表最后一项填0标记结束

prp_list->prp_entry[entry_idx] = 0;

将指令写入IO WR提交队列中;

发起提交门铃,向提交门铃寄存器(IO WR为0x80001008)写提交队列尾指针。

③等待完成队列

返回的status为0表示写入成功。向完成门铃寄存器(IO WR为0x8000100C)写完成队列头指针。

7、从NVME SSD读取数据

①定义写数据基地址及链表地址

#define READ_DATA_BUF 0x28000000

#define PRP_LIST_BUF 0x10400000UL

链表结构,单条数据长度64B.

typedef struct {

u64 prp_entry[256];

} PrpList;

②提交队列信息

// DW0: CID + Read 操作码

sqe.cdw0 = (((u32)cid) << 16) | 0x02;

sqe.nsid = nsid;

sqe.rsvd1 = 0;

sqe.mptr = 0;

// DW10/DW11:64位起始LBA

sqe.cdw10 = (u32)lba;

sqe.cdw11 = (u32)(lba >> 32);

// DW12:NLB =nlb- 1

sqe.cdw12 = (nlb - 1) & 0xFFFF;

sqe.cdw13 = 0;

sqe.cdw14 = 0;

sqe.cdw15 = 0;

// dptr表示访问的内存空间地址,向nvme中写数据只用提交队列告诉NVME数据存放在哪个地址,nvme自行进行dma取数据,

sqe.dptr[0] = cur_addr; //这里为0x28000000

// 仅1页(4KB),PRP2置0

sqe->dptr[1] = 0;

//多页情况:

// 剩余页数填充PRP链表

sqe->dptr[1] = PRP_LIST_BUF; // PRP2指向PRP列表首地址,这里为0x10400000

//在链表中填入需要写入的页地址

while(remain > 0)//剩余需要写入的数据大于0

{

prp_list->prp_entry[entry_idx] = cur_addr;//实际地址填入链表,0x28000000地址基础上进行4K累加后的地址

cur_addr += 4096; //地址按4K累加

if(remain > 4096)

remain -= 4096;

else

remain = 0;

entry_idx++;

}

// PRP链表最后一项填0标记结束

prp_list->prp_entry[entry_idx] = 0;

将指令写入IO RD提交队列中;

发起提交门铃,向提交门铃寄存器(IO RD为0x80001010)写提交队列尾指针。

③等待完成队列

返回的status为0表示写入成功。向完成门铃寄存器(IO RD为0x80001014)写完成队列头指针。

三、M_AXI总线配置

M_AXI总线由EP端(NVME SSD)发起,主要用于NVME SSD进行DMA读写。将M_AXI总线与ZYNQ或DDR连接,读、写操作时NVME SSD会自动通过M_AXI总线访问内存。