Linux下PCI设备驱动开发详解(二)

Linux下PCI设备驱动开发详解(二)

根据上一章的概念,PCI驱动包括PCI通用的驱动,以及根据实际需要设备本身的驱动。

所谓的编写设备驱动,其实就是编写设备本身驱动,因为linux内核的PCI驱动是内核自带的。

为了更好的学习PCI设备驱动,我们需要明白内核具体做了什么,下面我们研究一下,linux PCI通用的驱动到底做了什么?

注:代码对应的 kernel-3.10.1

一、PCI 拓扑架构

1.1 PCI的系统拓扑

在分析PCIe初始化枚举流程之前,先描述下PCIe的拓扑结构。 如下图所示:

整个PCIe是一个树形的拓扑:

arduino 复制代码
(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个目录:

bash 复制代码
drivers/pci/*
drivers/acpi/pci/*
arch/arm/match-xxx/pci.c

将PCIe代码按照如下层次划分:

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决定模块的启动顺序:

perl 复制代码
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初始化相关的调用:

c 复制代码
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():

scss 复制代码
(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的根总线号;
c 复制代码
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枚举流程

我们先看内核代码:

c 复制代码
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空间等信息;

以下依次函数调用是:

c 复制代码
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步:

markdown 复制代码
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(),详细代码如下:

c 复制代码
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的资源枚举过程可以概括为如下:

markdown 复制代码
1. 获取上游PCI桥设备所管理的系统资源范围;
2. 使用DFS对所有的pci ep device进行bar资源的分配;
3. 使用DFS对当前PCI桥设备的base limit的值,并对这些寄存器更新;

四、总结

1. 枚举过程

主要是发现设备,主要流程如下:

markdown 复制代码
1.  发现主桥设备和根总线
2.  发现主桥设备下的所有PCI设备
3.  如果主桥下面的是PCI bridge,那么再次遍历这个PCI bridge桥下的所有PCI设备,依次递归,直到将当前PCI总线树遍历完毕,返回host bridge的subordinate总线号。

2. 资源分配过程

主要是管理设备,方便我们使用设备,主要流程如下:

markdown 复制代码
1. 获取上游PCI桥设备所管理的系统资源范围;
2. 使用DFS对所有的pci ep device进行bar资源的分配;
3. 使用DFS对当前PCI桥设备的base limit的值,并对这些寄存器更新;

五、未完待续

Linux下PCI设备驱动开发详解(三),从内核角度来说,一切皆文件,下面从总线、设备、驱动的角度,详细看一下PCI设备如何变成文件的。

四、参考资料

blog.csdn.net/kunkliu/art...

<PCI Express Base Specification Revision 5.0, Version 1.0>

pcisig.com/

相关推荐
努力的小郑2 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3562 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3562 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁3 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp3 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴4 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友4 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒5 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan6 小时前
Go 内存回收-GC 源码1-触发与阶段
后端
shining6 小时前
[Golang]Eino探索之旅-初窥门径
后端