引言
总结一下最近设备驱动的学习成果,记录一下心得。文章里穿插记录字符设备驱动的相关知识。
硬件相关
一 编写驱动程序
如上图所示,应用层要操作硬件,需要调用驱动。驱动程序是直接操作硬件的。从这个图中我们可以看出,驱动程序需要具备以下内容:
1. 模块加载,模块删除
2. 填充cdev结构体,dev_t,file_operations
3. 完善read(),write(),ioctl(),open(),close()等函数
首先说加载驱动。驱动写完了,需要动态加载如内核,让内核知道有这个驱动设备。如何注册进内核呢?一般在驱动程序里指定:
module_init(beep_init);
module_exit(beep_cleanup);
指定好加载,卸载函数以后,然后再在初始化设备,注册设备;
/*注册设备*/
static int beep_init(void)
{
dev_t dev = MKDEV(beep_major, 0);
char dev_name[]="beep";
if (beep_major)
result = register_chrdev_region(dev, 1, dev_name);
else {
result = alloc_chrdev_region(&dev, 0, 1, dev_name);
beep_major = MAJOR(dev);
}
beep_setup_cdev(&BeepDevs, 0, &beep_remap_ops);
}
static void beep_setup_cdev(struct cdev *dev, int minor,struct file_operations *fops)
{
int err, devno = MKDEV(beep_major, minor);
cdev_init(dev, fops);
dev->owner = THIS_MODULE;
dev->ops = fops;
err = cdev_add (dev, devno, 1);
}
/*删除设备*/
static void beep_cleanup(void)
{
cdev_del(&BeepDevs);
unregister_chrdev_region(MKDEV(beep_major, 0), 1);
}
如果是2.6以上的内核,注册设备和删除设备是用如下函数:
注册: cdev_init(struct cdev *dev, struct file_operations *fops);
cdev_add (struct cdev *dev, dev_t num, unsigned count);
删除: cdev_del(struct cdev *dev);
在注册或者删除设备号的之前,需要知道设备号,设备号如何分配。所以接下来再讲结构体,设备号的分配。
从上面的图1可以看出,内核删除、加载都是直接操作cdev结构体,那cdev结构体是什么?
struct cdev {
struct kobject kobj;
struct module *owner; //所属模块
const struct file_operations *ops;//文件操作结构,在写驱动时,其结构体内的大部分函数要被实现
struct list_head list;
dev_t dev; //设备号,int 类型,高12位为主设备号,低20位为次设备号
unsigned int count;
};
cdev结构体里包含了dev_t和file_operations结构体。这也是为什么图1中cdev可以指向dev_t类型和file_operations结构体。也就是说,内核操作cdev,然后cdev找到dev_t设备号和file_operations函数,最终利用file_operations里面的函数操作硬件。接下来说明一下这两个数据类型。
dev_t其实是一个32为的unsigned int类型数据。用来区分设备号。设备可以分为主设备和次设备。
主设备是一类设备的标识,占12位。
次设备是一类设备的具体设备,占20位。
比如说tty0,tty就是主设备,0表示次设备。但是如何给设备编号呢?一般有两种方式,一种是手动分配,另一种是自动分配。
手动:register_chrdev_region(dev_t first, unsigned int count, char *name);
自动:alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, cchar *name);
删除:unregister_chrdev_region(dev_t dev, unsigned int count);
有了主设备号和次设备号,这都是int型。为了方便,还需要与dev_t进行转换,所以右多了几个转换函数:
从dev_t获得主、次设备号;MAJOR(dev_t),MINOR(dev_t)
将主、次设备号转换成dev_t类型:MKDEV(int major ,int minor)
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(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, 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);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, 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(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
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);
}
这个结构体包含了对硬件的具体操作函数。所有的系统调用,最终都是操作这里面的函数。这些函数,比如说open,需要根据硬件编写程序,这里就不详述了。注意看一下file_operations里面的open等函数的参数是file,indoe两个结构体。为什么又搞出来两个结构体,这两个结构体有什么用呢?
我们知道,系统调用open等函数的时候,都是使用文件名,没有使用cdev结构体的,
int open(const char *pathname,flags,int perms)
那我们打开文件名的时候,它就如何找到了cdev呢?因为我们在使用的时候都创建了节点:
mknod /dev/node_name c major minor
我们通过这个方法,将文件名与我们的节点连接起来了。为什么这样弄?因为设备文件名主要在用户空间使用(比如用户空间程序调用open函数时),而内核空间则使用inode来表示相应的文件。那接下来inode节点如何连接到cdev呢?这里我们就要说说inode结构体了。inode有两个总要的字段:
dev_t i_rdev 该字段包含了真正的设备编号
struct cdev *i_cdev; 这就是指向了我们的cdev结构体了,连接了我们的用户空间和内核空间
这里再扩展一下,从一个inode中获得主设备和次设备号:
unsigned int iminor(struct inode *inode)
unsigned int imajor(struct inode *inode)
这里还有一个file结构体需要说明一下(只是自己的理解,没有确认)。如果一个设备可以同时被多个进程操作,比如说读取nand flash。如果一个进程process1读到了p1位置,另外一个进程process2也开始读nand了,假设读到了p2,这个时候第一个进程process1难道要从p2开始读?肯定不能吧,它肯定想接着p1开始读。那怎么办呢?所以用一个file结构体表示正在打开的设备文件,来区别。file结构体重要成员如下:
1.mode_t f_mode;
文件模式确定文件是可读的或者是可写的(或者都是),通过位FMODE_READ和FMODE_WRITE.你可能想在你的open或者ioctl函数中检查这个成员的读写许可,但是不需要检查读写许可,因为内核在调用你的方法之前检查.当文件还没有为那种存取而打开时读或写的企图被拒绝,驱动甚至不知道这个情况.
2.loff_t f_pos;
当前读写位置. loff_t在所有平台都是64位(在gcc术语里是long long ).驱动可以读这个值,如果它需要知道文件中的当前位置,但是正常地不应该改变它;读和写应当使用它们作为最后参数而收到的指针来更新一个位置,代替直接作用于filp->f_pos.这个规则的一个例外是在llseek方法中,它的目的就是改变文件位置.
3.unsigned int f_flags;
这些是文件标志,例如O_RDONLY, O_NONBLOCK,和O_SYNC.驱动应当检查O_NONBLOCK标志来看是否是请求非阻塞操作;其他标志很少使用.特别地,应当检查读/写许可,使用f_mode而不是f_flags.所有的标志在头文件中定义.
4.struct file_operations *f_op;
和文件关联的操作.内核安排指针作为它的open实现的一部分,接着读取它当它需要分派任何的操作时. filp->f_op中的值从不由内核保存为后面的引用;这意味着你可改变你的文件关联的文件操作,在你返回调用者之后新方法会起作用.例如,关联到主编号1 (/dev/null, /dev/zero,等等)的open代码根据打开的次编号来替代filp->f_op中的操作.这个做法允许实现几种行为,在同一个主编号下而不必在每个系统调用中引入开销.替换文件操作的能力是面向
对象编程的"方法重载"的内核对等体.
5.void *private_data;
open系统调用设置这个指针为NULL,在为驱动调用open方法之前.你可自由使用这个成员或者忽略它;你可以使用这个成员来指向分配的数据,但是接着你必须记住在内核销毁文件结构之前,在release方法中释放那个内存. private_data是一个有用的资源,在系统调用间保留状态信息,我们大部分例子模块都使用它.
6.struct dentry *f_dentry;
关联到文件的目录入口( dentry )结构.
二 编写makefile
ifneq ($(KERNELRELEASE),)
obj-m := led.o
else
PWD := $(shell pwd)
KVER ?= 2.6.31
KDIR :=/usr/local/linux-2.6.31
all:
$(MAKE) -C $(KDIR) M=$(PWD)
clean:
rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
endif
先说明以下makefile中一些变量意义:
(1)KERNELRELEASE在linux内核源代码中的顶层makefile中有定义
(2)PWD会取得当前工作路径
(3)KVER 取得当前内核的版本号
(4)KDIR变量便是当前内核的源代码目录。
这段代码,这个maikefile会被编译两次。
1. 编译开始的时候,没有定义KERNELRELEASE,执行else,获取linux内核,模块的地址,然后执行make。但是这个时候有-C,会改变工作目录,去linux内核的目录下
2. 到linux内核中,执行linux顶层的makefile,这里定义了KERNELRELEASE,执行完了返回模块的当前地址
3. 到了模块的当前地址,再次执行这个makefile,由于前面已经定义了KERNELRELEASE,会执行if的那一段,生成Ko文件。
三 下载到开发板,并装载insmod beep.ko。
四 查看主设备号
如果装载的时候打印了设备号,就直接可以查看。如果没有,利用命令查看
cat /proc/devices|grep beep
五 设置设备节点
mknod /dev/beep c major 0
六 编写测试代码
参考文献:
http://blog.chinaunix.net/uid-26960488-id-3250671.html
http://blog.csdn.net/shanzhizi/article/details/8626474
http://blog.chinaunix.net/uid-21161467-id-108104.html
驱动程序例程:
c int beep_major = 0;
module_param(beep_major, int, 0);
MODULE_AUTHOR("Hanson He");
MODULE_LICENSE("Dual BSD/GPL");
#define BEEP_MAGIC 'k'
#define BEEP_START_CMD _IO (BEEP_MAGIC, 1)
#define BEEP_STOP_CMD _IO (BEEP_MAGIC, 2)
int beep_open (struct inode *inode, struct file *filp)
{
return 0;
}
ssize_t beep_read(struct file *file, char __user *buff, size_t count, loff_t *offp)
{
return 0;
}
ssize_t beep_write(struct file *file, const char __user *buff, size_t count, loff_t *offp)
{
return 0;
}
void beep_stop( void )
{
s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPIO_OUTPUT);
s3c2410_gpio_setpin(S3C2410_GPB(0),0);
}
void beep_start( void )
{
s3c2410_gpio_pullup(S3C2410_GPB(0),1);
s3c2410_gpio_cfgpin(S3C2410_GPB(0), S3C2410_GPIO_OUTPUT);
s3c2410_gpio_setpin(S3C2410_GPB(0),1);
}
static int beep_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
switch ( cmd ) {
case BEEP_START_CMD:
{
beep_start(); break;
}
case BEEP_STOP_CMD: {
beep_stop(); break;
}
default: {
break;
}
}
return 0;
}
static int beep_release(struct inode *node, struct file *file)
{
return 0;
}
static void beep_setup_cdev(struct cdev *dev, int minor,
struct file_operations *fops)
{
int err, devno = MKDEV(beep_major, minor);
cdev_init(dev, fops);
dev->owner = THIS_MODULE;
dev->ops = fops;
err = cdev_add (dev, devno, 1);
if (err)
printk (KERN_NOTICE "Error %d adding beep%d", err, minor);
}
static struct file_operations beep_remap_ops = {
.owner = THIS_MODULE,
.open = beep_open,
.release = beep_release,
.read = beep_read,
.write = beep_write,
.ioctl = beep_ioctl,
};
static struct cdev BeepDevs;
static int beep_init(void)
{
int result;
dev_t dev = MKDEV(beep_major, 0);
char dev_name[]="beep";
/* Figure out our device number. */
if (beep_major)
result = register_chrdev_region(dev, 1, dev_name);
else {
result = alloc_chrdev_region(&dev, 0, 1, dev_name);
beep_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "beep: unable to get major %d\n", beep_major);
return result;
}
if (beep_major == 0)
beep_major = result;
/* Now set up cdev. */
beep_setup_cdev(&BeepDevs, 0, &beep_remap_ops);
printk("beep device installed, with major %d\n", beep_major);
printk("The device name is: %s\n", dev_name);
return 0;
}
static void beep_cleanup(void)
{
cdev_del(&BeepDevs);
unregister_chrdev_region(MKDEV(beep_major, 0), 1);
printk("beep device uninstalled\n");
}
module_init(beep_init);
module_exit(beep_cleanup);
EXPORT_SYMBOL(beep_major);