按键抖动问题复现
在博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145176361 中,我们在测试按键KEY2时,发现每按一次按键,产生了3次中断,本来应该只有2次【分别为上升沿和下降沿各一次】,当时就在博文中分析出了是由于按键抖动时产生的问题,理想的电平波形应该是下面这样的:

但实际上可能成了类似下面这样的:

这个问题我一直没有去解决它,所以在博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145228617的程序中,按下KEY2时,仍然是检查到了两次下降沿中断事件,所以按一次产生了2个甚至3个按键值,如图所示:

从截图中,我们看到:
第1次按KEY2键时检测到了3个来自编号为110的GPIO产生的下降沿中断(编号为110的GPIO与KEY2相连),
第2次按KEY2键时检测到了2个来自编号为110的GPIO产生的下降沿中断(编号为110的GPIO与KEY2相连),
第3次按KEY2键时检测到了2个来自编号为110的GPIO产生的下降沿中断(编号为110的GPIO与KEY2相连),
其实我们人在操作时只按了一次,这就不符合我们的要求。
由于KEY1和KEY2的原理图是一模一样的,所以硬件的原理是没有问题的,问题应该是KEY2按键本身或与之相关的电子元件出了问题。
更换KEY2按键或与之相关的电子元件是很麻烦的,所以咱们这里就在软件上弥补电子元件上的问题吧。
主要思路是利用内核定时器,为每个按键设一个定时器,每次中断产生后,延迟一定的时间再去读按键值(实际上是去检测哪个按键按下),这样就可以实现软件去抖。
注意:为什么称为“内核定时器”?
答:因为本博文中的定时器只能在内核空间使用,不能在用户空间中使用。
本文实现的内核定时器实际上是中断下半部的软中断,关于中断上半部和下半部的概念,请参见我的另一篇博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145309140。定时器由硬中断处理函数进行定时值设置,然后硬中断处理完后(硬中断为中断上半部),再去处理定时器的软中断,这是典型的中断下半部的软中断类型。
本篇博文的代码是在哪个代码的基础上修改的?
问:本篇博文的代码是在哪个代码的基础上修改的?
答:是在博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145228617 的基础上修改而来的。
内核定时器只能在内核空间使用
为什么称为“内核定时器”?
答:因为本博文中的定时器只能在内核空间使用,不能在用户空间中使用。
完整源代码
驱动程序gpio_key_drv.c中的代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/timer.h>
struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
} ;
static struct gpio_key *gpio_keys_100ask;
/* 主设备号 */
static int major = 0;
static struct class *gpio_key_class;
static int g_key = 0;
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);
/* 环形缓冲区 */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;
#define NEXT_POS(x) ((x+1) % BUF_LEN)
static int is_key_buf_empty(void)
{
return (r == w);
}
static int is_key_buf_full(void)
{
return (r == NEXT_POS(w));
}
static void put_key(int key_value)
{
if (!is_key_buf_full())
{
g_keys[w] = key_value;
w = NEXT_POS(w);
}
}
static int get_key(void)
{
int key_value = 0;
if (!is_key_buf_empty())
{
key_value = g_keys[r];
r = NEXT_POS(r);
}
return key_value;
}
/* 实现文件操作结构体中的read函数 */
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
int err;
int key_value;
wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());
//从缓形缓冲区中取出数据
key_value = get_key();
err = copy_to_user(buf, &key_value, 4);
// 返回值为4表明读到了4字节的数据
return 4;
}
/* 定义自己的file_operations结构体 */
static struct file_operations gpio_key_drv = {
.owner = THIS_MODULE,
.read = gpio_key_drv_read,
};
// 计时器的中断处理函数
static void key_timer_expire(unsigned long data)
{
/* data ==> gpio */
struct gpio_key *gpio_key = (struct gpio_key *)data;
int val;
printk("I am key_timer_expire_fun\n");
// 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
val = gpiod_get_value(gpio_key->gpiod);
// 打印中断号、GPIO引脚编号和电平值
// printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);
// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
g_key = (gpio_key->gpio << 8) | val;
//装按键值放入环形缓冲区
put_key(g_key);
wake_up_interruptible(&gpio_key_wait);
}
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key *gpio_key = dev_id;
printk("IRQ happened: GPIO pin number: %d\n", gpio_key->gpio);
mod_timer(&gpio_key->key_timer, jiffies + msecs_to_jiffies(180));
return IRQ_HANDLED;
}
/* 1. 从platform_device获得GPIO
* 2. gpio=>irq
* 3. request_irq
*/
static int gpio_key_probe(struct platform_device *pdev)
{
int err;
// 获取设备树节点指针
struct device_node *node = pdev->dev.of_node;
// count用于存储设备树中描述的GPIO口的数量
int count;
int i;
enum of_gpio_flags flag;
unsigned flags = GPIOF_IN;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
count = of_gpio_count(node);
if (!count)
{
printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);
if (!gpio_keys_100ask) {
printk("Memory allocation failed for gpio_keys_100ask\n");
return -ENOMEM;
}
for (i = 0; i < count; i++)
{
// 获取GIPO的全局编号及其标志位信息的代码
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);
if (gpio_keys_100ask[i].gpio < 0)
{
printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);
return -1;
}
// 获取GPIO口的GPIO描述符的代码
gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
if (!gpio_keys_100ask[i].gpiod) {
printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);
return -EINVAL;
}
// 结构体gpio_key的成员flag用于存储对应的GPIO口是否是低电平有效,假如是低电平有效,成员flag的值为1,假如不是低电平有效,成员flag的值为0。
// 后续代码实际上并没有用到成员flag,这里出现这句代码只是考虑到代码的可扩展性,所以在这里是可以删除的。
gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;
// 每次循环都重新初始化flags
flags = GPIOF_IN;
// 假如GPIO口是低电平有效,则把flags添加上低电平有效的信息
if (flag & OF_GPIO_ACTIVE_LOW)
flags |= GPIOF_ACTIVE_LOW;
// 请求一个GPIO硬件资源与设备结构体`pdev->dev`进行绑定
// 注意,这个绑定操作会在调用函数platform_driver_unregister()注销platform_driver时自动由内核解除绑定操作,所以gpio_key_remove函数中不需要显示去解除绑定
// 由`devm`开头的函数通常都会内核自动管理资源,咱们在退出函数中不用人为的去释放资源或解除绑定。
err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
// 获取GPIO口的中断请求号
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
// 以下3行代码用于设置用于消除按键抖动的计时器
// setup_timer的第2个参数是计时器的时间到了后之后调用的函数,称为叫计时器的中断处理函数;
// setup_timer的第3个参数是提供给计时器的中断处理函数的存储数据的指针
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, (unsigned long)&gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0; //各个计时器刚开始的计时时间设置为无穷大,在GPIO的中断服务处理程序中再去设置具体的倒计时的值
add_timer(&gpio_keys_100ask[i].key_timer); // add_timer函数调用后计时器就开始执行了
}
for (i = 0; i < count; i++)
{
char irq_name[32]; // 用于存储动态生成的中断名称
//使用snprintf()函数将动态生成的中断名称写入irq_name数组
snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i); // 根据i生成名称
//调用函数request_irq()来请求并设置一个中断
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);
}
/* 注册file_operations */
major = register_chrdev(0, "swh_read_keys_major", &gpio_key_drv);
gpio_key_class = class_create(THIS_MODULE, "swh_read_keys_class");
if (IS_ERR(gpio_key_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "swh_read_keys_major");
return PTR_ERR(gpio_key_class);
}
// 由于这里是把多个按键看成是一个设备,你可以想像一个键盘上对应多个按键,但键盘本身是一个设备,所以只有一个设备文件
device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "read_keys0"); /* /dev/read_keys0 */
return 0;
}
static int gpio_key_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count;
int i;
device_destroy(gpio_key_class, MKDEV(major, 0));
class_destroy(gpio_key_class);
unregister_chrdev(major, "swh_read_keys_major");
count = of_gpio_count(node);
for (i = 0; i < count; i++)
{
// 释放定时器资源,经测试如果不释放的话卸载模块后系统会崩溃
del_timer_sync(&gpio_keys_100ask[i].key_timer);
// 只有在irq有效时才释放中断资源
if (gpio_keys_100ask[i].irq >= 0) {
// 释放GPIO中断资源,下面这句代码做了下面两件事:
// 1、解除 `gpio_keys_100ask[i].irq` 中断号和 `gpio_key_isr` 中断处理函数的绑定。
// 2、解除 `gpio_keys_100ask[i].irq` 中断号和中断处理函数与 `gpio_keys_100ask[i]` 数据结构的绑定。
free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);
}
// 释放GPIO描述符
if (gpio_keys_100ask[i].gpiod) {
gpiod_put(gpio_keys_100ask[i].gpiod);
}
}
// 释放内存
kfree(gpio_keys_100ask);
return 0;
}
static const struct of_device_id irq_matach_table[] = {
{ .compatible = "swh-gpio_irq_key" },
{ },
};
/* 1. 定义platform_driver */
static struct platform_driver gpio_keys_driver = {
.probe = gpio_key_probe,
.remove = gpio_key_remove,
.driver = {
.name = "swh_irq_platform_dirver",
.of_match_table = irq_matach_table,
},
};
/* 2. 在入口函数注册platform_driver */
static int __init gpio_key_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = platform_driver_register(&gpio_keys_driver);
return err;
}
/* 3. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
* 卸载platform_driver
*/
static void __exit gpio_key_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_keys_driver);
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(gpio_key_init);
module_exit(gpio_key_exit);
MODULE_LICENSE("GPL");
测试程序button_test.c中的代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <time.h>
/*
* ./button_test /dev/100ask_button0
*
*/
// 打印线程的执行函数
void* print_while_waiting(void* arg)
{
while (1)
{
printf("I am another thread, and while the main thread is waiting for a button to be pressed, I can still run normally.\n");
sleep(10); // 每隔10秒打印一次
}
return NULL;
}
int main(int argc, char **argv)
{
int fd;
int val;
pthread_t print_thread;
int keystroke = 0; //记录按键次数
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd == -1)
{
printf("Can not open file %s\n", argv[1]);
return -1;
}
// 创建一个线程,每隔一段时间打印输出一条信息表示在等待按键期间,另外的线程在继续正常执行。
if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0)
{
printf("Failed to create print thread\n");
close(fd);
return -1;
}
while (1)
{
/* 3. 读文件 */
read(fd, &val, 4);
/* 提取 GPIO 编号和逻辑值 */
int gpio_number = (val >> 8) & 0xFF; // 高8位为 GPIO 编号
int gpio_value = val & 0xFF; // 低8位为逻辑值
keystroke++;
/* 打印读到的信息 */
printf("GPIO Number: %d, Logical Value: %d\n", gpio_number, gpio_value);
printf("keystrokes is %d\n", keystroke);
}
//pthread_join的作用是使主线程等待线程print_threa结束后再继续执行剩下的代码。
//如果主线程在结束时未等待子线程完成,可能会导致未完成的资源清理或意外的程序终止。
//这里由于主线程中有个条件永远为真的while循环,实际上这句代码没有实际作用。
pthread_join(print_thread, NULL);
close(fd);
return 0;
}
驱动程序中与定时器相关的代码分析
因为我们是为每个按键都要单独设置一个定时器,所以在按键结构体中得有一个类型为struct timer_list的成员:

结构体struct timer_list中存储着每一个定时器的相关信息,timer_list 结构体的定义通常如下:
struct timer_list {
struct hlist_node entry; // 用来将定时器挂到定时器管理的链表中
unsigned long expires; // 定时器的到期时间(通常是从系统启动时刻的时间戳)
void (*function)(unsigned long); // 定时器到期时调用的回调函数
unsigned long data; // 传递给回调函数的参数
int base; // 定时器所在的定时器池
};
各个成员的意义已经说得比较清楚了,这里不再详述。
然后在platform总线驱动程序中的probe操作函数gpio_key_probe为每一个按键初始化一个定时器:

// 以下3行代码用于设置用于消除按键抖动的计时器
// setup_timer的第2个参数是计时器的时间到了后之后调用的函数,称为叫计时器的中断处理函数;
// setup_timer的第3个参数是提供给计时器的中断处理函数的存储数据的指针
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, (unsigned long)&gpio_keys_100ask[i]);
gpio_keys_100ask[i].key_timer.expires = ~0; //各个计时器刚开始的计时时间设置为无穷大,在GPIO的中断服务处理程序中再去设置具体的倒计时的值
add_timer(&gpio_keys_100ask[i].key_timer); // add_timer函数调用后计时器就开始执行了
这三行代码的作用已经被注释写得比较清楚了。
需要补充说明的是:
setup_timer的第2个参数的值key_timer_expire是内核定时器的中断处理函数(定时器计时完毕也是一种中断,只是是软中断)。第3个参数 (unsigned long)&gpio_keys_100ask[i]是传给其中断处理函数的数据,至于为什么要进行强制类型转换转换为 (unsigned long)型,你看结构体struct timer_list的第3个字段就清楚了,结构体struct timer_list的定义如下:
struct timer_list {
struct hlist_node entry; // 用来将定时器挂到定时器管理的链表中
unsigned long expires; // 定时器的到期时间(通常是从系统启动时刻的时间戳)
void (*function)(unsigned long); // 定时器到期时调用的回调函数
unsigned long data; // 传递给回调函数的参数
int base; // 定时器所在的定时器池
};
你看第3个字段是一个函数指针,对应的函数的参数类型就要求是unsigned long,所以这里要进行一次强制类型转换了。
接下来当然就是去看内核定时器的中断处理函数key_timer()了,其代码如下:
// 计时器的中断处理函数
static void key_timer_expire(unsigned long data)
{
/* data ==> gpio */
struct gpio_key *gpio_key = (struct gpio_key *)data;
int val;
printk("I am key_timer_expire_fun\n");
// 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
val = gpiod_get_value(gpio_key->gpiod);
// 打印中断号、GPIO引脚编号和电平值
// printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);
// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
g_key = (gpio_key->gpio << 8) | val;
//装按键值放入环形缓冲区
put_key(g_key);
wake_up_interruptible(&gpio_key_wait);
}
这个代码没啥好说的,其实就是把之前在博文 https://blog.youkuaiyun.com/wenhao_ir/article/details/145228617 中的GPIO中断处理函数中的代码搬过来。
GPIO中断处理函数中的代码搬过来了,那现在的GPIO中断处理函数干啥呢?来看一看其代码就知道了:
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
printk("IRQ happened: GPIO pin number: %d\n", gpio_key->gpio);
mod_timer(&gpio_key->key_timer, jiffies + msecs_to_jiffies(180));
return IRQ_HANDLED;
}
可见,就只干了一件事,即把计时器的时间值从新设置为180毫秒,相当于每来一次按键中断,我就重新开始计时,等计时到了之后我再去定时器中断中去读取一次按键值,这样就可以实现软件去抖的目的了。
这里要注意jiffies这个变量,jiffies这个变量是一个系统级的全局变量,用于记录自系统启动以来经过的“节拍(tick)”数量。它是内核计时机制的核心。jiffies 由内核初始化,并在系统启动后开始递增。
要想使用它,需要引入头文件#include <linux/timer.h>,倒不是说在头文件中定义它,而是在头文件中声明它,比如类似下面这样的声明语句:
extern unsigned long jiffies;
每个内核模块是独立编译的,并不直接共享内核代码的全局作用域,为了访问像 jiffies 这样的全局变量,模块代码需要通过头文件中的声明告诉编译器这个变量的类型和位置在外部。
而函数msecs_to_jiffies(180) 的作用是将 180 毫秒转换为对应的节拍数量。
至于为什么是180 毫秒,那是我实际试验出的最小值。虽然大家一致的经验是,在软件驱动中,5毫秒到20毫秒一般就够了,但是为什么我这里要设为180 毫秒呢?那就是因为KEY2按键或者与KEY2按键相关的电子元件出问题了,经测试KEY1是没有按键抖动的问题,而且KEY1和KEY2的电路原理图是一模一样的…这些情况都说明了是KEY2按键本身或与KEY2按键相关的电子元件出问题了。
至此,整个代码中与的“内核计时器”的使用相关的代码部分基本就介绍完毕了。
最后,还有一点要注意:那就是在platform的remove操作函数中要删除定时器,否则模块卸载后系统会崩溃。
注意:模块卸载时请删除定时器
如果在模块卸载时没有把内核计时器删除,系统会崩溃的,我曾经就忘记删除它导致系统崩溃了,如下图所示:


模块退出函数中调用了函数platform_driver_unregister,函数platform_driver_unregister是platform总线的注销函数,它会调用platform的remove操作函数,具体在这里是函数gpio_key_remove,所以我们需要在函数gpio_key_remove中删除掉计器器,相关代码如下:

注意-Linux_5.x以上对内核定时器进行了修改
我这里使用的内核是Linux-4.9.88,所以还属于旧的内核定时器。
新的内核定时器把函数setup_timer更名为timer_setup,并且第3个参数的意义也发生了变化:
旧版的setup_timer的使用示例如下:
setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, (unsigned long)&gpio_keys_100ask[i]);
新版的setup_timer的使用示例如下:
timer_setup(&gpio_keys_100ask[i].key_timer, key_timer_expire, 0);
可见,在新版的timer_setup里第3个参数不再是结构体struct timer_list key_timer的成员unsigned long data的实际值,而是一个标志位。
那在新版Linux里内核定时器的中断函数怎么获取数据呢?
首先你要知道在新版的内核定时器里,结构体struct timer_list也发生了变化,旧版的struct timer_list的定如下:
struct timer_list {
struct hlist_node entry; // 用来将定时器挂到定时器管理的链表中
unsigned long expires; // 定时器的到期时间(通常是从系统启动时刻的时间戳)
void (*function)(unsigned long); // 定时器到期时调用的回调函数
unsigned long data; // 传递给回调函数的参数
int base; // 定时器所在的定时器池
};
新版的如下:
struct timer_list {
/*
* 与定时器关联的回调函数。
* 在定时器超时时被调用,参数是指向该定时器的指针。
*/
void (*function)(struct timer_list *t);
/*
* 定时器的到期时间(以 jiffies 为单位)。
*/
unsigned long expires;
/*
* 定时器内部使用的链表节点,用于挂载到全局定时器列表。
*/
struct hlist_node entry;
/*
* 定时器的回调上下文(通常不需要手动操作)。
*/
unsigned int flags;
/*
* 用于调试的字段。
*/
#ifdef CONFIG_TIMER_STATS
void *start_site; // 定时器启动位置
char start_comm[16]; // 启动定时器的进程名
int start_pid; // 启动定时器的进程 ID
#endif
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
可见,旧版里的第3个字段:
void (*function)(unsigned long);
被修改成了:
/*
* 与定时器关联的回调函数。
* 在定时器超时时被调用,参数是指向该定时器的指针。
*/
void (*function)(struct timer_list *t);
可见,最大的区别是参数变了,在旧版里回调函数的参数是unsigned long类型,而新版里为struct timer_list *t,即新版里的这个参数就是指定该定时器的指针,那此时怎么获取数据呢?像下面这样获取【利用from_timer函数】:
static void key_timer_expire(struct timer_list *t)
{
/* data ==> gpio */
struct gpio_key *gpio_key = from_timer(gpio_key, t, key_timer);
int val;
int key;
// 返回引脚电平的逻辑值,注意:如果是低电平有效,则当物理电平为低电平时,其返回值为1;则当物理电平为高电平时,其返回值为0.
// 如果要得到物理电平值,可以用函数gpiod_get_raw_value()得到
val = gpiod_get_value(gpio_key->gpiod);
// 打印中断号、GPIO引脚编号和电平值
// printk("Interrupt number: %d; GPIO pin number: %d; Pin Logical value: %d\n", irq, gpio_key->gpio, val);
// g_key的高8位中存储的是GPIO口的编号,低8位中存储的是按键按下时的逻辑值
g_key = (gpio_key->gpio << 8) | val;
//装按键值放入环形缓冲区
put_key(g_key);
wake_up_interruptible(&gpio_key_wait);
}
能获得数据的原理分析如下:
在下面的代码中:
timer_setup(&gpio_keys_100ask[i].key_timer, key_timer_expire, 0);
把计时器的回调函数key_timer_expire与数据&gpio_keys_100ask[i].key_timer绑定起来了,在调用函数key_timer_expire 时,其实可以根据&gpio_keys_100ask[i].key_timer反向得到&gpio_keys_100ask[i],所以其实数据已经绑定上了。
在回调函数中我们可以利用宏定义from_timer通过&gpio_keys_100ask[i].key_timer反向得到&gpio_keys_100ask[i],具体的代码如下:
static void key_timer_expire(struct timer_list *t)
{
/* data ==> gpio */
struct gpio_key *gpio_key = from_timer(gpio_key, t, key_timer);
....
其中宏定义from_timer的第1个参数gpio_key代表结构体gpio_key,结构体gpio_key的定义如下:
struct gpio_key{
int gpio;
struct gpio_desc *gpiod;
int flag;
int irq;
struct timer_list key_timer;
} ;
宏定义from_timer的第2个参数就是回调函数的输入参数struct timer_list *t ,第3个参数key_timer就是结构体gpio_key的类型为struct timer_list的字段名。
新版的定时器与旧版的定时器的不同就是以上这些,由于我的内核源码的版本号是Linux-4.9.88,所以无法把程序修改后去编译测试,只能以后遇到的时候再测试了。
设备树文件的修改和更新
和下面的博文一样:
https://blog.youkuaiyun.com/wenhao_ir/article/details/145225508
https://blog.youkuaiyun.com/wenhao_ir/article/details/145176361
Makfile文件内容
# 使用不同的Linux内核时, 一定要修改KERN_DIR,KERN_DIR代表已经配置、编译好的Linux源码的根目录
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
all:
make -C $(KERN_DIR) M=`pwd` modules
# 因为测试程序中有线程的创建,所以下面的语句需要添加 -lpthread 链接选项
$(CROSS_COMPILE)gcc -o button_test_02 button_test.c -lpthread
clean:
make -C $(KERN_DIR) M=`pwd` clean
rm -rf modules.order
rm -f button_test_02
obj-m += gpio_key_drv.o
交叉编译出驱动模块和测试程序
源码复制到Ubuntu中。

make

复制到NFS文件系统中备用。

加载模块
打开串口终端→打开开发板→挂载网络文件系统
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/timer/gpio_key_drv.ko
检查设备文件生成没有
ls /dev/
有了:

运行测试程序
cd /mnt/timer
./button_test_02 /dev/read_keys0
按下KEY2键:

每次按下KEY2只产生了一次读按键值的行为,所以程序是没有问题的。
卸载驱动模块
rmmod gpio_key_drv.ko
运行上面命令后,过了较长时间系统仍然能正常运行,说明卸载没有问题。
至于设备文件、设备类、驱动程序还在不在,在之前的博文中已经测试了,这里就不测试了。
这里我主要是要看计时器是不是被正确释放了,如果没有正确释放,系统是会崩溃的。

被折叠的 条评论
为什么被折叠?



