简单了解PCI总线
开始之前
由于部分原因,笔者最近需要了解PCI总线,但是身边没有相关的资料,因此本文有很多东西会因为笔者太菜的原因一笔带过(大嘘),希望读者见谅。
如果读者需要更专业的知识请参考《PCI_Express_Technology》
PCI 基础
总线结构简述
计算机中的总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,CPU 及各种设备都通过这跟总线进行通信
总线按功能可以分为以下三种类型:
- 片内总线:芯片内的总线,位于 CPU 内部,用以在寄存器与寄存器、寄存器与 ALU 之间进行数据交换
- 系统总线:计算机系统内各功能单元(CPU、主存、I/O)之间的公共通信干线,也称之为 内总线
- 通信总线:用于计算机系统之间或是计算机系统与其他系统(例如远程通信设备)之间进行通信的总线,也称之为 外总线
总线是可以扩展的,即可以存在多个不同类型的总线相连,不同的设备接入到不同类型的总线上
PCI概念简述
PCI全称Peripheral Component Interconnect,外围部件互连。其通过多根 PCI bus 完成 CPU 与 多个 PCI 设备间的连接,,在 X86 硬件体系结构中几乎所有的设备都以各种形式连接到 PCI 设备树上
PCI express
是新一代的总线标准,它沿用既有的PCI编程概念及信号标准,并且构建了更加高速的串行通信系统标准
PCI 总线主要被分成三部分:
- PCI 设备。符合 PCI 总线标准的设备就被称为 PCI 设备,PCI 总线架构中可以包含多个 PCI 设备。图中的 Audio、LAN 都是一个 PCI 设备。PCI 设备同时也分为主设备和目标设备两种,主设备是一次访问操作的发起者,而目标设备则是被访问者。
- PCI 总线。PCI 总线在系统中可以有多条,类似于树状结构进行扩展,每条 PCI 总线都可以连接多个 PCI 设备/桥。上图中有两条 PCI 总线。
- PCI 桥。当一条 PCI 总线的承载量不够时,可以用新的 PCI 总线进行扩展,而 PCI 桥则是连接 PCI 总线之间的纽带。主要有以下三种:
- HOST/PCI 桥:也称为 PCI 主桥或者 PCI 总线控制器,用以连接 CPU 与 PCI 根总线,隔离设备地址空间与存储器地址空间,现代 PC 通常还会在其中集成内存控制器,称之为北桥芯片组(North Bridge Chipset)
- PCI/ISA 桥:用于连接旧的 ISA 总线,通常还会集成中断控制器(如 i8359A),称之为南桥芯片组(South Bridge Chipset)
- PCI-to-PCI 桥:用于连接 PCI 主总线(Primary Bus)与次总线(Secondary Bus)
PCI采用树形拓扑结构,一个典型的 PCI 树状结构如下图所示:
由此,一个多层 PCI 总线结构如下图所示:
在 Linux 下我们可以使用 lspci
指令查看插在当前机器的 PCI bus 上的 PCI 设备,使用 -t
参数查看树形结构,-v
参数可以查看详细信息:
1 | root@compute1:~# lspci -t -v |
我们还可以使用 lshw -businfo
命令来获取设备信息:
1 | root@compute01:~# lshw -businfo |
PCI 设备是在内核启动初始化阶段进行枚举的,这个时候可能有的设备还没准备好,从而没被枚举到,这种情况下我们可以使用如下命令重新进行设备枚举:
1 | echo "1" > /sys/bus/pci/rescan |
设备编号
每个PCI 设备都有着三个编号:总线编号(Bus Number)、设备编号(Device Number)与功能编号(Function Number),作为设备的唯一标识;在此之上还有 PCI 域的概念,一个 PCI 域上最多可以连接 256 根 PCI 总线
当我们使用 lspci
命令查看 PCI 设备信息时,在每个设备开头都可以看到形如 xx:yy.z
的十六进制编号,这个格式其实是 总线编号:设备编号.功能编号
,当我们使用 lspci -v
查看 PCI 设备信息时,在总线编号前面的 4 位数字便是 PCI 域的编号
PCI设备配置空间
PCIE配置空间是每个PCIe设备的一段独立空间,系统通过访问这段空间可以获取设备的信息并进行配置。配置空间的结构包括头标区和设备相关区,配置头长度为64字节,包含用于唯一识别设备的字段;剩余的字节则因设备而异,用于描述设备的特定功能。
PCI设备配置空间为256字节,PCIE设备的配置空间为4K字节,两种设备配置头均为64字节,但由于PCIE设备的功能更强大,需要更多扩展空间来描述其特定功能,又为了兼容PCI,PCIE扩展了地址0x100~0xfff用于PCIE扩展配置空间。
在每次启动时,PCIe设备的配置空间地址一般有BOIS分配,分配完成后主设备系统启动后可扫描PCIe设备的配置空间,获取PCIe设备并完成PCIE相关配置。
PCI及PCIE配置空间分配如下图所示
PCIE配置头
PCIE 64字节的配置头分为两种,Type0和Type1,Type0用于描述PCIE Endpoint,Type1用于描述PCIE Bridge或者Switch。
Agent 类型配置空间又被称为 Type 00h
,格式如下图所示:
Bridge 类型配置空间被称为 Type 01h
,与 Agent 类型配置空间大同小异:
简单介绍几个比较重要的字段:
-
设备标识相关:
Vendor ID
:生产厂商的 ID,例如 Intel 设备通常为0x8086
Device ID
:具体设备的 ID,通常也是由厂家自行指定的Class Code
:类代码,用于区分设备类型Revision ID
:PCI 设备的版本号,可以看作 Device ID 的扩展
-
设备状态相关:
-
Status
:设备的状态字寄存器,各 bit 含义如下图所示: -
Command
:设备的状态字寄存器,各 bit 含义如下图所示:
-
-
设备配置相关:
Base Address Registers
:决定了 PCI 设备空间映射到系统空间的具体位置,有两种映射方式:MMIO 与 PMIO,映射方式由最低位决定,不可更改Interrupt Pin
:中断引脚,该寄存器表示设备所连接的引脚Interrupt Line
:中断编号
前面我们讲到 lspci 命令,我们可以使用 -s
来通过指定查看的具体 PCI 设备,通过 -m
查看部分信息,通过 -nn
查看比较详细的信息:
1 | root@compute01:~# lspci -vv -s e2:00.1 -m |
我们还可以直接使用 -x
参数来查看 PCI 设备的配置空间:
1 | root@compute01:~# lspci -s e2:00.1 -x |
在 Linux 当中我们也可以通过 procfs 或 sysfs 这样的文件系统来查看设备的相关配置信息,例如通过 /proc/bus/pci/e2/00.1
文件我们同样可以查看 PCI 设备的配置空间:
1 | root@compute01:~# cat /proc/bus/pci/e2/00.1 | xxd |
通过 /sys/devices/pci0000:e2/0000:e2:00.1
下的其他文件也可以访问该设备的一些其他资源信息(例如通过 resource0
可以直接访问 MMIO 空间,resource1
则为其 PMIO 空间)
PCI Base Address register
BAR(Base Address Register)空间是在PCIe设备中的。BAR是一组特殊的寄存器,用于定义设备的内存和I/O地址空间。这些地址是设备和主机系统通信的基础。
为什么需要BAR?
设备内部使用 RAM 或者寄存器来实现的一些功能,有时需要让外部来访问,比如网卡的队列描述符,DMA 控制器等,需要让系统来探作和写入才能工作;或者 GPU 的显存 RAM 需要系统传输数据,然后 GPU 才能渲染和计算。
CPU和DMA控制器通过物理地址访问内存或设备,这些内部的 RAM 或者寄存器要被访问,就需要统一寻址,也就是统一映射到主机的物理地址空间,BAR的作用是将这些设备资源映射到主机共享的物理地址空间(如内存地址或I/O地址)。
BAR 里面原本存在的值,告诉主机该设备需要多大的内存,后续CPU或DMA访问该地址时,PCIe总线会将其路由到对应设备。
BAR的类型
我们都知道与设备通信有两种方式:MMIO 与 Port IO,相应地 BAR 的格式也有如下两种:
- Memory-Mapped I/O (MMIO) BAR: 这是最常见和推荐的类型。它请求将设备资源(寄存器、内存)映射到系统的物理内存地址空间。CPU 和 DMA 引擎使用普通的
mov
等内存访问指令来读写这些资源,就像访问普通内存一样(虽然实际访问的是设备)。MMIO BAR 还可以细分为:- 32-bit BAR: 请求32位地址空间(最大4GB以下)。
- 64-bit BAR: 请求64位地址空间(大于4GB)。一个64位 BAR 会占用两个连续的32位 BAR 位置(BAR[n] 存放低32位地址,BAR[n+1] 存放高32位地址)。
- Prefetchable vs. Non-Prefetchable: 指示该内存区域的内容是否可以被CPU预取或缓存(通常设备上的内存缓冲区是可预取的,寄存器是不可预取的)。
- I/O Space BAR: 请求将设备资源(通常是寄存器)映射到系统的I/O地址空间(x86架构特有的、较小的独立地址空间)。CPU 需要使用专门的
in
和out
指令来访问这些资源。现代设备和操作系统强烈倾向于使用 MMIO,I/O BAR 已很少见。
BAR的初始化
通过 BAR 进行资源分配的具体过程如下:
-
BAR中低位会在PCI复位时,通过将低位的 bit 设置为 read only 的 0 来标识最小地址空间大小,低位也就不可写入并且为0了。
-
Host 软件 (BIOS 或者操作系统) 读取设备的 BAR 个数和大小。Host 通过向BAR 写全 1,然后读取返回值。设备硬件会返回一个掩码,其中低位连续的
0
表示所需空间的大小(如返回0xFFFF0000
表示需要64KB空间)。如果返回 0, 说明没有实现 BAR。 -
系统根据探测结果,在物理地址空间中找到一个未使用的、对齐的地址范围(例如
0x50000000
),将其写入BAR。此地址成为设备资源的“入口”。配置完成后,设备内部的寄存器或缓冲区(如0x00
~0xFF
)会被映射到主机物理地址的连续范围。
处理器域与 PCI 域间访问
需要注意的一点是,处理器使用存储器域的地址,而 BAR 寄存器存放 PCI 总线域的地址,因此处理器不能直接通过 BAR + offset
的方式访问 PCI 设备的 BAR 空间,而应当要将 PCI 总线域的地址转换为存储器域的地址
由此,PCI BAR 中地址在存储器域中皆有着相应的映像,当处理器访问 PCI 设备的地址空间时,首先访问该设备在存储器域中的地址空间,之后通过 HOST 主桥将存储器域上地址空间转换为 PCI 总线域的地址空间,最后通过 PCI 总线将数据发送到指定的设备中
反之亦然,当 PCI 设备需要访问存储器域的地址空间时(DMA 操作),首先需要访问该存储器地址空间所对应的 PCI 总线空间,之后通过 HOST 主桥将其转换为存储器地址空间,再由 DDR 控制器完成对存储器的读写
例如:
CPU或DMA控制器发出PCI设备的物理地址(如
0x50000008
,地址0x50000000
+偏移0x08
),HOST 主桥会将其转换为 PCI 总线域地址(如0xE000_0008
),再通过 PCI 总线发送给设备。同样,当设备执行 DMA 写入内存时,它使用 PCI 总线域地址(如0xE000_0000
),HOST 主桥会将其转换为存储器域地址(如0x5000_0000
),最终由内存控制器完成写入。
如前面描述,PCIE配置空间中不同type下的BAR配置空间不同,Type0有6个可配置BAR空间,Type1有2个可配置BAR空间。
PCI 设备内存 & 端口空间与访问方式
前面我们讲了 PCI 设备与特性和配置相关的配置空间,现在我们来看与 PCI 设备与实际操作相关的内存映射空间与端口映射空间。
所有 IO 设备的内存与端口空间需要被映射到对应的地址空间/端口空间中才能访问,这需要占用部分的内存地址空间与端口地址空间,上一章已经提到过两种映射外设资源的方式MMIO和PMIO。
完成映射之后通过相应的内存/端口访问到的便是 PCI 设备的内存/端口地址空间。
通过 procfs 的 /proc/iomem
我们可以查看物理地址空间的情况,其中我们便能看到各种设备所占用的地址空间
1 | root@compute01:~# cat /proc/iomem |
通过 procfs 的 /proc/ioports
我们可以查看 IO 端口情况,其中便包括各种设备对应的 PMIO 端口:
1 | root@compute01:~# cat /proc/ioports |
总结PCIE设备通信流程
第一阶段:PCIe 设备接入与总线发现
当一块 PCIe 设备被插入主板时:
- PCIe Root Complex(RC)通过 LTSSM(Link Training and Status State Machine)与设备端口建立物理链路;
- 设备进入 L0 状态,表示链路准备就绪;
- 这时设备尚未有地址,也不在主机内存映射范围内,仅在 RC 侧可探测;
这一步 是自动发生的,不依赖 CPU 或操作系统参与,完全是硬件层完成的。
RC 通常是主板芯片组的一部分(比如 Intel 的 PCH),或者直接集成在 CPU 里,它暴露出多个 PCIe Root Port,每个 Root Port 可以连接一个 PCIe 设备或 switch。负责设备枚举、地址分配、配置、事务处理。是连接 CPU/内存子系统与 PCIe 设备的"网关"。
当你插上一个 PCIe 设备后,主机与设备之间的物理链路还未建立,它们之间要经过一系列握手、训练、信号同步过程才能通信。这个过程就是由 LTSSM 控制的,类似于网络连接建立中的“三次握手”。LTSSM还可以实时维持信号完整性并在纳秒级内恢复链路错误。
通过枚举发现设备
主机并不知道PCIe设备具体在哪,但是主机可以构造配置一个一个的去探测,从总线0、设备0、功能0(BDF 0:0.0) 开始,向每个可能的BDF(Bus/Device/Function)发送配置读请求。如果没有设备则读请求超时,如果都会有效的Vendor ID 和 Device ID,主机就知道这里有一个真实的设备或Bridge存在。
1 | TLP: CFG Read Type 0 → BDF = 00:00.0, Offset = 0x00 (Vendor ID) |
疑惑:主机不知道PCIe的地址其实就无法访问PCIe设备,为什么还能通过BDF枚举来访问到设备?
这里其实是CPU向
CONFIG_ADDRESS
写入一个格式化的值(包含B, D, F, Register Number)后,再对CONFIG_DATA
执行I/O读/写操作时,RC硬件会自动将这个操作转换成一个 配置事务TLP (Configuration Read/Write TLP),而RC能通过PCIe总线发送TLP,从而能找到设备。
资源分配
枚举到设备后,RC 读取其配置空间中的 BAR 寄存器(位于 offset 0x10 开始),主机会尝试向该 BAR 写入全 1(0xFFFFFFFF),设备按协议屏蔽掉其低位(如低 4 位),返回其所需地址大小掩码,主机用该返回值计算出所需资源大小。
主机为此设备 分配未被占用的内存空间,主机将分配好的地址重新写入 BAR,告诉设备:“你应该监听这个物理地址范围的访问。”
设备初始化与驱动加载
PCI中断机制🕊️
PCI 设备有两种打中断的方法:传统的 INTx 中断与 MSI 中断,出于兼容的需要 PCIe 完全继承了这个特性
INTx中断
PCIE协议的分层结构🕊️
PCIe 设备内部层次包括:
- 设备核心层以及它与事务层的接口。设备核心层实现设备的主要功能。如果设备是一个端点,那么它最多可以包含 8 个功能(function),每个功能实现自己的配置空间。如果设备是一个交换机,那么它的核心由数据包路由逻辑和为了实现路由的内部总线构成。如果设备是一个 RC,那么其核心会实现一个虚拟的 PCI 总线 0,在这个虚拟的 PCI 总线 0中存在着所有的芯片组嵌入式端点以及虚拟桥。
- 事务层。事务层负责在发送端产生 TLP(Transaction Layer Packet,事务层包),在接收端对 TLP 进行译码,用户对数据进行组帧和解析是在本层进行。这一层也负责 QoS(Quality of Service,服务质量)、流量控制以及事务排序。
- 数据链路层。数据链路层负责在发送端产生 DLLP(Data Link Layer Packet,数据链路层包),在接收端对 DLLP 进行译码。这一层也负责链路错误检测以及修正,这个数据链路层功能被称为 Ack/Nak 协议。这两个数据链路层功能会在本书的第三部分进行讲解。
- 物理层。物理层负责在发送端产生字符序列包,在接收端对字符序列包进行译码。这一层将处理上述三种类型的包(TLP、DLLP、字符序列包)在物理链路上的发送与接收。数据包在发送端要经过字节条带化逻辑、扰码器、8b/10b 编码器(对于 Gen1/Gen2)或是 128b/130b 编码器(对于 Gen3)以及数据包并串转换模块的处理。最终数据包以训练后的链路速率在所有通道上按照时钟以差分形式输出。在物理层的接收端,数据包处理包括串行地接收差分形式的比特信号,将其转换为数字信号形式,然后将输入比特流做串并转换。这个操作基于来源于 CDR(Clock and Data Recovery,时钟数据恢复)电路所提供的恢复时钟。接收下来的数据包要经过弹性缓存、8b/10b 解码器(对于 Gen1/Gen2)或者 128b/130b 解码器(对于 Gen3)、解扰器以及字节交换恢复逻辑。最终,物理层的 LTSSM(Link Training and Status State Machine,链路训练状态机)负责进行链路初始化以及训练。