基于树莓派4B的Linux驱动------按键中断(消抖、下半部处理)

基于树莓派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 松开
好的,实验完成

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是一个简单的树莓按键切换歌曲的代码示例,供您参考: ```python import RPi.GPIO as GPIO import os import time # 设置GPIO口的模式 GPIO.setmode(GPIO.BCM) # 定义GPIO口 button1 = 18 button2 = 23 # 设置GPIO口为输入模式,上拉电阻 GPIO.setup(button1, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(button2, GPIO.IN, pull_up_down=GPIO.PUD_UP) # 定义歌曲列表 songs = ['/path/to/song1.mp3', '/path/to/song2.mp3', '/path/to/song3.mp3'] current_song = 0 # 定义按键响应函数 def button1_callback(channel): global current_song current_song = (current_song + 1) % len(songs) os.system('mpg321 ' + songs[current_song]) def button2_callback(channel): global current_song current_song = (current_song - 1) % len(songs) os.system('mpg321 ' + songs[current_song]) # 注册按键事件 GPIO.add_event_detect(button1, GPIO.FALLING, callback=button1_callback, bouncetime=200) GPIO.add_event_detect(button2, GPIO.FALLING, callback=button2_callback, bouncetime=200) # 主循环 try: while True: time.sleep(0.1) except KeyboardInterrupt: GPIO.cleanup() ``` 上述代码中,我们先定义了两个GPIO口,然后使用`GPIO.setup`函数将它们设置为输入模式,并启用上拉电阻。接着,我们定义了一个歌曲列表`songs`和一个全局变量`current_song`,用于记录当前正在播放的歌曲。然后,我们定义了两个按键响应函数`button1_callback`和`button2_callback`,分别用于处理两个按键的按下事件。在按键响应函数中,我们使用`os.system`函数来调用`mpg321`命令播放对应的歌曲,并更新`current_song`变量。最后,我们使用`GPIO.add_event_detect`函数来注册按键事件,并进入主循环等待按键事件的发生。 请注意,上述代码仅供参考,具体的实现方式需要根据您的具体需求和硬件环境进行调整。另外,为了保证代码的正确性和稳定性,建议使用按键模块或接线端子等硬件设备连接外部按键
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值