中断是指在 CPU 正常运行期间,由外部或内部事件引起的一种机制。当中断发生时,CPU会停止当前正在执行的程序,并转而执行触发该中断的中断处理程序。处理完中断处理程序后, CPU 会返回到中断发生的地方,继续执行被中断的程序。而Linux为了让系统可以更好地处理中断事件,提高实时性和响应能力,将中断服务程序划分为上下文两部分:中断上文是中断服务程序的第一部分,它主要处理一些紧急且需要快速响应的任务。中断下文是中断服务程序的第二部分,它主要处理一些相对耗时的任务。由于中断上文需要尽快完成,因此中断下文负责处理那些不能立即完成的、需要更多时间的任务。Linux提供了诸如tasklet、软中断和工作队列的方式实现中断下文。
本次实验将采用工作队列的方式实现在中断下文中处理 input 子系统的事件上报。工作队列又包括共享工作队列和自定义工作队列两种类型,其中延迟工作是一种将工作的执行延迟到稍后时间点进行处理的技术,并且延迟工作可以用来执行定时任务,由于Linux内核对于定时器服务函数进行了改动,故采用延迟工作的机制实现按键消抖。
此次实验将利用开发板上的GPIO4_A4口设计一个按键,GPIO口配置为下拉,中断采用上升沿触发方式,当按键按下,GPIO变为高电平,产生上升沿,设备树配置参考:《Linux 驱动开发之使用文件操作集函数控制GPIO口电平》
第一步,设备树配置
在 /kernel/arch/arm64/boot/dts/rockchip 目录下找到主设备树,并在根节点下添加按键测试节点以及对pinctrl节点进行追加。
key_test:key_test{
compatible = "key_test";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&key>;
test {
gpio_num = <&gpio4 RK_PA4 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&gpio4>; // 指定所属的中断控制器
interrupts = <RK_PA4 IRQ_TYPE_EDGE_RISING>; // 设置为上升沿触发
};
};
&pinctrl {
keypin{
key: key{
rockchip,pins =
<4 RK_PA4 RK_FUNC_GPIO &pcfg_pull_down>; // 设置为下拉
};
};
};
第二步,驱动程序编写
按键驱动采用平台总线模型,实现设备树资源的获取,GPIO中断的注册,自定义工作队列的实现以及input子系统申请和注册等。
<头文件包含>
#define GPIO_CNT 1 /* 设备号个数 */
#define KEYINPUT_NAME "keyinput" /* 名字 */
int num;
int irq;
struct keytest_dev{
dev_t devid;
struct device_node *nd;
int test_gpio;
struct input_dev *inputdev; /* input 结构体 */
};
struct keytest_dev keytest;
struct workqueue_struct *test_workqueue;
struct delayed_work test_workqueue_work;
// 工作项处理函数
void test_work(struct work_struct *work)
{
unsigned char value;
value = gpio_get_value(keytest.test_gpio);
if(value == 1){ /* 按下按键 */
input_report_key(keytest.inputdev, KEY_0, 1);
input_sync(keytest.inputdev);
} else { /* 按键松开 */
input_report_key(keytest.inputdev, KEY_0, 0);
input_sync(keytest.inputdev);
}
}
//中断处理函数
static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
//提交延迟工作项到自定义工作队列,延迟10ms
queue_delayed_work(test_workqueue, &test_workqueue_work, msecs_to_jiffies(10));
return IRQ_HANDLED;
}
static int my_platform_probe(struct platform_device *pdev)
{
int ret = 0;
printk(KERN_INFO "my_platform_probe: Probing platform device\n");
keytest.nd = of_find_node_by_path("/key_test/test"); //查找对应节点
if(keytest.nd == NULL) {
printk("keytest node cant not found!\r\n");
return -EINVAL;
} else {
printk("keytest node has been found!\r\n");
}
keytest.test_gpio = of_get_named_gpio(keytest.nd, "gpio_num", 0);
if(keytest.test_gpio < 0) {
printk("can't get gpio");
return -EINVAL;
}
printk("gpio num = %d\r\n", keytest.test_gpio);
// 将 GPIO 转换为中断号
irq = gpio_to_irq(keytest.test_gpio);
printk("irq is %d\n", irq);
ret = gpio_direction_input(keytest.test_gpio); //设置gpio为输入
if(ret < 0) {
printk("can't set gpio!\r\n");
}
if (request_irq(irq, gpio_irq_handler, IRQF_TRIGGER_RISING, "gpio_irq_test", NULL) != 0){
printk(KERN_ERR "Failed to request IRQ %d\n", irq);
gpio_free(keytest.test_gpio);
return -ENODEV;
}
test_workqueue = create_workqueue("test_workqueue"); // 创建工作队列
INIT_DELAYED_WORK(&test_workqueue_work, test_work); // 初始化延迟工作项
keytest.inputdev = input_allocate_device();
keytest.inputdev->name = KEYINPUT_NAME;
__set_bit(EV_KEY, keytest.inputdev->evbit); /*按键事件 */
__set_bit(EV_REP, keytest.inputdev->evbit); /* 重复事件 */
/* 初始化 input_dev,设置产生哪些按键 */
__set_bit(KEY_0, keytest.inputdev->keybit);
ret = input_register_device(keytest.inputdev);
if (ret) {
printk("register input device failed!\r\n");
return ret;
}
return 0;
}
static int my_platform_remove(struct platform_device *pdev)
{
printk(KERN_INFO "my_platform_remove: Removing platform device\n");
free_irq(irq, NULL);
cancel_delayed_work_sync(&test_workqueue_work);
flush_workqueue(test_workqueue);
destroy_workqueue(test_workqueue);
input_unregister_device(keytest.inputdev);
input_free_device(keytest.inputdev);
return 0;
}
const struct of_device_id of_match_table_id[] = {
{.compatible="key_test"},
};
static struct platform_driver my_platform_driver = {
.probe = my_platform_probe,
.remove = my_platform_remove,
.driver = {
.name = "my_platform_device",
.owner = THIS_MODULE,
.of_match_table = of_match_table_id,
},
};
static int __init my_platform_driver_init(void)
{
int ret;
ret = platform_driver_register(&my_platform_driver);
if (ret)
{
printk(KERN_ERR "Failed to register platform driver\n");
return ret;
}
printk(KERN_INFO "my_platform_driver: Platform driver initialized\n");
return 0;
}
static void __exit my_platform_driver_exit(void)
{
platform_driver_unregister(&my_platform_driver);
printk(KERN_INFO "my_platform_driver: Platform driver exited\n");
}
module_init(my_platform_driver_init);
module_exit(my_platform_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("tester");
第三步,测试程序编写
测试程序实现获取 input 子系统上报的事件信息并判断事件类型以及打印按键状态。
<头文件包含>
static struct input_event inputevent;
int main(int argc, char *argv[])
{
int fd;
int err = 0;
char *filename;
filename = argv[1];
if(argc != 2) {
printf("Error Usage!\r\n");
return -1;
}
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("Can't open file %s\r\n", filename);
return -1;
}
while (1) {
err = read(fd, &inputevent, sizeof(inputevent));
if (err > 0) {
switch (inputevent.type) {
case EV_KEY:
if (inputevent.code < BTN_MISC) { /* 按键键值 */
printf("key %d %s\r\n", inputevent.code,inputevent.value ? "press" : "release");
}
break;
/* 其他类型的事件,不做处理 */
case EV_REL:
break;
case EV_ABS:
break;
case EV_MSC:
break;
case EV_SW:
break;
}
} else {
printf("读取数据失败\r\n");
}
}
return 0;
}
第四步,运行测试
将按键驱动程序放在 /kernel/drivers/char/test 目录下,并添加到上级Kconfig和Makefile中,使用图形化配置界面对内核进行配置,将test编译为模块,结果如下。
由于虚拟机上没有装交叉编译工具,故将测试程序及驱动模块一起推到开发板上,在开发板上使用gcc编译该测试程序,结果如下。
在加载驱动之前,查看一下 /dev/input 下的事件组,便于定位驱动加载之后的按键事件。
加载驱动,并查看 /dev/input 事件组,新增的 event4 即为按键的KEY_0事件。
运行测试程序,连续按几次按键,观察终端打印情况。
可以看出,每当按键按下一次,则打印一组press与release,由于在驱动中申请按键事件时注册的是KEY_0值,通过查看内核中 include/uapi/linux/input.h 中的定义,KEY_0所对应的值就是11,说明驱动运行完全正常。测试完毕,卸载驱动。
总结:此次实验采用工作队列的方式在中断下文中实现了 input 子系统事件上报,并且采用了不同于使用定时器中断的延迟工作的方式实现了按键消抖。