姓名:谢焕彬 学号:19020100303
前面我们讲了linux驱动框架linux驱动(一)驱动框架,对驱动的基本框架有了了解。现在我们来说一说字符设备驱动,我们一般讲驱动分为三类,字符设备、块设备、网络设备。字符设备和块设备是按照传输时的基本单位来划分的,字符设备就是传输时是按字符来传输的,比如串口、GPIO、SPI等。字符设备如硬盘等按照块传输的设备,块设备和网络设备的驱动我们跟多是做移植的工作,字符设备种类繁多且不算复杂,所以就会自己来写。
一 设备号
这么多设备如何区分,这就是设备号的作用,设备号又分为主设备号和次设备号。主设备号表征设备属于哪一类设备,比如串口设备。次设备号表示主设备号下的具体哪个设备。比如说串口1、串口2、串口3等等。
用4字节来表示设备号,其中主设备号占用 高 12位,次设备号占用 低 20位。
(1) linux提供了一组宏来生成设备号
#define MINORBITS20
define MINORMASK((1U << MINORBITS) - 1)
define MAJOR(dev)((unsigned int) ((dev) >> MINORBITS)) 获取主设备号
define MINOR(dev)((unsigned int) ((dev) & MINORMASK)) 获取次设备号
define MKDEV(ma,mi)(((ma) << MINORBITS) | (mi)) // ma << 20 | mi 生成设备号
用起来还是很简单的。
int major = 250;
int minor = 0;
int devno;
devno = MKDEV(major, minor);
(2) 申请注册设备号
上面生成设备号之后,我们要将这个设备号注册到内核当中,确保没有注册过即可。这是静态注册,还有动态注册。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:from 设备号
count 次设备的数量
name 设备号的名称
返回值:成功 0, 出错负数的错误码
这个注册函数可以注册多个设备,在count处设置即可。设备号的名称是让内核用的,一会儿我们说到创建设备文件名称是让用户看的,这两者不要混淆。
有注册就会有取消注册
void unregister_chrdev_region(dev_t from, unsigned count)
参数:from 设备号
count 次设备的数量
设备注册之后那里可以查看呢?看这里 proc/devcies文件
这里分了字符设备和块设备。前面表示的就是设备号
二 字符设备对象
C语言是面向过程的语言,其他高级语言如c++,java等都是面向对象的,面向对象的好处不言而喻,这里不说了。c语言中也可以利用结构体来实现一个面向对象的过程。上面我们向内核注册了一个设备号,那这个设备号用来干嘛呢?我们对应这个设备号就要生成并向内核注册一个字符设备对象,来表明这个设备实现的功能。
一个字符设备对象如下
struct cdev {
struct kobject kobj; // 设备对象的基类
struct module *owner; // 直接赋值为THIS_MODULE 模块的拥有者
const struct file_operations *ops; // 文件操作集合
struct list_head list; // 包含此结构体的成员,都是内核循环双链表节点
dev_t dev; // 设备号
unsigned int count; // 次设备的数量
};
我们主要关注ops和dev就行,dev就是我们的设备号,每一个设备号对应一个设备对象。ops中就是一堆的操作方法。对象中必须得有方法,设备对象的方法在哪里?全部都在ops当中。我们来看看ops到底都有啥
struct file_operations {
struct module owner; //直接赋值为THIS_MODULE 模块的拥有者
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);
};
ops中有好多操作方法,我们不用全部实现。我们重点关注标红的那几个即可,我们想想在应用层操作一个文件时常用的有那几个呢?open close read write,就是对应我们这里的open release read write,操作设备时我们还经常会用ioctl,比如说串口设置波特率,对应我们这里的unlocked_ioctl。
该实现的方法都实现后,我们将实现的方法和对象绑定到一起,其实就是设备对象的方法集合初始化。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数:cdev 字符设备对象
fops 文件操作集合
使用时候的框架如下
dev_t devno;
int major = 250;
int minor = 0;
int count = 1;
struct cdev cdev; //设备对象
int demo_open(struct inode *inodep, struct file * filep) // 打开设备
{
return 0;
}
int demo_release(struct inode * inodep, struct file * filep) // 关闭设备
{
return 0;
}
ssize_t demo_read(struct file * filep, char __user * buffer, size_t size, loff_t * offlen)
{
return size;
}
ssize_t demo_write(struct file *filep, const char __user *buffer, size_t size, loff_t * offlen)
{
return size;
}
long demo_ioctl(struct file * filep, unsigned int cmd, unsigned long arg)
{
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
.unlocked_ioctl = demo_ioctl,
};
好,设备对象的初始化也完成了,我们还得把这个设备对象告诉内核,内核才知道有这个东东。这就是设备对象的注册,或者叫做设备对象的添加。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:p 字符设备对象
dev 设备号
count 次设备的数量
返回值:成功 0,出错 负数的错误码
有注册就会有取消注册
void cdev_del(struct cdev *p)
这里稍微注意注册设备号和注册设备对象的顺序,先注册设备号,再注册设备对象。很好理解嘛,注册设备对象时都得有设备号的参数了,内核要把设备对象绑定到设备号上了,你不得先向内核注册设备号吗?
取消注册的时候顺序反过来,先取消注册设备对象,再取消注册设备号。
上面讲的都是内核层面,我们在应用层使用open时的第一个参数是啥,是要打开文件的路径,详细情况参考linux学习(十六):文件IO。那怎么和我们之前说的一大堆对应上呢?我们知道linux中,一切皆文件。驱动设备也一样。驱动设备文件在哪里,我们查看/dev/,那里就是设备文件统一聚集地。这里的文件就是和应用层交互用的。
我们怎么创建一个设备文件呢?我们来看看手动方式,执行shell命令
mknod name c 主设备号 次设备号
name表示设备文件名称,注意和之前说的注册设备号名称区别,那个是给内核用的,这两者名称不必相同。
c 表示设备类型为字符设备
后面为 主设备号和次设备号
执行这条指令之后,linux变会将创建出来的设备文件和之前注册的设备号和设备对象绑定在一起。具体的linux如何从应用层到底层,我们下一篇再来详细描述一下。
总结一下上面字符设备驱动的一个基本过程:
1、生成设备号
2、向内核注册该设备号
3、初始化设备对象,完成操作方法集
4、向内核注册该设备对象
5、生成设备文件,供用户层调用。
百闻不如一见,看一个简单的例子
驱动层:
include
include
include
include
include
MODULE_LICENSE("GPL");
dev_t devno;
int major = 250;
int minor = 0;
int count = 1;
struct cdev cdev;
int demo_open(struct inode *inodep, struct file * filep) // 打开设备
{
printk("%s,%d\n", func, LINE);
return 0;
}
int demo_release(struct inode * inodep, struct file * filep) // 关闭设备
{
printk("%s,%d\n", func, LINE);
return 0;
}
struct file_operations fops = {
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
};
static int __init demo_init(void)
{
int ret = 0;
printk("%s,%d\n", __func__, __LINE__);
devno = MKDEV(major, minor);
printk("devno:%d\n", devno);
ret = register_chrdev_region(devno, count, "xxx");
if(ret)
{
printk("Failed to register_chrdev_region.\n");
return ret;
}
cdev_init(&cdev, &fops);
cdev.owner = THIS_MODULE;
ret = cdev_add(&cdev, devno, count);
if(ret)
{
printk("Failed to cdev_add.\n");
unregister_chrdev_region(devno, count);
return ret;
}
return 0;
}
static void __exit demo_exit(void)
{
printk("%s,%d\n", func, LINE);
cdev_del(&cdev);
unregister_chrdev_region(devno, count);
}
module_init(demo_init);
module_exit(demo_exit);
这里只实现了简单的open close,框架嘛对吧 其余的根据功能实现就是了。insmod之后,可以看到/proc/devices下有了个名字为xxx的设备号
看一看应用层
include
include
include
include
int main(int argc, const char *argv[])
{
int fd;
fd = open("/dev/hello", O_RDWR);
if(fd < 0)
{
perror("Failed to open.");
return -1;
}
else
{
printf("open success.\n");
}
getchar();
close(fd);
return 0;
}
编译后看一下执行结果
应用层成功打开了该设备文件。按下回车键后就关闭了文件。
我们看看内核层执行的结果
成功的执行了open和release函数。
————————————————
版权声明:本文为CSDN博主「念念有余」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012142460/article/details/78906576