RK3588 Linux实例应用(4)——KEY(设备介绍与设备树介绍)
KEY(字符设备)
- 一、前言
- 二、驱动的分类
- 三、按键字符设备驱动
- 3.1、设备的编写
- 3.1.1、创建驱动(驱动的加载和卸载)
- 3.1.2、设备号简介
- 3.1.3、创建设备号
- 3.1.4、创建自定义设备结构体
- 3.1.5、将系统字符设备结构体加入自定义设备结构体
- 3.1.6、自动创建设备节点
- 3.1.7、添加我们用得到的驱动操作函数
- 3.2、GPIO的编写(整个失败了,没达到预期效果,学习用,真正项目里直接设备树)
- 3.2.1、地址映射
- 3.2.2、I/O 内存访问
- 3.2.3、引脚复用设置
- 3.2.4、引脚驱动能力设置
- 3.2.5、GPIO 输入输出设置
- 3.2.6、GPIO 引脚高低电平设置
- 3.3、设备树的编写
- 3.3.1、设备树概念
- 3.3.2、DTS、DTB 和 DTC
- 3.3.3、DTS 语法
- 3.3.3.1、版本
- 3.3.3.2、注释
- 3.3.3.3、头文件
- 3.3.3.4、设备树节点
- 3.3.3.4.1、根节点
- 3.3.3.4.2、子节点
- 3.3.3.4.3、节点命名规则(适合根节点以外的节点,根节点只有`/`)
- 3.3.3.4.4、标签
- 3.3.3.4.5、别名(特殊节点,了解即可)
- 3.3.3.4.6、标签与别名
- 3.3.3.5、设备树属性
- 3.3.3.5.1、model属性(字符串)
- 3.3.3.5.2、compatible属性(字符串或字符串列表)
- 3.3.3.5.3、reg属性(地址,长度对)
- 3.3.3.5.4、#address-cells属性(整数)和#size-cells属性(整数)
- 3.3.3.5.5、status属性(字符串)
- 3.3.3.5.6、device_type属性(字符串)
- 3.3.3.5.7、自定义属性
一、前言
我们看了原子哥的系统开发手册,知道SDK是分层的,如下:
二、驱动的分类
Linux将驱动分为字符设备,网络设备,块设备三类。
- 字符设备指那些必须以串行顺序依次进行访问的设备,如鼠标。
- 块设备可以按照任意顺序进行访问,如硬盘。
- 网络设备是面向数据包的接收和发送。
接下来我们要做的按键就是字符设备。
三、按键字符设备驱动
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
我们要写一个按键,肯定不止写它的驱动,还要写它的应用程序。比如,当我们按键按下,我就打开LED灯。按键的驱动检测到按键按下的事件,将这个事件上报给应用程序,应用程序在调用LED的驱动,来点灯,这才是完整的流程。那么我们就要了解,应用程序如何对驱动程序进行调用。
Linux 应用程序对驱动程序的调用如图所示:
在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做/dev/key 的驱动文件,此文件是 key 按键的驱动文件。应用程序使用 open 函数来打开文件/dev/key,使用完成以后使用 close 函数关闭/dev/key 这个文件。open 和 close 就是打开和关闭 key 驱动的函数,如果要读取 key,那么就使用 read 函数来操作。同理 write 函数,也就是向驱动写入数据,如果是 led 驱动,这个数据就是要关闭还是要打开 led 的控制参数。
应用程序运行在用户空间,而 Linux 驱动属于内核的一部分,因此驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/key 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入”到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如下:
应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数。
那么就像我们操作单片机一样,我们的按键驱动有2种,一个是在应用程序种循环读取按键状态,判断到按键状态改变后在做相应处理(这种只作为学习了解用);另一种是采用中断的方式(这种才是工程中的正确方式)。
自己写驱动的时候可以通过之前装的VS查找其他驱动的相关函数调用,看看其他人写的驱动怎么调用这个函数的!!!
3.1、设备的编写
不管用什么方式实现我们的按键功能,都要先把设备写出来。
3.1.1、创建驱动(驱动的加载和卸载)
按照上一章讲解的,我们先搞一个简单的驱动,后面再给里面添加东西。
key.c
#include <linux/init.h>
#include <linux/module.h>
/* 驱动入口函数 */
static int __init key_init(void)
{
/* 入口函数具体内容 */
printk("key init ok! \r\n");
return 0;
}
/* 驱动出口函数 */
static void __exit key_exit(void)
{
/* 出口函数具体内容 */
printk("key init err \r\n");
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bjp");
MODULE_VERSION("V1.0");
3.1.2、设备号简介
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
typedef u32 __kernel_dev_t;
......
typedef __kernel_dev_t dev_t;
可以看出 dev_t 是 u32 类型的,也就是 unsigned int,所以 dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件 include/linux/kdev_t.h 中提供了几个关于设备号的操作函数(本质是宏),如下所示:
#define MINORBITS 20 //宏 MINORBITS 表示次设备号位数,一共是 20 位。
#define MINORMASK ((1U << MINORBITS) - 1) //宏 MINORMASK 表示次设备号掩码。
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) //宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) //宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
- 静态分配设备号
注册字符设备的时候需要给设备指定一个设备号,这个设备号可以是驱动开发者静态指定的一个设备号,比如 200 这个主设备号。有一些常用的设备号已经被 Linux 内核开发者给分配掉了,具体分配的内容可以查看文档 Documentation/devices.txt。并不是说内核开发者已经分配掉的主设备号我们就不能用了,具体能不能用还得看我们的硬件平台运行过程中有没有使用这个主设备号,使用“cat /proc/devices”命令即可查看当前系统中所有已经使用了的设备号。 - 动态分配设备号
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。怎么静态申请和动态申请在下面注册的时候有介绍。
3.1.3、创建设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
- major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两部分。
- name:设备名字,指向一串字符串。
- fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
字符设备驱动开发重点是使用 register_chrdev 函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev 函数注销字符设备,驱动模块加载成功以后还需要手动使用 mknod 命令创建设备节点(这里我们自己开发怎么可能还要手动去输入命令,明显不用这个方式了)。驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。
使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,这里就是静态分配设备号,但是这样会带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 KEY 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 KEY 一个设备分走了。这样太浪费次设备号了!一个 LED 设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是在使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。
如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。
注销字符设备之后要释放掉设备号,不管是通过 alloc_chrdev_region 函数还是 register_chrdev_region 函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
动态申请设备号如下:
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); /* 获取分配号的次设备号 */
}
3.1.4、创建自定义设备结构体
描述一个设备,我们用设备结构体表示
/* newchrkey设备结构体*/
struct newchrkey_dev{
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
struct newchrkey_dev newchrkey;
到了这里我们把字符设备的框架就搭好了,如下(GPIO、具体驱动操作都还没填充,后面填充):
key.c
#include <linux/init.h>
#include <linux/module.h>
/* newchrkey设备结构体*/
struct newchrkey_dev{
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
struct newchrkey_dev newchrkey;
/* 驱动入口函数 */
static int __init key_init(void)
{
int ret = 0;
/* 2、申请设备号 */
if (newchrkey.major)
{ /* 定义了主设备号 */
newchrkey.devid = MKDEV(newchrkey.major, 0); /* 大部分驱动次设备号都选择 0 */
ret = register_chrdev_region(newchrkey.devid, 1, "test");
}
else
{ /* 没有定义设备号 */
ret = alloc_chrdev_region(&newchrkey.devid, 0, 1, "test"); /* 申请设备号 */
newchrkey.major = MAJOR(newchrkey.devid); /* 获取分配号的主设备号 */
newchrkey.minor = MINOR(newchrkey.devid); /* 获取分配号的次设备号 */
}
if(ret < 0)
{
printk("newchrkey register err \r\n");
return -1;
}
printk("newchrkey major: %d, minor: %d\r\n", newchrkey.major, newchrkey.minor);
return 0;
}
/* 驱动出口函数 */
static void __exit key_exit(void)
{
/* 出口函数具体内容 */
unregister_chrdev_region(newchrkey.devid, 1);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bjp");
MODULE_VERSION("V1.0");
3.1.5、将系统字符设备结构体加入自定义设备结构体
到这里,我们的设备还没有描述完,因为我们只是自定义了一个字符设备描述结构体,在 Linux 中有它自己的描述结构体,我们还要初始化它,并把它放到我们自定义的结构体里面。在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 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;
} __randomize_layout;
在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合 file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备,我们把他加到自定义的设备结构体。
/* newchrkey设备结构体*/
struct newchrkey_dev{
struct cdev cdev; /* 字符设备,它的变量名居然可以和结构体重名 */
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
之后使用 cdev_init 函数对其进行初始化
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。file_operations后面讲。
初始化完成后,在添加设备,cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
那我们的代码 key.c 现在如下:
#include <linux/init.h>
#include <linux/module.h>
/* newchrkey设备结构体*/
struct newchrkey_dev{
struct cdev cdev; /* 字符设备,它的变量名居然可以和结构体重名 */
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
struct newchrkey_dev newchrkey;
/* 设备操作函数 */
static struct file_operations newchrkey_fops =
{
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
/* 驱动入口函数 */
static int __init key_init(void)
{
int ret = 0;
/* 2、申请设备号 */
if (newchrkey.major)
{ /* 定义了主设备号 */
newchrkey.devid = MKDEV(newchrkey.major, 0); /* 大部分驱动次设备号都选择 0 */
ret = register_chrdev_region(newchrkey.devid, 1, "test");
}
else
{ /* 没有定义设备号 */
ret = alloc_chrdev_region(&newchrkey.devid, 0, 1, "test"); /* 申请设备号 */
newchrkey.major = MAJOR(newchrkey.devid); /* 获取分配号的主设备号 */
newchrkey.minor = MINOR(newchrkey.devid); /* 获取分配号的次设备号 */
}
if(ret < 0)
{
printk("newchrkey register err \r\n");
return -1;
}
printk("newchrkey major: %d, minor: %d\r\n", newchrkey.major, newchrkey.minor);
/* 3、字符设备注册 */
newchrkey.cdev.owner = THIS_MODULE;
cdev_init(&newchrkey.cdev, &newchrkey_fops);
ret = cdev_add(&newchrkey.cdev, newchrkey.devid, 1);
return 0;
}
/* 驱动出口函数 */
static void __exit key_exit(void)
{
/* 1、删除字符设备 */
cdev_del(&newchrkey.cdev);
/* 2、注销设备号 */
unregister_chrdev_region(newchrkey.devid, 1);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bjp");
MODULE_VERSION("V1.0");
3.1.6、自动创建设备节点
设备结构体搞完了。前面说使用静态设备号的时候,注册设备的时候要手动创建设备节点,这里在代码中改成自动创建。
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。首先要创建一个 class 类,class 是个结构体,定义在文件include/linux/class.h 里面。class_create 是类创建函数,class_create 是个宏定义,内容如下:
extern struct class * __must_check __class_create(struct module
*owner,const char *name,
struct lock_class_key *key);
extern void class_destroy(struct class *cls);
/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#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)
创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
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 这个设备文件。返回值就是创建好的设备。
那我们的代码 key.c 现在如下:
#include <linux/init.h>
#include <linux/module.h>
/* newchrkey设备结构体*/
struct newchrkey_dev{
struct cdev cdev; /* 字符设备,它的变量名居然可以和结构体重名 */
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
struct newchrkey_dev newchrkey;
/* 设备操作函数 */
static struct file_operations newchrkey_fops =
{
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
/* 驱动入口函数 */
static int __init key_init(void)
{
int ret = 0;
/* 2、申请设备号 */
if (newchrkey.major)
{ /* 定义了主设备号 */
newchrkey.devid = MKDEV(newchrkey.major, 0); /* 大部分驱动次设备号都选择 0 */
ret = register_chrdev_region(newchrkey.devid, 1, "test");
if(ret < 0)
{
pr_err("cannot register %s char driver [ret=%d]\n","test", 1);
goto fail_map;
}
}
else
{ /* 没有定义设备号 */
ret = alloc_chrdev_region(&newchrkey.devid, 0, 1, "test"); /* 申请设备号 */
if(ret < 0)
{
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n","test", ret);
goto fail_map;
}
newchrkey.major = MAJOR(newchrkey.devid); /* 获取分配号的主设备号 */
newchrkey.minor = MINOR(newchrkey.devid); /* 获取分配号的次设备号 */
}
printk("newchrkey major: %d, minor: %d\r\n", newchrkey.major, newchrkey.minor);
/* 3、字符设备注册 */
newchrkey.cdev.owner = THIS_MODULE;
cdev_init(&newchrkey.cdev, &newchrkey_fops);
ret = cdev_add(&newchrkey.cdev, newchrkey.devid, 1);
if(ret < 0)
{
goto del_unregister;
}
/* 4、自动创建设备节点 */
newchrkey.class = class_create(THIS_MODULE, "test");
if(IS_ERR(newchrkey.class)){
goto del_cdev;
}
newchrkey.device = device_create(newchrkey.class, NULL, newchrkey.devid, NULL, "test");
if(IS_ERR(newchrkey.device))
{
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(newchrkey.class);
del_cdev:
cdev_del(&newchrkey.cdev);
del_unregister:
unregister_chrdev_region(newchrkey.devid, 1);
return -EIO;
}
/* 驱动出口函数 */
static void __exit key_exit(void)
{
/* 1、删除设备 */
device_destroy(newchrkey.class, newchrkey.devid);
/* 2、删除类 */
class_destroy(newchrkey.class);
/* 3、删除字符设备 */
cdev_del(&newchrkey.cdev);
/* 4、注销设备号 */
unregister_chrdev_region(newchrkey.devid, 1);
}
module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bjp");
MODULE_VERSION("V1.0");
3.1.7、添加我们用得到的驱动操作函数
在 Linux 内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合,内容如下所示:
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;
我们这只要写4个,读写打开关闭。
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrkey; /* 设置私有数据 */
return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations newchrkey_fops =
{
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取 private_data 即可得到设备结构体。
这里没法完善这些函数,因为我们还没加GPIO,这个也要加上,才能完善这些函数。
3.2、GPIO的编写(整个失败了,没达到预期效果,学习用,真正项目里直接设备树)
Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。在 Linux 下编写驱动要符合 Linux 的驱动框架。
3.2.1、地址映射
我们看他的原理图,先找一个按键的IO出来,正点原子的 ATK-DLRK3588B 开发板上虽然有 5 个按键,但是这 5 个按键是 ADC 方式驱动的,所以没法用这 5 个按键来做 GPIO 输入实验。但是开发板上 JP5 这个双排排针引出了 22 个 IO,我们使用 GPIO0_C6 这个引出的 IO 来完成 GPIO 输入驱动程序。
默认情况下 GPIO0_C6 是低电平,所以我们通过使用杜邦线将图中 GPIO0_C6 这个引脚接到 VCC 上的方式来模拟按键按下。也就是模拟按键按下,GPIO0_C6 为高电平,松开按键为低电平。
在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无 MMU 的处理器了。MMU 主要完成的功能如下:
①、完成虚拟空间到物理空间的映射。
②、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
我们重点来看一下第①点,也就是虚拟空间到物理空间的映射,也叫做地址映射。首先了解两个地址概念:虚拟地址(VA,Virtual Address)、物理地址(PA,Physcical Address)。对于 64 位的处理器来说,虚拟地址范围是 2^64=16EB(1EB=1024PB=1024*1024TB)。虚拟地址范围比物理地址范围大的问题处理器自会处理,这里我们不要去深究,因为 MMU 是很复杂的一个东西。
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。(打开《Rockchip RK3588 TRM V1.0-Part1-20220309(RK3588 参考手册 1).pdf》)
RK3588 的 GPIO0_C6 引脚的 IO 复用寄存器 PMU2_IOC_GPIO0C_IOMUX_SEL_H 物理地址为 PMU2_IOC(0xFD5F4000) + offset(0x0008) =0xFD5F4008。
如果没有开启 MMU 的话直接向 0xFD5F4008这个寄存器地址写入数据就可以配置 GPIO0_C6 的引脚的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0xFD5F4008 这个地址写入数据了。我们必须得到 0xFD5F4008 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。(改,地址没改)
这里照搬原子哥的,但是自己设计的时候原理图和寄存器手册一定要会看,后面会讲。
ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在 arch/arm/include/asm/io.h 文件中,定义如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
函数的实现是在 arch/arm/mm/ioremap.c 文件中,实现如下:
void __iomem *ioremap(resource_size_t res_cookie, size_t size)
{
return arch_ioremap_caller(res_cookie, size, MT_DEVICE,
__builtin_return_address(0));
}
EXPORT_SYMBOL(ioremap);
ioremap 有两个参数:res_cookie 和 size,真正起作用的是函数 arch_ioremap_caller。 ioremap 函数有两个参数和一个返回值,这些参数和返回值的含义如下:
res_cookie:要映射的物理起始地址。
size:要映射的内存空间大小。
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
假如我们要获取 RK3588 的 PMU2_IOC_GPIO0C_IOMUX_SEL_H 寄存器对应的虚拟地址,使用如下代码即可:
#define PMU2_IOC_GPIO0C_IOMUX_SEL_H (0xFD5F4008)
static void __iomem* PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI;
PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI = ioremap(PMU2_IOC_GPIO0C_IOMUX_SEL_H, 4);
宏 PMU2_IOC_GPIO0C_IOMUX_SEL_H 是寄存器物理地址,PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI 是映射后的虚拟地址。对于 RK3588 来说一个寄存器是 4 字节(32 位),因此映射的内存长度为 4。映射完成以后直接对 PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI 进行读写操作即可。
这里稍微看下手册就知道他的寄存器是4(32:0)字节。可以看下:
卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:
void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。假如我们现在要取消掉 PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI 寄存器的地址映射,使用如下代码即可:
iounmap(PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI);
3.2.2、I/O 内存访问
使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
读操作函数有如下几个:
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)
readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。
写操作函数有如下几个:
void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)
writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要写入的数值,addr 是要写入的地址。
现在操作介绍完了,我们要将IO驱动起来,需要配置具体的寄存器了。总共4个寄存器需要配置。
3.2.3、引脚复用设置
RK3588 的一个引脚一般用多个功能,也就是引脚复用,比如 GPIO0_C6 这个 IO 就可以用作:GPIO,I2S1_SDI1_M1、NPU_AVS、UART0_RTSN、SPI0_CLK_M0 这5个功能,所以我们首先要设置好当前引脚用作什么功能,这里我们要使用GPIO0_C6 的 GPIO 功能。
打开《Rockchip RK3588 TRM V1.0-Part1-20220309(RK3588 参考手册 1).pdf》这份文档,找到PMU2_IOC_GPIO0C_IOMUX_SEL_H 这个寄存器,寄存器描述如下图所示:
可以看出 PMU2_IOC_GPIO0C_IOMUX_SEL_H 寄存器地址为:base+offset,其中 base 就是 PMU2_IOC 外设的基地址,为0xFD5F4000,offset 为 0x0008,所以 PMU2_IOC_GPIO1A_IOMUX_SEL_L 寄存器地址为 0xFD5F4000+ 0x0008 = 0xFD5F4008。
PMU2_IOC_GPIO0C_IOMUX_SEL_H 寄存器分为 2 部分:
① 、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16 就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写操作。
② 、bit15:0:功能设置位。
可以看出,PMU2_IOC_GPIO0C_IOMUX_SEL_H 寄存器用于设置 GPIO0_C4~C7 这 4 个 IO 的复用功能,其中 bit11:8 用于设置 GPIO0_C6 的复用功能,有六个可选功能:
4’h0: GPIO
4’h1: I2S1_SDI1_M1
4’h2: NPU_AVS
4’h4: UART0_RTSN
4’h8: Refer to BUS_IOC.GPIO0C_IOMUX_SEL_H
我们要将 GPIO0_C6 设置为 GPIO,所以 PMU2_IOC_GPIO0C_IOMUX_SEL_H 的 bit11:8 这四位设置 0000。另外 bit27:24 要设置为 1111,允许写 bit11:8。
3.2.4、引脚驱动能力设置
RK3588 的 IO 引脚可以设置不同的驱动能力,GPIO0_C6 的驱动能力设置寄存器为 PMU2_IOC_GPIO0C_DS_H,寄存器结构如下图所示:
PMU2_IOC_GPIO0C_DS_H 寄存器地址为:PMU2_IOC(0xFD5F4000)
base+offset=0xFD5F4000 + 0x001C = 0xFD5F401C。
PMU2_IOC_GPIO0C_DS_H 寄存器也分为 2 部分:
① 、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16 就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写操作。
② 、bit15:0:功能设置位。
可以看出,PMU2_IOC_GPIO0C_DS_H 寄存器用于设置 GPIO0_C4~C7 这 4 个 IO 的驱动能力,其中 bit10:8 用于设置 GPIO0_C6 的驱动能力,一共有 6 级。这里我们将 GPIO0_C6 的驱动能力设置为 40ohm,所以 PMU2_IOC_GPIO0C_DS_H 的 bit10:8 这三位设置 110。另外 bit25:23 要设置为 111,允许写 bit10:8。
3.2.5、GPIO 输入输出设置
GPIO 是双向的,也就是既可以做输入,也可以做输出。GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 这两个寄存器用于设置 GPIO 的输入输出功能。
RK3588 一共有 GPIO0、GPIO1、GPIO2、GPIO3 和 GPIO4 这五组 GPIO。其中 GPIO0 ~ 3 这四组每组都有 A0 ~ A7、B0 ~ B7、C0 ~ C7 和D0~D7 这 32 个 GPIO。
每个 GPIO 需要一个 bit 来设置其输入输出功能,一组 GPIO 就需要32bit,GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 这两个寄存器就是用来设置这一组 GPIO 所有引脚的输入输出功能的。
其中 GPIO_SWPORT_DDR_L 设置的是低 16bit,GPIO_SWPORT_DDR_H 设置的是高 16bit。一组 GPIO 里面这 32 给引脚对应的 bit 如下表所示:
GPIO0_C6 很明显要用到 GPIO_SWPORT_DDR_H 寄存器,寄存器描述如下图所示:
GPIO_SWPORT_DDR_L 寄存器地址也是 base+offset,其中 GPIO0~GPIO4 的基地址如下表所示:
所以 GPIO0_C6 对应的 GPIO_SWPORT_DDR_H 基地址就是 0xFD8A0000+0X000C=0xFD8A000C。
GPIO_SWPORT_DDR_H 寄存器也分为 2 部分:
①、bit31:16:低 16 位写使能位,这 16 个 bit 控制着寄存器的低 16 位写使能。比如 bit16 就对应着 bit0 的写使能,如要要写 bit0,那么 bit16 要置 1,也就是允许对 bit0 进行写操作。
③ 、bit15:0:功能设置位。
这里我们将 GPIO0_C6 设置为输入,所以 GPIO_SWPORT_DDR_H 的 bit6 要置 1,另外 bit22 要设置为 1,允许写 bit6。
3.2.6、GPIO 引脚高低电平设置
我这里是输入,不是输出,但是不妨碍这里讲一下。
GPIO 配置好以后就可以控制引脚输出高低电平了,需要用到 GPIO_SWPORT_DR_L 和 GPIO_SWPORT_DR_H 这两个寄存器,这两个原理和上面讲的 GPIO_SWPORT_DDR_L 和 GPIO_SWPORT_DDR_H 一样,这里就不再赘述了。
GPIO0_C6 需要用到 GPIO_SWAPORT_DR_H 寄存器,寄存器描述如下图所示:
同样的,GPIO0_C6 对应 bit6,如果要输出低电平,那么 bit6 置 0,如果要输出高电平,bit6 置 1。bit22 也要置 1,允许写 bit6。
关于 RK3588 的 GPIO 配置原理就讲到这里。那我们整体的代码就出来了。
key.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define PMU2_BASE (0xFD5F4000)
#define PMU2_IOC_GPIO0C_IOMUX_SEL_H (PMU2_BASE +0x0008)
#define PMU2_IOC_GPIO0C_DS_H (PMU2_BASE + 0x001C)
#define GPIO0_BASE (0xFD8A0000)
#define GPIO_SWPORT_DR_H (GPIO0_BASE + 0X0004)
#define GPIO_SWPORT_DDR_H (GPIO0_BASE + 0X000C)
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI;
static void __iomem *PMU2_IOC_GPIO0C_DS_H_PI;
static void __iomem *GPIO_SWPORT_DR_H_PI;
static void __iomem *GPIO_SWPORT_DDR_H_PI;
u32 keystatus = 0;
/* newchrkey设备结构体*/
struct newchrkey_dev{
struct cdev cdev; /* 字符设备,它的变量名居然可以和结构体重名 */
struct class *class; /* 类 */
struct device *device; /* 设备 */
dev_t devid; /* 设备号 */
int major; /* 主设备 */
int minor; /* 次设备号 */
};
struct newchrkey_dev newchrkey;
void key_remap(void)
{
PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI = ioremap(PMU2_IOC_GPIO0C_IOMUX_SEL_H, 4);
PMU2_IOC_GPIO0C_DS_H_PI = ioremap(PMU2_IOC_GPIO0C_DS_H, 4);
GPIO_SWPORT_DR_H_PI = ioremap(GPIO_SWPORT_DR_H, 4);
GPIO_SWPORT_DDR_H_PI = ioremap(GPIO_SWPORT_DDR_H, 4);
}
void key_unmap(void)
{
/* 取消映射 */
iounmap(PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI);
iounmap(PMU2_IOC_GPIO0C_DS_H_PI);
iounmap(GPIO_SWPORT_DR_H_PI);
iounmap(GPIO_SWPORT_DDR_H_PI);
}
/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrkey; /* 设置私有数据 */
return 0;
}
/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
u32 val = 0;
val = readl(GPIO_SWPORT_DR_H_PI);
if(val != keystatus)
{
keystatus = val;
printk("change!\r\n");
}
return 0;
}
/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations newchrkey_fops =
{
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
/* 驱动入口函数 */
static int __init key0C6_init(void)
{
int ret = 0;
u32 val = 0;
/* 1、寄存器地址映射 */
key_remap();
/* 2、设置 GPIO0_C6 为 GPIO 功能。*/
val = readl(PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI);
val &= ~(0X0F00 << 0); /* bit11:8,清零 */
val |= ((0X0F00 << 16) | (0X0 << 0)); /* bit27:24 置 1,允许写 bit11:8,bit11:8:0,用作 GPIO0_C6 */
writel(val, PMU2_IOC_GPIO0C_IOMUX_SEL_H_PI);
/* 3、设置 GPIO0_C6 驱动能力为 40ohm */
val = readl(PMU2_IOC_GPIO0C_DS_H_PI);
val &= ~(0X0700 << 0); /* bit10:8 清零*/
val |= ((0X0700 << 16) | (0X0600 << 0)); /* bit26:24 置 1,允许写 bit10:8,bit10:8:110,用作 GPIO0_C6 */
writel(val, PMU2_IOC_GPIO0C_DS_H_PI);
/* 4、设置 GPIO0_C6 为输入 */
val = readl(GPIO_SWPORT_DDR_H_PI);
val &= ~(0X40 << 0); /* bit6 清零*/
val |= ((0X40 << 16) | (0X0 << 0)); /* bit19 置 1,允许写 bit3,bit3,高电平 */
writel(val, GPIO_SWPORT_DDR_H_PI);
/* 5、设置 GPIO0_C6 高低电平,我设置输入了,这里就不举例了*/
/* 6、申请设备号 */
if (newchrkey.major)
{ /* 定义了主设备号 */
newchrkey.devid = MKDEV(newchrkey.major, 0); /* 大部分驱动次设备号都选择 0 */
ret = register_chrdev_region(newchrkey.devid, 1, "test");
if(ret < 0)
{
pr_err("cannot register %s char driver [ret=%d]\n","test", 1);
goto fail_map;
}
}
else
{ /* 没有定义设备号 */
ret = alloc_chrdev_region(&newchrkey.devid, 0, 1, "test"); /* 申请设备号 */
if(ret < 0)
{
pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n","test", ret);
goto fail_map;
}
newchrkey.major = MAJOR(newchrkey.devid); /* 获取分配号的主设备号 */
newchrkey.minor = MINOR(newchrkey.devid); /* 获取分配号的次设备号 */
}
printk("newchrkey major: %d, minor: %d\r\n", newchrkey.major, newchrkey.minor);
/* 7、字符设备注册 */
newchrkey.cdev.owner = THIS_MODULE;
cdev_init(&newchrkey.cdev, &newchrkey_fops);
ret = cdev_add(&newchrkey.cdev, newchrkey.devid, 1);
if(ret < 0)
{
goto del_unregister;
}
/* 8、自动创建设备节点 */
newchrkey.class = class_create(THIS_MODULE, "test");
if(IS_ERR(newchrkey.class)){
goto del_cdev;
}
newchrkey.device = device_create(newchrkey.class, NULL, newchrkey.devid, NULL, "test");
if(IS_ERR(newchrkey.device))
{
goto destroy_class;
}
return 0;
destroy_class:
class_destroy(newchrkey.class);
del_cdev:
cdev_del(&newchrkey.cdev);
del_unregister:
unregister_chrdev_region(newchrkey.devid, 1);
fail_map:
key_unmap();
return -EIO;
}
/* 驱动出口函数 */
static void __exit key0C6_exit(void)
{
/* 取消映射 */
key_unmap();
/* 1、删除设备 */
device_destroy(newchrkey.class, newchrkey.devid);
/* 2、删除类 */
class_destroy(newchrkey.class);
/* 3、删除字符设备 */
cdev_del(&newchrkey.cdev);
/* 4、注销设备号 */
unregister_chrdev_region(newchrkey.devid, 1);
}
module_init(key0C6_init);
module_exit(key0C6_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bjp");
MODULE_VERSION("V1.0");
然后搞一个APP程序
makefile
export CROSS_COMPILE=/home/bjp/rk3588/sdk/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
KERNELDIR := /home/bjp/rk3588/sdk/kernel
CURRENT_PATH := $(shell pwd)
obj-m := key.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
keyApp.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开 led 驱动 */
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
while(1)
{
read(fd, databuf, sizeof(databuf));
}
retvalue = close(fd); /* 关闭文件 */
if(retvalue < 0)
{
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
编译验证一下
key.ko
make ARCH=arm64 //ARCH=arm64 必须指定,否则编译会失败
编译测试 APP,keyApp.c
/opt/atk-dlrk3588-toolchain/bin/aarch64-buildroot-linux-gnu-gcc keyApp.c -o keyApp
传输模块(注意,如果你这个IO被占用,要先把之前的禁用了,重编kernel,烧录kernel)
adb push key.ko /lib/modules/5.10.160
adb push keyApp /lib/modules/5.10.160
加载
depmod //第一次加载驱动的时候需要运行此命令
modprobe key //加载驱动
3.3、设备树的编写
3.3.1、设备树概念
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、 内存基地址、IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等,如下图所示:
在上图中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按照图中所示的结构来描述板子上的设备信息,DTS 文件描述设备信息是有相应的语法规则要求的。
3.3.2、DTS、DTB 和 DTC
DTS 是设备树源码文件
DTB是将 DTS 编译以后得到的二进制文件
将.c文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下
- 设备树(Device Tree) :一种用于描述硬件设备和资源连接的数据结构,是一种中立、可移植的设备描述方法。
- DTS (设备树源文件 Device Tree Source) :设备树的源码文件,可以理解成c语言的.c文件。
- DTSI (设备树包含文件 Device Tree Include) :包含了设备树源文件中的可重复使用的部分,可以通过#include指令在其他设备树源文件中引用。通常用于共享公共的设备树定义和配置,可以理解成c语言的.h文件。
- DTC (设备树编译器Device Tree Compiler) :用于将设备树源文件(DTS)编译成设备树二进制文件(DTB)的工具,可以理解成c语言的gcc编译器。
- DTB (设备树二进制文件Device Tree Blob) :设备树源文件编译生成的二进制文件,可以理解成c语言的.hex或者bin。
- 节点(Node) :在设备树中用来描述硬件设备或资源的一个独立部分。每个节点都有一个唯一的路径和一组属性。
- 属性(Property) :用于描述节点的特征和配置信息,包括设备的名称、地址、中断号、寄存器配置等。
- 属性值(Property Value) :属性中的具体数据,可以是整数、字符串、布尔值等各种类型。
- 父节点和子节点(Parent Node and Child Node) :在设备树中,每个节点都可以有一个父节点和多个子节点,用于描述设备之间的连接关系。
3.3.3、DTS 语法
虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的 .dts 文件上进行修改。但是 DTS 文件语法我们还是需要详细的学习一遍,因为后续工作中肯定需要修改 .dts 文件。
3.3.3.1、版本
在设备树文件中通过以下代码来指定版本
/dts-v1/;
一般写在dts的第一行,这个声明非常重要的,因为他描述了了设备树文件所使用的语法和规范版本。如果没有这个声明,设备树编译器可能会无法正确解释文件中的语法,导致编译错误或者设备树在加载时出现问题。
3.3.3.2、注释
和C语言一样,2种注释方式
/*注释*/
//注释
3.3.3.3、头文件
- 设备树方式包含头文件
/include/ "xxxx.dtsi" //xxxx是你要包含的文件名称
- C语言包含头文件
#include "rk3588.dtsi"
#include <dt-bindings/phy/phy-snps-pcie3.h>
3.3.3.4、设备树节点
3.3.3.4.1、根节点
设备树的根节点是设备树的顶层节点,也是整个设备树的入口点,根节点的名字是 /
/dts-v1/; //这里指定设备树的版本V1
/ { //根节点开始
/*
根节点的内容,可以是一些全局属性,
也可以是一些子节点
*/
}; //根节点结束
3.3.3.4.2、子节点
子节点通常有以下几个元素组成
- 节点名和可选的单元地址:节点名通常是相关硬件设备或功能的名称,可选的单元地址表示设备的特定实例或资源,如内存地址设备ID等。
- 花括号:用于封装节点的属性和子节点内容。
- 属性:属性有名称和值,具体值可以是数字、字符串、数组等。属性和值用
=
相连。 - 子节点定义:一个节点可以包含多个子节点,这些子节点又可以进一步定义更详细的属性或包含他们自己的子节点,从而创建了一个结构层次。
标签: 节点名[@单元地址]{ //标签: 和 @单元地址 不是必须,标签不是名字,冒号后面的都算名字
子节点名1{
子节点1的子节点{
};
};
子节点名2{
};
};
3.3.3.4.3、节点命名规则(适合根节点以外的节点,根节点只有/
)
- 命名易懂,见名知意
/dts-v1/; //这里指定设备树的版本V1
/ {
//串口设备
serial{
};
//I2C控制器
i2c{
};
//usb控制器
usb{
};
};
- 起名用小写字母,下划线或连字符
节点名通常全是小写字母,多个单词,用下划线(_)或连字符(-)来分割单词。
/dts-v1/; //这里指定设备树的版本V1
/ {
serial_port{//看这个就知道是端口
};
gpio-controller{//看这个就知道是gpio控制器
};
};
- 遵守已有约定
如果要描述的硬件信息是现有描述过的,就尽量不要自己命名,可以去设备树文件里面复制官方的
//绑定文档
SDK/kernel/Documentation/devicetree/bindings
//64位设备树
SDK/linux/kernel/arch/arm64/boot/dts
//32位设备树
SDK/linux/kernel/arch/arm/boot/dts
- 避免特殊字符
名称中避免使用空格、点(.
)、斜杠(/
)、反斜杠(\
)等特殊字符。
错误示例:
/dts-v1/; //这里指定设备树的版本V1
/ {
node1\1{ //这里有个特殊字符
};
node2{
};
};
- 唯一性
设备树同一级别层次,节点名称唯一。
/dts-v1/; //这里指定设备树的版本V1
/ {
node1{ //节点名相同
child_node{
};
};
node1{ //节点名相同
};
};
//修改后
/ {
node1{
child_node{//节点名相同
};
};
node2{
child_node{//节点名相同
};
};
};
- 地址和类型
节点名称中可以包含节点所代表的硬件地址信息和类型。例如,i2c@1c2c0000指的是位于1c2c0000位置的 I2C 控制器。注意:这个并不是实际寄存器只是拿来看的增加可读性和避免命名冲突的,实际的地址我们后面属性会讲reg属性才是实际描述的寄存器地址。
/dts-v1/;
/ {
// 串口设备示例,地址不同
serial@80000000 {
};
// 串口设备示例,地址不同
serial@90000000 {
};
// I2C 控制器及设备示例
i2c@91000000 {
};
// USB 控制器示例
usb@92000000 {
};
};
3.3.3.4.4、标签
上面子节点格式中我们还提到了标签,标签在节点名中不是必须的,但是我们可以通过他来更方便的操作节点,在设备树文件中有大量使用到。下面例子中定义了标签,并通过引用 uart1 标签方式往 serial@80000000 中追加一个 node_add2 节点。
创建一个名为device_tree.dts的文件并填入以下内容
/dts-v1/;
/ {
// 串口设备示例,地址不同,uart1是标签
uart1: serial@80000000 {
node_add1{
};
};
// 串口设备示例,地址不同,uart2是标签
uart2: serial@90000000 {
};
// I2C 控制器及设备示例,i2c1是标签
i2c1: i2c@91000000 {
};
// USB 控制器示例,USB是标签
usb1: usb@92000000 {
};
};
&uart1{ // 通过引用标签的方式往 serial@80000000 中追加一个节点非覆盖。
node_add2{
};
};
编译:
dtc -I dts -O dtb -o device_tree.dtb device_tree.dts
反编译:
dtc -I dtb -O dts -o backdevice_tree.dts device_tree.dtb
查看:
cat backdevice_tree.dts
3.3.3.4.5、别名(特殊节点,了解即可)
aliases是一种在设备树中提供简化标识符的方式。它们主要用来为复杂的节点提供一个简单的别名,目的是为了方便引用,和标签有异曲同工之妙,但他们的作用是用途都不同,标签是针对特定设备节点的命名,而别名是针对设备节点路径的命名。
//通过aliases来定义别名
aliases{
//这里面描述的都是别名
[别名]=[标签]
[别名]=[节点路径]
};
/dts-v1/;
/ {
// 别名定义
aliases {
uart1 = &uart1; // uart1 别名指向名为 uart1 的设备节点
uart2 = &uart2; // uart2 别名指向名为 uart2 的设备节点
uart3 = "/serial@10000000"; // uart3 别名指向路径为 /serial@10000000 的设备节点,这里的/表示根目录
};
// 串口设备示例,地址不同,uart1 是标签
uart1: serial@80000000 {
// 可在此处添加串口设备的配置信息
};
// 串口设备示例,地址不同,uart2 是标签
uart2: serial@90000000 {
// 可在此处添加串口设备的配置信息
};
// 串口设备示例,地址不同,uart3 是标签,通过路径方式定义
serial@10000000 {
// 可在此处添加串口设备的配置信息
};
};
3.3.3.4.6、标签与别名
个人的理解标签是为了在设备树中使用舒服的,有了标签我们访问就不需要访问全名了,别名是为了在内核源码中使用舒服有了别名在内核中查找设备树就不必写完整路径直接写别名就行。
全路径查找设备树:
grep -r -n "of_find_node_by_path" ./
别名查找设备树:
grep -r -n "of_alias_get_id" ./
3.3.3.5、设备树属性
属性是键值对,定义了节点的硬件相关参数,属性有很多种我们下面只讲常用的标准属性,其他属性大家用到的时候再查。属性有对应的值,其中值的类型也有好几种,各种属性我们等会一一列举,我们先把属性能填哪些值搞明白。在设备树中,属性的值类型可以有多种,这些类型通常用于描述设备或子系统的各种特性和配置需求。
- 字符串(String):
属性名称:compatible
示例值:compatible = "lckfb,tspi-v10", "rockchip,rk3566";
描述:指定该设备或节点与哪些设备或驱动兼容。 - 整数(Integer):
属性名称:reg
示例值:reg = <0x1000>;
描述:定义设备的物理地址和大小,通常用于描述内存映射的I/O资源。 - 数组(Array):
属性名称:reg
示例值:reg = <0x1000,0x10>;
描述:定义设备的物理地址和大小,通常用于描述内存映射的I/O资源。 - 列表(List):
属性名称:interrupts
示例值:interrupts = <0 39 4>, <0 41 4>,<0 40 4>;
描述:用于定义例如中断列表,其中每个元组可以表示不同的中断属性(如编号和触发类型)。 - 空值(Empty):
属性名称:regulator-always-on;
示例值:regulator-always-on;
描述:表示该节点下的regulator是永久开启的,不需要动态控制。 - 引用(Reference):
属性名称:gpios
示例值:gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>;
描述:提供一个句柄(通常是一个节点的路径或标识符),用于在其他节点中引用该节点。
3.3.3.5.1、model属性(字符串)
model的值是字符串,主要是用于描述开发板型号,有助于用户和开发人员识别硬件。
/{
model = "lckfb tspi V10 Board";
}
3.3.3.5.2、compatible属性(字符串或字符串列表)
compatible:这是最关键的属性之一,它用于标识设备的兼容性字符串。操作系统使用这个属性来匹配设备与相应的驱动程序。
rk_headset: rk-headset {
compatible = "rockchip_headset","rockchip_headset2";
};
耳机检测驱动中会通过"rockchip_headset"来匹配驱动
kernel/drivers/headset_observe/rockchip_headset_core.c
.......
static const struct of_device_id rockchip_headset_of_match[] = {
{ .compatible = "rockchip_headset", }, // 定义设备树匹配项,指定兼容性字符串,与上面的设备树匹配
{}, // 结束符号
};
MODULE_DEVICE_TABLE(of, rockchip_headset_of_match); // 定义设备树匹配表供内核使用
static struct platform_driver rockchip_headset_driver = {
.probe = rockchip_headset_probe, // 注册设备探测函数
.remove = rockchip_headset_remove, // 注册设备移除函数
.resume = rockchip_headset_resume, // 注册设备恢复函数
.suspend = rockchip_headset_suspend, // 注册设备挂起函数
.driver = {
.name = "rockchip_headset", // 设备名称
.owner = THIS_MODULE, // 持有模块的指针
.of_match_table = of_match_ptr(rockchip_headset_of_match), // 设备树匹配表指针
},
};
.........
3.3.3.5.3、reg属性(地址,长度对)
描述了设备的物理地址范围,包括基址与大小,与address-cells
和size-cells
结合使用。
gmac1: ethernet@fe010000 {
reg = <0x0 0xfe010000 0x0 0x10000>;
}
3.3.3.5.4、#address-cells属性(整数)和#size-cells属性(整数)
用于说明父节点如何解释它的子节点中的reg
属性。
reg
属性的一般格式:
reg = <[address1] [length1] [address2] [length2] ...>;
[addressN]
:表示区域的起始物理地址。用多少个无符号整数来表示这个地址取决于父节点定义的#address-cells
的值。例如,如果#address-cells
为1,则使用一个32位整数表示地址;如果#address-cells
为2,则使用两个32位整数表示一个64位地址。
[lengthN]
:表示区域的长度(或大小)。用多少个无符号整数来表示这个长度同样取决于父节点定义的#size-cells
的值。
根据#address-cells
和#size-cells
的定义,单个[address, length]
对可能会占用2、3、4个或更多的元素。
例如,如果一个设备的寄存器空间位于地址0x03F02000
上,并且占用0x1000
字节的大小,假设其父节点定义了#address-cells = <1>
和 #size-cells = <1>
,reg
属性的表达方式如下:
reg = <0x03F02000 0x1000>;
如果地址是64位的,父节点#address-cells = <2>
和 #size-cells = <1>
,那么reg
属性可能会这样写,以表示地址0x00000001 0x03F02000
和大小0x1000
:
reg = <0x00000001 0x03F02000 0x1000>;
案例:
/ {
#address-cells = <2>;
#size-cells = <2>;
cpus {
#address-cells = <2>;
#size-cells = <0>;
cpu0: cpu@0 {
/*
受cpus节点的影响
#address-cells = <2>;
#size-cells = <0>;
所以地址就是0x0,大小就是 0x0
*/
reg = <0x0 0x0>;
};
};
gmac1: ethernet@fe010000 {
/*
受根节点的影响
#address-cells = <2>;
#size-cells = <2>;
所以地址就是0xfe010000 ,大小就是 0x10000
*/
reg = <0x0 0xfe010000 0x0 0x10000>;
};
};
3.3.3.5.5、status属性(字符串)
这个属性非常重要,我们设备树中其实修改的最大的就是打开某个节点或者关闭某个节点,status属性的值是字符串类型的,他可以有以下几个值,最常用的是okay和disabled。
status 属性值包括:
- “okay”:表示设备是可操作的,即设备当前处于正常状态,可以被系统正常使用。
- “disabled”:表示设备当前是不可操作的,但在未来可能变得可操作。这通常用于表示某些设备(如热插拔设备)在插入后暂时不可用,但在驱动程序加载或系统配置更改后可能会变得可用。
- “fail”:表示设备不可操作,且设备检测到了一系列错误,且设备不太可能变得可操作。这通常表示设备硬件故障或严重错误。
- “fail-sss”:与 “fail” 含义相同,但后面的 sss 部分提供了检测到的错误内容的详细信息。
//用户三色灯
&leds {
status = "okay";
};
//耳机插入检测,不使用扩展板情况需关闭,否则默认会检测到耳机插入
&rk_headset {
status = "disabled";
};
3.3.3.5.6、device_type属性(字符串)
device_type属性通常只用于cpu节点或memory节点。例如,在描述一个CPU节点时,device_type可能会被设置为"cpu",而在描述内存节点时,它可能会被设置为"memory"。
device_type = "cpu";
3.3.3.5.7、自定义属性
自定义属性需要注意不要和标准属性冲突,而且尽量做到见名知意
/ {
my_custom_node { /* 自定节点 */
compatible = "myvendor,my-custom-device"; /* 兼容性属性 */
my_custom_property = <1>; /* 自定义属性,假设为整数类型 */
my_custom_string_property = "My custom value"; /* 自定义字符串属性 */
};
};
设备树的介绍就到这里,接下来我们在下一节开始写代码。