我们现在走在马路上,经常看到马路两侧有一些LED点阵广告牌,这些广告牌看起来绚烂夺目,非常吸引人,而且还会变化很多种不同的显示方式。本章我们就会学习到点阵LED的控制方式,按照惯例,先普及部分C语言知识。
7.1 变量的作用域
所谓的作用域就是指变量起作用的范围。变量按他的作用域可以分为局部变量和全局变量
1.局部变量
在一个函数内部声明的变量是内部变量,他只在本函数内有效,在此函数以外是不能使用的,这样的变量就是局部变量。此外,函数的形参也是局部变量,形参我们后边详细解释。
比如上节课定义的unsigned long stopwatch = 0,这个变量是定义在main函数内部的,所以只能由main函数使用,中断函数就不能用这个变量。同理,我们如果在中断函数内部定义的变量,在main函数中也是不能使用的。
2.全局变量
在函数外声明的变量是全局变量。一个源程序文件可以包含一个或者多个函数,全局变量的作用范围是从它开始声明的位置一直到程序结束。
比如上节课的unsigned char LedNumber[6] = {0}; 这个数组的作用域就是从开始定义的位置一直到程序结束,不管是main函数,还是中断函数InterruptTimer0,都可以直接使用这个数组。
局部变量只有在声明它的函数范围内有效,而全局变量可以被作用域内的所有的函数直接引用。所以在一个函数内既可以使用本函数内声明的局部变量,也可以使用全局变量。在习惯上,我们把全局变量定义在我们程序所有函数的最前边。
由于函数通常只能有一个返回值,但是我们希望一个函数运行完了可以提供多个结果值给我们使用的时候,我们就可以利用全局变量来实现。但是考虑到全局变量的一些特征,应该限制全局变量的使用,过多使用全局变量也会带来一些问题。
(1)全局变量可以被作用域内所有的函数直接引用,可以增加函数间数据联系的途径,但同时加强了函数模块之间的数据联系,使这些函数的独立性降低,对其中任何一个函数的修改都可能会影响到其他函数,函数之间过于紧密的联系不利于程序的维护。
(2)全局变量的应用会降低函数的通用性,函数在执行的时候过多依赖于全局变量,不利于函数的重复利用。我们现在程序编写比较简单,就一个.c文件,将来以后我们要学到一个程序中有多个.c文件,当一个函数被另外一个.c文件调用的时候,必须将这个全局变量的变量值一起移植,而全局变量不只被一个函数调用,这样会引起一些不可预见的后果。
(3)过多使用全局变量会降低程序的清晰度,使程序的可读性下降。在各个函数执行的时候都可能改变全局变量值,往往难以清楚的判断出每个时刻各个全局变量的值。
(4)定义全局变量会直接占用单片机的内存单元,而局部变量只有进入定义局部变量的函数内才会分配内存,函数退出后会自动释放所占用的内存。所以大量的全局变量会额外增加内存占用。
综上所述之原因,我们一项原则就是尽量减少全局变量的使用,能用局部变量代替的就不用全局变量。
还有一种特殊情况,大家在看别人程序的时候注意。C语言是允许局部变量和全局变量同名的,他们定义后在内存中占有不同的内存单元。如果在同一源文件中,全局变量和局部变量同名,在局部变量作用域范围内,只有局部变量有效,全局变量不起作用,也就是说局部变量具有更高优先级。但是我们在编写程序的时候,尽量不要让变量重名,以避免不必要的误解。
.2 变量的存储类别
变量的存储类别分为自动、静态、寄存器和外部这四种。其中后两种我们暂不介绍,主要是自动变量和静态变量这两种。
函数中的局部变量,如果不加static这个关键字来进行特别声明,都属于自动变量,也叫做动态存储变量。这些存储类别的变量,在调用该函数的时候系统会给他们分配存储空间,在函数调用结束后会自动释放这些存储空间。动态存储变量的关键字是auto,但是这个关键字是可以省略的,所以我们平时都不用。
那么与动态变量对应的就是静态变量。首先,全局变量均是静态变量,此外,还有一种特殊的局部变量也是静态变量。即我们在局部变量声明前边加上static这个关键字,加上这个关键字的变量就称之为静态局部变量,他的特点是,在整个生存期中只赋一次初值,函数调用的时候,如果是第一次调用,它的值就是我们给定的那个初值;如果不是第一次调用,那么它的值就是上一次函数调用结束后的值。
在某一些场合中,一些变量只在一个函数中使用了,但是这个变量每次变化的值我们还想保存,如果定义成局部动态变量的话,每次进入函数后上一次的值就丢失了,如果定义成全局变量的话,又违背了我们上面提到的关于全局变量使用的一般原则,这个时候我们就可以定义成局部静态变量了。
比如上节课中断程序中有一个用于动态刷新数码管控制的变量j,我们上节课的程序是定义成了全局变量,现在我们可以直接改成局部静态变量来试试。
#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 LedNumber[6] = {0}; //定义全局变量
unsigned int counter = 0;
void main()
{
unsigned long stopwatch =0;
ENLED = 0; ADDR3 = 1; P0 = 0XFF; //74HC138和P0初始化部分
TMOD = 0x01; //设置定时器0为模式1
TH0 = 0xFC;
TL0 = 0x67; //定时值初值,定时1ms
TR0 = 1; //打开定时器0
EA = 1; //打开中中断
ET0 = 1; //打开定时器0中断
while(1)
{
if(1000 == counter) //判断定时器0溢出是否达到50次
{
counter = 0;
stopwatch++;
LedNumber[0] = stopwatch%10;
LedNumber[1] = stopwatch/10%10;
LedNumber[2] = stopwatch/100%10;
LedNumber[3] = stopwatch/1000%10;
LedNumber[4] = stopwatch/10000%10;
LedNumber[5] = stopwatch/100000%10;
}
}
}
void InterruptTimer0() interrupt 1 //中断函数的特殊写法,数字’1’为中断入口号
{
static unsigned char j = 0;
TH0 = 0xFC; //溢出后进入中断重新赋值
TL0 = 0x67;
counter++; //计数值counter加1
P0 = 0xFF; //消隐
switch(j)
{
case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[0]]; break;
case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; P0=LedChar[LedNumber[1]]; break;
case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[2]]; break;
case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; P0=LedChar[LedNumber[3]]; break;
case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; P0=LedChar[LedNumber[4]]; break;
case 5: ADDR0=1; ADDR1=0; ADDR2=1; j=0; P0=LedChar[LedNumber[5]]; break;
default: break;
} //动态刷新
}
大家注意看这个程序的中断函数的静态变量j,如果加上了static,他的初始化j = 0操作只进行一次,下边的程序会进行j++操作,下次进入中断函数的时候,j会保持上次的值。但是如果去掉static这个关键字,那每次进入函数后,j都会被初始化成0,大家可以自己修改程序做尝试。
1.3 点阵LED的初步认识
点阵LED显示屏作为一种现代电子媒体,具有灵活的显示面积(可分割、任意拼装)、高亮度、长寿命、数字化、实时性等特点,应用非常广泛。
前边学了LED小灯和LED数码管后,学LED点阵就要轻松得多了。一个数码管是8个LED组成,同理,一个8*8的点阵是由64个LED小灯组成。图7-1就是一个点阵LED最小单元,一个8*8的点阵LED,图7-2是它的内部结构图。
图7-1 8*8点阵LED
7-2 8*8点阵LED结构原理图
点阵LED内部原理图如图7-2所示,从7-2图上可以看出来,其实点阵LED点亮原理还是很简单的。在我们图上蓝色方框外侧的就是点阵LED的引脚号,左侧的8个引脚是接的内部LED的阳极,上侧的8个引脚接的是内部LED的阴极。那从图上可以看出来,我们的9脚如果是高电平,13脚是低电平的话,最左上角的那个LED小灯就会亮,那我们用程序来实现一下,特别注意,我们现在用的74HC138是原理图上的U4。
#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;
ADDR0 = 0;
ADDR1 = 0;
ADDR2 = 0;
ADDR3 = 0; //74HC138开启三极管
LED = 0; //点亮点阵的一个点
while(1); //程序停止在这里
}
同样的方法,我们可以点亮点阵的任意一行,74HC 138的导通点阵所用的三极管的方法和数码管很类似,那我们现在来点亮第二行整行的LED。
#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;
ADDR0 = 1;
ADDR1 = 0;
ADDR2 = 0;
ADDR3 = 0; //74HC138开启三极管
P0 = 0x00; //点亮小灯
while(1); //程序停止在这里
}
从这里我们逐步发现了一个问题,其实我们讲一个数码管就是8个LED小灯,一个点阵是64个LED小灯。同样的道理,我们还可以把一个点阵理解成8个数码管。我们前边掌握了6个数码管的同时显示方法,那8个数码管也应该轻轻松松了。我们先把这个点阵全部点亮。
#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;
ADDR3 = 0;
TMOD = 0x01; //设置定时器0为模式1
TH0 = 0xFC;
TL0 = 0x67; //定时值初值,定时1ms
TR0 = 1; //打开定时器0
EA = 1; //打开中中断
ET0 = 1; //打开定时器0中断
while(1); //程序停止在这里,定时器运行,等待定时器中断
}
void InterruptTimer0() interrupt 1 //中断函数
{
static unsigned char j = 0;
TH0 = 0xFC; //溢出后进入中断重新赋值
TL0 = 0x67;
P0 = 0XFF; //消隐
switch(j)
{
case 0: ADDR0=0; ADDR1=0; ADDR2=0; j++; break;
case 1: ADDR0=1; ADDR1=0; ADDR2=0; j++; break;
case 2: ADDR0=0; ADDR1=1; ADDR2=0; j++; break;
case 3: ADDR0=1; ADDR1=1; ADDR2=0; j++; break;
case 4: ADDR0=0; ADDR1=0; ADDR2=1; j++; break;
case 5: ADDR0=1; ADDR1=0; ADDR2=1; j++; break;
case 6: ADDR0=0; ADDR1=1; ADDR2=1; j++; break;
case 7: ADDR0=1; ADDR1=1; ADDR2=1; j=0; break;
default: break;
} //动态刷新
P0=0x00;
}
7.4 点阵LED图形显示
我们的小灯可以实现流水灯,数码管可以显示数字,那点阵LED就得来显示点花样了。
我们要显示花样的时候,往往要做出来一些小图形,这些小图形的数据要转换到我们的程序当中去,这个时候就需要取模软件。来给大家介绍一款简单的取模软件,这个取模软件可以在http://www.51hei.com 的软件下载区下载到,大家来了解一下如何用,先看一下操作界面,如图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 取模结果
大家注意,我们虽然用软件取模,但是也得知道其原理是什么,在这个图片里,黑色的一个格子表示一个二进制的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 LedChar[] = {
0xFF,0x99,0x00,0x00,0x00,0x81,0xc3,0xE7
};
void main() //主函数
{
ENLED = 0;
ADDR3 = 0;
TMOD = 0x01; //设置定时器0为模式1
TH0 = 0xFC;
TL0 = 0x67; //定时值初值,定时1ms
TR0 = 1; //打开定时器0
EA = 1; //打开中中断
ET0 = 1; //打开定时器0中断
while(1); //程序停止在这里,定时器运行,等待定时器中断
}
void InterruptTimer0() interrupt 1 //中断函数
{
static unsigned char j = 0;
TH0 = 0xFC; //溢出后进入中断重新赋值
TL0 = 0x67;
P0 = 0XFF; //消隐
switch(j)
{
case 0: ADDR0=0; ADDR1=0; ADDR2=0; break;
case 1: ADDR0=1; ADDR1=0; ADDR2=0; break;
case 2: ADDR0=0; ADDR1=1; ADDR2=0; break;
case 3: ADDR0=1; ADDR1=1; ADDR2=0; break; //动态刷新
case 4: ADDR0=0; ADDR1=0; ADDR2=1; break;
case 5: ADDR0=1; ADDR1=0; ADDR2=1; break;
case 6: ADDR0=0; ADDR1=1; ADDR2=1; break;
case 7: ADDR0=1; ADDR1=1; ADDR2=1; break;
default: break;
}
P0 = LedChar[j++];
if(8==j) j=0;
}
对于8*8的点阵来说,我们可以显示一些简单的图形,字符等。一个汉字正常占的点数是16*16的,8*8的点阵只能显示一些简单笔画的汉字,大家可以自己取模做出来试试。使用大屏显示汉字的方法和小的方法是类似的,这个内容我们考虑以后扩展实验的时候再进行讲解。
7.5 点阵LED动画显示
点阵的动画显示,说到底就是我们对多张图片进行取模,使用程序算法巧妙的切换图片,多张图片组合起来就成了一段动画了,我们所看到的动画片、游戏等等,都是这么做的。
7.5.1 点阵的纵向移动
上一节我们学了如何在点阵上画一个❤形,有时候我们希望这些显示是动起来的,而不是静止的。对于点阵已经没有太多的知识点可以介绍了,主要就是编程的算法问题了。
比如我们现在要让我们的点阵显示一个I ❤ U这样的一个动画,首先我们要把这个图形用取模软件画出来看一下,如图7-10所示。
图7-10 上下移动横向取模
这张图片共有40行,每8行组成一张点阵图片,并且每向上移动一行就出现了一张新图片,一共组成了32张图片。
用一个变量index来代表每张图片的起始位置,每次从index起始向下数8行代表了当前的图片,250ms改变一张图片,然后不停的动态刷新,这样图片就变成动画了。首先我们要对显示的图片进行横向取模,虽然这是32张图片,由于我们每一张图片都是和下一行连续的,所以实际的取模值只需要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 graph[] = {
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
};
unsigned char index = 0; //图片刷新索引
void main(void)
{
P0 = 0xFF; //P0口初始化
ADDR3 = 0; //选择LED点阵
ENLED = 0; //LED显示总使能
TMOD = 0x01; //设置定时器0为模式1
TH0 = 0xFC; //定时器初值,定时1ms
TL0 = 0x67;
TR0 = 1; //打开定时器0
ET0 = 1; //使能定时器0中断
EA = 1; //打开总中断开关
while(1);
}
void InterruptTimer0() interrupt 1
{
static unsigned char j = 0;
static unsigned char tmr = 0;
TH0 = 0xFC; //溢出后进入中断重新赋值
TL0 = 0x67;
P0 = 0xFF; //LED点阵动态刷新
switch (j)
{
case 0: ADDR0=0; ADDR1=0; ADDR2=0; break;
case 1: ADDR0=1; ADDR1=0; ADDR2=0; break;
case 2: ADDR0=0; ADDR1=1; ADDR2=0; break;
case 3: ADDR0=1; ADDR1=1; ADDR2=0; break;
case 4: ADDR0=0; ADDR1=0; ADDR2=1; break;
case 5: ADDR0=1; ADDR1=0; ADDR2=1; break;
case 6: ADDR0=0; ADDR1=1; ADDR2=1; break;
case 7: ADDR0=1; ADDR1=1; ADDR2=1; break;
default: break;
}
P0 = graph[index+j];
j++;
if (j >= 8)
{
j = 0;
}
tmr++; //图片刷新频率控制
if (tmr >= 250) //每隔250ms刷新一帧
{
tmr = 0;
index++;
if (index >= 32)
{
index = 0;
}
}
}
大家把这个程序下载进去看看效果,一个I ❤ U一直往上走动的动画出现了,现在还有哪位敢说我们工科同学不懂浪漫的?还需要用什么玫瑰花取悦女朋友吗?一点技术含量都没有,要玩就玩点高科技,呵呵。
当然,别光图开心,学习我们还要继续。往上走动的动画我写出来了,那往下走动的动画,大家就要自己独立完成了,不要偷懒,一定要去写代码调试代码。瞪眼看只能了解知识,而能力是在真正的写代码、调试代码这种实践中培养起来的。
7.5.2 点阵的横向移动
上下移动移动我们会了,那我们还想左右移动该如何操作呢?
方法一、最简单,就是把板子侧过来放,纵向取模就可以完成。
这里大家是不是有种头顶冒汗的感觉?我们要做好技术,但是不能沉溺于技术。技术是我们的工具,我们在做开发的时候除了用好这个工具外,也得多扩展自己的解决问题的思路,要慢慢培养自己的多角度思维方式。
那把板子正过来,左移移动就完不成了吗?当然不是。大家慢慢的学多了就会培养了一种感觉,就是一旦硬件设计好了,我们要完成一种功能,大脑就可以直接思考出来能否完成这个功能,这个在我们进行电路设计的时候最为重要。我们在开发产品的时候,首先是设计电路,设计电路的时候,工程师就要在大脑中通过思维来验证板子硬件和程序能否完成我们想要的功能,一旦硬件做好了,做好板子回来剩下的就是靠编程来完成了。只要是硬件逻辑上没问题,功能上软件肯定可以实现。
当然了,我们在进行硬件电路设计的时候,也得充分考虑下软件编程的方便性。因为我们的程序是用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 动画取模图片
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 graph[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
};
unsigned char index = 0; //图片刷新索引
void main(void)
{
P0 = 0xFF; //P0口初始化
ADDR3 = 0; //选择LED点阵
ENLED = 0; //LED显示总使能
TMOD = 0x01; //设置定时器0为模式1
TH0 = 0xFC; //定时器初值,定时1ms
TL0 = 0x67;
TR0 = 1; //打开定时器0
ET0 = 1; //使能定时器0中断
EA = 1; //打开总中断开关
while(1);
}
void InterruptTimer0() interrupt 1
{
static unsigned char j = 0;
static unsigned char tmr = 0;
TH0 = 0xFC; //溢出后进入中断重新赋值
TL0 = 0x67;
P0 = 0xFF; //LED点阵消隐
switch (j)
{
case 0: ADDR0=0; ADDR1=0; ADDR2=0; break;
case 1: ADDR0=1; ADDR1=0; ADDR2=0; break;
case 2: ADDR0=0; ADDR1=1; ADDR2=0; break;
case 3: ADDR0=1; ADDR1=1; ADDR2=0; break;
case 4: ADDR0=0; ADDR1=0; ADDR2=1; break;
case 5: ADDR0=1; ADDR1=0; ADDR2=1; break;
case 6: ADDR0=0; ADDR1=1; ADDR2=1; break;
case 7: ADDR0=1; ADDR1=1; ADDR2=1; break;
default: break;
}
P0=graph[index][j]; //刷新的是二维数组的列数据
j++;
if (j >= 8)
{
j = 0;
}
tmr++; //图片刷新频率控制
if (tmr >= 250) //每隔250ms刷新一帧
{
tmr = 0;
index++; //索引代表了每一行的起始位置
if (index >= 30)
{
index = 0;
}
}
}
下载进到板子上瞧瞧,是不是有一种帅到掉渣的感觉呢。技术这东西,外行人看的是很神秘的,其实我们做出来会发现,也就是那么回事而已,每250ms更改一张图片,每1ms在定时器中断里刷新单张图片的某一行。
不管是上下移动还是左右移动,大家要建立一种概念,就是我们是对一帧帧的图片的切换,这种切换带给我们的视觉效果就是一种动态的了。比如我们的DV拍摄动画,实际上就是快速的拍摄了一帧帧的图片,然后对这些图片的快速回放,把动画效果给显示了出来。因为我们硬件设计的缘故,所以我们在写上下移动程序的时候,数组定义的元素比较少,但是实际上大家也得理解成为32张图片的切换显示,而并非是真正的“移动”。