前言
现在的 x86 架构的 SOC 普遍支持 ACPI机制,在此我们只基于新机制下的PCI设备枚举的过程进行分析。
acpi_init: /drivers/acpi/bus.c
--> acpi_scan_init
--> acpi_pci_root_init
--> acpi_scan_add_handler_with_hotplug(&pci_root_handler, "pci_root") : static struct acpi_scan_handler pci_root_handler
--> acpi_pci_root_add
--> pci_acpi_scan_root
--> pci_create_root_bus
--> pci_scan_child_bus
--> acpi_pci_link_init
这些函数向我们描述了ACPI初始化中PCI的相关操作,主要可分为两个部分:
第一部分:acpi_pci_root_init 完成PCI 设备的相关操作(包括PCI主桥,PCI桥、PCI设备的枚举,配置空间的设置,总线号的分配等);
第二部分:acpi_pci_link_init 完成PCI中断的相关操作,在此不做具体分析。
一、添加顶层父设备:acpi_pci_root_add
函数分析:该函数通过 ACPI 表中的 _SEG 和 _BBN 参数获得 HOST 主桥的 Segment 和 Bus 号,创建 acpi_pci_root 结构(表示HOST主桥信息),在本系统中,由于只有一个HOST主桥,所以acpi_pci_root_add 只调用一次,acpi_pci_root 也就一个。最终,在完成数据结构的创建以及一些初始化后,就调用 pci_acpi_scan_root 函数对这条主桥下的PCI节点进行遍历。
二、开始枚举:pci_acpi_scan_root
函数分析:这个函数先调用 pci_find_bus 去判断当前总线号是否已经存在,如果存在就退出,如果不存在就调用 acpi_pci_root_create 函数去对这条PCI总线进行遍历,在这里我们需注意一个结构就是acpi_pci_root_ops,它是新版本的内核提供给我们对于PCI信息的一些接口函数的集合(这其中就包括对配置空间的读写方法)。
注意:该函数的实现每个linux的版本会有所差异。
三、创建HOST桥:acpi_pci_root_create -> pci_create_root_bus
函数分析:在进入此函数时,系统首先会对这个root的信息进行一些初始化和添加操作,之后将这些resources、读写方法、总线号等数据传入 pci_create_root_bus 这个函数,通过这个函数返回一个总线结构 pci_bus(注意:pci_create_root_bus 返回的 pci 总线是总线号为0的总线,即直连HOST桥的总线),最后将得到的结构体送入 pci_scan_child_bus 开始从总线 0 进行遍历。
四、遍历PCI设备:acpi_pci_root_create -> pci_scan_child_bus
函数分析:我们先来说明一下这个函数整体的实现过程:通过for循环先遍历当前总线上的每一个PCI设备(包括PCI桥设备),遍历后将其注册为 pci_dev 结构体,之后通过递归调用来进入当前总线的下一级子总线继续完成上述过程,直到某条PCI总线上无PCI桥设备,返回最大的总线号。在这个函数中,一上来就是探测总线0上的所有设备,这里通过一个for循环来实现(为什么是0x100,回答:0x100即256,由于一条PCI总线最多可以有32个设备,而每个设备最多能有8个功能,故 32*8=256,一条总线最多有256个逻辑设备,那就执行256次把它们一个不漏的全部枚举一次,去发现他们),在本函数中,使用 pci_scan_slot 来遍历探测总线上的每一个设备,使用 pci_scan_bridge 递归调用 pci_scan_child_bus 进入下一级总线。先分析 pci_scan_slot。
1. pci_scan_slot -> pci_scan_single_device
函数分析:在 pci_scan_single_device 函数进一步调用了两个函数 pci_scan_device 函数和 pci_device_add 函数,先说说这两个函数是干啥的,首先 pci_scan_device 函数完成的事情是依据BIOS遍历的结果去填充 pci_dev 结构,而pci_device_add函数的工作就简单了,把这个结构体加到当前PCI总线的设备链表上去,然后注册设备。一个个分析。
1. 1 pci_scan_single_device -> pci_scan_device
直接看下重点函数 pci_setup_device
首先获取配置空间中的一些信息 Header Typer、Class Code
通过获取到的Header Typer、Class Code来操作对象
函数分析:这个函数很长,我们先总体说明该的功能,即:继续初始化 pci_dev 结构体的版本号,类型,存储空间,中断线等问题,在代码中,我用一条线将函数分成两个部分。在上半部就是一些常规的操作(从配置空间读取一些信息例如:配置空间类型,设备类型等,将读取到的信息填充到设备结构体,做一些早期的参数修正)。在下半部中函数通过switch语句将设备的配置空间分为三类即:标准类型(6个BAR)、桥类型(2个BAR)、CardBus桥类型(1个BAR),当一个设备在准备遍历的时候会依据该设备配置空间的信息来确定这是个什么设备(例如:读取配置空间的Class code寄存器,得到一个值0x0101表示这是一种大容量存储控制器且使用的是IDE控制器,查表可得)依据不同的设备类型,来获取配置空间的信息填充pci_dev结构。由此我们可知,本函数看似很长,其实内部做的更多的只是依据不同类型的设备去为pci_dev结构填充数据而已。
1.2 pci_scan_single_device -> pci_device_add
函数分析:本函数所做的事情没什么好说的,就是对pci_dev做一些最后的填充,修正后,添加到当前PCI总线的设备链表中,并且调用device_add函数注册相应的kobject。
2. pci_scan_bridge
函数内容过多,不贴了。
函数分析:本函数调用了两次(通过for循环),原因是:在不同架构的对于PCI设备的枚举实现是不同的,例如在x86架构中有BIOS为我们提前去遍历一遍PCI设备,但是在ARM或者 powerPC 中 uboot 是没有这种操作的,所以为了兼容这两种情况,这里就执行两次对应于两种不同的情况,当 pci_scan_slot 函数执行完了后,我们就得到了一个当前 PCI 总线的设备链表,在执行 pci_scan_bridge 函数前,会遍历这个设备链表,如果存在 PCI 桥设备,就调用 pci_scan_bridge 函数,而在本函数内部会再次调用pci_scan_child_bus 函数,去遍历子PCI总线设备(注意:这时的BUS就已经不是PCI BUS 0了)就是通过这种一级一级的递归调用,在遍历总PCI总线下的每一条PCI子总线。直到某条PCI子总线下无PCI桥设备,就停止递归,并修改 subbordinate 参数,(最大PCI总线号)返回。
五、总结
至此,PCI 总线的枚举过程执行完毕,经历了上述这些函数,我们已经得到了 PCI总线下的所有设备的信息,并将它们注册为了 pci_dev 结构体,这时 pci 总线上的设备信息已经准备完毕,现在就是等待后续将要注册的 PCI 驱动与他们进行匹配。梳理了整个过程之后,我们可以了解到,BIOS的枚举:初始化所有PCI设备的配置空间;系统的枚举:依据 BIOS枚举后的配置空间信息生成 pci_dev 设备结构体。