一、PCI-E/PCI拓扑结构
PCI-E点对点结构
PCI总线结构
二、PCI-E总线特性
协议支持258个bus、每条bus最多支持32个device,每个device最多支持8个function
由BDF构成了每个PCI-E设备几点的身份证,用于PCI-E地址空间寻址。
三、PCI-E一般拓扑图
CPU --> RC --> SWITCH
--> ENDPOINT
--> PCIE2PCI Bridge
RC里面包括:
1.host_bridge
2.多个virtual_p2p(每个virtual_p2p也是一个PCIE设备)
四、设备枚举过程
1、PCI-E扫描法则
深度优先
2、扫描过程
(1) 对CPU来说,最开始仅仅知道Bus0的存在,Bus0下面都有什么设备,以及PCI-E树是什么样的一概不知
(2) 从bus0的dev0(是一个virtual_p2p)开始,读dev0的func0的DID&VID,如果不为0表示功能存在;读取
Header寄存器(0:表示是端点设备;1:表示是桥)。如果返回0xFFFF,表示设备不存在。
(3) 如果通过Header寄存器判断是桥设备,那么要写配置空进间:
Primary Bus Number = 2;
Secondary Bus Number = 3;
Subordinate Bus Number = 3;
然后再更新所有上游桥的:Subordinate Bus Number = 3
(这里,2/3是示例值,以实际为准)
(4) a.如果通过Header寄存器判断是EndPoint设备,并且bit7是1,表示一台多功能设备,那么继续扫描每个fun(0~7)。完成之后就返回扫描其他总线
b.如果通过Header寄存器判断是EndPoint设备,并且bit7是0,表示一台单一功能设备,处理完fun0后,返回扫描其他总线("注意:VID是vender ID;DID是device ID")。
五、PCI/PCIE配置空间
1、pci的配置空间是256字节,其中64字节是标准配置空间header, 后面的192字节是Capability结构, 展示pci能提供的能力。 为了兼容PCI,PCIe的配置空间前256字节与PCI保持一致,256~4096字节是pcie 扩展配置空间,包含pcie的扩展能力如AER。
2、PCI标准配置空间头(0 ~ 64 bytes)
PCI标准配置空间分type0和type1两种。type0主要是针对PCI的endpoint设备,type1主要是针对PCI bridge, switch。(知道”五、设备枚举过程“为什么通过0/1来判断是端点设备还是桥设备了吧?)
六、BAR寄存器如何实现地址映射
1、在BAR寄存器有些bit是只读的,是PCI设备在出厂前就固定好的bit,写全1进去,如果值保持不变,就说明这些bit是厂家固化好的,这些固化好的bit提供了这块内部空间的一些信息。
2、举个例子:
上电时,系统软件首先会读取PCIe设备的BAR0(未初始化的BAR寄存器),得到数据:
-------------------------------------------------------------------
|xxxx xxxx xxxx xxxx xxxx | 0000 0000 | 0 | 0 0 | 0 |
-------------------------------------------------------------------
31 12 11 4 3 2 1 0
然后系统软件往该BAR0写入全1(BAR寄存器写0xffffffff),得到:
-------------------------------------------------------------------
|1111 1111 1111 1111 1111 | 0000 0000 | 0 | 0 0 | 0 |
-------------------------------------------------------------------
31 12 11 4 3 2 1 0
3、低12bit没变,表明该设备空间大小是4KB(2的12次方),BAR地址的低4位表明了该存储空间的一些属性([0]: IO映射还是memory映射,[2:1]32bit地址还是64bit地址,[3]能否可预取)。然后系统软件根据这些信息,在PCI总线地址空间分配4KB,并把分配到的基地址(PCI总线地址空间)写入到BAR寄存器(bit12~bit31),这样我们就可以在构建一张表,用于记录所有PCI设备所需要的PCI总线空间空间。这也是PCI枚举的主要任务之一。
这里,非常重要的一个概念是,BAR读取到的是PCI地址空间中的地址,不等同于CPU认识的内存地址。虽然在x86上如果没有开启IOMMU时,它们的值一般是相同的,但是对于其他构架的CPU如PowerPC就可以是不一样的。所以正确的使用BAR空间的方法:
pciaddr=pci_resource_start(pdev,1);
if(pciaddr!=NULL)
{
ioremap(pciaddr,xx_SIZE);
}
错误的方法:
pci_read_config_dword(pdev,1,&pciaddr);
ioremap(pciaddr,xx_SIZE);
4、BAR寄存器存储的总线地址,应用程序是不能直接利用的,应用程序首先要做的就是读出BAR寄存器的值,然后用mmap函数建立应用程序内存空间和总线地址空间的映射关系。这样应用程序往PCIE设备内存读写数据的时候,直接利用PCIE设备映射到应用程序中的内存地址即可。但是应用程序的内存地址该由谁解析到PCIE设备对应的总线空间给EP呢,这个工作是由北桥或者是RC(root complex)来完成的,解析到总线地址空间之后,EP会把总线的地址空间解析成PCIE设备对应的设备内存地址。
七、CPU mem地址空间/IO地址空间以及PCI Memory/IO/Configuration空间
(Fig. 1 CPU Memory空间、CPU I/O空间和PCI Memory/IO/Configuration空间)
PCI体系结构中支持三种地址空间:Memory空间、I/O空间、Configuration空间(Fig.1紫色部分)。
x86的处理器可以直接访问CPU Memory空间和I/O空间。PCI的Memory/IO 空间可以映射到CPU的Memory/IO空间以供CPU读写。特别要注意的是:PCI Memory地址空间支持32bit/64bit寻址;在PCI I/O地址空间中,PCI支持32bit地址,但是x86只用了16bit访问I/O空间,所以很多平台的I/O空间大小限制在64KB。
PCI还引入了配置空间(Configuration Space),而且CPU只能间接访问配置空间。每个功能(Function)包含配置空间的内部寄存器,允许软件以一种标准化的方式显示和控制其地址和资源,在PC中提供一个真正的“即插即用”环境。每个PCI功能(PCI Function)最大包含256 Bytes配置空间。鉴于在一个系统中,最多支持256条总线,每条总线最多支持32个设备,每个设备最多支持8个功能(Function),那么总的配置空间大小就是:256 Bytes * 8 * 32 * 256 = 16MB。
鉴于CPU不能直接访问PCI设备的配置空间,所以只能通过IO寄存器进行间接访问(PCIe设备还支持将配置空间映射到Memory空间进行访问)。就像图Fig.1所展现的那样,CPU通过配置地址端口(Configuration Address Space)和配置数据端口(Configuration Data Port)访问PCI设备的配置空间。配置地址端口的地址是CF8h~CFBh(写入配置空间某一行在配置空间中的地址),配置数据端口的地址是CFCh~CFFh(写入配置空间某一行的数据)。
lspci -vvv命令基本上就是读取所有PCI设备的配置空间信息,格式化后输出:
八、CPU如何读写PCI设备空间——CPU地址如何转换到PCI地址
关于地址相关的问题,搞清楚这三个地址之间的关系就可以了:
存储器地址(就是CPU,DMA等设备直接读写的地址)。
TLP中的地址。
BAR空间地址。
如果两两组合的话,能够形成三种关系,但是事实上,这三者之间的关系其实就两部分:
存储器地址和TLP地址字段的关系。
TLP地址字段和BAR空间地址的关系。
解决这两个问题,地址相关的问题就应该都清楚了。
1、存储器地址和TLP地址字段的关系
TLP中的地址哪里来?ATU转换过来的。这个问题就是这么的简单。ATU是什么?是一个地址转换单元,负责将一段存储器域的地址转换到PCIe总线域地址,除了地址转换外,还能提供访问类型等信息,这些信息都是ATU根据总线上的信号自己做的,数据都打包到TLP中,不用软件参与。软件需要做的是配置ATU,所以如果ATU配置完成,并且能正常工作,那么CPU访问PCIe空间就和访问本地存储器空间方法是一样的,只要读写即可。
这就解释了存储器地址和TLP地址字段的关系了。至此,地址相关的问题就解决了。
ATU配置举例:以kernel 4.4中designware PCIe host驱动为例:
static void dw_pcie_prog_outbound_atu(struct pcie_port *pp, int index,
int type, u64 cpu_addr, u64 pci_addr, u32 size)
{
// 使用哪个ATU
dw_pcie_writel_rc(pp, PCIE_ATU_REGION_OUTBOUND | index,
PCIE_ATU_VIEWPORT);
// source地址(存储器域)的低32位
dw_pcie_writel_rc(pp, lower_32_bits(cpu_addr), PCIE_ATU_LOWER_BASE);
dw_pcie_writel_rc(pp, upper_32_bits(cpu_addr), PCIE_ATU_UPPER_BASE);
// space size
dw_pcie_writel_rc(pp, lower_32_bits(cpu_addr + size - 1),
PCIE_ATU_LIMIT);
// 目标地址空间(PCIe总线地址)
dw_pcie_writel_rc(pp, lower_32_bits(pci_addr), PCIE_ATU_LOWER_TARGET);
dw_pcie_writel_rc(pp, upper_32_bits(pci_addr), PCIE_ATU_UPPER_TARGET);
// 空间类型(mem or IO)
dw_pcie_writel_rc(pp, type, PCIE_ATU_CR1);
// 使能ATU
dw_pcie_writel_rc(pp, PCIE_ATU_ENABLE, PCIE_ATU_CR2);
}
2、TLP地址字段和BAR空间地址的关系
首先要知道BAR有什么用?通过BAR寄存器,我们首先知道这个基址对应的空间属性,然后给这段空间分配一个基址(这个基址只是用来路由寻址用的,不能和存储器空间的地址搞混,很多软件实现上会把两个地址设置成一样,但是本质上没有任何关系,只是TLP寻址的时候用的!)。这样的话,TLP就能根据地址被路由到对应设备的BAR空间中去。比如说现在有一个mem read request,如果路由地址(地址信息包含在TLP中)是0x71000000,而有一个设备func0的mem空间范围是0x70000000~0x80000000,那么这个TLP就会被这个func处理。从func0的0x71000000对应的地址读取相应数据。
九、Linux访问PCIe的Memory空间、I/O空间、Configuration空间
1、访问配置空间(位于drivers/pci/access.c)
1.1 读配置空间
pci_read_config_byte(const struct pci_dev *dev, int where, u8 *val);
pci_read_config_word(const struct pci_dev *dev, int where, u16 *val);
pci_read_config_dword(const struct pci_dev *dev, int where, u32 *val);
1.2 写配置空间
pci_write_config_byte(const struct pci_dev *dev, int where, u8 *val);
pci_write_config_word(const struct pci_dev *dev, int where, u16 *val);
pci_write_config_dword(const struct pci_dev *dev, int where, u32 *val);
2、访问BAR空间
1、在枚举过程中,PCI驱动已经分配了address给各个BAR,通过一些接口就可以访问到BAR Resource。
2、获取BAR resource(位于include/linux/pci.h)
pci_resource_start(dev, bar)
pci_resource_end(dev, bar)
pci_resource_flags(dev, bar)
pci_resource_len(dev, bar)
通过pci_resource_start宏取得bar值之后,Linux认为这个地址是IO地址,如果要访问的话可以通过ioremap映射到内核空间,然后通过readl/writel等IO接口进行操作。
参考资料:
PCIe实践之路:Linux访问PCIe空间_Camus-CSDN博客
(17条消息) PCI Address Space Map_老王的笔记-CSDN博客
(17条消息) PCIe实践之路:BAR空间和TLP_Camus-CSDN博客
深入PCI与PCIe之二:软件篇 - 知乎 (zhihu.com)