当我为PCI express编写Xillybus IP核时,我很快发现很难开始:在线资源和官方规格用关于螺母和螺栓的血腥细节轰炸你,但对机器应该做什么却很少说。因此,一旦我努力自己弄清楚这一点,我就决定写这个小指南,希望能帮助其他人获得一个更温和的开始。这基于官方的 PCI Express 规范 1.1,但非常适用于更高版本。不过,阅读原始规范是无可替代的。游戏的名称,如果获得正确的细节,以便设备在手头没有测试的环境中正常工作。
不要因为我没有描述全貌或使用不准确的定义而挑剔我。准确是规范的目的。我在这里要做的就是让它更人性化。我还发布了一个会话的 TLP 嗅探转储示例,这可能有助于了解该机制的工作原理。
关于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术语中充当根复合体)会生成一个内存写入数据包,用于通过总线传输。此数据包由一个标头组成,该标头的长度为 3 或 4 个 32 位字(取决于使用的是 32 位还是 64 位寻址)和一个包含要写入的字的 32 位字。这个数据包只是说“将此数据写入此地址”。
然后,该数据包在芯片组的PCIe端口上传输(如果有多个端口,则为其中一个端口)。目标外设可以直接连接到芯片组,或者它们之间可能存在交换网络。以这种方式或另一种方式,数据包被路由到外围设备,解码,并通过执行所需的写入操作来执行。
这种简单化的观点忽略了几个细节。例如,底层通信机制,它由三层组成:事务层、数据链路层和物理层。上面对数据包的描述被定义为事务层数据包 (TLP),它与 PCIe 的最上层有关。
数据链路层负责确保每个 TLP 正确到达其目的地。它使用自己的标头和链路 CRC 包装 TLP,从而确保 TLP 的完整性。确认-重传机制可确保在传输过程中不会丢失任何 TLP。流量控制机制确保仅在链路伙伴准备好接收数据包时才发送数据包。总而言之,每当TLP移交给数据链路层进行传输时,即使到达时间略有不确定性,我们也可以依靠它的到来。未能提供 TLP 是总线的主要故障。
在讨论积分和数据包重新排序时,我们将回到数据链路层。但为此,只要意识到经典的总线操作被通过PCIe结构传输的TLP所取代就足够了。
我还想提一下,内存写入 TLP 的数据有效载荷可能比单个 32 位字长得多,从而形成 PCIe 写入突发。TLP 的大小限制是在外设的配置阶段设置的,但典型数字为每个 TLP 的最大 128、256 或 512 字节。
在继续之前,值得注意的是,内存写入 TLP 的发送方不会收到数据包已到达其最终目的地的指示,更不用说它已经执行了。即使数据链路层得到肯定的确认,这仅意味着数据包安全地到达了附近的交换机。从未进行过端到端的确认,也没有必要。
我们以上面提到的数据写入案例为例,看看TLP的细节。假设 CPU 使用 0 位寻址将值 12345678x0 写入物理地址040xfdaff32。然后,数据包可以由四个 32 位字(4 个 DW,双字)组成,如下所示:
内存写入请求 TLP 示例
因此,数据包以 0x40000001、0x0000000f、0xfdaff040 0x12345678 的形式传输。
我们来解释一下颜色编码:
现在让我们简要说明有效字段:
现在让我们看看当 CPU 想要从外围设备读取数据时会发生什么。读取操作有点棘手,因为不可避免地会涉及两个数据包:一个从 CPU 到外设的 TLP,要求后者执行读取操作,另一个 TLP 返回数据。在PCIe术语中,我们有一个请求者(在我们的例子中为CPU)和一个完成者(外围设备)。
我们假设 CPU 需要来自地址 32xfdaff0 的单个 DW(040 位字)(与之前相同)。和以前一样,它可能会在与其内存控制器共享的总线上启动读取操作,该总线包含根复合体,而根复合体又会生成要通过 PCIe 总线发送的 TLP。这是一个读取请求 TLP,可能如下所示:
内存读取请求 TLP 示例
因此,此数据包由 3 个 DW 组成,0x00000001、0x00000c0f 0xfdaff040。它告诉外设在地址 0xfdaff040 处读取一个完整的 DW,并将结果返回给 ID 为 0x0000 的总线实体。
它与上面所示的 Write Request 示例惊人地相似,因此我将重点介绍其中的差异:
“长度”字段指示应读取一个 DW,“地址”字段指示从哪个地址读取。这两个 BE 字段保留与写入请求相同的含义和规则,只是它们选择要读取的字节,而不是要写入的字节。
当外围设备收到读取请求 TLP 时,它必须使用某种完成 TLP 进行响应,即使它无法完成请求的操作。我们将看一个成功的案例:外设从其内部资源中读取数据块,现在需要将结果返回给请求者(在本例中为 CPU)。
数据包可能如下所示:
完成 TLP 示例
因此,TLP 由 0x4a000001、0x01000004、0x00000c00 0x12345678组成。数据包基本上是说“告诉总线实体0x0000,其对实体0x0100的请求(标记为0x0c)的答案是0x12345678。CPU(或者实际上,内存控制器 = Root Complex)现在可以在其内部记录中查找该请求的内容,并完成相关的总线周期。让我们把它切成碎片:
顺便说一句,Completer 可能会返回切片成多个数据包的数据。然后,可以通过检查
Length == ((LowerAddress & 3) + ByteCount + 3) >> 2
如果我们碰巧在请求中将自己限制在 DW 粒度上,这只会变成
Length == ByteCount >> 2
这两个例子就这么多。现在更笼统地说。
如果我们将总线写入操作的生命周期与读取操作的生命周期进行比较,就会发现一个明显的区别:写入 TLP 操作是即发即弃的。一旦数据包形成并移交给数据链路层,就无需再担心它了。另一方面,读取操作要求请求者等待完成。在完成数据包到达之前,请求者必须保留有关请求是什么的信息,有时甚至保留 CPU 的总线:如果 CPU 的总线开始读取周期,则必须将其保持在等待状态,直到总线数据线上所需的读取操作的值可用。这可能是总线的可怕减速,这在最近的系统中是理所当然的。
即发即弃操作的术语,例如“内存写入即报告”操作。此类操作仅包含一个请求。当然,由“请求”和“完成”组成的操作称为“非报告”操作。
如上所述,读取和写入请求中给出的地址可以是 32 位或 64 位宽,使标头的长度为 3 或 4 DW。但是,PCIe 规范中的第 2.2.4.1 节指出,仅在必要时才必须使用 4 DW 标头格式:
对于小于 4 GB 的地址,请求者必须使用 32 位格式。如果接收到寻址低于 64 GB 的 4 位格式请求(即,地址的上限为 32 位均为 0),则不会指定接收器的行为。
实际上,任何外设的寄存器都很少映射在 4 GB 范围内,但 DMA 缓冲区很可能超出 4 GB 边界。因此,在设计新器件时,应支持具有 64 位寻址的读写 TLP。
PCIe 总线仅出于向后兼容性而支持 I/O 操作,强烈建议不要在新设计中使用 I/O TLP。原因之一是 I/O 空间中的读取和写入请求都是未发布的,因此请求者也被迫等待写入操作的完成。另一个问题是 I/O 操作仅采用 32 位地址,而 PCIe 规范通常支持 64 位。
由于PCIe本质上是一个分组网络,因此交换机可能会在路上,因此这些交换机需要知道将每个TLP发送到何处。有三种路由方法:按地址、按 ID 和隐式。按地址路由应用于内存和 I/O 请求(读取和写入)。隐式路由仅用于某些消息 TLP,例如来自根复合体的广播和始终转到根复合体的消息。所有其他 TLP 均按 ID 路由。
ID 是一个 16 位字,由众所周知的三元组组成:总线号、设备号和功能号。它们的含义与传统PCI总线中的含义完全相同。ID 的格式如下:
PCIe ID的形成
如果你运行的是 Linux,我建议你尝试 lspci 实用程序及其众多标志,以结识总线结构。
在PCIe之前,这个问题曾经有点令人毛骨悚然。毕竟,告诉 CPU 离开总线有一些侵入性,现在我正在主持节目。
在PCIe上,它的异国情调要小得多。它归结为一个简单的概念,即总线上的任何人都可以在总线上发送读写 TLP,就像根复合体一样。这允许外设直接访问 CPU 的内存 (DMA) 或与对等外设交换 TLP(在交换实体支持的情况下)。
与任何PCI设备一样,首先需要做两件事:需要通过在其中一个标准配置寄存器中设置“Bus Master Enable”位来授予外设总线主控权。第二件事是,驱动程序软件需要通知外设相关缓冲区的物理地址,最有可能是通过写入BAR映射寄存器来实现的。
PCIe 支持两种中断:传统 INTx 和 MSI。
支持 INTx 中断是为了与传统软件兼容,也是为了允许在经典 PCI 总线和 PCIe 之间桥接。由于 INTx 中断是电平触发的(即,只要物理 INTx 线路处于低电压状态,中断请求就处于活动状态),因此有一个 TLP 数据包表示该线路已被置位,另一个数据包表示该线路已被解除置位。这不仅本身很奇怪,而且 INTx 中断仍然存在一些老问题,例如中断共享以及每个中断处理例程都需要检查中断的真正对象。
正是由于这些问题,在(传统)PCI 2.2 中引入了一种新的中断形式 MSI。我们的想法是,既然几乎所有的PCI外设都具有总线主控功能,为什么不让外设通过写入某个地址来发出中断信号呢?
PCIe对生成MSI的作用完全相同:发出中断信号仅包括通过总线发送TLP,该TLP只是一个已发布的写入请求,具有一个特殊地址,主机在初始化期间已将其写入外设的配置空间。任何现代操作系统(当然包括 Linux)都可以调用正确的中断例程,而无需猜测谁生成了中断。如果外设不需要确认,则实际上也没有必要“清除”中断。