本文记录一段旅程–让一颗LED灯闪烁起来。
背景
我随手拿了一块普中的51单片机的开发板,上面恰好有很多led灯,我想让其中一个按我想象的方式闪烁起来。
硬件驱动器
虽然开发板上已经有了硬件驱动,我并不需要走这一段路,但对于旅行来说,更引人入胜的或许是沿途的一簇花草,一片树叶,终点也只不过是下一段旅途的起点。
先来瞧一瞧开发板上的LED驱动器罢。8个LED被接成了共阳极的形式,LED的阴极接了470R的排阻,另一端引到单片机的IO口。
我不想给自己增添麻烦,我决定只观察D1二极管,我需要另外画一个更简单的电路来分析。
上手之前先查查攻略,看看大家怎么说。
红色发光二极管的正向导通压降一般是1.8-2.2V,工作电流一般是5-20mA,用于指示灯的话一般10mA就比较亮了。
回过头来看这个电路,当VCC=5V,b点的电位为0V的时候,主干线上的电流与a点的电位是像下面这样子的。实际分析的电路设计参数与网上的描述有所差异,但看起来感觉应该可以,实际效果好与不好就在实际中大锤八十小锤一百地验证看看。
发光二极管压降(自变量) | Va(因变量) | 主干线电流(因变量) |
---|---|---|
1.8V | 3.2V | 6.8mA |
2.2V | 2.8V | 5.9mA |
针对这个感觉可以就可以的二极管驱动器,不讨论单片机的噪声容限,不讨论能让led亮起来的电压范围,我们对控制电压进行数字抽象,想让二极管亮起来的时候就在b点送入逻辑0,想让二极管熄灭的时候就在b点送入逻辑1。
软件驱动器
控制LED灯的单片机端口是P2.0端口。要点亮LED灯就往P2.0端口寄存器写0,要熄灭就写1。这是一个非常简单的操作,但也值得去摸一摸。
#include "reg52.h"
sbit LED1=P2^0;
void main(){
LED1=0; //点亮
//LED1=1; //熄灭
while(1){
}
}
灯亮了,现在可以顺手看一下硬件驱动器部分的分析与实际情况的对比。使用万用表简单测量一下VCC,a点和b点的电位,下面的图是b点的电位,图太多会影响观感,就用表格看看好了。
VCC | Ea | Eb | Vled=VCC-Ea | I |
---|---|---|---|---|
5.15V | 3.258V | 0.302V | 1.892V | 2.97mA |
实际开发板上使用的限流电阻标称值是是1k,测量下来996R,所以主干线电流是2.97mA,但是看起来LED灯的亮度感觉就很行,完全依靠经验与度娘不可取,不同厂家、不同型号的器件有差异,多看手册多动手才是避坑的秘籍。
像航障灯那样闪烁
我想让它像高楼大厦楼顶的航空障碍灯那样闪烁,粗略地查了一下,我觉得40闪/分比较好看。40闪/分意味着每1.5S发光管要切换一次状态,那么LED的控制信号应该是下面这样子的,1和0每次的保持时间为1.5s。
接下来开始设计程序,目标是在单片机的P2.0口上产生上面的无限循环周期逻辑。开发板有一个demo程序,忍不住ctrl+c,ctrl+v。但是demo中的延时函数void delay_10us(u16 ten_us)并不能直接实现1.5s延时,因为ten_us的最大值只能为65535,假定这个函数是精确的单位10us延时,当ten_us=65535时,延时时间为655.35ms,小于需要的1500ms,我们可以简单粗暴地堆叠3个延时为500ms的延时单元来处理这个问题。
while循环语句可以让一个过程无限循环下去,所以我们只需要实现一个周期:点亮保持–>延时1.5s–>熄灭保持–>延时1.5s,这是一个简单的逻辑。
#include "reg52.h"
typedef unsigned int uint16_t;
sbit LED1=P2^0;
void delay_10us(uint16_t ten_us){
while(ten_us--);
}
void main(){
while(1){
LED1=0; //点亮
delay_10us(51176); //11.0592MHz,延时500ms
delay_10us(51176);
delay_10us(51176);
LED1=1; //熄灭
delay_10us(51176);
delay_10us(51176);
delay_10us(51176);
}
}
出于各种原因,通过理论推算延时函数所需要传入的值有些许复杂,耍个花招,通过实际调试来确定数值。修改调试环境中晶振值和开发板上的值相同,这里是11.0592MHz,然后启动软件模拟调试,运行到光标所在行,感觉可以就可以。
想怎么闪就怎么闪
冲一杯茶,歇一歇,收拾收拾心情,我不知道是从哪架飞机翅膀上看见的,它那个灯隔段时间闪一下,也有隔段时间闪两下的,我觉得隔段时间闪两下的灯比较漂亮,就这么决定了。
为了让时间的计算更加简单准确,配置过程更快捷,我决定用定时器的办法来抵达这个目标,因为里面有两种不同的时间,也或许有更多不同长度的时间片段。
我们构造一个趁手的工具–虚拟定时器virtualTimer,它由一个递减计数器,重载时间和重载标志组成,如果定时器的重载标志为1,那么它就是一个周期定时器,反之它是一个单次定时器。
虚拟定时器由一个硬件定时器驱动,硬件定时每滴答作响一次,虚拟定时器的递减计数器就减一个数,所以我们需要精心地设计硬件定时器的滴答时间和虚拟定时器的重载值来配置实际时间。
在虚拟定时器的递减计数器倒计时的过程中,我们通过指定在不同的计数区间led的不同状态来实现亮灭的随意操控,也就是想怎么闪就怎么闪。
最后付上代码
#include "reg52.h"
#define TIMER0_MIDDLE_VALUE 9174 //11.0592MHz晶振,10ms溢出一次,定时器数9174个数
typedef unsigned int uint16_t;
typedef struct virtualTimer { //虚拟定时器,定时时间与使用的定时器定时间隔有关系
unsigned short counter;
unsigned short reloadTime;
unsigned short reloadFlag;
}virtualTimer_t;
sbit LED1=P2^0;
virtualTimer_t LEDStateIndicatorTimer = {0,450,1};
void main(){
TMOD = 0x01; //T0方式1->16位不自动重装定时器
TMOD &= ~(1<<2); //T0定时器模式
TMOD &= ~(1<<3); //T0启停仅受TCON的TR0控制
TH0 = (65535 - TIMER0_MIDDLE_VALUE) / 256;
TL0 = (65535 - TIMER0_MIDDLE_VALUE) % 256;
ET0 = 1; //T0中断使能
EA = 1; //总中断使能
TR0 = 1; //T0使能
while(1){
if(LEDStateIndicatorTimer.counter > 50 ||
(LEDStateIndicatorTimer.counter < 35 && LEDStateIndicatorTimer.counter >= 15))
LED1=1; //熄灭
else
LED1=0; //点亮
}
}
void T0_interrupt(void) interrupt 1 {
TH0 = (65535 - TIMER0_MIDDLE_VALUE) / 256; //重装初值
TL0 = (65535 - TIMER0_MIDDLE_VALUE) % 256;
if(LEDStateIndicatorTimer.counter != 0)
--LEDStateIndicatorTimer.counter;
if(!LEDStateIndicatorTimer.counter && LEDStateIndicatorTimer.reloadFlag)
LEDStateIndicatorTimer.counter = LEDStateIndicatorTimer.reloadTime;
}