CXL.cache 协议规范详解:请求与响应
这一章我们深入聊聊 CXL.cache 协议的请求和响应。说白了,没有请求和响应,一致性就是个空壳。我在做第一个 CXL 验证项目时,就被这块的复杂交互绊了一跤——本来以为简单发个读请求,结果返回的状态码让我排查了三天。
2.1 协议消息的分类
CXL.cache 的消息分两大类:D2H(Device to Host) 和 H2D(Host to Device)。D2H 是设备发起的,H2D 是主机发起的。嗯,你想想看,设备自己也能主动发请求,这和传统 PCIe 的从设备模式很不一样。
具体说,D2H 请求包括:读请求、写请求、预取、需要主机的窥探响应等。H2D 请求主要是主机的窥探请求、写入请求、消息通知等。我习惯把 D2H 看作是设备“向上喊话”,H2D 是主机“向下指挥”。
2.2 D2H 请求类型速览
先看设备发起的请求。下表是 D2H 事务的简要分类,注意每个请求都包含完整的元数据字段(如标记 ID、地址、大小、属性等)。
| 事务类型 | 编码 | 方向 | 作用简述 |
|---|---|---|---|
| ReadReq | 0x00 | D2H | 设备读私有数据(独占或共享) |
| ReadReq_I | 0x01 | D2H | 设备读并计划修改(获取唯一权) |
| WriteReq | 0x02 | D2H | 设备写回脏数据 |
| WriteReq_I | 0x03 | D2H | 设备写回并保持唯一权 |
| PrefetchReq | 0x04 | D2H | 预取数据,不修改 |
| PrefetchReq_I | 0x05 | D2H | 预取并预修改 |
| NonDataReq | 0x08-0x0B | D2H | 各种非数据请求(如刷新、完成通知) |
注意表中的“_I”后缀——它代表“Intent to Modify”。我曾经遇到过一个问题:设备发了 ReadReq 而不是 ReadReq_I,结果后面再写数据时,主机认为设备没权限独占,导致协议降级,性能掉了 20%。这里提醒:如果你的设备要改数据,必须用带 _I 的请求。
2.3 H2D 请求类型速览
主机发起的请求主要是窥探(Snoop)和消息。窥探的作用是让设备保有一份缓存副本,主机要读取或修改时,需要先“问”设备。
| 事务类型 | 编码 | 方向 | 作用 |
|---|---|---|---|
| SnoopReq | 0x00 | H2D | 主机窥探设备,请求数据/状态 |
| SnoopReq_I | 0x01 | H2D | 主机窥探并打算修改 |
| MsgReq | 0x08-0x0F | H2D | 主机发送的消息(如刷新、回写触发) |
2.4 响应格式与状态码
每个请求都必须有响应。响应分为数据响应和非数据响应。数据响应携带缓存行数据(通常 64B),非数据响应只携带状态码和元数据。
状态码(Status Code)是协议交互的“裁判”。下表列出最关键的几个:
| 状态码 | 名称 | 含义 | 典型场景 |
|---|---|---|---|
| 0x00 | Success(Shared) | 成功,数据以共享状态返回 | ReadReq 被命中,返回共享缓存行 |
| 0x01 | Success(Exclusive) | 成功,数据以独占状态返回 | ReadReq_I 得到唯一权 |
| 0x02 | Success(Modified) | 成功,数据以修改状态返回(包含脏数据) | 设备写回脏数据 |
| 0x08 | No Data | 无数据 | PrefetchReq 不需要数据时 |
| 0x10 | Need Data | 主机要求设备提供数据 | SnoopReq 命中设备缓存有脏数据 |
| 0x1F | Invalid | 请求无效 | 协议违规或地址不存在 |
2.5 交互流程与状态机
现在我们把请求和响应串起来看一个完整例子。假设设备要读一块数据并准备修改(ReadReq_I):
- 设备发送 D2H ReadReq_I,带地址和标记 ID。
- 主机收到后,检查自己的缓存目录。如果发现该地址在另一设备缓存中,先向那设备发 SnoopReq。
- 那设备返回响应,可能带脏数据或释放所有权。
- 主机从自身内存或接收到的数据中,构造对原设备的响应:带独占状态(0x01)和 64B 数据。
- 原设备收到响应,将数据装入缓存,状态设为“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 秒内回顾本章核心。
2.8 ATE 测试中的注意事项
基于本章的内容,我总结几条 ATE 测试的检查点:
- 请求命令编码完整性:所有 D2H 和 H2D 命令都要测到,包括保留编码(应该返回 Invalid)。
- 响应状态码覆盖:每个请求至少测试一次 Success、No Data、Need Data、Invalid 的返回。
- 标记 ID 乱序:设备可以同时发送多个请求,标记 ID 可以不按顺序回。我写过脚本让设备乱序发出 64 个 ReadReq,然后主机随机穿插 SnoopReq 来测试状态机。
- 地址对齐边界:非 64B 对齐地址必须返回 Invalid。
好了,本章把请求与响应的骨架搭建完了。下一块我们就要看这些消息怎么携带数据,以及数据状态如何在主机和设备间传递。先消化这些,后面的就顺了。