<Linux开发>驱动开发 -之-内核定时器与中断

这篇具有很好参考价值的文章主要介绍了<Linux开发>驱动开发 -之-内核定时器与中断。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

<Linux开发>驱动开发 -之-内核定时器与中断

交叉编译环境搭建:
<Linux开发> linux开发工具-之-交叉编译环境搭建

uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)

Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)

Linux文件系统构建移植参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
<Linux开发>系统移植 -之-使用buildroot构建BusyBox根文件系统

Linux驱动开发参考以下:
<Linux开发>驱动开发 -之-pinctrl子系统
<Linux开发>驱动开发 -之-gpio子系统
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的LED驱动
<Linux开发>驱动开发 -之-基于pinctrl/gpio子系统的beep驱动
<Linux开发>驱动开发 -之-资源的并发与竞争处理

一 前言

定时器是我们最常用到的功能,一般用来完成定时功能,熟悉一下 Linux 内核提供的定时器 API 函数,通过这些定时器 API 函数我们可以完成很多要求定时的应用。Linux内核也提供了短延时函数,比如微秒、纳秒、毫秒延时函数,本章我们就来学习一下这些和时间有关的功能。中断也是频繁使用的功能,Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。下面我们就来一一分析。

二 内核定时器

2.1 内核时间管理-系统时钟

如果读者之前接触过UCOS或FreeRTOS就会知道,UCOS或FreeRTOS是需要一个硬件定时器提供系统时钟的,一般情况下使用Systick作为系统时钟源,但也有使用普通定时器 作为时钟源的。相同的,Linux要能正常运行,也是需要有一个系统时钟的,至于所使用的的哪个定时器作为系统时钟源,这个笔者也不知道,哈哈,毕竟linux的东西太多了,有兴趣的读者可以自己查阅资料了解一下(如果有熟悉的读者也可以分享一下哈);有一份文档中有描述一个通用定时器,在《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的“chapter B8 The Generic Timer”章节;至于实际linux是否是不是使用这个定时器,有兴趣的可以研究下。
在Linux内核有很多需要时间管理来参与的场景或程序,比如在周期性的调度程序,或者延时程序中;其实对于开发驱动程序来说,我们不用太关注具体的系统时钟是什么,因为这是系统/liunx内核工程师关注的内容;对于驱动编写来说只需关注如何使用内核定时器完成相关时间管理即可。硬件定时器提供时钟源,时钟源的频率可以设置, 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率),比如1000Hz, 500Hz,100Hz 等等,这些说的就是系统节拍率。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,编译Linux内核前使用‘make menuconfig' 命令即可打开配置界面,按照如下路径设置配置:

-> Kernel Features 
	 -> Timer frequency (<choice> [=y]) 

<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断
在上图配置中有多个配置值,配置自己想要的频率即可,保存到“arch/arm/configs/imx_v7_water_emmc_defconfig”后可在该文件内查看配置如下:
<Linux开发>驱动开发 -之-内核定时器与中断
该定义会被linux内核使用来配置时钟频率。相关引用如下:

路径:include/asm-generic/param.h
#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H

#include <uapi/asm-generic/param.h>

# undef HZ
# define HZ		CONFIG_HZ	/* Internal kernel timer frequency */
# define USER_HZ	100		/* some user interfaces are */
# define CLOCKS_PER_SEC	(USER_HZ)       /* in "ticks" like times() */
#endif /* __ASM_GENERIC_PARAM_H */

<Linux开发>驱动开发 -之-内核定时器与中断
从第7行的注释也能看出其作用是作为linux内部定时器频率。

2.2 内核时间管理-节拍率

在2.1小节我们知道了配置linux内部定时器频率;在上述定义的宏HZ,表示的是1秒的节拍数,也就是我们常说的频率了。
大家看到设置的值是100Hz,相对是一个比较小的值,按我们学过STM32等单片机的了解,怎么也得MHz起步吧?我们知道频率越高需要的中断次数也就越频繁,频率的高低会有其优缺点之处,主要如下:
(1) 优点:较高的频率/节拍率能够提高系统时间精度,如采用100Hz频率,时间精度为10ms,而采用1000Hz频率,时间精度是1ms,后者相对前者提升了10倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。
(2)缺点:高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担,1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。根据自己的实际情况,选择合适的系统节拍率,作者所做实验全部采用默认的100Hz 系统节拍率。

讲到节拍率,如果读者以前在开发过程中有涉及到开机时间等要求的,相比接触过全局变量jiffies;Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0,jiffies 定义定义如下:

路径:include/linux/jiffies.h
/*
 * The 64-bit value is not atomic - you MUST NOT read it
 * without sampling the sequence number in jiffies_lock.
 * get_jiffies_64() will do this for you as appropriate.
 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

<Linux开发>驱动开发 -之-内核定时器与中断
第 76 行,定义了一个 64 位的 jiffies_64。
第 77 行,定义了一个 unsigned long 类型的 32 位的 jiffies。
jiffies_64 和 jiffies 其实是同一个东西,jiffies_64 用于 64 位系统,而 jiffies 用于 32 位系统。为了兼容不同的硬件,jiffies 其实就是 jiffies_64 的低 32 位,jiffies_64 和 jiffies 的结构如下图:
<Linux开发>驱动开发 -之-内核定时器与中断
当我们访问 jiffies 的时候其实访问的是 jiffies_64 的低 32 位,使用 get_jiffies_64 这个函数可以获取 jiffies_64 的值。在 32 位的系统上读取 jiffies 的值,在 64 位的系统上 jiffes 和 jiffies_64表示同一个变量,因此也可以直接读取 jiffies 的值。所以不管是 32 位的系统还是 64 位系统,都可以使用 jiffies。
在2.1小节说了 HZ 表示每秒的节拍数,jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。 time = jiffies/HZ,以100hz为例,则time = jiffies/100 (单位:秒);
不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回来了,因此有些资料也将这个现象也叫做绕回。假如 HZ 为最大值 1000 的时候,32 位的 jiffies 大约只需要 49.7(0xffffffff / 1000/3600/24) 天就发生了绕回,对于 64 位的 jiffies 来说大概需要5.8 亿年才能绕回,因此 jiffies_64 的绕回忽略不计。处理 32 位 jiffies 的绕回显得尤为重要,Linux 内核提供了如下表所示的几个 API 函数来处理绕回。
函数路径:include/linux/jiffies.h

函数 描述
time_after(unkown, known) unkown 通常为 jiffies,known 通常是需要对比的值。
time_before(unkown, known) unkown 通常为 jiffies,known 通常是需要对比的值。
time_after_eq(unkown, known) unkown 通常为 jiffies,known 通常是需要对比的值。
time_before_eq(unkown, known) unkown 通常为 jiffies,known 通常是需要对比的值。
函数 使用描述 结果
time_after(unkown, known) 如果 unkown 超过 known 的话 返回true,否则返回false
time_before(unkown, known) 如果 unkown 没有超过 known 的话 返回true,否则返回false
time_after_eq(unkown, known) 如果 unkown 超过或等于 known 的话 返回true,否则返回false
time_before_eq(unkown, known) 如果 unkown 没有超过或等于 known 的话 返回true,否则返回false

使用示例如下:

unsigned long timeout;
timeout = jiffies + (1 * HZ); /* 超时的时间点 */
 
/*************************************
 具体功能实现代码
 ************************************/
 
 /* 判断有没有超时 */
 if(time_before(jiffies, timeout)) {
	 	/* 超时未发生 */
 } else {
		 /* 超时发生 */
 }
unsigned long timeout;
timeout = jiffies + (1 * HZ); /* 超时的时间点 */
 
/*************************************
 具体功能实现代码
 ************************************/
 
 /* 判断有没有超时 */
 if(time_after(jiffies, timeout)) {
	 	/* 超时发生 */
 } else {
		 /* 超时未发生 */
 }

在上述两个示例中,变量timeout表示超时时间点,上述中设置判断代码执行时间不超过1s,那么超时时间点就是开始运行代码的时间(当前jiffies)+超时时间1s(1*HZ);然后在代码运行后判断当前时间(jiffies) 与设置的超时时间点(timeout)关系。
在time_before示例中,表示jiffies 比timeout大,则不超时,反之超时;
在time_after示例中,表示jiffies 比timeout小,则超时,反之不超时;

前面我们都是以jiffies 节拍率来计算时间,为了开发方便些,Linux内核提供了一些转换函数,方便jiffies 与ms\us\ns之间的转换,如下表:
函数路径:include/linux/jiffies.h

函数 描述
int jiffies_to_msecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的毫秒
int jiffies_to_usecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的微秒
int jiffies_to_nsecs(const unsigned long j) 将 jiffies 类型的参数 j 分别转换为对应的纳秒
long msecs_to_jiffies(const unsigned int m 将毫秒转换为 jiffies 类型。
long usecs_to_jiffies(const unsigned int u) 将微秒转换为 jiffies 类型。
unsigned long nsecs_to_jiffies(u64 n) 将纳秒转换为 jiffies 类型。

2.3 内核定时器介绍

在学习单片机的时候定时器是比较常用的功能,可以做周期性定时工作,也可以做捕获计时等功能。Linux 内核定时器采用系统时钟来实现,并不是imx6ull中的的 PIT 等硬件定时器。Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。
值得注意的,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
Linux 内核使用 timer_list 结构体表示内核定时器,内容如下:

路径:include/linux/timer.h
struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	struct list_head entry;
	unsigned long expires;   /* 定时器超时时间,单位是节拍数 */
	struct tvec_base *base;

	void (*function)(unsigned long);  /* 定时处理函数 */
	unsigned long data;    /* 要传递给 function 函数的参数 */

	int slack;

#ifdef CONFIG_TIMER_STATS
	int start_pid;
	void *start_site;
	char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

<Linux开发>驱动开发 -之-内核定时器与中断

当要使用内核定时器时,要先定义一个 timer_list 变量,表示定时器;tiemr_list 结构体的expires 成员变量表示超时时间,单位为节拍数。比如我们现在需要定义一个周期为 4 秒的定时器,那么这个定时器的超时时间就是 jiffies+(4HZ),因此 expires=jiffies+(2HZ)。function 就是定时器超时以后的定时处理函数,我们要做的工作就放到这个函数里面,这个定时处理函数是需要我们自己编写的。

2.4 内核定时器API

在2.3小节介绍定时器中,定义好定时器之后,就需要通过linux内核提供的相关API来操作初始化这个定时器了。相关API函数如下表:
函数路径:include/linux/timer.h

函数原型 描述 参数 返回值
void init_timer(struct timer_list *timer) init_timer 函数负责初始化 timer_list 类型变量,当我们定义了一个 timer_list 变量以后一定要先用 init_timer 初始化一下 timer:要初始化定时器 返回值:没有返回值
void add_timer(struct timer_list *timer) add_timer 函数用于向 Linux 内核注册定时器,使用 add_timer 函数向内核注册定时器以后,定时器就会开始运行 timer:要注册的定时器 返回值:没有返回值
void del_timer(struct timer_list *timer) del_timer 函数用于删除一个定时器,不管定时器有没有被激活,都可以使用此函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用 del_timer 函数删除定时器之前要先等待其他处理器的定时处理器函数退出。 timer:要删除的定时器 返回值:0,定时器还没被激活;1,定时器已经激活。
void del_timer_sync(struct timer_list *timer) del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除,del_timer_sync 不能使用在中断上下文中。 timer:要初始化定时器 返回值:0,定时器还没被激活;1,定时器已经激活。
int mod_timer(struct timer_list *timer, unsigned long expires) mod_timer 函数用于修改定时值,如果定时器还没有激活的话,mod_timer 函数会激活定时器。 timer:要修改超时时间(定时值)的定时器。expires:修改后的超时时间。 返回值:0,调用 mod_timer 函数前定时器未被激活;1,调用 mod_timer 函数前定时器已被激活。

了解了内核定时器相关API,后面我们将使用这些API为我们开发一些功能代码。

2.5 Linux 内核短延时函数

其实在linux内核中提供了一些较短的延时函数;有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。Linux 内核提供了毫秒、微秒和纳秒延时函数,如下表:
路径:include/linux/delay.h

函数 描述
void ndelay(unsigned long nsecs) 延时纳秒函数
void udelay(unsigned long usecs) 延时微秒函数
void mdelay(unsigned long mseces) 延时毫秒函数

使用示例,
路径:drivers/i2c/busses/i2c-au1550.c
在该驱动中,有如下内容:

路径:drivers/i2c/busses/i2c-au1550.c
...
#include <linux/delay.h>
.....

static int wait_xfer_done(struct i2c_au1550_data *adap)
{
	int i;

	/* Wait for Tx Buffer Empty */
	for (i = 0; i < adap->xfer_timeout; i++) {
		if (RD(adap, PSC_SMBSTAT) & PSC_SMBSTAT_TE)
			return 0;

		udelay(1);
	}

	return -ETIMEDOUT;
}

其中头文件,就是引用linux内核延时函数的头文件; 其次代码中的udelay(1); 就是调用1微秒延时。该函数的功能就是一直循环等待发送完成,每1微秒判断一次是否发送完成。

2.6 内核定时器使用模板

使用内核定时器模板如下:

#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/semaphore.h>

/* XXX 设备结构体 */
struct XXX_dev{
	dev_t devid; 					/* 设备号 */
	 struct cdev cdev; 			/* cdev */
	struct class *class; 			/* 类 */
	struct device *device; 		/* 设备 */
	int major; 						/* 主设备号 */
	int minor; 						/* 次设备号 */
	struct device_node *nd; 	/* 设备节点 */
	int timeperiod; 				/* 定时周期,单位为 ms */
	struct timer_list timer; 		/* 定义一个定时器 */
	spinlock_t lock; 				/* 定义自旋锁 */
	/*所开发设备的其它参数*/
	....
};

struct XXX_dev XXXdev; /* XXX设备 */

/* 
驱动中的其它相关函数
.......
*/

 /* 定时器回调函数 */
void timer_function(unsigned long arg)
{
	struct XXX_dev *dev = (struct XXX_dev *)arg;
	int timerperiod;
	unsigned long flags;
	
	/* 
	定时器中需要做的内容
	.....
	*/
	
	/* 重启定时器 */
	spin_lock_irqsave(&dev->lock, flags);
	timerperiod = dev->timeperiod;
	spin_unlock_irqrestore(&dev->lock, flags);
	mod_timer(&dev->timer, jiffies +
	msecs_to_jiffies(dev->timeperiod));
}


static int __init XXX_init(void)
{
	/* 初始化自旋锁 */
	spin_lock_init(&XXXdev.lock);

	/*
	 驱动初始化的 其它操作
	 ........
	 */
	
	/* 初始化 timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */
	init_timer(&timerdev.timer);
	timerdev.timer.function = timer_function;
	timerdev.timer.data = (unsigned long)&timerdev;
	return 0;
}

/*
 驱动的其它函数内容
 ........
 */

三 单片机STM32中的中断

相信在接触Linux之前,大家都接触过STM32单片机;我们先来看下STM32的中断,然后在分析IMX6ULL的中断,这样有个对比,能更好的理解。

在STM32中的中断系统主要的关键内容有如下所列:
(1) 中断向量表;
(2) NVIC-内嵌向量中断控制器;
(3) 中断设置使能;
(4) 中断服务函数;

3.1 中断向量表

中断向量表是一个表,这个表里面存放的是中断向量。中断服务程序的入口地址或存放中断服务程序的首地址成为中断向量,因此中断向量表是一系列中断服务程序入口地址组成的表。这些中断服务程序(函数)在中断向量表中的位置是由半导体厂商定好的,当某个中断被触发以后就会自动跳转到中断向量表中对应的中断服务程序(函数)入口地址处。中断向量表在整个程序的最前面,比如 STM32F103 的中断向量表如下所示:
在STM32的汇编文件startup_stm32f10x_hd.s中:

............
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC
                DCD     EXTI0_IRQHandler           ; EXTI Line 0
                DCD     EXTI1_IRQHandler           ; EXTI Line 1
                DCD     EXTI2_IRQHandler           ; EXTI Line 2
                DCD     EXTI3_IRQHandler           ; EXTI Line 3
                DCD     EXTI4_IRQHandler           ; EXTI Line 4
                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel 1
                DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel 2
                DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel 3
                DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel 4
                DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel 5
                DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel 6
                DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel 7
                DCD     ADC1_2_IRQHandler          ; ADC1 & ADC2
                DCD     USB_HP_CAN1_TX_IRQHandler  ; USB High Priority or CAN1 TX
                DCD     USB_LP_CAN1_RX0_IRQHandler ; USB Low  Priority or CAN1 RX0
                DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
                DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
                DCD     EXTI9_5_IRQHandler         ; EXTI Line 9..5
                DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
                DCD     TIM1_UP_IRQHandler         ; TIM1 Update
                DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
                DCD     TIM1_CC_IRQHandler         ; TIM1 Capture Compare
                DCD     TIM2_IRQHandler            ; TIM2
                DCD     TIM3_IRQHandler            ; TIM3
                DCD     TIM4_IRQHandler            ; TIM4
                DCD     I2C1_EV_IRQHandler         ; I2C1 Event
                DCD     I2C1_ER_IRQHandler         ; I2C1 Error
                DCD     I2C2_EV_IRQHandler         ; I2C2 Event
                DCD     I2C2_ER_IRQHandler         ; I2C2 Error
                DCD     SPI1_IRQHandler            ; SPI1
                DCD     SPI2_IRQHandler            ; SPI2
                DCD     USART1_IRQHandler          ; USART1
                DCD     USART2_IRQHandler          ; USART2
                DCD     USART3_IRQHandler          ; USART3
                DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
                DCD     RTCAlarm_IRQHandler        ; RTC Alarm through EXTI Line
                DCD     USBWakeUp_IRQHandler       ; USB Wakeup from suspend
                DCD     TIM8_BRK_IRQHandler        ; TIM8 Break
                DCD     TIM8_UP_IRQHandler         ; TIM8 Update
                DCD     TIM8_TRG_COM_IRQHandler    ; TIM8 Trigger and Commutation
                DCD     TIM8_CC_IRQHandler         ; TIM8 Capture Compare
                DCD     ADC3_IRQHandler            ; ADC3
                DCD     FSMC_IRQHandler            ; FSMC
                DCD     SDIO_IRQHandler            ; SDIO
                DCD     TIM5_IRQHandler            ; TIM5
                DCD     SPI3_IRQHandler            ; SPI3
                DCD     UART4_IRQHandler           ; UART4
                DCD     UART5_IRQHandler           ; UART5
                DCD     TIM6_IRQHandler            ; TIM6
                DCD     TIM7_IRQHandler            ; TIM7
                DCD     DMA2_Channel1_IRQHandler   ; DMA2 Channel1
                DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
                DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY
............

上述代码内容就是 STM32F103 的中断向量表,中断向量表都是链接到代码的最前面,在比如一般 ARM 处理器都是从地址 0X00000000 开始执行指令的,那么中断向量表就是从 0X00000000 开始存放的。上述代码中第 7 行的“__initial_sp”就是第一条中断向量,存放的是栈顶指针,接下来是第 8 行复位中断复位函数 Reset_Handler 的入口地址,依次类推,直到第84 行的最后一个中断服务函数 DMA2_Channel4_5_IRQHandler 的入口地址,这样 STM32F103 的中断向量表就建好了。

我们说 ARM 处理器都是从地址 0X00000000 开始运行的,但是我们学习 STM32 的时候代码是下载到 0X8000000 开始的存储区域中。因此中断向量表是存放到 0X8000000 地址处的,而不是 0X00000000,这样不是就出错了吗?为了解决这个问题,Cortex-M 架构引入了一个新的概念——中断向量表偏移,通过中断向量表偏移就可以将中断向量表存放到任意地址处,中断向量表偏移配置在函数 SystemInit 中完成,通过向 SCB_VTOR 寄存器写入新的中断向量表首地址即可,在文件“system_stm32f10x.c”中,代码如下所示:

void SystemInit (void)
{
  /* Reset the RCC clock configuration to the default reset state(for debug purpose) */
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */   
  
  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;      
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
    
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
  SetSysClock();

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 
}

<Linux开发>驱动开发 -之-内核定时器与中断
第 265 行和第 267 行就是设置中断向量表偏移,第 265 行是将中断向量表设置到 RAM 中,第267 行是将中断向量表设置到 ROM 中,基本都是将中断向量表设置到 ROM 中,也就是地址0X8000000 处。第 267 行用到了FALSH_BASE 和 VECT_TAB_OFFSET,这两个都是宏,定义如下所示:

文件:system_stm32f10x.c
/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET  0x0 /*!< Vector Table base offset field. 
                                  This value must be a multiple of 0x200. */

<Linux开发>驱动开发 -之-内核定时器与中断
因此第 267 行的代码就是:SCB->VTOR=0X080000000,中断向量表偏移设置完成。

通过上面的讲解我们了解了两个跟 STM32 中断有关的概念:中断向量表和中断向量表偏移,那么这个跟 I.MX6U 有什么关系呢?因为 I.MX6U 所使用的 Cortex-A7 内核也有中断向量表和中断向量表偏移,而且其含义和 STM32 是一模一样的!只是用到的寄存器不同而已,概念完全相同!

3.2 中断控制器

有那么多中断,那中断系统得有个管理机构,对于 STM32 这种 Cortex-M 内核的单片机来说这个管理机构叫做 NVIC,全称叫做 Nested Vectored Interrupt Controller。这里不作详细的讲解,既然 Cortex-M 内核有个中断系统的管理机构—NVIC,那么 I.MX6U 所使用的 Cortex-A7 内核是不是也有个中断系统管理机构?答案是肯定的,不过 Cortex-A 内核的中断管理机构不叫做NVIC,而是叫做 GIC,全称是 general interrupt controller。

3.3 中断设置使能

要使用某个外设的中断,肯定要先使能这个外设的中断,以 STM32F103 的 PA0 这个 IO 为例,假如我们要使用 PA0 的输入中断肯定要使用如下代码来使能对应的中断:

NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);

上述代码就是使能 PA0 对应的 EXTI0 中断,同理,如果要使用 I.MX6U 的某个中断的话也需要使能其对应的中断。

3.4 中断服务函数

我们使用中断的目的就是为了使用中断服务函数,当中断发生以后中断服务函数就会被调用,我们要处理的工作就可以放到中断服务函数中去完成。同样以 STM32F103 的 PA0 为例,其中断服务函数如下所示:

/* 外部中断 0 服务程序 */
void EXTI0_IRQHandler(void)
{
/* 中断处理代码 */
}

在3.1小节的代码31行中,就已经设置好EXTI Line 0的中断函数入口为EXTI0_IRQHandler; 当 PA0 引脚的中断触发以后就会调用其对应的中断处理函数 EXTI0_IRQHandler,我们可以在函数 EXTI0_IRQHandler 中添加中断处理代码。同理,I.MX6U 也有中断服务函数,当某个外设中断发生以后就会调用其对应的中断服务函数。通过对 STM32 中断系统的回顾,我们知道了 Cortex-M 内核的中断处理过程,那么 Cortex-A 内核的中断处理过程是否是一样的,有什么异同呢?接下来我们看看Cortex-A7 内核的中断系统。

四 IMX6ULL中的中断

4.1 中断向量表

与Cortex-M一样,Cortex-A7 也有中断向量表,中断向量表也是在代码的最前面。Cortex-A7 内核有 8 个异常中断,这 8 个异常中断的中断向量表如下表 所示:

向量地址 中断类型 中断模式
0x00 复位中断(Rest) 特权模式(SVC)
0x04 未定义指令中断(Undefined Instruction) 未定义指令中止模式(Undef)
0x08 软中断(Software Interrupt,SWI) 特权模式(SVC)
0x0C 指令预取中止中断(Prefetch Abort) 中止模式
0x10 数据访问中止中断(Data Abort) 中止模式
0x14 未使用(Not Used) 未使用
0x18 IRQ 中断(IRQ Interrupt) 外部中断模式(IRQ)
0x1C FIQ 中断(FIQ Interrupt) 快速中断模式(FIQ)

中断向量表里面都是中断服务函数的入口地址,因此一款芯片有什么中断都是可以从中断向量表看出来的。从上表中可以看出,Cortex-A7 一共有 8 个中断,而且还有一个中断向量未使用,实际只有 7 个中断。

与前面第三章介绍的STM32中断向量表相比,Cortex-A7的中断少很多;按理解,Linux系统应该有很多中断才对,STM32拥有的EXTI0_IRQHandler、TIM3_IRQHandler等中断,在Cortex-A7中应该也有类似的才对,还有SPI、IIC等中断。

Cortex-A 和 Cotex-M 在中断向量表的区别,对于 Cortex-M 内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于 Cotex-A 内核来说并没有这么做,在上表中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理。这些外部中断和 IRQ 中断的关系如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
上图左侧的 Software0_IRQn~PMU_IRQ2_IRQ 这些都是 I.MX6U 的中断,他们都属于 IRQ 中断。当上图左侧这些中断中任意一个发生的时候 IRQ 中断都会被触发,所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处理。
在上表中一共有 7 个中断类型,简单介绍一下这 7 个中断:
①、复位中断(Rest),CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断,Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此
中断的发生。
⑦、FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
常用的就是复位中断和 IRQ 中断。

4.2 中断控制器

4.2.1 GIC 控制器总览

GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。目前 GIC 有 4 个版本:V1~V4,V1 是最老的版本,已经被废弃了。V2-V4 目前正在大量的使用。GIC V2 是给 ARMv7-A 架构使用的,比如 Cortex-A7、Cortex-A9、Cortex-A15 等,V3 和 V4 是给 ARMv8-A/R 架构使用的,也就是 64 位芯片使用的。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,他们之间的关系如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
在上图中,GIC 接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给 ARM 内核,这四个信号的含义如下:
VFIQ:虚拟快速 FIQ。
VIRQ:虚拟外部 IRQ。
FIQ:快速中断 IRQ。
IRQ:外部中断 IRQ。
VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了,我们前面都讲了很多次了。本次我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号。那么 GIC 是如何完成这个工作的呢?在文档《ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf》的第23页有GICV2 总体框图:
<Linux开发>驱动开发 -之-内核定时器与中断
上图中左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分,GIC 将众多的中断源分为
分为三类:
①、SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。

4.2.2 GIC 控制器中断 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.MX6UL 参考手册》的“3.2 CortexA7 interrupts”小节,部分内容如下:

IRQ ID 中断源 中断描述
32 IOMUXC IOMUXC的通用寄存器1。用于在启动时通知核心出现异常情况。
33 DAP 调试访问端口中断请求。
34 SDMA 来自所有通道的SDMA中断请求。
35 TSC TSC((触摸)中断。
36 SNVS_LP /SNVS_HP SNVS 中断
37 LCDIF
88 EPIT1 EPIT1输出比较中断。
89 EPIT2 EPIT2输出比较中断。
90 GPIO1 INT7中断请求
91 GPIO1 INT6中断请求
92 GPIO1 INT5中断请求
93 GPIO1 INT4中断请求
149 PWM8 累计中断线路。包括比较中断,边沿中断,FIFO中断等
150 ENET1 ENET1中断
151 ENET1 ENET1 1588定时器中断[同步]请求。
152 ENET2 ENET2中断
153 ENET2 MAC 0 1588定时器中断[同步]请求。
154 Reserved Reserved
155 Reserved Reserved
156 Reserved Reserved
157 Reserved Reserved
158 Reserved Reserved
159 PMU core、gpu或soc调节器上的Brown out事件。

更多中断源请查阅《I.MX6UL 参考手册》的“3.2 CortexA7 interrupts”小节。

NXP 官方 SDK中的文件 MCIMX6Y2C.h,在此文件中定义了一个枚举类型 IRQn_Type,此枚举类型就枚举出了 I.MX6U 的所有中断,代码如下所示:

/** Interrupt Number Definitions */
#define NUMBER_OF_INT_VECTORS 160                /**< Number of interrupts in the Vector table */

typedef enum IRQn {
  /* Auxiliary constants */
  NotAvail_IRQn                = -128,             /**< Not available device specific interrupt */

  /* Core interrupts */
  Software0_IRQn               = 0,                /**< Cortex-A7 Software Generated Interrupt 0 */
  Software1_IRQn               = 1,                /**< Cortex-A7 Software Generated Interrupt 1 */
  Software2_IRQn               = 2,                /**< Cortex-A7 Software Generated Interrupt 2 */
  Software3_IRQn               = 3,                /**< Cortex-A7 Software Generated Interrupt 3 */
  Software4_IRQn               = 4,                /**< Cortex-A7 Software Generated Interrupt 4 */
  Software5_IRQn               = 5,                /**< Cortex-A7 Software Generated Interrupt 5 */
  Software6_IRQn               = 6,                /**< Cortex-A7 Software Generated Interrupt 6 */
  Software7_IRQn               = 7,                /**< Cortex-A7 Software Generated Interrupt 7 */
  Software8_IRQn               = 8,                /**< Cortex-A7 Software Generated Interrupt 8 */
  Software9_IRQn               = 9,                /**< Cortex-A7 Software Generated Interrupt 9 */
  Software10_IRQn              = 10,               /**< Cortex-A7 Software Generated Interrupt 10 */
  Software11_IRQn              = 11,               /**< Cortex-A7 Software Generated Interrupt 11 */
  Software12_IRQn              = 12,               /**< Cortex-A7 Software Generated Interrupt 12 */
  Software13_IRQn              = 13,               /**< Cortex-A7 Software Generated Interrupt 13 */
  Software14_IRQn              = 14,               /**< Cortex-A7 Software Generated Interrupt 14 */
  Software15_IRQn              = 15,               /**< Cortex-A7 Software Generated Interrupt 15 */
  VirtualMaintenance_IRQn      = 25,               /**< Cortex-A7 Virtual Maintenance Interrupt */
  HypervisorTimer_IRQn         = 26,               /**< Cortex-A7 Hypervisor Timer Interrupt */
  VirtualTimer_IRQn            = 27,               /**< Cortex-A7 Virtual Timer Interrupt */
  LegacyFastInt_IRQn           = 28,               /**< Cortex-A7 Legacy nFIQ signal Interrupt */
  SecurePhyTimer_IRQn          = 29,               /**< Cortex-A7 Secure Physical Timer Interrupt */
  NonSecurePhyTimer_IRQn       = 30,               /**< Cortex-A7 Non-secure Physical Timer Interrupt */
  LegacyIRQ_IRQn               = 31,               /**< Cortex-A7 Legacy nIRQ Interrupt */

  /* Device specific interrupts */
  IOMUXC_IRQn                  = 32,               /**< General Purpose Register 1 from IOMUXC. Used to notify cores on exception condition while boot. */
  DAP_IRQn                     = 33,               /**< Debug Access Port interrupt request. */
  SDMA_IRQn                    = 34,               /**< SDMA interrupt request from all channels. */
  TSC_IRQn                     = 35,               /**< TSC interrupt. */
  SNVS_IRQn                    = 36,               /**< Logic OR of SNVS_LP and SNVS_HP interrupts. */
  LCDIF_IRQn                   = 37,               /**< LCDIF sync interrupt. */
  RNGB_IRQn                    = 38,               /**< RNGB interrupt. */
  CSI_IRQn                     = 39,               /**< CMOS Sensor Interface interrupt request. */
  PXP_IRQ0_IRQn                = 40,               /**< PXP interrupt pxp_irq_0. */
  SCTR_IRQ0_IRQn               = 41,               /**< SCTR compare interrupt ipi_int[0]. */
  SCTR_IRQ1_IRQn               = 42,               /**< SCTR compare interrupt ipi_int[1]. */
  WDOG3_IRQn                   = 43,               /**< WDOG3 timer reset interrupt request. */
  Reserved44_IRQn              = 44,               /**< Reserved */
  APBH_IRQn                    = 45,               /**< DMA Logical OR of APBH DMA channels 0-3 completion and error interrupts. */
  WEIM_IRQn                    = 46,               /**< WEIM interrupt request. */
  RAWNAND_BCH_IRQn             = 47,               /**< BCH operation complete interrupt. */
  RAWNAND_GPMI_IRQn            = 48,               /**< GPMI operation timeout error interrupt. */
  UART6_IRQn                   = 49,               /**< UART6 interrupt request. */
  PXP_IRQ1_IRQn                = 50,               /**< PXP interrupt pxp_irq_1. */
  SNVS_Consolidated_IRQn       = 51,               /**< SNVS consolidated interrupt. */
  SNVS_Security_IRQn           = 52,               /**< SNVS security interrupt. */
  CSU_IRQn                     = 53,               /**< CSU interrupt request 1. Indicates to the processor that one or more alarm inputs were asserted. */
  USDHC1_IRQn                  = 54,               /**< USDHC1 (Enhanced SDHC) interrupt request. */
  USDHC2_IRQn                  = 55,               /**< USDHC2 (Enhanced SDHC) interrupt request. */
  SAI3_RX_IRQn                 = 56,               /**< SAI3 interrupt ipi_int_sai_rx. */
  SAI3_TX_IRQn                 = 57,               /**< SAI3 interrupt ipi_int_sai_tx. */
  UART1_IRQn                   = 58,               /**< UART1 interrupt request. */
  UART2_IRQn                   = 59,               /**< UART2 interrupt request. */
  UART3_IRQn                   = 60,               /**< UART3 interrupt request. */
  UART4_IRQn                   = 61,               /**< UART4 interrupt request. */
  UART5_IRQn                   = 62,               /**< UART5 interrupt request. */
  eCSPI1_IRQn                  = 63,               /**< eCSPI1 interrupt request. */
  eCSPI2_IRQn                  = 64,               /**< eCSPI2 interrupt request. */
  eCSPI3_IRQn                  = 65,               /**< eCSPI3 interrupt request. */
  eCSPI4_IRQn                  = 66,               /**< eCSPI4 interrupt request. */
  I2C4_IRQn                    = 67,               /**< I2C4 interrupt request. */
  I2C1_IRQn                    = 68,               /**< I2C1 interrupt request. */
  I2C2_IRQn                    = 69,               /**< I2C2 interrupt request. */
  I2C3_IRQn                    = 70,               /**< I2C3 interrupt request. */
  UART7_IRQn                   = 71,               /**< UART-7 ORed interrupt. */
  UART8_IRQn                   = 72,               /**< UART-8 ORed interrupt. */
  Reserved73_IRQn              = 73,               /**< Reserved */
  USB_OTG2_IRQn                = 74,               /**< USBO2 USB OTG2 */
  USB_OTG1_IRQn                = 75,               /**< USBO2 USB OTG1 */
  USB_PHY1_IRQn                = 76,               /**< UTMI0 interrupt request. */
  USB_PHY2_IRQn                = 77,               /**< UTMI1 interrupt request. */
  DCP_IRQ_IRQn                 = 78,               /**< DCP interrupt request dcp_irq. */
  DCP_VMI_IRQ_IRQn             = 79,               /**< DCP interrupt request dcp_vmi_irq. */
  DCP_SEC_IRQ_IRQn             = 80,               /**< DCP interrupt request secure_irq. */
  TEMPMON_IRQn                 = 81,               /**< Temperature Monitor Temperature Sensor (temperature greater than threshold) interrupt request. */
  ASRC_IRQn                    = 82,               /**< ASRC interrupt request. */
  ESAI_IRQn                    = 83,               /**< ESAI interrupt request. */
  SPDIF_IRQn                   = 84,               /**< SPDIF interrupt. */
  Reserved85_IRQn              = 85,               /**< Reserved */
  PMU_IRQ1_IRQn                = 86,               /**< Brown-out event on either the 1.1, 2.5 or 3.0 regulators. */
  GPT1_IRQn                    = 87,               /**< Logical OR of GPT1 rollover interrupt line, input capture 1 and 2 lines, output compare 1, 2, and 3 interrupt lines. */
  EPIT1_IRQn                   = 88,               /**< EPIT1 output compare interrupt. */
  EPIT2_IRQn                   = 89,               /**< EPIT2 output compare interrupt. */
  GPIO1_INT7_IRQn              = 90,               /**< INT7 interrupt request. */
  GPIO1_INT6_IRQn              = 91,               /**< INT6 interrupt request. */
  GPIO1_INT5_IRQn              = 92,               /**< INT5 interrupt request. */
  GPIO1_INT4_IRQn              = 93,               /**< INT4 interrupt request. */
  GPIO1_INT3_IRQn              = 94,               /**< INT3 interrupt request. */
  GPIO1_INT2_IRQn              = 95,               /**< INT2 interrupt request. */
  GPIO1_INT1_IRQn              = 96,               /**< INT1 interrupt request. */
  GPIO1_INT0_IRQn              = 97,               /**< INT0 interrupt request. */
  GPIO1_Combined_0_15_IRQn     = 98,               /**< Combined interrupt indication for GPIO1 signals 0 - 15. */
  GPIO1_Combined_16_31_IRQn    = 99,               /**< Combined interrupt indication for GPIO1 signals 16 - 31. */
  GPIO2_Combined_0_15_IRQn     = 100,              /**< Combined interrupt indication for GPIO2 signals 0 - 15. */
  GPIO2_Combined_16_31_IRQn    = 101,              /**< Combined interrupt indication for GPIO2 signals 16 - 31. */
  GPIO3_Combined_0_15_IRQn     = 102,              /**< Combined interrupt indication for GPIO3 signals 0 - 15. */
  GPIO3_Combined_16_31_IRQn    = 103,              /**< Combined interrupt indication for GPIO3 signals 16 - 31. */
  GPIO4_Combined_0_15_IRQn     = 104,              /**< Combined interrupt indication for GPIO4 signals 0 - 15. */
  GPIO4_Combined_16_31_IRQn    = 105,              /**< Combined interrupt indication for GPIO4 signals 16 - 31. */
  GPIO5_Combined_0_15_IRQn     = 106,              /**< Combined interrupt indication for GPIO5 signals 0 - 15. */
  GPIO5_Combined_16_31_IRQn    = 107,              /**< Combined interrupt indication for GPIO5 signals 16 - 31. */
  Reserved108_IRQn             = 108,              /**< Reserved */
  Reserved109_IRQn             = 109,              /**< Reserved */
  Reserved110_IRQn             = 110,              /**< Reserved */
  Reserved111_IRQn             = 111,              /**< Reserved */
  WDOG1_IRQn                   = 112,              /**< WDOG1 timer reset interrupt request. */
  WDOG2_IRQn                   = 113,              /**< WDOG2 timer reset interrupt request. */
  KPP_IRQn                     = 114,              /**< Key Pad interrupt request. */
  PWM1_IRQn                    = 115,              /**< hasRegInstance(`PWM1`)?`Cumulative interrupt line for PWM1. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.`:`Reserved`) */
  PWM2_IRQn                    = 116,              /**< hasRegInstance(`PWM2`)?`Cumulative interrupt line for PWM2. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.`:`Reserved`) */
  PWM3_IRQn                    = 117,              /**< hasRegInstance(`PWM3`)?`Cumulative interrupt line for PWM3. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.`:`Reserved`) */
  PWM4_IRQn                    = 118,              /**< hasRegInstance(`PWM4`)?`Cumulative interrupt line for PWM4. Logical OR of rollover, compare, and FIFO waterlevel crossing interrupts.`:`Reserved`) */
  CCM_IRQ1_IRQn                = 119,              /**< CCM interrupt request ipi_int_1. */
  CCM_IRQ2_IRQn                = 120,              /**< CCM interrupt request ipi_int_2. */
  GPC_IRQn                     = 121,              /**< GPC interrupt request 1. */
  Reserved122_IRQn             = 122,              /**< Reserved */
  SRC_IRQn                     = 123,              /**< SRC interrupt request src_ipi_int_1. */
  Reserved124_IRQn             = 124,              /**< Reserved */
  Reserved125_IRQn             = 125,              /**< Reserved */
  CPU_PerformanceUnit_IRQn     = 126,              /**< Performance Unit interrupt ~ipi_pmu_irq_b. */
  CPU_CTI_Trigger_IRQn         = 127,              /**< CTI trigger outputs interrupt ~ipi_cti_irq_b. */
  SRC_Combined_IRQn            = 128,              /**< Combined CPU wdog interrupts (4x) out of SRC. */
  SAI1_IRQn                    = 129,              /**< SAI1 interrupt request. */
  SAI2_IRQn                    = 130,              /**< SAI2 interrupt request. */
  Reserved131_IRQn             = 131,              /**< Reserved */
  ADC1_IRQn                    = 132,              /**< ADC1 interrupt request. */
  ADC_5HC_IRQn                 = 133,              /**< ADC_5HC interrupt request. */
  Reserved134_IRQn             = 134,              /**< Reserved */
  Reserved135_IRQn             = 135,              /**< Reserved */
  SJC_IRQn                     = 136,              /**< SJC interrupt from General Purpose register. */
  CAAM_Job_Ring0_IRQn          = 137,              /**< CAAM job ring 0 interrupt ipi_caam_irq0. */
  CAAM_Job_Ring1_IRQn          = 138,              /**< CAAM job ring 1 interrupt ipi_caam_irq1. */
  QSPI_IRQn                    = 139,              /**< QSPI1 interrupt request ipi_int_ored. */
  TZASC_IRQn                   = 140,              /**< TZASC (PL380) interrupt request. */
  GPT2_IRQn                    = 141,              /**< Logical OR of GPT2 rollover interrupt line, input capture 1 and 2 lines, output compare 1, 2 and 3 interrupt lines. */
  CAN1_IRQn                    = 142,              /**< Combined interrupt of ini_int_busoff,ini_int_error,ipi_int_mbor,ipi_int_txwarning and ipi_int_waken */
  CAN2_IRQn                    = 143,              /**< Combined interrupt of ini_int_busoff,ini_int_error,ipi_int_mbor,ipi_int_txwarning and ipi_int_waken */
  Reserved144_IRQn             = 144,              /**< Reserved */
  Reserved145_IRQn             = 145,              /**< Reserved */
  PWM5_IRQn                    = 146,              /**< Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */
  PWM6_IRQn                    = 147,              /**< Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */
  PWM7_IRQn                    = 148,              /**< Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */
  PWM8_IRQn                    = 149,              /**< Cumulative interrupt line. OR of Rollover Interrupt line, Compare Interrupt line and FIFO Waterlevel crossing interrupt line */
  ENET1_IRQn                   = 150,              /**< ENET1 interrupt */
  ENET1_1588_IRQn              = 151,              /**< ENET1 1588 Timer interrupt [synchronous] request. */
  ENET2_IRQn                   = 152,              /**< ENET2 interrupt */
  ENET2_1588_IRQn              = 153,              /**< MAC 0 1588 Timer interrupt [synchronous] request. */
  Reserved154_IRQn             = 154,              /**< Reserved */
  Reserved155_IRQn             = 155,              /**< Reserved */
  Reserved156_IRQn             = 156,              /**< Reserved */
  Reserved157_IRQn             = 157,              /**< Reserved */
  Reserved158_IRQn             = 158,              /**< Reserved */
  PMU_IRQ2_IRQn                = 159               /**< Brown-out event on either core, gpu or soc regulators. */
} IRQn_Type;

4.2.3 GIC 控制器逻辑分块

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

Distributor(分发器端):从4.2.1小节总图可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:
①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组 0 还是组 1。

CPU Interface(CPU 接口端):CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在4.2.1小节总图 中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。CPU 接口端就是分发器和 CPU Core 之间的桥梁,CPU 接口端主要工作如下:
①、使能或者关闭发送到 CPU Core 的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。

在IMX6UL官方配套的SDK包里有 core_ca7.h这个头文件;定义一些内容如下:

/*******************************************************************************
 *                 GIC相关内容
 *有关GIC的内容,参考:ARM Generic Interrupt Controller(ARM GIC控制器)V2.0.pdf
 ******************************************************************************/

/*
 * GIC寄存器描述结构体,
 * GIC分为分发器端和CPU接口端
 */
typedef struct
{
        uint32_t RESERVED0[1024];
  __IOM uint32_t D_CTLR;                 /*!< Offset: 0x1000 (R/W) Distributor Control Register */
  __IM  uint32_t D_TYPER;                /*!< Offset: 0x1004 (R/ )  Interrupt Controller Type Register */
  __IM  uint32_t D_IIDR;                 /*!< Offset: 0x1008 (R/ )  Distributor Implementer Identification Register */
        uint32_t RESERVED1[29];
  __IOM uint32_t D_IGROUPR[16];          /*!< Offset: 0x1080 - 0x0BC (R/W) Interrupt Group Registers */
        uint32_t RESERVED2[16];
  __IOM uint32_t D_ISENABLER[16];        /*!< Offset: 0x1100 - 0x13C (R/W) Interrupt Set-Enable Registers */
        uint32_t RESERVED3[16];
  __IOM uint32_t D_ICENABLER[16];        /*!< Offset: 0x1180 - 0x1BC (R/W) Interrupt Clear-Enable Registers */
        uint32_t RESERVED4[16];
  __IOM uint32_t D_ISPENDR[16];          /*!< Offset: 0x1200 - 0x23C (R/W) Interrupt Set-Pending Registers */
        uint32_t RESERVED5[16];
  __IOM uint32_t D_ICPENDR[16];          /*!< Offset: 0x1280 - 0x2BC (R/W) Interrupt Clear-Pending Registers */
        uint32_t RESERVED6[16];
  __IOM uint32_t D_ISACTIVER[16];        /*!< Offset: 0x1300 - 0x33C (R/W) Interrupt Set-Active Registers */
        uint32_t RESERVED7[16];
  __IOM uint32_t D_ICACTIVER[16];        /*!< Offset: 0x1380 - 0x3BC (R/W) Interrupt Clear-Active Registers */
        uint32_t RESERVED8[16];
  __IOM uint8_t  D_IPRIORITYR[512];      /*!< Offset: 0x1400 - 0x5FC (R/W) Interrupt Priority Registers */
        uint32_t RESERVED9[128];
  __IOM uint8_t  D_ITARGETSR[512];       /*!< Offset: 0x1800 - 0x9FC (R/W) Interrupt Targets Registers */
        uint32_t RESERVED10[128];
  __IOM uint32_t D_ICFGR[32];            /*!< Offset: 0x1C00 - 0xC7C (R/W) Interrupt configuration registers */
        uint32_t RESERVED11[32];
  __IM  uint32_t D_PPISR;                /*!< Offset: 0x1D00 (R/ ) Private Peripheral Interrupt Status Register */
  __IM  uint32_t D_SPISR[15];            /*!< Offset: 0x1D04 - 0xD3C (R/ ) Shared Peripheral Interrupt Status Registers */
        uint32_t RESERVED12[112];
  __OM  uint32_t D_SGIR;                 /*!< Offset: 0x1F00 ( /W) Software Generated Interrupt Register */
        uint32_t RESERVED13[3];
  __IOM uint8_t  D_CPENDSGIR[16];        /*!< Offset: 0x1F10 - 0xF1C (R/W) SGI Clear-Pending Registers */
  __IOM uint8_t  D_SPENDSGIR[16];        /*!< Offset: 0x1F20 - 0xF2C (R/W) SGI Set-Pending Registers */
        uint32_t RESERVED14[40];
  __IM  uint32_t D_PIDR4;                /*!< Offset: 0x1FD0 (R/ ) Peripheral ID4 Register */
  __IM  uint32_t D_PIDR5;                /*!< Offset: 0x1FD4 (R/ ) Peripheral ID5 Register */
  __IM  uint32_t D_PIDR6;                /*!< Offset: 0x1FD8 (R/ ) Peripheral ID6 Register */
  __IM  uint32_t D_PIDR7;                /*!< Offset: 0x1FDC (R/ ) Peripheral ID7 Register */
  __IM  uint32_t D_PIDR0;                /*!< Offset: 0x1FE0 (R/ ) Peripheral ID0 Register */
  __IM  uint32_t D_PIDR1;                /*!< Offset: 0x1FE4 (R/ ) Peripheral ID1 Register */
  __IM  uint32_t D_PIDR2;                /*!< Offset: 0x1FE8 (R/ ) Peripheral ID2 Register */
  __IM  uint32_t D_PIDR3;                /*!< Offset: 0x1FEC (R/ ) Peripheral ID3 Register */
  __IM  uint32_t D_CIDR0;                /*!< Offset: 0x1FF0 (R/ ) Component ID0 Register */
  __IM  uint32_t D_CIDR1;                /*!< Offset: 0x1FF4 (R/ ) Component ID1 Register */
  __IM  uint32_t D_CIDR2;                /*!< Offset: 0x1FF8 (R/ ) Component ID2 Register */
  __IM  uint32_t D_CIDR3;                /*!< Offset: 0x1FFC (R/ ) Component ID3 Register */

  __IOM uint32_t C_CTLR;                 /*!< Offset: 0x2000 (R/W) CPU Interface Control Register */
  __IOM uint32_t C_PMR;                  /*!< Offset: 0x2004 (R/W) Interrupt Priority Mask Register */
  __IOM uint32_t C_BPR;                  /*!< Offset: 0x2008 (R/W) Binary Point Register */
  __IM  uint32_t C_IAR;                  /*!< Offset: 0x200C (R/ ) Interrupt Acknowledge Register */
  __OM  uint32_t C_EOIR;                 /*!< Offset: 0x2010 ( /W) End Of Interrupt Register */
  __IM  uint32_t C_RPR;                  /*!< Offset: 0x2014 (R/ ) Running Priority Register */
  __IM  uint32_t C_HPPIR;                /*!< Offset: 0x2018 (R/ ) Highest Priority Pending Interrupt Register */
  __IOM uint32_t C_ABPR;                 /*!< Offset: 0x201C (R/W) Aliased Binary Point Register */
  __IM  uint32_t C_AIAR;                 /*!< Offset: 0x2020 (R/ ) Aliased Interrupt Acknowledge Register */
  __OM  uint32_t C_AEOIR;                /*!< Offset: 0x2024 ( /W) Aliased End Of Interrupt Register */
  __IM  uint32_t C_AHPPIR;               /*!< Offset: 0x2028 (R/ ) Aliased Highest Priority Pending Interrupt Register */
        uint32_t RESERVED15[41];
  __IOM uint32_t C_APR0;                 /*!< Offset: 0x20D0 (R/W) Active Priority Register */
        uint32_t RESERVED16[3];
  __IOM uint32_t C_NSAPR0;               /*!< Offset: 0x20E0 (R/W) Non-secure Active Priority Register */
        uint32_t RESERVED17[6];
  __IM  uint32_t C_IIDR;                 /*!< Offset: 0x20FC (R/ ) CPU Interface Identification Register */
        uint32_t RESERVED18[960];
  __OM  uint32_t C_DIR;                  /*!< Offset: 0x3000 ( /W) Deactivate Interrupt Register */
} GIC_Type;


/* 
 * GIC初始化
 * 为了简单使用GIC的group0
 */
FORCEDINLINE __STATIC_INLINE void GIC_Init(void)
{
  uint32_t i;
  uint32_t irqRegs;
  GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);

  irqRegs = (gic->D_TYPER & 0x1FUL) + 1;

  /* On POR, all SPI is in group 0, level-sensitive and using 1-N model */

  /* Disable all PPI, SGI and SPI */
  for (i = 0; i < irqRegs; i++)
    gic->D_ICENABLER[i] = 0xFFFFFFFFUL;

  /* Make all interrupts have higher priority */
  gic->C_PMR = (0xFFUL << (8 - __GIC_PRIO_BITS)) & 0xFFUL;

  /* No subpriority, all priority level allows preemption */
  gic->C_BPR = 7 - __GIC_PRIO_BITS;

  /* Enable group0 distribution */
  gic->D_CTLR = 1UL;

  /* Enable group0 signaling */
  gic->C_CTLR = 1UL;
}

/*  
 * 使能指定的中断
 */
FORCEDINLINE __STATIC_INLINE void GIC_EnableIRQ(IRQn_Type IRQn)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	gic->D_ISENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
}

/*  
 * 关闭指定的中断
 */

FORCEDINLINE __STATIC_INLINE void GIC_DisableIRQ(IRQn_Type IRQn)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	gic->D_ICENABLER[((uint32_t)(int32_t)IRQn) >> 5] = (uint32_t)(1UL << (((uint32_t)(int32_t)IRQn) & 0x1FUL));
}

/* 
 * 返回中断号 
 */
FORCEDINLINE __STATIC_INLINE uint32_t GIC_AcknowledgeIRQ(void)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	return gic->C_IAR & 0x1FFFUL;
}

/* 
 * 向EOIR写入发送中断的中断号来释放中断
 */
FORCEDINLINE __STATIC_INLINE void GIC_DeactivateIRQ(uint32_t value)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	gic->C_EOIR = value;
}

/*
 * 获取运行优先级
 */
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetRunningPriority(void)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	return gic->C_RPR & 0xFFUL;
}

/*
 * 设置组优先级
 */
FORCEDINLINE __STATIC_INLINE void GIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	gic->C_BPR = PriorityGroup & 0x7UL;
}

/*
 * 获取组优先级
 */
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriorityGrouping(void)
{
  GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);

  return gic->C_BPR & 0x7UL;
}

/*
 * 设置优先级
 */
FORCEDINLINE __STATIC_INLINE void GIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
  	GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  	gic->D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] = (uint8_t)((priority << (8UL - __GIC_PRIO_BITS)) & (uint32_t)0xFFUL);
}

/*
 * 获取优先级
 */
FORCEDINLINE __STATIC_INLINE uint32_t GIC_GetPriority(IRQn_Type IRQn)
{
  GIC_Type *gic = (GIC_Type *)(__get_CBAR() & 0xFFFF0000UL);
  return(((uint32_t)gic->D_IPRIORITYR[((uint32_t)(int32_t)IRQn)] >> (8UL - __GIC_PRIO_BITS)));
}

第 58 行是 GIC 的 CPU 接口端相关寄存器,其相对于 GIC 基地址的偏移为 0X2000,同样的,获取到 GIC 基地址以后只需要加上 0X2000 即可访问 GIC 的 CPU 接口段寄存器。
那么问题来了?GIC 控制器的寄存器基地址在哪里呢?这个就需要用到 Cortex-A 的 CP15 协处理器了,接着了解一下 CP15 协处理器。

4.2.4 CP15 协处理器

关于 CP15 协处理器和其相关寄存器的详细内容请参考下面两份文档:《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》第 1469 页“B3.17 Oranization of the CP15 registers in a VMSA implementation”。《Cortex-A7 Technical ReferenceManua.pdf》第55 页“Capter 4 System Control”。

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有16 个 32 位寄存器。CP15 协处理器的访问通过如下另个指令完成:

MRC: 将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中。
MCR: 将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中。

MRC 就是读 CP15 寄存器,MCR 就是写 CP15 寄存器,MCR 指令格式如下:

MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>

	cond:指令执行的条件码,如果忽略的话就表示无条件执行。
	opc1:协处理器要执行的操作码。
	Rt:ARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中。
	CRn:CP15 协处理器的目标寄存器。
	CRm:协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将 CRm 设置为 C0,否则结果不可预测。
	opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0。

MRC 的指令格式和 MCR 一样,只不过在 MRC 指令中 Rt 就是目标寄存器,也就是从CP15 指定寄存器读出来的数据会保存在 Rt 中。而 CRn 就是源寄存器,也就是要读取的写处理器寄存器。

假如我们要将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,那么就可以使用如下命令:MRC p15, 0, r0, c0, c0, 0

CP15 协处理器有 16 个 32 位寄存器,c0~c15,本章来看一下 c0、c1、c12 和 c15 这四个寄存器,主要涉及GIC和重要信息四个寄存器,其他的寄存器大家参考《ARM
ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》和《Cortex-A7 Technical ReferenceManua.pdf》”。两个文档即可。

c0 寄存器
CP15 协处理器有 16 个 32 位寄存器,c0~c15,在使用 MRC 或者 MCR 指令访问这 16 个寄存器的时候,指令中的 CRn、opc1、CRm 和 opc2 通过不同的搭配,其得到的寄存器含义是不同的。比如 c0 在不同的搭配情况下含义如下图所示(《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1471页):
<Linux开发>驱动开发 -之-内核定时器与中断
《Cortex-A7 Technical ReferenceManua.pdf》-58页:
<Linux开发>驱动开发 -之-内核定时器与中断
在上图中当 MRC/MCR 指令中的 CRn=c0,opc1=0,CRm=c0,opc2=0 的时候就表示
此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器,这个也是 c0 的基本作用。对于 Cortex-A7内核来说,c0 作为 MDIR 寄存器的时候其含义如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
各bit位所代表的含义如下:
bit31:24:厂商编号,0X41,ARM。
bit23:20:内核架构的主版本号,ARM 内核版本一般使用 rnpn 来表示,比如 r0p1,其中 r0后面的 0 就是内核架构主版本号。
bit19:16:架构代码,0XF,ARMv7 架构。
bit15:4:内核版本号,0XC07,Cortex-A7 MPCore 内核。
bit3:0:内核架构的次版本号,rnpn 中的 pn,比如 r0p1 中 p1 后面的 1 就是次版本号。

c1 寄存器
c1 寄存器同样通过不同的配置,其代表的含义也不同,如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1472页):
<Linux开发>驱动开发 -之-内核定时器与中断
《Cortex-A7 Technical ReferenceManua.pdf》-59页:
<Linux开发>驱动开发 -之-内核定时器与中断
在上图中当 MRC/MCR 指令中的 CRn=c1,opc1=0,CRm=c0,opc2=0 的时候就表示
此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,这个是 c1 的基本作用。SCTLR 寄存器主要是完成控制功能的,比如使能或者禁止 MMU、I/D Cache 等,c1 作为 SCTLR 寄存器的时候其含义如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
SCTLR 的位比较多,我们就只看主要用到的几个位:
bit13:V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可以使用 VBAR 来重映射此基地址,也就是中断向量表重定位。为 1 的话中断向量表基地址为0XFFFF0000,此基地址不能被重映射。
bit12:I,I Cache 使能位,为 0 的话关闭 I Cache,为 1 的话使能 I Cache。
bit11:Z,分支预测使能位,如果开启 MMU 的话,此位也会使能。
bit10:SW,SWP 和 SWPB 使能位,当为 0 的话关闭 SWP 和 SWPB 指令,当为 1 的时候就使能 SWP 和 SWPB 指令。
bit9~3:未使用,保留。
bit2:C,D Cache 和缓存一致性使能位,为 0 的时候禁止 D Cache 和缓存一致性,为 1 时使能。
bit1:A,内存对齐检查使能位,为 0 的时候关闭内存对齐检查,为 1 的时候使能内存对齐检查。
bit0:M,MMU 使能位,为 0 的时候禁止 MMU,为 1 的时候使能 MMU。

如果要读写 SCTLR 的话,就可以使用如下命令:

MRC p15, 0, <Rt>, c1, c0, 0 ;读取 SCTLR 寄存器,数据保存到 Rt 中。
MCR p15, 0, <Rt>, c1, c0, 0 ;将 Rt 中的数据写到 SCTLR(c1)寄存器中。

c12 寄存器
c12 寄存器通过不同的配置,其代表的含义也不同,如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf -1479页):
<Linux开发>驱动开发 -之-内核定时器与中断
《Cortex-A7 Technical ReferenceManua.pdf》-67页:
<Linux开发>驱动开发 -之-内核定时器与中断
在上图中当 MRC/MCR 指令中的 CRn=c12,opc1=0,CRm=c0,opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中,比如在前面的uboot启动kernel中,代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下:

ldr r0, =0X87800000 ; r0=0X87800000
MCR p15, 0, r0, c12, c0, 0 ;将 r0 里面的数据写入到 c12 中,即 c12=0X87800000

c15 寄存器
c15 寄存器也可以通过不同的配置得到不同的含义,如下图所示( 《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf 无描述,《Cortex-A7 Technical ReferenceManua.pdf》-68页:):
<Linux开发>驱动开发 -之-内核定时器与中断
在上图中,我们需要 c15 作为 CBAR 寄存器,因为 GIC 的基地址就保存在 CBAR中,我们可以通过如下命令获取到 GIC 基地址:

MRC p15, 4, r1, c15, c0, 0 ; 获取 GIC 基础地址,基地址保存在 r1 中。

获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了,比如我们可以读取当前中断 ID,当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址相对于 CPU 接口端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下:

MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器GIC_IAR 的值

简单总结一下,通过 c0 寄存器可以获取到处理器内核信息;通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;通过 c12 寄存器可以设置中断向量偏移;通过 c15 寄存器可以获取 GIC 基地址。关于 CP15 的其他寄存器,大家自行查阅本节前面列举的 2 份 ARM 官方资料。

4.3 中断使能

中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020个中断源的使能。
IRQ 和 FIQ 总中断使能
IRQ 和 FIQ 分别是外部中断和快速中断的总开关,就类似家里买的进户总电闸,然后ID0~ID1019 这 1020 个中断源就类似家里面的各个电器开关。要想开电视,那肯定要保证进户总电闸是打开的,因此要想使用 I.MX6U 上的外设中断就必须先打开 IRQ 中断(本教程不使用FIQ)。在程序状态寄存器中,寄存器 CPSR 的 I=1 禁止 IRQ,当 I=0 使能 IRQ;F=1 禁止 FIQ,F=0 使能 FIQ。我们还有更简单的指令来完成 IRQ 或者 FIQ 的使能和禁止,如下表所示:

指令 描述
cpsid i 禁止 IRQ 中断。
cpsie i 使能 IRQ 中断。
cpsid f 禁止 FIQ 中断
cpsie f 使能 FIQ 中断

ID0~ID1019 中断使能和禁止
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 中断的。

4.4 中断优先级设置

优先级数配置
了解STM32或从3.3节内容可知Cortex-M 的中断优先级分为抢占优先级和子优先级,两者是可以配置的。同样的 Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置的。GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高!Cortex-A7 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级,寄存器结构如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
GICC_PMR 寄存器只有低 8 位有效,这 8 位最多可以设置 256 个优先级,其他优先级数设置如下表所示:

bit7:0 优先级数
11111111 256 个优先级
11111110 128 个优先级
11111100 64 个优先级
11111000 32 个优先级
11110000 16 个优先级

I.MX6U 是 Cortex-A7内核,所以支持 32 个优先级,因此 GICC_PMR 要设置为 0b11111000。

抢占优先级和子优先级位数设置
抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的,GICC_BPR 寄存器结构如下图所示:
<Linux开发>驱动开发 -之-内核定时器与中断
寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如下表所示:

Binary Point 抢占优先级域 子优先级域 描述
0 [7:1] [0] 7 级抢占优先级,1 级子优先级
1 [7:2] [1:0] 6 级抢占优先级,2 级子优先级
2 [7:3] [2:0] 5 级抢占优先级,3 级子优先级
3 [7:4] [3:0] 4 级抢占优先级,4 级子优先级
4 [7:5] [4:0] 3 级抢占优先级,5 级子优先级
5 [7:6] [5:0] 2 级抢占优先级,6 级子优先级
6 [7] [6:0] 1 级抢占优先级,7 级子优先级
7 [7:0] 0 级抢占优先级,8 级子优先级

为了简单起见,一般将所有的中断优先级位都配置为抢占优先级,比如 I.MX6U 的优先级位数为 5(32 个优先级),所以可以设置 Binary point 为 2,表示 5 个优先级位全部为抢占优先级。

优先级设置
前面已经设置好了 I.MX6U 一共有 32 个抢占优先级,数字越小优先级越高。具体要使用某个中断的时候就可以设置其优先级为 0~31。某个中断 ID 的中断优先级设置由寄存器D_IPRIORITYR 来完成,前面说了 Cortex-A7 使用了 512 个中断 ID,每个中断 ID 配有一个优先级寄存器,所以一共有 512 个 D_IPRIORITYR 寄存器。如果优先级个数为 32 的话,使用寄存器 D_IPRIORITYR 的 bit7:4 来设置优先级,也就是说实际的优先级要左移 3 位。比如要设置ID40 中断的优先级为 5,示例代码如下:

GICD_IPRIORITYR[40] = 5 << 3;

优先级设置主要有三部分:
①、设置寄存器 GICC_PMR,配置优先级个数,比如 I.MX6U 支持 32 级优先级。
②、设置抢占优先级和子优先级位数,一般为了简单起见,会将所有的位数都设置为抢占优先级。
③、设置指定中断 ID 的优先级,也就是设置外设优先级。

五 Linux kernel 中断

前面第四章分析那么多具体的硬件GIC相关内容,是想读者对gic有个全面的了解。实际开发中断相关驱动并不需要我们太多操作,比如配置寄存器,使能 IRQ 等等,我们所使用的kernel是基于NXP发布的基线开发的,所以最底层的gic寄存器相关配置,根本不用我们去关注。Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。

硬件中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

在 Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的一些内容。

5.1 中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中
断线。在 Linux 内核中使用一个 int 变量表示中断号,关于中断号我们已经在前面4.2.2节讲解过了。

5.2 kernel提供的中断API

5.2.1 request_irq 函数

在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断,request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:
路径:include/linux/interrupt.h

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

函数参数和返回值含义如下:
irq:要申请中断的中断号。
handler:中断处理函数,当中断发生以后就会执行此中断处理函数。
flags:中断标志,可以在文件 include/linux/interrupt.h 里面查看所有的中断标志,这里我们介绍几个常用的中断标志,如下表所示:

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

标志可以通过“|”来实现多种组合.
name:中断名字,设置以后可以在设备的/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。

5.2.2 free_irq函数

使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:

void free_irq(unsigned int irq, 
					void *dev)

函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无。

5.2.3 中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

irqreturn_t (*irq_handler_t) (int, void *)

第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义如下所示:

路径:include/linux/irqreturn.h
#ifndef _LINUX_IRQRETURN_H
#define _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;
#define IRQ_RETVAL(x)	((x) ? IRQ_HANDLED : IRQ_NONE)

#endif

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

return IRQ_RETVAL(IRQ_HANDLED)

5.2.4 中断使能与禁止函数

常用的中断使用和禁止函数如下所示:

路径:include/linux/interrupt.h
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)

enable_irq 和 disable_irq 用于使能和禁止指定的中断,irq 就是要禁止的中断号。disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。

在这种情况下,可以使用另外一个中断禁止函数:

void disable_irq_nosync(unsigned int irq)

disable_irq_nosync 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习 STM32 的时候常说的关闭全局中断,这个时候可以使用如下两个函数:

路径:include/linux/interrupt.h
local_irq_enable()
local_irq_disable()

local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断系统。

多个任务操作local_irq_enable 和local_irq_disable时,存在区间重叠的可能。可能导致任务崩溃。所以需要如下两个函数:

路径:include/linux/irqflags.h
local_irq_save(flags)
local_irq_restore(flags)

这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。local_irq_restore 用于恢复中断,将中断状态保存到 flags 状态。

5.3 中断的上半部和下半部

我们在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:

上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。

下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。

这里提供一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。

上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。

5.3.1 软中断

一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux内核开始 “bottom half”机制 已经被抛弃了。Linux 内核使用结构体 softirq_action 表示软中断, softirq_action结构体定义在文件 include/linux/interrupt.h 中,内容如下:

路径:include/linux/interrupt.h
/* 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 *);
};

在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:

路径:kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
路径: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
};

可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。

softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下:

void open_softirq(int nr, void (*action)(struct softirq_action *))

函数参数和返回值含义如下:
nr:要开启的软中断,在示例代码 51.1.2.3 中选择一个。
action:软中断对应的处理函数。
返回值:没有返回值。

注册好软中断以后需要通过 raise_softirq 函数触发,raise_softirq 函数原型如下:

void raise_softirq(unsigned int nr)

函数参数和返回值含义如下:
nr:要触发的软中断,在示例代码 51.1.2.3 中选择一个。
返回值:没有返回值。

软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断,softirq_init 函数定义在 kernel/softirq.c 文件里面,函数内容如下:

路径:kernel/softirq.c
void __init softirq_init(void)
{
	int cpu;

	for_each_possible_cpu(cpu) {
		per_cpu(tasklet_vec, cpu).tail =
			&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
			&per_cpu(tasklet_hi_vec, cpu).head;
	}

	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

从上诉代码 可以看出,softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和HI_SOFTIRQ。

5.3.2 tasklet

tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet:

路径:include/linux/interrupt.h
struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};

<Linux开发>驱动开发 -之-内核定时器与中断
第 489 行的 func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:

void tasklet_init(struct tasklet_struct *t,
							void (*func)(unsigned long), 
							unsigned long data);

函数参数和返回值含义如下:
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
返回值:没有返回值。

也可以使用宏 DECLARE_TASKLET 来一次性完成 tasklet 的定义和初始化,DECLARE_TASKLET 定义在 include/linux/interrupt.h 文件中,定义如下:

DECLARE_TASKLET(name, func, data)

其中 name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量,func就是 tasklet 的处理函数,data 是传递给 func 函数的参数。

在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行,tasklet_schedule 函数原型如下:

void tasklet_schedule(struct tasklet_struct *t)

函数参数和返回值含义如下:
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值:没有返回值。

关于 tasklet 的参考使用示例如下所示:

/* 定义 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);
	 ......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
	 ......
	 /* 初始化 tasklet */
	tasklet_init(&testtasklet, testtasklet_func, data);
	 /* 注册中断处理函数 */
	 request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
	 ......
}

5.3.3 工作队列

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

路径:include/linux/workqueue.h
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;				/* 工作队列处理函数 */
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

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

路径:kernel/workqueue.c
/*
 * The externally visible workqueue.  It relays the issued work items to
 * the appropriate worker_pool through its pool_workqueues.
 */
struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */
	struct list_head	list;		/* PR: list of all workqueues */

	struct mutex		mutex;		/* protects this wq */
	int			work_color;	/* WQ: current work color */
	int			flush_color;	/* WQ: current flush color */
	atomic_t		nr_pwqs_to_flush; /* flush in progress */
	struct wq_flusher	*first_flusher;	/* WQ: first flusher */
	struct list_head	flusher_queue;	/* WQ: flush waiters */
	struct list_head	flusher_overflow; /* WQ: flush overflow list */

	struct list_head	maydays;	/* MD: pwqs requesting rescue */
	struct worker		*rescuer;	/* I: rescue worker */

	int			nr_drainers;	/* WQ: drain in progress */
	int			saved_max_active; /* WQ: saved pwq max_active */

	struct workqueue_attrs	*unbound_attrs;	/* WQ: only for unbound wqs */
	struct pool_workqueue	*dfl_pwq;	/* WQ: only for unbound wqs */

#ifdef CONFIG_SYSFS
	struct wq_device	*wq_dev;	/* I: for sysfs interface */
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map	lockdep_map;
#endif
	char			name[WQ_NAME_LEN]; /* I: workqueue name */

	/*
	 * Destruction of workqueue_struct is sched-RCU protected to allow
	 * walking the workqueues list without grabbing wq_pool_mutex.
	 * This is used to dump all workqueues from sysrq.
	 */
	struct rcu_head		rcu;

	/* hot fields used during command issue, aligned to cacheline */
	unsigned int		flags ____cacheline_aligned; /* WQ: WQ_* flags */
	struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */
	struct pool_workqueue __rcu *numa_pwq_tbl[]; /* FR: unbound pwqs indexed by node */
};

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

路径:kernel/workqueue_internal.h
/*
 * The poor guys doing the actual heavy lifting.  All on-duty workers are
 * either serving the manager role, on idle list or on busy hash.  For
 * details on the locking annotation (L, I, X...), refer to workqueue.c.
 *
 * Only to be used in workqueue and async.
 */
struct worker {
	/* on idle list while idle, on busy hash table while busy */
	union {
		struct list_head	entry;	/* L: while idle */
		struct hlist_node	hentry;	/* L: while busy */
	};

	struct work_struct	*current_work;	/* L: work being processed */
	work_func_t		current_func;	/* L: current_work's fn */
	struct pool_workqueue	*current_pwq; /* L: current_work's pwq */
	bool			desc_valid;	/* ->desc is valid */
	struct list_head	scheduled;	/* L: scheduled works */

	/* 64 bytes boundary on 64bit, 32 on 32bit */

	struct task_struct	*task;		/* I: worker task */
	struct worker_pool	*pool;		/* I: the associated pool */
						/* L: for rescuers */
	struct list_head	node;		/* A: anchored at pool->workers */
						/* A: runs through worker->node */

	unsigned long		last_active;	/* L: last active timestamp */
	unsigned int		flags;		/* X: flags */
	int			id;		/* I: worker id */

	/*
	 * Opaque string set with work_set_desc().  Printed out with task
	 * dump for debugging - WARN, BUG, panic or sysrq.
	 */
	char			desc[WORKER_DESC_LEN];

	/* used only by rescuers to point to the target workqueue */
	struct workqueue_struct	*rescue_wq;	/* I: the workqueue to rescue */
};

从worker结构体最后一个成员可以看出,每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:

#define INIT_WORK(_work, _func)

_work 表示要初始化的工作,_func 是工作对应的处理函数。

也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化,宏定义如下:

#define DECLARE_WORK(n, f)

n 表示定义的工作(work_struct),f 表示工作对应的处理函数。

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

bool schedule_work(struct work_struct *work)

函数参数和返回值含义如下:
work:要调度的工作。
返回值:0 成功,其他值 失败。

关于工作队列的参考使用示例如下所示:

/* 定义工作(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);
	 ......
}

5.4 设备树中断信息节点

目前基本都是使用设备树,所以就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。对于中断 控制器而言 ,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件,其中的 intc 节点就是I.MX6ULL 的中断控制器节点,节点内容如下所示:

路径:arch/arm/boot/dts/imx6ull.dtsi

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

第 4 行,compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7-gic”即可找到 GIC 中断控制器驱动文件(drivers/irqchip/irq-gic.c)。

第 5 行,#interrupt-cells 和#address-cells、#size-cells 一样。表示此中断控制器下设备的 cells大小,对于设备而言,会使用 interrupts 属性描述中断信息,#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 掩码。

第 6 行,interrupt-controller 节点为空,表示当前节点是中断控制器。

对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示:

路径:arch/arm/boot/dts/imx6ull.dtsi
		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;
				#interrupt-cells = <2>;
			};

第 5 行,interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI,触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1,有如下图所示的内容:
<Linux开发>驱动开发 -之-内核定时器与中断
从上图可以看出,GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应 GPIO5_IO00-GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。

第 9 行,interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO的中断。

第 10 行,将#interrupt-cells 修改为 2。

飞思卡尔使用例子:
打开arch/arm/boot/dts/imx6ull-water-emmc.dts文件,找到如下所示内容:

fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
		position = <0>;
		interrupt-parent = <&gpio5>;
		interrupts = <0 8>;
	};

fxls8471 是 NXP 官方的 6ULL 开发板上的一个磁力计芯片,fxls8471 有一个中断引脚链接到了 I.MX6ULL 的 SNVS_TAMPER0 因脚上,这个引脚可以复用为GPIO5_IO00。

第 5 行,interrupt-parent 属性设置中断控制器,这里使用 gpio5 作为中断控制器。
第 6 行,interrupts 设置中断信息,0 表示 GPIO5_IO00,8 表示低电平触发。

简单总结一下与中断有关的设备树属性信息:
①、#interrupt-cells,指定中断源的信息 cells 个数。
②、interrupt-controller,表示当前节点为中断控制器。
③、interrupts,指定中断号,触发方式等。
④、interrupt-parent,指定父中断,也就是中断控制器。

5.5 获取中断号

在编写中断有关的驱动时,我们需要在驱动里获取中断号,前面第5.4小节,w欧美讲了,中断号和中断相关信息已经写入设备树中了;那么我们在写驱动时,需要用到,则可以去设备树中获取中断号和中断相关信息。
kernel中提供了函数 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:

路径:drivers\of\irq.c
unsigned int irq_of_parse_and_map(struct device_node *dev,
 int index)

函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如
下:

路径 :include\linux\gpio.h
			Documentation\gpio\gpio-legacy.txt
int gpio_to_irq(unsigned int gpio)

函数参数和返回值含义如下:
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。

上面将了那么多,那么该如何使用中断呢?

六 Linux kernel 中断 实验

前面我们只分析了内核定时器,当然还有系统硬件定时器(但是我们暂时没用到);还分析了Cortex-a7架构下的中断控制器GIC,及其工作流程。如果不是基于Linux系统开发,想要参考STM32单片机一样,当裸机开发的话,我们需要配置的东西很多。如:
硬件中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

如果使用linux开发中断相关功能代码就方便很多了,大体上就是申请中断注册一下中断函数即可。其实并不是说我们完全不需要按照逻辑的流程来处理中断;首先我们使用的是NXP官方开发板配套的kernel代码,里面的相关板级代原厂都移植好了。NXP半导厂,会借助Linux kernel的框架代码,将硬件相关的配置操作隐藏起来,一般驱动开发人员无需关注,这类工作内容是系统工程师做bring up时的工作内容。

第四节的内容主要是想让读者对中断的流程有一个全面的了解。

接下来我们就以一个按键来编写一个中断,验证一下对应的功能。

6.1 新增key设备树节点

笔者所用的开发板上有一个key,该按键链接 I.MX6U 的 UART1_CTS 这个 IO。
<Linux开发>驱动开发 -之-内核定时器与中断

结合前面我们学习过的pinctrl子系统首先新增pinctrl节点,添加内容如下:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
pinctrl_key: keygrp {
			fsl,pins = <
				MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 	/* KEY0 */
			>;
		};

<Linux开发>驱动开发 -之-内核定时器与中断

继续在dts文件,新增key设备树节点,内容如下:

路径:arch\arm\boot\dts\imx6ull-water-emmc.dts
key {	
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "water-key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
		status = "okay";
	};

<Linux开发>驱动开发 -之-内核定时器与中断
上述两个设备树节点新增完毕后,可通过“make dts” 或全编 kernel,然后用新的DTB文件启动,在设备中可看到新增的设备树节点信息,如下:

sudo cp arch/arm/boot/dts/imx6ull-water-emmc.dtb  ../../../tftp/

<Linux开发>驱动开发 -之-内核定时器与中断

6.2 编写key驱动代码-手动挂载方式

编译单独的ko文件,然后动态挂载设备。

6.2.1 新建key目录及c文件

新建key目录,并以vscode工程打开,然后新建key.c文件如下:
<Linux开发>驱动开发 -之-内核定时器与中断

6.2.2 新建key目录

配置kernel头文件路径,同时按下ctrl+shitf+P,选择“c/c++:编辑配置(JSON)”,添加linux的头文件,注意是绝对路径,内容如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated/"

            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

<Linux开发>驱动开发 -之-内核定时器与中断

6.2.3 编写驱动

在key.c中输入以下内容:

/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名 : key.c
作者 : water
版本 : V1.0
描述 : Linux 中断驱动实验
其他 : 无
日志 : 初版 V1.0 2023/06/12 water创建
***************************************************************/
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define	KEYIRQ_CNT	1						/*	设备号个数	*/
#define	KEYIRQ_NAME	"KEYIRQ"				/*设备名*/
#define	KEY0VALUE		0X01				/*	key0按键值	*/
#define	INVAKEY				0XFF			/*	无效按键值	*/
#define	KEY_NUM			1					/*	按键数量	*/

/*	IO中断描述结构体	*/
struct irq_keydesc	{
	int gpio;						/* 使用的GPIO */
	int irqnum;						/* GPIO中断号 */
	unsigned char value;			/* 按键键值 */
	char name[10];					/* 设备名 */
	irqreturn_t (*handler)(int, void *);	/* 中断处理函数 */
};

/* keyirq设备结构体 */
struct keyirq_dev{
	dev_t devid;				 /* 设备号 */
	struct cdev cdev; 			/* cdev */
	struct class *class; 		/* 类 */
	struct device *device; 		/* 设备 */
	int major;					 /* 主设备号 */
	int minor; 					/* 次设备号 */
	struct device_node *nd; 	/* 设备节点 */
	atomic_t keyvalue;		 /* 有效的按键键值 */
	atomic_t releasekey; 		/* 标记是否完成一次完成的按键*/
	struct timer_list timer;		 /* 定义一个定时器*/
	struct irq_keydesc irqkeydesc[KEY_NUM]; 	/* 按键描述数组 */
	unsigned char curkeynum;		 /* 当前的按键号 */
};

struct keyirq_dev  keyirq; /* irq 设备 */

/* @description		: 中断服务函数,开启定时器,延时10ms,
 *				  	  定时器用于按键消抖。
 * @param - irq 	: 中断号 
 * @param - dev_id	: 设备结构。
 * @return 			: 中断执行结果
 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
	struct keyirq_dev *dev = (struct keyirq_dev *)dev_id;

	dev->curkeynum = 0;
	dev->timer.data = (volatile long)dev_id;
	mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10));	/* 10ms定时 */
	return IRQ_RETVAL(IRQ_HANDLED);
}

/* @description	: 定时器服务函数,用于按键消抖,定时器到了以后
 *				  再次读取按键值,如果按键还是处于按下状态就表示按键有效。
 * @param - arg	: 设备结构变量
 * @return 		: 无
 */
void timer_function(unsigned long arg)
{
	unsigned char value;
	unsigned char num;
	struct irq_keydesc *keydesc;
	struct keyirq_dev *dev = (struct keyirq_dev *)arg;

	num = dev->curkeynum;
	keydesc = &dev->irqkeydesc[num];

	value = gpio_get_value(keydesc->gpio); 	/* 读取IO值 */
	if(value == 0){ 						/* 按下按键 */
		atomic_set(&dev->keyvalue, keydesc->value);
	}
	else{ 									/* 按键松开 */
		atomic_set(&dev->keyvalue, 0x80 | keydesc->value);
		atomic_set(&dev->releasekey, 1);	/* 标记松开按键,即完成一次完整的按键过程 */			
	}	
}

/*
 * @description	: 按键IO初始化
 * @param 		: 无
 * @return 		: 无
 */
static int keyio_init(void)
{
	unsigned char i = 0;
	int ret = 0;
	
	keyirq.nd = of_find_node_by_path("/key");
	if (keyirq.nd== NULL){
		printk("key node not find!\r\n");
		return -EINVAL;
	} 

	/* 提取GPIO */
	for (i = 0; i < KEY_NUM; i++) {
		keyirq.irqkeydesc[i].gpio = of_get_named_gpio(keyirq.nd ,"key-gpio", i);
		if (keyirq.irqkeydesc[i].gpio < 0) {
			printk("can't get key%d\r\n", i);
		}
	}
	
	/* 初始化key所使用的IO,并且设置成中断模式 */
	for (i = 0; i < KEY_NUM; i++) {
		memset(keyirq.irqkeydesc[i].name, 0, sizeof(keyirq.irqkeydesc[i].name));	/* 缓冲区清零 */
		sprintf(keyirq.irqkeydesc[i].name, "KEY%d", i);		/* 组合名字 */
		gpio_request(keyirq.irqkeydesc[i].gpio, keyirq.irqkeydesc[i].name);
		gpio_direction_input(keyirq.irqkeydesc[i].gpio);	
		keyirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(keyirq.nd, i);
#if 0
		keyirq.irqkeydesc[i].irqnum = gpio_to_irq(keyirq.irqkeydesc[i].gpio);
#endif
		printk("key%d:gpio=%d, irqnum=%d\r\n",i, keyirq.irqkeydesc[i].gpio, 
                                         keyirq.irqkeydesc[i].irqnum);
	}
	/* 申请中断 */
	keyirq.irqkeydesc[0].handler = key0_handler;
	keyirq.irqkeydesc[0].value = KEY0VALUE;
	
	for (i = 0; i < KEY_NUM; i++) {
		ret = request_irq(keyirq.irqkeydesc[i].irqnum, keyirq.irqkeydesc[i].handler, 
		                 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, keyirq.irqkeydesc[i].name, &keyirq);
		if(ret < 0){
			printk("irq %d request failed!\r\n", keyirq.irqkeydesc[i].irqnum);
			return -EFAULT;
		}
	}

	/* 创建定时器 */
	init_timer(&keyirq.timer);
	keyirq.timer.function = timer_function;
	return 0;
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int keyirq_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &keyirq;	/* 设置私有数据 */
	return 0;
}

 /*
  * @description     : 从设备读取数据 
  * @param - filp    : 要打开的设备文件(文件描述符)
  * @param - buf     : 返回给用户空间的数据缓冲区
  * @param - cnt     : 要读取的数据长度
  * @param - offt    : 相对于文件首地址的偏移
  * @return          : 读取的字节数,如果为负值,表示读取失败
  */
static ssize_t keyirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int ret = 0;
	unsigned char keyvalue = 0;
	unsigned char releasekey = 0;
	struct keyirq_dev *dev = (struct keyirq_dev *)filp->private_data;

	keyvalue = atomic_read(&dev->keyvalue);
	releasekey = atomic_read(&dev->releasekey);

	if (releasekey) { /* 有按键按下 */	
		if (keyvalue & 0x80) {
			keyvalue &= ~0x80;
			ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
		} else {
			goto data_error;
		}
		atomic_set(&dev->releasekey, 0);/* 按下标志清零 */
	} else {
		goto data_error;
	}
	return 0;
	
data_error:
	return -EINVAL;
}

/* 设备操作函数 */
static struct file_operations keyirq_fops = {
	.owner = THIS_MODULE,
	.open = keyirq_open,
	.read = keyirq_read,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init keyirq_init(void)
{
	/* 1、构建设备号 */
	if (keyirq.major) {
		keyirq.devid = MKDEV(keyirq.major, 0);
		register_chrdev_region(keyirq.devid, KEYIRQ_CNT, KEYIRQ_NAME);
	} else {
		alloc_chrdev_region(&keyirq.devid, 0, KEYIRQ_CNT, KEYIRQ_NAME);
		keyirq.major = MAJOR(keyirq.devid);
		keyirq.minor = MINOR(keyirq.devid);
	}

	/* 2、注册字符设备 */
	cdev_init(&keyirq.cdev, &keyirq_fops);
	cdev_add(&keyirq.cdev, keyirq.devid, KEYIRQ_CNT);

	/* 3、创建类 */
	keyirq.class = class_create(THIS_MODULE, KEYIRQ_NAME);
	if (IS_ERR(keyirq.class)) {
		return PTR_ERR(keyirq.class);
	}

	/* 4、创建设备 */
	keyirq.device = device_create(keyirq.class, NULL, keyirq.devid, NULL, KEYIRQ_NAME);
	if (IS_ERR(keyirq.device)) {
		return PTR_ERR(keyirq.device);
	}
	
	/* 5、初始化按键 */
	atomic_set(&keyirq.keyvalue, INVAKEY);
	atomic_set(&keyirq.releasekey, 0);
	keyio_init();
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit keyirq_exit(void)
{
	unsigned int i = 0;
	/* 删除定时器 */
	del_timer_sync(&keyirq.timer);	/* 删除定时器 */
		
	/* 释放中断 */
	for (i = 0; i < KEY_NUM; i++) {
		free_irq(keyirq.irqkeydesc[i].irqnum, &keyirq);
		gpio_free(keyirq.irqkeydesc[i].gpio);
	}
	cdev_del(&keyirq.cdev);
	unregister_chrdev_region(keyirq.devid, KEYIRQ_CNT);
	device_destroy(keyirq.class, keyirq.devid);
	class_destroy(keyirq.class);
}

module_init(keyirq_init);
module_exit(keyirq_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("water");

6.2.4 编写Makefile

在key.c同级目录下新建Makefile,并输入以下内容:

ARCH=arm 
CROSS_COMPILE=arm-linux-gnueabihf-

KERNELDIR := /home/water/imax/NXP/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/
CURRENT_PATH := $(shell pwd)
obj-m := key.o

KBUILD_CFLAGS += -fno-pie

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

6.2.5 编译验证

在Makefile同级目录下执行make命令,完成编译,如下:
<Linux开发>驱动开发 -之-内核定时器与中断
将编译生成的key.ko文件放到设备的“/lib/modules/4.1.15+”目录下,如下:
<Linux开发>驱动开发 -之-内核定时器与中断

命令行进入到“/lib/modules/4.1.15+”目录下,然后执行以下命令进行挂在驱动:

depmod //第一次加载驱动的时候需要运行此命令
modprobe key.ko //加载驱动

<Linux开发>驱动开发 -之-内核定时器与中断

驱动加载成功以后可以通过查看/proc/interrupts 文件来检查一下对应的中断有没有被注册上,输入如下命令:

cat /proc/interrupts

如下图:
<Linux开发>驱动开发 -之-内核定时器与中断
其中47表示中断号,18表示GPIO序号,与驱动挂载时输出的信息一致。

后续编写测试APP验证,可直接跳到6.4节进行APP验证。

6.3 编写key驱动代码-并入kernel方式

当一个硬件设计固定后,为了方便使用外设,一般都是直接编译进内核,这样就不需要每次手动挂载了,接下来就开看下如下编写编进内核的key驱动。

6.3.1 新建 驱动文件

首先我们要了解一下,键盘也是属于按键,键盘在linux中作为input设备使用,在kernel有单独的目录管理,所以我们把key也当成input设备放到其对应的目录下。对于键盘功能使用后续文章再进行讲解。

找到"drivers\input\keyboard"目录,并新建“gpio_waterkeys.c”,并输入6.2.3小节的代码内容。

6.3.2 增加编译条件

找到同级目录下的Makefile文件“drivers\input\keyboard\Makefile”,再末尾添加:

obj-$(CONFIG_KEYBOARD_WATERKEY)		+= gpio_waterkeys.o

<Linux开发>驱动开发 -之-内核定时器与中断

6.3.3 增加配置条件

主要时使用“make menuconfig”时可以配置是否使用这个驱动模块。
找到同级目录下的Kconfig文件“drivers\input\keyboard\Kconfig”,再末尾添加:

config KEYBOARD_WATERKEY
	tristate "WATER KEY GPIO Buttons"
	depends on GPIOLIB
	help
	  This driver implements support for buttons connected
	  to GPIO pins of various CPUs (and some other chips).

	  Say Y here if your device has buttons connected
	  directly to such GPIO pins.  Your board-specific
	  setup logic must also provide a platform device,
	  with configuration data saying which GPIOs are used.

	  To compile this driver as a module, choose M here: the
	  module will be called gpio_keys.

<Linux开发>驱动开发 -之-内核定时器与中断

6.3.4 编译

上述内容添加完成后,我们就可以编译了,
首先运行“make menuconfig”,进行配置使用我们添加的key模块,如下:
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断
然后保存
<Linux开发>驱动开发 -之-内核定时器与中断

然后再进行整体kernel编译;
命令:make

上述驱动准备完成后,我们就来编写key测试app验证。

6.4 app验证

6.4.1 编写app代码

编写key测试app,新建文件key_app.c,并输入一下内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"
/***************************************************************
Copyright © OneFu Co., Ltd. 2018-2023. All rights reserved.
文件名		: key_app.c
作者	  	: water
版本	   	: V1.0
描述	   	: 中断测试应用程序
其他	   	: 无
使用方法	:./key_app /dev/keyirq 打开测试App
日志	   	: 初版V1.0 2023/06/12 water创建
***************************************************************/

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int keyfd;
	int ret = 0;
	char *filename;
	unsigned char data;

	int ledstatu = 0;
	char ledfilename[]="/dev/gpioled";
	int ledfd,retvalue;
	unsigned char databuf[1];       //定义的buf,用来读写数据用

	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	keyfd = open(filename, O_RDWR);
	if (keyfd < 0) {
		printf("Can't open file %s\r\n", filename);
		return -1;
	}
	ledfd = open(ledfilename,O_RDWR);                         /*打开驱动文件*/
    if(ledfd < 0){
        printf("Can't open file %s\r\n",ledfilename);      /*打开失败,输出提示*/
        return -1;
    }

	while (1) {
		ret = read(keyfd, &data, sizeof(data));
		if (ret < 0) {  /* 数据读取错误或者无效 */
			
		} else {		/* 数据读取正确 */
			if (data)	/* 读取到数据 */
			{
				printf("key value = %#X\r\n", data);
				ledstatu = !ledstatu;
				databuf[0] = ledstatu;
				retvalue = write(ledfd, databuf, sizeof(databuf));     /*向设备驱动写入数据*/
				if(retvalue < 0){
					printf("LED Control Failed!\r\n",ledfilename);     /*写入错误输出提示*/
				}
			}
				
		}
	}
	retvalue = close(keyfd);
	if(retvalue < 0){
        printf("Can't close file %s\r\n",filename);     /*关闭错误输出提示*/
        ret = -1;
		goto OUT1;
    }
OUT1:
	retvalue = close(ledfd);
	if(retvalue < 0){
        printf("Can't close file %s\r\n",ledfilename);     /*关闭错误输出提示*/
        ret = -1;
		goto OUT2;
    }
OUT2:
	return ret;
}

//编译:arm-linux-gnueabihf-gcc key_app.c -o key_app

6.4.2 编译验证

编译key_app.c,使用以下命令:

arm-linux-gnueabihf-gcc key_app.c -o key_app

将编译生成的key_app,放到设备中,并运行,命令如下:

cp key_app ../../../nfs/buildrootfs/root/water_soft/key/

<Linux开发>驱动开发 -之-内核定时器与中断
运行命令:

 ./key_app  /dev/KEYIRQ

<Linux开发>驱动开发 -之-内核定时器与中断
<Linux开发>驱动开发 -之-内核定时器与中断

根据按键按下控制LED灯的开关状态。
实际现象与设计现象相符。

不管是动态挂载还是编译进内核都会有 “/dev/KEYIRQ”这个设备;那么app就可以通过操作这个设备,控制获取key的输入了。

七 总结

主要体现掌握以下几点:
1、熟悉内核定时器的使用方法步骤;
2、了解cortex-a7的中断流程原理;
3、熟悉掌握linux kernel中断驱动的编写方法步骤。

内核定时器和中断是开发功能的常用技术手段,只有掌握更多的技术才能更好的为我们的开发服务。

额外的,使用 ./key_app /dev/KEYIRQ & 然app运行在后台,然后通过top命令看一下cpu占用,结果如下:
<Linux开发>驱动开发 -之-内核定时器与中断
可看到cpu占用很高,这是因为app是一直循环read设备的,这样驱动就会一直去获取按键值 并返回给app;如此反复所以cpu占用率就特别高,这种情况是不能满足实际使用的。
下一篇文章将讲解如何处理这样现象。

由于文章参考文档资源上传异常,文章所参考文档可联系作者获取,QQ759521350文章来源地址https://www.toymoban.com/news/detail-481859.html

到了这里,关于<Linux开发>驱动开发 -之-内核定时器与中断的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux 内核定时器

    一、相关知识点 (一)知识点 1、内核定时器分类     1)标准定时器或系统定时器     2)高精度定时器(HRT)         头文件:#include linux/hrtimer.h 2、检查系统是否可用HRT     1)查看内核配置文件              2)查看机器         cat proc/timer_list  | grep \\\"resolution\\\" 

    2024年02月11日
    浏览(29)
  • 09_Linux内核定时器

    目录 Linux时间管理和内核定时器简介 内核定时器简介 Linux内核短延时函数 定时器驱动程序编写 编写测试APP 运行测试          学习过UCOS或FreeRTOS的同学应该知道, UCOS或FreeRTOS是需要一个硬件定时器提供系统时钟, 一般使用Systick作为系统时钟源。同理 , Linux要运行 , 也是需

    2024年02月13日
    浏览(29)
  • Linux内核 -高精度定时器

    高精度定时器使用示例

    2024年01月19日
    浏览(36)
  • Linux 内核定时器(高级字符设备五)

      在 Linux 内核中很多函数是基于定时器进行驱动的,但是内核定时器的精度并不高,所以不能作为高精度定时器使用。并且内核定时器的运行没有周期性,到达计时终点后会自动关闭。如果要实现周期性定时,就要在定时处理函数中重新开启定时器。   Linux 内核中使用

    2024年02月08日
    浏览(28)
  • 基于STM32CUBEMX驱动低压步进器电机驱动器STSPIN220(3)----定时器中断产生指定数量脉冲

    在步进电机控制过程中,为了实现精确的位置和速度控制,经常需要输出指定数量的脉冲。这就需要使用定时器功能来生成PWM脉冲信号。本文将详细介绍如何利用STM32CUBEMX配置定时器以输出指定数量的PWM脉冲。 定时器是STM32微控制器的一个重要功能模块,可用于生成各种定时

    2024年02月14日
    浏览(35)
  • STM32标准库开发——TIM定时器中断

    定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断 16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时 根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型 不仅具备基本的定时中断功能

    2024年01月18日
    浏览(32)
  • 【STM32笔记】STM32的定时器开发基础(二)(基于STM32CubeMX实现定时器中断)

      传统STM32外部中断 的设计步骤:  (1)将GPIO初始化为输入端口。  (2)配置相关I/O引脚与中断线的映射关系。  (3)设置该I/O引脚对印的中断触发条件。  (4)配置NVIC,并使能中断。  (5)编写中断服务函数。   基于STM32CubeMX的外部中断 设计步骤  (1)在STM3

    2024年02月20日
    浏览(40)
  • 【Linux 裸机篇(八)】I.MX6U EPIT 定时器中断、定时器按键消抖

    EPIT 的全称是: Enhanced Periodic Interrupt Timer,直译过来就是增强的周期中断定时器,它主要是完成周期性中断定时的。学过 STM32 的话应该知道, STM32 里面的定时器还有很多其它的功能,比如输入捕获、 PWM 输出等等。但是 I.MX6U 的 EPIT 定时器只是完成周期性中断定时的,仅此一

    2024年02月02日
    浏览(50)
  • 学习笔记|定时器|STC中断|定时器时间计算|STC32G单片机视频开发教程(冲哥)|第十一集:定时器的作用和意义

    什么是定时器:定时器-与非网 上节课的一段代码: TimeCount++然后一个延时1毫秒,每运行1ms,变量就会加一。 系统已经运行了多少个毫秒。 实际使用时的代码如下, 判断按键有沿有按下的时候,我们等待按键松开,还有一个while循环。 如果没有松开,会一直死在这一行。所以,

    2024年02月09日
    浏览(45)
  • STM32控制步进电机:基于HAL库定时器中断的闭环步进电机驱动+精准控制脉冲数

    该篇文章中用到的步进电机闭环驱动器为Emm42_V4.0步进电机闭环驱动器。该闭环驱动器自带FOC矢量闭环控制算法,能实现力矩、速度、位置三环控制。 如下图所示,该42步进闭环电机驱动器的A+、A-、B+、B-连接步进电机,通过右侧的使能、脉冲、方向端对步进电机进行驱动控制

    2024年02月01日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包