3. NVMe控制器寄存器映射与访问
任务说明
本案例通过 SPDK 的 PCI 接口映射 NVMe 控制器的 BAR0 空间,读取 CAP、VS、CC、CSTS 四个核心寄存器,并解析打印关键字段。这是理解 NVMe 控制器初始化流程的基础。
环境与依赖
- SPDK 已编译(提供
spdk_pci.h、spdk_pci_nvme.h、spdk_pci_api.h) - 以 root 运行(需要访问
/sys/bus/pci与/dev/uio*或 VFIO) - 链接
libspdk_env_dpdk.a等 SPDK 库,或直接放入 SPDK 示例目录编译
寄存器布局(BAR0 偏移)
核心代码
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <spdk/pci.h>
#include <spdk/pci_nvme.h>
#include <spdk/env.h>
/* NVMe 控制器寄存器偏移 */
#define NVME_REG_CAP 0x00
#define NVME_REG_VS 0x08
#define NVME_REG_CC 0x14
#define NVME_REG_CSTS 0x1C
static void dump_cap(uint64_t cap)
{
printf(" CAP = 0x%016lx\n", cap);
printf(" MQES (max queue entries) = %u\n", (unsigned)(cap & 0xFFFF));
printf(" CQR (contiguous queues) = %u\n", (unsigned)((cap >> 16) & 1));
printf(" TO (timeout, 500ms) = %u\n", (unsigned)((cap >> 24) & 0xFF));
printf(" DSTRD (doorbell stride) = %u\n", (unsigned)((cap >> 32) & 0xF));
printf(" CSS (cmd sets supported)= 0x%x\n", (unsigned)((cap >> 37) & 0xFF));
printf(" MPSMIN(min page size) = %u\n", (unsigned)((cap >> 48) & 0xF));
}
static void dump_csts(uint32_t csts)
{
printf(" CSTS = 0x%08x\n", csts);
printf(" RDY (ready) = %u\n", csts & 1);
printf(" CFS (fatal status) = %u\n", (csts >> 1) & 1);
printf(" SHST (shutdown) = %u\n", (csts >> 2) & 3);
}
int main(void)
{
struct spdk_pci_device *pci_dev = NULL;
struct spdk_pci_addr addr;
struct spdk_pci_id nvme_id = { SPDK_PCI_CLASS_NVME, SPDK_PCI_ANY_ID,
SPDK_PCI_ANY_ID, SPDK_PCI_ANY_ID };
volatile uint32_t *bar0;
uint64_t cap;
uint32_t vs, cc, csts;
if (spdk_pci_addr_parse(&addr, "0000:00:00.0") != 0) {
fprintf(stderr, "addr parse failed\n");
return 1;
}
/* 遍历 PCI 总线,找到第一个 NVMe 控制器 */
while ((pci_dev = spdk_pci_get_next_device(pci_dev)) != NULL) {
if (spdk_pci_device_get_class(pci_dev) == SPDK_PCI_CLASS_NVME) {
break;
}
}
if (!pci_dev) {
fprintf(stderr, "no NVMe PCI device found\n");
return 1;
}
printf("NVMe controller: %04x:%02x:%02x.%x\n",
spdk_pci_device_get_domain(pci_dev),
spdk_pci_device_get_bus(pci_dev),
spdk_pci_device_get_dev(pci_dev),
spdk_pci_device_get_func(pci_dev));
/* 映射 BAR0 */
bar0 = spdk_pci_device_map_bar(pci_dev, 0, NULL);
if (!bar0) {
fprintf(stderr, "map BAR0 failed\n");
return 1;
}
/* 读取寄存器 */
cap = *(volatile uint64_t *)((char *)bar0 + NVME_REG_CAP);
vs = *(volatile uint32_t *)((char *)bar0 + NVME_REG_VS);
cc = *(volatile uint32_t *)((char *)bar0 + NVME_REG_CC);
csts = *(volatile uint32_t *)((char *)bar0 + NVME_REG_CSTS);
printf(" VS = 0x%08x (NVMe %u.%u)\n", vs,
(vs >> 16) & 0xFFFF, (vs >> 8) & 0xFF);
printf(" CC = 0x%08x\n", cc);
printf(" EN (enable) = %u\n", cc & 1);
printf(" CSS (cmd set) = %u\n", (cc >> 4) & 7);
printf(" MPS (page size) = %u\n", (cc >> 7) & 0xF);
printf(" IOSQES(sub q size)= %u\n", (cc >> 16) & 0xF);
printf(" IOCQES(cpl q size)= %u\n", (cc >> 20) & 0xF);
dump_cap(cap);
dump_csts(csts);
spdk_pci_device_unmap_bar(pci_dev, 0, (void *)bar0);
return 0;
}
编译与运行
# 放入 SPDK 示例目录(如 examples/nvme/regdump/)后:
gcc -o regdump regdump.c \
-I$SPDK_ROOT/include \
-L$SPDK_ROOT/build/lib \
-lspdk_env_dpdk -lspdk_util -lspdk_log \
-ldpdk -lnuma -lpthread -ldl
sudo ./regdump
验证方法与通过条件
- 程序打印出 NVMe 控制器的 BDF 地址(如
0000:01:00.0)。 - CAP 的 MQES 字段非零(典型值 0x3FF 或 0x7FF),DSTRD 通常为 0 或 4。
- VS 字段高 16 位为 1(NVMe 1.x),低 8 位为次版本号。
- CC.EN 在控制器未启动时为 0;CSTS.RDY 同样为 0。
- 若
spdk_pci_get_next_device返回 NULL,说明当前环境未绑定 NVMe 设备到 SPDK 可访问的驱动(需vfio-pci或uio_pci_generic)。
常见问题
- map BAR0 失败:确认设备已绑定到 VFIO/UIO,且以 root 运行。
- 读到的 CAP 全 0xFF:BAR 映射地址无效,检查
spdk_pci_device_map_bar返回值。 - 找不到设备:用
lspci -nn | grep -i nvme确认系统存在 NVMe 控制器,并检查 SPDK 的 PCI 白名单配置。