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内核定时器的编程方法与使用示例

i.MX6ULL驱动开发 | 37 - FT5426电容触摸屏幕调试并修复驱动问题(基于linux 5.4.32内核)

i.MX6ULL驱动开发 | 20 - Linux input 子系统