1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban
第九章 设备树下的LED驱动实验
上一章我们详细的讲解了设备树语法以及在驱动开发中常用的OF函数,本章我们就开始第一个基于设备树的Linux驱动实验。本章在第七章实验的基础上完成,只是将其驱动开发改为设备树形式而已。
9.1 设备树LED驱动原理
在《第七章 新字符设备驱动实验》中,我们直接在驱动文件newchrled.c中定义有关寄存器物理地址,然后使用io_remap函数进行内存映射,得到对应的虚拟地址,最后操作寄存器对应的虚拟地址完成对GPIO的初始化。本章我们在第七章实验基础上完成,本章我们使用设备树来向Linux内核传递相关的寄存器物理地址,Linux驱动文件使用上一章讲解的OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的IO。本章实验还是比较简单的,本章实验重点内容如下:
①、在rk3568-atk-evb1-ddr4-v10.dtsi文件中创建相应的设备节点。
②、编写驱动程序(在第七章实验基础上完成),获取设备树中的相关属性值。
③、使用获取到的有关属性值来初始化LED所使用的GPIO。
9.2 硬件原理图分析
本实验的硬件原理参考6.2小节即可。
9.3 实验程序编写
9.3.1 修改设备树文件
在根节点“/”下创建一个名为“rk3568_led”的子节点,打开rk3568-atk-evb1-ddr4-v10.dtsi文件,在根节点“/”最后面输入如下所示内容:
示例代码9.3.1.1 rk3568_led节点
1 rk3568_led {
2 compatible = "atkrk3568-led";
3 status = "okay";
4 reg = <0x0 0xFDC20010 0x0 0x08 /* PMU_GRF_GPIO0C_IOMUX_L */
5 0x0 0xFDC20090 0x0 0x08 /* PMU_GRF_GPIO0C_DS_0 */
6 0x0 0xFDD60004 0x0 0x08 /* GPIO0_SWPORT_DR_H */
7 0x0 0xFDD6000C 0x0 0x08 >;/* GPIO0_SWPORT_DDR_H */
8 };
第2行,属性compatible设置rk3568_led节点兼容为“atkrk3568-led”。
第3行,属性status设置状态为“okay”。
第4~7行,reg属性,非常重要!reg属性设置了驱动里面所要使用的寄存器物理地址,比如第4行的“0x0 0xFDC20010 0x0 0x08”表示RK3568的PMU_GRF_GPIO0C_IOMUX_L寄存器,其中寄存器地址为0xFDC20010,长度为8个字节。
设备树修改完成以后在SDK顶层目录输入如下命令重新编译一下内核:
./build.sh kernel
编译完成以后得到boot.img,如图9.3.1.1所示:
图9.3.1.1 内核编译出来的zboot.img
图9.3.1.1中zboot.img就是编译出来的内核+设备树打包在一起的文件。
烧写方法很简单,分两步:
②、开发板进入LOADER模式,前提是开发板已经烧写了uboot,并且uboot正常运行。
③、打开RKDevTool工具,导入配置文件,然后将图9.3.1.1中的boot.img放到要烧写的目录,然后只烧写boot.img,详细请看《【正点原子】 ATK-DLRK3568嵌入式Linux系统开发手册》第5.2.3小节-烧录。
烧写完成以后启动开发板。Linux启动成功以后进入到/proc/device-tree/目录中查看是否有“rk3568_led”这个节点,结果如图9.3.1.3所示:
图9.3.1.2 rk3568_led节点
如果没有“rk3568_led”节点的话请重点检查下面两点:
①、检查设备树修改是否成功,也就是rk3568_led节点是否为根节点“/”的子节点。
②、检查是否使用新的设备树启动的Linux内核。
可以进入到图9.3.1.3中的rk3568_led目录中,查看一下都有哪些属性文件,结果如图9.3.1.4所示:
图9.3.1.4 rk3568_led节点文件
大家可以用cat命令查看一下compatible、status等属性值是否和我们设置的一致。
9.3.2 LED灯驱动程序编写
设备树准备好以后就可以编写驱动程序了,本章实验在第七章实验驱动文件newchrled.c的基础上修改而来。新建名为“04_dtsled”文件夹,然后在04_dtsled文件夹里面创建vscode工程,工作区命名为“dtsled”。工程创建好以后新建dtsled.c文件,在dtsled.c里面输入如下内容:
示例代码9.3.2.1 dtsled.c文件内容
1 #include <linux/types.h>
2 #include <linux/kernel.h>
3 #include <linux/delay.h>
4 #include <linux/ide.h>
5 #include <linux/init.h>
6 #include <linux/module.h>
7 #include <linux/errno.h>
8 #include <linux/gpio.h>
9 #include <linux/cdev.h>
10 #include <linux/device.h>
11 #include <linux/of.h>
12 #include <linux/of_address.h>
13 //#include <asm/mach/map.h>
14 #include <asm/uaccess.h>
15 #include <asm/io.h>
16 /***************************************************************
17 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
18 文件名 : dtsled.c
19 作者 : 正点原子
20 版本 : V1.0
21 描述 : LED驱动文件。
22 其他 : 无
23 论坛 : www.openedv.com
24 日志 : 初版V1.0 2022/12/06 正点原子团队创建
25 ***************************************************************/
26 #define DTSLED_CNT 1 /* 设备号个数 */
27 #define DTSLED_NAME "dtsled" /* 名字 */
28 #define LEDOFF 0 /* 关灯 */
29 #define LEDON 1 /* 开灯 */
30
31 /* 映射后的寄存器虚拟地址指针 */
32 static void __iomem *PMU_GRF_GPIO0C_IOMUX_L_PI;
33 static void __iomem *PMU_GRF_GPIO0C_DS_0_PI;
34 static void __iomem *GPIO0_SWPORT_DR_H_PI;
35 static void __iomem *GPIO0_SWPORT_DDR_H_PI;
36
37 /* dtsled设备结构体 */
38 struct dtsled_dev{
39 dev_t devid; /* 设备号 */
40 struct cdev cdev; /* cdev */
41 struct class *class; /* 类 */
42 struct device *device; /* 设备 */
43 int major; /* 主设备号 */
44 int minor; /* 次设备号 */
45 struct device_node *nd; /* 设备节点 */
46 };
47
48 struct dtsled_dev dtsled; /* led设备 */
49
50 /*
51 * @description : LED打开/关闭
52 * @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
53 * @return : 无
54 */
55 void led_switch(u8 sta)
56 {
57 u32 val = 0;
58 if(sta == LEDON) {
59 val = readl(GPIO0_SWPORT_DR_H_PI);
60 val &= ~(0X1 << 0); /* bit0 清零*/
61 val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,
62 bit0,高电平*/
63 writel(val, GPIO0_SWPORT_DR_H_PI);
64 }else if(sta == LEDOFF) {
65 val = readl(GPIO0_SWPORT_DR_H_PI);
66 val &= ~(0X1 << 0); /* bit0 清零*/
67 val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,
68 bit0,低电平 */
69 writel(val, GPIO0_SWPORT_DR_H_PI);
70 }
71 }
72
73 /*
74 * @description : 取消映射
75 * @return : 无
76 */
77 void led_unmap(void)
78 {
79 /* 取消映射 */
80 iounmap(PMU_GRF_GPIO0C_IOMUX_L_PI);
81 iounmap(PMU_GRF_GPIO0C_DS_0_PI);
82 iounmap(GPIO0_SWPORT_DR_H_PI);
83 iounmap(GPIO0_SWPORT_DDR_H_PI);
84 }
85
86 /*
87 * @description : 打开设备
88 * @param - inode : 传递给驱动的inode
89 * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
90 * 一般在open的时候将private_data指向设备结构体。
91 * @return : 0 成功;其他 失败
92 */
93 static int led_open(struct inode *inode, struct file *filp)
94 {
95 filp->private_data = &dtsled; /* 设置私有数据 */
96 return 0;
97 }
98
99 /*
100 * @description : 从设备读取数据
101 * @param - filp : 要打开的设备文件(文件描述符)
102 * @param - buf : 返回给用户空间的数据缓冲区
103 * @param - cnt : 要读取的数据长度
104 * @param - offt : 相对于文件首地址的偏移
105 * @return : 读取的字节数,如果为负值,表示读取失败
106 */
107 static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
108 {
109 return 0;
110 }
111
112 /*
113 * @description : 向设备写数据
114 * @param - filp : 设备文件,表示打开的文件描述符
115 * @param - buf : 要写给设备写入的数据
116 * @param - cnt : 要写入的数据长度
117 * @param - offt : 相对于文件首地址的偏移
118 * @return : 写入的字节数,如果为负值,表示写入失败
119 */
120 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
121 {
122 int retvalue;
123 unsigned char databuf[1];
124 unsigned char ledstat;
125
126 retvalue = copy_from_user(databuf, buf, cnt);
127 if(retvalue < 0) {
128 printk("kernel write failed!\r\n");
129 return -EFAULT;
130 }
131
132 ledstat = databuf[0]; /* 获取状态值 */
133
134 if(ledstat == LEDON) {
135 led_switch(LEDON); /* 打开LED灯 */
136 } else if(ledstat == LEDOFF) {
137 led_switch(LEDOFF); /* 关闭LED灯 */
138 }
139 return 0;
140 }
141
142 /*
143 * @description : 关闭/释放设备
144 * @param - filp : 要关闭的设备文件(文件描述符)
145 * @return : 0 成功;其他 失败
146 */
147 static int led_release(struct inode *inode, struct file *filp)
148 {
149 return 0;
150 }
151
152 /* 设备操作函数 */
153 static struct file_operations dtsled_fops = {
154 .owner = THIS_MODULE,
155 .open = led_open,
156 .read = led_read,
157 .write = led_write,
158 .release = led_release,
159 };
160
161 /*
162 * @description : 驱动出口函数
163 * @param : 无
164 * @return : 无
165 */
166 static int __init led_init(void)
167 {
168 u32 val = 0;
169 int ret;
170 u32 regdata[16];
171 const char *str;
172 struct property *proper;
173
174 /* 获取设备树中的属性数据 */
175 /* 1、获取设备节点:rk3568_led */
176 dtsled.nd = of_find_node_by_path("/rk3568_led");
177 if(dtsled.nd == NULL) {
178 printk("rk3568_led node not find!\r\n");
179 goto fail_find_node;
180 } else {
181 printk("rk3568_led node find!\r\n");
182 }
183
184 /* 2、获取compatible属性内容 */
185 proper = of_find_property(dtsled.nd, "compatible", NULL);
186 if(proper == NULL) {
187 printk("compatible property find failed\r\n");
188 } else {
189 printk("compatible = %s\r\n", (char*)proper->value);
190 }
191
192 /* 3、获取status属性内容 */
193 ret = of_property_read_string(dtsled.nd, "status", &str);
194 if(ret < 0){
195 printk("status read failed!\r\n");
196 } else {
197 printk("status = %s\r\n",str);
198 }
199
200 /* 4、获取reg属性内容 */
201 ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 16);
202 if(ret < 0) {
203 printk("reg property read failed!\r\n");
204 } else {
205 u8 i = 0;
206 printk("reg data:\r\n");
207 for(i = 0; i < 16; i++)
208 printk("%#X ", regdata[i]);
209 printk("\r\n");
210 }
211
212 /* 初始化LED */
213 /* 1、寄存器地址映射 */
214 PMU_GRF_GPIO0C_IOMUX_L_PI = of_iomap(dtsled.nd, 0);
215 PMU_GRF_GPIO0C_DS_0_PI = of_iomap(dtsled.nd, 1);
216 GPIO0_SWPORT_DR_H_PI = of_iomap(dtsled.nd, 2);
217 GPIO0_SWPORT_DDR_H_PI = of_iomap(dtsled.nd, 3);
218
219 /* 2、设置GPIO0_C0为GPIO功能。*/
220 val = readl(PMU_GRF_GPIO0C_IOMUX_L_PI);
221 val &= ~(0X7 << 0); /* bit2:0,清零 */
222 val |= ((0X7 << 16) | (0X0 << 0)); /* bit18:16 置1,允许写bit2:0,
223 bit2:0:0,用作GPIO0_C0 */
224 writel(val, PMU_GRF_GPIO0C_IOMUX_L_PI);
225
226 /* 3、设置GPIO0_C0驱动能力为level5 */
227 val = readl(PMU_GRF_GPIO0C_DS_0_PI);
228 val &= ~(0X3F << 0); /* bit5:0清零*/
229 val |= ((0X3F << 16) | (0X3F << 0)); /* bit21:16 置1,允许写bit5:0,
230 bit5:0:0,用作GPIO0_C0 */
231 writel(val, PMU_GRF_GPIO0C_DS_0_PI);
232
233 /* 4、设置GPIO0_C0为输出 */
234 val = readl(GPIO0_SWPORT_DDR_H_PI);
235 val &= ~(0X1 << 0); /* bit0 清零*/
236 val |= ((0X1 << 16) | (0X1 << 0)); /* bit16 置1,允许写bit0,
237 bit0,高电平 */
238 writel(val, GPIO0_SWPORT_DDR_H_PI);
239
240 /* 5、设置GPIO0_C0为低电平,关闭LED灯。*/
241 val = readl(GPIO0_SWPORT_DR_H_PI);
242 val &= ~(0X1 << 0); /* bit0 清零*/
243 val |= ((0X1 << 16) | (0X0 << 0)); /* bit16 置1,允许写bit0,
244 bit0,低电平 */
245 writel(val, GPIO0_SWPORT_DR_H_PI);
246
247 /* 注册字符设备驱动 */
248 /* 1、创建设备号 */
249 if (dtsled.major) { /* 定义了设备号 */
250 dtsled.devid = MKDEV(dtsled.major, 0);
251 ret = register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
252 if(ret < 0) {
253 pr_err("cannot register %s char driver [ret=%d]\n",DTSLED_NAME, DTSLED_CNT);
254 goto fail_devid;
255 }
256 } else { /* 没有定义设备号 */
257 ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); /* 申请设备号 */
258 if(ret < 0) {
259 pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", DTSLED_NAME, ret);
260 goto fail_devid;
261 }
262 dtsled.major = MAJOR(dtsled.devid); /* 获取分配号的主设备号 */
263 dtsled.minor = MINOR(dtsled.devid); /* 获取分配号的次设备号 */
264
265 }
266
267 printk("dtsled major=%d,minor=%d\r\n",dtsled.major, dtsled.minor);
268
269 /* 2、初始化cdev */
270 dtsled.cdev.owner = THIS_MODULE;
271 cdev_init(&dtsled.cdev, &dtsled_fops);
272
273 /* 3、添加一个cdev */
274 ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);
275 if(ret < 0)
276 goto del_unregister;
277
278 /* 4、创建类 */
279 dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
280 if (IS_ERR(dtsled.class)) {
281 goto del_cdev;
282 }
283
284 /* 5、创建设备 */
285 dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
286 if (IS_ERR(dtsled.device)) {
287 goto destroy_class;
288 }
289
290 return 0;
291
292 destroy_class:
293 class_destroy(dtsled.class);
294 del_cdev:
295 cdev_del(&dtsled.cdev);
296 del_unregister:
297 unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
298 fail_devid:
299 led_unmap();
300 fail_find_node:
301 return -EIO;
302 }
303
304 /*
305 * @description : 驱动出口函数
306 * @param : 无
307 * @return : 无
308 */
309 static void __exit led_exit(void)
310 {
311 /* 取消映射 */
312 led_unmap();
313
314 /* 注销字符设备驱动 */
315 cdev_del(&dtsled.cdev);/* 删除cdev */
316 unregister_chrdev_region(dtsled.devid, DTSLED_CNT); /* 注销设备号 */
317
318 device_destroy(dtsled.class, dtsled.devid);
319 class_destroy(dtsled.class);
320 }
321
322 module_init(led_init);
323 module_exit(led_exit);
324 MODULE_LICENSE("GPL");
325 MODULE_AUTHOR("ALIENTEK");
326 MODULE_INFO(intree, "Y");
dtsled.c文件中的内容和第七章的newchrled.c文件中的内容基本一样,只是dtsled.c中包含了处理设备树的代码,我们重点来看一下这部分代码。
第45行,在设备结构体dtsled_dev中添加了成员变量nd,nd是device_node结构体类型指针,表示设备节点。如果我们要读取设备树某个节点的属性值,首先要先得到这个节点,一般在设备结构体中添加device_node指针变量来存放这个节点。
第176~182行,通过of_find_node_by_path函数得到rk3568_led节点,后续其他的OF函数要使用device_node。
第185~190行,通过of_find_property函数获取rk3568_led节点的compatible属性,返回值为property结构体类型指针变量,property的成员变量value表示属性值。
第193~198行,通过of_property_read_string函数获取rk3568_led节点的status属性值。
第201~210行,通过of_property_read_u32_array函数获取rk3568_led节点的reg属性所有值,并且将获取到的值都存放到regdata数组中。第208行将获取到的reg属性值依次输出到终端上。
第214~217行,使用of_iomap函数一次性完成读取reg属性以及内存映射,of_iomap函数是设备树推荐使用的OF函数。
9.3.3 编写测试APP
本章直接使用第七章的测试APP,将上一章的ledApp.c文件复制到本章实验工程下即可。
9.4.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第五章实验基本一样,只是将obj-m变量的值改为dtsled.o,Makefile内容如下所示:
示例代码9.4.1.1 Makefile文件
1 KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
......
4 obj-m := dtsled.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,设置obj-m变量的值为dtsled.o。
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译成功以后就会生成一个名为“dtsled.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试ledApp.c这个测试程序:
/opt/atk-dlrk356x-toolchain/bin/aarch64-buildroot-linux-gnu-gcc ledApp.c -o ledApp
编译成功以后就会生成ledApp这个应用程序。
9.4.2 运行测试
先在开发板中输入如下命令关闭LED的心跳灯功能:
echo none > /sys/class/leds/work/trigger
在Ubuntu中将上一小节编译出来的dtsled.ko和ledApp这两个文件通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push dtsled.ko ledApp /lib/modules/4.19.232
发送成功以后进入到开发板目录lib/modules/4.19.232中,输入如下命令加载dtsled.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe dtsled //加载驱动
驱动加载成功以后会在终端中输出一些信息,如图9.4.2.1所示:
图9.4.2.1 驱动加载成功以后输出的信息
从图9.4.2.1可以看出,rk3568_led这个节点找到了,并且compatible属性值为“atkrk3568-led”,status属性值为“okay”,reg属性的值为“0x0 0xFDC20010 0x0 0x08 0x0 0xFDC20090 0x0 0x08 0x0 0xFDD60004 0x0 0x08 0x0 0xFDD6000C 0x0 0x08”,这些都和我们设置的设备树一致。
驱动加载成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:
./ledApp /dev/dtsled 1 //打开LED灯
输入上述命令以后观察开发板上的红色LED灯是否点亮,如果点亮的话说明驱动工作正常。在输入如下命令关闭LED灯:
./ledApp /dev/dtsled 0 //关闭LED灯
输入上述命令以后观察开发板上的红色LED灯是否熄灭。如果要卸载驱动的话输入如下命令即可:
rmmod dtsled.ko