LV10-02-字符设备驱动-04-字符设备开发实例

本文主要是字符设备驱动开发实例的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
Linux开发板 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2
u-boot 2013.01
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
文件下载链接
------

一、单个次设备

1. 开发步骤

字符设备驱动基本开发步骤如下:

  • 1、如果设备有自己的一些控制数据,则定义一个包含 struct cdev mydev 成员的结构体 struct __my_dev ,其它成员根据设备需求,设备简单则直接用 struct cdev
  • 2、定义一个 struct mydevstruct cdev 的全局变量来表示本设备;也可以定义一个 struct __my__devstruct cdev 的全局指针(需要在 init 时动态分配)。
  • 3、定义三个全局变量分别来表示主设备号、次设备号、设备数(只有一个次设备的话也可以写进 struct __my__dev 结构体中)。
  • 4、定义一个 struct file_operations 结构体变量,其 owner 成员置成 THIS_MODULE
  • 5、 module init 函数流程:

(1)申请设备号;

(2)如果是全局设备指针则动态分配代表本设备的结构体元素 ;

(3)初始化 struct cdev 成员;

(4)设置 struct cdevowner 成员为 THIS_MODULE

(5)添加字符设备到内核;

(6)创建类;

(7)创建设备;

  • 6、 module exit 函数流程:

(1)删除设备;

(2)删除类;

(3)注销设备号;

(4)从内核中移除 struct cdev

(5)如果是全局设备指针则释放其指向空间.

  • 7、编写各个操作函数并将函数名初始化给 struct file_operations 结构体变量。

2. 开发实例

2.1 new_char_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
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: new_char_dev.c
* Author : qidaink
* Date : 2022-08-24
* Version :
* Description: 新字符设备开发框架——单个次设备
* Others :
* Log :
* ======================================================
*/
//=======================================================
/* 头文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h> /* copy_from_user copy_to_user */
#include <linux/device.h> /* class_create class_destroy device_create */
#include "new_char_dev.h"
//=======================================================
#define NEWCHRLED_NAME "new_char_dev" /* 设备名称: /proc/devices 文件中与该设备对应的名字,方便用户层查询主设备号 */
#define BUF_LEN 100 /* 数据读写缓冲区大小 */

/* 新字符设备的属性 */
struct _my_dev
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
int dev_num; /* 申请的设备数量 */
/* 2.字符设备对象 */
struct cdev mydev; /* 定义一个字符设备对象设备 */
/* 3.设备读写相关成员变量 */
char mydev_buf[BUF_LEN]; /* 读写缓冲区 */
int curlen; /* 读写缓冲区中当前数据字节数 */
/* 4.自动创建设备节点相关成员变量 */
struct class *class; /* 类 */
struct device *device; /* 设备 /dev/dev_name */
};
struct _my_dev g_my_dev; /* 定义一个新字符设备 */

//=======================================================
/* 新字符设备操作函数声明 */
int mydev_open(struct inode *pnode, struct file *pfile); /* 打开设备 */
int mydev_close(struct inode *pnode, struct file *pfile); /* 关闭设备 */
ssize_t mydev_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos); /* 读设备 */
ssize_t mydev_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos); /* 写设备 */
long mydev_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg); /* 设备属性获取 */
/* 操作函数集定义 */
struct file_operations myops = {
/* data */
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_close,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
};

/**
* @Function : mydev_open
* @brief : 打开设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .open
*/
int mydev_open(struct inode *pnode, struct file *pfile)
{
/* 获取 struct _my_dev g_my_dev 地址 */
pfile->private_data = (void *)(container_of(pnode->i_cdev, struct _my_dev, mydev));
printk("mydev_open is called!\n");
return 0;
}

/**
* @Function : mydev_close
* @brief : 关闭设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .close
*/
int mydev_close(struct inode *pnode, struct file *pfile)
{
printk("mydev_close is called!\n");
return 0;
}


/**
* @Function : mydev_read
* @brief : 读设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次read对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :__user * 类型,指向用户空间一块内存,用来保存读到的数据。
* @param count :size_t 类型,用户期望读取的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功读取的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .read
*/
ssize_t mydev_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos)
{
int size = 0;
int ret = 0;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
if (count > p_my_dev->curlen)
size = p_my_dev->curlen;
else
size = count;
ret = copy_to_user(pbuf, p_my_dev->mydev_buf, size);
if (ret)
{
printk("copy to user buf failed!\n");
return -1;
}
memcpy(p_my_dev->mydev_buf, p_my_dev->mydev_buf + size, p_my_dev->curlen - size);
p_my_dev->curlen -= size;

return size;
}

/**
* @Function : mydev_write
* @brief : 写设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次write对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :__user * 类型,指向用户空间一块内存,用来保存被写的数据。
* @param count :size_t 类型,用户期望写入的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功写入的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .write
*/
ssize_t mydev_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos)
{
int size = 0;
int ret = 0;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
if (count > BUF_LEN - p_my_dev->curlen)
size = BUF_LEN - p_my_dev->curlen;
else
size = count;
ret = copy_from_user(p_my_dev->mydev_buf + p_my_dev->curlen, pbuf, size);
if (ret)
{
printk("copy from user buf failed!\n");
return -1;
}
p_my_dev->curlen += size;

return size;
}

/**
* @Function : mydev_ioctl
* @brief : 设备属性获取
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次ioctl对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param cmd :unsigned int 类型,用来表示做的是哪一个操作。
* @param arg :unsigned long 类型,和cmd配合用的参数。
* @return : 成功返回0,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .unlocked_ioctl
*/
long mydev_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg)
{
int __user *pret = (int *)arg;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
int maxlen = BUF_LEN;
switch (cmd)
{
case MYCHAR_IOCTL_GET_MAXLEN:
if (copy_to_user(pret, &maxlen, sizeof(int)))
{
printk("copy_to_user maxlen failed!\n");
return -1;
}
break;
case MYCHAR_IOCTL_GET_CURLEN:
if (copy_to_user(pret, &p_my_dev->curlen, sizeof(int)))
{
printk("copy_to_user curlen failed!\n");
return -1;
}
break;
default:
printk("The cmd is unknown!\n");
return -1;
}
return 0;
}

/* 驱动入口函数 */
int __init new_char_dev_init(void)
{
/* 1.分配设备号 */
g_my_dev.major = 200; /* 手动指定主设备号 */
g_my_dev.minor = 0; /* 大部分驱动次设备号都选择0 */
g_my_dev.dev_num = 1; /* 申请的设备数量 */
g_my_dev.devno = MKDEV(g_my_dev.major, g_my_dev.minor); /* MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号 */
if (register_chrdev_region(g_my_dev.devno, g_my_dev.dev_num, NEWCHRLED_NAME))
{
/* 手动指定设备号分配失败,申请自动分配设备号 */
if (alloc_chrdev_region(&g_my_dev.devno, 0, g_my_dev.dev_num, NEWCHRLED_NAME))
{
printk("get devno failed!\n");
return -1;
}
/* 重新获取主设备号和次设备号 */
g_my_dev.major = MAJOR(g_my_dev.devno); /* 自动分配设备号后,分离出主设备号 */
g_my_dev.minor = MINOR(g_my_dev.minor); /* 自动分配设备号后,分离出次设备号 */
}
/* 2.向Linux内核注册新的字符设备 */
/* 2.1初始化 mydev 对象, 给该对象添加操作函数集 */
cdev_init(&g_my_dev.mydev, &myops);
/* 2.2向 Linux 系统添加字符设备 (cdev结构体变量,将会被添加到一个hash链表中) */
g_my_dev.mydev.owner = THIS_MODULE;
cdev_add(&g_my_dev.mydev, g_my_dev.devno, g_my_dev.dev_num); /* 会在 /proc/devices 中创建设备号和对应的名称记录 */
/* 3.自动创建设备节点(省去 mknod 命令在 /dev 下创建设备节点步骤) */
/* 3.1创建类 */
g_my_dev.class = class_create(THIS_MODULE, NEWCHRLED_NAME);
if (IS_ERR(g_my_dev.class))
{
return PTR_ERR(g_my_dev.class);
}
/* 3.2创建设备(创建的设备为 /dev/dev_name) */
g_my_dev.device = device_create(g_my_dev.class, NULL, g_my_dev.devno, NULL, NEWCHRLED_NAME);
if (IS_ERR(g_my_dev.device))
{
return PTR_ERR(g_my_dev.device);
}
/* 4.打印提示信息 */
printk("g_my_dev: major = %d, minor = %d\r\n", g_my_dev.major, g_my_dev.minor);
return 0;
}

/* 驱动出口函数 */
void __exit new_char_dev_exit(void)
{
/* 1.重新获取设备号(有可能是动态分配了的设备号) */
g_my_dev.devno = MKDEV(g_my_dev.major, g_my_dev.minor); /* MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号 */
/* 2.删除设备节点 */
/* 2.1删除设备(/dev/dev_name 将会被删除) */
device_destroy(g_my_dev.class, g_my_dev.devno);
/* 2.2删除类 */
class_destroy(g_my_dev.class);
/* 3.删除字符设备对象 cdev */
cdev_del(&g_my_dev.mydev);
/* 4.注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录)*/
unregister_chrdev_region(g_my_dev.devno, g_my_dev.dev_num);
/* 5.打印卸载模块的提示信息 */
printk("This module is exited!\n");
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(new_char_dev_init);
module_exit(new_char_dev_exit);

/* 模块信息(通过 modinfo new_char_dev 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2.2 new_char_dev.h

点击查看详情
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
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: new_char_dev.h
* Author : qidaink
* Date : 2022-08-26
* Version :
* Description:
* Others :
* Log :
* ======================================================
*/
#ifndef __NEW_CHAR_DEV_H__
#define __NEW_CHAR_DEV_H__

/* 头文件 */
#include <asm/ioctl.h>

#define MY_CHAR_MAGIC 'k'

#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC, 1, int *)
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC, 2, int *)

/* printk和printf 打印输出的颜色定义 */
/* 前景色(字体颜色) */
#define CLS "\033[0m" /* 清除所有颜色 */
#define BLACK "\033[1;30m" /* 黑色加粗字体 */
#define RED "\033[1;31m" /* 红色加粗字体 */
#define GREEN "\033[1;32m" /* 绿色加粗字体 */
#define YELLOW "\033[1;33m" /* 黄色加粗字体 */
#define BLUE "\033[1;34m" /* 蓝色加粗字体 */
#define PURPLE "\033[1;35m" /* 紫色加粗字体 */
#define CYAN "\033[1;36m" /* 青色加粗字体 */
#define WHITE "\033[1;37m" /* 白色加粗字体 */
#define BOLD "\033[1m" /* 加粗字体 */

#endif

2.3 app_new_char_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
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: app_new_char_dev.c
* Author : qidaink
* Date : 2022-08-24
* Version :
* Description: 新字符设备测试 app —— 单个次设备
* Others :
* Log :
* ======================================================
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> /* close */
#include <sys/ioctl.h> /* ioctl */

#include "new_char_dev.h"

int main(int argc, char *argv[])
{
/* 0.相关变量定义 */
int fd = -1; /* 文件描述符 */
int max = 0; /* 驱动中缓冲区大小,驱动中为 100 字节 */
int len = 0; /* 当前缓冲区数据字节数 */
char buf[20] = ""; /* 数据缓冲区 */
/* 1.判断参数个数是否合法 */
if (argc < 2)
{
printf("\n%s /dev/dev_name\n\n", argv[0]);
return -1;
}
/* 2.打开字符设备 */
if ((fd = open(argv[1], O_RDWR)) < 0)
{
printf("open %s failed\n", argv[1]);
return -1;
}
/* 3.向字符设备写入数据 */
if (write(fd, "hello world!", 15) < 0)
{
printf("write failed!\n");
return -1;
}
/* 4.获取缓冲区的大小和当前数据个数 */
ioctl(fd, MYCHAR_IOCTL_GET_MAXLEN, &max);
ioctl(fd, MYCHAR_IOCTL_GET_CURLEN, &len);
printf("BUF_LEN = %d, curlen = %d\n", max, len);
/* 5.从字符设备读取刚才写入的数据 */
if (read(fd, buf, 15) < 0)
{
printf("read failed!\n");
return -1;
}
else
printf("buf = %s\n", buf);
/* 6.关闭字符设备 */
close(fd);
fd = -1;

return 0;
}

2.4 Makefile

点击查看详情
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
##============================================================================#
# Copyright © hk. 2022-2025. All rights reserved.
# File name: Makefile
# Author : qidaink
# Date : 2022-08-26
# Version :
# Description:
# Others :
# Log :
##============================================================================#
##
# 模块名和模块测试APP名称
MODULE_NAME := new_char_dev
APP_NAME := app_$(MODULE_NAME)

# NFS 共享目录
ROOTFS ?= /home/hk/4nfs/rootfs
MODULE_DIR ?= $(ROOTFS)/01myDrivers

ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= /home/hk/5linux/linux-3.14
GCC_TOOL ?= arm-none-linux-gnueabi-gcc
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
GCC_TOOL ?= gcc
endif

PWD := $(shell pwd)

# 编译模块和测试程序
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
$(GCC_TOOL) $(APP_NAME).c -Wall -o $(APP_NAME)

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

# 拷贝相关文件到nfs共享目录
copy:
sudo cp $(MODULE_NAME).ko $(MODULE_DIR)
sudo cp $(APP_NAME) $(MODULE_DIR)

.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk $(APP_NAME)

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"
else
CONFIG_MODULE_SIG=n
obj-m += $(MODULE_NAME).o
endif

二、多个次设备

1. 开发步骤

多个次设备的时候步骤与单个次设备的时候其实是一样的,不同之处在于:

  • 1、设备号申请的时候要直接连续申请多个;
  • 2、向内核注册新字符设备的时候,每个设备都要注册;
  • 3、创建类的时候只需要创建一个;
  • 4、创建设备的时候需要创建多个设备;
  • 5、删除设备的时候需要删除多个;
  • 6、删除字符设备对象的时候需要删除多个。

2. 开发实例

2.1 new_char_dev_multi.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
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: new_char_dev_multi.c
* Author : qidaink
* Date : 2022-08-24
* Version :
* Description: 新字符设备开发框架——多个次设备
* Others :
* Log :
* ======================================================
*/
//=======================================================
/* 头文件 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/uaccess.h> /* copy_from_user copy_to_user */
#include <linux/device.h> /* class_create class_destroy device_create */
#include "new_char_dev_multi.h"
//=======================================================
#define NEWCHAR_DEV_NAME "new_char_dev_multi" /* 设备名称:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号 */
#define NEWCHAR_DEV_CNT 3 /* 次设备数量 */
#define BUF_LEN 100 /* 数据读写缓冲区大小 */

//=======================================================
/** 父设备号相关结构体,为了创建子设备号(子设备 0 的设备号与父设备号相同)
* 父设备可以看做是代表了整个驱动程序,父设备下挂3个具体的设备
*/
struct _my_dev_parent
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
int dev_cnt; /* 次设备数量 */
/* 2.自动创建设备节点相关成员变量 */
struct class *class; /* 类(无论几个次设备,这里的类只有一个) */
};
struct _my_dev_parent parent_dev={
.major = 200,
.minor = 0,
.dev_cnt = NEWCHAR_DEV_CNT,
};


/* 新字符设备的属性 */
struct _my_dev
{
/* 1.设备号与设备数量 */
int major; /* 主设备号:占高12位,用来表示驱动程序相同的一类设备,Linux系统中主设备号范围为 0~4095 */
int minor; /* 次设备号:占低20位,用来表示被操作的哪个具体设备 */
dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
/* 2.字符设备对象 */
struct cdev mydev; /* 定义一个字符设备对象设备 */
/* 3.设备读写相关成员变量 */
char mydev_buf[BUF_LEN]; /* 读写缓冲区 */
int curlen; /* 读写缓冲区中当前数据字节数 */

/* 自动创建设备节点相关成员变量 */
struct device *device; /* 设备 */
};
struct _my_dev g_my_dev[NEWCHAR_DEV_CNT]; /* 定义新字符设备数组 */

//=======================================================
/* 新字符设备操作函数声明 */
int mydev_open(struct inode *pnode, struct file *pfile); /* 打开设备 */
int mydev_close(struct inode *pnode, struct file *pfile); /* 关闭设备 */
ssize_t mydev_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos); /* 读设备 */
ssize_t mydev_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos); /* 写设备 */
long mydev_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg); /* 设备属性获取 */
/* 操作函数集定义 */
struct file_operations myops = {
/* data */
.owner = THIS_MODULE,
.open = mydev_open,
.release = mydev_close,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
};

/**
* @Function : mydev_open
* @brief : 打开设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .open
*/
int mydev_open(struct inode *pnode, struct file *pfile)
{
/* 获取 struct _my_dev g_my_dev 地址 */
pfile->private_data = (void *)(container_of(pnode->i_cdev, struct _my_dev, mydev));
printk("mydev_open is called!\n");
return 0;
}

/**
* @Function : mydev_close
* @brief : 关闭设备
* @param pnode : struct inode * 类型,内核中记录文件元信息的结构体
* @param pfile : struct file * 类型,读写文件内容过程中用到的一些控制性数据组合而成的对象
* @return : 0,操作完成
* @Description : 函数名初始化给 struct file_operations 的成员 .close
*/
int mydev_close(struct inode *pnode, struct file *pfile)
{
printk("mydev_close is called!\n");
return 0;
}

/**
* @Function : mydev_read
* @brief : 读设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次read对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :__user * 类型,指向用户空间一块内存,用来保存读到的数据。
* @param count :size_t 类型,用户期望读取的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功读取的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .read
*/
ssize_t mydev_read(struct file *pfile, char __user *pbuf, size_t count, loff_t *ppos)
{
int size = 0;
int ret = 0;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
if (count > p_my_dev->curlen)
size = p_my_dev->curlen;
else
size = count;
ret = copy_to_user(pbuf, p_my_dev->mydev_buf, size);
if (ret)
{
printk("copy to user buf failed!\n");
return -1;
}
memcpy(p_my_dev->mydev_buf, p_my_dev->mydev_buf + size, p_my_dev->curlen - size);
p_my_dev->curlen -= size;

return size;
}

/**
* @Function : mydev_write
* @brief : 写设备
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次write对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param pbuf :__user * 类型,指向用户空间一块内存,用来保存被写的数据。
* @param count :size_t 类型,用户期望写入的字节数。
* @param ppos :loff_t * 类型,对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置。
* @return : 成功返回本次成功写入的字节数,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .write
*/
ssize_t mydev_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *ppos)
{
int size = 0;
int ret = 0;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
if (count > BUF_LEN - p_my_dev->curlen)
size = BUF_LEN - p_my_dev->curlen;
else
size = count;
ret = copy_from_user(p_my_dev->mydev_buf + p_my_dev->curlen, pbuf, size);
if (ret)
{
printk("copy from user buf failed!\n");
return -1;
}
p_my_dev->curlen += size;

return size;
}

/**
* @Function : mydev_ioctl
* @brief : 设备属性获取
* @param pfile : struct file * 类型,指向open产生的struct file类型的对象,表示本次ioctl对应的那次open。
* (读写文件内容过程中用到的一些控制性数据组合而成的对象)
* @param cmd :unsigned int 类型,用来表示做的是哪一个操作。
* @param arg :unsigned long 类型,和cmd配合用的参数。
* @return : 成功返回0,失败返回-1
* @Description : 函数名初始化给 struct file_operations 的成员 .unlocked_ioctl
*/
long mydev_ioctl(struct file *pfile, unsigned int cmd, unsigned long arg)
{
int __user *pret = (int *)arg;
struct _my_dev *p_my_dev = (struct _my_dev *)(pfile->private_data);
int maxlen = BUF_LEN;
switch (cmd)
{
case MYCHAR_IOCTL_GET_MAXLEN:
if (copy_to_user(pret, &maxlen, sizeof(int)))
{
printk("copy_to_user maxlen failed!\n");
return -1;
}
break;
case MYCHAR_IOCTL_GET_CURLEN:
if (copy_to_user(pret, &p_my_dev->curlen, sizeof(int)))
{
printk("copy_to_user curlen failed!\n");
return -1;
}
break;
default:
printk("The cmd is unknown!\n");
return -1;
}
return 0;
}

/* 驱动入口函数 */
int __init new_char_dev_multi_init(void)
{
int i = 0;
/* 1.分配设备号(将会申请 parent_dev.dev_cnt 个从 0 开始的连续设备号,他们的主设备号相同,表示他们使用的是同一个驱动) */
if (parent_dev.major) /* 定义了设备号 */
{
parent_dev.devno = MKDEV(parent_dev.major, 0); /* MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号 */
if (register_chrdev_region(parent_dev.devno, parent_dev.dev_cnt, NEWCHAR_DEV_NAME))
{
/* 手动指定设备号分配失败,申请自动分配设备号 */
if (alloc_chrdev_region(&parent_dev.devno, 0, parent_dev.dev_cnt, NEWCHAR_DEV_NAME))
{
printk("get parent_dev failed!\n");
return -1;
}
}
}
else /* 未定义主设备号 */
alloc_chrdev_region(&parent_dev.devno, 0, parent_dev.dev_cnt, NEWCHAR_DEV_NAME); /* 申请设备号 */
parent_dev.major = MAJOR(parent_dev.devno); /* 获取分配号的主设备号 */
parent_dev.minor = MINOR(parent_dev.devno); /* 获取分配号的次设备号 */
/* 2.向Linux内核注册新的字符设备 */
for (i = 0; i < parent_dev.dev_cnt; i++)
{
/* 2.1从父设备号得到三个子设备号(子设备号是连续的) */
g_my_dev[i].devno = MKDEV(parent_dev.major, parent_dev.minor + i); /* MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号 */
g_my_dev[i].major = MAJOR( g_my_dev[i].devno); /* 获取分配号的主设备号 */
g_my_dev[i].minor = MINOR( g_my_dev[i].devno); /* 获取分配号的次设备号 */

/* 2.2初始化 mydev 对象, 给该对象添加操作函数集 */
cdev_init(&g_my_dev[i].mydev, &myops);
/* 2.3向 Linux 系统添加字符设备 (cdev结构体变量,将会被添加到一个hash链表中) */
g_my_dev[i].mydev.owner = THIS_MODULE;
/* 会在 /proc/devices 中创建设备号和对应的名称记录(最终只会有一条主设备号的记录,但还是要分多次添加) */
cdev_add(&g_my_dev[i].mydev, g_my_dev[i].devno, 1); /* 此处是固定的一个一个添加 */
/* 2.4打印提示信息 */
printk("g_my_dev[%d]: devno=%d, major=%d, minor=%d\n", i, g_my_dev[i].devno, g_my_dev[i].major, g_my_dev[i].minor);
}
/* 3.自动创建设备节点(省去 mknod 命令在 /dev 下创建设备节点步骤) */
/* 3.1创建类(要注意无论是一个设备还是多个设备,类只有一个) */
parent_dev.class = class_create(THIS_MODULE, NEWCHAR_DEV_NAME);
if (IS_ERR(parent_dev.class))
{
return PTR_ERR(parent_dev.class);
}
/* 3.2创建设备(有几个次设备,就创建几个) */
for (i = 0; i < parent_dev.dev_cnt; i++)
{
/* 创建的设备为 /dev/dev_name */
g_my_dev[i].device = device_create(parent_dev.class, NULL, g_my_dev[i].devno, NULL, "%s%d", NEWCHAR_DEV_NAME, i);
if (IS_ERR(g_my_dev[i].device))
{
return PTR_ERR(g_my_dev[i].device);
}
/* 3.3打印提示信息 */
printk("g_my_dev[%d]: [/dev/%s%d] has been created!\n", i, NEWCHAR_DEV_NAME, i);
}
return 0;
}

//=======================================================
/* 驱动出口函数 */
void __exit new_char_dev_multi_exit(void)
{
int i = 0;
/* 1.重新获取设备号(有可能是动态分配了的设备号) */
/* 全局变量中已获取过,这里可以不再获取 */
/* 2.删除设备节点 */
/* 2.1删除设备(多个次设备,逐个进行删除) */
for (i = 0; i < parent_dev.dev_cnt; i++)
{
/* 删除的是 /dev/dev_name */
device_destroy(parent_dev.class, g_my_dev[i].devno);
printk("g_my_dev[%d]: [/dev/%s%d] has been deleted!\n", i, NEWCHAR_DEV_NAME, i);
}
/* 2.2删除类(只有一个类,删除一次即可) */
class_destroy(parent_dev.class);
/* 3.删除字符设备对象 cdev */
for (i = 0; i < parent_dev.dev_cnt; i++)
{
cdev_del(&g_my_dev[i].mydev);
/* 4.注销设备号 */
// unregister_chrdev_region(g_my_dev[i].devno, 1); /* 一个一个删除,这样似乎有bug,真正的主设备号并未释放,除非后边数字改成 NEWCHAR_DEV_CNT */
}
/* 4.注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */
unregister_chrdev_region(parent_dev.devno, parent_dev.dev_cnt);

/* 5.打印卸载模块的提示信息 */
printk("This module is exited!\n");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(new_char_dev_multi_init);
module_exit(new_char_dev_multi_exit);

/* 模块信息(通过 modinfo new_char_dev_multi 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("qidaink <2038035593@qq.com>"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2.2 new_char_dev_multi.h

点击查看详情
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
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: new_char_dev_multi.h
* Author : qidaink
* Date : 2022-08-26
* Version :
* Description:
* Others :
* Log :
* ======================================================
*/
#ifndef __NEW_CHAR_DEV_MULTI_H__
#define __NEW_CHAR_DEV_MULTI_H__

/* 头文件 */
#include <asm/ioctl.h>

#define MY_CHAR_MAGIC 'k'

#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC, 1, int *)
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC, 2, int *)

/* printk和printf 打印输出的颜色定义 */
/* 前景色(字体颜色) */
#define CLS "\033[0m" /* 清除所有颜色 */
#define BLACK "\033[1;30m" /* 黑色加粗字体 */
#define RED "\033[1;31m" /* 红色加粗字体 */
#define GREEN "\033[1;32m" /* 绿色加粗字体 */
#define YELLOW "\033[1;33m" /* 黄色加粗字体 */
#define BLUE "\033[1;34m" /* 蓝色加粗字体 */
#define PURPLE "\033[1;35m" /* 紫色加粗字体 */
#define CYAN "\033[1;36m" /* 青色加粗字体 */
#define WHITE "\033[1;37m" /* 白色加粗字体 */
#define BOLD "\033[1m" /* 加粗字体 */

#endif

2.3 app_new_char_dev_multi.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
/** =====================================================
* Copyright © hk. 2022-2025. All rights reserved.
* File name: app_new_char_dev_multi.c
* Author : qidaink
* Date : 2022-08-24
* Version :
* Description: 新字符设备测试 app —— 多个次设备
* Others :
* Log :
* ======================================================
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h> /* close */
#include <sys/ioctl.h> /* ioctl */

#include "new_char_dev_multi.h"

int main(int argc, char *argv[])
{
/* 0.相关变量定义 */
int fd = -1; /* 文件描述符 */
int max = 0; /* 驱动中缓冲区大小,驱动中为 100 字节 */
int len = 0; /* 当前缓冲区数据字节数 */
char buf[20] = ""; /* 数据缓冲区 */
/* 1.判断参数个数是否合法 */
if (argc < 2)
{
printf("\n%s /dev/dev_name\n\n", argv[0]);
return -1;
}
/* 2.打开字符设备 */
if ((fd = open(argv[1], O_RDWR)) < 0)
{
printf("open %s failed\n", argv[1]);
return -1;
}
/* 3.向字符设备写入数据 */
if (write(fd, "hello world!", 15) < 0)
{
printf("write failed!\n");
return -1;
}
/* 4.获取缓冲区的大小和当前数据个数 */
ioctl(fd, MYCHAR_IOCTL_GET_MAXLEN, &max);
ioctl(fd, MYCHAR_IOCTL_GET_CURLEN, &len);
printf("BUF_LEN = %d, curlen = %d\n", max, len);
/* 5.从字符设备读取刚才写入的数据 */
if (read(fd, buf, 15) < 0)
{
printf("read failed!\n");
return -1;
}
else
printf("buf = %s\n", buf);
/* 6.关闭字符设备 */
close(fd);
fd = -1;

return 0;
}

2.4 Makefile

点击查看详情
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
##============================================================================#
# Copyright © hk. 2022-2025. All rights reserved.
# File name: Makefile
# Author : qidaink
# Date : 2022-08-26
# Version :
# Description:
# Others :
# Log :
##============================================================================#
##
# 模块名和模块测试APP名称
MODULE_NAME := new_char_dev_multi
APP_NAME := app_$(MODULE_NAME)

# NFS 共享目录
ROOTFS ?= /home/hk/4nfs/rootfs
MODULE_DIR ?= $(ROOTFS)/01myDrivers

ifeq ($(KERNELRELEASE),)

# 选择可执行文件运行的平台
ifeq ($(ARCH), arm)
KERNELDIR ?= /home/hk/5linux/linux-3.14
GCC_TOOL ?= arm-none-linux-gnueabi-gcc
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
GCC_TOOL ?= gcc
endif

PWD := $(shell pwd)

# 编译模块和测试程序
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
$(GCC_TOOL) $(APP_NAME).c -Wall -o $(APP_NAME)

modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

# 拷贝相关文件到nfs共享目录
copy:
sudo cp $(MODULE_NAME).ko $(MODULE_DIR)
sudo cp $(APP_NAME) $(MODULE_DIR)

.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk $(APP_NAME)

help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "insmod <module_name>.ko # Load module"
@echo "rmmod <module_name> # Uninstall the module"
@echo "dmesg -C # Clear the kernel print information"
@echo "lsmod # Check the kernel modules that have been inserted"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"
else
CONFIG_MODULE_SIG=n
obj-m += $(MODULE_NAME).o
endif