设备号

Linux的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev目录下,称为设备文件。应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,而次设备号用来区分同一类型的多个设备。对于常用设备,Linux有约定俗成的编号,如硬盘的主设备号是3。

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

函数定义:

1
2
3
4
5
6
#include <sys/sysmacros.h>
#include <linux/cdev.h>
MKDEV(int major,int minor) //major为主设备号 minor为次设备号。
#define MKDEV(major,minor) (((major) << MINORBITS) | (minor))。
unsigned int major(dev_t dev);//获取主设备号
unsigned int minor(dev_t dev);//获取次设备号

在程序中用宏MAJOR(dev_t dev)可以解析出主设备号,用宏MINOR(dev_t dev)可以解析出次设备号

申请设备号

动态申请设备号,没有指定设备号用以下函数:

1
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

dev :alloc_chrdev_region函数向内核申请下来的设备号

baseminor :次设备号的起始

count: 申请次设备号的个数

name :执行 cat /proc/devices显示的名称

静态申请设备号,给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:

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

from:起始设备号

count:数量设备

name:设备名字

注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region 函 数 还 是register_chrdev_region 函数申请的设备号,统一使用如下释放函数:

1
void unregister_chrdev_region(dev_t from, unsigned count)

新字符设备驱动下,设备号分配如下:

1
2
3
4
5
6
7
8
9
10
11
12
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */
if (major)
{ /* 定义了主设备号 */
devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0*/
register_chrdev_region(devid, 1, "test");
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
major = MAJOR(devid); /* 获取分配号的主设备号 */
minor = MINOR(devid); /* 获取分配号的次设备号 */
}

字符设备结构

1
2
3
4
5
6
7
8
struct cdev {
struct kobject kobj;//是一个嵌入在该结构中的内核对象。它用于该数据结构的一般管理
struct module *owner;//owner指向提供驱动程序的模块
const struct file_operations *ops;//ops是一组文件操作,实现了与硬件通信的具体操作
struct list_head list;//list用来实现一个链表,其中包含所有表示该设备的设备特殊文件的inode
dev_t dev;//dev指定了设备号
unsigned int count;//表示与该设备关联的从设备的数目
};

cdev两种定义初始化方式: Linux驱动|cdev_init、cdev_alloc区别_一口Linux的博客-CSDN博客

静态初始化:cdev_init函数

1
void cdev_init(struct cdev *cdev, const struct file_operations *fops) 
1
2
3
4
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
my_cdev.ops = &fops;

动态初始化:cdev_alloc函数

1
2
3
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;

将cdev添加到系统中去 cdev_add函数

1
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
1
2
3
4
5
6
7
8
9
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */

cdev_del 函数

卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备。

1
void cdev_del(struct cdev *p)

参数 p 就是要删除的字符设备。如果要删除字符设备,参考如下代码:

1
cdev_del(&testcdev); /* 删除 cdev */

cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函数。

自动创建设备节点

早期的Linux内核(版本2.4之前)并没有实现一个统一的设备模型,设备节点的创建一般是mknod命令手动创建或利用devfs文件系统创建。早期的Linux发行版一般会采用手动创建的方式预先把通常用到的节点都创建出来,而嵌入式系统则会采用devfs的方式。起初Linux2.6 内核还支持devfs,但从2.6.18开始,内核完全移除了devfs系统而采用的udev的方式动态的创建设备节点。因此,新的Linux发行版都采用udev的方式管理设备节点文件。
class可以自动创建设备节点,不需要udev,而udev自动创建节点需要用到class。

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类, class 是个结构体,定义在文件include/linux/device.h 里面。 class_create 是类创建函数, class_create 是个宏定义。

1
2
3
4
5
6
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)

根据上述代码,将宏 class_create 展开以后内容如下:

1
struct class *class_create (struct module *owner, const char *name)

class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:

1
void class_destroy(struct class *cls);

参数 cls 就是要删除的类。

创建设备

使用 device_create 函数在类下面创建设备, device_create 函数原型如下:

1
2
3
4
5
struct device *device_create(	struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)

device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:

1
void device_destroy(struct class *class, dev_t devt)

参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。

设置文件私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式 ,一般以设备的所有属性来作为一个结构体。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct test_dev testdev;
/* open 函数 */
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /* 设置私有数据 */
return 0;
}

在 open 函数里面设置好私有数据以后,在 write、 read、 close 等函数中直接读取 private_data即可得到设备结构体。

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/fs.h>

#define NEWCHRLED_CNT 1
#define NEWCHRLED_NAME "newchrled"
#define LED_MAJOR 200
#define LED_NAME "led"

#define CCM_CCGR1_BASE (0X020C406C)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)

#define on 1
#define off 0
static void __iomem *CCM_CCGR1;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
//设备属性
struct newchrled_dev{
dev_t devid;
struct cdev cdev;
struct class *class;
struct device *device;
int major;
int minor;
};

struct newchrled_dev newchrled;//led 设备

void led_switch(u8 sta)
{
u32 num;
if(sta == on)
{
num =readl(GPIO1_DR);
num &= ~(1 << 3);
writel( num, GPIO1_DR);
printk("on\n");
}
else if(sta == off)
{
num = readl(GPIO1_DR);
num |= (1 << 3);
writel( num, GPIO1_DR);
printk("off\n");
}
}

static int led_dev_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled;//设置私有数据

return 0;
}

static int led_dev_release(struct inode *inode, struct file *filp)
{
return 0;
}

static ssize_t led_dev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}

static ssize_t led_dev_write(struct file *filp,const char __user *buf, size_t cnt, loff_t *offt)
{
int val = 0;
unsigned char databuf[1];
unsigned char state ;

val = copy_from_user(databuf, buf, cnt);
if(val < 0)
{
printk("kernl write failed \r\n");
return -EFAULT;
}
state = databuf[0];
if(state == on)
led_switch(on);
else if(state == off)
led_switch(off);

return 0;

}

static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_dev_open,
.read = led_dev_read,
.write = led_dev_write,
.release = led_dev_release,
};
static int __init led_init(void)
{
//int value = 0;
u32 val = 0;
CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

val = readl(CCM_CCGR1);
val &= ~(3 << 26);//CCM_CCGR1的第27和26位控制GPIO1 clock的使能
val |= (3 << 26);
writel(val, CCM_CCGR1);
//GPIO复位
writel(5,SW_MUX_GPIO1_IO03);
//配置电器属性
writel(0x10B0, SW_PAD_GPIO1_IO03);
//配置输出模式
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
//默认关闭led
val = readl(GPIO1_DR);
val |= (1 << 3);
writel( val , GPIO1_DR);

//注册设备驱动
if(newchrled.major)
{
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid, 1, "led");
}
else{
alloc_chrdev_region(&newchrled.devid, 0, 1, "led");
newchrled.major = MAJOR(newchrled.devid);
newchrled.minor = MINOR(newchrled.devid);
}
//初始化
cdev_init(&newchrled.cdev, &led_fops);
newchrled.cdev.owner = THIS_MODULE;

//添加到系统中
cdev_add(&newchrled.cdev, newchrled.devid, 1);
//创建类
newchrled.class = class_create(THIS_MODULE , "led");
if(IS_ERR(newchrled.class))
{
return PTR_ERR(newchrled.class);
}
//创建设备
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, "led");
if(IS_ERR(newchrled.device))
{
return PTR_ERR(newchrled.class);
}
return 0;
}
static void __exit led_exit(void)
{
iounmap(CCM_CCGR1);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(SW_MUX_GPIO1_IO03);
//注销led设备
cdev_del(&newchrled.cdev);/* 删除cdev */
unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT); /* 注销设备号 */

device_destroy(newchrled.class, newchrled.devid);
class_destroy(newchrled.class);
printk("led exit\r\n");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");