使用生活实例引入中断
假设有个大房间里面有小房间,婴儿正在睡觉,他的妈妈在外面看书。
问:这个母亲怎么才能知道这个小孩醒?
- 过一会打开一次房门,看婴儿是否睡醒,然后接着看书
- 一直等到婴儿发出声音以后再过去查看,期间都在读书
第一种方法叫做查询方式:
- 优点:简单
- 缺点: 累
while(1)
{
1 read book(读书)
2 open door(开门)
if(小孩还在睡)
return(继续读书)
else
照顾小孩
}
第二种方法叫中断方式:
- 优点:不累
- 缺点:复杂
如何写程序:
while(1)
{
read book
}
中断服务程序() //核心问题:如何被调用?
{
处理照顾小孩
}
我们还是看看母亲被小孩哭声打断如何照顾小孩?
母亲的处理过程
- 平时看书
- 发生了各种声音,如何处理这些声音
- 有远处的猫叫(听而不闻,忽略)
- 门铃声有快递(开门收快递)
- 小孩哭声(打开房门,照顾小孩)
- 母亲的处理
- 只会处理门铃声和小孩哭声
- 先在书中放入书签,合上书(保存现场)
- 去处理 (调用对应的中断服务程序)
- 继续看书(恢复现场)
- 只会处理门铃声和小孩哭声
不同情况,不同处理
- 对于门铃:开门取快件
- 对于哭声:照顾小孩
ARM系统中异常与中断处理流程
所有的中断源(按键、定时器等),它们发出的中断汇聚到中断控制器,再由中断控制器发送信号给CPU,告诉它发生了哪些紧急情况。
除了这些中断,还有什么可以打断CPU的运行:
- 指令不对
- 数据访问有问题
- Reset信号
- …
这些都可以打断CPU,这些被称为异常,中断属于异常的一种。
ARM系统中如何处理异常与中断?重点在于保存现场以及恢复现场。
- 保存现场(各种寄存器)
- 处理异常(中断属于一种异常)
- 恢复现场
细化一下,ARM系统中如何使用异常(中断)?
- 初始化
设置中断源,让它可以产生中断
设置中断控制器(可以屏蔽某个中断,优先级)
设置CPU总开关,使能中断 - 执行其它程序:正常程序
- 产生中断,举例:按下按键->中断控制器->CPU
- CPU每执行完一条指令都会检查有无中断/异常发生
- 发现有中断/异常产生,开始处理:
- 保存现场
- 分辨异常/中断,调用对于异常/中断的处理函数
- 恢复现场
不同的芯片,不同的架构,在这方面的处理有差别:
不同的芯片,不同的架构,在这方面的处理稍有差别:
- 保存/恢复现场:cortex M3/M4是硬件实现的,cortex A7是软件实现的
- CPU中止当前执行,跳转去执行处理异常的代码:也有差异
- cortex M3/M4在向量表上放置的是函数地址
- cortex A7在向量表上放置的是跳转指令
ARM架构中异常与中断的处理
处理流程是一样的
每执行完一条指令都会检查有无中断/异常产生
发现有中断/异常产生,开始处理:
- 保存现场
- 分辨异常/中断,调用对应的异常/中断处理函数
- 恢复现场
不同的芯片,不同的架构在这方面处理稍有差别:
- CPU中止当前执行,跳转去执行处理异常的代码:也有差异
cortex M3/M4在向量表上放置的是函数地址
cortex A7在向量表上放置的是跳转指令 - 保存/恢复现场:cortex M3/M4是硬件实现的,cortex A7是软件实现的
Cortex M3/M4的向量表
要想理解这个处理流程,需要从向量表说起。
向量,在数学定义里是有方向的量,在程序里可以认为向量就是一个数组,里面有多个项。
在ARM架构里,对于异常/中断,它们的处理入口会整齐地排放在一起。
M3/M4的向量表中,放置的是具体异常/中断的处理函数的地址。
比如发生Reset异常时,CPU就会从向量表里找到第1项,得到Reset_Handler函数的地址,跳转去执行。
比如发生EXTI Line0中断时,CPU就会从向量表里找到第22项,得到EXTI0_IRQHandler函数的地址,跳转去执行。
跳转之前,硬件会保存现场。
函数执行完毕,返回之后,硬件会恢复现场。
M3/M4的异常/中断处理流程
发生异常/中断时,硬件上实现了这些事情:
- 保存现场:把被中断瞬间的寄存器的值保存进栈里
- 根据异常/中断号,从向量表中得到函数地址,跳转过去执行
- 函数执行完后,从栈中恢复现场
保存现场、分辨异常/中断、跳转执行,都是硬件实现的。
我们只需要在向量表中,把处理函数的地址填进去就可以了。
硬件承包了大部分工作。
M3/M4的向量表中,存放的是函数地址。
Cortex A7 向量表
实际上,以前的S3C2440属于ARM9处理器,它的异常/中断处理流程与Cortex A7一样。
A7的向量表中,放置的是某类异常的跳转指令。
比如发生Reset异常时,CPU就会从向量表找到第0项,得到b reset指令,执行后就跳转到reset函数。
比如发生任何的中断时,CPU就会从向量表里找到第6项,得到ldr pc,_irq指令,执行后就跳转到_irq函数。
跳转之前,硬件只会保存CPSR寄存器。
跳转之后,软件要保存现场。
函数执行完毕,返回之前,软件恢复现场。
_start:
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
A7的异常/中断处理流程
- CPU切换到对应的异常模式,比如IRQ模式、未定义模式、SVC模式
- 保存被中断时的CPSR到SPSR
CPSR:当前程序状态寄存器
SPSR:保存的程序状态寄存器 - 跳到这个异常的入口地址去,执行指令,这通常是一条跳转指令
软件要做的事情就比较多了:
- 保存现场
- 分辨异常/中断
- 调用对应的处理函数
- 恢复现场
A7的向量表中,存放的是跳转指令
中断/异常通用处理流程
CPU每执行完一条指令都会检查有无中断/异常产生,发现有中断/异常产生,开始处理:
- 保存现场
- 分辨异常/中断,调用对应的异常/中断处理函数
- 恢复现场
对于不同的处理器,具体处理工作差别:
- 保存现场:cortex M3/M4里是硬件完成,cortex A7等是软件实现
- 分辨异常/中断:cortex M3/M4里是硬件完成,cortex A7等是软件实现
- 调用处理函数:cortex M3/M4里是硬件来调用,cortex A7等是软件自己去调用
- 恢复现场:cortex M3/M4里是软件触发、硬件实现,cortex A7等是软件实现
不管是硬件还是软件实现,第一步都是保存现场。
为什么要保存现场
任何程序,最终都会转换为机器码,上述C代码可以转换为右边的汇编指令。
对于这4条指令,它们可能随时被异常打断,怎么保存异常处理完后,被打断的程序还能正常运行?
- 这4条指令涉及R0、R1寄存器,程序被打断时、恢复运行时,R0、R1要保持不变。
- 执行完第3条指令,比较结果保存在程序状态寄存器里,程序被打断时、恢复运行时,程序状态寄存器保持不变。
- 这4条指令,读取a、b内容,程序被打断时,恢复运行时,a、b内容保持不变。
内存保持不变,这很容易实现,程序不越界就可以。
所以,关键在于R0、R1、程序状态寄存器要保持不变(当然不止这些寄存器):
- 在处理异常前,把这些寄存器保存在栈中,这称为保存现场
- 在处理完异常后,从栈中恢复这些寄存器,这称为恢复现场
保存现场
在arm中有个ATPCS(ARM-THUMB procedure call standard)(ARM-Thumb过程调用标准)。
约定R0-R15寄存器的用途:
- R0-R3:调用者和被调用者之间传参数
- R4-R11:函数可能被使用,所以在函数的入口保存它们,在函数的出口恢复它们、
还有一个程序状态寄存器,对于M3/M4它被称为XPSR,对于A7它被称为CPSR,我们简称为PSR。
R0-R15、PSR,就是所谓的现场。
发生异常/中断后,在处理异常/中断前,需要保存现场,难道需要保存所有这些寄存器吗?
不需要!
在C函数中,可以修改R0-R3、R12、R14(LR)以及PSR。
如果C函数要用到这些寄存器,就要把它们保存到栈里,在函数结束前从栈中恢复它们。
这些寄存器被拆分成2部分:调用者保存的寄存器(R0-R3、R12、LR、PSR)、被调用者保存的寄存器(R4-R11)。
比如函数A调用函数B,函数A应该知道:
- R0-R3是用来传参数给函数B的
- 函数B可以肆意修改R0-R3
- 函数A不要指望函数B帮你保存R0-R3
- 保存R0-R3,是函数A的事情
- 对于LR、PSR也是同样的事情,保存它们是函数A的责任。
对于函数B:
- 我用到R4-R11中的某一个,我都会在函数入口保存、在函数返回前恢复
- 保证在B函数调用前后,函数A看到的R4-R11保持不变
假设函数B就是异常/中断处理函数,函数B本身能保证R4-R11不变,那么保存现场时,只需要保存这些:
- 调用者保存的寄存器R0-R3、R12、LR、PSR
- PC
硬件保存现场
调用C函数
中断处理程序执行完后,它返回LR所指示的位置。
难道把LR设置为被中断的程序的地址就行了吗?
如果只是返回LR所指示的地方,硬件帮我们保存在栈里的寄存器,怎么恢复?
M3/4在调用异常处理函数前,把LR设置为一个特殊的值,转给特殊的值称为EXC_RETURN。
当PC寄存器的值等于EXC_RETURN时,会触发异常返回机制,简单地说,会从栈里恢复R0-R3,R12,LR,PC,PSR等寄存器。
操作模式:M3/4有两个操作模式
- 处理模式:执行中断服务程序等异常处理时,处于处理模式。
- 线程模式:执行普通应用程序代码时,处于线程模式。
M3/M4有多个SP寄存器:SP_process、SP_main
- 有些RTOS在运行用户程序时会使用SP_process,默认使用SP_main。
对于A7
处理器有9中模式:User、Sys、FIQ、IRQ、ABT、SVC、UND、MON、HYP。
上图中深色的寄存器,表示该模式下的"Banked"寄存器,比如SPSR寄存器,在很多模式下都有自己的、单独的寄存器。
比如IRQ模式下访问SPSR时,访问到的是IRQ模式下自己的SPSR irq,别的模式下无法访问SPSR irq。
比较值得关注的是FIQ模式,名为“快中断”,它有很多“Banked”寄存器:R8-R12,SP,LR。
在FIQ模式下,它既然能使用自己的R8-R12,SP,LR,自然不需要去保存被中断的程序的R8-R12,SP,LR寄存器,省去保存这几个寄存器的时间,处理中断时自然就快很多,所以被称为FIQ。
从上图可以看出,几乎每个模式下都有自己的SP寄存器,意味着这些模式下有自己的栈。
当发生异常时,以IRQ为例:
- CPU会自动切换进入对应的模式,比如进入IRQ模式。
- 并且会把被中断时的CPSR保存到SPSR_irq里
所以发生异常/中断时,在保存现场时,只需要保存 - 调用者保存的寄存器(R0-R3、R12、LR)
- PC