MCTP over PCIe VDM:BMC带外通道的原理与抓包实践
简介这份规范文档由 DMTF 于2021年3月发布版本1.2.0文档编号DSP0238取代了此前1.1.0版本。它规定了 Management Component Transport ProtocolMCTP如何通过 PCIe VDM 传输绑定来实现组件管理适用于服务器管理、数据中心互操作与 PCIe 设备开发场景旨在让不同厂商的管理控制器与 PCIe 设备能够基于统一标准交换管理信息。资源包内为一份 PDF 文件大小约364KB正文共25节系统阐述了传输架构、VDM 数据格式、消息封装与路由、错误处理等核心内容并包含版权与专利声明可作为标准实现的法律与技术参照文档状态为 Published采用英文编写结构上包含前言、引言与正文各章便于按需检索。对于正在开发 BMC、PCIe 驱动或带外管理功能的工程师而言这份规范能帮助厘清 MCTP 报文的实际封装格式与收发流程减少协议实现中的歧义是与 PCI-SIG 规范配合使用的必备参考。这份文档目前已有1957人学习下载具备较高的行业参考价值。1. 为什么是 MCTP PCIe VDM一条不需要额外管理总线的带外通道服务器平台管理里BMC 要和 CPU、NVMe、CXL 设备交换管理信息传统做法是拉 I2C/SMBus 线速度慢、接线多。PCIe 的 Vendor Defined MessageVDM提供了另一条路不新增物理总线直接在主链路上用厂商自定义 TLP 承载管理消息。DMTF 的 DSP0238MCTP PCIe VDM Transport Binding Specification1.2.02021-03-02把这条路钉死了MCTP 包怎么封装进 Type 1 VDM、BDF 怎么映射成物理地址、总线所有者怎么分配 EID、端点怎么被发现。CXL 1.0/1.1/2.0 都把它列为规范性引用做 CXL 模块管理、BMC 固件、PCIe Switch 管理的人迟早要读。下面按实现顺序拆开讲末尾附一套抓包验证顺序。2. VDM 包格式逐字段拆解Type 1 VDM 与 MCTP 消息头2.1 为什么偏偏是 Type 1 VDM还要带数据PCIe 的 TLP 类型里厂商自定义类型叫 VDM分 Type 0 和 Type 1 两种。Type 0 不带地址字段只能用于本地广播之类的场景Type 1 带地址字段适合按 BDF 寻址送到具体端点。MCTP 消息要发给某个确定的 PCIe 功能所以选 Type 1。又因为 MCTP 包是完整的消息负载必须用 with data 形式首字节是 0x7FFmt110、Type11111无数据的 VDM 是 0x7E解析时最容易把这两个搞混。DMTF 的厂商 ID 会被多个规范共用所以只判断 Vendor ID 还不够。DSP0238 规定 MCTP 消息必须使用 MCTP VDM code 0000b用来和其它 DMTF VDM 区分。这个 code 放在 Vendor Specific HeaderVSH的低 4 位。抓包时先看 Vendor ID 是不是 DMTF 登记值再看 VSH 低 4 位是否为 0000b两个条件都满足才进入 MCTP 解析否则当成普通厂商消息跳过。这个双重判断看起来多余实际抓包时能过滤掉大量无关的厂商 VDM尤其是用同一 vendor ID 跑私有协议的设备。2.2 从 TLP 头到 MCTP 负载的逐字段解析整包分三层PCIe VDM TLP 头3DW12 字节其中偏移 6 是 Vendor ID、偏移 8 是 VSH紧接着从偏移 12 开始是 MCTP 消息。MCTP 消息开头是 4 字节传输头bit[7:4] 是头版本、bit[2:0] 是消息类型、随后是目的 EID、源 EID第四个字节是 tag/seq/TO 相关位再往后才是控制命令或 PLDM、SPDM 之类的载荷。下面给一个最小可用的解码器import struct def dump_mctp_over_pcie_vdm(data: bytes): if len(data) 16: return 报文太短放不下 VDM 头加 MCTP 头 if data[0] ! 0x7F: return f首字节 0x{data[0]:02x}不是 Type 1 VDM with data # VDM TLP 头固定 3DW: 偏移 6 是 Vendor ID偏移 8 是 VSH均小端 vendor_id struct.unpack_from(H, data, 6)[0] vsh struct.unpack_from(I, data, 8)[0] if (vsh 0x0F) ! 0x0000: return VDM code 非 0000b这条不是 MCTP 消息 # MCTP 传输头从偏移 12 开始按 DSP0236 布局逐字节取位 m data[12:16] ver (m[0] 4) 0x0F mtyp m[0] 0x07 dst, src m[1], m[2] to (m[3] 3) 0x01 seq (m[3] 2) 0x01 tag m[3] 0x03 info (fVendorID0x{vendor_id:04x} ver{ver} msg_type0x{mtyp:x} fEID {src}-{dst} tag{tag} to{to} seq{seq}) body data[16:] if mtyp 0x00 and body: # MCTP Control 消息 cmds {0x00: Set EID req, 0x01: Set EID rsp, 0x02: Get EID req, 0x03: Get EID rsp, 0x18: Discovery Notify req, 0x19: Discovery Notify rsp} info f cmd{cmds.get(body[0], hex(body[0]))} return info if __name__ __main__: # 示例: 从协议分析仪导出的一段完整 TLP共 24 字节 sample bytes.fromhex( 7f 00 04 00 00 00 b9 12 00 00 00 00 01 08 25 00 02 aa bb cc dd ee ff 00) print(dump_mctp_over_pcie_vdm(sample))代码里 vendor_id 和 vsh 用H、I解因为 PCIe TLP 头遵循小端约定从 MCTP 传输头开始则按 DSP0238 5.2 的 Big Endian 规则逐字节取位不再做端序转换这两套约定混在一个报文里刚接触时最容易踩。tag 位宽在不同版本的 DSP0236 里有过调整代码按 bit3TO、bit2PktSeq、bit1:0tag 的常见布局取实际对照手上的 Base Spec 微调即可。示例里 src0x25 的端点向 dst0x08 的总线所有者发 Get EID 请求命令码 0x02正好对应 Full Discovery 流程的第二阶段。2.3 保留位、ECRC 与未知 code 的处理字段TLP 偏移说明解析注意Fmt/Type0Type 1 VDM with data 0x7F0x7E 是无数据 VDMRequester ID/Tag3..5请求方标识VDM 里通常保留按平台实现决定是否校验Vendor ID6..7DMTF 登记的厂商 ID小端读取VSH 低 4 位8..11MCTP VDM code 0000b非 0 直接跳过MCTP 传输头12..15版本、消息类型、EID、tag 位按 5.2 的 Big Endian 规则控制/数据负载16控制命令或上层协议长度由 TLP Length 决定规范对保留位的约定是写 0、读忽略所以解析器遇到保留值不要直接报错把原始字节打出来留给人工判断。另一个高频坑是 ECRC链路上开了 TLP Digest 时TLP 尾部会追加 4 字节 ECRC抓包工具导出格式不同有带的有不带的。不先对齐这个尾巴MCTP 头解析位置整体错 4 字节最典型的现象就是 EID 方向看起来反了其实是把 ECRC 的头 4 字节当成了 MCTP 头。过滤器里顺手把不支持的消息类型按 Get Message Type Support 的协商结果跳过不要靠猜。3. 寻址与路由从 BDF 物理地址到 MCTP EID 的映射3.1 控制消息里的物理地址格式MCTP 控制消息Set EID、Get Routing Table Entries、Routing Information Update 等的消息体里会携带物理地址用来指认“哪个端点”。在 PCIe 这条 binding 上物理地址就是端点自己的 BDFBus 号 8 位、Device 号 5 位、Function 号 3 位从高到低拼成两个字节按 Big Endian 存放。位段宽度内容Bus8 bitPCIe 枚举分配的 Bus 号Device5 bitDevice 号Function3 bitFunction 号这个格式的意义在于MCTP 层用 EID 做逻辑寻址PCIe 层只认识 BDF两者之间的翻译由总线所有者完成。拿到一个 Set EID 请求时既能从 MCTP 头里看到目的 EID也能从消息体里读到该端点自报的物理地址两边能对上这条链路才算真正打通。抓包时把每个端点的 EID 和 BDF 对应关系列成一张表后面查路由问题会快很多。3.2 消息路由与 PCIe Switch 场景MCTP over PCIe VDM 的路由分两层。PCIe 层按 BDF 路由由 Root Complex 和 Switch 的转发逻辑完成MCTP 层按 EID 路由由总线所有者维护的路由表完成。一个 MCTP 消息在 PCIe fabric 里就是一条普通 Type 1 VDM TLPSwitch 对它的转发和 Memory 读写一样按 BDF 匹配出口。这里有个现实坑部分 Switch 对 VDM 的转发默认是丢弃的必须额外打开厂商寄存器里的 VDM forwarding 位否则总线所有者发的 Discovery Notify 连 Root Port 都出不去后面什么都不用谈。多级 Switch 拓扑里要沿 TLP 转发路径逐级确认而不是只在端点侧找原因。所以在实验室里先用直连拓扑把 MCTP 功能验证通再引入 Switch排错效率会高很多这跟调 PCIe 链路训练LTSSM时先降速再提速是一个思路。3.3 总线地址分配与 PCIe 枚举过程的耦合EID 分配发生在 PCIe 枚举之后。host 必须先完成枚举、给每个功能分配好 BDFVDM 才能寻址到它MCTP 的地址分配完全建立在枚举结果之上。DSP0238 6.6 定义的 bus address assignment本质就是总线所有者拿着 BDF 清单逐个用 Set EID 把合法 EID 写进端点。所以排查 MCTP 问题第一步永远是确认 PCIe 枚举过程本身没有异常# 确认设备被正确枚举并确认目标 BDF lspci -s 03:00.0 -vvv # 读 Command 寄存器看 Bus Master Enable(BME) 是否置位 setpci -s 03:00.0 COMMAND # 返回十六进制值: bit21 表示 BME 已开0x7IOMemBME0x3还没开 BMEBME 这个位经常被忽略。端点能不能收 VDM 由实现决定但能不能回 VDM 响应很多实现把它挂在 Bus Master Enable 上。主机侧驱动没开 BME 时Get EID 发出去永远没响应这时候去查 MCTP 路由表是查不出结果的。顺带一提端点如果因为 ASPM 睡在 L1/L2VDM 同样可能被丢弃调试阶段先关 ASPM 是标准做法。提示setpci -s 03:00.0 COMMAND的返回值按十六进制看bit21 表示 BME 已打开0x7 表示 IO、Memory、BME 全部使能只有 0x3 则说明 BME 没开。3.4 功能独立性一个 PF 就是一条 MCTP 链规范开头有一段容易被忽略的约束端点至少要在一个 Physical Function 上支持 MCTP over PCIe VDM如果多个 PF 都支持每个 PF 上的 MCTP 通信必须相互独立。意思是同一颗芯片的两个 PF即使挂在同一个 Switch 后面在 MCTP 层也是两个互不感知的端点各自要有独立的 EID、独立的 tag 计数器和独立的发现状态机。实现时不要为了省资源让两个 PF 共用状态跨 PF 串包是这类问题里最难查的报错往往出现在完全不相干的链路上。4. 端点发现流程Discovery Notify、Get EID 与 Set EID 的配合4.1 Full Discovery 与 Partial Endpoint DiscoveryDSP0238 6.9 把发现分成两种流程。Full Discovery 用于系统上电或总线所有者重启后的初始建立所有者认为总线上没有有效 EID从零开始把端点逐个找出来并分配地址。Partial Endpoint Discovery 用于热插拔或个别端点复位后老端点的 EID 要保留只能发现新增的部分不能把全局 EID 空间重置一遍否则正在跑的 PLDM/SPDM 会话全部断掉。Discovery Notify 在两种流程里的用法差别就在这。Full 模式下通知发出后所有未分配 EID 的端点都要进入可被发现状态Partial 模式下通知相当于一次定向广播只有新端点响应老端点确认自己 EID 仍有效就继续工作。Figure 2 和 Figure 3 分别画了这两条流实现时建议把状态机的分支按图分开写不要两个流程共用同一个处理函数否则很容易在热插拔后把老端点误判成新端点。对比项Full DiscoveryPartial Discovery触发时机上电 / 所有者初始化热插拔 / 单端点复位已有 EID全部视为无效重新分配保留只补新端点风险会中断所有在线通信需防止新旧端点 EID 冲突典型场景首次加电、BMC 重启NVMe 热插、CXL 设备添加4.2 所有者侧状态机与命令序列无论哪种流程命令序列都跑在 MCTP Control 消息框架里。以 Full Discovery 为例所有者侧的状态机可以简化成下面这段def discovery_run(owner): # 阶段1: 发 Discovery Notify让未分配 EID 的端点暴露自己 owner.send_notify(broadcastTrue) for bdf in owner.pcie_bdf_list(): # 来自上一步的枚举结果 # 阶段2: Get EID 询问端点的当前 EID 与物理地址 rsp owner.get_eid(bdf) if rsp is None or rsp.addr ! bdf: continue # 没响应留给 Partial 发现 # 阶段3: EID 非法或冲突时用 Set EID 写入合法值 if rsp.eid in owner.used_eids or rsp.eid 0: new_eid owner.allocate_eid() owner.set_eid(bdf, new_eid) # 阶段4: 登记路由表 owner.routing_table[bdf] rsp.eid阶段 1 和阶段 3 最容易出问题。Discovery Notify 在 PCIe 侧是按 BDF 逐个发还是按域广播规范没写死平台差异很大直连拓扑没问题不代表到了 Switch 后面每个端点都能收到。Set EID 是带 tag 的请求响应配对响应超时后不能原地重发同一个 tagtag 必须递增否则端点会把重发当成重复请求丢掉。这也解释了为什么 DMTF 坚持让每个 PF 的 tag 空间独立tag 复用的边界就是 MCTP 端点的边界。4.3 控制消息时序先把超时放宽再逐步收紧DSP0238 6.10 对 MCTP Control 消息在 PCIe VDM 上的时序有明确约束包括端点收到 Discovery Notify 后完成初始化的时限、控制请求到响应之间的最大间隔、以及重试之间的最小间隔具体毫秒值都在 Table 4 里。我的做法是先按表里上限的两到三倍设超时整套流程跑通后再逐步收到规范值。这样能把协议栈 bug 和平台慢启动分开如果某个端点总是在接近两倍超时时才回包它的固件大概率没按规范做异步处理而是同步轮询等 DMA 完成这类问题在收紧超时时会立刻暴露。5. 时序要求与排错发现不到端点时先查这六个点5.1 链路层与配置空间先行排错顺序决定效率。发现不到端点时我按这个顺序查先lspci -s bdf -vvv确认设备被枚举且 BME 置位再确认端点没睡在低功耗状态调试阶段先关 ASPM然后确认路径上每个 Switch 都开了 VDM 转发。这三条用 lspci 和 setpci 几分钟就能验证完能排除掉一大半物理层问题不用急着上协议分析仪。5.2 用 trace 定位协议层VDM 报文不经过网卡tcpdump 看不到要么用 PCIe 协议分析仪在链路上抓要么在 BMC 侧固件里打 VDM 接收 trace。拿到 trace 后用第二章的 decoder 逐包解析重点看三样东西VSH 低 4 位是不是 0000b、MCTP 头的 EID 方向是否符合预期、控制命令的 tag 在请求响应里是否配对。请求有响应但 tag 对不上多半是端点的 tag 管理实现有问题响应里的源 EID 不是自己分配的则是 Set EID 阶段的地址冲突。5.3 把 decoder 接到 trace 上做超时统计最后给一个收尾技巧把 decoder 的输出存成ts src dst tag cmd is_rsp格式的文本然后用下面这段脚本找出所有发出后没有响应的请求req {} for line in open(parsed.log): ts, src, dst, tag, cmd, is_rsp line.split() key (src, dst, tag, cmd) if is_rsp R: req.pop(key, None) # 响应到达配对成功 else: req[key] ts # 记录请求发出时间 for key, ts in req.items(): # 剩下的都是超时未响应的 print(no response:, key, sent at, ts)把sent at的时间和 Table 4 的时限做差就能量化每个端点慢了多少。这个过滤条件按 (src, dst, tag, cmd) 四元组配对比只按 tag 配对更稳因为不同端点可能复用 tag四元组能避免误判。上线前把这条命令加进自动化脚本基本能覆盖大部分 MCTP over PCIe VDM 的回归检查。本文还有配套的精品资源点击获取