ARM中断基础知识
一、ARM内核工作模式
因为中断会设计到ARM内核工作模式的切换,所以先简要介绍一下各个模式:ARM模式的切换要设计到寄存器 CPSR,下面是各个位表示的含义, CPSR[4:0]是工作模式切换控制位。T=0时是ARM指令模式,T=1时是Thumb指令模式。F=0时是允许FIQ, F=1是禁止FIQI=0时是允许IRQ, I=1是禁止IRQ
在开发板刚刚启动起来的时候首先得关闭所有中断,等把开发板的硬件初始化,各种参数设置好之后就可以打开中断了。
CPSR有4个8位区域:标志域(F)、状态域(S)、扩展域(X)、控制域(C)
C 控制域屏蔽字节( CPSR[7:0] )
X 扩展域屏蔽字节( CPSR[15:8] )
S 状态域屏蔽字节( CPSR[23:16] )
F 标志域屏蔽字节( CPSR[31:24] )
常用于MRS或MSR指令,用于cpsr中的值转移到寄存器或把寄存器的内容加载到cpsr中.
如: MSR CPSR_c,#0xd3
ARM 内核工作模式的切换是要自己手动切换的, 设置CPSR 寄存器低5位进行切换(第五位始终是1)
cpsr[4:0] | 处理器模式 | 英文表示 |
1,0000 | 用户模式 | usr |
1,0001 | 快速中断模式 | fiq |
1,0010 | 中断模式 | irq |
1,0011 | 管理模式 | svc |
1,0111 | 中止模式 | abt |
1,1011 | 未定义 | und |
1,1111 | 系统模式 | sys |
CPU刚刚 复位后的模式是管理模式(SVC),如果我们写纯裸机代码(不使用uboot),刚开始是在stepping stone中运行的,我们在这个里面得完成代码的一些硬件的初始化,比如时钟初始化,sdram初始化,nandflash初始化(从nandflash启动)等等,然后把程序从nandflash中拷贝到sdram中运行,在这段启动代码中我们免不了要初始化各种堆栈,而且是各个模式下的堆栈最好都提前设置好,这里就要考虑各种因素了。
比如我们是在OK6410平台上面测试,当复位或者开机后处于8K 大小的stepping stone中,此时我们设置堆栈是SVC下的堆栈,如果我们要设置其他模式下的堆栈的话我们就必须手动的设置CPSR下的模式位,来跳转到各个模式下设定好对应的堆栈。因为以后是要在sdram中运行了,所以这些堆栈应当设定为sdram中的地址。OK6410中的sdram是256M的,所以我们可以从顶部开始设置,比如说切换到irq模式,把堆栈设置为SP = 0x60000000 (因为S3C6410中内存起始地址是从0x50000000开始的,加上256M,即0x10000000,得到栈顶为0x60000000),然后切换到fiq模式下,把堆栈设置为SP = 0x5f000000,一次类推。但是要注意的是,我们还在stepping stone中运行的时候SVC的堆栈还是得保持在片内内存的顶部即SP = 8*1024,直到要跳转到sdram中运行的时候了才把SVC的堆栈设置到sdram中去。
有些人喜欢刚开始只设置SVC的堆栈(因为刚开启的时候就是这个模式,所以必须设置一下),而不设置别的模式的堆栈,等到中断发生了之后,在中断函数中设置堆栈的地址。我觉得这样有些不妥,提前都设置好,以后中断进来后就省去了每次设置堆栈的步骤,提高了效率。
中断向量表:
以下代码就是切换CPU工作模式的示例
;********** Begin init stact ***********/
;在6种模式下切换并设置堆栈指针
MRS R0,CPSR ;把CPSR读取到R0BIC R0,#0x1f ;低5位清零LDR R1,=MODE_Fiq ;设置R1 为0b10001,跳转到fiq模式ORR R0,R0,R1 ;R0和R1相或,设置低5位MSR CPSR_c,R0 ;把R0的值重新赋值到CPSRLDR SP,=Stact_Fiq ;设置fiq的栈指针BIC R0,#0x1f ;低5位清零LDR R1,=MODE_Irq ;跳转Irq模式ORR R0,R0,R1MSR CPSR_c,R0LDR SP,=Stact_Irq ;设置irq的栈指针BIC R0,#0x1fLDR R1,=MODE_Svc ;跳转到svc模式ORR R0,R0,R1MSR CPSR_c,R0LDR SP,=Stact_Svc ;设置svc的栈指针BIC R0,#0x1fLDR R1,=MODE_Abort ;跳转到abort模式ORR R0,R0,R1MSR CPSR_c,R0LDR SP,=Stact_Abort ;设置abort的栈指针BIC R0,#0x1fLDR R1,=MODE_Undef ;跳转到undef模式ORR R0,R0,R1MSR CPSR_c,R0LDR SP,=Stact_Undef ;设置undef栈指针BIC R0,#0x1fLDR R1,=MODE_Sys ;跳转到sys模式ORR R0,R0,R1MSR CPSR_c,R0LDR SP,=Stact_Sys ;设置sys栈指针
与S3C2440相比,S3C6410增加中断向量控制器,这样在S3C2440里需要用软件来跳转的中断处理机制,在S3C6410中可以完全由硬件来跳转。只要把ISR(中断处理函数)地址存在连续向量寄存器空间,而不必像在S3C2440自行分配空间进行管理。换句话说,在S3C2440下是由cpu触发IRQ/FIQ异常,由异常处理函数里再查找相关中断寄存器来跳到指定的ISR,而可以全部由S3C6410的VIC硬件来自动处理。这样就大大简化了中断处理编程。
另外一变化是S3C6410外部中断加入了滤波电路,这样原来需要软件去毛刺的地方均可以采用硬件来进行滤波了,这样大大简化了外部中断处理。
S3C6410具有187个多功能IO端口,其中有127个用于外部中断。这127个引脚可以分为10组:
外部中断分组 | 对应GPIO |
External interrupt Group 0 | GPN0~GPN15 GPL8~GPL14 GPM0~GPM4 (16+7+5=28,所以EINT0PEND有28bit来识别这28个中断) |
External interrupt Group 1 | GPA0~GPA7 GPB0~GPB6 |
External interrupt Group 2 | GPC0~GPC7 |
External interrupt Group 3 | GPD0~GPD5 |
External interrupt Group 4 | GPF0~GPF14 |
External interrupt Group 5 | GPG0~GPG7 |
External interrupt Group 6 | GPH0~GPH9 |
External interrupt Group 7 | GPO0~GPO15 |
External interrupt Group 8 | GPP0~GPP14 |
External interrupt Group 9 | GPQ0~GPQ9 |
6410支持64种中断源,即有64个中断号。INT_EINT0仅仅对应0号中断,INT_EINT0又包含几个中断。
在VIC中,10组外部中断占用的中断源情况如下:
中断号 | 中断源 | 对应外部中断 | VIC组 |
0 | INT_EINT0 | External interrupt 0~3 | VIC0 |
1 | INT_EINT1 | External interrupt 4~11 | VIC0 |
32 | INT_EINT2 | External interrupt 12~19 | VIC1 |
33 | INT_EINT3 | External interrupt 20~27 | VIC1 |
53 | INT_EINT4 | External interrupt group1~group9 | VIC1 |
举个例子(OK6410的6个按键做外部中断):
按键 | 对应引脚 | 对应外部中断 | 中断源 |
KEY1 | GPN0 | External interrupt 0 | INT_EINT0 |
KEY2 | GPN1 | External interrupt 1 | INT_EINT0 |
KEY3 | GPN2 | External interrupt 2 | INT_EINT0 |
KEY4 | GPN3 | External interrupt 3 | INT_EINT0 |
KEY5 | GPN4 | External interrupt 4 | INT_EINT1 |
KEY6 | GPN5 | External interrupt 5 | INT_EINT1 |
KEY1~KEY4都是属于INT_EINT0,即都是属于中断号为0的中断,这四个按键产生的外部中断所调用的中断处理函数的地址都是存在VIC0VECTADDR0寄存器中的,这四个随便哪个按下都会调用同一个中断处理函数,所以在处理函数里面必须判别是哪个按键所触发的中断。
中断相关寄存器的设计演变(寄存器以外部按键中断为例)
IC ARM 7(4510) | IC ARM 9(2440) | IC ARM 11(6410) | |
内核 (core) | CPSR I-bit | CPSR I-bit | CPSR I-bit VIC Port(Enable) VIC interface(PC <--> A0~A31) |
中断控制器 (IC) | INTMOD INTPND INTMSK | INTOFFSET INTPRI INTPND INTMOD INTMSK SRCPND | VectADDRESS(32bit -> A0~A31,中断函数地址的注册 ) Vectors(handlers中断处理函数) Priority(优先级的判别) VIC0IRQSTATUS/VIC0FIQSTATUS(IRQ/FIQ的中断悬起位) VIC0INTSELECT (选择是IRQ还是FIQ模式) VIC0INTENABLE(MASK功能) VIC0RAWINTR(显示FIQ中断是否置位,从EINT0PND上传输过来的) |
中断源控制器 (GPIO) | EINTCON (F/R/L) GPXCON (EINT) | EINTCON (F/R/L) GPXCON (EXIT) | INTMASK EINT0PND (需要手动清除) EINT0CON0 (Low/High level Falling/Rising/Both edge) GPxCON (EINT) |
硬件层 | key/UART/USB/Timer | key/UART/USB/Timer | key/UART/USB/Timer |
我用的是OK6410,分析一下最后一列:
最后一列从下到上,是从最外层一直到内核各个寄存器的顺序,以外部key按键中断为例( GPN0~GPN5-->key1~key6)
中断源 GPIO Controller
GPNCON [1:0] --> key1 Set the pin mux function as Ext.Interrupt[0]
00 = Input
01 = Output
* 10 = Ext. Interrupt[0]
11 = Key pad ROW[0]
EINT0CON0[2:0] --> EINT1,0 Sets the signaling method of Ext.Interrupt[0]
000 = Low level
001 = High level
* 01x = Falling edge triggered
10x = Rising edge triggered
11x = Both edge triggered
EINT0PEND[0]
0 = Not occur
1 = Occur interrupt
EINT0MASK[0]
* 0 = Enables Interrupt
1 = Masked
我们这里的中断是使用中断向量控制器VIC
如何测试和中断相关的各个寄存器到底是怎么工作的,上下层的相互关系是怎么样的?
先看下面的关系图(从下到上越来越接近cpu内核):
首先我们不采用中断方式来看各个寄存器的变化,我们用查询的方式查看,以按键中断为例,当有按键按下的时候,各个寄存器怎么变化,相互之间的关系如何:
准备一个能够在串口输出字符的程序。
OK6410的KEY1是接在 GPN0上面的,通过GPNCON把它设置成中断方式,且通过EINT0CON0设置成下降沿触发方式,代码如下:
/* set GPNIO to EINT mode , 10 --> Eint */temp = GPNCON ;temp &= ~(0x3<<0);temp |= (0x2<<0);GPNCON = temp;/* set EINT triger mode to falling eage ,01x = Falling edge */temp = EINT0CON0 ;temp &= ~(0x7<<0);temp |= (0x3<<0);EINT0CON0 = temp;
当按下KEY的时候,会触发中断,并且把这个信号传送到最下层的中断悬起位寄存器EINT0PEND,这个寄存器不同的bit对应不同的中断。EINT0PEND的上层是中断屏蔽寄存器INTMASK,当这个寄存器设置为屏蔽的时候中断触发信号是无法通过它传送到上层去的。我们先来看看当中断发生的时候EINT0PEND是如何变化的,他的变化会带来怎么样的结果。
while (1){ for(ch='a';ch<='z';ch++){ if( (EINT0PEND & 0x1)==0x1){ //轮询EINT0PEND有没有被置位 uart_putchar('+'); } uart_putchar(ch); delay(); }}
上面的程序是在不停的查询EINT0PEND[0],该bit对应的是外部中断0,如果按下KEY产生一个下降沿,对应位会置位,if语句满足,会打印出一个“+”,然后出来打印字母,按键松开后是否还会满足if呢?看现象:
发现按下一次之后“+”会不停的打印出来,也就是说if语句都是满足的,这就意味着EINT0PEND置位之后没有被清除,每次查询都还有,所以会不停的打印加号,所以这个寄存器我们必须手动清除,否则触发一次中断后就会不停的响应该中断。
清除的方式就是向对应位写1,我们这里是:
至于清除中断悬起位的方式:
3种清0的写法,只有最后一种是正确的清除。
PEND |= 1<<0; (not good)PEND = 0xFFFFFFFF; (not good)PEND = 1<<0; (Good!)
如果PEND寄存器某个bit是0,你写一个1进去,那么会变成1。只有当这位是1的时候写个1进去才会变成0.
第一种:用或的方式,若PEND = 00011111(二进制),PEND | (1<<0) = 00011111,也就是将00011111写入到值为00011111的PEND中,那么PEND的值变成00000000,所有的位都被清0了,而不是我们原本的意思要清除第一位。
第二种:直接赋值,其实和第一种方式是一样的,只不过第一种是先求出或的值然后写进寄存器,如果原本是00011111的PEND,写进0xFFFFFFFF那么PEND中原本是0的位还是0,是1的位全部都清为0 ,还是不是我们想要的结果。
第三种:PEND = 1<< 0 ,加入PEND是00011111,把00000001赋值进来,那么得到PEND的值为00011110,刚好是我们想要的,清除我们想要清除的位。
下面我们看看EINT0MASK的屏蔽作用是怎样的效果:
首先看看EINT0MASK的描述信息:
可以看出哪位置1就是屏蔽对应的中断信号
/* EINT0MASK[0] = 1 : Mask EINT0 */temp = EINT0MASK ;temp &= ~(0x1<<0);temp |= (0x1<<0);EINT0MASK = temp;
屏蔽外部中断0,看中断触发信号能不能通过它传达到上面的寄存器VIC0RAWINTR。
看看VIC0RAWINTR的描述:
先不屏蔽中断,看看能否在VIC0RAWINTR看到中断信号:
/* EINT0MASK[0] = 0 : disMask EINT0 */temp = EINT0MASK ;temp &= ~(0x1<<0);EINT0MASK = temp;uart_init();while (1){ for(ch='a';ch<='z';ch++){ if( (VIC0RAWINTR & 0x1)==0x1){ uart_putchar('+'); } uart_putchar(ch); delay(); }}
按下KEY,触发中断,发现现象如下(没有清理EINT0PEND):
说明在 VIC0RAWINTR 检测到中断触发信号了。
如果把EINT0MASK[0]设置为1,即屏蔽信号,看看结果:
怎么按KEY也检测不到中断触发信号,说明EINT0MASK是这里的一道关卡,中断触发信号能否传到上级要看这里有没有屏蔽掉,它相当于一个开关作用。
继续dismask中断触发信号,让它传递到VIC0RAWINTR ,然后清除中断悬起位,看现象是怎样的:
/* EINT0MASK[0] = 0 : disMask EINT0 */temp = EINT0MASK ;temp &= ~(0x1<<0);EINT0MASK = temp;uart_init();while (1){ for(ch='a';ch<='z';ch++){ if( (VIC0RAWINTR & 0x1)==0x1){ uart_putchar('+'); EINT0PEND = 0x1; } uart_putchar(ch); delay(); }}
按一下KEY打印出一个“+”,然后查询VIC0RAWINTR 寄存器,发现中断触发信号没有了,说明EINT0PEND的值直接影响到VIC0RAWINTR 的值,VIC0RAWINTR 跟随EINT0PEND变化,EINT0PEND的变化实时反映到VIC0RAWINTR上来,当然得看MASK有没有屏蔽掉中断触发信号啦。
信号接着往上面传送,直到cpu内核检测到中断触发信号并产生对应的操作为止:
使用VIC(中断向量控制器)
VIC0INTENABLE的描述:
对应的清除寄存器VIC0INTENCLEAR的描述:
VIC0SELECT的描述:
我们这里选择IRQ方式,也可以是FIQ方式。
VIC0IRQSTATUS的描述:
VIC0FIQSTATUS描述:
我们选择IRQ方式,并且使能中断功能,看能不能到VIC0IRQSTATUS中查询到中断触发信号:
void mymain(void){unsigned char ch;unsigned int temp=0;/* set GPNIO to EINT mode , 10 --> Eint */temp = GPNCON ;temp &= ~(0x3<<0);temp |= (0x2<<0);GPNCON = temp;/* set EINT triger mode to falling eage ,01x = Falling edge */temp = EINT0CON0 ;temp &= ~(0x7<<0);temp |= (0x3<<0);EINT0CON0 = temp;/* EINT0MASK[0] = 0 : disMask EINT0 */temp = EINT0MASK ;temp &= ~(0x1<<0);//temp |= (0x1<<0);EINT0MASK = temp;/* VIC0INTENABLE[0]=1 : enable interrupt *//* clear bit by VIC0INTENCLEAR */temp = VIC0INTENABLE ;temp &= ~(0x1<<0);temp |= (0x1<<0);VIC0INTENABLE = temp;/* VIC0INTSELECT[0] = 0 : set to IRQ */temp = VIC0INTSELECT ;temp &= ~(0x1<<0);VIC0INTSELECT = temp;uart_init();while (1){ for(ch='a';ch<='z';ch++){ if( (VIC0IRQSTATUS & 0x1)==0x1){ uart_putchar('+'); EINT0PEND = 0x1; } uart_putchar(ch); delay(); }}按下KEY出现下面的现象:
按一次打印一个“+”,说明
如果是把VIC0INTENABLE对应位设置成disable,那么就会屏蔽掉中断信号,这里相当于又建立了一道关卡,它没同意就不能把信号传达到上一层。
/* VIC0INTENABLE[0]=0 : disable interrupt *//* clear bit by VIC0INTENCLEAR */temp = VIC0INTENABLE ;temp &= ~(0x1<<0);VIC0INTENABLE = temp;
现象:
怎么按KEY也不会打印出“+”,说明中断信号被屏蔽了。
当信号传达到这里的时候还得继续往上,还需要经过中断优先级的寄存器,判别那个中断优先级最高,这个只影响到中断的顺序,不影响中断信号的有无。但是在cpu内核的最近的地方还有个中断的开关,是个总开关——CPSR的I/F-bit,默认是关闭的。这个一打开了就会把中断信号送给cpu内核了。
打开最后这个总开关有两个写法,一个是在汇编里面写,一个是在c语言里面的嵌入汇编里面写,我们这里在c语言潜入汇编来写:
/* init CPSR I-bit *///0101,0011__asm{ mov r0,#0x53 msr cpsr,r0}
但是编译的时候有警告,说应该吧这个r0定义成一个变量:
解决方法:
声明一个变量来代替寄存器
int val;/* init CPSR I-bit *///0101,0011__asm{ mov val,#0x53 msr cpsr,val}
编译警告就没有了,这里出现警告是因为R0可能被别的使用的,这里使用会冲突,我们定义一个变量来代替R0就行了。
但是编译运行后发现还是按下就有“+”打印出来,没有什么跳转现象啊!
cpsr修改成为cpsr_cxsf或者CPSR_cxsf,再次编译运行后,按下按键会发现,不打印了,停下来了:
说明cpu检测到了中断信号,跳出了原来运行的主函数,但是按理来说是要重启的,这里可能是VIC没有打开。下面介绍怎么打开VIC
打开VIC:
int val2;/* VIC Enable (cp15) */__asm{ mrc p15,0,val2,c1,c0,0 orr val2,val2,#(1<<24) mcr p15,0,val2,c1,c0,0}
这样cpu内核就能捕捉到中断了。编译运行,果然重启了。
但是怎么跳转的,跳转到哪里去呢??
中断发生了之后怎么办?
接下来有2种处理方法:
A) 简单的办法就是使用VIC 向量中断控制器的功能
1、跳转的地址向量要提前设置好2、通知ARM11内核,启动VIC Port 功能紧接着的问题是,如何在执行完beep之后返回主程序?(假设我们的目的是蜂鸣beep)原因:beep程序不能作为IRQ_handler1)保存cpu现场STMFD2)清除掉Pending bit,调用beep3)恢复cpu现场LDMFD (LR-4)->PC ,SPSR -> CPSR修改start.s,实现IRQ_handler1)IRQ模式下的sp指针需要初始化2)除了清除pending bit之外,还需要清除VIC0ADDRESS =0 ;
B)复杂的办法就是不用VIC,自己实现全过程
1、当 IRQ 异常发生的时候,cpu 跳转到 0x182、背景知识:reset 0 地址被映射 map 到 iROM (内容不可修改)0 地址 在 iROM 中 (0xD0000000)iRAM (0xD0020000) -> 0x20000 (iROM 被映射到了 0x20000)通过 md 命令,查看相关内存单元值,发现 0x18: 0xEA000018经过一系列分析,最终在 iROM 中的跳转指令会加载从 0xD0037400 地址开始的值,作为异常发生后要跳转的地址+offset ,因此只需要修改 0xD0037418 的向量即可。3、(int)IRQ_handler -> 0xD0037400 + 0x18如果是 SWI 软件中断,则在 0xD0037408 处填写swi_handler的地址
第一种方法
当程序有中断且cpu检测到中断的时候轮询就没有意义了,所以去掉轮训。在主函数外面声明一个handler函数:
void C_IRQ_handler(void){ EINT0PEND = 0x1; uart_putchar(' '); uart_putchar('+'); uart_putchar(' ');}
那要怎么样才能调用到这个handler呢?
这就要注册这个函数地址到向量地址里面去了
/* VIC0VECTADDR[31:0] --> 0x71200100 : 0x7120017C *//* Contains ISR vector addresses */#define VIC0VECTADDR0 (*((volatile unsigned int *)0x71200100 ))#define VIC1VECTADDR0 (*((volatile unsigned int *)0x71300100 ))/* install IRQ handler to Vectors *//* EINT0 --> VIC0VECTADDR[0] */VIC0VECTADDR0 = (int)C_IRQ_handler;
这样注册后是不是就能跳转到这个函数去了呢?当然不是!VIC默认是关闭的(uboot里面可能打开VIC了,但是完全裸机的话,VIC默认是关闭的,要自己手动打开)。上面手动打开了VIC了,这里注册好函数之后编译运行,发现开发板还是重启,没有进入到handler里面来,什么 原因呢??
************************************************************************************************************
下面是在汇编里面写的handler,要做中断sp的设置,返回地址lr的计算,现场保存,然后调用c语言里面的handler,返回后要恢复现场:
import C_IRQ_handlerexport asm_IRQ_handlerasm_IRQ_handlerldr sp,=0x58000000;lr=lr-4sub r14,r14,#4stmfd r13!,{r1-r12,r14}bl C_IRQ_handlerldmfd r13!,{r1-r12,pc}^
下面是c语言的handler,它要完成中断悬起位的清除,中断处理函数地址寄存器的清除,然后做别的事情:
void C_IRQ_handler(void){ EINT0PEND = 0x1;//clear pending bit VIC0ADDRESS = 0;//clear address uart_putchar(' '); uart_putchar('+'); uart_putchar(' ');}
这个程序直接下载到ram中运行,按下按键,中断触发就直接重启开发板了,但是放在ads上去调试却没有什么毛病能够进入中断正常运行(代码参考OK6410裸机调试笔记--> 6),不知道什么原因。