参考:《Linux驱动开发入门与实战》
《正点原子:I.MX6U 嵌入式 Linux 驱动开发指南》
1. 中断的概念
中断是指在CPU正常运行期间,由外部事件或由程序预先安排的事件引起的CPU暂停正在运行的程序,转而为该外部事件或预先安排的事件服务,服务完毕后再返回去继续运行被暂时中断的程序。
2. 中断的分类
2.1 中断的宏观分类
中断从宏观上可以分为两大类,即硬中断和软中断。
硬中断
硬中断就是由系统硬件产生的中断。硬中断通常由外部事件引起,外部事件具有随机性和突发性,因此硬中断也具有随机性和突发性。另外,硬中断是可以屏蔽的。
软中断
软中断是执行中断指令时产生的。处理器接收软中断有两个来源,一个是处理器执行到错误的指令代码,如除零操作。另一个是由软件产生的中断,如进程的调度就是使用的软中断方式。
2.2 中断按照产生的位置分类
从中断产生的位置,可以将中断分为外部中断和内部中断。
外部中断
外部中断一般是外设发出的中断请求,键盘中断,定时器中断等。外部中断可以通过编程方式予以屏蔽。
内部中断
内部中断是指硬件出错(如突然掉电,奇偶校验出错等)或运算出错(除数为零、运算溢出、单步中断等)所引起的中断。内部中断是不可屏蔽的中断。通常情况下,大多数内部中断都由Linux内核处理,程序员往往不需要关心这些问题。
2.3 异步中断和同步中断
从指令执行的角度,中断又可以分为同步中断和异步中断。
同步中断
同步中断由CPU产生,指令执行的过程由CPU控制。在指令执行的过程中,即使有中断到来,只要指令还没执行完,CPU就不会去执行该中断。同步中断一般是因为程序错误引起的,例如内存管理中的缺页中断,除零出错等。当CPU处理同步中断时,会调用异常处理函数,使系统从错误的状态恢复过来。当错误不可恢复时,设备会出现挂死现象。
异步中断
异步中断是由硬件设备随机产生的,产生中断时并不考虑与处理器的时钟同步问题。例如在网卡驱动程序中,当网卡接收到数据包后,会向CPU发送一个异步中断事件,表示数据到来,CPU并不知道何时接收该事件。异步中断的中断处理函数与内核的执行顺序是异步执行的,两者没有必然的联系,也不会相互影响。
2.4 中断小结
硬件中断可以是外部中断也可以是异步中断,同时,软件中断可以是内部中断也可以是同步中断。如下图所示:
3. 中断的实现
中断的实现过程是一个比较复杂的过程,其中涉及中断信号线、中断控制器等概念。
3.1 中断信号线(Interrupt Request,IRQ)
中断信号线是对中断输入线和中断输出线的统称。中断输入线是指接收中断信号的引脚,中断输出线是指发送中断信号的引脚。每一个能够产生中断的外设都有一条或多条中断输出线,用来通知处理器产生中断。相应的,处理器也有一组中断输入线,用来接收连接到它的外部设备发出的中断信号。
如图所示,外设1、2、3通过自己的中断输出线连接到ARM处理器上不同的中断输入线上。每一条IRQ都是有编号的,一般从0开始编号,编号也可以叫中断号。在写设备驱动程序的过程中,中断号往往需要驱动开发人员来指定。这时,可以查看硬件开发板的原理图,找到设备与ARM处理器的连接关系,如果接到0号中断线,那么中断号就是0。
3.2 中断控制器
中断控制器位于ARM处理器核心和中断源之间,外部中断源将中断发到中断控制器。中断控制器进行优先级进行判断,然后通过引脚将中断请求发送给ARM处理器核心。中断控制器如图所示:
当外部设备同时产生中断时,中断优先级产生逻辑会判断哪一个中断将被执行。如上图中的中断屏蔽寄存器,当屏蔽位为1时,表示对应的中断被禁止;当屏蔽位为0时,表示对应的中断可以正常执行。不同的处理器屏蔽位0/1的意义可能有所不同。
3.3 中断的处理过程
中断的处理过程如下图所示:
1)外设产生一个中断信号,该中断通过中断线以电信号的方式发送给中断控制器。
2)中断控制器一直检查IRQ线,检查是否有信号产生。如果有一条或多条IRQ线产生信号,那么中断控制器就优先处理中断编号较小的IRQ线,其优先级较高。
3)中断控制器将收到的中断号放到I/O端口A中,该端口直接连接到CPU的数据总线上。这样,CPU就可以通过数据总线读出端口A中的中断号。
4)当一切准备就绪后,中断控制器才发送一个信号给CPU的INTR引脚,这时CPU在指令周期的适当时刻,就会分析该信号,以决断中断的类型。
5)如果中断是由外部设备引起的,就会发送一个应答信号给中断控制器的端口B。端口B被设置为一个中断的挂起值,表示CPU正在执行该中断,此时不允许该中断再一次产生。
6)CPU根据中断号确定相应的中断处理函数。
4. Linux中断API函数
4.1 申请中断号
通常情况下,一个驱动程序只要申请中断并添加中断处理函数就可以了。中断的到达和中断处理函数的调用,都是由内核完成。申请中断号可以使内核知道外设应该使用哪一个中断号,哪一个中断处理函数。申请中断号在需要与外部设备发生交互时发生。Linux内核提供request_irq()函数申请中断号。该函数在<kernel/irq/Manage.c中实现>。
request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断,request_irq 函数原型如下:
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 里面查看所有的中断标志,这里我们介绍几个常用的中断标志,这些标志可以通过“|”来实现多种组合。
name:中断名字,设置以后可以在/proc/interrupts 文件中看到对应的中断名字。
dev:如果将 flags 设置为 IRQF_SHARED 的话,dev 用来区分不同的中断,一般情况下将dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。
返回值:0 中断申请成功,其他负值中断申请失败,如果返回-EBUSY 的话表示中断已经被申请了。
4.2 释放中断号
使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。free_irq
函数原型如下所示:
void free_irq(unsigned int irq, void *dev)
函数参数和返回值含义如下:
irq:要释放的中断。
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
返回值:无
4.3 中断处理函数
使用 request_irq 函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就
是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,
dev 也可以指向设备数据结构。中断处理函数的返回值为 irqreturn_t 类型,irqreturn_t 类型定义
如下所示:
示例代码 irqreturn_t 结构
10 enum irqreturn {
11 IRQ_NONE = (0 << 0),
12 IRQ_HANDLED = (1 << 0),
13 IRQ_WAKE_THREAD = (1 << 1),
14 };
15
16 typedef enum irqreturn irqreturn_t;
可以看出 irqreturn_t 是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)
4.4 中断使能与禁止函数
常用的中断使用和禁止函数如下所示:
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 函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()
local_irq_enable 用于使能当前处理器中断系统,local_irq_disable 用于禁止当前处理器中断系统。假如 A 任务调用 local_irq_disable 关闭全局中断 10S,当关闭了 2S 的时候 B 任务开始运行,B 任务也调用 local_irq_disable 关闭全局中断 3S,3 秒以后 B 任务调用 local_irq_enable 函数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时 A 任务要关闭 10S 全局中断的愿望就破灭了,然后 A 任务就“生气了”,结果很严重,可能系统都要被 A 任务整崩溃。为了解决这个问题,B 任务不能直接简单粗暴的通过 local_irq_enable 函数来打开全局中断,而是将中断状态恢复到以前的状态要考虑到别的任务的感受,此时就要用到下面两个函数:
local_irq_save(flags)
local_irq_restore(flags)
这两个函数是一对,local_irq_save 函数用于禁止中断,并且将中断状态保存在 flags 中。 local_irq_restore 用于恢复中断,将中断到 flags 状态。
4.5 上半部与下半部
在有些资料中也将上半部和下半部称为顶半部和底半部。使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知 SOC 有触摸事件发生,SOC 响应中断,然后通过 IIC 接口读取触摸坐标值并将其上报给系统。但是我们都知道 IIC 的速度最高也只有400Kbit/S,所以在中断中通过 IIC 读取数据就会浪费时间。我们可以将通过 IIC 读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
因此,Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:
1)如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
2)如果要处理的任务对时间敏感,可以放到上半部。
3)如果要处理的任务与硬件有关,可以放到上半部
4)除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux 内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。
4.5 软中断
Linux 内核使用结构体 softirq_action 表示软中断, softirq_action 结构体定义在文件 include/linux/interrupt.h 中,内容如下:
示例代码softirq_action 结构体
433 struct softirq_action
434 {
435 void (*action)(struct softirq_action *);
436 };
在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:
示例代码 softirq_vec 数组
static struct softirq_action softirq_vec[NR_SOFTIRQS];
NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:
示例代码 51.1.2.3 softirq_vec 数组
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 文件里面,函数内容如下:
示例代码 51.1.2.4 softirq_init 函数内容
634 void __init softirq_init(void)
635 {
636 int cpu;
637
638 for_each_possible_cpu(cpu) {
639 per_cpu(tasklet_vec, cpu).tail =
640 &per_cpu(tasklet_vec, cpu).head;
641 per_cpu(tasklet_hi_vec, cpu).tail =
642 &per_cpu(tasklet_hi_vec, cpu).head;
643 }
644
645 open_softirq(TASKLET_SOFTIRQ, tasklet_action);
646 open_softirq(HI_SOFTIRQ, tasklet_hi_action);
647 }
从示例代码可以看出,softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。
4.6 tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使用 tasklet。Linux 内核使用结构体:
示例代码 tasklet_struct 结构体
484 struct tasklet_struct
485 {
486 struct tasklet_struct *next; /* 下一个 tasklet */
487 unsigned long state; /* tasklet 状态 */
488 atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
489 void (*func)(unsigned long); /* tasklet 执行的函数 */
490 unsigned long data; /* 函数 func 的参数 */
491 };
第 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);
......
}
4.7 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。 Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):
示例代码 work_struct 结构体
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉条件编译):
示例代码 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 结构体内容如下:
示例代码 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_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);
......
}
4.8 设备树中断信息节点
如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux 内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档 Documentation/devicetree/bindings/arm/gic.txt。打开 imx6ull.dtsi 文件,其中的 intc 节点就是I.MX6ULL 的中断控制器节点,节点内容如下所示:
示例代码 中断控制器 intc 节点
1 intc: interrupt-controller@00a01000 {
2 compatible = "arm,cortex-a7-gic";
3 #interrupt-cells = <3>;
4 interrupt-controller;
5 reg = <0x00a01000 0x1000>,
6 <0x00a02000 0x100>;
7 };
第 2 行,compatible 属性值为“arm,cortex-a7-gic”在 Linux 内核源码中搜索“arm,cortex-a7- gic”即可找到 GIC 中断控制器驱动文件。
第 3 行,#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 掩码。
第 4 行,interrupt-controller 节点为空,表示当前节点是中断控制器。 对于 gpio 来说,gpio 节点也可以作为中断控制器,比如 imx6ull.dtsi 文件中的 gpio5 节点内容如下所示:
示例代码 51.1.3.2 gpio5 设备节点
1 gpio5: gpio@020ac000 {
2 compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
3 reg = <0x020ac000 0x4000>;
4 interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
5 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
6 gpio-controller;
7 #gpio-cells = <2>;
8 interrupt-controller;
9 #interrupt-cells = <2>;
10 };
第 4 行,interrupts 描述中断源信息,对于 gpio5 来说一共有两条信息,中断类型都是 SPI, 触发电平都是 IRQ_TYPE_LEVEL_HIGH。不同之处在于中断源,一个是 74,一个是 75,打开 可以打开《IMX6ULL 参考手册》的“Chapter 3 Interrupts and DMA Events”章节,找到表 3-1,如图所示的内容:
从上图可以看出,GPIO5 一共用了 2 个中断号,一个是 74,一个是 75。其中 74 对应GPIO5_IO00~GPIO5_IO15 这低 16 个 IO,75 对应 GPIO5_IO16~GPIOI5_IO31 这高 16 位 IO。
第 8 行,interrupt-controller 表明了 gpio5 节点也是个中断控制器,用于控制 gpio5 所有 IO 的中断。
第 9 行,将#interrupt-cells 修改为 2。
打开 imx6ull-alientek-emmc.dts 文件,找到如下所示内容:
示例代码 fxls8471 设备节点
1 fxls8471@1e {
2 compatible = "fsl,fxls8471";
3 reg = <0x1e>;
4 position = <0>;
5 interrupt-parent = <&gpio5>;
6 interrupts = <0 8>;
7 };
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,指定父中断,也就是中断控制器。
4.9 获取中断号
编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 irq_of_parse_and_map 函数从 interupts 属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev,
int index)
函数参数和返回值含义如下:
dev:设备节点。
index:索引号,interrupts 属性可能包含多条中断信息,通过 index 指定要获取的信息。
返回值:中断号。
如果使用 GPIO 的话,可以使用 gpio_to_irq 函数来获取 gpio 对应的中断号,函数原型如
下:
int gpio_to_irq(unsigned int gpio)
函数参数和返回值含义如下:
gpio:要获取的 GPIO 编号。
返回值:GPIO 对应的中断号。