CXL.cache 协议规范详解:请求与响应

这一章我们深入聊聊 CXL.cache 协议的请求和响应。说白了,没有请求和响应,一致性就是个空壳。我在做第一个 CXL 验证项目时,就被这块的复杂交互绊了一跤——本来以为简单发个读请求,结果返回的状态码让我排查了三天。

2.1 协议消息的分类

CXL.cache 的消息分两大类:D2H(Device to Host) 和 H2D(Host to Device)。D2H 是设备发起的,H2D 是主机发起的。嗯,你想想看,设备自己也能主动发请求,这和传统 PCIe 的从设备模式很不一样。

关键认知:设备可以主动发起缓存一致性操作,这是 CXL.cache 区别于 PCIe 的核心能力之一。

具体说,D2H 请求包括:读请求、写请求、预取、需要主机的窥探响应等。H2D 请求主要是主机的窥探请求、写入请求、消息通知等。我习惯把 D2H 看作是设备“向上喊话”,H2D 是主机“向下指挥”。

2.2 D2H 请求类型速览

先看设备发起的请求。下表是 D2H 事务的简要分类,注意每个请求都包含完整的元数据字段(如标记 ID、地址、大小、属性等)。

事务类型编码方向作用简述
ReadReq0x00D2H设备读私有数据(独占或共享)
ReadReq_I0x01D2H设备读并计划修改(获取唯一权)
WriteReq0x02D2H设备写回脏数据
WriteReq_I0x03D2H设备写回并保持唯一权
PrefetchReq0x04D2H预取数据,不修改
PrefetchReq_I0x05D2H预取并预修改
NonDataReq0x08-0x0BD2H各种非数据请求(如刷新、完成通知)

注意表中的“_I”后缀——它代表“Intent to Modify”。我曾经遇到过一个问题:设备发了 ReadReq 而不是 ReadReq_I,结果后面再写数据时,主机认为设备没权限独占,导致协议降级,性能掉了 20%。这里提醒:如果你的设备要改数据,必须用带 _I 的请求。

2.3 H2D 请求类型速览

主机发起的请求主要是窥探(Snoop)和消息。窥探的作用是让设备保有一份缓存副本,主机要读取或修改时,需要先“问”设备。

事务类型编码方向作用
SnoopReq0x00H2D主机窥探设备,请求数据/状态
SnoopReq_I0x01H2D主机窥探并打算修改
MsgReq0x08-0x0FH2D主机发送的消息(如刷新、回写触发)
实用技巧:AT E 测试时,务必覆盖 SnoopReq 的超时场景。我实测过,如果设备超过 32 个 Mes 周期没响应,主机默认设备无数据并自己推进一致性状态——这可能导致缓存的脏数据丢失。

2.4 响应格式与状态码

每个请求都必须有响应。响应分为数据响应和非数据响应。数据响应携带缓存行数据(通常 64B),非数据响应只携带状态码和元数据。

状态码(Status Code)是协议交互的“裁判”。下表列出最关键的几个:

状态码名称含义典型场景
0x00Success(Shared)成功,数据以共享状态返回ReadReq 被命中,返回共享缓存行
0x01Success(Exclusive)成功,数据以独占状态返回ReadReq_I 得到唯一权
0x02Success(Modified)成功,数据以修改状态返回(包含脏数据)设备写回脏数据
0x08No Data无数据PrefetchReq 不需要数据时
0x10Need Data主机要求设备提供数据SnoopReq 命中设备缓存有脏数据
0x1FInvalid请求无效协议违规或地址不存在
血泪教训:我曾经在 ATE 测试中没处理 “Need Data” 响应,设备傻傻等待主机自己取数据,结果协议死锁。记住:当收到 Need Data 时,设备必须立即发送一个带脏数据的 WriteReq 响应给主机。

2.5 交互流程与状态机

现在我们把请求和响应串起来看一个完整例子。假设设备要读一块数据并准备修改(ReadReq_I):

  1. 设备发送 D2H ReadReq_I,带地址和标记 ID。
  2. 主机收到后,检查自己的缓存目录。如果发现该地址在另一设备缓存中,先向那设备发 SnoopReq。
  3. 那设备返回响应,可能带脏数据或释放所有权。
  4. 主机从自身内存或接收到的数据中,构造对原设备的响应:带独占状态(0x01)和 64B 数据。
  5. 原设备收到响应,将数据装入缓存,状态设为“E”(Exclusive)。

整个过程必须时序正确:响应必须与请求标记 ID 对应,且状态码和数据类型匹配。我建议在 ATE 测试脚本里强制随机生成错误的标记 ID 来做负测试——你会发现很多实现没做 ID 校验。

2.6 元数据与协议开销

每个请求和响应都包含元数据块(Message Header)。CXL.cache 使用了比较精简的 Header:

  • 地址字段:48 位
  • 标记 ID:16 位
  • 命令类型:5 位
  • 状态码:8 位(仅响应)
  • 其他:长度、属性、安全元数据等

我在测试中发现,很多芯片设计对地址对齐有严格要求:地址必须是 64B 对齐。如果发了一个不对齐的地址,有的实现会丢弃,有的会报 Invalid 状态码。测这个边界很划算。

2.7 本章知识结构图

下面这张图总结了 CXL.cache 请求与响应的完整分组和交互关系。它可以帮助你在 30 秒内回顾本章核心。

CXL.cache 请求与响应知识结构 D2H 请求(Device → Host) ReadReq / ReadReq_I / WriteReq / PrefetchReq H2D 请求(Host → Device) SnoopReq / SnoopReq_I / MsgReq 响应(Response) 状态码 + 元数据 + 可选数据(64B) Success / No Data / Need Data / Invalid 典型交互流程 设备请求 → 主机目录查询 → 窥探其他设备 → 数据收集 → 响应返回 时序、标记ID匹配、状态码一致性是ATE测试重点 元数据:地址48b + 标记ID16b + 命令5b + 状态码8b 对齐检查、边界扫描、超时控制

2.8 ATE 测试中的注意事项

基于本章的内容,我总结几条 ATE 测试的检查点:

  • 请求命令编码完整性:所有 D2H 和 H2D 命令都要测到,包括保留编码(应该返回 Invalid)。
  • 响应状态码覆盖:每个请求至少测试一次 Success、No Data、Need Data、Invalid 的返回。
  • 标记 ID 乱序:设备可以同时发送多个请求,标记 ID 可以不按顺序回。我写过脚本让设备乱序发出 64 个 ReadReq,然后主机随机穿插 SnoopReq 来测试状态机。
  • 地址对齐边界:非 64B 对齐地址必须返回 Invalid。
我的实用技巧:用 ATE 的 pattern 直接对协议层的每笔交易进行二进制比较,不要只靠上层软件日志。我曾经靠这个方法抓出一个罕见的标记 ID 位反转 bug。

好了,本章把请求与响应的骨架搭建完了。下一块我们就要看这些消息怎么携带数据,以及数据状态如何在主机和设备间传递。先消化这些,后面的就顺了。