本帖最后由 正点原子运营 于 2023-6-12 11:30 编辑
第二十六章 Linux按键输入实验 1)实验平台:正点原子 DFZU2EG_4EV MPSoC开发板
2) 章节摘自【正点原子】DFZU2EG_4EV MPSoC开发板之嵌入式Linux 驱动开发指南 V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=692450874670
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz-MPSOC.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)Linux技术交流QQ群:299746173
在前几章我们都是使用的GPIO输出功能,还没有用过GPIO输入功能,本章我们就来学习一下如何在Linux下编写GPIO输入驱动程序。ZYNQ MPSoC开发板上有4个用户按键,本章我们就以PS_KEY1按键为例,使用此按键来完成GPIO输入驱动程序,同时利用第二十五章讲的互斥锁来对按键值进行保护。
26.1 Linux下按键驱动原理按键驱动和LED驱动原理上来讲基本都是一样的,都是操作GPIO,只不过一个是读取GPIO的高低电平,一个是从GPIO输出高低电平。本章我们实现按键输入,在驱动程序中实现read函数,读取按键值并将数据发送给上层应用测试程序,在read函数中,使用了互斥锁对读数据过程进行了保护,后面会讲解为什么使用互斥锁进行保护。Linux下的按键驱动原理很简单,接下来开始编写驱动。
注意,本章例程只是为了演示Linux下GPIO输入驱动的编写,实际中的按键驱动并不会采用本章中所讲解的方法,Linux下的input子系统专门用于输入设备!
26.2 硬件原理图分析打开ZYNQ MPSoC底板原理图,找到PS_KEY1按键原理图,如下所示: 从原理图可知,当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内核源码目录下执行下面这条命令重新编译设备树: 然后将新编译出来的system-top.dtb文件重命名为system.dtb,将system.dtb文件拷贝到SD启动卡的fat分区,替换以前的system.dtb文件(先将sd卡中原来的system.dtb文件删除),替换完成插入开发板,然后开发板重新上电启动。启动成功以后进入“/proc/device-tree”目录中查看“key”节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证),结果如图 26.3.2所示: 26.3.2 按键驱动程序编写设备树准备好以后就可以编写驱动程序了,在drivers目录下新建名为“10_key”的文件夹,然后在10_key文件夹里面新建一个名为key.c的源文件,在key.c里面输入如下内容: - 示例代码26.3.2.1key.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 #defineKEY_CNT 1 /* 设备号个数 */
- 28 #defineKEY_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 intmajor; /* 主设备号*/
- 37 intminor; /* 次设备号*/
- 38 struct device_node *nd; /*设备节点 */
- 39 intkey_gpio; /* GPIO编号*/
- 40 intkey_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 intret = 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 intret;
- 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 keynode\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 compatibleproperty\n");
- 150 return ret;
- 151 }
- 152
- 153 if(strcmp(str, "alientek,key")){
- 154 printk(KERN_ERR"key: Compatible matchfailed\n");
- 155 return -EINVAL;
- 156 }
- 157
- 158 printk(KERN_INFO"key: device matchingsuccessful!\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 getkey-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 requestkey-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("AlientekGpio 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.1keyApp.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_KEY1Press, 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 编译驱动程序和测试APP1、编译驱动程序 编写Makefile文件,将9_mutex实验目录下的Makefile文件拷贝到本实验目录下,打开Makefile文件,将obj-m变量的值改为key.o,修改完之后Makefile内容如下所示: - 示例代码26.4.1.1Makefile.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文件修改完成之后保存退出,在本实验目录下输入如下命令编译出驱动模块文件: 编译成功以后就会生成一个名为“key.ko”的驱动模块文件。
2、编译测试APP 输入如下命令编译测试keyApp.c这个测试程序: 编译成功以后就会生成keyApp这个应用程序。
26.4.2 运行测试使用scp命令将上一小节编译出来的key.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.19.0目录中,重启开发板,进入到目录/lib/modules/4.19.0中,输入如下命令加载key.ko驱动模块: - depmod //第一次加载驱动的时候需要运行此命令
- modprobe key.ko //加载驱动
复制代码如下所示: 驱动加载成功以后如下命令来测试: 按下开发板上的PS_KEY1按键,keyApp就会获取并且输出按键信息,如下图所示: 从上图可以看出,当我们按下PS_KEY1再松开以后就会打印出“PS_KEY1 Press, value = 0x0”,表示这是一次完整的按键按下、松开事件。但是大家在测试过程可能会发现,有时候按下PS_KEY1会输出好几行“PS_KEY1 Press, value = 0x0”,这是因为我们的代码没有做按键消抖处理,是属于正常情况。
如果要卸载驱动的话输入如下命令即可: |