跳到主要内容

第7章 变量进阶与点阵LED

走在马路上的时候,经常会看到马路两侧有一些LED点阵的广告牌,这些广告牌看起来绚烂夺目,非常吸引人,而且还会变化很多种不同的显示方式。本章就会学习到点阵LED的控制方式,同时也会学习C语言变量的进阶知识——变量的作用域和存储类别。

7.1变量的作用域

B站在线视频教程:7-1 变量的作用域和存储类别

所谓的作用域就是指变量起作用的范围,也是变量的有效范围。变量按他的作用域可以分为局部变量和全局变量。

7.1.1局部变量

在一个函数内部声明的变量是内部变量,它只在本函数内有效,在本函数以外是不能使用的,这样的变量就是局部变量。此外,函数的形参也是局部变量,形参会在后面讲解函数的时候再详细解释。 比如第6章程序中定义的unsigned long sec这个变量,它是定义在main函数内部的,所以只能由main函数使用,中断函数就不能使用这个变量。同理,如果在中断函数内部定义的变量,在main函数中也是不能使用的。

7.1.2全局变量

在函数外声明的变量就是全局变量。一个源程序文件可以包含一个或者多个函数,全局变量的作用范围是从它开始声明的位置一直到程序结束。

比如第6章程序中定义的unsigned char LedBuff[6]这个数组,它的作用域就是从开始定义的位置一直到程序结束,不管是main函数,还是中断函数InterruptTimer0,都可以直接使用这个数组。

局部变量只有在声明它的函数范围内可以使用,而全局变量可以被作用域内的所有的函数使用。所以在一个函数内既可以使用本函数内声明的局部变量,也可以使用全局变量。从编程规范上讲,一个程序文件内所有的全局变量都应定义在文件的开头部分,在文件中所有函数之前。

由于C语言函数只有一个返回值,但是却经常会希望一个函数可以提供或影响多个结果值,这时就可以利用全局变量来实现。但是考虑到全局变量的一些特征,应该限制全局变量的使用,过多使用全局变量也会带来一些问题。

  1. 全局变量可以被作用域内所有的函数直接引用,可以增加函数间数据联系的途径,但同时加强了函数模块之间的数据联系,使这些函数的独立性降低,对其中任何一个函数的修改都可能会影响到其它函数的执行结果,函数之间过于紧密的联系不利于程序的维护。
  2. 全局变量的应用会降低函数的通用性,函数在执行的时候过多依赖于全局变量,不利于函数的重复利用。目前编写的程序还都比较简单,就一个.c文件,但以后要学到一个程序中有多个.c文件,当一个函数被另外一个.c文件调用的时候,必须将这个全局变量的变量值一起移植,而全局变量不只被一个函数调用,这样会引起一些不可预见的后果。
  3. 过多使用全局变量会降低程序的清晰度,使程序的可读性下降。在各个函数执行的时候都可能改变全局变量值,往往难以清楚的判断出每个时刻各个全局变量的值。
  4. 定义全局变量会永久占用单片机的内存单元,而局部变量只有进入定义局部变量的函数时才会占用内存单元,函数退出后会自动释放所占用的内存。所以大量的全局变量会额外增加内存消耗。

综上所述之原因,在编程规范上有一条原则,就是尽量减少全局变量的使用,能用局部变量代替的就不用全局变量。

还有一种特殊情况,在看别人程序的时候请注意,C语言是允许局部变量和全局变量同名的,它们定义后在内存中占有不同的内存单元。如果在同一源文件中,全局变量和局部变量同名,在局部变量作用域范围内,只有局部变量有效,全局变量不起作用,也就是说局部变量具有更高优先级。但是从编程规范上讲,是要避免全局变量与局部变量重名的,从而避免不必要的误解和误操作。

7.2变量的存储类别

变量的存储类别分为自动、静态、寄存器和外部这四种。其中后两种暂不介绍,主要介绍自动变量和静态变量这两种。

函数中的局部变量,如果不加static这个关键字来修饰,都属于自动变量,也叫做动态存储变量。这种存储类别的变量,在调用该函数的时候系统会给他们分配存储空间,在函数调用结束后会自动释放这些存储空间。动态存储变量的关键字是auto,但是这个关键字是可以省略的。

与动态变量对应的就是静态变量。首先,全局变量均是静态变量,此外,还有一种特殊的局部变量也是静态变量。即在定义局部变量时前边加上static这个关键字,加上这个关键字的变量就称之为静态局部变量,它的特点是,在整个生存期中只赋一次初值,在第一次执行该函数时,它的值就是给定的那个初值,而之后在该函数所有的执行次数中,它的值都是上一次函数执行结束后的值,即它可以保持前次的执行结果。

有这样一种情况,某个变量只在一个函数中使用,但是却想在函数多次调用期间保持住这个变量的值而不丢失,也就是说在该函数的本次调用中该变量值的改变要依赖与上一次调用函数时的值,而不能每次都从初值开始。如果使用局部动态变量的话,每次进入函数后上一次的值就丢失了,它每次都从初值开始,如果定义成全局变量的话,又违背了上面提到的尽量减少全局变量的使用这条原则,那么此时,局部静态变量就是最好的解决方案了。

比如第6章最后的例程中有一个控制数码管动态扫描显示用的索引变量i和实现秒定时的计数变量cnt,当时就是定义成了全局变量,现在就可以改成局部静态变量来试试。

#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
};
unsigned char flag1s = 0; //1秒定时标志

void main()
{
unsigned long sec = 0; //记录经过的秒数

EA = 1; //使能总中断
ENLED = 0; //使能U3,选择控制数码管
ADDR3 = 1; //因为需要动态改变ADDR0-2的值,所以不需要再初始化了
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x67;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0

while (1)
{
if (flag1s == 1) //判断1秒定时标志
{
flag1s = 0; //1秒定时标志清零
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];
}
}
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static unsigned char i = 0; //动态扫描的索引,定义为局部静态变量
static unsigned int cnt = 0; //记录T0中断次数,定义为局部静态变量

TH0 = 0xFC; //重新加载初值
TL0 = 0x67;
cnt++; //中断次数计数值加1
if (cnt >= 1000) //中断1000次即1秒
{
cnt = 0; //清零计数值以重新开始下1秒计时
flag1s = 1; //设置1秒定时标志为1
}
//以下代码完成数码管动态扫描刷新
P0 = 0xFF; //显示消隐
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;
}
}

注意看程序中中断函数里的局部变量i,为其加上了static关键字来修饰,就成为了静态局部变量。它的初始化i = 0操作只进行一次,程序执行代码中会进行i++等操作,那么下次再进入中断函数的时候,i会保持上次中断函数执行完毕后的值。如果去掉static这个关键字,那么每次进入中断函数后,i都会被初始化成0,可以自己修改程序看一下实际效果是否和理论相符。

7.3点阵的初步认识

B站在线视频教程:7-2 认识LED点阵

点阵LED显示屏作为一种现代电子媒体,具有灵活的显示面积(可任意分割和拼装)、高亮度、长寿命、数字化、实时性等特点,应用非常广泛。

通过学习LED小灯和LED数码管后,再学习LED点阵就要轻松得多了。一个数码管是8个LED组成,同理,一个88的点阵就是由64个LED小灯组成。图7-1就是一个点阵LED最小单元,即一个88的点阵LED,图7-2是它的内部结构原理图。

图7-1  8*8LED点阵外观

图7-2  8*8点阵结构原理图

从图7-2上可以看出,其实点阵LED点亮原理还是很简单的。在图中大方框外侧的就是点阵LED的引脚号,左侧的8个引脚是接的内部LED的阳极,上侧的8个引脚接的是内部LED的阴极。那么如果把9脚置成高电平、13脚置成低电平的话,左上角的那个LED小灯就会亮了。下面就用程序来实现一下,特别注意,控制点阵左侧引脚的74HC138是原理图上的U4,8个引脚自上而下依次由U4的Y0~Y7输出来控制。

#include <reg52.h>

sbit LED = P0^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main()
{
ENLED = 0; //U3、U4两片74HC138总使能
ADDR3 = 0; //使能U4使之正常输出
ADDR2 = 0; //经U4的Y0输出开启三极管Q10
ADDR1 = 0;
ADDR0 = 0;
LED = 0; //向P0.0写入0来点亮左上角的一个点
while(1); //程序停止在这里
}

同样的方法,通过对P0的整体赋值可以一次点亮点阵的一行,这次用程序来点亮点阵的第二行,对应的就需要编号U4的74HC138在其Y1引脚输出低电平了。

#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main()
{
ENLED = 0; //U3、U4两片74HC138总使能
ADDR3 = 0; //使能U4使之正常输出
ADDR2 = 0; //经U4的Y1输出开启三极管Q11
ADDR1 = 0;
ADDR0 = 1;
P0 = 0x00; //向P0写入0来点亮一行
while(1); //程序停止在这里
}

从这里可以逐步发现点阵的控制原理了。前面讲了一个数码管就是8个LED小灯,一个点阵是64个LED小灯。同样的道理,还可以把一个点阵理解成是8个数码管。经过前面的学习已经掌握了6个数码管同时显示的方法,那8个数码管也应该轻轻松松了。下面就利用定时器中断和数码管动态显示的原理来把这个点阵全部点亮。

#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main()
{
EA = 1; //使能总中断
ENLED = 0; //使能U4,选择LED点阵
ADDR3 = 0; //因为需要动态改变ADDR0-2的值,所以不需要再初始化了
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x67;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
while (1); //程序停在这里,等待定时器中断
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static unsigned char i = 0; //动态扫描的索引

TH0 = 0xFC; //重新加载初值
TL0 = 0x67;
//以下代码完成LED点阵动态扫描刷新
P0 = 0xFF; //显示消隐
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=0x00; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=0x00; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=0x00; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=0x00; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=0x00; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=0x00; break;
case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; P0=0x00; break;
case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; P0=0x00; break;
default: break;
}
}

7.4点阵的图形显示

B站在线视频教程:7-3 点阵图形显示

LED小灯可以实现流水灯,数码管可以显示多位数字,点阵LED可以显示一些花样。要显示花样,往往要先做出一些小图形,这些小图形的数据要转换到程序当中,这时就需要取模软件。给大家介绍一款简单的取模软件,这种取模软件在网上都可以下载到,其操作界面如图7-3所示。

图7-3  字模提取软件界面

单击“新建图像”菜单,根据开发板上的点阵,把宽度和高度分别改成8,然后点击“确定”,如图7-4所示。

图7-4  新建图形

单击左侧的“模拟动画”菜单,再点击“放大格点”选项,一直放大到最大,那就可以在8*8的点阵图形中用鼠标填充黑点画图形了,如图7-5所示。

图7-5  字模提取软件画图

经过一番精心设计,画出来一个心形图形,并且填充满,最终出现想要的效果图,如图7-6所示。

图7-6  心型图形

由于取模软件是把黑色取为1,白色取为0,但点阵是1对应LED熄灭,0对应LED点亮,而这里需要的是一颗点亮的“心”,所以要选“修改图像”菜单里的“黑白反显图像”这个选项,再点击“基本操作”菜单里边的“保存图像”可以把设计好的图片进行保存,如图7-7所示。

图7-7  保存图形

保存文件只是为了再次使用或修改使方便,当然也可以不保存。操作完了这一步后,点击“参数设置”菜单里的“其他选项”,如图7-8所示。

图7-8  选项设置

这里的选项,要结合图7-2来进行设置,可以看到P0口控制的是一行,所以选择“横向取模”,如果控制的是一列,就要选“纵向取模”。选中“字节倒序”这个选项,是因为图7-2中左边是低位DB0,右边是高位DB7,所以是字节倒序,其它两个选项自己了解,点确定后,选择“取模方式”这个菜单,点一下“C51格式”后,在“点阵生成区”自动产生了8个字节的数据,这8个字节的数据就是取出来的“模”,如图7-9所示。

图7-9  取模结果

大家注意,虽然用了软件来取模,但是也得知道其原理是什么,在这个图片里,黑色的一个格子表示一位二进制的1,白色的一个格子表示一位二进制的0。第一个字节是0xFF,其实就是这个8*8图形的第一行,全黑就是0xFF;第二个字节是0x99,低位在左边,高位在右边,黑色的表示1,白色的表示0,就组成了0x99这个数值。 那么下面就用程序把这些数据依次送到点阵上去,看看运行效果如何。

#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 image[] = { //图片的字模表
0xFF, 0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7
};

void main()
{
EA = 1; //使能总中断
ENLED = 0; //使能U4,选择LED点阵
ADDR3 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x67;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
while (1);
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static unsigned char i = 0; //动态扫描的索引

TH0 = 0xFC; //重新加载初值
TL0 = 0x67;
//以下代码完成LED点阵动态扫描刷新
P0 = 0xFF; //显示消隐
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=image[0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=image[1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=image[2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=image[3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=image[4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=image[5]; break;
case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; P0=image[6]; break;
case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; P0=image[7]; break;
default: break;
}
}

对于88的点阵,可以显示一些简单的图形,字符等。但大部分汉字通常要用到1616个点,8*8的点阵只能显示一些简单笔画的汉字。使用大屏显示汉字的方法和小屏的方法是类似的,所需要做的只是按照相同的原理来扩展行数和列数而已。

7.5点阵的动画显示

点阵的动画显示,说到底就是对多张图片分别进行取模,使用程序算法巧妙的切换图片,多张图片组合起来就成了一段动画了,动画片、游戏等等基本原理也都是如此。

7.5.1点阵的纵向移动

B站在线视频教程:7-4 点阵纵向移动

7.4节介绍了如何在点阵上画一个❤形,有时候希望这些显示是动起来的,而不是静止的。对于点阵本身已经没有多少的知识点可以介绍了,主要就是编程算法来解决问题。比如现在要让点阵显示一个I ❤ U的动画,首先要把这个图形用取模软件画出来看一下,如图7-10所示。

图7-10  上下移动横向取模

这张图片共有40行,每8行组成一张点阵图片,并且每向上移动一行就出现了一张新图片,一共组成了33张图片。

用一个变量index来代表每张图片的起始位置,每次从index起始向下数8行代表了当前的图片,250ms改变一张图片,然后不停的动态刷新,这样图片就变成动画了。首先要对显示的图片进行横向取模,虽然这是33张图片,由于每一张图片都是和下一行连续的,所以实际的取模值只需要40个字节就可以完成。

#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 image[] = { //图片的字模表
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xC3,0xE7,0xE7,0xE7,0xE7,0xE7,0xC3,0xFF,
0x99,0x00,0x00,0x00,0x81,0xC3,0xE7,0xFF,
0x99,0x99,0x99,0x99,0x99,0x81,0xC3,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
};

void main()
{
EA = 1; //使能总中断
ENLED = 0; //使能U4,选择LED点阵
ADDR3 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x67;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
while (1);
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static unsigned char i = 0; //动态扫描的索引
static unsigned char tmr = 0; //250ms软件定时器
static unsigned char index = 0; //图片刷新索引

TH0 = 0xFC; //重新加载初值
TL0 = 0x67;
//以下代码完成LED点阵动态扫描刷新
P0 = 0xFF; //显示消隐
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=image[index+0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=image[index+1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=image[index+2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=image[index+3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=image[index+4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=image[index+5]; break;
case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; P0=image[index+6]; break;
case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; P0=image[index+7]; break;
default: break;
}
//以下代码完成每250ms改变一帧图像
tmr++;
if (tmr >= 250) //达到250ms时改变一次图片索引
{
tmr = 0;
index++;
if (index >= 33) //图片索引达到33后归零
{
index = 0;
}
}
}

将这个程序下载到单片机上看看效果,即可看到I ❤ U一直往上走动的动画。

7.5.2点阵的横向移动

B站在线视频教程:7-5 点阵横向移动

上下移动会了,那还想左右移动该如何操作呢?
方法一、最简单的办法就是把板子侧过来放,纵向取模。
要做好技术,但是不能沉溺于技术。技术是工具,在做开发的时候除了用好这个工具外,也得多拓展自己解决问题的思路,要慢慢培养自己的多角度思维方式。
那把板子正过来,左右移动就完不成了吗?当然不是。一旦硬件设计好了,要完成一种功能,大脑就可以直接确定能否完成这个功能,这在进行电路设计的时候尤为重要。开发产品的时,首先是设计电路,此刻就要在大脑中通过思维来验证板子硬件和程序能否完成想要的功能。一旦硬件做好了,剩下的就是靠编程来完成了。只要是硬件逻辑上没问题,所有的功能均可由软件实现。
在进行硬件电路设计的时候,也得充分考虑软件编程的方便性。因为程序是用P0来控制点阵的整行,所以对于这样的电路设计,上下移动程序是比较好编写的。那如果设计电路的时候知道图形要左右移动,那设计电路画板子的时候就要尽可能的把点阵横过来放,有利于编程方便,减少软件工作量。
方法二、利用二维数组来实现,算法基本上和上下移动相似。
二维数组的声明方式是:
数据类型 数组名[数组长度1][数组长度2];
与一维数组类似,数据类型是全体元素的数据类型,数组名是标识符,数组长度1和数组长度2分别代表数组具有的行数和列数。数组元素的下标一律从0开始。
例如:unsigned char a[2][3];声明了一个具有2行3列的无符号字符型的二维数组a。
二维数组的数组元素总个数是两个长度的乘积。二维数组在内存中存储的时候,采用行优先的方式来存储,即在内存中先存放第0行的元素,再存放第一行的元素......,同一行中再按照列顺序存放,刚才定义的那个a[2][3]的存放形式就如表7-1所示。
表7-1 二维数组的物理存储结构

a[0][0]a[0][1]a[0][2]a[1][0]a[1][1]a[1][2]

二维数组的初始化方法分两种情况,前边一维数组讲过,数组元素的数量可以小于数组元素个数,没有赋值的会自动给0。当数组元素的数量等于数组个数时,如下所示:
unsigned char a[2][3] = {{1,2,3}, {4,5,6}}; 或者是
unsigned char a[2][3] = {1,2,3,4,5,6};
当数组元素的数量小于数组个数的时候,如下所示:
unsigned char a[2][3] = {{1,2}, {3,4}}; 等价于
unsigned char a[2][3] = {1,2,0,3,4,0};
而反过来的写法
unsigned char a[2][3] = {1,2,3,4}; 等价于
unsigned char a[2][3] = {{1,2,3}, {4,0,0}};
此外,二维数组初始化的时候,行数可以省略,编译系统会自动根据列数计算出行数,但是列数不能省略。

讲这些,是为了看别人写的代码的时候别发懵,但是今后自己写程序要按照规范,行数列数都不要省略,全部写齐,初始化的时候,全部写成unsigned char a[2][3] = {{1,2,3}, {4,5,6}};的形式,而不允许写成一维数组的格式,防止大家出错,同时也是提高程序的可读性。 那么下面要进行横向做I ❤ U的动画了,先把需要的图片画出来,再逐一取模,和上一张图片类似的是,这个图形共有30张图片,通过程序每250ms改变一张图片,就可以做出来动画效果了。但是不同的是,这个是要横向移动,横向移动的图片切换时的字模数据不是连续的,所以这次要对30张图片分别取模,如图7-11所示。

图7-11  横向动画取模图片

图7-11中最上面的图形是整个连续图形,实际上要把它分解为30个帧,每帧图片单独取模,取出来都是8个字节的数据,一共就是30*8个数据,用一个二维数组来存储它们。

#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 image[30][8] = {
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}, //动画帧1
{0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F}, //动画帧2
{0xFF,0x3F,0x7F,0x7F,0x7F,0x7F,0x7F,0x3F}, //动画帧3
{0xFF,0x1F,0x3F,0x3F,0x3F,0x3F,0x3F,0x1F}, //动画帧4
{0xFF,0x0F,0x9F,0x9F,0x9F,0x9F,0x9F,0x0F}, //动画帧5
{0xFF,0x87,0xCF,0xCF,0xCF,0xCF,0xCF,0x87}, //动画帧6
{0xFF,0xC3,0xE7,0xE7,0xE7,0xE7,0xE7,0xC3}, //动画帧7
{0xFF,0xE1,0x73,0x73,0x73,0xF3,0xF3,0xE1}, //动画帧8
{0xFF,0x70,0x39,0x39,0x39,0x79,0xF9,0xF0}, //动画帧9
{0xFF,0x38,0x1C,0x1C,0x1C,0x3C,0x7C,0xF8}, //动画帧10
{0xFF,0x9C,0x0E,0x0E,0x0E,0x1E,0x3E,0x7C}, //动画帧11
{0xFF,0xCE,0x07,0x07,0x07,0x0F,0x1F,0x3E}, //动画帧12
{0xFF,0x67,0x03,0x03,0x03,0x07,0x0F,0x9F}, //动画帧13
{0xFF,0x33,0x01,0x01,0x01,0x03,0x87,0xCF}, //动画帧14
{0xFF,0x99,0x00,0x00,0x00,0x81,0xC3,0xE7}, //动画帧15
{0xFF,0xCC,0x80,0x80,0x80,0xC0,0xE1,0xF3}, //动画帧16
{0xFF,0xE6,0xC0,0xC0,0xC0,0xE0,0xF0,0xF9}, //动画帧17
{0xFF,0x73,0x60,0x60,0x60,0x70,0x78,0xFC}, //动画帧18
{0xFF,0x39,0x30,0x30,0x30,0x38,0x3C,0x7E}, //动画帧19
{0xFF,0x9C,0x98,0x98,0x98,0x9C,0x1E,0x3F}, //动画帧20
{0xFF,0xCE,0xCC,0xCC,0xCC,0xCE,0x0F,0x1F}, //动画帧21
{0xFF,0x67,0x66,0x66,0x66,0x67,0x07,0x0F}, //动画帧22
{0xFF,0x33,0x33,0x33,0x33,0x33,0x03,0x87}, //动画帧23
{0xFF,0x99,0x99,0x99,0x99,0x99,0x81,0xC3}, //动画帧24
{0xFF,0xCC,0xCC,0xCC,0xCC,0xCC,0xC0,0xE1}, //动画帧25
{0xFF,0xE6,0xE6,0xE6,0xE6,0xE6,0xE0,0xF0}, //动画帧26
{0xFF,0xF3,0xF3,0xF3,0xF3,0xF3,0xF0,0xF8}, //动画帧27
{0xFF,0xF9,0xF9,0xF9,0xF9,0xF9,0xF8,0xFC}, //动画帧28
{0xFF,0xFC,0xFC,0xFC,0xFC,0xFC,0xFC,0xFE}, //动画帧29
{0xFF,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFF} //动画帧30
};
void main()
{
EA = 1; //使能总中断
ENLED = 0; //使能U4,选择LED点阵
ADDR3 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x67;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
while (1);
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static unsigned char i = 0; //动态扫描的索引
static unsigned char tmr = 0; //250ms软件定时器
static unsigned char index = 0; //图片刷新索引

TH0 = 0xFC; //重新加载初值
TL0 = 0x67;
//以下代码完成LED点阵动态扫描刷新
P0 = 0xFF; //显示消隐
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=image[index][0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=image[index][1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=image[index][2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=image[index][3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=image[index][4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; P0=image[index][5]; break;
case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; P0=image[index][6]; break;
case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; P0=image[index][7]; break;
default: break;
}
//以下代码完成每250ms改变一帧图像
tmr++;
if (tmr >= 250) //达到250ms时改变一次图片索引
{
tmr = 0;
index++;
if (index >= 30) //图片索引达到30后归零
{
index = 0;
}
}
}

下载进到板子上瞧瞧,是不是有一种特别好的感觉。技术外行人看的很神秘,其实做出来会发现逻辑很简单,每250ms更改一张图片,每1ms在中断里刷新单张图片的某一行。

不管是上下移动还是左右移动,都是对一帧帧的图片的切换,这种切换带来的视觉效果就是一种动态的了。录像实际上就是快速的拍摄了一帧帧的图片,然后对这些图片的快速回放,把动画效果给显示出来。因为硬件设计的缘故,所以在写上下移动程序的时候,数组定义的元素比较少,但是实际上也得理解成是32张图片的切换显示,而并非是真正的“移动”。

7.6练习题

  1. 掌握变量的作用域以及存储类别。
  2. 了解点阵的显示原理,理解点阵动画显示原理。
  3. 独立完成点阵显示I❤U向下移动的程序。
  4. 独立完成点阵显示I❤U向右移动的程序。
  5. 用点阵做一个9到0的倒计时牌显示。
  6. 尝试实现流水灯、数码管和点阵的同时显示。