nvme总结

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这层是如何工作的。

  1. host根据pcie协议扫描pcie device

  2. host扫描device的capability,得知这是个NVMe设备

  3. host配置device pcie寄存器

  4. host配置BAR地址

    BAR地址是一个非常重要的一个寄存器,它长这个样子。

    host先写BAR寄存器为全1,然后读回来,检查哪些bit被设置为1了,这样就可以知道哪些是只读的,根据这个只读bit位数就可以知道BAR的大小是多少。然后申请一段这么大的空间给这个BAR,并把该空间的地址写入这个BAR寄存器中,这样controller就知道了BAR空间地址了。而且你会发现,这样设计会强迫BAR地址按照大小对齐。

  5. host配置msix表和pba表。是否执行这步取决于host。

NVMe层次初始化

  1. host检查nvme capability

  2. host为admin completion queue申请空间

  3. host为admin submission queue申请空间

  4. host设置ASQ/ACQ/AQA,准备admin command queue信息(基地址,深度)

  5. host设置CC.EN=1,通知device说host已经准备好了。

  6. device从寄存器中拿到queue信息

  7. device设置CSTS.RDY=1,通知host,说device准备好了

  8. host就可以发送identify command来获得更多的信息了

  9. host发送IO CQ command来创建IO CQ

  10. device从IOCQ command中得到IO completion queue信息

  11. host发送IO SQ command来创建IO SQ

  12. device从IOSQ command中获得IO submission queue信息

  13. host和device就可以传输IO command了

command details

以一条identify命令为例,看下具体的细节。也就是上面第8条的具体细节。

  1. host申请4KB memory buffer,构建一个nvme命令(opcode=identify, prp1=buffer_phy_address…),将命令放入submission queue
  2. Host 建立TLP MEM_WRITE来写admin SQ Doorbell(offset 0x1000 in the nvme register region)
  3. device收到admin sq doorbell,就知道有新的admin命令了。device建立TLP MEM_READ来拿到SQ命令(16DW)
  4. device解析这条命令,知道是个identify命令。device建立TLP MEM_WRITE(s)发送identify数据(4KB)给host memory buffer(PRP1)
  5. device建立TLP MEM_WRITE发送completion status给host memory ADMIN_CQ buffer(4DW)
  6. device发送interrupt通知host有命令完成了
  7. host检查ADMIN_CQ来知道哪些command完成了
  8. 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维护。

  1. 初始状态,host和device的head和tail都指向第一个entry。当head和tail相等时,表示所有entry为空。
  2. 当host放入一笔command,就把host的tail往后移一个。这时第一个entry被占用。但此时device还不知道。
  3. host把tail放入SQ Doorbell里面,告诉device现在tail到哪了。
  4. device拿到SQ Doorbell后,就知道有多少个entry被占用了,也把tail移动到指定位置。
  5. device处理完SQ command后,把head往后移,表示处理完了。但此时host还不知道。
  6. device把head写入CQ entry,并通知host,告诉host哪些处理完了。
  7. 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什么值表示空。

  1. 初始状态,head和tail指针都指向第一个entry,phase tag=0表示为空
  2. device放入一个command,然后把tail指针往后移。这是CQ第一个entry被占用,当然host还不知道。
  3. device通过中断通知host
  4. host从当前head往后查,看到是否有phase tag等于1的entry,直到phase tag不等于1为止。
  5. host通过CQ Doorbell通知device,告诉head走到哪了。
  6. device拿到head,也把device的head指针往后移

其中CQ Doorbell结构如下。

再举一个例子来解释phase tag的变化。

  1. 假设初始状态是,head和tail都指向第3个entry,并且phase tag为0表示为空
  2. device放入3个command,然后把tail指针往后移,每个command的phase tag填入empty的反值,注意当回到第一个entry时,empty的标识要反转,所以第一个entry要填入0。
  3. device通过中断通知host
  4. host从当前head往后查,第3个entry是1,表示有值;第4个entry是1,表示有值;再往后就回到第1个entry了,此时先将empty反转为1;再看第1个entry的值为0,表示有值;再看第2个entry的值为1,表示空。这样就知道了有3个entry需要处理。
  5. host通过CQ Doorbell通知device,告诉head走到第2个entry了。
  6. device拿到Doorbell,就把head也移动到那个位置。

附录

[点击下载] NVM-Express-1_4-2019.06.10-Ratified.pdf