裸机环境下NVMe提交/完成队列PCIe C代码实现入门咨询
裸机NVMe队列管理与命令处理入门实现
前置基础说明
- 裸机环境需提前完成:
- 能直接访问PCIe配置空间,获取NVMe设备的BAR0(MMIO寄存器空间)物理地址
- 拥有一段物理连续、64字节对齐的内存区域(用于SQ/CQ,NVMe控制器通过DMA访问,不支持虚拟地址)
- 了解NVMe核心寄存器:
CC(控制器配置)、CQA(完成队列属性)、SQA(提交队列属性)、CQH/CQT(完成队列头/尾指针)、SQH/SQT(提交队列头/尾指针)
核心步骤与代码示例
1. 定义NVMe基础结构与寄存器
#include <stdint.h> // NVMe控制器MMIO寄存器偏移(基于BAR0) #define NVME_REG_CC 0x0000 // 控制器配置寄存器 #define NVME_REG_CAP 0x0008 // 控制器能力寄存器 #define NVME_REG_CQA 0x0030 // 完成队列属性寄存器 #define NVME_REG_SQA 0x0038 // 提交队列属性寄存器 #define NVME_REG_CQT 0x0040 // 完成队列尾指针 #define NVME_REG_SQT 0x0048 // 提交队列尾指针 #define NVME_REG_CSTS 0x001C // 控制器状态寄存器 // NVMe命令条目(SQ Entry,64字节) typedef struct { uint32_t cmd_op : 8; // 命令操作码(NOP命令是0x00) uint32_t fuse : 2; uint32_t resv0 : 6; uint32_t nsid : 16; // 命名空间ID(测试用0) uint32_t cdw1; uint64_t metadata; uint64_t prp1; // 物理区域指针(测试用0) uint64_t prp2; uint32_t cdw10; uint32_t cdw11; uint32_t cdw12; uint32_t cdw13; uint32_t cdw14; uint32_t cdw15; } nvme_sq_entry_t; // NVMe完成条目(CQ Entry,16字节) typedef struct { uint32_t dw0; uint32_t dw1; uint32_t dw2; uint32_t dw3; } nvme_cq_entry_t; // 全局变量(裸机环境需确保内存物理连续且对齐) volatile uint8_t *nvme_bar0 = (volatile uint8_t *)0xE0000000; // 假设BAR0物理地址 nvme_sq_entry_t *sq_base = (nvme_sq_entry_t *)0x10000000; // SQ物理基地址(64字节对齐) nvme_cq_entry_t *cq_base = (nvme_cq_entry_t *)0x10001000; // CQ物理基地址(16字节对齐) const uint16_t queue_size = 32; // 队列大小(必须是2的幂)
2. 控制器与队列初始化
void nvme_init() { // 1. 重置控制器(CC寄存器的EN位清零) uint32_t cc_val = *(volatile uint32_t*)(nvme_bar0 + NVME_REG_CC); cc_val &= ~(1 << 0); // 清除EN位 *(volatile uint32_t*)(nvme_bar0 + NVME_REG_CC) = cc_val; // 等待控制器进入非忙碌状态 while (*(volatile uint32_t*)(nvme_bar0 + NVME_REG_CSTS) & (1 << 1)); // 2. 配置完成队列(CQ) // CQA格式:[31:16]队列大小-1,[15:0]队列ID(测试用0) uint32_t cqa_val = ((queue_size - 1) << 16) | 0; *(volatile uint32_t*)(nvme_bar0 + NVME_REG_CQA) = cqa_val; // 设置CQ物理地址(低32位,64位系统需处理高32位) *(volatile uint64_t*)(nvme_bar0 + NVME_REG_CQA + 8) = (uint64_t)cq_base; // 3. 配置提交队列(SQ) // SQA格式:[31:16]队列大小-1,[15:0]队列ID(测试用0),关联CQ ID 0 uint32_t sqa_val = ((queue_size - 1) << 16) | 0; *(volatile uint32_t*)(nvme_bar0 + NVME_REG_SQA) = sqa_val; // 设置SQ物理地址(低32位) *(volatile uint64_t*)(nvme_bar0 + NVME_REG_SQA + 8) = (uint64_t)sq_base; // 4. 启用控制器(设置CC寄存器的EN位) cc_val |= (1 << 0); *(volatile uint32_t*)(nvme_bar0 + NVME_REG_CC) = cc_val; // 等待控制器就绪 while (!(*(volatile uint32_t*)(nvme_bar0 + NVME_REG_CSTS) & (1 << 0))); }
3. 构造NOP命令并提交
void nvme_submit_nop_cmd() { uint16_t sq_tail = *(volatile uint16_t*)(nvme_bar0 + NVME_REG_SQT); // 构造NOP命令(操作码0x00,最简单的测试命令) nvme_sq_entry_t *cmd = &sq_base[sq_tail]; cmd->cmd_op = 0x00; // NOP命令 cmd->nsid = 0; // 无命名空间 cmd->prp1 = 0; // 无数据传输 cmd->prp2 = 0; // 更新SQ尾指针,通知控制器有新命令 sq_tail = (sq_tail + 1) % queue_size; *(volatile uint16_t*)(nvme_bar0 + NVME_REG_SQT) = sq_tail; }
4. 轮询完成队列获取结果
int nvme_wait_completion() { uint16_t cq_head = *(volatile uint16_t*)(nvme_bar0 + NVME_REG_CQH); uint16_t cq_tail = *(volatile uint16_t*)(nvme_bar0 + NVME_REG_CQT); // 轮询等待CQ尾指针更新 while (cq_head == cq_tail); // 处理完成条目 nvme_cq_entry_t *completion = &cq_base[cq_head]; uint32_t status = (completion->dw3 >> 17) & 0x7FF; // 提取状态码 if (status == 0) { // 命令执行成功 return 0; } else { // 命令失败,裸机下可通过串口/LED提示 return -1; } // 更新CQ头指针,标记条目已处理 cq_head = (cq_head + 1) % queue_size; *(volatile uint16_t*)(nvme_bar0 + NVME_REG_CQH) = cq_head; }
5. 主流程示例
int main() { nvme_init(); nvme_submit_nop_cmd(); if (nvme_wait_completion() == 0) { // 裸机环境下通过串口/LED提示成功 } while (1); // 裸机循环 return 0; }
关键指引与注意事项
- 内存要求:SQ/CQ必须是物理连续内存,且SQ条目64字节对齐、CQ条目16字节对齐。裸机环境下可通过链接脚本预留固定内存区域,避免碎片化。
- 寄存器操作:所有NVMe寄存器访问必须加
volatile修饰,防止编译器优化导致的错误。64位地址寄存器在32位系统下需分两次写入高、低32位。 - 命令规范:示例用NOP命令(0x00)做入门测试,其他命令(如读/写)需严格按照NVMe 1.4规范填充PRP指针、命名空间ID等字段。
- 错误处理:示例仅做基础轮询,实际需检查
CSTS寄存器的错误位,以及完成条目中的状态码(参考NVMe规范第5.12节)。 - 中断替代轮询:若裸机环境支持中断,可配置NVMe控制器的中断寄存器,通过中断方式处理完成队列,减少CPU占用。
内容的提问来源于stack exchange,提问作者mpatch54
相关产品推荐
相关产品推荐

