[正点原子]Linux驱动学习笔记--11.Linux中断实验

adtxl
2022-10-22 / 0 评论 / 311 阅读 / 正在检测是否收录...

1. Cortex-A7中断系统详解

1.1 Cortex-A7中断系统简介

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

image51dbe76ff1870146.png

中断向量表里面都是中断服务函数的入口地址,因此一款芯片有什么中断都是可以从中断
向量表看出来的。从表 17.1.2.1 中可以看出, Cortex-A7 一共有 8 个中断,而且还有一个中断向量未使用,实际只有 7 个中断。难道一个能跑 Linux 的芯片只有这 7 个中断?明显不可能的!I2C、 SPI、定时器等等的中断怎么处理呢?这个就是 Cortex-A 和 Cotex-M 在中断向量表这一块的区别,对于 Cortex-M 内核来说,中断向量表列举出了一款芯片所有的中断向量,包括芯片外设的所有中断。对于 CotexA 内核来说并没有这么做,在表 17.1.2.1 中有个 IRQ 中断, Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,进而根据具体的中断做出
相应的处理。这些外部中断和 IRQ 中断的关系如图 17.1.2.1 所示:

image92d676902a0799aa.png

在图 17.1.2.1 中,左侧的 Software0_IRQn~PMU_IRQ2_IRQ 这些都是 I.MX6U 的中断,他们都属于 IRQ 中断。当图 17.1.2.1 左侧这些中断中任意一个发生的时候 IRQ 中断都会被触发,所以我们需要在 IRQ 中断服务函数中判断究竟是左侧的哪个中断发生了,然后再做出具体的处理。

在表 17.1.2.1 中一共有 7 个中断,简单介绍一下这 7 个中断:

1、复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、 DDR 等等。
2、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
3、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断, Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
4、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
5、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
6、IRQ 中断(IRQ Interrupt),外部中断,前面已经说了,芯片内部的外设中断都会引起此中断的发生。
7、 FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。

在上面的 7 个中断中,我们常用的就是复位中断和 IRQ 中断,所以我们需要编写这两个中断的中断服务函数,稍后我们会讲解如何编写对应的中断服务函数。首先我们要根据表 17.1.2.1的内容来创建中断向量表,中断向量表处于程序最开始的地方,比如我们前面例程的 start.S 文件最前面,中断向量表如下:

// 示例代码 17.1.1.1 Cortex-A 向量表模板
1 .global _start /* 全局标号 */
2
3 _start:
4 ldr pc, =Reset_Handler /* 复位中断 */
5 ldr pc, =Undefined_Handler /* 未定义指令中断 */
6 ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
7 ldr pc, =PrefAbort_Handler /* 预取终止中断 */
8 ldr pc, =DataAbort_Handler /* 数据终止中断 */
9 ldr pc, =NotUsed_Handler /* 未使用中断 */
10 ldr pc, =IRQ_Handler /* IRQ 中断 */
11 ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
12
13 /* 复位中断 */
14 Reset_Handler:
15 /* 复位中断具体处理过程 */
16
17 /* 未定义中断 */
18 Undefined_Handler:
19 ldr r0, =Undefined_Handler
20 bx r0
21
22 /* SVC 中断 */
23 SVC_Handler:
24 ldr r0, =SVC_Handler
25 bx r0
26
27 /* 预取终止中断 */
28 PrefAbort_Handler:
29 ldr r0, =PrefAbort_Handler
30 bx r0
31
32 /* 数据终止中断 */
33 DataAbort_Handler:
34 ldr r0, =DataAbort_Handler
35 bx r0
36
37 /* 未使用的中断 */
38 NotUsed_Handler:
39
40 ldr r0, =NotUsed_Handler
41 bx r0
42
43 /* IRQ 中断!重点!!!!! */
44 IRQ_Handler:
45 /* 复位中断具体处理过程 */
46
47 /* FIQ 中断 */
48 FIQ_Handler:
49 ldr r0, =FIQ_Handler
50 bx r0

第 4 到 11 行是中断向量表,当指定的中断发生以后就会调用对应的中断复位函数,比如复位中断发生以后就会执行第 4 行代码,也就是调用函数 Reset_Handler,函数 Reset_Handler就是复位中断的中断复位函数,其它的中断同理。

第 14 到 50 行就是对应的中断服务函数,中断服务函数都是用汇编编写的,我们实际需要编写的只有复位中断服务函数 Reset_Handler 和 IRQ 中断服务函数 IRQ_Handler,其它的中断本教程没有用到,所以都是死循环。在编写复位中断复位函数和 IRQ 中断服务函数之前我们还需
要了解一些其它的知识,否则的话就没法编写。

1.2 GIC控制器简介

1、 GIC 控制器总览

I.MX6U(Cortex-A)的中断控制器叫做 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,他们之间的
关系如图 17.1.3.1 所示:

image12bff868d1428f82.png

在图 17.1.3.1 中, GIC 接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给 ARM 内核,这四个信号的含义如下:

VFIQ:虚拟快速 FIQ。
VIRQ:虚拟外部 IRQ。
FIQ:快速中断 IRQ。
IRQ:外部中断 IRQ。

VFIQ 和 VIRQ 是针对虚拟化的,我们不讨论虚拟化,剩下的就是 FIQ 和 IRQ 了,我们前面都讲了很多次了。本教程我们只使用 IRQ,所以相当于 GIC 最终向 ARM 内核就上报一个 IRQ信号。那么 GIC 是如何完成这个工作的呢? GICV2 的逻辑图如下图所示:

image2c44adafa6a3d900.png

上中左侧部分就是中断源,中间部分就是 GIC 控制器,最右侧就是中断控制器向处理器内核发送中断信息。我们重点要看的肯定是中间的 GIC 部分, GIC 将众多的中断源分为分为三类:

①、 SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、 PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。

2、中断 ID

中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,那么这三类中断是如何分配这 1020 个中断 ID 的呢?这 1020 个 ID 分配如下:
ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,像 GPIO 中断、串口中断等这些外部中断 ,至于具体到某个 ID 对应哪个中断那就由半导体厂商根据实际情况去定义了。比如 I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID, I.MX6U 的中断源共有 128+32=160个,这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 Cortex A7 interrupts”小节,中断源如表 17.1.3.1 所示:

imagede5f8fd4c7921ef1.png

限于篇幅原因,表 17.1.3.1 中并没有给出 I.MX6U 完整的中断源,完整的中断源自行查阅《I.MX6ULL 参考手册》的 3.2 小节。在 NXP 官方 SDK中的文件 MCIMX6Y2C.h,在此文件中定义了一个枚举类型 IRQn_Type,此枚举类型就枚举出了 I.MX6U 的所有中断,代码如下所示:

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

3、GIC 逻辑分块

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

Distributor(分发器端): 从图 17.1.3.2 可以看出,此逻辑块负责处理各个中断事件的分发问题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中断源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:

1、全局中断使能控制。
2、控制每一个中断的使能或者关闭。
3、设置每个中断的优先级。
4、设置每个中断的目标处理器列表。
5、设置每个外部中断的触发模式:电平触发或边沿触发。
6、设置每个中断属于组 0 还是组 1。

CPU Interface(CPU 接口端): CPU 接口端听名字就知道是和 CPU Core 相连接的,因此在图 17.1.3.2 中每个 CPU Core 都可以在 GIC 中找到一个与之对应的 CPU Interface。 CPU 接口端就是分发器和 CPU Core 之间的桥梁, CPU 接口端主要工作如下:

1、使能或者关闭发送到 CPU Core 的中断请求信号。
2、应答中断。
3、通知中断处理完成。
4、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
5、定义抢占策略。
6、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。

文件 core_ca7.h 定义了 GIC 结构体,此结构体里面的寄存器分为了分发器端和 CPU 接口端,寄存器定义如下所示:

// 示例代码 17.1.3.2 GIC 控制器结构体
/*
* GIC 寄存器描述结构体,
* GIC 分为分发器端和 CPU 接口端
*/
1 typedef struct
2 {
3 /* 分发器端寄存器 */
4 uint32_t RESERVED0[1024];
5         __IOM uint32_t D_CTLR; /* Offset: 0x1000 (R/W) */
6         __IM uint32_t D_TYPER; /* Offset: 0x1004 (R/ ) */
7         __IM uint32_t D_IIDR; /* Offset: 0x1008 (R/ ) */
8         uint32_t RESERVED1[29];
9         __IOM uint32_t D_IGROUPR[16]; /* Offset: 0x1080 - 0x0BC (R/W) */
10         uint32_t RESERVED2[16];
11         __IOM uint32_t D_ISENABLER[16];/* Offset: 0x1100 - 0x13C (R/W) */
12         uint32_t RESERVED3[16];
13         __IOM uint32_t D_ICENABLER[16];/* Offset: 0x1180 - 0x1BC (R/W) */
14         uint32_t RESERVED4[16];
15         __IOM uint32_t D_ISPENDR[16]; /* Offset: 0x1200 - 0x23C (R/W) */
16         uint32_t RESERVED5[16];
17         __IOM uint32_t D_ICPENDR[16]; /* Offset: 0x1280 - 0x2BC (R/W) */
18         uint32_t RESERVED6[16];
19         __IOM uint32_t D_ISACTIVER[16];/* Offset: 0x1300 - 0x33C (R/W) */
20         uint32_t RESERVED7[16];
21         __IOM uint32_t D_ICACTIVER[16];/* Offset: 0x1380 - 0x3BC (R/W) */
22         uint32_t RESERVED8[16];
23         __IOM uint8_t D_IPRIORITYR[512];/* Offset: 0x1400 - 0x5FC (R/W) */
24         uint32_t RESERVED9[128];
25         __IOM uint8_t D_ITARGETSR[512];/* Offset: 0x1800 - 0x9FC (R/W) */
26         uint32_t RESERVED10[128];
27         __IOM uint32_t D_ICFGR[32]; /* Offset: 0x1C00 - 0xC7C (R/W) */
28         uint32_t RESERVED11[32];
29         __IM uint32_t D_PPISR; /* Offset: 0x1D00 (R/ ) */
30         __IM uint32_t D_SPISR[15]; /* Offset: 0x1D04 - 0xD3C (R/ ) */
31         uint32_t RESERVED12[112];
32         __OM uint32_t D_SGIR; /* Offset: 0x1F00 ( /W) */
33         uint32_t RESERVED13[3];
34         __IOM uint8_t D_CPENDSGIR[16];/* Offset: 0x1F10 - 0xF1C (R/W) */
35         __IOM uint8_t D_SPENDSGIR[16];/* Offset: 0x1F20 - 0xF2C (R/W) */
36         uint32_t RESERVED14[40];
37         __IM uint32_t D_PIDR4; /* Offset: 0x1FD0 (R/ ) */
38         __IM uint32_t D_PIDR5; /* Offset: 0x1FD4 (R/ ) */
39         __IM uint32_t D_PIDR6; /* Offset: 0x1FD8 (R/ ) */
40         __IM uint32_t D_PIDR7; /* Offset: 0x1FDC (R/ ) */
41         __IM uint32_t D_PIDR0; /* Offset: 0x1FE0 (R/ ) */
42         __IM uint32_t D_PIDR1; /* Offset: 0x1FE4 (R/ ) */
43         __IM uint32_t D_PIDR2; /* Offset: 0x1FE8 (R/ ) */
44         __IM uint32_t D_PIDR3; /* Offset: 0x1FEC (R/ ) */
45         __IM uint32_t D_CIDR0; /* Offset: 0x1FF0 (R/ ) */
46         __IM uint32_t D_CIDR1; /* Offset: 0x1FF4 (R/ ) */
47         __IM uint32_t D_CIDR2; /* Offset: 0x1FF8 (R/ ) */
48         __IM uint32_t D_CIDR3; /* Offset: 0x1FFC (R/ ) */
49
50 /* CPU 接口端寄存器 */
51         __IOM uint32_t C_CTLR; /* Offset: 0x2000 (R/W) */
52         __IOM uint32_t C_PMR; /* Offset: 0x2004 (R/W) */
53         __IOM uint32_t C_BPR; /* Offset: 0x2008 (R/W) */
54         __IM uint32_t C_IAR; /* Offset: 0x200C (R/ ) */
55         __OM uint32_t C_EOIR; /* Offset: 0x2010 ( /W) */
56         __IM uint32_t C_RPR; /* Offset: 0x2014 (R/ ) */
57         __IM uint32_t C_HPPIR; /* Offset: 0x2018 (R/ ) */
58         __IOM uint32_t C_ABPR; /* Offset: 0x201C (R/W) */
59         __IM uint32_t C_AIAR; /* Offset: 0x2020 (R/ ) */
60         __OM uint32_t C_AEOIR; /* Offset: 0x2024 ( /W) */
61         __IM uint32_t C_AHPPIR; /* Offset: 0x2028 (R/ ) */
62         uint32_t RESERVED15[41];
63         __IOM uint32_t C_APR0; /* Offset: 0x20D0 (R/W) */
64         uint32_t RESERVED16[3];
65         __IOM uint32_t C_NSAPR0; /* Offset: 0x20E0 (R/W) */
66         uint32_t RESERVED17[6];
67         __IM uint32_t C_IIDR; /* Offset: 0x20FC (R/ ) */
68         uint32_t RESERVED18[960];
69         __OM uint32_t C_DIR; /* Offset: 0x3000 ( /W) */
70 } GIC_Type;

“示例代码 17.1.3.2”中的结构体 GIC_Type 就是 GIC 控制器,列举出了 GIC 控制器的所有寄存器,可以通过结构体 GIC_Type 来访问 GIC 的所有寄存器。

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

1.3 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 这四个寄存器,因为我们本章实验要用到这四个寄存器,其他的寄存器大家参考上面的两个文档即可。

1、c0 寄存器

CP15 协处理器有 16 个 32 位寄存器, c0~c15,在使用 MRC 或者 MCR 指令访问这 16 个寄存器的时候,指令中的 CRn、 opc1、 CRm 和 opc2 通过不同的搭配,其得到的寄存器含义是不同的。比如 c0 在不同的搭配情况下含义如图 17.1.4.1 所示:

image69e5ec8dab60d1eb.png

在图 17.1.4.1 中当 MRC/MCR 指令中的 CRn=c0, opc1=0, CRm=c0, opc2=0 的时候就表示此时的 c0 就是 MIDR 寄存器,也就是主 ID 寄存器,这个也是 c0 的基本作用。对于 Cortex-A7内核来说, c0 作为 MDIR 寄存器的时候其含义如图 17.1.4.2 所示:

imageb4a6e9577c8a6208.png

在图 17.1.4.2 中各位所代表的含义如下:

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 就是次版本号。

2、c1 寄存器

c1 寄存器同样通过不同的配置,其代表的含义也不同,如图 17.1.4.3 所示:

image795a6111730edf17.png

在图 17.1.4.3 中当 MRC/MCR 指令中的 CRn=c1, opc1=0, CRm=c0, opc2=0 的时候就表示此时的 c1 就是 SCTLR 寄存器,也就是系统控制寄存器,这个是 c1 的基本作用。 SCTLR 寄存器主要是完成控制功能的,比如使能或者禁止 MMU、 I/D Cache 等, c1 作为 SCTLR 寄存器的时候其含义如图 17.1.4.4 所示:

imagecfca8863849e4ef6.png

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)寄存器中。

3、c12 寄存器

c12 寄存器通过不同的配置,其代表的含义也不同,如图 17.1.4.4 所示:

image9d98bf7eedbc9a99.png

在图 17.1.4.4 中当 MRC/MCR 指令中的 CRn=c12, opc1=0, CRm=c0, opc2=0 的时候就表示此时 c12 为 VBAR 寄存器,也就是向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中,比如在前面的例程中,代码链接的起始地址为
0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。所以就需要设置 VBAR 为 0X87800000,设置命令如下:

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

4、c15 寄存器

c15 寄存器也可以通过不同的配置得到不同的含义,参考文档《Cortex-A7 Technical ReferenceManua.pdf》 第 68 页“4.2.16 c15 registers”,其配置如图 17.1.4.5 所示:

image0f7dee6dd95cbc77.png

在图 17.1.4.5 中,我们需要 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 的值

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

1.4 中断使能

中断使能包括两部分,一个是 IRQ 或者 FIQ 总中断使能,另一个就是 ID0~ID1019 这 1020个中断源的使能。

1、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 的使能和禁止,图表 17.1.5.1 所示:

imaged4c0e56cdc45e3bc.png

2、ID0~ID1019 中断使能和禁止

GIC 寄存器 GICD_ISENABLERnGICD_ 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 中断的。

1.5 中断优先级设置

1、优先级数配置

Cortex-A7 的中断优先级也可以分为抢占优先级和子优先级,两者同样是可以配置的。 GIC 控制器最多可以支持 256 个优先级,数字越小,优先级越高! Cortex-A7 选择了 32 个优先级。在使用中断的时候需要初始化 GICC_PMR 寄存器,此寄存器用来决定使用几级优先级,寄存器结构如图 17.1.6.1 所示:

imagec552a2664ba6add5.png

GICC_PMR 寄存器只有低 8 位有效,这 8 位最多可以设置 256 个优先级,其他优先级数设
置如表 17.1.6.1 所示:

imagee2094faac558503c.png

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

2、抢占优先级和子优先级位数设置

抢占优先级和子优先级各占多少位是由寄存器 GICC_BPR 来决定的, GICC_BPR 寄存器结构如图 17.1.6.2 所示:

imageec46b3d13318727a.png

寄存器 GICC_BPR 只有低 3 位有效,其值不同,抢占优先级和子优先级占用的位数也不同,配置如表 17.1.6.2 所示:

image6757fc37e3b202a6.png

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

3、优先级设置

前面已经设置好了 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;

有关优先级设置的内容就讲解到这里,优先级设置主要有三部分:

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

2. Linux中断简介

Linux 内核提供了完善的中断框架,我们只需要申请中断,然后注
册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。本章我们就来学习一下如何在 Linux 下使用中断。

2.1 Linux 中断 API 函数

先来回顾一下裸机实验里面中断的处理方法:
1、使能中断,初始化相应的寄存器。
2、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
3、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

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

1、中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。

2、request_irq 函数

在 Linux 内核中要想使用某个中断是需要申请的, request_irq 函数用于申请中断, 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 里面查看所有的中断标志,这里我们介绍几个常用的中断标志,如下表所示:

image80b94d8cbde23ff9.png

比如 I.MX6U-ALPHA 开发板上的 KEY0 使用 GPIO1_IO18,按下 KEY0 以后为低电平,因此可以设置为下降沿触发,也就是将 flags 设置为 IRQF_TRIGGER_FALLING。表 51.1.1.1 中的这些标志可以通过“|”来实现多种组合。

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

3、free_irq 函数

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

void free_irq(unsigned int irq,
                void *dev)

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

4、中断处理函数

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

irqreturn_t (*irq_handler_t) (int, void *)

第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向 void 的指针,也就是个通用指针,需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备,dev 也可以指向设备数据结构。中断处理函数的返回值为 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)

5、中断使能与禁止函数

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

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 的时候常说的关闭全局中断,这个时候可以使用如下两个函数:

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 状态。

2.2 上半部与下半部

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

上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

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

1、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
2、如果要处理的任务对时间敏感,可以放到上半部。
3、如果要处理的任务与硬件有关,可以放到上半部
4、除了上述三点以外的其他任务,优先考虑放到下半部。

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

1、软中断

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

433 struct softirq_action
434 {
435     void (*action)(struct softirq_action *);
436 };

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

static struct softirq_action softirq_vec[NR_SOFTIRQS];

NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:

enum
{
    HI_SOFTIRQ=0, /* 高优先级软中断 */
    TIMER_SOFTIRQ, /* 定时器软中断 */
    NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ, /* tasklet 软中断 */
    SCHED_SOFTIRQ, /* 调度软中断 */
    HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    RCU_SOFTIRQ, /* RCU 软中断 */
    NR_SOFTIRQS
};

可以看出,一共有 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:要开启的软中断,在示例代码中选择一个。
action:软中断对应的处理函数。
返回值: 没有返回值。

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

void raise_softirq(unsigned int nr)

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

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

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 }

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

2、 tasklet

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

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);
    ......
}

2、工作队列

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

Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):

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

这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下(省略掉条件编译):

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

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

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

从示例代码可以看出,每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_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);
    ......
}

2.3 设备树中断信息节点

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

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 节点内容如下所示:

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,

image6f74f71851c32eb9.png

从图 51.1.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 文件,找到如下所示内容:

// 示例代码 51.1.3.3 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 表示低电平触发。

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

2.4 获取中断号

编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过 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 对应的中断号。

3. 实验程序

本次实验我们驱动 I.MX6U-ALPHA 开发板上的 KEY0 按键,不过我们采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。通过本章我们可以学习到 Linux 内核中断的使用方法,以及对 Linux 内核定时器的回顾。

实验步骤:

1.修改设备树文件

本章实验使用到了按键 KEY0,按键 KEY0 使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

image.png

设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。

设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18号 IO。 IRQ_TYPE_EDGE_BOTH 定义在文件 include/linux/irq.h 中,IRQ_TYPE_EDGE_BOTH 表示上升沿和下降沿同时有效,相当于 KEY0 按下和释放都会触发中断。

2.按键中断驱动程序

https://github.com/ADTXL/imx_linux/tree/master/kmod/13_irq

4. 其它

4.1 imx6ull设备树中的中断树

设备树是如何描述整个中断系统信息的。

1.顶层中断控制器

打开 ./arch/arm/boot/dts/ 目录下的 imx6ull.dtsi 设备树文件, 找到“interrupt-controller”节点,如下所示。

intc: interrupt-controller@a01000 {
    compatible = "arm,cortex-a7-gic";
    #interrupt-cells = <3>;
    interrupt-controller;
    reg = <0xa01000 0x1000>,
          <0xa02000 0x100>;
};
  • compatible:compatible属性用于平台设备驱动的匹配
  • reg:reg指定中断控制器相关寄存器的地址及大小
  • interrupt-controller:声明该设备树节点是一个中断控制器。
  • #interrupt-cells :指定它的“子”中断控制器用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。

2.gpc一级子中断控制器

在imx6ull.dtsi文件中直接搜索节点标签“intc”即可找到“一级子中断控制器”

gpc: gpc@20dc000 {
    compatible = "fsl,imx6ul-gpc", "fsl,imx6q-gpc";
    reg = <0x20dc000 0x4000>;
    interrupt-controller;
    #interrupt-cells = <3>;
    interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-parent = <&intc>;
    fsl,mf-mix-wakeup-irq = <0xfc00000 0x7d00 0x0 0x1400640>;
};

结合以上代码介绍如下:

  • interrupt-controller:声明该设备树节点是一个中断控制器,只要是中断控制器都要用该标签声明。
  • #interrupt-cells:用于规定该节点的“子”中断控制器将使用三个参数来描述子控制器的信息。
  • interrupt-parent:指定该中断控制器的“父”中断控制器。除了“顶层中断控制器”其他中断控制器都要声明“父”中断控制器。
  • interrupts:具体的中断描述信息,在该节点的中断控制器的“父”中断控制器,规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:

第一个参数用于指定中断类型,在GIC中中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。

第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-987]。

第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:

#define IRQ_TYPE_NONE           0
#define IRQ_TYPE_EDGE_RISING    1
#define IRQ_TYPE_EDGE_FALLING   2
#define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH     4
#define IRQ_TYPE_LEVEL_LOW      8

[8-15]位在PPI中断中用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU,一位代表一个CPU, 为1则将PPI中断发送到CPU,否则屏蔽。imx6ull是单核CPU,所以我们不用设置这些位。

3.二级子中断控制器

同样在imx6ull.dtsi文件中直接搜索节点标签“gpc”即可找到“二级子中断控制器”如下所示。

soc {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "simple-bus";
    interrupt-parent = <&gpc>;
    ranges;

    //busfreq子节点
        busfreq {
        ................  //表示省略
    }
    ...............     //表示省略

soc 节点即片上外设“总节点”,翻阅源码可以发现该节点很长,我们使用的外设大多包含在里面。 具体外设(例如GPIO)也可作为中断控制器,这里声明了它们的“父”中断控制器是 <&gpc>节点。

soc节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键, 使用的是GPIO5_1,所以这里以GPIO5为例介绍。在imx6ull.dtsi文件中直接搜索GPIO5,找到GPIO5对应的设备树节点,如下所示。

gpio5: gpio@20ac000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x20ac000 0x4000>;
    interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
                 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_GPIO5>;
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    gpio-ranges = <&iomuxc 0 7 10>, <&iomuxc 10 5 2>;
};

以上是gpio5节点的全部内容,这里主要介绍和中断相关的节点信息。

  • interrupts:用来描述GPIO5能产生中断类型及中断编号、触发方式,查看imx6ull的数据手册我们可以知道, GPIO5能产生的中断只有两个,分配的中断ID为106、107,对于SPI中断它们的编号是74(106-32),75(107-32)。
  • interrupt-controller:声明该节点是一个中断控制器
  • #interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。
0

评论 (0)

取消