IMX6ULL嵌入式Linux驱动开发学习
以下内容是我在学习正点原子IMX6ULL
开发板alpha
中记录的笔记,部分摘录自正点原子IMX6ULL开发手册
。
一、Linux内核中断处理
1.1 裸机中断
- 使能中断,初始化相应的寄存器。
- 注册中断服务函数,也就是向
irqTable
数组(裸机例程)的指定标号处写入中断服务函数。 - 中断发生后进入
IRQ
中断服务函数,在IRQ
中断服务函数中,根据中断号在irqTable
里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
1.2 Linux中断
先知道要使用的中断对应的中断号。
根据终端号申请
request_irq
,request_irq
函数可能会导致睡眠,此函数同时会激活中断。1
2
3
4
5
6
7
8
9/* @param irq 中断号
* @param handler 中断服务函数
* @param flags 中断标志、中断触发方式
* @param name 中断名字
* @param dev 使用共享中断时,唯一用来区分的标志。当多个设备共享一个中断线,共享的所有中断都必须指定此标志。
* @return 0 中断申请成功,其他负值 中断申请失败,如果返回 -EBUSY 的话表示中断已经被申请了。
*/
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);不再使用中断时,需要释放中断
free_irq
。1
2
3
4
5/* @param irq 中断号
* @param dev 如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。
* 共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
*/
void free_irq(unsigned int irq, void *dev);在使用
request_irq
函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示1
irqreturn_t (*irq_handler_t) (int, void *)
第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向
void
的指针,也就是个通用指针,需要与request_irq
函数的dev
参数保持一致。用于区分共享中断的不同设备,dev
也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t
类型,irqreturn_t
类型定义如下所示:1
2
3
4
5
6
7enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;irqreturn_t
是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:return IRQ_RETVAL(IRQ_HANDLED)
中断使能和禁止
enable_irq
和disable_irq
用于使能和禁止指定的中断,irq
就是要禁止的中断号。1
2void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);disable_irq
函数要等到当前正在执行的中断处理函数执行完才返回,因此需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:1
void disable_irq_nosync(unsigned int irq)
disable_irq_nosync
函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习
STM32
的时候常说的关闭全局中断,这个时候可以使用如下两个函数:1
2local_irq_enable();
local_irq_disable();local_irq_enable
用于使能当前处理器中断系统,local_irq_disable
用于禁止当前处理器中断系统。但是在任务中使用这两个函数会出现问题。比如假如
A
任务调用local_irq_disable
关闭全局中断10秒,当关闭了2秒的时候B
任务开始运行,B
任务也调用local_irq_disable
关闭全局中断3秒, 3秒以后B
任务调用local_irq_enable
函数将全局中断打开了。此时才过去 2+3=5 秒的时间,然后全局中断就被打开了,此时A
任务要关闭10秒全局中断的愿望就破灭了,然后A
任务就“生气了”,结果很严重,可能系统都要被A
任务整崩溃。为了解决这个问题,B
任务不能直接简单粗暴的通过local_irq_enable
函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:1
2local_irq_save(flags);
local_irq_restore(flags);这两个函数是一对,
local_irq_save
函数用于禁止中断,并且将中断状态保存在flags
中。local_irq_restore
用于恢复中断,将中断恢复到flags
状态。
1.3 上半部和下半部
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
关于代码属于上半部或下半部的参考
- 如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
- 如果要处理的任务对时间敏感,可以放到上半部。
- 如果要处理的任务与硬件有关,可以放到上半部。
- 除了上述三点以外的其他任务,优先考虑放到下半部。
Linux
对下半部的处理方式。
1.3.1 软中断
软中断必须在编译的时候静态注册(写入到内核中)!软中断不要去用。
Linux
内核使用结构体softirq_action
表示软中断,softirq_action
结构体定义在文件 include/linux/interrupt.h
中,内容如下:
1 | struct softirq_action |
在 kernel/softirq.c
文件中一共定义了 10 个软中断,如下所示:
1 | static struct softirq_action softirq_vec[NR_SOFTIRQS]; |
NR_SOFTIRQS
是枚举类型,定义在文件 include/linux/interrupt.h
中,定义如下:
1 | enum |
softirq_action
结构体中的 action
成员变量就是软中断的服务函数,数组 softirq_vec
是个全局数组,因此所有的 CPU
(对于 SMP
系统而言)都可以访问到,每个 CPU
都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU
所执行的软中断服务函数确是相同的,都是数组 softirq_vec
中定义的 action
函数。
要使用软中断要先注册。
1
2
3
4
5/* @brief 注册软中断服务函数
* @param nr 要开启的软中断 是上面枚举中的一个。
* @param action 软中断对应的处理函数
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))软中断必须在编译的时候静态注册!
触发软中断
注册好软中断以后需要通过
raise_softirq
函数触发,raise_softirq
函数原型如下:1
2
3
4/* @brief 触发软中断
* @param nr 要触发的软中断
*/
void raise_softirq(unsigned int nr)
1.3.2 tasklet
tasklet
是利用软中断来实现的另外一种下半部机制,建议使用 tasklet
。
1 | struct tasklet_struct |
如果要使用 tasklet
,必须先定义一个 tasklet
,然后使用 tasklet_init
函数初始化 tasklet
,taskled_init
函数原型如下:
1 | /* @brief 初始化 tasklet |
也 可 以 使 用 宏 DECLARE_TASKLET
来 一 次 性 完 成 tasklet
的 定 义 和 初 始 化 ,DECLARE_TASKLET
定义在 include/linux/interrupt.h
文件中,定义如下:
1 | /* name 为要定义的 tasklet 名字, |
在上半部,也就是中断处理函数中调用 tasklet_schedule
函数就能使 tasklet
在合适的时间运
行, tasklet_schedule
函数原型如下:
1 | /* @param t 要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。*/ |
tasklet
使用顺序:
- 定义一个
tasklet
。 - 初始化
tasklet
,重点是设置对应的处理函数。 - 在上半部中调用
tasklet_schedule
函数,使tasklet
在合适的时间运行。
1 | /* 定义 taselet */ |
1.3.3 工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet
。
Linux
内核使用 work_struct
结构体表示一个工作,内容如下(省略掉条件编译):
1 | struct work_struct { |
这些工作组织成工作队列,工作队列使用 workqueue_struct
结构体表示,内容如下(省略掉条件编译):
1 | struct workqueue_struct { |
Linux
内核使用工作者线程(worker thread)
来处理工作队列中的各个工作, Linux
内核使用worker
结构体表示工作者线程, worker
结构体内容如下:
1 | struct worker { |
每个 worker
都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,只需要定义工作(work_struct)
即可,关于工作队列和工作者线程基本不用去管。
创建工作直接定一个
work_struct
结构体,然后使用INIT_WORK
宏来初始化工作即可,INIT_WORK
宏定义如下:1
2/* _work 表示要初始化的工作, _func 是工作对应的处理函数。*/
也可以使用
DECLARE_WORK
宏一次性完成工作的创建和初始化,宏定义如下:1
2/* n 表示定义的工作(work_struct), f 表示工作对应的处理函数。*/
同
tasklet
一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work
,函数原型如下所示:1
2
3
4
5/* @brief 调度工作
* @param work 要调度的工作
* @return 结果 0 成功,其他值 失败
*/
bool schedule_work(struct work_struct *work)
工作队列使用顺序:
- 定义一个
work
。 - 初始化
work
,重点同样是是设置对应的处理函数。 - 在上半部中调用
schedule_work
函数,使work
在合适的时间运行。
1 | /* 定义工作(work) */ |
1.4 设备树中断节点信息
#interrupt-cells
指定中断域编码中断说明符所需的单元数。对于设备来说,会使用interrupts
属性来描述中断信息,而#interrupt-cells
则指定了描述一个中断信息需要几个数值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
interrupt-controller;
reg = <0x00a01000 0x1000>,
<0x00a02000 0x100>;
};
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;
interrupt-controller; // 表示当前节点是中断控制器
};从
gpio5
的interrupts
属性可以看到描述一个中断信息需要三个cell
,分别是
- 第一个
cell
:中断类型,0
表示SPI
中断(共享中断,不是SPI
通讯的中断),1
表示PPI
中断。- 第二个
cell
:中断号,对于SPI
中断来说中断号的范围为0~987
,对于PPI
中断来说中断号的范围为0~15
。- 第三个
cell
:标志,bit[3:0]
表示中断触发类型,为1
的时候表示上升沿触发,为2
的时候表示下降沿触发,为4
的时候表示高电平触发,为8
的时候表示低电平触发。bit[15:8]
为PPI
中断的CPU 掩码
。
在NXP的官方6ull
开发板上有一个磁力计芯片fxls8471
,fxls8471
的中断引脚链接到I.MX6ULL
的SNVS_TAMPER0
引脚上,而这个引脚可以复用为GPIO_IO0
。在设备树中fxls8471
的描述如下:
1 | fxls8471@1e { |
interrupt-parent
属性设置中断控制器,这里使用gpio5
作为中断控制器。interrupts
属性设置中断信息,因为在上面gpio5
的节点中将#interrupt-cells
设置为了2
,所以这里的interrupts
属性,需要使用两个cell
来描述中断信息。
从设备树中获取中断号
1 | /* @brief 获取中断号 |
如果使用 GPIO
的话,可以使用 gpio_to_irq
函数来获取 gpio
对应的中断号,函数原型如下:
1 | /* @brief 获取gpio对应的中断号 |
二、编写按键中断实验驱动
2.1 配置设备树
1 | /dts-v1/; |
2.2 按键中断驱动程序
1 |
|
2.3 使用下半部tasklet
的按键中断驱动程序
1 |
|
2.4 测试APP程序
1 |
|
- 本文作者: 路痴的兔子
- 本文链接: https:/proudrabbit.gitee.io/IMX6ULL嵌入式Linux驱动学习笔记(九).html
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!