【51单片机实验笔记】LED篇(一)单色LED的基本控制


前言

本节内容我们学习如何控制一颗LED,并简单控制它的行为:闪烁。并对多个LED实现流水灯跑马灯效果。

本节涉及到的封装源文件可在《模块功能封装汇总》中找到。

本节完整工程文件已上传GitHub仓库地址,欢迎下载交流!


硬件介绍

LED全称Light Emitting Diode,即发光二极管。其特点是功耗低、高亮度、抗振动、低光衰,属于冷光源

  • 响应时间短LED 的响应时间为纳秒级,而白炽灯的响应时间要达到毫秒级
  • 发光色谱宽。例如:砷化镓二极管发红光,磷化镓二极管发绿光,碳化硅二极管发黄光,氮化镓二极管发蓝光
  • 驱动电压低。通常工作电压为1.6 ~ 2.2V电流在5 ~ 20mA之间,亮度随电流变化。

封装形式

一般LED两种封装直插式贴片式


正负极判断

如何判断LED的正负极?仔细观察。

  • 直插式LED
    • 长脚正极短脚负极
    • 大电极片负极小电极片正极
  • 贴片式LED
    • 三角形顶点所对的为负极底边所对的为正极

原理图分析

LED电路符号如下:

最大的特点即具有单向导通性。由于单颗LED驱动电流比较小,过大的电流会烧毁LED,故一般在电路中串联阻值100Ω左右的电阻,称为限流电阻

由于开发板中已经帮我们焊好了外设和电路,所以我们不需要自己搭建电路,但需要去查看电路原理图(各个开发板不一样,但这没有关系),找到它所对应的控制引脚,这有利于我们的后续编程。

在我的开发板中,可以看到他设计的是所有 LED 共阳极(即 LED 的阳极接在一起),故当对应 IO 引脚置低电平LED 发光,高电平LED 熄灭。


软件实现

点亮一颗LED

方法一

#include <REGX52.H>
#define LED_PORT P2

void main(){
    
    
	//法一:直接定义整个P2口的电平
	LED_PORT = 0xfe; //1111 1110  D1灯点亮
	while(1); //使程序在这里死循环。
}

首先引入头文件 “REGX52.H”,这是51系列单片机的常用头文件,里面定义了各种寄存器,方便我们操作硬件。这里对P2口使用宏定义是需要注意的编程习惯,因为单片机的P0~P3口不同的硬件电路设计中连接的外设并不相同,并且,通用的引脚名并不能让阅读者立马知晓它的作用,利用宏定义可以增强代码的可读性可维护性。以后的代码中,我都会采用这样的代码风格。

其次,由原理图可知,8颗LED对应P2口的8位引脚,故若要使D1灯点亮,则需要将P2.0置低电平,其余全部置高电平,即1111 1110,常用16进制表示,即0xfe

最后,我们希望灯点亮之后保持这样的状态,而不是反复被点亮,所以应使程序一直停滞在main函数中,故采用一个死循环实现这样的目的。当然,如果没有写这样的死循环,你会发现实验现象并没有改变。但事实情况是,当执行完赋值语句后,程序会退出main函数,随后会不断的反复执行main函数,P2.0也会反复的置低电平,这当然不会有问题,但当任务复杂时,反复执行main函数会产生意想不到的错误。


方法二

#include <REGX52.H>
#define LED_PORT P2
//法二:定义P2口需要控制的位,并赋值电平
sbit LED_1 = LED_PORT^0; //D1灯

void main(){
    
    	
	//赋值低电平
	LED_1 = 0;
	while(1); //使程序在这里死循环。
}

在方法一中,我们定义了8个引脚的电平情况,但事实上这是没有必要的。因为我们P2口内部有上拉电阻,即引脚默认输出高电平,我们实际只需要将一个引脚置低电平即可。故方法二中,我们先定义了需要控制的引脚位,再进行赋值

注意

  • sbitC51扩展标识符,而非数据类型sbit声明的部分是编译器预处理的部分,是在函数没有编译之前必须完成的,所以必须写在main函数外且不能写进数组或是结构体循环调用
  • 位定义时,^代表的是位的位置,而不是异或运算符

LED闪烁

#include <REGX52.H>
#define LED_PORT P2
//typedef可以将一些复杂的关键字重命名
typedef unsigned char u8; //0-255,1字节
typedef unsigned int u16; //0-65535,2字节

void main(){
    
    
	//申明延时函数
	void delay(u16 msec);
	while(1){
    
    
		LED_PORT = 0xfe; // 1111 1110
		delay(50000); //大约450ms
		LED_PORT = 0xff; // 1111 1111
		delay(50000); 
	}
}

//延时函数 大致10微秒
void delay(u16 msec){
    
    
	while(msec--);
}

所有视频、动画、游戏的理论基础都是人眼的暂留效应。一般暂留时间大约为50ms,即一秒钟20帧以上的画面就会产生动态的视觉效果。单片机的晶振频率约为12MHz,计算其机器周期大约为1微秒,也就是说,两个连续的语句执行时间间隔相当短暂,以致于人眼根本无法分别LED灯的实际状态。

为了能够看到闪烁的效果,必须在点亮熄灭的语句之间加入延时函数延时函数的本质即让运行中的程序暂停一段时间(CPU空转一段时间)。经过Keil软件仿真50000次自减循环大致消耗450ms,这个时间间隔足以让人眼观察到灯的熄灭和点亮了。

当然你也可以自己去计算精确的延时时间,但这在本节中意义不大,我们仅需观察到闪烁现象即可。精确的延时需要配合定时器实现。


流水灯

我理解的流水灯是这种累加的效果
在这里插入图片描述
具体代码实现

#include <REGX52.H>
#define LED_PORT P2

typedef unsigned char u8;
typedef unsigned int u16;

void delay(u16 t){
    
    
	while(t--);
}

void main(){
    
    
	
	while(1){
    
    
		u8 i;
		LED_PORT = 0xfe; //1111 1110
		delay(50000);
		for(i=0;i<8;i++){
    
    
			LED_PORT <<= 1; //左移一位
			delay(50000); //延时450ms
		}
	}
}

实现的关键是左移运算符<<,每往左移1位,最低位补0,即表现为LED 逐一点亮的效果。


跑马灯

跑马灯应该是单个灯循环跑动的效果
在这里插入图片描述

方法一

#include <REGX52.H>
#define LED_PORT P2
#define DELAY_TIME 20000 //设置跑马灯时间间隔

typedef unsigned char u8;
typedef unsigned int u16;

void delay(u16 sec){
    
    
	while(sec--);
}

//法一:列举法,流水灯一共8种状态。
void ledTest_1(){
    
    
	LED_PORT = 0xfe; // 1111 1110
	delay(DELAY_TIME);
	LED_PORT = 0xfd; // 1111 1101
	delay(DELAY_TIME);
	LED_PORT = 0xfb; // 1111 1011
	delay(DELAY_TIME);
	LED_PORT = 0xf7; // 1111 0111
	delay(DELAY_TIME);
	LED_PORT = 0xef; // 1110 1111
	delay(DELAY_TIME);
	LED_PORT = 0xdf; // 1101 1111
	delay(DELAY_TIME);
	LED_PORT = 0xbf; // 1011 1111
	delay(DELAY_TIME);
	LED_PORT  = 0x7f; // 0111 1111
	delay(DELAY_TIME);
}

void main(){
    
    
	while(1){
    
    
		ledTest_1();
	}
}

方法一的思路就是把所有情况对应的十六进制都写出来,总共也8种状态,写呗。但缺陷也很明显,当LED数量多了之后,这种代码写起来就很痛苦了。


方法二

#include <REGX52.H>
#define LED_PORT P2
#define DELAY_TIME 20000 //设置跑马灯时间间隔

typedef unsigned char u8;
typedef unsigned int u16;

void delay(u16 sec){
    
    
	while(sec--);
}

//法二:使用左移和取反运算,配合循环实现
void ledTest_2(){
    
    
	u8 i;
	for(i=0;i<8;i++){
    
    
		LED_PORT = ~(0x01<<i); //将1左移i位后补0,取反,即第i+1位灯亮
		delay(50000); //延迟450ms
	}
}

void main(){
    
    
	while(1){
    
    
		ledTest_2();
	}
}

方法二的思路是,用一个循环实现每次将 0 移位。但直接应用左移运算符<<会存在一个问题:移位后低位自动补 0 ,这样低位的灯也会被点亮了。那怎么办?正面算法行不通,就尝试从反面去实现,即用一个循环实现每次将 1 移位,再将它按位取反~)即可。

:当我们只想改变某一位的状态不改变其他位的状态时,这是一个非常常用的位操作技巧

// 指定位, 置1
port |= 0x01<<i 
// 指定位, 置0
port &= ~(0x01<<i) 

方法三

#include <REGX52.H>
#include <INTRINS.H> // 定义了移位函数
#define LED_PORT P2
#define DELAY_TIME 20000 //设置跑马灯时间间隔

typedef unsigned char u8;
typedef unsigned int u16;

void delay(u16 sec){
    
    
	while(sec--);
}

//法三:使用左移函数_crol_()
void ledTest_3(){
    
    
	LED_PORT = _crol_(LED_PORT, 1);  //左移1位(跟左移运算符不同,高位循环补至低位)
	delay(50000); //延迟450ms
}

void main(){
    
    
	
	//法三:先初始化P2
	LED_PORT = 0xfe;
	delay(50000); //延迟450ms
	
	while(1){
    
    
		ledTest_3();
	}
}

方法三的思路是调用C51定义好的函数实现。首先引入头文件INTRINS.H,然后就可以使用左移函数_crol_()。当然也有相应的右移 函数_cror_()


LED常用函数封装

在复杂场景中,LED仅仅作为一个小模块来配合整个项目(比如指示灯)。每次对硬件编程是耗时耗力的。基于模块化硬件抽象的思想,有必要将LED功能封装,像系统库函数或是应用软件API 一样,对外提供接口,直接调用,这样方便以后快速构建项目

delay.h

#ifndef _DELAY_H_
#define _DELAY_H_

#include <regx52.h>

typedef unsigned char u8;
typedef unsigned int u16;

void delay_10us(u16);
void delay_ms(u16);

#endif

delay.c

#include "delay.h"
/** 
 **  @brief    通用函数
 **  @author   QIU
 **  @data     2023.08.23
 **/

/*-------------------------------------------------------------------*/

/**
 **  @brief   延时函数(10us)
 **  @param   t:0~65535,循环一次约10us
 **  @retval  无
 **/
void delay_10us(u16 t){
    
    
	while(t--);
}


/**
 **  @brief   延时函数(ms)
 **  @param   t:0~65535,单位ms
 **  @retval  无
 **/
void delay_ms(u16 t){
    
    
	while(t--){
    
    
		delay_10us(100);
	}
}

led.h

#define _LED_H_

#include "delay.h"


// 定义led引脚
#define LED_PORT P2


void led_on(u8);
void led_stream(u16);
void led_run(u16);

#endif

led.c

#include "led.h"

/** 
 **  @brief    LED控制程序
 **  @author   QIU
 **  @data     2023.08.23
 **/

/*-------------------------------------------------------------------*/


/**
 **  @brief  指定某个LED亮
 **  @param  pos: 位置(1~8)
 **  @retval 无
 **/
void led_on(u8 pos){
    
    
	LED_PORT &= ~(0x01<<(pos-1));
}


/**
 **  @brief  指定某个LED灭
 **  @param  pos: 位置(1~8)
 **  @retval 无
 **/
void led_off(u8 pos){
    
    
	LED_PORT |= 0x01<<(pos-1);
}


/**
 **  @brief   LED流水灯
 **  @param   time 延时时间
 **  @retval  无
 **/
void led_stream(u16 time){
    
    
	u8 i;
	for(i=0;i<8;i++){
    
    
		led_on(i+1);
		delay_10us(time);
	}
	
	// 全部熄灭
	for(i=0;i<8;i++){
    
    
		led_off(i+1);
	}
}


/**
 **  @brief   LED跑马灯
 **  @param   time 延时时间
 **  @retval  无
 **/
void led_run(u16 time){
    
    
	u8 i;
	for(i=0;i<8;i++){
    
    
		led_on(i+1);
		delay_10us(time);
		led_off(i+1);
	}
}

main.c

#include "led.h"
/** 
 **  @brief    仅提供一个demo
 **  @author   QIU
 **  @data     2023.08.23
 **/

/*-------------------------------------------------------------------*/

void main(){
    
    
	// 点亮一颗LED
	//led_on(2); 
	while(1){
    
    

		// 流水灯
		//led_stream(50000);  
		
		// 跑马灯
		led_run(50000);  
	}
}

多文件调用C语言内容,本文不再赘述。我只写了几种LED显示函数,各位童鞋可以自行扩充,改写,实现更加丰富的LED效果。


总结

单片机是软件和硬件的桥梁,这跟C语言在编程语言中的地位类似。一般是先了解硬件的原理硬件电路图之后,才开始软件的算法编程,实现相应的效果。路还很长,共勉。

猜你喜欢

转载自blog.csdn.net/m0_46500149/article/details/128376565