zl程序教程

您现在的位置是:首页 >  系统

当前栏目

【正点原子FPGA连载】第三十章Linux按键输入实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Linux开发指南

Linux嵌入式输入开发 指南 实验 FPGA 原子
2023-09-11 14:20:38 时间

1)实验平台:正点原子MPSoC开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=692450874670
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html

第三十章Linux按键输入实验

在前几章我们都是使用的GPIO输出功能,还没有用过GPIO输入功能,本章我们就来学习一下如何在Linux下编写GPIO输入驱动程序。ZYNQ MPSoC开发板上有4个用户按键,本章我们就以PS_KEY1按键为例,使用此按键来完成GPIO输入驱动程序,同时利用第二十五章讲的互斥锁来对按键值进行保护。

26.1Linux下按键驱动原理

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

26.2硬件原理图分析

打开ZYNQ MPSoC底板原理图,找到PS_KEY1按键原理图,如下所示:
在这里插入图片描述

图 26.2.1 PS_KEY按键原理图
在这里插入图片描述

图 26.2.2 PS_KEY按键引脚
从原理图可知,当PS_KEY1按键按下时,对应的管脚MIO40为低电平状态,松开的时候MIO40为高电平状态,所以可以通过读取MIO40管脚的电平状态来判断按键是否被按下或松开!

26.3实验程序编写

本实验对应的例程路径为:开发板光盘资料(A盘)\4_SourceCode\ 3_Embedded_Linux\Linux驱动例程\10_key。
26.3.1修改设备树文件
打开system-user.dtsi文件,在根节点“/”下创建一个按键节点,节点名为“key”,节点内容如下:

示例代码26.3.1.1创建key节点
 19 key {
 20     compatible = "alientek,key";
 21     status = "okay";
 22     key-gpio = <&gpio 40 GPIO_ACTIVE_LOW>;
 23 };
这个节点内容

很简单。
第20行,设置节点的compatible属性为“alientek,key”。
第22行,key-gpio属性指定了PS_KEY1按键所使用的GPIO。
设备树编写完成以后使用,在linux内核源码目录下执行下面这条命令重新编译设备树:
make dtbs
在这里插入图片描述

图 26.3.1 重新编译设备树
然后将新编译出来的system-top.dtb文件重命名为system.dtb,将system.dtb文件拷贝到SD启动卡的fat分区,替换以前的system.dtb文件(先将sd卡中原来的system.dtb文件删除),替换完成插入开发板,然后开发板重新上电启动。启动成功以后进入“/proc/device-tree”目录中查看“key”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图 26.3.2所示:
在这里插入图片描述

图 26.3.2 key节点
26.3.2按键驱动程序编写
设备树准备好以后就可以编写驱动程序了,在drivers目录下新建名为“10_key”的文件夹,然后在10_key文件夹里面新建一个名为key.c的源文件,在key.c里面输入如下内容:

示例代码26.3.2.1 key.c文件代码
1   /***************************************************************
2   Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3   文件名    : key.c
4   作者      : 邓涛
5   版本      : V1.0
6   描述      : Linux按键输入驱动实验
7   其他      : 无
8   论坛      : www.openedv.com
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/uaccess.h>
21  #include <asm/io.h>
22  #include <linux/cdev.h>
23  #include <linux/of.h>
24  #include <linux/of_address.h>
25  #include <linux/of_gpio.h>
26  
27  #define KEY_CNT     1       /* 设备号个数 */
28  #define KEY_NAME    "key"   /* 名字 */
29  
30  /* dtskey设备结构体 */
31  struct key_dev {
32      dev_t devid;               /* 设备号 */
33      struct cdev cdev;           /* cdev */
34      struct class *class;          /* 类 */
35      struct device *device;       /* 设备 */
36      int major;                 /* 主设备号 */
37      int minor;                 /* 次设备号 */
38      struct device_node *nd;      /* 设备节点 */
39      int key_gpio;               /* GPIO编号 */
40      int key_val;                /* 按键值 */
41      struct mutex mutex;         /* 互斥锁 */
42  };
43  
44  static struct key_dev key;      /* led设备 */
45  
46  /*
47   * @description     : 打开设备
48   * @param – inode : 传递给驱动的inode
49   * @param – filp  : 设备文件,file结构体有个叫做private_data的成员变量
50   *                    一般在open的时候将private_data指向设备结构体。
51   * @return          : 0 成功;其他 失败
52   */
53  static int key_open(struct inode *inode, struct file *filp)
54  {
55      return 0;
56  }
57  
58  /*
59   * @description     : 从设备读取数据 
60   * @param – filp    : 要打开的设备文件(文件描述符)
61   * @param – buf    : 返回给用户空间的数据缓冲区
62   * @param – cnt    : 要读取的数据长度
63   * @param – offt    : 相对于文件首地址的偏移
64   * @return         : 读取的字节数,如果为负值,表示读取失败
65   */
66  static ssize_t key_read(struct file *filp, char __user *buf,
67              size_t cnt, loff_t *offt)
68  {
69      int ret = 0;
70  
71      /* 互斥锁上锁 */
72      if (mutex_lock_interruptible(&key.mutex))
73          return -ERESTARTSYS;
74  
75      /* 读取按键数据 */
76      if (!gpio_get_value(key.key_gpio)) {
77          while(!gpio_get_value(key.key_gpio));
78          key.key_val = 0x0;
79      } else
80          key.key_val = 0xFF;
81  
82      /* 将按键数据发送给应用程序 */
83      ret = copy_to_user(buf, &key.key_val, sizeof(int));
84  
85      /* 解锁 */
86      mutex_unlock(&key.mutex);
87  
88      return ret;
89  }
90  
91  /*
92   * @description     : 向设备写数据 
93   * @param – filp    : 设备文件,表示打开的文件描述符
94   * @param – buf    : 要写给设备写入的数据
95   * @param – cnt    : 要写入的数据长度
96   * @param – offt    : 相对于文件首地址的偏移
97   * @return         : 写入的字节数,如果为负值,表示写入失败
98   */
99  static ssize_t key_write(struct file *filp, const char __user *buf,
100             size_t cnt, loff_t *offt)
101 {
102     return 0;
103 }
104 
105 /*
106  * @description     : 关闭/释放设备
107  * @param – filp    : 要关闭的设备文件(文件描述符)
108  * @return         : 0 成功;其他 失败
109  */
110 static int key_release(struct inode *inode, struct file *filp)
111 {
112     return 0;
113 }
114 
115 /* 设备操作函数 */
116 static struct file_operations key_fops = {
117     .owner     = THIS_MODULE,
118     .open      = key_open,
119     .read      = key_read,
120     .write      = key_write,
121     .release    = key_release,
122 };
123 
124 static int __init mykey_init(void)
125 {
126     const char *str;
127     int ret;
128 
129     /* 初始化互斥锁 */
130     mutex_init(&key.mutex);
131 
132     /* 1.获取key节点 */
133     key.nd = of_find_node_by_path("/key");
134     if(NULL == key.nd) {
135         printk(KERN_ERR "key: Failed to get key node\n");
136         return -EINVAL;
137     }
138 
139     /* 2.读取status属性 */
140     ret = of_property_read_string(key.nd, "status", &str);
141     if(!ret) {
142         if (strcmp(str, "okay"))
143             return -EINVAL;
144     }
145 
146     /* 3.获取compatible属性值并进行匹配 */
147     ret = of_property_read_string(key.nd, "compatible", &str);
148     if(ret) {
149         printk(KERN_ERR "key: Failed to get compatible property\n");
150         return ret;
151     }
152 
153     if (strcmp(str, "alientek,key")) {
154         printk(KERN_ERR "key: Compatible match failed\n");
155         return -EINVAL;
156     }
157 
158     printk(KERN_INFO "key: device matching successful!\r\n");
159 
160     /* 4.获取设备树中的key-gpio属性,得到按键所使用的GPIO编号 */
161     key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
162     if(!gpio_is_valid(key.key_gpio)) {
163         printk(KERN_ERR "key: Failed to get key-gpio\n");
164         return -EINVAL;
165     }
166 
167     printk(KERN_INFO "key: key-gpio num = %d\r\n", key.key_gpio);
168 
169     /* 5.申请GPIO */
170     ret = gpio_request(key.key_gpio, "Key Gpio");
171     if (ret) {
172         printk(KERN_ERR "key: Failed to request key-gpio\n");
173         return ret;
174     }
175 
176     /* 6.将GPIO设置为输入模式 */
177     gpio_direction_input(key.key_gpio);
178 
179     /* 7.注册字符设备驱动 */
180      /* 创建设备号 */
181     if (key.major) {
182         key.devid = MKDEV(key.major, 0);
183         ret = register_chrdev_region(key.devid, KEY_CNT, KEY_NAME);
184         if (ret)
185             goto out1;
186     } else {
187         ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);
188         if (ret)
189             goto out1;
190 
191         key.major = MAJOR(key.devid);
192         key.minor = MINOR(key.devid);
193     }
194 
195     printk(KERN_INFO "key: major=%d, minor=%d\r\n", key.major, key.minor);
196 
197      /* 初始化cdev */
198     key.cdev.owner = THIS_MODULE;
199     cdev_init(&key.cdev, &key_fops);
200 
201      /* 添加cdev */
202     ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
203     if (ret)
204         goto out2;
205 
206      /* 创建类 */
207     key.class = class_create(THIS_MODULE, KEY_NAME);
208     if (IS_ERR(key.class)) {
209         ret = PTR_ERR(key.class);
210         goto out3;
211     }
212 
213      /* 创建设备 */
214     key.device = device_create(key.class, NULL,
215                 key.devid, NULL, KEY_NAME);
216     if (IS_ERR(key.device)) {
217         ret = PTR_ERR(key.device);
218         goto out4;
219     }
220 
221     return 0;
222 
223 out4:
224     class_destroy(key.class);
225 
226 out3:
227     cdev_del(&key.cdev);
228 
229 out2:
230     unregister_chrdev_region(key.devid, KEY_CNT);
231 
232 out1:
233     gpio_free(key.key_gpio);
234 
235     return ret;
236 }
237 
238 static void __exit mykey_exit(void)
239 {
240     /* 注销设备 */
241     device_destroy(key.class, key.devid);
242 
243     /* 注销类 */
244     class_destroy(key.class);
245 
246     /* 删除cdev */
247     cdev_del(&key.cdev);
248 
249     /* 注销设备号 */
250     unregister_chrdev_region(key.devid, KEY_CNT);
251 
252     /* 释放GPIO */
253     gpio_free(key.key_gpio);
254 }
255 
256 /* 驱动模块入口和出口函数注册 */
257 module_init(mykey_init);
258 module_exit(mykey_exit);
259 
260 MODULE_AUTHOR("DengTao <773904075@qq.com>");
261 MODULE_DESCRIPTION("Alientek Gpio Key Driver");
262 MODULE_LICENSE("GPL");
第31~42行,结构体key_dev为按键的设备结构体,第39行的key_gpio表示按键对应的GPIO编号,第40行key_val用来保存读取到的按键值,第41行定义了一个互斥锁变量mutex,用来保护按键读取过程。

第66~89行,在key_read函数中,通过gpio_get_value函数读取按键值,如果当前为按下状态,则使用while循环等待按键松开,松开之后将key_val变量置为0x0,从按键按下状态到松开状态视为一次有效状态;如果当前为松开状态,则将key_val变量置为0xFF,表示为无效状态。使用copy_to_user函数将key_val值发送给上层应用;第72行,调用mutex_lock_interruptible函数上锁(互斥锁),第86行解锁,对整个读取按键过程进行保护,因为在于用于保存按键值的key_val是一个全局变量,如果上层有多个应用对按键进行了读取操作,将会出现第二十五章说到的并发访问,这对系统来说是不利的,所以这里使用了互斥锁进行了保护。应用程序通过read函数读取按键值的时候key_read函数就会执行!
第130行,调用mutex_init函数初始化互斥锁。
第177行,调用gpio_direction_input函数将按键对应的GPIO设置为输入模式。
key.c文件代码很简单,重点就是key_read函数读取按键值,要对读取过程进行保护。
26.3.3编写测试APP
在本章实验目录下新建名为keyApp.c的文件,然后输入如下所示内容:

示例代码26.3.3.1 keyApp.c文件代码
1  #include <stdio.h>
2  #include <unistd.h>
3  #include <sys/types.h>
4  #include <sys/stat.h>
5  #include <fcntl.h>
6  #include <stdlib.h>
7  #include <string.h>
8  
9  /*
10  * @description         : main主程序
11  * @param - argc        : argv数组元素个数
12  * @param - argv        : 具体参数
13  * @return              : 0 成功;其他 失败
14  */
15 int main(int argc, char *argv[])
16 {
17     int fd, ret;
18     int key_val;
19 
20     /* 判断传参个数是否正确 */
21     if(2 != argc) {
22         printf("Usage:\n"
23                "\t./keyApp /dev/key\n"
24                 );
25         return -1;
26     }
27 
28     /* 打开设备 */
29     fd = open(argv[1], O_RDONLY);
30     if(0 > fd) {
31         printf("ERROR: %s file open failed!\n", argv[1]);
32         return -1;
33     }
34 
35     /* 循环读取按键数据 */
36     for ( ; ; ) {
37 
38         read(fd, &key_val, sizeof(int));
39         if (0x0 == key_val)
40             printf("PS_KEY1 Press, value = 0x%x\n", key_val);
41     }
42 
43     /* 关闭设备 */
44     close(fd);
45     return 0;
46 }
第36~41行,循环读取/dev/key文件,也就是循环读取按键值,如果读取到的值为0,则表示是一次有效的按键(按下之后松开标记为一次有效状态),并打印信息出来。

26.4运行测试
26.4.1编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,将9_mutex实验目录下的Makefile文件拷贝到本实验目录下,打开Makefile文件,将obj-m变量的值改为key.o,修改完之后Makefile内容如下所示:

示例代码26.4.1.1 Makefile.c文件内容
KERN_DIR := /home/shang/git.d/linux-xlnx
obj-m := key.o
all:
	make -C $(KERN_DIR) M=`pwd` modules
clean:
	make -C $(KERN_DIR) M=`pwd` clean

Makefile文件修改完成之后保存退出,在本实验目录下输入如下命令编译出驱动模块文件:
make
编译成功以后就会生成一个名为“key.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译测试keyApp.c这个测试程序:
$CC keyApp.c -o keyApp
编译成功以后就会生成keyApp这个应用程序。
26.4.2运行测试
使用scp命令将上一小节编译出来的key.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.19.0目录中,重启开发板,进入到目录/lib/modules/4.19.0中,输入如下命令加载key.ko驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe key.ko //加载驱动
如下所示:
在这里插入图片描述

图 26.4.1 加载key驱动模块
驱动加载成功以后如下命令来测试:
./keyApp /dev/key
按下开发板上的PS_KEY1按键,keyApp就会获取并且输出按键信息,如下图所示:
在这里插入图片描述

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