1. 异常与中断的概念引入与处理流程
上图解释了何为中断何为异常,其中中断也是属于一种异常。
引申拓展为ARM对异常(中断)的处理过程:
1)初始化
(1)设置中断源,让他可以产生中断。如某个按键可以产生中断的话,我们可以设置他的gpio引脚为中断引脚
(2)设置中断控制器(屏蔽,优先级),屏蔽的话就是在要使用的时候打开,优先级是同时有多个中断,我们先去处理那一个
(3)设置CPU总开关(使能中断)
2)执行正常程序
3)有中断产生。如按键按下---->中断控制器----->发信号给CPU
注意:CPU每次执行完一条指令,都会去判断有无中断产生,有无异常产生
当发现有中断产生时,就会去进行相应的处理。对于不同的异常,会跳去不同的地址执行程序,在这些地址上,只是一条跳转指令,跳去执行某个函数。此处不同的地址即是异常向量,通常地址都排在一起。
4)这些函数进行中断处理:
(1)保存现场(设置相应寄存器)
(2)进行处理:先分辨中断源,再调用相应的中断处理函数
(3)恢复现场
查看这些向量表,打开任意一个uboot的汇编代码,因为uboot是裸板程序的集大成者,如下:
第一个reset:是发生复位异常,比如按下复位键,回跳到这执行
第二个:指令无法辨别,或者指令不存在
第三个:软中断
…
注意:当发生中断时,CPU强制跳到24(即0x18)这个地方执行(硬件)
我们可以在0x18的地方放一条指令:ldr pc , _irq(一条伪指令)
于是CPU就会跳去执行_irq的代码,即:
保护现场、调用中断处理函数、恢复现场
总结:中断程序如何被调用?
硬件决定,发生中断时,CPU强制跳到相应的异常向量,再通过跳转指令执行其他函数。函数内部处理中断这是由软件做的。
处理过程:
- 保存现场
- 调用函数:分辩中断源、调用对应函数
- 恢复现场
————————————————
版权声明:本文为CSDN博主「今天天气眞好」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_51118175/article/details/116293643
===================================================================================================
2.CPU的模式、状态与寄存器
2.1 CPU的7种模式(mode)
1.用户模式(usr):不可直接进入其他模式。显示是给上层应用程序使用的,限制应用程序权限,防止破坏整个系统
2.系统模式(sys)
3.异常模式
(1)未定义指令模式(und):CPU执行碰到不认识的指令会进入该模式
(2)管理模式(svc):管理模式是CPU上电后默认模式,因此在该模式下主要用来做系统的初始化,软中断处理也在该模式下,当用户模式下的用户程序请求使用硬件资源时通过软件中断进入该模式。
(3)中止模式(abt):也分为指令预取中止和数据访问中止
指令预取中止:CPU执行程序时会去读指令,CPU是以流水线的方式来进行操作的,在执行当前指令的时候,已经在解析下一条指令,在读取第三条指令,此处在读取第三条指令的时候就是预取,有可能会出错
数据访问中止:读写某个地址的过程中可能会出错
(4)中断模式(IRQ)
(5)快中断模式(FIQ):快速处理,可以将某个中断配置为快中断(在Linux下一般不会用到)
特权模式:可以在除了用户模式以外的6种模式之间随意切换
通过编程操作CPSR寄存器直接会进入其他模式
2.2 CPU的两种状态 : ARM state 和 Thumb stste
ARM state:ARM指令集,每个指令4byte
Thumb stste:Thumb指令集,每个指令2byte
注意:Thumb指令集可以减少程序的存储空间,但是在ARM下的nor flash 和nand flash都很大,没有必要节省这么一点空间
例如:
mov r0 , r1指令:
对于ARM 是4byte的机器码
对于Thumb是2byte的机器码
2.3 各种模式下的寄存器
以下是各个模式下,能够访问的寄存器。
注:上图中有标黑色三角的寄存器,表示这是这个模式下专属的寄存器 (专属的寄存器称为备份寄存器 banked register)。其中,除了sys和usr模式,其他的模式至少都有两个专属的寄存器,r13和r14。r13用作sp栈指针,r14用作LR(link register返回地址,保存发生异常时的指令地址)。
Q:那么为何快中断模式(fiq)有这么多专属寄存器?
A: 因为需要快速处理。
回顾一下中断处理的过程:
1.保存:保存被中断模式下的寄存器
例如在用户模式下,先进行保存,再去处理异常,保存的时候也不用全部保存,因为模式下的寄存器也会不同,如在FIQ模式下的R8-R14都是专属寄存器,就可以只用保存R0-R7即可,加快处理的速度2.处理
3.恢复
此外, 还有程序状态寄存器 :
CPSR/SPSR:
CPSR:当前程序状态寄存器
SPSR:保存被中断模式下的CPSR(备份寄存器)
fig2-6为程序状态寄存器的格式:
- M0~M4:模式位,可以查表Table2-1,看CPU处于什么模式。
- T:状态位,表明是ARM还是Thumb状态
- F:FIQ禁止,为1时表示禁止,禁止FIQ中断
- I:IRQ禁止,为1时表示禁止,禁止IRQ中断
- 27~8位表示保留位
- N,Z,C,V为状态位
如:
cmp r0 , r1:影响Z位,if r0 == r1,则Z = 1
beq xxx:影响Z位,if Z == 1,则跳转
2.4 异常处理与异常返回
下面来看异常处理的流程以及从异常返回的流程
异常处理的流程:
1) 把下一条指令的地址保存在LR寄存器中
异常模式下的LR寄存器LR_(哪一种异常模式) = 被中断的下一条指令的地址(PC+4 / PC+8),取决于不同的情况
(2)SPSR_异常 = CPSR
(3)修改CPSR的M4~M0,进入中断模式
(4)跳向异常向量
(以上都是硬件完成的)
处理异常:
(1)获得出错时候的堆栈
(2)保存用户模式寄存器
(3)跳转到对应的中断处理函数
(4)恢复现场,恢复寄存器的值
从异常返回:
(1)PC = LR_异常 - offset
(2)CPSR = SPSR_异常(之前保存的)
(3)清中断
3. CPU的Und模式示例
程序从异常向量,进入Und模式之后,要进行以下操作:
- 设置sp_und ,即设置Und模式下的栈,之前我们设置的栈是在(svc模式管理模式)下的栈,当发生异常进入Und模式,所以要另外设置该模式下的栈。函数的实现都需要依靠栈。所以要在跳转到函数前先分配好栈。
- 保护现场:把R0~R12的值压栈保存起来。
用到的指令为:
含义为,sp = sp - 4,先压lr,(即将lr中的内容放入sp所指的内存地址)。sp = sp - 4,再压r12。sp = sp - 4,再压r11,....sp = sp - 4,最后压r0。stmdb sp!,{r0-r12, lr} //STMDB(decrease before地址先减而后完成操作)
- 处理Und异常:把状态寄存器的值送到通用寄存器
- 恢复现场:
ldmia sp!, {r0-r12, pc}^ //ldmia(increase after先读后(地址)增)
含义为,r0=sp当前地址的内容,sp=sp+4, r1=sp当前地址的内容,sp=sp+4,。。。,最后pc=sp当前地址,sp=sp+4。
另外,^ 表示会把spsr的值恢复到cpsr里
编程中有一个细节需要注意, ldr pc, =do_und ,ldr语句实际是一条伪指令,并不是直接就把do_und的地址给到pc,而是在某个地址存下do_und的地址,执行这条命令时,再到该地址取出里面的值给PC。 编译器通常会这个存do_und的地址都会在汇编文件的末尾,因此当汇编文件过于大了的时候,超过了4K的空间,那么如果是Nand启动的话,程序都会被复制到SDRAM中,这会导致do_und的地址在4K之外, 使得其无法被访问到。
_start: b reset /* vector 0 : reset */ ldr pc, = do_und /* vector 4 : reset */ do_und: ……
所以改进的方法是将do_und的地址人为地放在前面。
und_addr:
.word do_und
意为und_addr的地址所在的内存中存放do_und的地址。
然后,不用伪指令Ldr了,直接读und_addr中的值进pc。这样也就做到了,人为地将do_und的地址放在了4K内的空间。
_start: b reset /* vector 0 : reset */ ldr pc, und_addr /* vector 4 : reset */ /*非伪指令!!!*/ und_addr: .word do_und do_und: ……
在完成重定位工作之后,由于之后的函数可能在4K之外,因此在重定位之后,用强制跳转,跳转到sdram标签处,然后从存在SDRAM中的代码sdram标签处继续运行。
bl sdram_init bl copy2sdram /* src,dest,len */ bl clean_bss /* start, end*/ ldr pc, =sdram sdram:
最后如果使用了 .string " ..." ,字符串的长度会影响字节对齐,导致后面的程序无法执行,所以要加 .align 4 ,做4字节的对齐。
4. Swi异常模式程序示例
按照例子中清除并修改cpsr的模式bit位,设置为用户模式,当有软件中断swi时,通过异常向量0x00000008,到某个地址处理swi。
详细见转载 https://blog.csdn.net/qq_51118175/article/details/117511490
5. 按键中断程序示例
按键可以产生中断,那么中断的处理流程是?
1.初始化
(1)设置中断,让他能够发出中断信号
- 配置GPIO为中断引脚
- 设置中断触发方式: 双边沿触发
- 将外部中断屏蔽寄存器位清零,使能中断
(2)设置中断控制器,让他能够产生中断发给CPU
如上图所示,对于某些中断源(上支线)需要经过几个中断源状态寄存器才能够到达中断控制器,而另外一些(下支线)经过的中断源状态寄存器要少一些。对于需要经过的中断源状态寄存器,我们可以通过查看它们,来分辨哪个中断产生了,并清除对应位,为迎接下一次中断。
/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
* bit0-eint0
* bit2-eint2
* bit5-eint8_23
*/
可以看到,eint0占用bit10,eint2占用bit2,eint8~23共用一个bit,因此还需要另外判断是8-23中到底是哪个口产生了外部中断。
/* INTMSK 用来屏蔽中断, 1-masked,即使外部中断到我这里,我也不会发给CPU
* bit0-eint0
* bit2-eint2
* bit5-eint8_23
*/
/* 多个中断最后通过优先级之后只会有一个通知CPU,可以通过读INTPND来看哪一个优先级最高
* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
* bit0-eint0
* bit2-eint2
* bit5-eint8_23
*/
/* INTOFFSET : 用来显示INTPND中哪一位被设置为1
* 这个位不需要清除,因为当我们清除SRCPND和INTPND时候,这个位会自动清除
*/
(3)设置CPU,可以发现cpsr的bit7 I ,他是中断的总开关, 通常在初始化文件中设置。
即bit7中断的总开关,需要将它清0,当bit7 == 1时,CPU无法相应任何中断
2.进行处理,要分辨中断源
通过读取INTOFFSET寄存器里的值可以知道外部中断源。读取了源以后进行处理。
3.处理完清中断
中断源——>中断控制器——>CPU 这三个环节,要从源头,中断源开始,一步一步清中断,任何一个环节没有清,下次中断发生的时候,CPU都无法正确响应。因此要清每一个中断源状态寄存器。
6. 定时器中断程序
6.1定时器的工作原理
大致步骤:
1.每来一个clk,TCNTn都要-1
2.TCNTn继续-1,当TCNTn == 0时,可以产生中断,PWM引脚再次反转
3.TCNTn == 0时,可以自动加载初值
那么怎么使用定时器timer呢
1.设置时钟
2.设置初值
3.加载初值,启动timer
4.设置为自动加载
5.中断相关
我们可以发现每添加一个中断,都需要修改interrupt.c中初始化函数,清零相应的中断屏蔽寄存器,并且还需要修改中断处理函数中的函数调用,这样显然太麻烦了,可不可以保持原有的
interrupt.c
文件不变?? 我们需要用到函数指针数组。================================================================================================
先复习一下指针的相关概念:
char * fump(int); //返回字符指针的函数; char (* frump)(int); //指向函数的指针,该函数的返回类型为char; char (* flump[3])(int); //内含3个指针的数组,每个指针都指向返回类型为char的函数,该函数的参数是一个int类型; /* 分辨二者的区别, 一个是指针,一个是类型 */ char (* frump)(int); //frump指向函数的指针,该函数的返回类型为char; typedef char (* frump)(int); //定义了frump类型,且这种类型为指向函数的指针,函数的类型是带int类型参数,返回类型是char的函数;
================================================================================================
自定义一个指向函数的指针类型irq_func,注意要记得加括号(*__),这样才是指针。说白了就是用(*pointer) 代替了function_name
typedef void(*irq_func)(int)
之后定义irq_func 类型的数组,即数组中的每一个元素都是指向函数的指针。所以可以把中断函数的地址放入数组中来,通过发生中断时产生的中断号(INTOFFSET), 来判断发生了什么样的中断,然后据此从数组中调用相对应的函数。
所以为了生成中断号与函数一一对应的关系,需要一个注册函数,同时在里面清零中断屏蔽器,使能中断。void register_irq (int irq, irq_func fp) { irq_array[irq] = fp; INTMASK &= ~(1 << irq) }
使用注册函数 “注册”过以后,就直接可以通过函数指针数组调用函数。
irq_array[bit](bit);