PCIe总线-Linux内核PCIe设备枚举流程分析(十三)

1.简介

当系统启动时或者有新的PCIe设备接入时,PCIe主机会扫描PCIe总线上的PCIe设备,读取设备配置空间信息,建立设备的拓扑关系,然后为设备分配资源(如内存空间、I/O空间、中断、总线编号等),最后根据设备的类型匹配驱动。下面以Linux内核为例,介绍PCIe主机枚举PCIe设备的流程。

2.pci_host_probe

Linux内核中,PCIe RC驱动调用pci_host_probe函数枚举PCIe总线上的所有设备。pci_host_probe的执行流程如下图所示,总共有5部分,具体如下:

  1. 调用pci_scan_root_bus_bridge函数扫描总线上的桥和设备。
    1. 首先注册Host主桥。主桥上有root bus,root bus的编号通常是0。然后设置msi_controller和msi_domain,用于MSI/MSI-X中断。最后将root bus的数据结构挂到pci_root_buses上,便于统一管理。
    2. 调用pci_scan_child_bus函数扫描设备,后面详细介绍。
  2. 确定连接到PCI总线上的PCI桥(包括标准PCI桥和PCI到PCIe桥)所需的地址空间大小,并分配相应的内存和I/O资源。最后,调整桥设备的子总线号,以确保正确的总线层次结构和设备访问路径。
  3. 给总线上的设备和桥分配资源,即设置配置空间的BAR、BASE、Limit等寄存器,后面详细介绍。
  4. 遍历PCIe总线上设备和桥,设置MPS、MRRS,可以参考PCIe Linux MRRS和MPS参数设置策略
  5. 遍历PCIe总线上设备和桥,添加设备,调用PCIe总线提供的匹配方法匹配PCIe设备驱动。

pci_host_probe

3.pci_scan_child_bus

Linux内核调用pci_scan_child_bus函数,从root bus开始枚举PCIe总线上的所有设备。整个枚举流程分为三步,具体如下:

  1. 第一步:首先从root bus开始扫描。PCIe设备和功能号组合总共有256种,因此需要全部遍历一遍,devfn高5位是设备号,低3位是功能号,每次循坏扫描一个设备,默认扫描Fun0,如果设备是多功能设备,则会扫描其他Function。扫描设备的单个Function使用pci_scan_single_device函数,后面详细介绍。
  2. 第二步:若bus下面挂的桥被BIOS配置过,则扫描桥下面的设备,如此递归,直到扫面完整个总线,若桥没有被配置过或需要重新配置,则先跳过,在第三步重新配置。对于被BIOS配置过的桥,其Primary、Secondary、Subordinate已经设置,内核会创建pci_bus,为其预留bus编号,同时将bus资源管理起来。
  3. 第三步:遍历总线上的所有桥,设置所有没有配置的桥,即设置桥的Primary、Secondary、Subordinate,同时创建pci_bus,为其预留bus编号,同时将bus资源管理起来。

pci_scan_child_bus

4.pci_scan_single_device

内核调用pci_scan_single_device函数扫描设备的单个Function,先扫描Function,再添加Function。执行的流程如下:

  1. 内核通过读取Function的VID来判断设备是否存在,超时时间为60000毫秒。若读取到的VID是0xffffffff、0x00000000、0x0000ffff、0xffff0000,则表示没有设备,若读取到的是0x1,说明返回了CRS的completion包,设备还没准备好,延时循环读取PCI_VENDOR_ID(等待设备准备好),若读到CRS的completion包,则延时翻倍。
  2. 扫描到Function,则分配pci_dev数据结构,每个Function对对应一个pci_dev数据结构。
  3. 初始化Function(设备)。
    1. 设置Function的bus_typepci_bus_type,用于匹配和初始化PCIe设备驱动。
    2. 读取Function的port类型,通过PCI Express Capability registers获取,主要是确定Function是upstream还是downstream ports。
    3. 获取Function的配置空间大小。通过读偏移地址为256处的值,若读到有效的扩展空间头则为4096字节,否则为256字节。
    4. 解析非桥设备的PCIe配置空间头。具体有PCI_INTERRUPT_PINPCI_BASE_ADDRESSPCI_SUBSYSTEM_VENDOR_IDPCI_SUBSYSTEM_IDPCI_BASE_ADDRESS会读取Function的BAR,详细的后面说明。
    5. 解析桥设备的PCIe配置空间头。具体有PCI_INTERRUPT_PINPCI_BASE_ADDRESSPCI_EXP_SLTCAPPCI_CAP_ID_SSVIDPCI_SSVID_VENDOR_ID和桥的地址空间。
  4. 配置、初始化和管理Function。主要是配置MPS、Extended Tags、ordering、LTR等,初始化MSI、PM、VPD、IOV、ATS、AER等,将pci_dev挂到pci_busdevices链表,最后建立MSI IRQ domain。

pci_scan_single_device

4.1.解析BAR

内核使用pci_read_bases函数解析Function的BAR和ROM,包括桥的BAR0和BAR1,非桥设备的BAR0-BAR5。BAR的使用方法参考3.3节PCIe总线-配置空间介绍(三)pci_read_bases函数的执行流程如下所示,主要有:

  1. 先读BAR,读出的全为F,表示BAR不正常,意味着设备可能已经被移除,不再存在于总线上,或者设备已经进入了一种不能响应读操作的错误或低功耗状态。
  2. 向BAR写入0xFFFFFFFF,向ROM写入0xFFFF8000,然后再度BAR和ROM,以确定BAR和ROM的属性和大小。
  3. 将第一次读的BAR和ROM的默认值写回,恢复默认值。
  4. 根据第一次读取BAR的值,解析BAR的类型和属性。具体有IO空间IORESOURCE_IO,32位非预取存储器空间IORESOURCE_MEM、64位非预取存储器空间IORESOURCE_MEM_64(虽然BAR是64位,但实际上RC只会给BAR配置32位PCIe总线地址),预取存储器空间IORESOURCE_PREFETCH。内核会根据解析的BAR信息分配PCIe总线地址。
  5. 如果是64位BAR,则还需要解析高32位的BAR,步骤和第4步一样。
  6. 计算BAR所需地址空间大小。
  7. 将PCIe总线地址region转换成存储域地址resource,将存储域地址resource转换成PCIe总线地址Region,两者的起始地址必须相等。

pci_read_bases

4.2.解析桥windows

内核使用pci_read_bridge_windows函数解析桥设备的PCI_IO_BASEPCI_PREF_MEMORY_BASEPCI_PREF_BASE_UPPER32。若PCI_IO_BASE寄存器存在,则设置bridge->io_window = 1。若PCI_PREF_MEMORY_BASE寄存器存在,则设置bridge->pref_window = 1。若支持64位预取地址,且PCI_PREF_BASE_UPPER32寄存器存在,则设置bridge->pref_64_window = 1PCI_MEMORY_BASE寄存器必须实现,这里可不用解析,后面直接分配地址。

[drivers/pci/probe.c]
static void pci_read_bridge_windows(struct pci_dev *bridge)
{
	u16 io;
	u32 pmem, tmp;

	pci_read_config_word(bridge, PCI_IO_BASE, &io);
	if (!io) {
		pci_write_config_word(bridge, PCI_IO_BASE, 0xe0f0);
		pci_read_config_word(bridge, PCI_IO_BASE, &io);
		pci_write_config_word(bridge, PCI_IO_BASE, 0x0);
	}
	if (io)
		bridge->io_window = 1;

	/*
	 * DECchip 21050 pass 2 errata: the bridge may miss an address
	 * disconnect boundary by one PCI data phase.  Workaround: do not
	 * use prefetching on this device.
	 */
	if (bridge->vendor == PCI_VENDOR_ID_DEC && bridge->device == 0x0001)
		return;

	pci_read_config_dword(bridge, PCI_PREF_MEMORY_BASE, &pmem);
	if (!pmem) {
		pci_write_config_dword(bridge, PCI_PREF_MEMORY_BASE,
					       0xffe0fff0);
		pci_read_config_dword(bridge, PCI_PREF_MEMORY_BASE, &pmem);
		pci_write_config_dword(bridge, PCI_PREF_MEMORY_BASE, 0x0);
	}
	if (!pmem)
		return;

	bridge->pref_window = 1;

	if ((pmem & PCI_PREF_RANGE_TYPE_MASK) == PCI_PREF_RANGE_TYPE_64) {

		/*
		 * Bridge claims to have a 64-bit prefetchable memory
		 * window; verify that the upper bits are actually
		 * writable.
		 */
		pci_read_config_dword(bridge, PCI_PREF_BASE_UPPER32, &pmem);
		pci_write_config_dword(bridge, PCI_PREF_BASE_UPPER32,
				       0xffffffff);
		pci_read_config_dword(bridge, PCI_PREF_BASE_UPPER32, &tmp);
		pci_write_config_dword(bridge, PCI_PREF_BASE_UPPER32, pmem);
		if (tmp)
			bridge->pref_64_window = 1;
	}
}

5.分配资源(地址)

内核使用pci_bus_assign_resources函数给PCIe桥和非桥设备分配资源。详细的流程如下所示。针对每一条子总线,递归调用__pci_bus_assign_resources分配资源。一条子总线上的设备和桥分配资源的步骤如下:

  1. 遍历子总线上的所有设备,给设备需要的资源排序。
  2. 遍历子总线上设备需要的资源,调用pci_assign_resource函数分配资源,最终会调用到allocate_resource函数,先查找资源,再请求资源。这里的资源是PCIe总线地址。
  3. 分配好资源后,调用pci_update_resource函数将资源配置到设备的BAR或ROM中,同时回读BAR和ROM进行校验。将PCIe总线地址设置到BAR或者ROM寄存器当中。
  4. 如果是桥设备,则调用pci_setup_bridge初始化桥。桥的IORESOURCE_IOIORESOURCE_MEMIORESOURCE_PREFETCH资源范围,是桥下面所有设备相同资源类型之和。

pci_bus_assign_resources

参考资料

  1. PCIEXPRESS体系结构导读
  2. PCI Express technology 3.0
  3. PCI Express® Base Specification Revision 5.0 Version 1.0
  4. Rockchip RK3588 TRM
  5. Linux kernel 5.10
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值