linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动
异常(exception)就是发生了意外的情况,它会中止处理器正常的执行流程。发生异常时,处理器要去执行对应的程序(称为异常处理程序)。异常有很多种,每种都有自己的处理程序,中断是一种异常。处理完异常后,要恢复发生异常之前的、被打断的操作。
CPU每执行完一条指令,都会检查一下是否发生了某个异常,若是则中断当前执行流程,转去处理异常。导致中断发生的情况有很多,比如:按键、 定时器、ADC转换完成、 UART发送完数据、收到数据 等等。这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知CPU。
arm对异常(中断)处理过程:
① 初始化:
a. 设置中断源,让它可以产生中断
b. 设置中断控制器(可以屏蔽某个中断,优先级)
c. 设置CPU总开关(使能中断)
② 执行其他程序:正常程序
③ 产生中断:比如按下按键—>中断控制器—>CPU
④ CPU 每执行完一条指令都会检查有无中断/异常产生
⑤ CPU发现有中断/异常产生,开始处理。
对于不同的异常,跳去不同的地址执行程序。
这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。
③④⑤都是硬件做的。
⑥ 这些函数做什么事情?
软件做的:
a. 保存现场(各种寄存器)
b. 处理异常(中断):
分辨中断源,再调用不同的处理函数
c. 恢复现场
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
① 对内存只有读、写指令
② 对于数据的运算是在CPU内部实现
③ 使用RISC指令的CPU复杂度小一点,易于设计
比如对于a=a+b这样的算式,需要经过下面4个步骤才可以实现:
深入ARM处理器的内部。简单概括如下,我们先忽略各种CPU模式(系统模式、用户模式等等)。
CPU运行时,先去取得指令,再执行指令:
① 把内存a的值读入CPU寄存器R0
② 把内存b的值读入CPU寄存器R1
③ 把R0、R1累加,存入R0
④ 把R0的值写入内存a
从上图可知,CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场,也叫保存上下文。
保存在哪里?内存,这块内存就称之为栈,这个栈就是用来保存程序上下文的地方。
程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。
这个场景并不局限于中断,下图可以概括进程A、B的切换过程,其他情况是类似的:
a. 函数调用:
在函数A里调用函数B,实际就是中断函数A的执行。
那么需要把函数A调用B之前瞬间的CPU寄存器的值,保存到栈里;
再去执行函数B;
函数B返回之后,就从栈中恢复函数A对应的CPU寄存器值,继续执行。
b. 中断处理
进程A正在执行,这时候发生了中断。
CPU强制跳到中断异常向量地址去执行,
这时就需要保存进程A被中断瞬间的CPU寄存器值,
可以保存在进程A的内核态栈,也可以保存在进程A的内核结构体中。
中断处理完毕,要继续运行进程A之前,恢复这些值。
c. 进程切换
在所谓的多任务操作系统中,我们以为多个程序是同时运行的。
如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程A的时间用完了,就切换到进程B。
CortexA7 内核有 8 个异常中断,这 8 个异常中断的中断向量表如表:
①、复位中断(Rest), CPU 复位以后就会进入复位中断,我们可以在复位中断服务函数里面做一些初始化工作,比如初始化 SP 指针、 DDR 等等。
②、未定义指令中断(Undefined Instruction),如果指令不能识别的话就会产生此中断。
③、软中断(Software Interrupt,SWI),由 SWI 指令引起的中断, Linux 的系统调用会用 SWI指令来引起软中断,通过软中断来陷入到内核空间。
④、指令预取中止中断(Prefetch Abort),预取指令的出错的时候会产生此中断。
⑤、数据访问中止中断(Data Abort),访问数据出错的时候会产生此中断。
⑥、 IRQ 中断(IRQ Interrupt),外部中断,芯片内部的外设中断都会引起此中断的发生。
⑦、 FIQ 中断(FIQ Interrupt),快速中断,如果需要快速处理中断的话就可以使用此中断。
“向量”里存有一条指令,用来处理某个异常。触发异常时ARM核会跳转到某个“向量”,执行其中的指令。这些“向量”汇集在一起,把它们称为“向量表”,它位于内存中的特定位置。默认向量基址为0x00000000,但大多数ARM核允许将向量基址移至0xFFFF0000(或HIVECS)。所有Cortex-A系列处理器都允许这样做,这是Linux内核选择的默认地址。
Non-secure状态下SCTLR.V位决定了向量表的基地址,如果V == 0的话,Non-secure VBAR保存了异常向量基地址;如果v==1的话,异常向量基地址为0xFFFF0000。
ARM体系结构定义了通用中断控制器(GIC),该控制器包括一组用于管理单核或多核系统中的中断的硬件资源。GIC提供了内存映射寄存器,可用于管理中断源和为,以及(在多核系统中)用于将中断路由到各个CPU核。它使软件能够屏蔽,启用和禁用来自各个中断源的中断,以(在硬件中)对各个中断源进行优先级排序和生成软件触发中断。它还提供对TrustZone安全性扩展的支持。GIC接受系统级别中断的产生,并可以发信号通知给它所连接的每个内核,从而有可能导致IRQ或FIQ异常发生。
从软件角度来看,GIC具有两个主要功能模块,简单画图如下:
分发器(Distributor)
此逻辑块负责处理各个中断事件的分发问
题,也就是中断事件应该发送到哪个 CPU Interface 上去。分发器收集所有的中源,可以控制每个中断的优先级,它总是将优先级最高的中断事件发送到 CPU 接口端。分发器端要做的主要工作如下:
①、全局中断使能控制。
②、控制每一个中断的使能或者关闭。
③、设置每个中断的优先级。
④、设置每个中断的目标处理器列表。
⑤、设置每个外部中断的触发模式:电平触发或边沿触发。
⑥、设置每个中断属于组 0 还是组 1。
CPU接口单元(CPU Interface)
CPU核通过控制器的CPU接口单元接收中断。CPU接口单元寄存器用于屏蔽,识别和控制转发到CPU核的中断的状态。系统中的每个CPU核心都有一个单独的CPU接口。CPU 接口端就是分发器和 CPU Core 之间的桥梁, CPU 接口端主要工作如下:
①、使能或者关闭发送到 CPU Core 的中断请求信号。
②、应答中断。
③、通知中断处理完成。
④、设置优先级掩码,通过掩码来设置哪些中断不需要上报给 CPU Core。
⑤、定义抢占策略。
⑥、当多个中断到来的时候,选择优先级最高的中断通知给 CPU Core。
中断ID
中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI,那么这三类中断是如何分配这 1020 个中断 ID 的呢?这 1020 个 ID 分配如下:
①、 SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
②、 PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
③、 SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信
Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的GICC_IAR寄存器来判断发生的具体是什么中断,进而根据具体的中断做出相应的处理。这些外部中断和 IRQ 中断的关系如图所示:
编写 start.S 文件
1 .global _start /* 全局标号 */
2
3/*
4 * 描述: _start 函数,首先是中断向量表的创建
5 */
6 _start:
7 ldr pc, =Reset_Handler /* 复位中断 */
8 ldr pc, =Undefined_Handler /* 未定义指令中断 */
9 ldr pc, =SVC_Handler /* SVC(Supervisor)中断*/
10 ldr pc, =PrefAbort_Handler /* 预取终止中断 */
11 ldr pc, =DataAbort_Handler /* 数据终止中断 */
12 ldr pc, =NotUsed_Handler /* 未使用中断 */
13 ldr pc, =IRQ_Handler /* IRQ 中断 */
14 ldr pc, =FIQ_Handler /* FIQ(快速中断) */
15
16 /* 复位中断 */
17 Reset_Handler:
18
19 cpsid i /* 关闭全局中断 */
20
21 /* 关闭 I,DCache 和 MMU
22 * 采取读-改-写的方式。
23 */
24 mrc p15, 0, r0, c1, c0, 0 /* 读取 CP15 的 C1 寄存器到 R0 中 */
25 bic r0, r0, #(0x1 << 12) /* 清除 C1 的 I 位,关闭 I Cache */
26 bic r0, r0, #(0x1 << 2) /* 清除 C1 的 C 位,关闭 D Cache */
27 bic r0, r0, #0x2 /* 清除 C1 的 A 位,关闭对齐检查 */
28 bic r0, r0, #(0x1 << 11) /* 清除 C1 的 Z 位,关闭分支预测 */
29 bic r0, r0, #0x1 /* 清除 C1 的 M 位,关闭 MMU */
30 mcr p15, 0, r0, c1, c0, 0 /* 将 r0 的值写入到 CP15 的 C1 中 */
31
32
33 #if 0
34 /* 汇编版本设置中断向量表偏移 */
35 ldr r0, =0X87800000
36
37 dsb
38 isb
39 mcr p15, 0, r0, c12, c0, 0
40 dsb
41 isb
42 #endif
43
44 /* 设置各个模式下的栈指针,
45 * 注意: IMX6UL 的堆栈是向下增长的!
46 * 堆栈指针地址一定要是 4 字节地址对齐的!!!
47 * DDR 范围:0X80000000~0X9FFFFFFF 或者 0X8FFFFFFF
48 */
49 /* 进入 IRQ 模式 */
50 mrs r0, cpsr
51 bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
52 orr r0, r0, #0x12 /* r0 或上 0x12,表示使用 IRQ 模式 */
53 msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
54 ldr sp, =0x80600000 /* IRQ 模式栈首地址为 0X80600000,大小为 2MB */
55
56 /* 进入 SYS 模式 */
57 mrs r0, cpsr
58 bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
59 orr r0, r0, #0x1f /* r0 或上 0x1f,表示使用 SYS 模式 */
60 msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
61 ldr sp, =0x80400000 /* SYS 模式栈首地址为 0X80400000,大小为 2MB */
62
63 /* 进入 SVC 模式 */
64 mrs r0, cpsr
65 bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
66 orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 */
67 msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
68 ldr sp, =0X80200000 /* SVC 模式栈首地址为 0X80200000,大小为 2MB */
69
70 cpsie i /* 打开全局中断 */
71
72 #if 0
73 /* 使能 IRQ 中断 */
74 mrs r0, cpsr /* 读取 cpsr 寄存器值到 r0 中 */
75 bic r0, r0, #0x80 /* 将 r0 寄存器中 bit7 清零,也就是 CPSR 中
76 * 的 I 位清零,表示允许 IRQ 中断
77 */
78 msr cpsr, r0 /* 将 r0 重新写入到 cpsr 中 */
79 #endif
80
81 b main /* 跳转到 main 函数 */
82
83 /* 未定义中断 */
84 Undefined_Handler:
85 ldr r0, =Undefined_Handler
86 bx r0
87
88 /* SVC 中断 */
89 SVC_Handler:
90 ldr r0, =SVC_Handler
91 bx r0
92
93 /* 预取终止中断 */
94 PrefAbort_Handler:
95 ldr r0, =PrefAbort_Handler
96 bx r0
97
98 /* 数据终止中断 */
99 DataAbort_Handler:
100 ldr r0, =DataAbort_Handler
101 bx r0
102
103 /* 未使用的中断 */
104 NotUsed_Handler:
105
106 ldr r0, =NotUsed_Handler
107 bx r0
108
109 /* IRQ 中断!重点!!!!! */
110 IRQ_Handler:
111 push {lr} /* 保存 lr 地址 */
112 push {r0-r3, r12} /* 保存 r0-r3, r12 寄存器 */
113
114 mrs r0, spsr /* 读取 spsr 寄存器 */
115 push {r0} /* 保存 spsr 寄存器 */
116
117 mrc p15, 4, r1, c15, c0, 0 /* 将 CP15 的 C0 内的值到 R1 寄存器中
118 * 参考文档 ARM Cortex-A(armV7)编程手册 V4.0.pdf P49
119 * Cortex-A7 Technical ReferenceManua.pdf P68 P138
120 */
121 add r1, r1, #0X2000 /* GIC 基地址加 0X2000, 得到 CPU 接口端基地址 */
122 ldr r0, [r1, #0XC] /* CPU 接口端基地址加 0X0C 就是 GICC_IAR 寄存器,
123 * GICC_IAR 保存着当前发生中断的中断号,我们要根据
124 * 这个中断号来绝对调用哪个中断服务函数
125 */
126 push {r0, r1} /* 保存 r0,r1 */
127
128 cps #0x13 /* 进入 SVC 模式,允许其他中断再次进去 */
129
130 push {lr} /* 保存 SVC 模式的 lr 寄存器 */
131 ldr r2, =system_irqhandler /* 加载 C 语言中断处理函数到 r2 寄存器中*/
132 blx r2 /* 运行 C 语言中断处理函数,带有一个参数 */
133
134 pop {lr} /* 执行完 C 语言中断服务函数, lr 出栈 */
135 cps #0x12 /* 进入 IRQ 模式 */
136 pop {r0, r1}
137 str r0, [r1, #0X10] /* 中断执行完成,写 EOIR */
138
139 pop {r0}
140 msr spsr_cxsf, r0 /* 恢复 spsr */
141
142 pop {r0-r3, r12} /* r0-r3,r12 出栈 */
143 pop {lr} /* lr 出栈 */
144 subs pc, lr, #4 /* 将 lr-4 赋给 pc */
145
146 /* FIQ 中断 */
147 FIQ_Handler:
148
149 ldr r0, =FIQ_Handler
150 bx r0
第 17 到 81 行是复位中断服务函数 Reset_Handler, 第 19 行先调用指令“cpsid i”关闭 IRQ,第 24 到 30 行是关闭 I/D Cache、 MMU、对齐检测和分支预测。第 33 行到 42 行是汇编版本的中断向量表重映射。第 50 到 68 行是设置不同模式下的 sp 指针,分别设置 IRQ 模式、 SYS 模式和 SVC 模式的栈指针,每种模式的栈大小都是 2MB。第 70 行调用指令“cpsie i”重新打开IRQ 中断,第 72 到 79 行是操作 CPSR 寄存器来打开 IRQ 中断。当初始化工作都完成以后就可以进入到 main 函数了,第 81 行就是跳转到 main 函数。第 110 到 144 行是中断服务函数 IRQ_Handler,这个是本章的重点,因为所有的外部中断最终都会触发 IRQ 中断, 所以 IRQ 中断服务函数主要的工作就是区分当前发生的什么中断(中断 ID)?然后针对不同的外部中断做出不同的处理。第 111 到 115 行是保存现场,第 117 到 122行是获取当前中断号,中断号被保存到了 r0 寄存器中。