3. NVMe控制器初始化:控制器复位流程、寄存器映射、AQA与ASQ/ACQ配置、CC.EN使能
好,咱们进入NVMe控制器初始化的核心环节。说实话,这部分是很多工程师容易翻车的地方。我见过不少项目,硬件连线没问题,驱动代码也写了,但就是死活枚举不上——十有八九是复位流程没走对,或者队列配置出了岔子。
今天咱们就把这块彻底捋清楚。从复位开始,到寄存器映射,再到Admin队列的配置,最后使能控制器。每一步我都会结合我踩过的坑来讲。
3.1 控制器复位流程
NVMe控制器的复位,说白了就是让芯片回到一个已知的初始状态。你想想看,一个刚从电源域上电的设备,内部状态机可能是乱的,寄存器值可能是随机的。不先复位一下,后面没法干活。
复位分两种:
- 功能复位(Function Level Reset):通过PCIe配置空间的寄存器触发,不影响链路层。
- 控制器复位(Controller Reset):通过NVMe寄存器CC.EN写0触发,这是咱们最常用的。
我个人习惯用控制器复位。流程是这样的:
- 读取CSTS.RDY位,确认控制器当前处于就绪状态。
- 向CC.EN写入0,发起复位。
- 轮询CSTS.RDY,等待它变为0,表示复位完成。
这里有个小技巧:复位期间不要访问任何NVMe寄存器,除了CSTS。否则可能会触发未定义行为。嗯,这是spec里明确写的,但很多人会忽略。
3.2 寄存器映射
NVMe的寄存器映射,是通过PCIe的BAR空间暴露给软件的。每个NVMe控制器至少有一个BAR0,里面放着所有控制寄存器。
映射结构大致如下:
| 偏移地址 | 寄存器名称 | 大小 | 描述 |
|---|---|---|---|
| 0x00 | CAP | 64-bit | 控制器能力寄存器 |
| 0x08 | VS | 32-bit | 版本寄存器 |
| 0x0C | INTMS | 32-bit | 中断掩码设置 |
| 0x10 | INTMC | 32-bit | 中断掩码清除 |
| 0x14 | CC | 32-bit | 控制器配置寄存器 |
| 0x1C | CSTS | 32-bit | 控制器状态寄存器 |
| 0x24 | AQA | 32-bit | Admin队列属性 |
| 0x28 | ASQ | 64-bit | Admin提交队列基地址 |
| 0x30 | ACQ | 64-bit | Admin完成队列基地址 |
你可能会问:为什么这些寄存器要放在BAR0里?因为PCIe规范要求,功能级别的寄存器必须放在BAR0,而且必须是Memory Space。我刚开始做NVMe驱动时,曾经试图把BAR1也映射了,结果发现根本没必要——所有控制都在BAR0里。
3.3 AQA与ASQ/ACQ配置
Admin队列是NVMe控制器的生命线。所有的管理命令——比如创建I/O队列、获取日志页、设置特征——都走Admin队列。所以配置Admin队列是初始化中最关键的一步。
配置顺序是这样的:
- 先配AQA(Admin Queue Attributes)寄存器,设置队列大小。
- 再配ASQ(Admin Submission Queue)寄存器,设置提交队列的基地址。
- 最后配ACQ(Admin Completion Queue)寄存器,设置完成队列的基地址。
AQA寄存器的结构很简单:
- 低16位:ASQS(Admin Submission Queue Size),表示提交队列的深度。
- 高16位:ACQS(Admin Completion Queue Size),表示完成队列的深度。
注意:队列深度必须是2的幂,最小是2,最大取决于CAP寄存器里的MQES字段。我一般习惯设成64或者128,够用又不浪费内存。
ASQ和ACQ寄存器存的是物理地址。嗯,这里有个坑——地址必须4KB对齐。我曾经因为偷懒,直接用了kmalloc分配的内存,结果地址没对齐,控制器直接罢工。后来改成dma_alloc_coherent才搞定。
核心要点: Admin队列的内存必须满足以下条件:
- 物理连续
- 4KB对齐
- 在配置ASQ/ACQ之前,内存必须已经分配好并且内容清零
3.4 CC.EN使能
所有配置都准备好了,最后一步就是使能控制器。这一步通过写CC寄存器的EN位来实现。
CC寄存器的关键字段:
| 位域 | 名称 | 描述 |
|---|---|---|
| 0 | EN | 使能位,写1使能,写0复位 |
| 4:3 | CSS | 命令集选择,一般用NVM命令集 |
| 6:4 | MPS | 内存页面大小,必须和系统页面大小一致 |
| 10:7 | AMS | 仲裁机制选择,默认用Round Robin |
| 13:11 | IOCQES | I/O完成队列条目大小 |
| 16:14 | IOSQES | I/O提交队列条目大小 |
使能流程:
- 确保CSTS.RDY为0,控制器处于复位状态。
- 配置CC寄存器:设置EN=1,同时配好CSS、MPS等字段。
- 轮询CSTS.RDY,等待它变为1,表示控制器就绪。
这里有个细节:写CC.EN的时候,必须一次性把整个CC寄存器写完整。不能先写EN再写MPS,否则控制器行为是未定义的。我建议用read-modify-write的方式:先读CC,修改对应位,再写回去。
使能成功后,控制器就进入Ready状态了。这时候你可以通过Admin队列发送命令了。第一个命令通常是Get Features或者Identify,用来确认控制器的能力。
嗯,到这里,NVMe控制器的初始化流程就走完了。总结一下核心步骤:复位→映射寄存器→配Admin队列→使能。每一步都有坑,但只要你按照spec来,别偷懒,基本不会出大问题。
下一章咱们讲I/O队列的创建和中断配置,那才是真正考验性能优化功底的地方。