以下内容参考韦东山老师的《嵌入式Linux应用开发完全手册》。
在说ARM中断体系结构前,需要了解ARM体系CPU的工作模式,因CPU进入中断时需要切换到不同的工作模式,然后执行中断响应。主要有7种工作模式:
1.用户模式(usr):ARM处理器正常的运行状态。
2.中断模式(irq):用于通用的中断处理。
3.快速中断模式(fiq):用于高速数据传输或通道处理。
4.管理模式(svc):操作系统使用的保护模式。
5.系统模式(sys):运行具有特权的操作系统任务。
6.数据访问终止模式(abt):当数据或者指令预取终止时进入该模式。可用于虚拟存储和数据保护。
7.未定义指令终止模式(und):当未定义的指令执行时进入该模式。可用于支持硬件协处理的软件仿真。
除了用户模式,其他模式可称为特权模式。大多数程序运行于用户模式,进入特权模式是为了处理中断、异常、或者访问被保护的系统资源。
所以当有中断发生时,CPU须由用户模式进入到中断模式。那么CPU怎么进入中断模式呢?这就需要了解不同模式下的寄存器的使用喽。
ARM920T有31个通用的32为寄存器和6个程序状态寄存器。当进入不同的模式时就使用该模式下的寄存器。
部分寄存器在不同的模式下有自己的副本,当进入某工作模式时,该工作模式下的副本寄存器将被使用。由图中可以看到FIQ(快速中断模式)的副本寄存器最多,这就使得进入快速中断模式时需要保存的寄存器状态也就最少,倘若r1-r8不做修改的话几乎不需要保存。这应该也是它被称为快速中断的原因。
在这些寄存器中,r13称为栈指针寄存器;r14称为程序连接寄存器;r15称为程序计数器。
关于r14和r15,当执行BL指令时,r14得到r15的备份。指令执行完后,再将r14的值赋给r15.当发生中断或者异常时,不同模式下的r14得到r15的备份,异常处理完后再赋给r15。
除了上述的16个寄存器外,还有一个非常重要的主角:CPSR寄存器(当前程序状态寄存器),该寄存器标识了各种状态和工作模式。
有了该图就不难理解head.S中Reset下面的语句了:
Reset:
ldr sp, =4096 @ 设置栈指针,以下都是C函数,调用前需要设好栈
bl disable_watch_dog @ 关闭WATCHDOG,否则CPU会不断重启
msr cpsr_c, #0xd2 @ 进入中断模式
ldr sp, =3072 @ 设置中断模式栈指针
msr cpsr_c, #0xd3 @ 进入管理模式
ldr sp, =4096 @ 设置管理模式栈指针,
@ 其实复位之后,CPU就处于管理模式,
@ 前面的“ldr sp, =4096”完成同样的功能,此句可省略
0xd2为0x11010010其中后五位”10010“设置进入中断模式,前三位”110“设置IRQ和FIQ禁止,CPU工作于ARM工作状态。
0xd3为0x11010011其中后五位”10011“设置进入管理模式,前三位”110“仍然是IRP和FIQ禁止,CPU工作于ARM工作状态。
为什么要设置栈指针,且该指针指向4096和3072呢?
这是分别设定中断模式和管理模式下的堆栈指针到4096 (SRAM顶层),ARM在各种执行模式下都需要设置各自的堆栈指针,这就是为什么你看到那么多次ldr sp的操作了。另外,由于堆栈向下生长,这是为什么可以将堆栈指针设定到地址空间顶层的原因。
除此之外还有SPSR寄存器(程序状态保存寄存器),当不同的模式进行切换时,在SPSR中保存前一个工作模式的CPSR值,这样,当返回前一个模式时,只需要将SPSR的值恢复到CPSR即可。
当中断发生时,具体的操作是怎么进行的呢?
一、当冲断发生时,CPU将切换进入相应的工作模式,此时,CPU核将自动完成如下事情:
1)在异常工作模式的连接寄存器R14中保存前一个工作模式的下一个指令的地址,对于ARM状态,进入或者退出异常模式时PC的地址:
2)将CPSR的值复制到异常模式的SPSR。
3)将CPSR的工作模式设置为该异常的工作模式。
4)令PC的值等于该异常模式在异常向量表中的地址,即跳转去执行异常向量表中的相应指令。
二、从异常工作模式退出到之前的工作模式时,需要通过软件完成如下操作。
1)将连接寄存器保存的前一个工作模式的指令地址减去一个适当的值赋给PC寄存器,具体如下。
2)将SPSR的值恢复到CPSR。
前面介绍了当有中断发生时的处理情况,那么CPU是如何检测到有异常发生呢?主要有两种方式:
1)查询方式:程序循环检测割设备的状态并做出相应的响应。
2)中断方式:当事件发生时,硬件会设置某个寄存器。CPU每执行完一个指令,就通过硬件查看该寄存器,如果该寄存器的值发生变化,CPU终止当前操作去处理该事件。
通常使用中断方式。既然中断已经检测到,那么CPU将会做出怎样的处理呢?
1)中断控制器汇集各类外设发出的中断信号,然后告诉CPU。
2)CPU保存当前程序的运行环境(当前程序使用到的各个寄存器的值等),调用中断服务程序(ISR Interrupt Service Routine)来处理这些中断。
3)在ISR中通过读取中断控制器、外设的相关寄存器来识别是哪个中断。
4)针对相应的中断做出具体的操作。
5)具体的操作完成后需要清除中断,该清除通过读写中断控制器和外设的相关寄存器来实现。
6)最后恢复被中断程序的运行环境,继续执行。
中断需要具体的设置才能使用,不是现成的。中断的具体使用步骤如下:
1)设置好中断模式和快速中断模式下的栈。当发生中断IRQ时,CPU进入中断模式,这时使用中断模式下的栈;当发生快速中断FIQ时,CPU进入快速中断模式,这时使用快速中断模式下的栈。
2)准备好中断处理函数
在异常向量表中设置好进入中断模式或者快速中断模式时的跳转函数,异常向量地址分别为:0x00000018,0x0000001c。
对于不同的中断跳转函数,最终将调用相应的中断的服务函数。
对于IRQ,读取INTPNQ寄存器或者INTOFFSET寄存器的值来确定中断源,然后分别处理。
对于FIQ,只有一个中断可以设置为FIQ,无需判断中断源。
- 清除中断:可以在调用ISR之前或者之后清除中断,这取决于在ISR执行的过程中,该中断是否还可能发生。若发生,则在之前清除,若不发生,则在之后清除。清除中断时,从源头开始,首先操作具体外设清除中断信号;其次,清除SUBSRCPND、SRCPND寄存器的相应位(往相应位写入1);最后清除INTPND寄存器中的相应位,最简单的方法是:INTPND=INTPND;
3)进入、退出中断模式或者快速中断模式时,需要保存、恢复被中断程序的运行环境。
对于IRQ,进入、退出的代码如下:
对于FIQ,进入、退出的代买如下:
4)根据具体中断设置相关外设。例如设置GPIO中断引脚,部分中断拥有自己的屏蔽寄存器,还要开启它。
5)对于“Request sources(without sub-register)”中的中断,将INTSUBMSK寄存器中响应位设为0。
6)确定使用中断的方式:FIQ或IRQ。
如果是FIQ,则在INTMOD寄存器中设置相应位为1。
若果是IRQ,则在PRIORITY寄存器中设置优先级。
7)如果是IRQ,则在INTMSK寄存器中响应位设为0(FIQ不受INTMSK寄存器控制)
8)设置CPSR寄存器中的I-bit(对于IRQ)或者F-bit(对于FIQ)为0,使能IRQ或FIQ。
这么复杂呀。晕...
下面看具体的源代码:
先看Makefile文件
objs := head.o init.o interrupt.o main.o
int.bin: $(objs)
arm-linux-ld -Ttext 0x00000000 -o int_elf $^
arm-linux-objcopy -O binary -S int_elf $@
arm-linux-objdump -D -m arm int_elf > int.dis
%.o:%.c
arm-linux-gcc -Wall -O2 -c -o $@ $<
%.o:%.S
arm-linux-gcc -Wall -O2 -c -o $@ $<
clean:
rm -f int.bin int_elf int.dis *.o
主要关系到四个源文件。依次来看head.S init.c interrupt.c main.c
@******************************************************************************
@ File:head.S
@ 功能:初始化,设置中断模式、管理模式的栈,设置好中断处理函数
@******************************************************************************
.extern main
.text
.global _start
_start:
@******************************************************************************
@ 异常向量,本程序中,除Reset和HandleIRQ外,其它异常都没有使用
@******************************************************************************
b Reset
@ 0x04: 未定义指令中止模式的向量地址
HandleUndef:
b HandleUndef
@ 0x08: 管理模式的向量地址,通过SWI指令进入此模式
HandleSWI:
b HandleSWI
@ 0x0c: 指令预取终止导致的异常的向量地址
HandlePrefetchAbort:
b HandlePrefetchAbort
@ 0x10: 数据访问终止导致的异常的向量地址
HandleDataAbort:
b HandleDataAbort
@ 0x14: 保留
HandleNotUsed:
b HandleNotUsed
@ 0x18: 中断模式的向量地址
b HandleIRQ
@ 0x1c: 快中断模式的向量地址
HandleFIQ:
b HandleFIQ
Reset:
ldr sp, =4096 @ 设置栈指针,以下都是C函数,调用前需要设好栈
bl disable_watch_dog @ 关闭WATCHDOG,否则CPU会不断重启
msr cpsr_c, #0xd2 @ 进入中断模式
ldr sp, =3072 @ 设置中断模式栈指针
msr cpsr_c, #0xd3 @ 进入管理模式
ldr sp, =4096 @ 设置管理模式栈指针,
@ 其实复位之后,CPU就处于管理模式,
@ 前面的“ldr sp, =4096”完成同样的功能,此句可省略
bl init_led @ 初始化LED的GPIO管脚
bl init_irq @ 调用中断初始化函数,在init.c中
msr cpsr_c, #0x53 @ 设置I-bit=0,开IRQ中断
ldr lr, =halt_loop @ 设置返回地址
ldr pc, =main @ 调用main函数
halt_loop:
b halt_loop
HandleIRQ:
sub lr, lr, #4 @ 计算返回地址
stmdb sp!, { r0-r12,lr } @ 保存使用到的寄存器
@ 注意,此时的sp是中断模式的sp
@ 初始值是上面设置的3072
ldr lr, =int_return @ 设置调用ISR即EINT_Handle函数后的返回地址
ldr pc, =EINT_Handle @ 调用中断服务函数,在interrupt.c中
int_return:
ldmia sp!, { r0-r12,pc }^ @ 中断返回, ^表示将spsr的值复制到cpsr
在中断向量表中,每个向量表地址依次增加四个字节。其中b Reset地址为0x00
这其中主要涉及到了中断Reset和IRQ。
关于Reset中的“ msr cpsr_c, #0xd2 @ 进入中断模式"MSR为写状态寄存器指令。
关于stmdb
sub lr, lr, #4 @ 计算返回地址
stmdb sp!, { r0-r12,lr } @ 保存使用到的寄存器
关于LDMIA
int_return:
ldmia sp!, { r0-r12,pc }^ @ 中断返回, ^表示将spsr的值复制到cpsr
在开启中断之前
msr cpsr_c, #0x53 @ 设置I-bit=0,开IRQ中断
需要先设置好中断处理函数和必要的中断环境(中断初始化)在初始化函数Init.c定义了初始化函数
/*
* init.c: 进行一些初始化
*/
#include "s3c24xx.h"
/*
* LED1,LED2,LED4对应GPB5、GPB6、GPB7、GPB8
*/
#define GPB5_out (1<<(5*2))
#define GPB6_out (1<<(6*2))
#define GPB7_out (1<<(7*2))
#define GPB8_out (1<<(8*2))
#define GPB5_msk (3<<(5*2))
#define GPB6_msk (3<<(6*2))
#define GPB7_msk (3<<(7*2))
#define GPB8_msk (3<<(8*2))
/*
* K1,K2,K3,K4对应GPF1、GPF4、GPF2、GPF0
*/
#define GPF0_int (0x2<<(0*2))
#define GPF1_int (0x2<<(1*2))
#define GPF2_int (0x2<<(2*2))
#define GPF4_int (0x2<<(4*2))
#define GPF0_msk (3<<(0*2))
#define GPF1_msk (3<<(1*2))
#define GPF2_msk (3<<(2*2))
#define GPF4_msk (3<<(4*2))
/*
* 关闭WATCHDOG,否则CPU会不断重启
*/
void disable_watch_dog(void)
{
WTCON = 0; // 关闭WATCHDOG很简单,往这个寄存器写0即可
}
void init_led(void)
{
// LED1,LED2,LED3,LED4对应的4根引脚设为输出
GPBCON &= ~(GPB5_msk | GPB6_msk | GPB7_msk | GPB8_msk);
GPBCON |= GPB5_out | GPB6_out | GPB7_out | GPB8_out;
}
/*
* 初始化GPIO引脚为外部中断
* GPIO引脚用作外部中断时,默认为低电平触发、IRQ方式(不用设置INTMOD)
*/
void init_irq( )
{
// K1,K2,K3,K4对应的4根引脚设为中断功能
GPFCON &= ~(GPF0_msk | GPF1_msk | GPF2_msk | GPF4_msk);
GPFCON |= GPF0_int | GPF1_int | GPF2_int | GPF4_int;
// 对于EINT4,需要在EINTMASK寄存器中使能它
EINTMASK &= ~(1<<4);
/*
* 设定优先级:
* ARB_SEL0 = 00b, ARB_MODE0 = 0: REQ1 > REQ2 > REQ3,即EINT0 > EINT1 > EINT2
* 仲裁器1、6无需设置
* 最终:
* EINT4 > EINT0> EINT1 > EINT2 即K4 > K1 > K3 > K2
*/
PRIORITY = (PRIORITY & ((~0x01) & ~(0x3<<7)));
// EINT0、EINT1、EINT2、EINT4_7使能
INTMSK &= (~(1<<0)) & (~(1<<1)) & (~(1<<2)) & (~(1<<4));
}
为什么要单独设置EINT4呢?
// 对于EINT4,需要在EINTMASK寄存器中使能它
EINTMASK &= ~(1<<4);
这要涉及到ARM中断的控制流程:
本副图片来自:http://blog.csdn.net/mr_raptor/article/details/6556258
从图中可以看到EINT0-EINT3并没有经过EINTPEND和EINTMASK而是直接到达SRCPND(总中断源位),而EINT4-EINT23则是经过EINTPEND和EINTMASK,所以在使用到EINT4-EINT23时需要在EINTMASK寄存器中使能该中断。
四个中断已经介绍完了,当中断发生时,首先响应哪个中断呢?这要涉及到中断的优先级设定,PRIORITY寄存器。中断优先级的判断通过七个仲裁器来完成,包括一个二级仲裁器和六个一级仲裁器。结构图如下:
PRIORITY寄存器使用三位来控制其行为:一位用于选择仲裁器的工作模式,两位用来控制各输入信号的优先级。PRIORITY寄存器的[0:6]对应七个仲裁器的ARB_MODE位(位0为ARB_MODE0,,依次类推)位【7:20】对应七个仲裁器的ARB_SEL位(【7:8】是ARB_SEL0,依次类推)
按键与EINT0-EINT4的对应关系如下:
/*
* 设定优先级:
* ARB_SEL0 = 00b, ARB_MODE0 = 0: REQ1 > REQ2 > REQ3,即EINT0 > EINT1 > EINT2
* 仲裁器1、6无需设置
* 最终:
* EINT0 > EINT1> EINT2 > EINT4 即K4 > K1 > K3 > K2
*/
PRIORITY = (PRIORITY & ((~0x01) | ~(0x3<<7)))
~0x01来选择仲裁器的工作模式,位【0】是ARB-MODE0。
~(0x3<<7)用来选择仲裁器输入信号的优先级。
接下来使能中断(将相应位置0使能中断):
// EINT0、EINT1、EINT2、EINT4_7使能
INTMSK &= (~(1<<0)) & (~(1<<1)) & (~(1<<2)) & (~(1<<4));
前面讲述了中断的相关过程,那么中断来时做出怎么样的操作呢?
interrupt.c
#include "s3c24xx.h"
void EINT_Handle()
{
unsigned long oft = INTOFFSET;
unsigned long val;
/*
* K1,K2,K3,K4对应GPF1、GPF4、GPF2、GPF0
* 即 EINT1, ETIN4, EINT2, EINT0
* oft为 1, 4, 2, 0 (对应INTMSK寄存器)
*/
switch( oft )
{
// K1被按下
case 1:
{
GPBDAT |= (0xF<<5); // 所有LED熄灭
GPBDAT &= ~(1<<5); // LED1点亮
break;
}
// K2被按下
case 4:
{
GPBDAT |= (0xF<<5); // 所有LED熄灭
GPBDAT &= ~(1<<6); // LED2点亮
break;
}
// K3被按下
case 2:
{
GPBDAT |= (0xF<<5); // 所有LED熄灭
GPBDAT &= ~(1<<7); // LED3点亮
break;
}
// K4被按下
case 0:
{
GPBDAT |= (0xF<<5); // 所有LED熄灭
GPBDAT &= ~(1<<8); // LED4点亮
break;
}
default:
break;
}
//清中断
if( oft == 4 )
EINTPEND = (1<<4); // EINT4_7合用IRQ4
SRCPND = 1<main.c
int main()
{
while(1);
return 0;
}
是的。main函数就这么几条语句。
s3c24xx.h
/* WOTCH DOG register */
#define WTCON (*(volatile unsigned long *)0x53000000)
/* SDRAM regisers */
#define MEM_CTL_BASE 0x48000000
#define SDRAM_BASE 0x30000000
/* NAND Flash registers */
#define NFCONF (*(volatile unsigned int *)0x4e000000)
#define NFCMD (*(volatile unsigned char *)0x4e000004)
#define NFADDR (*(volatile unsigned char *)0x4e000008)
#define NFDATA (*(volatile unsigned char *)0x4e00000c)
#define NFSTAT (*(volatile unsigned char *)0x4e000010)
/*GPIO registers*/
#define GPBCON (*(volatile unsigned long *)0x56000010)
#define GPBDAT (*(volatile unsigned long *)0x56000014)
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
#define GPFUP (*(volatile unsigned long *)0x56000058)
#define GPGCON (*(volatile unsigned long *)0x56000060)
#define GPGDAT (*(volatile unsigned long *)0x56000064)
#define GPGUP (*(volatile unsigned long *)0x56000068)
#define GPHCON (*(volatile unsigned long *)0x56000070)
#define GPHDAT (*(volatile unsigned long *)0x56000074)
#define GPHUP (*(volatile unsigned long *)0x56000078)
/*UART registers*/
#define ULCON0 (*(volatile unsigned long *)0x50000000)
#define UCON0 (*(volatile unsigned long *)0x50000004)
#define UFCON0 (*(volatile unsigned long *)0x50000008)
#define UMCON0 (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0 (*(volatile unsigned long *)0x50000010)
#define UTXH0 (*(volatile unsigned char *)0x50000020)
#define URXH0 (*(volatile unsigned char *)0x50000024)
#define UBRDIV0 (*(volatile unsigned long *)0x50000028)
/*interrupt registes*/
#define SRCPND (*(volatile unsigned long *)0x4A000000)
#define INTMOD (*(volatile unsigned long *)0x4A000004)
#define INTMSK (*(volatile unsigned long *)0x4A000008)
#define PRIORITY (*(volatile unsigned long *)0x4A00000c)
#define INTPND (*(volatile unsigned long *)0x4A000010)
#define INTOFFSET (*(volatile unsigned long *)0x4A000014)
#define SUBSRCPND (*(volatile unsigned long *)0x4A000018)
#define INTSUBMSK (*(volatile unsigned long *)0x4A00001c)
/*external interrupt registers*/
#define EINTMASK (*(volatile unsigned long *)0x560000a4)
#define EINTPEND (*(volatile unsigned long *)0x560000a8)
到此全部结束。