自制工业控制系统:NVMe固态硬盘驱动实战——队列、PRP与块设备抽象
Contents
起因
固态盘驱动是基础篇的倒数第二块。NVMe 名声吓人,裸机视角其实是最干净的存储协议:没有端口 IO、没有 ATAPI 历史包袱,一切都是 MMIO 寄存器和内存队列。干净不是偶然:SATA 时代的命令协议 AHCI 是 2004 年照着机械硬盘设计的,单命令队列、逐条排队,闪存根本吃不满;2011 年 Intel 牵头攒起 NVMe 联盟,直接按 SSD 的并行性重新设计——多队列、深队列、寄存器直达,机械时代的包袱一样没背。这篇从 BAR0 一直写到块设备抽象,寄存器表和命令格式全给。
一、控制器寄存器(BAR0 起)
PCIe 枚举找到 NVMe 控制器(类码 0x010802)后,BAR0 是一段寄存器窗口:
| 偏移 | 寄存器 | 关键字段 |
|---|---|---|
| 0x00 | CAP | bits0-15 MQES(队列最大深度);bits24-31 TO(就绪超时,单位 500ms);bits32-35 DSTRD(门铃步长) |
| 0x08 | VS | 规范版本 |
| 0x14 | CC | bit0 EN 使能;CSS/MPS/IOSQES/IOCQES 位域见规范 §3.1.8 |
| 0x1C | CSTS | bit0 RDY 就绪;bit1 CFS 致命错误 |
| 0x24 | AQA | 管理队列深度(提交 bits0-11、完成 bits16-27,写"深度-1") |
| 0x28 / 0x30 | ASQ / ACQ | 管理提交/完成队列基址(4KB 对齐、物理连续) |
| 0x1000+ | 门铃 | 每队列一对:SQ tail、CQ head;步长 = 4 « DSTRD |
二、初始化序列(顺序错了全是坑)
|
|
第 7、8 步的顺序不能反:先建完成队列,提交队列才能引用它。PC 位(physically contiguous)= 我们全用连续内存,走最简路径。
三、命令提交与完成
命令格式(64 字节,提交队列一项):cdw0 = opcode + 标志;cdw1 = NSID(IO 命令必填);cdw6-7 = PRP1/PRP2(数据指针);cdw10-15 = 命令专属字段。
常用 opcode:管理队列 Identify=0x06、建 IO CQ=0x05、建 IO SQ=0x01;IO 队列 Read=0x02、Write=0x01、Flush=0x00。
IO 读写命令的专属字段:cdw10-11 = 起始 LBA(64 位);cdw12 低 16 位 = 块数-1。
飞行流程:
|
|
完成队列项的关键是 phase 位(dw0 bit0):控制器每写一圈翻转一次极性,软件靠它区分"新完成"和"上一圈的旧数据"——忘了判 phase 会被旧数据坑死。
四、PRP:数据指针的规则
MPS=0(4KB 页)下的规矩:PRP1 指向第一页(可以带页内偏移);数据跨两页以内,PRP2 直接给第二页地址;跨三页以上,PRP2 指向一张 PRP 列表(8 字节一項的页地址数组,除第一项外必须页对齐)。
工控负载的简化决策:单命令传输上限压到 8KB(两页),更大的读写由上层切块——PRP 列表这条分支直接不写,代码少一半,性能对 G 代码文件这种量级毫无影响。真要跑大吞吐(比如以后做整盘备份),再回头补列表分支,接口不用动。
五、中断模型与块设备抽象
中断走 MSI(PCIe 篇已接线),向量在设备区(0x20+)。ISR 里只做一件事:reap 完成队列、把完成事件塞环形缓冲给协作层。同步接口(加载配置、读程序文件时用)就在提交后带超时轮询完成——单任务同步模型简单可靠;真到需要异步流水线的那天再演进。
对上层暴露统一的块设备接口:
|
|
这个 trait 是存储层的"协议":NVMe 实现它,后面的 U 盘(MSC+SCSI)也实现它,文件系统只认接口不认硬件——换介质不改上层。
运行期还要盯一个位:CSTS.CFS(bit1,控制器致命状态)。置位说明控制器内部出事了(过热、固件异常),IO 靠超时兜底发现,恢复路径是标准三步:CC.EN=0 → 轮询 CSTS.RDY=0 → 从管理队列开始整套重初始化。工控盘上这辈子里最好一次都别走到,但代码里必须有——走到的时候没有,就是数据静默丢失。
掉电策略提前立规矩:控制器写缓存默认开启(性能好但掉电丢数据),参数修改和加工完成节点必须 Flush;整机掉电保护(UPS/超级电容)是硬件侧设计,软件侧保证"flush 过的数据一定在盘上"。
总结
实战清单:CAP 读 DSTRD/TO、管理队列 4KB 对齐建好、CC 的 MPS/IOSQES/IOCQES 三个尺寸位设对再 EN、带超时等 RDY、Identify 拿容量和 LBA 大小、先建 IO CQ 再建 IO SQ、命令的 cdw 布局、门铃敲法、phase 位判完成、CFS 致命位的重初始化路径、单命令 8KB 上限绕开 PRP 列表、BlockDevice trait 收口、关键节点 Flush。跑通标志:QEMU -drive file=test.img,if=none,id=nvme -device nvme 下 Identify 出容量,读写 LBA0 回读一致。下一篇:U 盘驱动——MSC+SCSI 把 USB 存储也接进同一个 BlockDevice,然后是 exFAT 文件系统。
参考链接
- NVM Express 规范——寄存器/命令/PRP 的权威定义
- OSDev Wiki: NVMe
- 本系列 PCIe 篇:《PCIe总线枚举实战》(设备发现、BAR、MSI、DMA 池)
- 本系列第一篇:自制工业控制系统:从UEFI到内核
Author 软件开发大郭
LastMod 2026-09-11