i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键
Posted Mculover666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键相关的知识,希望对你有一定的参考价值。
本系列文章所编写的驱动源码仓库,欢迎Star~
https://github.com/Mculover666/linux_driver_study。
一、按键原理图
正点原子alpha开发板板载了两个按键,一个复位按键,一个用户按键,用户按键原理图如下:
按键KEY0连接到UART1_CTS引脚,并有上拉电阻。
二、在设备树中添加节点
1. 设置引脚功能及电气属性
找到 iomuxc 节点,添加按键引脚复用:
pinctrl_key0: key0grp
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080
>;
;
2. 添加key0节点
在根节点下添加key0节点:
//08-key-irq实验, 用于自己编写的KEY驱动
key0
compatible = "atk, gpio-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key0>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
;
3. 检查PIN是否被使用
设备树中搜索 UART1_CTS_B
, 未找到。
4. 编译设备树
make dtbs
使用新的设备树重新启动:
三、编写按键驱动
1. 编写模块
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init key_module_init(void)
return 0;
static void __exit key_module_exit(void)
module_init(key_module_init);
module_exit(key_module_exit);
MODULE_AUTHOR("Mculover666");
MODULE_LICENSE("GPL");
编写Makefile:
KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m = key.o
build: kernel_module
kernel_module:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
编译成功,继续下一步。
2. 编写平台设备驱动框架
引入头文件:
#include <linux/platform_device.h>
编写平台设备驱动框架:
static int gpio_key_probe(struct platform_device *pdev)
return 0;
static int gpio_key_remove(struct platform_device *pdev)
return 0;
static const struct of_device_id gpio_key_of_match[] =
.compatible = "atk, gpio-key" ,
,
;
static struct platform_driver gpio_key_device_driver =
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver =
.owner = THIS_MODULE,
.name = "gpio-key",
.of_match_table = of_match_ptr(gpio_key_of_match),
;
static int __init key_module_init(void)
return platform_driver_register(&gpio_key_device_driver);
static void __exit key_module_exit(void)
platform_driver_unregister(&gpio_key_device_driver);
3. 编写字符设备驱动框架
引入头文件:
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
封装全局变量:
struct key_dev
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
;
static struct key_dev key;
编写字符设备驱动框架:
static int key_open(struct inode *inode, struct file *fp)
return 0;
static int key_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
return 0;
static int key_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
return 0;
static int key_release(struct inode *inode, struct file *fp)
return 0;
static struct file_operations key_fops =
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
;
static int gpio_key_probe(struct platform_device *pdev)
int ret;
//分配cdev设备号
ret = alloc_chrdev_region(&key.dev, 0, 1, "key");
if (ret != 0)
printk("alloc_chrdev_region fail!");
return -1;
//初始化cdev
key.cdev = cdev_alloc();
if (!key.cdev)
printk("cdev_alloc fail!");
return -1;
//设置fop操作函数
key.cdev->owner = THIS_MODULE;
key.cdev->ops = &key_fops;
//注册cdev
cdev_add(key.cdev, key.dev, 1);
// 创建设备类
key.class = class_create(THIS_MODULE, "key_class");
if (!key.class)
printk("class_create fail!");
return -1;
//创建设备节点
key.device0 = device_create(key.class, NULL, key.dev, NULL, "key0");
if (IS_ERR(key.device0))
printk("device_create device0 fail!");
return -1;
return 0;
static int gpio_key_remove(struct platform_device *pdev)
// 将设备从内核删除
cdev_del(key.cdev);
// 释放设备号
unregister_chrdev_region(key.dev, 1);
// 删除设备节点
device_destroy(key.class, key.dev);
// 删除设备类
class_destroy(key.class);
4. 按键初始化与去初始化
引入头文件:
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
在全局变量中添加中断号:
struct key_dev
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
int irq; /*!< 中断号 */
;
编写按键初始化函数,完成以下事情:
- 从设备树中解析到gpio
- 设置gpio引脚为输入
- 请求中断
static int key_init(void)
int ret;
// 获取设备树节点
key.node = of_find_node_by_name(NULL, "key0");
if (!key.node)
printk("key0 node find fail!\\n");
return -1;
// 提取gpio
key.gpio = of_get_named_gpio(key.node, "key-gpio", 0);
if (key.gpio < 0)
printk("find key-gpio propname fail!\\n");
return -1;
// 初始化gpio
ret = gpio_request(key.gpio, "key-gpio");
if (ret < 0)
printk("gpio request fail!\\n");
return -1;
gpio_direction_input(key.gpio);
// 获取中断号
key.irq = gpio_to_irq(key.gpio);
if (key.irq < 0)
printk("gpio_to_irq fail!\\n");
gpio_free(key.gpio);
return -1;
// 申请中断
ret = request_irq(key.irq, key0_handler, IRQF_TRIGGER_FALLING, "key_irq", &key);
if (ret < 0)
printk("irq request fail, ret is %d!\\n", ret);
gpio_free(key.gpio);
return -1;
return 0;
中断函数如下:
static irqreturn_t key0_handler(int irq, void *dev_id)
int val;
struct key_dev *dev = (struct key_dev *)dev_id;
val = gpio_get_value(dev->gpio);
printk("key press on gpio %d, val is %d!\\n", dev->gpio, val);
return IRQ_RETVAL(IRQ_HANDLED);
编写按键去初始化函数:
static void key_deinit(void)
// 释放中断
free_irq(key.irq, &key);
// 释放gpio
gpio_free(key.gpio);
在 probe 函数中调用按键初始化函数:
ret = key_init();
if (ret < 0)
printk("key init fail!\\n");
return -1;
在 remove 函数中调用按键去初始化函数:
// 按键去初始化
key_deinit();
5. 测试
编译驱动模块,加载:
按下开发板按键,可以看到驱动模块打印的数据:
按键中断是完成了,但是其中还有部分抖动情况,要添加去抖功能。
四、GPIO去抖
1. GPIO子系统自带的去抖功能
GPIO子系统自带去抖功能,API如下:
/**
* gpiod_set_debounce - sets @debounce time for a @gpio
* @gpio: the gpio to set debounce time
* @debounce: debounce time is microseconds
*
* returns -ENOTSUPP if the controller does not support setting
* debounce.
*/
int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce)
第二个值 debounce 是去抖时长,单位us。
2. 设备树节点中添加去抖时长值
在设备树中添加 debounce-interval 属性,设置去抖时长,单位ms:
//08-key-irq实验, 用于自己编写的KEY驱动
key0
compatible = "atk, gpio-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key0>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>;
debounce-interval = 16;
status = "okay";
;
3. 驱动添加对去抖的支持
优化按键初始化函数,解析设备树给出的去抖时长,并通过gpio子系统设置该值:
// 解析设备树,获取去抖时长
if (of_property_read_u32(key.node, "debounce-interval", &key.debounce_interval))
key.debounce_interval = 10; // default
// 设置去抖时长
if (key.debounce_interval)
ret = gpio_set_debounce(key.gpio, key.debounce_interval * 1000);
if (ret < 0)
printk("gpio_set_debounce fail, ret is %d!\\n", ret);
4. 测试结果
果然,返回结果表示不支持设置去抖时长,只能用软件定时器消抖。
五、给应用传递键值——原子变量
1. 原子变量
在头文件include/linux/types.h
中定义:
typedef struct
int counter;
atomic_t;
原子变量的操作,针对每个架构都不同,ARM架构的在 arch/arm/include/asm/atomic.h
中。
(1)初始化原子变量
#define ATOMIC_INIT(i) (i)
(2)读取或设置原子变量的值
/*
* On ARM, ordinary assignment (str instruction) doesn't clear the local
* strex/ldrex monitor on some implementations. The reason we can use it for
* atomic_set() is the clrex or dummy strex done on every exception return.
*/
#define atomic_read(v) ACCESS_ONCE((v)->counter)
#define atomic_set(v,i) (((v)->counter) = (i))
2. 设置键值
在全局变量中添加键值原子变量,再添加一个标志位原子变量,用于表示按键按下:
struct key_dev
dev_t dev; /*!< 设备号 */
struct cdev *cdev; /*!< cdev对象 */
struct class *class; /*!< 设备类 */
struct device *device0; /*!< 设备节点 */
struct device_node *node; /*!< 设备树节点 */
int gpio; /*!< key使用的gpio编号 */
int debounce_interval; /*!< 去抖时长 */
atomic_t keyval; /*!< 键值 */
atomic_t press; /*!< 标志按键是否按下 */
int irq; /*!< 中断号 */
;
在按键初始化函数中初始化键值:
// 初始化键值与标志位
atomic_set(&key.keyval, 0xFF);
atomic_set(&key.press, 0);
定义KEY0键值:
#define KEY0_VALUE 0x01
在按键中断函数中设置键值:
static irqreturn_t key0_handler(int irq, void *dev_id)
int val;
struct key_dev *dev = (struct key_dev *)dev_id;
val = gpio_get_value(dev->gpio);
if (val == 0)
atomic_set(&dev->keyval, KEY0_VALUE);
atomic_set(&key.press, 1);
else
atomic_set(&dev->keyval, 0xFF); // 无效键值
return IRQ_RETVAL(IRQ_HANDLED);
3. 传递键值
引入头文件:
#include <linux/uaccess.h>
编写驱动:
static int key_open(struct inode *inode, struct file *fp)
fp->private_data = &key;
return 0;
static int key_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
int ret;
int keyval, press;
struct key_dev *dev = (struct key_dev *)fp->private_data;
press = atomic_read(&dev->press);
keyval = atomic_read(&dev->keyval);
if (press == 1 && keyval != 0xFF)
atomic_set(&key.press, 0);
ret = copy_to_user(buf, &keyval, sizeof(keyval));
return 0;
return -1;
static int key_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
return 0;
static int key_release(struct inode *inode, struct file *fp)
fp->private_data = NULL;
return 0;
4. 编写应用程序
应用程序使用死循环读取键值:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include 以上是关于i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键的主要内容,如果未能解决你的问题,请参考以下文章
i.MX6ULL驱动开发 | 17 - Linux中断机制及使用方法(taskletworkqueue软中断)
i.MX6ULL驱动开发 | 17 - Linux中断机制及使用方法(taskletworkqueue软中断)
i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例
i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例