基于FPGA实现NVMe硬盘的读写功能

本文主要分享Xilinx系列FPGA开发使用NVMe硬盘过程中的相关事项,比如RC、EP 的初始化(两端的PCI配置空间初始化,MVMe控制器初始化),Admin和IO命令的设计流程等。

主要参考的资料:

  • FPGA中PCIE IP核datasheet,比如7系列的《pg054-7series-pcie-en-us》;u/+系列的《pg195-pcie-dma-en-us》

  • PCIE文档《PCI Express Base Specification》

  • NVMe文档《NVM-Express-Base-Specification》,《NVM-Express-NVM-Command-Set-Specification》,存储随笔《PCIe科普教程》

  • 其他知网文献:《NVMe高速存储的FPGA实现》,《基于FPGA的NVMe接口设计》,《基于FPGA的固态硬盘存储控制器设计》

  • 相关博客链接:https://blog.csdn.net/u011037593/article/details/136999447?spm=1001.2014.3001.5502系列博文等

一、开发流程

1.1 、概述

PCIE总线架构如下:

在以往的FPGA板卡与上位机的开发使用PCIE总线时,FPGA常作为Endpoint使用;而在FPGA外挂NVMe硬盘的应用中,FPGA作为RootComplex,NVMe硬盘作为 Endpoint使用 。NVMe协议是基于PCIe总线实现的,属于PCIe的一种特殊应用。

1.1.1、PCIe概述

PCIE协议主要涉及两种机制、三层结构、四种事务类型。

数据交换是基于请求与完成(响应)的机制,两种模式:发送数据不需要接收端响应,要求接收端对发送数据进行响应。

分为事务层、链路层和物理层,以此实现用户数据的交互。有点类似于SRIO,用户主要关注事物层构造/解析各类包,但PCIe比SRIO复杂的多。

数据类型被分为内存、I/O、配置和消息四类,分别完成不同的功能。

1.1.2 、TLP事务层的包

TLP包的头部如下:64bit地址为4DW,32bit地址为3DW。具体含义可参考PCIE Specification。应用时是多少位的地址可参考PCIE的BARs设置。

Fmt:Format of TLP,3bit,用来确定TLP包头格式是3DW还是4DW;Fmt与下面的type配合就可以表示PCIE支持的所有类型了。

TYPE这个5bit数据,表示传输类型的,与fmt结合,就确定了数据包的属性。PCIE的事务类型有存储、I/O、配置和消息但完成这些事务类型的数据交换有多种数据包:比如枚举NVME SSD的过程涉及CfgRd0、Cpl、CplD。

FPGA开发NVMe硬盘过程中主要用到MRd、MWr、CfgRd0、 CfgWr0、Cpl、CplD、Msg。

1.1.3 、PCIe配置空间

RC(FPGA)、EP(NVMe)都需要访问配置其PCI配置空间,结构如下图,配置空间大小共4KB。PCIe设备的每个Function都对应一个配置空间。0-3Fh(64字节) 是PCI兼容配置空间头,按类型可分为Type 0和Type 1。PCIe设备使用Type 0配置空间头,PCIe桥使用Type 1配置空间头。40h-FFh(192字节) 主要存放一些与MSI或者MSI-X中断和相关的能力结构体Capability Structures。**100h-FFFh(3840字节)**是PCIe协议扩展的配置空间,主要存放设备序列号等Capability Structures。

其前64字节的头结构如下图:(本文只涉及Type0

能力结构体是PCIe开发过程中必须扫描获取的。能力结构体的具体配置信息位于PCIe配置空间的第65byte~256byte之间共192字节。设备类型不同具有不同的能力类型,根据Capability Pointer可获取设备支持的所有能力类型。第一个Capabilities Structures的地址由配置空间Header的Capabilities Pointer寄存器保存,可以据此遍历所有的Capabilities Structures。Capabilities Structures的链表,直至最后的指针为0为止。如下图所示:

1.2、 建链、枚举

1.2.1、建链

此过程在正确设置FPGA后,上电启动板子后自动完成建链。建立链接的过程如下:主要流程为上电后两侧根据PCIE总线协议进入LTSSM流程,链路双方自动协商速率和宽度,调节发送和接收参数。

只要无硬件问题,NVMe先上电,FPGA后上电,参考UG477,LTSSM的状态及相关含义:当两端设备正常上电后应处于L0状态

相关状态、参数等可通过PCIe核接口参数获取检测:

1.2.2、枚举

Active后开始进行设备枚举。

PCIe总线中的每一个功能都有一个唯一的标识符与之对应,该标识符为(Bus,Device,Function);RC(BUS 0)搜索总线中DBF标识确定PCIE总线拓扑结构(CfgRd),通过读取Function的VendorseID寄存器来确认该节点是否存在,遍历整个BDF确定设备拓扑结构。

比如本次使用场景中只有一个RC和EP;RC(FPGA)通过CfgRd访问EP(NVME SSD),如果RC收到相应CPLD则代表SSD存在。 RC发起CfgRd0 TLP请求包,轮询Device ID 和Vendor ID、ClassCode,如果收到CPLD完成包则枚举到NVME盘;如果收到CPL包,则没有查询到SSD盘。CPLD数据部分包含了EP的Device ID。

比如NVMe硬盘的ClassCode为0x010802:当收到CPLD数据包且该值符合时,即枚举到NVMe硬盘。下图为PC上获取的PCIe总线下的设备信息:

1.3、RC、EP初始化流程

在实现NVMe硬盘读写前,RC(FPGA)、EP(NVMe)需要按顺序进行相应的PCIe配置空间初始化,之后进行NVMe控制器寄存器进行配置。

PCIe配置空间是每个PCIe设备独立的一段内存区域,用于存储设备的配置信息。RC在枚举设备时需要先访问配置空间,获取设备厂家、型号、类型、所需资源等信息,然后再分配资源,最后才能访问PCIe设备的存储或IO地址空间。本场景只使用设备配置空间Type 0。

1.3.1、RC的PCIe配置空间初始化

RC的配置空间通过PCIE IP核的设置已经实现了其相关配置寄存器的初始化;比如ID、BAR、能力结构等。

FPGA(RC)端的PCIe配置空间相关信息的访问、重配置可通过核的配置管理接口CMI(Config Management Interface)进行实现的。

1.3.2、EP的PCIe配置空间初始化

NVMe硬盘的PCIe配置空间初始化需要按照如下流程操作:

①获取NVMe设备的所有Capabilities结构体信息

②配置NVMe设备结构能力信息

③配置NVMe设备BAR基地址

④配置NVMe设备中断列表

获取 NVMe 设备的所有Capabilities结构体信息:

首先通过CfgRd访问寄存器地址为0x34的Capability指针,根据指针内容依次访问能力结构体链表直至结束(直至Next Capability Pointer值为0),获取EP设备(NVMe硬盘)的所有结构能力信息。

每一个Capabilities Structures都有一个独一无二的Capability ID,该ID保存在Capabilities Structures的开始地址,系统软件根据此判断Capabilities Structures的类型。比如Cap ID = 0x11时表示该能力结构表示MSI-X中断。

配置 NVMe 设备的能力结构体信息:

Cap ID为0x10即为PCIe能力结构,其指针即为该能力指针。

比如通过CfgRd访问能力结构体指针偏移为0x04的Device Capabilityies Register;通过CfgWr设置能力结构体指针偏移为0x08的Device Control寄存器。

配置 NVMe 设备BAR基地址:

如果基地址设置为32位,只需设置BAR0即可;如果设置基地址设置为64位,配置BAR0和BAR1。消费级的NVMe硬盘一页对应4KByte,通过CfgWr设置TYPE0配置空间偏移为0x04的BAR0为4K对齐并设置其具体基地址。

配置NVMe设备MSI-X中断列表:

MSI-X的中断机制是向RC的某个地址写Message数据以产生中断。MSI-X每个中断都有独立的Message Address和Message Data,Message Address和Message Data组成一个中断向量表,MSI-X使用了独立的中断Pending表。中断向量表和中断Pending表存放在BAR空间中。因此MSI-X支持的中断数量更多,且不需要中断号连续。

MSI-X Capability Structures主要的作用是记录中断向量表和Pending表保存的位置。MSI-X Capability Structure如下图所示。

Table BIR指定中断列表处于哪个table bir值的BAR下,偏移地址由Table Offset指定。Message Control寄存器位域的定义如下表所示:

|--------|---------------|---------------------------------------------------------------------------|--------|
| 位域 | 定义 | 描述 | 属性 |
| 15 | MSI-X Enable | MSI-X中断机制使能位,当MSI、MSI-X和INTx中断只能使用其中一个 | RW |
| 14 | Function Mask | MSI-X中断全局Mask位,当此位为1时,无论Pending表如何设置,所有中断都会被屏蔽 | RO |
| 13:11 | Reserved | 保留 | RsvdP |
| 10:0 | Table Size | MSI-X中断向量表的大小,存放Message Address和Message Data。若系统软件读取的值为0x3,则中断向量表的大小为4字节。 | RO |

1.3.3、NVMe控制器的寄存器配置

在完成上述FPGA和NVMe硬盘的PCI和PCIe寄存器配置后,需要按照下述流程进行NVMe控制器配置:(主要涉及MRd和MWr,内容太多不再展开)

① 等待CSTS.RDY变为0;否则通过一系列配置使其满足该状态;

② 配置AQA、ASQ、ACQ寄存器;

③ 配置CC寄存器;

④ 将CC.EN置1;

⑤ 等待CSTS.RDY置1;

NVMe控制器寄存器位于其BAR0、BAR1所映射的内存空间中,该BAR不同偏移地址对应的寄存器如下:

偏移量0x1000的为DoorBell寄存器,DB寄存器定义如下:

相关寄存器的含义:

CAP:控制器能力,定义了内存页大小的最大最小值、支持的I/O指令集、DB寄存器步长、等待时间界限、仲裁机制、队列是否物理上连续、队列大小;

VS:版本号,定义了控制器实现NVMe协议的版本号;

CC:控制器配置,定义了I/O SQ和CQ队列元素大小、关机状态提醒、仲裁机制、内存页大小、支持的I/O指令集、使能;

CSTS:控制器状态,包括关机状态、控制器致命错误、就绪状态;

AQA:Admin 队列属性,包括SQ大小和CQ大小;

ASQ:Admin SQ基地址;

ACQ:Admin CQ基地址;

1000h之后的寄存器定义了队列的头、尾DB寄存器。

CAP寄存器标识的是Controller具有多少能力,而CC寄存器则是指当前Controller选择了哪些能力,可以理解为CC是CAP的一个子集;如果重启(reset)的话,可以更换CC配置;

CC.EN置1,表示Controller已经可以开始处理NVMe命令,从1到0表示Controller重启;

CC.EN与CSTS.RDY关系密切,CSTS.RDY总是在CC.EN之后由Controller改变置为1,其他不符合执行顺序的操作都将产生未定义的行为;

Admin队列由host直接创建,AQA、ASQ、ACQ三个寄存器标识了Admin队列,而其他I/O队列则由Admin命令创建;

Admin队列的头、尾DB寄存器标识为0,其他I/O队列标识由host按照一定规则分配;只有16bit的有效位,是因为队列深度最大64K。

1.4、NVMe协议命令

在完成上述操作后,即可通过Admin命令操作NVMe硬盘:

Host通过Identify命令,确定Controller的数据结构等;

Host通过set/get features获取I/O SQ和CQ信息,配置中断机制等;

Host分配适当的I/O CQ、SQ队列;

然后才可发起对NVMe硬盘的读写IO指令。

1.4.1、NVMe协议命令

NVMe命令主要分为Admin命令和IO命令,根据位于的队列分类;Admin命令只能提交到Admin SQ CQ中,主要负责管理NVMe控制器的一些控制指令。IO命令只能提交到I/O SQ CQ中,主要负责完成数据的传输。

命令均为16 DW,具有相同的格式,某些字段根据命令的不同有不同的定义。

|--------|-------------------|
| Dword0 | CID、传输方式、聚合操作、操作码 |
| 1 | NID(命名空间ID) |
| 2 | 保留 |
| 3 | 保留 |
| 4、5 | 元数据指针(MPTR) |
| 6-9 | 数据指针(DPTR) |
| 10-15 | 根据命令指定 |

完成命令具有相同的格式,某些字段根据命令的不同有不同的定义。

|--------|------------|
| Dword0 | 根据命令指定 |
| 1 | 保留 |
| 2 | SQID、SQ头指针 |
| 3 | 状态域、P位、CID |

1.4.2、Admin命令

Admin命令执行的操作类型是通过Dword0中的8位操作码定义,通过SQID(提交队列ID)+CID(命令ID)唯一标识完成的命令。常用的命令如下:

|---------|----------------|----------------------------------------------|
| 操作码 | 指令 | 作用 |
| 00h | 删除I/O SQ, | 释放SQ空间 |
| 01h | 创建 I/O SQ, | 分配给SQ的地址、队列优先权、队列大小 |
| 02h | 获取日志 | 返回所选日志页于缓冲区 |
| 04h | 删除 I/O CQ, | 释放CQ空间 |
| 05h | 创建 I/O CQ, | 分配给CQ的地址、中断向量、队列大小等 |
| 06h | Identify | 返回关于controller与namespace能力和状态的数据结构(2k字节) |
| 09h | 设置features | 根据FID设置相应的features |
| 0Ah | 获取features | 根据FID返回队列数量、仲裁信息等 |
| 80h | 格式化 | 擦除LB内容 |

Admin队列是通过配置ASQ等寄存器创建的;先创建CQ再创建SQ

1.4.3、IO命令

IO命令主要进行数据读写,NVMe硬盘的读写以LB为单位进行。

IO命令与Admin命令结构完全相同,也是通过DW0中的8位操作码来定义命令类型的。

|---------|---------------------|----------------------------|
| 操作码 | 指令 | 作用 |
| 00h | Flush | 将数据(和元数据)提交到NVMe中,所有命令都要执行 |
| 01h | Write | 将数据写入NVMe |
| 02h | Read | 读NVMe中的数据 |
| 04h | Wirte Uncorrectable | 标记无效数据块 |
| 05h | Compare | 比较从NVMe读出的数据和比较数据缓冲区的数据 |

1.4.4、命令交互过程

命令交互过程如下图所示:

对于FPGA与NVMe硬盘的Admin命令交互过程为

①host(FPGA)将16 DW的Admin命令(1条或者多条)写入提前分配好的SQ中(FPGA准备好命令,等待Controller获取时发出)

②host(FPGA)通过NWr将DoorBell写入到Controller(NVMe)的提交队列SQ0TDBL中(地址为BAR0+0x1000)。(DB中的数据SQT-上次DB中的SQT值 = 本次待执行的命令个数,SQT值最大不能超过NVMe控制寄存器中的AQA属性值队列深度)

③NVMe(通过HDB和TDB可以判断是否有未完成命令)通过(MRd)向地址ASQ的DW地址(上章节配置的NVMe寄存器ASQ),发起读取每个命令16DW的请求,以用于获取FPGA准备的命令。Host(FPGA)发起(CPLd),即1中准备好的每条命令16DW给Controller(NVMe硬盘)

④Controller(NVMe硬盘)执行具体命令

⑤NVMe硬盘在命令完成后,通过MWr将完成命令4DW写入host内存SQ对应的ACQ偏移地址中(ACQ基地址为上节配置的NVMe寄存器ACQ)

⑥NVMe硬盘执行完上述后,发起中断(比如常用的MSI-X,能够支持2K个中断向量。在产生MSI-X中断信息前,需要检查该中断在相应寄存器中不被屏蔽),向中断地址写入中断信息

⑦host(FPGA)接收到中断信息,得知NVMe硬盘已处理完相应的命令

⑧Host(FPGA)通过MWr发起DoorBell写入到Controller(NVMe)的完成队列中(即CQ0HDBL,地址为BAR0+0x1004);本次DB中的数据CQH-上次DB中的CQH值 = 本次执行的命令个数,CQH值最大不能超过NVMe控制寄存器中的AQA属性值队列深度。

1.4.5、Admin命令执行顺序

RC在读写NVMe硬盘前需要按顺序执行如下操作执行Admin命令:

①执行Identify命令,获取Controller的数据结构

②执行Set Features命令,申请IO队列数量

③执行创建IO完成队列命令

④执行创建IO提交队列命令

⑤执行创建IO完成队列命令

⑥执行创建IO提交队列命令

**创建的IO完成、提交队列数(最大4K,最小2个)**与NVMe控制器中的AQA寄存器中ACQS和ASQS数量相对应,与Set Features(0x09)命令,申请IO队列数量一致。

上述每条Admin的命令交互过程如上节图示,每条命令由host提交到内存中的SQ队列中,更新TDBxSQ后,NVMe控制器通过DMA的方式将SQ中的命令(怎么取,如何取,取多少,因命令而异)取到控制器缓冲区,执行命令;执行完成后,根据执行状态,组装完成命令,通过DMA的方式将完成命令写入内存CQ的队列中;NVMe控制器通过MSI-X中断方式通知host已完成命令;最后,host处理CQ命令,更新控制器中HDBxCQ,标识着该条命令完成。

1.4.6、IO命令执行实现读写硬盘

在完成上述操作后,即可通过IO命令读写NVMe硬盘

至少建立两个IO队列,一个用于写操作,一个用于读操作。操作码0x01表示写数据到NVMe硬盘,操作码0x02表示从NVMe硬盘读出数据。

需要设计PRP1、PRP2或SGL方式,IO队列的深度,准备好提交对应页数的IO读/写命令后及sub DB,通知NVMe进行相应的读写操作。

IO读写命令交互过程与Admin命令类似,只是响应地址的不同

二、NVMe硬盘读写测试

2.1 、硬件信息

做硬盘读写的测试成本太高了,FPGA板卡、M.2接口、SATA接口、NVMe硬盘、SATA硬盘;只做这些,大几千的成本就搭进去了。

为了节省成本,从之前的电脑上拆了一个M.2接口的NVMe硬盘,此硬盘会影响测速,有条件的建议直接上三星980或990,或者工业盘。

本次测试的硬盘标签型号信息如下:共512GB容量,其中LB为512字节LBA数量共1000215216个,约477GB;之后的测试FPGA可将相关信息获取出来。

FPGA板卡为7系列的,在此系列上PCIe Gen2 x4的理论速率上限为2GB/S

2.2 、文件系统

NVMe硬盘支持NTFS、exFAT等文件系统。

在之前的文章中FPGA实现SD卡内文件的读写功能(FAT32文件系统),分析实现了SD卡FAT32文件系统中的文件读写,感兴趣的可以参考。

exFAT文件系统是FAT32系统的扩展。

FPGA纯逻辑实现文件系统中的文件读写,难度不大 ,但是繁琐

本文测试不包含文件系统;对于很多应用场景,比如数据采集回放、记录仪等不使用文件系统也可以完成,只需记录每次的数据量LBA的地址范围即可,数据回放时根据LB号对应读取即可。

2.3 、硬盘信息获取

程序启动后可获得的NVMe硬盘信息如下:

w_Namespase_Size :NVMe硬盘LB数目,0x3B9E12B0即 1000215216,与硬盘标签信息一致;

w_lb_size:每个LB的字节数 2^lb_size,即512B,与硬盘标签信息一致

w_nvme_config_done:相关配置已完成,NVMe硬盘具备读写状态

w_link_width:lane数000100=x4

w_link_rate :gen 0010=5.0GT/s,与FPGA系列速率相符

w_user_app_rdy、w_user_lnk_up、w_pl_phy_lnk_up:PCIE核的状态

w_pl_ltssm_state:L0状态

2.4 、读和写功能测速

通过VIO控制对硬盘的读写过程,由于BAR设置中为4K对齐 ,即每页大小4KB;比如1GB的读写数据量大小为0x40000个页。设置相应的页起始地址、操作的页数,再使能对应的读写使能即可进行硬盘读写测试。

分别测试1GB、10GB、50GB、100GB的数据量读和写SSD的速率。读、写测速结果如下(为节省时间每种容量只做1次实验,不再进行多次比对):

|-----------|--------------|--------------|
| 测试数据量 | 写速率 | 读速率 |
| 1GB | 1094MB/S | 1251MB/S |
| 10GB | 920MB/S | 1261MB/S |
| 50GB | 843MB/S | 1262MB/S |
| 100GB | 836MB/S | 1261MB/S |

写1GB的测试:根据用户时钟(125Mhz)计数的时间为116938546 x 8ns,写速率为1094MB/S。

读1GB的测试:根据用户时钟(125Mhz)计数的时间为102270401 x 8ns,读速率为1251MB/S。

往期 回顾

FPGA光通信系列4 --- 基于64b/66b编码的自定义协议

FPGA光通信系列3 --- 基于8b/10b编码的自定义协议应用

FPGA光通信系列2------Aurora 64B/66B的使用

FPGA实现Aurora光通信应用(8B/10B)

FPGA外挂存储器应用3------NVMe协议 M.2硬盘的读写功能测试

FPGA外挂存储器应用2------QSPI Flash读写等功能

FPGA实现SD卡内文件的读写功能(FAT32文件系统)

JESD204B的使用系列------3、DAC的应用(AD9164 9.6GSPS)

JESD204B的使用系列------2、协议及ADC的应用(AD9689)

JESD 204B的使用系列---1、时钟芯片的应用

相关推荐
FPGA小c鸡13 小时前
FPGA卷积层流水线加速:从入门到精通(附完整SystemVerilog实现)
fpga开发
数字芯片实验室13 小时前
仿真器出bug了?分频时钟竞争的诡异仿真现象
fpga开发·bug
从此不归路14 小时前
FPGA 结构与 CAD 设计(第4章)下
fpga开发
Terasic友晶科技15 小时前
7-DE10-Nano的HDMI方块移动案例的整体实现(含Quartus完整工程免费下载)
fpga开发·i2c·pll·de10-nano·hdmi传输·方块移动案例·quartus prime
碎碎思15 小时前
使用 Arm Cortex-M1 实现低成本图像处理系统 的 FPGA 方案详解
arm开发·图像处理·人工智能·fpga开发
minglie116 小时前
PetaLinux工程目录设备树文件结构与作用
fpga开发
最遥远的瞬间16 小时前
二、FPGA程序固化
fpga开发
Ghost Face...16 小时前
内存调试:2T/3T模式配置实战指南
fpga开发
海涛高软16 小时前
Verlog实现串口的收发功能
fpga开发
从此不归路16 小时前
FPGA 结构与 CAD 设计(第4章)上
ide·fpga开发