说明:
开发环境:ubuntu14.04
硬件环境:EasyArm-i.mx283A
Linux下设备以文件的形式呈现,应用程序对设备的操作就像操作一般的文件IO一样,open,write、read、close,这样做的好处就是为上层应用提供了统一的编程接口,操作简单易行。对于字符设备来说,设备号、cdev、操作方法集合至关重要,内核找到路径名所对应的inode后,要和驱动建立连接,首先要做的就是根据inode中的设备号找到cdev,然后根据cdev找到关联的操作方法集合,从而调动驱动所提供的操作方法完成对设备的具体操作。简而言之,字符设备驱动框架就是围绕着设备号、cdev和操作方法集合来实现的。cdev是放在在cdv_map散列表中的一个结构体,驱动加载时构造除cdev,并添加到cdev_map散列表中,构造cdev的同时还实现了一个操作方法集合,cdev的成员ops指向它。
设备号分为主设备号和次设备号,主设备号用于区分同一类设备,次设备号用于区分同一类设备的不同个体,比如在一个开发设备上装有led1、led2、led3、led4,则主设备号用于led这类设备,次设备号分别用于led1、led2、led3、led4。在32位机子下,设备号用32位来表示,其中高12位用于主设备号,低20位用于次设备号。驱动开发中可以使用静态分配和动态分配两种方法分配设备号,静态分配可能会出现设备号冲突的情况,导致模块加载失败,动态分配不会出现这种情况,但是动态分配无法从程序中预先配置设备号,设备号只能在/proc/devices
下查看。
#include
#include
#include
#include
#include
#define CH_MAJOR 256
#define CH_MINOR 0
#define CH_DEV_CNT 1
#define CH_DEV_NAME "CHDEV"
static struct cdev chdev_s;//定义一个字符设备结构体
//定义一个字符设备操作集合
static struct file_operations chdev_ops={
.owner = THIS_MODULE,
};
/*模块初始化*/
static int __init chdev_init(void)
{
int ret;
dev_t dev;
//静态分配设备号
dev = MKDEV(CH_MAJOR,CH_MINOR);//将主次设备号合并成一个设备号
ret = register_chrdev_region(dev,CH_DEV_CNT,CH_DEV_NAME);//静态注册一个设备号
if(ret)
{
goto reg_err;
}
cdev_init(&chdev_s,&chdev_ops);
chdev_s.owner = THIS_MODULE;
ret = cdev_add(&chdev_s,dev,CH_DEV_CNT);
if(ret)
{
goto add_err;
}
return 0;
add_err:
unregister_chrdev_region(dev,CH_DEV_CNT);
reg_err:
return ret;
}
/*模块退出*/
static void __exit chdev_exit(void)
{
dev_t dev;
dev = MKDEV(CH_MAJOR,CH_MINOR);
cdev_del(&chdev_s);
unregister_chrdev_region(dev,CH_DEV_CNT);//注销一个设备号
}
module_init(chdev_init);//定义模块初始化别名函数
module_exit(chdev_exit);//定义模块退出别名函数
//模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangZhengQing <[email protected]>");
MODULE_DESCRIPTION("Character device driver frame");
在该驱动程序中采用静态分配的方式注册设备号,所以在程序中定义了主设备号为256,次设备号的起始号为0,设备个数为1个,表示次设备号从0开始注册一个设备。设备号一旦被注册,在没有卸载模块之前,该设备号会一直被占用,其他模块不允许再注册此设备号。如前所述,字符设备驱动围绕着设备号、cdev、操作方法集合来编写,chdev_s是一个cdev类型的结构体变量,该结构体包含这类设备的所有属性,比如设备号、操作方法集合、此设备的个数等,结构体如下:
struct cdev {
struct kobject kobj;
struct module *owner;//该字符设备所在的内核模块的对象指针.
const struct file_operations *ops;//指向该字符设备的操作方法集合
struct list_head list;//用来将已经向内核注册的所有字符设备形成链表
dev_t dev;//设备号
unsigned int count;//次设备号的个数
};
chdev_ops是一个file_operations类型的结构体变量,该结构体包含操作设备的方法的指针,比如open、write、read、close等,结构体如下:
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 *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *, 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 **);
};
可以看到该结构体中的成员都是函数指针,说明后面编写的操作函数都要初始化到这个结构体中,上层应用对设备的具体操作才会生效。
讲完字符设备驱动所需要的变量之后,就应该来讲讲如何使用和配置这些变量了。首先在chdev_init
函数中通过宏MKDEV将主设备号和次设备号合并成一个设备号,然后通过register_chrdev_region
函数静态注册这个设备号,该函数的声明如下:
int register_chrdev_region(dev_t, unsigned, const char *);
第一个表示设备号,第二个参数表示次设备的个数,第三个参数表示该设备的名字,通过这个名字,当insmod加载成功模块后,cat/proc/devices
可以查看到相应的设备号和设备名。至此,设备号的初始化就已完成,后面就是初始化cdev的过程。
函数cdev_init
作用就是初始化cdev的部分成员,其中最重要的就是将ops指针指向file_operations类型的chdev_ops操作方法集合,这样就可以通过设备号找到操作方法集合,进行相应的操作。函数的声明如下:
void cdev_init(struct cdev *cdev,const struct file_operations *fops);
第一个参数传入cdev的地址,第二个参数传入file_operations的地址。函数内部实现将两者关联起来,手牵手,一起走向未来!
chdev_s.owner = THIS_MODULE
意思是说通过chdev_s找到对应的模块。owner是一个指向struct module类型变量的指针,THIS_MODULE是包含驱动的模块中struct module类型对象的地址,相当于c++中的this指针。
cdev初始化完成之后,应该将cdev添加到内核的cdev_map散列表中,内部生成一个probe的结构对象,其中data成员指向cdev。散列表相当于哈希表,调用的函数是:
int cdev_add(struct cdev *, dev_t, unsigned);
同时,既然有添加函数,自然有删除函数cdev_del
,在卸载模块的函数中就有用到。
简而言之,字符驱动的框架其实很简单,主要用到以下的一些函数:
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR := /home/yzq/EasyArm-imx28xx/kernel/linux-2.6.35.3
ROOTFS := /nfsroot/rootfs
else
KERNELDIR := /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.ko *.order *.symvers *.cmd *.o *.mod.c *.tmp_versions .*.cmd .tmp_versions
else
obj-m := chdev.o
endif
这个Makefile实现了两种架构的编译模式,一种是编译成ubuntu平台的驱动模块,并将模块安装到/lib/modules/4.4.0-31-generic/extra
目录下,具体操作如下:
另一种是编译成arm平台的驱动模块,并将模块安装到/nfsroot/rootfs/lib/modules/2.6.35.3-571-gcca29a0/extra
下,nfsroot是通过NFS服务器搭建的一个网络共享文件,开发板远程启动该目录下的rootfs根文件系统,将模块安装到该rootfs下的之后,可以直接登陆开发板进行安装操作。具体操作如下:
测试字符驱动
模块加载完成后,第一步cat /proc/devices
查看设备号,第二步mknod 安装设备节点,第三步cat /dev/CHDEV
查看是否驱动是否正常运行。
没有加载模块之前,cat查看字符设备节点显示没有该设备。
可以看到显示读取错误,无效的参数。此时可以判断驱动正在运行,只不过在驱动编写时并没有实现具体读取设备的操作方法。