跳到主要内容汇编算法
基于 FPGA 实现 NVMe 硬盘读写功能
综述由AI生成在 FPGA 上作为 Root Complex 控制 NVMe 硬盘的实现流程。涵盖 PCIe 总线架构、TLP 事务、配置空间初始化(RC/EP)、BAR 设置、MSI-X 中断配置及 NVMe 控制器寄存器(AQA、ASQ、ACQ、CC)配置。详细阐述了 Admin 命令(Identify、队列创建)与 IO 命令(Read/Write)的交互过程及 DoorBell 机制。最后通过实际测试验证了读写性能,读速约 1260MB/S,写速约 840MB/S。
云间漫步38 浏览 概述
PCIe 总线架构如下:

在以往的 FPGA 板卡与上位机的开发使用 PCIe 总线时,FPGA 常作为 Endpoint 使用;而在 FPGA 外挂 NVMe 硬盘的应用中,FPGA 作为 Root Complex,NVMe 硬盘作为 Endpoint 使用。NVMe 协议是基于 PCIe 总线实现的,属于 PCIe 的一种特殊应用。
PCIe 概述
PCIe 协议主要涉及两种机制、三层结构、四种事务类型。
数据交换是基于请求与完成(响应)的机制,两种模式:发送数据不需要接收端响应,要求接收端对发送数据进行响应。
分为事务层、链路层和物理层,以此实现用户数据的交互。用户主要关注事务层构造/解析各类包,但 PCIe 比 SRIO 复杂的多。
数据类型被分为内存、I/O、配置和消息四类,分别完成不同的功能。
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。
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 为止。如下图所示:
建链、枚举
建链
此过程在正确设置 FPGA 后,上电启动板子后自动完成建链。建立链接的过程如下:主要流程为上电后两侧根据 PCIe 总线协议进入 LTSSM 流程,链路双方自动协商速率和宽度,调节发送和接收参数。
只要无硬件问题,NVMe 先上电,FPGA 后上电,参考 UG477,LTSSM 的状态及相关含义:当两端设备正常上电后应处于 L0 状态。
相关状态、参数等可通过 PCIe 核接口参数获取检测:
枚举
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 总线下的设备信息:
RC、EP 初始化流程
在实现 NVMe 硬盘读写前,RC(FPGA)、EP(NVMe)需要按顺序进行相应的 PCIe 配置空间初始化,之后进行 NVMe 控制器寄存器进行配置。
PCIe 配置空间是每个 PCIe 设备独立的一段内存区域,用于存储设备的配置信息。RC 在枚举设备时需要先访问配置空间,获取设备厂家、型号、类型、所需资源等信息,然后再分配资源,最后才能访问 PCIe 设备的存储或 IO 地址空间。本场景只使用设备配置空间 Type 0。
RC 的 PCIe 配置空间初始化
RC 的配置空间通过 PCIe IP 核的设置已经实现了其相关配置寄存器的初始化;比如 ID、BAR、能力结构等。
FPGA(RC)端的 PCIe 配置空间相关信息的访问、重配置可通过核的配置管理接口 CMI(Config Management Interface)进行实现的。
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 中断。
Cap ID 为 0x10 即为 PCIe 能力结构,其指针即为该能力指针。
比如通过 CfgRd 访问能力结构体指针偏移为 0x04 的 Device Capabilities Register;通过 CfgWr 设置能力结构体指针偏移为 0x08 的 Device Control 寄存器。
如果基地址设置为 32 位,只需设置 BAR0 即可;如果设置基地址设置为 64 位,配置 BAR0 和 BAR1。消费级的 NVMe 硬盘一页对应 4KByte,通过 CfgWr 设置 TYPE0 配置空间偏移为 0x04 的 BAR0 为 4K 对齐并设置其具体基地址。
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 |
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。
NVMe 协议命令
在完成上述操作后,即可通过 Admin 命令操作 NVMe 硬盘:
Host 通过 Identify 命令,确定 Controller 的数据结构等;
Host 通过 set/get features 获取 I/O SQ 和 CQ 信息,配置中断机制等;
Host 分配适当的 I/O CQ、SQ 队列;
然后才可发起对 NVMe 硬盘的读写 IO 指令。
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 |
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。
IO 命令
IO 命令主要进行数据读写,NVMe 硬盘的读写以 LB 为单位进行。
IO 命令与 Admin 命令结构完全相同,也是通过 DW0 中的 8 位操作码来定义命令类型的。
| 操作码 | 指令 | 作用 |
|---|
| 00h | Flush | 将数据(和元数据)提交到 NVM 中,所有命令都要执行 |
| 01h | Write | 将数据写入 NVM 中 |
| 02h | Read | 读 NVM 中的数据 |
| 04h | Write Uncorrectable | 标记无效数据块 |
| 05h | Compare | 比较从 NVMe 读出的数据和比较数据缓冲区的数据 |
命令交互过程
对于 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 属性值队列深度。
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,标识着该条命令完成。
IO 命令执行实现读写硬盘
在完成上述操作后,即可通过 IO 命令读写 NVMe 硬盘。
至少建立两个 IO 队列,一个用于写操作,一个用于读操作。操作码 0x01 表示写数据到 NVMe 硬盘,操作码 0x02 表示从 NVMe 硬盘读出数据。
需要设计 PRP1、PRP2 或 SGL 方式,IO 队列的深度,准备好提交对应页数的 IO 读/写命令后及 sub DB,通知 NVMe 进行相应的读写操作。
IO 读写命令交互过程与 Admin 命令类似,只是响应地址的不同。
NVMe 硬盘读写测试
硬件信息
硬件测试涉及 FPGA 板卡及 M.2 接口等,为节省成本采用拆机盘测试。有条件的建议直接上工业盘。
本次测试的硬盘标签型号信息如下:共 512GB 容量,其中LB 为 512 字节,LBA 数量共 1000215216 个,约 477GB;之后的测试 FPGA 可将相关信息获取出来。
FPGA 板卡为 7 系列的,在此系列上 PCIe Gen2 x4 的理论速率上限为 2GB/S。
文件系统
NVMe 硬盘支持 NTFS、exFAT 等文件系统。
FPGA 纯逻辑实现文件系统中的文件读写,难度不大,但是繁琐。
本文测试不包含文件系统;对于很多应用场景,比如数据采集回放、记录仪等不使用文件系统也可以完成,只需记录每次的数据量 LBA 的地址范围即可,数据回放时根据 LB 号对应读取即可。
硬盘信息获取
w_Namespace_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 状态
读和写功能测速
通过 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。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online