51入门之数码管

时间:2024-04-08 11:58:27

目录

1.数码管硬件简介

        1.1数码管位选

        1.2数码管数选

2.静态数码管显示

3.动态数码管显示


1.数码管硬件简介

        数码管,在我们生活中无处不在,红绿灯的倒计时,电梯的显示屏,还有电子表的时间显示,都使用了数码管的相应原理。

        数码管分为两大类:共阴极和共阳极数码管,共阳极数码管就是阳极连接到同一接口处,阴极分开连接的一种结构,我们一般把公共端的输入叫做位选,把非公共端的输入叫做数选。

 

        比如我们需要这个共阳极数码管显示数字2,我们首先就要位选到这个数码管,也就是我们要把这个共阳极数码管的阳极输入高电平1,这个就叫做位选;然后就是数选,我们就要让这个数码管对应的a,b,g,e,d都接上低电平0,其它的接上高电平1,这样就完成了一个共阳极数码管显示数字2的操作。

        共阴极使用的方法正好和共阳极相反,共阴极使用阴极相接,我们位选数码管就只要把这个阴极置为低电平0即可,然后再进行数选。

        我们开发板上使用的是两个四位一体的共阴极数码管,下面是原理图:

        从原理图里我们可以知道,我们的数码管使用了两个芯片分别进行位选和数选。

        1.1数码管位选

        首先介绍74HC138芯片,我们把它称作138译码器,它是用来我们开发板上用来位选数码管的一个芯片,只需要输入3个引脚,就可以通过这三个引脚计算出一个二进制数,然后来确定哪个LED亮,这样的操作神乎其神,左边还有三个端口分别是G1,G2A,G2B,其中,G2A和G2B的头上有一个横线,表示低电平有效,G1没有这个横线就表示高电平有效,这几个就是用来确定芯片工作还是不工作的,这里G1接VCC,G1A和G2B接GND,直接表示芯片永久有效工作。所以,我们只需要关心这个P22到P24端口的输入就可以了。

        这里,我们的输入端口P22,P23,P24是和它们的输出反过来的:

比方说这个,用ABC分别表示P22,P23,P24的话,它的输入其实是需要反过来读的,比如说A = 0,B = 0, C = 1,我们需要把这里的001反过来,表示成100,也就是对应十进制的4,对应的就是Y4,LED5,所以这个4表示的其实是第5个数码管,也就是LED5,问题又来了,这里需要注意一下,我们开发板上的数码管从左到右是LED8到LED1,所以,我们的所谓”第五个数码管“其实是从右往左数第五个数码管;同样的,假设输入的是A = 1,B = 1,C = 0,表示的就是011,也就是Y3,LED4,就是从右往左数第四个数码管,以此类推。

        1.2数码管数选

数码管的数选需要考虑两部分,先说芯片部分,如图所示,我们使用的是74HC245芯片,VCC和GND就是正负极不需要解释,OE是使能引脚,上面有一个横线表示的就是低电平有效,然就就是DIR引脚,它表示的意思就是DIRECTION(方向),接正极的时候就是表示信号从左到右,接负极的时候就表示从右输到左,在开发板上就是以一个跳线帽的形式存在(在数码管右侧,名字叫J24),我们可以调整跳线帽使它接VCC或者是GND,当它接VCC的时候,这个数码管的数选才会有作用,否则输入的数据就是无效的。

       这个芯片A0到A7依次对应B0到B7,再对应a到g和dp,这样做的目的不为了什么,就是为了让这个从P0端口输入的数据只是一个信号,主要驱动数码管亮灭的能力在于B端口——这个经过了芯片处理,保证数码管的驱动是电源而不是微弱的信号,保证数码管亮度足够。

        其次我们之前说过了我们的开发板上的数码管其实是共阴极数码管,共阴极数码管的数选是要输入高电平驱动的,所以我们只要对P0进行操控就行了。

        而这里也是有一点要注意的,就是这里的P0输入还是类似LED,就像一个栈一样,我们输入1000 0000对应单片机得到的信号就是0000 0001,刚好是反过来的,所以我们想要实现对应的数字的话,我们的也要把这个输入反过来。

到目前为止我们在C51里面接触到了很多这样的倒过来存储的方式,可以猜测的是,这个是一个特定的存储方式,可以和Keil5使用的小端存储方式有关,我们平时使用的别的编译器使用的大部分是大端存储,这里就介绍一下大端存储和小端存储的区别:

大端存储(Big Endian)和小端存储(Little Endian)是指在多字节数据存储时,字节的排列顺序不同的两种方式。

  • 大端存储(Big Endian):

在大端存储中,多字节数据的最高有效字节(Most Significant Byte,MSB)存储在最低地址,而最低有效字节(Least Significant Byte,LSB)存储在最高地址。
举例来说,十六进制数0x12345678,在大端存储中会被存储为:0x12 0x34 0x56 0x78。

  • 小端存储(Little Endian):

在小端存储中,多字节数据的最低有效字节(LSB)存储在最低地址,而最高有效字节(MSB)存储在最高地址。
举例来说,十六进制数0x12345678,在小端存储中会被存储为:0x78 0x56 0x34 0x12。

  • 形象例子:

想象一列连续排列的房子,每个房子里有一位数字。这些数字组成一个大的十进制数。现在考虑将这个数写在一张纸上,而你选择的写入顺序就代表了字节序。

大端存储:你从最高位数字开始写,然后逐渐向低位数字写。
小端存储:你从最低位数字开始写,然后逐渐向高位数字写。
假设这个数是12345678,那么在大端存储中,写出的顺序是12345678;而在小端存储中,写出的顺序是87654321。

所以,我们就会经常在51单片机里接触到这样的数据倒过来存储的方式

(以上大端存储和小端存储的知识都是正确的,但是具体这些数据的输入是不是和这个有关系还暂时只是个人的一个猜测,并没有相应的考究)

2.静态数码管显示

        静态数码管的实现很简单,就是我们前面示范的那样使用位选+数选即可,掌握上面的两个芯片的对应关系,其实还是比较简单的,这里主要是需要使用数组和函数把我们的位选和数选简化成简单的一个接口函数,我们只要向接口函数里输入位置参数和数字参数,就可以实现在数码管对应 的位上显示数字的效果。

        首先我们可以配置一下这个数码管显示的数字对应的十六进制码,使用数组存储,这里我们不止可以让我们的数码管输出数字,还可以让它输出英文。从A到F,刚好可以完整表示一个十六进制可以表示的数。

        按照顺序的话,我们的对应的就是

数字 0: 0x3F
数字 1: 0x06
数字 2: 0x5B
数字 3: 0x4F
数字 4: 0x66
数字 5: 0x6D
数字 6: 0x7D
数字 7: 0x07
数字 8: 0x7F
数字 9: 0x6F
数字 A: 0x77
数字 B: 0x7C
数字 C: 0x39
数字 D: 0x5E
数字 E: 0x79
数字 F: 0x71

 我们把它们写进数组里面,就可以得到:

const unsigned char SMG[] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F, // 9
    0x77, // A
    0x7C, // B
    0x39, // C
    0x5E, // D
    0x79, // E
    0x71  // F
};

然后我们需要实现函数接口,第一个参数为位,第二个参数为数,函数实现如下:

const unsigned char SMG[] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F, // 9
    0x77, // A
    0x7C, // B
    0x39, // C
    0x5E, // D
    0x79, // E
    0x71  // F
};

void SMGDisPlay(unsigned char loc,unsigned char num)
{
	num%=16;
	switch (loc)
	{
		case 1:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 2:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 3:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 4:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 0;
			break;
		case 5:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 6:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 7:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 8:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 0;
			break;
	}
	P0 = SMG[num];
}

        这样就算大功告成了,我们只要输入3,8就表示”在第三个数码管显示数字8“.在主函数调用一下就好了。

3.动态数码管显示

        动态数码管显示的原理就是静态数码管的变化版,我们需要实现一个动态的数码管显示,比如在一个数码管上从1数到9,间隔0.5s,我们只要在每次调用我们刚刚为静态数码管写的函数之后加上一个Delay暂停函数就好了。

        具体实现如下:

#include <REGX52.H>
#include <INTRINS.H>

void Delay(int xms)		//@11.0592MHz
{
	while(xms--)
	{
			unsigned char i, j;
			_nop_();
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);
	}
}
const unsigned char SMG[] = {
    0x3F, // 0
    0x06, // 1
    0x5B, // 2
    0x4F, // 3
    0x66, // 4
    0x6D, // 5
    0x7D, // 6
    0x07, // 7
    0x7F, // 8
    0x6F, // 9
    0x77, // A
    0x7C, // B
    0x39, // C
    0x5E, // D
    0x79, // E
    0x71  // F
};

void SMGDisPlay(unsigned char loc,unsigned char num)
{
	num%=16;
	switch (loc)
	{
		case 1:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 2:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 3:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 4:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 0;
			break;
		case 5:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 6:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 7:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 8:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 0;
			break;
	}
	P0 = SMG[num];
}

void main()
{
	unsigned char i =0;
	while(1)
	{
			for(i = 1;i <= 9 ;i++)
			{
				SMGDisPlay(1,i);
				Delay(500);
			}
	}
}

然后就成功显示了。

        接下来我们会遇到一个问题:怎么一次性显示多个数字呢?其实很简单,类似这种显示都是使用快速刷新来实现的(电脑,电视,手机等),我们可以把暂停间隔减小到很小,直至我们人眼无法看见,循环播放想要显示的几个位上的数字,这样我们就成功一次显示多个数字了。

        我们代码写出来是下面这样:

void main()
{
	while(1)
	{
			SMGDisPlay(1,1);
			SMGDisPlay(2,2);
			SMGDisPlay(3,3);
	}
}

        没有一点问题,就是按照上面的思路实现的,甚至整个时间间隔几乎没有,应该显示的更加流畅才是。但是我们看看效果:

        数字模糊不清,不是摄像头的问题,本身就是这样的,根本很难看出来数码管显示的数字。

        这样的现象我们叫做数码管的”影“,也就是数码管很常见的问题,我们稍微研究一下数码管显示的工作原理就不难发现,其实数码管操作就是由位选和数选组成的。当我们多个位选和数选没有间隔的连接在一起,比如:位选,数选,位选,位选,数选,我们会发现,中间其实会有这样的现象:第一次的数选后到第二次的数选前,正好是第一次的数选和第二次的位选相互组合,就变成了用第二次的位显示第一次的数,但是我们原本想要的其实是第二次的位就匹配第二次的数,所以我们这里出现了这样的现象。

        想要消除这样的现象,我们一般把它叫做”数码管的消隐“,就是我们每次位选+数选一遍之后,都把我们前面的数选清零,然后再进行下一次的循环,这样我们就得到了下面的代码和效果:
代码:

void main()
{
	while(1)
	{
			SMGDisPlay(1,1);
			
			P0 = 0X00;
		
			SMGDisPlay(2,2);
			
			P0 = 0X00;
		
			SMGDisPlay(3,3);
			
			P0 = 0X00;
	}
}

现象:

没有出现错乱的情况了,但是亮度感觉有点不够,所以我们可以在每次函数调用之后,数选清零之前加一个小小的Delay,延长一下显示的时间占比,从而提高显示的亮度

代码:

void main()
{
	while(1)
	{
			SMGDisPlay(1,1);
			Delay(1);
			P0 = 0X00;
		
			SMGDisPlay(2,2);
			Delay(1);
			P0 = 0X00;
		
			SMGDisPlay(3,3);
			Delay(1);
			P0 = 0X00;
	}
}

现象:

肉眼可见亮了不少。

        其实还有这样一种情况,这里就简单描述一下: 就是我们只使用Delay而不使用把数选重新清零的情况下,还是可以显示似乎”很正常“的现象,但其实只要你认真看,它的非发光的其他位置还是有一点点的亮度的,也就是说这些地方还是亮的,只是我们使用Delay之后看起来我们想要的显示的很亮,就没有注意到其实还有很多其他的不该亮的地方也是亮着的,只是较为微弱,总而言之,这样还是不对的,所以我们还是需要数选清零这个操作来”消隐“。

        为了更方便以后我们可以抄写我们的代码,而不用消隐我们其实还可以把我们的代码改造一下:

void SMGDisPlay(unsigned char loc,unsigned char num)
{
	P0 = 0X00;
	num%=16;
	switch (loc)
	{
		case 1:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 2:
			P2_4 = 1;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 3:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 4:
			P2_4 = 1;
			P2_3 = 0;
			P2_2 = 0;
			break;
		case 5:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 1;
			break;
		case 6:
			P2_4 = 0;
			P2_3 = 1;
			P2_2 = 0;
			break;
		case 7:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 1;
			break;
		case 8:
			P2_4 = 0;
			P2_3 = 0;
			P2_2 = 0;
			break;
	}
	P0 = SMG[num];
	Delay(1);
}

        这样的表示就很巧妙,我们在函数开头的地方加上了一个数选清零用来消隐,最后面加上了一个Delay函数用来实现我们的”增加占比时间“使数码管亮度增加,并且这样的使用在我们只有一个数码管的时候也是适用的,简直可以称作天才般的融合!哈哈,其实最好还是不要这样写,因为我们刚入门,所以需要多次实践”消隐“这个步骤,以免忘记这个重要的操作,所以我们还是使用原来的函数多使用几次,等完全掌握并记住了”消隐“操作之后,再使用后面这个优化后的代码也不是不可以的。