
简介这份资源是 Mellanox 网卡编程参考手册PRM的最新版本面向从事 RDMA 驱动开发、固件调试与高性能网络协议栈研究的工程师以及需要深入理解网卡软硬件接口的开发者。手册系统梳理了以太网段字段、Padding 段格式、用户态内存注册UMRWQE 格式等底层数据结构对 WQE 中 insert_vlan、insert_trailer、inline_headers 等字段的位域定义与关联规则均有细致说明是排查数据包封装与内存注册问题的关键依据。资源包共 1 个 PDF 文件大小约 8.51MB内容完整、便于离线查阅与检索。目前已有 22 人学习适合需要对照寄存器与 WQE 布局进行驱动开发或协议分析的读者可帮助快速定位字段含义、理解硬件行为并提升调试效率。1. 从一份寄存器手册说起Mellanox 网卡到底怎么被程序“指挥”很多人第一次接触 Mellanox 网卡是从ethtool -i看到driver: mlx5_core开始的但真正决定这块卡能跑多快、支持哪些卸载能力的不是驱动名字而是它背后那套被固件和驱动共同解释的寄存器与命令接口。Programmer’s Reference ManualPRM就是描述这套接口的文档第二册通常聚焦在命令队列、门铃寄存器、完成事件和初始化流程这些偏底层的部分。它解决的问题很具体当你要写一个用户态驱动、做 RDMA 性能调优、或者排查“链路起来了但队列不工作”这类玄学问题时PRM 是唯一能告诉你每个比特含义的东西。适合谁做高性能网络、DPDK 二次开发、自研网卡驱动、或者需要绕过内核直接操作硬件的工程师。如果你只是用ib_write_bw跑个带宽那暂时不用碰它但一旦要改队列深度、调中断合并、或者理解为什么mlx5驱动要那样初始化这份手册就是绕不开的参考。2. 先搞懂 PRM 第二册里的命令接口模型从 HCA 到队列对2.1 命令队列与门铃驱动和固件之间的“黑匣子”通道Mellanox 网卡现在叫 NVIDIA ConnectX 系列的固件对外暴露的是一套基于命令的接口。驱动不直接写业务寄存器而是把命令描述符command descriptor写进一块叫 Command QueueCQ注意和 Completion Queue 区分的环形缓冲区然后按门铃寄存器doorbell register通知固件取走。固件处理完再把结果写回另一个缓冲区并产生一个完成事件。这个模型在 PRM 第二册里叫 “Command Interface”是所有初始化、建 QP、改 MTU、查温度的基础。我一般会先确认三件事命令队列的基地址寄存器偏移、门铃寄存器的触发方式是写一个 64 位值还是写两个 32 位、以及完成事件里 status 字段的编码。PRM 里通常用表格给出这些寄存器的地址偏移和位域定义。比如初始化命令INIT_HCA的输入邮箱里cmd字段在第一个 32 位字的低 16 位opcode又分opmod和opcode两部分。这些细节如果搞错固件会直接返回0x1错误码但不会告诉你具体哪个字段错了——这就是很多人说的“黑匣子”体验。一个常见的做法是先用内核驱动mlx5_core把卡初始化好然后通过pread/pwrite访问/sys/class/infiniband/mlx5_0/device/resource0这类资源文件在用户态发命令。下面这段 Python 片段演示了如何映射 PCI 资源并读取一个命令队列的头部仅用于理解结构实际生产环境要用 C 或 Rust 做内存屏障和 volatile 访问。import mmap import os import struct # 打开 PCI 设备的 resource0 文件通常需要 root 权限 # 路径因系统而异这里用占位符表示 pci_resource /sys/bus/pci/devices/0000:01:00.0/resource0 fd os.open(pci_resource, os.O_RDWR | os.O_SYNC) size 0x100000 # 映射 1MB实际大小看 BAR 长度 mem mmap.mmap(fd, size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) # 假设命令队列基地址在 BAR 的 0x1000 偏移处 # 读取第一个命令描述符的前 8 个字节 cmd_queue_base 0x1000 desc mem[cmd_queue_base:cmd_queue_base8] opcode, opmod, flags struct.unpack(HHH, desc[:6]) print(fopcode0x{opcode:04x}, opmod0x{opmod:04x}, flags0x{flags:04x}) # 门铃寄存器通常在 BAR 的另一个偏移写 64 位值触发 doorbell_offset 0x2000 # 注意实际写门铃需要保证命令描述符已经写入并刷新 # 这里只是演示结构不要直接在生产环境执行 mem[doorbell_offset:doorbell_offset8] struct.pack(Q, 1)这段代码的逻辑是先映射 PCI BAR 空间然后按 PRM 给出的偏移读取命令描述符解析出 opcode 和 opmod。参数说明上opcode决定命令大类比如 0x1 是 INIT_HCA0x2 是 TEARDOWN_HCAopmod是子操作flags里可能有icmd位表示这是初始化命令。门铃写入的值通常是队列的生产索引producer index固件靠这个值判断有多少新命令要处理。注意用户态直接写门铃需要确保 CPU 写顺序x86 上一般用sfenceARM 上要用dmb否则固件可能看到旧数据。2.2 队列对QP上下文PRM 里最容易被误读的 64 字节队列对是 RDMA 通信的基本单位每个 QP 有一份上下文context存在网卡片上内存或主机内存里。PRM 第二册会给出 QP context 的精确布局前 16 字节是通用字段state、mtu、pd 等后面是发送队列和接收队列的基地址、深度、门铃记录等。很多人调 QP 时只改state从 RESET 到 INIT 再到 RTR 再到 RTS但忽略了log_sq_stride和log_rq_stride这两个字段结果就是队列能建起来但一发数据就报local protection error。我一般会按这个顺序检查 QP context先看state是否按 PRM 要求的顺序迁移再看mtu是否和路径 MTU 匹配然后确认log_sq_stride是否等于log2(每条 WQE 的字节数)。PRM 里通常写 “SQ stride is 64 bytes for mlx5”但如果你用了带内联数据的 WQEstride 可能变成 128 或 256。这个参数设错硬件会按错误的步长读 WQE轻则数据错乱重则触发 PCIe 错误。下面是一个用ibv_modify_qp的 C 代码片段展示了状态迁移时哪些属性必须一起设。虽然这是 verbs API但底层最终会转换成 PRM 里的 QP context 字段。#include infiniband/verbs.h #include stdio.h #include string.h int modify_qp_to_rts(struct ibv_qp *qp, uint32_t dest_qp_num, uint16_t pkey_index) { struct ibv_qp_attr attr; memset(attr, 0, sizeof(attr)); // 先迁移到 INIT设置端口和 pkey attr.qp_state IBV_QPS_INIT; attr.pkey_index pkey_index; attr.port_num 1; attr.qp_access_flags IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE; if (ibv_modify_qp(qp, attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS)) { perror(modify to INIT failed); return -1; } // 再迁移到 RTR设置目标 QP 号和路径 MTU memset(attr, 0, sizeof(attr)); attr.qp_state IBV_QPS_RTR; attr.path_mtu IBV_MTU_1024; attr.dest_qp_num dest_qp_num; attr.rq_psn 0; attr.max_dest_rd_atomic 1; attr.min_rnr_timer 12; if (ibv_modify_qp(qp, attr, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER)) { perror(modify to RTR failed); return -1; } // 最后到 RTS memset(attr, 0, sizeof(attr)); attr.qp_state IBV_QPS_RTS; attr.timeout 14; attr.retry_cnt 7; attr.rnr_retry 7; attr.sq_psn 0; attr.max_rd_atomic 1; if (ibv_modify_qp(qp, attr, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC)) { perror(modify to RTS failed); return -1; } return 0; }逻辑说明这段代码按 PRM 要求的状态机顺序迁移 QP。参数上pkey_index通常为 0port_num为 1单口卡path_mtu要和交换机 MTU 一致rq_psn和sq_psn是初始序列号一般设 0 但两端要匹配。min_rnr_timer是接收端没准备好时发送端等待的时间设太小会频繁重试设太大会增加延迟。timeout是本地 ACK 超时单位是 4.096 微秒乘以 2 的 timeout 次方14 大约对应 67 毫秒。这些值在 PRM 里都有编码表不要凭感觉填。2.3 完成事件与错误码PRM 里最该打印出来贴在显示器边上的表PRM 第二册的完成事件Completion Event章节通常有一张几十行的错误码表从0x0成功到0x1本地保护错误再到0x22远程访问错误。很多人看到ibv_poll_cq返回一个非零 status 就懵了其实只要对着 PRM 查syndrome字段就能定位。比如0x4是本地长度错误通常意味着 WQE 里写的byte_count超过了 MR 的长度0x5是本地保护错误多半是 MR 的rkey或lkey不对。我习惯在代码里把完成事件的vendor_err和syndrome都打出来然后对照 PRM 的表格。下面是一个解析完成事件的片段#include infiniband/verbs.h #include stdio.h void dump_wc(struct ibv_wc *wc) { printf(wc.status%d (%s)\n, wc-status, ibv_wc_status_str(wc-status)); printf(wc.opcode%d\n, wc-opcode); printf(wc.vendor_err0x%x\n, wc-vendor_err); printf(wc.byte_len%u\n, wc-byte_len); printf(wc.qp_num%u\n, wc-qp_num); // syndrome 在 mlx5 里通常放在 vendor_err 的高 8 位 uint8_t syndrome (wc-vendor_err 24) 0xff; printf(syndrome0x%02x\n, syndrome); }参数说明wc.status是 verbs 层抽象后的状态vendor_err是硬件原始错误码syndrome是 PRM 里定义的详细错误原因。比如 syndrome 为0x1表示 “Local Protection Error”具体是哪个保护域出错还要看 QP context 里的pd字段。注意不同固件版本 syndrome 的位域可能略有差异以你手头 PRM 第二册的表格为准。3. 用 PRM 第二册的寄存器定义做一次最小初始化从复位到能发命令3.1 复位和固件版本读取第一步别急着建 QP很多新手一上来就想建 QP 发数据结果卡在INIT_HCA返回0x1。我一般会先做两件事读固件版本确认卡处于可操作状态然后发一个QUERY_HCA_CAP命令看看固件支持哪些能力。PRM 第二册里QUERY_HCA_CAP的输入邮箱通常只需要填opcode和opmod输出邮箱会返回一大块能力位图包括最大 QP 数、是否支持 RoCEv2、是否支持原子操作等。复位操作在 PRM 里叫TEARDOWN_HCA或通过写reset寄存器。但注意如果你是在用户态操作复位前要确保没有其他驱动在访问这块卡否则会触发 PCIe 错误甚至主机重启。我一般会在虚拟机或隔离环境里做这类实验物理机上至少要先rmmod mlx5_core并确认没有残留的 VF。下面是一个用pread读取固件版本寄存器的例子假设你已经映射了 BARimport mmap import os import struct # 假设固件版本寄存器在 BAR 的 0x0 偏移长度 4 字节 # 实际偏移请查 PRM 第二册的 Firmware Version Register 章节 fw_ver_offset 0x0 fd os.open(/sys/bus/pci/devices/0000:01:00.0/resource0, os.O_RDONLY) mem mmap.mmap(fd, 0x1000, mmap.MAP_SHARED, mmap.PROT_READ) val struct.unpack(I, mem[fw_ver_offset:fw_ver_offset4])[0] major (val 24) 0xff minor (val 16) 0xff subminor (val 8) 0xff print(fFW {major}.{minor}.{subminor:04d})逻辑说明固件版本寄存器通常是只读的读出来按字节拆分就是主版本、次版本和子版本。参数上偏移量必须查 PRM不同型号ConnectX-4/5/6可能不同。如果你读出来全是0xffffffff说明 BAR 没映射对或者卡没上电。3.2 创建命令队列邮箱地址、深度和门铃记录命令队列的创建在 PRM 第二册里通常叫 “Create Command Queue” 或通过INIT_HCA的输入邮箱指定。你需要分配一块 DMA 内存作为命令队列把物理地址填进邮箱设置深度一般是 64 或 256然后写门铃寄存器通知固件。固件会返回一个cmdq_addr或者直接开始从队列取命令。我一般会分配 4KB 对齐的内存深度设 64每条命令描述符 64 字节。这样队列总大小是 4KB正好一页。门铃寄存器的偏移和触发方式在 PRM 里叫 “Command Doorbell”通常是一个 64 位寄存器写生产索引的低 16 位有效。下面是一个用posix_memalign分配 DMA 内存并获取物理地址的片段需要配合/proc/self/pagemap或 VFIO 的 IOMMU 映射#include stdlib.h #include stdio.h #include stdint.h #include fcntl.h #include unistd.h #define CMDQ_DEPTH 64 #define CMDQ_ENTRY_SIZE 64 #define CMDQ_SIZE (CMDQ_DEPTH * CMDQ_ENTRY_SIZE) int main() { void *cmdq; if (posix_memalign(cmdq, 4096, CMDQ_SIZE) ! 0) { perror(posix_memalign); return -1; } // 清零队列 for (int i 0; i CMDQ_SIZE; i) ((char *)cmdq)[i] 0; // 获取物理地址在 VFIO 环境下用 ioctl 获取 IOMMU 映射 // 这里只是演示分配实际物理地址获取方式取决于你的运行环境 printf(cmdq virtual address: %p\n, cmdq); // 接下来要把物理地址写入 INIT_HCA 邮箱的 cmdq_addr 字段 // 然后写门铃寄存器 return 0; }参数说明CMDQ_DEPTH必须是 2 的幂PRM 里通常要求 16 到 4096 之间。CMDQ_ENTRY_SIZE在 mlx5 上是 64 字节但有些老型号是 32 字节查 PRM 确认。物理地址的获取在用户态比较麻烦常见做法是用 VFIO 把设备直通给用户态程序然后通过VFIO_IOMMU_MAP_DMA拿到 IOVA这个 IOVA 就是固件看到的地址。如果你没有 VFIO 环境可以先在内核模块里做实验用dma_alloc_coherent拿物理地址。3.3 发第一条 QUERY_HCA_CAP验证命令通道是否真的通了命令队列建好后发一条QUERY_HCA_CAP是最稳妥的验证。输入邮箱里填opcode0x1具体值查 PRMopmod0x0然后写门铃。等一小会儿读完成队列或者轮询输出邮箱的status字段。如果返回0x0说明命令通道通了如果返回0x1检查邮箱地址是否对齐、门铃是否写对、固件是否已经初始化。我一般会写一个简单的轮询循环超时设 1 秒。如果超时先看门铃寄存器的值是否真的写进去了用pread读回来再看命令队列的内存是否被固件修改过比如固件把status写成了0x1。这个排查过程很枯燥但 PRM 里通常有一节 “Command Interface Error Handling” 会列出常见错误原因。// 假设 cmdq 是命令队列基地址doorbell 是门铃寄存器映射地址 // 构造 QUERY_HCA_CAP 命令描述符 struct cmd_desc { uint16_t opcode; uint16_t opmod; uint16_t flags; uint16_t reserved; uint32_t inbox_addr_lo; uint32_t inbox_addr_hi; uint32_t outbox_addr_lo; uint32_t outbox_addr_hi; // ... 其他字段 }; void send_query_hca_cap(void *cmdq, volatile uint64_t *doorbell, uint64_t inbox_pa, uint64_t outbox_pa) { struct cmd_desc *desc (struct cmd_desc *)cmdq; desc-opcode 0x1; // QUERY_HCA_CAP具体值查 PRM desc-opmod 0x0; desc-flags 0; desc-inbox_addr_lo (uint32_t)(inbox_pa 0xffffffff); desc-inbox_addr_hi (uint32_t)(inbox_pa 32); desc-outbox_addr_lo (uint32_t)(outbox_pa 0xffffffff); desc-outbox_addr_hi (uint32_t)(outbox_pa 32); // 内存屏障确保描述符写入完成 __sync_synchronize(); // 写门铃生产索引加 1 *doorbell 1; }逻辑说明这段代码构造了一个命令描述符填入输入邮箱和输出邮箱的物理地址然后写门铃。参数上opcode和opmod必须查 PRM 第二册的命令表不同固件版本可能不同。inbox_pa和outbox_pa是 DMA 内存的物理地址或 IOVA。门铃写入的值是生产索引第一次发命令就是 1。注意写门铃后不要立刻读输出邮箱要等固件处理完通常通过轮询输出邮箱的status字段或等待完成事件。4. 避坑与排查PRM 第二册实操中最容易翻车的 5 个点4.1 现象命令队列门铃写了但固件没反应原因门铃寄存器的偏移搞错了或者写门铃前没有做内存屏障固件看到的是旧的描述符。还有一种可能是命令队列的物理地址没有按 PRM 要求的对齐通常是 4KB 对齐固件直接忽略。解决先用pread读回门铃寄存器的值确认写入生效。然后在写门铃前加sfencex86或dmb syARM。最后检查命令队列的物理地址低 12 位是否全零如果不是重新分配对齐内存。4.2 现象INIT_HCA 返回 0x1但不知道哪个字段错了原因PRM 里的错误码0x1是通用错误具体原因要看输出邮箱里的syndrome字段。很多人只读status就停了没往下看。解决把输出邮箱的整个 64 字节 dump 出来按 PRM 的 “INIT_HCA Output Mailbox” 表格逐字段解析。常见错误包括cmdq_addr没对齐、log_cmdq_size设成了非 2 的幂、fw_page_size和实际页大小不匹配。4.3 现象QP 迁移到 RTR 时报 local protection error原因rkey或lkey无效或者 MR 的访问权限没有包含REMOTE_WRITE。还有一种可能是path_mtu设得比交换机 MTU 大导致路径 MTU 发现失败。解决先确认 MR 是用IBV_ACCESS_REMOTE_WRITE注册的然后检查rkey是否从对端正确交换过来。path_mtu一般设 1024 或 4096但必须和交换机配置一致。如果交换机 MTU 是 1500你设 4096 就会在 RTR 阶段被拒绝。4.4 现象完成事件里 syndrome 显示 0x4但 WQE 长度看起来没错原因byte_count字段的单位是字节但有些 WQE 里用的是 “数量” 而不是字节数比如原子操作。如果你把原子操作的byte_count当成字节数填就会触发本地长度错误。解决查 PRM 里对应 opcode 的 WQE 格式确认byte_count的单位。原子操作的byte_count通常是 8表示 8 字节的原子数据。另外inline数据的 WQE 里byte_count包含内联部分不要重复计算。4.5 现象用户态直接写 BAR 后系统不稳定或重启原因没有隔离驱动内核驱动和用户态程序同时访问同一块 BAR导致寄存器状态冲突。或者写入了保留位触发了未定义行为。解决在用户态操作前先rmmod内核驱动或用 VFIO 把设备直通给用户态。写寄存器前先读一遍确认当前值只修改 PRM 里定义的位域保留位保持原值。如果必须在内核态做用readl/writel并加锁。5. 进阶用 PRM 第二册的计数器做微基准测试与性能归因PRM 第二册里通常有一章专门讲性能计数器Performance Counters包括端口计数器、QP 计数器、PCIe 计数器等。这些计数器是硬件自动累加的读出来就能知道丢包发生在哪一层。我一般会先读端口计数器里的rx_discards和tx_discards如果这两个在涨说明缓冲区不够或者 MTU 不匹配如果rx_errors在涨看物理层如果 QP 计数器里的out_of_sequence在涨说明网络乱序严重。读计数器的方法和发命令类似也是通过命令队列发QUERY_XXX_COUNTERS命令输出邮箱里返回一组 64 位值。下面是一个用ibv_query_port和ibv_query_qp的 verbs 层例子虽然 verbs 封装了底层但你可以对照 PRM 看它实际读了哪些寄存器。#include infiniband/verbs.h #include stdio.h void dump_port_counters(struct ibv_context *ctx, uint8_t port) { struct ibv_port_attr attr; if (ibv_query_port(ctx, port, attr)) { perror(query_port); return; } printf(state%d, max_mtu%d, active_mtu%d\n, attr.state, attr.max_mtu, attr.active_mtu); // 扩展计数器需要 ibv_query_port 的扩展版本或直接读 sysfs // 这里只演示基础属性 } void dump_qp_counters(struct ibv_qp *qp) { struct ibv_qp_attr attr; struct ibv_qp_init_attr init_attr; if (ibv_query_qp(qp, attr, IBV_QP_STATE | IBV_QP_PATH_MTU, init_attr)) { perror(query_qp); return; } printf(qp_state%d, path_mtu%d\n, attr.qp_state, attr.path_mtu); // 更细的计数器如 retry_cnt、rnr_retry 在 attr 里 printf(retry_cnt%d, rnr_retry%d\n, attr.retry_cnt, attr.rnr_retry); }逻辑说明ibv_query_port返回的active_mtu是实际协商的 MTU如果它比max_mtu小说明路径上有设备不支持大 MTU。ibv_query_qp返回的retry_cnt和rnr_retry是 QP 属性不是实时计数器但可以帮你确认配置是否符合预期。真正的硬件计数器在 sysfs 里比如/sys/class/infiniband/mlx5_0/ports/1/counters/下面有一堆文件每个文件对应 PRM 里的一个计数器。你可以写个脚本定期读这些文件然后和 PRM 的计数器表对照做性能归因。一个我常用的技巧是在跑ib_write_bw之前先cat一遍所有计数器跑完再cat一遍差值就是这次测试产生的流量和错误。如果port_rcv_errors涨了但port_rcv_data没怎么涨说明物理链路有问题如果port_xmit_discards涨了说明发送缓冲区满了需要调大tx_queue_size或降低发送速率。这些计数器在 PRM 第二册里都有定义包括每个计数器的位宽、溢出行为和清零方式。注意有些计数器是 32 位的高频率下会溢出读的时候要处理回绕。最后说一个我自己的习惯每次调完 QP 或改完 MTU先跑一个ibv_rc_ping确认基本连通再跑ib_write_bw看带宽最后读一遍计数器确认没有隐藏错误。这个顺序能帮你把问题隔离在最小的范围内不至于一上来就怀疑固件有 bug。希望帮到你。本文还有配套的精品资源点击获取