注:文档全部借助于蓝桥杯实训指导手册书写,如有疏漏之处请参考蓝桥杯原文手册。
链接: [link](链接:https://pan.baidu.com/s/12zGILFAKxsEibzt5hILVWA?pwd=j9gl
提取码:j9gl)
本文记录个人的学习心得体会,也为了弥补当时的一些遗憾,无意做蓝桥杯教程,所以前面的环境配置这些东西有需要了解的同学可以自行去蚂蚁工厂,小蜜蜂等哔哩哔哩UP主处了解配置。
下面是正文:
LED电路的分析
锁存器的妙用
由图可见,板子上的LED灯一共有八个,它们一端接VCC高电平一段接了一个M74HC573M1R芯片,这个芯片是一个数据锁存器,当你将LE端口置高电平时,D1 ~ D8的数据就可以“透传”到Q1 ~ Q8端口。电路采用 M74HC573M1R 锁存器的原因呢,有两个作用:一个是对单片机 P0 口的输出信号进行缓冲,另一个是增强信号驱动能力进而驱动 LED 发光。
举个例子,比如D1~D8端口的电平信号我们高电平用0表示,低电平用1(x个人图方便写的,注意一下)表示,分别为00000001。那么当LE的端口使能时,这些电平信号就会从D1 ~ D8“透传”到另一端的Q1 ~ Q8端口。此时,LED8点亮。
这就是LED电路点亮的第一个要点。锁存器在51单片机的设计中经常使用,因为芯片的电流输出能力是有限的,51本来也就是低功耗的产品。当我们需要输出大电流驱动像LED这样子的外设时,可以使用锁存器加强信号的驱动能力从而驱动LED灯发光。同时,锁存器在我们遇到一些外设共用端口的情况时,也可以用于解决这个端口冲突的问题。
或非门的使用
74HC02是一个或非门,这里用到这个的目的呢是切换板子上的IO口模式和MM模式。关于MM模式我做比赛的时候没有了解过,据说代码上会很简洁,不过实际比赛的人好像没人用这个模式。所以这一块大家其实不用太过于关注。我们主要了解这个位置带来的电平变化问题。下面是逻辑变化情况
我们将J13跳线帽接到IO口模式对应的位置时,WR网络将会置为低电平。如下图:
![跳线帽接法示意图
所以与非门这一块我们大可不必关注,记住一点就是:Y4置为低电平时Y4C才为高电平,锁存器使能,数据“透传”到另一端。
八位译码器(死去的数电开始攻击我)
P25,P26,P27三个引脚高电平用“1”表示,低电平用“0”表示。三个端口可以组成一个三位的2进制数,最高可以表示8。当三个端口分别为001时,表示1,Y1使能,为低电平。(注意:当Yn端口没使能时,端口为高电平)所以我们P25,P26,P27三个端口(A,B,C,C为高位哈)的电平分别为001时,则Y4为低电平,然后通过或非门,使得Y4C为高电平,锁存器使能,单片机P0~ P7引脚的数据就被透传过去了。~ (恭喜你,看到这里被我绕晕了,因为我突然发现我写的顺序有问题,我是从LED电路反着来分析的,你可以从这里倒着回去重新推一遍,会清晰很多。实在觉得看不懂,我的建议是忽略它,直接copy,然后背背代码,或者看视频,狗头)
LED代码书写
辣么我们怎么开始写我们神圣的LED电灯代码呢?
我们梳理一下前面的内容,想让LED灯点亮,首先我们要将单片机P0~P7这几个端口分别置低电平。然后将P25,P26,P27三个端口分别置为“低电平”,“低电平”,“高电平”,选通Y4引脚,通过或非门使得Y4C引脚置高电平,使能锁存器,将单片机P0 ~ P7几个信号引脚(锁存器的D0 ~D4引脚)的输出信号“透传”到另一端驱动LED点亮。
基础代码1
下面是代码书写部分:
//函数名:LED显示函数
//入口参数:要显示数值的十六进制数据,例如0XAA=1010 1010,L8~L1的顺序
//返回值:无
//函数功能:按照入口参数顺序将LED点亮,1为亮,0为灭
//函数示例:Led_Disp(0x0F);0x0F=00001111,故而是LED,1,2,3,4全部点亮。
void Led_Disp(unsigned char ucLed)
{
P0 = ~ucLed;//将输入数据取反,做好数据准备,1为亮,0为灭。
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去。注意这里的写法,涉及到位运算。& 0x1F 的作用是保持低五位不变,最高位全部置为0,|0x80的作用是保持低7位不变,使得最高位(P27引脚)置为高电平。
P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
}
基础代码示例工程1
完整工程代码:
#include "STC15F2K60S2.H"
//函数名:LED显示函数
//入口参数:要显示数值的十六进制数据,例如0XAA=1010 1010,L8~L1的顺序
//返回值:无
//函数功能:按照入口参数顺序将LED点亮,1为亮,0为灭
void Led_Disp(unsigned char ucLed)
{
P0 = ~ucLed;//将输入数据取反,做好数据准备,1为亮,0为灭。
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
}
void main()
{
unsigned char led=0x0F;
while(1)
{
Led_Disp(led);
}
//或者
// while(1)
// {
// Led_Disp(0X0F);
// }
}
在上面的代码中,我们加入了一个看似冗余的位运算操作,然而事实上,在51单片机这种运算性能有限的单片机中,我们需要考虑使用一些运算执行效率比较高的运算操作符,比如这里的位运算符是要比直接赋值要快速的多。同时,我们这样子还可以保证不影响P2系列引脚其他端口的信号电平。可以说,非常的nice。这一段代码是蚂蚁工厂教学视频推荐写的。
基础代码2
下面再讲一下,我现在对于这部分的代码重新封装了一下,对于提高代码书写效率以及代码可读性有一定的效果,但是代码执行效率还是直接使用上面的函数,使用较少的代码进行位操作效率较高,不用每一次点亮LED灯都需要自己去想一下8个LED对应的参数。有兴趣的同学可以考虑参考一下(狗头)。
#define LED1 0x01
#define LED2 0x02
#define LED3 0x04
#define LED4 0x08
#define LED5 0x10
#define LED6 0x20
#define LED7 0x40
#define LED8 0x80
// 函数名:LED初始化函数
// 入口参数:要点亮的LED编号
// 返回值:无
// 函数功能:根据入口参数设置对应的LED灯
// 使用示例:LED_init(LED1|LED4|LED6);即可让LED灯1,4,6分别点亮
void LED_display(unsigned char ucLed)
{
P0 = ~(ucLed & 0xFF); // 将要点亮的LED编号取反,做好数据准备,1为亮,0为灭。
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
P2 &= 0x1F; // 将打开的锁存器关闭,使任何一个锁存器都不打开
}
基础代码工程示例2
完整工程代码:
#include "STC15F2K60S2.H"
#define LED1 0x01
#define LED2 0x02
#define LED3 0x04
#define LED4 0x08
#define LED5 0x10
#define LED6 0x20
#define LED7 0x40
#define LED8 0x80
// 函数名:LED显示函数
// 入口参数:要显示的LED编号
// 返回值:无
// 函数功能:根据入口参数只显示对应的LED灯,其他LED灯熄灭
void LED_display(unsigned char ucLed)
{
P0 = ~ucLed; // 将要显示的LED编号取反,做好数据准备,1为亮,0为灭。
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
P2 &= 0x1F; // 将打开的锁存器关闭,使任何一个锁存器都不打开
}
void main()
{
unsigned char led=LED1|LED2|LED6;
while(1)
{
LED_display(led);
}
//或者
// while(1)
// {
// LED_display(LED1|LED2|LED6);
// }
}
我的这种写法在某些情况下可能确实简单,舒服,但是不一定就是最好的,在面对一些流水灯之类的任务时还是使用之前的位操作简单快捷。
下面给出一个流水灯工程示例,这种就是使用第一种方案的典型案例(因为一些小情况,所以我把函数名进行了一点点修改,注意识别):
几种流水灯模式代码
流水灯demo:
#include "STC15F2K60S2.H"
typedef enum
{
led_mode1,
led_mode2,
led_mode3,
led_mode4
}led_mode;
led_mode mode=led_mode1;
/*
模式1:流水灯从左到右
模式2:流水灯从右到左
模式3:流水灯从两边到中间
模式4:流水灯从中间到两边
*/
unsigned char ucled;//LED显示变量
unsigned char i=0;//循环变量
void led_proc()
{
switch(mode)
{
case led_mode1:
if(++i>=8)i=0;
ucled=0x01<<i;
break;
case led_mode2:
if(++i>=8)i=0;
ucled=0x80>>i;
break;
case led_mode3:
if(++i>=4)i=0;
ucled=(0x80>>i)|(0x01<<i);
break;
case led_mode4:
if(++i>=4)i=0;
ucled=(0x10<<i)|(0x08>>i);
break;
}
}
//函数名:定时器1初始化函数
//入口参数:无
//返回值:无
//函数功能:系统频率为 12.000MHz,设置定时时长为 1ms,
// 选择定时器 1,选择定时器模式为 16 位自动重载( 15 系列),
//选择定时器时钟为 12T( FOSC/12)
void Timer1Init_lhc(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x18; //设置定时初值
TH1 = 0xFC; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1; //使能定时器1中断
}
//函数名:LED显示函数
//入口参数:要显示数值的十六进制数据,例如0XAA=1010 1010,L8~L1的顺序
//返回值:无
//函数功能:按照入口参数顺序将LED点亮,1为亮,0为灭
void Led_Disp_lhc(unsigned char ucLed)
{
P0 = ~ucLed;//将输入数据取反,做好数据准备,1为亮,0为灭。
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
}
//函数名:关闭外设的初始化函数
//入口参数:无
//返回值:无
//函数功能:将LED、蜂鸣器和继电器全部关闭
void Cls_Peripheral_lhc(void)
{
// P0 = 0x00;//将外设全都灭掉的数据准备
// P2 = P2 & 0x1F | 0xA0; // 0x80,选通Y5,也就是外设的锁存器,将数据透传过去
// P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
//
// P0 = 0xFF;//将LED全都灭掉的数据准备
// P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
// P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
P2 = P2 & 0x1F | 0xA0; // 0x80,选通Y5,也就是外设的锁存器,将数据透传过去
P0 = 0x00;//将外设全都灭掉的数据准备
P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
P2 = P2 & 0x1F | 0x80; // 0x80,选通Y4,也就是LED的锁存器,将数据透传过去
P0 = 0xFF;//将LED全都灭掉的数据准备
P2 &= 0x1F; //将打开的锁存器关闭,使任何一个锁存器都不打开
}
//void main
void main()
{
Cls_Peripheral_lhc();//关闭外设的函数,将LED灯和蜂鸣器先关掉,可以按上面说的推一下这个函数关闭蜂鸣器和LED灯的原理
Timer1Init_lhc();
EA=1;//打开总中断
while(1);
}
int time_num=0;
void timer1()interrupt 3
{
if(++time_num>=500)
{
led_proc();
Led_Disp_lhc(ucled);
time_num=0;
}
}