UEFI——PCIe子系统(I) PCIe基础知识


PCIe设备的相关知识是UEFI中一个非常重要的部分,我们现在使用的机器,大多数的外围设备都是依赖于PCIe协议继续数据传输的。开始我的想法是通过PCIe设备对UEFI 的Driver进行一个深入的学习,但是开始着手才发现PCIe相关的内容这么多,不仅仅是PCIe本身,还有很大一部分涉及了微机原理里面的底层知识。一个成熟的UEFI工程师是一定要对PCIe的这一套东西熟稔于心的,但是我目前还是一个非常不成熟的搬砖人。所以只能一步步来,先将常用的相关知识搞清楚,在一步一步深入。

PCIe概述

刚刚接触这些知识的我已经错过了PCI的时代。在目前的遇到的机器中,我是没有遇到过PCI的接口了,所有关于PCIe和PCI发展之类的故事我都是听说的,也就是纸上谈兵,但是个人感觉从目前的经验,这二者的区别并不需要过分的关注。毕竟我当前做的最多的还是去访问设备的寄存器,查找对应的信息,而这个部分,PCIe是兼容PCI的访问方式,所以就这一点来说这两者基本没有区别。
目前我能够说出的PCI与PCIe的区别简单来说就是以下几点:

  1. PCI是并行总线 PCIe是串行总线

为什么串行的PCIe总线比并行的PCI总线的速度快?
在我浅薄的认知中,一直认为串行的传输效率是低于并行的,所以当我看到PCI-PCIe发展趋势的时候,我是很奇怪的。直到进行深入的了解才有了一个初步的解释。
PCI是并行的信号,在低速传输的时候速度相对于串行信号是有着绝对优势的。但是,当速度逐渐提升,并行的局限性就越来越明显。
首先,假设发送端以并行的方式同时传输N个信号,当时钟达到上升沿,信号被传送出去。在接收端,必须能够在当前时钟周期内将N个信号同时锁住,才能够保证信息的正确性。这就要求在始终周期内,N个信号能够全部传输完成。这就造成了两个限制:
a)时钟的频率不能够太高
b)发送端和接收端的距离不能够太远(太远也会超过时钟周期导致信息不能被成功锁存)
时钟信号的传递也是需要时间的,发送端的N个信号,接收到的时钟上升沿可能并不同时,所以并行信号可能并不是同时发出,与此同时,N路信号的传输速率收到物理条件不同的影响也很难保证相同,以上就会导致信号到达接收端的时间不同。为了避免这个问题,就需要接收端接收的时候将采样点留在更加长的时间内以保证数据的完整性,这样又存在一个问题:
c)为保证数据的完整性,采样间隔需要增加
还有,并行数据相互之间还存在某种干扰(具体我没有深入研究 ),这种干扰会使信号发生变形,并行信号中并没有能够很好解决信号畸变的方式或者手段。
以上都是PCI在发展过程中遇到的实际问题,故开始将并行转向串行,这样就不需要担心时钟频率过高导致信号不能别及时的锁留,也不需要担心物理传输时不同线路达到时间不同–因为PCIe是按照数据流的编码方式恢复数据而非同时到达的数据为一个数据元。PCIe也并非完全丢弃了并行的思路,我们常说的X2 X6 X8等表示的就是一个PCIe有几个Lane在同时传输,这也是一种并行的应用。同时PCIe使用差分信号进行传输,增强了信号的抗畸变能力。
(仅为个人理解 如有错误请赐教)


  1. PCIe速度远超PCI
  2. PCI是总线结构,而PCIe是点对点的结构
  3. PCIe的配置空间从PCI的256B扩展为了4k,同时可以使用内存访问的方式进行访问
    以上就是常见到的二者的区别。我个人觉得了解即可,重点还是应该放在如何访问读取设备的相关信息上面。

PCI的地址空间

在深入讨论之前,首先需要明确的是:CPU 是没有办法直接访问设备的存储空间情况的,CPU能够直接访问的只有内存(memory)。 其次需要澄清的:PCI的地址空间与CPU能够访问的地址空间并不是同一个空间,二者是映射的关系。 PCI有自己独立的地址空间
通俗的来说。我们可以将所有的PCI设备想象成位于一个子系统中,这个子系统中有着统一的地址空间——PCI地址空间,所有的设备共享这个地址空间,每个设备占用一段连续的地址。同时这个地址空间也是PCI系统对外的接口,CPU就是通过访问某一设备对应的PCI地址空间映射的CPU能够访问的地址来对相应PCI进行设备操作的。
根据前述我们知道了,PCI 地址空间与CPU能访问的空间之间存在映射关系,这个映射关系是中间商来生成并且管理的,这个中间商是谁?
答:在PCI中这个中间商就是 RC当CPU想要访问某个设备的内容的时候,就会访问其对应的映射的内存位置,RC发现这个位置是自己管控的PCI设备的映射地址,就会将CPU 的访问信息转换成PCI 层级对应的信息并对PCI域中的对应地址进行操作。同样如果某一个PCI设备想要与主机进行通信,设备就会向对应PCI域中的地址发送信息进行操作,RC会将PCI域的信息同步到CPU域对应地址中,这样CPU就能够知道设备的操作了。(以上为本人自己的大致理解)

PCI协议定义了三种地址空间:Memory Address Space ,I/O Address SpaceConfiguration Address Space. 1
在这里插入图片描述
其中Memory Address Space ,I/O Address Space 与前面 MMIO PMIO中的两种类型的地址空间是类似的概念。事实上,当PCI空间和CPU地址空间发生映射关系的时候,这两个空间就是对应的映射关系。当设备映射在PCI Space的IO Space中,CPU 就需要通过访问IO Space中其映射的地址对其进行访问,若设备映射在PCI Space的Memory Space中,CPU 就需要通过访问Memory Space中的对应的映射地址对其进行访问。

还有一个特殊的地址空间Configuration Address Space(配置空间),明显这个空间在CPU域中是没有能够与之对应的空间的,那么CPU想要对这个空间进行访问,就只能通过特殊的方式进行了,PCI/PCIe Spec 中提供了两种方式对Configuration Address Space进行访问:IO 端口 CF8/CFC访问 ECAM方式访问,对于Configuration Address Space的访问后面会进行详细的介绍。

配置空间的访问

IO访问

想要通过IO方式访问PCIe设备的配置空间,首先就要知道通过什么端口进行访问呢? CF8h && CFCh (不要问我为什么是这两个 问就是历史遗留问题)


以下内容可以详细参考 PCI local bus Spec 3.2.2.3.2 1


CF8h & CFCh端口

以上两个端口都是32bit(DWORD)的寄存器,分别于配置读写的地址和数据。CF8h 称为**CONFIG_ADDRESS register**,CFCh称为CONFIG_DATA register
首先看CONFIG_ADDRESS register的数据意义
在这里插入图片描述

bit[31] : 使能位。如果想要通过该端口进行读取,此位必须设置为1
bit[30:24] : 保留位。只读。读取时只返回 0
bit[23:16] : 填充PCI/PCIe 设备的 bus number
bit[15:11] : 填充PCI/PCIe 设备的 device number
bit[10:8] : 填充PCI/PCIe 设备的 function number
bit[7:0] : 填充PCI/PCIe 设备的 register number

  1. bus/device/function number(BDF),现在只需要知道这是系统中用来确定PCIe设备的标志。就是说每一个PCIe设备对应的bus/device/function number 组合一定是唯一的。并且通过寄存器位数也能够看到能够支持的最大Bus/Device/Function 范围为256个(2^8) , 32 (2^5 )8 (2^3)
  2. register number : 注意观察可以发现 我是将最后bit 1 bit 0都算在了 register number中,个人觉得这样便于理解register number。但是虽然占用了 bit[7:0] 八位 ,但是其中bit 0 和bit 1 只能为0,这两位是read only的

整个CONFIG_ADDRESS register可以拆解为三个部分:可以读取(bit 31)+ 确定读取设备(BDF)+确定设备的寄存器(bit[7:0])。这一套下来,就能够精准的拿到我们想要的信息了。

CONFIG_DATA register比较简单。当我们在CF8h写入想要读取的设备以及对应的寄存器后,相应的数据就会由CFCh返回,读取CFCh就能得到我们想要的结果了。

利用IO端口读取设备信息

说了这么多,我觉得不如直接写个代码看看怎么用的。前面说过我们需要确定自己访问的设备和对应的设备寄存器。我首先用RW将我自己笔记本上的PCIe设备都找到了,如下图:
在这里插入图片描述
设备还是很多的,并且我们可以看到 设备对应的BDF都已经列出来了。当前我只是为了验证IO形式访问PCIe设备的配置空间,我们选取其中一个设备进行读取即可,我选择了BUS 0X02 /DEVICE 0X00 /FUNCTION 00设备 的register 0x00开始读取4byte。(此时我并不关心这个设备是什么 主要是为了验证IO方式的读取)
根据之前说的 读取设备寄存器信息的代码如下

// PciTest.c

#include <Uefi.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Library/UefiLib.h>
#include <Protocol/UsbIo.h>
#include <Library/IoLib.h>
#include <Base.h>
#include <Library/PostCodeLib.h>
#include <Library/PcdLib.h>


#define CONFIG_ADDRESS 0Xcf8
#define CONFIG_DATA 0Xcfc

EFI_STATUS
EFIAPI
READPCI(
	UINT32 BUS,
	UINT32 DEVICE,
	UINT32 FUNCTION
  )
{
	UINT32 address=0x01;
	UINT32 reg= 0x00;
	UINT32 value = 0x00;
	address= address<<31;

	address = address | (BUS<<16);
	address = address | (DEVICE<<11);
	address = address | (FUNCTION<<8);
	address = address | reg;
	IoWrite32(CONFIG_ADDRESS, address);
	value=IoRead32(CONFIG_DATA);
	Print(L"The value fill in CF8h is : 0x%08x \n",address);
	Print(L"The value of bus:0x%02x  device:0x%02x  function:0x%02x  register:0x%02x is : 0x%08x \n",BUS,DEVICE,FUNCTION,reg, value);
  return EFI_SUCCESS;
}
 
EFI_STATUS
EFIAPI
PciTest (
  IN  EFI_HANDLE                    ImageHandle,
  IN  EFI_SYSTEM_TABLE              *SystemTable
  )
{
  READPCI(0X02,0X00,0X00);
  return EFI_SUCCESS;
}
// PciTest.inf
[Defines]
  INF_VERSION                = 0x00010015
  BASE_NAME                  = PciTest
  FILE_GUID                  = 16BEFBED-60DC-4EA2-8E81-A343DF6C2117
  MODULE_TYPE                = UEFI_APPLICATION        
  VERSION_STRING             = 1.0
  ENTRY_POINT                = PciTest

[Sources]
  PciTest.c

[Packages]
	MdePkg/MdePkg.dec

[LibraryClasses]
	UefiApplicationEntryPoint
	UefiBootServicesTableLib
	MemoryAllocationLib
	DebugLib
	UefiUsbLib
	UefiLib
	IoLib
	PcdLib

另外需要在在对应的dsc 文件夹下添加当前使用的lib 和 inf
在这里插入图片描述
在这里插入图片描述

将编译完成得到的efi在机器上的UEFI Shell下面打开运行,结果如下:
在这里插入图片描述
可以看到BUS 0X02 DEVICE 0X00 FUNCTION 00设备 的register 0x00开始读取4byte得到的结果是:

03h02h01h00h
A80A144D

与RW读取的结果对比 结果一致
在这里插入图片描述

CF8h数据构成

之前说过传入CF8h的数据构成情况。一共32bit 按照对应的规则进行填充
在这里插入图片描述

代码中我们选择读取的register是0x00 所以后8位直接转换成 0000 0000,无需任何操作。但是之前说过,bit 0 和bit 1是固定为0的,这样就出现一个问题 如果我们想读取的寄存器是 0x02,该怎么填写register这个位置?

答案很简单,仍然填写 0000 0000

观察我们读回来的结果 我们可以看到,每次读回来的都是4 byte的数据,在读取OXOO的时候接受到的返回值是 0x00-0x03 四个byte的 。我们直接从接收数据中找到0x020对应数据即可。在本次读取的数据中 ,0x02的对应值就是 0A
由于Register的最后两个bit是固定为0的值,所以我们每次写入的register地址一定是DWORD对齐的。

配置空间大小

其实内存访问方式有一个重要的问题:内存没有初始化完成的时候,没办法使用这种方式访问设备。而IO访问就比较牛逼了,任何时间都可以用这种方式进行访问。独立编址中最为著名的就是Intel X86的架构,X86的架构是如此的经典,现代机器中的很多问题、情况、操作都能够在其中找到雏形;同时Intel架构目前还是市场上占有重要的位置,这也导致了后续我们经常使用的还是IO访问。既然IO访问的能力这么强,为什么还是需要内存访问的方式呢?主要的原因就是之前提过的PCIe配置空间扩展的问题。
配置空间是PCIe设备中一些寄存器按照某些规则进行排列的寄存器,这些寄存器有的保存了设备相关的信息,有的是提供给上层软件接口用以PCIe配置设备。配置空间是UEFI研发中经常需要访问的位置,详细的内容后面表述。
起初PCI设备的时代,每个PCI设备的配置空间为256B,靠着IO访问方式中低8位寄存器,还是能够全部访问到的,所以问题也就不大。但是当设备的配置空间扩展到4K的时候,传统IO访问方式就没有办法访问到256B之外的寄存器,也就是有4K-256B的位置我们无法访问。为了能够访问到这些寄存器,才有了另外一种内存的访问方式。

内存访问

前面说过为了解决PCIe的配置空间扩展到4K大小,无法使用IO访问,内存访问应运而生。
在开始介绍内存访问之前,首先来计算一下PCIe配置空间的最大能有有多大?
前面说过PCI一共支持256个Bus,32个Dev,8个Fun。 因此在满负载的情况下,配置空间的极限大小 = 4k * 256 * 32* 8 = 256M

ECAM 2

这种直接利用 memory 方式对PCIe配置空间进行访问的方式是PCIe Spec中提供的一种方式-- PCI Express Enhanced Configuration Access Mechanism(ECAM)
这种访问方式其实很简单,在软件下直接利用访问内存地址的方式访问配置空间映射的对应地址即可。对应地址的转换也有固定的规律
在这里插入图片描述
其中MMCFG表示的是memory mapped config ,而MMCFG BASE则表示在Memory Map中配置空间的起始地址。这个地址的值根据平台的不同而不同,一般是由BIOS进行配置的。虽然我们不能自己设置这个寄地址的值,但是我们可以通过ACPI Table 读出当前平台的MMCFG Base的具体值。
如果当前使用的是Windows OS,那么用RW就能轻松的读出当前的MMCFG Base,如下图是我当前PC的情况
在这里插入图片描述

如果当前使用的是linux 系统,我们也可以使用系统下的命令将MCFG表dump 出来 ,操作命令为

acpidump -n MCFG

我用一台linux 机器dump了对应的内容
在这里插入图片描述
当前dump的结果是十六进制的一个表,我们可以从PCI Firmware Specification 3 找到这个表每一位对应的定义
在这里插入图片描述
上面这个表中可以看到,从offset44开始就是base address struct
在这里插入图片描述
base address struct 的结构体定义同样可以从PCI Firmware Specification中找到
在这里插入图片描述
通过上述对比查找 可以确定当前机器的MMCFG Base 0x00000000E0000000

利用ECAM读取设备信息

根据上述得到的信息,我利用ECAM的方式写了一个Application 读取之前BDF2/0/0 设备的前256B的配置空间代码如下

//PciTest.c
#define MMCFG_BASE 0xC0000000

  EFI_STATUS
  EFIAPI
  MMIOReadConfigSpace(
	  UINT32 BUS,
	  UINT32 DEVICE,
	  UINT32 FUNCTION
	)
  {
	  UINT32 address=0x00;
	  UINT32 reg= 0x0000;
	  UINT32 value = 0x00;
	  address= MMCFG_BASE;
      Print(L"MMCFG_BASE:The value of address is : 0x%08x \n",address);
	  address = address | (BUS<<20);
      Print(L"BUS :The value of address is : 0x%08x \n",address);
	  address = address |(DEVICE<<15);
      Print(L"DEVICE :The value of address is : 0x%08x \n",address);
	  address = address | (FUNCTION<<12);
      Print(L"FUNCTION :The value of address is : 0x%08x \n",address);
	  address = address | reg ;
	  Print(L" \n\n\n\n");
	  
	  Print(L"The Configuration Space of bus:0x%02x  device:0x%02x  function:0x%02x : \n",BUS,DEVICE,FUNCTION);
	  Print(L" The value of \n");
	  
	  for(UINT32 offset = 0; offset <64; offset++){
	  	  if( ( (offset%8) == 0) && (offset != 0))  Print(L"\n");
		  value=MmioRead32(address);
		  Print(L"register 0x%08x is : 0x%08x  ",address, value);
	  	  
	  	  //Print(L"The value of bus:0x%02x  device:0x%02x  function:0x%02x  register:0x%02x is : 0x%08x \n",BUS,DEVICE,FUNCTION,reg, value);
		  address=address+4;
	  }
	return EFI_SUCCESS;
  }

上述程序读取结果如下图
在这里插入图片描述
和前面用RW读出来的数据对比,可以发现是一致,所以证明我的代码没有问题,能够成功利用这种方式读出来ConfigSpace的内容
更大的配置空间用这种办法也是能够读出来的:
在这里插入图片描述
至此我们就完成利用IO端口读取PCIe设备的信息。目前还有两个关键的问题:

  1. BDF是如何分配的
  2. 读取回来的信息是什么意思?
    首先我们解决第二个问题

PCIe 配置空间

前面我们分别通过IO /ECAM的方式从PCIe设备的配置空间读取回来了一些信息,他们又表示什么?这些问题的回答就是 Configuration Address Space(配置空间)
Configuration Address Space(配置空间),就是存储了PCIe设备信息的地方。如果想要直接从PCIe设备得到该设备的信息(这样得到的信息是最准确的),那么配置空间的读取和意义就是我们必须了解的内容了。
上一节曾经写代码访问了PCIe寄存器 0x00-0x03 ,其实我们获得的就是配置空间的一部分信息了。PCIe设备的配置空间的头部有两种模式:Type00Type01 。分别对应了非桥设备(Endpoint),和桥设备(Root和Switch端口中的P2P桥)来使用,两种模式的Header Type有着很多共同的部分,这些共同的部分也是常用的部分,故仅对这些共同部分进行简单介绍,其余部分以及详细介绍可以参考 PCIe Spce Section2 7.5

Common Cinfiguration Space

在这里插入图片描述

上图表示的就是Type00 和Type 01 配置空间头通用的部分
通过IO 方式读取目标 PCIe设备BUS 0X02 DEVICE 0X00 FUNCTION 00 的Configuration Space Header

EFI_STATUS
EFIAPI
IOGetConfigSpaceHeader(
	UINT32 BUS,
	UINT32 DEVICE,
	UINT32 FUNCTION
  )
{
	UINT32 address=0x01;
	UINT32 reg= 0x00;
	UINT32 value = 0x00;
	address= address<<31;
	address = address | (BUS<<16);
	address = address |(DEVICE<<11);
	address = address | (FUNCTION<<8);
	address = address | reg;
	for(UINT32 offset = 0; offset <16; offset++){
		IoWrite32(CONFIG_ADDRESS, address);
		value=IoRead32(CONFIG_DATA);
		Print(L"The value of register 0x%08x is : 0x%08x \n",address, value);
		address=address+4;
	}
  return EFI_SUCCESS;
}

UEFI Shell下的运行结果:
在这里插入图片描述
RW的读取结果:
在这里插入图片描述
结合实际设备分析一下头部空间各个数据的含义 (根据 PCI Local Bus Spec的介绍顺序)

Device Identification

1 6.2.1
在PCIe配置空间的头中,有五个部分是与设备识别(device identification)相关的,所有的PCI设备都要填写这些位置,这五个寄存器都是只读的

Vendor ID

Vendor ID : 制造厂商号。16 byte 表示设备的制造厂商。

  1. 各个厂商的代号由PCI SIG进行分配以确保唯一。
  2. 0XFFFF为保留值。读取系统中没有的额PCIe设备的时候即返回该值
Device ID

Device ID : 设备号。16 byte 表示特定的设备。此位由厂商进行分配。

Revision ID

Revision ID : 版本号。16 byte 表示设备的版本,由厂商进行分配。版本号可以看成是设备号的扩展。
Revision ID与Vender ID和Device ID一起, 能够确定驱动的版本。
用于确定软件应该加载哪个驱动程序的一种机制;供应商必须确保所选的值不会导致使用不兼容的设备驱动程序。

Header Type

Header Type: 此位定义了Header 第二部分(从10h开始)的空间分布类型,同时定义了该设备是否能支持多个设备功能。
bit7 : 定义是否为多功能设备。 1 - 设备是多功能的 0 - 设备是单功能的
bit[6:0] : 定义了Header 空间分布类型。
1. 00h - Type 00 配置空间分布
2. 01h - Type 01 配置空间分布


Device Control

目前,这个 Command Register 寄存器在实际情况中我还没有用过,所以只能看Spec上面的一些说明了解这个寄存器。
这个寄存器提供了对于设备一些基本功能的控制。所有的设备都必须支持这个寄存器。具体寄存器表示的功能可以参考Spec
在这里插入图片描述
当前设备在UEFI Shell下 Command Register 获取的值为0x0006;在OS下获取的值为0x0406。
bit[1] : 允许设备对 MemorySpace接入请求进行响应
bit[2] :允许设备在PCI bus上作为 bus master
bit[10] : 是否禁止 INTx 功能 (关于这个功能 可以参考 PCIe扫盲——中断机制介绍(INTx)-Felix-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台


Class Code

Class Code : 用来定义设备的功能。24byte 这个寄存器分为三个部分:

  1. Base Class : 定义了设备的基础功能 ,基类
  2. Sub-Class : 更详细的定义设备的功能,表示设备的子类
  3. Programming Interface : 编程接口,读取这个位置能够知道当前设备使用的接口规范是什么==(这个位置与底层结构 传输层级相关)==
    在这里插入图片描述ClassCode 代表的详细含义可以从 PCI Code and ID Assignment Specification 中进行查找 最新版本 Revision 1.14 PCI Local Bus Specification附录中的时间太久远 参考价值不大
    PCI Code and ID Assignment Specification 下载网站 Review Zone | PCI-SIG

根据上面的说明,在前面读出的设备信息中找一下当前设备的对应信息:
Vendor ID : 0x144D
DeviceID : 0xA80A
Revision ID: 0x00
对应结果:
在这里插入图片描述
看得出来这个设备的制造厂商是三星啊~ 而且这是一个 NVMe 的 SSD的 Controller
HeaderType: 0x00 这是一个Type00 类型的设备
ClassCode : 0x010802
在这里插入图片描述
在这里插入图片描述
这里能够看出这是Mass storage controller ,控制器使用的是NVMe规范

DeviceStatus

表示设备状态的只有一个 Status Register ,各位代表的含义如下图
在这里插入图片描述
Status Register 的值为 0x0010,就是只有bit 4 为1 。这表示这个设备是有 扩充的Capabilities的,跳转至 offset 34h 进行查找。
在这里插入图片描述
由于PCIe设备发展,在PCIe Spec 6.0 中 ,Capabilities List已经被定义成必须被写为1的了,如下图。为了避免误会,特此说明。
在这里插入图片描述

Miscellaneous Registers

CacheLine Size

表示当前PCI设备的CacheLine大小 当前读取设备的CacheLine 大小为0x10

Latency Timer

在PCIe设备中 这个寄存器的值一般为0 PCI设备中该位是有意义的

Built-in Self Test

可选的寄存器,展示当前的自检状态,不支持该功能的设备返回0 .

Interrupt Line

PCI设备的中断控制 PCI设备的Interrupt pin 会连接到中断控制器上位置 没用过

Subsystem Vendor ID and Subsystem ID

和Device ID和Vendor ID配合对PCIe设备进行标识*

Capabilities Pointer

只有当Status Register 中Capabilities List位为1 当前寄存器才有效。生效时,底部两位保留为00,剩下的为Capabilities List的头指针 ,根据头指针找到下一个结构体位置后,还可以继续向下查找。
每一个 capability都是相似的构成,8bit 表示当前的 capabilityID ,接下来8bit是一个指向 下一个capability指针,当指针00则表示后面没有capability了。
根据前面读出的寄存器情况可以看到 前面读出的设备中存在Capabilities List,并且list上共有四个类型的capability register
第一个capability register 的位置在0x40 ,其capabilityID为01 表示PCI Power Management Interface
第二个capability register 的位置在0x50 ,其capabilityID为05 表示Message Signaled Interrupts
第三个capability register 的位置在0x70 ,其capabilityID为10 表示CompactPCI central resource control
第四个capability register 的位置在0xB0 ,其capabilityID为11 表示MSI-X
在这里插入图片描述

Base Address

前面大概描述了type00/01 header 通用的部分, 接下来以type00header 为例,说明BAR寄存器
在这里插入图片描述

Type00的设备中,Bar寄存器是PCIe配置空间中从0x10开始的6个32bit的寄存器。上图可以看到BAR寄存器的具体情况和位置。Bar寄存器有两个最重要的功能:

  1. 起始阶段枚举PCIe设备构建PCIe 拓扑的时候,读取Bar确定设备所需的最小的空间
  2. 后续为设备分配空间,Bar寄存器负责保存设备在对应地址空间的基地址
    需要明确的是BAR寄存器表示的是PCIe设备在PCIe地址空间中的基地址。在PCI的地址空间节中我就已经说过,PCI设备在PCI空间中会占用一段连续的地址。每个PCI设备占用的地址必定必定不能重叠,那么怎么确定每个设备需要申请多大的地址?PCI设备的起始地址是多少呢? 就是我们上述的Bar寄存器的作用了。

IO or Memory 空间

前面曾经说过 ,PCIe子系统本身也是区分IO空间和memory空间,那么在Bar中,就需要说明当前是想要映射到哪一个空间。Bar寄存器的最后一位就是用来区分映射到哪一个空间的。如下图
在这里插入图片描述

所有BAR 的 Bit 0都是只读的,这一位被用来决定映射到IO或者Memory 空间。映射到 Memory Space的寄存器bit 0 必须被设置为0;映射到IO空间的bit 0 必须被设置为1

在这里插入图片描述

映射到IO空间总是32bit的位宽,此时bit 0 写为1,而bit 1 为保留位,如果读取bit 1,其返回值一定是0,剩余的其他bit被用来进行IO映射

在这里插入图片描述

映射到Memory空间可以是32或者64bit的位宽,此时bit 0 为 0。bit 2&1 不同的组合情况下代表不同的涵义。Bit2&1为01&11为保留情况,为00表示此时请求的映射空间宽位32bit,为10表示映射空间位宽为64bit
Bit3 设置为1 表示当前数据是可以预取的,设置为0表示不可以预取
Bit3-0是只读的

Bar Setup

在PCIe枚举阶段,会对Bar进行一系列的操作,获取到当前需要的最小空间并且将对应空间的基地址写入Bar中,接下来分别针对不同情况的Bar空间构建所需要的步骤进行简单的介绍。
以下只是简单的流程介绍,方便理解整个过程,这些操作都会在代码中PCI 设备枚举的部分进行实现

32 bit Memory 空间请求

在这里插入图片描述
如上图右侧所示,一般来说针对一个Bar的处理分成三步:

  1. 刚刚上电或者刚刚重启过的设备,其Bar未经过初始化,其中的一些可编辑的位会处于一种不确定的状态,如图中用XXX表示的位置。此时为了确定当前设备的Bar情况,向Bar寄存器的所有位写入1
  2. 向寄存器写入FFFFFFFF后,读取当前Bar寄存器的状态进行分析:
    1. Bit 0 为0 表示当前映射到memory space
    2. Bit 2&1 为00 表示当前想要映射的空间的width 为32bit
    3. Bit 3为1表示当前数据可以预取
    4. 可编辑的最低位为第20位,表示当前需要的空间最小为2^20=1MB
  3. 现在已经知道了当前需要的空间大小和基本情况,接下来就向可编辑的部分写入为此设备分配的及地址,上图为此设备分配的基地址为0x80000000,说明当前分配的基地址为2GB,该设备对应的空间范围为2GB-2GB+1MB

64 bit Memory 空间请求

在这里插入图片描述
64bit的memory空间请求和32bit的情况唯一的一个区别就是此时需要将下一个Bar寄存器一起操作分析,具体过程如下

  1. 两组Bar寄存器当前都是未初始化的状态,此时同样向两组寄存器中写入全1(此时将这两组寄存器看成一个整体,即一个大小为64bit的BAR)
  2. 写入全1后读取当前Bar寄存器的状态进行分析:
    1. Bit 0 为0 表示当前映射到memory space
    2. Bit 2&1 为10 表示当前想要映射的空间的width 为64bit
    3. Bit 3为1表示当前数据可以预取
    4. 可编辑的最低位为第26位,表示当前需要的空间最小为2^26=64MB
  3. 现在已经知道了当前需要的空间大小和基本情况,接下来就向可编辑的部分写入为此设备分配的及地址,上图为此设备分配的基地址为0x0000000200000000,说明当前分配的基地址为16GB,该设备对应的空间范围为16GB-16GB+64MB

IO memory 空间请求

IO 空间的请求基本和32bit的Memory空间请求没有区别,前面我们也说过 映射到IO空间的总是32bit的位宽
在这里插入图片描述

需要说明的是,实际代码中,为了方便代码结构针对不同的类型的空间进行不同的处理,与上述的步骤描述稍有出入,代码中:

  1. 读取Bar寄存器的最后一位判断当前是IO or Memory类型的空间请求
  2. 如果判断是IO类型的,正常全部写入1进行读取和基地址设置
  3. 如果判断是Memory类型的,继续读取bit2&1位,判断当前是32 or 64 bit的空间请求
  4. 如果判断是32bit的,和IO类型的相同,对当前Bar寄存器进行写1操作,设置基地址
  5. 如果是64bit的,需要向下多申请32bit的寄存器(也就是将下一组Bar寄存器也一并操作读取)

  1. PCI Local Bus Specification Revision 3.0 ↩︎ ↩︎ ↩︎

  2. PCI Express® Base Specification Revision 6.0 ↩︎ ↩︎

  3. PCI Firmware Specification ↩︎

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值