找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 1|回复: 0
收起左侧

第6章 中断与数码管动态显示6.4

[复制链接]
ID:1167894 发表于 2026-3-30 14:35 | 显示全部楼层 |阅读模式
6.4数码管的动态显示
6.4.1 动态显示的基本原理
        学习数码管静态显示的时候提到,74HC138只能在同一时刻导通一个三极管,而数码管是靠了6个三极管来控制,那如何来让数码管同时显示呢?这就用到了动态显示的概念。
多个数码管显示数字的时候,实际上是轮流点亮数码管(一个时刻内只有一个数码管是亮的),利用人眼的视觉暂留现象(也叫余辉效应),就可以做到看起来是所有数码管都同时亮了,这就是动态显示,也叫做动态扫描。
        例如:有2个数码管,要显示“12”这个数字,先让高位的位选三极管导通,然后控制段选让其显示“1”,延时一定时间后再让低位的位选三极管导通,控制段选让其显示“2”。把这个流程以一定的速度循环运行就可以让数码管显示出“12”,由于交替速度非常快,人眼识别到的就是“12”这两位数字同时亮了。
        那么一个数码管需要点亮多长时间呢?也就是说要多长时间完成一次全部数码管的扫描呢(很明显:整体扫描时间=单个数码管点亮时间*数码管个数)?答案是10ms以内。当电视机和显示器还处在CRT(电子显像管)时代的时候,有一句很流行的广告语——“100Hz无闪烁”,只要刷新率大于100Hz,即刷新时间小于10ms,就可以做到无闪烁,这也就是动态扫描的硬性指标。有最小值的限制吗?理论上没有,但实际上做到更快的刷新却没有任何进步的意义了,因为已经无闪烁了,再快也还是无闪烁,只是徒然增加CPU的负荷而已(因为1秒内要执行更多次的扫描程序)。所以,通常设计程序的时候,都是取一个接近10ms,又比较规整的值就行了。Kingst51开发板上有6个数码管,现在就来着手写一个数码管动态扫描的程序,实现兼验证动态显示原理。
        目标还是实现秒表功能,只不过这次有6个位了,最大可以计到999999秒。现在要实现的这个程序相对于前几章的例程来说就要复杂的多了,既要处理秒表计数,又要处理动态扫描。在编写这类稍复杂的程序时,建议初学者们先用程序流程图来把程序的整个流程理清,在动手写程序之前先把整个程序的结构框架搭好,把每一个环节要实现的功能先细化出来,然后再用程序代码一步一步的去实现出来。这样就可以避免无处下笔的迷茫感了。如图6-1就是本例的程序流程图,先根据流程图把程序的执行经过在大脑里走一遍,然后再看接下来的程序代码,体会一下流程图的作用,看是不是能帮助你更顺畅的理清程序流程。
6-1.png
图6-1  数码管动态显示秒表程序流程图
#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, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = {  //数码管显示缓冲区,初值0xFF确保启动时都不亮
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

void main()
{
    unsigned char i = 0;    //动态扫描的索引
    unsigned int  cnt = 0;  //记录T0中断次数
    unsigned long sec = 0;  //记录经过的秒数

    ENLED = 0;    //使能U3,选择控制数码管
    ADDR3 = 1;    //因为需要动态改变ADDR0-2的值,所以不需要再初始化了
    TMOD = 0x01;  //设置T0为模式1
    TH0  = 0xFC;  //为T0赋初值0xFC67,定时1ms
    TL0  = 0x67;
    TR0  = 1;     //启动T0
   
    while (1)
    {
        if (TF0 == 1)          //判断T0是否溢出
        {
            TF0 = 0;            //T0溢出后,清零中断标志
            TH0 = 0xFC;        //并重新赋初值
            TL0 = 0x67;
            cnt++;              //计数值自加1
            if (cnt >= 1000)  //判断T0溢出是否达到1000次
            {
                cnt = 0;       //达到1000次后计数值清零
                sec++;         //秒计数自加1
                //以下代码将sec按十进制位从低到高依次提取并转为数码管显示字符
                LedBuff[0] = LedChar[sec%10];
                LedBuff[1] = LedChar[sec/10%10];
                LedBuff[2] = LedChar[sec/100%10];
                LedBuff[3] = LedChar[sec/1000%10];
                LedBuff[4] = LedChar[sec/10000%10];
                LedBuff[5] = LedChar[sec/100000%10];
            }
            //以下代码完成数码管动态扫描刷新
            if (i == 0)
            { ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; }
            else if (i == 1)
            { ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; }
            else if (i == 2)
            { ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; }
            else if (i == 3)
            { ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; }
            else if (i == 4)
            { ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; }
            else if (i == 5)
            { ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; }
        }
    }
}
        这段程序可以先抄到Keil中,结合程序流程图理解,最终下载到实验板上看一下运行结果。其中if...else语句就是每1ms快速的刷新一个数码管,这样6个数码管整体刷新一遍的时间就是6ms,视觉感官上就是6个数码管同时亮起来了。
         在C语言中,“/”等同于数学里的除法运算,而“%”等同于小学数学的求余数运算,这个前边已有介绍。如果是123456这个数字,要正常显示在数码管上,个位显示,就是直接对10取余数,这个“6”就出来了,十位数字就是先除以10,然后再对10取余数,以此类推,就把6个数字全部显示出来了。
        对于多选一的动态刷新数码管的方式,如果用switch会有更好的效果,来看一下用switch语句完成的情况。
#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, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[6] = {  //数码管显示缓冲区,初值0xFF确保启动时都不亮
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

void main()
{
    unsigned char i = 0;    //动态扫描的索引
    unsigned int  cnt = 0;  //记录T0中断次数
    unsigned long sec = 0;  //记录经过的秒数

    ENLED = 0;    //使能U3,选择控制数码管
    ADDR3 = 1;    //因为需要动态改变ADDR0-2的值,所以不需要再初始化了
    TMOD = 0x01;  //设置T0为模式1
    TH0  = 0xFC;  //为T0赋初值0xFC67,定时1ms
    TL0  = 0x67;
    TR0  = 1;     //启动T0
   
    while (1)
    {
        if (TF0 == 1)         //判断T0是否溢出
        {
            TF0 = 0;           //T0溢出后,清零中断标志
            TH0 = 0xFC;       //并重新赋初值
            TL0 = 0x67;
            cnt++;              //计数值自加1
            if (cnt >= 1000)  //判断T0溢出是否达到1000次
            {
                cnt = 0;       //达到1000次后计数值清零
                sec++;         //秒计数自加1
                //以下代码将sec按十进制位从低到高依次提取并转为数码管显示字符
                LedBuff[0] = LedChar[sec%10];
                LedBuff[1] = LedChar[sec/10%10];
                LedBuff[2] = LedChar[sec/100%10];
                LedBuff[3] = LedChar[sec/1000%10];
                LedBuff[4] = LedChar[sec/10000%10];
                LedBuff[5] = LedChar[sec/100000%10];
            }
            //以下代码完成数码管动态扫描刷新
            switch (i)
            {
                case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;
                case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;
                case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;
                case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;
                case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;
                case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;
                default: break;
            }
        }
    }
}
        程序完成的功能是一模一样的,但switch语句是不是比if...else语句显得要整齐清爽呢。
6.4.2数码管显示消隐
不知道读者是否发现了,这两个数码管动态显示程序的运行效果似乎并不是那么完美,第一个小问题,仔细看的话数码管的不应该亮的段,似乎有微微的发亮,这种现象叫做“鬼影”,这个“鬼影”严重影响了视觉效果,该如何解决呢?
在今后可能会遇到各种各样的实际问题,可能很多都是教材没有讲过的,遇到问题怎么办呢?作为初学者,遇到的问题肯定不是第一个遇到的,肯定有前辈已经遇到过相同的或类似的问题,一般都会在网上发表各种帖子,各种讨论,所以遇到问题,首先就应该形成一个到网上搜索的条件反射,这个问题大家可以到网上搜:“数码管消隐”或者“数码管鬼影解决”,多找相关关键词搜索试试,会搜索也是一种能力。而且随着技术发展,各种AI大模型也越来越强大,也可以直接咨询AI大模型。
解决这类问题的方法有两个,其中之一是延时,延时之后肉眼就可能看不到这个“鬼影”了。但是延时是一个非常拙劣的手段,且不说延时多久能看不到“鬼影”,延时后,数码管亮度会普遍降低。解决问题,不能只知其然,还要知其所以然,那么首先就来弄明白为什么会出现“鬼影”。
“鬼影”的出现,主要是在数码管位选和段选产生的瞬态造成的。举个简单例子,在数码管动态显示的那部分程序中,实际上每一个数码管点亮的持续时间是1ms的时间,1ms后进行下个数码管的切换。在进行数码管切换的时候,比如从case 5要切换到case 0的时候,case 5的位选用的是
ADDR0=1; ADDR1=0; ADDR2=1;
假如此刻case 5也就是最高位数码管对应的值是0,要切换成的case 0的数码管位选是 ADDR0=0; ADDR1=0; ADDR2=0;
而对应的数码管的值假如是1
又因为C语言程序是一句一句顺序往下执行的,每一条语句的执行都会占用一定的时间,即使这个时间非常非常短暂case5case0,要改变2条语句。当把ADDR0=1”改变成“ADDR0=0”的时候,这个瞬间存在了一个中间状态
ADDR0=0; ADDR1=0; ADDR2=1;
在这个瞬间上,就给case 4对应的数码管DS5瞬间赋值了0。当全部写完了ADDR0=0; ADDR1=0; ADDR2=0;后,这个时候,P0还没有正式赋值,而P0此刻保持了前一次的值,也就是在这个瞬间,又给case 0对应的数码管DS1赋值了一个0。直到把case 0后边的语句全部完成后刷新才正式完成。在这个刷新过程中,有2个瞬间给错误的数码管赋了值,虽然很弱(因为亮的时间很短),但是还是能够发现。
搞明白了原理后,解决起来就不是困难的事情了,只要避开这个瞬间错误就可以了。不产生瞬间错误的方法是,在进行位选切换期间,避免一切数码管的赋值即可。方法有两个,一个方法是刷新之前关闭所有的段,改变好了位选后,再打开段即可;第二个方法是关闭数码管的位,赋值过程都做好后,再重新打开即可。
关闭段:switch(i)这句程序之前,加一句P0=0xFF;这样就把数码管所有的段都关闭了,当把“ADDR”的值全部完成后,再给P0赋对应的值即可。
关闭位:switch(i)这句程序之前,加上一句ENLED=1;等到把ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0];这几条刷新程序全部写完后,再加上一句ENLED=0;然后再进行break操作即可。
这个地方逻辑思路上稍微有点复杂,但是一定要理解深刻,彻底弄明白,把这个瞬间的问题弄明白了,后边很多牵扯到此类情况的问题,就可以一通百通。
上边的数码管程序还有第二个问题,数码管上的数字每一秒变化一次,变化的时候,不参加变化的数码管可能出现一次抖动,这个抖动没有什么专业的名字,称之为数码管抖动吧。这种数码管抖动是什么原因造成的呢?为何在数据改变的时候才抖动呢?
来分析一下程序,程序在定时到1秒的时候,执行了“秒数+1并转换为数码管显示字符”这个操作,一个32位整型数的除法运算,实际上是比较耗费时间的,至于这一段程序究竟耗费了多少时间,可以通过前边讲的调试方法来看看这段程序运行用了多少时间。由于每次定时到1秒的时候,程序都多运行了这么一段,导致了某个数码管的点亮时间比其他情况下要长一些,总时间就变成了1ms+本段程序运行时间,于此同时,其它的数码管就熄灭了5ms+本段程序运行时间,如果这段程序运行时间非常短,可以忽略不计,但很明显,现在这段程序运行时间已经比较长了,以致于严重影响到视觉效果了,所以要采取另外一种思路去解决这个问题。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表