15 Linux 按键

一、Linux 按键驱动原理

  其实案件驱动和 LED 驱动很相似,只不过区别在于,一个是读取GPIO高低电平,一个是从GPIO输出高低电平。

  在驱动程序中使用一个整形变量来表示按键值,应用程序通过 read 函数来读取按键值,判断按键有没有按下。在这里,这个保存按键值的变量就是个共享资源,驱动程序要向其写入按键值,应用程序要读取按键值。所以我们要对其进行保护,对于整形变量而言我们首选的就是原子操作,使用原子操作对变量进行赋值以及读取。  

  这个例程只是演示 Linux 下的 GPIO 输入驱动,实际上有一个 Input 子系统,这个子系统是专门用于输入设备。

 

二、硬件原理图分析

  开发板一共就四个按键,一个是 RESET 按键,比较特殊。另外三个分别是 KEY0、KEY1 和 WK_UP,原理图如下:

  KEY0 接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 PG3 应该是高电平,当 KEY0 按下以后 PG3 就是低电平。 

 

三、实验程序编写

  首先修改设备树:在 "/" 根节点下创建 KEY 节点,节点内容如下:

key {
		compatible = "alientek,key";
		status = "okay";
		key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
	};

  修改完的设备树需要编译后复制:

cd /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts
cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/

  之后在 /linux/atk-mpl/Drivers 目录下创建 11_key,并创建好 Vscode 工程文件。创建 key.c 文件:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*/

/* 定义按键值 */
#define KEY0VALUE		0XF0	/* 按键值 		*/
#define INVAKEY			0X00	/* 无效的按键值  */

/* key设备结构体 */
struct key_dev{
	dev_t devid;			/* 设备号 	 */
	struct cdev cdev;		/* cdev 	*/
	struct class *class;	/* 类 		*/
	struct device *device;	/* 设备 	 */
	int major;				/* 主设备号	  */
	int minor;				/* 次设备号   */
	struct device_node	*nd; /* 设备节点 */
	int key_gpio;			/* key所使用的GPIO编号		*/
	atomic_t keyvlaue;		// 按键值(被原子操作保护的)	
};

static struct key_dev keydev;		/* key设备 */

// 为什么这里这个放在这,是因为我们每打开设备的时候都初始化一次按键
/*
 * @description	: 初始化按键IO,open函数打开驱动的时候
 * 				  初始化按键所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	int ret;
	const char *str;
	
	/* 设置LED所使用的GPIO */
	/* 1、获取设备节点:keydev */
	keydev.nd = of_find_node_by_path("/key");
	if(keydev.nd == NULL) {
		printk("keydev node not find!\r\n");
		return -EINVAL;
	}

	/* 2.读取status属性 */
	ret = of_property_read_string(keydev.nd, "status", &str);
	if(ret < 0) 
	    return -EINVAL;

	if (strcmp(str, "okay"))
        return -EINVAL;
    
	/* 3、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(keydev.nd, "compatible", &str);
	if(ret < 0) {
		printk("keydev: Failed to get compatible property\n");
		return -EINVAL;
	}

    if (strcmp(str, "alientek,key")) {
        printk("keydev: Compatible match failed\n");
        return -EINVAL;
    }

	/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */
	keydev.key_gpio = of_get_named_gpio(keydev.nd, "key-gpio", 0);
	if(keydev.key_gpio < 0) {
		printk("can't get key-gpio");
		return -EINVAL;
	}
	printk("key-gpio num = %d\r\n", keydev.key_gpio);

	/* 5.向gpio子系统申请使用GPIO */
	ret = gpio_request(keydev.key_gpio, "KEY0");
    if (ret) {
        printk(KERN_ERR "keydev: Failed to request key-gpio\n");
        return ret;
	}

	/* 6、设置PG3输入模式 */
	ret = gpio_direction_input(keydev.key_gpio);
	if(ret < 0) {
		printk("can't set gpio!\r\n");
		return ret;
	}
	return 0;
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int key_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp->private_data = &keydev; 	/* 设置私有数据 */

	ret = keyio_init();				/* 初始化按键IO */
	if (ret < 0) {
		return ret;
	}

	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	int value;
	struct key_dev *dev = filp->private_data;

	if (gpio_get_value(dev->key_gpio) == 0) { 		/* key0按下 */
		while(!gpio_get_value(dev->key_gpio));		/* 等待按键释放 */
		atomic_set(&dev->keyvalue, KEY0VALUE);		// 按键按下并释放后会赋值 0xF0
	} else {	
		atomic_set(&dev->keyvalue, INVAKEY);		/* 无效的按键值 */ // 没有按下则赋值 0x00
	}

	value = atomic_read(&dev->keyvalue);
	ret = copy_to_user(buf, &value, sizeof(value));
	return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	struct key_dev *dev = filp->private_data;
	gpio_free(dev->key_gpio);
	
	return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = 	key_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init mykey_init(void)
{
	int ret;
	/* 1、初始化原子变量    */
	keydev.keyvalue= (atomic_t)ATOMIC_INIT(0);

	/* 2、原子变量初始值为INVAKEY */
	atomic_set(&keydev.keyvalue, INVAKEY);

	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	if (keydev.major) {		/*  定义了设备号 */
		keydev.devid = MKDEV(keydev.major, 0);
		ret = register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
		if(ret < 0) {
			pr_err("cannot register %s char driver [ret=%d]\n", KEY_NAME, KEY_CNT);
			return -EIO;
		}
	} else {						/* 没有定义设备号 */
		ret = alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
		if(ret < 0) {
			pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);
			return -EIO;
		}
		keydev.major = MAJOR(keydev.devid);	/* 获取分配号的主设备号 */
		keydev.minor = MINOR(keydev.devid);	/* 获取分配号的次设备号 */
	}
	printk("keydev major=%d,minor=%d\r\n",keydev.major, keydev.minor);	
	
	/* 2、初始化cdev */
	keydev.cdev.owner = THIS_MODULE;
	cdev_init(&keydev.cdev, &key_fops);
	
	/* 3、添加一个cdev */
	cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
	if(ret < 0)
		goto del_unregister;
		
	/* 4、创建类 */
	keydev.class = class_create(THIS_MODULE, KEY_NAME);
	if (IS_ERR(keydev.class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
	if (IS_ERR(keydev.device)) {
		goto destroy_class;
	}
	return 0;

destroy_class:
	device_destroy(keydev.class, keydev.devid);
del_cdev:
	cdev_del(&keydev.cdev);
del_unregister:
	unregister_chrdev_region(keydev.devid, KEY_CNT);
	return -EIO;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit mykey_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&keydev.cdev);/*  删除cdev */
	unregister_chrdev_region(keydev.devid, KEY_CNT); /* 注销设备号 */

	device_destroy(keydev.class, keydev.devid);
	class_destroy(keydev.class);
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

  编写测试 APP:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"

/* 定义按键值 */
#define KEY0VALUE	0XF0
#define INVAKEY		0X00

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, ret;
	char *filename;
	int keyvalue;
	
	if(argc != 2){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 打开key驱动 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}

	/* 循环读取按键值数据! */
	while(1) {
		read(fd, &keyvalue, sizeof(keyvalue));
		if (keyvalue == KEY0VALUE) {	/* KEY0 */
			printf("KEY0 Press, value = %#X\r\n", keyvalue);	/* 按下 */
		}
	}

	ret= close(fd); /* 关闭文件 */
	if(ret < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

 

四、运行测试

  编写 Makefile 文件:

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31	# Linux内核源码路径
CURRENT_PATH := $(shell pwd)

obj-m := key.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  编译 key.c 和 keyApp.c 文件:

make
arm-none-linux-gnueabihf-gcc keyApp.c -o keyApp

  将编译出来的 key.ko 和 keyApp 文件复制:

sudo cp keyApp key.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f

  开启开发板,输入以下命令:

cd /lib/modules/5.4.31
depmod
modprobe key.ko

  加载驱动:

./keyApp /dev/key

  按下开发板上的 KYE0,终端就会显示:

  按下 KEY0 打印  KEY0 Press, value = 0XF0 ,表示按键按下,但有时候按一次 KEY0 输出好几行,是因为没有坐消抖处理。

  卸载驱动:

rmmod key.ko

  总结:其实这跟 LED 驱动很类似,但后面会学到 Input 子系统,会让这个过程更加简单。

本文作者:烟儿公主

本文链接:https://www.cnblogs.com/toutiegongzhu/p/17657231.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   烟儿公主  阅读(102)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 夏日大冒险 暴躁的兔子
夏日大冒险 - 暴躁的兔子
00:00 / 00:00
An audio error has occurred.

作词 : 暴躁的兔子

作曲 : 暴躁的兔子

编曲 : IOF

混音:Gfanfan

出品:网易飓风

夏天 不要再浪费时间

实现 你承诺过的改变

别再 找一堆借口拖延

现在就和我一起飞向海边

人生苦短 你应该学会如何作乐

低着头还怎么应对挫折

人应该为自己活着

不用去迎合

要去寻欢作乐

撮合我的浪漫和悲欢

把这荒诞人生都塞满

生活难免磕磕绊绊

对抗生活的平庸就是浪漫

学会取悦自己逆风翻盘

去反抗变态的三观

把条条框框都砸烂

建立新的规则推翻谈判

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

去冒险

我真的不care你是否会喜欢我

不跟风被定义的美 全都是灾祸

我才不讨好大多数绝不与示弱

过好你的生活

你管我应该怎么快活

没有人能有资格审判

别人的生活和牵绊

快闭上你的高谈阔论

乘风破浪吧 理想的风帆

我就是肆意张扬又如何

我就是锋芒毕露又如何

我就是离经叛道又如何

我就是要出格 你管我要如何

我就是与众不同又如何

我就是特立独行又如何

我就是不知好歹又如何

你管我怎样出格 你管我如何

无可救药的人呐

和我一起去海边

看那日出和晚霞 海天一线

看阳光穿越地平线

现实交织的明天

就在这个夏天

为自己改变

别怕山高路远

不知进退的人呐

和我一起去海边

聊聊曾经的理想 一起想当年

那曾想改变世界的人

是否还满腔热忱

不羁的我们放肆着

反抗那命运的指针

解放灵魂

推广:网易飓风

企划:贾焱祺

监制:徐思灵

出品人:谢奇笛