这几天将以前为内核2.6写的驱动移植到了4.1下,在这里记录一下过程,以及从头整理一下linux下pci驱动的编写方法。
以前的驱动没有使用到linux下的probe方法,在4.1内核下成功编译后,一直无法进入中断,因此参考ch36x的驱动,重新写了驱动初始化部分,当应用层的程序可以调用驱动正确读回采集卡数据的时候,那份欣喜与满足感是难以言表的。
驱动模块初始化相关函数定义
PCI驱动和其它linux驱动一样,需要定义init和exit两个函数作为加载模块的入口点和卸载模块的出口点,可以使用module_pci_driver宏,只要将pci_driver结构体变量作为参数调用这个宏,就会自动定义init和exit这两个函数,好处是代码整洁,缺点就是不够灵活,我这里仍然用传统的方法使用module_init和module_exit两个宏:
module_init( init_m );
module_exit( cleanup_m );
init_m是使用insmod命令加载驱动模块时调用的函数,cleanup_m是用rmmod卸载模块时调用的函数。
在init_m函数中先调用alloc_chrdev_region来给设备分配编号,然后调用class_create函数创建一个设备类,只有调用这个函数后才能够使用device_create函数在/dev目录下动态创建设备节点(在probe函数中调用),最后调用pci_register_driver宏把pci专用的结构体pci_driver变量放到pci总线设备组中,让内核知道有我们这个驱动模块的存在。
alloc_chrdev_region函数说明如下:
函数原型:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
参数说明:
dev:向内核申请下来的设备号
baseminor :次设备号的起始
count: 申请次设备号的个数
name :执行 cat /proc/devices显示的名称,由驱动编写者定义
返回值:执行成功返回0
class_create函数说明如下:
函数原型:
struct class *class_create(struct module *owner, const char *name);
参数说明:
owner:指定类的所有者是哪个模块,可以用THIS_MODULE获取本驱动模块;
name:类名,由驱动编写者定义。
返回值:创建的类指针。
可以用IS_ERR判断返回的类指针是否有错误,如果有错误,则使用PTR_ERR来返回错误代码。
在cleanup_m函数中注销掉pci驱动结构体以及之前申请到的设备号。
pci_driver结构体:
这个结构体的声明在include/linux/pci.h里面:
struct pci_driver {
struct list_head node;
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 (*save_state) (struct pci_dev *dev, u32 state);
int (*suspend)(struct pci_dev *dev, u32 state);
int (*resume) (struct pci_dev *dev);
int (*enable_wake) (struct pci_dev *dev, u32 state, int enable);
};
我的驱动中定义的有如下几项:
name:驱动模块名称
id_table:PCI设备的厂商ID,设备ID等
probe:总线发现我们设备的时候调用的函数,对于已经插好的PCI板卡,加载驱动模块的时候就会调用此函数。
remove:移除设备的时候调用,在卸载驱动模块的时候也会调用。
probe函数
在这个函数中要为设备的私有数据结构变量分配空间,使能设备,获取设备所需的资源,注册中断等,其中涉及到的相关函数介绍如下:
pci_enable_device:使能设备的IO和内存空间,如果设备处于挂起状态则唤醒之。
pci_request_regions:通知内核设备IO和内存空间已经被指定名称的设备占用,其它就不要再占用了。
pci_set_drvdata:设置PCI驱动私有数据,本来这个函数将私有数据结构变量和pci_dev关联起来了,但是在file_operations相关操作中没有pci_dev获取方法,所以我最后极不优雅的使用全局变量来记录设备私有数据。
request_irq:注册中断
cdev_init:初始化字符设备的cdev变量
cdev_add:添加一个字符设备到系统中
device_create:创建字符设备,这个函数调用完成后,在系统的/dev目录下将会有函数设定的设备文件出现。
remove函数
此函数清理了probe函数中所申请的资源,注意清理资源时要和申请资源的顺序相反。
驱动模块文件操作相关函数
之前的工作是将驱动模块加载到内核中,并且为相关设备分配资源。接下来要想应用层访问到设备,操控设备,则需要进行一些文件操作。对于应用程序来讲,一个设备也就是一个文件。
file_operations结构体
这个结构体的声明在include/linux/fs.h文件中
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);int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
结构体成员函数很多,但是我的驱动只实现了open,release,unlocked_ioctl,read。下面一一介绍。
open函数
应用层调用open函数时,驱动模块的open函数就会被调用(有点废话的感觉),驱动模块的open函数会传递进两个参数:
inode结构和file结构,inode结构对于我们比较有用的就是获取设备索引,获得设备索引后就可以填充file结构中的private_data成员变量,这个成员变量实际上就是前面probe函数中定义的私有数据成员变量,在我的程序里,我用一个全局指针数组来存放所有设备的私有数据成员指针。伪代码如下:
int zt_pl2360_open(struct inode *inode,struct file *filp)
{int nRet = 0;PZT_PL2360_Dev pd = 0; int nInx = MINOR( inode->i_rdev );pd = g_pZT_PL2360_MyDev[nInx];filp->private_data = pd;return nRet;
}
release函数在应用层调用close函数时会被调用,它是open的反向操作,就不多说了。
read函数将内核数据复制到应用层,我这里就是将采集卡读出的数据交给应用程序。
unlocked_ioctl函数
这个函数在低版本内核中就是ioctl,对应应用层的ioctl调用,在kernel 2.6.36后使用unlocked_ioctl和compat_ioctl代替,其中unlocked_ioctl在应用层和内核层相同位数的时候调用,compat_ioctl在32位应用,64位内核驱动的时候调用。
在我的驱动中,这个函数是应用程序与设备通讯的主要函数,设置设备参数,读回设备状态等操作均在这个函数中完成。伪代码如下:
long zt_pl2360_ioctl(struct file *filp,unsigned int cmd, unsigned long arg)
{int retval = 0;PZT_PL2360_Dev pd = (PZT_PL2360_Dev)(filp->private_data);if (_IOC_TYPE(cmd) != PL2360_IOC_MAGIC) return -ENOTTY;if (_IOC_NR(cmd) > PL2360_IOC_MAXNR) return -ENOTTY;switch(cmd) {case PL2360_IOC_OUTL:retval = PL2360_IOC_Outl( pd, arg );break;case PL2360_IOC_INL:retval = PL2360_IOC_Inl( pd, arg );break;……default: return -ENOTTY;}return retval;
}
最后在说一点,驱动完成后,我安装的时候遇到了“-1 Unknown symbol in module”错误,需要在文件添加:MODULE_LICENSE(“GPL”);问题解决。
修改设备节点权限,让非root用户可以使用
首先看你节点信息:
udevadm info --attribute-walk --name=/dev/pl21080
/dev/pl21080是节点名称
我的信息:
looking at device ‘/devices/pci0000:00/0000:00:1c.1/0000:02:00.0/0000:03:0c.0/pl2108_class/pl21080’:
KERNEL==“pl21080”
SUBSYSTEM==“pl2108_class”
DRIVER==“”
然后在/etc/udev/rules.d目录下新建文件
vi 10-myrule.rules
添加
SUBSYSTEM==“pl2108_class”, MODE=“0666”
保存退出
此时再insmod就可以让普通用户有权限了