【PCIe 验证每日学习・Day20】PCIe 事务排序规则(Ordering)彻底吃透 + 死锁 / 阻塞验证
·
大家好,继续我们「PCIe 验证 30 分钟每日打卡」系列。
前 19 天快速串联
- Day1–7:PCIe 三层架构、LTSSM 链路训练、TLP 基础结构、BAR 地址映射
- Day8–10:MemRd/MemWr、CfgRd/CfgWr、UR/CA/CRS 异常响应
- Day11–14:Capability 链表、DLLP/ACK/NAK 重传、Credit 流控机制
- Day15:Completion 完成包、Tag 匹配、字节计数与重组
- Day16:地址路由、ID 路由、隐式路由三大规则
- Day17:Msg TLP、INTx 虚拟中断、MSI/MSI-X 消息中断
- Day18:物理 / 链路 / 事务三层错误处理、ECRC、Poison TLP、Recovery 恢复
- Day19:PM 电源管理、D0/D3 设备状态、L0s/L1/L2 链路休眠与唤醒
- 今日核心:PCIe 最抽象、最容易出 Bug 的模块 ——事务排序规则(Ordering)它决定了 TLP 谁先走、谁后走、谁不能超车、谁必须等待,直接关系系统是否死锁、数据是否错乱。
今日学习结构
- 0–12 分钟:为什么需要排序?PCIe Ordering 核心思想与模型
- 13–24 分钟:各类交易之间的强序 / 弱序规则
- 25–34 分钟:Posted 与 Non-Posted 交易本质区别、阻塞与死锁成因
- 35–40 分钟:UVM 验证方案、断言监控、典型测试场景
- 40 分钟:必测项 + 高频易错坑点总结
一、为什么 PCIe 必须严格规定排序规则?
在 CPU、多核心、多设备、多级 Switch 同时收发数据的场景下,如果所有 TLP 都可以 “随意超车、随意插队”,会出现两个致命问题:
-
数据一致性错误比如 CPU 先写一个标志位,再读状态。如果读请求 “超车” 跑到写前面,就会读到旧值,软件逻辑直接崩溃。
-
死锁(Deadlock)两个设备互相等待对方的资源,谁都不退让,整个链路卡死不动。
PCIe 规范为了避免这两类灾难,强制定义了一套不可违反的交易执行顺序,这就是 Ordering。
1.1 两个最基础的分类(理解排序的前提)
所有 TLP 只分为两大类,所有排序规则都围绕它们展开:
(1)Posted Transaction(Posted,简写 P)
- 发出去不需要 Completion 回应
- 发送后不用等回复,直接发下一个
- 典型:MemWr、CfgWr、Msg、MSI
特点:
- 不阻塞、不等待、效率高
- 发送端不知道对方是否收到
- 属于 “发完不管” 类型
(2)Non-Posted Transaction(Non-Posted,简写 NP)
- 发出去必须等 Completion 回来
- 必须收到回应才能结束事务
- 典型:MemRd、CfgRd、IORd
特点:
- 可靠、有确认
- 会阻塞、会等待
- 属于 “一问一答” 类型
1.2 排序的核心原则
- 同一数据流必须保持顺序同一个流的多个包,不能乱序。
- 不同数据流允许乱序,以提高性能互不相关的请求可以互相超车。
- 某些组合绝对禁止乱序(强序规则)一旦乱序会导致数据错误或死锁。
- Switch、Bridge 必须严格遵守转发顺序不能私自重排,否则系统崩溃。
二、PCIe 强序规则逐条详解
下面是 PCIe 规范中最核心、验证必考、绝对不能错的排序规则。我用最直白的方式逐条解释,不使用缩写歧义,确保一次看懂。
规则 1:Write → Read 禁止乱序
先写后读,读不允许超车跑到写前面。
解释:
- CPU 先发一个 MemWr 写数据
- 紧接着发一个 MemRd 读同一个地址
- Rd绝对不能跑到 Wr 前面
- 否则读到旧数据,逻辑错误
这是最重要、最基础、最不能违反的一条强序规则。
规则 2:Write → Write 允许乱序(不同地址)
- 两个写操作地址不同
- 可以互相超车、乱序
- 不影响一致性
但如果是同一地址,芯片内部通常会保证顺序,避免数据覆盖错乱。
规则 3:Read → Read 允许乱序
- 两个读请求之间可以互相超车
- 只要它们访问不同地址
- 完成包返回顺序可以不一致
规则 4:Read → Write 允许乱序
- 先读后写,写可以超车跑到读前面
- 规范允许,不会造成一致性问题
规则 5:Non-Posted 不能越过另一个 Non-Posted
- 两个读请求(都需要 Completion)
- 后来的读不能插到前面的读之前
- 避免完成包乱序导致 Tag 匹配混乱
规则 6:Posted 可以越过 Non-Posted
- 写操作可以 “超车” 插到读前面
- 这是 PCIe 性能优化的关键
- 但必须保证不会引发死锁
规则 7:Non-Posted 绝对不能越过 Posted
- 读请求不允许插到写操作前面
- 这是强序规则,违反直接协议错误
三、Posted / Non-Posted 深度模型与死锁成因
3.1 为什么会出现死锁?
死锁的经典场景:
- DeviceA 发一个 Non-Posted(读)给 DeviceB
- DeviceB 发一个 Non-Posted(读)给 DeviceA
- 双方都在等对方的 Completion
- 中间 Switch 缓冲区被占满
- 谁都无法发送,也无法接收→ 死锁,链路卡死
PCIe 通过排序规则 + 流控 Credit 机制,从根源上避免这种情况。
3.2 死锁三大必要条件
- 循环等待:A 等 B,B 等 A
- 资源互斥:缓冲区不能共享
- 不剥夺:已经占有的资源不能被抢走
PCIe 排序规则就是用来打破循环等待。
3.3 规范如何避免死锁?
- 强制 Posted 优先级高于 Non-Posted
- 限制转发顺序,避免环路占用
- 流控 Credit 保证缓冲区不会被完全堵死
- Completion 必须走 ID 路由,快速回流释放资源
四、UVM 排序验证实现
4.1 验证思路
- 构造一对 先写后读 的交易序列
- Monitor 监测 TLP 发送顺序与返回顺序
- 断言确保 Read 没有跑到 Write 前面
- 压力测试多并发场景,检查是否乱序、死锁
4.2 先写后读序列示例
class pcie_wr_then_rd_seq extends uvm_sequence #(pcie_tlp_trans);
`uvm_object_utils(pcie_wr_then_rd_seq)
virtual task body();
pcie_tlp_trans wr, rd;
// 先发写
wr = pcie_tlp_trans::type_id::create("wr");
start_item(wr);
wr.tlp_type = MEM_WR;
wr.addr = 32'h10000000;
wr.data = 32'h12345678;
finish_item(wr);
// 后发读
rd = pcie_tlp_trans::type_id::create("rd");
start_item(rd);
rd.tlp_type = MEM_RD;
rd.addr = 32'h10000000;
finish_item(rd);
endtask
endclass
4.3 核心排序断言(绝对不能违反)
module pcie_ordering_assert;
`include "uvm_macros.svh"
input clk, rst_n;
input wr_vld, rd_vld;
input [31:0] addr;
// 强序规则:同一地址 先写后读,读不得超车
property p_wr_before_rd;
@(posedge clk) disable iff(!rst_n)
wr_vld |-> not ##[1:100] (rd_vld && !wr_complete);
endproperty
a_wr_before_rd: assert property(p_wr_before_rd) else
`uvm_error("ORDER_ERR", "违反强序规则:读请求超车跑到写前面!")
endmodule
五、今日必测项 + 高频易错点总结
必测项
- 同一地址先写后读,读不超车,顺序严格
- 不同地址读写允许合理乱序,提升性能
- Non-Posted 不越过 Posted
- 多并发读写不出现死锁、不阻塞
- Switch/Bridge 转发严格遵守排序规则
- 压力场景下无协议报错、无卡死
高频易错点
- 误以为 “所有 TLP 都必须按顺序”→ 过度约束,性能下降
- 允许 Read 超车 Write → 数据一致性错误
- 死锁测试不充分,现场复现困难
- 忽略 Switch 内部转发顺序,只看端口顺序
- 错误认为 Completion 可以随便乱序
明日 Day21 预告
【PCIe 验证每日学习・Day21】PCIe 复位与功能级复位(FLR)机制 + 冷 / 热 / 位宽 / 链路复位全验证内容包括:
- Cold/Hot/Link/Function Level Reset 全类型
- 复位时序、隔离规则、寄存器复位值
- 复位期间 TLP 处理、中断清除、状态恢复
- UVM 复位序列、断言、异常场景验证
更多推荐



所有评论(0)