从上一博文《8051单片机实战分析(以STC89C52RC为例) | 05 - 静态数码管驱动》,我们可以了解到数码管一些驱动原理,实际上静态扫描在项目中基本不会用到,在项目中我们经常用动态扫描,所以便有了本文。如果不懂数码管的段选与位选请先看这篇:《3分钟带你彻底弄懂数码管的段选与位选》
1 动态扫描
那什么是动态扫描呢?
举个例子:有 2 个数码管,我们要显示“12”这个数字,可以先让高位的位选三极管导通,然后控制段选让其显示“1”,延时一定时间后再让低位的位选三极管导通,然后控制段选让其显示“2”。把这个流程以一定的速度循环运行就可以让数码管显示出“12”,由于交替速度非常快,人眼识别到的就是“12”这两位数字同时亮了。
在多个数码管显示数字的时候,我们可以轮流点亮数码管(一个时刻内只有一个数码管是亮的),利用人眼的视觉暂留现象(也叫余辉效应),就可以做到看起来是所有数码管都同时亮了,这就是动态显示,也叫做动态扫描。
那么一个数码管需要点亮多长时间呢?也就是说要多长时间完成一次全部数码管的扫描呢(很明显:整体扫描时间=单个数码管点亮时间*数码管个数)?答案是:10ms 以内。
当电视机和显示器还处在 CRT(电子显像管)时代的时候,有一句很流行的广告语——“100Hz无闪烁”,没错,只要刷新率大于 100Hz,即刷新时间小于 10ms,就可以做到无闪烁,这也就是我们的动态扫描的硬性指标。那么你也许会问,有最小值的限制吗?理论上没有,但实际上做到更快的刷新却没有任何进步的意义了,因为已经无闪烁了,再快也还是无闪烁,只是徒然增加 CPU 的负荷而已(因为 1 秒内要执行更多次的扫描程序)。
所以,通常我们设计程序的时候,都是取一个接近 10ms,又比较规整的值就行了。
2 原理图
① 数码管原理图:
② 数码管的位选使用138译码器进行解析,关于这块我们可以参考这篇文章:《数字器件认识 | 74HC138三八译码器的应用》。
③ MCU原理图:
3 代码
了解原理之后,我们就可以写个Demo让数码管从右至左显示0-7:
#include "reg52.h" //此文件中定义了单片机的一些特殊功能寄存器
typedef unsigned int u16; //对数据类型进行声明定义
typedef unsigned char u8;
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;
u8 code LedChar[]={
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"
0xff, //全亮
0x00 //熄灭
};
/*******************************************************************************
* 函 数 名 : delay
* 函数功能 : 延时函数,i=1时,大约延时10us
*******************************************************************************/
void delay(u16 i)
{
while(i--);
}
/*******************************************************************************
* 函 数 名 : DigDisplay
* 函数功能 : 数码管动态扫描函数,循环扫描8个数码管显示
*******************************************************************************/
void DigitalDisplay()
{
u8 i;
for(i=0;i<8;i++)
{
switch(i) //位选,选择点亮的数码管,
{
case(0):
LSA=0;LSB=0;LSC=0; break;//显示第0位
case(1):
LSA=1;LSB=0;LSC=0; break;//显示第1位
case(2):
LSA=0;LSB=1;LSC=0; break;//显示第2位
case(3):
LSA=1;LSB=1;LSC=0; break;//显示第3位
case(4):
LSA=0;LSB=0;LSC=1; break;//显示第4位
case(5):
LSA=1;LSB=0;LSC=1; break;//显示第5位
case(6):
LSA=0;LSB=1;LSC=1; break;//显示第6位
case(7):
LSA=1;LSB=1;LSC=1; break;//显示第7位
}
P0=LedChar[i];//发送段码
delay(100); //间隔一段时间扫描:大约1ms
P0=0x00;//消隐
}
}
/*******************************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void main()
{
while(1)
{
DigitalDisplay(); //数码管显示函数
}
}
简要分析:
① main
函数单纯给DigitalDisplay
子函数进行无限循环。
② DigitalDisplay
子函数通过for
循环语句在每一次循环指定一个位选与段选。
例如i=0;
时,通过LSA=0;LSB=0;LSC=0;
语句控制138译码器选择Y0
端口输出0
,从而选定LED1
网络的位选。
然后通过P0=LedChar[i];
给P0端口发送段选值,i=0
的时候发送的是'0'
字符的段选值,每次for
循环中间只延时大约1ms。
③ 注意这句话:P0=0x00;//消隐
。如果没有这句话就会产生“鬼影”,这个动态扫描的一个bug
。
“鬼影”的出现,主要是在数码管位选和段选产生的瞬态造成的。
举个简单例子,我们在数码管动态显示的那部分程序中,实际上每一个数码管点亮的持续时间是1ms
的时间,1ms
后进行下个数码管的切换。
在进行数码管切换的时候,比如我们从 case 7
要切换到 case 0
的时候,case 7
的位选用的是LSA=1;LSB=1;LSC=1;
。
==》假如此刻case 7
也就是最高位数码管对应的值是0
,我们要切换成的 case 0
的数码管位选是LSA=0;LSB=0;LSC=0;
,而对应的数码管的值假如是1
。
==》又因为C语言程序是一句一句顺序往下执行的,每一条语句的执行都会占用一定的时间,即使这个时间非常非常短暂。
==》但是当我们把“LSA=1”
改变成“LSA=0”
的时候,这个瞬间存在了一个中间状态 LSA=0; LSB=1; LSC=1;
在这个瞬间上,我们就给 case 6
对应的数码管LED7
网络位选瞬间赋值了0
。
==》当我们全部写完了LSA=0;LSB=1; LSC=0;
后,这个时候,我们的 P0
还没有正式赋值,而 P0
此刻却保持了前一次的值,也就是在这个瞬间,我们又给case 4
对应的数码管 LED5 网络位选赋值了一个 0
。。。
==》直到我们把 case 0
后边的语句全部完成后,我们的刷新才正式完成。而在这个刷新过程中,有 3
个瞬间我们给错误的数码管赋了值,虽然很弱(因为亮的时间很短),但是我们还是能够发现。
搞明白了原理后,我们只要避开这个瞬间错误就可以了。不产生瞬间错误的方法是,在进行位选切换期间,避免一切数码管的赋值即可。方法有两个,一个方法是刷新之前关闭所有的段选,改变好了位选后,再打开段选即可;第二个方法是关闭数码管的位选,赋值过程都做好后,再重新打开即可。这里用到了第一种方法!!
转载:https://blog.csdn.net/Neutionwei/article/details/117405211