PCI是外围设备互连(Peripheral Component Interconnect)的简称,作为一种通用的总线接口标准,它已经普遍使用在了计算机中。PCI总线常见于x86体系,本文默认面向的体系为x86,注意x86架构下IO与内存是独立编址的。
附: 本文默认读者熟悉Linux设备驱动模型,不熟悉的可以先阅读这两篇blog。 Linux驱动之I2C子系统剖析 Linux驱动之SPI子系统剖析
PCI寻址
PCI系统总体布局组织为树状,从CPU连接的Host Bridge引出PCI主桥,主桥连接的是PCI总线0,可以直接连接PCI设备,或者再挂上PCI桥引出下一级PCI总线。
每个PCI设备由一个总线号、设备号和功能号确定。PCI规范允许一个系统最多拥有256条总线,每条总线最多带有32个设备,每个设备可以是最多8个功能的多功能板,但是对于大型系统而言总线数不够,故还支持PCI域,每个PCI域可最多支持256个总线。
- PCI域: 16位
- 总线号: 8位
- 设备号: 5位
- 功能号: 3位
在PC机上可以使用lspci
查看计算机上PCI设备信息,笔者在自己电脑上执行该命令后输出如下
每一行表示一个PCI设备或者PCI桥,而每行的开头即表示总线号、设备号和功能号。
PCI配置寄存器
所有的PCI设备都有至少256字节的地址空间,其中前64字节是标准化的,被称为PCI配置寄存器,剩下的字节是设备相关的 (取决于具体的厂商,需要查看datasheet得知)。
PCI配置寄存器如下图所示。
- Vendor ID: 标识硬件厂商,需要向特定组织进行注册。
- Device ID: 由硬件厂商来分配的设备ID,无需对ID进行注册。
- Subsystem ID、Subsystem Vendor ID: 用来进一步标识设备。
硬件标识信息在硬件出厂时就写入相应设备中了。
当BIOS启动时,会为每个PCI设备分配内存、IO空间以及irq号,并写入相应PCI设备的配置寄存器中。Linux内核启动时会从PCI设备的配置寄存器里读取内存/IO起始地址以及irq,并把这些信息赋值给struct pci_dev
的相应成员来生成软件描述的PCI设备。
从上图的寄存器分布中可以看到中间有一段地址空间描述BARS(Base Address Register),这些寄存器组用来存储备PCI设备工作时的io地址、irq号和mem地址起始地址以及长度。这些信息存储的具体位置需要查阅相应PCI设备的datasheet方可得知,在内核中提供了以下几个接口来获取这些资源。
代码语言:javascript复制/*
* dev为PCI设备的软件抽象,bar的取值为0 ~ 5
* 这三个函数分别返回第bar个区域的首地址、尾地址和长度
*/
unsigned long pci_resource_start(struct pci_dev *dev, int bar);
unsigned long pci_resource_end(struct pci_dev *dev, int bar);
unsigned long pci_resource_len(struct pci_dev *dev, int bar);
/*
* 返回和这个bar相关联资源的标识
* IORESOURCE_IO:io端口
* IORESOURCE_MEM:内存
*/
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
内核提供了一组接口来访问配置空间。
代码语言:javascript复制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)
PCI驱动的注册及匹配
BIOS在启动时,会为每个PCI设备分配地址和irq等信息,并写入各个PCI设备的配置寄存器中,所以PCI设备无需像其他总线那样去注册设备。
内核中使用struct pci_dev
来描述PCI设备的抽象。当linux系统启动时,会探测系统中的所有PCI设备,并为探测到的每个PCI设备做如下操作:
1.分配一个struct pci_dev
结构体,用来表示相应的PCI设备
2.为这个结构体填充设备vendor id、device id、subvendor id、subdevice id以及地址和irq信息(通过读取PIC配置寄存器得到)
3.最后把这个struct pci_dev
结构体挂接到pci_bus
上
内核中使用struct pci_driver
来描述PCI驱动的抽象
struct pci_driver {
struct list_head node;
char *name;
const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */
void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */
int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */
int (*suspend_late) (struct pci_dev *dev, pm_message_t state);
int (*resume_early) (struct pci_dev *dev);
int (*resume) (struct pci_dev *dev); /* Device woken up */
void (*shutdown) (struct pci_dev *dev);
struct pci_error_handlers *err_handler;
struct device_driver driver;
struct pci_dynids dynids;
};
其中id_table
用来匹配设备
struct pci_device_id {
__u32 vendor, device; /* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice; /* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask; /* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data; /* Data private to the driver */
};
PCI驱动的注册接口为pci_register_driver(struct pci_driver *drv)
,当调用该接口后,会调用PCI总线下的match方法来进行匹配
static int pci_bus_match(struct device *dev, struct device_driver *drv)
{
struct pci_dev *pci_dev = to_pci_dev(dev);
struct pci_driver *pci_drv = to_pci_driver(drv);
const struct pci_device_id *found_id;
found_id = pci_match_device(pci_drv, pci_dev);
if (found_id)
return 1;
return 0;
}
可以看到pci_bus_match
调用的是pci_match_device
函数
static const struct pci_device_id *pci_match_device(struct pci_driver *drv,
struct pci_dev *dev)
{
struct pci_dynid *dynid;
/* Look at the dynamic ids first, before the static ones */
spin_lock(&drv->dynids.lock);
list_for_each_entry(dynid, &drv->dynids.list, node) {
if (pci_match_one_device(&dynid->id, dev)) {
spin_unlock(&drv->dynids.lock);
return &dynid->id;
}
}
spin_unlock(&drv->dynids.lock);
return pci_match_id(drv->id_table, dev);
}
最终调用的是pci_match_id
匹配
const struct pci_device_id *pci_match_id(const struct pci_device_id *ids,
struct pci_dev *dev)
{
if (ids) {
while (ids->vendor || ids->subvendor || ids->class_mask) {
if (pci_match_one_device(ids, dev))
return ids;
ids ;
}
}
return NULL;
}
遍历id_table,调用pci_match_one_device
进行严格匹配
static inline const struct pci_device_id *
pci_match_one_device(const struct pci_device_id *id, const struct pci_dev *dev)
{
if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) &&
(id->device == PCI_ANY_ID || id->device == dev->device) &&
(id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) &&
(id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) &&
!((id->class ^ dev->class) & id->class_mask))
return id;
return NULL;
}
分别对vendor、device、subvendor、subdevice和class进行匹配,除非某一项配置为PCI_ANY_ID
,否则都要进行严格匹配,只要有一项匹配不上则直接匹配失败。
本文作者: Ifan Tsai (菜菜)
本文链接: https://cloud.tencent.com/developer/article/2164590
版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!