本篇位置:FPGA NVMe Host 实战连载 · 🟩 第一幕|共同底座 · 第 03 篇
第 02 篇已经把 M.2 SSD 的供电、参考时钟、复位和 Lane 检查了一遍。假设现在user_lnk_up=1,LTSSM 已经进入 L0,说明 FPGA 与 SSD 的 PCIe 物理链路已经可以传输数据。
但这时 SSD 还没有真正“被识别”。如果 FPGA 不主动发起配置访问,SSD 不会自己把 Vendor ID、BAR 或 NVMe 寄存器送到 FPGA 面前。
原因在于两端的 PCIe 角色不同:三星 980 这样的 NVMe SSD 是Endpoint;FPGA 直连 SSD 时,需要站在Root Port和 Host 一侧,负责从配置空间访问开始,把后面的枚举、BAR、NVMe 初始化和数据通路逐步建立起来。
本文沿着下面这条控制路径展开:
FPGA Root Port → PCIe Configuration Request → NVMe SSD Endpoint → Completion → FPGA
先把这几个角色分清,后面的 Vendor ID、BAR、CAP、CC、CSTS、队列和 DMA 才不会混在一起。
PC 和 FPGA 直连 SSD,谁在做 Host
在 PC 里,CPU、芯片组、固件和操作系统共同承担 Host 一侧工作。SSD 插入 M.2 插槽后,系统会完成配置访问、资源分配和驱动初始化,用户平时几乎看不到这些步骤。
FPGA 直连 SSD 时,PC 的那套软件环境不在链路上。SSD 面前的上游设备就是 FPGA,因此配置访问和 NVMe Host 所需的逻辑,要由 FPGA 工程实现。
图 1:PC 和 FPGA 直连 SSD 的共同点是:SSD 都处于 PCIe Endpoint 一侧;差别在于由 PC 或 FPGA 承担 Host 工作。
这里有一个词需要分开说。
- Root Complex可以理解为 PCIe 树最上游的 Host 一侧整体。它把处理器、内存和 PCIe 域接在一起,并为下游设备提供访问路径和资源管理。
- Root Port是 Root Complex 面向下游设备的端口。它直接与 Endpoint 建链,并能够向下游发起 PCIe 访问。
- Endpoint是被上游发现和配置的设备。本文的三星 980 1TB 对 PCIe 来说是 Endpoint;它内部包含 NVMe 控制器和 NAND Flash。
在一个 FPGA 直连单块 SSD 的工程里,没有必要照搬完整 PC 的软件体系。但从 SSD 的角度看,FPGA 仍要完成这个 Root Port 需要完成的工作:先发起配置访问,再建立可访问的 NVMe 控制器路径。
Root Port 不是 Endpoint 的另一种叫法
很多 FPGA PCIe 示例工程默认采用 Endpoint 模式。它们的目标通常是把 FPGA 做成一张 PCIe 扩展卡,让 PC 枚举 FPGA,并访问 FPGA 暴露出来的 BAR。这个方向没有问题,只是和“FPGA 直连 SSD”正好相反。
| 场景 | 上游 Root Port / Host | Endpoint | 谁先发起配置访问 |
|---|---|---|---|
| PC 插 FPGA 加速卡 | PC | FPGA | PC |
| FPGA 直连 NVMe SSD | FPGA | NVMe SSD | FPGA |
因此,不能只把 PCIe 差分线接好,再拿一个 Endpoint 示例工程去等待 SSD 工作。即使链路训练成功,两个只按 Endpoint 方式工作的设计之间,也没有一方承担枚举、资源分配和下游配置请求的责任。
这里要说得严谨一些:这不等于“两个 Endpoint 的电气链路一定无法建立”。问题在于,即便底层链路能够进入 L0,SSD 也不会因此自动变成可供 FPGA 使用的 NVMe 设备。它仍在等待上游 Host 的后续动作。
AMD 的 7 系列 PCIe IP 本身支持 Endpoint 和 Root Port 两种配置。本文使用的 XC7Z100 平台将 PCIe IP 配置为 Root Port;但 IP 角色配置只是起点,后续的配置请求、Completion 处理和 NVMe Host 逻辑仍要由工程逻辑完成。
Root Port 自己也有配置空间
Root Port 和 Endpoint 都有配置空间,但使用的配置头类型不同。AMD PG054 中明确区分了两者:Endpoint 使用 Type 0 Header,Root Port 应用使用 Type 1 Header。
图 2:AMD《PG054 v3.3》Table 25,p.44。Root Port 使用的 Type 1 PCI Configuration Space Header,非本项目实测截图。
这张表不用在本篇逐字段背下来,但可以先看出 Root Port 和普通 Endpoint 的侧重点不同。Type 1 Header 中出现了 Primary Bus Number、Secondary Bus Number、Subordinate Bus Number,以及 Memory Base、Memory Limit 等桥接和下游资源相关字段。
对本项目来说,最重要的理解是:FPGA 不只是“链路另一端”。它需要作为上游端,知道怎样访问下游 SSD,并为后续的地址分配和控制器寄存器访问做好准备。先把最小的配置读取走通,才能对这些后续工作建立可靠的判断。
SSD 作为 Endpoint,会暴露什么给 Host
NVMe SSD 并不是一块把 NAND 地址直接接到 PCIe 总线上的存储器。它首先是一个 PCIe Endpoint,具有 PCI Header、PCIe Capability、MSI/MSI-X 和 AER 等 PCIe 结构;在此基础上,才通过 BAR 暴露 NVMe 控制器寄存器。
NVM Express 的 NVMe over PCIe Transport 规范列出了这些 PCIe 寄存器结构,并说明控制器属性映射在 MLBAR/MUBAR 指定的地址范围,也就是 PCI BAR0/BAR1 对应的内存窗口。
图 3:NVM Express《NVMe over PCIe Transport Specification v1.4》Figure 3,p.9。图中列出 NVMe PCIe 控制器的 PCI Header、Capability 与 AER 结构;下方文字说明控制器属性由 BAR0/BAR1 指定的内存窗口访问。
这也解释了为什么不能跳过 PCIe 枚举,直接读 CAP 或写 CC:在访问 NVMe 控制器寄存器前,Host 先要确认 Endpoint 存在,读取配置空间,并处理 BAR 等 PCIe 层的前置条件。
Link Up 后,第一件事是谁发起
链路进入 L0 后,SSD 不会先“报告自己是 NVMe SSD”。最小的下一步是由 FPGA Root Port 发起 Configuration Read。SSD 作为 Endpoint 返回 Completion with Data,FPGA 可以先从返回数据中确认 Vendor ID,判断下游设备是否已经被正确识别。
图 4:从 PCIe Link Up 到读取配置空间标识信息的最小控制路径。图为项目调试路径示意,不是完整 PCIe 枚举状态机。
这张图有两个方向,不能混淆。
- FPGA 先发出 Configuration Read,请求读取 SSD 的配置空间。
- SSD 返回 Completion with Data,带回这次读取的结果。
因此,user_lnk_up=1的意义是“现在可以开始发起这类访问了”,而不是“Vendor ID 已经读到”。读到有效的 Vendor ID,才是枚举真正有了第一个可验证结果。
这条路径在板上需要确认三件事:请求是否从 FPGA 发出、SSD 是否返回 Completion with Data、返回的配置空间字段能否被正确解析。后续上板实测章节会用这三个观察点给出完整结果;本篇先把 Root Port 与 Endpoint 的职责和通信方向理清。
SSD 后面也会主动发数据,为什么它仍不是 Host
这个问题很容易让人困惑。后面的 NVMe 读命令执行时,SSD 会通过 PCIe Memory Write 把读取结果 DMA 到 Host 提供的内存地址;写入 SSD 时,SSD 还会通过 Memory Read 取得 Host 准备好的数据。看上去 SSD 也会主动发 TLP。
但这不改变它是 Endpoint 的角色。因为这些动作发生在 Host 已经完成控制器初始化、建立队列,并通过 PRP 提供 Host Memory 地址之后。SSD 是按照 Host 提交的 NVMe 命令执行数据传输,不是主动承担配置、枚举和资源管理的一方。
可以把两件事分开看:
| 阶段 | 谁先发起 | SSD 的角色 |
|---|---|---|
| PCIe 枚举 | FPGA Root Port | 返回配置访问的 Completion |
| NVMe 控制器初始化 | FPGA NVMe Host | 响应寄存器访问,进入 Ready |
| NVMe 读取数据 | FPGA 提交 NVM Read | 依据 PRP,DMA 写入 Host Memory |
| NVMe 写入数据 | FPGA 提交 NVM Write | 依据 PRP,读取 Host Memory 中的数据 |
角色说的是 PCIe 拓扑和控制责任,不是“哪一侧永远不能发起任何数据包”。
从 Root Port IP 到可用系统,工程怎样分层
“PCIe IP 已配置为 Root Port”并不等于“FPGA 已经是完整 NVMe Host”。在这个工程里,至少还有下面几层工作。
图 5:本篇重点在第 2 层 PCIe Host。第 02 篇解决物理链路,后续章节再进入 NVMe Host 与两条数据通路。
- 物理链路层:确认供电、REFCLK、
PERST#、Lane 和 LTSSM,最终进入 L0。这是第 02 篇解决的问题。 - PCIe Host 层:Root Port 发起配置访问、读取配置空间、完成枚举并处理 BAR。这是当前和接下来几篇的重点。
- NVMe Host 层:读取 CAP、配置 CC、等待 CSTS,建立 Admin Queue 和 I/O Queue。
- 数据通路层:让 SSD 能依据 PRP 访问 PS DDR 或 PL DDR,并完成 DMA、缓冲和校验。
这四层按顺序依赖。前一层没有证据,后面的调试就容易变成猜测。例如,如果配置空间还读不到,就不应先检查 CC.CSTS;如果队列还没有建立,也不能要求 SSD 把用户数据 DMA 到 DDR。
三个容易走偏的判断
1. “Link Up 了,说明 NVMe 驱动有问题”
不一定。Link Up 只说明 PCIe 物理链路可用。下一步应该观察 Configuration Request 有没有发出、Completion 有没有回来、返回数据是否合理。NVMe 初始化还在更后面。
2. “用 Endpoint 示例工程,SSD 应该会自动被识别”
不会。Endpoint 示例适合让 PC 枚举 FPGA;而 FPGA 直连 SSD 时,FPGA 需要改为 Root Port 和 Host 侧角色。两种场景的上游、下游方向不同。
3. “SSD 会 DMA,所以 SSD 也算 Host”
不算。SSD 的 DMA 是在 Host 提交 NVMe 命令并给出 PRP 地址后执行的。枚举、BAR、控制器初始化和队列建立仍由 FPGA Host 发起。
本篇自查清单
| 检查项 | 需要看到的结果 | 还不能据此断定什么 |
|---|---|---|
| PCIe IP 角色 | Root Port 模式 | 不代表已经完成枚举 |
| 物理链路 | LTSSM=L0,user_lnk_up=1 | 不代表配置空间可读 |
| 配置访问 | FPGA 发起请求,收到 Completion | 不代表 BAR 已配置完成 |
小结
FPGA 直连 NVMe SSD 时,SSD 是 PCIe Endpoint,FPGA 需要位于 Root Port 和 Host 一侧。链路进入 L0 后,第一件需要做的事不是提交 NVMe 命令,而是由 FPGA 发起 Configuration Request,读取 SSD 配置空间并确认设备存在。具体的板上读取结果放在后续实测章节展示。
Root Port 模式解决的是“谁来面对下游 SSD”;从这里到稳定的 NVMe 读写系统,还会经历枚举、BAR、控制器初始化、队列和数据通路等层次。下一篇先看链路进入 L0 后真正在线上运行的内容:TLP、DLLP 和 Completion 分别是什么,Configuration Read、Memory Read 与 Memory Write 各自承担什么角色。随后几篇会从配置空间访问开始,把请求发送、返回接收和结果解析的工程过程逐步讲清楚。
参考资料
- AMD,PG054 v3.3:7 Series FPGAs Integrated Block for PCI Express,本文图 2 引用 Table 25,p.44;文中 Root Port / Endpoint 配置能力参见 p.5。
- NVM Express,NVMe over PCIe Transport Specification v1.4,本文图 3 引用 Figure 3,p.9;BAR0/BAR1 映射说明位于同页。
- NVM Express,Base Specification v2.4,用于 Host、Controller 和 PCIe Memory-Based Transport 术语核对。
- PCI-SIG Specifications,PCIe 术语与规范入口。