基于树莓派4B的Linux驱动------按键中断(消抖、下半部处理)
本人也是接触Linux不久,可能有些问题也没考虑到,以下仅是个人观点,欢迎留言,共同进步,话不多说,直接步入正题。
一、实验说明
本次实验采用设备树pinctrl方式编程
开发板基于树莓派4B
linux内核版本:linux-rpi-5.15.y
开发平台:ubuntu交叉编译
按键按下为高电平
本次实验是续上次《基于树莓派4B的Linux驱动------按键中断》
二、修改设备树文件
为了方便,我这里是直接在根节点上添加了以下内容,我这里修改的是arch/arm/boot/bts/bcm2711-rpi-4-b.dts文件,在该文件下面添加以下内容,具体的一些pinctrl的设备树语法可以参考内核说明文档,在Documentation/devicetree/bindings/pinctrl目录下面的文档,如brcm,bcm2835-gpio.txt文件
bcm2711-rpi-4-b.dts根节点添加以下内容
keytest{
#address-cells = <1>;
#size-cells = <1>;
compatible = "keytest";
gpios = <&gpio 19 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&key1_test>;
status = "okay";
};
bcm2711-rpi-4-b.dts添加以下追加内容
&gpio {
key1_test: key1_test {
brcm,pins = <19>;
brcm,function = <0>;
brcm,pull = <2>;
};
};
在属性brcm,pins中,0表示GPIO0,1表示GPIO1,2表示GPIO2 ···
在属性brcm,function中,0表示in,1表示out,2表示ALT0,3表示ALT1 ···
在属性brcm,pull 中,0表示none, 1表示up, 2表示down
可以参考内核文档Documentation/devicetree/bindings/pinctrl/brcm,bcm2835-gpio.txt
ALT0,ALT1,ALT3,ALT4,ALT5可以去看bcm2711的芯片手册,在GPIO章节,手册可以到树莓派官网去下载
三、编写驱动程序
驱动程序可以参考内核的驱动程序,如drivers/input/keyboard/gpio_keys.c文件
以下驱动程序可以兼容使用多个按键,但是我在设备树里面只设置了一个按键,所以如果想用多个按键的时候,可以只修改设备树就能实现了
of_gpio_count 函数获取设备树描述的该节点的GPIO个数
kzalloc 函数用于申请内存
of_get_gpio_flags 函数用于获取GPIO编号
gpio_to_irq 函数用于获取中断编号
request_irq 函数用于申请中断,第一个参数是中断编号,第二个参数是中断服务函数的指针,第三个参数是触发方式,第四个参数是中断名字,第五个参数是传进去一个地址,这个地址后面会传到中断服务函数里面,然后中断服务函数就可以使用某些变量的值,当然如果不用到也可以不传,写个NULL就可以了
由于中断讲究的是快进快出,为了避免中断处理的时间过长,所以linux提出了中断上半部和中断下半部,简单来说就把紧急的事放到上半部,不那么紧急的事放到下半部,linux处理上半部的时候,不允许别的中断打断,也就是不存在中断优先级,而linux处理下半部的时候,允许被别的中断打断,所以如果有两个中断到来,那么linux会先处理先到的那个中断,那如果第一个中断执行的时间很长,那么第二个中断要等很久才会被执行,我们不愿意看到这种情况,所以我们可以把执行时间比较长的那部分程序放到下半部,这样子当第二个中断来临就可以打断当前的中断,这样子就可以让每个中断得以很快执行,那对于下半部,当被打断后,linux会对这部分作一个标记,当所有的上半部执行完后,linux就会去执行下半部,会有对应的单元去寻找之前的标记,然后执行,执行完后把标记清除,简单的说,上半部就是中断服务函数,中断服务函数里面我们就可以调用一个处理下半部的函数,我这里只是粗略的讲了一下,具体可以去网上找一些linux对于中断的处理的书籍,这部分我就讲到这里。
还有就是按键的消抖,我们知道,按键按下时会存在硬件的抖动,这样会使得我们只按下了一次,系统会识别到很多次,这种情况我们可以在电路里面加一个电容,利用电容的充放电就可以解决这个问题,那么要知道加上电容会提高硬件的成本,而且电容坏了就没用了,那么有没有别的方式呢?学过51或stm32单片机的朋友应该知道,对于51或stm32单片机扫描读取按键的处理,可以加软件延时,目的是等按键稳定下来我们在去读取按键值,就是忽略掉按键按下后开头和结尾部分,那么对于linux呢?也是差不多的,目的还是忽略掉按键按下后开头和结尾部分,等按键稳定下来我们在去读取按键值,那么我们就可以加定时器,就是当按键中断来临时,我们可以在中断服务函数里面加一个定时器,让定时器计时,过一段时间我才去读取,这样就可以实现了。但是,如果直接把定时的过程放到中断服务函数里面,这样是不好的,因为中断讲究的是快进快出,放到中断服务函数里面会占用中断的资源,别的中断就等你执行完了他才可以执行,所以我们应该把定时的过程放到中断下半部。
梳理一下思路,首先按键按下,按下就可以产生一个按键中断,产生中断就会进入中断服务函数,中断服务函数调用中断下半部处理函数,中断下半部处理函数再调用定时器处理函数,然后我们就可以把自己想要实现的功能写在定时器处理函数里面。以下是我写的程序,可以参考一下。
testkey.c
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/sched.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio_keys.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_platform.h>
#include <linux/of_irq.h>
#include <linux/spinlock.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
struct gpio_key{
int gpio;
int irq;
enum of_gpio_flags flag;
};
struct pi4irq_dev{
struct tasklet_struct tasklet; /* 中断下半部 */
struct timer_list timer; /* 定时器 */
int timePeriod; /* 定时周期,单位为ms */
};
static struct gpio_key *gpio_keys;
static struct pi4irq_dev pi4irq;
/* 中断上半部,中断服务函数 */
static irqreturn_t gpio_key_irq_handle(int irq, void *dev_id)
{
/* 调度tasklet */
tasklet_schedule(&pi4irq.tasklet);
return IRQ_HANDLED;
}
/* 中断下半部tasklet */
static void key_tasklet(unsigned long data)
{
struct pi4irq_dev *dev = &pi4irq;
int timerPeriod;
timerPeriod = dev->timePeriod;
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerPeriod));
}
/* 定时器 */
static void timer_function(struct timer_list* time)
{
struct gpio_key *dev = gpio_keys;
int value;
value = gpio_get_value(dev->gpio);
if(value == 0)
{
printk("中断号:%d 松开\n", dev->irq);
}
else
{
printk("中断号:%d 按下\n", dev->irq);
}
}
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count, i;
int ret = 0;
count = of_gpio_count(node);
if(count <= 0)
{
ret = -EINVAL;
goto fail_count;
}
gpio_keys = kzalloc(count * sizeof(struct gpio_key), GFP_KERNEL);
if(!gpio_keys)
{
printk("内存分配失败\n");
ret = -ENOMEM;
goto fail_kzalloc;
}
for(i = 0; i < count; i++)
{
gpio_keys[i].gpio = of_get_gpio_flags(node, i, &gpio_keys[i].flag);
if (!gpio_is_valid(gpio_keys[i].gpio))
{
printk("设备树获取失败 key: %d\n", i);
ret = -EINVAL;
goto fail_flags;
}
gpio_keys[i].irq = gpio_to_irq(gpio_keys[i].gpio);
if(gpio_keys[i].irq < 0)
{
printk("中断号获取失败 key: %d\n", i);
ret = gpio_keys[i].irq;
goto fail_irq;
}
ret = request_irq(gpio_keys[i].irq, gpio_key_irq_handle, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "test_gpio_key", &gpio_keys[i]);
if (ret != 0)
{
ret = -1;
printk("无法请求 gpio_keys irq\n");
free_irq(gpio_keys[i].irq, &gpio_keys[i]);
goto fail_request;
}
}
tasklet_init(&pi4irq.tasklet, key_tasklet, (unsigned long)gpio_keys);
return 0;
fail_request:
fail_irq:
fail_flags:
fail_kzalloc:
kfree((struct gpio_key*)gpio_keys);
fail_count:
return ret;
}
static const struct of_device_id key_gpios[] = {
{.compatible = "keytest"},
{},
};
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
struct device_node *node = pdev->dev.of_node;
int count, i;
count = of_gpio_count(node);
for(i = 0; i < count; i++)
{
free_irq(gpio_keys[i].irq, &gpio_keys[i]);
}
kfree((struct gpio_key*)gpio_keys);
return 0;
}
static struct platform_driver test_gpio_drv = {
.probe = chip_demo_gpio_probe,
.remove = chip_demo_gpio_remove,
.driver = {
.name = "keytest",
.of_match_table = key_gpios,
},
};
static __init int test_gpio_init(void)
{
printk("test_gpio_init\n");
platform_driver_register(&test_gpio_drv);
/* 初始化定时器 */
timer_setup(&pi4irq.timer, timer_function, 0);
pi4irq.timePeriod = 20;
pi4irq.timer.expires = jiffies + msecs_to_jiffies(pi4irq.timePeriod);
return 0;
}
static __exit void test_gpio_exit(void)
{
printk("test_gpio_exit\n");
platform_driver_unregister(&test_gpio_drv);
/* 删除 timer */
del_timer_sync(&pi4irq.timer);
}
module_init(test_gpio_init);
module_exit(test_gpio_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");
四、编写MakeFile程序
KERNELDIR := /home/pi/linux/pi4_kernel/linux-rpi-5.15.y
CURRENT_PATH := $(shell pwd)
obj-m := testkey.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
五、编译测试
make 编译驱动程序,并把编译好的.ko文件拷贝到树莓派
到内核目录 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs 编译设备树,把编译好的设备树文件拷贝到树莓派boot目录下,重启树莓派,执行
sudo insmod testkey.ko 加载驱动程序
可以使用dmesg命令查看内核日志
按下按键,查看内核日志,打印如下
中断号:80 按下
中断号:80 松开
好的,实验完成