只要是接入系统的 pci 设备就需要和系统软件进行交互,设备和系统之间的交流主要包含以下两部分:
1,系统要能访问到设备的寄存器
用于控制设备行为,包括DMA,数据收发等;设备通过寄存器报告自身的状态,如是否工作正常,有没有什么错误;
2,设备和系统之间互相传输数据
系统定位设备寄存器的位置以及数据收发的位置。换句话说,设备内部的这些资源应该是可寻址的,能够被系统访问到进而完成一些读写操作。一种思路是设备出厂的时候,就固化自己片上存储空间对应在系统中的地址空间,比如对应到0xFE000000h ~ 0xFE000FFF(4KB),这段地址被该设备独占,对这段地址进行ioremap映射后进行访问就可以和该设备交互。可想而知这种方式可扩展性差,毕竟设备有很多,照这样分配地址,那估计系统有多少地址宽度都不够造的。
另外每个设备对于其所需要的地址空间类型(无论是IO映射还是MMIO映射)和大小有多种多样的需求,但是又不能自己配置,所以必须以一定方式提供信息(包括请求的地址空间类型和大小)给系统,从而让BIOS或操作系统内核来为其分配。PCIe设备可能会有一些自己的内部资源,Host通过BAR(Base Address Register)空间访问这些资源,BAR指的是设备配置空间中的4个字节,针对一个设备来说,其有6个BAR地址(实际中所有的BAR并不会都使用,不使用的BAR必须由厂商硬编码为0),设备的每个BAR对应设备上的一段存储空间(或者寄存器空间),系统通过将系统地址写入BAR就建立了存储空间的卡上地址和系统地址空间的映射。BAR空间实际上是Host将这段空间映射到自己的物理地址空间中,因此主机驱动需要先知道这段空间的大小和属性,然后找一段可用的物理地址,再把这个物理地址配置到PCIe地址空间中去。从系统的物理地址空间中分配一段地址空间,并把相应的基地址写回到BAR寄存器。(写入BAR寄存器的地址其实是PCI地址空间的地址,只是x86架构中两种地址空间重合,有的架构比如PowerPC就需要将CPU的物理地址转换为PCI地址空间的地址)。BAR空间在Host上只是分配了一段地址空间,实际对应设备的资源。当Host读写BAR空间时,都会通过PCIe接口(PCI TLP消息)进行实际的数据传输。
BAR 空间实际上是主机将这段空间映射到自己的物理地址空间中,因此主机驱动需要先知道这段空间的大小和属性,然后找一段可用的物理地址,再把这个物理地址配置到PCIe地址空间中去。
在硬件加电初始化时,BIOS固件统一检查了所有的PCI设备, 并统一为他们分配了一个和其他互不冲突的地址,让他们的驱动程序可以向这些地址映射他们的寄存器,这些地址被BIOS写进了各个设备的配置空间的BAR寄存器中,因为这个活动是一个PCI的标准的活动,所以自然写到各个设备的配置空间里而不是他们风格各异的控制寄存器空间里。当然只有BIOS可以访问配置空间。当操作系统 初始化时,他为每个PCI设备分配了pci_dev结构,并且把BIOS获得的并写到了配置空间中的地址读出来写到了pci_dev中的resource 字段中。这样以后我们在读这些地址就不需要在访问配置空间了,直接跟pci_dev要就可以了,我们这里的四个函数就是直接从pci_dev读出了相关数 据,代码在include/linux/pci.h中。定义如下:
#define pci_resource_start(dev,bar) ((dev)->resource[(bar)].start)
#define pci_resource_end(dev,bar) ((dev)->resource[(bar)].end)
在内核中pci_setup_device()函数完成了对单个pci 设备的设备和检测,并将获取的信息存取设备结构体中,用于后期具体设备的使用,在pci_setup_device->pci_read_bases-> __pci_read_base中会decode bar读取bios 中配好的系统地址,并写到创建的 resource中去,这里的地址都是物理的(即写到bar中的地址值),必须要在pci设备驱动的probe函数中调用ioremap 或者 pci_iomap 来将这些地址map成虚拟地址才可以使用,访问这个虚拟地址就可以直接操作pcie 设备上的寄存器(片上物理地址)了:
base_addr = pci_resource_start(pci_dev, bar_idx);
size = pci_resource_len(pci_dev, bar_idx);
virt_addr = pci_iomap(vpri_dev->pci_dev, bar_idx, 0);
pci_read_bases 作用:
1,pci_read_bases首先会依次遍历howmany PCI/PCIe设备的BAR配置;
2,struct resource *res = &dev->resource[pos]获取对应的PCI/PCIe设备对应的BAR的resource结构体,为后面的获取到资源信息做填充使用;
3,__pci_read_base用于具体的PCI/PCIe设备资源获取和解析;
4,如果rom是有效的,则会进一步去解析PCI_ROM_RESOURCE的扩展配置空间的资源信息( #6: expansion ROM resource );
__pci_read_base 的作用:
1,读取PCI/PCIe设备的BAR寄存器,解析PCI/PCIe设备的属性以及资源需求信息;
2,读取BAR寄存器的过程
·读BAR,保留原值;
·写0xFFFFFFFF到BAR;
·再读出来,解析出所需要的地址空间大小,记录在pci_dev->resource[ ]里;
pci_dev->resource[ ].start = 0;
pci_dev->resource[ ].end = size - 1;
·将原值写会BAR寄存器中;
·根据上一步读取出的l和sz去计算设备当前BAR所需要资源的基址和大小,通过pcibios_bus_to_resource配置到对应设备BAR的resource里面(设置resource.start);
涉及的代码如下:
/* 读取BAR寄存器的过程 */
pci_read_config_dword(dev, pos, &l); /* 读BAR到l(起始地址) */
pci_write_config_dword(dev, pos, mask); /* 写BAR全1 */
pci_read_config_dword(dev, pos, &sz); /* 读BAR到sz(长度) */
pci_write_config_dword(dev, pos, l); /* 写BAR l(恢复) */