这篇文章我们来讲解异常与中断的概念,理解发生异常,和需要用到中断时,我们需要做哪些工作。
预备知识:ARM学习-ARM寻址方式与异常中断
完整嵌入式Linux教程:嵌入式Linux教程—裸机、应用、驱动完整教程目录
1. 异常
对于ARM CPU有7种模式:
1 usr :用户模式:正常程序执行模式
2 sys :系统模式:用于运行特权级操作系统任务
5种异常模式
1 未定义指令中止模式(Undefined):执行未定义指令会进入该模式
2 特权模式(Supervisor):复位和软中断指令会进入该模式;
3 数据访问中止模式(Abort):当存储异常时会进入该模式
a 指令预取终止(读写某条错误的指令导致终止运行)
b 数据访问终止 (读写某个地址,这个过程出错)都会进入终止模式
4 外部中断模式(IRQ):低优先级中断产生会进入该模式,用于普通的中断处理
5 快速中断模式(FIQ):高优先级的中断产生会进入该种模式,用于高速通道传输
Linux应用程序工作在usr用户模式。
每当程序的正常流程必须临时停止时,就会出现异常,例如从外围设备处理中断。
在处理异常之前,必须保留当前的处理器状态,以便在处理程序例程结束时恢复原始程序。
有可能同时出现几个例外情况。如果发生这种情况,它们将按固定顺序处理。
在五种异常模式中每个模式都有自己专属的R13 R14寄存器,R13用作SP(栈) R14用作LR(返回地址)
LR是用来保存发生异常时的指令的地址
发生异常时CPU会做什么事情:
1把下一条指令的地址保存在LR寄存器里(某种异常模式的LR等于被中断的下一条指令的地址)
它有可能是PC + 4有可能是PC + 8,到底是那种取决于不同的情况
2 把CPSR保存在SPSR里面(某一种异常模式下SPSR里面的值等于CPSR)
3 修改CPSR的模式为进入异常模式(修改CPSR的M4 ~ M0进入异常模式)
4 将PC设置为指向异常向量表中的相关指令,跳到向量表
异常向量表是一段特定内存地址空间,每种ARM异常对应一个字长空间(4Bytes),正好是一条32位指令长度,当异常发生时,CPU强制将PC的值设置为当前异常对应的固定内存地址。
跳入异常向量表操作是异常发生时,硬件自动完成的,剩下的异常处理任务完全交给了程序员。
由上表可知,异常向量是一个固定的内存地址,我们可以通过向该地址处写一条跳转指令,让它跳向我们自己定义的异常处理程序的入口,就可以完成异常处理了。
退出异常时CPU会做什么事情:
1 让LR减去(或者不减)某个值,然后赋值给PC(PC = 某个异常LR寄存器减去 offset)
减去什么值呢?
也就是我们怎么返回去继续执行原来的程序,根据下面这个表来取值
如果发生的是SWI可以把 R14_svc复制给PC
如果发生的是IRQ可以把R14_irq的值减去4赋值给PC
2 利用SPSR把CPSR的值恢复
3 清中断(如果是中断的话,对于其他异常不用设置)
2. 中断
中断属于异常的一部分
中断可以简单的理解为,暂停正在做的事,去做另外一件事。比如你在打游戏,你妈喊你吃饭,你就被中断了,吃完回来再打。
中断控制器可以发信号给CPU告诉它发生了那些紧急情况
中断源有按键、定时器、有其它的(比如网络数据、指令不对、数据访问有问题、reset信号)
这些信号都可以发送信号给中断控制器,再由中断控制器发送信号给CPU表明有这些中断产生了,这些成为中断(属于一种异常)
重点在于保存现场以及恢复现场
中断处理过程:
a 保存现场(各种寄存器)
b 处理异常(中断属于一种异常)
c 恢复现场
总结一下完整的对异常(中断)处理过程
1. 初始化:
a 设置中断源,让它可以产生中断
b 设置中断控制器(可以屏蔽某个中断,优先级)
c 设置CPU总开关,(使能中断)
2. 执行其他程序(正常程序)
3. 产生中断。例如:按下按键—>中断控制器—>CPU
4. cpu每执行完一条指令都会检查有无中断/异常产生
5. 发现有中断/异常产生,开始处理。
对于不同的异常,跳去不同的地址执行程序。这地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。
3-5都是硬件强制做的。
6. 软件做的:
a 保存现场(各种寄存器)
b 处理异常(中断):
分辨中断源
再调用不同的处理函数
c 恢复现场
3. und异常模示程序示例
未定义指令异常,就是cpu无法识别指令时进行的异常处理。
我们要进入这个异常可手工定义一条无法被识别的指令
und_code:
.word 0xdeadc0de /* undefine instruction */
首先把向量表设置好
_start:
b reset
ldr PC,=Undefined_Handler
然后写未定义异常处理函数,我们需要做什么?
1.因为已经进入了异常模式,异常模式有自己的栈指针,所以要设置栈指针sp
2.在真正进入异常处理子函数时,r0-r12,lr都有可能被改变,所以我们要保存这些寄存器
3.跳转到异常处理子程序,
4.恢复r0-r12,lr
5.将lr中保存的地址赋值给pc,就能跳回进入异常时的地址
6.通过进入异常时,硬件自动备份的SPSR,恢复CPSR状态
4-6步可通过一条指令完成
ldmia sp!, {r0-r12, pc}^
完整的异常处理程序
Undefined_Handler:
ldr sp, =0x34000000
stmdb sp!,{r0-r12,lr} ;
ldr r0,=und_string
bl puts
ldmia sp!, {r0-r12, pc}^
puts是我定义的一个c函数,作用是通过串口输出
ldr r0,=und_string
通过上述语句,给puts传入参数
und_string:
.string "undefined instruction exception"
当成功时,电脑就会收到开发板发送的:"undefined instruction exception"
完整测试代码:异常与中断_未定义指令异常
4. 按键中断
目标效果:通过uart不断输出hello,按键中断触发led灯亮。
中断属于异常,所以和异常是相似的,但是也有一些不同的地方
1. 初始化:
a 设置中断源,让它可以产生中断
b 设置中断控制器(可以屏蔽某个中断,优先级)
c 设置CPU总开关,(使能中断)
2. 处理中断时要分辨中断源;
3. 处理完,要清除中断。
先看初始化
1. 设置CPU总开关:CPSR中有I位,是IRQ中断的总开关,bit7就是I,需要清0
/* 复位之后, cpu处于svc模式
* 现在, 切换到usr模式
*/
mrs r0, cpsr /* 读出cpsr */
bic r0, r0, #0xf /* 修改M4-M0为0b10000, 进入usr模式 */
bic r0, r0, #(1<<7) /* 清除I位, 使能中断 */
msr cpsr, r0
2.初始化中断控制器
INTMAK这个寄存器有32位,每个位都与一个中断源相关。如果特定位设置为1,则CPU不处理来自相应中断源的中断请求(注意,即使在这种情况下,SRCPND寄存器的相应位设置为1)。如果掩码位为0,则可以处理中断请求。
void interrupt_init(void)
{
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}
3. 设置中断源:按键
根据开发板原理图,找到按键对应的IO:GPF0、GPF2、GPG3、GPG11
/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{
/* 配置GPIO为中断引脚 */
GPFCON &= ~((3<<0) | (3<<4));
GPFCON |= ((2<<0) | (2<<4)); /* S2,S3被配置为中断引脚 */
GPGCON &= ~((3<<6) | (3<<22));
GPGCON |= ((2<<6) | (2<<22)); /* S4,S5被配置为中断引脚 */
/* 设置中断触发方式: 双边沿触发 */
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12); /* S5 */
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
}
注意EINTMASK中断屏蔽寄存器,相关位为1,则相应中断会被屏蔽。
4. 中断处理
分辨中断源:INTOFFSET和EINTPEND
两者都可以用来分辨中断源的,INTPND 则是中断信号在中断处理模块里经历的最后一个寄存器。它的每个位对应一个中断请求,若该位被置1,则表示相应的中断请求被触发,INTOFFSET寄存器的功能则很简单,它的作用只是用于表明哪个中断正在被处理。
注意:完成中断处理时,INTOFFSET在完成下面说的SRCPND、INTPND清除后自动清除,
而EINTPEND需要手动清除,并且是通过写1清除。
void key_eint_irq(int irq)
{
unsigned int val = EINTPEND;
unsigned int val1 = GPFDAT;
unsigned int val2 = GPGDAT;
if (irq == 0) /* eint0 : s2 控制 D12 */
{
if (val1 & (1<<0)) /* s2 --> gpf6 */
{
/* 松开 */
GPFDAT |= (1<<6);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<6);
}
}
else if (irq == 2) /* eint2 : s3 控制 D11 */
{
if (val1 & (1<<2)) /* s3 --> gpf5 */
{
/* 松开 */
GPFDAT |= (1<<5);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<5);
}
}
else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
{
if (val & (1<<11)) /* eint11 */
{
if (val2 & (1<<3)) /* s4 --> gpf4 */
{
/* 松开 */
GPFDAT |= (1<<4);
}
else
{
/* 按下 */
GPFDAT &= ~(1<<4);
}
}
else if (val & (1<<19)) /* eint19 */
{
if (val2 & (1<<11))
{
/* 松开 */
/* 熄灭所有LED */
GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
}
else
{
/* 按下: 点亮所有LED */
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
}
}
EINTPEND = val;
}
SRCPND和INTPND
SRCPND是中断源引脚寄存器,中断触发时对应位自动置1,但我们知道在同一时刻内系统可以触发若干个中断,只要中断被触发了,SRCPND的相应位便被置1,也就是说SRCPND在同一时刻可以有若干位同时被置1。
然而INTPND则不同,他在某一时刻只能有1个位被置1,INTPND 某个位被置1(该位对应的中断在所有已触发的中断里具有最高优先级且该中断没有被屏蔽),则表示CPU即将或已经在对该位相应的中断进行处理。于是我们可以有一个总结:SRCPND说明了有什么中断被触发了,INTPND说明了CPU即将或已经在对某一个中断进行处理。
特别注意:每当某一个中断被处理完之后,我们必须手动地把SRCPND/SUBSRCPND , INTPND三个寄存器中与该中断相应的位由1设置为0。
注意: 通过手册,我们知道这两个寄存器都是通过写1清零。
void handle_irq_c(void)
{
/* 分辨中断源 */
int bit = INTOFFSET;
/* 调用对应的处理函数 */
if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,eint8_23 */
{
key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
}
/* 清中断 : 从源头开始清 */
SRCPND = (1<<bit);
INTPND = (1<<bit);
}
完整代码:异常与中断_按键中断