ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第六章:FPGA 如何发出 PCIe Configuration Read?从 TLP 到 AXI4-Stream 发送接口

第六章:FPGA 如何发出 PCIe Configuration Read?从 TLP 到 AXI4-Stream 发送接口 本篇位置FPGA NVMe Host 实战连载 · 第一幕共同底座 · 第 06 篇第 05 篇已经确定了第一条读取的目标直连 SSD 的 PCIe 配置空间 Offset00h。这个位置的 32 位字段中包含 Vendor ID 和 Device ID。现在还差最关键的一步FPGA 怎样把“请读取这个位置”组织成一个符合 PCIe 要求的请求并真正交给 Root Port IP 发送本篇只讨论请求侧而且整个过程可以在 PL 内完成不需要 PS 或 Vitis 参与。控制路径如下PL 请求状态机 → Configuration Read Type 0 TLP → AXI4-Stream 发送接口 → PCIe Root Port IP → NVMe SSD EndpointSSD 收到请求后如何返回 Completion with Data、FPGA 又怎样检查和解析返回将在下一篇单独展开。把两个方向分开后上板时更容易判断问题出在“请求没有发出”还是“返回没有处理好”。先分清不是读 Root Port 自己的配置空间使用 AMD 7 系列 PCIe IP 时容易看到一组cfg_mgmt_*信号。它们用于访问 PCIe IP 所代表的 Root Port 自身配置空间不能拿来读取下游 M.2 SSD 的配置空间。SSD 是 Root Port 下游的 Endpoint。要读取它的 Vendor ID用户逻辑必须通过 PCIe IP 的事务发送接口主动发出一个 Configuration Read TLP。IP 负责将合规的 TLP 发到 PCIe 链路上TLP 的类型、目标 BDF、读取位置和发送时序则由 FPGA 逻辑负责组织。这也是为什么“链路已经进入 L0”仍然不等于“SSD 已经被识别”。L0 只说明现在具备传输 TLP 的条件识别要从一次实际的 Configuration Read 开始。第一条请求读哪里本项目的第一条 Configuration Read 将 TLP 目标填写为01:00.0即 Bus01、Device00、Function0。它读取配置空间的 DWORD 0也就是 Offset00h。这是一项适合起步的最小读取长度只有一个 DWORD不带写入数据返回时却能得到用于确认 Endpoint 存在的基础 ID 字段。01:00.0是当前直连拓扑中的目标 BDF不是 SSD 出厂时写死的“型号编号”也不是所有 FPGA 板卡都会使用的固定数值。工程里应将 Bus、Device、Function 作为明确的请求参数如果中间增加桥、Switch 或改变总线分配目标 BDF 也需要随之检查。为什么这里是 01:00.0PCIe 用 BDF 表示一个 Function 在当前 PCIe 树中的位置写法为Bus:Device.Function。因此01:00.0可以逐段读成在 Bus01上选择 Device00的 Function0。它描述的是访问路径不包含 SSD 的厂商、容量、固件或零售型号信息。图 1PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 1-2PDF p.142。示例 PCIe 拓扑展示 Root Complex、Root Port、Switch、Endpoint 与层级边界之间的关系。图 1 不给某个 Endpoint 指定具体 BDF它说明的是编号发生在哪种树形关系中。项目的直连 SSD 对应其中一条最简单的分支Root Port 通过一条 PCIe Link 连接一个 Endpoint插入 Switch 后Endpoint 就会落在更深的分支中。对于本章使用的 3-DW、非 ARI Configuration Request目标 BDF 放在请求的 Byte 8/9Bus Number 是 Byte 8 的 8 位Device Number 是 Byte 9 的 bit[7:3]Function Number 是 Byte 9 的 bit[2:0]。这三个字段组合后才得到Bus:Device.Function这样的写法。在这条 Root Port 直连单块 SSD 的拓扑中项目将唯一直接面对的 SSD Function 作为01:00.0访问。若中间接入 PCIe Switch下游 Endpoint 可能位于另一个 Bus若设备公开多个 Function最后一位也可能不是0。所以移植工程时不能只复制01:00.0而要先按自己的 PCIe 树确认目标 BDF。图 2PCI-SIG《PCI Express Base Specification Revision 7.0》§7.5.1.3.2§7.5.1.3.4PDF p.1087。该页说明 Type 1 Header 中 Primary、Secondary、Subordinate Bus Number 寄存器的含义以及 Secondary / Subordinate Bus Number 由配置软件写入并参与请求路由。图 2 说的是桥或 Root Port 一类 Type 1 Header 的总线范围管理它解释了 Bus Number 为什么会随 PCIe 树变化。当前工程没有把01当作通用常量而是在本板直连拓扑下将它作为目标请求参数写入 TLP。增加 Switch、桥或更换拓扑后应该先确认新的 Bus 分配再生成请求。在项目 RTL 中目标字段由发送逻辑的target_bus、target_device、target_function输入提供并在请求 DW2 中拼接。当前顶层将它们设置为8h01、5h00、3h0。这就是“为什么本项目的 TLP 写成01:00.0”的直接代码依据。.target_bus (8h01), .target_device (5h00), .target_function (3h0), .register_dw (10h000) // 配置空间 Offset 00h还要区分Requester ID和目标 BDF。Requester ID 填在 DW1表示请求由哪个 Root Port Function 发出项目从 PCIe IP 的cfg_bus_number、cfg_device_number、cfg_function_number输出取得该值。目标 BDF 填在 DW2表示请求要送往哪个下游 Function。两者方向相反不能用一个替代另一个。AMD PG054 的 Table 16 也给出一个容易混淆的边界Root Port 的cfg_ds_bus_number、cfg_ds_device_number、cfg_ds_function_number仅用于IP 内部生成的 TLP不会改变用户经 AXI4-Stream 提交的 TLP。本文的 Configuration Read 属于后者因此01:00.0必须由 DW2 的目标字段明确写入而不是依赖cfg_ds_*信号。AMD PG054Configuration Interface这次访问选择Configuration Read Type 0。它是一个 Non-Posted Request请求发出后发起方需要等待对端的 Completion读取成功时Completion 还会带回一个 DWORD 数据。请求本身不带 Payload只有 3 个 DWORD 的 TLP Header。Fmt 与 TypeByte 0 的两个字段怎么决定这是一条什么包每个 Non-Flit Mode TLP 的 DW0 都从 Byte 0 开始。这个字节的高 3 位是Fmt[2:0]低 5 位是Type[4:0]。Fmt决定剩余 Header 是 3 DW 还是 4 DW、包后是否带 PayloadType决定这是哪类事务例如 Configuration Read、Memory Write 或 Completion。两者必须连在一起看不能把某个十六进制数当作需要死记的“包编号”。图 3PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-5 与 Table 2-2PDF p.158。Byte 0 的高 3 位为 Fmt低 5 位为 TypeTable 2-2 给出 3-DW/4-DW Header 是否带数据的 Fmt 取值。图 3 只说明 Fmt 的格式含义不能用它推断某个Type具体代表什么事务。Type 的完整合法组合由 Table 2-3 给出本章要用到的 CfgRd0以及下一章会接收到的 Cpl/CplD都在该表中。图 4PCI-SIG《PCI Express Base Specification Revision 7.0》Table 2-3PDF p.158159。Non-Flit Mode 的 Fmt[2:0] 与 Type[4:0] 编码表图中完整保留 CfgRd0、Cpl 与 CplD 的 Type 值及规范描述。本章涉及的三个 Byte 0 编码可以从规范的 Table 2-2、Table 2-3 直接推导出来。TLPFmt[2:0]Type[4:0]Byte 0 二进制Byte 0 十六进制含义CfgRd0000001000000_01008h043-DW、无 Payload 的 Type 0 Configuration ReadCpl000010100000_10108h0A3-DW、无 Payload 的 CompletionCplD010010100100_10108h4A3-DW、带 Payload 的 Completion第一条请求使用 CfgRd0因此Fmt000它本身不带写入数据只有 3-DW Header。这里的Length1表示“请求读取 1 个 DWORD”不是说请求 TLP 自己有 1 个 DWORD 的 Payload。读取成功后返回的 CplD 才会因Fmt010而携带 1 个 DWORD 数据。在 RTL 中这个 Byte 0 的组合就是下面的写法拼接结果自然得到8h04不需要另外记忆一个数值。wire [7:0] cfg_read_byte0 {3b000, 5b00100}; // 8h04CfgRd0图 5PCI-SIG《PCI Express Base Specification Revision 7.0》Figure 2-39PDF p.211。Configuration Request 的 3-DW、Non-Flit Mode 请求头Byte 8/9 的 Destination ID 即为前文说明的目标 B/D/F 字段。三个 DWORD 各自负责什么第一次看 TLP Header 不必背下全部位号先把每个 DWORD 的职责分清。下面的配置适用于“读取一个完整 DWORD”的最小请求。DWORD关键字段本次读取的设置作用DW0Fmt、Type、LengthFmt000、Type00100、Length1表明这是 3-DW、无数据载荷的 Configuration Read Type 0并读取 1 个 DWORDDW1Requester ID、Tag、Byte EnableRoot Port 请求者编号、每次请求分配的 Tag、First DW BEF标明请求来源并为后续匹配返回包保留编号F表示 4 个字节都需要读取DW2Bus、Device、Function、Register Number01:00.0、Register Number000指向下游 SSD 的目标 Function 和配置空间 DWORD 位置Length1和First DW BEF经常被忽略。前者说明本次只需要一个 32 位 DWORD后者说明这个 DWORD 的四个字节都有效。对于 Offset00h的 ID 字段不能只读取其中一两个字节否则收到的数据不具备完整的字段含义。DW2 中的 Register Number 按 DWORD 编号表示而不是直接填写字节地址。读取 Offset00h时它为000如果之后读取 Offset04h的 Command/Status DWORD则应改为001。低两位固定为00保证配置访问按 DWORD 对齐。Requester ID 与目标 BDF 的作用不同。Requester ID 表示“谁发出的请求”目标 BDF 表示“请求交给谁”。Tag 则是请求侧生成的关联编号。下一篇接收 Completion 时需要用 Requester ID 和 Tag 判断返回包是否属于当前这一次读取因此它们不能只当作包头里的固定填充位。DW0、DW1、DW2 怎么送到 64 位接口上TLP 是按字节顺序定义的而 PCIe IP 给用户逻辑的是 64 位 AXI4-Stream 接口。两者之间最容易出现的错误不是字段含义而是字节位置。AMD PG054 在TLP Format on the AXI4-Stream Interface一节明确说明第一个 PCIe 字节出现在首个 QWORD 的s_axis_tx_tdata[31:24]而不是直觉上最左侧的tdata[63:56]。后续第 8 个 PCIe 字节再从第二个 QWORD 的tdata[31:24]开始。因此逻辑中组合 64 位发送数据时必须同时按 TLP 字节顺序和 AXI4-Stream 位位置检查。图 6AMD《PG054 v3.37 Series FPGAs Integrated Block for PCI Express》Figure 3PDF p.46。图说明 PCIe TLP 字节在 64 位 AXI4-Stream 接口中的排列本章使用的是发送侧s_axis_tx_tdata。对于 3-DW、无 Payload 的 Configuration Read64 位接口需要两个有效拍第一拍容纳 DW0 与 DW1第二拍只需要 DW2。第二拍剩余的 4 个字节不用发送由tkeep标明有效范围。图 7一个 3-DW Configuration Read 在 64 位 AXI4-Stream 上的最小发送形式。第 2 拍只有低 32 位有效因此tkeep8h0Ftlast1表示本 TLP 在这一拍结束。发送侧可以按下面的方式组织两个拍。这里的dw0、dw1、dw2已经分别根据图 5 的字段组合完成代码片段只展示它们如何放进接口不包含任何 SSD 的实际返回数据。case (state) ST_TX_DW01: begin s_axis_tx_tdata {dw1, dw0}; s_axis_tx_tkeep 8hFF; s_axis_tx_tlast 1b0; s_axis_tx_tvalid 1b1; end ST_TX_DW2: begin s_axis_tx_tdata {32b0, dw2}; s_axis_tx_tkeep 8h0F; s_axis_tx_tlast 1b1; s_axis_tx_tvalid 1b1; end endcase这里{dw1, dw0}不是随意调整高低位它使 DW0 处于首个 QWORD 的低 32 位从而让 TLP 的 Byte 0 落在 PG054 规定的s_axis_tx_tdata[31:24]。第二拍的 DW2 同样位于低 32 位高 32 位即使赋为 0也不会被当作包内容因为tkeep8h0F已经标明只有低四个字节有效。tvalid 不等于已经发出必须等 treadytvalid1的含义是“当前拍的数据有效发送端准备交给 PCIe IP”。只有tvalid和tready在同一个时钟沿同时为 1这一拍才真正被接收。若 IP 暂时拉低tready发送端必须保持当前拍的tdata、tkeep和tlast不能自行跳到下一拍。这条规则对两拍小包同样重要。若第一拍还没有被接收状态机却提前切到 DW2IP 看到的就是字段错位的 TLP若第二拍的tlast与有效字节没有一起保持包边界也会被破坏。这类问题在 ILA 中往往表现为“链路仍在 L0但 SSD 没有给出预期 Completion”。发送引擎应在start到来时先锁存本次所有参数包括 Requester ID、Tag、目标 BDF、Register Number 和 Byte Enable。这样即使外部状态机在等待期间准备了下一条任务当前包头也不会在tready0时被修改。图 8发送侧状态机的最小推进关系。两个发送状态都只在tvalid tready成立后前进第二个握手完成后才进入等待 Completion 的阶段。项目中的发送逻辑按这个顺序实现IDLE接收启动请求并锁存字段先输出 DW0/DW1再输出 DW2 和tlast随后转入接收等待。即使配置读取只有两个拍也建议将“参数锁存”“首拍发送”“末拍发送”“等待返回”明确拆开后面加入 Configuration Write、Memory Read 或连续请求时这个边界仍然可以复用。不要把发送成功和读取成功混为一件事发送侧完成时只能说明请求 TLP 已经被 PCIe IP 接收。它还不能证明 SSD 已经返回数据更不能证明 Offset00h的字段已经解析正确。观察到的现象当前能得出的结论还不能得出的结论LTSSM 已进入 L0物理链路具备传输条件Configuration Read 已经发出两个发送拍都完成valid ready握手PCIe IP 已接收该请求 TLPSSD 已经收到请求或给出返回接收侧看到一个 Completion链路上出现了返回事务该包一定属于当前请求且数据一定有效Completion 与 Tag、Requester ID、状态和长度都匹配一次配置读取往返可以继续解析NVMe 控制器已经完成初始化或能够读写用户数据表中的第三、第四项属于下一篇的范围。这样拆分不是为了增加步骤而是因为 PCIe 读取天然就是一次跨链路的请求—返回事务发送接口只负责把请求交出去结果是否可靠必须由接收侧再确认。仿真和上板先看这几个点请求侧先不需要观察 SSD 的具体 ID 数值也能完成一轮有效验证。仿真中可以用模型在发送后返回一个合法的 Completion with Data重点先检查请求本身是否按预期出现。上板时ILA 也应先把发送侧和链路状态放在同一组探针里。检查点仿真或 ILA 中应看到什么常见问题启动条件只有链路稳定进入 L0 后才拉起start链路尚未稳定就开始发包字段锁存从首拍到末拍BDF、Tag、Register Number 保持同一笔请求外部参数直接穿透到发送数据等待时被改写第 1 拍tvalid1、tkeepFF、tlast0内容是 DW0/DW1把 DW0 放到错误的 32 位半区第 2 拍tvalid1、tkeep0F、tlast1低 32 位为 DW2忘记缩短tkeep或提前/遗漏tlast握手每一拍仅在tready1时推进只看tvalid没有处理反压发送后状态第二拍握手后转入等待返回第二拍尚未发送完成就开始解析接收数据一个实用的仿真用例是主动将tready拉低几个周期。若 RTL 正确首拍或末拍会在这段时间保持不变直到重新握手最终 TLP 仍应由 DW0、DW1、DW2 按同一顺序组成。这个用例比只在tready永远为 1 的条件下跑通更能暴露状态机问题。这一章完成了什么到这里PL 已经具备发起第一条配置读取所需的请求侧条件链路进入 L0 后状态机锁存目标 BDF 与读取位置组织一个 3-DW Configuration Read Type 0 TLP再通过两个 AXI4-Stream 拍将它交给 PCIe Root Port IP。完整控制路径现在可以写成PL 状态机 → Configuration Read Type 0 → PCIe AXI4-Stream TX → Root Port IP → NVMe SSD Endpoint → Completion with Data → PL RX 解析本篇完成了箭头左半段。下一篇进入接收侧Completion with Data 的包头和 Payload 怎样到达 FPGA为什么要用 Tag 关联请求以及超时、错误 Completion 和无关返回包应怎样处理。参考资料AMDPG054 v3.37 Series FPGAs Integrated Block for PCI ExpressChapter 4 的TLP Format on the AXI4-Stream InterfacePDF p.4546与Basic TLP Transmit OperationPDF p.47说明事务接口的字节排列和发送规则。AMDPG054Configuration InterfaceTable 16 说明 Root Portcfg_ds_*信号的适用范围以及它们不影响 AXI4-Stream 上的 TLP。AMDPG054Basic TLP Transmit Operation7 系列 PCIe IP 的事务发送接口说明。PCI-SIGPCI Express Base Specification Revision 7.0Figure 1-2PDF p.142、Figure 2-5 与 Table 2-2/2-3PDF p.158159、Table 2-8PDF p.191、Figure 2-39PDF p.211与 §7.5.1.3.2§7.5.1.3.4PDF p.1087是本章拓扑、Fmt/Type 编码、BDF 字段、Configuration Request Header 和桥 Bus Number 说明的直接来源。NVM ExpressNVMe over PCIe Transport Specification v1.4NVMe SSD 作为 PCIe Endpoint 的传输与配置空间说明。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进