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寄存器数据,对应地址分别为0x80000000(CAP低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总线访问内存。
