PCI 总线学习系列,参考自
技术大牛博客: PCIe 扫盲系列博文连载目录篇
书籍:王齐老师的《PCI Express 体系结构导读》
下面的文章中加入了自己的一些理解和实际使用中遇到的一些场景,供日后查询和回忆使用
阅读本篇文章前,请阅读 PCI 总线学习笔记(二),对 BDF、BAR 空间、Primary、Secondary、Subordinate Bus Number Register、Base&Limit Register 有所了解
1、PCI 总线的枚举
在 PCI 总线中,系统软件使用深度优先 DFS 算法对 PCI 总线树进行遍历,DFS 算法和广度优先 BFS 算法是遍历树型结构的常用算法。与 BFS 算法相比,DFS 算法的空间复杂度较低,因此绝大多数系统系统在遍历 PCI 总线树时,都使用 DFS 算法而不是 BFS 算法。
DFS 是搜索算法的一种,其实现机制是沿着一颗树的深度遍历各个节点,并尽可能深地搜索树的分支,DFS 的算法为线性时间复杂度,适合对拓扑结构未知的树进行遍历。在一个处理器系统的初始化阶段,PCI 总线树的拓扑结构是未知的,适合使用 DFS 算法进行遍历。下文以图 2-13 为例,说明系统软件如何使用 DFS 算法,分配 PCI 总线号,并初始化 PCI 桥中的 Primary Bus Number、Secondary Bus Number 和 Subordinate Bus number 寄存器。所谓 DFS 算法是指按照深度优先的原则遍历 PCI 树,其步骤如下:
- HOST 主桥开始扫描 PCI 总线 0 上的设备,即开始使用 BDF(0, x, x) 访问 bus 0 上的设备
- HOST 主桥首先发现 PCI 桥 1 ,并将 PCI 桥 1 的 Secondary Bus 命名为 PCI 总线 1。系统软件将初始化 PCI 桥 1 的配置空间,将 PCI 桥 1 的 Primary Bus Number 寄存器赋值为 0,而将 Secondary Bus Number 寄存器赋值为1,即 PCI 桥 1 的上游 PCI 总线号为0,而下游 PCI 总线号为 1
通常这里还会先将 PCI 桥的 Subordinate Bus Number 设置为 0xff。PCI 桥在转发配置空间读写事务时,会判断事务中的 bus 号范围,如果不在范围内,就会丢掉该事务。下面涉及到 PCI 桥的初始化都会先将 Subordinate Bus Number 设置为 0xff,而后再去更新该寄存器。后面就不再赘述了
- 扫描 PCI 总线 1,发现 PCI 桥 2,并将 PCI 桥 2 的 Secondary Bus 命名为 PCI 总线 2。系统软件将初始化 PCI 桥 2 的配置空间,将 PCI 桥 2 的 Primary Bus Number 寄存器赋值为 1,而将 Secondary Bus Number 寄存器赋值为 2
- 扫描 PCI 总线 2,发现 PCI 桥3,系统软件将 PCI 桥 3 的 Primary Bus Number 寄存器赋值为 2,而将Secondary Bus Number 寄存器赋值为 3
- 扫描 PCI 总线 3,没有发现任何 PCI 桥,此时系统软件将 PCI 桥 3 的 Subordinate Bus number 寄存器赋值为 3。系统软件在完成 PCI 总线 3 的扫描后,将回退到 PCI 总线 3 的上一级总线,即 PCI 总线2,继续进行扫描
- 在重新扫描 PCI 总线 2 时,系统软件发现 PCI 总线 2 上除了 PCI 桥 3 之外没有发现新的 PCI 桥,而 PCI 桥 3 之下的所有设备已经完成了扫描过程,此时系统软件将 PCI 桥 2 的 Subordinate Bus number 寄存器更新为3。继续回退到 PCI 总线1
- PCI 总线 1 上除了 PCI 桥 2 外,没有其他桥片,于是继续回退到 PCI 总线 0,并将 PCI 桥 1 的 Subordinate Bus number 寄存器更新为 3
- 在 PCI 总线 0 上,系统软件扫描到 PCI 桥4,则首先将 PCI 桥 4 的 Primary Bus Number 寄存器赋值为 0,而将 Secondary Bus Number 寄存器赋值为4,即 PCI 桥 1 的上游 PCI 总线号为0,而下游PCI 总线号为4
- 系统软件发现 PCI 总线 4 上没有任何 PCI 桥,将结束对 PCI 总线 4 的扫描,并将 PCI 桥 4 的Subordinate Bus number 寄存器更新为4,之后回退到 PCI 总线 4 的上游总线,即 PCI 总线 0 继续进行扫描
- 系统软件发现在 PCI 总线 0 上的两个桥片 PCI 总线 0 和 PCI 总线 4 都已完成扫描后,将结束对PCI 总线的 DFS 遍历全过程
从以上算法可以看出,PCI 桥的 Primary Bus 和 Secondary Bus 号的分配在遍历 PCI 总线树的过程中从上向下分配,而 Subordinate Bus 号是从下向上分配的,因为只有确定了一个 PCI 桥之下究竟有多少条 PCI 总线后,才能初始化该 PCI 桥的 Subordinate Bus 号。
从软件结构上来看,DFS 可以理解为一个递归函数,不断地增加 bus 号,尝试访问当前 bus 上的设备。通常我们会使用 BDF(bus, dev, func) 去访问设备的配置空间
- 如果访问到当前设备为 PCI 桥设备,则 bus number = bus number + 1,继续遍历下一级 bus
- 如果访问到当前设备为 PCI Agent 设备,则 dev = dev + 1,继续遍历当前 bus 上的其他设备
- 如果访问不到当前设备,则返回
2、实战
Linux 源码过于复杂,不易于初学者了解。uboot 是一个很好的切口,代码很少、精炼,更容易理解。
以 uboot 源码为例:
/*
* u-boot-2021.07/drivers/pci/pci.c
*/
pci_hose_scan()
--> pci_hose_scan_bus()
--> pciauto_config_device()
--> pciauto_setup_device
--> pci_hose_scan_bus
--> pciauto_postscan_setup_bridge
从上面的代码逻辑看,递归函数就是 pci_hose_scan_bus
。
int pci_hose_scan_bus(struct pci_controller *hose, int bus)
{
unsigned int sub_bus, found_multi = 0;
......
sub_bus = bus;
/*
* 使用 BDF 遍历当前 bus 上的设备
*/
for (dev = PCI_BDF(bus,0,0);
dev < PCI_BDF(bus, PCI_MAX_PCI_DEVICES - 1,
PCI_MAX_PCI_FUNCTIONS - 1);
dev += PCI_BDF(0, 0, 1)) {
......
/*
* 尝试读取当前设备配置空间中的 PCI_HEADER_TYPE、PCI_VENDOR_ID 参数
* 来判断当前设备是否存在
*/
pci_hose_read_config_byte(hose, dev, PCI_HEADER_TYPE, &header_type);
pci_hose_read_config_word(hose, dev, PCI_VENDOR_ID, &vendor);
/* 设备不存在 */
if (vendor == 0xffff || vendor == 0x0000)
continue;
......
/*
* 代码走到这里,说明当前设备(bus, dev, func)是存在的
* 接下来就是对当前设备进行分析、配置
*/
sub_bus = max((unsigned int)pciauto_config_device(hose, dev),
sub_bus);
}
return sub_bus;
}
pciauto_config_device
函数分析如下:
/*
* HJF: Changed this to return int. I think this is required
* to get the correct result when scanning bridges
*/
int pciauto_config_device(struct pci_controller *hose, pci_dev_t dev)
{
......
unsigned short class;
/* 读取设备配置空间 PCI_CLASS_DEVICE 寄存器 */
pci_hose_read_config_word(hose, dev, PCI_CLASS_DEVICE, &class);
/* 判断当前设备类型,PCI 桥还是 PCI Agent 设备 */
switch (class) {
case PCI_CLASS_BRIDGE_PCI:
/*
* 如果是 PCI 桥设备
*/
/* 配置当前设备,主要是配置 BAR 空间地址 */
pciauto_setup_device(hose, dev, 2, pci_mem,
pci_prefetch, pci_io);
/* bus number = bus number + 1 */
hose->current_busno++;
/*
* 配置当前设备,主要是配置 Primary、Secondary、Subordinate Bus Number
* 以及 Memory Base
*/
pciauto_prescan_setup_bridge(hose, dev, hose->current_busno);
/* 递归遍历下一级 bus */
n = pci_hose_scan_bus(hose, hose->current_busno);
/*
* 配置当前设备,主要是更新 Subordinate Bus Number
* 以及 Memory Limit Base
*/
sub_bus = max((unsigned int)n, sub_bus);
pciauto_postscan_setup_bridge(hose, dev, sub_bus);
sub_bus = hose->current_busno;
break;
case PCI_CLASS_PROCESSOR_POWERPC: /* an agent or end-point */
/*
* 如果是 PCI Agent 设备
*/
debug("PCI AutoConfig: Found PowerPC device\n");
default:
/* 配置当前设备,主要是配置 BAR 空间地址 */
pciauto_setup_device(hose, dev, 6, pci_mem,
pci_prefetch, pci_io);
break;
}
return sub_bus;
}
pciauto_prescan_setup_bridge
、pciauto_postscan_setup_bridge
函数分析如下:
void pciauto_prescan_setup_bridge(struct pci_controller *hose,
pci_dev_t dev, int sub_bus)
{
......
/* Configure bus number registers */
pci_hose_write_config_byte(hose, dev, PCI_PRIMARY_BUS,
PCI_BUS(dev) - hose->first_busno);
pci_hose_write_config_byte(hose, dev, PCI_SECONDARY_BUS,
sub_bus - hose->first_busno);
pci_hose_write_config_byte(hose, dev, PCI_SUBORDINATE_BUS, 0xff);
if (pci_mem) {
/* Round memory allocator to 1MB boundary */
pciauto_region_align(pci_mem, 0x100000);
/* Set up memory and I/O filter limits, assume 32-bit I/O space */
pci_hose_write_config_word(hose, dev, PCI_MEMORY_BASE,
(pci_mem->bus_lower & 0xfff00000) >> 16);
cmdstat |= PCI_COMMAND_MEMORY;
}
......
}
void pciauto_postscan_setup_bridge(struct pci_controller *hose,
pci_dev_t dev, int sub_bus)
{
......
/* Configure bus number registers */
pci_hose_write_config_byte(hose, dev, PCI_SUBORDINATE_BUS,
sub_bus - hose->first_busno);
if (pci_mem) {
/* Round memory allocator to 1MB boundary */
pciauto_region_align(pci_mem, 0x100000);
pci_hose_write_config_word(hose, dev, PCI_MEMORY_LIMIT,
(pci_mem->bus_lower - 1) >> 16);
}
......
}