NTF的博客

记录留下点东西

linux 内核PCI驱动总结记录

1.  介绍

Peripheral ComponentInterconnect (PCI,外围设备互联)。总线由电气接口、编程接口组成。主要讨论编程接口。最常用的总线,内核支持最好的总线。ISA裸金属总线,电子爱好者偏爱。

2.  PCI的特点

是一种完整的规范,定义计算机计算机不同部分之间的通信。

 

获取、访问PCI设备。

 

对比ISA总线三个目标:

比ISA有更好的性能。

尽可能平台无关的。

简化系统添加、删除外设。

 

支持32位、64位数据总线。

对驱动编写者最相关的是接口板的自动发现。PCI设备是无跳线设备,在系统引导阶段自动配置。包括设备的配置信息等一些工作都是自动完成的,不需要任何的探测。之后,驱动编写者就能够访问设备的配置信息,以便初始化设备。

 

3.  PCI寻址

每个PCI外设由bus:device.function一个16位地址标识。bus(8位)、device(5位)、function(3位)。单个总共256个总线、每个总线最多32个设备、每个设备最多8个功能(比如声音功能)。linux为了扩展总线数量,提供domain(16位)。系统把PCI设备抽象为pci_dev结构,因此不需要访问这些二进制地址。

 

当前的工作站一般都配有2个以上的pci总线。不同PCI总线之间通过PCI桥连接(一个特殊的PCI设备)。PCI系统的整体布局是一个树状的结构。每个总线都连接上一级总线,一直到根总线0.

可以使用lspci命令查看当前系统的pci。或者在文件系统/proc/pci和/proc/bus/pci中。


外设板电路响应三种地址空间:内存、IO、配置空间。前两种地址空间在同一个PCI总线上是共享的。配置空间是物理寻址的,每次只对一个槽寻址。

内存和IO空间通常通过inb、readb等方式访问。配置空间需要通过特殊的内核函数访问配置寄存器。每个PCI槽有4个中断引脚,每个设备功能使用其中的一个。

         1个PCI总线使用32位的地址总线用于IO寻址(4G),32位的地址总线(现在设备有的支持64位)用于内存寻址。在系统启动阶段,固件初始化PCI硬件的时候,把每个区域映射到不同的地址。驱动程序不需要探测,而从配置空间读取映射的地址。

         对于每个设备功能,PCI配置空间由256字节组成(PCIE的是64KB),并且配置空间的布局是标准的。配置空间的4个字节(哪4个字节?)标识唯一的功能ID。

 

4.  引导阶段

主板上的固件(比如BIOS),读写PCI设备中的寄存器,访问配置空间。

系统引导阶段,linux内核为每个PCI地址区域申请安全的处理器地址。后续驱动可以从/sys/bus/pci/devices/*目录中读取映射的地址。

$ tree /sys/bus/pci/devices/0000:00:10.0
/sys/bus/pci/devices/0000:00:10.0
|-- class
|-- config
|-- detach_state
|-- device
|-- irq
|-- power
|  `-- state
|-- resource
|-- subsystem_device
|-- subsystem_vendor
`-- vendor

其中,config包含配置信息,resource包含分配给该设备的内存资源。irq包含了该PCI设备的中断号。


5.  配置寄存器和初始化

所有的PCI设备至少包含256字节的配置地址空间(PCIE是64KB)。其中前64字节是标志的。


PCI配置寄存器包括可选和必需两部分,必需的部分声明功能和其他字段是否可用。

PCI寄存器是小端字节序。

  • vendorID

全局性、全球性。16位标识。比如intel的0x8086.

  • deviceID

厂商定义的16位标识。通常使用vendorID+deviceID 32位标识一个设备。驱动根据该32位标识,定位到一个设备。

  • class

16位的标识,高8位标识基本类(group)。比如,以太网、令牌环网属于网络group,串行、并行属于通信group。一些驱动支持多种相同类型的设备,驱动可以根据类型区分支持的设备。

  • subsystem vendorID
  • subsystem deviceID

subsystem类型的标识,用于进一步识别设备。当一个芯片是连接到本地板载上的通用芯片时,它可能有多用用途。驱动使用subsystem标识,识别具体连接的设备。

内核标识设备ID的结构是:

struct pci_device_id {

         __u32vendor, device;               /* Vendorand device ID or PCI_ANY_ID*/

         __u32subvendor, subdevice;  /* Subsystem ID'sor PCI_ANY_ID */

         __u32class, class_mask; /*(class,subclass,prog-if) triplet */

         kernel_ulong_tdriver_data;   /* Data private to thedriver */

};

6.  MODULE_DEVICE_TABLE

通过把pci_device_id结构导出到用户空间中,使热插拔和模块加载系统知道什么模块对应什么设备。

例如:

MODULE_DEVICE_TABLE(pci, i810_ids);

具体的实现是:

extern const typeof(name)__mod_##type##__##name##_device_table              \

 __attribute__ ((unused, alias(__stringify(name))))

 

其中pci是模块名,i810_ids是pci_device_id变量名。MODULE_DEVICE_TABLE宏把例如i810_ids的变量名,起一个__mod_pci_device_table结构的别名。模块编译之后,在对应的模块ELF文件中会有相应的__mod_pci_device_table结构符号。在内核构建时,depmod搜索所有模块的类似__mod_pci_device_table结构的符号,从中解析出type和name,并取出pci_device_id数据导出到/lib/modules/KERNEL_VERSION/modules.pcimap文件中。之后内核所有模块支持的设备和模块的名字可在该文件中找到。当内核告知热插拔系统,发现一个新的设备时,热插拔系统根据modules.pcimap文件找到对应的驱动。

 

注:模块的概念。PCI是一个模块。

 

7.  PCI驱动注册

为了正确的注册到内核,PCI驱动必须创建一个结构:

struct pci_driver {

    struct list_head node;

    const 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-plugcapable driver) */

    int  (*suspend)(struct pci_dev*dev, pm_message_tstate); /* Device suspended */

    int  (*suspend_late)(struct pci_dev*dev, pm_message_tstate);

    int  (*resume_early)(struct pci_dev*dev);

    int  (*resume)(struct pci_dev*dev);                  /* Device woken up */

    void (*shutdown)(struct pci_dev*dev);

    int (*sriov_configure)(struct pci_dev*dev,int num_vfs);/* PF pdev */

    const struct pci_error_handlers *err_handler;

    struct device_driver    driver;

    struct pci_dynids dynids;

};

包括一些回调函数和描述PCI驱动与PCI核心对应的变量。

其中一些区域需要PCI驱动注意。

const char*name;

const struct pci_device_id*id_table;

int  (*probe) (struct pci_dev*dev,const struct pci_device_id*id);

void (*remove)(struct pci_dev*dev);

int  (*suspend)(struct pci_dev*dev, pm_message_tstate);

int  (*resume)(struct pci_dev*dev);

总的来说,一个PCI驱动结构只需要4个区域被初始化。

static struct pci_driver pci_driver = {

.name ="pci_skel",

.id_table = ids,

.probe = probe,

.remove =remove,
};

通常在模块初始化代码中,注册pci驱动。比如:

static int __init pci_skel_init(void)

{

return pci_register_driver(&pci_driver);

}

2.6更新后,在支持PCI热插拔、或CardBus系统上,PCI设备可以出现在任何时刻。

在系统运行时刻,通过写值到驱动的new_id中,指定驱动支持的新设备(原内核未认知的设备)。

当PCI驱动被卸载时,需要调用pci_unregister_driver。例如:

static void __exit pci_skel_exit(void)

{

pci_unregister_driver(&pci_driver);

}

8.  使能PCI设备

在PCI的探测函数中,在驱动访问PCI设备的任何资源之前(IO区域或资源),驱动程序必须调用函数:

int pci_enable_device(struct pci_dev *dev);

用来激活设备。

9.  访问配置空间

在驱动监测到设备之后,通常需要访问三个区域:内存、IO区域、配置空间。

访问配置空间尤其重要,因为需要通过配置空间找到内存区域映射和IO区域映射。

linux提供了一套访问配置空间的标准接口。

对驱动而言,可通过8、16、32位数据传输访问配置空间。相关的函数定义在<linux/
pci.h>
:中:

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);

dev:访问设备的逻辑表示

where:要读取位置在配置空间中的位移

*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);

dev:写入设备的逻辑表示

where:要写入位置在配置空间中的位移

*val:写入的值

不需要考虑字节序,会自动转换。

 

在驱动未获得pci_dev时,可使用如上函数读写配置空间

int pci_bus_read_config_byte (structpci_bus *bus, unsigned int devfn, int
where, u8 *val);
int pci_bus_read_config_word (struct pci_bus *bus, unsigned int devfn, int
where, u16 *val);
int pci_bus_read_config_dword (struct pci_bus *bus, unsigned int devfn, int
where, u32 *val);

 

int pci_bus_write_config_byte (structpci_bus *bus, unsigned int devfn, int
where, u8 val);
int pci_bus_write_config_word (struct pci_bus *bus, unsigned int devfn, int
where, u16 val);
int pci_bus_write_config_dword (struct pci_bus *bus, unsigned int devfn, int
where, u32 val);

 

访问配置空间的最好方式是通过pci_read_系列函数,例如:

static unsigned charskel_get_revision(struct pci_dev *dev)
{

u8 revision;

pci_read_config_byte(dev,PCI_REVISION_ID, &revision);

return revision;

}

10.  访问IO和内存空间

一个PCI设备最多可实现6个IO地址区域。每个区域可以是内存或者IO地址。大多数设备在内存区域实现IO寄存器,这也是一个明智的方法。需要注意的是,和常规内存不同,IO寄存器不应该由CPU缓存,因为每次访问都可能边缘效应。为了取消这个默认设置,内存区域实现的IO寄存器,可以通过在其配置寄存器中设置“memory-is-prefetchable”。若是可预取的,CPU可缓存其内容并进行各种优化。若不是可预取的,则不能优化,因为每次访问都有边际效应,就行IO端口一样。

 

接口板(PCI设备)通过6个32位的寄存器(PCI_BASE_ADDRESS_0到PCI_BASE_ADDRESS_5)声明区域的大小和位置。所以最多实现6IO地址区域。因为PCI的IO地址空间是32位的,所以不管是内存或IO区域使用相同的配置接口是有道理的。如果设备的数据总线是64位的,那么每个区域使用两个连续的32位寄存器实现。一个PCI设备既提供32位区域又提供64位区域是有可能的。

 

内核已经把PCI设备的IO区域信息映射进了通用资源管理中。所以,不需要通过访问配置寄存器来获取IO区域信息。可以通过访问/sys/bus/pci/devices/*/resource的内容获取。但首选的方法是通过下列函数获取。

unsigned long pci_resource_start(structpci_dev *dev, int bar);

unsigned long pci_resource_end(structpci_dev *dev, int bar);

bar:指定要获取的区域(0到5)

 

unsigned long pci_resource_flags(structpci_dev *dev, int bar);

资源flag用来定义某个区域的特性。其中几个重要的标志如下:

IORESOURCE_IO

IORESOURCE_MEM

IORESOURCE_PREFETCH

IORESOURCE_READONLY(PCI资源从不设置该标志)

 

驱动程序不需要访问配置寄存器去获得这些资源信息,因为系统已经构建了这些资源信息,驱动直接使用pci_resource_系列函数获取即可。

11.  PCI中断

在linux系统启动时,已经为PCI设备分配了一个唯一的中断号,位于配置空间的第60寄存器(PCI_INTERRUPT_LINE),一个字节长度,最多256个中断号。第61个寄存器(PCI_INTERRUPT_PIN)说明PCI设备是否支持中断,如果不支持,则为0,如果支持,非0.

如果是非0的,PCI_INTERRUPT_PIN的值是中断引脚的编号()。

驱动通过下面代码读取中断号,以便使用:

result = pci_read_config_byte(dev,PCI_INTERRUPT_LINE, &myirq);

if (result) {

/* deal witherror */

}

 

12.  总结归纳

linux中:

先module初始化,内含pci初始化(对于pci设备而言)
阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/chen98765432101/article/details/79962934
文章标签: linux PCI驱动
个人分类: 驱动
上一篇OVS架构解析--dpdk datapath数据通路
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭