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 <stdint.h>

int main(int argc, char *argv[])
{
    
    
    int fd;
    int ret;
    int keyval;

    // 检查参数
    if (argc != 2) {
    
    
        printf("usage: ./test_key [device]\n");
        return -1;
    }

    fd = open(argv[1], O_RDWR);

    while (1) {
    
    
        ret = read(fd, &keyval, sizeof(keyval));
        if (ret < 0) {
    
    
            // 键值无效
        } else {
    
    
            if (keyval == 0x01) {
    
    
                printf("key 0 press!\n");
            }
        }
    }
}

编译:

arm-linux-gnueabihf-gcc test_key.c -o test_key

5. 测试

找到设备节点:

运行测试程序:

./test_key /dev/key0

六、使用软件定时器进行消抖

参考:i.MX6ULL驱动开发 | 19 - Linux内核定时器的编程方法与使用示例

1. 优化驱动

引入头文件:

#include <linux/timer.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 debounce_interval;      /*!< 去抖时长           */
    atomic_t    keyval;         /*!< 键值               */
    atomic_t    press;          /*!< 标志按键是否按下    */
    struct timer_list  timer;   /*!< 用于软件消抖        */

    int irq;                    /*!< 中断号             */
};

修改按键中断处理函数,如果使用软件定时器消抖,则在中断中重启定时器:

static irqreturn_t key0_handler(int irq, void *dev_id)
{
    
    
    int val;
    struct key_dev *dev = (struct key_dev *)dev_id;

    if (dev->debounce_interval) {
    
    
        // 启动软件定时器, 消抖时间后再去读取
        mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->debounce_interval));
        dev->timer.data = (volatile long)dev_id;
    } else {
    
    
        // 立即读取
        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);
}

编写定时器中断处理函数:

static void timer_handler(unsigned long arg)
{
    
    
    int val;
    struct key_dev *dev = (struct key_dev *)arg;

    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);
    }
}

在按键初始化函数中,首先判断设备树有没有描述消抖时长,如果有则优先通过gpio子系统设置,gpio子系统不支持再使用软件定时器消抖:

// 设置去抖时长
if (debounce_interval) {
    
    
    ret = gpio_set_debounce(key.gpio, key.debounce_interval * 1000);
    if (ret < 0) {
    
    
        printk("gpio_set_debounce not support, use soft timer!\n");
        
        // 设置软件定时器用于消抖
        init_timer(&key.timer);
        key.timer.function = timer_handler;
        key.debounce_interval = debounce_interval;
    }
}

在按键去初始化函数中,将软件定时器从内核删除:

// 删除定时器
del_timer(&key.timer);

优化完成,重新编译。

2. 测试

加载驱动模块:

运行:

猜你喜欢

转载自blog.csdn.net/Mculover666/article/details/124267028