本文源码:https://github.com/Philon/rpi-drivers/tree/master/02-gpio_key
在上一篇中主要学习了GPIO原理及Linux字符设备,其过程大致是这样子的:
| 1
 | 电路图“看引脚” --> 手册“看物理地址” --> 寄存器手册“看GPIO逻辑控制” --> 复用功能 --> 地址操作
 | 
可以看到,整个过程是非常繁琐的,驱动程序必须精确到处理器的每一根引脚的状态,如果所有驱动都这么写估计当场就跪了。因此,本文除了学习GPIO中断原理之外,更重要是掌握以下知识:
- 混杂设备机制
- Linux内核GPIO接口
- ARM中断基础
- Linux内核中断接口
ARM中断基础
中断,就是由外部电路产生的一个电信号,强制CPU从当前执行代码区转移到中断处理函数。ARM架构的CPU中断硬件原理和这个差不多,注意这里说的是ARM的CPU,仅仅代表处理器当中的一个核,不要把CPU和SoC划等号。
CPU能提供的中断资源是非常有限的,一般也就一两个“引脚”,但我们在看芯片手册的时候就会发现,几乎每个GPIO都具备中断功能,那可是几十上百个中断啊!这归功于内部继承的PIC——可编程中断控制器,它负责监听所有GPIO的中断信号,并在外设给出中断信号时真正去触发CPU中断,并告诉CPU是谁触发的。这种中断信号源被抽象为——中断号。
在现代多核处理器架构下,ARM用的是GIC(通用中断控制器),它能支持SGI(软件生成中断)、PPI(单核私有外设中断)、SPI(多核共享外设中断)。默认情况下,ARM处理器的外设中断总是先给到CPU0,如果其忙不过来才往后传递。
那么CPU收到中断信号后又如何处理呢?ARM共有7种工作模式,常规情况下会运行于用户模式(用户代码区),一旦中断触发,会立刻切换至中断模式(响应函数),中断模式分为IRQ(中断)和FIQ(快速中断),它们二者的区别是,FIQ可以进一步中断IRQ。
由于是实战操作,过于理论的东西就不往上放了,如果要进一步了解ARM中断,可以参考这篇文章👉:https://my.oschina.net/u/914989/blog/121585
这里只需要掌握两个重要概念:
- 中断号本身可以看作一种独立的CPU资源,通过中断控制器监听真实的物理资源(引脚)状态
- CPU的外设中断会直接触发PC跳转到指定代码区
Linux/IRQ基础
正如前文所说,中断是让CPU切换执行上下文,尽管Linux操作系统通过时间切片的方式实现多任务,但IRQ切换是硬件层级的,进入中断函数就意味着什么进程、调度、并发等软件概念将全部失效。举例来说,一个进程调用sleep只会让自身运行停止并让出CPU资源,但在内核中断函数当中sleep,那就真睡过去了——整个操作系统的调度机制都会崩溃掉。
所以,Linux将中断处理分为“顶半部”和“底半部”,可以简单粗暴地理解:
- 顶半部,硬件级响应,处理内容必须快准狠,尽快将CPU资源交还操作系统
- 底半部,交由系统任务队列调度,处理耗时的响应业务
打个比方,顶半部好比医院挂号,底半部好比排队就诊的过程。但不要死脑筋,如果响应业务本身并不耗时,就没必要再拆分为两个处理部分了,比如出院缴费,直接在顶半部搞定。
带着这个原则,看一下Linux中断编程接口:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 
 | #include <linux/interrupt.h> 
 
 int gpio_to_irq(unsigned gpio);
 
 
 
 
 
 
 
 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
 
 
 void *free_irq(unsigned int irq, void *dev);
 
 | 
下面来实际操作一把——Linux中断的顶半部处理实现。
最简单的GPIO中断
先来看个接线图,为了更好地展示中断,“继承”了上一篇文章的三色LED接线,预期要实现是“每按一次键改变一种颜色”。按键Key的两个脚接到了树莓派的GPIO17和3V3上,换句话说,就是用GPIO17接收上升沿中断信号。而LED的控制电路保持之前的不变。


实现GPIO上升沿中断大体分为4步:
- 设置GPIO复用功能为输入模式 gpio_request()
- 获取GPIO对应中断号 gpio_to_irq()
- 申请中断号、中断类型、绑定处理函数 request_irq()
- 释放中断(卸载驱动时) free_irq()
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 
 | #include <linux/module.h>#include <linux/gpio.h>
 #include <linux/interrupt.h>
 
 MODULE_LICENSE("Dual BSD/GPL");
 MODULE_AUTHOR("Philon | https://ixx.life");
 
 
 static unsigned int key_irq = 0;
 
 
 static const struct gpio key = {
 .gpio = 17,
 .flags = GPIOF_IN,
 .label = "Key0"
 };
 
 
 static irqreturn_t on_key_press(int irq, void* dev)
 {
 printk(KERN_INFO "key pressed\n");
 return IRQ_HANDLED;
 }
 
 static int __init gpiokey_init(void)
 {
 int rc = 0;
 
 
 if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0) {
 printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc);
 return rc;
 }
 
 
 key_irq = gpio_to_irq(key.gpio);
 if (key_irq < 0) {
 printk(KERN_ERR "ERROR%d:cannot get irq num\n", key_irq);
 return key_irq;
 }
 
 
 if (request_irq(key_irq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) {
 printk(KERN_ERR "cannot request irq\n");
 return -EFAULT;
 }
 
 return 0;
 }
 module_init(gpiokey_init);
 
 static void __exit gpiokey_exit(void)
 {
 
 free_irq(key_irq, NULL);
 gpio_free(key.gpio);
 }
 module_exit(gpiokey_exit);
 
 | 
上述代码非常简单,就是在按下按键的时候,打印一条消息。可以通过dmesg命令查看内核打印消息:
| 12
 3
 4
 5
 6
 7
 8
 
 | philon@rpi:~/modules $ sudo insmod gpiokey.ko philon@rpi:~/modules $ dmesg
 ...
 [   77.238326] gpiokey: no symbol version for module_layout
 [   77.238345] gpiokey: loading out-of-tree module taints kernel.
 [   79.310635] key pressed
 [   79.463206] key pressed
 [   79.463262] key pressed
 
 | 
正如文章最开始所说,Linux对各种资源的调用是有相关API的,要尽量使用内核接口编写驱动程序,一能保证底层代码的质量,二能提高代码的移植性。关于GPIO资源的调用,要熟悉以下接口:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | #include <linux/gpio.h>
 struct gpio {
 unsigned	gpio;
 unsigned long	flags;
 const char	*label;
 };
 
 
 int gpio_request_one(unsigned gpio, unsigned long flags, const char *label);
 void gpio_free(unsigned gpio);
 
 
 int gpio_request_array(const struct gpio *array, size_t num);
 void gpio_free_array(const struct gpio *array, size_t num);
 
 
 int gpio_get_value(unsigned gpio);
 void gpio_set_value(unsigned gpio, int value);
 
 | 
按键防抖,中断的底半部与定时器接口
由于前边的代码没有做防抖,明明只按了一下按键,中断函数却被连续触发了3次。话说在单片机里实现按键防抖是非常简单的,无非就是睡个50毫秒,再确认是否真的按下即可。但是前文也明确说了,Linux是多任务系统,永远不要试图在中断函数里睡眠。因此,防抖只能放在Linux中断的底半部。
此外,慎用睡眠函数!除非你很清楚它不是忙等待。在多任务系统下,按键防抖的逻辑应该是——触发中断后,让出CPU资源50毫秒,然后再确认是否真的按下。
先来认识一下底半部机制,Linux内核提供的底半部机制主要有软中断、tasklet、工作队列、线程IRQ。
- 软中断,是有内核软件模拟的一种中断机制,注意不要和ARM指令触发的中断混淆,后者本质上是硬中断
- tasklet,基于软中断实现的中断调度机制,本质上还是中断,不允许在处理函数中sleep
- 工作队列,类似于tasklet,区别在于工作队列底层基于线程,可以在处理函数中sleep
- 线程IRQ,不用解释了,就是个线程
有关Linux底半部的知识不适合放在这里,建议参考此文:http://chinaunix.net/uid-20768928-id-5077401.html
这里了解底半部机制的目的,仅仅是为了挑选一种何时的响应方式,首先可以明确,软中断和tasklet不能睡,pass。线程维护麻烦,pass。就只剩工作队列了。尽管工作队列可以睡,但内核提供的usleep/msleep等接口本质上是忙等待,依旧占用CPU资源,pass。怎么办呢——工作队列+定时器。当中断来临后:
- 顶半部迅速定义个工作队列,交由内核调度
- 当工作队列被调度时,迅速定义个定时器——延时50ms
- 当定时器到时中断,才真的去做防抖判断
工作队列API:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | #include <linux/workqueue.h>
 
 struct work_struct {
 atomic_long_t data;
 struct list_head entry;
 work_func_t func;
 #ifdef CONFIG_LOCKDEP
 struct lockdep_map lockdep_map;
 #endif
 };
 
 
 typedef void (*work_func_t)(struct work_struct *work);
 
 
 INIT_WORK(work, func);
 
 schedule_work(&my_wq);
 
 | 
定时器API:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 
 | #include <linux/timer.h>
 
 
 extern unsigned long volatile jiffies;
 
 #define HZ 100
 
 
 
 
 
 
 struct timer_list {
 struct hlist_node	entry;
 unsigned long		expires;
 void			(*function)(struct timer_list *);
 u32			flags;
 #ifdef CONFIG_LOCKDEP
 struct lockdep_map	lockdep_map;
 #endif
 };
 
 
 #define timer_setup(timer, callback, flags)
 void add_timer(struct timer_list *timer);
 
 
 int del_timer(struct timer_list *timer);
 
 
 int mod_timer(struct timer_list *timer, unsigned long expires)
 
 | 
下面是本文的完整代码,按一次按键,切换一次彩色led的颜色:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 
 | #include <linux/module.h>#include <linux/fs.h>
 #include <linux/miscdevice.h>
 #include <linux/gpio.h>
 #include <linux/interrupt.h>
 #include <linux/workqueue.h>
 #include <linux/timer.h>
 
 MODULE_LICENSE("Dual BSD/GPL");
 MODULE_AUTHOR("Philon | https://ixx.life");
 
 
 static const struct gpio key = {
 .gpio = 17,
 .flags = GPIOF_IN,
 .label = "Key0"
 };
 
 
 static const struct gpio leds[] = {
 { 2, GPIOF_OUT_INIT_HIGH, "LED_RED" },
 { 3, GPIOF_OUT_INIT_HIGH, "LED_GREEN" },
 { 4, GPIOF_OUT_INIT_HIGH, "LED_BLUE" },
 };
 
 static unsigned int keyirq = 0;
 static struct work_struct keywork;
 static struct timer_list timer;
 
 
 static irqreturn_t on_key_press(int irq, void* dev)
 {
 schedule_work(&keywork);
 return IRQ_HANDLED;
 }
 
 
 void start_timer(struct work_struct *work)
 {
 mod_timer(&timer, jiffies + (HZ/20));
 }
 
 
 void on_delay_50ms(struct timer_list *timer)
 {
 static int i = 0;
 if (gpio_get_value(key.gpio)) {
 gpio_set_value(leds[i].gpio, 0);
 i = ++i == 3 ? 0 : i;
 gpio_set_value(leds[i].gpio, 1);
 }
 }
 
 static int __init gpiokey_init(void)
 {
 int rc = 0;
 
 
 if ((rc = gpio_request_one(key.gpio, key.flags, key.label)) < 0
 || (rc = gpio_request_array(leds, 3)) < 0) {
 printk(KERN_ERR "ERROR%d: cannot request gpio\n", rc);
 return rc;
 }
 
 
 keyirq = gpio_to_irq(key.gpio);
 if (keyirq < 0) {
 printk(KERN_ERR "can not get irq num.\n");
 return -EFAULT;
 }
 
 
 if (request_irq(keyirq, on_key_press, IRQF_TRIGGER_RISING, "onKeyPress", NULL) < 0) {
 printk(KERN_ERR "can not request irq\n");
 return -EFAULT;
 }
 
 
 INIT_WORK(&keywork, start_timer);
 
 
 timer_setup(&timer, on_delay_50ms, 0);
 add_timer(&timer);
 
 return 0;
 }
 module_init(gpiokey_init);
 
 static void __exit gpiokey_exit(void)
 {
 free_irq(keyirq, NULL);
 gpio_free_array(leds, 3);
 gpio_free(key.gpio);
 del_timer(&timer);
 }
 module_exit(gpiokey_exit);
 
 | 
小结
- ARM有7种工作模式,其中IRQ和FIQ为中断模式,会导致CPU跳转到指定代码区
- Linux/IRQ分为顶半部和底半部机制
- 顶半部处理要快且不是睡眠
- 底半部又分为4种机制,软中断、tasklet、工作队列、线程IRQ
- 我们可以通过gpio_xxx函数访问CPU资源,而无需地操作底层寄存器
- 如果有延时需求,最好采用内核提供的定时器接口