实现定时/计数,有3种主要方法:软件延时、数字电路硬件定时和可编程定时/计数器。
软件定时:让机器执行一个程序段,这个程序段本身没有具体的目的,通过挑选适当的指令和设置合适的循坏次数来实现软件延时,由于执行每条指令都需要时间,执行这个程序段所需要的时间就是延时时间。常见的指令有for循环,通过改过循环次数来改变延时的时间。缺点:软件延时占用CPU,降低CPU的利用率。
数字电路硬件定时:采用小规模集成电路器件,外接定时部件(电阻和电容)。通过改变电阻和电容的值来改变定时范围。缺点:硬件电路连接好之后,不方便修改。
可编程定时/计数器:硬件定时,同时可以通过软件来确定和改变它的定时值,通过初始化编程,能够满足各种定时和计数需求,也是应用较多的一种方式。
时钟周期,即时钟源频率频率(晶振)分之一,一个机器周期为12个时钟周期。机器周期就是定时器的计数周期,在我们设定好初值之后,每经过一个机器周期,定时器自动加1,加到一定数值之后就会溢出。对于8位的定时器,加到255后再加1溢出;对于16位的定时器,加到65535后再加1溢出。
定时器既可以工作在定时方式也可以工作在计数方式。这两种方式本质都是对脉冲进行计数,不过所计脉冲的来源不同。常用的是定时方式,(以T0为例)每过一个机器周期,计数器的初值加1,直至计满预设的个数,TH0和TL0回零,溢出中断标志位TF0置1,产生溢出中断。当我们设定了定时器的工作方式并启动后,定时器就按照规定的方式工作,不占用CPU的操作时间,此时CPU可以执行其他其他程序,除非定时器溢出,才会中断CPU正在执行的程序(类似于人在工作或者睡觉的时候,手表依然滴滴滴滴在走,到了设定的时间,闹钟会响)。
标准的51单片机内,常用的定时器有T0和T1。TH0和TL0分别用于存储定时器T0定时初值的高八位和低八位;TH1和TL1用于存储定时器T1定时初值的高八位和低八位。
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
TF1:T1溢出中断请求标志。T1有溢出中断请求,TF1硬件置 1;T1无溢出中断请求,TF1 =0。有两种清零方式,软件清零和进入定时器中断时硬件清零。
TR1:T1运行控制位。TR1=1,启动T1工作;TR1=0,停止T1工作。
TF0:T0溢出中断请求标志。T1有溢出中断请求,TF0硬件置1;T0无溢出中断请求,TF0 =0。有两种清零方式,软件清零和进入定时器中断时硬件清零。
TR0:T0运行控制位。TR0=1,启动T1工作;TR0=0,停止T0工作。
注:硬件置1或清零是指符合条件后,单片机自动完成;软件置1或清零是指需要通过编写程序实现。
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | GATE(T1) | C/T(T1) | M1(T1) | M0(T1) | GATE(T0) | C/T(T0) | M1(T0) | M0(T0) |
GATE:门控信号。GATE=0, TRx=1时,可启动定时器工作;GATE=1,除TRx=1外,还需INTx= 1才可启动定时器工作。
C /T:定时/计数器选择控制位。置1为计数器,置0为定时器。
M1M0:定时器的工作模式选择位。
定时器初值的计算:(设定时时间为T)
①方式0:13位定时器。T=(2^13-T0初值)* 12 / fosc
TH0 = (2^13-T * fosc/12) / 32
TL0 = (2^13-T * fosc/12) / 32;
②方式1:16位定时器。T=(2^16-T0初值)* 12 / fosc
TH0 = (2^16-T * fosc/12) /256,
TL0 = (2^16-T * fosc/12) / 256;
③方式2:8位自动重载定时器。T=(2^8-T0初值)* 12 / fosc
TH0 = TL0 = T=(2^8-T0初值)* 12 / fosc
定时器应用可以按照以下四个步骤:
①设置特殊功能寄存器TMOD,配置好工作模式。
②设置计数寄存器THO和TL0的初值。
③设置TCON,通过TRO置1来让定时器开始计数。
④判断TCON寄存器的TF0位,监测定时器溢出情况。
以上整个过程即为中断。8XX51有5个中断源,3个在片内,2个在片外(如下图)。它们在程序存储器中有固定的中断入口地址,当CPU响应中断时,硬件自动形成这些地址,由此进入中断服务程序;5个中断源有两级中断优先级,可形成中断嵌套。
使用定时器时一般也会用到中断,但要注意定时器和中断不是一个概念,定时器是单片机模块的一个资源,而中断只是单片机的一种中断机制。
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | EA | … | ET 2 | ES | ET1 | EX1 | ET0 | EX0 |
IE寄存器的各位对应相应的中断源,如果允许该中断源中断,则该位置1,禁止中断则该位为0。其中EA为中断总控开关,是CPU是否响应中断的前提。ES为串行口中断允许位,ET2为定时器T2中断允许位,ET1为定时器T1中断允许位,ET0为定时器T0中断允许位,EX0为外部中断INT0允许位,EX1为外部中断INT1允许位。
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
IE1:外部中断1(INT1)运行控制位。IE1=1.启动INT1控制;
IE1=0,停止INT1控制。
IT1:外部中断1(INT1)触发方式控制位。IT1=1,下降沿触发;IT1=0,低电平触发。
IE0:外部中断0(INT0)运行控制位。IE0=1.启动INT1控制;
IE0=0,停止INT1控制。
IT0:外部中断0(INT0)触发方式控制位。IT0=1,下降沿触发;IT0=0,低电平触发。
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | … | … | PT2 | PS | PT1 | PX1 | PT0 | PX0 |
当某几个中断源同时发出中断请求时,由内部查询确定优先级,查询的顺序为:(INT0的优先级最高)
INT0——T0——INT1——T1——串行口 ——T2
响应条件:
中断的应用:
void timer() interrupt n
{
}
注:timer()为函数名,自己随意取;interrupt为关键字,不能更改,n为中断函数序列编号。
数码管的原理图如图3-1,本质就是把8段LED小灯组合在一起。
数码管可以分为共阴极和共阳极,内部结构如下图3-2所示:
共阴极数码管,即把8只LED灯的阴极连接在一起,由阳极来控制单个小灯的亮灭(1亮0灭);同理,共阳数码管就是阳极连接在一起,通过阴极来控制小灯的亮灭(1灭0亮)。
将数码管的8个段当成8个LED小灯来控制,即a、b、c、d. e、f、g、dp- -共8个LED小灯。以图3-1,如果点亮b和c这两个LED小灯,也就是数码管的b段和c段,其他的所有的段都熄灭的话,就可以让数码管显示出一个数字1,以共阳极数码管为例,二进制数字为0b11111001,十六进制为0xF9。同理可得其他数字多对应的数码管的真值,如下:
字符 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
数值 | 0XC0 | 0XF9 | 0XA4 | 0XB0 | 0X99 | 0X92 | 0X82 | 0XF8 |
字符 | 8 | 9 | A | B | C | D | E | F |
数值 | 0X80 | 0X90 | 0X88 | 0X83 | 0XC6 | 0XA1 | 0X86 | 0X8E |
unsigned char code LEDchar[]= {
0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,
0x80,0x90,0x88,0x80,0xc6,0xc0,0x86,0x8e,0xbf,0x7f};
//共阳极数码管从0到F的真值
字符 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
数值 | 0X3F | 0X06 | 0X5B | 0X4F | 0X66 | 0X6D | 0X7D | 0X07 |
字符 | 8 | 9 | A | B | C | D | E | F |
数值 | 0X7F | 0X6F | 0X77 | 0X7C | 0X39 | 0X5E | 0X79 | 0X71 |
unsigned char code LEDchar[]={
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71};
//共阴极数码管从0到F的真值
unsigned char
或者 unsigned int
定义的变量都是放在单片机的RAM中,这些变量的值可以通过程序改变。而在定义时使用code修饰的变量是存储到存储空间FLASH里的,这样可以大大的节省的RAM的空间,但是这些变量的值在程序中不可以修改,所以我们在定义一些程序中不需要修改的变量时,可以使用code关键字,例如上面提到的数码管的真值的定义:unsigned char code LEDchar[]={};
原理图如下:
代码功能:一位数码管静态显示0到F,数字(字母)切换时间为1秒。
//数码管静态显示,0到F
#include<reg52.h>
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;
typedef unsigned char u8;
unsigned char code LEDchar[]={
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07,
0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71}; //共阴极数码管从0到F的真值
void TimeInit(); //定时器初始化函数
void main()
{
u8 cnt = 0; //记录T0中断次数
u8 sec =0; //记录经过的秒数
LSA = 0; //数码管的位选
LSB = 0;
LSC = 0;
TimeInit(); //定时器初始化
while(1)
{
if(TF0 == 1) //判断T0是否溢出
{
TF0 = 0; //T0溢出后,清楚中断标志
TH0 = (65536-2*110592/12)/256; //0xB8,定时20ms 0.02秒*11059200
TL0 = (65536-2*110592/12)%256; //0x00
cnt++;
if(cnt>=50) //判断是否达到50次,即一秒钟
{
cnt = 0; //清零
P0 = LEDchar[sec]; //当前秒数对应的真值表中的真值送到P0口
sec++; //秒数记录加1
if(sec>=16) //秒数达到0x0F后,重新从0开始
{
sec = 0;
}
}
}
}
}
void TimeInit()
{
TMOD = (TMOD & 0xf0)|0x01; //设置定时器工作在模式1
TH0 = (65536-2*110592/12)/256; //0xB8,定时20ms
TL0 = (65536-2*110592/12)%256; //0x00
TR0 = 1; //允许定时器T0工作
}
CT107D开发板是蓝桥比赛用开发板,原理图如下:
代码如下:
//数码管静态显示,0到F
#include<reg52.h>
typedef unsigned char u8;
unsigned char code LEDchar[]={
0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,
0x80,0x90,0x88,0x80,0xc6,0xc0,0x86,0x8e,
0xbf,0x7f}; //共阳极数码管从0到F的真值
void TimeInit(); //定时器初始化函数
void HC138(u8 channel); //138译码器通道选择函数
void ShowSMG_Bit(u8 date,u8 pos); //数码管显示函数
void main()
{
u8 cnt = 0; //记录T0中断次数
u8 sec =0; //记录经过的秒数
TimeInit(); //定时器初始化
while(1)
{
if(TF0 == 1) //判断T0是否溢出
{
TF0 = 0; //T0溢出后,清楚中断标志
TH0 = (65536-2*110592/12)/256; //重新加载初值
TL0 = (65536-2*110592/12)%256;
cnt++;
if(cnt>=50) //判断是否达到50次,即一秒钟
{
cnt = 0; //清零
ShowSMG_Bit(LEDchar[sec],7); //数码管显示对应的秒数
sec++; //秒数记录加1
if(sec>=16) //秒数达到0x0F后,重新从0开始
{
sec = 0;
}
}
}
}
}
void HC138(u8 channel)
{
switch(channel)
{
case 4:
P2 = (P2&0x1f)|0x80; //选择Y4对应模块(LED小灯) ,运算结果为P2高三位为100
break;
case 5:
P2 = (P2&0x1f)|0xa0; //选择Y5对应模块(蜂鸣器),运算结果为P2高三位为101
break;
case 6:
P2 = (P2&0x1f)|0xc0; //选择Y6对应模块(数码管的位选),运算结果为P2高三位为110
break;
case 7:
P2 = (P2&0x1f)|0xe0; //选择Y7对应模块(数码管的段选),运算结果为P2高三位为111
break;
}
}
void ShowSMG_Bit(u8 date,u8 pos)
{
HC138(6); //Y6模块工作,数码管进行位选
P0 = 0x01<<pos; //数码管的位选传到P0口
HC138(7); //Y7模块工作,数码管进行段选
P0 = date; //当前数码管要显示数值对应的真值传到P0口
}
void TimeInit()
{
TMOD = (TMOD & 0xf0)|0x01; //设置定时器工作在模式1
TH0 = (65536-2*110592/12)/256; //0xB8,定时20ms 0.02秒*11059200
TL0 = (65536-2*110592/12)%256; //0x00
TR0 = 1; //允许定时器T0工作
}
动态数码管,即同时有多个数码管显示数字,实际上是轮流点亮各个数码管(同一时刻只有一个数码管被点亮),利用人眼的视觉暂留现象(余晖效应),使我们看到多个数码管同时点亮的效果。就像我们看到的电影也是由一帧一帧的画面组成的,但因为速度足够快,我们看到它就是动态的。刷新时间小于10毫秒时,对于人眼来说就是无闪烁的。
注:数码管的显示涉及段选和位选。段选即选择数码管的哪一段 (a、b、c、d、e、f、g、dp),位选即选择哪一个数码管。
以普中科技的开发板为例(共阴极数码管),原理图如下:
代码功能:电子钟,从右到左,1、2位为秒,3、4位为分,5、6位为时,7、8位为天。
#include<reg52.h>
sbit LSA = P2^2;
sbit LSB = P2^3;
sbit LSC = P2^4;
typedef unsigned char u8;
typedef unsigned int u16;
u8 LedChar[] = {
0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f}; //共阴极数码管从0到9的真值
u8 LedBuff[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓冲区,初值0xFF确保启动时都不亮
u8 flags = 1; //1秒定时标志
void TimeInit(); //定时器中断初始化函数
int main()
{
u8 sec = 0, minutes = 0, hours = 0, days = 0; //定义记录秒,分,时,天的变量
TimeInit(); //定时器中断初始化
while(1)
{
if(flags == 1) //判断1秒定时标志
{
flags = 0; //标志位清零
sec++; //秒加1
if(sec == 60) //判断秒变量到达60秒
{
sec = 0; //清0零
minutes++; //分钟位加1
}
if(minutes == 60) //判断达到60分
{
minutes = 0; //清零
hours++; //小时位加1
}
if(hours == 24) //判断达到24小时
{
hours =0; //清零
days++; //天数加1
}
LedBuff[0]= LedChar[sec%10]; //秒的低位
LedBuff[1]= LedChar[sec/10%10]; //秒的高位
LedBuff[2]= LedChar[minutes%10]; //分钟的低位
LedBuff[3]= LedChar[minutes/10%10]; //分钟的高位
LedBuff[4]= LedChar[hours%10]; //小时的低位
LedBuff[5]= LedChar[hours/10%10]; //小时的高位
LedBuff[6]= LedChar[days%10]; //天数的低位
LedBuff[7]= LedChar[days/10%10]; //天数的高位
}
}
}
void TimeInit()
{
EA = 1; //打开中断总开关
ET0 = 1; //打开T0中断开关
TMOD = (TMOD & 0xf0)|0x01; //设置定时器工作在模式1
TH0 = (65536-1*11059/12)/256; //0xFC,定时1ms 0.001秒*11059200
TL0 = (65536-1*11059/12)%256; //0x67
TR0 = 1; //允许定时器T0工作
}
void InterruptTimer0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
static u16 cnt = 0; //用来记录中断次数,设置的中断定时为1ms,中断1000次即一秒钟
TH0 = (65536-1*11059/12)/256; //重新加载初值
TL0 = (65536-1*11059/12)%256;
cnt++; //中断次数计数值加1
if(cnt>=1000) //中断次数达到1000次即为1秒
{
cnt = 0; //清零,重新记录中断次数
flags = 1; //设置1秒定时标志位1
}
P0 = 0xff; //消隐
switch(i) //根据动态索引i的值进行位选,依次点亮八个数码管
{
case 0 :LSA = 0; LSB = 0; LSC = 0;i++; P0 = LedBuff[0];break;
case 1 :LSA = 1; LSB = 0; LSC = 0;i++; P0 = LedBuff[1];break;
case 2 :LSA = 0; LSB = 1; LSC = 0;i++; P0 = LedBuff[2];break;
case 3 :LSA = 1; LSB = 1; LSC = 0;i++; P0 = LedBuff[3];break;
case 4 :LSA = 0; LSB = 0; LSC = 1;i++; P0 = LedBuff[4];break;
case 5 :LSA = 1; LSB = 0; LSC = 1;i++; P0 = LedBuff[5];break;
case 6 :LSA = 0; LSB = 1; LSC = 1;i++; P0 = LedBuff[6];break;
case 7 :LSA = 1; LSB = 1; LSC = 1;i=0; P0 = LedBuff[7];break;
default:break;
}
}
以CT107D开发板为例(共阳极数码管),原理图如下:
功能同上,代码如下:
#include<reg52.h>
typedef unsigned char u8;
typedef unsigned int u16;
u8 LedChar[] ={
0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //共阳极数码管从0到9的真值
u8 LedBuff[] = {
0xFF, 0xFF, 0xFF,0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓冲区,初值0xFF确保启动时都不亮
u8 flags = 1; //1秒定时标志
void TimeInit(); //定时器中断初始化函数
void HC138(u8 channel); //138译码器通道选择函数
void ShowSMG_Bit(u8 date,u8 pos); //数码管显示函数
int main()
{
u8 sec = 0, minutes = 0, hours = 0, days = 0; //定义记录秒,分,时,天的变量
TimeInit(); //定时器中断初始化
while(1)
{
if(flags == 1) //判断1秒定时标志
{
flags = 0; //标志位清零
sec++; //秒加1
if(sec == 60) //判断秒变量到达60秒
{
sec = 0; //清0零
minutes++; //分钟位加1
}
if(minutes == 60) //判断达到60分
{
minutes = 0; //清零
hours++; //小时位加1
}
if(hours == 24) //判断达到24小时
{
hours = 0; //清零
days++; //天数加1
}
LedBuff[0]= LedChar[sec%10]; //秒的低位
LedBuff[1]= LedChar[sec/10%10]; //秒的高位
LedBuff[2]= LedChar[minutes%10]; //分钟的低位
LedBuff[3]= LedChar[minutes/10%10]; //分钟的高位
LedBuff[4]= LedChar[hours%10]; //小时的低位
LedBuff[5]= LedChar[hours/10%10]; //小时的高位
LedBuff[6]= LedChar[days%10]; //天数的低位
LedBuff[7]= LedChar[days/10%10]; //天数的高位
}
}
}
void TimeInit()
{
EA = 1; //打开中断总开关
ET0 = 1; //打开T0中断开关
TMOD = (TMOD & 0xf0)|0x01; //设置定时器工作在模式1
TH0 = (65536-1*11059/12)/256; //0xFC,定时1ms 0.001秒*11059200
TL0 = (65536-1*11059/12)%256; //0x67
TR0 = 1; //允许定时器T0工作
}
void HC138(u8 channel)
{
switch(channel)
{
case 4:
P2 = (P2&0x1f)|0x80; //选择Y4对应模块(LED小灯) ,运算结果为P2高三位为100
break;
case 5:
P2 = (P2&0x1f)|0xa0; //选择Y5对应模块(蜂鸣器),运算结果为P2高三位为101
break;
case 6:
P2 = (P2&0x1f)|0xc0; //选择Y6对应模块(数码管的位选),运算结果为P2高三位为110
break;
case 7:
P2 = (P2&0x1f)|0xe0; //选择Y7对应模块(数码管的段选),运算结果为P2高三位为111
break;
}
}
void ShowSMG_Bit(u8 date,u8 pos)
{
HC138(6); //Y6模块工作,数码管进行位选
P0 = 0x01<<pos; //数码管的位选传到P0口
HC138(7); //Y7模块工作,数码管进行段选
P0 = date; //当前数码管要显示数值对应的真值传到P0口
}
void InterruptTimer0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
static u16 cnt = 0; //用来记录中断次数,设置的中断定时为1ms,中断1000次即一秒钟
TH0 = (65536-1*11059/12)/256; //重新加载初值
TL0 = (65536-1*11059/12)%256;
cnt++; //中断次数计数值加1
if(cnt>=1000) //中断次数达到1000次即为1秒
{
cnt = 0; //清零,重新记录中断次数
flags = 1; //设置1秒定时标志位1
}
P0 = 0xff; //消隐
switch(i) //根据动态索引i的值进行位选,依次点亮八个数码管
{
case 0 : i++; ShowSMG_Bit(LedBuff[0],7); break; //最右边一位数码管(com8)对应P0^7口
case 1 : i++; ShowSMG_Bit(LedBuff[1],6); break;
case 2 : i++; ShowSMG_Bit(LedBuff[2],5); break;
case 3 : i++; ShowSMG_Bit(LedBuff[3],4); break;
case 4 : i++; ShowSMG_Bit(LedBuff[4],3); break;
case 5 : i++; ShowSMG_Bit(LedBuff[5],2); break;
case 6 : i++; ShowSMG_Bit(LedBuff[6],1); break;
case 7 : i=0; ShowSMG_Bit(LedBuff[7],0); break;
default:break;
}
}