中断在嵌入式里面是很常见的一个功能了。通过这个功能,可以让CPU减轻很多负担,不用不断的查询设备的状态。提高了CPU的效率。
中断的大体过程如下:
中断源检测中断信号产生,然后将中断信号发送给中断控制器,中断控制器判断该中断是否被屏蔽,从而决定该中断信号是否要发送给CPU。中断信号发送给CPU后,CPU对中断进行处理,也就是调用中断函数。上述过程,基本上是嵌入式的通过中断处理过程,只是不同的嵌入式在这三部分配置有区别而已。
S3C6410共有64个中断源。
上图是S3C6410的中断控制器,这里就关心红色框部分。这两个是中断控制器,分别管理各自的32个中断。 如下图:
上图红框中的就是今天我们需要关心的中断部分(外部中断)。
S3C6410共有127个外部中断,被分成10组,分组及引脚对应情况如下:
分组对应中断号如下:
从上图我们知道,并不是每一个外部中断引脚都分配了中断号,因此,在中断服务程序中,为了知道具体是哪一个中断,还需要去查询寄存器以知道是哪一个中断产生。
学习过2440的朋友可能知道,2440的中断的处理采用的是非向量方式,就是当中断产生时,都跳转到中断异常去,然后这个中断异常中,编写程序,判断是哪一个中断产生,然后去执行对应的中断处理程序。如下图:
为了向下兼容,S3C6410中断处理有向量模式和非向量方式。在向量模式中,提前设定每个中断对应的入口地址,这样当中断产生的时候,就不用跳转到中断异常去了,直接跳转到对应的中断程序去了。这样中断处理的效率就提高了。向量中断模式如下图:
明显看出,中断向量方式的效率要高。因此,在学习ok6410开发板的中断时,我职无旁贷的选择了向量中断模式。
综上,可以总结出S3C6410的外部中断程序设计基本步骤如下:
1、 设置外部管脚为中断;
2、 设置中断触发方式;
3、 取消中断屏蔽,使外部中断不屏蔽;
4、 设置中断滤波(可不设置,这里忽略);
5、 使能外部中断;
6、 设置中断号的入口地址;
7、 设置中断号的中断选择,是irq还是fiq,默认为是irq;
8、 开启向量中断方式并打开全局中断;
9、 编写中断处理函数,中断函数前和后要使用嵌入汇编,保存环境和恢复环境。中断处理后,要清除中断挂起位和中断执行地址。
接下来我们就以OK6410开发板为载体来逐步的分析外部中断的实现步骤。OK6410开发板上共有6个用户按键,我们接下来就通过这6个按键完成外部中断,实现按键控制led和蜂鸣器的运行情况。
要想实现上述的控制功能,就得知道这些按键、led和蜂鸣器对应的引脚情况,这些可以通过查看OK6410开发板原理图得到。原理图部分截图如下:
用户按键与芯片部分
led
蜂鸣器
从截图上可以看出来按键对应的引脚是GPN0~5、led对应的引脚是GPM0~3、蜂鸣器对应的引脚是GPF15。需要用到的引脚已经确认,下面开始按步骤配置外部中断。
1.配置外部引脚位中断
通过查看s3c6410手册GPIO章节,我们找到GPN相关配置寄存器如下:
这三个寄存器我们只需设置GPNCON,详细说明如下:
从上图可知,要想将开发板用户按键设置成外部中断,只需要将GPNCON寄存器的GPN0~5这几个位设为10即可。实现代码如下:
#define GPNCON *((volatile unsigned long*)0x7f008830) /* GPN控制寄存器 */
#define GPNDAT *((volatile unsigned long*)0x7f008834) /* GPN数据寄存器 此处未使用*/
#define GPNPUD *((volatile unsigned long*)0x7f008838) /* GPN上下拉配置寄存器 此处未使用*/
/* 按键初始化
* 设置按键对应引脚为外部中断模式
*/
void button_init(void)
{
/* 方式1 移位
GPNCON = (0b10 << 0) | (0b10 << 2) | (0b10 << 4) | (0b10 << 6) | (0b10 << 8) | (0b10 << 10);
*/
/* 方式2 逻辑运算 */
GPNCON &= (~0x00000aaa);
GPNCON |= 0x00000aaa;
}
2.设置中断触发方式
通过原理图我们发现,按键部分是作了上拉处理的,所以这里设定外部中断触发方式为下降沿触发。接着在s3c6410手册的GPIO章节中找到外部中断相关寄存器说明部分,如下:
由GPNCON寄存器我们知道GPN0~5对应的是外部中断分组0 ,所以这里我们只需要配置EINT0CON0寄存器即可,EINT0CON0说明如下:
红框中的位就是我们需要设置,从图中可知,只需将对应为设置位010即可,代码如下:
/*interrupt registes*/
#define EXT_INT_0_CON *((volatile unsigned int *)0x7f008900) /* 外部中断0~27配置寄存器 */
EXT_INT_0_CON &= ~(0x00000222);
EXT_INT_0_CON |= 0x00000222; /* 配置为下降沿触发 */
3.取消中断屏蔽,使外部中断不屏蔽
既然想要取消中断屏蔽,那就得配置中断屏蔽寄存器,该寄存器在s3c6410手册的GPIO章节中找到外部中断相关寄存器说明部分。因为这里是外部中断分组0的0~5,所以只需配置EINT0MASK寄存器(0x7f008920)的bit0~5,如下红框部分:
从上图可以知道,需要将EINT0MASK寄存器的bit0~5配置为0,代码如下:
#define EXT_INT_0_MASK *((volatile unsigned int *)0x7f008920) /* 外部中断0~27屏蔽寄存器 */
EXT_INT_0_MASK &= 0xffffffc0; /* 取消屏蔽外部中断 */
4.设置中断滤波
滤波主要是消除毛刺干扰,需要配置滤波寄存器,如下图:
代码如下“
”
#define EXT_INT_0_FLTCON0 *((volatile unsigned int *)0x7f008910) /* 外部中断组0滤波寄存器 */
EXT_INT_0_FLTCON0 = (0xff) | (0xff << 8) | (0xff << 16); /* 设置外部中断0~5的滤波*/
5.使能外部中断
要想使能外部中断,就得配置中断使能寄存器VICxINTENABLE,又因为这里是外部中断分组0的0~5,根据s3c6410手册中断章节中断源介绍部分可知,这里只需要配置VIC0INTENABLE,如下红框:
中断使能寄存器VIC0INTENABLE地址及说明如下:
从上图可知,只需将中断使能寄存器VIC0INTENABLE的bit0~1设置为1即可。配置代码如下:
#define VIC0INTENABLE *((volatile unsigned int *)0x71200010) /* 中断使能寄存器 */
VIC0INTENABLE &= ~(0x00000003); /* 使能外部中断*/
VIC0INTENABLE |= (0x00000003);
6.设置中断号的入口地址
要想设置中断的入口地址,就得找到中断向量地址寄存器。通过上面的讲解可以知道外部中断组0的0~5中断由中断源0、1产生,隶属于VIC0,所以这里的中断向量地址寄存器就是VIC0VECRADDR0和VIC0VECRADDR1,地址如下:
中断向量地址寄存器说明如下:
所以,这里只需要将自定义的中断处理函数的地址赋给VIC0VECRADDR0和VIC0VECRADDR1即可,代码如下:
#define EINT0_3_VECTADDR *((volatile unsigned int *)0x71200100) /* 外部中断0~3向量地址寄存器 */
#define EINT4_7_VECTADDR *((volatile unsigned int *)0x71200104) /* 外部中断4~7向量地址寄存器 */
EINT0_3_VECTADDR = (int)INT_TINT0_ISR; /* 中断产生时,CPU就会自动的将VIC0VECTADDR0的值赋给VIC0ADDRESS并跳转到这个地址去执 */
EINT4_7_VECTADDR = (int)INT_EINT1_ISR; /* INT_TINT0_ISR INT_TINT1_ISR即为中断处理函数 */
7.设置中断号的中断选择,是irq还是fiq,默认为是irq
既然这里说了默认是irq,那我们就是使用默认的中断模式,不去进行设置。如果想要更改,配置中断选择寄存器即可,如下:
8.开启向量中断方式并打开全局中断
开启向量中断方式需要操作系统控制协处理器(P15),而打开全局中断需要操作状态寄存器(CPSR)。别问我是怎么知道的,我不会告诉你是uboot告诉我这么做的。
开启向量中断方式:首先从arm11内核技术参考手册中找到系统控制协处理器章节(3.2节),找到与开启向量中断方式相关的协处理器控制寄存器,如下:
根据协处理器控制寄存器介绍得知,只需将控制寄存器的bit24置1就行了,代码如下:
/* 开启向量中断方式 */
__asm__(
"mrc p15,0,r0,c1,c0,0\n"
"orr r0,r0,#(1<<24)\n"
"mcr p15,0,r0,c1,c0,0\n"
:
:
);
开全局中断:从arm架构参考手册中找到程序状态寄存器相关章节,如下:
红框中这两个位就是关于开启中断的操作为,具体说明如下:
arm11内核技术参考手册中也有相关的介绍,如下:
设置代码如下:
/*
打开全局中断(开总中断)
*/
__asm__(
"mrs r0,cpsr\n"
"bic r0, r0, #0x80\n"
"msr cpsr_c, r0\n"
:
:
);
至此,有关外部中断的初始化就完成,下面就可以进行中断服务函数的设计了。
9.编写中断处理函数,中断函数前和后要使用嵌入汇编,保存环境和恢复环境。中断处理后,要清除中断挂起位和中断执行地址
保存环境:这里需要嵌入汇编,代码如下:
/* 保存坏境 */
__asm__(
"sub lr, lr, #4\n"
"stmfd sp!, {r0-r12, lr}\n"
:
:
);
这是一个固定的代码,目的是为了保存环境,将r0-r12,lr寄存器的值给压入栈中。这里有sub lr,lr,#4。将lr的值减去4。这个原因就要从ARM的流水线说起了。ARM采用流水线,取址,译码,执行。所以pc的值永远是当前执行指令的地址+8。中断跳转的时候,会将pc的值给lr,pc的值为当前执行程序地址+8,lr的值就是pc的值。而返回的地址应该是执行阶段的下一条地址,也就是当前执行程序地址+4,所以直接返回lr的值就不对了,应该返回lr-4的值。
恢复环境:这里也需要嵌入汇编代码,如下:
__asm__(
"ldmfd sp!, {r0-r12, pc}^ \n"
:
:
);
这段代码也是固定的,目的是为了中断执行完后,在将这些值返回给r0-r12,pc寄存器。这样r0-r12寄存器的内容就恢复了,同时pc得到返回地址,就返回到中断前的程序地址去了。
有关于保存环境和恢复环境的代码。参考arm架构参考手册的A2,6章节,如下:
中断处理代码主要实现按键控制led和蜂鸣器运行状态,按下按键s2,点亮led1,再按一次s2,关闭led1;依次类推,详见代码:
/* 外部中断0~3处理函数 */
if((EXT_INT_0_PEND & (1<<0)) == (1 << 0))
led_Toggle(1);
if((EXT_INT_0_PEND & (1<<1)) == (1 << 1))
led_Toggle(2);
if((EXT_INT_0_PEND & (1<<2)) == (1 << 2))
led_Toggle(3);
if((EXT_INT_0_PEND & (1<<3)) == (1 << 3))
led_Toggle(4);
/* 外部中断4~7处理函数 */ if((EXT_INT_0_PEND & (1<<4)) == (1 << 4)) beep_Toggle();if((EXT_INT_0_PEND & (1<<5)) == (1 << 5)){ led_Toggle(0); beep_Toggle();}
中断处理完后,需要将中断挂起位给清零。这里,很简单的将所有中断位都给清零。然后再将中断执行地址给清0。
中断位挂起寄存器如下:
中断向量0地址寄存器如下:
代码如下:
/* 清除中断 */
EXT_INT_0_PEND = ~0x0;
VIC0ADDRESS = 0;
这样,也就完成了S3C6410的外部中断程序设计了。但是,此时的代码下载至Ok6410开发板并不能正常工作,那是因为中断处理函数的代码是用c代码写的,c代码需要什么?需要栈啊。不然怎么时间环境保护和恢复环境了。可能有人会问之前不是设置过栈吗?怎么这里还需要设置了?那是因为之前设置的栈是SVC模式下的栈,而不是irq模式下的栈。不同模式,有自己的备份寄存器,其中,栈SP是每个模式都有自己的。所以需要设置下irq模式下的栈。代码也是比较简单的。在之前的设置栈的汇编代码中,将模式切换为irq模式,再设置sp。
代码如下:
@栈初始化 64M内存用于栈
init_stack:
msr cpsr_c, #0xd2
ldr sp, =0x53000000 @ 初始化r13_irq
msr cpsr_c, #0xd3
ldr sp, =0x54000000 @ 将0x54000000写入sp寄存器中,栈大小64M 0x5400000000-0x500000000
mov pc, lr @ 返回调用处继续往下执行
这样再将代码编译后下载至开发板里就可以正常工作了。
代码下载地址:OK6410实现外部中断控制led与蜂鸣器