Linux下PCI设备驱动开发详解(二)
根据上一章的概念,PCI驱动包括PCI通用的驱动,以及根据实际需要设备本身的驱动。
所谓的编写设备驱动,其实就是编写设备本身驱动,因为linux内核的PCI驱动是内核自带的。
为了更好的学习PCI设备驱动,我们需要明白内核具体做了什么,下面我们研究一下,linux PCI通用的驱动到底做了什么?
注:代码对应的 kernel-3.10.1
一、PCI 拓扑架构
1.1 PCI的系统拓扑
在分析PCIe初始化枚举流程之前,先描述下PCIe的拓扑结构。 如下图所示:
::: hljs-center
:::
整个PCIe是一个树形的拓扑:
(1) root complex是树的根,它一般实现了一个主桥设备(host bridge),一条内部PCIe总线bus0,以及通过若干PCI bridge扩展出一些root port。host bridge可以完成CPU地址总线到PCI域地址的转换,pci bridge用于系统扩展,没有地址转换功能;
(2) switch是转换设备,目的是扩展PCIe总线。switch中有一个upstream port和若干个downstream port,每个端口相当于一个pci bridge;
(3) PCIe EP device是叶子节点设备,比如PCIe网卡,显卡。NVMe卡等;
1.2 PCIe的软件框架
PCIe模块涉及到的代码文件很多,在分析PCIe的代码前,先对PCIe涉及的代码梳理如下: 这里以arm架构为例,PCIe代码主要分散在3个目录:
drivers/pci/*
drivers/acpi/pci/*
arch/arm/match-xxx/pci.c
将PCIe代码按照如下层次划分:
::: hljs-center
:::
arch PCIe driver:放一些和架构强相关的PCIe的函数实现,对应arch/arm/xxx/pci.c
acpi PCIe driver: acpi扫描时所涉及的PCIe代码,包括host bridge的解析初始化,PCIe bus的创建,ecam的映射等,对应drivers/acpi/pci*.c
PCIe core driver:PCIe的子系统代码,包括PCIe的枚举流程,资源分配流程,中断流程等,主要对应drivers/pci/*.c
PCIe port bus driver:PCIe port的四个service代码的整合,四个service主要是指PCIe dpc/pme/aer/hp,对应drivers/pci/pcie/*
PCIe ep driver:叶子节点的设备驱动,比如显卡、网卡、NVMe;
二、Linux内核实现
PCIe的代码文件这么多,初始化涉及的调用也很多,从哪里开始看呢?
1. PCIe初始化流程
内核通过initcore的level决定模块的启动顺序:
cat System.map |grep pci|grep initcall
可以看出关键symbol的调用顺序如下:
pcibus_class_init:注册pci_bus_class,完成后创建了/sys/class/pci_bus目录;
pci_driver_init:注册pci_bus_type,完成后创建了/sys/bus/pci目录;
acpi_pci_init:注册acpi_pci_bus,并设置电源管理相应的操作;
acpi_init():acpi启动所涉及到的初始化流程,PCIe基于acpi的启动流程从该接口进入;
下面对acpi_init()流程展开,主要找和PCI初始化相关的调用:
static int __init acpi_init(void)
{
...
pci_mmcfg_late_init();
acpi_scan_init();
...
acpi_pci_root_init();
...
static struct acpi_scan_handler pci_root_handler = {
.ids = root_device_ids,
.attach = acpi_pci_root_add,
.detach = acpi_pci_root_remove,
}
acpi_pci_link_init();
acpi_platform_init();
acpi_lpss_init();
acpi_container_init();
acpi_memory_hotplug_init();
acpi_dock_init();
...
acpi_ec_init();
acpi_debugfs_init();
acpi_sleep_proc_init();
acpi_wakeup_device_init();
...
}
mmcfg_late_init():acpi先扫描MCFG表,MCFG表定义了ecam的相关资源;
acpi_pci_root_init():定义pcie host bridge device的attach函数,ACPI的definition block中使用PNP0A03表示一个PCI host bridge;
acpi_pci_link_init():注册pci_link_handler,主要和PCIe IRQ相关;
acpi_bus_scan():会通过acpi_walk_namespace()遍历system中所有的device,并为这些acpi device创建数据结构,执行对应device的attach函数。根据ACPI spec定义,PCIe host bridge device定义在DSDT表中,acpi在扫描中扫描DSDT,如果发现了PCIe host bridge,就会执行device对应的attach函数,调用acpi_pci_root_add();
acpi_pci_root_add():
(1)通过ACPI的SEG参数,获取host bridge使用的segment号,segment指的是PCIe domain,主要目的是为了突破PCIe最大256条bus的限制;
(2)通过ACPI的CRS里的bus range类型资源取得该host bridge的secondary总线范围,保存在root->secondary这个resource中;
(3)通过ACPI的BNN参数获取host bridge的根总线号;
printk(KERN_INFO PREFIX "%s [%s] (domain %04x %pR)\n",
acpi_device_name(device), acpi_device_bid(device),
root->segment, &root->secondary);
以上流程主要是获取PCI设备的bdf号;
1. PCIe枚举流程
我们先看内核代码:
struct pci_bus *pci_acpi_scan_root(struct acpi_pci_root *root)
{
struct acpi_device *device = root->device;
struct pci_root_info *info = NULL;
int domain = root->segment;
int busnum = root->secondary.start;
...
if (!setup_mcfg_map(info, domain, (u8)root->secondary.start,
(u8)root->secondary.end, root->mcfg_addr))
bus = pci_create_root_bus(NULL,busnum, &pci_root_ops, sd, &resources);
...
}
这个函数主要是建立ecam映射,将ecam的空间进行映射,这样cpu就可以通过内存访问到相应设备的配置空间;
pci_create_root_bus():用来创建该{segment: busnr}下的根总线。传递的参数:
NULL:host bridge设备的parent节点;
busnum:总线号;
pci_root_ops:配置空间的操作接口;
resource:私有数据,用来保存总线号,IO空间,mem空间等信息;
以下依次函数调用是:
pci_scan_child_bus()
+-> pci_scan_child_bus_extend()
+-> for dev range(0, 256)
pci_scan_slot()
+-> pci_scan_single_device()
+-> pci_scan_device()
+-> pci_bus_read_dev_vendor_id()
+-> pci_alloc_dev()
+-> pci_setip_device()
+-> pci_add_device()
+-> for each pci bridge
+-> pci_scan_bridge_extend()
更详细的分析请参见后面的参考资料
总的来说,枚举流程分为3步:
1. 发现主桥设备和根总线
2. 发现主桥设备下的所有PCI设备
3. 如果主桥下面的是PCI bridge,那么再次遍历这个PCI bridge桥下的所有PCI设备,依次递归,直到将当前PCI总线树遍历完毕,返回host bridge的subordinate总线号。
3. PCIe的资源分配
PCIe设备枚举完成后,PCI总线号已经分配,PCIe ecam的映射、PCIe设备信息、bar的个数以及大小等已经ready,但是此时并没有给PCI device的bar、IO、mem分配资源。
这时就需要走到PCIe的资源分配流程,整个资源分配的过程就是从系统的总资源里给每个PCI device的bar分配资源。给每个PCI桥的base、limit的寄存器分配资源。
PCIe的资源分配流程整体比较复杂,主要介绍下总体的流程,对关键的函数再做展开。
PCIe资源分配的入口在pci_acpi_scan_root()->pci_bus_assign_resources(),详细代码如下:
void __ref __pci_bus_assign_resources(const struct pci_bus *bus,
struct list_head *realloc_head,
struct list_head *fail_head)
{
struct pci_bus *b;
struct pci_dev *dev;
pbus_assign_resources_sorted(bus, realloc_head, fail_head);
list_for_each_entry(dev, &bus->devices, bus_list) {
b = dev->subordinate;
if (!b)
continue;
__pci_bus_assign_resources(b, realloc_head, fail_head);
switch (dev->class >> 8) {
case PCI_CLASS_BRIDGE_PCI:
if (!pci_is_enabled(dev))
pci_setup_bridge(b);
break;
case PCI_CLASS_BRIDGE_CARDBUS:
pci_setup_cardbus(b);
break;
default:
dev_info(&dev->dev, "not setting up bridge for bus "
"%04x:%02x\n", pci_domain_nr(b), b->number);
break;
}
}
}
其中pbus_assign_resources_sorted,这个函数先对当前总线下设备请求的资源进行排序。
总而言之,PCIe的资源枚举过程可以概括为如下:
1. 获取上游PCI桥设备所管理的系统资源范围;
2. 使用DFS对所有的pci ep device进行bar资源的分配;
3. 使用DFS对当前PCI桥设备的base limit的值,并对这些寄存器更新;
四、总结
1. 枚举过程
主要是发现设备,主要流程如下:
1. 发现主桥设备和根总线
2. 发现主桥设备下的所有PCI设备
3. 如果主桥下面的是PCI bridge,那么再次遍历这个PCI bridge桥下的所有PCI设备,依次递归,直到将当前PCI总线树遍历完毕,返回host bridge的subordinate总线号。
2. 资源分配过程
主要是管理设备,方便我们使用设备,主要流程如下:
1. 获取上游PCI桥设备所管理的系统资源范围;
2. 使用DFS对所有的pci ep device进行bar资源的分配;
3. 使用DFS对当前PCI桥设备的base limit的值,并对这些寄存器更新;
五、未完待续
Linux下PCI设备驱动开发详解(三),从内核角度来说,一切皆文件,下面从总线、设备、驱动的角度,详细看一下PCI设备如何变成文件的。
四、参考资料
https://blog.csdn.net/kunkliu/article/details/108950970
<PCI Express Base Specification Revision 5.0, Version 1.0>
https://pcisig.com/