2017-07-25 12:53:30 wangwangpengpeng 阅读数 39718
  • 单片机控制第一个外设-LED灯-第1季第6部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第6个课程,主要讲解LED的工作原理和开发板原理图、实践编程等,通过学习目的是让大家学会给单片机编程控制LED灯,并且为进一步学习其他外设打好基础。

    3997 人正在学习 去看看 朱有鹏
利用51单片机制作一个秒表的详细过程

前面的话:

和很多朋友一样,在学51单片机的过程中我们肯定会涉及到制作一个秒表,牵涉到把单片机的多个部分组合起来使用,这对于我们初学者来说可能显得有些困难,我同大家一样,百思不得其解,最后头都弄大了才把这个秒表制作出来,为了给以后的朋友们一些思路,一些参考,所以在这里我把自己制作的整个详细过程整理出来供大家参考。我调试出来是没有问题的,各方面都稳定运行,由于我水平有限,中间可能会有不对的地方,欢迎大家指正,我们一起学习,一起进步!

我将分为三个部分来介绍:1.整体思路,2.硬件电路方面,3.软件编程方面。


1.整体思路

利用51单片机制作秒表时,我介绍精确到十分位(即0.1s)的制作,并让其拥有启动,暂停,复位三个功能。
用到的单片机部分:定时器部分,独立按键的检测与应用,数码管的显示,并结合一些简单的程序即可实现。
用5位数码管来进行显示,分别显示秒的十分位,秒的个位,秒的十位,分的个位,分的十位。用定时器定时50ms,2个定时器中断即是0.1s,即秒的十分位,20个定时器中断即是1s,60个1s即是1分钟,通过程序将5位数码管的值分离出来,并进行显示。这就是我在数码管显示方面的思路,如果不是太清楚,结合我下面软件编程方面的程序来看你可能就会明白,我会在那部分做详细介绍,看完了可能你就懂了。
利用独立按键设置启动/暂停键和清零键,利用独立按键的检测,若启动/暂停按键按下,秒表则启动或者暂停,按下复位键,秒表清零复位。我在程序后面全都有注释,不用担心。看完你就会明白了。
这是我制作的的流程图:




“ms100”表示秒的十分位,"s"表示秒的个位,“s1”表示秒的十位,“min”表示分的个位,“min1”表示分的十位。
“cnt”表示秒的计数位,即多少个一秒,定时满一秒加1,“minu”表示分的计数位,即多少个一分钟,一分钟加1,
这个流程图提供了大致思路,要结合下面的程序部分一起看。可能有些简陋,请多多包涵!


看起来是不是好简单?有木有?请继续往下看。

2.硬件电路方面

每个人的硬件可能都不一样,(哪些控制数码管的位选,哪些控制数码管的段选,哪些控制独立按键等等),大家在自己制作过程中都要仔细考虑,我在这里就用我自己的硬件进行介绍。

这是我的数码管的电路图:(将J12用杜邦线接到对应的P0口,将J16用杜邦线接到对应的P1口)


位选端我用P1口控制,段选端我用P0口控制。在图中我已标明秒的个位s,秒的十位s1,分的个位min,分的十位min1所对应的数码管。该数码管是共阴极。
秒的十分位我用的是另一个共阳数码管。我用P3口控制该数码管的段选,其图如下:



而独立按键我用的P2^7控制“启动/暂停键”,P2^6控制复位键:电路图如下:(将JP5通过杜邦线接到P3口)

硬件电路基本上就是这些,由于分和秒之间要用小数点分隔开,所以分的个位对应的数码管的段码要用带小数点的段码,其余的数码管则不用。

硬件方面是为看懂软件程序做准备,下面我们就开始我们的软件程序方面吧!  

3.软件程序方面


#include <REGX51.H>
#define uchar unsigned char
#define uint unsigned int

#define PIN0 P0  //数码管段选端
#define PIN1 P1  //数码管位选端
#define PIN3 P3	 //十分位数码管的段码端
sbit start=P2^7;  //启动/暂停按钮
sbit reset=P2^6;  //复位按钮
uint cnt=0;   //1s的次数的计数值
uint num=0;     //进入中断次数的计数值(一次50ms)
uint num1=0;		//0.1s的次数的计数值
uchar code table[]={0x3f,0x06,0x5b,
0x4f,0x66,0x6d,0x7d,0x07, 0x7f,0x6f};
//不带小数点的共阴极数码管段码0-9
uchar code table1[]={0xbf,0x86,
0xdb,0xcf,0xe6,0xed,0xfd,0x87,0xff,0xef};
//带小数点的共阴极数码管段码0-9

char code table2[]={0xc0,0xf9,0xa4,
0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
//不带小数点的共阳极数码管段码0-9

void Display(uint w,uint a,uint b,uint c,uint d);  //声明显示函数
void InitTimer0();  //声明定时器初始化函数
void Delayms(uint x);  //声明延时函数

void main()
{
	uint ms100=0;	//秒的十分位
	uint s,s1,min,min1,minu=0; 
	//依次是秒的个位,秒的十位,分的个位,分的十位
	InitTimer0();	  //初始化定时器
	while(1)			//进入死循环
{	
	if(start==0)   //检测启动/暂停键是否按下
			Delayms(20); //延时消抖
	{
		if(start==0)  //消抖之后再次检测
		{
			TR0=!TR0;		//定时器的控制位取反
			while(!start);  //等待按键释放
		}
	}
	
if(reset==0)  //检测复位键是否按下
{
	Delayms(20);	//延时消抖
	if(reset==0)  	//消抖之后再次检测
	{
		num=0;		
		num1=0;		//进入中断次数值清0
		ms100=0;	//十分位的计数值清0
		cnt=0;		//秒的计数值清0
		minu=0;		//分的计数值清0
		while(!reset);		 //等待按键释放
	}
}
	if(num1>=2)		//检测是否达到0.1s,(两个50ms)
	{
		num1=0;  		//进入中断次数值清零
		ms100++;		//秒的十分位的计数值加1
		if(ms100>=10)  //若秒的十分位计数值达到10
		{
			ms100=0;  //清零秒的十分位的计数值
			num1=0;	//进入中断次数值置0
		}
	}
	
	if(num>=20)  //判断计时时间是否达到1s(20个50ms)
	{
		num=0;     //进入中断次数值置0
		cnt++;		 //秒的计数值加1
		if(cnt>=60) //判断是否达到60s
	{
		cnt=0;		//若达到60s,将秒的计数值置零
		minu++;			//分的计数值加1
		if(minu>=60)  //判断是否达到60分钟,一般秒表是用不到60分的,这是为了程序的严谨性

		{
			TR0=!TR0;			//达到60分钟则关闭定时器并清零所有的计数值
			num1=0;				
			num=0;
			ms100=0;
			cnt=0;		
			minu=0;
			
		}
	}
	}	
	s=cnt%10;  	//从秒的计数值里面分离秒的个位
	s1=cnt/10; 	//从秒的计数值里面分离秒的十位
	min=minu%10;	//从分的计数值里面分离分的个位
	min1=minu/10;	//从分的计数值里面分离分的十位
	
	Display(ms100,s,s1,min,min1);  //显示这5个数
}	
}

void InitTimer0() //定时器初始化函数
{
	
	TMOD=0x01; //选择定时器0的工作方式1
	TH0=(65536-45872)/256;  //装初值(定时50ms),晶振11.0592MHz
	TL0=(65536-45872)%256;
	EA=1;		//打开总中断
	ET0=1;	//打开定时器中断
	TR0=0;	//先不要启动定时器
	
}

void TIMER0()  interrupt 1  //定时器中断服务函数
{
	TH0=(65536-45872)/256;  //重装初值
	TL0=(65536-45872)%256;
	num++;		//让进入中断次数值加1,用于判断是否达到1s
	num1++;		//让进入中断次数值加1,用于判断是否达到0.1s
}

void Delayms(uint x)   //延时xms
{
	uint i,j;
	for(i=x;i>0;i--)
		for(j=110;j>0;j--);
}

void Display(uint w,uint a,uint b,uint c,uint d) //定义显示函数
{
	PIN3=table2[w];		//秒的十分位的段选
	Delayms(5);
	
	PIN0=table[a]; //秒的个位的段选
	PIN1=0x7f;		 //秒的个位的位选
	Delayms(5);			
	
	PIN0=table[b];  //秒的十位的段选
	PIN1=0xbf;			//秒的十位的位选
	Delayms(5);
	
	PIN0=table1[c];  //分的个位的段选(带小数点)
	PIN1=0xdf;			 //分的个位的位选
	Delayms(5);
		
	PIN0=table[d];		//分的十位的段选
	PIN1=0xef;				//分的十位的位选
	Delayms(5);		
		
}

由于在打字时可能不小心会弄错一丁点程序,我都是一个字母一个字母手打的,所以请大家理解思路即可,根据这个思路自己去写,这样得到的知识才是属于自己的。毕竟 “读别人的故事,悟自己的人生”。
下面来几张实物图给大家看一下我这个秒表的实际效果。

编译结果:





烧写进单片机的效果:(没有按启动按钮之前)






按了启动按钮之后,秒表将会自动开始计时,由于无法演示动态过程,只能在中间按下暂停键给大家看看效果:











按下复位键后:



由于我用的是一个比较完整的51开发板,所以里面有很多部分,大家只需要注意观察数码管部分就好。
下面再来一个完整的图:





结束语

至此,利用51单片机制作的秒表就已经完成了!若是文章中有什么不懂的地方或者是自己在制作过程中遇到什么问题都可以联系我,或者给我留言,我一定竭尽全力帮助大家!我们一起学习,一起进步!希望大家都能在看完这篇文章后自己动手成功制作出一个秒表,祝愿大家学好单片机!当然,由于自身水平有限,文章中肯定会存在错误或者是考虑不周的的地方,恳请不吝赐教!谢谢大家!

(PS:我写了一整天了,反复的看,反复的改,现在头昏眼花,但是只要能对大家有所帮助,一切都是值得的。)




2011-11-27 18:41:42 jinmmd 阅读数 6067
  • 单片机控制第一个外设-LED灯-第1季第6部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第6个课程,主要讲解LED的工作原理和开发板原理图、实践编程等,通过学习目的是让大家学会给单片机编程控制LED灯,并且为进一步学习其他外设打好基础。

    3997 人正在学习 去看看 朱有鹏

下午自学了一下单片机的前几章的知识,理解了数码管的显示原理以及静态显示和动态扫描的编程方法。其中,比较重要的几个概念有:数码管公共端类型(共阴极、共阳极)、位选(决定多位数码管中哪一位显示)、段选(决定数码管显示的数字),另外还有锁存器(教材P36,可通过控制锁存端来改变数据输出的状态)。

上周单片机实验课的内容是编一个秒表,在4位数码管中分别显示分和秒还有分秒,即最多可显示“9:59.9”。这里我用两个键实现了秒表的启动/暂停和清零功能,代码如下:

#include <reg51.h>
#define uint unsigned int
#define uchar unsigned char
sbit S1 = P1^3;
sbit S2 = P1^1;
sbit S3 = P1^2;
sbit S4 = P1^0;
sbit beep = P2^7;
sbit a = P0^0;
sbit b = P0^1;
sbit c = P0^2;
sbit d = P0^3;
sbit e = P0^4;
sbit f = P0^5;
sbit g = P0^6;
sbit p = P0^7;
sbit key1 = P1^4;
sbit key2 = P1^5;
sbit key3 = P3^6;
sbit key4 = P3^7;
uchar num,kms,sec,min;
uchar code N[10] = {0xc0, 0xf9 ,0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //0, 1, 2, 3, ...8, 9
uchar code Z[10] = {0x40, 0x79 ,0x24, 0x30, 0x19, 0x12, 0x02, 0x78, 0x00, 0x10}; //0., 1., 2., ... 8., 9.

void delayms(uint xms)
{
	uint i,j;
	for(i = xms; i > 0; i--)
		for(j = 110; j > 0; j--);
}

void init()
{
	TMOD = 0x01;
	TH0 = 0x3c;
	TL0 = 0xb0;
	S3 = 1;
	S1 = S2 = S4 = 0;
	P0 = N[0];
	delayms(5);

	S3 = 0;
	S1 = S2 = S4 = 1;
	P0 = Z[0];
	delayms(5);

	EA = 1;
	TR0 = 0;
	ET0 = 1;	
	kms = sec = min = 0;
}

void display()
{
	uchar shi, ge;
	if(kms >= 0)
	{
		S4 = 0;
		S1 = S2 = S3 = 1;
		P0 = N[kms];
	}
	delayms(5);
	if(sec >= 0)
	{
		shi = sec/10;
		ge = sec%10;

		S3 = 0;
		S1 = S2 = S4 = 1;
		P0 = Z[ge];
		delayms(5);

		S2 = 0;
		S1 = S3 = S4 = 1;
		P0 = N[shi];
	}
	delayms(5);
	if(min >= 0)
	{
		S1 = 0;
		S2 = S3 = S4 = 1;
		P0 = N[min];
	}
	delayms(5);
}

void keyscan()
{
	if(key1 == 0)
	{
		delayms(10);
		if(key1 == 0)
		{
			while(!key1);
			TR0 = ~TR0;
		}
	}
	if(key2 == 0)
	{
		delayms(10);
		if(key2 == 0)
		{
			min = sec = kms = 0;
			while(!key1);
		}
	}
}

main()
{
	init();
	while(1)
	{
		keyscan();
		display();		
	}
}

void T0_time() interrupt 1
{
	TH0 = 0x3c;
	TL0 = 0xb0;
		num++;
	if(num == 2)
	{
		num = 0;
		kms++;
		if(kms == 10)
		{
			kms = 0;
			sec++;
			if(sec == 60)
			{
				sec = 0;
				min++;
				if(min == 10)
				{
					TR0 = 0;
					min = 9;
					sec = 59;
					kms = 9;
				}
			}
		}
	}
} 

程序通过“Keil uVision4”调试,大家也可以在板子上运行一下试试。

注意:需要根据板子的端口定义相关变量,不要不分青红皂白就直接使用我的程序哈。

最后上个图,第一次做单片机,希望以后还有时间可以做更深入的更好玩的东西。


2020-01-14 16:04:38 lymtics1111 阅读数 162
  • 单片机控制第一个外设-LED灯-第1季第6部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第6个课程,主要讲解LED的工作原理和开发板原理图、实践编程等,通过学习目的是让大家学会给单片机编程控制LED灯,并且为进一步学习其他外设打好基础。

    3997 人正在学习 去看看 朱有鹏

用定时器做一个秒表,精确到0.1,S7按下,开始计时,S6按下清零

#include <stc15f2k60s2.h>
#include <intrins.h>
void Timer0Init();
void Timer1Init();
unsigned char abile[]={0XC0,0XF9,0XA4,0XB0,0X99,0X92,0X82,0XF8,0X80,0X90,0XBF,0XFF};
unsigned char saomiao[]={0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff};
void Delay100us();
void shumaguan();
void anjian();
sbit S7=P3^0;
sbit S6=P3^1;
unsigned int y;

void main()
{
	P2=0XC0;P0=0X01;P2=0XFF;P0=0XFF;
	Timer0Init();
	Timer1Init();
while(1)
{
  shumaguan();
	anjian();
	Delay100us();
}

}
void Timer0Init()		//100??@11.0592MHz
{
	AUXR |= 0x80;	
	TMOD &= 0xF0;	
	TL0 = 0xAE;	
	TH0 = 0xFB;		
	TF0 = 0;		
	TR0 = 1;		
	EA=1;
	ET0=1;
}
void weixuan() interrupt 1
{
   unsigned char i;
	P2=0XE0;
	P0=0XFF;
	P2=0X1F;
	
	P2=0XC0;
	P0=1<<i;
	P2=0X1F;
	
	P2=0XE0;
	P0=saomiao[i];
	P2=0X1F;
	
	i++;
	if(i==8)
		i=0;
}


void shumaguan()
{
  saomiao[0]=abile[y/100];
  saomiao[1]=abile[y%100/10]&0x7f;
	saomiao[2]=abile[y%10];
	saomiao[3]=0xff;
	saomiao[4]=0xff;
	saomiao[5]=0xff;
	saomiao[6]=0xff;
	saomiao[7]=0xff;
}
void Delay100us()		//@11.0592MHz
{
	unsigned char i, j;

	_nop_();
	_nop_();
	i = 2;
	j = 15;
	do
	{
		while (--j);
	} while (--i);
}
void Timer1Init(void)		//1??@11.0592MHz
{
	AUXR |= 0x40;		//?????1T??
	TMOD &= 0x0F;		//???????
	TL1 = 0xCD;		//??????
	TH1 = 0xD4;		//??????
	TF1 = 0;		//??TF1??
	TR1 = 1;		//???1????
	
	EA=1;
}

void jishu() interrupt 3
{
   unsigned char i;
		 i++;
	 if(i==100)
	 {i=0;
	y++;}
}
void anjian()
{
if(S7==0)
{
	Delay100us();
	if(S7==0)
	{
	ET1=~ET1;
	}
while(!S7);
}
if(S6==0)
{
	Delay100us();
	if(S6==0)
	{
	y=0;
	}
while(!S7);
}
}
2016-07-07 05:36:10 softn 阅读数 2449
  • 单片机控制第一个外设-LED灯-第1季第6部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第6个课程,主要讲解LED的工作原理和开发板原理图、实践编程等,通过学习目的是让大家学会给单片机编程控制LED灯,并且为进一步学习其他外设打好基础。

    3997 人正在学习 去看看 朱有鹏

不同数据类型间的相互转换

在 C 语言中,不同数据类型之间是可以混合运算的。当表达式中的数据类型不一致时,首先转换为同一种类型,然后再进行计算。C 语言有两种方法实现类型转换,一是自动类型转换,另外一种是强制类型转换。这块内容是比较繁杂的,因此我们根据常用的编程应用来讲部分相关内容。

当不同数据类型之间混合运算的时候,不同类型的数据首先会转换为同一类型,转换的主要原则是:短字节的数据向长字节数据转换。比如:
  1. unsigned char a;
  2. unsigned int b;
  3. unsigned int c;
  4. c = a *b;
在运算的过程中,程序会自动全部按照 unsigned int 型来计算。比如 a=10,b=200,c 的结果就是 2000。那当 a=100,b=700,那 c 是 70000 吗?新手最容易犯这种错误,大家要注意每个变量类型的取值范围,c 的数据类型是 unsigned int 型,取值范围是 0~65535,而 70000超过 65535 了,其结果会溢出,最终 c 的结果是(70000 - 65536) = 4464。

那要想让 c 正常获得 70000 这个结果,需要把 c 定义成一个 unsigned long 型。我们如果写成:
  1. unsigned char a=100;
  2. unsigned int b=700;
  3. unsigned long c=0;
  4. c = a*b;
有做过实验的同学,会发现这个 c 的结果还是 4464,这个是个什么情况呢?

大家注意,C 语言不同类型运算的时候数值会转换同一类型运算,但是每一步运算都会进行识别判断,不会进行一个总的分析判断。比如我们这段代码中 a 和 b 相乘的时候,是按照 unsigned int 类型运算的,运算的结果也是 unsigned int 类型的 4464,只是最终把 unsigned int类型 4464 赋值给了一个 unsigned long 型的变量而已。我们在运算的时候如何避免这类问题的产生呢?可以采用强制类型转换的方法。

在一个变量前边加上一个数据类型名,并且这个类型名用小括号括起来,就表示把这个变量强制转换成括号里的类型。如 c = (unsigned long)a * b;由于强制类型转换运算符优先级高于*,所以这个地方的运算是先把 a 转换成一个 unsigned long 型的变量,而后与 b 相乘,根据 C 语言的规则 b 会自动转换成一个 unsigned long 型的变量,而后运算完毕结果也是一个unsigned long 型的,最终赋值给了 c。

不同类型变量之间的相互赋值,短字节类型变量向长字节类型变量赋值时,其值保持不变,比如:
  1. unsigned char a=100;
  2. unsigned int b=700;
  3. b=a;
那么最终 b 的值就是 100 了。但是如果我们的程序是
  1. unsigned char a=100;
  2. unsigned int b=700;
  3. a=b;
那么 a 的值仅仅是取了 b的低 8 位,我们首先要把 700 变成一个 16 位的二进制数据,然后取它的低 8 位出来,也就是 188,这就是长字节类型给短字节类型赋值的结果,会从长字节类型的低位开始截取刚好等于短字节类型长度的位,然后赋给短字节类型。

在 51 单片机里边,有一种特殊情况,就是 bit 类型的变量,这个 bit 类型的强制类型转换,是不符合上边讲的这个原则的,比如:
  1. bit a=0;
  2. unsigned char b;
  3. a=(bit)b;
这个地方要特别注意,使用 bit 做强制类型转换,不是取 b 的最低位,而是它会判断 b 这个变量是 0 还是非 0的值,如果 b 是 0,那么 a 的结果就是 0,如果 b 是任意非 0 的其它值,那么 a 的结果都是 1。

定时时间精准性调整

在 6.5.2 章节有一个数码管秒表显示程序,那个程序是 1 秒数码管加 1,但是细心的同学做了实验后,经过长时间运行会发现,和我们实际的时间有了较大误差了,那如何去调整这种误差呢?要解决问题,先找到问题是什么原因造成的。

先对我们前面讲过的中断内容做一个较深层次的补充。还是讲解中断的那个场景,当我们在看电视的时候,突然发生了水开的中断,我们必须去提水的时候,第一,我们从电视跟前跑到厨房需要一定的时间,第二,因为我们看的电视是智能数字电视,因此在去提水之前我们可以使用遥控器将我们的电视进行暂停操作,方便回来后继续从刚才的剧情往下进行。

那么暂停电视,跑到厨房提水,这一点点时间是很短的,在实际生活中可以忽略不计,但是在单片机秒表程序中,误差是会累计的,每 1 秒钟都差了几个微妙,时间一久,造成的累计误差就不可小觑了。

单片机系统里,硬件进入中断需要一定的时间,大概是几个机器周期,还要进行原始数据保护,就是把进中断之前程序运行的一些变量先保存起来,专业术语叫做中断压栈,进入中断后,重新给定时器 TH 和 TL 赋值,也需要几个机器周期,这样下来就会消耗一定的时间,我们得把这些时间补偿回来。

方法一,使用软件 debug 进行补偿。
我们在前边讲过使用 debug 来观察程序运行时间,那我们可以把我们 2 次进入中断的时间间隔观察出来,看看和我们实际定时的时间相差了几个机器周期,然后在进行定时器初值赋值的时候,进行一个调整。我们用的是 11.0592M 的晶振,发现差了几个机器周期,就把定时器初值加上几个机器周期,这样就相当于进行了一个补偿。

方法二,使用累计误差计算出来。
有的时候,除了程序本身存在的误差外,硬件精度也可能会影响到时钟的精度,比如晶振,会随着温度变化出现温漂现象,就是实际值和标称值要差一点。那么我们还可以采取累计误差的方法来提高精度。比如我们可以让时钟运行半个小时或者一个小时,看看最终时间差了几秒,然后算算一共进了多少次定时器中断,把这差的几秒平均分配到每次的定时器中断中,就可以实现时钟的调整。

大家要明白,这个世界上本就没有绝对的精确,我们只能在一定程度上提高精确度,但是永远都不会使误差为零,如果在这个基础上还感觉精度不够的话,不要着急,后边我们会专门讲时钟芯片的,通常时钟芯片计时的精度比单片机的精度要高一些。

字节操作修改位的技巧

这里再介绍个编程小技巧,在编程时,有的情况下需要改变一个字节中的某一位或者几位,但是又不想改变其它位原有的值,该如何操作呢?

比如我们学定时器的时候遇到一个寄存器 TCON,这个寄存器是可以进行位操作的,可以直接写 TR0=1;TR0 是 TCON 的一个位,因为这个寄存器是允许位操作,这样写是没有任何问题的。还有一个寄存器 TMOD,这个寄存器是不支持位操作的,那如果我们要使用 T0的模式 1,我们希望达到的效果是 TMOD 的低 4 位是 0b0001,但如果我们直接写成 TMOD =0x01 的话,实际上已经同时操作到了高 4 位,即属于 T1 的部分,设置成了 0b0000,如果T1 定时器没有用到的话,那我们随便怎么样都行,但是如果程序中既用到了 T0,又用到了T1,那我们设置 T0 的同时已经干扰到了 T1 的模式配置,这是我们不希望看到的结果。

在这种情况下,就可以用我们前边学过的“&”和“|”运算了。对于二进制位操作来说,不管该位原来的值是 0 还是 1,它跟 0 进行&运算,得到的结果都是 0,而跟 1 进行&运算,将保持原来的值不变;不管该位原来的值是 0 还是 1,它跟 1 进行|运算,得到的结果都是 1,而跟 0 进行|运算,将保持原来的值不变。

利用上述这个规律,我们就可以着手解决刚才的问题了。如果我们现在要设置 TMOD 使定时器 0 工作在模式 1 下,又不干扰定时器 1 的配置,我们可以进行这样的操作:TMOD =TMOD & 0xF0; TMOD = TMOD | 0x01;第一步与 0xF0 做&运算后,TMOD 的高 4 位不变,低4 位清零,变成了 0bxxxx0000;然后再进行第二步与 0x01 进行|运算,那么高 7 位均不变,最低位变成 1 了,这样就完成了只将低 4 位的值修改位 0b0001,而高 4 位保持原值不变的任务,即只设置了 T0 而不影响 T1。熟练掌握并灵活运用这个方法,会给你以后的编程带来便利。

另外,在 C 语言中,a &= b;等价于 a = a&b;同理,a |= b;等价于 a = a|b;那么刚才的一段代码就可以写成 TMOD &= 0xF0;TMOD |= 0x01 这样的简写形式。这种写法可以一定程度上简化代码,是 C 语言常用的一种编程风格。

数码管扫描函数算法改进

在学习数码管动态扫描的时候,为了方便大家理解,我们程序写的细致一些,给大家引入了 switch 的用法,随着编程能力与领悟能力的增强,对于 74HC138 这种非常有规律的数字器件,我们在编程上也可以改进一下逻辑算法,让程序变的更简洁。这种逻辑算法,通常不是靠学一下可以全部掌握的,而是通过不断的编写程序以及研究他人程序的过程中一点点积累起来的,从今天开始,大家就要开始积累吧。

前边动态扫描刷新函数我们是这么写的:
  1. P0 = 0xFF;
  2. switch (i){
  3. case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;
  4. case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;
  5. case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;
  6. case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;
  7. case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;
  8. case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;
  9. default: break;
  10. }
我们来分析每一个 case 分支,它们的结构是相同的,即改变 ADDR2~0、改变索引 i、取数据写入 P0,只要把 case 后的常量与 ADDR2~0 和 LedBuff 的下标对比,就可以发现它们其实是相等的,那么我们可以直接把常量值(实际上就是 i 在改变前的值)赋值给它们即可,而不必写上 6 遍。还剩下一个 i 的操作,它进行了 5 次相同的++与一次归 0 操作,那么很明显用++和 if 判断就可以替代这些操作。下面就是我们据此改进后的代码:
  1. P0 = 0xFF;
  2. P1 = (P1 & 0xF8) | i;
  3. P0 = LedBuff[i];
  4. if (i < 5){
  5. i++;
  6. }else{
  7. i = 0;
  8. }
大家看一下,P1 = (P1 & 0xF8) | i;这行代码就利用了上面讲到的&和|运算来将 i 的低 3 位直接赋值到 P1 口的低 3 位上,而 P0 的赋值也只需要一行代码,i 的处理也很简单。这样写成的代码是不是要简洁的多,也巧妙的多,而功能与前面的 switch 是一样的,同样可以完美实现动态显示刷新的功能。

秒表程序

做了一个秒表程序给同学们做参考,程序中涉及到的知识点我们都讲过了,包括了定时器、数码管、中断、按键等多个知识点。多知识点同时应用到一个程序中的小综合,因此需要大家完全消化掉。此程序是一个“真正的”并且“实用的”秒表程序,第一它有足够的分辨率,保留到小数点后两位,即每 10ms 计一次数,第二它也足够精确,因为我们补偿了定时器中断延时造成的误差,如果你愿意,它完全可以为用来测量你的百米成绩。这种小综合也是将来做大项目程序的基础,因此还是老规矩,大家边抄边理解,理解透彻后独立写出来就算此关通过。
  1. #include <reg52.h>
  2. sbit ADDR3 = P1^3;
  3. sbit ENLED = P1^4;
  4. sbit KEY1 = P2^4;
  5. sbit KEY2 = P2^5;
  6. sbit KEY3 = P2^6;
  7. sbit KEY4 = P2^7;
  8. unsigned char code LedChar[] = { //数码管显示字符转换表
  9. 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
  10. 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
  11. };
  12. unsigned char LedBuff[6] = { //数码管显示缓冲区
  13. 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
  14. };
  15. unsigned char KeySta[4] = { //按键当前状态
  16. 1, 1, 1, 1
  17. };
  18. bit StopwatchRunning = 0; //秒表运行标志
  19. bit StopwatchRefresh = 1; //秒表计数刷新标志
  20. unsigned char DecimalPart = 0; //秒表的小数部分
  21. unsigned int IntegerPart = 0; //秒表的整数部分
  22. unsigned char T0RH = 0; //T0 重载值的高字节
  23. unsigned char T0RL = 0; //T0 重载值的低字节
  24. void ConfigTimer0(unsigned int ms);
  25. void StopwatchDisplay();
  26. void KeyDriver();
  27. void main(){
  28. EA = 1; //开总中断
  29. ENLED = 0; //使能选择数码管
  30. ADDR3 = 1;
  31. P2 = 0xFE; //P2.0 置 0,选择第 4 行按键作为独立按键
  32. ConfigTimer0(2); //配置 T0 定时 2ms
  33. while (1){
  34. if (StopwatchRefresh){ //需要刷新秒表示数时调用显示函数
  35. StopwatchRefresh = 0;
  36. StopwatchDisplay();
  37. }
  38. KeyDriver(); //调用按键驱动函数
  39. }
  40. }
  41. /* 配置并启动 T0,ms-T0 定时时间 */
  42. void ConfigTimer0(unsigned int ms){
  43. unsigned long tmp; //临时变量
  44. tmp = 11059200 / 12; //定时器计数频率
  45. tmp = (tmp * ms) / 1000; //计算所需的计数值
  46. tmp = 65536 - tmp; //计算定时器重载值
  47. tmp = tmp + 18; //补偿中断响应延时造成的误差
  48. T0RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
  49. T0RL = (unsigned char)tmp;
  50. TMOD &= 0xF0; //清零 T0 的控制位
  51. TMOD |= 0x01; //配置 T0 为模式 1
  52. TH0 = T0RH; //加载 T0 重载值
  53. TL0 = T0RL;
  54. ET0 = 1; //使能 T0 中断
  55. TR0 = 1; //启动 T0
  56. }
  57. /* 秒表计数显示函数 */
  58. void StopwatchDisplay(){
  59. signed char i;
  60. unsigned char buf[4]; //数据转换的缓冲区
  61. //小数部分转换到低 2 位
  62. LedBuff[0] = LedChar[DecimalPart%10];
  63. LedBuff[1] = LedChar[DecimalPart/10];
  64. //整数部分转换到高 4 位
  65. buf[0] = IntegerPart%10;
  66. buf[1] = (IntegerPart/10)%10;
  67. buf[2] = (IntegerPart/100)%10;
  68. buf[3] = (IntegerPart/1000)%10;
  69. for (i=3; i>=1; i--){ //整数部分高位的 0 转换为空字符
  70. if (buf[i] == 0){
  71. LedBuff[i+2] = 0xFF;
  72. }else{
  73. break;
  74. }
  75. }
  76. for ( ; i>=0; i--){ //有效数字位转换为显示字符
  77. LedBuff[i+2] = LedChar[buf[i]];
  78. }
  79. LedBuff[2] &= 0x7F; //点亮小数点
  80. }
  81. /* 秒表启停函数 */
  82. void StopwatchAction(){
  83. if (StopwatchRunning){ //已启动则停止
  84. StopwatchRunning = 0;
  85. }else{ //未启动则启动
  86. StopwatchRunning = 1;
  87. }
  88. }
  89. /* 秒表复位函数 */
  90. void StopwatchReset(){
  91. StopwatchRunning = 0; //停止秒表
  92. DecimalPart = 0; //清零计数值
  93. IntegerPart = 0;
  94. StopwatchRefresh = 1; //置刷新标志
  95. }
  96. /* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
  97. void KeyDriver(){
  98. unsigned char i;
  99. static unsigned char backup[4] = {1,1,1,1};
  100. for (i=0; i<4; i++){ //循环检测 4 个按键
  101. if (backup[i] != KeySta[i]){ //检测按键动作
  102. if (backup[i] != 0){ //按键按下时执行动作
  103. if (i == 1){ //Esc 键复位秒表
  104. StopwatchReset();
  105. }else if (i == 2){//回车键启停秒表
  106. StopwatchAction();
  107. }
  108. }
  109. backup[i] = KeySta[i]; //刷新前一次的备份值
  110. }
  111. }
  112. }
  113. /* 按键扫描函数,需在定时中断中调用 */
  114. void KeyScan(){
  115. unsigned char i;
  116. static unsigned char keybuf[4] = { //按键扫描缓冲区
  117. 0xFF, 0xFF, 0xFF, 0xFF
  118. };
  119. //按键值移入缓冲区
  120. keybuf[0] = (keybuf[0] << 1) | KEY1;
  121. keybuf[1] = (keybuf[1] << 1) | KEY2;
  122. keybuf[2] = (keybuf[2] << 1) | KEY3;
  123. keybuf[3] = (keybuf[3] << 1) | KEY4;
  124. //消抖后更新按键状态
  125. for (i=0; i<4; i++){
  126. if (keybuf[i] == 0x00){
  127. //连续 8 次扫描值为 0,即 16ms 内都是按下状态时,可认为按键已稳定的按下
  128. KeySta[i] = 0;
  129. }else if (keybuf[i] == 0xFF){
  130. //连续 8 次扫描值为 1,即 16ms 内都是弹起状态时,可认为按键已稳定的弹起
  131. KeySta[i] = 1;
  132. }
  133. }
  134. }
  135. /* 数码管动态扫描刷新函数,需在定时中断中调用 */
  136. void LedScan(){
  137. static unsigned char i = 0; //动态扫描索引
  138. P0 = 0xFF; //关闭所有段选位,显示消隐
  139. P1 = (P1 & 0xF8) | i; //位选索引值赋值到 P1 口低 3 位
  140. P0 = LedBuff[i]; //缓冲区中索引位置的数据送到 P0 口
  141. if (i < 5){ //索引递增循环,遍历整个缓冲区
  142. i++;
  143. }else{
  144. i = 0;
  145. }
  146. }
  147. /* 秒表计数函数,每隔 10ms 调用一次进行秒表计数累加 */
  148. void StopwatchCount(){
  149. if (StopwatchRunning){ //当处于运行状态时递增计数值
  150. DecimalPart++; //小数部分+1
  151. if (DecimalPart >= 100){ //小数部分计到 100 时进位到整数部分
  152. DecimalPart = 0;
  153. IntegerPart++; //整数部分+1
  154. if (IntegerPart >= 10000){ //整数部分计到 10000 时归零
  155. IntegerPart = 0;
  156. }
  157. }
  158. StopwatchRefresh = 1; //设置秒表计数刷新标志
  159. }
  160. }
  161. /* T0 中断服务函数,完成数码管、按键扫描与秒表计数 */
  162. void InterruptTimer0() interrupt 1{
  163. static unsigned char tmr10ms = 0;
  164. TH0 = T0RH; //重新加载重载值
  165. TL0 = T0RL;
  166. LedScan(); //数码管扫描显示
  167. KeyScan(); //按键扫描
  168. //定时 10ms 进行一次秒表计数
  169. tmr10ms++;
  170. if (tmr10ms >= 5){
  171. tmr10ms = 0;
  172. StopwatchCount(); //调用秒表计数函数
  173. }
  174. }
关于这个程序有两点值得提一下:首先是定时器配置函数,虽然这样在程序里通过计算得出初值(重载值)增加了些许代码,但它换来的是便利性和编程效率,因为只要你完成这个函数,之后所有需要用定时器定时 x 毫秒的场合,你都可以直接把函数拿过去,用所需要的毫秒数作为实参调用它即可,不需要在用计算器埋头算一通了,是不是很值呢。其次是我们没有使用矩阵按键的程序,而是只用矩阵按键的第 4 行作为独立按键来使用,因为秒表只需要 2 个键就够了,这里是想告诉大家,处理问题要灵活,千万不能墨守成规,能用简单方法解决的问题,就不要选择复杂的方案。
2018-10-28 10:34:57 Wapiti_y 阅读数 5421
  • 单片机控制第一个外设-LED灯-第1季第6部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第6个课程,主要讲解LED的工作原理和开发板原理图、实践编程等,通过学习目的是让大家学会给单片机编程控制LED灯,并且为进一步学习其他外设打好基础。

    3997 人正在学习 去看看 朱有鹏
/*60S倒计时*/
/*个位每1S变一次,从0~9*/
/*十位,个位为0的下一秒十位发生变化*/

#include<reg52.h>

sbit ADDR0=P1^0;
sbit ADDR1=P1^1;
sbit ADDR2=P1^2;
sbit ADDR3=P1^3;
sbit ENLED=P1^4;

unsigned char code LedChar[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90
};

void main()
{
	unsigned int cnt=0; //记录中断次数
	unsigned int sec=0; //记录秒数
	unsigned char g=0;   //个位数码管
	unsigned char s=6;   //十位数码管 

	ENLED=0;			 //使能U3 选中数码管DS1
	TMOD=0x01;			 //T0为模式1
	TH0=0xB8;			 //T0赋初值0xB800
	TL0=0x00;
	TR0=1;				 //启动T0

	while(1)
	{
		ADDR3=1;
		P0=0xff;
		ADDR2=0;
		ADDR1=0;
		ADDR0=0;
		P0=LedChar[g];   //选中个位数码管
				
		if(TF0==1)		 //判断T0是否溢出
		{
			TF0=0;		 //T0溢出后清零中断标志
			TH0=0xB8;        //并重新赋值
			TL0=0x00;
			cnt++;
		}
		if(cnt>=50)	  //判断溢出是否达到50次
		{
			cnt=0;    //达到五十次(1s)清零
			sec--;    //秒数累加
		}

		P0=0xff;
		ADDR0=1;
		P0=LedChar[s];   //选中十位数码管,并显示当前秒数下的数字

		if(s==0&&g==0)    //避免00状态的出现,状态60~60时间为60s
			s=6;
		if(g==0&&sec%10==1)
			s--;
		if(sec%10!=0)
		{
			g=10-sec%10;
		}
		if(sec%10==0)
		{
			g=0;
		}
	}
}

(1)实际上数码管状态变化为01~00~60,只不过00存在的时间过短,无法被人眼识别。

(2)在切换数码管前先让 P0=0xff 避免了前一个数码管留下的余辉。

 

上面程序较为繁琐,以下为更新程序:

/*60倒计时*/


#include<reg52.h>

sbit ADDR0=P1^0;
sbit ADDR1=P1^1;
sbit ADDR2=P1^2;
sbit ADDR3=P1^3;
sbit ENLED=P1^4;

unsigned char code LedChar[]=
{
0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,
0x80,0x90
};

unsigned int cnt=0;     //记录中断次数
unsigned char flag=0;   //中断标志位
unsigned char i=0;	//遍历
unsigned char g=0xff;   //初始化个位数码管
unsigned char s=0xff;   //初始化十位数码管
unsigned int sec=61;    //秒数

void main()
{

	EA=1;           //定时器总中断
	ENLED=0;	//?使能U3
	ADDR3=1;        //T0模式为1
	TMOD=0x01;	//定时1ms
	TH0=0xfc;	
	TL0=0x67;
	ET0=1;
	TR0=1;		//启动T0

	while(1)
	{
		if(flag==1)            //中断标志
		{		
			g=sec%10; 
			s=sec/10;
			flag=0;        //标志清0
			if(sec==0)     //重新倒计时
				sec=61;

		}
	}
}
void InterruptTimer0() interrupt 1
{
	TH0=0xfc;			 //重新赋值
	TL0=0x67;
	cnt++;
	if(cnt>=1000)	  //中断累计1000 1s
	{
		cnt=0;    //清0
		flag=1;
		sec--;    //秒数自减	
	}
	P0=0xff;
	switch(i)
	{
		case 0:ADDR0=0;ADDR1=0;ADDR2=0;P0=LedChar[g];i++;break;   //选中个位数码管
		case 1:ADDR0=1;ADDR1=0;ADDR2=0;P0=LedChar[s];i=0;break;   //选中十位数码管
		default:break;
	}
}

改进后,程序更为简洁直观。采用中断+switch遍历优化程序。

注意:遍历中,为了使几个数码管看起来像是同时亮,故大大缩短了定时时间,由20ms变为1ms。

定时与中断系统

阅读数 1723

没有更多推荐了,返回首页