第一部分 PCI Express 设备如何通信(深入到 TLP)--PCI Express 不是总线
首先我们要明白,翻译这篇文章的目的是什么,如果你对内核感兴趣,但是又摸不着头脑时,我建议大家多去学习一些机器设计,通讯原理以及操作系统原理等方面的内容,毕竟操作系统的内核实际是系统应用程序和硬件之间的一层抽象。否则你看似能看懂内核,实际你真不懂。
https://madpcb.com/glossary/bus/
原文地址: http://xillybus.com/tutorials/pci-express-tlp-pcie-primer-tutorial-guide-1
前言
当我为 PCI express 编写 Xillybus IP 核时,我很快发现它很难开始:在线资源和官方规范用关于具体细节的血腥细节轰炸你,但很少说明什么机器应该做的。因此,一旦我努力为自己解决这个问题,我就决定编写这个小指南,希望它能帮助其他人有一个更轻松的开始。这是基于官方 PCI Express 规范 1.1,但非常适用于更高版本。但是,阅读原始规范是无可替代的。游戏的名称是否要获得正确的细节,以便设备在不适合测试的环境中正常工作。
不要因为我没有描述全貌或使用不准确的定义而责怪我。准确是规范的目的。我在这里要做的就是让它更易于阅读。我还发布了一个会话的TLP 嗅探转储示例,这可能有助于了解机器的工作原理。
我依靠其他来源来描述外形尺寸、通道数、数据速率等。对于这些的概述,我建议维基百科的条目。我还建议阅读PCI 配置,特别是关于枚举的部分。
因此,让我们从一些基本的见解开始。
PCI system layout(第一代PCI的系统架构)
PCI Express 不是总线
总线是电气设计原语。它是表示多线连接的折线对象。总线可用于放置在 PCB 设计软件的原理图编辑器中。没有 PCB 总线路由和布局,现代计算根本不可能实现。许多并行处理数据的数字系统也是如此。
关于 PCI Express(以后称为 PCIe),首先要意识到的是,它不是PCI-X,或任何其他 PCI 版本。以前的 PCI 版本,包括 PCI-X,是真正的总线:有平行的铜轨物理上到达外围卡的几个插槽。PCIe 更像是一个网络,每张卡都通过一组专用电线连接到网络交换机。与本地以太网网络完全一样,每个卡都有自己到交换结构的物理连接。相似性更进一步:通信采用通过这些专用线路传输的数据包的形式,具有流量控制、错误检测和重传。没有 MAC 地址,但我们有卡的物理(“地理”)位置来定义它,然后再用高级寻址方法(I/O 和地址空间中的块)分配它。
事实上,一个最小的 (1x) PCIe 连接仅包含四根用于数据传输的线(每个方向两个差分对)和另一对为卡提供参考时钟的线。就是这样。
另一方面,PCIe 标准被故意设计得非常像经典 PCI。尽管它是一个基于数据包的网络,但它全都与地址、读取、写入中断有关。
即插即用配置仍已完成,卡的访问方式与之前一样,是通过地址和 I/O 空间的读写方式进行的。仍然有供应商/产品 ID,以及几种模仿旧行为的机制。长话短说,对于不了解 PCIe 的操作系统来说,PCIe 标准在很大程度上看起来像是很好的旧 PCI。
所以PCIe是伪造传统PCI总线的分组网络。它的整个设计使得在不对软件进行任何更改的情况下将 PCI 设备迁移到 PCIe 成为可能,和/或在 PCI 和 PCIe 之间透明地桥接而不丢失任何功能。
一个简单的总线事务
为了了解整个事情,让我们看看当 PC 的 CPU 想要将 32 位字写入 PCIe 外设时会发生什么。为了简单起见,在下面的描述中有意省略了一些细节和可能性。
由于它是一台 PC,CPU 本身很可能在其自己的总线上执行简单的写入操作,并且连接到 CPU 总线的内存控制器芯片组直接连接到 PCIe 总线。所以发生的事情是芯片组(在 PCIe 术语中用作Root Complex)生成一个内存写入数据包以通过总线传输。该数据包包含一个标头,其长度为 3 或 4 个 32 位字(取决于使用的是 32 位还是 64 位寻址)和一个包含要写入的字的 32 位字。这个数据包只是说“将这个数据写入这个地址”。
然后,此数据包在芯片组的 PCIe 端口(或其中之一,如果有多个)上传输。目标外设可能直接连接到芯片组,或者它们之间可能有一个交换网络。通过这种或另一种方式,数据包被路由到外围设备、解码并通过执行所需的写入操作来执行。
仔细看看
这种简单化的观点忽略了几个细节。例如,底层通信机制,它由三层组成:事务层、数据链路层和物理层。上面对数据包的描述定义为Transaction Layer Packet (TLP),涉及到PCIe的最上层。 PCIe TLP(Transaction Layer Packet)则是PCIe协议中的一种数据包,用于在不同端点或叫终点(endpoint)之间传输数据。
数据链路层负责确保每个 TLP 正确到达其目的地。它用自己的标头和链接 CRC 包装 TLP,从而确保 TLP 的完整性。确认重传机制确保没有 TLP 在途中丢失。流量控制机制确保仅当链路伙伴准备好接收数据包时才发送数据包。总而言之,无论何时将 TLP 交给数据链路层进行传输,我们都可以相信它会到达,即使到达时间存在轻微的不确定性。未能传送 TLP 是总线的主要故障。
在讨论信用和数据包重新排序时,我们将回到数据链路层。但为此,只需认识到经典总线操作已被 PCIe 结构上的 TLP 传输所取代。
我还想提一下,内存写入 TLP 的数据有效负载可能比单个 32 位字长得多,形成 PCIe 写入突发。TLP 的大小限制是在外设的配置阶段设置的,但典型的数字是每个 TLP 的最大值为 128、256 或 512 字节。
在继续之前,值得注意的是,内存写入 TLP 的发送者没有得到数据包已到达其最终目的地的指示,更不用说它已被执行了。即使数据链路层得到肯定的确认,这也仅意味着数据包已安全到达附近的交换机。从来没有进行过端到端的确认,也没有必要。
下面是简单的PCIE模型
示例写入数据包
让我们以上面提到的数据写入案例为例,看看 TLP 的细节。假设 CPU 使用 32 位寻址将值 0x12345678 写入物理地址 0xfdaff040。然后数据包可以包含四个 32 位字(4 个 DW,双字),如下所示:
因此数据包被传输为 0x40000001、0x0000000f、0xfdaff040、0x12345678。
让我们来解释一下颜色编码:
- 灰色字段是保留的,这意味着发送方必须将零放在那里(并且接收方忽略它们)。一些灰色字段标记为“R”,表示该字段始终保留,有些带有名称,表示由于该特定数据包的性质而保留该字段。
- 允许绿色字段具有非零值,但端点外围设备很少使用它们(据我所见)。
- 特定数据包的值以红色标记。
现在让我们简单解释一下有效字段:
- Fmt 字段与 Type 字段一起表明这是一个内存写入请求。
- TD 位为零,表示 TLP 数据(TLP 摘要)上没有额外的 CRC。如果我们相信我们的硬件不会破坏 TLP,那么这个额外的 CRC 就没有理由,因为链路层有自己的 CRC 来确保在传输过程中没有任何错误。
- Length 字段的值为 0x001,表示该 TLP 有一个 DW(32 位字)数据。
- 请求者 ID 字段表示此数据包的发送者通过 ID 零而为人所知——它是 Root Complex(最接近 CPU 的 PCIe 端口)。虽然是强制性的,但除了报告错误外,该字段在写请求中没有实际用途。
- 在这种情况下,标签是一个未使用的字段。发件人可以在这里放任何东西,所有其他组件都应该忽略它。稍后我们将仔细研究它。
- 第一个 BE 字段(第一个双字字节启用)允许选择第一个数据 DW 中的四个字节中的哪一个是有效的,应该被写入。在我们的例子中设置为 0xf,它标志着所有四个字节都已写入。
- 当 Length 为 1 时,Last BE 字段必须为零,因为第一个 DW 和最后一个 DW 是相同的。
- 地址字段只是写入第一个数据 DW 的地址。嗯,这个地址的第 31-2 位。请注意,TLP 中 DW 2 的两个 LSB 为零,因此 DW 2 实际上读取的是写入地址本身。将 0x3f6bfc10 乘以四,得到 0xfdaff040。
- 最后,我们有一个 DW 数据。现在是提及 PCIe 运行大字节序,而英特尔处理器认为小字节序的好时机。因此,如果这是一台普通的 PC 计算机,它会在其软件表示中写入 0x78563412。
读取请求
现在让我们看看当 CPU 想要从外设读取数据时会发生什么。读取操作有点棘手,因为不可避免地会涉及两个数据包:一个是从 CPU 到外设的 TLP,要求后者执行读取操作,另一个是返回数据的 TLP。在 PCIe 术语中,我们有一个请求者(在我们的例子中是 CPU)和一个完成者(外设)。
我们假设 CPU 需要来自地址 0xfdaff040 的单个 DW(32 位字)(与之前相同)。和以前一样,它很可能在与其内存控制器共享的总线上启动读取操作,该总线包含 Root Complex,而 Root Complex 又会生成要通过 PCIe 总线发送的 TLP。这是一个读取请求 TLP,可能如下所示:
所以这个数据包由 3 个 DW 0x00000001、0x00000c0f、0xfdaff040 组成。它告诉外设在地址 0xfdaff040 处读取一个完整的 DW,并将结果返回给 ID 为 0x0000 的总线实体。
它与上面显示的 Write Request 示例惊人地相似,因此我将重点介绍不同之处:
- Fmt/Type 字段已更改(实际上,只有 Fmt)以指示这是一个读取请求。
- 和以前一样,请求者 ID 字段表示此数据包的发送者的 ID 为零。它与以前的字段相同,但在读取请求中它在功能上至关重要,因为它告诉完成者将其响应发送到哪里。我们将在下面看到有关此 ID 的更多信息。
- 标签在读取请求中很重要。重要的是要认识到它本身并不意味着什么,但它具有跟踪编号的功能:当 Completer 响应时,它必须将此值复制到 Completion TLP。这允许请求者将完成答案与其请求相匹配。毕竟,允许来自总线上单个设备的多个请求。这个Tag是Requester根据自己的需要设置的,标准没有要求一定的枚举方式,只要所有未完成的请求的Tag是唯一的即可。尽管分配了 8 位,但只允许使用 5 个 LSB,其余必须默认为零。这允许一对总线实体之间最多有 32 个未完成的请求。对于需要它的应用程序,标准扩展可能允许多达 2048 个。
Length字段表示应该读取一个DW,Address字段表示从哪个地址读取。这两个 BE 字段保留与写入请求相同的含义和规则,只是它们选择读取哪些字节而不是写入哪些字节。
另一个教程讨论了应用读取请求的注意事项。
完成
当外设收到读取请求 TLP 时,它必须以某种完成 TLP 进行响应,即使它无法完成请求的操作。我们来看一个成功的案例:外设从其内部资源读取数据块,现在需要将结果返回给请求者(在我们的例子中是 CPU)。
数据包可能如下所示:
所以 TLP 由 0x4a000001、0x01000004、0x00000c00、0x12345678 组成。这些数据包基本上是说“告诉总线实体 0x0000 其对标记为 0x0c 的实体 0x0100 的请求的答案是 0x12345678。” CPU(或者实际上,内存控制器 = Root Complex)现在可以在其内部记录中查找该请求的内容,并完成相关的总线周期。让我们把它分解成碎片:
- Fmt 字段与 Type 字段一起表明这是一个包含数据的完成数据包。
- Length 字段的值为 0x001,表示该 TLP 有一个 DW(32 位字)数据。可是等等。无论如何,请求者不应该知道吗?答案是 TLP 的长度有限制,它可能小于请求的 DW 数量。发生这种情况时,会发回多个完成 TLP。所以 Length 字段说明了这个特定数据包中有多少 DW。但那是另外一回事了。
- 如果我们在这里,我们有字节计数字段。在我们的单 TLP 完成示例中,它只是数据包中有效负载字节的数量。由于请求中的第一个 DW BE 字段全部为 1,因此我们有四个有效字节,如该字段中所述。仅供一般了解,该字段的真正定义是剩余要传输的字节数,包括当前数据包中的字节数。这在多个 TLP 完成中很有用,如下所示。
- 然后我们有 Lower Address 字段。它是地址的 7 个最低有效位,从中读取了此 TLP 中的第一个字节。在我们的例子中是 0x40,来自 0xfdaff040 的低位。此字段可用于多个 TLP 完成。
- Completer ID 标识此数据包的发送者,即 0x0100。我将在下面剖析这个 ID。
- 请求者 ID 标识此数据包的接收者,它是零 ID(Root Complex)。如果有一些 PCIe 交换机要路由,这将作为目标地址。
- Status 字段为零,表示完成成功。可以猜到,其他值表示不同类型的拒绝。
- BCM 字段始终为零,除非数据包来自具有 PCI-X 的网桥。所以它是零。
- 最后,我们有一个 DW 数据。
顺便说一下,Completer 可能会返回分成几个数据包的数据。然后可以通过检查来检测完成中的最后一个数据包
长度 == ((LowerAddress & 3) + ByteCount + 3) >> 2
如果我们碰巧在我们的请求中将自己限制在 DW 粒度,这就变成了
长度 == 字节数 >> 2
这两个例子就这么多了。现在更笼统地说。
发布和非发布操作
如果我们将总线写入操作的生命周期与读取操作的生命周期进行比较,就会发现一个明显的区别:写入 TLP 操作是即发即弃的。一旦数据包形成并交给数据链路层,就不需要再担心了。另一方面,读取操作需要请求方等待完成。在完成数据包到达之前,请求者必须保留有关请求是什么的信息,有时甚至会保留 CPU 的总线:如果 CPU 的总线开始读取周期,则它必须保持等待状态,直到所需读取操作的值是在总线的数据线上可用。这可能是总线的可怕减速,这在最近的系统中是正确避免的。
即发即弃操作的术语,例如 Memory Write is Posted操作。此类操作仅包含请求。自然地,由 Request 和 Completion 组成的操作称为非 Posted操作。
32 与 64 位寻址
如上所述,读取和写入请求中给出的地址可以是 32 位或 64 位宽,使标头长度为 3 或 4 个 DW。然而,PCIe 规范中的第 2.2.4.1 节指出,只有在必要时才必须使用 4 DW 标头格式:
对于 4 GB 以下的地址,请求者必须使用 32 位格式。如果接收到寻址低于 4 GB 的 64 位格式请求(即,地址的高 32 位全为 0),则接收方的行为未指定。
实际上,任何外围设备的寄存器都很少映射到 4 GB 范围内,但是 DMA 缓冲区很可能会超出 4 GB 边界。因此,在设计新设备时,应支持具有 64 位寻址的读写 TLP。
输入/输出请求
PCIe总线支持I/O操作只是为了向后兼容,强烈建议不要在新设计中使用I/O TLP。原因之一是 I/O 空间中的读取和写入请求都是非 Posted 的,因此 Requester 也被迫等待写入操作的完成。另一个问题是 I/O 操作仅采用 32 位地址,而 PCIe 规范通常支持 64 位地址。
识别和路由
由于 PCIe 本质上是一个分组网络,在途中可能存在交换机,这些交换机需要知道将每个 TLP 发送到哪里。共有三种路由方法:按地址、按 ID 和隐式。通过地址路由应用于内存和 I/O 请求(读取和写入)。隐式路由仅用于某些消息 TLP,例如来自 Root Complex 的广播和始终到达 Root Complex 的消息。所有其他 TLP 均按 ID 路由。
ID 是一个 16 位字,由众所周知的三元组构成:总线号、设备号和功能号。它们的含义与传统 PCI 总线完全相同。ID 的构成如下:
如果您正在运行 Linux,我建议您尝试使用 lspci 实用程序及其众多标志来了解总线结构。
总线控制 (DMA)
在 PCIe 之前,这个问题曾经有点令人毛骨悚然。 在 PCIe 上,它的异国情调要少得多。它归结为一个简单的概念,即总线上的任何人都可以在总线上发送读写 TLP,就像 Root Complex 一样。这允许外设直接访问 CPU 的内存 (DMA) 或与对等外设交换 TLP(在交换实体支持的范围内)。
好吧,与任何 PCI 设备一样,首先需要做两件事: 需要通过在标准配置寄存器之一中设置“总线主控启用”位来授予外围设备总线主控权。第二件事是驱动程序软件需要通知外设相关缓冲区的物理地址,很可能是通过写入 BAR 映射寄存器。
中断
PCIe 支持两种中断:Legacy INTx 和 MSI。
支持 INTx 中断是为了与旧软件兼容,也是为了允许在经典 PCI 总线和 PCIe 之间进行桥接。由于 INTx 中断是电平触发的(即,只要物理 INTx 线处于低电压,中断请求就处于活动状态),因此有一个 TLP 数据包表示该线路已被断言,另一个表明它已被取消断言。这不仅本身很奇怪,而且 INTx 中断的老问题仍然存在,例如中断共享以及每个中断处理例程都需要检查中断的真正用途。
正是由于这些问题,在(传统的)PCI 2.2 中引入了一种新的中断形式 MSI。当时的想法是,既然几乎所有 PCI 外围设备都具有总线主控功能,为什么不让外围设备通过写入某个地址来发出中断信号呢?
PCIe 生成 MSI 的方式完全相同:发出中断信号仅包括通过总线发送 TLP,这只是一个已发布的写请求,带有一个特殊地址,主机在初始化期间已将其写入外设的配置空间。任何现代操作系统(当然包括 Linux)都可以调用正确的中断例程,而无需猜测是谁产生了中断。如果外设不需要确认,也没有必要“清除”中断。