1. 概述
- 假如存在这样一个需求:应用程序需要监控某个硬件GPIO口的电平状态,当发生变化时,应用程序就做出相应的动作。利用之前已经介绍的知识,是可以完成这个需求的。比如:在驱动程序中不停的读取GPIO口的状态,一旦发生变化,就把新的电平状态通过信号发送到应用层。这样的方式称作:
轮询 - 轮询方式的缺点显而易见:轮询的时间间隔应该是多少毫秒(or 微秒),才比较合适呢?
轮询太慢:可能会丢失信号;轮询太快:消耗 CPU 资源! 因此,在实际的产品中,用中断触发的方式才是更切合实际的选择! 本文所有的描述和测试,都是在 x86 平台上完成的;
2. 中断分类
3. 中断号和中断向量
- 中断号与中断控制器(
PIC/APIC )相关 - 中断向量与
CPU 相关
4. 中断处理
- 中断服务程序,就是针对每一个
中断 如何进行处理 。如果您了解Linux中断的相关内容,一定会看到这样的描述:中断处理分为上半部分和下半部分。
4.1 上半部
- 上半部分不能消耗太多的时间,主要处理与硬件相关的重要工作`;其他不重要的工作,都放在下半部分去做。
4.2 下半部
- 下列图示是针对每一种“下半部分”处理机制的一些特点,注意:有些机制在新版本中已经废弃不用了,了解即可
4.2.1 软中断(soft_irq)
4.2.2 小任务(tasklet)
4.2.3 工作队列(work_queue)
- 用来完成下半部分工作有好几种机制可以选择,每一种方式都是针对不同的需求场景。在每一种下半部分机制中,Linux都设计了非常方便的接口函数。作为开发者的我们来说,使用这些下半部分的机制很简单,只需要几个函数调用即可。例如:如果使用工作队列来实现下半部分的工作,只需要
2步动作 :
4.3 注册中断函数
- 中断注册,就是告诉操作系统:我对哪个中断感兴趣。当这些中断发生的时候,请通知我。通知的方式就是:调用一个预先注册好的回调函数。驱动程序可以通过函数
request_irq() ,向操作系统注册,并且激活指定的中断线 :
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *devname, void *dev_id);
参数说明:
irq: 申请的硬件中断号;
handler: 中断处理函数。一旦中断发生,这个函数就被调用;
flags: 中断的属性,例如:IRQF_DISABLED,IRQF_TIMER,IRQF_SHARED;
devname: 中断驱动程序的名称,在 /proc/interrupts 文件中看到对应的内容;
dev_id: 中断程序的唯一标识,比如:在共享中断中,可以用来区分不同的中断处理程序;
驱动程序通过函数 free_irq(),向操作系统注销一个中断处理函数:
void free_irq(unsigned int irq, void *dev_id);
参数说明:
irq: 硬件中断号;
dev_id: 中断程序的唯一标识;
INIT_WORK(&mywork, mywork_handler);
schedule_work(&mywork);
4.4 定义处理函数
static struct work_struct mywork;
static void mywork_handler(struct work_struct *work)
{
printk("This is myword_handler...\n");
}
4.5 捕获键盘中断
示例代码,捕获键盘的中断,在中断处理函数中,打印出按键的扫描码,如果是 ESC 键被按下,就打印出指定的信息。操作的目录位于:/linux-4.15/drivers 目录下。
$ mkdir my_driver_interrupt
$ touch driver_interrupt.c
4.5.1 driver_interrupt.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
static int irq;
static char * devname;
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
#define MY_DEV_ID 1211
struct myirq
{
int devid;
};
struct myirq mydev ={ MY_DEV_ID };
#define KBD_DATA_REG 0x60
#define KBD_STATUS_REG 0x64
#define KBD_SCANCODE_MASK 0x7f
#define KBD_STATUS_MASK 0x80
static irqreturn_t myirq_handler(int irq, void * dev)
{
struct myirq mydev;
unsigned char key_code;
mydev = *(struct myirq*)dev;
if (MY_DEV_ID == mydev.devid)
{
key_code = inb(KBD_DATA_REG);
if (key_code == 0x01)
{
printk("EXC key is pressed! \n");
}
}
return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
printk("myirq_init is called. \n");
if(request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev)!=0)
{
printk("register irq[%d] handler failed. \n", irq);
return -1;
}
printk("register irq[%d] handler success. \n", irq);
return 0;
}
static void __exit myirq_exit(void)
{
printk("myirq_exit is called. \n");
free_irq(irq, &mydev);
}
MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);
4.6 加载驱动时传参
示例代码中,在调用 request_irq 时,需要指定中断号和驱动程序的名称。这两个参数是在加载驱动模块的时候,从命令行传入的。在驱动程序中,通过下面两行代码即可实现参数的接收:
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
module_param 是一个宏定义,定义在 include/linux/moduleparam.h 文件中,具体定义如下:
#define module_param(name, type, perm)
module_param_named(name, name, type, perm);
name : 存储参数的变量名; type : 变量的类型; perm : 访问参数的权限,表示此参数在sysfs文件系统中所对应的文件节点的属性;
4.8 IO地址
读取 IO 外设的两种不同方式:IO内存和IO端口
4.8.1 IO内存
4.8.1 IO端口
IO 端口有两种编址方式:统一编址和独立编址。
4.8.1.1 统一编址
- 把主存单元所在的地址空间,划出一部分出来,专门用来
把IO外设寄存器的地址映射 到这部分划出来的内存地址空间 中。统一编址的好处是:读取IO外设的时候,就好像读取普通的内存地址空间中的数据一样。
4.8.1.2 独立编址
- IO 外设的地址空间,与主存单元的地址空间是两个独立的地址空间,此时,IO地址一般称作: IO端口。我们在读写IO外设的时候,从这些 “IO端口” 中读写就可以了。不同的外设,被分配了不同的 IO 端口号。CPU 提供了一些列函数来读写 IO 端口,例如:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
4.7 编译、验证
编译驱动模块:
$ make
输出文件:driver_interrupt.ko
因为我们捕获的是键盘中断(中断号:1),先看一下在加载驱动模块之前的中断驱动程序 head /proc/interrupts : 可以把 demsg 的输出也清理一下:dmesg -c 执行下面指令来加载驱动模块(传递2个参数):insmod driver_interrupt.ko irq=1 devname=myirq 再次执行一下指令 head /proc/interrupts 查看驱动程序: 在中断号 1 的右侧,是不是看到了我们的驱动程序:my_irq 再来看一下 dmesg 的输出信息: 成功注册了中断号1 的处理函数! 此时,按几次键盘左上角的 ESC 键,然后再查看 dmesg 的输出信息:
|