一、前期准备
1. 定时器工作模式的设置
由于定时器工作模式寄存器TMOD是不允许位寻址的,所以对在两个定时器模式的设置上存在的一些技巧进行介绍。
① 直接对寄存器TMOD进行十六进制形式的赋值。
② 对寄存器进行“按位与 按位或”形式的赋值。
例如:定时器1保持原来的工作方式,使定时器0工作在模式1下。(假设此时定时器1工作在模式0下)
//方式1
TMOD = 0x01; //0000 0001 同时对两个定时器的工作模式进行设置
//方式2
TMOD = TMOD & 0xF0; //1111 0000 将TMOD的低4位清零,高4位保持不变。
TMOD = TMOD | 0x01; //0000 0001 将TMOD的最低位置1,高4位保持不变。
上面的方式②看似没有改变定时器1的工作模式,只改变了定时器0的工作模式。但是由于定时器工作模式寄存器TMOD是不允许位寻址的,所以虽然Timer1的工作模式没有被改变,但是它的工作模式是被重新定义过的。实际上,它的新模式与原来的模式是一样的,所以这个改变并没有引起任何实际效果。直接对TMOD进行赋值的方法更加简洁明了,而按位与和按位或操作更加灵活和精细。
2. 初值的计算&初值的设定
1)初值的计算:
在51单片机中,定时器初值的计算公式根据定时器的模式和需要定时的时间不同而有所不同。
公式:(2^n-初值)×(12 ÷ 晶振频率)= 定时时间
n:定时器位数 ,由定时器模式进行决定;时钟周期 = 1 / 晶振频率;机械周期 = 12 × 时钟周期 = 12 / 晶振频率,如果这里认定晶振频率为12MHz,那么对应的机械周期就是12/12MHz=1us。那么进而可以得到上述公式的变形:初值 = 2^n - 定时时间,单位均为us。
2)初值的设定:
① 上面得到的“初值”是十进制形式,可以将得到的十进制数转换成16进制数,然后分别对THn和TLn进行初值的设定。
② 同时还可以通过对“初值”进行256取整,得到THn的值,即THn = (2^n - 定时时间) / 256;通过对“初值”进行256取余,得到TLn的值,即TLn = (2^n - 定时时间) % 256。(定时器是8位的,最大计数值位255,即2^8-1,记到256时TLn向THn进1。)
例如:使工作在模式1下的定时器0工作50ms后产生溢出。
利用上面公式,(2^16 - x) × (12 ÷ 12) = 50ms = 50000us,解得 x = 15536,对应16进制形式为3cb0H。
//方式1
TH0 = 0x3c;
TL0 = 0xb0;
利用“取整”+“取余”的方法,TH0 = (65536 - 50000) / 256,TL0 = (65536 - 50000) % 256。
//方式2
TH0 = (65536 - 50000) / 256;
TH0 = (65536 - 50000) % 256;
3)给定的定时时间超过了最大定时时间65.536ms怎么办?
定时器从开始计时到最后的溢出,期间会经历65536个机器周期,按照每个机器周期为1us来算,那么65536个机器周期相当于计时65536us,也就是65.536ms。如果想要定时的时间超过了65.536ms,可以通过添加循环进行实现。
例如通过“TH0 = (65536 - 50000) / 256; TH0 = (65536 - 50000) % 256;”语句可以实现50ms定时,如果想要实现1s的定时,可以通过1s=50ms×20,即添加“循环”的方法进行实现。(下方实例中有相应代码详解)
3. 中断允许设置
由于中断使能(允许)寄存器IE是允许位寻址的,所以在对中断源允许中断的设置上存在的一些不同的实现方法。
① 直接对寄存器IE进行十六进制形式的赋值。
② 对所使用的中断源对应的控制位的状态进行单独设置。
例如:同时使用定时器T0和T1的中断。
//方法1
IE = 0x8A; //对IE寄存器进行整体赋值,D6位和D5位默认取0。
//方式2
EA=0;
ET0=1;
ET1=1;
4. 中断服务函数
中断服务函数是一种特殊的函数,用于处理单片机内部的中断事件。当中断事件发生时,单片机会暂停当前正在执行的程序,转而去执行中断服务函数,处理完中断事件后再返回原来被中断的地方,继续执行原来的程序。中断服务函数的特点是,它属于后台触发、前台执行的函数体。与其他函数不同的是,其他函数都是前台调用执行的函数体。
//中断服务函数的格式
void ISR_Name(void) interrupt ISR_Number
{
// 中断处理代码
}
//ISR_Name 是中断服务函数的名称,可以根据需求进行自定义。
//interrupt ISR_Number 指定了中断号,即中断源的唯一标识。
例如:中断服务函数来处理外部中断0(INT0)。
void Int0_Routine(void) interrupt 0
{
// 中断处理代码
}
5. 外部中断的触发方式
① 低电平触发:指当外部中断引脚的电平为低电平时,中断请求被激活。
② 下降沿触发:指当外部中断引脚的电平从高电平变为低电平时,中断请求被激活。
1)当采用低电平触发的方式时,外部中断源(输入到INT0/1)必须一直保持低电平有效,直到该中断被CPU 响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.2/P3.3要变为高电平),否则将产生另一次中断。即:
· 只要P3.2(或P3.3)保持低电平,IE0(或IE1)就保持为1,并请求中断。
· CPU响应该中断后,即使硬件会立刻将IE0(或IE1)清零,但如果P3.2(或P3.3)还保持为低电平,IE0(或IE1)将又会被置1,进而继续请求中断。
· 中断服务程序执行完返回后,将会继续执行下一次中断,即使期间使用软件对IE0(或IE1)置零也不会使下一次中断停止。
· 只有当P3.2(或P3.3)恢复高电平,在最后一次中断服务程序执行完,IE0(或IE1)由硬件清零之后,才不会再引发新的中断请求。
2)当采用下降沿触发的方式时,要考虑按键抖动所产生的的影响,必须进行按键的消抖。
6. 自定义中断优先级
由于中断优先级寄存器IP是允许位寻址的,所以在对中断优先级的设置上存在的一些不同的实现方法。
① 直接对寄存器IP进行十六进制形式的赋值。
② 对所使用的中断源对应的控制位的状态进行单独设置。
例如:将定时器T0的优先级设置为高优先级
//方式1
IP=0x02
//方式2
PT0=0;
二、应用实例
1. 定时器T0在模式1下实现流水灯,闪烁间隔为500ms。(定时器的应用)
#include <reg51.h>
void main()
{
unsigned char k,n,i;
n=0x01; //由LED模块的公共端电平所决定
k=0;
// 1.报备:将定时器T0的工作模式设置为模式1(定时功能)
TMOD=0x01;
// 2.置初值:设置定时时间为50ms,后续通过循环实现目标定时时间
TH0=(65536-50000)/256; //TH0 = 0x3c;
TL0=(65536-50000)%256; //TL0 = 0xb0;
// 3.启动:使定时器T0开始工作
TR0=1;
while(1)
{
n=0x01;
//循环8次之后,进入下一个while(1)循环
for(i=0;i<8;i++) //根据LED的个数,来决定i的取值范围
{
P0=~n;
while(k<10) //此处通过一个while循环实现闪烁间隔为500ms=50ms×10
{
// 4.等待:当定时器最高位产生溢出时,TF0会由硬件使其置1
while(TF0==0); //当TF0=0时,一直执行while循环,直到TF0=1
// 5.重置初值:使每次的溢出时间保持相同
TH0=(65536-50000)/256;
TL0=(65536-50000)%256;
// 6.清溢出:将中断标志位重新置0
TF0=0;
k++;
}
k=0;
n=n<<1;
}
}
}
2. 定时器T0的中断法实现流水灯,工作模式为模式1,闪烁间隔为100ms。(定时器+中断)
#include <reg52.h>
unsigned char k; //main函数和中断服务函数中均用到了k,所以k不能在main函数中进行定义
//中断服务函数
void Timer0_Routine (void) interrupt 1
{
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //重置初值,再次进行50ms计时
k++;
}
void main()
{
unsigned char n,i;
k=0;
P0=n;
TMOD=0x01; //将定时器T0的工作模式设置为模式1(定时功能)
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //设置定时时间为50ms,通过多次执行中断服务函数实现目标定时时间
//配置定时器T0的中断开关
EA=1; //闭合中断总开关
ET0=1; //闭合定时器T0的中断允许开关
TR0=1; //定时器T0开始工作
while(1)
{
n=0x01; //由LED模块的公共端电平决定
//循环8次之后,进入下一个while(1)循环
for(i=0;i<8;i++) //根据LED的个数,来决定i的取值范围
{
P0=~n;
while(k<2); //此处通过中断服务函数实现闪烁间隔为100ms=50ms×2
k=0;
n=n<<1;
}
}
}
问:和上面的程序1相比,程序2中并没有“清溢出”这一步骤。那是不是在“定时器”和“中断”同时使用时,定时器溢出标志位TFn不起作用?如果定时器溢出标志位TFn起作用,那为什么不用对其进行清零?
答:显然,定时器溢出标志位 TFn 在定时加中断的情况下依然会被使用。TFn 是用来指示定时器是否溢出的标志位,当定时器计数达到设定值时,TFn 会被设置为 1,表示定时器已经溢出。这时,如果开启了中断,那么中断会被触发,TFn会被硬件电路自动清零。程序1中,之所以要进行手动TF0清零,是因为程1序没有使用中断,无法被自动清零。
3. 定时器T0在模式2下实现流水灯,闪烁间隔为100ms。(模式2下的定时器中断)
#include <reg52.h>
unsigned int k; //main函数和中断服务函数中均用到了k,所以k不能再main函数中进行定义
//中断服务函数
void Timer0_Routine (void) interrupt 1
{
k++;
}
void main()
{
unsigned char n,i;
k=0;
TMOD=0x02; //将定时器T0的工作模式设置为模式2(定时功能)
TH0=56; //初值=256-定时时间 56=256-200
TL0=56; //设置定时时间为200us
//配置定时器T0的中断开关
EA=1; //闭合总中断开关
ET0=1; //闭合定时器T0的中断允许开关
TR0=1; //定时器T0开始工作
while(1)
{
n=0x01; //由LED模块的公共端电平决定
//循环8次之后,进入下一个while(1)循环
for(i=0;i<8;i++) //根据LED的个数,来决定i的取值范围
{
P0=~n;
while(k<500); //此处通过中断服务函数实现闪烁间隔为100ms=200us×500
k=0;
n=n<<1;
}
}
}
注意:
1) 定时器在模式2下工作时,TLi的溢出不仅置位TFi,而且将THi中的内容重新装入TLi,该过程是由硬件自动实现的,不需要再利用软件进行重置初值。THi中的内容由软件预置,重装时THi内容保持不变,因此计算出初值后对THn和TLn同时赋初值即可。
2) 由于此处实现闪烁间隔为100ms是利用100ms=200us×500进行实现的,对于变量k的取值范围需要大于500,我们知道unsigned char类型所能表示的数值范围为0~255,unsigned int类型所能表示的数值范围为0~65535,因此将k定义为unsigned int类型。
4. 按键采用外部中断法实现流水灯的启停控制(外部中断与定时器中断)
#include <reg52.h>
sbit key1=P3^2; //将P3端口的第2位定义为名为key1的位变量
unsigned char k; //main函数和中断服务函数中均用到了k,所以k不能再main函数中进行定义
unsigned char led[]={0xff,0xfe,0xfc,0xf8,0xf0,0xe0,0xc0,0x80}; //8个状态,形成流水灯现象
//延时函数 实现1ms的延时
void Delay(unsigned int xms) //@12.0000MHz
{
unsigned char i, j;
while(xms)
{ //_nop_();
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
//外部中断Int0的中断服务程序 —— 控制流水灯的启停
void Int0_Routine (void) interrupt 0
{
Delay(10); //检测到下降沿在进入到Int0()后,首先进行10ms的延时,防止按键抖动造成的影响
if(!key1) //检测按键是否按下,如果key1为低电平表示按键按下,程序就会继续执行
TR0=~TR0; //对定时器运行控制位进行取反,实现“启停”功能
}
//定时器中断T0的中断服务程序 —— 实现50ms延时
void Timer0_Routine (void) interrupt 1
{
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //重置初值,再次进行50ms计时
k++;
}
void main()
{
unsigned char i;
k=0;
TMOD=0x01; //将定时器T0的工作模式设置为模式1(定时功能)
TH0=(65536-50000)/256;
TL0=(65536-50000)%256; //设置定时时间为50ms,通过多次执行中断服务函数实现目标定时时间
//配置中断开关
EA=1; //闭合中断总开关
ET0=1; //闭合定时器T0的中断允许开关
EX0=1; //闭合外部中断Int0的中断允许开关
IT0=1; //将外部中断设置为下降沿触发
TR0=1; //定时器T0开始工作
while(1) //循环上面设置的LED的8种状态
{
for(i=0;i<8;i++)
{
P0=led[i];
while(k<4); //使状态转换时长为200ms=50ms×4
k=0;
}
}
}
注意:
1) 上面的程序中“流水灯的启停”和“状态转换时长”分别由“外部中断Int0”和“内部定时器T0”进行控制,因此“外部中断Int0的中断服务程序”中的“Delay(10);”语句并不会影响LED的状态转换时长。
2) 同时,在“流水灯的启停”过程中,其“状态转换时长”也不会发生改变。LED各状态之间转换的时间为200ms,若在按键按下之前,距离上一次的状态改变已经经过了100ms,那么当按键再次按下时,计时时间会继续从100ms开始累加计时,等过了100ms后LED的状态就会再次发生改变。(可以将上述代码中的变量k定义为unsigned int类型,并将while(k<4)中的4换为一个较大的数,然后观察现象即可。比如将4换为40,状态转换时间就变为了:50ms×40=2000ms=2s,方便观察。)
5. 按键采用计数器中断实现LED灯的点亮和熄灭(计数器中断)
//按键按下3次,LED灯的状态变化一次
#include <reg52.h>
//中断服务函数
void Timer0_Routine () interrupt 1
{
P0=~P0; //通过对LED的状态进行取反,来实现LED的亮灭
}
void main()
{
P0=0x00; //LED灯状态初始化,全亮
TMOD=0x06; //T0作为计数器,工作模式为模式3 P3.4引脚接收到一次下降沿,计数器加1
TH0=0xfd; //256-初值=3 初值=(253)D=(11111101)B=(FD)H
TL0=0xfd; //计数器T0工作在模式3下,将初值同时赋值给TH0和TL0
EA=1; //闭合中断总开关
ET0=1; //闭合计数器T0的中断总开关
TR0=1; //计数器T0开始工作
while(1);
}
注意:
1) 如果用按键模拟下降沿,LED可能不会按照理想状态进行亮灭,可以使用红外对管模块进行替代。
2) 注意区分“外部中断”和“计数器中断”。
· 外部中断:是针对外部设备或环境事件(如按键、定时器、串行口等)的中断。
· 计数器中断:是由定时器(如T0、T1等)接收外部信号的下降沿产生计数溢出实现的。
6. 按键控制流水灯的状态(中断优先级及中断嵌套)
1)外部中断INT0(P3.2)和INT1(P3.3)分别与按键key1和key2相连,key1控制LED灯循环左移,key2控制LED灯循环右移;定时器T0控制LED状态改变的间隔。
#include <reg52.h>
sbit key1=P3^2; //将P3端口的第2位定义为名为key1的位变量
sbit key2=P3^3; //将P3端口的第3位定义为名为key2的位变量
unsigned char k=0; //main函数和中断服务函数中均用到了k,所以k不能再main函数中进行定义
bit mode=0; //定义一个名为mode的位变量,并将该位变量进行初始化
//定时器中断T0 —— 控制LED状态改变的时间间隔
void Timer0_Routine () interrupt 1
{
TH0=0x3c;
TL0=0xb0; //重置初值,中再次进行50ms计时
k++;
}
//外部中断Int0 —— key1控制,进行左移操作
void Int0_Routine () interrupt 0
{
mode=0;
}
//外部中断Int1 —— key2控制,进行右移操作
void Int1_Routine () interrupt 2
{
mode=1;
}
void main()
{
unsigned char n=0x01; //LED初始状态,由LED模块的公共端电平决定
TMOD=0x01; //将定时器T0的工作模式设置为模式1(定时功能)
TH0=0x3c;
TL0=0xb0; //设置定时时间为50ms,通过多次执行中断服务函数实现目标定时时间
IE=0x87; //1000 0111 EA=1 EX1=1 ET0=1 EX0=1
IT0=1;
IT1=1; //将两外部中断的触发方式设置为下降沿触发
TR0=1; //定时器T0开始工作
while(1)
{
P0=~n;
if(k==10) //状态改变间隔为500m=50ms×10
{
k=0; //现将计数变量进行清零
if(!mode)
{
n=n<<1; //如果mode为0,进行状态左移
//如果n为0,相当于1"向左"溢出,对n进行重新赋值 第8个灯亮完,第1个灯亮
if(!n) //if(n==0x00)
n=0x01;
}
else
{
n=n>>1; //如果mode为1,进行状态右移
//如果n为0,相当于1“向右”溢出,对n进行重新赋值 第1个灯亮完,第8个灯亮
if(!n) //if(n==0x00)
n=0x80;
}
}
}
}
考虑到按键的抖动可能会对结果产生影响,在此对上面程序进行改进。
2) 外部中断INT0(P3.2)和INT1(P3.3)分别与按键key1和key2相连,key1控制LED灯状态的改变,key2控制LED灯的启停;定时器T0控制LED状态改变的间隔。
#include <reg52.h>
sbit key1=P3^2; //将P3端口的第2位定义为名为key1的位变量
sbit key2=P3^3; //将P3端口的第3位定义为名为key2的位变量
unsigned char k=0; //main函数和中断服务函数中均用到了k,所以k不能再main函数中进行定义
bit mode=0; //定义一个名为mode的位变量,并将该位变量进行初始化
//延时函数 实现1ms的延时 —— 用于按键的消抖
void Delay(unsigned int xms) //@12.0000MHz
{
unsigned char i, j;
while(xms)
{ //_nop_();
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
//定时器中断T0 —— 控制LED状态改变的时间间隔
void Timer0_Routine () interrupt 1
{
TH0=0x3c;
TL0=0xb0; //重置初值,中再次进行50ms计时
k++;
}
//外部中断Int0 —— 按下key1,LED灯的状态就会发生改变
void Int0_Routine () interrupt 0
{
Delay(10);
if(key1==0)
mode=~mode;
}
//外部中断Int1 —— key2控制LED的启停
void Int1_Routine () interrupt 2
{
Delay(10);
if(key2==0)
TR0=~TR0;
}
void main()
{
unsigned char n=0x01; //LED初始状态,由LED模块的公共端电平决定
TMOD=0x01; //将定时器T0的工作模式设置为模式1(定时功能)
TH0=0x3c;
TL0=0xb0; //设置定时时间为50ms,通过多次执行中断服务函数实现目标定时时间
IE=0x87; //1000 0111 EA=1 EX1=1 ET0=1 EX0=1
IT0=1;
IT1=1; //将两外部中断的触发方式设置为下降沿触发
TR0=1; //定时器T0开始工作
while(1)
{
P0=~n;
if(k==10) //状态改变间隔为500m=50ms×10
{
k=0; //现将计数变量进行清零
if(!mode)
{
n=n<<1; //如果mode为0,进行状态左移
//如果n为0,相当于1"向左"溢出,对n进行重新赋值 第8个灯亮完,第1个灯亮
if(!n) //if(n==0x00)
n=0x01;
}
else
{
n=n>>1; //如果mode为1,进行状态右移
//如果n为0,相当于1“向右”溢出,对n进行重新赋值 第1个灯亮完,第8个灯亮
if(!n) //if(n==0x00)
n=0x80;
}
}
}
}
考虑到中断优先级的问题,由于“外部中断Int0/Int1的固有优先级”高于“定时器T0的固有优先级”,所以当“定时器中断”和“外部按键中断”同时来临时(概率很小),CPU会优先响应Int0/Int1的中断。但是由于按键的消抖会进行10ms的延时(此处不考虑程序执行时间),所以当定时器中断被响应后,LED灯的状态改变的时间会多出10ms。若对LED状态改变的时间要求特别严格的话,可以对程序进行以下改进。(此处不考虑两外部中断同时来临的问题,因为两按键同时按下几乎不可能。)
3) 外部中断INT0(P3.2)和INT1(P3.3)分别与按键key1和key2相连,key1控制LED灯状态的改变,key2控制LED灯的启停;定时器T0控制LED状态改变的间隔。 —— 提高T0的中断优先级
#include <reg52.h>
sbit key1=P3^2; //将P3端口的第2位定义为名为key1的位变量
sbit key2=P3^3; //将P3端口的第3位定义为名为key2的位变量
unsigned char k=0; //main函数和中断服务函数中均用到了k,所以k不能再main函数中进行定义
bit mode=0; //定义一个名为mode的位变量,并将该位变量进行初始化
//延时函数 实现1ms的延时 —— 用于按键的消抖
void Delay(unsigned int xms) //@12.0000MHz
{
unsigned char i, j;
while(xms)
{ //_nop_();
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
//定时器中断T0 —— 控制LED状态改变的时间间隔
void Timer0_Routine () interrupt 1
{
TH0=0x3c;
TL0=0xb0; //重置初值,中再次进行50ms计时
k++;
}
//外部中断Int0 —— 按下key1,LED灯的状态就会发生改变
void Int0_Routine () interrupt 0
{
Delay(10);
if(key1==0)
mode=~mode;
}
//外部中断Int1 —— key2控制LED的启停
void Int1_Routine () interrupt 2
{
Delay(10);
if(key2==0)
TR0=~TR0;
}
void main()
{
unsigned char n=0x01; //LED初始状态,由LED模块的公共端电平决定
TMOD=0x01; //将定时器T0的工作模式设置为模式1(定时功能)
TH0=0x3c;
TL0=0xb0; //设置定时时间为50ms,通过多次执行中断服务函数实现目标定时时间
IE=0x87; //1000 0111 EA=1 EX1=1 ET0=1 EX0=1
IT0=1;
IT1=1; //将两外部中断的触发方式设置为下降沿触发
PT0=1; //将定时器T0的优先级设置为高优先级
TR0=1; //定时器T0开始工作
while(1)
{
P0=~n;
if(k==10) //状态改变间隔为500m=50ms×10
{
k=0; //现将计数变量进行清零
if(!mode)
{
n=n<<1; //如果mode为0,进行状态左移
//如果n为0,相当于1"向左"溢出,对n进行重新赋值 第8个灯亮完,第1个灯亮
if(!n) //if(n==0x00)
n=0x01;
}
else
{
n=n>>1; //如果mode为1,进行状态右移
//如果n为0,相当于1“向右”溢出,对n进行重新赋值 第1个灯亮完,第8个灯亮
if(!n) //if(n==0x00)
n=0x80;
}
}
}
}