本系列文章所编写的驱动源码仓库,欢迎Star~
​https://github.com/Mculover666/linux_driver_study​​。


一、按键原理图

正点原子alpha开发板板载了两个按键,一个复位按键,一个用户按键,用户按键原理图如下:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux

按键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

使用新的设备树重新启动:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux驱动_02

三、编写按键驱动

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. 测试

编译驱动模块,加载:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux驱动_03

按下开发板按键,可以看到驱动模块打印的数据:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_按键检测_04

按键中断是完成了,但是其中还有部分抖动情况,要添加去抖功能。

四、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. 测试结果

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_按键检测_05

果然,返回结果表示不支持设置去抖时长,只能用软件定时器消抖

五、给应用传递键值——原子变量

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. 测试

找到设备节点:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_imx6ull_06

运行测试程序:

./test_key /dev/key0

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux驱动_07

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

参考:​​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. 测试

加载驱动模块:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux_08

运行:

i.MX6ULL驱动开发 | 18 - 使用中断方式检测按键_linux驱动_09