| 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 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.
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.
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)
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
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 │
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
| 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 |
| 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 |
| 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 |
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
- 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
Identify→MDTS→ 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.
| 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 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.
- PRP1 (bytes 24-31 of SQE): Points to the first physical page of data
- 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
- 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)
- 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.
The single biggest performance lever: one I/O SQ/CQ pair per CPU core, pinned to the local NUMA node.
// 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);
}| 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.
Create (NS Management) → Attach (NS Attachment) → Format (Admin Format NVM) → Use (I/O) → Detach → Delete
- 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).
// 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.
| 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 |
| 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 |
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.
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 |
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/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 |
#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);
}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);// 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);
}- Check the CQE Status — Before looking anywhere else, print the completion status
- Verify PRP alignment — 90% of silent data corruption is a PRP bug
- Check doorbell stride — CAP.DSTRD varies by platform. Wrong stride corrupts the wrong queue.
- Validate physical addresses — DMA uses physical addresses, not virtual. A V2P translation bug corrupts data silently.
- Enable verbose kernel logging —
echo 1 > /sys/module/nvme/parameters/trace_eventsfor kernel-level tracing
- 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)
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)