PCI软件接口
这篇文章主要是总结PCI使用过程中软件上的一些概念,面向对象是软件开发人员,如果错误请指正。 首先介绍PCI和PCIe硬件架构上的一些基本常识,之后着重解析PCI的配置空间。
本篇主要是大概介绍一下PCI诞生的历史背景,PCI 属于局部总线,它的目的就是为了连接各种外部设备,设计的目标是速度快,带宽更高,损耗更低(可以挂接更多的外设)。通常它不和CPU绑定,可以当作一个通用的外设接入到各个arch的CPU上,不过其中很特别的是根桥,各个芯片厂商都在自研桥片来实现PCI协议。 PCI使用并行总线,在频率较低的情况下,并行总线比串行总线传输的带宽更高,但是在高频下并行总线容易被外部干扰,后来PCIe使用高速串行总线逐渐代替并行总线。下面介绍他们拓扑结构上的一些差异 PCI vs PCIe拓扑差异
- PCI是并行共享总线,多个设备可以挂在同一条总线下;PCIe是高速串行总线,一条总线上最多一个设备,不会有多个设备共享总线
- PCI总线被多个设备共享,同一时间只能有一个设备占用总线,所以需要有一个仲裁器来决定哪个设备获得访问权限;PCIe没有仲裁器
- PCIe拓扑结构和PCI中有很大不同,PCIe的体系架构一般由root complex,switch,endpoint等类型的PCIe设备组成。
root complex: 根桥设备,是PCIe最重要的一个组成部件,隔离CPU和PCI空间并负责地址转换; switch: PCIe的转接器设备,目的是扩展PCIe总线,它采用了高速差分总线,并采用端到端的连接方式, 在每一条PCIe链路中两端只能各连接一个设备, 如果需要挂载更多的PCIe设备,那就需要用到switch转接器。switch内部通过串行总线扩展出了多个桥,switch在linux下不可见,软件层面可以看到的是switch的上行口(upstream port, 靠近RC的那一侧)和下行口(downstream port)。 PCIe endponit: PCIe终端设备,是PCIe树形结构的叶子节点。 lane: 一对 Tx/Rx 线代表一个lane,可以使用增加 lane 的方式提高带宽,例如 x2, x4, x8, x16, x32。 - PCIe总线兼容PCI协议,它可以直接接入PCI设备
CPU如何和PCI设备交互呢? PCI提供了几种方式:配置空间和MMIO/IO port,所有的PCI设备都必须要提供标准的配置空间用来提供设备信息和控制接口。 PCI设备有自己的唯一标示地址,但并不是固定写死在设备中的,而是根据PCI插槽的位置来分配的,地址由三部分组成:Bus,Device,Function,简称BDF。 Bus: PCI总线号,占用8 bit,所以PCI/PCIe总线最多支持256个子总线。 Device:总线上PCI的设备ID,占用5位, 所以每个子总线最多支持32个设备。有时也称为slot,代表PCI插槽的位置。 Function:PCI设备的功能ID, 一个PCI 物理设备可以实现多个功能设备,且逻辑功能相互独立,占用3位,所以每个物理设备最多支持8个功能。 传统的 PCI 总线最多支持256个总线,为了突破这个限制,后来又加了 PCI domain,传统的 PCI 只是默认的domain 0。
对于软件开发人员来说,主要需要了解PCI提供的软件接口:配置空间,下面主要就是对配置空间的详细解读。
配置空间访问方式
PCI规范规定了软件需要通过配置空间来初始化和配置设备,其中PCI的配置空间有256字节,而PCIe设备的配置空间有4K字节,其中前面256字节兼容PCI。配置空间的扩张主要是设备越来越复杂,需要更多的空间来,主要是各种capability来使用。
PCI/PCIe总共可以容纳的配置空间数目是相同的:2^16,通过 MMIO 访问配置空间的需要预留一部分的地址空间给 PCI/PCIe 配置空间,所以需要预留出 32M 地址空间,16M给type 0,16M给type 1,而需要为PCIe 预留出 256M 地址空间。
PCI设备规定数据访问使用小端的方式,所以如果CPU使用大端,多个字节访问时需要转换大小端。
访问PCI总共有两种方式,通过 io port 或者 MMIO 的方式。PCI总线最早是在x86上使用的,所以其中充斥着x86的影子,x86上有ioport空间,所以规定使用Ioport访问,但是其他架构上没有 ioport 这种设计,所以其他架构使用PCI时通过MMIO的方式来访问配置空间。
ioport访问配置空间
通过两个端口 CONFIG_ADDRESS(0xCF8)和 CONFIG_DATA (0xCFC) ,通过CONFIG_ADDRESS 指定地址和方向, 而CONFIG_DATA 存放要写的数据或者时读的结果。下面是CONFIG_ADDRESS的规定:
Bit 31 | Bits 30-24 | Bits 23-16 | Bits 15-11 | Bits 10-8 | Bits 7-0 |
---|
Enable Bit | Reserved | Bus Number | Device Number | Function Number | Register Offset |
Enable: 1表示要读数据,0表示写数据 Bus, Device, Function: PCI中的bdf Register Offset: 总共可以表示256字节的偏移。访问总是4字节,所以bit 1:0 总是0
读写配置空间示例:
uint16_t pciConfigReadWord(uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset) {
uint32_t address;
uint32_t lbus = (uint32_t)bus;
uint32_t lslot = (uint32_t)slot;
uint32_t lfunc = (uint32_t)func;
uint16_t tmp = 0;
// Create configuration address as per Figure 1
address = (uint32_t)((lbus << 16) | (lslot << 11) |
(lfunc << 8) | (offset & 0xFC) | ((uint32_t)0x80000000));
// Write out the address
outl(0xCF8, address);
// Read in the data
// (offset & 2) * 8) = 0 will choose the first word of the 32-bit register
tmp = (uint16_t)((inl(0xCFC) >> ((offset & 2) * 8)) & 0xFFFF);
return tmp;
}
void pciConfigWriteWord(uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset, uint32_t val) {
uint32_t address;
uint32_t lbus = (uint32_t)bus;
uint32_t lslot = (uint32_t)slot;
uint32_t lfunc = (uint32_t)func;
uint16_t tmp = 0;
// Create configuration address as per Figure 1
address = (uint32_t)((lbus << 16) | (lslot << 11) |
(lfunc << 8) | (offset & 0xFC));
// Write out data
outl(0xCFC, val);
// Write out the address
outl(0xCF8, address);
}
当访问一个不存在的地址时,写数据时会丢弃所有的请求,读请求时会返回0xffffffff.
MMIO访问配置空间
对于不支持ioport的设计,芯片在划分外设地址空间时会留出来一部分地址空间给PCI配置空间,根据偏移来访问对应的地址空间,这个就需要 host bridge 识别地址空间,将访问 PCI 转发到设备上。
对于PCI的地址计算如下:
Physical_Address = MMIO_Starting_Physical_Address + ((bus - MMIO_Starting_Bus) << 20 | device << 15 | func << 12) + offset
对于PCIe的地址计算
Physical_Address = MMIO_Starting_Physical_Address + (((bus * 256) + (device * 8) + func) * 4096) + offset
在linux 内核中受到x86影响仍然使用 in/out 接口来访问PCI, 使用MMIO方式访问PCI的需要实现这个接口, 将 BDF 转换为对应的MMIO 地址,其中最重要的是地址转换基址MMIO_Starting_Physical_Address。
配置空间解析
PCI/PCIe 设备分为 type0, type1 两种,所以配置空间也有两种类型,他们两者配置空间字段有些不同,但是都需要支持标准的Vendor ID, Device ID, Command, Status , Revision ID, Class Code, Header Type 域,而且位置相同。type0 代表PCI设备,type1代表 PCI 桥。
Vendor ID: 设备厂商ID,这个需要向PCI SIG申请。访问不存在设备的配置空间时返回0xFFFF,代表没有设备 Device ID: 设备ID,也需要申请。一般 Vendor ID + Device ID 在 linux 中用来进行匹配驱动的 Status: 用来显示PCI 相关的状态信息,后面会有字段详细讲 Command: 软件可以通过向其中写数据来完成对设备的控制。其中比较特殊的是向其中写0,设备就会从PCI总线上断开,不会响应除了配置空间之外的任何访问请求 Class Code: 设备的分类,PCI SIG对此有定义。设备支持多个function时,只会有一个function工作,指的时此时的分类 Subclass: 设备类型的子类 Revision ID: 硬件的版本号 BIST: 可以展示BIST的状态和用于控制,一般用来设备自检。 Header Type: 用来识别设备的分类,根据类型确定配置空间的布局。0x0代表 Agent 普通设备,0x1代表 PCI-PCI桥,0x2代表CardBus桥。如果bit7 是 1,代表支持多个function。 Latency Timer: 在PCI总线中,多个设备共享同一条总线带宽,该寄存器用来控制PCI设备占用PCI总线的时间。PCIe设备不需要使用该寄存器,该寄存器的值必须为0。因为PCIe总线的仲裁方法与PCI总线不同,使用的连接方法也与PCI总线不同。 Cache line size: cache缓存大小。对于PCIe设备,该寄存器的值无意义。
Type 0的详细配置空间如下
Interrupt Pin: PCI设备中断引脚, PCI 设备有4个 INTX A/B/C/D四个中断引脚, 0x0代表不使用这种方式的引脚,0x1/2/3/4 分别对应 INT A/B/C/D。当前的设备绝大部分都使用MSI/MSIX 这种基于消息的中断方式了,一般不需要关注这4个引脚了。 Interrupt line: PCI设备使用的中断号, PCI设备的Interrupt pin 会连接到中断控制器上的哪根线。同上,不需要关注。 CardBus CISpointer: 只读,可选,用于表明访问CIS(card info structure)的地址空间, 通常不会涉及。 Subsystem ID/ subsystem Vendor ID: 子系统和子厂商ID,可以结合Device ID和Vendor ID来组成完成的PCI设备标识。 Max_lat: 设备期望的最大延时,只读。 Min_Gbt: 设备期望的最小延时,只读。 Capabilties Pointer: 指向第一个Capability的位置,而第一个Capability中Next又指定下一个Capability,构成了一串Capability,具体如下图所示: 下面详细解释其中比较重要的几个域:
Command 位域
Bits 11-15 | Bit 10 | Bit 9 | Bit 8 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|
Reserved | Interrupt Disable | Fast Back-to-Back Enable | SERR# Enable | Reserved | Parity Error Response | VGA Palette Snoop | Memory Write and Invalidate Enable | Special Cycles | Bus Master | Memory Space | I/O Space |
Interrupt Disable :如果PCI 设备使用INTx 投递中断,它代表是否mask中断。 Memory Space:PCI设备是否允许内存空间访问 I/O Space:PCI设备是否允许 I/O 空间访问
Status 位域
Bit 15 | Bit 14 | Bit 13 | Bit 12 | Bit 11 | Bits 9-10 | Bit 8 | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bits 0-2 |
---|
Detected Parity Error | Signaled System Error | Received Master Abort | Received Target Abort | Signaled Target Abort | DEVSEL Timing | Master Data Parity Error | Fast Back-to-Back Capable | Reserved | 66 MHz Capable | Capabilities List | Interrupt Status | Reserved |
一般需要重点关注Capabilities List 和 Interrupt Status。 Capabilities List:如果设置为1,代表在offset 0x34的位置指向第一个capability,否则代表不支持capability。 Interrupt Status:如果PCI 设备使用INTx 投递中断,显示它当前是否有中断。
BAR BAR: Base Address register, 它负责PCI设备内部空间的映射。PCI 配置空间只是一个标准的头,但是它不参与实际业务,PCI 设备许多的寄存器接口,PCI 允许通过BAR来显示自己需要的地址空间大小,并且在配置映射地址空间之后可以直接访问。
type0 有 6个32bit的BAR寄存器,每个bar 可以是不同的类别:memory space 和 io space, 前者代表是 MMIO 方式访问 PCI 设备,后者代表是x86 上 ioport 方式访问设备。
Memory Space BAR Layout
Bits 31-4 | Bit 3 | Bits 2-1 | Bit 0 |
---|
16-Byte Aligned Base Address | Prefetchable | Type | Always 0 |
Type: 0x0代表bar size最大32位并且只能映射到0-4G; 0x2代表是64位的bar,需要将两个bar合起来才能获得它的长度和设置它的地址
I/O Space BAR Layout
Bits 31-2 | Bit 1 | Bit 0 |
---|
4-Byte Aligned Base Address | Reserved | Always 1 |
如何获取bar的size
- 向BAR寄存器写全1
- 读回寄存器里面的值,然后清除低位编码的值,(IO 中bit0-1, memory中bit0-3)。
- 对读回来的值去反,加一就得到了该设备需要占用的地址内存空间。
系统软件根据bar提供的信息,在系统内存空间找到这样一块地方分配给PCI设备,把分配的基地址写入到BAR寄存器。
Type 1的详细配置空间如下
Primary Bus Number: 表示PCI设备挂在的PCI总线号。 Subordinate Bus Number: PCI桥可以管理其下的PCI总线子树。其中Subordinate Bus Number寄存器存放当前PCI子树中,编号最大的PCI总线号。 Secondary Bus Number: 存放当前PCI桥Secondary Bus使用的总线号,这个PCI总线号也是该PCI桥管理的PCI子树中编号最小的PCI总线号 Secondary Latency Timer: PCI桥下游的延时寄存器,和Latency Timer寄存器的含义相近 I/O base: 表示IO寻址的基地址, 低4bit只读,所以PCI设备默认IO寻址4K对齐,高4bit可写。 I/O limit: IO寻址的上限,高4bit可写,低4bit为全F,所以io寻址上限是I/O limit地址 + 4K. Secondary status: 保存PCI下游总线和设备的状态。 Memory Base: 表示memory寻址的基地址。 Memory Limit: 表示memroy寻址的上限,和IO limit类似。 Prefetchable Memory Base: 可预期内存的基地址 Prefetchable Memory Limit: 可预期内存寻址的上限。 Bridge Control Register:管理PCI桥的Secondary Bus, 可以控制 Secondary Bus的 Reset。
PCI的中断
PCI 支持三种方式的中断:中断线,MSI中断,MSIX中断。 中断线方式需要在硬件上进行连线,设备较多时会共享中断效率较低,中断映射关系复杂。现代的PCI设备基本上都是使用后两者,基于消息的中断。
为什么推出MSI和MSI-X机制 MSI/MSI-X机制的解决了传统Line-based Interrupt机制的种种限制,包括: ? 无需经过I/O APIC转发中断,直接通过PCI/PCIe Memory Write Transaction向CPU发送中断,效率更高 ? 每个PCI Function可以支持分配多个中断向量,满足同一个设备有多个不同中断请求的需要 ? 当分配多个中断向量给同1个PCI Function时,提供按中断向量进行屏蔽的功能,更为灵活
PCI 2.2 支持MSI,此时是可选的,而在PCIe中是必须支持MSI的。MSI/MSIX是基于消息的中断,使用前需要向MSI/MSIX中设置发送消息的地址和数据,当有中断来时根据中断号向地址中写入预先设置好的数据,地址和数据的格式是各个vendor自定义的。这些消息是从PCI设备发起的,需要经过根桥/RC,此时就可以转换消息成中断格式。 Message Control:
Bits 15-9 | Bit 8 | Bit 7 | Bits 6-4 | Bits 3-1 | Bit 0 |
---|
Reserved | Per-vector masking 64-bit | Multiple Message Enable | Multiple Message Capable | Enable | |
Message Address和 Message Data是各个vendor自定义的,例如x86上要求地址格式为 0xFEE0000 + APIC ID信息 , 数据定义为:vector irq + edge 。
uint64_t arch_msi_address(uint64_t *data, size_t vector, uint32_t processor, uint8_t edgetrigger, uint8_t deassert) {
*data = (vector & 0xFF) | (edgetrigger == 1 ? 0 : (1 << 15)) | (deassert == 1 ? 0 : (1 << 14));
return (0xFEE00000 | (processor << 12));
}
而在loongarch 上定义就比较简单,要求地址是0x2ff00000,数据是中断号,中断的分发放在了中断控制器上。 MSI支持一次性发多个中断,例如高性能网卡,raid控制器等,他们有多个队列,一次可以发起多个MSI中断。MSI支持最多32个中断,提供了32bit的 Mask 和 Pending位,其中Pending 位显示它是否被CPU接收到,而Mask 控制当前中断是否可以向CPU投递。
Multiple messages:
MME / MMI | 中断数量 |
---|
000 | 1 | 001 | 2 | 010 | 4 | 011 | 8 | 100 | 16 | 101 | 32 |
MSI-X从设计上解决了MSI存在的不足: ? MSI机制只允许每个PCI Function最多拥有32个中断向量,这对某些应用来说完全不够 ? MSI机制下每个PCI Function的所有中断向量都共用1个Message Address,无法将其分配到不同CPU以实现中断服务在CPU间均衡分配 ? MSI机制下每个PCI Function的所有中断向量都是连续的,在某些平台连续的中断向量意味着同样的中断优先级,无法满足区分中断优先级的需求 MSI-X最多支持2048个中断,它使用了一个基于内存的表存储MSI消息的格式,capabilitiy本身存储数据有限,所以它需要借助 Bar 来向系统申请,BIR指定了哪个Bar 映射了MSI-X消息表, Table Offset 指定了偏移。
Message Control:
Bit 15 | Bit 14 | Bits 13-11 | Bits 10-0 |
---|
Enable | Function Mask | Reserved | Table Size |
Table Size: 实际的表项数量是Table Size + 1,最少有一个表项
MSI-X消息表格式:
Bits 127-96 | Bits 95-64 | Bits 63-32 | Bits 31-0 |
---|
Vector Control (0) | Message Data (0) | Message Address High (0) | Message Address Low (0) | Vector Control (1) | Message Data (1) | Message Address High (1) | Message Address Low (1) | … | … | … | … | Vector Control (N - 1) | Message Data (N - 1) | Message Address High (N - 1) | Message Address Low (N - 1) |
linux pci接口
lspci
lspci:显示所有的pci设备信息。包括设备的BDF,设备类型,厂商信息等。 lspci -s [BDF]:显示指定BDF号的设备信息。 lspci -t:以树的形式显示pci设备信息。 lspci -v/-vv/-vvv:显示详细的pci设备信息,v越多,越详细,当然,上限3个。 lspci -n/-nn:显示设备的vendor厂商号和device设备号;显示厂商等信息和名称。
linux sys PCI接口
PCI 提供了/sys/bus/pci/drivers/{driver}/
# ls /sys/bus/pci/drivers/ixgb/
bind module new_id remove_id uevent unbind
bind 与 new_id 是绑定驱动过程中会使用到的文件,unbind/remove_id 是解绑驱动过程中会使用到的文件。具体的绑定与解绑的过程就是向这几个文件中写入规定格式的数据完成的。 echo "DDDD:BB:DD.F" > bind ,驱动将会和该位置的设备进行绑定,主要用来覆盖掉默认的驱动匹配关系。 echo "DDDD:BB:DD.F" > unbind ,驱动将会该位置的设备进行解绑,通过驱动的 exit 路径停止设备工作。
PCI驱动匹配规则是根据 vendor ID 和 device ID 来为设备加载驱动,默认的 ID 列表在驱动代码中硬编码完成的,但是linux PCI 驱动提供了接口允许在运行时由用户手动指定设备和驱动匹配的规则:
echo “vendor device subvendor subdevice class class_mask driver_data” > new_id
其中 vendor 和 device 是必须要指明的,其他都是可选项。PCI 驱动维护了一个动态的 ID 列表,允许运行时增加和删除,写入之后会触发 PCI 驱动匹配, 扫描 PCI 总线上所有的设备来尝试绑定驱动。 而 remove_id 和new_id的作用相反, 将 PCI ID 从驱动的动态 ID 列表中删除,但并不会主动将驱动和设备解绑。 echo “vendor device subvendor subdevice class class_mask ” > remove_id
下面是在 VFIO 场景下如何使用这些接口:
06:0d.0 Ethernet controller: Beijing Wangxun Technology Co., Ltd. Ethernet Controller RP2000 for 10GbE SFP+ (rev 03)
../../../../kernel/iommu_groups/26
//加载 vfio-pci 驱动
// 查看设备的vendor 和 device
06:0d.0 0200: 8088:2001 (rev 03)
// 将原始驱动和设备解绑
// 将PCI 的 vendor 和 device 添加到 vfio-pci 驱动的动态 ID 列表中,这会触发 vfio-pci 驱动绑定该设备
// 现在已经可以直通该设备到虚拟机中了
.........
// 解绑该设备
//绑定设备到原始驱动中
|