Cortex-A7中断系统

中断向量表

向量地址 中断类型 中断模式
0X00 复位中断(Rest) 特权模式(SVC)
0X04 未定义指令中断(Undefined Instruction) 未定义指令中止模式(Undef)
0X08 软中断(Software Interrupt,SWI) 特权模式(SVC)
0X0C 指令预取中止中断(Prefetch Abort) 中止模式
0X10 数据访问中止中断(Data Abort) 中止模式
0X14 未使用(Not Used)未使用0X18IRQ中断 (IRQ Interrupt) 外部中断模式(IRQ)
0X18 IRQ中断 (IRQ Interrupt) 外部中断模式(IRQ)
0X1C FIQ中断 (FIQ Interrupt) 快速中断模式(FIQ)

我们常见的SDMA_IRQn UART_IRQn等中断都是通过0X18地址的IRQ传递

GIC 控制器简介

../../_images/image-20200604141948859.png

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

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

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

中断ID

中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些ID就是 中断ID。每一个CPU最多支持 1020个中断 ID,中断ID号为 ID0~ID1019。这1020个ID包 含了PPI、SPI和SGI,那么这三类中断是如何分配这 1020个中断 ID的呢?这 1020个ID分 配如下:

  • ID0~ID15:这 16个 ID分配给 SGI。

  • ID16~ID31:这 16个 ID分配给 PPI。

  • ID32~ID1019:这 988个 ID分配给 SPI,像 GPIO中断、串口中断等这些外部中断

至于具体到某个ID对应哪个中断那 就由半导体厂商根据实际情况去定义了。比如 I.MX6U的总共 使用了128个中断 ID,加上前面属于 PPI和 SGI的 32个ID I.MX6U的中断源共有 128+32=160个. 这128个中断ID对应的中断在《 I.MX6ULL参考手册》的[3.2 Cortex A7 interrupts]有定义, 限于篇幅原因,摘部分如下。

#define NUMBER_OF_INT_VECTORS 160 /* 中断源160个,SGI+PPI+SPI */
typedef enum IRQn {
	/* Auxiliary constants */
	NotAvail_IRQn = -128,
	/* Core interrupts */
	Software0_IRQn = 0,
	Software1_IRQn = 1,
	Software2_IRQn = 2,
	Software3_IRQn = 3,
	Software4_IRQn = 4,
	Software5_IRQn = 5,
	Software6_IRQn = 6,
	Software7_IRQn = 7,
	Software8_IRQn = 8,
	Software9_IRQn = 9,
	Software10_IRQn = 10,
	Software11_IRQn = 11,
	Software12_IRQn = 12,
	Software13_IRQn = 13,
	Software14_IRQn = 14,
	Software15_IRQn = 15,
	VirtualMaintenance_IRQn = 25,
	HypervisorTimer_IRQn = 26,
	VirtualTimer_IRQn = 27,
	LegacyFastInt_IRQn = 28,
	SecurePhyTimer_IRQn = 29,
	NonSecurePhyTimer_IRQn = 30,
	LegacyIRQ_IRQn = 31,
	/* Device specific interrupts */
	IOMUXC_IRQn = 32,
	DAP_IRQn = 33,
	SDMA_IRQn = 34,
	TSC_IRQn = 35,
	SNVS_IRQn = 36,
	//...... ......
	ENET2_1588_IRQn = 153,
	Reserved154_IRQn = 154,
	Reserved155_IRQn = 155,
	Reserved156_IRQn = 156,
	Reserved157_IRQn = 157,
	Reserved158_IRQn = 158,
	PMU_IRQ2_IRQn = 159
}IRQn_Type;

linux中断常用API函数

申请中断

/*
* 
* @irq:要申请中断的中断号。
* @handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
* @flags  :中断标志,可以在文件include/linux/interrupt.h里面查看定义
* @name   :中断名字,设置以后可以在 /proc/interrupts文件中看到对应的中断名字。
* @dev    :如果将 flags设置为 IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,
*           dev会传递给中断处理函数 irq_handler_t的第二个参数。
* @return :返回值:0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY的话表示中断已经被申请了
*/
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

flags 参数常用的一些如下:

标志 描述
IRQF_SHARED 多个设备共享一个中断线,如果使用共享中断的话,dev参数就是唯一区分他们的标志
IRQF_ONESHOT 单次中断,中断执行一次就 结束
IRQF_TRIGGER_NONE 无触发
IRQF_TRIGGER_RISING 上升沿触发
IRQF_TRIGGER_FALLING 下降沿触发
IRQF_TRIGGER_HIGH 高电平 触发
IRQF_TRIGGER_LOW 低电平触发

释放中断

/*
* 
* @irq:要释放中断的中断号。
* @dev    :如果将 flags设置为 IRQF_SHARED的话,dev用来区分不同的中断,共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
*           dev会传递给中断处理函数 irq_handler_t的第二个参数。
*/
void free_irq(unsigned int irq, void *dev)

中断处理函数

irqreturn_t (*irq_handler_t) (int, void *)
// 返回值如下:
enum irqreturn {
	IRQ_NONE = (0 << 0),
	IRQ_HANDLED = (1 << 0),
	IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;

// 可以看出irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)

中断使能与禁止函数

void enable_irq(unsigned int irq) 
void disable_irq(unsigned int irq) 

注意 :函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中 断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外 一个中断禁止函数:

void disable_irq_nosync(unsigned int irq) // 立即返回

关闭使能整个中断系统

local_irq_enable() 
local_irq_disable()
local_irq_save(flags) 
local_irq_restore(flags)

中断上下半部

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

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

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

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

上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?

下半部处理机制

软中断
/* 体定义在文件 include/linux/interrupt.h中,内容如下*/
struct softirq_action
{
	void (*action)(struct softirq_action *);
};
/*在 kernel/softirq.c文件中一共定义了 10个软中断,如下所示:*/
static struct softirq_action softirq_vec[NR_SOFTIRQS];
 
/*NR_SOFTIRQS是枚举类型,定义在文件 include/linux/interrupt.h中,定义如下:*/
enum { 
    HI_SOFTIRQ=0, 		/* 高优先级软中断 */ 
	TIMER_SOFTIRQ, 		/* 定时器软中断 */ 
	NET_TX_SOFTIRQ, 	/* 网络数据发送软中断 */ 
	NET_RX_SOFTIRQ, 	/* 网络数据接收软中断 */ 
	BLOCK_SOFTIRQ, 
	BLOCK_IOPOLL_SOFTIRQ, 
	TASKLET_SOFTIRQ, 	/* tasklet软中断 */ 
	SCHED_SOFTIRQ, 		/* 调度软中断 */ 
	HRTIMER_SOFTIRQ, 	/* 高精度定时器软中断 */ 
	RCU_SOFTIRQ, 		/* RCU软中断 */ 
	NR_SOFTIRQS 
};

注册对应的软中断处理函数

/*
* @nr:    要开启的软中断类型 上面的枚举中选一个
* @action:软中断对应的处理函数。
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))

函数触发

void raise_softirq(unsigned int nr)

注意:软中断必须在编译的时候静态注册

tasklet(小任务)

tasklet是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet之间,建议大家使用tasklet linux中关于tasklet的定义

struct tasklet_struct
{
	struct tasklet_struct *next; 	/* 下一个tasklet */
	unsigned long state; 			/* tasklet状态 */
	atomic_t count; 				/* 计数器,记录对tasklet的引用数 */
	void (*func)(unsigned long); 	/* tasklet执行的函数需要用户自己定义 */
	unsigned long data; 			/* 函数func的参数 */
};

初始化函数

/*
*@t:要初始化的 tasklet
*@func: tasklet的处理函数用户定义好后传入函数指针即可
*@data: 要传递给 func函数的参数
*/

void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
也可以使用宏一次完成定义和初始化
定义在 include/linux/interrupt.h
DECLARE_TASKLET(name, func, data)
/*
*@t:要调度的 tasklet,也就是 DECLARE_TASKLET宏里面的 name
*/
void tasklet_schedule(struct tasklet_struct *t)

使用示例

/* 定义taselet */ 
struct tasklet_struct testtasklet; 
/* tasklet处理函数 */ 
void testtasklet_func(unsigned long data) 
{ 
	/* tasklet具体处理内容 */ 
} 
/* 中断处理函数 */ 
irqreturn_t test_handler(int irq, void *dev_id) 
{ 
	...... 
	/* 调度tasklet */ 
	tasklet_schedule(&testtasklet);  /*让testtasklet在合适的时机引起调度*/
	...... 
} 
/* 驱动入口函数 */ 
static int __init xxxx_init(void) 
{ 
	...... 
	/* 初始化tasklet */
	tasklet_init(&testtasklet, testtasklet_func, data); /* 注册中断处理函数 */ 
	request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); 
	...... 
}
work queue工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet linux中work_struct结构体表示一个工作,内容如下

struct work_struct { 
	atomic_long_t data; 
	struct list_head entry; 
	work_func_t func; /* 工作队列处理函数 */ 
};

这些工作组织成工作队列,工作队列使用 workqueue_struct结构体表示,内容如下

struct workqueue_struct { 
	struct list_head pwqs; 
	struct list_head list; 
	struct mutex mutex; 
	int work_color; 
	int flush_color; 
	atomic_t nr_pwqs_to_flush; 
	struct wq_flusher *first_flusher; 
	struct list_head flusher_queue; 
	struct list_head flusher_overflow; 
	struct list_head maydays; 
	struct worker *rescuer; 
	int nr_drainers; 
	int saved_max_active; 
	struct workqueue_attrs *unbound_attrs; 
	struct pool_workqueue *dfl_pwq; 
	char name[WQ_NAME_LEN]; 
	struct rcu_head rcu; 
	unsigned int flags ____cacheline_aligned; 
	struct pool_workqueue __percpu *cpu_pwqs; 
	struct pool_workqueue __rcu *numa_pwq_tbl[]; 
};

Linux内核使用工作者线程 (worker thread)来处理工作队列中的各个工作, Linux内核使用worker结构体表示工作者线程, worker结构体内容如下:

struct worker { 
	union { 
		struct list_head entry; 
		struct hlist_node hentry; 
	}; 
	struct work_struct *current_work; work_func_t current_func; 
	struct pool_workqueue *current_pwq; 
	bool desc_valid; 
	struct list_head scheduled; 
	struct task_struct *task; 
	struct worker_pool *pool; 
	struct list_head node; 
	unsigned long last_active; 
	unsigned int flags; 
	int id; 
	char desc[WORKER_DESC_LEN]; 
	struct workqueue_struct *rescue_wq; 
};

可以看出,每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作 (work_struct)即可,关于工作队列和工作者线程我们基本不用去管。初始化一个工作

/*
*@_work: 自己定义的 work_struct
*@_func: 处理函数 需要用户自己定义
*/
#define INIT_WORK(_work, _func)

或者直接使用宏一次性完成定义和初始化

/*
*@n: 自己定义的 work_struct
*@f: 处理函数 需要用户自己定义
*/
#define DECLARE_WORK(n, f)

和 tasklet一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原型如下所示: bool schedule_work(struct work_struct *work)

使用示例代码

/* 定义工作(work) */ 
struct work_struct testwork; 
/* work处理函数 */
void testwork_func_t(struct work_struct *work)
{ 
	/* work具体处理内容 */ 
} 
/* 中断处理函数 */ 
irqreturn_t test_handler(int irq, void *dev_id) 
{ 
	// ...... 
	/* 调度work */ 
	schedule_work(&testwork); 
	// ...... 
} 
/* 驱动入口函数 */ 
static int __init xxxx_init(void) 
{ 
	// ...... 
	/* 初始化work */ 
	INIT_WORK(&testwork, testwork_func_t); 
	/* 注册中断处理函数 */ 
	request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); 
	// ...... 
}
新技术threaded irq
/*
*@irq: 中断号
*@handler:  中断服务函数,可以为空
*@thread_fn:线程函数
*@flags:
*@name:
*@dev:
*/
extern __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,irq_handler_t thread_fn, unsigned long flags, const char *name ,void *dev)

你可以只提供thread_fn ,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。 说你懒是开玩笑,内核开发者也不会那么在乎懒人。 以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个worker 线程来处理, 在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多CPU 空着,你偏偏让多个中断挤在这个 CPU 上? 新技术threaded irq ,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU上执行,这提高了效率。

设备树对中断解析

imx6ull.dtsi文件,其中的intc就是中断控制器节点
intc: interrupt-controller@00a01000 {
	compatible = "arm,cortex-a7-gic";/* 可以通过该属性在内核里面找到GIC控制器驱动代码 */
	#interrupt-cells = <3>;
	interrupt-controller;            /* 节点为空,表示当前节点是中断控制器 */
	reg = <0x00a01000 0x1000>,
	<0x00a02000 0x100>;
};

第一个 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文件中的 gpio5节点内容如下所示:

gpio5: gpio@020ac000 {
	compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
	reg = <0x020ac000 0x4000>;
	interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
				 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
	gpio-controller;
	#gpio-cells = <2>;
	interrupt-controller;   //表明了 gpio5节点也是个中断控制器用于控制gpio5所有IO的中断
	#interrupt-cells = <2>; //interrupt-cells修改为2
};

GPIO5一共用了2个中断号,一个是74,一个是75。可以打开《 IMX6ULL参考手册》的【Chapter 3 Interrupts and DMA Events】章节, 找到表3-1可以确定

74对应 GPIO5_IO00~GPIO5_IO15 这低 16个 IO 
75对应 GPIO5_IO16~GPIOI5_IO31这高 16位 IO 

在imx6ull-alientek-emmc.dts文件,我们又可以发现:

fxls8471@1e {
	compatible = "fsl,fxls8471";
	reg = <0x1e>;
	position = <0>;
	interrupt-parent = <&gpio5>; //设置中断控制器,这里使用 gpio5作为中断控制器。
	interrupts = <0 8>;          //0表示 GPIO5_IO00  8表示低电平触发。
};

xls8471是 NXP官方的 6ULL开发板上的一个磁力计芯片,fxls8471有一个中断引脚链接到了I.MX6ULL的 SNVS_TAMPER0因脚上,这个引脚可以复用为GPIO5_IO00所以当我们在fxls8471驱动代码里面就可以得到中断控制的所有信息找到中断号

/*
*@dev   : 设备节点。
*@index : 索引号 interrupts属性可能包含多条中断信息,通过 index指定要获取的信息。
*@return:中断号
*/
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

// 如果是GPIO中断,可以使用下面函数来获取 gpio对应的中断号,函数原型如
/*
*@gpio   : 要获取GPIO编号。
*@return:GPIO对应中断号
*/
int gpio_to_irq(unsigned int gpio)

其它的外设驱动代码获取中断的情况

之前我们提到过,设备树中的节点有些能被转换为内核里的platform_device有些不能转换(转换规则请参考前面章节),

对于能转换为platform_device的获取方式

/*
* platform_get_resource get a resource for a device
@dev: platform device
@type: resource type // 取哪类资源? IORESOURCE_MEM 、 IORESOURCE_REG、IORESOURCE_IRQ 等
@num: resource inde x // 这类资源中的哪一个?
*/
struct resource *platform_get_resource(struct platform_device *dev,unsigned int type, unsigned int num);

对于I2C 设备节点

I2C总线驱动在处理设备树里的I2C子节点时,也会处理其中的中断信息。一个I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下 drivers/i2c/i2c core.c ../../_images/image-20200604161152379.png 对于SPI 设备节点

SPI总线驱动在处理设备树里的 SPI子节点时,也会处理其中的中断信息。

一个SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里,代码如下 drivers/spi/spi.c ../../_images/image-20200604161241173.png

调用 of_irq_get 获得中断号

如果你的设备节点既不能转换为platform_device ,它也不是 I2C 设备,不是 SPI 设备,那么在驱动程 序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号。

对于GPIO

参考:drivers input keyboard gpio_keys.c,可以使用gpio_to_irq 或 gpiod_irq 获得中断号。 举例,假设在设备树中有如下节点:

gpio-keys {
	compatible    = "gpio keys";
	pinctrl-names = "default";
	user{
		label = "User Button";
		gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
		gpio-key,wakeup;
		linux,code = <KEY_1>;
	};
};

那么可以使用下面的函数获得引脚和 flags

button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);

再去使用gpiod_to_irq 获得中断号:

irq = gpiod_to_irq(bdata ->gpiod);