本篇位置:FPGA NVMe Host 实战连载 · 第二阶段 · 第 11 篇
第 08 章通过 Configuration Read 读到了三星 980 的 Vendor ID,第 10 章又说明了 BAR、NVMe 控制器属性和队列之间的关系。接下来要访问CAP、VS、CC、CSTS和 Doorbell,不能继续使用 Configuration Read,也不能把目标 BDF 换成一个 BAR 数值后照搬原来的包头。
此时控制路径变为:
PL 请求逻辑 → PCIe Memory Read/Write → SSD BAR 寄存器窗口 → Completion with Data → PL 解析
本章先把 Configuration TLP 和 Memory TLP 的差异讲清楚。我们仍以项目使用的 64 位 AXI4-Stream 接口、Non-Flit Mode TLP 为例,但本章不宣称已经完成新的 BAR 或 NVMe 寄存器板测。下一章会在这些规则基础上处理 BAR0/BAR1 的探测和地址分配。
1. 先看结论:它们不是同一种地址访问
Configuration Request 和 Memory Request 都从 FPGA 的 PCIe 事务接口发出,也都属于 TLP,但它们解决的问题不同。
| 对比项 | Configuration Read/Write | Memory Read/Write |
|---|---|---|
| 目标空间 | PCIe Function 的配置空间 | 已映射的系统内存地址空间 |
| 定位方式 | 目标 BDF + Register Number | 32 位或 64 位地址 |
| 典型用途 | 枚举、读 Vendor ID、设置 Command 和 BAR | 访问 BAR/MMIO、队列内存和数据缓冲区 |
| 请求头 | 固定为 3-DW Header | 32 位地址用 3-DW,64 位地址用 4-DW |
| 读取返回 | CplD;失败时可能为不带数据的 Cpl | CplD;较大读取可能拆成多条 Completion |
| 成功写入 | 需要 Completion | 普通 Memory Write 是 Posted Request,不返回事务层 Completion |
这张表里最重要的不是包头多一个 DWORD,而是路由依据变了。配置访问的目标是 PCIe 树中的某个 Function,Memory 访问的目标是系统已经分配好的地址。BAR 正是连接两者的桥梁:Host 先在配置空间发现并配置 BAR,之后才使用对应的 Memory 地址访问控制器寄存器。
图 1:项目事务方向示意。Configuration Read 按 BDF 和寄存器号定位;Memory Read/Write 按地址定位。MRd 需要 CplD,普通 MWr 没有对应的事务层 Completion。
图 1 也说明,不能只用“有没有返回”判断请求类型。Configuration Read 和 Memory Read 都要等返回;区别主要在请求头和目标定位。Memory Write 没有 Completion,也不等于写入结果可以完全不检查,Host 仍应通过后续寄存器状态、读取回值或更高层协议状态确认写入是否产生了预期效果。
2. Fmt 与 Type 要一起看
Non-Flit Mode TLP 的 Byte 0 由Fmt[2:0]和Type[4:0]拼成。Fmt表示 Header 长度及是否带 Payload,Type表示事务类别。
图 2:PCI-SIG《PCI Express Base Specification Revision 7.0》§2.2.1.1、Figure 2-4/2-5、Table 2-2 与 Table 2-3 起始行及脚注 4,PDF p.157~158 原文裁切拼接。完整保留 Fmt/Type 的字段定义、Fmt 取值表及 MRd 两种 Fmt 对应 32/64 位地址格式的说明。
以本章涉及的几类 TLP 为例:
| TLP | Fmt | Type | Byte 0 | 含义 |
|---|---|---|---|---|
| 3-DW MRd | 000 | 00000 | 8'h00 | 32 位地址、无 Payload 的 Memory Read |
| 4-DW MRd | 001 | 00000 | 8'h20 | 64 位地址、无 Payload 的 Memory Read |
| 3-DW MWr | 010 | 00000 | 8'h40 | 32 位地址、带 Payload 的 Memory Write |
| 4-DW MWr | 011 | 00000 | 8'h60 | 64 位地址、带 Payload 的 Memory Write |
| CfgRd0 | 000 | 00100 | 8'h04 | Type 0 Configuration Read |
| CfgWr0 | 010 | 00100 | 8'h44 | Type 0 Configuration Write,带写入数据 |
| Cpl | 000 | 01010 | 8'h0A | 不带数据的 Completion |
| CplD | 010 | 01010 | 8'h4A | 带数据的 Completion |
图 3:PCI-SIG《PCI Express Base Specification Revision 7.0》Table 2-3 续表,PDF p.159。图中完整显示 MWr、CfgRd0/CfgWr0、CfgRd1/CfgWr1、Cpl 与 CplD 的 Fmt、Type 和用途说明。
这里有两个容易混淆的地方。
第一,MRd 和 MWr 的Type都是00000,读写差别由Fmt是否带数据体现。第二,8'h40只是“3-DW、带数据的 Memory Write”,而项目采用 64 位 MMIO 地址时使用 4-DW MWr,对应Fmt=011,Byte 0 是8'h60。不能看到 Memory Write 就把包头第一个字节固定写成40h。
可以在 RTL 中保留字段含义,不必把每个组合写成没有来历的常量:
localparam logic [4:0] TLP_TYPE_MEM = 5'b00000; wire [7:0] mrd64_byte0 = {3'b001, TLP_TYPE_MEM}; // 8'h20 wire [7:0] mwr64_byte0 = {3'b011, TLP_TYPE_MEM}; // 8'h60实际 DW0 还包含 TC、Attr、AT、TD、EP 和 Length 等字段。只检查 Byte 0 能识别大类,不能代替对完整 Header 的生成和校验。
3. Memory Request 为什么有 3-DW 和 4-DW 两种 Header
PCIe Memory Request 可以携带 32 位或 64 位地址。32 位地址只需要一个地址 DWORD,因此整个 Header 是 3 DW;64 位地址需要高、低两个地址 DWORD,因此 Header 是 4 DW。
图 4:PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-36、Figure 2-37,PDF p.209。上图为 64 位地址的 4-DW Memory Request Header,下图为 32 位地址的 3-DW Header。
两个格式都包含 Requester ID、Tag、Last DW BE、First DW BE 和 Length。区别集中在地址部分:
- 4-DW Header 的 DW2 放
Address[63:32],DW3 放Address[31:2]。 - 3-DW Header 的 DW2 放
Address[31:2]。 - Memory Read 没有 Payload;Memory Write 在 Header 后继续携带写入数据。
地址低两位没有作为普通地址位直接放进图中的字段,因为 TLP 数据按 DWORD 对齐,首尾哪些字节真正有效由 Byte Enable 配合描述。构造请求时,不能把一个未经对齐处理的 64 位地址原样塞进 DW2/DW3。
项目使用 64 位地址接口,因此后续 MMIO 请求采用 4-DW Header。即使当前分配的 BAR 地址落在 4 GiB 以下,也应让地址格式的选择与系统地址管理保持一致;若改用 3-DW Header,则必须确认目标地址确实能由 32 位表示。
4. Configuration Header 里的 BDF 到 Memory Header 中不见了
Configuration Request 的 Header 明确带有 Destination ID。对于前面使用的 3-DW、非 ARI Type 0 请求,Destination ID 中就是 Bus Number、Device Number 和 Function Number。
图 5:PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-39,PDF p.211。Configuration Request 使用 Destination ID 和 Register Number 定位目标配置空间。
图 4 的 Memory Request Header 没有 Destination ID。Memory Request 按地址路由,Switch、Root Port 和 Endpoint 根据地址窗口决定请求向哪里转发。对 NVMe 控制器 BAR 的访问,Host 需要先完成下面的关系:
SSD 配置空间中的 BAR0/BAR1 → Host 分配的 Memory 地址窗口 → NVMe 寄存器偏移
随后某个寄存器的实际请求地址才是:
MMIO 请求地址 = 已分配的 BAR 基地址 + NVMe 寄存器 Offset
例如CAP的 Offset 是00h,VS的 Offset 是08h。这里的 Offset 只是相对控制器寄存器窗口的位置,不是完整的 PCIe Memory 地址。若 BAR 尚未配置、Command 寄存器的 Memory Space Enable 尚未满足,直接对00h发 MRd 并不是在读取 NVMe CAP。
5. Requester ID 和 Tag 还在,但用途要分清
从图 4 和图 5 可以看到,Configuration Request 与 Memory Request 都有 Requester ID 和 Tag。
- Requester ID 表明请求由哪个 Function 发出。
- Tag 由请求方分配,用于把返回的 Completion 对应到原请求。
- Configuration Request 还带 Destination ID,用于指出目标 BDF。
- Memory Request 不带目标 BDF,它依靠 Address 路由。
对于 Memory Read,接收侧仍要保存 Requester ID 和 Tag,并核对 CplD 中回填的关联字段。以后允许多笔读取同时在链路上时,Tag 会从单个寄存器变成未完成请求表的索引之一。
Memory Write 虽然 Header 里也有 Requester ID 和 Tag,但普通 Posted Memory Write 不会像 MRd 那样返回一条成功 Completion。因此不能为每笔 MWr 都进入“等待同 Tag CplD”的状态,否则状态机只能等到超时。
6. Length 表示什么,不能只理解成 Payload 长度
PCIe Base 7.0 对 Non-Flit Mode Header 的说明是:Length 表示 Payload 的长度,或者请求所引用的数据长度,单位为 DWORD。
这句话放到三类请求中看就比较清楚:
- 对 MRd,Request 本身没有 Payload,
Length=1表示请求读取 1 DWORD。 - 对 MWr,Request 自身携带 Payload,
Length=1表示写入 1 DWORD。 - 对 CfgRd0/CfgWr0,配置访问按规范限制为 1 DWORD,因此
Length=1。 - 对 CplD,Length 表示当前这条 Completion 实际携带多少 DWORD 数据。
Memory Read 与前面的 1-DWORD Configuration Read 还有一个重要差别:较大的 MRd 可能由多条 CplD 返回。此时接收端不能看到第一条 CplD 就认为整笔请求已经完成,而要结合 Length、Byte Count、Lower Address、Tag 和累计接收量判断。
另外,Memory Request 的地址和长度组合不能跨越 4 KiB 边界。大块数据访问应在请求生成侧提前拆分,不能把跨界包交给对端后再依赖错误返回补救。
7. First DW BE 和 Last DW BE 决定哪些字节有效
First DW Byte Enable 和 Last DW Byte Enable 各有 4 位,对应首、尾 DWORD 的四个字节。对于最常见的“读取或写入一个完整 32 位寄存器”:
| 字段 | 取值 |
|---|---|
| Length | 1 DW |
| First DW BE | 4'b1111 |
| Last DW BE | 4'b0000 |
当 Length 为 1 DW 时,有效字节只由 First DW BE 描述,Last DW BE 必须为0000b。这和直觉上“首尾其实是同一个 DWORD,所以两个 BE 都写 F”不同。
读取 64 位 CAP 时,可以发起一个Length=2的 MRd,也可以按设计能力拆成两个 1-DWORD MRd。若采用 2-DWORD 请求,首尾 Byte Enable、地址对齐以及返回数据拼接都要一起检查;若拆成两笔,则要分别分配和匹配 Tag,并避免把两个返回顺序写死在没有依据的假设上。
8. Completion Header 没有因为读取空间不同而换一套格式
Configuration Read 和 Memory Read 成功后都通过 CplD 返回。Completion Header 中有 Completer ID、Completion Status、Byte Count、Requester ID、Tag 和 Lower Address。
图 6:PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-79,PDF p.243。Configuration Read 和 Memory Read 的返回都使用这一类 Completion Header。
这意味着第 07 章已有的 Completion 解析思路仍然有用:先确认包边界和 Cpl/CplD,再检查 Completion Status、Requester ID 和 Tag,最后交付 Payload。但进入 Memory Read 后,需要补上更多能力:
- 允许一笔 MRd 对应多条 CplD。
- 根据 Byte Count 和 Lower Address 放置每段返回数据。
- 管理多笔未完成请求及其 Tag。
- 处理 Completion 的乱序到达,但保持同一 Read Request 内的数据顺序要求。
- 区分寄存器小读和面向队列、数据缓冲区的较大读取。
所以,Vendor ID 示例中的“两拍 CplD 接收器”可以作为最小起点,但不能直接当作通用 Memory Read 返回引擎。
9. Memory Write 为什么没有成功 Completion
普通 Memory Write 是 Posted Request。发送端将完整 TLP 交给 PCIe IP 后,不会收到一条表示“写入成功”的事务层 Completion。
这种设计减少了每笔写入都等待应答的开销,但也改变了 RTL 的完成定义。MWr 发送状态机中的done最多表示:所有 Header 和 Payload 拍已经通过tvalid && tready交给 PCIe IP。它不能直接表示:
- SSD 已经执行了寄存器写入;
- NVMe 控制器状态已经改变;
- 写入的命令已经执行完成;
- 用户数据已经持久保存到 NAND。
例如后续写CC.EN后,应轮询CSTS.RDY;写 Submission Queue Tail Doorbell 后,应等待对应 Completion Queue Entry;写入用户数据后,应以 NVMe 命令完成状态和数据校验为准。不同层的“完成”不能混成一个信号。
Configuration Write 则不同。它是 Non-Posted Request,需要 Completion。不能因为 CfgWr 和 MWr 都携带写入 Payload,就给它们使用同一套结束条件。
10. 64 位 AXI4-Stream 上,一个最小请求占几拍
PCIe Base 定义 TLP 字段,AMD PG054 规定这些字节怎样放到 PCIe IP 的 AXI4-Stream 接口。项目当前使用 64 位接口,每拍最多传输 8 字节。
图 7:AMD《PG054 v3.3:7 Series FPGAs Integrated Block for PCI Express》Figure 3,PDF p.46。TLP 的第一个字节位于首个 QWORD 的tdata[31:24],Header 和 Payload 必须按该 Byte Order 放置。
对项目采用的 4-DW、64 位地址格式:
- 读取 1 DWORD 的 MRd 只有 16 字节 Header,占两个完整拍。
- 写入 1 DWORD 的 MWr 有 16 字节 Header 和 4 字节 Payload,占三个拍。
- 返回 1 DWORD 的 CplD 有 12 字节 Header 和 4 字节 Payload,共 16 字节,占两个完整拍。
图 8:项目接口布局示意。图中采用 4-DW Memory Request Header;每拍只有在tvalid && tready同时成立时才完成传输。
图 8 中的 MWr 第三拍只有低 32 位有效,所以tkeep=8'h0F,并在该拍给出tlast=1。MRd 的第二拍已经是 Header 末尾,因此tkeep=8'hFF、tlast=1。CplD 的第二拍同时放 DW2 和 Payload,两个 DWORD 都有效。
下面这段只展示 1-DWORD、64 位地址请求的 DW0 核心差异:
// MRd64:4-DW Header,无 Payload,请求读取 1 DW wire [31:0] mrd_dw0 = { 3'b001, 5'b00000, 1'b0, 3'b000, 4'b0000, 1'b0, 1'b0, 2'b00, 2'b00, 10'd1 }; // MWr64:4-DW Header,带 Payload,写入 1 DW wire [31:0] mwr_dw0 = { 3'b011, 5'b00000, 1'b0, 3'b000, 4'b0000, 1'b0, 1'b0, 2'b00, 2'b00, 10'd1 };这不是一个完整的通用 TLP 生成器。Requester ID、Tag、Address、Byte Enable、Payload、握手、流控和错误处理仍要由外围逻辑正确管理;TD、EP、Attr、AT 等字段也不能在所有应用中永远照抄为零。
11. 从 Configuration Read 改成 MRd/MWr,RTL 至少要改哪些地方
如果以第 08 章的最小配置读取引擎为起点,进入 BAR/MMIO 前至少要处理下面这些变化。
| 模块位置 | Configuration Read 阶段 | BAR/MMIO 阶段需要增加的能力 |
|---|---|---|
| 请求参数 | 目标 BDF、Register Number | 32/64 位 Memory Address、读写长度、写入 Payload |
| Header 生成 | 3-DW CfgRd0/CfgWr0 | 3-DW 或 4-DW MRd/MWr |
| 发送状态机 | 两拍配置读 Header | 两拍 MRd Header;MWr 还要连续发送 Payload |
| 返回匹配 | 单笔、单 DWORD CplD | MRd 分段、Byte Count、Lower Address、并发 Tag |
| 完成判定 | CplD 或配置写 Cpl | MRd 等 CplD;MWr 发送结束后由上层状态确认 |
| 地址管理 | 固定 BDF 和配置 Offset | BAR 基地址、寄存器 Offset、地址范围与对齐 |
还有一个常被漏掉的条件:Host 必须正确配置 SSD Function 的 Memory Space Enable,地址窗口也要与 BAR 探测和分配结果一致。否则 TLP 格式即使正确,Endpoint 也未必接受该 Memory Request。
12. 仿真和 ILA 应该怎样验证
这一阶段先验证事务结构,不必一开始就依赖真实 SSD 的 CAP 数值。
仿真建议
- 让 4-DW MRd 在两个 AXI 拍内完成,主动拉低
tready,确认未握手时所有输出保持不变。 - 为 MRd 返回合法的 1-DWORD CplD,检查 Requester ID、Tag、Status 和 Payload 重组。
- 为同一个 Tag 返回 UR 或 CA,确认失败状态不会被当成有效寄存器数据。
- 让 MWr 连续发送两个 Header 拍和一个 Payload 拍,确认末拍
tkeep=0F、tlast=1。 - 确认 MWr 发送结束后不会进入等待 CplD 的状态。
- 增加地址跨 4 KiB 边界的非法用例,要求请求生成侧拒绝或拆分。
上板 ILA 建议
| 检查点 | 需要观察的信号或字段 | 可以证明什么 |
|---|---|---|
| 请求起点 | BAR 基地址、寄存器 Offset、最终 Memory Address | 地址计算没有把 Offset 当完整地址 |
| TLP 首拍 | Fmt、Type、Length、Requester ID、Tag | 请求类别和关联信息正确 |
| 地址拍 | Address[63:32]、Address[31:2]、tlast、tkeep | 4-DW Header 排列正确 |
| MWr Payload | 写入数据、tkeep、tlast | Payload 没有错拍或丢字节 |
| MRd 返回 | CplD、Status、Byte Count、Lower Address、Tag | 返回属于当前读取且数据位置合理 |
| 上层结果 | 超时、成功、错误原因、读取数据 | 事务结束条件没有混淆 |
正式读取 SSD 寄存器时,建议第一笔选择只读的CAP或VS,不要从修改控制器状态的CC、Doorbell 开始。先证明 Memory Read 路径可靠,再加入 Memory Write,可以明显缩小调试范围。
13. 本章完成了什么
到这里,Configuration TLP 与 Memory TLP 的边界已经明确:
- Configuration Request 用 BDF 和寄存器号访问配置空间。
- Memory Request 用系统地址访问 BAR/MMIO 或 Host 内存。
- MRd 不带 Payload,但必须等待 CplD。
- MWr 自带 Payload,是没有成功 Completion 的 Posted Request。
- 64 位地址的 MRd/MWr 使用 4-DW Header。
- Length、Byte Enable、Tag 和 Completion 规则决定请求能否被可靠处理。
后续真正访问 NVMe 控制器时,数据路径为:
PL Memory Request → PCIe Root Port → SSD BAR/MMIO → CplD 或寄存器状态变化 → PL 校验
下一章将进入 BAR0/BAR1:怎样读取 BAR 属性、判断 32 位或 64 位窗口、探测所需空间、恢复原值并建立可用于 NVMe MMIO 的地址映射。
参考资料
- PCI-SIG,PCI Express Base Specification Revision 7.0,Figure 2-5、Table 2-2/2-3(PDF p.158~159):Non-Flit Mode TLP 的 Fmt、Type 和 Length 字段。
- PCI-SIG,PCI Express Base Specification Revision 7.0,§2.2.5~§2.2.7、Figure 2-36/2-37/2-39(PDF p.194~211):Byte Enable、Transaction ID、Memory Request 与 Configuration Request Header 及路由规则。
- PCI-SIG,PCI Express Base Specification Revision 7.0,§2.2.9、Figure 2-79(PDF p.243 起):Completion Header、Status、Byte Count、Requester ID、Tag 和 Lower Address。
- AMD,PG054 v3.3:7 Series FPGAs Integrated Block for PCI Express,Chapter 4 的TLP Format on the AXI4-Stream Interface与Basic TLP Transmit/Receive Operation(PDF p.45~48):64 位接口的字节顺序、tkeep、tlast 和握手规则。
- NVM Express,NVMe over PCIe Transport Specification Revision 1.4,§3.8、Figure 20/21(PDF p.19):NVMe 控制器使用 MLBAR/BAR0 和 MUBAR/BAR1 提供控制器寄存器内存空间基地址。
- NVM Express,NVM Express Base Specification Revision 2.4,§3.1.4、Figure 33/34(PDF p.78~80):CAP、VS、CC、CSTS、Admin Queue 属性和 Doorbell 的寄存器 Offset。