STC51单片机实现LED流水灯

首先引用孟军英老师的一句话,作为单片机编程的总指南:
对单片机所有的功能的实现,归根结底都是通过编程,操作P0, P1,P2等这些特殊功能寄存器(Special Functional Rigister, 简称SFR)里各个位的值实现的。

我们的目标是实现LED流水灯,编程做工程都需要模块化的思想,方便处理,这里我们也要把整体的需求拆分成若干个子需求,针对每个部分再设法分别解决,各个击破。就是
弄清需求->解决需求->实现需求
做LED流水灯,什么是流水呢,就是一个一个依次亮,我们这里就是8个LED小灯,从起始的那个开始亮, 按照顺序,每次只亮一个,一直到尾,然后再回头来从起始亮…这样循环往复。

拆分需求

需求1: 让小灯亮
需求2: 一个小灯亮了再让下一个亮,即实现“流动”的效果
需求3: 循环往复

解决需求1:让小灯亮

让某个LED小灯亮需要2个条件:
条件1.总开关LEDS6上加的电平为0
条件2. 此LED小灯上加的电平为0

解决条件1:

在LEDS6上加电平0
LEDS6其实是由74HC138控制的,74HC138是一个3-8线译码器,也就是说它有3个输入端,8个输出端,LEDS6就连接在8个输出端中的一个上,要想让LEDS6上的电平为0,需要74HC138能够工作并选择输出到LEDS6的电平为0
查74HC138芯片数据手册可知,要让对应的74HC138工作,需要开启ENABLE,使ENABLE开启的必要条件为:ENLED=0, E3 = 1;
ENLED连接到单片机寄存器P1.4上,所以应该使P1.4 = 0,而 E3是通过ADDR3这条线连接到P1.3上的,所以应该使P1.3 = 1
在74HC138能工作的前提下,还要选择输出到LEDS6的电平为0,74HC138有3个输入端用于选择,称为SELECT,分别是A0, A1, A2
查芯片数据手册可知,要使LEDS6输出0, 需要:
A0 = 0, A1 = 1, A2 = 1
由KST-51开发板原理图,A0通过ADDR0连接到P1.0,A1通过ADDR1连接到P1.1,A2通过ADDR2连接到P1.2,
所以应该使P1.0 = 0,P1.1 = 1,P1.2 = 1
好,我们及时总结一下,现在
通过开启ENABLE:
P1.3 = 1,P1.4 = 0,74HC138得以工作
在此基础上,SELECT:
P1.0 = 0,P1.1 = 1,P1.2 =1
使工作的74HC138输出到LEDS6的电平为0
从而解决了条件1。

条件2:

在条件1已满足的基础上,需要使LED灯上的电平为0
,由于8个LED灯通过DB0-DB7这8根线经由74HC245芯片连接到寄存器P0的P0.0-P0.7输出端上,又因为74HC245只起功率放大作用,不影响电平的极性,所以可以看作8个LED小灯直接连接到P0.0-P0.7上,( P0寄存器的大小是8位,从高到低依次对应P0.0到P0.7,8个引脚的输出电平与P0寄存器的相应位的值一致,例如,P0寄存器的8位为11111110时,P0.0引脚输出0,而其他输出端输出1,P0寄存器8位为01111111时,P0.7引脚输出0,其他引脚输出1。)

总结一下,
P0寄存器的某个位写0,对应的LED小灯上的电平就是0,从而解决了条件2
条件1和条件2都解决了,从而解决了需求1

解决需求2:实现流动效果

在需求1已解决的基础上,实现需求2,即实现流动的效果,其实就是: 让LED小灯按顺序依次被点亮,而且由于视觉暂留效应,点亮的小灯之间应该有一定的时间间隔,时间间隔可以用空循环实现,重点讨论按顺序依次点亮,其实就是按顺序依次使P0寄存器里前8位的值变化。

要让LED小灯从一端逐个亮到另一端,这8个值依次为,1111 1110,1111 1101,1111 1011,1111 0111,1110 1111,1101 1111,1011 1111,0111 1111

可以看出其实就是0从一端跑到另一端的过程
如何让P0的值按这样的顺序依次变化呢,大致上有3种方法

1.遍历数组法:
由于这8个数里每个数中都是只有一个0,我们可以把每种状态按位取反,这样就得到比原来小的数,方便存储,放进一个数组,然后遍历数组,又按位取反得到真正的值,依次写入P0寄存器即可
(当然,实际上我们是在用C语言编程,所以上面按位取反后的二进制数在代码里写成16进制就可以了,比如00000001写成0x01, 00000010写成0x02, …等等,而不用写一堆01了)
2.移位取反法:
上面的8个值按位取反后依次是
0000 0001,0000 0010,0000 0100,0000 1000,
0001 0000,0010 0000,0100 0000,1000 0000
可以看到从第二个数起,每一个数都是前一个数左移一位得到的。利用这个规律,可以从0000 0001开始左移,循环左移7次,对每次移动的结果按位取反赋给P0即可,写成代码就是:

for(i=1; i<8; i++)
 {
     delay(); //延时子函数
           P0 = ~(0x01<<count); //从0000 0001 开始循环左移count位
                count++;
                if (count >= 8) count = 0;
 }
3.移位置1修正法 前面两种方法都是取反,这次我们不取反,直接使用原始值,上面那8个数隔得太远了,这里再写一下 1111 1110,1111 1101,1111 1011,1111 0111, 1110 1111,1101 1111,1011 1111,0111 1111 我们用另一种方法来实现这8个数按顺序依次变化,那就是P0初始值为1111 1110即0xFE,如果它左移1位则0跑到第2个位置上了,变成1111 1100,我们是想让它变成上面的第二个数1111 1101,每次左移一位,必然导致最后一位补0,我们要让它变成1的办法是通过或运算来对最后一位置1,置1通过或运算来完成,也就是将1111 1100 与 0000 0001做或运算来修正 ,代码如下:
unsigned char i;
P0 = 0xfe;
for ( i = 0; i < 8; i++) {
    P0 = ( P0 << 1 ) | 0x01;
    delay();
}

总结一下,通过遍历数组法或者移位取反法或者移位置1修正法这3种方法均实现了使P0寄存器的值按顺序依次变化,从而使LED小灯按顺序依次亮,实现了“流动”效果,需求2被解决

解决需求3:循环往复

什么是循环往复呢,就是不能8个小灯从头到尾亮过去就完了,得反复亮,一轮又一轮
这个简单,外面加个while(1)无限循环就可以了
需求3被解决
总结,需求1,需求2,需求3都被解决,所以LED流水灯需求分析处理完毕

Talk is cheap,show me your code

开始写代码
为了使代码更具可读性,我们将使用一些具有明显意义的变量名。sbit是C51特有的语句,用作位地址声明,即取出 SFR中某一位进行操控。

#include <reg52.h>
//位地址声明, 用易读的变量名表示相应的控制位
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;
void delay();

void main()
{
    unsigned char i;
    unsigned char count = 0;

    //通过74HC138打开LED小灯总开关
    ADDR0 = 0;
    ADDR1 = 1;
    ADDR2 = 1;
    ADDR3 = 1;
    ENLED = 0;

    while(1)
    {
        for(i=1; i<8; i++)
        {
                delay(); //延时子函数
                P0 = ~(0x01<<count); //从0000 0001 开始循环左移count位
                count++;
                if (count >= 8) count = 0;
        }
    }
}

void delay()
{
    unsigned int i;
    for(i=0; i<30000; i++);
}