其实中断的概念很好理解:试想一下你正在教室里面搞单片机(这相当于与CPU正在执行主程序)这时,防空警报响了(一个中断信号),因此你得停下你手头的工作,去响应这个防空警报(中断响应),这个防空警报需要你执行一些行为(比如逃出教室或者躲在课桌底下),当你执行了一段时间之后,防空警报结束了(中断服务执行完了),那么你就从新回到教室继续搞单片机。
这个过程就是中断。
我们先看看中断都有哪几种:(1)外部中断0【中断号 0】,它的优先级是最高的。(2)定时器/计数器0 【中断号1】 (3)外部中断1【中断号 2】 (4)定时器/计数器 1【中断号 3】
(5)串口中断【中断号 4】 (6)定时器/计数器 2 【中断号 5】
先看看下面的图:其中,TCON, IE, IP 等都是与中断有关的寄存器。
要想在单片机中给一个中断(我们以外部中断0为例),看看需要什么条件:
下面,我们想实现这样一个效果:
【代码一】:实现数码管1,3,5亮,每隔一定时间同时亮一个数,然后我们在一些特定时刻给它外部中断0,中断时,让CPU点亮第一个LED灯,我们看看是什么效果:
#include
#define unchar unsigned char
#define uint unsigned int
sbit WEI = P2^7; //定义控制数码管的位选信号的单片机端口位(或者说定义单片机的P2.7口是WEI)
sbit DULA = P2^6; //定义单片机P2.6口是DULA(控制数码管的段选信号)
sbit D1 = P1^0; //定义控制第一个LED的单片机端口位
sbit int0 = P2^3;
uchar num;
uchar code table[] = {
0x3f, 0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //定义数码管的编码表,为了节约内存空间
void delay(uint);
void main()
{
EA=1; //先打开中断总允许
EX0=1; //打开中断源允许EX0,表示允许外部中断0输入
IT0 = 1; //表示下降沿触发(因为一上电默认单片机P3.2口是高电平的)
//下面是数码管的显示:先位选
WEI = 1; //打开位选
P0 = 0xea;
WEI = 0; //锁住位选信号
while(1)
{
for(num=0;num<16;num++)
{
DULA = 1; //打开段选
P0 = table[num];
DULA = 0;
if(num == 3)
{
//如果num == 3那么就给一个下降沿,即让P3.2口变成低电平
int0 = 0;
}
}
}
}
void exter0() interrupt 0
{
D1 = 0; //中断响应就是要先让第一个LED亮
delay(1000); //延时
D1 = 1; //灭LED
int0 = 1; //把P3.2口变回高电平,用以准备下一次的中断。
}
void delay(uint z)
{
//延时函数
uint x,y;
for(x=z;x>0;x--)
for(y=1000;y>0;y--);
}
也就是说:我们想要给出一个中断信号(这里我们暂时先不管定时器和串口)就以外部中断0、外部中断1为例。那么我们就首先需要打开总中断允许(即令 EA=1);接下来,就要选取中断种类(即令EX0, ET0, EX1, ET1,ES),选哪个就把那个置1,最后一步就是选择触发方式:(IT0, IT1)置1表示下降沿触发、置0表示低电平触发
这是单片机里面一个相当重要的概念:首先,51单片机里面有两个定时器 T0, T1。
定时器一但启动,它便在原来的数值上开始加1计数,若在程序开始时,我们没有设置 TH0 和TL0,它们的默认值都是0,假设时钟频率为12MHz,12个时钟周期为一个机器周期,那么此时机器周期为1us.
假如定时器工作在模式一:也就是我们先在 TL0 这8位里面记录时间,每当记满 TL0 的八位后,才向 TH0 进 1. 那么,在TH0、TL0的初始值都是0时,我们记满整个定时器需要的机器周期就是: ( 2 8 ) 2 = 256 (2^8)^2 = 256 (28)2=256x 256 256 256 = 65536 65536 65536个机器周期,再来多一个周期,那么计数器就溢出,那么随机向CPU 发出中断请求。
而上面的情况,从计数开始到向CPU发出中断,所花的时间是:1us x 65536 = 65536us =65.536ms
那么,假如我们给定计数器一个初始值,那么我们就可以控制计数器在经过规定的时间后产生中断。
要知道,在此之前,我们想要计时都是通过 d e l a y ( ) delay() delay() 去做的,不仅要手工调参,而且定时精度不佳,但是利用定时器定时就非常精确!
首先,51单片机的定时器由两个寄存器 T M O D TMOD TMOD 和 T C O N TCON TCON 控制:
TMOD也是一个八位寄存器,低4位控制 T0 定时器,高四位控制 T1 定时器。
如果我们只是使用T0定时器,那么我们就直接把高四位置0即可。
我们看看:00表示模式0、01表示模式1、10表示模式2、11表示模式3.
我们分析一下模式1:模式1就是我们在上文提到的那种计数方式:也就是我们先在 TL0 这8位里面记录时间,每当记满 TL0 的八位后,才向 TH0 进 1.
=========================================
那么举个例子:假设我们要定时 50ms,也就是50000us.那么我们看一下TH0和TL0里面的值到底怎么算:
因为我们想要计数器精准记到50000us,之后这个计数器得告诉我们从开始到现在刚好走完了50000us,这个“告诉我们”的过程,就是计数器发出中断(即计数到65536的时候)。那么也就是说,这个计数器就不可能是从0开始的,它得是从某一个值开始计数,记了50000次刚好到65535.
那么,很简单,这个初始值就是 (65536-50000) = 15536
现在我们要做的,就是把这个15536分配到 TH0 和 TL0 中。我们会议一下模式一,是在 TL0 得8位都记满,即256个机器周期,才会向 TH0 进一位。因此,TH0我们可以这样表示:
T H 0 = ( 65536 − 50000 ) / 256 TH0 = (65536-50000)/256 TH0=(65536−50000)/256
这里,’/’ 表示取模,也就是商(整数)
TL0 我们可以这样表示: T L 0 = ( 65536 − 15536 ) % 256 TL0 = (65536-15536)\%256 TL0=(65536−15536)%256
这里的 ‘%’ 表示 取余数
对于 TCON的设置,我们不用把所有位的置一次,只需要定义 T R 0 TR0 TR0(用于控制 T0 的启动和停止)
如果要同时用 T0 和 T1,那么我们就令 TR0 = 1; TR1 = 1; 即可。
由于定时器在计数满了溢出的时候也会向 CPU 发送中断请求,因此,我们也需要向调用外部中断0那样设置一些东西:比如打开中断总允许 EA(EA=1)、在打开 ET0(ET0=1)
重点:使用定时器的步骤!!
现在,我们还是希望点亮1、3、5三个数码管,每一次显示一个数字,但是闪烁的间隔时间我们不再使用之前的 d e l a y ( ) delay() delay() 函数,而是采用定时器。我们看看代码:
注意:我们这里是假定了单片机的振荡频率是 12MHz,因为在12MHz时,晶振周期是 1/12M,而恰好1个机器周期等于 12 个晶振周期,那么一个机器周期也就是 (1/12)x12 = 1us。如果不是12MHz,那么一个机器周期的时间需要重新算一下。
#include
#define uchar unsigned char
#define uint unsigned int
sbit WEI = P2^7; //定义位选信号端
sbit DULA = P2^6; //定义段选信号端
uchar num,t;
uchar code table[] = {
0x3f, 0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71};
void main()
{
TH0 = (65536-50000)/256; //定义定时器初始高位
TL0 = (65536-50000)%256; //定义定时器初始低位
TMOD = 0x01; //定义TMOD(也就是控制T0的第一个寄存器)
EA=1; //打开中断总允许
ET0=1; //打卡定时器T0的中断允许
TR0 = 1; //定时器T0启动
WEI=1;
P0=0Xea;
WEI=0;
for(num=0;num<8;num++)
{
if(t == 20)
{
DULA=1;
P0=table[num];
DULA=0;
}
}
}
void extraT0() interrupt 1
{
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //因为在CPU执行这一段中断代码时,计数器已经溢出了,所以要把它的初始值置成一开始的那个
t++; //这里 t 每自增一次,代表已经经过了50ms,那么t = 20时也就是1s
}