一、字符设备和块设备
Linux抽象了对硬件的处理,所有的硬件设备都可以像普通文件一样来看待:它们可以使用和操作文件相同的、标准的系统调用接口来完成打开、关闭、读写和I/O控制操作,而驱动程序的主要任务也就是要实现这些系统调用函数。Linux系统中的所有硬件设备都使用一个特殊的设备文件来表示,例如,系统中的第一个IDE硬盘使用/dev/hda表示。每个设备文件对应有两个设备号:一个是主设备号,标识该设备的种类,也标识了该设备所使用的驱动程序;另一个是次设备号,标识使用同一设备驱动程序的不同硬件设备。设备文件的主设备号必须与设备驱动程序在登录该设备时申请的主设备号一致,否则用户进程将无法访问到设备驱动程序。
在Linux操作系统下有两类主要的设备文件:一类是字符设备,另一类则是块设备。字符设备是以字节为单位逐个进行I/O操作的设备,在对字符设备发出读写请求时,实际的硬件I/O紧接着就发生了,一般来说字符设备中的缓存是可有可无的,而且也不支持随机访问。块设备则是利用一块系统内存作为缓冲区,当用户进程对设备进行读写请求时,驱动程序先查看缓冲区中的内容,如果缓冲区中的数据能满足用户的要求就返回相应的数据,否则就调用相应的请求函数来进行实际的I/O操作。块设备主要是针对磁盘等慢速设备设计的,其目的是避免耗费过多的CPU时间来等待操作的完成。一般说来,PCI卡通常都属于字符设备。
所有已经注册(即已经加载了驱动程序)的硬件设备的主设备号可以从/proc/devices文件中得到。使用mknod命令可以创建指定类型的设备文件,同时为其分配相应的主设备号和次设备号。例如,下面的命令:
[root@gary root]# mknod /dev/lp0 c 6 0
将建立一个主设备号为6,次设备号为0的字符设备文件/dev/lp0。当应用程序对某个设备文件进行系统调用时,Linux内核会根据该设备文件的设备类型和主设备号调用相应的驱动程序,并从用户态进入到核心态,再由驱动程序判断该设备的次设备号,最终完成对相应硬件的操作。
二、设备驱动程序接口
Linux中的I/O子系统向内核中的其他部分提供了一个统一的标准设备接口,这是通过include/linux/fs.h中的数据结构file_operations来完成的:
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);
};
当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程 序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。
三、设备驱动程序模块
Linux下的设备驱动程序可以按照两种方式进行编译,一种是直接静态编译成内核的一部分,另一种则是编译成可以动态加载的模块。如果编译进内核的话,会增加内核的大小,还要改动内核的源文件,而且不能动态地卸载,不利于调试,所有推荐使用模块方式。
从本质上来讲,模块也是内核的一部分,它不同于普通的应用程序,不能调用位于用户态下的C或者C++库函数,而只能调用Linux内核提供的函数,在/proc/ksyms中可以查看到内核提供的所有函数。
在以模块方式编写驱动程序时,要实现两个必不可少的函数init_module( )和cleanup_module( ),而且至少要包含和两 个头文件。一般使用LDD3 例程中使用的makefile 作为基本的版本,稍作改变之后用来编译驱动,编译生成的模块(一般为.ko文件)可以使用命令insmod载入Linux内核,从而成为内核的一个组成部分,此时内核会调用 模块中的函数init_module( )。当不需要该模块时,可以使用rmmod命令进行卸载,此进内核会调用模块中的函数cleanup_module( )。任何时候都可以使用命令来lsmod查看目前已经加载的模块以及正在使用该模块的用户数。
四、设备驱动程序结构
了解设备驱动程序的基本结构(或者称为框架),对开发人员而言是非常重要的,Linux的设备驱动程序大致可以分为如下几个部分:驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理。
1.驱动程序的注册与注销
向系统增加一个驱动程序意味着要赋予它一个主设备号,这可以通过在驱动程序的初始化过程中调用alloc_chrdev_region( )或者register_chrdev_region( )来完成。而在关闭字符设备时,则需要通过调用unregister_chrdev_region( )从内核中注销设备,同时释放占用的主设备号。
2.设备的打开与释放
打开设备是通过调用file_operations结构中的函数open( )来完成的,它是驱动程序用来为今后的操作完成初始化准备工作的。在大部分驱动程序中,open( )通常需要完成下列工作:
a. 检查设备相关错误,如设备尚未准备好等。
b. 如果是第一次打开,则初始化硬件设备。
c. 识别次设备号,如果有必要则更新读写操作的当前位置指针f_ops。
d. 分配和填写要放在file->private_data里的数据结构。
e. 使用计数增1。
释放设备是通过调用file_operations结构中的函数release( )来完成的,这个设备方法有时也被称为close( ),它的作用正好与open( )相反,通常要完成下列工作:
a. 使用计数减1。
b. 释放在file->private_data中分配的内存。
c. 如果使用计算为0,则关闭设备。
3.设备的读写操作
字符设备的读写操作相对比较简单,直接使用函数read( )和write( )就可以了。但如果是块设备的话,则需要调用函数block_read( )和block_write( )来进行数据读写,这两个函数将向设备请求表中增加读写请求,以便Linux内核可以对请求顺序进行优化。由于是对内存缓冲区而不是直接对设备进行操作的,因此能很大程度上加快读写速度。如果内存缓冲区中没有所要读入的数据,或者需要执行写操作将数据写入设备,那么就要执行真正的数据传输,这是通过调用数据结构blk_dev_struct中的函数request_fn( )来完成的。
4.设备的控制操作
除了读写操作外,应用程序有时还需要对设备进行控制,这可以通过设备驱动程序中的函数ioctl( )来完成,ioctl 系统调用有下面的原型: int ioctl(int fd, unsigned long cmd, …),第一个参数是文件描述符,第二个参数是具体的命令,一般使用宏定义来确定,第三个参数一般是传递给驱动中处理设备控制操作函数的参数。ioctl( )的用法与具体设备密切关联,因此需要根据设备的实际情况进行具体分析。
5.设备的中断和轮询处理
对于不支持中断的硬件设备,读写时需要轮流查询设备状态,以便决定是否继续进行数据传输。如果设备支持中断,则可以按中断方式进行操作。
五、PCI驱动程序框架
1.关键数据结构
PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。
Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。
在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:
a. pci_driver
这个数据结构在文件include/linux/pci.h里,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):
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-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);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;
};
b. pci_dev
这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等
struct pci_dev {struct list_head bus_list; /* node in per-bus list */struct pci_bus *bus; /* bus this device is on */struct pci_bus *subordinate; /* bus this device bridges to */void *sysdata; /* hook for sys-specific extension */struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */struct pci_slot *slot; /* Physical slot this device is in */unsigned int devfn; /* encoded device & function index */unsigned short vendor;unsigned short device;unsigned short subsystem_vendor;unsigned short subsystem_device;unsigned int class; /* 3 bytes: (base,sub,prog-if) */u8 revision; /* PCI revision, low byte of class word */u8 hdr_type; /* PCI header type (`multi' flag masked out) */u8 pcie_type; /* PCI-E device/port type */u8 rom_base_reg; /* which config register controls the ROM */u8 pin; /* which interrupt pin this device uses */struct pci_driver *driver; /* which driver has allocated this device */u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */struct device_dma_parameters dma_parms;pci_power_t current_state; /* Current operating state. In ACPI-speak, this is D0-D3, D0 being fully functional, and D3 being off. */int pm_cap; /* PM capability offset in the configuration space */unsigned int pme_support:5; /* Bitmask of states from which PME# can be generated */unsigned int d1_support:1; /* Low power state D1 is supported */unsigned int d2_support:1; /* Low power state D2 is supported */unsigned int no_d1d2:1; /* Only allow D0 and D3 */unsigned int wakeup_prepared:1;
#ifdef CONFIG_PCIEASPMstruct pcie_link_state *link_state; /* ASPM link state. */
#endifpci_channel_state_t error_state; /* current connectivity state */struct device dev; /* Generic device interface */int cfg_size; /* Size of configuration space *//** Instead of touching interrupt line and base address registers* directly, use the values stored here. They might be different!*/unsigned int irq;struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs *//* These fields are used by common fixups */unsigned int transparent:1; /* Transparent PCI bridge */unsigned int multifunction:1;/* Part of multi-function device *//* keep track of device state */unsigned int is_added:1;unsigned int is_busmaster:1; /* device is busmaster */unsigned int no_msi:1; /* device may not use msi */unsigned int block_ucfg_access:1; /* userspace config space access is blocked */unsigned int broken_parity_status:1; /* Device generates false positive parity */unsigned int irq_reroute_variant:2; /* device needs IRQ rerouting variant */unsigned int msi_enabled:1;unsigned int msix_enabled:1;unsigned int ari_enabled:1; /* ARI forwarding */unsigned int is_managed:1;unsigned int is_pcie:1;unsigned int needs_freset:1; /* Dev requires fundamental reset */unsigned int state_saved:1;unsigned int is_physfn:1;unsigned int is_virtfn:1;unsigned int reset_fn:1;unsigned int is_hotplug_bridge:1;pci_dev_flags_t dev_flags;atomic_t enable_cnt; /* pci_enable_device has been called */u32 saved_config_space[16]; /* config space saved at suspend time */struct hlist_head saved_cap_space;struct bin_attribute *rom_attr; /* attribute descriptor for sysfs ROM entry */int rom_attr_enabled; /* has display of the rom attribute been enabled? */struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE]; /* sysfs file for resources */struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE]; /* sysfs file for WC mapping of resources */
#ifdef CONFIG_PCI_MSIstruct list_head msi_list;
#endifstruct pci_vpd *vpd;
#ifdef CONFIG_PCI_IOVunion {struct pci_sriov *sriov; /* SR-IOV capability related */struct pci_dev *physfn; /* the PF this VF is associated with */};struct pci_ats *ats; /* Address Translation Service */
#endif
};
2.基本框架
在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。
/* 指明该驱动程序适用于哪一些PCI设备 */
static struct pci_device_id demo_pci_tbl[] ={{PCI_DEVICE(PCI_VENDOR_ID_DEMO,PCI_DEVICE_ID_DEMO),},{0,},
};/* 对特定PCI设备进行描述的数据结构 */
struct device_private
{
/* ... */};/* 中断处理模块 */
static irqreturn_t device_interrupt(int irq, void *dev_id)
{
/* ... */}static int demo_open(struct inode *inode,struct file *file)
{/* ... */try_module_get(THIS_MODULE);return 0;
}static int demo_read(struct file *file,char __user *buffer,size_t count,loff_t *offp)
{
/* ... */}static int demo_write(struct file *file,const char __user *buffer,size_t count,loff_t *offp)
{
/* ... */}static int demo_mmap(struct file *file, struct vm_area_struct *vma)
{
/* ... */}static int demo_ioctl(struct inode *inode,struct file *file, unsigned int cmd,unsigned long arg)
{
switch(cmd){
case CMD1:
device_func(arg);
break;
...
default:
}
/* ... */}static int demo_release(struct inode *inode,struct file *file)
{/* ... */
module_put(THIS_MODULE);
return 0;
}/* 设备文件操作接口 */
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, /* 释放设备操作*/
/* ... */}; static int __init demo_probe(struct pci_dev *pci_dev,const struct pci_device_id *pci_id)
{
/* ... */}; static void __devexit demo_remove(struct pci_dev *pci_dev)
{
/* ... */}; /* 设备模块信息 */
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 void __exit demo_exit_module(void)
{/* ... */
pci_unregister_driver(&demo_pci_driver);
}/* 加载驱动程序模块入口 */
module_init(demo_init_module);/* 卸载驱动程序模块入口
module_exit(demo_exit_module); */
上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上 __init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。
六、框架的具体实现之模块操作
1.struct pci_device_id
PCI驱动程序向PCI子系统注册其支持的厂家ID,设备ID和设备类编码。使用这个数据库,插入的卡通过配置空间被识别后,PCI子系统把插入的卡和对应的驱动程序绑定。
PCI设备列表
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_id被用在struct pci_device 中。在示例中,创建了一个结构体数组,每一个结构表明使用该结构体数组的驱动支持的设备,数组的最后一个值是全部设置为0的空结构体,也就是{0,}。这个结构体需要被导出到用户空间,使热插拔和模块装载系统知道什么模块对应什么硬件设备,宏MODULE_DEVICE_TABLE完成这个工作。例如:
MODULE_DEVICE_TABLE(pci, demo_pci_tbl);
2.初始化设备模块
在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:
a. 检查PCI总线是否被Linux内核支持;
b. 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
c. 读出配置头中的信息提供给驱动程序使用。
当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构。系统加载模块是调用pci_init_module函数,在这个函数中我们通过pci_register_driver 把new_pci_driver注册到系统中。在调用pci_register_driver时,需要提供一个pci_driver结构。这个函数首先检测id_table中定义的PCI信息是否和系统中的PCI信息有匹配,如果有则返回0,匹配成功后调用probe函数对PCI设备进行进一步的操作。
static int __init demo_init_module (void)
{
/* allocate (several) major number */ret = alloc_chrdev_region(&devno, 0, MAX_DEVICE, "buffer"); ret= pci_register_driver(&demo_pci_driver);
}
probe函数的作用就是启动pci设备,读取配置空间信息,进行相应的初始化。
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_drvdatapci_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;
}
3.卸载设备模块
卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:
static void __exit demo_cleanup_module (void)
{pci_unregister_driver(&demo_pci_driver);
}
在卸载模块时调用pci_cleanup_module,这个函数中通过pci_unregister_driver对new_pci_driver进行注销,这个会调用到remove函数。remove函数的职责就是释放一切分配过的资源,根据自己代码的需要进行具体的操作。
static void __devexit my_pci_remove(struct pci_dev *pci_dev)
{struct device_private *private;private= (struct device_private*)pci_get_drvdata(pci_dev); /*对硬件进行操作,如硬件复位*/Device_close(xx_device);pci_unmap_single(pdev, privdata->dma_mem,PAGE_SIZE * (1 << memorder), PCI_DMA_FROMDEVICE); // 释放分配的内存空间free_pages ((unsigned long) privdata->mem, memorder);pci_clear_master(pdev); /* Nobody seems to do this */tasklet_kill(&(privdata->my_tasklet));pci_release_regions(pci_dev); // 移除动态创建的设备号和设备device_destroy(device_class, device->my_dev);class_destroy(device_class);if(privdata->pci_dev!=NULL)cdev_del(privdata->cdev); privdata->pci_dev=NULL;pci_disable_device(pci_dev);pci_set_drvdata(pci_dev,NULL);
}
4.中断处理
中断处理,主要就是读取中断寄存器,然后调用中断处理函数来处理中断的下半部分,一般通过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;/* ... */}
七、框架的具体实现之设备文件操作
1.设备文件操作接口
当应用程序对设备文件进行诸如open、close、read、write等操作时,Linux内核将通过file_operations结构访问驱动程序提供的函数。例如,当应用程序对设备文件执行读操作时,内核将调用file_operations结构中的read函数。
/* 设备文件操作接口 */
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, /* 释放设备操作*//* ... */};
2.打开设备
open 方法提供给驱动来做任何的初始化来准备后续的操作.在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。 open 方法的原型是:
int (*open)(struct inode *inode, struct file *filp);
inode 参数有我们需要的信息,以它的 i_cdev 成员的形式, 里面包含我们之前建立的cdev 结构. 唯一的问题是通常我们不想要 cdev 结构本身, 我们需要的是包含 cdev 结构的 device_private 结构.
static int demo_open(struct inode *inode, struct file *filp)
{struct device_private *private;private= container_of(inode->i_cdev, struct device_private, my_cdev);filp->private_data = private;private->open_flag++; /*申请中断*/ret = request_irq(privdata->irq,interrupt_handler, IRQF_SHARED, DEV_NAME,privdata);if(ret)return -EINVAL; ...try_module_get(THIS_MODULE);return 0;
}
3.释放设备
release 方法的角色是 open 的反面,设备方法应当进行下面的任务:
a. 释放 open 分配在 filp->private_data 中的任何东西
b. 在最后的 close 关闭设备
static int demo_release(struct inode *inode,struct file *filp)
{struct device_private *private= filp->private_data;private->open_flag--;free_irq(pdev->irq, privdata);module_put(THIS_MODULE);printk("pci device close success\n");return 0;
}
4.设备数据读写和ioctl
PCI设备驱动程序可以通过device_fops 结构中的函数device_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里。
static int device_ioctl(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
{int retval = 0;struct device_private *privdata= filp->private_data; switch (cmd){case CMD1:device_func(arg);break;...default:retval = -EINVAL;}return retval;
}
5.内存映射
static int device_mmap(struct file *filp, struct vm_area_struct *vma)
{int ret;struct device_private *private = filp->private_data;vma->vm_page_prot = PAGE_SHARED;//访问权限vma->vm_pgoff = virt_to_phys(FCswitch->rx_buf_virts) >> PAGE_SHIFT;//偏移(页帧号)ret = remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, (unsigned long)(vma->vm_end-vma->vm_start), vma->vm_page_prot);if(ret!=0)return -EAGAIN;return 0;
}
对 remap_pfn_range()函数的说明:
remap_pfn_range()函数的原型
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
该函数的功能是创建页表。其中参数vma是内核根据用户的请求自己填写的,而参数addr表示内存映射开始处的虚拟地址,因此,该函数为addr~addr+size之间的虚拟地址构造页表。
另外,pfn(Page Fram Number)是虚拟地址应该映射到的物理地址的页面号,实际上就是物理地址右移PAGE_SHIFT位。如果PAGE_SHIFT为4kb,则 PAGE_SHIFT为12,因为PAGE_SHIFT等于1 << PAGE_SHIFT 。
最后一个参数prot是新页所要求的保护属性。
在驱动程序中,一般能使用remap_pfn_range()映射内存中的保留页(如X86系统中的640KB~1MB区域)和设备I/O内存。因此,如 果想把kmalloc()申请的内存映射到用户空间,则可以通过SetPageReserved把相应的内存设置为保留后就可以。
八、附录
1.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 //* .....*/
};
2.PCI配置寄存器
所有的PCI设备都有至少256字节的地址空间,前64字节是标准化的,而其余的是设备相关的。图1显示了设备无关的配置空间的布局。
在Linux系统上,可以通过cat /proc/pci 等命令查看系统中所有PCI设备的类别、型号以及厂商等等信息,那就是从这些寄存器来的。下面是用lspci -x命令截取的部分信息(lspci命令也是使用/proc文件作为其信息来源)(PCI寄存器是小端字节序格式的):
00:00.0 Host bridge: Intel Corp. 440BX/ZX/DX - 82443BX/ZX/DX Host bridge (rev 01)
00: 86 80 90 71 06 00 00 02 01 00 00 06 00 00 00 00
10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
20: 00 00 00 00 00 00 00 00 00 00 00 00 ad 15 76 19
30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
那么根据下面的PCI配置寄存器组的结构,这个Host bridge的Vendor ID就是0x8086。
图1.标准化的PCI配置寄存器
参考资料:
1. LINUX设备驱动程序(第三版)
2. Linux下PCI设备驱动程序开发
http://www.ibm.com/developerworks/cn/linux/l-pci/index.html
3. Linux PCI 设备驱动基本框架(一)
http://www.cnblogs.com/zhuyp1015/archive/2012/06/30/2571400.html
4. Linux PCI 设备驱动基本框架(二)
http://www.cnblogs.com/zhuyp1015/archive/2012/06/30/2571408.html
5. 浅谈Linux PCI设备驱动(一)
http://blog.csdn.net/linuxdrivers/article/details/5849698