第14课.异常与中断

1.ARM异常与中断的处理流程

硬件实现部分
    1.初始化
        a.设置中断源,让它可以产生中断
        b.设置中断控制器(屏蔽,优先级)
        c.设置CPU总开关(使能中断)
    2.执行程序
    3.产生中断
    4.CPU每执行一条代码都会检查有无中断异常
    5.发现有异常时,开始处理
        对于不同的异常,跳到不同的地址去执行;这个地址上,只是一条跳转指令。跳去执行某个函数。
软件实现部分
    6.这些函数处理
        a.保存现场:(各种寄存器)
        b.处理异常:
            分辨中断源
            再调用不同的函数
        c.恢复现场

2.CPU模式,状态与寄存器

CPU的7种mode

普通模式
    1.usr:用户模式
        不可直接进入其他模式
特权模式
    2.sys:系统模式
    3.异常:
        a.und:未定义指令模式
        b.SVC:管理模式
        c.abt:中止模式
            .指令预期中止(运行,解析,读取中的读取)
            .数据访问中止
        d.IRQ:中断模式
        e.FIQ:快中断模式
特权模式可以通过编程操作COSR直接进入其他模式

ARM状态下CPU 7种mode寄存器的使用详细

注:带三角型的为当前mode下的专属寄存器(备份寄存器)
专属寄存器在其工作的mode下用自己的副本,当其切换到其他mode下时,不需要备份。
其他的通用寄存器,在使用它时,需要备份

CPU的状态

ARM STtate : 使用ARM指令集,指令为4byte
Thumb State:使用Thumb指令集,指令为2byte
Thumb指令集可看做ARM指令集的压缩指令集,但效率不如ARM指令集,也不完整

CPSR, SPSR

CPSR: 当前程序状态寄存器
SPSR: 程序状态保存寄存器

[5]: CPU状态位:ARM, Thumb

当发生一个异常,将切换进入相应的工作模式时的流程

进入异常处理
    1.r14(lr)中保存前一个工作模式的下一条代码地址。对于ARM状态,这个值是当前pc值加4或加8(详情见下图)
    2.将CPSR的值复制到SPSR
    3.令CPSR的工作模式为设置为这个异常对应的工作模式
    4.令pc值等于这个异常模式在异常向量表中的地址。即跳转去执行异常向量表中的相应指令
退出异常处理
    1.lr寄存器中保存了前一工作模式的下一条指令地址,将它减去一个适当的值(详情见下图)后赋给pc寄存器
    2.将SPSR的值复制会CPSR
    3.清中断

中断向量表

3.中断框图

SUBSRCPND和SRCPND:寄存器表明有哪些中断被触发了,正在等待处理
SUBMASK(INTSUBMASK寄存器)和MASK(INTMASK寄存器):用来屏蔽某些中断 

1.Request sources(without sub-register)中的中断源被触发后,SRCPND寄存器中的相应位被置为1,如果此中断没有被INTMSK寄存器屏蔽或者快速中断(FIQ),它将进一步处理
2.Request sources(with sub-register)中的中断源被触发之后,SUBSRCPND寄存器中的相应位被置为1,如果此中断没有被INTSUBMASK寄存器屏蔽的话,它在SRCPND寄存器中的相应位也被置为1,之后的处理过程就和Request sources(without sub-register)一样
3.如果触发的中断中有快中断(FIQ)----MOOD(INTMOD寄存器)中为1位的位对应的中断是FIQ,则CPU进入快速中断模式进行处理
4.对于一般中断IRQ,可能同时有几个中断被触发,未被INTMASK寄存器屏蔽的中断经过比较后,选出优先级最高的中断,此中断在INTPND寄存器中的相应位被置为1,然后CPU进入中断模式进行处理。中断服务程序可以通过读取INTPND寄存器或者INTOFFSET寄存器来确定中断源

中断的优先级通过PRIORITY寄存器进行设置 
SUBSRCPND,SRCPND,INTPND: 三个状态寄存器使用完毕后需要被清零(写入1)。从左到右为其清零的顺序。

4.寄存器讲解

SRCPND

每一位被用来表示一个(或一类)中断是否已经发生。若想清楚某一位往此位写入1

INTMSK

INTMSK寄存器只能屏蔽被设置为IRQ的中断,不能屏蔽为设置为FIQ的中断

INTMOD

当INTMOD寄存器中某位被设置为1时,它对应的中断为FIQ,即此中断发生时,CPU将进入快速中断模式。

PRIORITY:优先级寄存器

INTPND

INTOFFSET

SUBSRCPND: 次级源挂起寄存器

INTSUBMSK

5.代码实现及其讲解

知识补充

.word
    在汇编中.word的用法是,在当前位置放入一个word型的值,这个就是word后面的内容
    .word und_addr
bic
    清除操作数为1的位,把其置为0
    bic r0, r0, #0x1f    //清除r0的[0-4]
mrs
    mrs r0, cpsr    // 把CPSR的值读入r0
    msr cpsr, r0    // 把r0的值写入cpsr
.string
    .string "xxx"   // 定义一个字符串xxx

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	*/
	b halt				/*	vector 0x0c :prefetch aboot	*/
	b halt				/*	vector 0x10 :data abort	*/
	b halt				/*	vector 0x14 :reserved	*/
	ldr pc, =irq_addr	/*	vector 0x18 :irq	*/
	b halt				/*	vector 0x1c :fiq	*/

und_addr:				/*	防止代码位于4k之外	*/
	.word do_und

swi_addr:
	.word do_swi

irq_addr:
	.word do_irq

do_und:
	/*	在执行到这里之前:
	 *	1.lr_und保存有被中断模式中的下一条即将执行的指令地址
	 *	2.SPSR_und保存有被中断模式的CPSR
	 *	3.CPSR中的M4-M0被设置为11011,进入到und模式
	 *	4.跳到0x4的地方执行程序
	 */

	/*	sp_und未设置,先设置它
	 *	因为后续使用到了c语言,必须设置栈
	 */
	ldr sp, =0x34000000

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

	/*	保存现场	*/
	/*	处理und异常	*/
	mrs r0, cpsr
	ldr r1, =und_string
	bl  printException

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

und_string:
	.string "undefined instruction exception"

.align 4

do_swi:
	/*	在执行到这里之前:
	 *	1.lr_swi保存有被中断模式中的下一条即将执行的指令地址
	 *	2.SPSR_swi保存有被中断模式的CPSR
	 *	3.CPSR中的M4-M0被设置为11111,进入到svc模式
	 *	4.跳到0x8的地方执行程序
	 */

	/*	sp_und未设置,先设置它
	 *	因为后续使用到了c语言,必须设置栈
	 */
	ldr sp, =0x33e00000

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

	/*	保存现场	*/
	/*	处理und异常	*/
	mrs r0, cpsr
	ldr r1, =swi_string
	bl  printException

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

swi_string:
	.string "swi exception"

.align 4

do_irq:
	/*	在执行到这里之前:
	 *	1.lr_irq保存有被中断模式中的下一条即将执行的指令地址
	 *	2.SPSR_irq保存有被中断模式的CPSR
	 *	3.CPSR中的M4-M0被设置为10010,进入到irq模式
	 *	4.跳到0x18的地方执行程序
	 */

	/*	sp_und未设置,先设置它
	 *	因为后续使用到了c语言,必须设置栈
	 */
	ldr sp, =0x33d00000

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

	/*	处理und异常	*/
	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

	ldr pc, =sdram

sdram:
	...

interrupt.c

#include "s3c2440_soc.h"

/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTMSK 用来屏蔽中断, 1-masked
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTOFFSET : 用来显示INTPND中哪一位被设置为1
 */

/*	初始化中断控制器	*/
void interrupt_init(void)
{
	INTMASK &= ~((1<<0) | (1<<2) | (1<<5));
}

/*	初始化按键,设为中断源	*/
void key_eint_init(void)
{
	/*	配置GPIO为中断源	*/
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |= ((2<<0) | (2<<4));	/*	S2, S3被设置为中断引脚	*/

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCOM |= ((2<<6) | (2<<22));	/*	S4, S5被设置为中断引脚	*/
	
	/*	设置中断触发方式: 双边沿触发	*/
	EXTINT0 |= (7<<0) | (7<<8);		/*	S2, S3	*/
	EXTINT1 |= (7<<12);				/*	S4	*/
	EXTINT2 |= (7<<12);				/*	S5	*/
	
	/*	设置EINTMASK使能eint11,eint19	
	 *	因为eint0-eint3能直接传到中断控制器
	 */
	EINTMASK &= ~((1<<11) | (1<<19));
}

/*	读EINTPEND分辨那个eint产生(eint4~eint23)
 *	清中断时,写EINTPEND的相应位
 */
 
void key_eint_irq(int irq)
{
	unsigned int val  = EINTPEND;
	unsigned int val2 = GPFDAT;
	unsigned int val3 = GPGDAT;

	if(irq == 0)	/* s2 --> gpf6 */
	{
		if(val1 & (1<<0))
		{
			/*	松开	*/
			GPFDAT |= (1<<6);
		}
		else
		{
			/*	按下	*/
			GPFDAT &= ~(1<<6);
		}
	}
	else if(irq == 2)	/* eint2 : s3 控制 D11 */
	{
		if (val1 & (1<<2)) /* s3 --> gpf5 */
		{
			/* 松开 */
			GPFDAT |= (1<<5);
		}
		else
		{
			/* 按下 */
			GPFDAT &= ~(1<<5);
		}
		
	}
	else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
	{
		if (val & (1<<11)) /* eint11 */
		{
			if (val2 & (1<<3)) /* s4 --> gpf4 */
			{
				/* 松开 */
				GPFDAT |= (1<<4);
			}
			else
			{
				/* 按下 */
				GPFDAT &= ~(1<<4);
			}
		}
		else if (val & (1<<19)) /* eint19 */
		{
			if (val2 & (1<<11))
			{
				/* 松开 */
				/* 熄灭所有LED */
				GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
			}
			else
			{
				/* 按下: 点亮所有LED */
				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,2,eint8_23	*/
	{
		key_eint_irq(bit);		/*	处理中断,清中断源EINTPEND	*/
	}

	/*	清中断: 从源头清	*/
	SRCPND = (1<<bit);
	INTPND = (1<<bit);
}

main.c

...

int main(void)
{
	led_init();
	interrupt_init();  /* 初始化中断控制器 */
	key_eint_init();   /* 初始化按键, 设为中断源 */

	...
}

6.定时器简介

S3C2440共有5个16位的定时器。其中定时器0,1,2,3有PWM功能。定时器4没有PWM功能输出引脚
定时器器件的时钟源为PCLK,首先通过两个8位的预分频器降低频率:定时器0,1共用第一个预分频器,定时器2,3,4共用一个预分频器。预分频器的输出将进入第二级分频器,它们输出5种频率的时钟:2分频,4分频,8分频,16分频或者外部时钟TCLK0/TCLK1。每个定时器的工作时钟可以从这5种频率中选择
这两个预分频都可以通过TCFGO寄存器来设置,每个定时器工作在那种频率下也可以通过TCFG1寄存器来选择

7.定时器内部控制逻辑

1.每来一个时钟(clk)TCNTn减1
2.当TCNTn = TCMPn时,可以让对应的PWM引脚翻转
3.TCNTn继续减1,当TCNTn = 0时。可以产生中断,PWM引脚再次翻转
4.TCNTn = 0时,可自动加载初值

注:
    Timer clk = PCLK / (prescaler valuer + 1) / (divider valuer)
                        TCFG0(0~255)            1,2,4,8,16
    eg:
        50MHz / (99 + 1) / 16 = 31500
        每计数一次用时:1/31500
        用时0.5s需计数15625次

8.定时器的使用逻辑

1.设置时钟
2.设置初值
3.加载初值,启动Timer
4.设置为自动加载
5.中断相关设置

9.定时器寄存器

TCFG0

位[7:0]、位[15:8]分别被用于控制预分频器0、1,它们的值为0~255

TCFG1

经过预分频器得到的时钟被2分频、4分频、8分频、16分频、除这4种频率外,额外地,定时器0、1还可以工作在外接的TCLK0时钟下,定时器2、3、4还可以工作在外接的TCLK1时钟下

TCNTBn/TCMPBn

这两个寄存器都只用到了[15:0],它们在启动定时器时就被传到定时器内部寄存器TCNTn、TCMPn中。没有TCMPB4,因为定时器4没有PWM输出引脚。
TCNTBn中保存定时器的初始计数值;
TCMPBn中保存比较值。

TCNTOn

定时器n被启动后,内部寄存器TCNTn在其工作时钟下不断减1计数,可以通过读取TCNTOn寄存器得知其值

TCON

注:
    第一次使用定时器时,需要设置“手动更新”位为1以使TCNTBn/TCMPBn寄存器的值装入内部寄存器TCNTn、TCMPn中。下一次如果还要设置这一位需要先将它清0。

10.代码实现

interrupt.c

typedef void(*irq_fun)(int);	/*	这里typedef定义了一个irq_func的类型	*/
irq_func irq_array[32];

...
	
void handle_irq_c(void)
{
	/*	分辨中断源	*/
	int bit = INTOFFSET;
	
	/*	调用对应的处理函数	*/
	irq_array[bit](bit);

	/*	清中断: 从源头开始清	*/
	SRCPND = (1 << bit );
	INTPND = (1 << bit );
}

void register_irq(int irq, irq_fun fb)
{
	irq_array[irq] = fb;
	
	INTMSK &= ~(1<<irq);		/*	使能中断	*/
}

/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{
	/* 配置GPIO为中断引脚 */
	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);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

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

	register_irq(0, key_eint_irq);
	register_irq(2, key_eint_irq);
	register_irq(5, key_eint_irq);
}

timer.c

void timer_irq(void)
{
	/*	点灯计数	*/
	static int cnt = 0;
	int tmp;

	cnt++;

	tmp = ~cnt;
	tmp &= 7;
	GPFDAT &= ~(7<<4);
	GPFDAT |= (tmp<<4);
}

void timer_init(void)
{
	/*	设置TIMER0的时钟	*/
	/*	Timer clk = PCLK / (prescaler value + 1) / (divider value)	
	 *			  = 50000000 / (99 + 1) / 16
	 *			  = 31250
	 */		
	TCFG0 = 99;		/*	Prescaler 0 = 99,用于定时器0,1	*/
	TCFG1 &= ~0xf;	/*	清零	*/
	TCFG1 |= 3;		/*	16分频	*/

	/*	设置TIMER0的初值	*/
	TCNTB0 = 15625; /*	0.5s中断一次*/

	/*	加载初值,启动timer0	*/
	TCON |= (1 << 1);		/*	此时为手动设置加载值	*/

	/*	设置为自动加载并启动
	 *	bit0: start
	 *	bit3: auto reload
	 */
	TCON &= ~(1<<1);
	TCON |= (1<<0) | (1<3);	

	/*	设置中断	*/
	register_irq(10, timer_irq);
}
posted @ 2020-01-20 10:15  人民广场的二道贩子  阅读(318)  评论(0编辑  收藏  举报