Linux内核中断编程
为什么会有中断机制?
中断产生的根本原因就是因为外设的数据处理速度远远慢于CPU,比如使用CPU读取UART接收缓冲区的数据,当使用CPU读取UART接收缓冲区的数据时,发现UART接收缓冲区的数据并没有准备就绪,一般做法是采用轮询方式等待UART接收缓冲区准备就绪,但是轮询等待的方式会导致CPU利用率降低,耗费大量的CPU资源,于是Linux内核引入了中断处理机制。
CPU采用中断方式获取UART接收缓冲区的数据流程如下:
 

当CPU读取UART接收缓冲区的数据,发现UART接收缓冲区并没有准备就绪,那么CPU可以先去处理别的事情。
一旦将来UART接收缓冲区数据准备就绪,UART控制器就会向CPU发送一个中断电信号告诉CPU数据准备就绪了。
CPU接收到这个中断信号后,会立刻停止正在处理的进程(处理进程,保存现场),转去处理UART接收缓冲区的数据,一旦处理完毕,CPU还会回到之前中断的位置(恢复现场)进行执行。
此时此刻,CPU至少做了两件事,大大提高了CPU的利用率。

中断的硬件触发流程
以按键触发中断为例说明。

当按键按下,产生一个下降沿电信号,此电信号首先跑到了中断控制器。
中断控制器拿到这个下降沿电信号后,开始进行一次判断。
中断控制器首先判断这个案件的GPIO管脚是否使能中断功能,如果禁止则将该信号直接丢弃,如果使能则继续判断。
接下来中断控制器判断此下降沿信号是否是有效的中断触发信号(初始化设置的五种的其中之一),如果无效则直接丢弃,否则进行判断。
接下来中断控制器判断当前CPU是否有正在处理的高优先级中断,如果有则直接丢弃,如果没有则继续判断。
中断控制器判断此中断信号应该送给哪个CPU核进行处理,是单独发给CPU0还是所有CPU核都要发送
最后中断控制器还要判断这个中断信号是以什么方式发送,IRQ( 外部中断模式)还是FIQ(快速中断请求),这里选择IRQ中断模式。然后发送给指定的CPU核
最后CPU接收到了这个按键触发的IRQ中断信号,CPU核会立刻触发一个IRQ中断异常,进入IRQ中断异常处理流程(CPU核硬件自动完成):
备份CPSR到SPSR_IRQ
设置CPSR
保存返回地址LR_IRQ = PC - 4
设置PC = 0X18,让CPU核跑到0X18地址去进行运行。
到此开启软件进一步处理IRQ中断异常处理流程
CPU软件处理IRQ中断异常流程:
首先要建立一个异常向量表,异常向量表就是在CPU核7种异常的处理入口地址(0x00 / 0x04 / 0x08 / 0x0c / 0x10 / 0x18 / 0x1c) 处放置自己的代码,每当异常发送时,CPU核都会去处理对应的代码。
CPU核一旦跑到0x18地址运行,则软件上需要做以下流程:
保护现场,保护CPU原先正在处理的任务的现场,做一个压栈处理。
根据用户需求完成IRQ中断的处理。
恢复现场,恢复到原先被打断的任务的现场,做一个出栈处理。
到此完成了一个IRQ中断处理。
使用中断处理编程
中断处理的软件编程需要完成以下四部分功能的实现:

编写异常向量表的代码。
编写保护现场的代码。
根据用户需求完成对中断的处理,也就是一个中断处理函数。
编写恢复现场的代码。
不过,在Linux内核中或者其他操作系统甚至裸板开发上我们只需要完成上面的第三步即可(中断处理的编写) ,其余都由芯片厂家完成了。
中断处理是有优先级并且可以嵌套的,如下图所示:

当处理主程序时接收到1号中断请求会保存现场转而处理1号中断。
但是在1号中断处理过程中接收到更高优先级的2号中断信号,继续保护现场去处理2号中断。
在执行2号中断处理过程中接收到了最高优先级的3号中断信号,继续保护现场去处理3号中断信号。
在处理完3号中断后恢复现场继续处理2号中断,完成2号中断处理后恢复到1号中断处理现场继续完成1号中断处理,最终完成最初的中断后恢复到原来打断的主程序流程中继续运行。(等待下次中断触发)
当反过来的时候就不一样了,当先遇到的是3号中断后,在处理3号中断中收到1号或者2号中断信号不会响应处理而是将低优先级的中断信号丢掉不响应。

 

Linux内核中断编程的操作步骤
驱动开发中断处理相关函数API
驱动开发只需要利用一下两个函数向内核注册或者删除硬件中断的中断处理函数即可,一旦注册成功,将来硬件中断触发内核会自动调用注册的中断函数:

request_irq

int request_irq(unsigned int irq,
				irq_handler_t handler,
      			unsigned long flags,
      			const char *name,
      			void *dev) 

函数功能:由于CPU的硬件中断(GPIO)对于内核来说都是一种宝贵的资源,所以驱动如果需要使用某个硬件中断,首先向内核申请硬件中断资源,然后向内核注册这个硬件中断对应的中断处理函数,一旦完成注册,将来中断触发时会自动调用注册的中断处理函数。

irq:Linux内核给每一个CPU的硬件中断都分配了一个指定的软件编号,该编号也就是中断号。(通过gpio_to_irq(gpio)获得)

handler:传递要注册的中断处理函数,中断处理函数的原型:
 

irqreturn_t (*irq_handler_t)(int irq, void *dev)
#irq:当前触发的硬件中断对应的中断号。
#dev:保存给中断处理函数传递的参数,之前request_irq中的 第五个参数。
#返回值:IRQ_NONE:中断处理函数执行失败。
#		IRQ_HANDLED:中断处理函数执行成功

 

flags:中断标志(外部中断,内部中断)

外部中断:就是芯片外部通过中断连接线传递给芯片的中断信号。
内部中断:处理器内部各个硬件控制器对应的中断传递方式,比如UART控制器和中断控制器之间的传递。
IRQF_TRIGGER_FALLING:下降沿触发
IRQF_TRIGGER_RISING:上升沿触发
IRQF_TRIGGER_HIGH:高电平触发
IRQF_TRIGGER_LOW:低电平触发
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING:双边沿
如果是内部中断,flags直接给0
name:中断名称,将来中断申请注册完毕后,通过执行 cat /proc/interrupts 能够查看到这个name(判断是否注册成功)

dev:给中断处理函数传递的参数,如果不需要传递参数可以给NULL(类似线程传参方式)。

free_irq

void free_irq(int irq, void *dev)

 

函数功能:释放中断资源并且删除中断处理函数
irq:指定释放的中断资源。
dev:给中断处理函数传递的参数(注册中断处理函数时传递的参数,释放时必须保持相同)
结论:一旦中断申请成功并且中断处理函数注册成功,只需要等待硬件中断触发,内核即会自动调用等待中的中断处理函数。

示例(按键中断触发)

代码实现:
btn_drv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h> //标准按键值:KEY_*

//声明描述按键信息相关的数据结构
struct btn_resource {
    int gpio; //按键对应的GPIO编号
    char *name; //按键名称
    int code; //按键值
};

//定义初始化按键硬件信息对象
static struct btn_resource btn_info[] = {
    {
        .gpio = PAD_GPIO_A+28,
        .name = "KEY_UP",
        .code = KEY_UP
    },
    {
        .gpio = PAD_GPIO_B+9,
        .name = "KEY_DOWN",
        .code = KEY_DOWN
    },
};

//中断处理函数
//一旦按键按下或者松开都会调用中断处理函数
//如果是KEY_UP按键有操作,此时irq=gpio_to_irq(GPIOA28)
//如果是KEY_DOWN按键有操作,此时irq=gpio_to_irq(GPIOB9)
//如果是KEY_UP按键有操作,dev=&btn_info[0]
//如果是KEY_DOWN按键有操作,dev=&btn_info[1]
static irqreturn_t button_isr(int irq, void *dev)
{
    int state;
    //1.获取对应的当前触发的硬件中断的硬件信息
    struct btn_resource *pdata = 
                        (struct btn_resource *)dev;
    
    //2.获取按键对应GPIO的电平状态,获取按键的操作状态
    state = gpio_get_value(pdata->gpio);

    //3.打印按键信息
    printk("%s:按键值[%d]按键状态[%s]\n",
            __func__, pdata->code, state?"松开":"按下");
    return IRQ_HANDLED; //执行成功
}

static int btn_init(void)
{
    int i;
    //1.申请GPIO资源
    //配置为输入功能由内核已经帮你实现
    //2.申请中断资源,注册中断处理函数
    //注意:多个按键共享一个中断处理函数
    for (i = 0; i < ARRAY_SIZE(btn_info); i++) {
        //由GPIO编号求中断号
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_request(btn_info[i].gpio, 
                        btn_info[i].name);
        request_irq(irq,  //中断号
                button_isr,//注册的中断处理函数
            IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                btn_info[i].name,//中断名称
                &btn_info[i] //将每个按键对应的硬件信息传                                递给中断处理函数 
                );
    }
    return 0;
}

static void btn_exit(void)
{
    int i;
    //1.释放中断资源删除中断处理函数
    //2.释放GPIO资源
    for(i = 0; i < ARRAY_SIZE(btn_info); i++) {\
        int irq = gpio_to_irq(btn_info[i].gpio);
        gpio_free(btn_info[i].gpio);
        free_irq(irq, &btn_info[i]);
    }
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");

Makefile

obj-m += btn_drv.o
all:
	make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
	make -C /opt/kernel SUBDIRS=$(PWD) clean

 执行结果:

 

能够发现,当按下按键后会显示对应的按键并打印状态为[on],抬起后打印状态为[off],这是因为我们采用的是双边沿触发。
大家可以更换中断触发标志试一下。 

Logo

更多推荐