参考:
【免费】NVME协议学习资料,FPGA控制NVMESSD硬盘读写资料资源-CSDN下载
一、PCIE总线介绍
1 、PCIE总线拓扑结构
常见的PCIe 总线系统结构如图所示,其中主要包含三种设备,分别是根复合体(Root Complex,RC)、Switch 和终端设备(EndPoint,EP)

- RC 是PCIe 体系树形结构中的根节点。RC 主要负责配置PCIe 总线上的所有设备,分配资源、处理传输请求,并管理数据流动。在处理器系统中,RC 是负责连接CPU 与PCIe 系统的桥,实现从内存域到PCIe 域的地址转换,以及事务的传递。
- Switch 主要用于拓展PCIe 链路,实现路由功能。由于PCIe 数据传输只能从一个端点到另一个端点,一条PCIe 链路上只能存在两个设备,只有Switch 的上下游能够存在多个对端设备,因此当一个链路上需要挂载多个设备时,需要使用Switch 进行拓展。Switch 内部由多个PCI to PCI 桥构成,其中还包含一条虚拟总线,用于连接各个PCI 桥。Switch 通过上、下游端口连接更多的Switch 或EP,实现链路的拓展。
- EP 是连接到PCIe 总线上的各种外部设备,例如网卡、显卡、SSD 等。EP 能够接收来自RC 或其它设备发出的请求并给出响应,也能主动发起请求,获取其它设备的响应。
2 、PCIE总线层次结构
PCIe 总线是一种分层协议总线,采用数据包进行数据传输。数据包在收发过程中需要经过事务层、数据链路层和物理层三个层次的处理和转发。PCIe 总线的分层结构如图所示。

- 事务层定义了PCIe 总线事务,是PCIe 总线层次结构中的最高层。事务层采用传输层报文(Transaction Layer Packet,TLP)实现设备核心事务与PCIe 事务的交互,并将事务与数据链路层交互。事务层通过TLP 实现了优先级服务、传输顺序控制、流量控制等多种功能,为PCIe 设备核心提供了丰富的功能服务。
- 数据链路层位于事务层和物理层之间。数据链路层主要负责保证TLP 传输的正确性,同时对PCIe 链路进行管理与监控,另外负责将事务层的报文转发到物理层或从物理层获取报文转发到事务层。为保证数据传输的完整性,数据链路层采用容错和重传机制,并定义了一系列数据链路层报文。
- 物理层位于PCIe 层次结构的最底层。物理层实现PCIe 的电气连接,由逻辑层和电气层组成。物理层使用LTSSM 状态机来管理链路,并实现链路训练、链路恢复、电源管理等功能。此外物理层对来自数据链路层的数据包进行字节条带化、扰码、编码等一系列操作,对接收到的对端数据包进行相反操作。
关于事务层,数据链路层和物理层的详细的功能如下所示:

TLP 在整个 PCIe 包结构的位置如以下两张图所示:(第一张为发送端,第二张为接收端)


2.1 、PCIE事务层
事务层采用TLP 传输事务,完整的TLP 由TLP Prefix、TLP 头、Payload 和TLPDigest 组成。
TLP 头是TLP 中最关键的部分,一般由三个或四个双字的长度,其格式定义如下所示。


常用的PCIe TLP 事务类型如下所示。其中Non-Posted 类型的事务请求需要使用完成报文(Completion Packet,Cpl)或带数据的完成报(Completion Packet withData,CplD)类型的响应包返回请求状态或数据,Posted 类型的事务请求不需要使用完成报文。
PCIe 总线协议定义了基于地址的路由、基于ID 的路由和隐式路由三种TLP 路由方式。
- 存储器读写和I/O 读写 TLP 采用基于地址的路由,该类型的报文可由RC 或EP 发出,根据TLP 中的Address 字段进行路由选径。
- 配置读写 报文、Cpl 和CplD 完成报文使用基于ID 的路由,配置读写报文只能由RC 发出;
- 完成报文可由任何设备发出,这些报文根据Transaction ID 进行路由选径,Transaction ID 则由PCIe总线号、设备号、功能号和Tag 字段构成。
- 消息报文 使用隐式路由,一般由RC 发出EP 响应,该类型报文的路由直接由下游端口到上游端口,或为RC 向EP 发出的广播。



2.1.1 存储器读写TLP 包格式
存储器读写TLP 包的Header 结构如图2-5 所示。其中Requester ID 标识了生成这个TLP 报文的设备的ID,它由设备的总线号、设备号和功能号三项组成;Tag 字段是可重复使用的TLP 包编号。Requester ID 和Tag 字段组成了Transaction ID,该字段用于使接收方知道TLP 包的来源。



2.1.2 配置读写TLP 包格式
根据配置设备为EP 还是Switch,配置读写TLP 包分为Type 0 和Type 1 两种。配置读写TLP Header 格式结构如图所示。其中Byte 0 至Byte 7 的寄存器与之前介绍的同名寄存器功能相同。Byte 8-9 中的Bus Number + Device + Function 字段唯一决定了配置读写TLP 包发往的目标设备;Ext Reg Number + Register Number标志了所需配置目标寄存器的偏移地址。确定了配置设备的ID 和配置空间的偏移地址,就能找到所需读写的寄存器。


2.1.3 完成报文TLP 包格式
下图所示为完成包的Header。完成包在Non-Posted TLP 之后才会出现,用于告知请求发起者TLP 的处理状态或者返回数据。Completion TLP Header 中的Requester ID 和Tag 字段完全照搬其对应的Non-Posted TLP Header 中的Requester ID 和Tag 数值,作为返回的完成包的目标地址。Completer ID 表示发送完成报文的PCIe 设备ID。Status 字段表示其对应的Non-Posted TLP 的完成状态,其值为3'b000 表示传输无误、正常完成。Byte Count 字段表示源设备还需要从目标设备中获得多少字节的数据才能完成全部的数据传递;设置该字段的原因是Non-Posted TLP 请求返回的数据可能需要很多带数据的完成包才能全部返回。


2.1.4 消息报文TLP 包格式
PCIe 中的 Message 主要是为了替代 PCI 中采用边带信号,这些边带信号的主要功能是中断,错误报告和电源管理等。所有的 Message 请求采用的都是 4DW 的 TLP Header,但是并不是所有的空间都被利用上了,例如有的 Message 就没有使用 Byte8 到 Byte15 的空间。
Message 请求的 TLP Header 格式如下图所示:



2.2 、PCIE数据链路层
数据链路层(Data Link Layer)主要进行链路管理(Link Management)、TLP 错误检测,Flow Control 和 Link 功耗管理。
数据链路层不仅可以转发来自事务层的包(TLP),还可以直接向另一个相邻设备的数据链路层直接发送 DLLP,比如应用于 Flow Control 和 Ack/Nak 的 DLLP。如下图所示:

数据链路层还实现了一种自动的错误校正功能,即 Ack/Nak 机制。如下图所示,发送方会对每一个 TLP 在 Replay Buffer 中做备份,直到其接收到来自接收方的 Ack DLLP,确认该 DLP 已经成功的被接受,才会删除这个备份。如果接收方发现 TLP 存在错误,则会向发送发发送 Nak DLLP,然后发送方会从 Replay Buffer 中取出数据,重新发送该 TLP。

2.3 、PCIE物理层
在 PCIe Spec 中,物理层是被分为两个部分,分别是物理层逻辑子层和物理层电气子层,其中后者一般都是基于 SerDes 来实现的。
由于物理层处于 PCIe 体系结构中的最底层,所以无论是 TLP 还是 DLLP 都必须通过物理层完成收发操作。来自数据链路层的 TLP 和 DLLP 都会被临时放入物理层的 Buffer 中,并被加上起始字符(Start & End Characters),这些起始字符有的时候也被称为帧字符(Frame Characters)。具体如下图所示:

3 、PCIE设备的配置空间
3.1 、BDF介绍
每一个 PCIe 设备可以只有一个功能(Function),即 Fun0。也可以拥有最多 8 个功能,即多功能设备(Multi-Fun)。不管这个 PCIe 设备拥有多少个功能,其每一个功能都有一个唯一独立的配置空间(Configuration Space)与之对应。
和 PCI 总线一样,PCIe 总线中的每一个功能(Function)都有一个唯一的标识符与之对应。这个标识符就是 BDF(Bus,Device,Function),PCIe 的配置软件(即 Root 的应用层,一般是 PC)应当有能力识别整个 PCIe 总线系统的拓扑逻辑,以及其中的每一条总线(Bus),每一个设备(Device)和每一项功能(Function)。
在 BDF 中,Bus Number 占用 8 位,Device Number 占用 5 位,Function Number 占用 3 位。显然,PCIe 总线最多支持 256 个子总线,每个子总线最多支持 32 个设备,每个设备最多支持 8 个功能。
PCIe 总线采用的是一种深度优先(Depth First Search)的拓扑算法,且 Bus0 总是分配给 Root Complex。Root 中包含有集成的 Endpoint 和多个端口(Port),每个端口内部都有一个虚拟的 PCI-to-PCI 桥(P2P),并且这个桥也应有设备号和功能号。
需要注意的是,每个设备必须要有功能 0(Fun0),其他的 7 个功能(Fun1~Fun7)都是可选的。
一个简单的例子如下图所示:

3.2 、配置空间介绍
PCIe 设备具有和PCI 设备相同的配置空间头类型,此外使用PCIe 拓展配置空间管理PCIe 总线。

PCI 配置空间头分为Type0 和Type1 两种类型,Type0 类型配置空间头在PCIe 总线中用于EP,Type1 类型配置空间头用于Switch 中的虚拟PCI 桥。Type0 类型的配置空间头结构如下所示。其中主要的寄存器的作用如下:
(1)设备ID 和供应商ID:由PCI-SIG 分配,当供应商ID 为16'hFFFF 时表示无效的设备;
(2)状态寄存器:保存PCIe 设备的状态信息;
(3)命令寄存器:初始值为0,需要合理配置该寄存器才能访问该设备的存储
器或者I/O 空间;
(4)头类型:当值为0 时表示设备使用Type0 的配置空间,值为1 时表示设备使用Type1 的配置空间;
(5)基地址寄存器(Base Address Register,BAR):保存PCIe 设备使用的地址空间的基地址;
(6)拓展功能指针:指向拓展配置空间的偏移地址。


Type1 类型的配置空间头如下所示。其中主要寄存器的作用如下:
(1)00h~14h 的寄存器作用与Type0 类型的相同;
(2)下级最大总线号、下一级总线号、上一级总线号:下级最大总线号为该设
备下游的PCIe 子树中最大的总线号,下一级总线号为直接连接在该设备下游端口的总线号,上一级总线号为与该设备上游端口直接连接的总线号,三者共同确定了该设备在PCIe 树中的位置;
(3)存储地址大小、存储基地址:两者共同表示分配到该设备的存储地址域;
(4)拓展功能指针:指向拓展配置空间的偏移地址。


3.3 、配置空间读写
PCIe 的 Spec 中明确规定只有 Root 有权限发起配置请求(Originate Configuration Requests),也就是说 PCIe 系统里面的其他设备是不允许去配置其他设备的配置空间的,即 peer-to-peer 的配置请求是不允许的。并且配置请求的路由(Routing)方式只能是采用 BDF(Bus, Device, Function)。
处理器一般不能够直接发起配置读写请求,因为其只能产生 Memory Request 和 IO Request。这就意味着 Root 必须要将处理器的相关请求转换为配置读写请求。针对传统的 PCI 设备(Legacy PCI),采用的是 IO 间接寻址访问(IO-indirect Accesses);针对 PCIe 设备,采用的是 Memory-Mapped Accesses。


4 、PCIE设备的中断机制
4.1 、MSI中断
MSI 本质上是一种 Memory Write,和 PCIe 总线中的 Message 概念没有关系。并且,MSI 的 Data Payload 也是固定的,始终为 1DW。
由于 MSI 也是从 PCI 总线继承而来的,因此 MSI 相关的寄存器也存在于配置空间中的 PCI 兼容部分(前 256 个字节)。如下图所示,MSI 有四种类型:

带 Per-Vector Masking(每向量屏蔽)功能的 MSI 中断配置空间结构:


其中 Capability ID 的值是只读的,05h 表示支持 MSI 功能。
Next Capability Pointer 也是只读的,其用于查找下一个 Capability Structure 的位置,其值为 00h 则表示到达 Linked List 的最后了。
Message Control Register 用于确定 MSI 的格式与支持的功能等信息,如下图所示:

Message Address Register:32-bit 最低两位固定为 0,使得该地址是 DW 对齐的。
当 Mask Bits 将相关的中断向量(Interrupt Vector)屏蔽后,该 MSI 将不会被发送。软件可以通过这种方式来使能或者禁止某些 MSI 的发送。如果相关中断向量没有被屏蔽,则如果发生了相关中断请求,这时 Pending Bits 中的相应 bit 则会被置位。一旦中断信息被发出,则该 bit 会立即被清零。
PCIe 设备会根据配置空间中的 MSI 请求信息,来创建 Memory Write TLP,来讲 MSI 信息发送出去。作为一种特殊的 TLP,传递 MSI 的 TLP 需要遵循以下规则:
- No Snoop 和 Relaxed Ordering bits 的值必须为 0;
- TLP 长度值必须为 01h;
- First BE 必须为 1111b;
- Last BE 必须为 0000b;
- 地址是直接从配置空间中的响应位置复制过来的。
如下图所示:

4.2 、MSI-X中断
MSI 只支持 32 个中断向量,而 MSI-X 支持多达 2048 个中断向量,但是 MSI-X 的相关寄存器在配置空间中占用的空间却更小。这是因为中断向量信息并不直接存储在这里,而是在一款特殊的 Memory(MIMO)中。并通过 BIR(Base address Indicator Register, or BAR Index Register)来确定其在 MIMO 中的具体位置。如下图所示:

Message Control 寄存器的具体描述如下:

查找表示意如下:

结构图如下:


无论是 MSI 还是 MSI-X,其本质上都是基于 Memory Write 的,因此也可能会产生错误。比如 PCIe 中的 ECRC 错误等。
二、NVME协议介绍
NVMe协议旨在解决使用基于PCIe 的固态硬盘或结构连接设备的企业和客户端系统的需求。NVMe协议采用命令提交与完成机制优化了命令提交和完成路径,支持命令的并行操作,支持I/O 队列的数量最大为65535个,每个I/O 队列支持最多64K条命令。
1 、NVMe 队列
NVMe 协议基于配对的提交和完成队列机制,并存在Admin 队列和I/O 队列两种队列。Admin 提交和完成队列负责管理和控制NVMe 设备,只有Admin 命令集的命令才可以提交到Admin 提交队列。I/O 提交和完成队列负责实现NVMe 设备的I/O命令集,在NVMe 协议中命名为NVM 命令集。在一个正常运行的NVMe 设备中,Admin 提交队列与Admin 完成队列对应,Admin 队列在NVMe 初始化过程中创建,有且仅有一对Admin 队列对。I/O 队列需要通过Admin 命令管理,多个I/O 提交队列可以关联到同一个I/O 完成队列,并且可以存在多组I/O 队列关联关系。
- 提交队列(Submission Queue,SQ)是一个具有固定大小的环形缓冲区。主机控制器使用提交队列提交命令,NVMe 设备读取提交队列执行命令。在主机端,主机控制器通过更新对应的SQ 尾门铃寄存器,来表示有新的命令需要执行。在设备端,当有新的门铃寄存器写入时,NVMe 设备根据先前的SQ 尾门铃寄存器值和新的写入值计算需要处理的SQ 条目数量,然后从提交队列中获取SQ 条目,随后按任意顺序执行这些命令。
- 完成队列(Completion Queue,CQ)与提交队列硬件结构相同,用于反馈已完成命令的状态。一个已完成的命令由和该完成队列关联的提交队列标识符和命令ID 的组合进行唯一标识。当有一个或多个已完成命令状态得到处理后,主机控制器会更新对应的CQ 头门铃寄存器。
每个队列在被创建时分配了大小,门铃寄存器的值在达到最大深度时会回到初始位置,队列满状态定义如图2.3 所示,队列空状态定义如图2.4 所示。当队列头门铃值等于尾门铃值时,队列处于空状态,当队列头门铃值比尾门铃值多一时,队列处于满状态,满状态下队列中存在的命令数量比实际队列缓存深度小一。
当Head条目指针等于Tail条目指针时(Head == Tail),队列为空。
当Head条目指针比Tail条目指针多1时(Head == Tail + 1),队列已满。满时队列中的条目数比队列Size少一个。注意:在确定队列是否已满时,应考虑队列封装条件。


Tail (尾指针):
一个Queue条目的提交者,使用当前Tail条目指针,来标识下一个空闲Queue条目空间。
提交者将新条目,提交到Tail指向的空闲Queue条目空间后,对Tail条目指针加1。
如果Tail条目指针增量,超过Queue大小,则Tail条目指针将归零。
只要Queue未满,提交者可以继续向Queue提交条目。
Head (头指针):
Queue上条目的消费者,使用当前的Head条目指针,来标识下一个要从Queue中取出的条目。
在从Queue中取出下一个条目后,消费者对Head条目指针加1。
如果Head条目指针增量,超过Queue大小,则Head条目指针将归零。
只要Queue不为空,消费者就可以继续从Queue中取出条目。
Submission Queue和关联的Completion Queue的创建和删除,需要由主机软件正确排序。
- 先创建Completion Queue,后创建Submission Queue:主机软件应在,创建任何关联的Submission Queue之前,创建Completion Queue。Submission Queue可以在,关联的Completion Queue创建后的任何时间创建。
- 先删除Submission Queue,后删除Completion Queue:主机软件应在,删除Completion Queue之前,删除所有相关联的Submission Queue。主机软件应该,只在Submission Queue处于空闲状态,且没有未完成的命令时才删除它。
2 、NVMe 数据结构
2.1 、NVMe 寄存器
基于PCIe 接口的NVMe 设备使用BAR0 空间作为NVMe 寄存器的存储空间,NVMe 的寄存器组部分寄存器定义如下所示,其中提交队列门铃寄存器的偏移地址计算公式为1000h+(2n*(4<<CAP.DSTRD)),完成队列门铃寄存器的偏移地址计算公式为1000h+(2(n+1)*(4<<CAP.DSTRD)),公式中的CAP.DSTRD 为控制器功能寄存器中的门铃跨度字段,n 表示门铃寄存器对应的队列ID 编号。



上表中的各NVMe 寄存器将在初始化过程中进行配置或提供信息,门铃寄存器是实现NVMe 机制的关键组件之一。
2.2 、NVMe提交队列格式
NVMe 协议规定提交队列条目即NVMe 命令的大小为64 字节,提交队列条目的数据结构如下所示。
命令Dword 0定义,如下表所示。

Admin命令集(Admin Command Set)和NVM命令集(NVM Command Set)的64字节命令格式,定义如下表所示。



NVMe 协议支持Admin 命令集和NVM 命令集,NVMe 设备将根据提交队列条目中的命令代码与所在提交队列选择具体执行的指令。当命令位于Admin 提交队列时,命令代码对应Admin 命令集,Admin 命令集和NVM 命令集的常用命令分别如下所示。
Admin 命令集:



NVM命令集:


2.2 、NVMe完成队列格式
NVMe 协议规定,NVMe 设备在完成提交指令的处理后,需要向完成队列写入指令处理状态信息,即完成队列中的完成条目,其数据长度为16 字节,数据结构如下所示。


Dword 0的内容是特定于命令的。如果命令使用Dword 0,则此Dword的定义包含在关联的命令定义中。如果命令不使用Dword 0,则该字段为保留字段。Dword 1已被保留。下图定义了Dword 2, Dword 3。将来定义的任何其他I/O命令集都可以使用备用的完成队列条目大小或格式。
DW2:

DW3:

状态字段定义:

状态码类型(SCT):
完成队列条目指示正在报告的完成类型的状态代码类型。

状态代码(SC):
完成队列条目中的状态代码(SC)字段指示有关正在报告的完成的更详细的状态信息。
每个状态代码值集分为三个范围:
00h--7Fh:适用于管理命令集,或跨多个命令集。
80h--BFh:I/O命令集特定状态代码。
C0h--FFh:特定于供应商的状态代码。
如果有多个状态代码适用于特定的命令故障,控制器应报告数值最低的状态代码。
通用命令状态定义:
状态代码类型为Generic Command Status的完成队列条目指示与该命令关联的状态值,该值在许多不同类型的命令中是通用的。



3 、NVMe 数据寻址方式
NVMe 协议支持物理区域内存页(Physical Region Page,PRP)和散集列表(ScatterGather List,SGL)两种寻址模型。
3.1 、PRP 寻址模型
PRP 寻址示意图如图2.5 所示,NVMe 将主机端存储分为页的集合,PRP 条目是指向物理内存页面的指针。PRP 被用作主机和存储设备之间的数据传输的散点收集机制,实现了有效的无序数据传输。一条PRP 条目长度为64 比特,包括基本页地址和偏移量两个字段,其中偏移量字段的长度取决于物理内存页大小。一条PRP 条目能够指向一个物理内存页大小的数据,当数据量较大时,PRP 条目也可作为指向一个PRP 列表的指针。每个PRP 列表中最多包含内存页大小数量的PRP 条目,当需要更多的PRP 条目时,PRP 列表的最后一个条目指向新的PRP 列表。PRP 具有内存页对齐的属性,除了首条PRP 条目可以带有偏移,其余的PRP 条目偏移字段必须为0,保持内存页对齐状态。

在NVMe 提交指令条目中存在一个长度为128 比特的数据指针字段,该字段即包含了两个PRP 条目,分别命名为PRP1、PRP2。PRP1 常用作PRP 条目指向数据传输地址,由于PRP 具有内存页对齐的属性,PRP2 根据数据传输长度以及PRP1 的偏移确定类型。PRP2 类型的确定方式如表2.10 所示。

3.2 、SGL 寻址模型
SGL是一个数据结构,用于描述一段数据空间,这个空间可以是数据源所在的空间,也可以是数据目标空间。SGL和PRP本质的区别在于:PRP描述的是物理页,而SGL可以描述任意大小的内存空间。Admin命令只支持PRP,I/O命令可以支持PRP或者SGL;

SGL首先是一个链表,由一个或者多个SGL段(Segment)组成,而每个SGL段又由一个或者多个SGL描述符(Descriptor)组成。SGL描述符是SGL最基本的单元,它描述了一段连续的物理内存空间:起始地址+空间大小。
每个SGL描述符大小是16字节。一块内存空间,可以用来放用户数据,也可以用来放SGL段,根据这段空间的不同用途,SGL描述符也分几种类型。


SGL每个类型的含义:
- 数据块描述符,用于表明这段空间是用户数据空间。
- 段描述符,SGL是由SGL段组成的链表,既然是链表,前面一个段就需要有一个指针指向下一个段,这个指针就是SGL段描述符,它描述的是它下一个段所在的空间。
- 对链表当中倒数第二个段,它的SGL段描述符我们称之为SGL末段描述符。它本质还是SGL段描述符,描述的还是SGL段所在的空间。目的是让SSD在解析SGL的时候,碰到SGL末段描述符,就知道链表快到头了,后面只有一个段了。
- SGL位桶也是一种描述符,它只对主机读有用,用于告诉SSD往这个内存写入的东西不是它要的,所以不用传了。
4 、NVMe指令提交与完成机制
NVMe 指令提交与完成机制是NVMe 协议的核心,该机制制定了NVMe 指令的交互流程和处理步骤。在基于PCIe 的NVMe 协议中,NVMe 主机控制器与NVMe设备的交互通过PCIe 进行,使用PCIe 的存储读写TLP 请求。指令的提交与完成机制如图2.6 所示,图中展示的指令提交与完成步骤如下:


(1)主机控制器向提交队列写入一个或多个提交命令;
(2)主机请求配置NVMe 设备的提交队列尾门铃寄存器,表示有新的命令需要
进行处理;
(3)NVMe 设备从提交队列中读取提交指令条目;
(4)NVMe 继续执行获取的命令,命令可以无序完成;
(5)命令完成执行后,控制器将完成队列条目写入关联的完成队列。NVMe 设
备在完成队列条目中提供最近的关联提交队列条目头指针;
(6)NVMe 设备可选地向主机生成一个中断,以表明有一个新的完成队列条目
要等待主机控制器使用和处理;
(7)主机控制器处理完成队列中的新完成队列条目;
(8)主机控制器请求配置完成队列头门铃寄存器,指示完成队列条目已被处理。
三、NVME寄存器定义
NVMe控制器寄存器,位于配置空间BAR0与BAR1,所映射的内存空间 中。
在PCI Header中,有如下定义:

BAR0为低32位,BAR1为高32位,一起组合为64位内存地址,表示PCIe设备内存空间的基址。

NVMe控制器寄存器,就位于该内存空间中,并且host访问这些寄存器,应按原始宽度或32位对齐来访问。NVMe控制器寄存器,定义如下:

1、Offset 00h: CAP -- Controller Capabilities
Controller Capabilities寄存器,共8字节,表示控制器对Host软件的基础能力。


2、Offset 08h: VS -- Version
Version寄存器,共4字节,表示控制器支持的NVM Express规范主和从版本。上面两个字节,表示主版本号;下面两个字节,表示从版本号。例如:版本3.12将表示为00030102h。本规范的有效版本是1.0。

3、Offset 0Ch: INTMS -- Interrupt Mask Set
Interrupt Mask Set寄存器,共4字节。当使用基于引脚的中断、单消息MSI或多消息MSI时,该寄存器用于屏蔽中断。当使用MSI-X时,应该使用定义为MSI-X一部分的中断掩码表来屏蔽中断。当为MSI-X配置时,主机软件不得访问此寄存器;为MSI-X配置的任何访问都是未定义的。

4、Offset 10h: INTMC -- Interrupt Mask Clear
Interrupt Mask Clear寄存器,共4字节。当使用基于引脚的中断、单消息MSI或多消息MSI时,该寄存器用于解除屏蔽中断。当使用MSI-X时,应该使用定义为MSI-X一部分的中断掩码表来解除中断掩码。当为MSI-X配置时,主机软件不得访问此寄存器;为MSI-X配置的任何访问都是未定义的。

5、Offset 14h: CC -- Controller Configuration
Controller Configuration寄存器,共4字节。该寄存器修改控制器的设置。主机软件应将Arbitration Mechanism(CC.AMS)、Memory Page Size(CC.MPS)和Command Set(CC.CSS)设置为有效值,然后通过将CC.EN设置为'1'来启用控制器。


6、Offset 1Ch: CSTS -- Controller Status
Controller Status寄存器,共4字节。

7、Offset 20h: NSSR -- NVM Subsystem Reset
NVM Subsystem Reset寄存器,共4字节。此可选寄存器,为主机软件提供初始化NVM Subsystem Reset的能力。对该寄存器的支持由NVM Subsystem Reset Supported(CAP.NSSRS)字段的状态表示。如果寄存器不被支持,则保留寄存器占用的地址范围。

8 、Offset 24h: AQA -- Admin Queue Attributes
Admin Queue Attributes寄存器,共4字节。这个寄存器定义了Admin Submission Queue和Admin Completion Queue的属性。Admin Submission Queue和Admin Completion Queue的队列标识符是0h。Admin Submission Queue的优先级由所选择的仲裁机制决定。
Admin Submission Queue和Admin Completion Queue需要在物理上连续的内存中。

9 、Offset 28h: ASQ -- Admin Submission Queue Base Address
Admin Submission Queue Base Address寄存器,共8字节。这个寄存器定义了,管理提交队列的内存基地址。

10 、Offset 30h: ACQ -- Admin Completion Queue Base Address
Admin Completion Queue Base Address寄存器,共8字节。此寄存器定义了,管理完成队列的内存基地址。

11 、Offset (1000h + ((2y) * (4 << CAP.DSTRD))): SQyTDBL --Submission Queue y Tail Doorbell
Submission Queue y Tail Doorbell寄存器,共4字节。这个寄存器定义了doorbell寄存器,用于更新Submission Queue y的尾部条目指针(Tail),y的值等于队列标识符。这向控制器表明,已经提交了新的命令待处理。
- Host不应该读取doorbell寄存器,如果读取doorbell寄存器,则返回特定于供应商的值。
- 向不存在的Submission Queue Tail Doorbell写入,结果未定义。

12 、Offset (1000h + ((2y + 1) * (4 << CAP.DSTRD))): CQyHDBL -- Completion Queue y Head Doorbell
Completion Queue y Head Doorbell寄存器,共4字节。这个寄存器定义了doorbell寄存器,用于更新Completion Queue y的头部条目指针(Head),y的值等于队列标识符。这表示已被Host软件处理完毕的,完成队列条目。
- Host不应该读取doorbell寄存器,如果读取doorbell寄存器,则返回特定于供应商的值。
- 写入一个不存在的Completion Queue Head Doorbell,有未定义的结果。
- Host软件应该确保,它继续处理Completion Queues中的完成队列条目,而不管在特定或任何Submission Queue中是否有可用条目。

13 、Doorbell寄存器组织结构

每个NVMe控制器有一个Admin Queue。因此,协议规定,Admin Queue的Doorbell,默认固定从1000h开始:
- 1000h ~ 1003h,Submission Queue 0 Tail Doorbell (Admin)
- 1000h + (1 * (4 << CAP.DSTRD)) ~ 1003h + (1 * (4 << CAP.DSTRD)),Completion Queue 0 Head Doorbell (Admin)
CAP.DSTRD,就是Controller Capabilities的Doorbell Stride (DSTRD)寄存器,表示Doorbell寄存器之间的间隔,间隔被指定为(2 ^ (2 + DSTRD))字节。DSTRD=0h表示4字节的间隔,其中Doorbell寄存器被打包,每个寄存器之间没有保留空间。
每个NVMe控制器最多64K个I/O Queue。Admin Submission Queue和Admin Completion Queue的队列标识符均为0h;因此,IO Queue标识符从1开始,一直到y,y最大可以为64k。
因此,协议规定,I/O Queue的Doorbell偏移范围为:
- 从1000h + (2 * (4 << CAP.DSTRD)) ~ 1003h + (2 * (4 << CAP.DSTRD))开始,Submission Queue 1 Tail Doorbell
- 至1000h + ((2y + 1) * (4 << CAP.DSTRD)) ~ 1003h + ((2y + 1) * (4 << CAP.DSTRD))结束,Completion Queue y Head Doorbell
四、NVME指令集
1 、Admin命令集(Admin Command Set)

1.1 Identify (识别)指令
Identify 指令用于确认控制器和各namespace 的配置属性信息,其仅使用了DW0 至DW10 字段,DW11 至DW15 字段为保留字段。EP 端在收到Identify 指令之后,会以规定的格式返回总量为4096byte 的数据结构,该数据结构表征控制器或namespace 的配置属性;Identify 指令中的DW6 至DW9 数据指针将表示返回的数据结构在主机内存中存放的起始地址;DW10 中主要标明了控制器ID 和所需返回的数据结构类型等两个重要参数。

所有 Identify 命令返回的数据长度固定为 4096 字节(4KB),主机必须提供一个 4KB 对齐的缓冲区。
1.2 Set Feature(设置特征)指令
Set Feature 指令用于配置NVMe 控制器的一些特性。其DW10 字段如表2-8所示,其中Feature ID 字段表征了Set Feature 指令具体所配置的特性类型。其使用的DW11字段格式如表2-9 所示,DW12 至DW15 字段为保留字段。DW11 字段中的NCQR和NSQR 字段表征主机端请求的提交队列和完成队列数量,EP 端NVMe 控制器在处理完该Set Feature 指令后,会在完成信息报文DW0 字段中反馈告知主机端实际分配的提交队列和完成队列数量。


1.3 Create I/O Completion Queues (创建I/O 完成队列)指令
Create I/O Completion Queues指令用于创建I/O 完成队列。由于完成和提交队列均位于主机内存中,因而这两条指令均使用指令中的DW6 和DW7 字段表示队列在主机内存中的起始地址;其指令格式中的DW10 和DW11 字段格式如下所示,DW12 至DW15 字段为保留字段。

1.4 Create I/O Submission Queues (创建I/O提交队列)指令
Create I/O Submission Queues 指令用于创建I/O提交队列。由于完成和提交队列均位于主机内存中,因而这两条指令均使用指令中的DW6 和DW7 字段表示队列在主机内存中的起始地址;其指令格式中的DW10 和DW11 字段格式分别如表2-10 和2-11 所示,DW12 至DW15 字段为保留字段。


1.5 Delete I/O Completion Queue (删除I/O完成队列)指令
Delete I/O Completion Queue命令用于删除I/O完成队列。Delete I/O Completion Queue命令使用命令Dword 10字段。保留所有其他命令特定字段。此命令完成后,描述完成队列的PRP列表可能会被主机软件释放。
主机软件应确保在删除完成队列之前删除任何相关的I/O提交队列。如果存在任何关联的I/O提交队列,则Delete I/O Completion Queue命令将失败,状态值为Invalid Queue Deletion。 注意:无法删除管理完成队列。

命令完成:
当指定的I/O完成队列已被删除时,完成队列条目将被发送到管理完成队列。下图定义了删除I/O完成队列命令特定的状态值。

1.6 Delete I/O Submission Queue (删除I/O提交队列)指令
删除 I/O提交队列命令用于删除I/O提交队列。Delete I/O提交队列命令使用命令Dword 10字段。保留所有其他命令特定字段。此命令完成后,主机软件可以释放描述提交队列的PRP列表。
成功完成Delete I/O提交队列命令后,先前提交到指定提交队列的所有I/O命令应显式完成或隐式完成。在返回Delete I/O Submission queue命令的完成队列条目之前,以前提交到要删除的I/O提交队列的其他命令可能会以适当的状态完成(例如,成功完成、由于SQ删除而中止的命令)。在成功完成Delete I/O提交队列命令之后,对于提交到deleted I/O提交队列的任何I/O命令,控制器不应发布完成状态。Delete I/O Submission Queue命令的成功完成表示由于以前提交的任何I/O命令的SQ删除而中止的命令的隐式完成状态,该命令没有由控制器发布完成队列条目。
注意:无法删除管理提交队列。

命令完成:
在提交到指定的I/O提交队列的所有命令完成或中止后,在删除队列时,将向管理完成队列发送完成队列条目。下图定义了Delete I/O提交队列命令特定的状态值。

2 、NVM命令集(NVM Command Set)

2.1 Read 和Write 指令
Read 和Write 指令分别用于从EP 端读和往EP 端写入数据,根据闪存介质的特性,其数据读写以逻辑块(logical block)为单位;逻辑块的大小可通过分析Identify 指令返回的数据结构获知。Read 和Write 指令中的DW6 至DW9 字段为读回的和写入的数据在主机内存中存放的地址;其指令格式中DW10 至DW15字段的组成格式如表2-12 所示,其中与数据传输密切相关的两个参数为SLBA和NLB,前者指明了读写操作起始逻辑块的编号,后者代表连续读或写的逻辑块的数量。


五、NVME设计流程
1 、建链、枚举
1.1、建链
基于7 Series FPGAs Integrated Block for PCIe IP
此过程在正确设置FPGA后,上电启动板子后自动完成建链。建立链接的过程如下:主要流程为上电后两侧根据PCIE总线协议进入LTSSM流程,链路双方自动协商速率和宽度,调节发送和接收参数。只要无硬件问题,NVMe先上电,FPGA后上电,参考UG477,LTSSM的状态及相关含义:当两端设备正常上电后应处于L0状态。

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



1.2、枚举
建链后开始进行设备枚举。PCIe总线中的每一个功能都有一个唯一的标识符与之对应,该标识符为(Bus,Device,Function);RC(BUS 0)搜索总线中BDF标识确定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总线下的设备信息:


2 、RC、EP初始化流程
在实现NVMe硬盘读写前,RC(FPGA)、EP(NVMe)需要按顺序进行相应的PCIe配置空间初始化,之后进行NVMe控制器寄存器进行配置。PCIe配置空间是每个PCIe设备独立的一段内存区域,用于存储设备的配置信息。RC在枚举设备时需要先访问配置空间,获取设备厂家、型号、类型、所需资源等信息,然后再分配资源,最后才能访问PCIe设备的存储或IO地址空间。本场景只使用设备配置空间Type 0。
2.1 、RC的PCIe配置空间初始化
RC的配置空间通过PCIE IP核的设置 已经实现了其相关配置寄存器的初始化 ;比如ID、BAR、能力结构等。FPGA(RC)端的PCIe配置空间相关信息的访问、重配置可通过核的配置管理接口CMI(Config Management Interface)进行实现的。
2.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寄存器位域的定义如下表所示:

3 、NVMe控制器的寄存器配置
在完成上述FPGA和NVMe硬盘的PCI和PCIe寄存器配置后,需要按照下述流程进行NVMe控制器配置:
① 等待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。
4 、NVMe协议命令
在完成上述操作后,即可通过Admin命令操作NVMe硬盘:
- Host通过Identify命令,确定Controller的数据结构等;
- Host通过set/get features获取I/O SQ和CQ信息,配置中断机制等;
- Host分配适当的I/O CQ、SQ队列;
- 然后才可发起对NVMe硬盘的读写IO指令。
NVMe命令主要分为Admin命令和IO命令,根据位于的队列分类;Admin命令只能提交到Admin SQ CQ中,主要负责管理NVMe控制器的一些控制指令。IO命令只能提交到I/O SQ CQ中,主要负责完成数据的传输。

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


4.1 、Admin命令执行顺序
RC在读写NVMe硬盘前需要按顺序执行如下操作执行Admin命令:
①执行Identify命令,获取Controller的数据结构;
②执行Set Features命令,申请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,标识着该条命令完成。
4.2 、IO命令执行实现读写硬盘
在完成上述操作后,即可通过IO命令读写NVMe硬盘。
至少建立两个IO队列,一个用于写操作,一个用于读操作。操作码0x01表示写数据到NVMe硬盘,操作码0x02表示从NVMe硬盘读出数据。需要设计PRP1、PRP2或SGL方式,IO队列的深度,准备好提交对应页数的IO读/写命令后及sub DB,通知NVMe进行相应的读写操作。
IO 读写命令交互过程与Admin命令类似,只是响应地址的不同。