pci总线扫描及pci网卡驱动

本文讲述的基于intel 总线架构的硬件架构为例来说明linux是如何扫描总线上的PCI设备。

https://images.cnblogs.com/cnblogs_com/xkfz007/201210/201210081409152871.png

CPU通过前端总线FS连接到北桥芯片North Bridge Chip(又称host Bridge), 北桥芯片本身也是PCI总线0上的PCI设备。北桥芯片通过DMI总线连接南桥芯片Sourth Bridge Chip。北桥芯片内置内存控制器,访问内存需要通过北桥芯片; 南桥芯片负责IO操作,以及下挂子pci总线,usb总线等。

http://images2015.cnblogs.com/blog/758933/201610/758933-20161009154757093-113304485.png

Linux kernel扫描pci总线从pci总线0开始,即pci0。

(下面的描述基于linux4.7.1版本, 使用CONFIG_PCI_DIRECT,即由kernel自己去扫描pci总线以及设备,不使用BIOS提供的)

arch/x86/pci/legacy.c

pci_legacy_init() -> pcibios_scan_root(0) /* 参数0表示pci bus 0 */

arch/x86/pci/common.c

pcibios_scan_root() -> pci_scan_root_bus()  /* 扫描root bus, 即PCI BUS 0*/

arch/x86/pci/probe.c

pci_scan_root_bus() -> pci_scan_root_bus_msi() -> pci_scan_child_bus() -> pci_scan_slot()

pci_scan_slot() -> pci_scan_single_device()

pci_scan_single_device() -> pci_scan_device() -> pci_bus_read_dev_vendor_id()

pci_bus_read_dev_vendor_id()会去读取head_type,提取当前pci插槽设备是否是支持多funtion的,即当前插槽为多个逻辑设备domain:bus:device:function, 逻辑设备最多8个。如果是多个逻辑设备,则for循环从逻辑设备1到7开始,继续scan。

pci_bus_read_dev_vendor_id()读取VENDOR_ID,如果ID有效,则该PCI设备存在且有效。

pci_bus_read_config_dword() -> bus->ops->read() /* 这个就是pci_read(), 在common.c */

pci_read() -> raw_pci_read() -> raw_pci_ops->read()

这个raw_pci_ops->read()就是pci_conf1_read(), arch/x86/pci/direct.c

pci_conf1_read()向0Xcf8端口写入要访问的PCI地址(由总线号,逻辑设备号,以及寄存器VENDOR_ID组成)

然后由0xCFC端口读回数据。

pci_scan_single_device()在pci_scan_device()扫描到设备后,调用pci_device_add()将设备添加到bus->p->klist_devices

pci_scan_single_device() -> pci_device_add() -> device_add() -> bus_add_device()

注: 这里的dev->bus是指该设备的bus_type,即pci_dev->dev->bus, 这个pci_dev->dev->bus指向下面的pci_bus_type。

当扫描完PCI bus0总线,得到该总线上的所有PCI设备,包括桥设备,比如PCI-ISA桥,以及PCI-PCI桥等。

下一步,就是扫描pci子总线上的设备。

pci_scan_root_bus() -> pci_scan_root_bus_msi() -> pci_scan_child_bus()调用pci_scan_slot()扫描pci root bus总线上的所有设备后,如果有pci总线级联,则开始扫描子PCI总线。

pci_scan_bridge()从桥设备上读取PCI_PRIMARY_BUS寄存器获得PCI子总线的总线号secondary.

然后通过pci_add_new_bus() -> pci_alloc_child_bus()分配子pci_bus结构内存,最后调用pci_scan_child_bus()扫描pci子总线。下面的操作就和操作pci root bus没什么区别了,直到所有的pci设备都扫描到。

当kernel完成扫描后,它构建了pci bus-device tree(如下图)。这样,kernel就了解了当前系统中有多少pci总线,每个pci总线上有多少pci设备。下一步,就是给对应的设备加载正确的驱动程序了。

PCI驱动加载

pcibios_scan_root() -> pci_bus_add_devices()

pci_bus_add_devices()循环遍历当前总线上的设备以及当前总线的子总线的设备。

pcibios_scan_root() -> pci_bus_add_devices() -> pci_bus_add_device() -> device_attach() -> __device_attach()

系统第一次时,dev->driver没有加载的,即dev->driver为NULL.

__device_attach() -> bus_for_each_drv()

Bus_for_each_drv()从driver list(bus->p->klist_drivers)中寻找匹配的driver.

Driver的注册是在驱动程序中通过调用pci_register_driver() -> __pci_register_driver() -> driver_register() -> bus_add_driver()来完成的。

这里的bus之前介绍过,这里再列一下

这样就进入到了__device_attach_driver()。

bus_for_each_drv() -> __device_attach_driver()

__device_attach_driver()调用__device_attach_driver() -> drv->bus->match()间接调用驱动的match函数。如果驱动的match函数不存在,则这里默认为match(使用通用驱动程序?), 即match返回1,然后进入driver_probe_device().

driver_probe_device() -> really_probe() -> dev->bus->probe(), 即调用pci_bus_type.probe, 这里为pci_device_probe().

如果你的驱动设置了pci_driver.driver.probe(),那么你的驱动程序需要更新了。

pci_device_probe() -> __pci_device_probe() -> pci_match_device() -> pci_match_one_device()

                                                                                -> pci_call_probe() -> local_pci_probe() -> pci_drv->probe()

__pci_device_probe()首先调用pci_match_device()去匹配驱动的vendor_id, device_id, sub_vendor_id, sub_device_id 是否和从pci设备上读取到的这些信息一致。

如果一致(相匹配),则通过pci_call_probe(),最终调用驱动的probe().以8139too.c为例(网卡驱动),就是调用了它的probe(),即rtl8139_init_one().

我们来看看rtl8139_init_one()做了些什么。

rtl8139_init_one():

  • rtl8139_init_board()

这个主要做一些板级初始化,比如通过pci_iomap()将PCI外设配置空间的6个BAR地址映射到cpu线性空间(IO_MEM),即将这6个BAR提供的物理地址更新到页表中。

  • 设置收包处理函数

这里通过netif_napi_add()将rtl8139_poll()设置为软中断上下文中调用一次。

rtl8139_poll()调用rtl8139_rx()从设备的rx_ring中(DMA将数据从pci设备传送到这个rx_ring的位置)取包,然后调用netif_receive_skb()进入协议栈处理。

  • register_netdev()

注册网络设备, 即给协议栈提供了TX发包接口rtl8139_start_xmit()

  •  

这样,这个pci设备就可被使用了。

总结下,kernel首先从pci总线0开始扫描总线0上的设备,如果被扫出的设备是桥设备,则扫描次级pci总线上的设备,如此来构建pci总线设备树。然后为每个pci设备查找合适的驱动程序,并调用驱动的probe来初始化板子,注册网络设备net-device(网卡).

如果pci设备是在上述程序完成后才插入pci插槽的,则设备是在kernel下一次引导之后(reboot)才会生效。

 

PCI设备配置空间

PCi配置空间前64个字节格式如下:

http://images2015.cnblogs.com/blog/758933/201610/758933-20161009155919579-1129326685.png

需要注意的有一下几项:

ClassCode 用于将设备分到具体的功能组,该字段分为两部分,前8个bit表示基类即大类别,后8个比特表示基类的一个子类。比如PCI_BASE_CLASS_STORAGE表示大类大容量存储器,而PCI_CLASS_STORAGE_IDE表明这个IDE控制器。

HeaderType表明头部类型。一般区分为0型头部(PCI设备)1型头部(PCI桥),注意不同头部的配置空间格式有差异。这里我们描述0型头部,即普通PCi设备的配置空间。

PCI HeaderType为一个字节的大小,最高位为0表示单功能,最高位为1表示多功能(即前面描述的逻辑设备),单功能情况下一个PCI设备就是一个逻辑设备。低7位表示头部类型。

前16个字节都是一些基本的信息就不在多说,重点看下接下来的6个BAR空间。每个BAR记录了该设备映射的一段地址空间。为了区分IO空间和IO内存,这里我们分开描述:

http://images2015.cnblogs.com/blog/758933/201610/758933-20161009162841828-1190089842.png

当BAR最后一位为0表示这是映射的IO内存,为1是表示这是IO 端口,当是IO内存的时候bit 1-2表示内存的类型,bit 2为1表示采用64位地址,为0表示采用32位地址。bit1为1表示区间大小超过1M,为0表示不超过1M.bit3表示是否支持可预取。

http://images2015.cnblogs.com/blog/758933/201610/758933-20161009163442463-1483350003.png

而相对于IO内存,当最后一位为1时表示映射的IO地址空间。IO地址空间一般不支持预取,所以这里是29位的地址。

一般情况下,6个BAR是足够使用的,大部分情况都是3-4个BAR。而这些BAR映射的空间是连续的,即这些BAR共同描述设备的地址空间范围

除了6个基本的BAR空间们还有一个额外的配置ROM区间,区间最低位表示是否使用ROM区间,高21位表示地址。中间的是保留项。其余原理和上面类似。

接下来需要注意的就是中断,因为大部分外设和系统交互就是通过中断的方式。

由配置空间中的IRQ Pin决定设备是否支持中断,1表示支持,0表示不支持。假如支持中断,IRQ Line表记录下中断号。

 

PCI桥配置空间

PCI桥同样是连接在PCI总线接口卡上的一个设备,只不过是一个桥设备,连接一条PCI总线。既然同属于设备,那么它同样也就有设备的配置空间,只是它的配置空间和普通设备的有些差异。PCI桥的配置空间在系统软件遍历PCI总线树的时候配置,并不需要专门的PCI驱动,故称为透明桥。PCI桥连接两条总线,和Host 桥近的称为上游总线(Primary Bus ),远的一条称为下游总线(Secondry Bus)。PCI桥的配置空间在前16个字节的格式和普通PCI设备并无区别,另外,桥还保留了普通设备的前两个BAR空间。所以从配置空间的0x18开始有了桥设备自身的配置格式。如前所述,桥设备记录了上游总线和下游总线,以及桥下最大的总线号。这里还有一个比较重要的概念就是窗口。在PCI桥的配置空间有三个窗口:IO地址区间窗口、存储器区间窗口、可预取存储器地址窗口。实际上每个窗口都是一段地址区间,就像一个门,规定了桥下设备映射的区间。从北桥出来的地址,如果在该区间内,就可以穿过该桥到达次级总线,这样依次寻找设备。反过来,从南桥出来的地址及由设备发出的,只有地址不在该区间范围内才可以穿过该桥。因为同一条总线上的设备交互不需要外部空间。就像是内网传输和公网传输一样的道理。

内核中关于桥配置空间的定义如下:

下面说说PCI设备的地址空间:

前面也简单介绍了下PCI设备的地址空间支持PIO和MMIO,即IO端口和IO内存。下面详细分析下这两种方式:

PIO

 IO端口的编址是独立于系统的地址空间,其实就是一段地址区域,所有外设的地址都映射到这段区域中。就像是一个进程内部的各个变量,公用进程地址空间一样。不同外设的IO端口不同。访问IO端口需要特殊的IO指令,OUT/IN,OUT用于write操作,in用于read操作。在此基础上,操作系统实现了读写不同大小端口的函数。为什么说是不同大小呢??因为前面也说到,IO端口实际上是一段连续的区域,每个端口理论上是字节为单位即8bit,那么要想读写16位的端口只能把相邻的端口进行合并,32位的端口也是如此。

例如下面的汇编指令:

OUT 21h,al

0x21是8259A中断控制器的中断屏蔽寄存器,该指令将Intel X86处理器的al寄存器中的值写到中断寄存器的控制寄存器中,从而达到屏蔽某些中断信号的目的。类似的,在下面的汇编指令中:

IN al,20h

0x20是8259A中断控制器的正在服务寄存器,该指令将此寄存器中的状态值传递到处理器的al寄存器中。

无论是windows还是Linux都会上述指令做了封装以满足读写不同长度端口的需要。不过这种方式缺点也比较明显,一般情况CPU分配给IO端口的空间都比较小,在当前外设存储日益增大的情况下很难满足需要。另一方面,

MMIO

IO内存是直接把寄存器的地址空间直接映射到系统地址空间,系统地址空间往往会保留一段内存区用于这种MMIO的映射(当然肯定是位于系统内存区),这样系统可以直接使用普通的访存指令直接访问设备的寄存器,随着计算机内存容量的日益增大,这种方式更是显出独特的优势,在性能至上的理念下,使用MMIO可以最大限度满足日益增长的系统和外设存储的需要。所以当前其实大多数外设都是采用MMIO的方式。

还有一种方式是把IO端口空间映射到内存空间,这样依然可以通过正常的访存指令访问IO端口,但是这种方式下依然受到IO空间大小的制约,可以说并没有解决实际问题。

但是上述方案只适用于在外设和内存进行小数据量的传输时,假如进行大数据量的传输,那么IO端口这种以字节为单位的传输就不用说了,IO内存虽然进行了内存映射,但是其映射的范围大小相对于大量的数据,仍然不值一提,所以即使采用IO内存仍然是满足不了需要,会让CPU大部分时间处理繁琐的映射,极大的浪费了CPU资源。那么这种情况就引入了DMA,直接内存访问。这种方式的传输由DMA控制器控制,CPU给DMA控制器下达传输指令后就转而处理其他的事务,然后DMA控制器就开始进行数据的传输,在完成数据的传输后通过中断的方式通知CPU,这样就可以极大的解放CPU。当然本次讨论的重点不在DMA,所以对于DMA的讨论仅限于此。

那么CPU是怎么访问这些配置寄存器的呢?要知道配置寄存器指定了设备存储寄存器的映射方式以及地址区间,但是配置寄存器本身的访问就是一个问题。为每个设备预留IO端口或者IO内存都是不现实的。暂且不说x86架构下IO端口地址空间只有区区64K,就是内存,虽然现在随着科技的发展,内存空间越来越大,但是也不可能为每个设备预留空间。那么折中的方式就是为所有设备的配置寄存器使用同一个IO端口,(虽然通过同一个端口访问配置空间,但可以通过配置寄存器中的总线号、设备号、功能号来区别是哪一个逻辑设备系统在IO地址空间预留了一段地址就是0xCF8~0xCFF一共八个字节,前四个字节做地址端口,后四个字节做数据端口。CPU访问某个设备的配置寄存器时,先向地址端口写入地址,然后从数据端口读写数据。这里的地址是一个综合地址,结构如下:

http://images2015.cnblogs.com/blog/758933/201610/758933-20161012105216671-963665617.png

Pci地址:<domain>:<bus>:<slot>.<func>

domain: 16bits

bus: 8bits     /* 总线号*/

slot: 8bits     /* slot号*/

func: 3bits    /* 逻辑功能号 */

这里就可以解释我们总线数量和逻辑设备数量的限制了。在寻找某一个逻辑设备时,先根据总线号找到总线,然后根据设备号找到总线上的某个接口,最后在根据功能号定位某一个逻辑设备

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值