build_embed_linux_system

驱动中断管理

在嵌入式系统开发中,中断是十分重要的知识点,在大部分单片机构建的应用产品中,基本都是以前后台方式(大循环加中断)的方式来实现功能。在中断中处理外部的触发信号,并在主循环或者线程中处理应用;基于RTOS构建的系统应用,一般也使用Systick时钟中断作为核心滴答时钟。在嵌入式Linux应用来说,中断也是处理CPU的突发事件的主要方法,主要实现以下功能。

  1. 对于溢出,除零等异常情况的通知
  2. 多核之间通讯交互
  3. 内部模块和外部器件的请求处理(如gpio,spi,i2c,dma等在工作中都支持相应的中断触发)

理解中断的背后执行逻辑,对于单片机和嵌入式Linux的开发都有重要意义,这里以Cortex-A7内核讲解嵌入式Linux的中断处理机制。

interrupt_mechanism

对于Linux内核中的中断系统,由三部分参与。

  1. 外设模块是中断发起的源头,基于特定事件触发中断信号。
  2. 中断控制器,管理中断信号通知的模块(Generic Interrupt Controller, 简称GIC),决定中断释放触发或屏蔽。
  3. CPU内核,处理上报的中断信息,执行中断调用。

上述就是简化的Linux中断处理流程,不过事实上Linux系统的中断机制远复杂于此,

0x00    复位中断(Reset)                        特权(Supervisor)
0x04    未定义指令中断(Undefined Instruction)   未定义指令(undef)
0x08    软中断(Software Interrupt, SWI)        特权(Supervisor)
0x0C    指令预取中止中断(Prefetch Abort)        中止(Abort)
0x10    数据访问中止中断(Data Abort)            中止(Abort)
0x14    未使用(No Used)                        /
0x18    IRQ中断(IRQ Interrupt)                 外部中断(IRQ)
0x1C    FIQ中断(FIQ Interrupt)                 快速中断(FIQ)

上述列出的就是Cortex-A7芯片支持的全部中断类型,其中0x00~0x10为系统中断,当芯片执行出现异常,或者由SWI指令主动触发时,就会执行这些中断,一般由Linux内核管理,详细理解如下。

基于对单片机的了解,芯片都是支持多个外设的,特别是对于Cortex-A系统芯片,往往更加复杂,那么仅依靠7个中断如何支持外部中断的需求?这就要提到IRQ和FIQ了, 对于Cortex-A系列芯片来说,任意一个外部中断如IO_Interrupt, Legacy_Interrupt等,都会触发IRQ或FIQ中断,进入对应的中断函数。并在此函数中通过软件读取寄存器的值来判断具体发生了什么中断,这样多个中断可以使用同一个入口线号,如IRQ_Interrupt_handler。其中管理外部信号到中断触发的器件被称为GIC(Generic Interrupt Controller),其框架如下所示。

image

参考上图,中断根据源头的不同,其说明如下所示。

另外对于Cortex-A7芯片来说,中断使能包含IRQ或FIQ总中断使能,以及ID0-ID1019可选的中断源使能两部分;优先级为32bit的数据,支持抢占优先级和子优先级,这部分和Cortex-M系列基本是一致的。

interrupt_process

在理解Cortex-A7内核处理机制之前,需要了解到芯片内核状态分类如下:

user mode: 用户模式,用户空间AP执行所处于的模式。
superiver mode: 超级模式,或者SVC模式,大部分Linux内核执行代码处于该模式下。
IRQ mode: 中断模式,触发中断后,处理器进入的模式。
Abort mode: 用来处理上面提到的Data Abortprefetch Abort异常

和单片机的流程类似,当有外部触发信号到达,并且所有中断相关的使能都打开的情况下,中断控制器GIC就会根据配置好的硬件信息,将IRQ(或FIQ)的中断触发信息告知指定的Core。处理器感知到该信号后到达时,对于进行irq模式前的系统状态值如cpsr寄存器值,PC指针进行保存(分别保存到SPSR和LR寄存器中),置位相应的中断状态标志位。计算中断向量的入口位置,将PC设置为该值并跳转,总结下来,在中断发生时,内核的硬件处理包含置位中断信息,保存中断前关键状态,进入IRQ模式,然后在跳转到中断向量的入口,后续就由软件接口进行后续处理。

对于软件部分的处理,则包含irq模式,svc/usr模式处理和应用代码处理。irq模式主要进行了r0,lr以及cpsr的保存,压栈处理(Irq模式下的堆栈设定为12字节),根据进入中断前的系统模式,维护中断处理表格,进行后续的中断处理,根据中断前模式的不同分别执行不同的入口函数_irq_usr(用户模式入口函数)和irq_svc(superior模式入口函数),同时将系统模式切换到SVC模式。以用户模式为例,主要实现流程如下。

  1. 保存用户现场
  2. 执行中断向量irq_hander,将当前硬件中断系统的状态转换为定义好的软件IRQ Number, 然后调用IRQ Number对应的处理函数。
  3. 执行完中断相关的处理函数,将进入中断时候保存的现场恢复到实际的ARM寄存器中
  4. 返回到中断触发时执行的流程,即实现了中断返回。

interrupt_interface

对于内核中,涉及到中断访问的接口主要由中断申请和释放的函数组成。

//gpio号映射到irq号
int gpio_to_irq(unsigned gpio);

//gpiod结构映射到irq编号
int gpiod_to_irq(const struct gpio_desc *desc);

//从设备树中解析获取interrupt属性对应的irq编号
unsigned int irq_of_parse_and_map(struct device_node *node, int index);

//从内核中申请irq对象
//handler为中断上半部,内部不能有延时和让系统进入休眠状态的代码
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

//释放申请的irq对象
void free_irq(unsigned int irq, void *dev);

//带内核管理申请irq对象接口
//request_irq的系统管理版本,支持驱动移除时自动释放
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
        unsigned long irqflags, const char *devname, void *dev_id)

//申请线程化的irq对象,其中handler为内核态执行函数,thread_fn为线程化的用户态执行函数
//hander,中断上半部
//thread_fn,中断下半部,线程化的中断执行函数,支持休眠和用户态的程序
//irqflags,定义的中断类型,需要与设备树中中断信息一致(或为0,则使用设备树定义的中断信息),否则重新加载驱动时会报错(第一次以驱动为准)
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
        irq_handler_t handler, irq_handler_t thread_fn,
        unsigned long irqflags, const char *devname,
        void *dev_id)
//中断回调函数
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    //中断的具体实现
    //...
    return IRQ_RETVAL(IRQ_HANDLED);
}

当然,也可以通过devm_request_irq来申请中断,可以在驱动卸载时不用主动调用free_irq显示释放中断请求。此外,也可以通过disable_irq(非中断函数中),disable_irq_nosync(中断函数中)和enable_irq来管理中断的开关。

在单片机中,我们了解到中断的执行会打断其它应用的执行,所以中断的动作应该尽可能的短,如果具体的操作过长,会把代码分为两部分,其中时间相关比较紧要的在中断中执行,而非必要的则移动到主循环中执行,如UART通讯中,数据接收往往通过中断或者DMA获取,而协议解析和应用处理则在主循环执行,那么对于嵌入式Linux来说,在驱动中也采用类似的机制,把中断的应用拆成两部分执行,一般称为顶半部和底半部机制执行。

其中顶半部即为上述的中断回调函数key0_handler,其在内核模式下调用,会影响到系统的执行;因此只能进行简单的事件触发、状态修改动作,不能够执行导致挂起,休眠的行为,否则会出错。具体的应用则在底半部执行,实现具体的功能,底半部可以执行挂起或者休眠动作,不影响系统功能。当然,在顶半部也可以使用timer软件定时器,tasklet工作队列等方式,模拟系统底半部的执行,单独即可实现功能,当然使用devm_request_threaded_irq接口可以在单个接口中实现上述应用。

理解到这,我们对嵌入式Linux中的中断触发和在内核的应用已经有了初步的了解,那么我们是如何获取外设对应的硬件中断号,这就需要涉及设备树中的中断信息节点,以i.MX6Ull为例,在<3.2 Cortex A7 interrupts>章节中,定义了内部的中断ID,以按键对应的GPIO1_18引脚中断为例,其属性gpio1控制器,对应中断就为GIC_SPI_BASE+66和GIC_SPI_BASE+67,隶属于gpio1下的引脚,中断触发时检测和执行即如下所示。

  1. 外部引脚检测动作,触发IRQ中断
  2. IRQ中断中检索对应中断信号,确认并通知GPIO1控制器
  3. GPIO1控制器则检索支持的中断线号,找到指定引脚的回调函数
  4. 执行完毕后通知应用层,既可以完成对于中断的处理

关于中断在设备树中的应用如下所示。

//定义中断控制器节点
gpio1: gpio@0209c000 {
    //其它已经注释
    interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
    <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

//具体的中断节点
key {
    interrupt-parent = <&gpio1>;
    interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};

//中断线号带两个参数类型,表示中断控制器为gpio1,对应gpio1的9号中断
interrput-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_LEVEL_LOW>;  
//中断线号带三个参数类型
//interrupts = <中断类型,中断号偏移值,中断触发类型>
//中断类型
//GPI_SGI: software generated interrupt,中断号:0~15
//GIC_PPI: peripheral processer interrupt, 中断号:16~31
//GIC_SPI:shared processer interrupt, 中断号:32~160
interrupts = <GIC_SPI 23 1> //对应中断线号32+23=55, 上升沿触发

中断触发条件在设备树中的定义如下。

在具体的代码实现中,即可通过函数irq_of_parse_and_map找到引脚对应的中断线号,用于后续中断回调函数的申请。

上面描述了中断引脚对应的设备树,下面描述中断使用的接口,具体如下。

bottom_half

在Linux内核中,中断处理分为上半部(top half)和下半部(bottom half)。上半部处理紧急的、对时间要求严格的任务,而下半部处理相对不紧急的任务。下半部可以通过多种机制实现,包括软中断(softirq)、tasklet和工作队列(workqueue)

softirq

软中断是一种在中断上下文执行的下半部机制。软中断可以在多个CPU上并行执行,因此适用于对响应时间要求较高的任务。软中断的处理函数必须是可重入的,因为它们可能在多个CPU上同时执行。

注册软中断

void softirq_handler(struct softirq_action *action)
{
    // 处理软中断的代码
}

open_softirq(TIMER_SOFTIRQ, softirq_handler);

/**
 * @brief 软中断类型枚举
 * 
 * 定义了Linux内核中各种软中断的类型。软中断是一种在中断上下文执行的下半部机制,
 * 用于处理相对不紧急的任务。软中断可以在多个CPU上并行执行,因此适用于对响应时间要求较高的任务。
 * 软中断的处理函数必须是可重入的,因为它们可能在多个CPU上同时执行。
 */
enum
{
    /**
     * @brief 高优先级软中断
     * 
     * 用于处理最高优先级的软中断任务。
     */
    HI_SOFTIRQ = 0,

    /**
     * @brief 定时器软中断
     * 
     * 用于处理定时器相关的软中断任务。
     */
    TIMER_SOFTIRQ,

    /**
     * @brief 网络发送软中断
     * 
     * 用于处理网络发送相关的软中断任务。
     */
    NET_TX_SOFTIRQ,

    /**
     * @brief 网络接收软中断
     * 
     * 用于处理网络接收相关的软中断任务。
     */
    NET_RX_SOFTIRQ,

    /**
     * @brief 块设备软中断
     * 
     * 用于处理块设备相关的软中断任务。
     */
    BLOCK_SOFTIRQ,

    /**
     * @brief 中断轮询软中断
     * 
     * 用于处理中断轮询相关的软中断任务。
     */
    IRQ_POLL_SOFTIRQ,

    /**
     * @brief 任务队列软中断
     * 
     * 用于处理任务队列相关的软中断任务。
     */
    TASKLET_SOFTIRQ,

    /**
     * @brief 调度软中断
     * 
     * 用于处理调度相关的软中断任务。
     */
    SCHED_SOFTIRQ,

    /**
     * @brief 高精度定时器软中断
     * 
     * 用于处理高精度定时器相关的软中断任务。
     */
    HRTIMER_SOFTIRQ,

    /**
     * @brief RCU软中断
     * 
     * 用于处理RCU(Read-Copy Update)相关的软中断任务。
     * RCU是一种用于实现高效的读-写同步的机制。
     */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    /**
     * @brief 软中断总数
     * 
     * 定义了软中断的总数,用于表示软中断类型的范围。
     */
    NR_SOFTIRQS
};

触发软中断

raise_softirq(TIMER_SOFTIRQ);

tasklet

在Linux内核中,中断处理分为上半部(top half)和下半部(bottom half)。上半部处理紧急的、对时间要求严格的任务,而下半部处理相对不紧急的任务。下半部可以通过多种机制实现,包括软中断(softirq)、tasklet和工作队列(workqueue)

//使用tasklet管理中断
//底半部应用部分执行
static void tasklet_do_func(unsigned long data)
{
    printk(KERN_INFO"key interrupt tasklet do:%ld!\r\n", data);
}
DECLARE_TASKLET(tasklet_func, tasklet_do_func, 0);

//顶半部中断向量执行
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    /*触发事件*/
    tasklet_schedule(&tasklet_func);
    return IRQ_RETVAL(IRQ_HANDLED);
}

int key_probe(platform_device *pdev)
{
    //申请中断
    devm_request_irq(&pdev->dev, chip->irq, key0_handler, 
                    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 
                    "key0", 
                    (void *)chip);
}

work_queue

工作队列是一种在进程上下文执行的下半部机制。工作队列适用于需要睡眠的任务,因为它们在进程上下文中执行,可以安全地调用睡眠函数。

定义和初始化工作队列

#include <linux/workqueue.h>

void work_handler(struct work_struct *work)
{
    // 处理工作队列的代码
}

DECLARE_WORK(work, work_handler);

调度工作队列

schedule_work(&my_work);

timer

内核定时器基于硬件定时器实现,它使用一个全局的定时器链表来管理所有的定时器。当一个定时器被创建并启动后,它会被插入到定时器链表中,并根据其到期时间进行排序。当硬件定时器触发中断时,内核会检查定时器链表,找出所有到期的定时器,并执行相应的处理函数。

struct key_data
{
    struct timer_list timer;
};

//使用timer管理中断
//底半部应用部分执行
static void timer_function(struct timer_list *arg)
{ 
    struct integration_data *chip = container_of(arg, struct integration_data, timer);

    chip->timer.expires = jiffies + 10;
    add_timer(&chip->timer);
    printk(KERN_INFO"timer function:%d!\r\n", chip->value);
}

//顶半部中断向量执行
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct key_data *chip = (struct key_data *)dev_id;

    chip->timer.expires = jiffies + 10;
    add_timer(&chip->timer);
    return IRQ_RETVAL(IRQ_HANDLED);
}

int key_probe(platform_device *pdev)
{
    key_data *chip;

    //.....

    //申请中断
    devm_request_irq(&pdev->dev, chip->irq, key0_handler, 
                    IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, 
                    "key0", 
                    (void *)chip);


    //初始化定时器    
    timer_setup(&chip->timer, timer_function, 0);
    chip->timer.expires = jiffies + 10;
    add_timer(&chip->timer);
}

带上下半段的中断向量应用

static irqreturn_t key_handler(int irq, void *data)
{
    struct key_data* chip = (struct key_data*)data;
    printk(KERN_INFO"key_handler:%d!\r\n", chip->data);
}

int key_probe(platform_device *pdev)
{
    key_data *chip;

    //.....

    //申请中断
    chip->irq = irq_of_parse_and_map(nd, 0);
    ret = devm_request_threaded_irq(&pdev->dev,
                            chip->irq, 
                            NULL, key_handler, 
                            IRQF_SHARED | IRQF_ONESHOT | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,       
                            "key0", 
                            (void *)chip);
}

总结

至此,我们对嵌入式应用中的中断机制进行了解读,当然这些都是理论知识的说明,对于嵌入式应用中,如何结合实际情况,配置合适的中断优先级,并实现应用从而满足产品的需求才是最重要的部分。这部分经验是需要实践积累和总结的,不过理解了中断实现的背后机制,在实践中知其然也知其所以然,以理论配合应用来学习,也是嵌入式开发的最佳提升之道。

next_chapter

返回目录

直接开始下一节说明: i2c设备和驱动管理框架