PCIe总线上面可以挂许多的设备,这些设备挂在Host Bridge下的Bridge或者Switch下,而对于设备来说,它的总线号(bus)是软件分配的,但设备号(device)以及功能号(function)都是硬件直接决定的。Bridge和Device是PCIe的两大类型设备,这里的桥从另一种方式来看也可以算一种设备,只不过桥只具备挂接设备的功能而已。而Bridge和Device的配置空间也是不一样的。如下图1与图2所示。配置空间的相关说明可以查看相关的文档了解,这里不多说。
图1-Device的配置空间 图2-Bridge的配置空间
所有的设备,只要存在,那么它的功能0一定是存在的。
一般来说Bridge或者设备都有可能具备多个功能呢,这些功能可能并不连续,对应的,两个挂在同一bridge下的设备的设备号也可能是不连续的,比如挂在同一bridge下的有两个设备,一个是设备2,另一个可能就是设备1F了。
而Switch,它是分线器,相当于usb的扩展口一样,据我的了解,它并不指一种类型,而是多个bridge的集合体,由于系统会为每个bridge都分配一个bus号,因此Switch下的设备的bus号可能是一样的,也可能是不一样的。
每个PCIE设备都有一个或多个function,这些设备可以通过switch来与其他的设备交互,也可以挂接在bridge上与CPU进行交互。
而这些设备都有自己的ROM/RAM/寄存器,是设备的内部Mem空间以及IO空间。
同时每个设备也都具备4k bytes的配置空间,配置空间的前256bytes是为了预留给兼容PCI的PCI兼容配置空间,而后面的4K-256bytes(这个是因为有PCI兼容配置空间,而设备的总配置空间是4k bytes,所以减去)的空间是PCIE扩展配置空间。
上面说了每个PCIE设备都有4k bytes的配置空间,那么系统就会为每个设备都分别分配一个4k bytes的内存空间。
而PCIE一共支持256条bus,32个device,8个function,假设负载全满的时候,内存分配的内存空间则是:4K * 256 * 32 * 8 = 256 * 1024K = 256 * 1M = 256M bytes。
映射图如下图3所示。
图3-设备空间在内存空间或IO空间的映射
对PCIE设备配置空间的地址映射有两种方式:内存映射以及IO映射,因此可以通过内存访问或者IO访问来访问其配置空间。
对于部分设备来说,比如Realtek网卡设备,它的配置空间可以映射到IO空间,但这种方式仅能访问前256 bytes的数据;也可以映射到内存空间,这种方式就都能访问到4k bytes的数据。
而对于Realtek网卡设备来说,它的内部IO空间仅仅映射在IO空间,而MAC等信息位于网卡设备的内部IO空间。因此想读取到MAC地址,有两种方式:
(1)可以通过IO端口访问配置空间前256 bytes,找出映射的IO空间的基地址,然后再进入到IO空间查看;
(2)也可以先通过内存访问的方式访问配置空间那256 bytes,找出映射的IO空间的基地址,然后再进入到IO空间查看。
然而,想访问前256 bytes的数据,则需要知道网卡设备的bus号、device号、function号,至于PCIe上存在哪些设备,则需要枚举PCIe上的设备,在知道bus号、device号、function号后才可以通过他们访问相关设备的配置空间,像Vendor ID是设备的厂商号,Device ID是设备的设备ID,Class Code则是哪种设备的分类寄存器。
那么比如在知道网卡的bus号、device号、function号后,如何访问网卡的配置空间呢,又是如何在知道了配置空间后读取IO空间或者Mem空间呢,其实很简单。
将bus号、device号、function号转换为相关的地址后,通过通过相关的函数就可以对配置空间进行读取操作了。转换原理如图4所示。
图4-转换原理
上图bus号、device号、function知道后,那么Doubleword指的是双字偏移地址,配置空间有256bytes的寄存器,而Doubleword仅占据6bit,6bit就意味着2^6=64的寻址范围,而双字就意味着4个字节,那么64*4正好是256个字节了,因此这个Doubleword指的是双字的地址。
PCI兼容配置空间通过映射到IO空间中,然后CPU可以通过IO端口(CF8/CFC端口,一个指定地址,一个指定数据)来访问这部分内容。
//此处代码会读取相关地址处起的32位数据
Address = BIT31|((BUS & 0XFF)<< 20)|((DEV & 0x1F)<<15)|((Fun & 0x7) << 12) | 偏移;
IoWrite32(0xCF8, address); //将要读取的地址写入到CF8
Date32 = IoRead32(0xCFC); //从CFC端口读出地址上的数据
//或使用这种方式
UINT32 BDFConvertTOAddress(UINT32 Bus , UINT32 Device , UINT32 Function , UINT32 Offset){
return Bus*0x100000 + Device*0x8000 + Function*0x1000 + Offset;
}
BIT31是保留位,默认为1,是配置空间映射的使能位。IO访问的方式只能读取前256bytes。
每个PCIE功能都有自己的4k扩展配置空间,它们的前256bytes可以映射到IO空间,也可以映射到内存中,而4k-256bytes只能被映射到内存中,映射到内存上的配置空间需要通过相应的内存基地址+(bus+device+function转换地址)+偏移进行访问。
内存基地址是指PCIe总线的基地址,在edk2或者AMI bios代码中都有相应是函数来获取它。比如PciExpressLib中就有相应是函数,如下图所示的函数可以获取PCIe总线的基地址。
下图的函数内置了上图获取基地址的函数,因此可以直接使用该函数传入由bus、device、function、Doubleword转换来的地址进行访问设备的配置空间。
而在获取了设备的配置空间后,配置空间里有6个base address寄存器可供使用,它们正是设备内部的IO空间以及内部内存Mem空间的基地址。在知道这些地址后,即可通过IO方式或者内存访问方式访问它们。
上面提到的设备是如何枚举的呢,由于PCIe只支持至多256条bus,32条device,8个function,因此最简单的是通过三个for循环,直接检索,判断该设备存不存在(Vendor ID为0xFFFF代表设备不存在,否则存在),存在的话记下该bus、device、function即可使用它们去访问配置空间。
但实际上,刚开始系统是只知道bus0的,它是通过一步步往下寻找才能找到其他设备的,这种寻找设备的方法据我了解其实和树的前序遍历是一模一样的,但值得注意的是这棵树并不是二叉树。
最后提一下,其实还可以通过使用Protocol的方式来读取配置空间,这个方式是更简单的。