Linux驱动之PCI子系统剖析

2022-11-15 21:28:11 浏览数 (1)

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 IDSubsystem 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驱动的抽象

代码语言:javascript复制
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用来匹配设备

代码语言:javascript复制
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方法来进行匹配

代码语言:javascript复制
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函数

代码语言:javascript复制
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匹配

代码语言:javascript复制
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进行严格匹配

代码语言:javascript复制
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 国际许可协议 进行许可。转载请注明出处!

0 人点赞