正点原子 发表于 2022-1-22 14:45:50

《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十一章 Linux按键

1)实验平台:正点原子领航者V2 ZYNQ开发板
2)章节摘自【正点原子】《领航者ZYNQ之嵌入式Linux开发指南_V2.0》
3)购买链接:https://detail.tmall.com/item.htm?id=609032204975
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-329957-1-1.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:90562473








第三十一章 Linux按键输入实验
       在前几章我们都是使用的GPIO输出功能,还没有用过GPIO输入功能,本章我们就来学习一下如果在Linux下编写GPIO输入驱动程序。领航者开发板上有5个按键,四个轻触式按键和一个触摸按键,本章我们就以PS_KEY0按键为例,使用此按键来完成GPIO输入驱动程序,同时利用第三十章讲的互斥锁来对按键值进行保护。

       1.1Linux下按键驱动原理
       按键驱动和LED驱动原理上来讲基本都是一样的,都是操作GPIO,只不过一个是读取GPIO的高低电平,一个是从GPIO输出高低电平。本章我们实现按键输入,在驱动程序中实现read函数,读取按键值并将数据发送给上层应用测试程序,在read函数中,使用了互斥锁对读数据过程进行了保护,后面会讲解为什么使用互斥锁进行保护。Linux下的按键驱动原理很简单,接下来开始编写驱动。
       注意,本章例程只是为了演示Linux下GPIO输入驱动的编写,实际中的按键驱动并不会采用本章中所讲解的方法,Linux下的input子系统专门用于输入设备!
       1.2硬件原理图分析
       打开领航者底板原理图,找到PS_KEY0按键原理图,如下所示:



图 31.2.1 按键原理图
       从原理图可知,当PS_KEY0按键按下时,对应的管脚MIO12为低电平状态,松开的时候MIO12为高电平状态,所以可以通过读取MIO管脚的电平状态来判断按键是否被按下或松开。
       1.3实验程序编写
       本实验对应的例程路径为:ZYNQ开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\11_key
1.3.1修改设备树文件
       打开system-top.dts文件,在根节点“/”下创建一个按键节点,节点名为“key”,节点内容如下:
<font size="2">示例代码 31.3.1 创建key节点
49 key {
50   compatible = "alientek,key";
51   status = "okay";
52   key-gpio = <&gpio0 12 GPIO_ACTIVE_LOW>;
53 };</font>       这个节点内容很简单。
       第50行,设置节点的compatible属性为“alientek,key”。
       第52行,key-gpio属性指定了PS_KEY0按键所使用的GPIO。
       设备树编写完成以后使用,在linux内核源码目录下执行下面这条命令重新编译设备树:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- system-top.dtb
图 31.3.1 重新编译设备树
       然后将新编译出来的system-top.dtb文件重命名为system.dtb,将system.dtb文件拷贝到SD启动卡的Fat分区,替换以前的system.dtb文件,替换完成之后重启开发板。启动成功以后进入“/proc/device-tree”目录中查看“key”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图 31.3.2所示:

图 31.3.2 key节点
1.3.2按键驱动程序编写
      设备树准备好以后就可以编写驱动程序了,在drivers目录下新建名为“11_key”的文件夹,然后在11_key文件夹里面新建一个名为key.c的源文件,在key.c里面输入如下内容:
<font size="2">示例代码 31.3.2 key.c文件代码
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名    : key.c
4作者      : 邓涛
5版本      : V1.0
6描述      : Linux按键输入驱动实验
7其他      : 无
8论坛      : <a href="www.openedv.com" target="_blank" style="">www.openedv.com</a>
9日志      : 初版V1.0 2019/1/30 邓涛创建
10***************************************************************/
11
12 #include <linux/types.h>
13 #include <linux/kernel.h>
14 #include <linux/delay.h>
15 #include <linux/ide.h>
16 #include <linux/init.h>
17 #include <linux/module.h>
18 #include <linux/errno.h>
19 #include <linux/gpio.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23 #include <linux/cdev.h>
24 #include <linux/of.h>
25 #include <linux/of_address.h>
26 #include <linux/of_gpio.h>
27
28 #define KEY_CNT                        1                /* 设备号个数 */
29 #define KEY_NAME                "key"      /* 名字 */
30
31 /* dtsled设备结构体 */
32 struct key_dev {
33   dev_t devid;                              /* 设备号 */
34   struct cdev cdev;                        /* cdev */
35   struct class *class;                        /* 类 */
36   struct device *device;                /* 设备 */
37   int major;                                        /* 主设备号 */
38   int minor;                              /* 次设备号 */
39   struct device_node *nd;                /* 设备节点 */
40   int key_gpio;                              /* GPIO编号 */
41   int key_val;                              /* 按键值 */
42   struct mutex mutex;                /* 互斥锁 */
43 };
44
45 static struct key_dev key;      /* led设备 */
46
47 /*
48* @description                        : 打开设备
49* @param – inode                        : 传递给驱动的inode
50* @param – filp                        : 设备文件,file结构体有个叫做private_data的成员变量
51*                                                一般在open的时候将private_data指向设备结构体。
52* @return                              : 0 成功;其他 失败
53*/
54 static int key_open(struct inode *inode, struct file *filp)
55 {
56   return 0;
57 }
58
59 /*
60* @description                        : 从设备读取数据
61* @param – filp                        : 要打开的设备文件(文件描述符)
62* @param – buf                        : 返回给用户空间的数据缓冲区
63* @param – cnt                        : 要读取的数据长度
64* @param – offt                        : 相对于文件首地址的偏移
65* @return                              : 读取的字节数,如果为负值,表示读取失败
66*/
67 static ssize_t key_read(struct file *filp, char __user *buf,
68             size_t cnt, loff_t *offt)
69 {
70   int ret = 0;
71
72   /* 互斥锁上锁 */
73   if (mutex_lock_interruptible(&key.mutex))
74         return -ERESTARTSYS;
75
76   /* 读取按键数据 */
77   if (!gpio_get_value(key.key_gpio)) {
78         while(!gpio_get_value(key.key_gpio));
79         key.key_val = 0x0;
80   } else
81         key.key_val = 0xFF;
82
83   /* 将按键数据发送给应用程序 */
84   ret = copy_to_user(buf, &key.key_val, sizeof(int));
85
86   /* 解锁 */
87   mutex_unlock(&key.mutex);
88
89   return ret;
90 }
91
92 /*
93* @description                        : 向设备写数据
94* @param – filp                        : 设备文件,表示打开的文件描述符
95* @param – buf                        : 要写给设备写入的数据
96* @param – cnt                        : 要写入的数据长度
97* @param – offt                        : 相对于文件首地址的偏移
98* @return                              : 写入的字节数,如果为负值,表示写入失败
99*/
100 static ssize_t key_write(struct file *filp, const char __user *buf,
101             size_t cnt, loff_t *offt)
102 {
103   return 0;
104 }
105
106 /*
107* @description                        : 关闭/释放设备
108* @param – filp                        : 要关闭的设备文件(文件描述符)
109* @return                              : 0 成功;其他 失败
110*/
111 static int key_release(struct inode *inode, struct file *filp)
112 {
113   return 0;
114 }
115
116 /* 设备操作函数 */
117 static struct file_operations key_fops = {
118   .owner                = THIS_MODULE,
119   .open                = key_open,
120   .read                = key_read,
121   .write                = key_write,
122   .release      = key_release,
123 };
124
125 static int __init mykey_init(void)
126 {
127   const char *str;
128   int ret;
129
130   /* 初始化互斥锁 */
131   mutex_init(&key.mutex);
132
133   /* 1.获取key节点 */
134   key.nd = of_find_node_by_path("/key");
135   if(NULL == key.nd) {
136         printk(KERN_ERR "key: Failed to get key node\n");
137         return -EINVAL;
138   }
139
140   /* 2.读取status属性 */
141   ret = of_property_read_string(key.nd, "status", &str);
142   if(!ret) {
143         if (strcmp(str, "okay"))
144             return -EINVAL;
145   }
146
147   /* 3.获取compatible属性值并进行匹配 */
148   ret = of_property_read_string(key.nd, "compatible", &str);
149   if(ret) {
150         printk(KERN_ERR "key: Failed to get compatible property\n");
151         return ret;
152   }
153
154   if (strcmp(str, "alientek,key")) {
155         printk(KERN_ERR "key: Compatible match failed\n");
156         return -EINVAL;
157   }
158
159   printk(KERN_INFO "key: device matching successful!\r\n");
160
161   /* 4.获取设备树中的key-gpio属性,得到按键所使用的GPIO编号 */
162   key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
163   if(!gpio_is_valid(key.key_gpio)) {
164         printk(KERN_ERR "key: Failed to get key-gpio\n");
165         return -EINVAL;
166   }
167
168   printk(KERN_INFO "key: key-gpio num = %d\r\n", key.key_gpio);
169
170   /* 5.申请GPIO */
171   ret = gpio_request(key.key_gpio, "Key Gpio");
172   if (ret) {
173         printk(KERN_ERR "key: Failed to request key-gpio\n");
174         return ret;
175   }
176
177   /* 6.将GPIO设置为输入模式 */
178   gpio_direction_input(key.key_gpio);
179
180   /* 7.注册字符设备驱动 */
181      /* 创建设备号 */
182   if (key.major) {
183         key.devid = MKDEV(key.major, 0);
184         ret = register_chrdev_region(key.devid, KEY_CNT, KEY_NAME);
185         if (ret)
186             goto out1;
187   } else {
188         ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);
189         if (ret)
190             goto out1;
191
192         key.major = MAJOR(key.devid);
193         key.minor = MINOR(key.devid);
194   }
195
196   printk(KERN_INFO "key: major=%d, minor=%d\r\n", key.major, key.minor);
197
198      /* 初始化cdev */
199   key.cdev.owner = THIS_MODULE;
200   cdev_init(&key.cdev, &key_fops);
201
202      /* 添加cdev */
203   ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
204   if (ret)
205         goto out2;
206
207      /* 创建类 */
208   key.class = class_create(THIS_MODULE, KEY_NAME);
209   if (IS_ERR(key.class)) {
210         ret = PTR_ERR(key.class);
211         goto out3;
212   }
213
214      /* 创建设备 */
215   key.device = device_create(key.class, NULL,
216               key.devid, NULL, KEY_NAME);
217   if (IS_ERR(key.device)) {
218         ret = PTR_ERR(key.device);
219         goto out4;
220   }
221
222   return 0;
223
224 out4:
225   class_destroy(key.class);
226
227 out3:
228   cdev_del(&key.cdev);
229
230 out2:
231   unregister_chrdev_region(key.devid, KEY_CNT);
232
233 out1:
234   gpio_free(key.key_gpio);
235
236   return ret;
237 }
238
239 static void __exit mykey_exit(void)
240 {
241   /* 注销设备 */
242   device_destroy(key.class, key.devid);
243
244   /* 注销类 */
245   class_destroy(key.class);
246
247   /* 删除cdev */
248   cdev_del(&key.cdev);
249
250   /* 注销设备号 */
251   unregister_chrdev_region(key.devid, KEY_CNT);
252
253   /* 释放GPIO */
254   gpio_free(key.key_gpio);
255 }
256
257 /* 驱动模块入口和出口函数注册 */
258 module_init(mykey_init);
259 module_exit(mykey_exit);
260
261 MODULE_AUTHOR("DengTao <<a href="mailto:773904075@qq.com" style="">773904075@qq.com</a>>");
262 MODULE_DESCRIPTION("Alientek Gpio Key Driver");
263 MODULE_LICENSE("GPL");</font>       第32~43行,结构体key_dev为按键的设备结构体,第40行的key_gpio表示按键对应的GPIO编号,第41行key_val用来保存读取到的按键值,第42行定义了一个互斥锁变量mutex,用来保护按键读取过程。
       第67~90行,在key_read函数中,通过gpio_get_value函数读取按键值,如果当前为按下状态,则使用while循环等待按键松开,松开之后将key_val变量置为0x0,从按键按下状态到松开状态视为一次有效状态;如果当前为松开状态,则将key_val变量置为0xFF,表示为无效状态。使用copy_to_user函数将key_val值发送给上层应用;第73行,调用mutex_lock_interruptible函数上锁(互斥锁),第87行解锁,对整个读取按键过程进行保护,因为在于用于保存按键值的key_val是一个全局变量,如果上层有多个应用对按键进行了读取操作,将会出现第二十九章说到的并发访问,这对系统来说是不利的,所以这里使用了互斥锁进行了保护。应用程序通过read函数读取按键值的时候key_read函数就会执行!
       第131行,调用mutex_init函数初始化互斥锁。
       第178行,调用gpio_direction_input函数将按键对应的GPIO设置为输入模式。
       key.c文件代码很简单,重点就是key_read函数读取按键值,要对读取过程进行保护。
1.3.3编写测试APP
       在本章实验目录下新建名为keyApp.c的文件,然后输入如下所示内容:
<font size="2">示例代码 31.3.3 keyApp.c文件代码
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名      : keyApp.c
4作者          : 邓涛
5版本          : V1.0
6描述          : 按键测试应用程序
7其他          : 无
8使用方法      : ./keyApp /dev/key
9论坛          : <a href="www.openedv.com" target="_blank" style="">www.openedv.com</a>
10日志          : 初版V1.0 2019/1/30 邓涛创建
11***************************************************************/
12
13 #include <stdio.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17 #include <fcntl.h>
18 #include <stdlib.h>
19 #include <string.h>
20
21 /*
22* @description                        : main主程序
23* @param – argc                        : argv数组元素个数
24* @param – argv                        : 具体参数
25* @return                        : 0 成功;其他 失败
26*/
27 int main(int argc, char *argv[])
28 {
29   int fd, ret;
30   int key_val;
31
32   /* 判断传参个数是否正确 */
33   if(2 != argc) {
34         printf("Usage:\n"
35                "\t./keyApp /dev/key\n"
36               );
37         return -1;
38   }
39
40   /* 打开设备 */
41   fd = open(argv, O_RDONLY);
42   if(0 > fd) {
43         printf("ERROR: %s file open failed!\n", argv);
44         return -1;
45   }
46
47   /* 循环读取按键数据 */
48   for ( ; ; ) {
49
50         read(fd, &key_val, sizeof(int));
51         if (0x0 == key_val)
52             printf("PS_KEY0 Press, value = 0x%x\n", key_val);
53   }
54
55   /* 关闭设备 */
56   close(fd);
57   return 0;
58 }</font>       第48~53行,循环读取/dev/key文件,也就是循环读取按键值,如果读取到的值为0,则表示是一次有效的按键(按下之后松开标记为一次有效状态),并打印信息出来。
       1.4运行测试
1.4.1编译驱动程序和测试APP
       1、编译驱动程序
       编写Makefile文件,将10_mutex实验目录下的Makefile文件拷贝到本实验目录下,打开Makefile文件,将obj-m变量的值改为key.o,修改完之后Makefile内容如下所示:
<font size="2">示例代码 31.4.1 Makefile.c文件内容
1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
2
3 obj-m := key.o
4
5 all:
6               make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules
7
8 clean:
9               make -C $(KERN_DIR) M=`pwd` clean</font>       第3行,设置obj-m变量的值为key.o。
       Makefile文件修改完成之后保存退出,在本实验目录下输入如下命令编译出驱动模块文件:
make
       编译成功以后就会生成一个名为“key.ko”的驱动模块文件。
       2、编译测试APP
       输入如下命令编译测试keyApp.c这个测试程序:
arm-linux-gnueabihf-gcc keyApp.c -o keyApp       编译成功以后就会生成keyApp这个应用程序。
1.4.2运行测试
       将上一小节编译出来的key.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录中,重启开发板,进入到目录/lib/modules/4.14.0-xilinx中,输入如下命令加载key.ko驱动模块:
<font size="2">depmod                              //第一次加载驱动的时候需要运行此命令
modprobe key.ko                //加载驱动</font>如下所示:

图 31.4.1 加载key驱动模块
       驱动加载成功以后如下命令来测试:
./keyApp /dev/key       按下开发板上的PS_KEY0按键,keyApp就会获取并且输出按键信息,如图 31.4.2所示:

图 31.4.2 打印按键值
       从图 31.4.2可以看出,当我们按下PS_KEY0再松开以后就会打印出“KEY0 Press, value = 0x0”,表示这是一次完整的按键按下、松开事件。但是大家在测试过程可能会发现,有时候按下PS_KEY0会输出好几行“KEY0 Press, value = 0x0”,这是因为我们的代码没有做按键消抖处理,是属于正常情况。
       如果要卸载驱动的话输入如下命令即可:
rmmod key.ko

页: [1]
查看完整版本: 《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十一章 Linux按键