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 偏移)

BAR0 (NVMe Controller Registers) CAP 0x00, 64bit VS 0x08, 32bit CC 0x14, 32bit CSTS 0x1C, 32bit CAP 关键字段: MQES[15:0] 队列深度 | CQR[16] 连续队列 | TO[31:24] 超时 DSTRD[35:32] 门铃步长 | MPSMIN[51:48] 最小页 | CSS[44:37] 命令集 CSTS 关键字段: RDY[0] 就绪 | CFS[1] 致命状态 | SHST[3:2] 关机状态

核心代码

#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

验证方法与通过条件

  1. 程序打印出 NVMe 控制器的 BDF 地址(如 0000:01:00.0)。
  2. CAP 的 MQES 字段非零(典型值 0x3FF 或 0x7FF),DSTRD 通常为 0 或 4。
  3. VS 字段高 16 位为 1(NVMe 1.x),低 8 位为次版本号。
  4. CC.EN 在控制器未启动时为 0;CSTS.RDY 同样为 0。
  5. 若 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 白名单配置。