编写字符设备小Demo

Linux内核——字符设备.


设备相关点解.

Linux驱动设备一共分为三种:字符设备块设备网络设备,其中只有网络设备比较特殊,其在/dev目录下是没有设备文件的。

区分文件类型:

符号文件类型
c字符设备
b块设备
d目录
l符号链接

字符设备

       该设备对数据的处理按照字节流的形式进行的,支持顺序访问(是有时间的概念),也可以支持随机访问。
典型的字符设备:串口、键盘、触摸屏、摄像头、I2C、SPI、声卡、帧缓冲设备….
顺序访问的设备:串口、键盘、触摸屏
随机访问的设备:帧缓冲设备

       应用程序,能够使用系统IO函数来就行访问:open、write、read、lseek、close…..

比如在根目录的/dev下我们能看到
/dev

       其中第五第六列分别是主设备号次设备号
如:fb0的主设备号为29,次设备号为0

1
crw-rw----  1 root video    29,   0 Feb  6 11:52 fb0

主设备号

主设备号主要用于区分某一类型的设备,比如显示器、鼠标、键盘、硬盘等

次设备号

       次设备号用来区分同一类型设备的不同个体或者不同分区,比如显示器A.a、显示器A.b 或者硬盘的分区1~分区7;


简述字符设备驱动的设计流程.

  1. 首先定义一个字符设备 struct cdev
  2. 申请设备号
    a. 静态注册
    MKDEV
    register_chrdev_region
    b. 动态注册
    alloc_chrdev_region
  3. 定义file_operations初始化打开、关闭、读、写等函数接口。
  4. cdev初始化
    .cdev_init
  5. 将cdev加入到内核
    .cdev_add
  6. 创建设备文件
    .手动创建,去/dev目录进行创建
    .自动创建
    1)class_create
    2)device_create

此处默认都能使用Source Insight来阅读内核源码了

定义一个字符设备.

使用到一个结构体,见include/linux/cdev.h –> cdev

1
2
3
4
5
6
7
8
9
10
#include <linux/cdev.h>

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

kobj:内核管理驱动的时候,自动管理的对象,自动链接调用(不需要我们管);
*owner:属于哪一个内核模块,初始化一般默认编写值为THIS_MODULE,指向当前模块;(重点)
*ops:字符设备文件操作集;(重点)
list:内核自动管理字符设备的链表,内核自动调用;
dev:设备号;
count:对当前设备申请次设备号的数目;(重点)

定义并初始化一个文件操作集.

定义一个file_operations结构体,见/include/linux/fs.h –> file_operations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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);
};

       当然很多时候我们用不到它结构体定义的这么多函数接口,只需保留最常用的即可;

       每个字符设备都必须有一个文件操作集,文件操作集是驱动程序给应用程序访问硬件的一个接口,应用层与驱动程序函数接口的对应关系如下:

应用层驱动层
openopen
closerelease
writewrite
readread
lseekllseek
ioctlunlocked_ioctl

用法例子:(摘自源码bsg.c)

1
2
3
4
5
6
7
8
9
10
static const struct file_operations bsg_fops = {
	.read		=	bsg_read,
	.write		=	bsg_write,
	.poll		=	bsg_poll,
	.open		=	bsg_open,
	.release	=	bsg_release,
	.unlocked_ioctl	=	bsg_ioctl,
	.owner		=	THIS_MODULE,
	.llseek		=	default_llseek,
};

对应的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ssize_t
bsg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
	struct bsg_device *bd = file->private_data;
        ...
        ...
        ...
	return bytes_read;
}

static ssize_t
bsg_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
	struct bsg_device *bd = file->private_data;
	ssize_t bytes_written;
        ...
        ...
        ...
	return bytes_written;
}
...
...
...

当应用层的 open函数 调用了 xxx.koxxx_open 函数
当应用层的 write函数 调用了 xxx.koxxx_write 函数
当应用层的 close函数 调用了 xxx.koxxx_release 函数

我们需要定义一个file_operations的结构体把自己编写的接口配置进去,以便后续cdev_init的初始化。


源码及详解.

led_dev.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
//1.定义字符设备
static struct cdev led_cdev;

//2.声明设备号
static dev_t led_dev_num;

static ssize_t led_read(struct file *file, char __user *buf, size_t count, loff_t *offset)
{
    printk("<3>""led_read\n");
    return 0;
}

static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *offset)
{
    printk("<3>""led_write\n");
    return 0;
}

static int led_open(struct inode *inode, struct file *file)
{
    printk("<3>""led_open\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
    printk("<3>""led_release\n");
    return 0;
}

//3.定义file_operations
static const struct file_operations led_fops = {
	.read		=	led_read,
	.write		=	led_write,
	.open		=	led_open,
	.release	=	led_release,
	.owner		=	THIS_MODULE
};

//入口函数
static int __init gec6818_led_init(void)
{
    int ret = 0;

    led_dev_num = MKDEV(239,0);     //2.1静态申请设备号  主设备号为239,次设备号为0

    //2.2注册设备号  int register_chrdev_region(dev_t from, unsigned count, const char *name)
    ret = register_chrdev_region(led_dev_num, 1, "myled");
    if (ret < 0)
    {
        printk("<3>""register chrdev region error\n");
        return -1;
    }

    //4.字符设备的初始化 void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    cdev_init(&led_cdev, &led_fops);
    //5.将设备加入到内核 int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    ret = cdev_add(&led_cdev, led_dev_num, 1);
    if(ret < 0)
    {
        printk("<3>""cdev_add error\n");
        goto err_cdev_add;

    }

    printk("<3>""gec6818 led init success\n");
    printk("<3>""device num: %d\n", MAJOR(led_dev_num));
    printk("<3>""device last num: %d\n", MANOR(led_dev_num));
    return 0;

err_cdev_add:
    unregister_chrdev_region(led_dev_num, 1);       //注销设备号
    return ret;
} 

//出口函数
static void __exit gec6818_led_exit(void)
{
    
    cdev_del(&led_cdev);                            //从系统中删除字符设备                                     

    unregister_chrdev_region(led_dev_num, 1);       //注销设备号


	printk("<4>""gec6818 led exit\n");
}



//驱动程序的入口:insmod led_drv.ko调用module_init,module_init又会去调用gec6818_led_init。
module_init(gec6818_led_init);

//驱动程序的出口:rmmod led_drv调用module_exit,module_exit又会去调用gec6818_led_exit。
module_exit(gec6818_led_exit)


//模块描述
MODULE_AUTHOR("LYQ");			                //作者信息
MODULE_DESCRIPTION("study kernal first code test");	//模块功能说明
MODULE_LICENSE("GPL");				        //许可证:驱动遵循GPL协议

要点详解

静态申请设备号 MKDEV

MKDEV 原型 参数1:主设备号,参数2:次设备号

1
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

注册设备号

register_chrdev_region 原型 参数1:声明的设备号,参数2:注册的数量,参数3:注册的名字 返回值:成功为0,失败返回负数的错误码

1
int `register_chrdev_region(dev_t from, unsigned count, const char *name)

字符设备的初始化

cdev_init 原型 参数1:定义的字符设备,参数2:file_operations

1
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

将设备加入内核

cdev_add 原型 参数1:定义的字符设备地址,参数2:声明的设备号,参数3:加入的数量,返回值:成功为0,失败为负数错误值

1
int cdev_add(struct cdev *p, dev_t dev, unsigned count)

设备号注销

unregister_chrdev_region 原型 参数1:声明的设备号,参数2:数量

1
void unregister_chrdev_region(dev_t from, unsigned count)

将字符设备从内核删除

cdev_del 原型 参数1:定义的字符设备地址

1
void cdev_del(struct cdev *p)

编写对应Makefile.

1
2
3
4
5
6
7
8
9
10
obj-m += led_dev.o
KERNEL_DIR := /home/bbigq/6818GEC/kernel
CROSS_COMPILE := /home/bbigq/6818GEC/prebuilts/gcc/linux-x86/arm/arm-eabi-4.8/bin/arm-eabi-
PWD := $(shell pwd)

default:
	$(MAKE) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	rm  *.o *.order .*.cmd  *.mod.c *.symvers .tmp_versions -rf

编译完成后,insmod到Linux系统中后,可以通过lsmod查看相关信息,也可以使用cat命令查看/proc/devices,能够找到注册成功的设备名字。

注意:由于没有这里只是小demo是需要手动创建设备节点的,如果配合应用程序使用时,需要为该设备在/dev目录下创建设备文件;
使用:

mknod /dev/led c 239 0
mknod 设备文件 设备类型 主设备号 次设备号

后续的博客中将会加入自动创建设备节点的代码,和讲解!
注意
1.手动创建设备文件的时候,主设备号和次设备号不能写错;
2.若此前的设备文件已经存在,再次创建相同名字的设备文件,会出现错误;(解决方法:先使用rm命令删除原来的设备文件,再使用mknod命令重新创建。

最后用ls -l /dev/xxx检查是否创建成功!


我的GITHUB

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大大棋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值