数码管计时器
希望大家也能关注一下新人up,以后会有更有意思的视频,也希望各位大佬提出些改进的建议。
正片开始,这是我们协会的作业,要求如下:
这个作业我个人觉得还是比较简单的,不过因为是个小白,所以有些知识点不太熟悉,所以也花了些时间,期间也在不断的试错,代码写得一般,希望有大佬能够帮忙指正。
我个人觉得这一部分是比较重要的,因为我用的是晋中科技的51单片机,所以它板子的按键比较容易误触,一开是我用的是比较简单的消抖算法如下:
void key_scan()
{
if (key == 0)//按键按下
{
Delay(10);//等待10ms
if(key == 0)//二次检测,防止误触
{
//执行代码
while(!key)//松手检测
{
}
}
}
}
这个算法计较简单,我看很多教程用的也是这种算法,但是我认为它有一点不太好,就是它需要等待松手,如果不松手,就会一直在循环里面,导致无法执行其他的程序。
如果是数码管显示,会使得每次按键后的刷新显示都会先熄灭再显示,就很不优雅,这种事情我肯定无法接受,所以就上网找有没有新的算法,让它的显示如吃了德芙一样丝滑(德芙打钱),还真让我找到了,B站UP主金善愚的视频,链接如下:独立按键进阶——非阻塞延时实现独立按键的消抖与按键识别方法
作为优雅的Ctrl c/v工程师,我好好“借鉴”了一下,以KEY1为例
#define Key_Delay_Time 100 //宏定义按键延迟的时间
unsigned char key1_lock_float; //定义KEY1自锁标志
unsigned char key1_cnt; //定义KEY1积累变量
unsigned char Key_Scan(void) //独立按键扫描函数,读取键值
{
unsigned char Key_Num = 0;
if(key1) //按键未按下
{
key1_lock_float = 0; //清除自锁标志
key1_cnt = 0; //清除计数标志
}
else if(!key1_lock_float) //!key1_lock_float && key1 ==0
{
key1_cnt++;//累计按键消抖延迟次数
if(key1_cnt > Key_Delay_Time)
{
key1_cnt = 0;//清零计数变量
key1_lock_float = 1;//自锁标志置1,防止按键多次触发
Key_Num = 1;//赋键值编码
}
}
return Key_Num;
}
我用我浅薄的理解来讲一下,
if(key1) //按键未按下或者有抖动
{
key1_lock_float = 0; //清除自锁标志
key1_cnt = 0; //清除计数标志
KEY1未按下时为高电平,或者当我们按下时,因为按键中间薄膜的抖动会有高低电平的转换,这一部分代码就是为了防止因为抖动造成电平变换导致误触
else if(!key1_lock_float) //!key1_lock_float && key1 ==0
{
key1_cnt++;//累计按键消抖延迟次数
if(key1_cnt > Key_Delay_Time)
{
key1_cnt = 0;//清零计数变量
key1_lock_float = 1;//自锁标志置1,防止按键多次触发
Key_Num = 1;//赋键值编码
}
原来KEY1未按下为高电平,else就是当它按下低电平的时候,之前我们未按下KEY1时,将key1_lock_float置零,!key1_lock_float后它就为1成立,接着执行下面的代码。
因为我们这个函数是放在while(1)里不断循环的,所以每调用函数,key1_cnt都会+1,当key1_cnt这个变量>Key_Delay_Time这个变量时,将我们的计数变量清零,自锁标志置1,防止多次触发,同给Key_Num赋值,方便后续switch()函数的使用。
Key_Delay_Time这个变量需要通过我们去预估我们程序需要多少时间去宏定义,同时这个值越小灵敏度就越高。
数码管显示的原理网上有很多,这里就不在这卖弄了,大家可以去B站大学找找看看,我这个代码也是参考
数码管动态显示编程之基于底层显示模块的应用层功能程序
这个视频写的,这里以一位数码管为例,八位数码管是一样的。
#define GPIO_DIC P0 //段码
#define GPIO_PLACE P2 //位码
/*编码表*/
unsigned char code leddate[] ={ 0x3f,//0
0x06,//1
0x5b,//2
0x4f,//3
0x66,//4
0x6d,//5
0x7d,//6
0x07,//7
0x7f,//8
0x6f,//9
0x77,//A
0x7c,//B
0x39,//C
0x5e,//D
0x79,//E
0x71,//F
0x40,//-
0x00//清空
};
unsigned char LEDBuf[] = {17,17,17,17,17,17,17,17};//数据显示缓冲区
unsigned char code PLACE_CODE[] = {0x00,0xff,0xfb,0xf7,0xf3,0xef,0xeb,0xe7,0xe3};//位选信号 0x00占位无具体位号
/*数码管动态显示显示*/
void Display()
{
unsigned char i;
switch(i)
{
case 0:GPIO_DIC =leddate[LEDBuf[0]]; //段选
GPIO_PLACE = PLACE_CODE[1]; //位选
Delay(1); //延迟
GPIO_DIC = 0x00; //消影
i++;
break;
}
}
编码表没什么好说的就给对应的管脚电平,然后二进制转十六进制就行了。
数据显示缓冲区就是方便我们以后如果需要显示不同的数据,直接去这里更改,方便后期维护与修改。
位选信号就是给锁存器对应的电平,让它显示第几位数码管,这里为了方便显示对应数码管,我在第一个位置,也就是数组的零号位,加了一个占位的数据,这样数组里数据的下标就和数码管的位数对上。
下面就是普通的调用,先进行段选,选择自己要选择的数据,我这里用的是数组嵌套
从里往外讲因为我将LEDBuf[0]赋17,所以
leddate[LEDBuf[0]] = leddate[17]
对应编码表里的第18个“清空”
然后进行位选,选择要第几位数码管显示,因为视觉暂留,我们延迟1ms,让它感觉是常亮的,其实是在不停的闪烁,最后消影,否则会有残影在数码管上,就很不优雅。
而这个i++,其实是为了动态显示用的,将这个程序放到while(1)里,它会不停循环,我们多加几个case ,每个case 都放入i++,这样当本次case 执行完因为i+1,所以它会进入到下一个case ,我们再最后一个case 将i初始化,使switch语句不断循环,实现动态显示。
由于本人技术一般,所以不不知道怎么使用for()循环进行流水灯的滚动显示,所以这里使用了定时器。
#include "reg52.h"
#include "bsp_key.h"
#include "bsp_timer.h"
void Timer0Init(void)
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
ET0 = 1; //使能定时器0中断
EA = 1; //使能中断总开关
TR0 = 1; //定时器0开始计时
}
/*中断函数*/
void Timer0_Routine() interrupt 1
{
static unsigned int c = 0;
TL0 = 0x18;
TH0 = 0xFC;
c++;
/*延迟300ms*/
if(c>=300)
{
c=0;
Dis_service3();
}
}
这里以晶振为12M的51单片机为例,TMOD |= 0X01 是将定时器设置为工作模式一。
然后设置TL0(低8位),TH0(高八位)的值,具体值计算如下:
晶体频率为12M,那么单片机的机械频率就为
12*(1/12M) = 1μs,
因为TH0和TL0总共能存储2^16 == 65536的数据,我们需要使定时器定时1ms,由前面的条件知道,要到1ms需要晶体发出1000次脉冲(1ms/1μs),但是1ms过去后,我们只往存储空间中存入1000,要到达65536,就需要先往存储空间里放入数据,当晶体发出1000次脉冲后,能够将存储空间填满,使TF0置1,所以
预先放入的值(后称预值) =(65536-1000)
同时我们需要将这个值分别放入TH0和TL0里,所以
TH0 = 预值 /256
TL0 = 预值 %256
256刚好为2^8,那么 预值 /256 就可以得到高八位,而 预值 %256 可以得到低八位。
接着就是将TF0置零,打开中断ET0和中断总开关EA0,开启定时器TR0
下面的函数实现就是当中断触发,将定时器初始化,当300ms后,使数码管显示。
接下来就是具体的实现
void main()
{
while(1)
{
key_service();
if(Num1 == 1)//进入数字设置模式
{
Dis_service2();
}
else if(Num1 == 0 &&Num3 == 1)//退出数字模式且按下KEY4,进入自检
{
Timer0Init();
}
else
{
Dis_service1();
}
Display();
}
}
void key_service()
{
switch(Key_Scan())
{
case 1:Num1++;if(Num1>1){Num1 = 0;}break;//Num1 == 1显示,Num1 ==0不显示
case 2:if(Num1 == 1){Num2++;}if(Num2>10){Num2 = 0;}break;//数码管显示Num2对应数字
case 3:if(Num1 == 1){Num2--;};if(Num2<1){Num2 = 10;}break;//数码管显示Num3对应数字
case 4:if(Num1 == 0){Num3++;};if(Num3>1){Num3 = 0;}break; //启动自检程序
}
}
/*KEY1程序 Num1 == 0不显示 Num2 == 1显示*/
void Dis_service1()
{
if(Num1 == 0)
{
LEDBuf[0] = 17;
LEDBuf[1] = 17;
LEDBuf[2] = 17;
LEDBuf[3] = 17;
LEDBuf[4] = 17;
LEDBuf[5] = 17;
LEDBuf[6] = 17;
LEDBuf[7] = 17;
}
else
{
LEDBuf[0] = 0;
LEDBuf[1] = 0;
LEDBuf[2] = 16;
LEDBuf[3] = 0;
LEDBuf[4] = 0;
LEDBuf[5] = 16;
LEDBuf[6] = 0;
LEDBuf[7] = 0;
}
}
/*KEY2 KEY3程序 显示Num2数字 按KEY2++ 按KEY3--*/
void Dis_service2()
{
LEDBuf[0] = Num2;
LEDBuf[1] = Num2;
LEDBuf[2] = 16;
LEDBuf[3] = Num2;
LEDBuf[4] = Num2;
LEDBuf[5] = 16;
LEDBuf[6] = Num2;
LEDBuf[7] = Num2;
}
/*KEY4程序 自检*/
void Dis_service3()
{
j++;
LEDBuf[0] = j;
LEDBuf[1] = j;
LEDBuf[2] = j;
LEDBuf[3] = j;
LEDBuf[4] = j;
LEDBuf[5] = j;
LEDBuf[6] = j;
LEDBuf[7] = j;
if(j>14)
{
j=17;
}
}
设置变量Num1 当KEY1按下,key_scan()函数返回1,进入switch函数,使Num1++
当Num>1时置零,当Num1 == 0时数码管不显示(退出数字显示模式)
当Num1 == 1时数码管显示00-00-00(进入数字显示模式)。
设置变量Num2 当KEY2按下,key_scan()函数返回2,进入switch函数
当KEY1 == 1(进入数字显示模式),显示数值,同时使Num2++
当Num2 >10时,将Num2置零(只显示0~A),实现0-A的循环。
当KEY1 == 0(退出数字显示模式),Num2不变化,实现只有在数字显示模式里才能对数码管显示的数值进行改变。
和任务二一样,将Num2++ 变为 Num2--,按键改为KEY3,key_scan()函数返回3,当Num2 <1时,将Num2置10。
设置变量Num3 当KEY4按下,key_scan()函数返回4,进入switch函数
当KEY1 == 0(退出数字显示模式),使Num3++
当KEY1 == 1(进入数字显示模式),Num3不变化
当 KEY1 == 0且 KEY3 == 1时,进入自检,够实现只有在退出数字显示模式里才能进行自检。