S3C2440中的异常与中断

1、异常与中断的简单介绍与实现

S3C2440中一共有7种模式(如图1),其中异常模式有svc(管理模式)、abt(中止模式)、und(未定义指令模式)、irq(中断模式)和 fiq(快中断模式)5种,中止模式又分为指令预取中止和数据访问中止;usr(用户模式)不可直接切换到其他模式,而其他6种模式都可以通过修改CPSR[4:0]与其他模式进行切换(参考图3和图4)。
之所以会有这么多的异常模式是为了能更好地处理程序遇到的各种异常,在ARM状态下(S3C2440的CPU有ARM状态和THUMB状态之分)这些异常模式的差别如图2所示。在supervisor、abortt、iqr和undefine中,R0-R12 和 CPSR是通用的寄存器,R13-R15 和 SPSR是各自私有的寄存器;而在fiq中 R0-R7和 CPSR是通用的寄存器,R8-R15和SPSR是自己私有的;user和system中的 R0-R15和CPSR都是通用的寄存器,无私有的SPSR。
中断是异常的一种情况,它是我们想要的,而其他的异常却是我们不太愿意见到的。

图 1
图 2
图 3
图 4

为了处理不同的异常(中断)CPU也是耗费了脑筋,那么CPU到底是怎么去处理异常(中断)的呢?下面进行简单地介绍(参考图7和图8):

  1. 每执行完一条指令CPU就会去检测有无异常(中断)产生;
  2. CPU一检测到有异常(中断)产生就会针对不同的异常(中断)跳到不同的地址去处理,这些地址被称为中断向量表(如图5);
  3. 在进入到具体的异常(中断)处理程序之前,CPU会将被中断处的下一条指令的地址保存在相应异常(中断)自己的LR寄存器里,将被中断程序的CPSR保存到相应异常(中断)自己的SPSR中同时将CPSR的低5位修改成自己的值(如图4),最后CPU强制将PC设为相应异常(中断)的向量地址(如图5);
  4. 保护现场:先根据图6修改LR的值,然后将可能被修改的通用寄存器(fiq: R0-R7;其他:R0-R12)和LR保存在相应异常(中断)自己的栈中;处理异常:执行异常(中断)处理函数;恢复现场:将被保存的寄存器从栈中弹出并将LR的值赋给PC,以让程序从被中断处的下一条指令开始执行。
图 5
图 6
图 7
图 8

下面是未定义指令异常和软件中断的代码示例(start.S):


.text
.global _start

_start:
	b reset				/* vector 0 : reset */
	ldr pc, und_addr	/* vector 4 : und */
	ldr pc, swi_addr	/* vector 8 : swi */

und_addr:		/* 存放跳转指令,以防前面部分超过4k从而导致不能NAND启动 */
	.word do_und	
	
swi_addr:
	.word do_swi

do_und:
	/* 执行到这里之前:
	 * 1. lr_und中保存了被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_und保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为11011,进入到und模式
	 * 4. 跳到0x4的地方执行程序
	 */
	
	/* 设置sp_und */
	ldr sp, =0x34000000		/* SDRAM为64M,将SP指向它的最顶端 */

	/* 在und异常处理函数中可能会修改r0-r12,要先保存它们 
	 * lr是异常模式处理函数完后的返回地址,也要保存
	 */	
	stmdb sp!, {r0-r12, lr}

	/* 处理异常 */
	mrs r0, cpsr
	ldr r1, =und_string
	bl printException

	/* 恢复现场 */
	ldmia sp!, {r0-r12,pc}^		/* ^会把spsr的值复制到cpsr */

und_string:
	.string "undefine instruction exception!"

.align 4	/* 4字节对齐 */

do_swi:
	/* 执行到这里之前:
	 * 1. lr_svc中保存了被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_svc保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为10011,进入到swi模式
	 * 4. 跳到0x8的地方执行程序
	 */
	
	/* 设置sp_und */
	ldr sp, =0x33e00000		/* 不能与前面设的SP相同 */

	/* 在swi异常处理函数中可能会修改r0-r12,要先保存它们 
	 * lr是异常模式处理函数完后的返回地址,也要保存
	 */	
	stmdb sp!, {r0-r12, lr}

	mov r4, lr	/* 为得到软件中断的值,r4根据ATPCS调用规则选取 */

	/* 处理异常 */
	mrs r0, cpsr
	ldr r1, =swi_string
	bl printException
	
	sub r0, r4, #4
	bl printSwiVal

	/* 恢复现场 */
	ldmia sp!, {r0-r12,pc}^		/* ^会把spsr的值复制到cpsr */

swi_string:
	.string "swi instruction exception!"

.align 4
reset:
.../* 部分省略 */
	/* 复位之后,CPU切换到svc模式
	 * 现在切换到usr模式
	 */
	mrs r0, cpsr
	bic r0, r0, #0xf	/* 修改M4-M0为0b10000, 进入usr模式 */
	msr cpsr, r0

	/* 设置sp_usr */
	ldr sp, =0x33f00000
.../* 部分省略 */
und_code:
	.word 0xdeadc0de		/* 未定义指令 */

	swi 0x123		/* 软件中断 */
.../* 部分省略 */

其中的printException和printSwiVal如下,

void printException(unsigned int cpsr, char *str)
{
	puts("Excption! cpsr= ");
	printHex(cpsr);
	puts(" ");
	puts(str);
	puts("\n\r");
}

void printSwiVal(unsigned int *pSWI)
{
	puts("swi val= ");
	printHex(*pSWI & ~0xff000000);
	puts("\n\r");
}

2、按键中断控制LED亮灭

在我之前的博文用LED实现流水灯和用按键控制LED灯的亮灭中设置过用按键控制LED亮灭的程序,但是前面我们是将按键设为的输入,并且是在主程序中通过不断地去检测按键所连引脚处输入的状态来决定灯该亮该灭,这极大的浪费了CPU的资源。为此我们即将让他变身为中断模式,当有按键按下时产生中断去执行点灯,无按键按下时CPU继续执行其他程序。
下面让我们先梳理用按键中断控制LED亮灭的过程:

  1. 初始化按键。将连接按键的引脚设为外部中断模式,设置对应引脚的中断触发方式(在图9中可以知到对应按键的外部中断号),根据图10设置EINTMASK的11bit和19bit为0,使能中断源。
    EINT0-EINT3不需要单独设置。
图 9
图 10
  1. 初始化中断。通过阅读S3C2440的芯片手册知道在这里我们只需要设置中断控制器中INTMASK寄存器的0bit/2bit/5bit为0以启用对应的外部中断。
图 11
  1. 清除CPSR中的 I 位(如图3)以使能中断。

以下为代码部分(strat.S,在上文的基础上添加了部分代码)

.text
.global _start

_start:
	b reset				/* vector 0x0 : reset */
	ldr pc, und_addr	/* vector 0x4 : undefined instruction */
	ldr pc, swi_addr	/* vector 0x8 : software interrupt */
	b halt				/* vector 0x0c : prefetch abort */
	b halt				/* vector 0x10 : data abort */
	b halt				/* vector 0x14 : reserved */
	ldr pc, irq_addr	/* vector 0x18 : irq */
	b halt	
.../* 部分省略 */
irq_addr:
	.word do_irq
.../* 部分省略 */
do_irq:
	/* 执行到这里之前:
	 * 1. lr_irq中保存了被中断模式中的下一条即将执行的指令的地址
	 * 2. SPSR_irq保存有被中断模式的CPSR
	 * 3. CPSR中的M4-M0被设置为10010,进入到irq模式
	 * 4. 跳到0x18的地方执行程序
	 */
	 
	/* 设置sp_irq */
	ldr sp, =0x33d00000	

	/* 在irq异常处理函数中可能会修改r0-r12,要先保存它们 
	 * lr-4是异常模式处理函数完后的返回地址,也要保存
	 */	
	sub lr, lr, #4
	stmdb sp!, {r0-r12, lr}

	/* 处理异常 */
	bl handle_irq_c

	/* 恢复现场 */
	ldmia sp!, {r0-r12, pc}^		/* ^会把spsr的值复制到cpsr */

.reset:
.../* 部分省略 */
	/* 复位之后,CPU切换到svc模式
	 * 现在切换到usr模式
	 */
	mrs r0, cpsr
	bic r0, r0, #0xf	/* 修改M4-M0为0b10000, 进入usr模式 */
	bic r0, r0, #(1<<7)	/* 清除 I 位,使能中断 */
	msr cpsr, r0

	/* 设置sp_usr */
	ldr sp, =0x33f00000
.../* 部分省略 */

其中handle_irq_c函数在 interrupt.c 中,如下,


#include "s3c2440_soc.h"

/* SRCPND, 用来显示哪个中断产生了,通过向相应位写入数据来清除对应中断源
 * SRCPND[0] : eint0
 * SRCPND[2] : eint2
 * SRCPND[5] : eint8-eint23
 */

/* INTMSK, 用来屏蔽中断, 1 : masked
 * INTMSK[0] : eint0
 * INTMSK[2] : eint2
 * INTMSK[5] : eint8-eint23
 */

/* INTPND, 用来显示当前优先级最高、正在发生的中断,通过向相应位写入数据来清除
 * INTPND[0] : eint0
 * INTPND[2] : eint2
 * INTPND[5] : eint8-eint23
 */

/* 通过读取 INTOFFSET 的值可以确定 INTPND 中哪一位被设置为 1 了 */

/* 初始化中断控制器 */
void interrupt_init(void)
{
	/* 配置GPIO为中断引脚 */
	INTMSK &= ~((1<<0) | (1<<2) | (1<<5));	
}

/* 初始化按键,设为中断源 
 * eint0--s2, eint2--s3, eint11--s4, eint19--s5
 * eint0--GPF0, eint2--GPF2, eint11--GPG3, eint19--GPG11
 */
void key_eint_init(void)
{
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |=  ((2<<0) | (2<<4));	/* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |=  ((2<<6) | (2<<22));	/* S4,S5被配置为中断引脚 */

	/* 设置中断触发方式,双边沿触发 */
	EXTINT0 |= (7<<0) |  (7<<8);	/* eint0, eint2 */
	EXTINT1 |= (7<<12);				/* eint11 */
	EXTINT2 |= (7<<12);				/* eint19 */

	/* 使能eint11,eint19 */
	EINTMASK &= ~((1<<11) | (1<<19));
}

/* 读EINTPEND分辨哪个EINT 产生(eint4-eint3) 
 * 清除中断时,写EINTPEND的相应位
 */

void key_eint_irq(int irq)
{
	unsigned int val = EINTPEND;

	if (irq == 0)	/* eint0, s2(GPF0) 控制 D12(GPF6) */
	{
		if ((GPFDAT & (1<<0)) == 0 )
		{
			/* 按下,点亮D12 */
			GPFDAT &= ~(1<<6);
		}
		else
		{
			/* 松开,熄灭D12 */
			GPFDAT |= (1<<6);
		}
	}
	else if (irq == 2)	/* eint2, s3(GPF2) 控制 D11(GPF5) */
	{
		if ((GPFDAT & (1<<2)) == 0 )
		{
			/* 按下,点亮D11 */
			GPFDAT &= ~(1<<5);
		}
		else
		{
			/* 松开,熄灭D11 */
			GPFDAT |= (1<<5);
		}
	}
	else if (irq == 5)	
	{
		if (val & (1<<11) )	/* eint11, s4(GPG3) 控制 D10(GPF4) */
		{
			if ((GPGDAT & (1<<3)) == 0 )
			{
				/* 按下,点亮D10 */
				GPFDAT &= ~(1<<4);
			}
			else
			{
				/* 松开,熄灭D10 */
				GPFDAT |= (1<<4);
			}
		}
		else if (val & (1<<19) )	/* eint19, s5(GPG11) 同时控制 D10/D11/D12 亮灭 */
		{
			if ((GPGDAT & (1<<11)) == 0 )
			{
				/* 按下,点亮所有灯 */
				GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
			}
			else
			{
				/* 松开,熄灭所有灯 */
				GPFDAT |=  ((1<<4) | (1<<5) | (1<<6));
			}
		}
	}

	/* 清源中断 */
	EINTPEND = val;	
}

void handle_irq_c(void)
{
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的中断处理函数 */
	if (bit ==0 || bit == 2 || bit == 5)	/* eint0\eint2\eint8-eint23 */
	{
		key_eint_irq(bit);		/* 处理中断,清源中断EINTPEND */
	}

	/* 清中断 */
	SRCPND = (1<<bit);
	INTPND = (1<<bit);
	
}

3、定时器中断的实现以及对中断初始化函数的升级

在实现定时器中断前我们要先对定时器做如下初始化(这里我们选择TIMER0)

  1. 设置TIMER0的时钟,根据图12得知我们需要设置预分频器(Prescaler)和分频器(Clock Divider),他们的值是根据我们的需要进行设置的;他俩所在对应寄存器中的位数以及时钟的计算公式从图13和图14中可以得到;
图 12
图 13
图 14
  1. 对TIMER0设置计数初值,根据我们需要定的时长对TCNTB0设值(这里我们没有用到TCMPB0,故不进行设置),这里需要注意TCNTB0的位数。
图 15
  1. 装载初值,刚开始需要手动更新初值,先将TCON[1]置1。
  2. 先清除手动更新位,再设为自动装载并启动定时器。
图 16

具体示例代码如下(timer.c)


#include "s3c2440_soc.h"

void timer_irq(int irq)
{
	static int statu = 7;	/* 初始三盏灯全熄灭,0b111 */
	
	/* 循环点亮LED0\1\2,对应GPF4\5\6 */
	GPFDAT &= ~(7<<4);
	GPFDAT |= (statu<<4);
	switch (statu)	/* 状态切换 */
	{
		case 7: statu = 6; break;	/* 状态1: 6 = 0b110, 点亮D10 */
		case 6: statu = 5; break;	/* 状态2: 5 = 0b101, 点亮D11 */
		case 5: statu = 3; break;	/* 状态3: 3 = 0b011, 点亮D12 */
		case 3: statu = 6; break;	/* 返回状态 1 以达到循环的目的 */
		default: statu = 7;			/* 如果都不是则熄灭D10\D11\D12 */
	}
}

void timer_init(void)
{
	/* 设置 TIMER0 时钟 
	 * Timer clk = PCLK / {prescaler value+1} / {divider value}
	 *			 = 50000000 / (99 + 1) / 16
	 *			 = 31250
	 */
	TCFG0 = 99;		/* prescaler0 = 99  */
	TCFG1 = 3;		/* MUX0 : 1/16 */
	
	/* 设置 TIMER0 初值 */
	TCNTB0 = 15625;	/* 0.5s 中断一次 */

	/* 加载初值,手动更新 TCONB0 */
	TCON |= (1<<1);
	
	/* 设为自动装载并启动定时器,先清除手动更新位 */
	TCON &= ~(1<<1);
	TCON |=  ((1<<0) | (1<<3));

	/* 设置中断 */
	register_irq(10, timer_irq);
}

(定时器中断服务子程序中这里我设置的是跑马灯,你也可以实现其他的功能)。
上面程序中的register_irq(10, timer_irq);就是我们对中断初始化函数做的升级了(它大大减少了我们每次执行新中断时对代码的修改),其具体实现如下(其中的handle_irq_c函数我们也做了修改,它们都是对上文interrupt.c的修改)

typedef void (*irq_func)(int);	/* 定义函数指针 */
irq_func irq_array[32];		/* 指针数组 */

/* 初始化中断控制器 */
void register_irq(int irq, irq_func fp)
{
	irq_array[irq] = fp;
	INTMSK &= ~(1<<irq);
}

void handle_irq_c(void)
{
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的中断处理函数 */
	irq_array[bit](bit);

	/* 清中断 */
	SRCPND = (1<<bit);
	INTPND = (1<<bit);
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值