中断
中断控制器
本篇讲一讲中断,上篇中已经讲述了异常和异常的处理方式,中断是异常的一种,因此处理方式也大同小异,与其他异常的主要差别是,存在中断控制器这一角色。
中断的流程是中断源->中断控制器->处理函数,中断源有很多种类,可以是串口中断、时钟中断或者是外部中断。而控制器则控制着中断是否屏蔽、中断的优先级以及中断的状态。处理函数则与异常的处理函数无异,只要根据自己需要进行编写即可。
上图为中断控制器框图,最左为中断源,分成了两种中断源,区别仅在于sub source有多两个寄存器SUBSRCPND和SUBMASK来控制中断。中断源归于哪一类在S3C2440中有表列出:
这里主要讨论的是source中断源,可以看到描述一列中有具体的中断源描述,可以根据自己需要一一对应,但是最重要理解的还是Arbiter一列中的分组,source中断源被分成了5组,每组最多不超过6个中断源,最终分配结果如下图,这种分配方式主要是为了中断优先级的实现,每一组的ARBITER存在两个控制信号:1bitARB_mode,2bitsARB_SEL,其中ARB_SEL用来选择中断优先级排列的方式。可以看到如下图,ARB_SEL两位的四种组合所表示的四种优先级顺序,其中REQ0为固定的最高优先级,相对应的REQ5为固定的最低优先级,而REQ1-4则随着ARB_SEL的增大循环左移,我们可以直接设置ARB_SEL来确定优先级顺序,但是优先级顺序还受到ARB_mode影响,当ARB_mode为0时,我们通过ARBS_SEL确定的优先级顺序则永久固定,除非我们再去修改。当ARB_mode为1时,每一次响应中断,都会让ARB_SEL增大1,即循环左移一个,从而导致最高优先级的中断变为最低优先级,从而保证较低优先级的中断能够有机会得到响应,防止饥饿,这种也叫轮转调度,跟os的调度类似,因此这里ARB_mode也可以说是轮转调度的开关。
中断控制器针对source中断源来说有5中控制寄存器:source pending register, interrupt mode register, mask
register, priority register和interrupt pending register。可以和文首的框图一一对应上。
其中source pending register和interrupt pending register类似,用来表示哪种中断触发了,当中断触发,对应中断位就会置1,差别在于SRCPND会将所有触发的中断置位,而INTPND只会对当前需要处理的最高优先级的中断置位,SRCPND通过MASK屏蔽和优先级寄存器筛选出最高优先级中断给到INTPND,INTPND接收到之后进入中断处理。需要注意的是,这两个寄存器需要在处理完成之后清除,否则会一直响应已经处理过重复的中断。且需要从源头开始SRCPND->INTPND,这样才不会清除INTPND后SRCPND再次置位。置位的方式是在对应的位上置1即可。
MASK register用来屏蔽中断,当中断源对应位上置1,则表示系统不响应中断,即使SRCPND有中断进来且置位,也不做响应,但是如果INTPND直接置位了,还是需要响应处理的(INTPND在MASK之后,不受MASK控制)。
Mode register用来控制中断是普通中断还是快中断,在上一节中已经说明FIQ因为有较多自己的专属寄存器,因此保存现场和恢复现场的开销较小,响应速度也比较快,因此被称为快中断。这里默认值为0,即默认情况下位普通中断。
priority register上文中已经讲过了,只需要根据需要开启轮转开关,设置中断优先级顺序即可。
除了上述控制器外,如果需要系统响应中断,还需要把上节中提到的CPSR中的I-bit 或者F-bit开启,这是系统的中断总开关,方法为将I-bit或者F-bit清0。
补充一点,S3C2440为了方便中断处理函数中分辨中断源,设置了INTOFFSET寄存器,不同中断源有不同的offset value,直接用标准值比较该寄存器即可。
外部中断
除了中断控制器需要配置,中断源也需要进行配置。这里以外部中断为例,外部中断触发开关为按键,期望为按键按下,触发中断,LED点亮。
在https://blog.csdn.net/G_METHOD/article/details/104271762中也编写过按键的内容,那次是使用轮询的方式来进行按键状态检测,比较消耗CPU资源,而使用外部中断的方式可以让无事件发生时CPU处理其他事情,而有事件触发时,又能够及时处理。copy一下按键的原理图和键脚对应的表格。
EINT0 | EINT2 | EINT11 | EINT19 |
GPF0 | GPF2 | GPG3 | GPG11 |
上次我们将其设置为输入引脚从而可以读引脚获取按键状态,这次需要用到外部中断,则需要将其设置为对应的外部中断模式。从下图可知,只要将GPxCON对应的引脚设置为外部中断引脚即可。
除了GPIO的寄存器需要设置,还需要设置外部中断的寄存器。如下,设置EXTINT可以指定外部中断的触发方式为低电平、高电平、上升沿、下降沿或者双边沿触发。这里我们选择使用双边沿触发,根据自己需要或者喜好进行选择即可。
需要注意的是有些外部中断引脚存在滤波,玩过32或者51的外部中断可以知道,按键按下电平是会存在抖动的,如果不对抖动处理,则一个触发中断的按键按下动作可能会触发多个中断事件,比如按键用来点灯,但是抖动触发了多一次,让你每次按下按键灯都亮灭一次。防抖动的方式可以是滤波,滤波这里粗暴的方式就是一段延时等待电平稳定,这里芯片帮我们做了滤波,当然可以写寄存器来关闭滤波功能,也有其他寄存器能够配置滤波的时钟和时间。
与中断控制器比较相似的是,外部中断寄存器也存在mask寄存器和PEND寄存器,同样,mask用来屏蔽中断,而PEND指示那一路外部中断触发。同样地,处理完中断之后需要清除PEND相应位。
上述中断寄存器的控制作用于中断源,但是中断处理函数中,在判断中断源时,如果使用中断控制器的PND不能判断出具体的中断源时(如EINT存在几个外部中断共用一个标志位),此时就需要借助外部中断寄存器PND来分辨具体是哪一个中断源了。
根据上述内容输出代码如下:
.text
.global _start
_start:
B RESET
LDR pc,UNDEFIE
LDR pc,SW_INTERRUPT
B ABORT_PREFETCH
B ABORT_DATA
B halt //reserve
LDR pc,INTERRUPT
B FIQ_HANDLE
UNDEFIE:
.word DO_UND
SW_INTERRUPT:
.word DO_SWI
INTERRUPT:
.word DO_INTERRUPT
ABORT_PREFETCH:
ABORT_DATA:
IRQ_HANDLE:
FIQ_HANDLE:
B halt
DO_UND:
//1.设置栈
LDR SP,=0x34000000
//2.保存现场
STMDB SP!,{R0-R12,LR}
//处理函数
MRS R0,CPSR
LDR R1,=UND_TEST_STRING
BL ExecptionHandle
//3.恢复现场,跳转回原来的位置
LDMIA SP!,{R0-R12,PC}^ //^ 表示将SPSR恢复到CPSR中
DO_SWI:
//1.设置栈
LDR SP,=0x33e00000
//2.保存现场
STMDB SP!,{R0-R12,LR}
//处理函数
MRS R0,CPSR
LDR R1,=SWI_TEST_STRING
SUB R2,LR,#4
BL SWIHandler
//3.恢复现场,跳转回原来的位置
LDMIA SP!,{R0-R12,PC}^ //^ 表示将SPSR恢复到CPSR中
DO_INTERRUPT:
//1.设置栈
LDR SP,=0x33d00000
//2.保存现场
SUBS LR, LR, #4
STMDB SP!,{R0-R12,LR}
//处理函数
BL InterruptHandler
bl testPrint1
//3.恢复现场,跳转回原来的位置
LDMIA SP!,{R0-R12,PC}^ //^ 表示将SPSR恢复到CPSR中
UND_TEST_STRING:
.string "enter undefin mode!\n"
SWI_TEST_STRING:
.string "enter swi mode!\n"
.align 4
RESET:
MOV R0,#0
LDR R1,[R0]
STR R0,[R0]
LDR R2,[R0]
CMP R2,R0
LDR SP,=0x40000000+4096
MOVEQ SP,#4096
STREQ R1,[R0]
BL HardwareInitAll
BL UartInit
TEST_UND:
bl testPrint
.word 0xdeadc0de
//bl testPrint
TEST_SWI:
MRS R0,CPSR
BIC R0,R0,#0x0F
MSR CPSR,R0
SWI 0x123
LDR pc,=main
halt:
B halt
interrupt.h
----------------------
#ifndef __INTERRUPT_H
#define __INTERRUPT_H
void AllInterruptInit(void);
void InterruptHandler(void);
#endif
interrupt.c
-------------------------------
#include "interrupt.h"
#include <stdint.h>
#include "led.h"
#include "s3c2440.h"
static void InterruptControllerInit(void)
{
//开启全局中断开关
asm(
"MRS R0,CPSR \n\t"
"BIC R0,R0,#0x80 \n\t"
"MSR CPSR,R0 \n\t"
);
//关闭相关中断的屏蔽
INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
}
static void ExternInterruptInit(void)
{
//设置为外部中断引脚
GPFCON &= ~((3<<0)|(3<<4));
GPFCON |= (2<<0)|(2<<4);
GPGCON &= ~((3<<6)|(3<<22));
GPGCON |= (2<<6)|(2<<22);
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12);
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
}
void AllInterruptInit(void)
{
InterruptControllerInit();
ExternInterruptInit();
}
void ExtInterruptHandler(uint32_t irq)
{
uint32_t ext_interrupt_bit = EINTPEND;
switch(irq)
{
case 0:
{
ToggleLed(kLed1);
break;
}
case 2:
{
ToggleLed(kLed2);
break;
}
case 5:
{
if(ext_interrupt_bit &( 1 << 9 ))
{
ToggleLed(kLed3);
}
else if(ext_interrupt_bit &( 1 << 11 ))
{
ToggleLed(kLed1);
ToggleLed(kLed2);
ToggleLed(kLed3);
}
break;
}
default:
break;
}
EINTPEND = ext_interrupt_bit;
}
void InterruptHandler(void)
{
uint32_t interrupt_bit = INTOFFSET;
if(interrupt_bit>=0 && interrupt_bit <=5)
{
ExtInterruptHandler(interrupt_bit);
}
SRCPND = 1<< interrupt_bit;
INTPND = 1<< interrupt_bit;
}
编写这段代码实际上工作量不大,有些寄存器直接使用默认值而不加修改,但是仍然卡了我很久,主要是按下按键执行中断函数后总是会莫名其妙跑飞或者莫名重启,最后看dis才发现是上一节中讲到的不同异常的返回值需要不同处理的这点,中断的返回值需要减去4才能返回到程序的正常流程。
定时器
定时器从名字可以知道是用来计时的,把它放在中断这里是因为他存在中断的功能,而非定时器只有中断的功能。定时器通过设定一定时间后产生中断可以用来周期性或者时间性地执行任务,另外一方面,通过定时器的比较寄存器能够让定时器引脚输出PWM波,供电机或者其他需要控制能量的外设使用,呼吸灯就可以通过该功能实现,同时也能够将其输出作为外设的时钟源。
S3C2440有5个16位时钟,不同时钟估计因为成本或者实现难度的原因,功能存在阉割或者共用资源的情况。TIMER0是功能最完整的一个定时器。
从左往右看,可以看到定时器使用PCLK为驱动时钟,之前的内容中已经将PCLK设置为50MHz。PCLK之后存在一个8位的预分频器,可以根据需要分频使用,即降低使用的时钟频率,其后还有divider可以在次降低频率,这里的分频通过设置MUX寄存器选择一个分频数。定时器存在TCMPB、TCNTB寄存器,其中TCMPB为比较寄存器,用于与当前计数值比较从而输出PWM波,TCNTB则为定时器设置的初值,当定时器计数值为0时,且我们设置了自动加载,此时TCNTB则会自动赋值到计数寄存器中。还要说明的是这两个寄存器可以随时修改,但是不会随时生效,需要在下一个计时周期开始的时候才能生效,这样能够保证当前正在进行的计数能够正常运行。随后出现的是数字电路中的取反,后面跟随的梯形是选择器,由一个invert bit来控制输出的波形是否取反。Dead Zone Generator死区产生器,是用于PWM带大功率电机时的一种保障措施,经典的应用在H桥电路控制电机。大致示意图如下(手头没有AD先用画图简单画一下···),中间M为电机,使用三极管作为放大电路器件,当A给导通电压时,B给0电势,此时A的电路导通,假设此时电机正转对应下图的第二种情况。当B给导通电压,而A给0时,B侧电路导通,电机此时反转,对应下图第三种情况。当电机需要从正转转为反转或者相反转换时,势必会经过一个A和B的电压交换时间,而真实电压都是模拟量,不可能突变。此时如果没有一段死区时间,则可能A和B同时处于导通电压范围,造成下图第四中情况,直接将电源和地短路,特别危险。死区可以帮助我们解决这个问题,具体的就不再拓展了。
S3C2440的计数器比较简单,之前使用32还能设置计数器增加或者减少,这里只能减少,也算省心,当计数器减为0时,即为定时器触发中断的时机。想要使用定时器中断,除了上节中设置的定时器控制器,还需要把定时器先设置好,并且启动。相关寄存器如下:
第一个是预分频的8位计数,我们使用的是定时器1,只用于中断,因此只设置预分频器0即可。这里还需要配合分频系数来使用,通过这两个数来让时钟降低到合适的值,这里的值其实够用就行,比如这里直接设置为99预分频,同时使用16分频,最终根据公式计算时钟为50MHz/((99+1)*16)=31250Hz。
确定了时钟后,需要确定计数值,从而得到定时器周期,这里因为不需要输出PWM因此不需要设置比较寄存器,只需要设置计数TCMPB0初值即可。假设我们期望0.5s中断一次,根据上面得到的时钟,可以知道我们需要设置初值为15625,即可得到0.5s中断一次的预期效果。
最后一个需要设置的是时钟控制寄存器,同样的只需要关注定时器0相关即可,设置定时器0自动重载初始值,由于不需要输出时钟到引脚,这里invert可以不用管,使用默认值,根据芯片手册所说,第一次启动需要手动把manual update手动设置,但是在下次写之前需要清除该位。最后启动定时器。
其他的内容就和外部中断类似,只需要在外部中断程序的基础上,加上定时器初始化、定时器中断处理函数,同时关闭定时器中断屏蔽相关即可,由此,输出代码如下:
interrupt.c
------------------------------------
#include "interrupt.h"
#include <stdint.h>
#include "led.h"
#include "s3c2440.h"
typedef void (*irq_func)(int);
irq_func irq_array[32];
static void register_irq(int irq, irq_func fp)
{
irq_array[irq] = fp;
INTMSK &= ~(1<<irq);
}
void Timer0InterruptHandler(int irq)
{
ToggleLed(kLed1);
}
void ExtInterruptHandler(int irq)
{
uint32_t ext_interrupt_bit = EINTPEND;
switch(irq)
{
case 0:
{
ToggleLed(kLed1);
break;
}
case 2:
{
ToggleLed(kLed2);
break;
}
case 5:
{
if(ext_interrupt_bit &( 1 << 9 ))
{
ToggleLed(kLed3);
}
else if(ext_interrupt_bit &( 1 << 11 ))
{
ToggleLed(kLed1);
ToggleLed(kLed2);
ToggleLed(kLed3);
}
break;
}
default:
break;
}
EINTPEND = ext_interrupt_bit;
}
static void InterruptControllerInit(void)
{
//开启全局中断开关
asm(
"MRS R0,CPSR \n\t"
"BIC R0,R0,#0x80 \n\t"
"MSR CPSR,R0 \n\t"
);
}
static void ExternInterruptInit(void)
{
//设置为外部中断引脚
GPFCON &= ~((3<<0)|(3<<4));
GPFCON |= (2<<0)|(2<<4);
GPGCON &= ~((3<<6)|(3<<22));
GPGCON |= (2<<6)|(2<<22);
EXTINT0 |= (7<<0) | (7<<8); /* S2,S3 */
EXTINT1 |= (7<<12); /* S4 */
EXTINT2 |= (7<<12);
/* 设置EINTMASK使能eint11,19 */
EINTMASK &= ~((1<<11) | (1<<19));
register_irq(0,ExtInterruptHandler);
register_irq(2,ExtInterruptHandler);
register_irq(5,ExtInterruptHandler);
}
static void Timer0InterruptInit(void)
{
TCFG0 = 99;
TCFG1 &= ~0xf;
TCFG1 |= 3;
TCNTB0 = 15625;
TCON |= (1<<1);
TCON &= ~(1<<1);
TCON |= (1<<0) | (1<<3);
/* 设置中断 */
register_irq(10, Timer0InterruptHandler);
}
void AllInterruptInit(void)
{
InterruptControllerInit();
ExternInterruptInit();
Timer0InterruptInit();
}
void InterruptHandler(void)
{
uint32_t interrupt_bit = INTOFFSET;
irq_array[interrupt_bit](interrupt_bit);
SRCPND = 1<< interrupt_bit;
INTPND = 1<< interrupt_bit;
}
有了前期的工作,定时器中断这里需要做的就很少了,这里除去正常功能外,使用函数指针数组优化了代码结构,在中断初始化时,要求注册中断号和中断处理函数,从而总中断处理函数在后续增加功能过程中不需要改动,代码耦合性降低。代码效果为led亮灭,目测0.5s哈哈。