简单了解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 | |
我们还可以使用 lshw -businfo 命令来获取设备信息:
1 | |
PCI 设备是在内核启动初始化阶段进行枚举的,这个时候可能有的设备还没准备好,从而没被枚举到,这种情况下我们可以使用如下命令重新进行设备枚举:
1 | |
设备编号
每个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 设备通常为0x8086Device 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 | |
我们还可以直接使用 -x 参数来查看 PCI 设备的配置空间:
1 | |
在 Linux 当中我们也可以通过 procfs 或 sysfs 这样的文件系统来查看设备的相关配置信息,例如通过 /proc/bus/pci/e2/00.1 文件我们同样可以查看 PCI 设备的配置空间:
1 | |
通过 /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 | |
通过 procfs 的 /proc/ioports 我们可以查看 IO 端口情况,其中便包括各种设备对应的 PMIO 端口:
1 | |
总结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 | |
疑惑:主机不知道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,告诉设备:“你应该监听这个物理地址范围的访问。”
主机为设备分配中断,主机通过设备的MSI/MSI-X Capability结构,分配一个或多个中断向量号(代表特定的中断处理程序)。主机将目标地址(通常是中断控制器特定的地址)和数据值(包含向量号等信息)写入设备的MSI/MSI-X配置寄存器。
当设备需要中断时,它不再拉信号线,而是向配置好的目标地址写入配置好的数据值(发起一个Memory Write TLP)。 中断控制器捕获这个写操作,根据数据值识别出中断向量,从而触发CPU执行相应的中断服务程序。这种方式相当于直接写内存的方式来触发中断,消除了中断线共享冲突的问题。
第四阶段:设备初始化与驱动加载
主机(OS)根据设备的 Vendor ID, Device ID, Class Code, Subsystem Vendor/Device ID 等信息,在驱动数据库中查找匹配的设备驱动程序。
找到匹配驱动后,OS加载该驱动模块。驱动读取设备的配置空间,获取到主机分配好的BAR基地址。驱动使用这些基地址,通过Memory Mapped I/O (MMIO)(访问内存BAR)或 Port I/O (PIO)(访问IO BAR,较少用)的方式,访问设备的寄存器,进行进一步的初始化、重置、参数配置等。
第五阶段:正常通信
主机CPU/其他设备可以通过 Memory Write TLP 向设备的内存映射寄存器(由BAR定义)写入控制命令或数据,通过 Memory Read TLP 读取设备的内存映射寄存器或数据缓冲区。
设备可以通过 Memory Write TLP 向主机内存写入数据(例如,网卡收到数据包DMA到内存,显卡完成渲染帧DMA到帧缓冲区),通过 Memory Read TLP 读取主机内存。
主机上电启动简易流程
x86
1 | |
SoC(ARM等)
1 | |
PCI中断机制🕊️
PCI 设备有两种打中断的方法:传统的 INTx 中断与 MSI 中断,出于兼容的需要 PCIe 完全继承了这个特性
INTx中断
INTx 类型的中断即传统的通过中断引脚来产生的中断,PCI 总线使用 INTA# 、INTB# 、INTC# 、INTD# 信号(低电平有效)向处理器发出中断请求,不过多数设备仅使用 INTA# 信号
下图为一个产生 INTA# 中断信号的流程:
- 设备向南桥上的中断控制器打一个
INTA#,中断控制器转为INTR信号后通过 APIC bus 打向处理器 - 接受中断信号的处理器(未设置则默认都打到 CPU0)通过中断向量表执行对应的处理程序

在 PCI 总线中,设备的 INTx 引脚最终要连接到中断控制器的 IRQ 引脚 ,下图是一个三 PCI 插槽与中断控制器引脚进行连接的例子:

还记得我们前文所讲的 PCI 配置空间中的 Interrupt Pin 与 Interrupt Line 域吗?现在我们可以进一步明确其具体用途了:
Interrupt Pin:记录设备应该使用哪一个 INTx 中断信号Interrupt Line:记录设备连接的引脚

MSI/MSI-X 中断
Message Signaled Interrupt 是一种更为现代化与普遍的 PCI 中断机制,MSI-eXtend 则为其升级版,该机制的引入是为了消除 INTx 的边带信号,目前绝大多数 PCIe 设备已不再使用传统的 INTx 中断,而是使用 MSI/MSI-X 提交中断请求
在 PCIe 设备中有着两个 Capability 结构,分别对应 MSI 与 MSI-X,通常一个 PCIe 设备仅会包含其中一个。对于 MSI 而言其 Capability ID 为 5,一共有四种结构,分别对应 32 位与 64 位的 Message 结构,以及对应的带上中断 Masking 的结构
MSI/MSI-X 本质上是通过向特定的内存区域进行写入来达到中断触发的效果,当 PCI 设备提交请求时,其向 MSI/MSI-x Capability 结构中的 Message Address 地址(PCI总线域)写入 Message Data 数据,从而产生一个存储器写 TLP,由此向处理器提交存储器写请求
MSI 仅支持 32 个连续的中断向量,而 MSI-X 支持 2048 个非连续的中断向量,但 MSI-X 的中断向量信息并不像 MSI 那样直接存放在配置空间,而是存放在 MMIO 空间中,通过BIR(Base address Indicator Register)与 BAR 来确定其在 MMIO 中的具体位置
其结构如下图所示:
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,链路训练状态机)负责进行链路初始化以及训练。