NVMe(Non-volatile Memory express)是一个逻辑设备接口规范。一般基于PCIe总线。
大概框图是这样的。Host和Controller通过PCIe连接,再连接到NVMe。其中PE_M0/PE_S0/PE_S1是AXI接口,命名是基于PCI这端来看的。PE_M0就是PCI是master,PE_S0就是PCI是slave。由于大部分的数据是由NVMe主动发起的,所以PE_S需要的带宽较大,可能会有多条AXI总线。
协议里面第2章主要讲的是PCIe的寄存器,NVMe也需要实现PCI Header以及register。这样Host就能通过这些寄存器识别到这是个NVMe设备。
协议第3章介绍了NVMe的寄存器。
PCIe层次初始化
先来看看Host和Controller在PCIe这层是如何工作的。
host根据pcie协议扫描pcie device
host扫描device的capability,得知这是个NVMe设备
host配置device pcie寄存器
host配置BAR地址
BAR地址是一个非常重要的一个寄存器,它长这个样子。
host先写BAR寄存器为全1,然后读回来,检查哪些bit被设置为1了,这样就可以知道哪些是只读的,根据这个只读bit位数就可以知道BAR的大小是多少。然后申请一段这么大的空间给这个BAR,并把该空间的地址写入这个BAR寄存器中,这样controller就知道了BAR空间地址了。而且你会发现,这样设计会强迫BAR地址按照大小对齐。
host配置msix表和pba表。是否执行这步取决于host。
NVMe层次初始化
host检查nvme capability
host为admin completion queue申请空间
host为admin submission queue申请空间
host设置ASQ/ACQ/AQA,准备admin command queue信息(基地址,深度)
host设置CC.EN=1,通知device说host已经准备好了。
device从寄存器中拿到queue信息
device设置CSTS.RDY=1,通知host,说device准备好了
host就可以发送identify command来获得更多的信息了
host发送IO CQ command来创建IO CQ
device从IOCQ command中得到IO completion queue信息
host发送IO SQ command来创建IO SQ
device从IOSQ command中获得IO submission queue信息
host和device就可以传输IO command了
command details
以一条identify命令为例,看下具体的细节。也就是上面第8条的具体细节。
- host申请4KB memory buffer,构建一个nvme命令(opcode=identify, prp1=buffer_phy_address…),将命令放入submission queue
- Host 建立TLP MEM_WRITE来写admin SQ Doorbell(offset 0x1000 in the nvme register region)
- device收到admin sq doorbell,就知道有新的admin命令了。device建立TLP MEM_READ来拿到SQ命令(16DW)
- device解析这条命令,知道是个identify命令。device建立TLP MEM_WRITE(s)发送identify数据(4KB)给host memory buffer(PRP1)
- device建立TLP MEM_WRITE发送completion status给host memory ADMIN_CQ buffer(4DW)
- device发送interrupt通知host有命令完成了
- host检查ADMIN_CQ来知道哪些command完成了
- host建立TLP WRITE写admin CQ doorbell通知device,表明host已经接收到了completion status。
Submission Queue数据结构
submission Queue entry的内容可以参考spec。这里主要介绍下指针的变化过程。
我们假设SQ有4个entry,host和device都要记录head和tail指针。其中tail指针由host维护,head指针由device维护。
- 初始状态,host和device的head和tail都指向第一个entry。当head和tail相等时,表示所有entry为空。
- 当host放入一笔command,就把host的tail往后移一个。这时第一个entry被占用。但此时device还不知道。
- host把tail放入SQ Doorbell里面,告诉device现在tail到哪了。
- device拿到SQ Doorbell后,就知道有多少个entry被占用了,也把tail移动到指定位置。
- device处理完SQ command后,把head往后移,表示处理完了。但此时host还不知道。
- device把head写入CQ entry,并通知host,告诉host哪些处理完了。
- host拿到CQ entry后,就知道哪些处理完了,然后把head也移动到指定位置。
其中SQ Doorbell和CQ entry结构如下。
completion queue数据结构
假设CQ有4个entry,host只看到head指针,device看到head和tail指针。host会通过CQ Doorbell告诉device,device只用维护tail指针就可以。host通过CQ entry里面的phase tag来判断哪些时新的entry。host和device都要维护一个标志来识别phase tag什么值表示空。
- 初始状态,head和tail指针都指向第一个entry,phase tag=0表示为空
- device放入一个command,然后把tail指针往后移。这是CQ第一个entry被占用,当然host还不知道。
- device通过中断通知host
- host从当前head往后查,看到是否有phase tag等于1的entry,直到phase tag不等于1为止。
- host通过CQ Doorbell通知device,告诉head走到哪了。
- device拿到head,也把device的head指针往后移
其中CQ Doorbell结构如下。
再举一个例子来解释phase tag的变化。
- 假设初始状态是,head和tail都指向第3个entry,并且phase tag为0表示为空
- device放入3个command,然后把tail指针往后移,每个command的phase tag填入empty的反值,注意当回到第一个entry时,empty的标识要反转,所以第一个entry要填入0。
- device通过中断通知host
- host从当前head往后查,第3个entry是1,表示有值;第4个entry是1,表示有值;再往后就回到第1个entry了,此时先将empty反转为1;再看第1个entry的值为0,表示有值;再看第2个entry的值为1,表示空。这样就知道了有3个entry需要处理。
- host通过CQ Doorbell通知device,告诉head走到第2个entry了。
- device拿到Doorbell,就把head也移动到那个位置。
附录
[点击下载] NVM-Express-1_4-2019.06.10-Ratified.pdf