总线¶
总线(Bus)是计算机系统中用于在各组件之间传输数据、地址和控制信号的公共通信通道。它负责连接 CPU、内存、存储设备、I/O 设备等,使它们能够高效协同工作。
总线发展的趋势是从并行演变为串行:
- 并行就是用多根线同时传输数据,串行就是用一根线顺序传输数据。看起来串行慢,但并行占用空间大,I/O 引脚过多变得无法接受,且随着传输速率的提高出现了难以解决的串扰问题。
 - 这两种总线架构不同:串行总线只能点对点连接两个设备,而并行总线可以在单一总线上连接多个设备,并且很容易增减总线上的设备。对于串行总线,我们需要使用交换机和多个总线设备连接。
 
Example
- PCI(Peripheral Component Interconnect)是并行总线,PCIe(PCI Express)是串行总线。
 - ATA(Advanced Technology Attachment)是并行总线,SATA(Serial ATA)是串行总线。
 - SCSI(Small Computer System Interface)是并行总线,SAS(Serial Attached SCSI)是串行总线。
 - USB(Universal Serial Bus)是串行总线。
 
PCIe¶
    PCIe 的学习可以从两个方向入手:
- 硬件方向:在 FPGA 上实现一个 SERDES(串行解串器)
 - 软件方向:学习 PCIe 协议栈
 
让我们从软件侧开始,从内核 PCI 驱动入手。
PCI 驱动¶
Quote
- Linux Device Drivers, 3rd Edition, Chapter 12: PCI Drivers
 
PCI 定义了三级分层架构,每个外围设备由三级架构定义:
| 层次 | 最大数量 | 解释 | 
|---|---|---|
| Domain | ? | Linux 用于支持大型系统 | 
| Bus | 256 | |
| Device | 32 | |
| Function | 8 | 一个设备可能有多个功能,如音频和 CD | 
因此每个 Function 可以通过 16 bit 标识。
Example
某个设备的地址为 0000:00:14.0:
| 字段 | 数值 | 
|---|---|
| Domain (16b) | 0000 | 
| Bus (8b) | 00 | 
| Device (5b) | 14 | 
| Function (3b) | 0 | 
总线之间的连接通过 Bridge 完成,整体组成树状结构,最顶层为 Bus 0:
flowchart
    n1["CPU, Memory"]
    n2["Host Bridge"]
    n1 --- n2
    n3@{ label: "Rectangle" }
    n4@{ label: "Rectangle" }
    n2 ---|"Bus0"| n3
    n2 ---|"Bus0"| n4["PCI Bridge"]
    n5@{ label: "Rectangle" }
    n3["PCI Bridge"] ---|"Bus1"| n5["Device"]
    n6@{ label: "Rectangle" }
    n4 ---|"Bus2"| n6["Device"]
PCI 设备具有三种地址空间:
- 内存位置、I/O 端口
- 在总线上广播,所有设备都能看到
 - 由固件决定映射到不冲突的地址范围,可以从配置空间直接得到,所以 PCI 不需要做设备探测
 
 - 配置寄存器(configuration register):
- 每次只寻址一个 slot,永远不会冲突
 - 每个 Function 的配置空间,PCI 有 256 Byte,PCIe 有 4KB,其中有 4 Byte 包含 Function ID
 - 二进制:
/proc/bus/pci/devices和/proc/bus/pci/*/*、/sys/bus/pci/devices 
 
总结:每个设备通过 Geographical 寻址得到配置寄存器,利用其中的信息进行 I/O
PCI 设备的启动过程:
- 上电:没有任何映射,所有功能和中断都关闭,只响应配置事务(configuration transaction)
 - BIOS 或操作系统执行配置,分配安全的地址空间
 - 当设备驱动要访问设备时,内存和 I/O 都已经映射完成
 
PCI 配置寄存器的前 64 字节是标准化的。一些重要的字段:
| 字段 | 内容 | 
|---|---|
| vendorID, deviceID, class | 厂商设置,只读,用于驱动程序寻找设备 | 
| subsystem vendorID, subsystem deviceID | 同上 | 
驱动程序使用内核提供的宏创建 struct pci_device_id 来表达自己支持的设备类型,并使用 MODULE_DEVICE_TABLE 导出到用户空间,让热插拔和模块加载系统知晓。具体来说,在构建内核的 depmod 过程,这些列表会被提取到 /lib/modules/KERNEL_VERSION/modules.pcimap。
过时的信息
.pcimap 和 .usbmap 已经被合并到 .alias 中
// USB driver
static const struct pci_device_id pci_ids[] = { {
    PCI_DEVICE_CLASS(((PCI_CLASS_SERIAL_USB << 8) | 0x20), ~0)
    },
    { /* end */ }
}
// I2C driver
static struct pci_device_id i810_ids[] = {
    { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82810_IG1) },
    { 0, },
}
MODULE_DEVICE_TABLE(pci, i810_ids)
PCI 驱动注册流程如下:
static struct pci_driver pci_driver = {
   .name = "pci_skel",
   .id_table = ids,
   .probe = probe,
   .remove = remove,
};
static int __init pci_skel_init(void)
{
   return pci_register_driver(&pci_driver);
}
- 
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id):当系统认为有应由该驱动程序控制的设备时调用在访问设备资源(I/O 区域或中断)前,需要调用
pci_enable_device() - 
remove()、suspend()、resume():其他状态变化时调用 
内核提供 pci_read/write_config_*() 来访问配置空间:
static unsigned char skel_get_revision(struct pci_dev *dev)
{
   u8 revision;
   pci_read_config_byte(dev, PCI_REVISION_ID, &revision);
   return revision;
}
配置寄存器标准化的字段中,定义了 6 个 Base Address,它们简称为 BAR(Base Address Register),因此设备支持 6 个 I/O 区域。大多数 PCI 设备将其实现为内存区域,涉及缓存问题,需要看配置寄存器中的 memory-is-prefetchable 位:
- 
如果设置了,则 CPU 可以缓存,做任何优化
例子:显卡的显存
 - 
如果未设置,则对该内存区域的访问可能存在副作用,不可以缓存
例子:控制寄存器(control register)
 
内核同样提供接口访问区域位置,无需直接读取寄存器:
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
_end
_flags // IORESOURCE_IO, IORESOURCE_MEM, IORESOURCE_PREFETCH, IORESOURCE_READONLY
中断号由固件分配,驱动只需读取:
本章的剩余部分介绍了 ISA 和其他总线。
PCIe 结构¶
Quote
- A Practical Tutorial on PCIe for Total Beginners on Windows (Part 1) – Reversing Engineering for the Soul:非常优质的 PCIe 入门,通过示例演示,并且给出了精美的可视化。
 
PCI 到 PCIe 的最大转变就是从总线变为了点对点结构,可以类比以太网从集线器到交换机的演进。
- Root Complex 位于 CPU 内,其上具有
- Root Port,可以连接其他 Endpoint 或 Switch。
 - Root Complex Integrated Endpoints:封装在 CPU 内,使用厂商特定协议连接的设备,但仍使用 PCIe 语义通信。例如:集成显卡。
 
 - 在 PCI 规范中:
- Endpoint 也称为 Type0,Switch 称为 Type1。
 - Bus/Device/Funtion (BDF) 也称为 RID(Requestor ID)
 
 
初学者应当正确地理解 PCIe 与 MMIO:
- 不要将操作理解为读写内存,而应该理解为与设备交互,因为实际上并不是内存读写的操作
 - 当读写 PCI 设备的内存区域时,硬件实际上将从 Root Complex 发出 TLP(Transmission-Layer Packet),这对软件完全不可见
 
读写 Configuration Register 的过程:
使用 BAR 的过程:
PCIe Switch¶
PCIe Switch 是一种用于扩展 PCIe 总线的设备,可以将一个 PCIe 总线扩展为多个 PCIe 总线。它利用了 PCIe 串行总线点对点通信的特点。它的应用场景有:
- 
扩展 PCIe 总线:将一个 PCIe 总线扩展为多个 PCIe 总线,可以连接多个 PCIe 设备。多见于 GPU 服务器,一般为 4U,支持 8 卡,比如我们的宁畅 X640 G40。
如下图所示,CPU 只需要向 PCIe Switch 提供 x4 PCIe,就能扩展出 4 个 x4 PCIe 设备和一个 x1 PCIe 设备。当然这肯定会有延迟、带宽之类的损失,比如在该 PCIe Switch 下的所有设备 Host to Device 的总带宽只能局限在 x4,但是 Device to Device 带宽一般可以达到满速。
    PCIe Switch 扩展 PCIe 总线  - 
拆分 PCIe 总线:将一个 PCIe 总线拆分为多个 PCIe 总线,可以连接多个 PCIe 设备。多见于 PCIe 转接卡。这里就不会有总带宽的问题,因为每个设备都有独立的 PCIe 总线。
Example
比如有一些 PCIe x16 转 4 U.2 的转接卡(因为 U.2 每个只占 x4 通道)。这些自带 PCIe Switch 的转接卡不需要主板支持 PCIe 拆分(PCie Bifurcation)。而比较便宜的 PCIe x16 转 4 U.2 转接卡就不带 PCIe Switch,需要主板支持 PCIe 拆分。这就是价格差异的原因。
    PCIe Switch 拆分 PCIe 总线  

