NVMe 命令位于 PCIe BAR 中的什么位置?
Where are NVMe commands located inside the PCIe BAR?
根据NVMe规范,BAR对每个队列都有tail和head字段。例如:
- 提交队列
y
尾门铃(SQyTDBL
):
- 开始:
1000h + (2y * (4 << CAP.DSTRD))
- 结束:
1003h + (2y * (4 << CAP.DSTRD))
- 提交队列
y
门铃(SQyHDBL
):
- 开始:
1000h + ((2y + 1) * (4 << CAP.DSTRD))
- 结束:
1003h + ((2y + 1) * (4 << CAP.DSTRD))
是否有队列本身或仅仅是指针? Is this correct? 如果是队列,我假设 DSTRD 表示 all 个队列的最大长度。
此外,规范讨论了两个 可选 区域:主机内存缓冲区 (HMB) 和控制器内存缓冲区 (CMB)。
- HMB:主机 DRAM(PCIe 根)中的一个区域
- CMB:NVMe 控制器的 DRAM(SSD 内部)内的区域
如果两者都是可选的,那么它位于何处?由于端点 PCIe 仅适用于 BAR 和 PCI 接头,我看不到它们可能位于的任何其他位置,除了 BAR。
抱歉,我是凭记忆做的,但我已经实现了一个 FPGA NVMe 主机,所以希望我的记忆足以回答你的问题等等,如果我出错了,但至少你知道为什么。我将提供您可以在此处找到的规范中的参考部分。 https://nvmexpress.org/wp-content/uploads/NVM-Express-1_4-2019.06.10-Ratified.pdf 另外,在我真正回答你的问题之前,我想澄清一些困惑,理解规范需要一些时间,老实说,我建议从头到尾阅读它,最后几节有助于为前几节提供背景知识,就像听起来。
这些是提交队列和完成队列,分别是子队列尾部和完成队列头(第 3.1 节)。稍后会详细介绍这一点,我只是想纠正这样一种误解,即您作为主机访问提交队列头,而不仅仅是控制器(传统上是驱动器)。一个简单的提醒提交是你要求驱动器做某事,完成是驱动器告诉你它是如何进行的。阅读第 7.2 节 了解更多信息。
在向这些队列发送任何内容之前,您必须先设置上述队列。在系统基线中这些队列不存在,您必须使用管理队列来设置它们。
28h 2Fh ASQ Admin Submission Queue Base Address
30h 37h ACQ Admin Completion Queue Base Address
你对 DSTRD 的陈述是一个巨大的误解。该字段来自功能寄存器 (0x0) 图 3.1.1。这个字段是控制器(驱动器)告诉你 "doorbell stride" 表示每个门铃之间有多少字节,我从来没有看到驱动器报告这个值除了 0 之外的任何东西,你为什么要死space 在门铃寄存器之间。
请注意写入的大小,根据我的经验,即使您只打算发送 1dword 的数据,大多数 NVMe 驱动器都要求您发送至少 2dwords(8 字节)的写入,只是一个注释。
为了真正帮助您将此东西用作主机,请参考第 7.6.1 节 以找到初始化顺序。注意你必须如何设置多个寄存器,读取某些参数和其他类似的东西。
假设您或其他人已经完成初始化,现在让我回答您问题的核心,即如何使用这些队列。问题是,这个答案跨越了规范的许多部分,并且是它的核心。因此,我将尽我所能将其分解为一个简单的写入命令。请注意,在您首先使用管理队列创建队列之前,您不能写入,该队列利用来自规范不同部分的不同操作码,抱歉,我无法写出所有这些内容。
将数据写入 NVMe 驱动器的步骤。
在创建提交队列时,您将指定此特定队列的大小。这是一次可以放入队列中进行处理的命令数。与此同时,您将指定队列基地址。因此,对于此示例,我们假设您将基地址设置为 0x1000_0000,大小为 16 (0x10)。 图 105 让我们知道每个提交队列条目的大小为 64 字节 (0x40),因此队列条目 0 在 0x1000_0000 条目 1 在 0x1000_0040 2 0x1000_0080 以此类推我们的 16 个条目,然后循环返回。
您将首先存储要写入的数据,假设您有 512 字节 (0x200) 的数据要写入。因此,为简单起见,您将该数据放在 0x2000_0000 - 0x2000_0200.
您创建提交队列命令。这不是一个简单的过程。我不会为您记录所有这些,但请理解您应该参考 图 104、图 346 和第 6.15 节。然而,这还不够。您还需要了解 PRP 与 SGL 以及您正在使用的(PRP 更容易上手)。 NLB(逻辑块数)决定您的写入大小,对于 NVMe,您不以字节为单位指定写入,但就大小由控制器(驱动器)指定的 NLB 而言,它可以实现多个 NLB 大小,但这取决于该驱动器不是您作为主机,您只需从它支持的内容中选择第 5.15.2.1 节,图 245您要查看标识名称space 以告诉您LBA(逻辑块地址)大小,这会让您陷入困境以确定实际大小,但没关系,信息就在那里。
好的,你完成了这个烂摊子并创建了提交命令。假设主机已经在此队列上完成了 2 个命令(开始时这将是 0,我选择 2 只是为了在我的示例中更清楚)。您现在需要做的是将此命令放在 0x1000_0080.
现在假设这是队列 1(根据您发布的等式,队列编号是 y 值。请注意队列 0 是管理队列) .你需要做的是按下控制器提交队列尾部的门铃,说出现在加载了多少命令(这样你就可以一次排队多个,并且只在你准备好时才告诉驱动器)。在这种情况下,数字是 2。因此您需要将值 2 写入寄存器 0x1008。
此时驱动会走。啊哈,主持人告诉我有新的命令要获取。因此,控制器将进入队列基址 + 命令大小*2 并获取 64 字节的数据,也就是 1 个命令(地址 0x1000_0080)。控制器会将此命令解码为写入,这意味着控制器(驱动器)必须从某个地址读取数据并将其放入内存中被告知的位置。这意味着您的写入命令应该告诉驱动器转到地址 0x2000_0000 并读取 512 字节的数据,如果您确定 PCIe 总线的范围,它就会这样做。此时驱动器将填写一个完成队列条目(第 4.6 节 中指定的 16 个字节)并将其放入您在创建队列时指定的完成队列地址(加上 0x20,因为这是2 次完成)。然后控制器会产生MSI-X中断。
此时你必须去完成队列放置的地方并读取响应以检查状态,如果你排队多个提交检查 SQID 以查看完成的内容,因为作业可以完成秩序。然后,您必须写入完成队列头 (0x100C) 以指示您已检索到完成队列(成功或失败)。请注意,您永远不会与提交队列头进行交互(这取决于控制器,因为只有他知道提交队列条目何时被处理)并且只有控制器将内容放入完成队列尾部,因为只有他才能创建新条目。
很抱歉这篇文章太长而且格式不正确,但希望您现在对 NVMe 有了更好的理解,一开始它有点乱,但是一旦您理解了它,一切就都有意义了。请记住我的示例假设您创建了一个基线不存在的队列。首先,您需要设置管理员提交和完成队列(0x28 和 0x30),队列 ID 为 0,因此 tail/head 门铃的地址分别为 0x1000、0x1004。然后您必须参考第 5 节 来找到操作码来使事情发生,但我相信您可以从我给您的内容中找出答案。如果您还有其他问题,请留下评论,我会看看我能做些什么。
根据NVMe规范,BAR对每个队列都有tail和head字段。例如:
- 提交队列
y
尾门铃(SQyTDBL
):- 开始:
1000h + (2y * (4 << CAP.DSTRD))
- 结束:
1003h + (2y * (4 << CAP.DSTRD))
- 开始:
- 提交队列
y
门铃(SQyHDBL
):- 开始:
1000h + ((2y + 1) * (4 << CAP.DSTRD))
- 结束:
1003h + ((2y + 1) * (4 << CAP.DSTRD))
- 开始:
是否有队列本身或仅仅是指针? Is this correct? 如果是队列,我假设 DSTRD 表示 all 个队列的最大长度。
此外,规范讨论了两个 可选 区域:主机内存缓冲区 (HMB) 和控制器内存缓冲区 (CMB)。
- HMB:主机 DRAM(PCIe 根)中的一个区域
- CMB:NVMe 控制器的 DRAM(SSD 内部)内的区域
如果两者都是可选的,那么它位于何处?由于端点 PCIe 仅适用于 BAR 和 PCI 接头,我看不到它们可能位于的任何其他位置,除了 BAR。
抱歉,我是凭记忆做的,但我已经实现了一个 FPGA NVMe 主机,所以希望我的记忆足以回答你的问题等等,如果我出错了,但至少你知道为什么。我将提供您可以在此处找到的规范中的参考部分。 https://nvmexpress.org/wp-content/uploads/NVM-Express-1_4-2019.06.10-Ratified.pdf 另外,在我真正回答你的问题之前,我想澄清一些困惑,理解规范需要一些时间,老实说,我建议从头到尾阅读它,最后几节有助于为前几节提供背景知识,就像听起来。
这些是提交队列和完成队列,分别是子队列尾部和完成队列头(第 3.1 节)。稍后会详细介绍这一点,我只是想纠正这样一种误解,即您作为主机访问提交队列头,而不仅仅是控制器(传统上是驱动器)。一个简单的提醒提交是你要求驱动器做某事,完成是驱动器告诉你它是如何进行的。阅读第 7.2 节 了解更多信息。
在向这些队列发送任何内容之前,您必须先设置上述队列。在系统基线中这些队列不存在,您必须使用管理队列来设置它们。
28h 2Fh ASQ Admin Submission Queue Base Address
30h 37h ACQ Admin Completion Queue Base Address
你对 DSTRD 的陈述是一个巨大的误解。该字段来自功能寄存器 (0x0) 图 3.1.1。这个字段是控制器(驱动器)告诉你 "doorbell stride" 表示每个门铃之间有多少字节,我从来没有看到驱动器报告这个值除了 0 之外的任何东西,你为什么要死space 在门铃寄存器之间。
请注意写入的大小,根据我的经验,即使您只打算发送 1dword 的数据,大多数 NVMe 驱动器都要求您发送至少 2dwords(8 字节)的写入,只是一个注释。
为了真正帮助您将此东西用作主机,请参考第 7.6.1 节 以找到初始化顺序。注意你必须如何设置多个寄存器,读取某些参数和其他类似的东西。
假设您或其他人已经完成初始化,现在让我回答您问题的核心,即如何使用这些队列。问题是,这个答案跨越了规范的许多部分,并且是它的核心。因此,我将尽我所能将其分解为一个简单的写入命令。请注意,在您首先使用管理队列创建队列之前,您不能写入,该队列利用来自规范不同部分的不同操作码,抱歉,我无法写出所有这些内容。
将数据写入 NVMe 驱动器的步骤。
在创建提交队列时,您将指定此特定队列的大小。这是一次可以放入队列中进行处理的命令数。与此同时,您将指定队列基地址。因此,对于此示例,我们假设您将基地址设置为 0x1000_0000,大小为 16 (0x10)。 图 105 让我们知道每个提交队列条目的大小为 64 字节 (0x40),因此队列条目 0 在 0x1000_0000 条目 1 在 0x1000_0040 2 0x1000_0080 以此类推我们的 16 个条目,然后循环返回。
您将首先存储要写入的数据,假设您有 512 字节 (0x200) 的数据要写入。因此,为简单起见,您将该数据放在 0x2000_0000 - 0x2000_0200.
您创建提交队列命令。这不是一个简单的过程。我不会为您记录所有这些,但请理解您应该参考 图 104、图 346 和第 6.15 节。然而,这还不够。您还需要了解 PRP 与 SGL 以及您正在使用的(PRP 更容易上手)。 NLB(逻辑块数)决定您的写入大小,对于 NVMe,您不以字节为单位指定写入,但就大小由控制器(驱动器)指定的 NLB 而言,它可以实现多个 NLB 大小,但这取决于该驱动器不是您作为主机,您只需从它支持的内容中选择第 5.15.2.1 节,图 245您要查看标识名称space 以告诉您LBA(逻辑块地址)大小,这会让您陷入困境以确定实际大小,但没关系,信息就在那里。
好的,你完成了这个烂摊子并创建了提交命令。假设主机已经在此队列上完成了 2 个命令(开始时这将是 0,我选择 2 只是为了在我的示例中更清楚)。您现在需要做的是将此命令放在 0x1000_0080.
现在假设这是队列 1(根据您发布的等式,队列编号是 y 值。请注意队列 0 是管理队列) .你需要做的是按下控制器提交队列尾部的门铃,说出现在加载了多少命令(这样你就可以一次排队多个,并且只在你准备好时才告诉驱动器)。在这种情况下,数字是 2。因此您需要将值 2 写入寄存器 0x1008。
此时驱动会走。啊哈,主持人告诉我有新的命令要获取。因此,控制器将进入队列基址 + 命令大小*2 并获取 64 字节的数据,也就是 1 个命令(地址 0x1000_0080)。控制器会将此命令解码为写入,这意味着控制器(驱动器)必须从某个地址读取数据并将其放入内存中被告知的位置。这意味着您的写入命令应该告诉驱动器转到地址 0x2000_0000 并读取 512 字节的数据,如果您确定 PCIe 总线的范围,它就会这样做。此时驱动器将填写一个完成队列条目(第 4.6 节 中指定的 16 个字节)并将其放入您在创建队列时指定的完成队列地址(加上 0x20,因为这是2 次完成)。然后控制器会产生MSI-X中断。
此时你必须去完成队列放置的地方并读取响应以检查状态,如果你排队多个提交检查 SQID 以查看完成的内容,因为作业可以完成秩序。然后,您必须写入完成队列头 (0x100C) 以指示您已检索到完成队列(成功或失败)。请注意,您永远不会与提交队列头进行交互(这取决于控制器,因为只有他知道提交队列条目何时被处理)并且只有控制器将内容放入完成队列尾部,因为只有他才能创建新条目。
很抱歉这篇文章太长而且格式不正确,但希望您现在对 NVMe 有了更好的理解,一开始它有点乱,但是一旦您理解了它,一切就都有意义了。请记住我的示例假设您创建了一个基线不存在的队列。首先,您需要设置管理员提交和完成队列(0x28 和 0x30),队列 ID 为 0,因此 tail/head 门铃的地址分别为 0x1000、0x1004。然后您必须参考第 5 节 来找到操作码来使事情发生,但我相信您可以从我给您的内容中找出答案。如果您还有其他问题,请留下评论,我会看看我能做些什么。