从上一博文《​​8051单片机实战分析(以STC89C52RC为例) | 05 - 静态数码管驱动​​》,我们可以了解到数码管一些驱动原理,实际上静态扫描在项目中基本不会用到,在项目中我们经常用动态扫描,所以便有了本文。如果不懂数码管的段选与位选请先看这篇:《​​3分钟带你彻底弄懂数码管的段选与位选​​》

1 动态扫描

那什么是动态扫描呢?

举个例子:有 2 个数码管,我们要显示“12”这个数字,可以先让高位的位选三极管导通,然后控制段选让其显示“1”,延时一定时间后再让低位的位选三极管导通,然后控制段选让其显示“2”。把这个流程以一定的速度循环运行就可以让数码管显示出“12”,由于交替速度非常快,人眼识别到的就是“12”这两位数字同时亮了。

在多个数码管显示数字的时候,我们可以轮流点亮数码管(一个时刻内只有一个数码管是亮的),利用人眼的视觉暂留现象(也叫余辉效应),就可以做到看起来是所有数码管都同时亮了,这就是动态显示,也叫做动态扫描

那么一个数码管需要点亮多长时间呢?也就是说要多长时间完成一次全部数码管的扫描呢(很明显:整体扫描时间=单个数码管点亮时间*数码管个数)?答案是:10ms 以内

当电视机和显示器还处在 CRT(电子显像管)时代的时候,有一句很流行的广告语——“100Hz无闪烁”,没错,只要刷新率大于 100Hz,即刷新时间小于 10ms,就可以做到无闪烁,这也就是我们的动态扫描的硬性指标。那么你也许会问,有最小值的限制吗?理论上没有,但实际上做到更快的刷新却没有任何进步的意义了,因为已经无闪烁了,再快也还是无闪烁,只是徒然增加 CPU 的负荷而已(因为 1 秒内要执行更多次的扫描程序)。

所以,通常我们设计程序的时候,都是取一个接近 10ms,又比较规整的值就行了。

2 原理图

① 数码管原理图:

8051单片机实战分析(以STC89C52RC为例) | 06 - 动态数码管驱动_数码管

② 数码管的位选使用138译码器进行解析,关于这块我们可以参考这篇文章:《​​数字器件认识 | 74HC138三八译码器的应用​​》。

8051单片机实战分析(以STC89C52RC为例) | 06 - 动态数码管驱动_stc89c52_02

③ MCU原理图:


8051单片机实战分析(以STC89C52RC为例) | 06 - 动态数码管驱动_单片机_03

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​​个瞬间我们给错误的数码管赋了值,虽然很弱(因为亮的时间很短),但是我们还是能够发现。

搞明白了原理后,我们只要避开这个瞬间错误就可以了。不产生瞬间错误的方法是,在进行位选切换期间,避免一切数码管的赋值即可。方法有两个,一个方法是刷新之前关闭所有的段选,改变好了位选后,再打开段选即可;第二个方法是关闭数码管的位选,赋值过程都做好后,再重新打开即可。这里用到了第一种方法!!