PCIe总线
PCIe 总线基础
Base Address Registers (BARs)
概述
example
补充
linux PCIe driver
kernel interface
MODULEDEVICETABLE宏
注册一个PCI驱动
使能PCI设备 pci_enable_device
访问配置空间
存取I/O和内存空间
PCIe 总线基础
PCIe扫盲系列PCIe扫盲系列博文连载目录篇(第一阶段)-Felix-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台
原版PDF
https://download.csdn.net/download/yumiano0/86169688?spm=1001.2014.3001.5503
PCI总线是一种树型结构,并且独立于CPU总线,可以和CPU总线并行操作。PCI总线上可以挂接PCI设备和PCI桥,PCI总线上只允许有一个PCI主设备(同一时刻),其他的均为PCI 从设备,而且读写操作只能在主从设备之间进行,从设备之间的数据交换需要通过主设备中转。
注:这并不意味着所有的读写操作都需要通过北桥中转,因为PCI总线上的主设备和从设备属性是可以变化的。
PCI总线是一种地址和数据复用的总线,即地址和数据占用同一组信号线AD。PCI总线的所有信号都与时钟信号同步,及所有的信号的变化都发生在时钟的上升沿,或者在时钟上升沿进行采样。
如下图所示,最左边的即为Memory Address Space,其中包括了多个PCI Memory、AGP Video(显卡)Memory以及Extended Memory、Boot ROM等。中间的为I/O Address Space,需要注意的是,虽然PCI支持32位的地址,但是由于x86的CPU只支持16位的I/O空间,这就限制了PCI的I/O Address Space最大只有64KB。最右边的则为Configuration Address Space,由于每一个PCI设备最多支持8种功能(Function),每一条PCI总线最多支持32个设备,而每一个PCI总线系统最多又支持256个子总线(通过PCI桥)。因此,总的Configuration Address Space的大小为:256 Bytes/function x 8 functions/device x 32 devices/bus x 256 buses/system = 16MB
和很多的串行传输协议一样,一个完整的PCIe体系结构包括应用层、事务层(Transaction Layer)、数据链路层(Data Link Layer)和物理层(Physical Layer)
8B / 10B Encode/Decode详解8B / 10B Encode/Decode详解-Felix-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台
一个Memory Read操作的例子PCIe扫盲——一个Memory Read操作的例子-Felix-电子技术应用-AET-中国科技核心期刊-最丰富的电子设计资源平台
PCIE BAR空间理解PCIE BAR空间_Buyi_Shizi的博客-CSDN博客
【精讲】PCIe基础篇——BAR(Base Address Register)详解【精讲】PCIe基础篇——BAR(Base Address Register)详解_pcie bar_咸鱼弟的博客-CSDN博客
Base Address Registers (BARs)
概述
在一个系统中的每一个设备(device)在地址空间的大小和类型是不同的,对应不同的访问需求。例如,某一个设备在内部寄存器或者存储空间有256Byte,可以通过IO地址空间访问,另个一设备有16K大小的空间则需要通过MMIO的方式进行访问;
哪些地址应该使用哪种方式(IO或Memory)来访问设备的内部位置,这是系统软件(即BIOS和OS内核)的工作,PCI设备无法自己决定,但是需要提供一个方法让系统软件去决定以何种方式去访问设备内部地址空间。一旦软件确认了设备在地址空间的需求,就会分配合适的地址范围,以合适的方式(IO, NP‐MMIO or P‐MMIO)去完成这个任务;
设备提供的方法就是 在configuration space的header 即 Base Address Registers (BARs)。如下图所示,Type0的header有6个可用BARs,每一个32bit,而type1只有两个可用BARs。Type 1报头在所有网桥设备中都可以找到,这意味着每个switch端口都有Type 1报头。Type 0报头在非网桥设备中,比如end point。
系统软件必须要先确认设备地址空间的大小(size)和类型(type,决定通过何种方式映射 IO, NP‐MMIO or P‐MMIO ),这些信息只有硬件设计者才清楚的,所以size和type信息都是通过hard-codes写死在BARs的低bits中,系统软件可以通过读到这些信息,构建出访问硬件地址空间的方法,实现按照device要求的方式去访问到设备内部的地址空间中的数据。
而BARs的高位bits是由软件去填写的,内容是系统软件为此设备分配的地址空间的基地址。
单个EP(EndPoint - type0 header)有6个BARs,最多可以有六个不同的地址空间,但是实际上大多数设备只用到1~3个。如果设备不需要通过BARs去映射内部寄存器,则会通过hard-code在BARs中bit全部填写0,用于告知系统软件这个BAR为不可用。对于被使用的BAR来说,其部分低比特位是不可以被软件操作的,只有其高比特位才可以被软件操作。而这些不可操作的低比特决定了当前BAR支持的操作类型和可申请的地址空间的大小
一旦BAR的值确定了(Have been programmed),其指定范围内的当前设备中的内部寄存器(或内部存储空间)就可以被访问了。当该设备确认某一个请求(Request)中的地址在自己的BAR的范围内,便会接受这请求。
example
下面用几个简单的例子来熟悉BAR的机制:
例1. 32-bit Memory Address Space Request
如下图所示,请求一个4KB的NP-MMIO一般需要以下三个步骤:
如图所示,未初始化的BAR的低比特(11~ 4 )都是0,高比特(31~12)都是不确定的值(用 X 表示)。所谓初始化,就是系统(软件)向整个BAR都写1,来确定BAR的可操作的最低位是哪一位。当前可操作的最低位为12,因此当前BAR可申请的(最小)地址空间大小为4KB(2 ^ 12 )如果可操作的最低位为20,则该BAR可申请的(最小)地址空间大小为1MB( 2^20)。
完成初始化(写1操作)之后,软件便开始读取BAR的值,来确定每一个BAR对应的地址空间大小和类型。其中操作的类型一般由最低四位所决定,具体如上图右侧部分所示。
最后一步是,系统软件在读到 size和type信息后,分配了对应的地址空间,并向BAR的高比特写入地址空间的起始地址(Start Address)。如图中所示,为0xF9000000。
例2. 64-bit Memory Address Space Request
下面是一个申请64MB P-MMIO地址空间的例子,由于采用的是64-bit的地址,因此需要两个BAR。具体如下图所示:
例3. IO Address Space Request
下面是一个申请IO地址空间的例子,如下图所示:
首先是没有初始化的BAR空间,系统软件开始从BAR0开始逐个向整个BAR空间所有bit位写1,然后系统软件去确认设备是否通过BAR3请求额外的地址空间;
系统软件回读BAR3的内容,根据回读数据,判断设备申请的地址空间大小(能操作的最低位 8,256byte)和类型(IO);
系统软件得到设备请求内容后,申请对应的地址空间,并将基地址写入到BAR寄存器;
补充
注:需要特别注意的是,软件对BAR的检测与操作(Evaluating)必须是顺序执行的,即先BAR0,然后BAR1,……,直到BAR5。当软件检测到那些被硬件设置为全0的BAR,则认为这个BAR没有被使用。
无论是PCI还是PCIe,都没有明确规定,第一个使用的BAR必须是BAR0。事实上,只要设计者原意,完全可以将BAR4作为第一个BAR,并将BAR0~BAR3,BAR5都设置为不使用。
但是系统软件提前无法知道每个BARs的使用情况,所以必须要从头开始遍历所有的BARs。
linux PCIe driver
一个总线由电气接口和程序接口组成;
kernel interface
下面我们重点关心一个PCI驱动如何完成匹配到对应的硬件设备并执行访问:
PCI设备有一个8bit的总线号,一个5bit的设备编号以及一个3bit的功能编号,即一个主桥最多有256个总线,总线上最多挂32个设备,每个设备有最多8个功能;
命令行执行lspci或者cat /proc/bus/pci/devices 和 tree /sys/bus/pci/devices/中信息排布。
上图显示PCI设备00:0f:2为例,分隔符将设备地址分为三个区间,首地址00表示总线号,中间1f表示设备编号,末位区间2是功能编号;
首先看 结构体 pci_device_id
static struct pci_device_id plda_pcie_id_table[] =
{
{
vendor : PLDA_VENDOR_ID, // 16bit 标识一个硬件制造商
device : PCI_ANY_ID, //16bit 供应商决定,用来唯一标识一个设备
subvendor : PCI_ANY_ID, //子系统供应商
subdevice : PCI_ANY_ID,
class : 0 , //每个外设都属于一个类. 类寄存器16-位, 它的高8位标识"基类".
class_mask : 0, //驱动指定支持一类PCI设备,如果可以适用任意ID,则指明为 PCI_ANY_ID
driver_data : 0, //这个值不用来匹配一个设备, 但是用来持有信息, PCI 驱动可用来区分不同的设备
},
{0,0,0,0,0,0,0 },
};
MODULEDEVICETABLE宏
这个 pci_device_id 结构需要被输出到用户空间, 当热插拔或者模块加载时系统才能知道什么驱动模块匹配什么硬件设备. 宏 MODULE_DEVICE_TABLE 完成这个. 例如:
MODULE_DEVICE_TABLE(pci, i810_ids);
这个语句创建一个局部变量称为 __mod_pci_device_table, 它指向 struct pci_device_id 的列表. 稍后在内核建立过程中, depmod 程序在所有的模块中寻找__mod_pci_device_table. 如果找到这个符号, 它将数据导出模块并且添加到文件/lib/modules/KERNEL_VERSION/modules.pcimap. 在 depmod 完成后, 所有的被内核中的模块支持的 PCI 设备以及它们的名字被列出在文件中. 当内核告知热插拔系统有新的 PCI 设备已找到, 热插拔系统使用 moudles.pcimap 文件来找到正确的驱动来加载.
注册一个PCI驱动
static struct pci_driver plda_pci_driver =
{
name: DRIVER_NAME,
id_table: plda_pcie_id_table,
probe: plda_pcie_probe,
remove: plda_pcie_remove,
suspend: plda_pcie_suspend,
resume: plda_pcie_resume
};
int __init plda_pcie_module_init(void)
{
int err;
...
if ( 0 != (err = pci_register_driver(&plda_pci_driver)))
{
plda_printk(KERN_ERR DRIVER_NAME ": Init Error, failed to call pci_register_driver.\n");
class_destroy(chardev_class);
plda_pcie_unregister_chrdev(char_major, DRIVER_NAME);
#if SUPPORT_NETLINK_INTERFACE
goto err_out2;
#else
return err;
#endif
}
...
}
使能PCI设备 pci_enable_device
在PCI driver的probe函数中,驱动访问PCI设备资源( I/O 区域 或者 中断)之前需要先调用 pci_enable_device函数:
int pci_enable_device(struct pci_dev *dev);
此函数真正使能设备,并且会分配中断线和I/O空间;
访问配置空间
在驱动匹配到设备后,通常会读写三个地址空间 :内存、端口和配置。而访问配置空间是唯一一个找到设备映射到内存和I/O空间的方法;
linux提供了标准接口去读写设备配置空间:
int pci_read_config_byte(struct pci_dev *dev, int where, u8 *val);
int pci_read_config_word(struct pci_dev *dev, int where, u16 *val);
int pci_read_config_dword(struct pci_dev *dev, int where, u32 *val);
int pci_write_config_byte(struct pci_dev *dev, int where, u8 val);
int pci_write_config_word(struct pci_dev *dev, int where, u16 val);
int pci_write_config_dword(struct pci_dev *dev, int where, u32 val);
存取I/O和内存空间
首先明确一下可预取和不可预取的概念:
可预取指的是CPU 可缓存它的内容并且对它做所有类型的优化;可预取性内存是指存储器空间的可预取能力。例如如果读操作没有副作用(即如同从 RAM 中读数据一样不会破坏数据),则称存储器空间可预取。必要时可将字节写操作合并成一个双字写操作。可预取是读取一次以后不会改变读取地址和存储状态的任何改变,因为CPU 可缓存它的内容并且对它做所有类型的优化;
非预取的内存就象FIFO地址影射到内存地址,读取数据以后会引起FIFO指针的改变.另外还象一些中断状态I/O影射到内存,读取这个内存后,可能会清除中断标志等等,所以CPU不可缓存这个内存地址;
如果满足以下全部条件,则应该设置为可预取的内存状态:
a. 多次读(写)一个长字节产生相同的数据;
b.如果主PCI丢弃读数据,将不会发生负的边际效应;
c.地址空间并没有映射为I/O;
d.允许在转发写缓冲器中进行字节合并;
一个PCI设备最多实现6个I/O地址区域。设备接口会输出对应地址区域的大小和当前位置,6个32bit的配置寄存器,PCI_BASE_ADDRESS_0到PCI_BASE_ADDRESS_5用于表示对应的区域。两个连续的PCI_BASE_ADDRESS寄存器可以用于表示一个64bit的地址空间;
读取设备的输出信息并分配相应的地址空间,内核已经完成这部分工作,并提供接口去获取这部分地址空间的信息:
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
/*The function returns the first address (memory address or I/O port number)
associated with one of the six PCI I/O regions. The region is selected by the integer
bar (the base address register), ranging from 0–5 (inclusive).*/
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
/*The function returns the last address that is part of the I/O region number bar.
Note that this is the last usable address, not the first address after the region.*/
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
//This function returns the flags associated with this resource.
IORESOURCE_IO
IORESOURCE_MEM
IORESOURCE_PREFETCH
IORESOURCE_READONLY
————————————————
版权声明:本文为CSDN博主「大裤衩儿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yumiano0/article/details/127267094