Skip to content

Latest commit

 

History

History
535 lines (427 loc) · 29.3 KB

File metadata and controls

535 lines (427 loc) · 29.3 KB
name nvme-programming
description NVMe 应用层高性能编程 — 协议核心、命令构造、多队列配置、性能调优、namespace 管理。 Use when writing, debugging, or tuning NVMe storage applications, constructing NVMe commands, configuring multi-queue SSDs, managing namespaces, or diagnosing NVMe performance bottlenecks. Covers Linux NVMe ioctl interface, PRP/SGL data structures, Admin & NVM command sets, interrupt coalescing, NUMA affinity, queue depth tuning. Triggers on: NVMe, SSD, nvme, namespace, PRP, SGL, submission queue, completion queue, SQ, CQ, doorbell, SPDK, libnvme, NVMe-oF, 队列深度, 中断合并, 多队列, ZNS.

NVMe 应用层高性能存储编程

Core Philosophy

NVMe is a queue protocol. Everything flows through Submission Queues (SQ) and Completion Queues (CQ). Understand this model before anything else — it's the foundation that all performance tuning builds on.

The hardware is fast. Your software is the bottleneck. Modern NVMe SSDs can saturate PCIe bandwidth. Performance problems are almost always in: queue configuration, interrupt handling, memory mapping, or data structure layout.

Measure with real hardware. NVMe performance is deeply coupled to the specific SSD controller, firmware, and PCIe topology. Benchmarks on one drive do not generalize. Always profile on the target hardware.

PRP is not optional knowledge. Physical Region Page (PRP) is the most common source of silent bugs in NVMe I/O — misunderstanding PRP layout causes data corruption that manifests far from the root cause.

NVMe Architecture: 30-Second Primer

Host Software
    │
    ├── Admin SQ ──► Controller ──► Admin CQ
    │   (1 pair, mandatory)         (management commands)
    │
    ├── I/O SQ 0 ──► Controller ──► I/O CQ 0
    ├── I/O SQ 1 ──► Controller ──► I/O CQ 1
    │   ...                              ...
    └── I/O SQ N ──► Controller ──► I/O CQ N
        (up to 64K pairs)              (data commands)

Command lifecycle:
  1. Host writes SQE (64 bytes) into SQ in host memory
  2. Host writes SQ Tail Doorbell → Controller MMIO register
  3. Controller fetches SQE via PCIe DMA
  4. Controller executes command, transfers data
  5. Controller writes CQE (16 bytes) into CQ in host memory
  6. Controller sends MSI-X interrupt (or host polls CQ)
  7. Host writes CQ Head Doorbell → Controller MMIO register

Key insight: The doorbell is the synchronization primitive. Until you ring the SQ doorbell, the controller sees nothing. Until you ring the CQ doorbell, the slot is not freed.

Command Construction: The Core Workflow

1. Controller Initialization

1. PCIe enumeration (B/D/F)
2. Map BAR0 (Controller Registers)
3. Controller reset (CC.EN = 0 → wait CSTS.RDY = 0 → set CC.EN = 1 → wait CSTS.RDY = 1)
4. Create Admin SQ/CQ (via AQA, ASQ, ACQ registers)
5. Identify Controller (Admin Opcode 0x06, CNS=1) → learn capabilities
6. Set Features: Number of Queues (Feature ID 0x07)
7. Create I/O SQ/CQ pairs (Admin Opcode 0x01)

2. Constructing and Submitting a Command

Every command follows this pattern:

1. Allocate a free slot in SQ (check SQ Head vs Tail)
2. Write SQE (64 bytes) into the slot
3. Memory fence (ensures SQE visible to controller before doorbell)
4. Write new SQ Tail value to SQ Tail Doorbell register
5. Wait for completion:
   a. Poll CQ: read CQ Phase Tag bit, or
   b. Interrupt: wait for MSI-X, then process CQ
6. Read CQE from CQ slot
7. Update CQ Head Doorbell to free the slot

3. SQE Format (Submission Queue Entry — 64 bytes)

Byte 0:   CDW0  ┌──────────────┬──────────────┐
                 │   Opcode (8) │  FUSE (2)   │  PRP/SGL (2) │  Reserved (4)  │
                 ├──────────────┴──────────────┤
Byte 4:   CDW1  │         Reserved             │
Byte 8:   CDW2  │         Reserved             │
Byte 12:  CDW3  │         Reserved             │
Byte 16:  CDW4  │      Namespace ID (NSID)     │
Byte 20:  CDW5  │         Reserved             │
Byte 24:  CDW6  │  Data Pointer: PRP Entry 1 / SGL Segment  │
Byte 32:  CDW7  │  Data Pointer: PRP Entry 2 / SGL Segment  │
Byte 40:  CDW8  │      Metadata Pointer         │  ← NVM Write: Starting LBA
Byte 48:  CDW9  │      Metadata Pointer         │  ← NVM Write: Starting LBA (upper)
Byte 52:  CDW10 │  Command-specific Dword 10    │  ← NVM Write: Number of Logical Blocks
Byte 56:  CDW11 │  Command-specific Dword 11    │  ← Dataset Management / Directive
Byte 60:  CDW12 │  Command-specific Dword 12    │
Byte 64:  CDW13 │  Command-specific Dword 13    │
Byte 68:  CDW14 │  Command-specific Dword 14    │
Byte 72:  CDW15 │  Command-specific Dword 15    │

4. CQE Format (Completion Queue Entry — 16 bytes)

Byte 0:   CDW0   Command-specific result
Byte 4:   CDW1   Reserved
Byte 8:   CDW2   SQ Identifier (SQID)        | SQ Head Pointer
Byte 12:  CDW3   Status Field:
           Bit 15: Do Not Retry (DNR)
           Bit 14: More (M)
           Bit 13: Command Retry Delay (CRD)
           Bits 9-12: Status Code Type (SCT)
           Bits 1-8:  Status Code (SC)
           Bit 0:  Phase Tag (P) — toggled each pass through the CQ

Key Command Reference

Admin Commands (Submission Queue 0, Opcodes 0x00-0x1F)

Opcode Name Description Performance Impact
0x00 Delete I/O SQ Delete an I/O Submission Queue Cleanup only
0x01 Create I/O SQ Create an I/O Submission Queue Sets queue depth
0x02 Get Log Page Retrieve log data (SMART, errors, etc.) Monitoring tool
0x04 Delete I/O CQ Delete an I/O Completion Queue Cleanup only
0x05 Create I/O CQ Create an I/O Completion Queue Sets interrupt vector
0x06 Identify Retrieve controller/namespace data structures Critical for tuning
0x08 Abort Abort a previously submitted command Error recovery
0x09 Set Features Configure controller features Critical for tuning
0x0A Get Features Read controller feature settings Verification
0x0C Asynchronous Event Request Register for async events Monitoring
0x11 Namespace Management Create/Delete namespaces Provisioning
0x15 Namespace Attachment Attach/detach namespaces to controllers Provisioning
0x18 Keep Alive Keep NVMe-oF connection alive NVMe-oF only
0x19 Directive Send Send directive to controller Advanced
0x1A Directive Receive Receive directive from controller Advanced

NVM I/O Commands (Opcode 0x00-0x03 in I/O SQ)

Opcode Name Key Dwords Description
0x00 Flush Flush volatile write cache to media
0x01 Write Dword 10-11: SLBA, Dword 12: NLB Write data to namespace
0x02 Read Dword 10-11: SLBA, Dword 12: NLB Read data from namespace
0x04 Write Uncorrectable Dword 10-11: SLBA, Dword 12: NLB Mark blocks as uncorrectable
0x05 Compare Dword 10-11: SLBA, Dword 12: NLB Compare data without transfer
0x08 Write Zeroes Dword 10-11: SLBA, Dword 12: NLB Zero-fill blocks
0x09 Dataset Management Dword 10-11: Attributes Deallocate/TRIM hint
0x0D Verify Dword 10-11: SLBA, Dword 12: NLB Verify data integrity
0x0E Reservation Register Persistent reservation
0x0F Reservation Report Query reservations
0x11 Reservation Acquire Acquire reservation
0x15 Reservation Release Release reservation

Critical Features (Set/Get Features, Admin Opcodes 0x09/0x0A)

Feature ID Name Tuning Relevance
0x01 Arbitration Medium — RR vs WRR with Urgent Priority Class
0x02 Power Management Low — power state transitions
0x04 Temperature Threshold Low — thermal throttling config
0x05 Error Recovery Medium — timeout values
0x07 Number of Queues Critical — sets max I/O SQ/CQ count
0x08 Interrupt Coalescing Critical — Aggregation Time/Threshold tuning
0x09 Interrupt Vector Config Medium — per-vector interrupt settings
0x0A Write Atomicity Normal High — ensures atomic write boundary
0x0B Async Event Config Low — event notification setup
0x0C Autonomous Power State Transition Low — idle power management
0x0D Host Memory Buffer Medium — host memory exposed to controller
0x10 Keep Alive Timer Medium — NVMe-oF keep-alive interval
0x14 Host Controlled Thermal Management Low — host-side thermal control
0x16 Submission Queue Arbitration Medium — per-SQ priority

Performance Tuning: The Complete Checklist

Before You Start: Know Your Hardware

Always begin with Identify Controller (CNS=1). Extract these fields:

- MDTS (Max Data Transfer Size): maximum I/O size
- NN (Number of Namespaces)
- SQES/CQES (Required Entry Sizes): typically 64/16 bytes
- OACS (Optional Admin Command Support): NS management, firmware, etc.
- ONCS (Optional NVM Command Support): Compare, Dataset Mgmt, Write Zeroes, etc.
- AWUN/NAWUN (Atomic Write Unit): must respect for data integrity
- MAXCMD (Maximum Commands): maximum outstanding commands
- VWC (Volatile Write Cache): if present, Flush is needed for durability

Performance Tuning Checklist (ordered by impact)

  • Queue Depth — Is your I/O queue depth sufficient to keep the SSD busy? Rule of thumb: start at 64, test upward. SSDs with high queue depth (256+) often show 2-3x throughput improvement.
  • Number of I/O Queue Pairs — Match to CPU cores (1:1 or 1:N). Each core should submit to a dedicated SQ. Contention on SQ Tail doorbell kills performance.
  • Interrupt Coalescing (Feature ID 0x08) — Balance latency vs throughput:
    • Aggregation Time (TIME): 0 = disabled (lowest latency, highest CPU). 100-200 µs typical for throughput.
    • Aggregation Threshold (THR): 0 = disabled. 8-16 typical. Higher = fewer interrupts = less CPU.
  • NUMA Affinity — I/O SQ memory and data buffers must be on the NUMA node closest to the PCIe slot. Cross-NUMA DMA adds 30-50% latency.
  • PRP Layout — Every PRP list page must be physically contiguous and 4KB-aligned. A misaligned PRP causes silent data corruption. Always validate with IdentifyMDTS → PRP calculation.
  • I/O Size Alignment — Align I/O to the namespace's LBA format (typically 4KB). Sub-LBA-size I/O incurs read-modify-write penalty. Use the largest I/O size ≤ MDTS.
  • Write Cache Awareness — If VWC=1, use Flush for durability. For throughput, batch Flush operations — one Flush after N writes, not one per write.
  • Arbitration Mechanism (Feature ID 0x01) — WRR (Weighted Round Robin) with Urgent Priority Class for mixed latency-sensitive + throughput workloads. Default RR is fine for homogeneous workloads.
  • SQ Memory Alignment — SQ must be physically contiguous memory. CQ must be physically contiguous. Both must align to page size (4KB).
  • Doorbell Stride — Doorbell registers are at BAR0 + 0x1000 + (QueueID * (4 << DSTRD)). Wrong stride = wrong queue = corruption. Always read CAP.DSTRD.
  • MSI-X Vector Assignment — Assign different I/O CQs to different MSI-X vectors for per-core interrupt handling. One vector for all CQs creates a single CPU bottleneck.

Common Performance Traps

Symptom Likely Cause Investigation Fix
Throughput plateaus at low QD Queue depth too shallow Increase SQ depth, check MAXCMD Raise queue entries to 256+
High CPU usage, moderate IOPS Interrupt storm (coalescing off) Check interrupt rate via /proc/interrupts Enable interrupt coalescing (TIME=100, THR=8)
Latency spikes on specific cores Cross-NUMA DMA Check NUMA topology: numactl --hardware Bind SQ to PCIe-local NUMA node
Silent data corruption PRP misalignment or overflow Verify PRP list pages are 4KB-aligned Fix PRP calculation, add alignment asserts
I/O stalls intermittently SQ doorbell contention Check if multiple threads share one SQ Dedicated SQ per thread
Write throughput far below read Volatile write cache + missing Flush Check VWC in Identify Batch Flush, or disable VWC for direct writes
Queue creation fails Exceeded MAXCMD or queue count Read Identify: Number of Queues Reduce queue size or count
CQ overrun (lost completions) CQ phase tag logic bug Check phase tag tracking Initialize CQE.P=1, flip at wrap
Low performance on small I/O Sub-LBA I/O causing RMW Check LBA format (FLBAS, LBAF) Align I/O to LBA Data Size
Controller reset timeout CSTS.RDY not checked properly Poll loop missing delay or max retries Add timeout with 500ms delay between checks

PRP: Physical Region Page — The Critical Data Structure

PRP is the most error-prone part of NVMe programming. Every data buffer referenced in a command uses either PRP or SGL. PRP is mandatory for Admin commands and the default for I/O.

PRP Rules (get these wrong = data corruption)

  1. PRP1 (bytes 24-31 of SQE): Points to the first physical page of data
  2. PRP2 (bytes 32-39 of SQE):
    • If data fits in 1 page → PRP2 = 0 (unused)
    • If data fits in 2 pages → PRP2 = physical address of 2nd page
    • If data spans > 2 pages → PRP2 = physical address of a PRP List page
  3. PRP List: A 4KB-aligned page containing an array of PRP entries (8 bytes each, up to 506 entries per page).
    • All entries except the last point to data pages (not other PRP lists)
    • The last entry is either: (a) a pointer to the next PRP List page (if more data pages follow), or (b) unused (0x0) if this is the final list page
    • Maximum entries per list page: 506 (4096/8 − 1 slot reserved for continuation pointer)
  4. Alignment: First data byte may have any offset within PRP1's page. All subsequent pages must be page-size aligned.
PRP Layout Examples:

[≤ 1 page]:  PRP1 → [Data Page 0]  PRP2 = 0

[≤ 2 pages]: PRP1 → [Data Page 0]
             PRP2 → [Data Page 1]

[> 2 pages]: PRP1 → [Data Page 0]
             PRP2 → [PRP List Page]
                       ├── PRP entry → [Data Page 1]
                       ├── PRP entry → [Data Page 2]
                       ├── ...
                       └── PRP entry → [Data Page N]

Critical gotcha: The PRP List itself must be a physically contiguous, 4KB-aligned single page. Only the LAST entry in a PRP list points to the next PRP list page — all other entries MUST point to data pages. Violating this rule causes silent data corruption.

See references/data-structures.md for complete PRP and SGL reference with code.

Multi-Queue Configuration: The Pattern

The single biggest performance lever: one I/O SQ/CQ pair per CPU core, pinned to the local NUMA node.

Recommended Configuration

// Pseudocode for optimal multi-queue setup
struct nvme_queue_pair {
    void *sq_mem;          // physically contiguous, 4KB-aligned
    void *cq_mem;          // physically contiguous, 4KB-aligned
    uint32_t sq_head;      // maintained by host
    uint32_t sq_tail;      // maintained by host
    uint32_t cq_head;      // maintained by host
    uint32_t sq_tail_db;   // doorbell address (MMIO)
    uint32_t cq_head_db;   // doorbell address (MMIO)
    uint16_t qid;          // queue ID
    uint16_t qsize;         // number of entries
    uint8_t  phase;         // current expected CQE phase tag
    int      cpu;           // bound CPU core
    int      numa_node;     // NUMA node
};

// Initialization
for (int i = 0; i < num_cores; i++) {
    int numa = numa_node_of_cpu(i);
    struct nvme_queue_pair *qp = &queues[i];

    // Allocate SQ/CQ memory on local NUMA node
    qp->sq_mem = numa_alloc_onnode(2 * qsize * 64, numa);  // 64-byte SQE
    qp->cq_mem = numa_alloc_onnode(2 * qsize * 16, numa);  // 16-byte CQE
    qp->qsize = qsize;
    qp->cpu = i;
    qp->numa_node = numa;

    // Create via Admin command
    nvme_create_io_cq(qp->cq_mem, qp->qsize, interrupt_vector[i]);
    nvme_create_io_sq(qp->sq_mem, qp->qsize, cq_id, qp->qid);
}

Queue Depth Guidelines

Workload Recommended QD Reasoning
Latency-sensitive (single-thread) 1-16 Minimize queuing delay
Throughput (sequential read) 32-128 Keep pipeline full
Throughput (random read) 128-512 Maximize device parallelism
Mixed R/W 256+ Allow read priority via arbitration
Extreme throughput (multi-SSD) Per-device max Bound by MAXCMD

Rule of thumb: Increase QD until throughput stops improving. Going beyond that only increases latency.

See references/queue-management.md and references/performance-tuning.md for deep dives.

Namespace Management

Namespace Lifecycle

Create (NS Management) → Attach (NS Attachment) → Format (Admin Format NVM) → Use (I/O) → Detach → Delete

Key Concepts

  • Namespace: A logical block storage unit. One controller can expose multiple namespaces.
  • LBA Format: Defines block size (512B, 4KB, etc.) and metadata size. Choose during Format.
  • NSID: Namespace ID (1-based). NSID=0xFFFFFFFF means "all namespaces" for some commands.
  • Identify Namespace (CNS=0): Returns namespace-specific capabilities (capacity, LBA formats, features).

Common Namespace Operations

// Create namespace
nvme_admin_ns_mgmt_create(total_size, lba_format_index);

// Attach to controller
nvme_admin_ns_attach(nsid, controller_id);

// Format (choose LBA format)
nvme_admin_format_nvm(nsid, lba_format_index);

// Identify to verify
struct nvme_id_ns ns_data;
nvme_admin_identify(nsid, CNS_NAMESPACE, &ns_data);

See references/namespace-mgmt.md for the full reference.

Error Handling

CQE Status Code Types (SCT)

SCT Name Handling
0x0 Generic Command Status Check SC for details
0x1 Command Specific Status Command-specific error
0x2 Media and Data Integrity Errors Data corruption — retry or fail
0x3 Path Related Status NVMe-oF path error — retry with failover

Common Status Codes (SC, SCT=0)

SC Name Typical Cause Fix
0x00 Successful Completion
0x01 Command ID Conflict Two outstanding commands with same CID Increment CID properly
0x02 Invalid Command Opcode Opcode not supported for this queue type Check opcode validity
0x03 Invalid Field in Command Bad PRP offset, unsupported feature value Validate command fields
0x04 Command ID Conflict (legacy) Duplicate CID (pre-2.3 naming) Increment CID properly
0x05 Data Transfer Error (PCIe) PCIe DMA failure Retry, check PCIe link
0x06 Internal Error Controller internal failure Reset controller
0x07 Command Abort Requested Command aborted by Abort command Reissue if needed
0x08 Command Aborted (SQ Delete) Command aborted due to SQ deletion Reissue on new queue
0x0C Invalid Namespace or Format NSID doesn't exist, wrong format Verify NSID, Format NVM
0x0D Command Sequence Error Admin commands executed out of order Fix command ordering
0x14 PRP Offset Invalid PRP1 has invalid offset or misalignment Fix PRP1 alignment
0x82 LBA Out of Range Read/Write beyond NS capacity Validate LBA range
0x84 Conflicting Attributes Incompatible parameters Fix command fields

Error Recovery Flow

1. Check CQE.SCT and CQE.SC
2. If SCT=0, SC=0: success, process normally
3. If DNR bit set: do NOT retry — error is non-recoverable
4. If CRD bits non-zero: wait the specified delay before retry
5. For transient errors (0x04, 0x06): retry up to N times
6. For fatal errors (0x0D): log and fail the operation
7. If Phase Tag mismatch: CQ overrun — check CQ head tracking

See references/error-handling.md for complete error reference.

Reference Files

Detailed reference material is organized by topic — load the relevant file when going deep:

File Use When
references/architecture.md Understanding NVMe controller model, registers, reset flow
references/command-sets.md Looking up Admin/NVM command opcodes, fields, and usage patterns
references/data-structures.md Working with PRP, SGL, SQE, CQE — field-level detail
references/queue-management.md Creating/deleting queues, doorbell mechanics, multi-queue patterns
references/namespace-mgmt.md Managing namespaces: create, delete, attach, format
references/performance-tuning.md Tuning interrupt coalescing, queue depth, NUMA, arbitration
references/error-handling.md Interpreting CQE errors, recovery strategies

Search Patterns

When you have the full NVMe Base Spec downloaded as searchable text (e.g., in references/base-spec-docs/), use grep to find specific details:

# Find PRP rules
grep -r "PRP Entry\|PRP List\|Physical Region Page" references/base-spec-docs/

# Find Identify command data structures
grep -r "Identify Controller Data Structure\|ID_CTRL\|Identify Namespace" references/base-spec-docs/

# Find specific feature
grep -r "Interrupt Coalescing\|Feature Identifier 0x08" references/base-spec-docs/

# Find error codes
grep -r "Status Code Type\|Generic Command Status" references/base-spec-docs/

# Find queue creation
grep -r "Create I/O Submission Queue\|Create I/O Completion Queue" references/base-spec-docs/

Code Examples

Ready-to-compile C examples using Linux NVMe ioctl interface:

File Demonstrates
examples/admin-identify.c Identify Controller & Namespace, parse capability structures
examples/nvm-read-write.c Full NVM Read/Write with PRP construction
examples/multi-queue-setup.c Create N I/O SQ/CQ pairs with CPU/NUMA binding
examples/perf-tuning-example.c Set interrupt coalescing, arbitration, number of queues

Building Blocks: Quick Code Snippets

NVMe Command via Linux ioctl

#include <linux/nvme_ioctl.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int nvme_submit_admin(int fd, struct nvme_admin_cmd *cmd) {
    return ioctl(fd, NVME_IOCTL_ADMIN_CMD, cmd);
}

int nvme_submit_io(int fd, struct nvme_passthru_cmd *cmd) {
    return ioctl(fd, NVME_IOCTL_IO_CMD, cmd);
}

Identify Controller

struct nvme_admin_cmd cmd = {
    .opcode = 0x06,       // Identify
    .nsid = 0,            // Not namespace-specific
    .addr = (uint64_t)identify_buffer,  // physically contiguous DMA buffer
    .data_len = 4096,
    .cdw10 = 1,           // CNS=1: Identify Controller
};
ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd);

Ringing the Doorbell (when using direct MMIO, not ioctl)

// SQ Tail Doorbell write — stride depends on CAP.DSTRD (stride = 4 << DSTRD)
static inline void nvme_sq_tail_doorbell(volatile uint32_t *db_base,
                                          uint32_t stride, uint16_t qid, uint16_t tail) {
    // Make sure all SQE writes are visible before the doorbell
    __sync_synchronize();  // full memory barrier
    // Doorbell offset: (2 * qid) * stride. For DSTRD=0 (stride=4), this is qid*8 bytes.
    uint32_t offset = (2 * (uint32_t)qid * stride) / sizeof(uint32_t);
    writel(tail, db_base + offset);
}

// CQ Head Doorbell write
static inline void nvme_cq_head_doorbell(volatile uint32_t *db_base,
                                          uint32_t stride, uint16_t qid, uint16_t head) {
    uint32_t offset = ((2 * (uint32_t)qid + 1) * stride) / sizeof(uint32_t);
    writel(head, db_base + offset);
}

Debugging NVMe Issues

First Response to a Bug

  1. Check the CQE Status — Before looking anywhere else, print the completion status
  2. Verify PRP alignment — 90% of silent data corruption is a PRP bug
  3. Check doorbell stride — CAP.DSTRD varies by platform. Wrong stride corrupts the wrong queue.
  4. Validate physical addresses — DMA uses physical addresses, not virtual. A V2P translation bug corrupts data silently.
  5. Enable verbose kernel loggingecho 1 > /sys/module/nvme/parameters/trace_events for kernel-level tracing

Checklist for "Command Returns Error"

  • Is the opcode valid for this queue type? (Admin commands only on SQ0)
  • Is NSID valid and namespace attached?
  • Does LBA + NLB stay within namespace capacity?
  • Is the data buffer physically contiguous for DMA?
  • Is PRP1 pointing to a valid physical address?
  • Are PRP entries properly aligned (page alignment for list pages)?
  • Does the transfer size exceed MDTS?
  • Is the SQ full? (SQ Tail + 1 == SQ Head after wraparound)

Quick Reference Card

NVMe Registers (BAR0):
  CAP    (0x00):  Controller Capabilities — MQES, DSTRD, CSS, etc.
  VS     (0x08):  Version
  CC     (0x14):  Controller Configuration — EN, I/O SQES, CQES, etc.
  CSTS   (0x1C):  Controller Status — RDY, etc.
  AQA    (0x24):  Admin Queue Attributes — ASQS, ACQS
  ASQ    (0x28):  Admin SQ Base Address (physical)
  ACQ    (0x30):  Admin CQ Base Address (physical)

Doorbell Registers (offset from BAR0 depends on CAP.DSTRD):
  SQ0TDBL (0x1000 + 0 * stride): Admin SQ Tail Doorbell
  CQ0HDBL (0x1000 + 1 * stride): Admin CQ Head Doorbell
  SQnTDBL (0x1000 + (2*n + 0) * stride): I/O SQ n Tail Doorbell
  CQnHDBL (0x1000 + (2*n + 1) * stride): I/O CQ n Head Doorbell

Stride = 4 << CAP.DSTRD  (typically 4 or 16 bytes)

SQE = 64 bytes, CQE = 16 bytes (typical; verify via Identify)