RK3588系列内核开发——字符设备与寄存器

自定义内核

在很早以前,我们就对内核进行了基础的解读,具体可以看早期的系列:【开发一个Linux系统吧】,当时并没有在内核的基础上进行二次开发,仅仅使用了一些Linux特有的API进行开发,而本系列的【RK3588系列内核开发】则是直接针对linux的内核进行开发。

开发一个Linux系统吧:https://blog.minloha.cn/posts/182214b18bc0992022122222.html

Kconfig详解

我们在编译Linux内核时会接触到终端UI界面,在我们使用make menuconfig时就可以看到,这个UI界面包含了基本的页面配置,此配置由Kconfig进行生成,首先介绍一下Kconfig语法:

1、读取另一个Kconfig文件(注意Kconfig文件的文件名也必须是Kconfig)

1
source "init/Kconfig"

2、生成一个菜单条目

1
2
3
menu "I2C support"
...
endmenu

这句话就是在menuconfig生成一个可供选择的条目,用户可以在menuconfig按回车进入其中,举个栗子:

1
2
3
4
5
6
menu "Menu Title"
config FOO
bool "Option Foo"
config BAR
int "Option Bar"
endmenu

在这句话中,用户需要按enter进入”Menu Title”才可以看到内部的两个config,它仅作为视觉上的分组,不存在配置值。

一个menu内可以有很多个config,或者menu或者menuconfig,这些都是可以嵌套的,当然存在最外层的Kconfig说明,在整个内核的根目录(main menu)

3、条件判断

直接举例:

1
2
3
4
5
6
7
8
menuconfig MODULE_FOO
bool "Enable Foo Module"
if MODULE_FOO
config FOO_OPTION1
bool "Option 1"
config FOO_OPTION2
int "Option 2"
endif

这里我使用到了一个新的字叫menuconfig,与menu类似,这是一个带有子选项的配置项,其本身带有开启或关闭选项(bool或tristate类型,tristate类型比bool除了是否以外多了一个编译为ko文件的选项),以例子解释,他会生成一个下面的样子,直接按enter里面是空的,仅当前面的方括号内有*即被选中才可以查看内部的选项:

1
[ ] Enable Foo Module  --->

1

如图所示I2C support即为menu,而I3C support、SPI support等均为menuconfig,包含一个可选的前框和可进入的菜单。

4、多选一

为了解决有些条目的互斥,需要进行多选一,因此有特殊的关键字,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
choice
prompt "Select the CPU architecture" # 选择菜单的标题
default ARM # 默认选中的选项
depends on PLATFORM_X86_OR_ARM # 整个choice的依赖条件

config X86
bool "Intel x86 architecture"

config ARM
bool "ARM architecture"

config RISCV
bool "RISC-V architecture"

endchoice

choice/endchoice就是多选条目的开头和结束,在区域内的条目仅允许勾选一次

5、信息提示

这是一个可以打出环境变量的提示语句,用起来非常简单:

1
comment "SPI Master Controller Drivers"

比如这样,就会在终端打出一句话。

2

6、config

作为全Kconfig的灵魂所在,config的作用不容小觑,比如我们需要定义一个可供用户选择的编译项,我们可以写:

1
2
3
4
5
config GPU_OPENGL
bool "OpenGL"
default y
help
OpenGL

其中config后面跟随的为开关的名字,在经过解释器后将会变成头文件中的一个宏,记为:CONFIG_开关名,在这里就是CONFIG_GPU_OPENGL。

bool为类型,取值仅为y或n,当然还有其他类型:

类型
booly/n
tristatey/m/n
string字符串
hex十六进制的数据
int十进制的int数

help为帮助,当光标位于对应条目上按h就可以看到提示信息。

7、组件依赖

我们的一个config可能无法满足所有内容,因此我们用到了依赖关系,如果我们勾选一个config,它有前置需要开启就只需要添加:

1
2
config A
depends on B

这样如果B没有勾选就去勾选A,则会被提示。

自定义菜单

我们直接在根目录创建一个just_kid文件夹,在里面新建一个Kconfig文件,然后在根目录的Kconfig添加just_kid下的Kconfig:

1
source "just_kid/Kconfig"

然后我们在just_kid的Kconfig内写入:

1
2
3
4
5
6
7
8
menu "just a kid"
config KID_CONFIG
bool "just kid hahaha"
default y
help
you can learn Kconfig programmer
comment "Kid comment"
endmenu

然后我们进入menuconfig就可以看到我们添加这个条目了:

3

4

使用这个数值也很简单,我们的Kconfig编译后生成了一个对应的宏,我们可以直接给makefile传递,以I3C举例,这是他的Makefile:

1
2
3
4
# SPDX-License-Identifier: GPL-2.0
i3c-y := device.o master.o
obj-$(CONFIG_I3C) += i3c.o
obj-$(CONFIG_I3C) += master/

其中他的Kconfig的config名为I3C,因此当I3C为y是直接编译进入内核,为m是编译为模块,为n是不编译。这些均在autoconf.h有所定义,但是我们一般不会导入此头文件,而是交给makefile进行取舍,当文件变成模块后则会定义一个新的宏,比如I3C,如果编译进入内核则多出:CONFIG_I3C,如果编译成模块则多出:CONFIG_I3C_MODULE

字符设备

接下来这部分是linux内核开发的初步,我们学习linux内核的目的就是在内核的基础上进行二次开发,由于内核已经非常完善了,我们仅仅是在其基础上进行外设或驱动的开发,因此我们所有的开发其实都是设备的驱动开发,因此我们这一章就是在对外设进行开发。

设备号

我们在使用内核的操作系统的时候经常会查看/dev/目录下的文件,这里面包含了所有设备的驱动,比如相机、串口、GPIO等,这些设备我们在使用的时候可以在C语言、python中采用文件的方法直接打开或写入,这是由于linux有一套虚拟文件系统,将所有外设都抽象为文件,对于内核的代码可以查看之前写过的这篇博客:

[4]开发一个Linux系统吧——VFS

所有的设备都是设备块,被inode所指向,而inode不仅仅是文件还是设备,因此我们要想声明一个自己的设备到/dev目录下,我们也需要一个inode,注意VFS的节点等级,node本身就保存在内存中,因此其空间管理较为繁琐,对于大块空间还存在超级块,这些并非我们的操作核心。

VFS核心源码位于:include\linux\fs.h

VFS的结构类似于双向链表,每个inode都存在一个super_node的指针用于索引,而每个inode又存在一个dev_t指向我们的字符设备,我们可以查看inode的具体实现:

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
struct inode {
// 节点模式
umode_t i_mode;
// 节点是否打开的一个标识
unsigned short i_opflags;
// 节点所有者的UID
kuid_t i_uid;
// 节点所处的组ID
kgid_t i_gid;
// ....
// 节点本身的操作函数,比如挂载、切盘、删除、重命名等
const struct inode_operations *i_op;
struct super_block *i_sb;
// .....
dev_t i_rdev;
loff_t i_size;

union {
// inode在不作为文件时,所有的内部函数操作结构体,包括读写释放等操作
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
void (*free_inode)(struct inode *);
};

union {
// inode不作为文件时的属性,管道、块驱动、字符驱动亦或者软链接等
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
// ....
} __randomize_layout;

我们可以看到inode的实现非常丰富,包含了几乎所有的操作。在inode以后就是dev_t和cdev了,其中dev_t是一个u32类型的数字,表示为设备号,高12位为主设备号,低20位为次设备号。主设备号表示设备驱动的类型或者叫设备驱动的大类,而次设备号用于表示设备的具体示例,用于区分同一个驱动的多个不同设备实例。比如v4l2对摄像头设备的驱动,就会生成相同的主设备号:

1
/dev/video0 /dev/video1

他们的主设备号相同,次设备号从0开始递增排列,因此我们一个字符设备尽量只申请一个主设备号,多个次设备号。

设备号的申请分为动态申请和静态申请,动态申请由char_dev中的find_dynamic_major在设备链表内寻找一个没有被使用的主设备号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int find_dynamic_major(void)
{
int i;
struct char_device_struct *cd;

for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) {
if (chrdevs[i] == NULL)
return i;
}

for (i = CHRDEV_MAJOR_DYN_EXT_START;
i >= CHRDEV_MAJOR_DYN_EXT_END; i--) {
for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next)
if (cd->major == i)
break;

if (cd == NULL)
return i;
}

return -EBUSY;
}

而次设备号不会被查询,默认为0,当指定返回多个设备号时才会在返回多个主设备号的前提下申请多个次设备号,否则仅为0。我们从根本开始,在kdev_t.h存在一些关键的宏:

1
2
3
4
5
6
7
8
9
10
// 次设备号的位数
#define MINORBITS 20
// 主设备号的掩码,其实就是12个1,但是没有以十六进制表示,即:0b0000 1111 1111 1111 1111
#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))

然后我们的注册静态设备号就存在这样的方法:

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
int register_chrdev_region(dev_t from, unsigned count, const char *name) {
struct char_device_struct *cd;
// 参数传递一个组合好的设备号(仅主设备号,不要次设备号的一个设备号)
dev_t to = from + count;
dev_t n, next;
// 寻找设备号是否被占用,
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
// __register_chrdev_region是标记设备号区间的方法。在给定主设备号的情况下标记count个次设备号,同时返回一个可以用的设备号(并不多余,动态分配时就可以指定主设备号为0,寻找n个连续的设备号)
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}

我们注意到一个非常关键的写法,内核出现报错并不会return,而是使用goto,这是由于一旦我们标记了一个区域的设备号出现问题后必须撤销操作,我们以前写代码直接return就是因为内核存在这种撤销操作的机制,否则我们不写的话内核也不会理会你的操作。

动态分配设备号的方法如下:

1
2
3
4
5
6
7
8
9
10
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}

此方法会返回一个没有被使用的设备号,linux判断指针是否有效也存在一个函数叫IS_ERR,原因是内核分页管理算法的最后一页就是给错误指针的位置,从0x ff ff ff ff ff ff f0 00~ 0x ff ff ff ff ff ff ff ff这段空间就是错误码,并进行一一对应。可以在errno-base.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
#define	EPERM		 1	/* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

介绍完API后我们正式开始学习使用这些API。

动态与静态设备号申请

我们需要的API已经在上面详细介绍了,我们直接写出一段代码:

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>

// major叫主设备号, minor叫次设备号
// 设备号是一个整数, 由主设备号和次设备号组成
// 主设备号用于标识设备驱动程序, 次设备号用于标识同一主设备号下的不同设备
// 例如: /dev/sda1, 主设备号是8, 次设备号是1
// 这里我们使用宏MAJOR和MINOR来获取设备号的主次部分
// MAJOR(dev_t) 获取主设备号
// MINOR(dev_t) 获取次设备号
// MKDEV(major, minor) 将主设备号和次设备号组合成一个设备号
static int major, minor = 0;

module_param(major, int, S_IRUGO);
module_param(minor, int, S_IRUGO);

// dev_t实际上为uint32_t类型的数据,高12位标识主设备号, 低20位表示次设备号
static dev_t dev_number;

static int __init chrdev_init(void){
int ret = 0;
// 如果有设备号(用户指定的设备号)
if(major) {
// 创建设备
dev_number = MKDEV(major, minor);
printk(KERN_INFO "Using major %d, minor %d\n", major, minor);
printk(KERN_INFO "Device number: %d\n", dev_number);

// 静态注册设备号
// dev_t结构体, 申请设备的数量, 设备名称
ret = register_chrdev_region(dev_number, 1, "chr_dev");
if (ret < 0) {
printk(KERN_INFO "Failed to register chrdev region: %d\n", ret);
return ret;
}
printk(KERN_INFO "chrdev region registered successfully\n");
} else {
// 没有设备号(用户没有指定的设备号)
// 分配设备号, 按照设备名申请
ret = alloc_chrdev_region(&dev_number, minor, 1, "chr_dev");
// 申请失败则输出错误信息
if (ret < 0) printk(KERN_INFO "Failed to allocate chrdev region: %d\n", ret);
// 申请成功,正常输出设备号
printk(KERN_INFO "chrdev region allocated successfully\n");
major = MAJOR(dev_number);
minor = MINOR(dev_number);
// 输出设备号
printk(KERN_INFO "Using allocated major %d, minor %d\n", major, minor);
}
return 0;
}

static void __exit chrdev_exit(void){
// 释放设备(切记一定要释放, 否则内核就会出现被污染的情况,只能重启解决)
unregister_chrdev_region(dev_number, 1);
printk(KERN_INFO "chrdev region unregistered\n");
}

module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

我们写一个makefile直接编译出ko文件,加载进rk3588内,查看dmesg就可以看到下面的输出,这是我没有进行指定设备号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
obj-m := dev_t.o

# 工具链和架构配置
ARCH := arm64
CROSS_COMPILE := /mnt/g/rk3588-linux/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
ccflags-y := -std=gnu11

# 内核源码路径(绝对路径)
KERNEL_DIR := /mnt/g/rk3588-linux/kernel


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

clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

5

我们指定设备号就是传递一个内核参数即可,即加载内核时使用:

1
sudo insmod dev_t.ko major=314 minor=2

这样我们的dmesg输出就是:

6

我们可以看到device number,这是组合后的dev_t的值,也就是314 << 20 + 2的大小,经过计算器验算很容易知道这是对的。我们可以在系统进程的设备里面看到这个设备,输出cat /proc/devices即可找到我们设备名

7

可以看到主设备号为314符合我们的设定。

设备功能函数

我们之前详细介绍了设备号的API,接下来介绍一下cdev.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
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
// 初始化一个字符设备, 参数为字符设备结构体和设备的函数
void cdev_init(struct cdev *, const struct file_operations *);
// 动态返回一个字符设备
struct cdev *cdev_alloc(void);
// 减少字符设备的引用计数
void cdev_put(struct cdev *p);
// 将字符设备放入内核,也就是/dev目录
int cdev_add(struct cdev *, dev_t, unsigned);

// 设置字符设备的父对象
void cdev_set_parent(struct cdev *p, struct kobject *kobj);
// 将字符设备 cdev 与 struct device 绑定,并注册到 sysfs
int cdev_device_add(struct cdev *cdev, struct device *dev);
// 解绑并移除设备
void cdev_device_del(struct cdev *cdev, struct device *dev);
// 注销字符设备
void cdev_del(struct cdev *);
// 清除 inode 对字符设备的引用
void cd_forget(struct inode *);

实现方法也很简单,我们在上一小节的代码进行修改即可:

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

// 设备号
static dev_t dev_number;
// 设备本身的实现
static struct cdev char_dev {
.owner = THIS_MODULE
};

static int __init _cdev_init(void) {
int ret, major, minor;
// 动态设备号申请, 自动返回一个未使用的设备号
ret = alloc_chrdev_region(&dev_number, 0, 1, "chardev_name");
if (ret < 0) {
printk(KERN_EMERG "Failed to allocate char dev region\n");
return ret;
}

// 设备号
major = MAJOR(dev_number);
minor = MINOR(dev_number);
printk(KERN_EMERG "major is %d\n", major);
printk(KERN_EMERG "minor is %d\n", minor);
// 静态初始化一个字符设备
cdev_init(&char_dev, NULL);
// 将此设备添加到内核中
ret = cdev_add(&char_dev, dev_number, 1);
if (ret < 0) {
printk(KERN_EMERG "Failed to add cdev\n");
unregister_chrdev_region(dev_number, 1);
return ret;
}

printk(KERN_EMERG "char_dev initialized\n");
return 0;
}

static void __exit _cdev_exit(void) {
// 销毁内核中添加的字符设备
cdev_del(&char_dev);
// 取消对设备号的使用标记
unregister_chrdev_region(dev_number, 1);
printk(KERN_EMERG "char_dev exited\n");
}


module_init(_cdev_init);
module_exit(_cdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("A simple char device");

编译ko文件并运行,dmesg输出:

8

这是我们使用指令cat /proc/devices可以看到我们的设备已经出现:

9

创建设备节点

到目前为止,我们的设备均只存在于进程空间的设备驱动,我们无法进行外部调用,因此我们需要在/dev创建一个设备节点,在Linux中由于VFS的存在,创建设备节点需要创建一个文件,因此我们需要对inode进行基础的操作。

在class.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
38
39
40
#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)
{
struct class *cls;
int retval;

cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}

cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;

retval = __class_register(cls, key);
if (retval)
goto error;

return cls;

error:
kfree(cls);
return ERR_PTR(retval);
}

void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;

class_unregister(cls);
}

class_create需要传递的参数为inode指向的模块,一般写作宏THIS_MODULE,也就是一个特殊的汇编值__this_module。这里的方法用于创建设备的描述文件,创建设备需要在core.c内使用下面的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;

va_start(vargs, fmt);
dev = device_create_groups_vargs(class, parent, devt, drvdata, NULL,
fmt, vargs);
va_end(vargs);
return dev;
}

void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;

dev = class_find_device_by_devt(class, devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}

这里写了设备创建和销毁的方法,给定设备描述的class后就可以在/dev内创建出我们的设备了。

我们开始编写对应的代码:

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

static dev_t dev_number;
struct cdev cdev_test;
struct file_operations cdev_opt = {
.owner= THIS_MODULE,
};

struct class *cls_test;

static int __init chrcls_init(void) {
int ret, major, minor;

// 动态设备号申请, 自动返回一个未使用的设备号
// dev_t结构体, 次设备号的最小值, 申请数量, 设备名称
ret = alloc_chrdev_region(&dev_number, 0, 1, "chardev_name");
if (ret < 0) {
printk(KERN_EMERG "Failed to allocate char dev region\n");
return ret;
}

// 设备号
major = MAJOR(dev_number);
minor = MINOR(dev_number);
printk(KERN_EMERG "major is %d\n", major);
printk(KERN_EMERG "minor is %d\n", minor);

cdev_init(&cdev_test, NULL);

ret = cdev_add(&cdev_test, dev_number, 1);
if (ret < 0) {
printk(KERN_EMERG "cdev add error");
}

// 类名称, 也就是在/sys/class中显示的名字,用于描述设备属性
cls_test = class_create(THIS_MODULE, "char_cls");
// 设备名称,显示在/dev/的名字
device_create(cls_test, NULL, dev_number, NULL, "cls_device_test");
return 0;
}

static void __exit chrcls_exit(void) {
cdev_del(&cdev_test); //删除添加的字符设备cdev_test
unregister_chrdev_region(dev_number, 1); //释放字符设备所申请的设备号
device_destroy(cls_test, dev_number); //删除创建的设备
class_destroy(cls_test); //删除创建的类
printk("moduleexit\n");
}

module_init(chrcls_init);
module_exit(chrcls_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

我们编译运行可以看到:

10

在/dev和/sys/class均出现了我们的设备节点和设备描述。到目前为止我们就正式的创建出了我们的设备节点。

驱动框架

在之前我们已经写过了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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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 (*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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);

ANDROID_KABI_RESERVE(1);
ANDROID_KABI_RESERVE(2);
ANDROID_KABI_RESERVE(3);
ANDROID_KABI_RESERVE(4);
} __randomize_layout;

这里面的函数与用户态的函数存在对应关系,比如open或read,均是对设备的读写操作等,我们这部分就是利用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
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

// 打开方法
static int chrdev_open(struct inode *inode, struct file *file) {
printk(KERN_EMERG "chrdev_open called\n");
return 0;
}

// 读方法, 这里返回一个固定的数组
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) {
char kbuf[32] = "test read data";
if (copy_to_user(buf, kbuf, count) != 0) {
printk(KERN_ERR "Failed to copy data to user space\n");
return -EFAULT;
}
printk(KERN_EMERG "chrdev_read: %s\n", kbuf);
return 0;
}

// 写方法, 这里用的写方法就是从用户态拷贝数据打印出来
static ssize_t chrdev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) {
char kbuf[32] = {0};
if (copy_from_user(kbuf, buf, count) != 0) {
printk(KERN_ERR "Failed to copy data from user space\n");
return -EFAULT;
}
printk(KERN_EMERG "chrdev_write: %s\n", kbuf);
printk(KERN_EMERG "chrdev_write called\n");
return count;
}

// 释放方法, 单纯的释放罢了
static int chrdev_release(struct inode *inode, struct file *file) {
printk(KERN_EMERG "chrdev_release called\n");
return 0;
}


static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.read = chrdev_read,
.write = chrdev_write,
.release = chrdev_release,
};


static dev_t dev_number;
static struct cdev chrdev_cdev;
static struct class *chrdev_class;

// 设备初始化函数, 与上一小节一模一样
static int __init chrdev_fops_init(void) {
int ret, minor, major;
ret = alloc_chrdev_region(&dev_number, 0, 1, "chrdev");
if (ret < 0) {
printk(KERN_ERR "Failed to allocate char device region\n");
return ret;
}
major = MAJOR(dev_number);
minor = MINOR(dev_number);

printk(KERN_ERR "Allocated char device with major: %d, minor: %d\n", major, minor);
cdev_init(&chrdev_cdev, &chrdev_fops);
ret = cdev_add(&chrdev_cdev, dev_number, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add char device\n");
unregister_chrdev_region(dev_number, 1);
return ret;
}

chrdev_class = class_create(THIS_MODULE, "chrdev");
if (IS_ERR(chrdev_class)) {
printk(KERN_ERR "Failed to create class\n");
cdev_del(&chrdev_cdev);
unregister_chrdev_region(dev_number, 1);
return PTR_ERR(chrdev_class);
}
device_create(chrdev_class, NULL, dev_number, NULL, "chrdev");
return 0;
}


static void __exit chrdev_fops_exit(void) {
device_destroy(chrdev_class, dev_number);
class_destroy(chrdev_class);
cdev_del(&chrdev_cdev);
unregister_chrdev_region(dev_number, 1);
}


module_init(chrdev_fops_init);
module_exit(chrdev_fops_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");

这里我用到了两个函数分别为copy_to_usercopy_from_user,正如字面意思是从用户态拷贝数据和传递数据,原因是内核与用户之间存在隔离,因此需要这两个函数作为桥梁进行数据沟通:

1
2
copy_to_user(用户态数组, 内核态数组, 数据长度);
copy_from_user(内核态数组, 用户态数组, 数据长度);

写完这个代码编译后我们可以在发现出现了一个文件叫:/dev/chrdev

然后我们写一个用户态的代码,在rk3588上编辑一个叫app.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
// 写在用户目录内,不要用makefile编译
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
int fd;
char buf[32];
fd = open(argv[1], O_RDWR, 0666);
if (fd < 0) {
perror("Failed to open device");
return 1;
}

if (!strcmp(argv[2], "read")) {
// 这里的参数与file_operations的read一一对应, 利用用户态与内核态的数据传输, 我们可以完全实现这两个读写功能
ssize_t ret = read(fd, buf, sizeof(buf));
if (ret < 0) {
perror("Failed to read from device");
close(fd);
return 1;
}
printf("Read from device: %s\n", buf);
} else if (!strcmp(argv[2], "write")) {
const char *msg = "Hello, Device!";
ssize_t ret = write(fd, msg, strlen(msg));
if (ret < 0) {
perror("Failed to write to device");
close(fd);
return 1;
}
printf("Wrote to device: %s\n", msg);
} else {
fprintf(stderr, "Unknown operation: %s\n", argv[2]);
close(fd);
return 1;
}

close(fd);
return 0;
}

然后我们在rk3588上使用下面的指令进行编译:

1
gcc app.c -o app

我们编译出的app需要有两个参数,第一个参数是设备的绝对路径,第二个参数为对设备的操作,比如读或者写,因此我们可以使用下面的两条指令分别测试:

1
2
sudo ./app /dev/chrdev read
sudo ./app /dev/chrdev write

11

12

可以看到我们的用户态和内核态均有输出,并且数据传输正常,这样我们就完成了驱动框架的使用。

面向对象的设备驱动

我们都知道Linux内核可以说是C语言之祖,现代编程语言都有Linux内核的影子,我们在C内核开发时也可以体现出面向对象和私有属性的特点,因此我们在多设备驱动时可以实现一个,让多设备共用一套属性,并利用file结构体自带的private_data进行数据私有化。

首先介绍一个宏:

1
2
3
4
5
6
7
#define container_of(ptr, type, member) ({				\
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })

这个宏是从一个结构体成员指针ptr,结合数据类型反推出整个结构体的位置。三个参数分别为:

  • ptr:指向结构体成员的指针。
  • type:结构体的类型。
  • member:结构体中成员的名称。
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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>

// 设备结构体(做好封装)
struct adapt_device {
dev_t dev_number;
int major, minor;
struct cdev chr_dev;
struct class *cls;
struct device *dev;
char kbuf[32];
};
typedef struct adapt_device adapt_device_t;

// 全局设备实例
static adapt_device_t dev1, dev2;

// 文件操作函数
static int _cdev_open(struct inode *inode, struct file *f) {
f->private_data = container_of(inode->i_cdev, adapt_device_t, chr_dev);
printk(KERN_INFO "cdev_open: minor=%d\n", MINOR(inode->i_rdev));
return 0;
}

static ssize_t _cdev_write(struct file *f, const char __user *buf, size_t size, loff_t *off) {
adapt_device_t *dev = f->private_data;
if (copy_from_user(dev->kbuf, buf, size) != 0) {
printk(KERN_ERR "cdev_write: copy_from_user failed\n");
return -EFAULT;
}
printk(KERN_INFO "cdev_write: minor=%d, data=%s\n", dev->minor, dev->kbuf);
return size;
}

static ssize_t _cdev_read(struct file *f, char __user *buf, size_t size, loff_t *off) {
adapt_device_t *dev = f->private_data;
if (copy_to_user(buf, dev->kbuf, size) != 0) {
printk(KERN_ERR "cdev_read: copy_to_user failed\n");
return -EFAULT;
}
printk(KERN_INFO "cdev_read: minor=%d, data=%s\n", dev->minor, dev->kbuf);
return size;
}

static int _cdev_release(struct inode *inode, struct file *f) {
printk(KERN_INFO "cdev_release: minor=%d\n", MINOR(inode->i_rdev));
return 0;
}

// 文件操作结构体
static struct file_operations _cdev_fopts = {
.owner = THIS_MODULE,
.open = _cdev_open,
.write = _cdev_write,
.read = _cdev_read,
.release = _cdev_release,
};

// 模块初始化
static int __init _cdev_init(void) {
int ret;

// 分配设备号(主设备号相同,次设备号0和1)
ret = alloc_chrdev_region(&dev1.dev_number, 0, 2, "mul_chrdev");
if (ret < 0) {
printk(KERN_ERR "alloc_chrdev_region failed\n");
return ret;
}

// 初始化dev1
dev1.major = MAJOR(dev1.dev_number);
dev1.minor = MINOR(dev1.dev_number);
printk(KERN_ERR "Driver1 loaded with major=%d, minor=%d\n", dev1.major, dev1.minor);
dev1.chr_dev.owner = THIS_MODULE;
cdev_init(&dev1.chr_dev, &_cdev_fopts);
cdev_add(&dev1.chr_dev, dev1.dev_number, 1);
dev1.cls = class_create(THIS_MODULE, "mul_chrdev0");
dev1.dev = device_create(dev1.cls, NULL, dev1.dev_number, NULL, "mul_chrdev%d", dev1.minor);


// 初始化dev2(设备号+1, 仅改变次设备号)
dev2.major = MAJOR(dev2.dev_number + 1);
dev2.minor = MINOR(dev2.dev_number + 1);
printk(KERN_ERR "Driver2 loaded with major=%d, minor=%d\n", dev2.major, dev2.minor);
dev2.chr_dev.owner = THIS_MODULE;
cdev_init(&dev2.chr_dev, &_cdev_fopts);
cdev_add(&dev2.chr_dev, dev2.dev_number, 1);
dev2.cls = class_create(THIS_MODULE, "mul_chrdev1");
dev2.dev = device_create(dev2.cls, NULL, dev2.dev_number, NULL, "mul_chrdev%d", dev2.minor);


return 0;
}

// 模块退出
static void __exit _cdev_exit(void) {
// 销毁设备(先创建的后销毁)
device_destroy(dev1.cls, dev2.dev_number);
device_destroy(dev1.cls, dev1.dev_number);

// 删除cdev
cdev_del(&dev2.chr_dev);
cdev_del(&dev1.chr_dev);

// 销毁类和释放设备号(切记全部销毁)
class_destroy(dev1.cls);
class_destroy(dev2.cls);

unregister_chrdev_region(dev1.dev_number, 2);

printk(KERN_INFO "Driver unloaded\n");
}

module_init(_cdev_init);
module_exit(_cdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("Multi Character Device Driver");

这个私有化的过程如下::

  • 用户打开设备文件
  • 内核不知道打开的是哪个文件也没有相关参数传递,因此内核需要从用户传递的设备文件中获取保存的设备结构体adapt_device,并将整个结构体保存在传递的设备文件的私有空间内
  • 当读取时,只需要打开设备文件的私有空间,转化为adapt_device结构体指针,进行读取即可
  • 写入和释放同理

这样我们再实现一个app.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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
int fd1, fd2;
char buf1[32] = "Hi /dev/mul_chrdev0";
char buf2[32] = "Hi /dev/mul_chrdev1";
fd1 = open("/dev/mul_chrdev1", O_RDWR);
if (fd1 < 0) {
perror("Failed to open /dev/mul_chrdev0");
return 1;
}
write(fd1, buf1, sizeof(buf1));
close(fd1);

printf("=======================\n");


fd2 = open("/dev/mul_chrdev2", O_RDWR);
if (fd2 < 0) {
perror("Failed to open /dev/mul_chrdev1");
close(fd1);
return 1;
}
write(fd2, buf2, sizeof(buf2));

close(fd2);

return 0;
}

编译运行可以看到下面的内容:

13

截至目前,我们就以及完全学完字符设备的开发了。

寄存器控制GPIO

这部分我们进行GPIO控制,我们选择从寄存器开始,因此我们需要选择一个瑞芯微的数据手册名为:Rockchip RK3588 TRM 以及你开发板的原理图,首先我需要控制一个GPIO,因此我需要选择一个GPIO,在讯为的板子上仅开放了5个GPIO。

14

在讯为的板子上,这些接口分别接在下表中:

15

GPIO想要控制必须要有复用寄存器方向寄存器数据寄存器进行配置。为了与原教程进行区分,我们采用GPIO2_C3_d这个IO口,也就是I2S2_SDI_M0_BT。

复用寄存器

首先我们需要查询数据手册,找到BUS_IOC功能(总线IO控制器),在其中找到GPIO2C的控制地址。

16

我们在数据手册的通用寄存器栏目找到 BUS_IOC Register Description,就可以找到这个寄存器的地址,其中分为高低基地址,然后我们往下找,寻找到gpio2c3_sel这个描述:

17

我们可以看到此偏移地址为0x0054,对应在BUS_IOC_GPIO2C_IOMUX_SEL_H寄存器上,所以我们记这个偏移地址0x54。

在数据手册的第一张第一部分有一个Address Mapping,寻找BUS_IOC所处的地址:

18

因此我们的基地址为0xfd 5f 80 00,所以此复用寄存器的地址为:基地址+偏移地址=0xfd 5f 80 00 + 0x50= 0xfd 5f 80 50。到这里我们就找到了GPIO2_C3这个寄存器所在的位置,我们可以看到在描述这个寄存器的地址是[15:12],因此我们需要控制这几位的值选择对应的功能:

数值功能
0GPIO
1ETH0_REFCLKO_25M
2I2S2_SDI_M0
8SPI1_CS0_M0
9I2C6_SCL_M2

方向寄存器

首先我们需要直到瑞芯微的IO命名规则,对rk系列IO命名为GPIO控制器、端口号、索引号。其中控制器有4个,分别为GPIO1、GPIO2、GPIO3、GPIO4。端口号有四个分别为ABCD,索引号每个端口均有8个,分别为0~7.

打开GPIO部分的 Registers Summary,我们需要方向寄存器,其中端口控制器为我们的基地址,我们需要组合端口号和索引号获取方向寄存器的偏移地址,AB组对应的端口为低方向,CD组对应为高方向。因此我们需要GPIO_SWPORT_DDR_H。

19

然后我们查看GPIO_SWPORT_DDR_H的描述,高16位为写入,低16位为读取

20

其中位次对应位模式,[31:16]为WO模式,也就是仅写入,这里是写标志位,无法单独使用。低16为位为读写控制。如果低16位中某一位要设置输入输出的话,那么对应的高位也需要设置对应的功能。举个例子,我们要控制c3为输出模式,那么我们第三位就需要设置为1,同时高16位的第16+3位也需要设置为1。

然后我们需要找到GPIO2的基地址:

21

这样我们就得到GPIO2_的CD组地址为:0xfe c3 00 00 + 0x00 0c = 0xfe c3 00 0c,然后我们只需要操作第三位和第十九位就可以设置GPIO2_C3的模式。

数据寄存器

22

数据寄存器我们依然选用高位的,比如我们需要让gpio2_c3输出一个高电平,与方向寄存器相同只需要让高16复制低16的状态即可,如果我们要让gpio2_c3输出高电平只需要写入1即可,可以看下面的计算器截图:

23

让第3位和第19位为1即可,原因很简单

24

高16位是写标志位,只有对应的位置1才可以写入,或者说输出电平。反之如果输出低电平可以写:

25

最终我们可以总结一下:

复用寄存器地址:基地址0xfd 5f 80 00,偏移地址0x00 54,操作地址0xfd 5f 80 54

GPIO方向寄存器地址:基地址0xfe c3 00 00,偏移地址0x00 0c,操作地址为0xfe c3 00 0c

数据值:输出模式0x0008 0008,输入模式:0x0000 0000

GPIO数据寄存器地址:基地址0xfe c3 00 00,偏移地址0x00 04,操作地址为0xfe c3 00 04

数据值:高电平0x0008 0008,低电平:0x0008 0000

当GPIO的方向位于输入时,数据寄存器对应的低16位就是IO的电平,因此我们还可以实现读取电压的功能,注意内核的地址需要利用ioremap函数进行重映射到寄存器中,然后我们写一下代码就好:

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

// 数据寄存器的地址
#define GPIO_DR 0xFEC30004
// 方向寄存器地址
#define GPIO_DDR 0xFEC3000C

struct adapt_device {
dev_t dev_number;
int major, minor;
struct cdev chr_dev;
struct class *cls;
struct device *dev;
char kbuf[32];
unsigned int *gpio_dir;
};

typedef struct adapt_device adapt_device_t;

adapt_device_t adapt_dev;

// 文件操作函数
static int _cdev_open(struct inode *inode, struct file *f) {
f->private_data = container_of(inode->i_cdev, adapt_device_t, chr_dev);
printk(KERN_INFO "cdev_open: minor=%d\n", MINOR(inode->i_rdev));
return 0;
}

// 写IO
static ssize_t _cdev_write(struct file *f, const char __user *buf, size_t size, loff_t *off) {
adapt_device_t* dev = (adapt_device_t* )f->private_data;

// 设置方向寄存器为输出模式
*((unsigned int*) (ioremap(GPIO_DDR, 4))) = 0x80008;

if (copy_from_user(dev->kbuf, buf, size) != 0) {
printk(KERN_ERR "cdev_write: copy_from_user failed\n");
return -EFAULT;
}

// 如果值为1, 则让数据寄存器的地址置为高电平(0x0008 0008)
if (dev->kbuf[0] == 1) {
*(dev->gpio_dir) = 0x80008;
printk(KERN_ERR "bufer is %d\n", dev->kbuf[0]);
}
// 如果值为0, 则让数据寄存器的地址置为低电平
else if (dev->kbuf[0] == 0) {
*(dev->gpio_dir) = 0x80000;
printk(KERN_ERR "bufer is %d\n", dev->kbuf[0]);
}

return size;
}

// 读IO模式
static ssize_t _cdev_read(struct file *f, char __user *buf, size_t size, loff_t *off) {
adapt_device_t* dev = (adapt_device_t*)f->private_data;

// 设置方向寄存器为输入模式
*((unsigned int*) (ioremap(GPIO_DDR, 4))) = 0x00000000;

// C3的偏移为4, 所以应该为 1 << (4-1) = 0x8
dev->kbuf[0] = (*(dev->gpio_dir) >> 3) & 0x01;
if (copy_to_user(buf, dev->kbuf, size) != 0) {
printk(KERN_ERR "cdev_read: copy_to_user failed\n");
return -EFAULT;
}
printk(KERN_INFO "cdev_read: minor=%d, data=%s\n", dev->minor, dev->kbuf);
return size;
}

static int _cdev_release(struct inode *inode, struct file *f) {
printk(KERN_INFO "cdev_release: minor=%d\n", MINOR(inode->i_rdev));
return 0;
}

static struct file_operations adapt_fops = {
.owner = THIS_MODULE,
.open = _cdev_open,
.write = _cdev_write,
.read = _cdev_read,
.release = _cdev_release,
};

static int __init chr_fops_init(void) {
int ret;
ret = alloc_chrdev_region(&adapt_dev.dev_number, 0, 1, "adapt_gpio");
if (ret < 0) {
printk(KERN_ERR "chr_fops_init: register_chrdev_region failed\n");
return ret;
}

adapt_dev.chr_dev.owner = THIS_MODULE;
cdev_init(&adapt_dev.chr_dev, &adapt_fops);
ret = cdev_add(&adapt_dev.chr_dev, adapt_dev.dev_number, 1);
if (ret < 0) {
printk(KERN_ERR "chr_fops_init: cdev_add failed\n");
unregister_chrdev_region(adapt_dev.dev_number, 1);
return ret;
}

adapt_dev.cls = class_create(THIS_MODULE, "adapt_gpio");
if (IS_ERR(adapt_dev.cls)) {
printk(KERN_ERR "chr_fops_init: class_create failed\n");
cdev_del(&adapt_dev.chr_dev);
unregister_chrdev_region(adapt_dev.dev_number, 1);
return PTR_ERR(adapt_dev.cls);
}

adapt_dev.dev = device_create(adapt_dev.cls, NULL, adapt_dev.dev_number, NULL, "adapt_gpio");
if (IS_ERR(adapt_dev.dev)) {
printk(KERN_ERR "chr_fops_init: device_create failed\n");
class_destroy(adapt_dev.cls);
cdev_del(&adapt_dev.chr_dev);
unregister_chrdev_region(adapt_dev.dev_number, 1);
return PTR_ERR(adapt_dev.dev);
}

// 让结构体保存数据寄存器的值
adapt_dev.gpio_dir = ioremap(GPIO_DDR, 4);

if(IS_ERR(adapt_dev.gpio_dir)) {
printk(KERN_ERR "chr_fops_init: ioremap failed\n");
device_destroy(adapt_dev.cls, adapt_dev.dev_number);
class_destroy(adapt_dev.cls);
cdev_del(&adapt_dev.chr_dev);
unregister_chrdev_region(adapt_dev.dev_number, 1);
return PTR_ERR(adapt_dev.gpio_dir);
}

return 0;
}

static void __exit chr_fops_exit(void) {
*((unsigned int*) (ioremap(GPIO_DDR, 4))) = 0x00;
iounmap(adapt_dev.gpio_dir);
device_destroy(adapt_dev.cls, adapt_dev.dev_number);
class_destroy(adapt_dev.cls);
cdev_del(&adapt_dev.chr_dev);
unregister_chrdev_region(adapt_dev.dev_number, 1);
}

module_init(chr_fops_init);
module_exit(chr_fops_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Minloha");
MODULE_DESCRIPTION("GPIO Control Driver");

读写代码如例子所示,看起来非常的简单,然后我们写一个用户空间的IO读写功能用于测试:

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

int main(int argc, char *argv[]) {
int fd;
char buf[32] = {0};
fd = open("/dev/adapt_gpio", O_RDWR);
if (fd < 0) {
perror("Failed to open /dev/adapt_gpio");
return 1;
}
buf[0] = atoi(argv[1]);
write(fd, buf, sizeof(buf));

// buf清零
buf[0] = 0;
read(fd, buf, sizeof(buf));
printf("IO value: ", buf[0]);

close(fd);
return 0;
}

我们可以添加一个gpiorock的驱动用于验证我们的IO输出电平,使用:

1
2
3
4
// 查看IO的输出方向
cat /sys/class/gpio/gpio83/direction
// 查看IO的电压
cat /sys/class/gpio/gpio83/value

为什么是GPIO83?这里用到了一个全局编号的计算方法,即:全局编号=端口号×32+组号×8+位号。

我们的GPIO2_C3的端口号为2,组号为C(A=0, B=1, C=2, D=3),位号为3,计算一下就得到:

2×32+2×8+3=64+16+3=83

然后我们分别编译运行,查看一下输出值:

26

这样我们掌握了如何利用寄存器完成对外设的开发。

总结

本期博客难度较大,建议读者认真学,多操作。如果喜欢博客的话可以分享出去。

文档与资料

寄存器:https://gitee.com/flying-cat/rk3588-TRM-and-Datasheet


RK3588系列内核开发——字符设备与寄存器
https://blog.minloha.cn/posts/23120354fb6b072025081201.html
作者
Minloha
发布于
2025年8月12日
更新于
2025年8月13日
许可协议