PCI-Express-Technology(三)
正如PCI一样,每个PCIe功能(Function)的标识在其所在的设备内,以及这个设备所连接的总线内,都是唯一的。其标识符一般被称为“BDF”。对于任意一个 PCIe 拓扑结构,配置软件负责检测出拓扑中的每个Bus、Device和Function,缩写为BDF。接下来的几节将会结合一个PCIe拓扑的示例,来讨论BDF的主要特征。图 3‑1展示了一个PCIe拓扑结构,图中着重标识了示例系统中的Buses、Devices和Functions。本章后续内容将解释总线编号和设备编号分配的过程。
图 3‑1示例系统
软件总共可以分配256个总线编号。第一个总线号,Bus 0,通常由硬件分配给RC(Root Complex)。Bus 0由一个集成有EP的虚拟PCI总线,一到多个虚拟PCI-to-PCI Bridges(P2P)组成。其中的P2P Bridges拥有不可更改、硬件编码(hard-coded)的设备号和功能号。每个P2P Bridge都会产生一个新的总线,其他PCIe设备可以连接在到这些新产生的总线上去。每个总线都必须被分配一个唯一的总线号。配置软件分配总线号的过程中,首先从Bus 0/Device 0/Function 0开始搜索其他的Bridges。当找到一个Bridge之后,软件就给这个Bridge产生的新总线分配一个与上一级总线的总线号不同的、数字更大的编号。一旦新总线被分配了一个总线号之后,软件就会从新总线继续搜索更新的Bridges,而不是在上一级总线上继续搜索。这被称为“深度优先搜索(depth first search)”,关于这种搜索的细节内容,请参阅“Enumeration – Discovering the Topology”一节。
PCIe允许在单个PCI总线上最多挂载32个设备,然而PCIe点对点(point-to-point)的性质意味着只有一个设备可以直接连接在PCIe链路上,也就是Device 0。但是,通过RC和Switches包含的虚拟PCI总线。更多的设备可以“连接”到总线上。
每个设备都必须实现Function 0,其最多可以有8个功能(Function)。当一个设备拥有2个或以上的Function时,称之为多功能设备(Multi-Function Device)。
正如此前所讨论的一样,Function被设计为每个设备中之内的一个逻辑层次。这些Function可能包含硬盘驱动接口、显示控制器、以太网控制器、USB控制器等等。多Function的设备不需要依次按照编号逐个实现 Function。例如,一个设备可以只实现Function 0、2、7。因此,当配置软件检测到了一个多Function设备时,必须检查所有可能的Function,以了解当前Device存在哪些Function。每个Function都有它们自己的配置地址空间,这个配置地址空间用于设置与Function相关的资源。
初期的PC需要用户设置开关和跳线来给每个安装上去的板卡分配资源,这样的方法经常会导致memory、IO和中断的设置出现冲突。这之后出现的两种IO架构——扩展ISA(EISA,Extended ISA)和IBM PS2系统,是初次实现了即插即用(plug and play)的架构。在这些架构中,配置文件随每个插件卡一起被提供,允许系统软件分配基本资源。PCI扩展了这种配置特性,它通过实现标准化的配置寄存器,允许通用的Shrink-Wrapped(压缩包装)操作系统管理几乎所有的系统资源。PCI 在拥有了一个标准化的方式来进行错误报告的开启与关闭、传送中断、进行地址映射以及更多其他的操作之后,就可以通过配置软件这一单一模块,来进行系统资源的分配和配置,这消除了几乎所有的资源冲突。
PCI为每个Function都定义了一个专用的配置地址空间块。映射在这个配置空间中的寄存器们使得软件可以发现这个Function的存在,并对这个Function进行一般操作和状态检查。大多数需要标准化的基本功能都存在于配置寄存器块的Header中,但是PCI架构师意识到若将可选功能(option feature,区别于前面的基本功能)也标准化可以带来很多好处,这些可选功能标准化后的结构被称为“能力结构”,(capability structures,例如电源管理、热插拔等等)。
对于每个Function,都含有256byte的PCI兼容配置空间(PCI-Compatible configuration space)。
在阅读下面的讨论内容时,请同时参阅图 3‑2。之所以将这256Byte命名为PCI-compatible configuration space(PCI兼容配置空间),是因为这些配置空间原本就是为PCI所设计的。这个配置空间的前16DW(64bytes)是配置头部(header),有两种类型的Header,分别为Type 0和Type 1。Type 0 Header对于每个Function都是必须含有的,除了Bridge,对于Bridge Function来说它使用的是Type 1 Header。剩余的48DW是一些可选寄存器,包括PCI能力结构(capability structure)。对于PCIe Functions而言,一部分 capability structure也是必须的。例如PCIe Function就必须实现如下的能力结构:
-
PCI Express能力(PCI Express Capability)
-
电源管理
-
MSI、MSI-X
在阅读下面的讨论内容时,请同时参阅图 3‑3。当引入PCIe之后,最初始的256byte配置空间已经不足以放下所有新需要的Capability Structure了。因此配置空间的大小从原先的每个Function 256Byte扩展至了每个Function 4KByte。新增加出来的960DW扩展配置空间只能通过增强配置机制(Enhanced configuration mechanism)来进行访问,因为传统的PCI软件无法发现这个区域并进行访问,所以这部分区域对于 PCI 是不可见的。在扩展配置空间内包含了新增加的PCIe可选扩展能力寄存器(Extended Capability register),图 3‑3罗列出了一部分扩展能力寄存器。
对Host-to-Bridge配置寄存器的访问不必使用前面的章节所提到的配置机制,在内存地址空间中,它通常映射为设备特定寄存器,这对平台固件是已知的。然而,它的配置寄存器格式排布和用法都必须遵从PCI 2.3协议规范中所定义的Type 0模板。
在协议规范中声明了,只有RC可以发起配置请求。RC作为 CPU 与 PCIe 拓扑的联络员,其传入 CPU 的 PCIe 请求包并在 PCIe 事务完成后向处理器报告。之所以限制 CPU 只能通过RC发起配置事务,是因为要是其他设备也有这种能力,那么他们可以任意改变配置内容,这样就会带来混乱。
由于只有RC能发起这些配置请求,所以这些配置请求只能在拓扑中向下转发,意味着Peer-to-Peer的配置请求是被无法实现的。
请求包使用目的设备ID作为路由信息进行路由转发,这个目的设备ID就是它的BDF(拓扑中的Bus编号,Bus上的Device编号,Device中的Function编号)。
处理器一般无法直接进行配置读写请求,因为他们只能产生memory请求和IO请求。这意味着RC需要将 CPU 的其他类型访问转换成配置请求,这样才能进行配置的操作过程。配置空间可以通过以下两种机制进行访问:
-
传统的PCI配置机制,使用IO间接访问(IO-indirect access)
-
增强型配置机制,使用内存映射访问(memory-mapped access)
在PCI协议规范中定义了IO-indirect方法,用来指示系统(RC或其等效的组件)进行PCI配置访问。在当时的历史背景下,占主导地位的PC处理器(Intel x86)被设计为仅能寻址64KB的IO地址空间。在 PCI 协议产生的时候,这个有限的IO空间已经变得非常混乱,只有少数几个可用的地址范围:0800h-08FFh,和0C00h-0CFFh。因此,将所有可能的Function的配置寄存器都映射到IO空间是不可行的。与此同时,内存地址空间也十分有限,将这些配置空间都映射进内存地址空间也不是个好方法。于是,协议规范的作者使用了一种常用的方法来解决这个问题,使用间接地址映射(indirect address mapping)。为此,需要使用一个寄存器保存目标地址,同时用第二个寄存器保存来自或是发往目标的数据。一次对目标Function的一次读写事务,需要先将待访问的地址写入地址寄存器,随后再读写数据寄存器。这很好的解决了地址空间有限的问题,但是这意味着产生一次配置访问需要两次IO访问。
PCI兼容机制使用RC的Host Bridge中的两个32bit的IO端口。它们分别是配置地址端口(Configuration Address Port),位于IO地址区域0CF8h-0CFBh,和配置数据端口(Configuration Data Port),位于IO地址区域0CFCh-0CFFh。
要访问一个Function的PCI兼容配置寄存器,首先要将目标的Bus、Device、Function和DW号写入配置地址端口,并将其使能bit置为有效。然后第二步,一个1或2或4Byte的IO读或写将会发送到配置数据端口。RC的Host Bridge将对给定的目标总线和在Bridge下现存的总线范围进行比较。若目标总线在这个范围内,这个Bridge则会发起配置读或写请求(这取决于对配置数据端口的IO访问是读操作还是写操作)。
配置地址端口仅在处理器对其完成一个完整的32bit写操作时,锁存住写入的信息,如图 3‑4,若对这个端口进行读操作则会返回它的这些内容。写入配置地址空间的信息必须遵照下面所描述的格式(图 3‑4)。
图 3‑4地址位于0CF8h的配置地址端口
-
Bits[1:0]固定为0不变,且只读的,在读取时只能返回0。它的位置是DW对齐的,不允许指定字节(byte-specific)偏移量。
-
Bits[7:2]用于标识目标Function的PCI兼容配置空间内的目标DW(target dword,其也被称作寄存器号),也就是用来标识Function内的寄存器号,要求这个寄存器必须位于兼容配置空间中。这种机制仅限于兼容 PCI 的配置空间中使用。(例如一个Function配置空间的前64DW)。
-
Bits[10:8]用于标识目标Device内的目标Function号(0-7)。
-
Bits[15:11]用于标识目标Device号(0-31)。
-
Bits[23:16]用于标识目标Bus号(0-255)。
-
Bits[30:24]为保留字段,必须为0。
-
Bit[31]为使能位,若要将随后的对配置数据端口的IO访问转换为配置访问则必须要将该bit置为1。当该bit为0时,若有IO读或者IO写被发送到配置数据端口,那么这些事务都会被当成普通的IO请求来处理。
如图 3‑5,RC内的Host Bridge实现了一个次级总线号(Secondary Bus Number)寄存器和一个从属总线号(Subordinate Bus Number)寄存器。次级总线号(图 3-5 中的 Sec)指的是当前Bridge下直接连接的总线的编号,例如图中RC的Host/PCI Bridge产生了Bus 0,因此Host/PCI Bridge的次级总线号就是0。从属总线号(图 3-5 中的 Sub)指的是Bridge之下的可作为目标总线的总线号,例如图中Device 1的Sub=9,那么就说明在Device 1下游最大的总线号是 Bus 9,而它的Sec=5则说明其下游的总线号编号从 Bus 5 开始,简单来说就是Sec和Sub共同指定出了这个设备下可访问总线的范围(对于Device 1来说就是5-9)。
在一个单RC的系统中,Host-Bridge的次级总线号应该被固定为0,也就是它的可读可写的次级总线号寄存器从一复位就被强制置为0,或者说,Host-Bridge知道它访问到的第一个总线一定是Bus 0。若配置地址端口(Configuration Address Port,见图 3‑4)的bit 31被置为1,那么Bridge就会将目标总线号与Bridge之下的从属总线范围进行比较,以此来检查这个目标总线是否从属于当前Bridge之下。
当Bridge收到一个请求,它将会评估目标总线号是否在其下的从属总线号范围内,这个范围大于等于从次级总线号(Sec),小于等于从属总线号(Sub)。若目标总线号与当前的次级总线号相匹配,那么说明当前的次级总线就是目标总线,这个请求就会被作为一个Type 0配置请求来传输。当设备们收到一个Type 0请求,它们就知道当前一级本地总线上的某个设备就是这个请求的目标设备(而不是在更下一级的总线所属的设备)。
若目标总线号要大于Bridge的次级总线号(Sec),但是小于或者等于Bridge的从属总线号(Sub),那么这个请求将会作为Type 1配置请求被转发到Bridge的次级总线上。对于一个Type 1配置请求,可以这样理解它:尽管这个请求需要经过这条总线,但是它的目标设备并不在这一级总线上,相反地,这个请求将会由当前总线上的Bridge们向下转发到各自的下一级总线上,当然转发该Type 1配置请求的Bridge必须是从属总线范围包含了目标总线的。所有,Type 1配置请求只对Bridge设备有作用。更多关于Type 0和Type 1配置请求的信息,请参阅“Configuration Request”一节。
RC中的Host/PCI Bridge会将写入配置地址端口(Configuration Address Port)的信息锁存起来,如图 3‑1。若bit 31被置为1且目标总线在当前Bridge下方从属总线范围内,那么Bridge将把接下来的处理器对配置数据端口(Configuration Data Port)的访问转换成针对Bus 0的配置请求。处理器将会向配置数据端口(0CFCh)发起一个IO读请求或者一个IO写请求。这促使Bridge生成一个配置请求,这个配置请求是读请求还是写请求取决于IO访问是读还是写。若目标总线为Bus 0,那么它将是一个Type 0配置请求。若目标总线是从属总线范围内的其他总线,那么它将是一个Type 1配置请求。若目标总线不在从属总线范围内,那么这个Bridge将不会对这个请求进行转发操作。
图 3‑5 单Root系统
若在一个系统中存在多个RC,如图 3‑6所示,那么配置地址端口和配置数据端口将被这两个RC各自的Host/PCI Bridge复用,且两种配置端口各自的IO地址在两个Host/PCI Bridge中相同,也就是两个RC使用相同的一个IO地址来访问各自配置地址寄存器,同理,访问配置数据寄存器也是。为了防止争用,在两个Host/PCI Bridge中同时只能有一个响应处理器对配置端口的访问。
-
当处理器发起对配置地址端口的IO写操作时,通过配置两个Host Bridge( the host bridges are configured, //TODO),仅有一个Host Bridge会参与到此次事务中来。
-
在枚举过程中(Enumeration),软件将搜索所有活跃(active)Bridge之下的所有总线,并对这些总线进行编号。当这一总线搜索过程完成后(不是枚举过程完成),软件将使能那个不活跃的Host Bridge,并给它赋予一个总线编号,这个总线编号会在之前那些活跃Bridge的总线编号范围之外,然后软件继续进行枚举过程。这两个Host Bridge都会看见请求,但是由于它们二者拥有相互不重叠的总线编号范围,而它们各自又只会对自己的范围内的请求作出响应,因此并不会存在冲突的情况。
-
在上面的过程之后,两个Host Bridge都会收到对各自配置地址端口的访问,而随后进行的的对配置数据端口的读访问或写访问,则仅会被包含目标总线的Host/PCI Bridge所接收。接收访问的Bridge将会响应处理器的事务,而另一个Bridge则会忽略这个事务。
- 若目标总线就是次级总线,那么Bridge将把这个访问转换成Type 0配置访问。
- 否则,这个访问将被转换成Type 1配置访问。
编者注:上文中的两个 RC 是对应于图 3-6 的情况,更一般的情况中会存在 1-N 个 RC。
图 3‑6多Root系统
当协议制定者在选择PCI-X和之后的PCIe该如何访问配置空间时,有两个考虑。第一个,每个Function的256Byte空间限制了那些想要在这个空间内放一些专有信息的厂商,而且未来的协议制定者可能也需要更多的空间来放置更多的能力结构(capability structure)。为了解决这个问题,这个空间从每个Function 256Bytes扩展至4Kbytes。第二个要考虑的是,PCI协议应用的时代多处理器系统还很少。对于旧的模型来说,当系统中仅有一个CPU且其只有一个线程时,生成一次访问需要两步并不会有什么问题。但是对于使用多核多线程CPU的计算机来说,使用IO间接访问模型就会产生一些问题,因为没有机制能够阻止多线程同一时间访问配置空间。因此,在没有线程锁机制(lock semantics)时不再使用“2步”模型(IO间接访问)。在没有线程锁机制的情况下,当线程A往配置地址端口(CF8h)写入一个值后,在它继续对配置数据端口(CFCh)做相应的操作之前,并没有一个机制来防止线程B将这个值覆盖掉。
为了解决这个新问题,协议制定者决定采用一个不同于IO间接访问的方法。他们不再尝试去保留地址空间,而是通过将所有配置空间都映射到内存地址,以此来创造出一个单步(single-step)、不可中断(uninterruptable)的访问过程。由于一个针对特定地址范围的memory请求会在总线上产生一个配置请求,所以这种访问方式只需要一个命令序列即可。这种方式带来的开销考量(trade-off)在于地址大小。每个Function映射地址空间需要4KB,实现最大数量的 function 需要256MB的memory(内存)地址空间。由于现代体系结构一般支持36到48位的物理内存地址空间,所以相较于现在的内存地址空间的总大小,256MB的空间占用很少。
为了处理地址映射,每个Function的4KB配置空间都以一个4KB对齐的地址作为起始地址,且这些4KB配置空间所映射的地址都要位于那段为了配置访问而预留的256MB内存地址空间内。并且现在的地址中的bit还携带了识别信息(identifying information),用于表示哪一个Function是访问目标(见表 3‑1)。
表 3‑1增强型配置机制的内存映射地址范围
如果一个访问跨了dword地址边界(跨越了相邻的两个内存DW的边界,即操作长度大于了一个DW,或者是操作长度等于一个DW但是起始地址没有位于DW对齐的地址),那么RC可以不支持它访问增强型配置内存空间。RC们也不需要支持某些总线锁定协议(bus locking protocol),一些处理器使用这些总线锁定协议来实现原子操作或是不可中断的一系列命令。软件在访问配置空间时,需要避免上述这两种情况,除非软件知道RC可以支持它们。
Bridge为了响应配置请求,将会产生两种类型的配置请求,分别为Type 0和Type 1。具体产生哪一种类型的配置请求,取决于目标总线号是否与当前Bridge的次级总线号(Secondary Bus Number)相匹配。下面将进行讲解。
Bridge为了响应配置请求,将会产生两种类型的配置请求,分别为Type 0和Type 1。具体产生哪一种类型的配置请求,取决于目标总线号是否与当前Bridge的次级总线号(Secondary Bus Number)相匹配。下面将进行讲解。
如果目标总线号与次级总线号相匹配,Bridge将产生一个Type 0配置读/写,并转发到它的次级总线上,并:
-
总线上的设备们检查配置请求中的Device Number(设备号),看谁才是这个配置请求的目标设备。需要注意,外部链路(external Link)上的EP总是Device 0。
-
被选中的设备检查Function Number,看看设备内的哪个Function被选中。
-
被选中的Function使用Register Number字段域来选择它配置空间中的DW,这个DW就是目标DW,并使用First Dword Byte Enable字段域来选出这个DW中要被进行读写操作的字节们。
如图 3‑7中给出了Type 0配置写请求和读请求的Header格式。在读写两种情况下,Type字段都是00100,而Format(Fmt)字段则用于指示是读请求还是写请求。
如图 3‑7中给出了Type 0配置写请求和读请求的Header格式。在读写两种情况下,Type字段都是00100,而Format(Fmt)字段则用于指示是读请求还是写请求。
图 3‑7 Type 0配置读请求和写请求的Header
当Bridge得到的配置请求的目标总线号并不是自己的次级总线号,但是目标总线号又在从属总线(Subordinate Bus)的范围内,那么Bridge将向次级总线转发一个Type 1请求包。次级总线上的非Bridge的设备(EP)将会忽略Type 1请求,因为这个请求的目标总线并不是当前总线。但是次级总线上的Bridge看到Type 1请求则将会进行与上一级Bridge相同的比对操作,当目标总线在从属总线范围内时:
-
如果目标总线就是当前Bridge的次级总线,这个请求包将从Type 1被转换成Type 0,并转发到次级总线上。次级总线上的本地设备将会像此前描述的一样检查请求包的Header。
-
若目标总线号不是当前Bridge的次级总线,但是在从属总线范围内,请求包将会继续以Type 1请求的格式被转发到当前Bridge的次级总线上。
如图 3-8 展示了Type 1配置读请求和写请求的Header格式。在读写两种情况下,Type字段域都是00101,而Format(Fmt)字段则用来指示这是读还是写。
图 3‑8 Type 1配置读请求和写请求的Header
请参阅图 3‑9。
译者:在此图中纠正了原书版本的一个错误(该错误为errata中的第1条),Bus 6的中间的Bridge的Sub应该为9而不是8,原图中为8。
为了更好说明传统的CF8h/CFCh机制来产生配置请求,请参考下面的x86汇编代码,这段代码使得RC执行一个2byte的读操作,操作目标为Bus 4,Device 0,Function 0,Register 0(这个Register 0就是厂商Vendor ID)。
mov dx,0CF8h ;将dx设置为配置地址端口的地址
mov eax,8004000h ;enable bit=1,bus 4,dev 0,func 0,DW 0
out dx,eax ;IO写来设置地址端口
mov dx,0CFCh ;将dx设置为配置数据端口的地址
in ax,dx ;从配置数据端口读出2byte数据
代码中的out指令将产生一个IO写操作,从处理器写入配置地址端口,这个配置地址端口位于RC的Host Bridge(0CF8h),如图 3‑4。
-
Host Bridge将配置地址端口中的目标总线号(这里是4)与Bridge下方的从属总线范围(这里是0到10)进行比较。很明显目标总线号在从属总线范围内,因此Bridge就准备好了接下来产生配置请求的目的地。
-
代码中的in指令将产生一个IO读事务,读事务产生自处理器并发往RC Host Bridge中的配置数据端口。这是一个对配置数据端口的前两个byte进行读取的读事务。
-
由于目标总线号不是bus 0,所以Host/PCI Bridge对bus 0发起一个Type 1配置读请求。
-
bus 0上的所有设备都会锁存这个事务请求,并发现它是一个Type 1配置请求。因此RC中的两个虚拟PCI-to-PCI Bridges各自都将Type 1请求中的目标总线号与下方的从属总线范围进行比较。
-
目的地bus 4位于左侧Bridge的下方从属总线范围中,所以左侧Bridge将把这个请求数据包发送到它的次级总线上,但是这个请求仍然是一个Type 1请求,因为目标总线并不是这个左侧Bridge的次级总线。
-
图左侧的Switch的上方端口将接收到这个请求数据包,并将它传送给Switch中靠上方的PCI-to-PCI Bridge。
-
这个Switch中靠上方的PCI-to-PCI Bridge确定目标总线在自己下方,但是并不是自己的次级总线,因此它将请求数据包继续以Type 1请求的格式转发到bus 2上。
-
bus 2上的两个Bridge都接收到了Type 1请求包。偏右侧的Bridge确定目标总线就是自己的次级总线。
-
bus 2上偏右侧的Bridge将这个配置读请求发送到bus 4上,而且这次会将原本为Type 1的请求转换成Type 0配置读请求来发送,这是因为这个请求的目标总线就是当前Bridge的次级总线。
-
bus 4上的Device 0接收到这个请求包,并对数据包中的目标Device号、Function号、Register号的字段进行译码,以此来选中配置空间中的目标DW。(如图 3‑3)
-
First Dword Byte Enable字段中的bit 0和bit 1都被置为有效,因此Function将在完成包中返回它的前两个字节(前两个字节也就是Vendor ID)。然后完成包使用从Type 0请求包中获得的Requester ID作为路由信息,通过路由转发,最终回到Host Bridge。
-
读出来的2byte数据将会呈交给处理器,以此来完成in指令的执行。Vendor ID的值被放置在处理器的AX寄存器中。
请参阅图 3‑9。
下面的x86汇编代码将使得RC执行一个2byte的读操作,操作目标为Bus 4,Device 0,Function 0,Register 0(这个Register 0就是厂商Vendor ID)。在这段代码工作之前,Host Bridge必须已经被分配了一个基地址(base address value)。在这个例子中,我们假设增强型配置内存映射范围(Enhanced Configuration memory-mapped range)的256MB对齐基址为E0000000h。
mov ax,[E0400000h] ;内存映射配置的读操作
-
地址[63:28]表示整体增强型配置地址范围的256MB对齐的基地址的高36bit,在本例中,这个整体增强型配置地址范围为00000000-E0000000h。
-
地址[27:20]用于选择目标bus(本例中为bus 4)
-
地址[19:15]用于选择bus上的目标device(本例中为device 0)
-
地址[14:12]用于选择device内的目标function(本例中为function 0)
-
地址[11:2]用于选择function配置空间中的目标DW(本例中为DW 0)
-
地址[1:0]用于定义选中的DW中的起始byte的位置(本例中为byte 0)
处理器发起一个2byte的memory read,读取的起始地址为E0400000h,这个地址会被RC中的Host Bridge锁存下来。Host Bridge识别到这个地址位于用来进行配置的区域之内,它将产生一个配置读请求,读取的位置为Bus 4—Device 0—Function 0—dword 0—首两个字节。剩下的操作就跟前面的小节中所讲的一样了,也就是如何把读出来的信息通过完成包返回给Host。
图 3‑9配置读访问示例
在完成了系统上电或是复位之后,配置软件需要扫描PCIe网络结构,来搜索发现整个机器的拓扑,并学习这个网络结构是如何被填充的(例如里面都有多少总线、多少设备以及它们的编号等等)。在这进行之前,如图 3‑10所示,软件唯一知道的就是拓扑中有一个Host/PCI Bridge以及这个Bridge的次级总线Bus 0。需要注意,一个Bridge自身上方相连的总线称为主总线(Primary Bus),而这个Bridge自身下方相连的总线称为次级总线(Secondary Bus)。扫描PCIe结构来发现整体拓扑结构的过程被称为枚举过程。
图 3‑10刚启动时软件所认为的拓扑结构
处理器上运行的配置软件发现一个Function的方式一般是读取这个Function的Vendor ID寄存器。PCI-SIG给每个厂商都分配了一个唯一的Vendor ID,每个厂商都会将自己设计的Function中的Vendor ID寄存器的值固定为自己的Vendor ID。通过读取整个系统中的Bus—Device—Function这三者所有组合中的Vendor ID寄存器,枚举软件可以搜索遍整个拓扑,并得知有哪些设备存在。这个过程相当的简单,但是可能会出现两个问题:目标设备可能不存在,或者它虽然存在但是没有准备好响应事务请求。下面将介绍如何处理这两种情况。
目标设备在系统中不存在的这个情况,可能会在枚举搜索过程中发生很多次。当它发生时,我们需要对它有正确的理解。在PCI中,配置读请求(Configuration Read Request)有可能在总线上执行超时(timeout),那么就会产生一个主设备放弃(Master Abort)的错误情况。由于没有设备来驱动总线,所有的信号都是拉高的,那么数据位在总线上就是全为1的,这个全为1的值将成为总线上被看见的数据值。这使得值为FFFFh的Vendor ID并没有被分配出去,而是作为一个保留值。如果枚举软件收到的读取Vendor ID的结果是FFFFh,那么它就知道它读取的设备不存在。由于这种情况其实并不是一种错误情况,因此在枚举过程中,Master Abort并不会被当做是一个错误来进行报告。
对于PCIe来说,针对一个不存在的设备的配置读请求将使得目标设备上方连接的Bridge返回一个不携带数据的完成包,这个完成包的状态字段将被置为UR(Unsupported Request,不被支持的请求)。为了向后兼容传统的枚举模型,若RC在枚举过程中收到这样的完成包,它将会给处理器返回数据FFFFh。注意,枚举软件是依赖于接收到一个返回值为全1(FFFFh)的配置读请求来判定目标设备是不存在的,而系统中的在读取这样一个不存在的设备Function的Vendor ID时,其实是返回了一个状态为Unsupported Request的不携带数据的完成包。
当出现设备不存在的情况时,也需要避免意外地发送错误报告,这很重要。尽管这种超时或者UR的结果在系统正常运行是确实是出错的情况,但是在枚举过程中它并不能被认为是错误,它是一种正常的可以预料到的结果。为了能更简单的避免这种混乱,设备们一般在这过程中都不启用错误信号,直到枚举完成才启用。对于PCIe来说,记录这种事件(目标设备不存在)仍然是有作用的,这也就是为什么PCIe能力寄存器块(PCIe Capability register block)中存在第4个“错误”状态位,被称为Unsupported Request Status,不受支持的请求状态(更多关于这部分的内容请见“Enabling/Disabling Error Reporting”一节)。由于有这个状态位的存在,发生目标设备不存在的情况就可以被记录下来,而且不会被当做是一个错误,这一点非常重要,因为若检测到一个错误那么枚举过程将被停止并调用系统错误处理程序。在枚举还没进行完的这个时间段,错误处理软件的能力可能十分有限,使得问题无法被解决。这将造成枚举软件运行失败,因为通常是在操作系统(OS)或者其他错误处理软件可用之前就已经执行枚举软件。为了避免这种风险,在枚举期间,通常不应该报告错误。
前面提到的可能会发生的另一个问题就是目标设备虽然存在,但是可能并未准备好响应配置访问。对于配置操作来说,需要考虑发起配置的时间点,因为设备准备好被访问是需要时间的。如果数据速率小于等于5.0GT/s,软件必须在复位后等待100ms再发起配置请求。如果数据速率高于5.0GT/s(Gen3速率),软件必须在链路训练(Link Training)完成100ms之后再尝试发起配置操作。之所以更高的速率需要更多的延时(Gen3需要在链路训练完成后等待100ms而不是复位完成等待100ms),这是因为Gen3的链路训练中的均衡过程(Equalization Process)可能需要比较长的时间(约50ms,关于这里的更多内容请参阅“Link Equalization Overview”一节)。
在PCI 2.3中定义了初始化时间(Initialization Time,Trhfa-从复位释放到第一个访问所经历的时间),初始化时间起始于RST#被置为无效,结束于225个PCI时钟周期之后。在这段长度为一秒的时间内,Function正在为响应第一次配置访问做准备,这个时间长度也被PCIe协议所使用,协议规定初始化时间为1.0s(+50%/-0%)。一个Function可以利用这段时间来填充自己的配置寄存器,例如加载一个外挂的串行EEPROM的内容做为寄存器初始值。加载EEPROM内容需要花费一点时间,加载完成前Function都没有准备好响应配置请求。
在PCI中,若在Function准备好之前就收到了一个配置访问,那么它有三个选择:忽略这个请求、重试(Retry)这个请求、接收这个请求但是延期响应直到Function自身完全准备好为止。最后一种选择的响应可能会给热插拔(Hot-plug)系统带来麻烦,因为延期响应的时长可能会达到1s,在这1s中总线都是停止的,直到这个请求被执行完,总线才重新工作。
在PCIe中我们也面临着相同的问题,但是问题的过程有一点不同。首先,PCIe Function在临时无法响应配置访问时必须要给出一个完成包,这个完成包也要被置为指定的状态,这个状态为Configuration Request Retry Status(CRS,配置请求重试状态)。这个状态只有在响应配置请求的时候才是合法的,如果其他的请求收到了这个状态的完成包,则可能会认为数据包格式错误(Malformed Packet error)。这种状态的响应也只在复位后的1秒后有效,因为在1秒之后Function就被认为是可以正常响应配置请求了,如果1秒之后这个Function都还无法正常响应配置请求,那么就认为它已经损坏了。
除了系统复位之后的那一段时间之外,RC处理配置读请求的CRS(配置重试状态)完成包的方式不是通用的。在系统复位后的那一小段时间里,RC有两个可以进行的操作供选择,选择哪一种则取决于Root控制寄存器(Root Control Register)中CRS Software Visibility这一bit的值(如图 3‑11)。
图 3‑11 PCIe能力块中的Root控制寄存器
-
如果这个bit被置为1,而且发出的请求是配置读请求,要读取Vendor ID寄存器的全部两个byte(枚举过程通过这样的操作来搜索发现一个Function是否存在),那么在接收到CRS完成包时RC需要给Host返回伪造的0001h作为读取的寄存器值,并将其他的所有字节都置为全1。这个Vendor ID并没有被任何真实的设备所使用,软件将把这个0001h的Vendor ID理解为访问这个设备可能需要很长的延迟。这个信息很有用,因为软件就可以选择去执行其他的任务,更充分的利用这段等待设备响应的时间,稍后再返回来查询这个设备。要进行这样子的操作,软件必须确保其在复位后对Function的第一次访问就是读取2 byteVendor ID的配置读访问。
-
对于配置写访问或者是其他的配置读访问(并不是读2byte Vendor ID),RC必须自动地以一个新请求的形式对之前的配置请求进行重新下达。
枚举过程的一个关键部分就是可以确定一个Function是Bridge还是EP。如图 3‑12所示,Header类型寄存器(Header Type Register,位于配置空间Header的偏移地址0Eh)的低7bit用于标识这个Function的基本种类,总共定义了三种不同的值:
-
0 = 不是一个Bridge(那也就是一个PCIe EP)
-
1 = PCI-to-PCI Bridge(缩写为P2P),用于连接两条总线
-
2 = 插件卡Bridge(CardBus Bridge,现在很少使用的历史遗留接口)
在图 3‑1中,每个虚拟P2P的Header Type字段(DW3,byte2)的返回值都为1,且PCI Express-to-PCI Bridge(Bus 8,Device 0)的Header Type字段也将返回1。但是对于EP来说,它返回的Header Type字段是0。
图 3‑12 Header类型寄存器
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)