本篇位置:FPGA NVMe Host 实战连载 · 🟩 第一幕|共同底座 · 第 07 篇
第 06 章已经把一条 Configuration Read 送进了 PCIe Root Port IP。发送接口完成两拍握手,只能说明 Root Port IP 接收了这个请求;它还不能证明 SSD 已经返回,也不能证明返回的数据属于当前这一次读取。
本章接着处理返回方向。对于读取直连 NVMe SSD 配置空间 Offset00h的请求,控制路径是:
NVMe SSD Endpoint → Completion with Data → PCIe Root Port IP 接收接口 → PL 接收状态机 → 状态、Requester ID、Tag 校验 → 32 位配置读取结果
本章说明如何取得有效的 32 位配置读取结果。下一章会把链路训练、请求发送和返回解析连成一次完整的板上验证。
1. Configuration Read 为什么必须等 Completion
PCIe 的 Configuration Read 是 Non-Posted Request。请求从 FPGA 发出后,不能像 Memory Write 那样只看发送端握手就结束;目标 Endpoint 需要给出一条 Completion。读取成功时,这条 Completion 还会携带读取到的数据,因此通常称为 Completion with Data,缩写为 CplD。
这条规则直接决定了接收侧的职责。它至少要回答四个问题:收到的是不是 Completion;它是否属于当前请求;Endpoint 是否报告成功;数据拍是否完整。只要其中一项不成立,就不能把 Payload 当作配置空间的有效字段。
对于本章的 1 DWORD Configuration Read,结果不会被拆成多条 Completion。PCIe Base Specification 对 I/O 和 Configuration Read 明确要求以恰好一条 Completion 完成。这个条件让第一条配置读取的接收逻辑比较紧凑;后续进入较大的 Memory Read 时,接收端还需要处理分段 Completion 与剩余 Byte Count。
2. Fmt 与 Type:先把 8’h0A、8’h4A 拆开
TLP 的 DW0 从 Byte 0 开始。对于本章使用的 Non-Flit Mode 格式,Byte 0 的高 3 位是Fmt[2:0],低 5 位是Type[4:0]。Fmt说明 Header 长度和是否带 Payload;Type说明事务类别。项目代码中的8'h0A、8'h4A正是这两个字段的拼接结果,不是需要记住的两条特殊常量。
图 1:PCI-SIG《PCI Express Base Specification Revision 7.0》Table 2-3,PDF p.158~159。表中列出 Non-Flit Mode 的 Fmt[2:0] 与 Type[4:0] 合法组合;本章用到 CfgRd0、Cpl 与 CplD 三行。
把相关的比特直接连起来,就得到下面三个 Byte 0 值。
| TLP | Fmt[2:0] | Type[4:0] | Byte 0 二进制 | Byte 0 十六进制 | 含义 |
|---|---|---|---|---|---|
| CfgRd0 | 000 | 00100 | 0000_0100 | 8'h04 | 3-DW、无 Payload 的 Type 0 Configuration Read |
| Cpl | 000 | 01010 | 0000_1010 | 8'h0A | 3-DW、无 Payload 的 Completion |
| CplD | 010 | 01010 | 0100_1010 | 8'h4A | 3-DW、带 Payload 的 Completion |
第 06 章发出的 CfgRd0 没有写入 Payload,因此使用Fmt=000。读取成功后的 CplD 保留同一个Type=01010,只把Fmt从000改为010,表示它在 3-DW Header 后还带有数据。无数据的 Cpl 则仍为Fmt=000。
项目使用 64 位 AXI4-Stream,PG054 规定首个 TLP 字节位于m_axis_rx_tdata[31:24]。因此接收首拍时,m_axis_rx_tdata[31:24]为8'h4A就是{Fmt=010, Type=01010},即 CplD;为8'h0A就是{Fmt=000, Type=01010},即 Cpl。它们既不是 SSD 的厂商/型号信息,也不该脱离 Fmt 与 Type 单独理解。
把判断条件写成拼接形式,和工程中比较8'h4A、8'h0A完全等价,但含义更直观:
localparam logic [7:0] TLP_BYTE0_CPL = {3'b000, 5'b01010}; // 8'h0A localparam logic [7:0] TLP_BYTE0_CPLD = {3'b010, 5'b01010}; // 8'h4A3. 再看 CplD 与 Cpl:有没有数据是两回事
Completion 的基础头始终是 3 DWORD。它的第一个 DWORD 中包含 Fmt 与 Type,第二个 DWORD 中有 Completer ID、Completion Status 和 Byte Count,第三个 DWORD 回填原请求的 Requester ID 与 Tag。CplD 在这 3 DWORD 之后继续带 Payload;没有数据的 Cpl 则到 Header 结束。
图 2:PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-79,PDF p.243。Non-Flit Mode Completion 使用 3-DW Header,包含 Completer ID、Completion Status、Byte Count、Requester ID、Tag 与 Lower Address。
对“读取 1 个完整 DWORD”的第一笔配置读取,可以先用下面的判断建立概念。
| 返回类型 | 典型含义 | 本章接收侧的处理 |
|---|---|---|
| CplD | 读取成功并返回数据 | 校验头字段与末拍有效字节后,解析 1 DWORD Payload |
| Cpl | 不带数据的 Completion | 读取请求出现非成功状态时,不能从中取得配置字段 |
不能只看“CplD”三个字母。一个设计若把任何接收包后面的四个字节都直接当成读取结果,遇到 Cpl、错误状态、残留数据或包边界不对时,都会得到看似合理但实际错误的值。
4. Completion Status 先决定结果能不能使用
Completion Header 的Completion Status[2:0]给出 Endpoint 对请求的处理结果。对于本章的读取,只有 Successful Completion(SC)才可以继续使用 Payload。
图 3:PCI-SIG《PCI Express Base Specification Revision 7.0》Table 2-37,PDF p.244。Completion Status 的 SC、UR、RRS、CA 编码。
| 状态 | 含义 | 对配置读取的判断 |
|---|---|---|
000,SC | Successful Completion | 可以继续检查 CplD、长度与 Payload |
001,UR | Unsupported Request | 目标不支持这次访问,不能使用返回数据 |
010,RRS | Request Retry Status | 设备在规定场景下暂时尚未就绪;应由上层按策略延后重试,而不是直接把它当作链路断开 |
100,CA | Completer Abort | Endpoint 中止了该请求,不能使用返回数据 |
RRS 很容易被误判。它不表示“已经读到了全零”,也不等于“PCIe 链路掉了”。PCIe 规范允许设备在某些复位后的就绪阶段,以 RRS 结束 Configuration Request;Host 需要保留这一状态,并在合适的等待后重新发起访问。当前最小读取引擎会把非 SC 结果明确输出给上层,是否重试由更高一层的控制逻辑决定。
本项目还使用一个本地超时标记:当等待返回超过设定周期,输出的诊断值为3'b111。它只是 RTL 为“没有等到匹配返回”定义的失败码,并不是图 3 中 PCIe Completion Status 的有效含义,不能把它解读为 Endpoint 实际返回的状态。
5. Requester ID 与 Tag:怎样确认返回包就是这一次请求的
第 06 章发送请求时,在 DW1 写入了 Requester ID 和 Tag。Completion 返回时,DW2 会带回同一个 Requester ID 与 Tag。PCIe 将这组关联信息用于把 Completion 路由回正确的请求方,并区分同一请求方发出的不同未完成请求。
这里要分清三个 ID 的方向:Completer ID 标识产生 Completion 的 Endpoint;Requester ID 是原请求的发起方,在 Completion 中作为返回路由目标;Tag 则是请求方分配的关联编号。对于本章的单笔读取,接收逻辑保存本次 Requester ID 和 Tag,再与返回头中的对应字段逐项比较。
即使现在一次只允许一个 Configuration Read 在路上,也应保留这两个比较。不过,它们只能排除关联编号不同的返回;如果超时后立即复用同一个 Tag,迟到的旧 Completion 仍可能与新请求匹配,因此重试还需要考虑 Tag 的复用时机。以后扩展为并发读取时,接收端应以Requester ID + Tag建立未完成请求表。
图 2 来自 PCIe 7.0,Header 里还有 T8、T9 两个 Tag 扩展位。本项目的接口只使用 8 位 Tag,不启用这些扩展,因此代码比较的是 DW2 中的Tag[7:0]。
首次访问还要注意 Completer ID。PCIe Base 7.0 §2.2.9.1(PDF p.244~245)规定,设备通过 Type 0 Configuration Write 捕获自身的 Bus Number 和 Device Number;在首次配置写入之前返回 Completion 时,这两个字段应填 0。规范进一步要求,在设备完成这类初始化之前,请求方忽略返回的 Completer ID 值。因此,第一次读配置空间时,不能要求返回的 Completer ID 必须等于目标 BDF,否则可能拒绝一条正常返回。
6. 从 PCIe IP 的接收口看到的是什么
AMD 7 系列 PCIe IP 通过m_axis_rx_*将接收 TLP 交给用户逻辑。项目使用 64 位接口,m_axis_rx_tvalid表示当前 QWORD 有效,m_axis_rx_tready表示用户逻辑能够接收;两者同时为 1 时,这一拍才完成传输。m_axis_rx_tlast标记一个 TLP 的最后一拍,m_axis_rx_tkeep标记这个 QWORD 中哪些字节有效。
图 4:AMD《PG054 v3.3:7 Series FPGAs Integrated Block for PCI Express》Basic TLP Receive Operation,PDF p.58。64 位接收接口中,用户逻辑先给出m_axis_rx_tready,IP 再用m_axis_rx_tvalid交付 QWORD;末拍的tlast与tkeep共同标记包结束和有效字节。
PG054 对 64 位接口给出了两个实用约束:非末拍的tkeep必须为8'hFF;末拍只能是8'hFF或8'h0F。tlast、tkeep与tdata只有在tvalid=1时才有意义。
当前读取引擎固定将m_axis_rx_tready置为 1,因此它没有引入接收反压。在这个前提下,RTL 中判断m_axis_rx_tvalid就等价于本拍已经完成接收。若后续改为可拉低tready的缓存式接收逻辑,所有状态推进条件都应写成tvalid && tready,并在未握手时保持当前接收上下文不变。
7. 一个 CplD 为什么正好占两个接收拍
读取一个 DWORD 的成功返回由 3-DW Completion Header 加 1-DW Payload 组成,共 16 个字节。64 位接口每拍承载 8 个字节,因此它正好占两个完整 QWORD:首拍为 DW0/DW1,末拍为 DW2/Payload,末拍的tkeep=8'hFF、tlast=1。
图 5:项目示意。成功的 1 DWORD Configuration Read 返回为 3-DW Header 加 1-DW Payload;图按 TLP 字节到达顺序排列。
与之对照,如果收到的是不带数据的 Cpl,它仍然有 3-DW Header,却只有 12 个字节:首拍为 DW0/DW1,末拍只含 DW2。因此末拍应为tkeep=8'h0F且tlast=1。接收逻辑正是利用“是否 CplD”和末拍tkeep的组合,区分这两种包长。
图 5 还有一个字节顺序细节。PG054 规定,每个 QWORD 的第一个 TLP 字节位于m_axis_rx_tdata[31:24]。Payload 位于第二拍的高 32 位,但它按 TLP 字节到达顺序出现;项目在交付read_data前将四个字节重组为常规 32 位字段顺序。
// CplD Payload 位于第二拍高 32 位,按 TLP 字节顺序重新组合。 read_data <= { m_axis_rx_tdata[39:32], m_axis_rx_tdata[47:40], m_axis_rx_tdata[55:48], m_axis_rx_tdata[63:56] };这段重组只解决“字节放在哪”的问题,不代替 Completion 校验。上层应在done=1且success=1时使用read_data;数据寄存器发生更新本身不表示读取成功。
这里还需要区分 Header 中的长度字段与接口末拍检查。对于本章成功读取一个完整 DWORD 的 Configuration Read,规范要求Length=1 DW、Byte Count=4、Lower Address=0。其中 Length 表示本条 Completion 的 Payload 长度;Byte Count 和 Lower Address 的这些取值来自 Configuration Completion 规则,不能直接套用 Memory Read 的分段返回解释。
当前示例检查了末拍的tlast和tkeep,但没有显式逐项比较上述三个 Header 字段,也没有检查首拍的tkeep和tlast。因此,本文所说的末拍检查并不等于完整的 TLP 格式校验。仿真时应分别观察这些字段与实际包长;完善接收器时,还需要明确哪些检查由 PCIe IP 完成,哪些由用户逻辑补充。
8. 接收状态机怎样逐拍判断
项目的最小读取引擎将发送后等待返回的过程拆为两个状态:先等 Completion 的首拍,确认类型并锁存是否带数据及 Completion Status;再等第二拍,确认它是末拍且 Requester ID、Tag 与本次请求一致。这样 Header 判断和 Payload 解析不会混在同一个时钟周期里。
// 首拍:只接受 CplD 或 Cpl,并锁存是否带数据和状态码。 if (m_axis_rx_tvalid && ((m_axis_rx_tdata[31:24] == 8'h4A) || (m_axis_rx_tdata[31:24] == 8'h0A))) begin received_completion_has_data <= (m_axis_rx_tdata[31:24] == 8'h4A); received_completion_status <= m_axis_rx_tdata[47:45]; state <= ST_WAIT_CPL_DW2_DATA; end第二拍的关键不是“第二拍来了”,而是它必须是当前 TLP 的末拍,并且回填的关联字段要和发送时锁存的值一致。对于这条 1 DWORD 读取,成功条件还要求 SC、CplD 与tkeep=8'hFF同时成立。
if (m_axis_rx_tvalid && m_axis_rx_tlast && (m_axis_rx_tdata[31:16] == request_requester_id) && (m_axis_rx_tdata[15:8] == request_tag)) begin success <= (received_completion_status == 3'b000) && received_completion_has_data && (m_axis_rx_tkeep == 8'hFF); end图 6:当前读取引擎的处理示意。匹配返回到达后结束请求,并由 Status、是否带数据及末拍 tkeep 决定 success;关联字段不匹配时仍停留在等待状态,若随后没有满足条件的返回,则以本地超时码结束。
当末拍到达且 Requester ID、Tag 匹配时,当前引擎立即结束请求并输出收到的 Completion Status。非 SC、无数据或末拍tkeep不符合读取要求,都会使success=0。其中tkeep不符并没有独立的诊断码,不能声称代码保存了每一种失败的具体原因。
如果关联字段不匹配,状态机继续停留在第二个等待状态;如果之后一直没有满足条件的接收拍,才通过超时路径输出本地失败码3'b111。它没有在看到不匹配的末拍后重新回到等待首拍状态,因此不能把这段代码理解为通用的 Completion 筛选器。
另一个限制是包边界:在等待首拍时,当前代码会逐个有效拍比较4A/0A;遇到其他 TLP 后,没有先将其接收到tlast再检查下一包。这样,其他包的数据拍若恰好在相同位置出现4A/0A,也可能被误认为 Completion 首拍。即使同时只有一笔配置读取,接收口仍可能出现其他事务,因此“一次一笔请求”本身不足以排除这个问题。
这段示例用于接收流量受控的配置读取验证。通用接收器应跟踪每个包的起止边界,跳过无关 TLP 的剩余拍,再从下一包首拍重新判断;需要并发读取时,再增加未完成请求表和接收缓冲。
9. 仿真与 ILA 应该看哪些信号
先用仿真确认字节顺序和包边界,再到板上确认真实 Endpoint 的返回。下面这些观察点足够覆盖第一条配置读取的接收侧。
| 检查点 | 应看到的现象 | 出错时优先排查 |
|---|---|---|
| 首拍类型 | m_axis_rx_tvalid=1,data[31:24]为4A或0A | Root Port 是否真的收到 Completion;首拍字节位置是否用错 |
| Completion Status | 从data[47:45]取得SC/UR/RRS/CA | 不要在 Status 非 SC 时解析 Payload |
| CplD 的末拍 | tlast=1、tkeep=FF,DW2 后紧跟 1 DWORD Payload | 忘记把 Payload 放在第二拍高 32 位,或把tkeep判为0F |
| Cpl 的末拍 | tlast=1、tkeep=0F,只有 3-DW Header | 把错误 Cpl 当成带数据 CplD |
| 关联字段 | 返回的 Requester ID、Tag 与锁存请求相同 | Requester ID 和目标 BDF 混用;Tag 在等待期间被改写 |
| 超时 | 超时后done=1、success=0,并记录本地超时原因 | 请求没有真正送出、目标 BDF 不对、接收状态机没有等到包结束 |
用于字节顺序的仿真不需要依赖任何 SSD 实测值。可以让测试平台构造一个合法 CplD,并在 Payload 放入例如32'h11223344的测试模式;接收端的read_data应恢复为同一个 32 位数。这个用例只验证接口字节摆放与重组,不代表板上 SSD 的返回内容。
上板时,ILA 建议把user_lnk_up、发送端s_axis_tx_*、接收端m_axis_rx_*、当前状态、锁存的 Requester ID/Tag、completion_status、done和success放在同一组探针中。触发条件可先放在m_axis_rx_tvalid=1;如果一直看不到接收拍,应先回到第 02、03、06 章检查链路、Root Port 角色和发送请求,而不要急着改 Payload 解析。
10. 这一章完成了什么
到这里,在本章限定的受控接收场景下,PL 接收侧能够识别 CplD 与 Cpl,读取 Completion Status,用 Requester ID 和 Tag 关联本次请求,并通过末拍检查交付 32 位配置读取结果。Header 字段的完整校验、无关包的跳过和超时后的 Tag 复用,还需要在完善接收器时继续处理。
完整控制路径现在是:
PL 请求状态机 → Configuration Read Type 0 → PCIe Root Port IP → NVMe SSD Endpoint → CplD → PCIe IP RX → PL 状态、ID、Tag 与末拍检查 → 32 位配置读取结果
下一章会把前 7 章的条件放到同一块板上:在链路进入 L0 后发出这条读取,捕获返回的 CplD,并从配置空间的结果中识别 SSD 厂商信息。
参考资料
- PCI-SIG,PCI Express Base Specification Revision 7.0,Figure 2-5、Table 2-2/2-3(PDF p.157~159)与 §2.2.9.1、Figure 2-79、Table 2-37(PDF p.243~245):Fmt/Type 编码、Completion Header、Completion Status、Requester ID、Tag,以及首次配置写入前 Completer ID 的处理规则。
- PCI-SIG,PCI Express Base Specification Revision 7.0,§2.3.1.1(PDF p.263):I/O 与 Configuration Read 使用一条 Completion 完成的规则;§6.6(PDF p.826 起):PCIe Reset 规则,以及 Configuration Request 在复位后可能使用 RRS 的相关约束。
- AMD,PG054 v3.3:7 Series FPGAs Integrated Block for PCI Express,Table 10(PDF p.25~27)、TLP Format on the AXI4-Stream Interface(PDF p.45~46)与Basic TLP Receive Operation(PDF p.58):64 位
m_axis_rx_*接口、字节位置、tkeep、tlast与接收握手规则。 - NVM Express,NVMe over PCIe Transport Specification v1.4,NVMe SSD 作为 PCIe Endpoint 的传输与配置空间相关说明。