本文参考:https://blog.csdn.net/cjecho/article/details/54934264
PCI(Peripheral Component Interconnect)外围设备互联是一组完整的规范,定义了计算机各个不同部分之间应该如何交互。每个PCI外设由一个总线编号、一个设备编号、一个功能编号来标识。Linux支持PCI域,每个域可以拥有256个总线,每个总线 可以支持32个设备,每个设备都可以是多功能版(如CD-ROM驱动器),最多可有8种功能。所以每种功能可以在硬件级由一个16位地址或键来标识。单个系统中插入多个总线可以通过桥完成,它是用来连接两个总线的特殊PCI外设。
lspci
会列出/prco/bus/pci/devices下的设备或/sys/bus/pci/devices
每个外设的硬件电路对如下三种地址空间的查询进行应答:内存位置、I/O端口、配置寄存器。固件在系统引导时初始化PCI硬件,把每个区域映射到不同的地址,这些区域映射的地址可以从配置空间读取,因此Linux驱动程序不需要探测就能访问设备。在读取配置寄存器后,驱动程序就可以安全访问其硬件。每个设备通过物理地址来获取其配置寄存器的。所有PCI设备至少有256字节的地址空间。
pci_device_id被用在struct pci_driver (它告诉用户空间这个特定的驱动程序支持哪些设备)中。在示例中,创建了一个结构体数组,每一个结构表明使用该驱动支持的设备,数组的最后一个值是全部设置为0的空结构体,也就是{0,}。这个结构体需要被导出到用户空间,使热插拔和模块装载系统知道什么模块对应什么硬件设备,宏MODULE_DEVICE_TABLE完成这个工作。pci_dev详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等。
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_DEVICE对pci_device_id 结构进行初始化
static struct pci_device_id demo_pci_tbl[] ={
{PCI_DEVICE(PCI_VENDOR_ID_DEMO,PCI_DEVICE_ID_DEMO),},
{0,},
};
MODULE_DEVICE_TABLE(pci, demo_pci_tbl);
当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构。
系统加载模块是调用pci_init_module函数,在这个函数中我们通过pci_register_driver 把new_pci_driver注册到系统中。在调用pci_register_driver时,需要提供一个pci_driver结构。这个函数首先检测id_table中定义的PCI信息是否和系统中的PCI信息有匹配,如果有则返回0,匹配成功后调用probe函数对PCI设备进行进一步的操作。probe函数的作用就是启动pci设备,读取配置空间信息,进行相应的初始化。
/* 设备模块信息 */
static struct pci_driver demo_pci_driver = {
.name= DEMO_MODULE_NAME, /* 设备模块名称 */
.id_table = demo_pci_tbl, /* 能够驱动的设备列表 */
.probe = demo_probe, /* 查找并初始化设备 */
.remove = demo_remove, /* 卸载设备模块 */
/* ... */};
static int __init demo_init_module (void)
{
pci_register_driver(&demo_pci_driver); //注册设备驱动
/* ... */}
static int __init demo_probe(struct pci_dev *pci_dev,const struct pci_device_id *pci_id)
{
int result;
printk("probe function is running\n");
struct device_privdata *privdata;
privdata->pci_dev = pci_dev;
//把设备指针地址放入PCI设备中的设备指针中,便于后面调用pci_get_drvdata
pci_set_drvdata(pci_dev, privdata);
/* 启动PCI设备 */
if(pci_enable_device(pci_dev))
{
printk(KERN_ERR "%s:cannot enable device\n", pci_name(pci_dev));
return -ENODEV;
}
/*动态申请设备号,把fops传进去*/
privdata->cdev = cdev_alloc();
privdata->cdev->ops=&jlas_fops;
privdata->cdev->owner = THIS_MODULE;
cdev_add(privdata->cdev,devno,1);
/*动态创建设备节点*/
privdata->cdev_class = class_create(THIS_MODULE,DEV_NAME);
device_create(privdata->cdev_class,NULL, devno, pci_dev, DEV_NAME);
privdata->irq=pci_dev->irq;
privdata->iobase=pci_resource_start(privdata->pci_dev, BAR_IO);
/*判断IO资源是否可用*/
if((pci_resource_flags(pci_dev, BAR_IO) & IORESOURCE_IO) != IORESOURCE_IO)
goto err_out;
/* 对PCI区进行标记 ,标记该区域已经分配出去*/
ret= pci_request_regions(pci_dev, DEVICE_NAME);
if(ret)
goto err_out;
/*初始化tasklet*/
tasklet_init(&(privdata->my_tasklet),jlas_1780_do_tasklet,(unsigned long )&jlas_pci_cdev);
/* 初始化自旋锁 */
spin_lock_init(&private->lock);
/*初始化等待队列*/
init_waitqueue_head(&(privdata->read_queue));
/* 设置成总线主DMA模式 */
pci_set_master(pci_dev);
/*申请内存*/
privdata->mem = (u32 *) __get_free_pages(GFP_KERNEL|__GFP_DMA | __GFP_ZERO, memorder);
if (!privdata->mem) {
goto err_out;
}
/*DMA映射*/
privdata->dma_addrp = pci_map_single(pdev, privdata->mem,PAGE_SIZE * (1 << memorder), PCI_DMA_FROMDEVICE);
if (pci_dma_mapping_error(pdev, privdata->dma_addrp)) {
goto err_out;
}
/*对硬件进行初始化设置,往寄存器中写一些值,复位硬件等*/
device_init(xx_device);
return 0;
err_out:
printk("error process\n");
resource_cleanup_dev(FCswitch); //如果出现任何问题,释放已经分配了的资源
return ret;
}
中断处理:在Linux引导阶段,计算机固件就为设备分配了一个唯一的中断号。中断处理,主要就是读取中断寄存器,然后调用中断处理函数来处理中断的下半部分,一般通过tasklet或者workqueue来实现。由于使用request_irq 获得的中断是共享中断,因此在中断处理函数的上半部需要区分是不是该设备发出的中断,这就需要读取中断状态寄存器的值来判断,如果不是该设备发起的中断则返回IRQ_NONE。
/* 中断处理模块 */
void jlas_do_tasklet(unsigned long data)
{
spin_lock(&(privdata->my_spin_lock));
//具体操作
spin_unlock(&(privdata->my_spin_lock));
wake_up_interruptible(&(privdata->read_queue));
}
static irqreturn_t device_interrupt(int irq, void *dev_id)
{
struct device_privdata *privdata = dev_id;
tasklet_schedule(&(privdata->my_tasklet));
return IRQ_HANDLED;
/* ... */}
//probe中调用的函数原型
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}
设备文件操作接口
当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。device_init(xx_device)在probe中调用。
/* 设备文件操作接口 */
static struct file_operations demo_fops={
.owner = THIS_MODULE, /* demo_fops所属的设备模块 */
.read = demo_read, /* 读设备操作*/
.write = demo_write, /* 写设备操作*/
.open = demo_open, /* 打开设备操作*//
.ioctl = demo_ioctl, /* 控制设备操作*/
.mmap = demo_mmap, /* 内存重映射操作*/
.release = demo_release, /* 释放设备操作*/
/* ... */};
PCI设备私有数据结构
/* 对特定PCI设备进行描述的数据结构 */
struct device_private
{
/*次设备号*/
unsigned int minor;
/*注册字符驱动和发现PCI设备的时候使用*/
struct pci_dev *pci_dev;
struct cdev *cdev;
struct class *cdev_class;
/*中断号*/
unsigned int irq;
/* 用于获取PCI设备配置空间的基本信息 */
unsigned long iobase;
/*用于保存分配给PCI设备的内存空间的信息*/
dma_addr_t dma_addrp;
char *virts_addr;
/*基本的同步手段*/
spinlock_t lock;
/*等待队列*/
wait_queue_head_t read_queue;
/*tasklet*/
struct tasklet_struct my_tasklet;
/*异步*/
struct fasync_struct *async_queue;
/*设备打开标记*/
int open_flag /
/* .....*/
};