一、概述
PCI架构支持三种地址空间,如图1-10所示:内存地址空间(Memory Map)、I/O地址空间(I/O Map)和配置地址空间(PCI Configure Space)。在x86处理器中,处理器可以直接访问内存和I/O空间。PCI设备可以映射到处理器的内存地址空间,支持32位或64位内存寻址。在I/O地址空间中,PCI支持32位地址,但由于x86 CPU只使用16位I/O地址空间,许多平台将I/O空间限制为64KB(16位地址的容量)。
此外,PCI引入了第三种地址空间,称为配置空间,该空间只能通过间接方式访问。每个功能包含内部寄存器,用于配置空间的管理。这些寄存器为软件提供了标准化的地址和资源控制,使得PC真正实现了“即插即用”环境。每个PCI功能最多有256字节的配置地址空间。PCI支持每个设备最多8个功能、每条总线最多32个设备、每个系统最多256条总线,因此系统的总配置空间量为:
256 Bytes/function x 8 functions/device x 32 devices/bus x 256 buses/system = 16MB。
由于x86 CPU无法直接访问配置空间,必须通过I/O寄存器间接访问。传统模型中,如图1-10所示,使用位于地址CF8h-CFBh的I/O端口,称为配置地址端口,以及映射到地址CFCh-CFFh的配置数据端口。
需要注意的是,在PCI Express中,引入了一种新的方法,通过将配置空间映射到内存地址空间来访问配置空间,从而简化了访问过程。
二、PCI内存地址空间
内存地址空间是最常用的地址空间之一,PCI设备可以将其资源(如设备寄存器、缓冲区等)映射到处理器的内存地址空间中,使CPU可以通过标准的内存读写指令访问PCI设备。PCI设备可以支持32位或64位内存寻址,这意味着它们可以使用32位或64位地址来映射设备资源。
在这种情况下,PCI设备的内存区域会映射到处理器的内存空间,CPU可以通过访问这些内存地址与PCI设备交互。例如,显卡或网络接口卡(NIC)等设备会将其寄存器或内存缓冲区映射到系统内存地址空间中,CPU可以直接读写这些地址来控制设备或传输数据。
三、PCI I/O地址空间
I/O地址空间是专门为I/O设备保留的空间,用于与外部设备通信。在x86系统中,CPU可以直接访问I/O地址空间,并使用特殊的IN和OUT指令与设备进行交互。PCI支持32位I/O地址空间,但由于x86 CPU的I/O空间通常限制为16位,这导致许多平台上的I/O地址空间被限制为64KB(即16位地址范围)。
I/O空间通常用于较小的外围设备,例如串口、并口等传统设备,它们只需要少量的I/O寄存器。由于I/O空间有限,现代的大多数PCI设备倾向于使用内存映射的方式,而不是依赖于有限的I/O地址空间。
四、PCI配置地址空间
每个PCI功能都有一个256字节的配置空间,用于存储设备的配置信息,如设备ID、供应商ID、状态、命令寄存器等。为了访问这个配置空间,操作系统和驱动程序会通过I/O地址0xCF8~0xCFF
与PCI设备进行交互。
2.1 地址和数据寄存器
2.1.1 地址寄存器
0xCF8~0xCFB(地址寄存器):总共4个字节(32位),用于指定要访问的PCI设备的配置空间的地址。这个地址寄存器会包含PCI总线号、设备号、功能号和配置空间中的寄存器偏移。
- 第31位:启用位,必须设置为1。
- 第30~24位:保留,通常为0。
- 第23~16位:总线号,用于指定哪个PCI总线上的设备。
- 第15~11位:设备号,用于指定总线上的设备(一个PCI总线上最多可以有32个设备)。
- 第10~8位:功能号,用于选择设备的不同功能(每个设备最多有8个功能)。
- 第7~2位:寄存器号,指定配置空间中的寄存器(以4字节为单位,每个寄存器占用4个字节)。
- 第1~0位:保留位,必须为0。
2.1.2 数据寄存器
0xCFC~0xCFF(数据寄存器):同样是4个字节(32位),用于读取或写入之前通过地址寄存器指定的PCI配置空间的数据。用于读取或写入通过地址寄存器(0xCF8)指定的PCI配置空间数据。读取或写入时,数据会被放置到这里。
虽然在0xCF8~0xCFF
范围内确实只有4个字节用作地址寄存器和4个字节用作数据寄存器,但它们是用于与设备的整个PCI配置空间交互的“窗口”,通过改变地址寄存器中的内容,可以访问PCI设备的256字节配置空间的任何部分。
2.2 工作机制
由于I/O地址空间有限,传统模型在使用地址时非常保守。常见的做法是使用两个寄存器:一个用于指向设备内部的位置,另一个用于读取或写入数据。在PCI配置过程中,这涉及到两个步骤:
第一步:通过I/O写入配置地址
CPU生成一次I/O写操作,将数据写入北桥的地址端口(I/O地址CF8h),以提供要访问的配置寄存器地址。这个地址包含三个主要部分,用于在PCI拓扑中定位特定的PCI功能(function):除此之外,还需要指定要访问该功能的配置空间中的哪一个64个双字(256字节)寄存器。
- 总线号(Bus Number):用于访问256条可能的总线中的一条。
- 设备号(Device Number):用于选择该总线上32个设备中的一个。
- 功能号(Function Number):用于选择该设备中的8个功能中的一个。
第二步:通过I/O读或写访问数据
CPU生成一个I/O读或I/O写操作,指向北桥的数据端口(I/O地址CFCh)。根据这一操作,北桥将生成一个配置读取或配置写入事务,发送到在地址端口中指定的PCI总线上。
这种两步访问方法允许CPU通过I/O地址空间与PCI设备的配置空间进行交互。虽然I/O地址空间有限,但通过这种机制,系统可以有效地管理和配置多达256条总线、每条总线32个设备、每个设备最多8个功能的复杂系统。
2.3 举例说明
假设我们要读取PCI总线0上的设备5的功能0的供应商ID(Vendor ID),它位于配置空间的偏移地址0x00~0x01
。
2.3.1 计算地址寄存器的值
根据PCI配置空间的地址格式,我们需要构造要写入0xCF8
的地址:
将这些位组合起来,我们得到地址寄存器的值为:
- 启用位:第31位 = 1
- 总线号:总线0,所以第23~16位 = 0x00
- 设备号:设备5,所以第15~11位 = 0x05
- 功能号:功能0,所以第10~8位 = 0x00
- 寄存器号:我们要读取供应商ID,它在寄存器偏移
0x00
,所以第7~2位 = 0x00- 保留位:第1~0位 = 0
0x80000000 | (0x00 << 16) | (0x05 << 11) | (0x00 << 8) | (0x00 << 2) = 0x80002000
2.3.2 向地址寄存器写入地址
使用outl
指令将0x80002000
写入0xCF8
,指定要访问的PCI设备地址:
outl(0x80002000, 0xCF8);
2.3.3 从数据寄存器读取数据
供应商ID占用配置空间的前两个字节,因此我们可以从0xCFC
读取前两字节:
uint16_t vendor_id = inl(0xCFC) & 0xFFFF;
这个操作会从PCI配置空间的偏移地址0x00
读取4字节数据,但我们只需要前两字节,所以我们通过& 0xFFFF
只获取低16位,这就是供应商ID。
假设读取到的数据是0x8086
,那么供应商ID就是0x8086
,这表明该设备由Intel制造,因为0x8086
是Intel的供应商ID。
2.4 总结
通过上述操作,我们成功从PCI总线0、设备5、功能0的配置空间读取了供应商ID。类似地,我们可以访问其他配置空间寄存器,比如设备ID、命令寄存器等。每次访问不同的寄存器时,只需调整地址寄存器中的寄存器号和其他位。
这个机制使得操作系统和驱动程序能够方便地与PCI设备交互,从而进行设备初始化、配置和控制。
五、PCI Function配置寄存器空间
每个PCI功能包含最多256字节的配置空间。在这256字节中,前64字节包含一个称为Header的结构体,而剩下的192字节用于支持可选功能。系统的配置首先由Boot ROM固件完成。当操作系统加载后,它可能会重新配置系统并重新分配资源,因此系统配置过程可能会被执行两次。
根据Header类型的不同,PCI功能可以分为两大类:
- Type 1 Header:用于标识一个桥接设备(bridge),这种功能创建了拓扑中的另一条总线,如图1-12所示。
- Type 0 Header:用于标识一个非桥接设备(non-bridge function),如图1-13所示。
这种Header类型信息存储在dword 3的字节2中,是软件在系统中发现功能时(即进行枚举的过程)首先要检查的字段之一。枚举过程帮助操作系统或固件识别系统中存在的所有PCI功能,并根据其类型进行进一步的资源分配和管理。
通过这种区分,系统可以识别出哪些设备是普通的I/O设备,哪些设备是桥接器,从而构建完整的总线拓扑结构并有效地进行设备配置。