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
<think>好的,用户想了解Linux内核如何分配PCIe设备的物理内存。这需要我回顾相关知识,并组织一个清晰的解释流程。首先,我得从PCIe设备的初始化开始,可能需要提到枚举过程,然后是资源分配,特别是物理内存区域,如BARs的处理。 记得用户提到系统级指令,要确保所有数学公式正确使用$...$或$$...$$。虽然这个问题可能公式不多,但如果有涉及地址计算的地方,可能需要用LaTeX表达。例如,物理地址的计算或映射可能需要用到公式。 接下来,结构要清晰。分步骤说明整个过程:枚举设备、读取BAR、地址分配、映射到内核空间、可能的DMA设置。每个步骤需要详细说明,比如BAR的读取和扩展,内核如何管理资源以避免冲突,使用哪些数据结构,如pci_dev和resource。 还要注意用户要求使用中文回答,所以术语要准确翻译,比如“MMIO”翻译为“内存映射I/O”,“DMA”可以保留英文但说明中文名称。需要检查是否有遗漏的关键点,比如PCI配置空间访问,资源管理中的pci_bus_region,以及iomem_resource的作用。 另外,用户可能对DMA的地址分配感兴趣,比如一致性DMA和流式DMA的区别,这里需要简要提及。同时,确保每个部分逻辑连贯,例如从探测设备到资源请求,再到实际映射和DMA配置,整个过程环环相扣。 最后,总结整个流程,强调内核如何确保资源合理分配,避免冲突,以及设备驱动如何访问这些资源。可能还需要提到不同架构下的差异,如ARM和x86在地址空间处理上的不同,但用户问题可能更关注通用逻辑,所以可以简略。 检查是否有技术错误,比如PCIe设备在启动时由固件(如BIOS或UEFI)进行初始配置,但Linux内核在启动时会重新扫描和分配资源,确保没有依赖错误的假设。同时,确认提到的函数和数据结构名称是否正确,如pci_read_bases()、request_resource()等。 最终,确保回答符合用户的结构要求,分步骤说明,语言简洁,逻辑清晰,正确使用LaTeX格式,满足所有系统级指令和用户可见层的要求。</think>Linux内核PCIe设备物理内存的分配是一个系统级过程,主要涉及以下几个关键步骤: $$以下流程基于Linux内核v5.10+架构通用实现$$ 1. **PCIe设备枚举** - 内核启动时通过ACPI/UEFI或直接扫描PCI总线 - 发现设备后创建`struct pci_dev`对象 - 读取设备配置空间中的BARs(Base Address Registers) ``` BAR0: 0xFE000000 (32-bit, prefetchable) BAR2: 0xFEB80000 (64-bit, non-prefetchable) ``` 2. **BAR解析与地址空间计算** - 通过`pci_read_bases()`解析BAR寄存器 - 确定请求的地址空间类型: - $Memory\ Space \in \{MMIO, PMIO\}$ - $Address\ Width \in \{32-bit, 64-bit\}$ - 计算所需内存大小:向BAR写入全1后读取掩码 3. **物理地址分配** - 调用`pci_bus_alloc_resource()`进行资源分配 - 通过`request_resource()`向内核资源树申请: ```c struct resource *res = &dev->resource[bar]; request_resource(&iomem_resource, res); ``` - 使用红黑树管理已分配区域,确保无重叠 4. **地址写入与映射** - 将分配的物理地址回写至设备BAR寄存器 $$ \text{BAR} = (\text{phys\_addr} \& \text{type\_mask}) | \text{enable\_bit} $$ - 建立内核虚拟地址映射: ```c void __iomem *addr = pci_iomap(pdev, bar, maxlen); ``` 5. **DMA地址空间处理** - 对需要DMA的设备- 一致性DMA:`dma_alloc_coherent()` - 流式DMA:`dma_map_single()` - 根据架构实现IOMMU/SMMU映射 **关键数据结构关系**: ``` pci_dev → resource[] → iomem_resource ↓ pci_bus_region ↓ host bridge address window ``` **典型物理内存布局示例**: ``` 0x00000000-0x3FFFFFFF : System RAM 0x40000000-0x7FFFFFFF : PCI MMIO Space 0x80000000-0xFFFFFFFF : PCI Prefetchable ``` 该过程确保了: 1. 设备物理地址不与系统内存和其他设备冲突 2. 支持热插拔设备的动态资源分配 3. 兼容不同架构的地址空间布局(x86/ARM等) 4. 提供统一的设备访问接口给驱动程序 驱动开发者最终通过`ioremap()`或`pci_iomap()`获得可直接访问的虚拟地址,物理地址的实际分配对驱动透明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值