源代码下载:git clone git@gitee.com:yangxuesong314/linux-driver.git(若之前已git拉取代码可以忽略) 代码位于:linux-driver/08.设备树/03.设备树实验
一、实验原理
1.1、设备树节点结构
本实验在设备树中定义了一个自定义节点 led_test,用于描述LED灯所使用的寄存器资源。
led_test {
#address-cells = <1>; // 子节点reg属性中地址字段的长度(1个u32)
#size-cells = <1>; // 子节点reg属性中大小字段的长度(1个u32)
compatible = "led_test"; // 兼容性字符串,驱动通过它匹配设备
ranges; // 允许子节点地址空间直接映射到父节点
led@0xfdd60004 {
reg = <0xfdd60004 0x00000004 0xfdd6000C 0x00000004>;
// reg格式:<地址0 大小0 地址1 大小1 ...>
// 地址0=0xfdd60004: GPIO0_H 数据寄存器(DR)
// 大小0=0x00000004: 寄存器长度4字节
// 地址1=0xfdd6000C: GPIO0_H 方向寄存器(DDR)
// 大小1=0x00000004: 寄存器长度4字节
status = "okay";
};
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键属性说明:
| 属性 | 说明 |
|---|---|
#address-cells = <1> | 子节点 reg 中每个地址占1个32位单元 |
#size-cells = <1> | 子节点 reg 中每个长度占1个32位单元 |
compatible = "led_test" | 驱动的匹配标识,驱动中的 of_device_id 需与此一致 |
ranges | 声明子节点地址空间直接映射到父节点地址空间,不做偏移转换 |
reg | 定义设备寄存器的物理地址和长度,驱动通过它做内存映射 |
1.2、地址映射核心函数解析
① of_iomap 函数
作用:将设备树中 reg 属性描述的物理地址,自动完成解析并映射为内核虚拟地址。
调用链:
of_iomap(np, index)
└── of_address_to_resource(np, index, &res) // 从设备树解析出resource
├── of_get_address(dev, index, &size, &flags) // 读取reg属性的地址/大小
└── __of_address_to_resource(...) // 填充resource结构体
└── ioremap(res.start, resource_size(&res)) // 映射为虚拟地址2
3
4
5
源码解析:
void __iomem *of_iomap(struct device_node *np, int index)
{
struct resource res;
// 第一步:从设备树节点np的第index个reg条目,解析出resource
if (of_address_to_resource(np, index, &res))
return NULL;
// 第二步:将物理地址区间映射为内核虚拟地址
return ioremap(res.start, resource_size(&res));
}2
3
4
5
6
7
8
9
10
11
参数说明:
| 参数 | 含义 |
|---|---|
np | 设备树节点指针(如 led 子节点) |
index | reg 属性中的第几组地址(从0开始) |
返回值 | 映射后的虚拟地址,失败返回NULL |
本实验中的使用:
// 映射第0组reg:0xfdd60004 0x00000004 -> 数据寄存器DR
led_res.va_DR = of_iomap(led_res.device_node, 0);
// 映射第1组reg:0xfdd6000C 0x00000004 -> 方向寄存器DDR
led_res.va_DDR = of_iomap(led_res.device_node, 1);2
3
4
5
② of_address_to_resource 函数
作用:将设备树节点的 reg 属性转换为内核 struct resource 结构体。
int of_address_to_resource(struct device_node *dev, int index,
struct resource *r)
{
const __be32 *addrp; // 指向设备树中地址数据的指针(大端格式)
u64 size; // 地址空间大小
unsigned int flags; // 资源标志(如IORESOURCE_MEM)
const char *name = NULL;// 资源名称(从reg-names属性获取)
// 从设备树读取第index个地址和大小
addrp = of_get_address(dev, index, &size, &flags);
if (addrp == NULL)
return -EINVAL;
// 尝试读取reg-names属性作为资源名(可选)
of_property_read_string_index(dev, "reg-names", index, &name);
// 将解析出的地址、大小、标志、名称填充到resource结构体
return __of_address_to_resource(dev, addrp, size, flags, name, r);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct resource 结构体:
struct resource {
resource_size_t start; // 资源起始物理地址
resource_size_t end; // 资源结束物理地址
const char *name; // 资源名称
unsigned long flags; // 资源类型标志(如IORESOURCE_MEM)
struct resource *parent, *sibling, *child; // 资源树指针
};2
3
4
5
6
7
③ of_get_address 函数
作用:从设备树节点的 reg 属性中,提取第 index 组地址和长度。
工作原理:
- 读取节点
#address-cells和#size-cells - 根据
index偏移到reg属性的对应位置 - 返回地址指针(
__be32 *)和大小(u64)
示例:对于 reg = <0xfdd60004 0x00000004 0xfdd6000C 0x00000004>:
index=0-> 地址0xfdd60004,大小0x00000004index=1-> 地址0xfdd6000C,大小0x00000004
④ __of_address_to_resource 函数
作用:将解析出的原始地址数据封装为标准 resource 结构体。
主要完成:
- 大端转小端(
be32_to_cpup) - 根据
#address-cells拼接64位地址 - 计算
end = start + size - 1 - 设置
flags为IORESOURCE_MEM
1.3、为什么使用 of_iomap 而不是直接 ioremap?
| 方式 | 优点 | 缺点 |
|---|---|---|
ioremap(phy_addr, size) | 直接、简单 | 硬编码物理地址,可移植性差 |
of_iomap(node, index) | 从设备树获取地址,硬件信息外置 | 需要正确配置设备树 |
设备树的优势:硬件寄存器地址变更时,只需修改 .dts 文件并重新编译,无需改动驱动源码。
二、设备树修改
修改文件位置
请在以下文件中添加设备树代码:
/home/book/rk/tspi/kernel/arch/arm64/boot/dts/rockchip/rk3566-tspi.dts在根节点内添加 led_test 节点
打开 rk3566-tspi.dts,在根节点 / {} 中找到合适位置(通常在文件末尾或已有节点之后),添加以下内容:
/ {
// 注意:保留根节点内原有所有内容,仅在末尾追加以下节点
led_test {
#address-cells = <1>;
#size-cells = <1>;
compatible = "led_test";
ranges;
led@0xfdd60004 {
reg = <0xfdd60004 0x00000004 0xfdd6000C 0x00000004>;
status = "okay";
};
};
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意:本实验使用 GPIO0_C7(RK3566系统LED所在引脚)进行测试。
0xfdd60004是 GPIO0 数据寄存器(DR),0xfdd6000C是 GPIO0 方向寄存器(DDR)。不同芯片地址可能不同,请查阅 RK3566 TRM 确认。
编译设备树
cd /home/book/rk/tspi/kernel
make ARCH=arm64 CROSS_COMPILE=/home/book/rk/tspi/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu- dtbs2
编译完成后,生成的 dtb 文件位于:
arch/arm64/boot/dts/rockchip/rk3566-tspi.dtb将新的 dtb 文件烧录到开发板。
三、驱动代码(led_platform_driver.c)
文件路径: 内核源码外任意目录(建议 ~/linux-driver/08.设备树/03.设备树实验/led_platform_driver.c)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.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/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/platform_device.h>
/*------------------字符设备内容----------------------*/
#define DEV_NAME "led_test"
#define DEV_CNT (1)
/*定义 led 资源结构体,保存获取得到的节点信息以及转换后的虚拟寄存器地址*/
struct led_resource {
struct device_node *device_node; //led的设备树节点
void __iomem *va_DR;
void __iomem *va_DDR;
};
static dev_t led_devno; //定义字符设备的设备号
static struct cdev led_chr_dev; //定义字符设备结构体chr_dev
struct class *class_led; //保存创建的类
struct device *device; // 保存创建的设备
struct device_node *led_test_device_node; //led_test的设备树节点结构体
/*定义led灯的led_resource 结构体,保存获取得到的节点信息*/
struct led_resource led_res;
/*字符设备操作函数集,open函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\n led_chr_dev_open \n");
return 0;
}
/*字符设备操作函数集,write函数*/
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
unsigned int register_data = 0; //暂存读取得到的寄存器数据
unsigned char write_data; //用于保存接收到的数据
int error = copy_from_user(&write_data, buf, cnt);
if (error < 0) {
return -1;
}
/*设置led引脚 输出电平*/
if (write_data) {
// GPIO0_C7引脚输出高电平,红灯灭
register_data |= ((unsigned int)0x1 << (23));
register_data |= ((unsigned int)0x1 << (7));
writel(register_data, led_res.va_DR);
} else {
// GPIO0_C7引脚输出低电平,红灯亮
register_data |= ((unsigned int)0x1 << (23));
register_data &= ~((unsigned int)0x1 << (7));
writel(register_data, led_res.va_DR);
}
return 0;
}
/*字符设备操作函数集*/
static struct file_operations led_chr_dev_fops = {
.owner = THIS_MODULE,
.open = led_chr_dev_open,
.write = led_chr_dev_write,
};
/*----------------平台驱动函数集-----------------*/
static int led_probe(struct platform_device *pdv)
{
int ret = -1; //保存错误状态码
unsigned int register_data = 0;
printk(KERN_EMERG "\t match successed \n");
/*获取led_test的设备树节点*/
led_test_device_node = of_find_node_by_path("/led_test");
if (led_test_device_node == NULL) {
printk(KERN_ERR "\t get led_test failed! \n");
return -1;
}
/*获取led子节点*/
led_res.device_node = of_find_node_by_name(led_test_device_node, "led");
if (led_res.device_node == NULL) {
printk(KERN_ERR "\n get led_device_node failed ! \n");
return -1;
}
/*获取 reg 属性并转化为虚拟地址*/
// 映射第0组reg:数据寄存器 DR
led_res.va_DR = of_iomap(led_res.device_node, 0);
if (led_res.va_DR == NULL) {
printk("of_iomap is error \n");
return -1;
}
// 映射第1组reg:方向寄存器 DDR
led_res.va_DDR = of_iomap(led_res.device_node, 1);
if (led_res.va_DDR == NULL) {
printk("of_iomap is error \n");
return -1;
}
// 设置方向寄存器:GPIO0_C7 配置为输出模式
// bit23=1: 使能GPIO0_C7的写权限
// bit7=1: 设置GPIO0_C7为输出方向
register_data = readl(led_res.va_DDR);
register_data |= ((unsigned int)0x1 << (23));
register_data |= ((unsigned int)0x1 << (7));
writel(register_data, led_res.va_DDR);
// 设置数据寄存器:默认输出高电平(LED灭)
register_data = readl(led_res.va_DR);
register_data |= ((unsigned int)0x1 << (23));
register_data |= ((unsigned int)0x1 << (7));
writel(register_data, led_res.va_DR);
/*---------------------注册 字符设备部分-----------------*/
// 第一步:动态分配设备号
ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0) {
printk("fail to alloc led_devno\n");
goto alloc_err;
}
// 第二步:初始化cdev
led_chr_dev.owner = THIS_MODULE;
cdev_init(&led_chr_dev, &led_chr_dev_fops);
// 第三步:添加字符设备
ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);
if (ret < 0) {
printk("fail to add cdev\n");
goto add_err;
}
// 第四步:创建类和设备节点
class_led = class_create(THIS_MODULE, DEV_NAME);
device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);
return 0;
add_err:
// 添加设备失败时,需要注销设备号
unregister_chrdev_region(led_devno, DEV_CNT);
printk("\n error! \n");
alloc_err:
return -1;
}
static const struct of_device_id led_ids[] = {
{.compatible = "led_test"},
{/* sentinel */}
};
/*定义平台设备结构体*/
struct platform_driver led_platform_driver = {
.probe = led_probe,
.driver = {
.name = "leds-platform",
.owner = THIS_MODULE,
.of_match_table = led_ids,
}
};
/*
* 驱动初始化函数
*/
static int __init led_platform_driver_init(void)
{
int DriverState;
DriverState = platform_driver_register(&led_platform_driver);
printk(KERN_EMERG "\tDriverState is %d\n", DriverState);
return 0;
}
/*
* 驱动注销函数
*/
static void __exit led_platform_driver_exit(void)
{
/*取消物理地址映射到虚拟地址*/
iounmap(led_res.va_DR);
iounmap(led_res.va_DDR);
/*删除设备*/
device_destroy(class_led, led_devno); //清除设备
class_destroy(class_led); //清除类
cdev_del(&led_chr_dev); //清除设备号
unregister_chrdev_region(led_devno, DEV_CNT); //取消注册字符设备
/*注销平台驱动*/
platform_driver_unregister(&led_platform_driver);
printk(KERN_EMERG "dts test led exit!\n");
}
module_init(led_platform_driver_init);
module_exit(led_platform_driver_exit);
MODULE_LICENSE("GPL");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
Makefile
export ARCH=arm64
export CROSS_COMPILE=/home/book/rk/tspi/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
obj-m += led_platform_driver.o #此处要和你的驱动源文件同名
KDIR := /home/book/rk/tspi/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作2
3
4
5
6
7
8
9
四、应用代码(led_test_app.c)
文件路径: 用户空间任意目录(建议与驱动同目录)
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
printf("led test_app\n");
/*判断输入的命令是否合法*/
if (argc != 2) {
printf(" command error ! \n");
printf(" usage : sudo ./led_test_app num [num can be 0 or 1]\n");
return -1;
}
/*打开设备文件*/
int fd = open("/dev/led_test", O_RDWR);
if (fd < 0) {
printf("open file : /dev/led_test failed !\n");
return -1;
}
unsigned char commend = atoi(argv[1]); //将接收值转化为数字
/*写入命令:0-亮灯,1-灭灯*/
int error = write(fd, &commend, sizeof(commend));
if (error < 0) {
printf("write file error! \n");
close(fd);
return -1;
}
/*关闭文件*/
error = close(fd);
if (error < 0) {
printf("close file error! \n");
}
return 0;
}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
五、编译与测试
1. 编译驱动
make生成 led_platform_driver.ko
2. 加载驱动
insmod led_platform_driver.ko加载成功后:
- 内核打印
match successed,表示设备树与驱动匹配成功 - 自动创建
/dev/led_test设备节点 - 默认LED处于熄灭状态(输出高电平)
3. 编译应用
aarch64-linux-gnu-gcc led_test_app.c -o led_test_app4. 运行测试
## 点亮LED(写入0)
sudo ./led_test_app 0
## 熄灭LED(写入1)
sudo ./led_test_app 12
3
4
5
注意:应用需要root权限操作
/dev/led_test,或使用sudo执行。
5. 卸载驱动
rmmod led_platform_driver