news 2026/9/8 18:37:50

第七章:PCIe Completion with Data 怎么接?FPGA 如何确认 SSD 的返回包

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第七章:PCIe Completion with Data 怎么接?FPGA 如何确认 SSD 的返回包

本篇位置: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'h0A8'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 值。

TLPFmt[2:0]Type[4:0]Byte 0 二进制Byte 0 十六进制含义
CfgRd0000001000000_01008'h043-DW、无 Payload 的 Type 0 Configuration Read
Cpl000010100000_10108'h0A3-DW、无 Payload 的 Completion
CplD010010100100_10108'h4A3-DW、带 Payload 的 Completion

第 06 章发出的 CfgRd0 没有写入 Payload,因此使用Fmt=000。读取成功后的 CplD 保留同一个Type=01010,只把Fmt000改为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'h4A8'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'h4A

3. 再看 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,SCSuccessful Completion可以继续检查 CplD、长度与 Payload
001,URUnsupported Request目标不支持这次访问,不能使用返回数据
010,RRSRequest Retry Status设备在规定场景下暂时尚未就绪;应由上层按策略延后重试,而不是直接把它当作链路断开
100,CACompleter AbortEndpoint 中止了该请求,不能使用返回数据

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;末拍的tlasttkeep共同标记包结束和有效字节。

PG054 对 64 位接口给出了两个实用约束:非末拍的tkeep必须为8'hFF;末拍只能是8'hFF8'h0Ftlasttkeeptdata只有在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'hFFtlast=1

图 5:项目示意。成功的 1 DWORD Configuration Read 返回为 3-DW Header 加 1-DW Payload;图按 TLP 字节到达顺序排列。

与之对照,如果收到的是不带数据的 Cpl,它仍然有 3-DW Header,却只有 12 个字节:首拍为 DW0/DW1,末拍只含 DW2。因此末拍应为tkeep=8'h0Ftlast=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=1success=1时使用read_data;数据寄存器发生更新本身不表示读取成功。

这里还需要区分 Header 中的长度字段与接口末拍检查。对于本章成功读取一个完整 DWORD 的 Configuration Read,规范要求Length=1 DWByte Count=4Lower Address=0。其中 Length 表示本条 Completion 的 Payload 长度;Byte Count 和 Lower Address 的这些取值来自 Configuration Completion 规则,不能直接套用 Memory Read 的分段返回解释。

当前示例检查了末拍的tlasttkeep,但没有显式逐项比较上述三个 Header 字段,也没有检查首拍的tkeeptlast。因此,本文所说的末拍检查并不等于完整的 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=1data[31:24]4A0ARoot Port 是否真的收到 Completion;首拍字节位置是否用错
Completion Statusdata[47:45]取得SC/UR/RRS/CA不要在 Status 非 SC 时解析 Payload
CplD 的末拍tlast=1tkeep=FF,DW2 后紧跟 1 DWORD Payload忘记把 Payload 放在第二拍高 32 位,或把tkeep判为0F
Cpl 的末拍tlast=1tkeep=0F,只有 3-DW Header把错误 Cpl 当成带数据 CplD
关联字段返回的 Requester ID、Tag 与锁存请求相同Requester ID 和目标 BDF 混用;Tag 在等待期间被改写
超时超时后done=1success=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_statusdonesuccess放在同一组探针中。触发条件可先放在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 厂商信息。

参考资料

  1. 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 的处理规则。
  2. 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 的相关约束。
  3. 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_*接口、字节位置、tkeeptlast与接收握手规则。
  4. NVM Express,NVMe over PCIe Transport Specification v1.4,NVMe SSD 作为 PCIe Endpoint 的传输与配置空间相关说明。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 18:36:49

AI生成代码在嵌入式场景中的分层验证实践

前几天我用AI代码助手补了一段UART环形缓冲区的解析代码&#xff0c;编译一次通过&#xff0c;代码看起来也工整&#xff0c;上板跑了不到半小时&#xff0c;缓冲区指针错位&#xff0c;整条串口链路直接卡死。查下来不复杂&#xff1a;AI把两个边界判断简化成了一个&#xff0…

作者头像 李华
网站建设 2026/9/8 18:36:37

把Agent网络延伸到物理世界:WRC 世界机器人大会现场,我们用Agent 调度了一台机器人

一台颁奖机器人,和背后的一个判断 在WRC世界机器人大会最后一天闭幕式上,一台机器人站在了颁奖台边,帮工作人员完成了礼仪环节。它是明略科技和海康机器人联合展台送上舞台的作品,也是当天现场为数不多能同时被观众和媒体镜头都记住的画面。 在2026世界机器人大会主论坛上&am…

作者头像 李华
网站建设 2026/9/8 18:35:42

一文搞懂光纤的结构、原理、分类与选型

文章目录前言1.光纤的基本结构1.1 结构组成1.2 折射率分布2.光纤的工作原理——全反射2.1 全反射原理2.2 光在光纤中的传播过程3.光纤的核心传输特性3.1 光纤损耗3.2 光纤色散4.光纤的分类4.1 按传输模式分类4.2 多模光纤的OM等级4.3 单模光纤的ITU-T标准分类5.光纤连接器与接口…

作者头像 李华
网站建设 2026/9/8 18:35:41

opencode实战:终端AI编码代理从安装到项目接手上手全流程

最近把自己日常开发里的AI辅助工具换成了opencode&#xff0c;一个月下来最直接的体感是&#xff1a;以前AI是给我出代码片段的助手&#xff0c;现在AI是能自己接需求、改代码、跑测试、看报错的同事。opencode不是又一个聊天插件&#xff0c;它是一款在终端里运行的AI编码代理…

作者头像 李华
网站建设 2026/9/8 18:33:53

opencode实战:从安装配置到LSP与Playwright的AI编程Agent调教

最近被问得最多的一个 AI 编程工具&#xff0c;不是 Claude Code&#xff0c;也不是 Codex&#xff0c;而是 opencode。一开始我以为又是个套壳的终端助手&#xff0c;直到自己把它装进一个多模块的 Go 项目里实际干了两个星期&#xff0c;才理解为什么越来越多人把它写进自己的…

作者头像 李华
网站建设 2026/9/8 18:33:42

LVDS 7:1 SerDes源同步接口设计:从原理到实战排错

XAPP585 这份文档我翻来覆去看了不下五遍&#xff0c;每次在项目里被 LVDS 源同步接口折磨到怀疑人生的时候&#xff0c;回头重新读一遍&#xff0c;总能有新的收获。如果你最近正在做 FPGA 之间的高速互联、接高速 ADC/DAC、或者调试 Camera Link 这类视频接口&#xff0c;大概…

作者头像 李华