嵌入式Linux中的按键中断控制
前面讨论的都是输出控制,这里结合Pinctrl和GPIO子系统,以野火STM32MP157开发板为例,通过引脚中断方式去获取开发板上的一个按键状态。
先来看一下按键部分的电路原理图,如下所示。

从上图中可以看到,按键接在了GPIOB_13端口。按键未按下时端口为低电平,按下时为高电平。
下面给出了本例使用的Pinctrl子系统节点内容。
&pinctrl{ ………… pinctrl_button: buttongrp { pins { pinmux = <STM32_PINMUX('B', 13, GPIO)>; drive-push-pull; }; }; };
上述中,pinctrl_button: buttongrp即为本次追加的子节点,放置在&pinctrl节点的最末尾。
下面给出本例GPIO子系统的节点内容。
button_interrupt { compatible = "fire,button"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_button>; button_gpio = <&gpiob 13 GPIO_ACTIVE_LOW>; interrupt-parent = <&gpiob>; interrupts = <13 IRQ_TYPE_EDGE_RISING>; status = "okay"; };
把上述节点内容添加到stm32mp157a-basic.dts设备树文件的根节点内,完成后就可以编译设备树了,编译完成后替换开发板上的设备树文件,具体操作可参见“嵌入式Linux中的LED驱动控制(设备树方式)”一文中的相关部分。
接下来看平台驱动程序部分,下面是驱动的全部代码,文件名为btn.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <asm/io.h> #include <linux/of_irq.h> #include <linux/device.h> #include <linux/platform_device.h> //以下定义一个btn的私有结构体 struct btn_dev { unsigned char button_GPIO_number; //GPIO引脚编号 unsigned int interrupt_number; //引脚中断编号 unsigned int interrupt_type; //中断类型 dev_t devid; //设备号 struct cdev btn_cdev; //定义字符型结构体 struct device *device_button; //创建的设备 struct device_node *button_device_node;//定义按键设备节点结构体 }; static struct class *btn_class; //定义类结构体 atomic_t button_status = ATOMIC_INIT(0); //定义整型原子变量,保存按键状态,设置初始值为0 //实现open函数,为file_oprations结构体成员函数 static int btn_open(struct inode *inode, struct file *filp) { struct btn_dev *btn = container_of(inode->i_cdev, struct btn_dev, btn_cdev); filp->private_data = btn;//把上面获取的btn结构体存入file的私有变量private_data中,后面的函数会用到 return 0; } //实现read函数,为file_oprations结构体成员函数 static int btn_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret; int button = 0; button = atomic_read(&button_status);//读取按键状态值 ret = copy_to_user(buf, &button, sizeof(button));//结果拷贝到用户空间 if(ret < 0) { printk("copy_to_user ret\n"); return -EFAULT; } atomic_set(&button_status,0);//清零按键状态值 return 0; } //实现release函数,为file_oprations结构体函数 static int btn_release(struct inode *inode, struct file *filp) { return 0; } //填充一个file_oprations类型的结构体,名为button_chr_dev_fops,包含上述声明的成员函数 static struct file_operations button_chr_dev_fops = { .owner = THIS_MODULE, .open = btn_open, .read = btn_read, .release = btn_release, }; //实现按键中断服务函数 static irqreturn_t btn_irq_hander(int irq, void *dev_id) { atomic_inc(&button_status); //按键状态值加1 return IRQ_HANDLED; } /*----------------平台驱动函数集-----------------*/ static int btn_pdrv_probe(struct platform_device *pdev) { int ret; struct btn_dev *btn; //定义一个btn_dev结构体的指针变量btn //以下动态申请led结构体大小的内存 btn = devm_kzalloc(&pdev->dev, sizeof(struct btn_dev), GFP_KERNEL); if(!btn) return -ENOMEM; //获取button_interrupt的设备树节点 btn->button_device_node = of_find_node_by_path("/button_interrupt"); if(btn->button_device_node == NULL) { printk(KERN_ERR "get button_interrupt failed!\n"); return -EFAULT; } //获取按键使用的GPIO btn->button_GPIO_number = of_get_named_gpio(btn->button_device_node ,"button_gpio", 0); if(btn->button_GPIO_number == 0) { printk("of_get_named_gpio failed!\n"); return -EFAULT; } ret = gpio_direction_input(btn->button_GPIO_number);//设置引脚为输入模式 btn->interrupt_number = irq_of_parse_and_map(btn->button_device_node, 0);//获取中断号 printk("Interrupt number is:%d\n",btn->interrupt_number); btn->interrupt_type = irq_get_trigger_type(btn->interrupt_number);//获取中断类型 printk("Interrupt type is:%d\n", btn->interrupt_type); //申请中断, 后面记得要释放 ret = request_irq(btn->interrupt_number,btn_irq_hander,btn->interrupt_type,"button_interrupt",btn->device_button); if(ret != 0) { printk("request_irq failed!\n"); free_irq(btn->interrupt_number, btn->device_button); return -EFAULT; } //注意,上述申请成功之后中断已经开启了,不要再去执行enable_irq(interrupt_number)开启 //下面申请主设备号 if (alloc_chrdev_region(&btn->devid, 0, 1, "btn") < 0) { printk("failed to alloc devid\n"); return -EFAULT; } btn->btn_cdev.owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&btn->btn_cdev, &button_chr_dev_fops); //填充上面申请到的主设备号到字符设备 if ( cdev_add(&btn->btn_cdev, btn->devid, 1) < 0) { printk("failed to add cdev\n"); return -EFAULT; } //创建一个设备节点 device_create(btn_class, NULL, btn->devid, NULL, "button"); //以下把btn数据信息存入在平台驱动结构体中pdev->dev->driver_data中,后面移除时会用到 platform_set_drvdata(pdev, btn); printk("platform driver probed!\n"); return 0; } //remove函数中,删除设备并释放设备号 static int btn_pdrv_remove(struct platform_device *pdev) { //platform_get_drvdata,获取当前LED灯对应的结构体 struct btn_dev *btn = platform_get_drvdata(pdev); //释放申请的中断 free_irq(btn->interrupt_number, btn->device_button); unregister_chrdev_region(btn->devid, 1); //释放主设备号 cdev_del(&btn->btn_cdev); //删除字符设备 device_destroy(btn_class, btn->devid); //销毁设备节点 printk("platform driver removed!\n"); return 0; } //填充of_device_id结构体,名为button,用于指明匹配表 static const struct of_device_id button[] = { {.compatible = "fire,button"}, //匹配内容 {/* sentinel */} }; //以下填充一个platform_driver结构体 struct platform_driver btn_platform_driver = { .probe = btn_pdrv_probe, //指定probe函数成员 .remove = btn_pdrv_remove, //指定remove函数成员 .driver = { .name = "button-platform", //指定设备名称 .owner = THIS_MODULE, .of_match_table = button, //指定匹配表名称 } }; //以下定义模块的入口函数 static int __init btn_driver_init(void) { btn_class = class_create(THIS_MODULE, "my_btn"); platform_driver_register(&btn_platform_driver); //注册一个platform驱动 printk("button platform driver initted!\n"); return 0; } //以下定义模块的出口函数 static void __exit btn_driver_exit(void) { platform_driver_unregister(&btn_platform_driver); //释放一个platform驱动 class_destroy(btn_class); //销毁类 printk("button platform driver exited!\n"); } module_init(btn_driver_init); module_exit(btn_driver_exit); MODULE_LICENSE("GPL");
在上述驱动程序中要注意一点,在通过函数irq_get_trigger_type()来获取设备树中配置的中断类型时,前面获取中断号必须使用函数irq_of_parse_and_map()来实现,不能使用函数gpio_to_irq(),否则获取的中断类型永远为0。配套的Makefile文件内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := btn.o all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
把上述驱动程序编译后,把生成的btn.ko文件通过NFS加载到开发板中,可查看到以下信息。

接下来是应用程序,文件名为app.c。
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> int main(int argc, char *argv[]) { int fd, ret, button_status; fd = open("/dev/button", O_RDWR); //打开设备节点 if (fd < 0) { printf("can`t open\n"); return -1; } printf("Waitting button down...\n"); do { //读取按键状态 ret = read(fd, &button_status, sizeof(button_status)); if (ret < 0) printf("read file error!\n"); usleep(1000 * 100); //延时100毫秒 }while(button_status == 0); printf("Button Down !\n"); ret = close(fd); //关闭设备节点 return 0; }
在上述应用程序中,由于需要循环读取驱动中的按键状态,所以在循环体内需要加入一定的延时,以免程序无响应。把程序交叉编译后,通过NFS在开发板上运行,如下图所示。

可看到程序正在等待按键按下,这时按下开发板上的按键,程序立刻捕获到按键动作,显示按键已按下并结束程序,如下图所示。

本例通过捕获按键的的上升沿来设置按键状态,并未获取按键的值。而且只记录按键状态,所以只使用了中断“上半部分”,并未启用“下半部分”。如果在按键按下后要处理的事情较多,需要启用“下半部分”来进行。最后,在实际使用按键中断时,还需要考虑按键消抖,以免按键不灵。

浙公网安备 33010602011771号