对于单片机来讲,不同的中断源,产生什么类型的中断信号能够触发申请中断,取决于芯片内部的硬件结构,而且通常也可以通过用户的软件来设定。
单片机的硬件系统会自动对这些中断信号进行检测。一旦检测到规定的信号出现,将会把相应的中断标志位置“1”(在I/O空间的控制或状态寄存器中),通知CPU进行处理。
● 中断向量
中断源发出的请求信号被CPU检测到之后,如果单片机的中断控制系统允许响应中断,CPU会自动转移,执行一个固定的程序空间地址中的指令。这个固定的地址称作中断入口地址,也叫做中断向量。中断入口地址往往是由单片机内部硬件决定的。
一个单片机有若干个中断源,每个中断源都有着自己的中断向量。这些中断向量一般在程序存储空间中占用一个连续的地址空间段,称为中断向量区。由于一个中断
向量通常仅占几个字节或一条指令的长度,所以在中断向量区一般不放置中断服务程序的。中断服务程序一般放置在程序存储器的其它地方,而在中断向量处放置一
条跳转到中断服务程序的指令。这样,CPU响应中断后,首先自动转向执行中断向量中的转移指令,再跳转执行中断服务程序。
● 中断优先级
单片机系统一般有多个中断源,当某一时刻同时有多个中断产生时,单片机该如何处理呢?这就有了中断优先级的概念。
通常,单片机可以接收若干个中断源发出的中断请求。但在同一时刻,MCU只能响应这些中断请求中的其中一个。为了避免MCU同时响应多个中断请求带来的混
乱,在单片机中为每一个中断源赋予一个特定的中断优先级。一旦有多个中断请求信号,MCU先响应中断优先级高的中断请求,然后再逐次响应优先级次一级的中
断。中断优先级也反映了各个中断源的重要程度,同时也是分析中断嵌套的基础。
对于中断优先级的确定,通常是由单片机的硬件结构规定的。一般的确定规则方式为两种:
实际上,MCU在两种情况下需要对中断的优先级进行判断:
第一种情况为同时有两(多)个中断源申请中断。在这种情况下,MCU首先响应中断优先级最高的那个中断,而将其它的中断挂起。待优先级最高的中断服务程序执行完成返回后,再顺序响应优先级较低的中断。
第二种情况是当MCU正处于响应一个中断的过程中。如已经响应了某个中断,正在执行为其服务的中断程序时,此时又产生一个其它的中断申请,这种情况也称作中断嵌套。
● 中断嵌套
对于中断嵌套的处理,不同的单片机处理的方式是不同的,应根据所使用单片机的特点正确实现中断嵌套的处理。
按照通常的规则,当MCU正在响应一个中断B的过程中,又产生一个其它的中断A申请时,如果这个新产生中断A的优先级比正在响应的中断B优先级高的话,就
应该暂停当前的中断B的处理,转入响应高优先级的中断A,待高优先级中断A处理完成后,再返回原来的中断B的处理过程。如果新产生中断A的优先级比正在处
理中断B的优先级低(或相同),则应在处理完当前的中断B后,再响应那个后产生的中断A申请(如果中断A条件还成立的话)。
一些单片机(如8051结构)的硬件能够自动实现中断嵌套的处理,既单片机内部的硬件电路能够识别中断的优先级,并根据优先级的高低,自动完成对高优先级中断的优先响应,实现中断的嵌套处理。
而另一类的单片机,如我们正在学习的AVR单片机,其硬件系统不支持自动实现中断嵌套的处理。如果在系统设计中,必须使用中断嵌套处理,则需要由用户编写相应的程序,通过软件设置来实现中断嵌套的功能。
● 中断控制(屏蔽)
单片机拥有众多中断源,但在某一具体设计中通常并不需要使用所有的中断源,或者在系统软件运行的某些关键阶段不允许中断打断现行程序的运行,这就需要一套
软件可控制的中断屏蔽/允许系统。在单片机的I/O寄存器中,通常存在一些特殊的标志位用于控制开放或关闭(屏蔽)MCU对中断响应处理,这些标志称为中
断屏蔽标志位或中断允许控制位。用户程序可以改变这些标志位的设置,在需要的时候允许MCU响应中断,而在不需要的时候则将中断请求信号屏蔽(注意:不是
取消),此时尽管产生了中断请求信号,MCU也不会响应中断请求。
从对中断源的控制角度讲,中断源还可分成2类:
● 中断响应条件
单片机在工作时,在每个机器周期都会查询一下各个中断源的中断标记,从而判断是否有中断申请,如果中断标志为1,说明有中断请求发生。
综合前面的介绍,我们可以知道,在单片机中,对应每一个中断源都有一个相应的中断标志位,该中断标志位将占据中断控制寄存器中的一位。当单片机检测到某一中断源产生符合条件的中断信号时,其硬件会自动将该中断源对应的中断标志位置“1”,这就意味着有中断信号产生了,向MCU申请中断。
但中断标志位的置“1”,并不代表MCU一定响应该中断。为了合理控制中断响应,在单片机内部还有相关的用于中断控制的中断允许标志位。最重要的一个中断允许标志位是全局中断允许标志位。当该标志位为“0”,表示禁止MCU响应所有的可屏蔽中断的响应。此时不管有否中断产生,MCU不会响应任何的中断请求。只有全局中断允许标志位为“1”,才允许单片机响应中断。
MCU响应中断请求的第二个条件是每个中断源所具有的各自独立的中断允许标志位。当某个中断允许标志位为“0”时,表示MCU不响应该中断的中断申请。
从上面的中断响应条件看出,只有当全局中断允许标志位为“1”(由用户软件设置),中断A允许标志位为“1”(由用户软件设置),中断A标志位为“1”
(符合中断条件时由硬件自动设置或由用户软件设置)时,MCU才会响应中断A的请求信号(如果有多个中断请求信号同时存在的情况下,还要根据中断A的优先
级来确定)。
用户程序对可屏蔽中断的控制,一般是通过设置相应的中断控制寄存器来实现的。除了设置中断的响应条件,用户程序还需要通过中断控制器来设置中断的其他特性,如:中断触发信号的类型、中断的优先级、中断信号产生的条件等等。
●中断响应过程(中断服务程序)
当所有的中断响应条件都满足了之后,就要进入中断响应过程进行相应处理了。单片机响应中断后,首先要把当前指令的下一条指令的地址送入堆栈(保护断点),
然后根据中断标记,将相应的中断入口地址送入程序指针,程序转到中断入口处继续执行(中断服务程序),中断程序执行完后,单片机再把堆栈中保存的地址取
出,程序从刚才的中断处继续向下执行。
需要注意的是,单片机硬件所做的保护工作只是保护了程序的一个指令地址,如果中断响应过程中修改了一些寄存器和变量的值,就需要在中断响应程序里面自己加以保护。
2、AVR单片机的外部中断
AVR单片机有很多中断,在后面的实例中我们会逐一介绍。本例中只介绍AVR单片机的外部中断,
ATmega16有INT0、INT1和INT23个外部中断源,分别由芯片外部引脚PD2、PD3、PB2上的电平的变化或状态作为中断触发信号。
●外部中断触发方式和特点
INT0、INT1、INT2的中断触发方式取决于用户程序对MCU控制寄存器MCUCR以及MCU控制与状态寄存器MCUCSR的设定。其中,INT0和INT1支持4种中断触发方式:上升沿触发、下降沿触发、任意电平变化触发、低电平触发。
INT2支持上升沿触发和下降沿触发。
任意电平变化触发表示只要引脚上有逻辑电平的变化就会产生中断申请(不管是上升沿还是下降沿都引起中断触发)。在这4种触发方式中,还有以下的一些不同的特点:
●低电平触发是不带中断标志类型的,即只要中断输入引脚PD2或PD3保持低电平,那
么将一直会产生中断申请。
●MCU对INT0和INT1的引脚上的上升沿或下降沿变化的识别(触发),需要I/O时钟信号的存在(由I/O时钟同步检测),属于同步边沿触发的中断类型。
●MCU对INT2的引脚上的上升沿或下降沿变化的识别(触发),以及低电平的识别(触发)是通过异步方式检测的,不需要I/O时钟信号的存在。因此,这
类触发类型的中断经常作为外部唤醒源,用于将处在Idle休眠模式,以及处在各种其它休眠模式的MCU唤醒。这是由于除了在空闲(Idel)模式
时,I/O时钟信号还保持继续工作,在其它各种休眠模式下,I/O时钟信号均是处在暂停状态的。
●如果使用低电平触发方式的中断作为唤醒源,将MCU从掉电模式(Power-down)中唤醒时,电平拉低后仍需要维持一段时间才能将MCU唤醒,这是
为了提高了MCU的抗噪性能。拉低的触发电平将由看门狗的时钟信号采样两次(在通常的5V电源和25℃时,看门狗的时钟周期为1µs)。如果电平拉低保持
2次采样周期的时间,或者一直保持到MCU启动延时(start-up
time)过程之后,MCU将被唤醒并进入中断服务。如果该电平的保持时间能够满足看门狗时钟的两次采样,但在启动延时(start-up
time)过程完成之前就消失了,那么MCU仍将被唤醒,但不会触发中断进入中断服务程序。所以,为了保证既能将MCU唤醒,又能触发中断,中断触发电平
必须维持足够长的时间。
●如果设置了允许响应外部中断的请求,那么即便是引脚PD2、PD3、PB2设置为输出方式工作,引脚上的电平变化也会产生外部中断触发请求。这一特性为用户提供了使用软件产生中断的途径。
(3)与外部中断相关的寄存器和标志位
在ATmega16中,除了寄存器SREG中的全局中断允许标志位I外,与外部中断有关的寄存器有4个,共有11个标志位。分别是:MCU控制寄存器—MCUCR、MCU控制和状态寄存器—MCUCSR、通用中断控制寄存器—GICR、通用中断标志寄存器—GIFR。其作用分别是3个外部中断各自的中断标志位、中断允许控制位和用于定义外部中断的触发类型。
具体寄存器各个标志位的意义和如何设置,请查阅相关ATmega16的数据手册,在此不做过多描述。
需要注意的是:在系统程序的初始化部分中对外部中断进行设置时(定义或改变触发方式),应先将GICR寄存器中该中断的中断允许位清零,禁止MCU响应该中断后再设置ISCn位。
而在开放中断允许前,一般应通过向GIFR寄存器中的中断标志位INTFn写入逻辑“1”,将该中断的中断标志位清除,然后开放中断。这样可以防止在改变ISCn的过程中误触发中断。
3、按键电路
按键电路与上一实例相同,在此略去。
4、外部中断程序的编写
我们已经知道,要实现中断程序,首先要在主程序里面对相关中断寄存器进行中断产生条件的设置。然后就是编写中断服务程序。
本例中中断寄存器的设置如下:
MCUCR |= (1 << ISC11) | (1 << ISC01) | (1 << ISC00);
//INT0设置为上升沿中断,INT1为下降沿中断请求
GICR |= (1 << INT0) | (1 << INT1); //允许INT0、INT1中断
GIFR |= (1 << INTF1) | (1 << INTF0); //清除INT0、INT1中断标志位
sei(); //使能全局中断
中断服务程序的编写具有一定的格式,在不同编译环境下各不相同,在WINAVR(GCC)环境下有两种方式,分别是:
● SIGNAL(中断向量名)
{
… //中断服务程序内容
}
● ISR(中断向量名 )
{
… //中断服务程序内容
}
在这两种方式中,需要分别添加头文件:#include <avr/signal.h>和#include <avr/interrupt.h>。
宏INTERRUPT 的用法与SIGNAL 类似,区别在于SIGNAL 执行时全局中断触发位被清除、其他中断被禁止;INTERRUPT 执行时全局中断触发位被置位、其他中断可嵌套执行。
另外avr-libc 提供两个API 函数用于置位和清零全局中断触发位,它们是经常用到的,
分别是:void sei(void) 和void cli(void) 由interrupt.h定义
在本实例中,我们采用包含头文件#include <avr/interrupt.h>,的方式,使用ISR(中断向量名 ){…}来编写中断函数。
2.2.3 电路
本实例的按键电路如图2.2.1所示,数码管接口电路与实例1.3中的电路相同,在此不再给出。
图2.2.1 按键电路
1、电路原理
本实例用K1、K2两个按键分别连接到单片机的PD2、PD3端口,PD2、PD3同时也是单片机的外部中断INT0、INT1的两个引脚。当按键按下时,外部中断INT0、INT1的两个引脚的电平发生变化,从而产生外部中断。
2、元器件选择
在这里列出和本例相关的、关键部分的器件名称及其在电路中的作用。
● ATmega16:单片机,检测按键按下情况并控制数码管显示数字。
●数码管:显示按键状态。
● R10:阻值为10K的电阻,下拉限流电阻。
● K1、K2:按键,当按键按下时,与按键连接的单片机端口的电平发生变化,产生外部中断。
3、管脚连接
在这里列出和本例相关的、关键部分的单片机端口与外围电路的连接。
● PB0-PB7:连接数码管的8个段,控制数码管的显示。
● PD2、PD3:连接按键K1、K2,检测两个按键的状态。
●PC6:数码管选通端口,该端口通过三极管9013控制数码管的选通,当PC6输出高电平时,数码管选通。
2.1.4 程序设计
1、程序功能
程序的功能是控制一个8段数码管显示“0”-“F”16个十六进制的数字。当系统上电时,显示“0”。K1键的作用是加“1”控制键:按1次K1键,显示
数字加1,依次类推。当第15次按K1键时,显示“F”,第16次按K1键,显示又从“0”开始。K2键的作用是减1控制键:按1次K1键,显示数字减
1,减到“0”后,再从“F”开始。
● 按键开关的软件去抖和释放
上一个实例已经讲过这种方法,本例不再重复。
● 单片机外部中断的编程
在本例中,需要使用单片机的INT0、INT1,所以在程序中需要对相应的寄存器进行设置,并且编写中断服务程序。
● 控制1位数码管的显示
在INT0中断服务程序中,当按键K1按下一次,数码管显示的数字加1;在INT1的中断服务程序中,当按键K2按下一次,数码管显示的数字减1。
2、主要变量和函数说明
本例中需要编写中断函数,函数的功能是:当有外部中断发生时,程序跳转到中断函数执行数码管显示的程序,处理完后跳回主程序继续执行。
程序中用到变量Counter和 Disp_Buff[16] 。变量Counter用于指示按键按下的次数,数组 Disp_Buff[16]存放数码管显示的字形编码。
3、使用WINAVR开发环境,makefile文件同前面的例子,直接复制到本实例程序的文件夹中即可。
4、程序代码
[code="c"]
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h> //中断函数头文件
unsigned char Disp_Buff[16] = {0xaf,0xa0,0xc7,0xe6,0xe8,0x6e,0x6f,0xa2,
0xef,0xee,0xeb,0x6d,0x0f,0xe5,0x4f,0x4b};
//数码管字型码表显示:0,1,2,3,4,5,6,7,8,9,A,b,C,d,E,F
volatile unsigned char Counter; //按键按下次数变量,如果在中断中调用全局变量,必须加
//volatile来定义,否则变量不会变化
int main(void)
{
PORTB = 0X00; //
DDRB = 0Xff; //
PORTC &= ~(1 << PC6); //配置数码管0的位选通口为低电平,不导通数码管
DDRC |= (1 << PC6); ///配置数码管0的位选通口为输出,选通数码管0
PORTD = 0X08; //一定要使能K2的上拉电阻,否则会有干扰
DDRD = 0XF3; //K1、K2按键(PD2、PD3)设置为输入端口
MCUCR |= (1 << ISC11) | (1 << ISC01) | (1 << ISC00);
//INT0设置为上升沿中断,INT1为下降沿中断请求
GICR |= (1 << INT0) | (1 << INT1); //允许INT0、INT1中断
GIFR |= (1 << INTF1) | (1 << INTF0); //清除INT0、INT1中断标志位
Counter = 0; //按键按下次数变量清零
PORTC |= (1 << PC6); //选通数码管0
sei(); //使能全局中断
while(1)
{
PORTB = Disp_Buff[Counter]; //数码管显示按键按下次数
}
}
//外部中断0函数,当按键K1按下后,进入此中断
ISR(INT0_vect )
{
_delay_ms(20); //按键按下,延时一会再判断是否按下, 以消除干扰
if((PIND & (1 << PD2))) // 按键真正按下后,进行相应处理
{
if(++Counter >= 16) Counter = 0; //次数大于15,清零
while((PIND & (1 << PD2)));//等待按键释放
}
}
//外部中断1函数,当按键K2按下后,进入此中断
ISR(INT1_vect)
{
_delay_ms(20); //判断按键按下,延时一会再判断是否按下, 以消除干扰
if(!(PIND & (1 << PD3))) // 按键真正按下后,进行相应处理
{
if(Counter) --Counter; // 次数减1
else Counter = 15; // 次数为零则改成15
while(!(PIND & (1 << PD3))); //
}
}
在语言C语言编写单片机程序过程,如果要使用外部中断服务程序时,要尽量减少中断服务程序的内容和长度。因为在主程序中可能还要相应别的中断,如果一个中断服务程序过长,很可能会影响到主程序对其他中断的响应。
常用的处理方法是:在中断服务程序中只改变变量的值,或者设置各种标志,在主程序里面对变量或标志进行判断和处理。本实例为了演示的方便,在中断程序中进
行了所有的操作。应该说明的是,这种方法是极不可取的。更为合理的方法是:在中断服务程序里面只改变Counter的值。其余部分都放到主程序里面进行处
理。
另外需要特别强调的一点是,在用WINAVR编写中断服务程序时,如果中断服务程序中用到了全局变量,则在定义全局变量时,必须在变量的数据类型前加
volatile来定义,否则该变量在中断服务程序中不会变化。这是因为在用C语言编写单片机程序时,都会用到编译器的“优化”代码功能,以使程序更加简
洁、紧凑。但是毕竟编译器的优化是很死板的,他会把一些对变量的读操作优化掉。这样就导致在全局中断中使用的变量被优化成一个静止变量,即该变量的值不再
改变。所以我们要把这些变量定义为volatile,意思是提示编译器:该变量是很容易变化的,不准对该变量的读取进行优化。这样在中断中每次对变量的读
写就都可以正确的执行了。