Linux设备驱动(LDD)

Linux设备驱动(LDD)

代码可以点击这里

Linux设备的几个要点:

  • 工作在内核态
  • 硬件设备作为文件看待
  • 使用文件接口对设备进行控制

Linux设备的分类(一种划分方式):

  1. 字符设备
  2. 块设备
  3. 网络设备

1. 了解设备

在Linux的/dev/目录(device缩写)下有我们的设备文件

r1U3PP.png

以c开头的都是字符设备,b开头的都是块设备,后面依次有主、次设备号,主设备号用于标识设备种类(范围为1-255),次设备号用来标识不同的硬件设备,最后是相应的设备文件。

2. 设备驱动程序

功能完整的LDD结构(一般至少要包含1,2,3,4):

  1. 设备的打开
  2. 设备的释放
  3. 驱动程序的注册
  4. 驱动程序的注销
  5. 设备的读操作
  6. 设备的写操作
  7. 设备的控制操作

我们将设备当作文件看待,所以驱动程序就是使用文件操作接口对设备进行控制,文件操作结构体如下:

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
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 *, 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 (*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 **);
};

当然我们不一定都需要使用上述全部的文件操作接口,尝试用read,write,open,release等接口

这里详细说明一下read接口,接口原型如下(write接口是类似的):

1
ssize_t (*read) (struct file * file, char __user * buf, size_t count, loff_t * ppos);

返回值:读取的大小

参数说明(形参名可以不同哈):

  1. file:与字符设备文件关联的file结构,由内核创建
  2. buf:从设备文件读取到的数据,需要保存到的位置。由read系统调用提供该参数
  3. count:请求传输的数据量,由read系统调用提供该参数
  4. ppos:文件的读写位置,由内核从file结构中取出后,传递进来

直观理解可以看下面这张图

r3xFpQ.png

读取的过程其实就是将设备中file中的内容读取到buf中,其中ppos表示file中的偏移量(单位是字节),而count表示需要读出的字节数。

读取可以使用copy_to_user()函数(将内核空间的数据拷贝到用户空间,相应的写入数据可以使用copy_from_user()函数)

3. 字符设备驱动程序开发(读取int数据)

3.1 驱动程序

定义了一个能够保存128个int数据的缓冲区,然后使用readwrite完成整型数据的读取,代码如下,注释应该还是比较详细 :-)

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("flygoast");
MODULE_DESCRIPTION("A Simple Character Device driver module");

static struct cdev cdev;
static dev_t devno;

static int tmp[128] = {0};

// 打开设备
static int
intDevice_open(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "open\n");
return 0;
}

// 释放设备
static int
intDevice_release(struct inode *inodep, struct file *filep)
{
printk(KERN_INFO "release\n");
return 0;
}

// 设备读操作
static ssize_t
intDevice_read(struct file *filep, char __user *buf, size_t count, loff_t *offset)
{
printk(KERN_INFO "read offset:%lld\\n", *offset);
if(count <= *offset){
// 如果count小于偏移量,那么直接读取count个字节
if (copy_to_user(buf, tmp + *offset - count, count) != 0) {
// 读取失败
return -EFAULT;
}
// 读取成功,偏移量减去count,并且返回读取字节数
*offset -= count;
return count;
}
else{
// count大于偏移量,无法读取count个字节,读取失败
return -EFAULT;
}
}

// 设备写操作
static ssize_t
intDevice_write(struct file *filep, const char __user *buf, size_t count,
loff_t *offset)
{
size_t avail;

printk(KERN_INFO "write offset:%lld\n", *offset);
// 缓冲区剩余字节数
avail = sizeof(tmp) - *offset;

memset(tmp + *offset, 0, avail);

if (count > avail) {
// 写入字节数count大于剩余字节数,那么就写入剩余字节数(有多少写多少)
if (copy_from_user(tmp + *offset, buf, avail) != 0) {
// 写入失败
return -EFAULT;
}
// 写入成功,偏移量加上写入字节数,并返回
*offset += avail;
return avail;

} else {
// 写入字节数count小于剩余字节数,直接写入count个字节
if (copy_from_user(tmp + *offset, buf, count) != 0) {
// 写入失败
return -EFAULT;
}
*offset += count;
return count;
}
}

// 重定位offset
static loff_t
intDevice_llseek(struct file *filep, loff_t off, int whence)
{
loff_t newpos;

switch (whence) {
case 0: /* SEEK_SET */
newpos = off;
break;
case 1: /* SEEK_CUR */
newpos = filep->f_pos + off;
break;
case 2: /* SEEK_END */
newpos = sizeof(tmp) + off;
break;
default:
return -EINVAL;
}

if (newpos < 0) {
return -EINVAL;
}

filep->f_pos = newpos;
return newpos;
}

// 文件操作结构映射
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = intDevice_open,
.release = intDevice_release,
.read = intDevice_read,
.llseek = intDevice_llseek,
.write = intDevice_write,
};

// 设备注册
static int __init intDevice_init(void) {
int ret;

printk(KERN_INFO "Load intDevice\n");

devno = MKDEV(112, 0); // 主设备号
ret = register_chrdev_region(devno, 1, "intDevice"); // 主设备号,次设备号,设备名称

if (ret < 0) {
return ret;
}

cdev_init(&cdev, &fops);
cdev.owner = THIS_MODULE;

cdev_add(&cdev, devno, 1);

return 0;
}
// 设备注销
static void __exit intDevice_cleanup(void) {
printk(KERN_INFO "cleanup intDevice\n");
unregister_chrdev_region(devno, 1);
cdev_del(&cdev);
}

module_init(intDevice_init);
module_exit(intDevice_cleanup);

MODULE_LICENSE("GPL");

3.2 安装配置驱动

然后编译即可,Makefile如下:

1
2
3
4
5
obj-m += intDevice.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

rGi0AA.png

然后安装驱动模块:

1
sudo insmod intDevice.ko

然后可以使用如下命令查看

1
cat /proc/devices

rGF8Ej.png

然后创建设备文件

1
mknod /dev/[设备名] c [主设备号] [次设备号]

rGFqRP.png

然后就能看到我们创建的设备文件

1
ls -l /dev/

rGF226.png

3.3 测试驱动

使用如下的测试代码对驱动程序进行简单测试

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>

int inputBuf[128], outputBuf[128];

int main()
{
int fd, m, n;
fd = open("/dev/intDevice", O_RDWR);
if (fd < 0) {
fprintf(stderr, "open file \"/dev/intDevice\" failed\n");
exit(-1);
}

llseek(fd, 0, 0);
inputBuf[0] = 666;
inputBuf[1] = 333;
n = write(fd, inputBuf, 2 * sizeof(int));

printf("read: ");
m = read(fd, outputBuf, 2 * sizeof(int));
printf("sum = %d\n", outputBuf[0] + outputBuf[1]);

close(fd);
return 0;
}

运行测试程序,似乎是没什么问题哈 :-)

(注意要用root权限,否则可能打开不了设备)

rGktdH.png