一、硬件分析
1.1 数码管
数码管就是一种特殊的发光二极管,它的外观和原理图是这样的:
每个“8”字的亮灭都由A~DP八个阳极引脚和一个阴极引脚控制。
一个发光二极管本身就是一个半导体,这就相当于电路中的“单向阀”:电流流入二极管的方向是阳极,流出的方向被称为阴极;反之则不能导通。如果我们将阳极和阴极做为解释,将二极管发光与否做为命题逻辑的真值,那么我们就有下面这张真值表:
1.2 最一般的点亮逻辑
1.2.1 阳极的连接
如上所示是两个数码管放在一起,按照最一般也是最自然的链接逻辑,我们只要将16个阳极引脚逐个连接到单片机上就差不多了。事实上这也确实行得通,但是单片机的引脚资源是极为珍贵的,我们能不能想想什么办法省略一些引脚同时还能实现我们的需求呢?
既然我们的需求只是点亮一位数字就好了,那么我们不妨将所有阳极都接在一起(如上所示),这样就一举省去了8个引脚。不过这样一来,所有的八个数码管都将同时显示同一个数字(因为此时我们假设阴极接地,即为0)。
1.2.2 阴极的连接
接下来,根据上面的真值表,我们想让哪位亮,就将阴极置为0就可以了。
假如我们现在的需求是让第二个数码管的第二位显示5,阳极的输入首先肯定应当是1111 1011;如果此时将阴极单纯接地,那么所有数码管上的数字都将显示5。为了达成某1位显示数字,其他不显示的效果,我们只要让阴极为1111 1011就可以了。
通过上面的梳理,因此我们可以得到下面这张表:
那么阴极该怎么和单片机连接呢?按照最一般也是最自然的想法:当然是把八个阴极引脚逐个和单片机的引脚相连啦!这种方法当然是正确的,也可以实现需求,但是这还是对引脚的极大浪费。
因为我们的需求十分简单:只需要让某一位亮就可以了,因此借用38译码器就可以实现这个功能:
上面是38译码器的真值表,也就是说,现在我们只需要使用3个单片机的引脚就可以操纵阴极了!
总结:阳极控制段选,阴极控制位选。这个逻辑就相当于先让所有数码管显示5(段选),然后再让某个数码管单独亮就可以了(位选)。
二、代码编写
2.1 了解引脚
在编写代码前我们需要了解单片机的哪8位引脚控制阳极,哪3位控制阴极。
通过观察原理图(这里就不发了)发现: P00~P07控制阳极,P13~P15控制阴极。
这样就好办了。
2.2 编写代码
我们试试编写下面的例子:
根据上面的分析,单个数码管只有bc引脚为1,其余都为0,因此段选为:0110 0000。位选则是:1111 1110。根据38译码器的真值表和段选对应的引脚,按照最一般的思路,我们可以给逐个引脚赋值,因此可以得出下面的C代码:
#include <STC89C5xRC.H> // 包含 STC89C52 的头文件
#define SMG_EN P36
// 增加自定义类型,简化代码编写
typedef unsigned char u8;
typedef unsigned int u16;
void main() {
// 打开数码管总开关
SMG_EN = 0;
//段选代码
P00=0;
P01=1;
P02=1;
P03=0;
P04=0;
P05=0;
P06=0;
P07=0;
//位选代码
P13=1;
P14=1;
P15=1;
// 卡住程序
while (1);
}
烧录运行,哇哦,真的和上面一模一样诶~ 但是这还不够,如果我们将需求稍微修改一下成第三个数码管给我显示4,这个代码就不能用了,有没有什么通用的办法呢?
.2.3 代码的优化
这个过程初看起来好像没有什么逻辑,但是这有点像给你一个数列让你找通项公式一样,我们需要发现其中的某些规律。
2.3.1 段选的规律
所谓段选的规律,就是在问:十进制的0~9,对应到单个数码管的段选的二进制是多少?很容易就可以得到下面这张表:
这就是所有可能的情况了,我们不妨将这些情况写到数组里面:
unsigned char digit_codes[10] = {
0x3F, // 数字 0
0x06, // 数字 1
0x5B, // 数字 2
0x4F, // 数字 3
0x66, // 数字 4
0x6D, // 数字 5
0x7D, // 数字 6
0x07, // 数字 7
0x7F, // 数字 8
0x6F // 数字 9
};
非常巧的是,段选的数字和下标是一致的,比如a[0]所要显示的数字就是0!当然,这里元素选用16进制数的原因是因为一个完整的16进制数是由8个2进制数构成,而我们这里由8个引脚做为阳极,那么这里用16进制数恰如其分。而unsigned char 类型在 C 语言中通常是 8 位,因此这里使用的是unsigned char 类型的数组。
更巧的是,51单片机的通用接口是由P1~P4四个端口组成,每个端口有8个引脚,因此,我们直接对P0端口操作就可以了!
2.4 位选的规律
位选的规律入下所示:
从真值表可以看出,当我们想要第一个灯亮,即输入0,其3个引脚是000,巧的是0对应的二进制也是000;同理想要第二个灯亮我们要输入1.其二进制和引脚电平都是010、以此类推,最后一个灯亮需要输入7,其二进制和引脚电平也正好是111。
如此一来,我们只要输入十进制下的0~7,经过38译码器的转换得到8位阴极位选码,最后将这个位选码和段选码做运算就可以了。
关键是,P1端口也有8个引脚,我们只用到了其中的P13、P14、P15,如果我们单纯输入10进制的7,它的二进制是0000 0111,而不是我们想要的0011 1000,这该怎么办呢?换言之,我们想要下面这样的数据形式才能正确操纵P13、P14、P15引脚:
观察原数据00000111和需要的数据00111000,我们发现,只要将原来的数据向左移位3位即可。
但是仅仅左移是不够的,我们希望左移之后的P15、14、13能够覆盖原数据,而其他的位不变,如下所示:
也就是说,我们希望只取上面红框中的数据覆盖到原来的P1端口的数据上,这样其他引脚的数据(蓝色框中所表示的)就不会发生变化,应该怎么做到呢?
我们可以先设法将P1端口的P15、P14、P13都先设置为0,即令P1=AA???BBB与11000111做与运算得到AA000BBB,因为任何数和1做与都是任何数本身,然后再和移位后的数字做或运算就可以了。
这个过程如下所示:
这样就做到了只修改指定位,而不影响P1其他位的效果。
事实上,我们有这样的结论:任意一个 n 位有限长的二进制数,可以通过有限次位运算(与、或、异或、取反、移位等)变换为任意另一个 n 位二进制数。
三、完整代码
进过上面的逐步梳理,我们终于得到了可以完全满足要求的代码:
#include <STC89C5xRC.H> // 包含 STC89C52 的头文件
#define SMG_EN P36
// 增加自定义类型,简化代码编写
typedef unsigned char u8;
u8 positive_codes[10] = { //阳极段选
0x3F, // 数字 0
0x06, // 数字 1
0x5B, // 数字 2
0x4F, // 数字 3
0x66, // 数字 4
0x6D, // 数字 5
0x7D, // 数字 6
0x07, // 数字 7
0x7F, // 数字 8
0x6F // 数字 9
};
void DigitalTube_DisplaySingle(u8 negetive_code,u8 positive_codes){ //negetive_code为阴极段选
negetive_code<<=3; //位选:从0开始,第几位显示数字,记得将数字左移3位
P1=P1&0xc7; //将P1原数据和11 000 111做与运算
P1=P1|negetive_code;//将与后的结果再和移位后的数据做或运算
P0=positive_codes;//段选:具体显示什么数字,指定阳极8个引脚的电平
}
void main() {
// 打开数码管总开关
SMG_EN = 0;
DigitalTube_DisplaySingle(6,positive_codes[2]);//(在第几位显示[0~7],显示什么数字positive_codes[0~9])
// 卡住程序
while (1);
}