linux中断编程

本文档只介绍中断编程所需的函数及应用,中断完整处理流程应参考文档《linux中断处理流程》。

本文档基于3.14内核。

0. 基础

中断取代了轮询的通知方式,DMA取代了轮询的读写数据方式。
中断分类
软件指令造成的中断(又叫异常,同步中断)。    svc, und, abt
硬件通过中断请求信号造成的中断(异步中断)。  irq,fiq
向量中断和非向量中断
采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同中断号的中断有不同的入口地址。非向量中断的多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。
向量中断由硬件提供中断服务程序的入口地址,非向量中断由软件提供向量中断服务程序入口地址。
中断的特点
1)中断随机的。
2)中断是可恢复的。
3)中断是自动进行处理的。

中断硬件

在ARM多核处理器中最常用的中断控制器是GIC,如I.MX6U(Cortex-A)的中断控制器为GIC。

I.MX6U 是 Cortex-A 内核的,GIC V2。GIC V2 最多支持 8 个核。ARM 会根据 GIC 版本的不同研发出不同的 IP 核,那些半导体厂商直接购买对应的 IP 核即可,比如 ARM 针对 GIC V2 就开发出了 GIC400 这个中断控制器 IP 核。当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况:VFIQ、VIRQ、FIQ 和 IRQ。VFIQ和VIRQ是针对虚拟化的。

一般情况下只关注IRQ,相当于GIC最终向ARM内核就上报一个IRQ信号。

GIC 将众多的中断源分为分为三类:

1)SPI(Shared Peripheral Interrupt),共享中断。顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。

2)PPI(Private Peripheral Interrupt),私有中断。GIC 是支持多核的,每个核有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。

3)SGI(Software-generated Interrupt),软件中断。由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。

每一个CPU最多支持1020个中断ID,中断ID号为ID0~ID1019。ID0~ID15,SGI;ID16~ID31,PPI;ID32~ID1019,988个,SPI,像GPIO中断、串口中断等外部中断,中断对应的ID由半导体厂商定义。

I.MX6U总共使用了128个SPI中断,加上PPI和SGI的32个,共计160个。

GIC 架构分为了两个逻辑块: Distributor 和 CPU Interface,也就是分发器端和 CPU 接口端。

Distributor(分发器端):此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。GIC 的分发器端相对于 GIC 基地址偏移为 0X1000。

CPU Interface(CPU 接口端):CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端就是分发器和 CPU Core 之间的桥梁。CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000。

GIC控制器的寄存机基地址通过CP15获取。CP15 协处理器通过 c0 寄存器可以获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。

中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020个中断源的使能。使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断。寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。

GIC 寄存器 GICD_ISENABLERn 和 GICD_ ICENABLERn 用来完成外部中断的使能和禁止,对于 Cortex-A7 内核来说中断 ID 只使用了 512 个。一个 bit 控制一个中断 ID 的使能,那么就需要 512/32=16 个 GICD_ISENABLER 寄存器来完成中断的使能。同理,也需要 16 个GICD_ICENABLER 寄存器来完成中断的禁止。其中 GICD_ISENABLER0 的 bit[15:0]对应ID15~0 的 SGI 中断,GICD_ISENABLER0 的 bit[31:16]对应 ID31~16 的 PPI 中断。剩下的GICD_ISENABLER1~GICD_ISENABLER15 就是控制 SPI 中断的。

Cortex-A7 最多可以支持 256 个优先级,数字越小,优先级越高!半导体厂商自行决定选择多少个优先级。I.MX6U 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级。

linux中断分为顶半部和底半部两部分

中断处理函数以共享方式注册时,要设置设备id,否则设备注册不成功。独享方式时,设备id为NULL。

共享中断返回IRQ_NONE, IRQ_HANDLED.
do_irq()(x86)  <====> asm_do_irq() (arm)

中断顶半部函数,禁止中断,不可嵌套(通过request_irq()申请)

中断处理函数是被硬件请求执行的内核代码,所以它属于中断上下文。在中断上下文中绝对不允许出现睡眠或者可能睡眠的代码,否则会死机。

睡眠代码:ssleep(), msleep()。
可能造成睡眠的代码:
kmalloc(2048, GFP_KERNEL);  //内存不足时可能睡眠,不能用于中断中。
kmalloc(2048, GFP_ATOMIC);  //内存不足时不睡眠,直接返回错误码,可以用在中断中。
copy_to_user(),copy_from_user(); //可能睡眠,不允许用在中断中。

 一. 申请和释放中断

一般在设备驱动模块的初始化中申请中断,在模块卸载函数中释放中断。

// linux/irqreturn.h
/**
 * enum irqreturn
 * @IRQ_NONE        interrupt was not from this device
 * @IRQ_HANDLED     interrupt was handled by this device
 * @IRQ_WAKE_THREAD handler requests to wake the handler thread
 */
enum irqreturn {
    IRQ_NONE        = (0 << 0),
    IRQ_HANDLED     = (1 << 0),
    IRQ_WAKE_THREAD     = (1 << 1),
};

typedef enum irqreturn irqreturn_t;
typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id);
#define IRQF_TRIGGER_NONE 0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010

irq要申请的硬件中断号。

handler是向系统登记的中断处理函数(顶半部),是一个回调函数,中断发生后,系统调用这个函数,dev_id参数将传递给handler。dev_id为void *指针,需要与request_irq函数的dev参数保持一致,可以指向设备数据结构。

irqflags是中断处理的属性,可以指定中断的触发方式以及处理方式。在触发方式方面,可以是IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW等。在处理方式方面,若设置了IRQF_DISABLED,表明中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序调用时不会屏蔽其他设备的驱动(4.1.15内核已经删除IRQF_DISABLED)。若设置了IRQF_SHARED,则表示多个设备共享中断,dev_id在中断共享时会用到,一般设置为这个设备的结构体或者NULL。

name是中断名,在/proc/interrupts中显示。

返回值0表示成功,返回-EINVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。

void free_irq(unsigned int irq, void *dev_id);

二. 使能和屏蔽中断

1. 屏蔽一个中断源irq

extern void disable_irq_nosync(unsigned int irq);
extern void disable_irq(unsigned int irq);
extern void disable_percpu_irq(unsigned int irq);
extern void enable_irq(unsigned int irq);
extern void enable_percpu_irq(unsigned int irq, unsigned int type);

disable_irq_nosync()与disable_irq()的区别在于前者立即返回,而后者等待目前的中断处理完成。由于disable_irq()会等待指定的中断被处理完,因此如果在n号中断的顶半部调用disable_irq(n),会引起系统的死锁,在这种情况下只能调用disable_irq_nosync(n)。

2. 屏蔽本CPU上所有中断

#define local_irq_enable()  
#define local_irq_disable() 
#define local_irq_save(flags)                  
#define local_irq_restore(flags) 

flags为unsigned long类型,被直接传递,而不是通过指针。

local_irq_save(flags);可以保存当前中断状态,完成后使用local_irq_restore(flags);返回原来中断状态,防止直接使用local_irq_enable()开启所有中断,使其他任务从中断中恢复,产生意外甚至崩溃。

3. 中断底半部的屏蔽使能

void local_bh_disable(void);
void local_bh_enable(void);

禁止和使能软中断和tasklet底半部机制的函数。

三. 中断底半部

中断底半部机制有三种,软中断、tasklet(小任务)和workqueue(工作队列)。一般不推荐使用软中断。

一般中断处理过程分为两部分:

顶半部:就是中断处理函数,短时间能完成的操作可以放在顶半部。

底半部:把比较耗时的代码提出来,放到底半部执行。

一些参考:

1)如果要处理的内容不希望被其他中断打断,那么可以放到上半部。

2)如果要处理的任务对时间敏感,可以放到上半部。

3)如果要处理的任务与硬件有关,可以放到上半部。

4)除了上述三点以外的其他任务,优先考虑放到下半部。

3.1 软中断

软中断的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。

在linux内核中,用softirq_action结构体表征一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq()函数可以触发一个软中断。

local_bh_disable()和local_bh_enable()是内核中用于禁止和使能软中断及tasklet底半部机制的函数。

本文档描述基于3.14.77内核。

softirq定义在linux/interrupt.h中,实现在kernel/softirq.c中。tasklet是基于软中断实现的,相关代码也在这两个文件中。

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

#define SOFTIRQ_STOP_IDLE_MASK (~(1 << RCU_SOFTIRQ))

/* map softirq index to softirq name. update 'softirq_to_name' in
 * kernel/softirq.c when adding a new softirq.
 */
extern const char * const softirq_to_name[NR_SOFTIRQS];

/* softirq mask and active fields moved to irq_cpustat_t in
 * asm/hardirq.h to get better cache usage.  KAO
 */

struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

说明:之前linux版本,struct softirq_action包含两项action(软中断服务程序)和data(服务程序输入参数),因为其参数为softirq_action,可定义为全局变量而省去data定义。

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
asmlinkage void do_softirq(void);
asmlinkage void __do_softirq(void);

#ifdef __ARCH_HAS_DO_SOFTIRQ
void do_softirq_own_stack(void);
#else
static inline void do_softirq_own_stack(void)
{
    __do_softirq();
}
#endif

extern void open_softirq(int nr, void (*action)(struct softirq_action *));
extern void softirq_init(void);
extern void __raise_softirq_irqoff(unsigned int nr);

extern void raise_softirq_irqoff(unsigned int nr);
extern void raise_softirq(unsigned int nr);

实现机制

软中断守护程序是软中断机制实现的核心,它的实现过程简单。通过查询软中断的状态来判断是否发生事件,当发生事件就会映射软中断向量表,调用执行注册的action()函数就可以了。从这一点分析可以看出,软中断的服务程序是daemon。在linux中软中断daemon线程函数为do_softirq()。

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())
        return;

    local_irq_save(flags);

    pending = local_softirq_pending();

    if (pending)
        do_softirq_own_stack();

    local_irq_restore(flags);
}

网络软中断举例:

当网络上有数据时,发生了硬件中断,硬件中断服务程序会接收网络数据,设置中断状态,并将网络数据挂接到链表中,进行中断调度,这一步可以通过net_schedule()函数完成。硬件中断服务程序最后退出并且CPU开始调度软中断,软中断daemon会发现网络软中断发生了事件,其会执行网络中断对应的服务程序,即进入网络协议栈处理程序。 

3.2 tasklet

tasklet是利用软中断实现的一种底半部机制,推荐使用tasklet而不是softirq。

tasklet在linux/interrupt.h中定义,属中断上下文,不能访问0-3G空间,不能用copy_to_user()函数。

3.3 workqueue 

把工作交给一个内核线程去执行,这个下半部是在进程上下文中执行的,因此,它可以重新调度还有睡眠。

工作队列属进程上下文,其执行晚于中断处理函数和tasklet,可休眠。

区分使用软中断/tasklet还是工作队列比较简单,如果推后的工作不需要睡眠,那么就选择软中断或tasklet,但如果需要一个可以重新调度,可以睡眠,可以获取内存,可以获取信号量,可以执行阻塞式I/O操作时,那么,请选择工作队列吧!

四. 设备树处理

    intc: interrupt-controller@00a01000 {
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;
        interrupt-controller;
        reg = <0x00a01000 0x1000>,
              <0x00a02000 0x100>;
    };

#interrupt-cells 描述了 interrupts 属性的 cells 大小,也就是一条信息有几个 cells。每个 cells 都是 32 位整形值,对于 ARM 处理的GIC 来说,一共有 3 个 cells,这三个 cells 的含义如下:

第一个 cells:中断类型,0 表示 SPI 中断,1 表示 PPI 中断。

第二个 cells:中断号,对于 SPI 中断来说中断号的范围为 0~987,对于 PPI 中断来说中断号的范围为 0~15。

第三个 cells:标志,bit[3:0]表示中断触发类型,为 1 的时候表示上升沿触发,为 2 的时候表示下降沿触发,为 4 的时候表示高电平触发,为 8 的时候表示低电平触发。bit[15:8]为 PPI 中断的 CPU 掩码。

对于 gpio 来说,gpio 节点也可以作为中断控制器

// imx6ull.dtsi                
gpio1: gpio@0209c000 {
                compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
                reg = <0x0209c000 0x4000>;
                interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
                         <GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
                gpio-controller;
                #gpio-cells = <2>;
                interrupt-controller;
                #interrupt-cells = <2>;
            };    
// imx6ull-emmc.dts
        edt-ft5x06@38 {
        compatible = "edt,edt-ft5306", "edt,edt-ft5x06";
        pinctrl-names = "default";
        pinctrl-0 = <&ts_int_pin
                 &ts_reset_pin>;

        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 0>;   // 设置中断信息,9表示GPIO1_IC09,0表示???
        reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
        irq-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>; 
        status = "okay";
    };

1) #interrupt-cells,指定中断源的信息 cells 个数。
2) interrupt-controller,表示当前节点为中断控制器。
3) interrupts,指定中断号,触发方式等。
4) interrupt-parent,指定父中断,也就是中断控制器。

获取中断号

irq_of_parse_and_map 函数从 interupts 属性中提取到对应的中断号:

unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。

int gpio_to_irq(unsigned int gpio)

如果是GPIO的话,可通过gpio_to_irq函数获取中断号。gpio:要获取的 GPIO 编号。返回值:GPIO 对应的中断号。

 

参考:

1. linux内核对中断的处理方式

2. linux中断处理流程

3. 设备驱动开发详解 宋宝华

4. 工作队列workqueue应用

5. 小任务tasklet应用

6. 中断函数

7. linux中断编程

8. linux网络编程 宋敬彬

posted @ 2018-03-18 15:31  yuxi_o  阅读(764)  评论(0编辑  收藏  举报