1.秒表项目简介
我们通过秒表这一项目完成通过定时器实现消抖的理解。
功能如下:
按独立按键1:开始/暂停计时。
按独立按键2:计时清零。
按独立按键3:把数据存至AT24C02。
按独立按键4:将AT24C02中的数据读出。
2.修改独立按键C文件:Key.c
先放出原先的Key.c文件内容:
1 |
|
我们可以看到,就像在《 STM51单片机学习记录(一)》中一样,可以通过在按键的前后加上Delay()函数实现消抖。但这样若长按按键会使程序陷入死循环。同时Delay()会对整个程序进行延时,在需要频繁检测按键变化时会占用大量的CPU资源。
因此,我们可以对Key.c文件进行改进,用定时器实现就是一个很好的思路。
我们的核心思路是复用定时中断,根据不同的定时时间触发不同的函数,以此实现类似“多个定时器”的效果。
下面是Key需要触发的中断,我们通过T0Count1进行计数,累计计数20次(每隔20ms),触发一次Key_Loop()函数,并将T0Count1清零重新计数:
1 | void Timer0_Routine() interrupt 1 |
Key_Loop()函数如下:
1 | void Key_Loop(){ |
核心逻辑是:通过判断20ms前(LastState)与20ms后(NowState)的电平变化来判断当前的按键状态。
State=0表示按键接通,具体的关系如下:
如果LastState=0,NowState=0,表示按键未按下;
如果LastState=0,NowState=!0,表示按键刚按下;
如果LastState=!0,NowState!=0,表示按键一直被按下;
如果LastState=!0,NowState=0,表示按键刚放开;
“按键刚放开的状态”(LastState=!0,NowState=0)可以代表按键已经完成按下这一动作,我们通过Key_GetState()函数获得瞬时捕获的State状态。Key_GetState()由原来的Key()改写而来:
1 | unsigned char Key_GetState() |
写到这里,Key_KeyNumber其实已经可以代表正确的消抖后的返回值了,我们再写一个Key()函数返回即可:
1 | unsigned char Key(){ |
需要注意的是,我们不能直接return Key_KeyNumber
,因为扫描是一直在进行的,每隔20msKey_KeyNumber就会更新一次。用一个例子来讲解:
独立按键1在20ms时刻被按下,在这之前Key_KeyNumber=0,按下后Key_KeyNumber=1。
若我们要判断60ms时按键的状态,按照实际情况,此时按键已经弹起,Key_KeyNumber应该=0,但由于再次调用时Key_KeyNumber没有重置,因此Key_KeyNumber仍然=1,与实际情况不符。
时刻 | 0 | 20 | 40 | 60 | 80 | 100 |
---|---|---|---|---|---|---|
Key_KeyNumber | 0 | 1 | 1 | 1 | 1 | 1 |
所以,我们每按一次就需要清零一次,可以用temp暂存一下,然后清零Key_KeyNumber,最终返回temp暂存的值。
2.修改数码管C文件:NiXie.c
先放出原先的NiXie.c文件内容:
1 |
|
我们用数码管来实现秒表的动态计时变化。在原先的NiXie.c中,我们为了消影会在段选结束后Delay(1),然后将P0置0。
出于同样的考量,我们选择用定时器实现同样的功能。
下面是NiXie需要触发的中断,我们通过T0Count2进行计数,累计计数2次(每隔2ms),触发一次NiXie_Loop()函数,并将T0Count2清零重新计数:
1 | void Timer0_Routine() interrupt 1 |
NiXie_Loop()函数如下:
1 | void NiXie_Loop(void){//在中断函数中每隔2ms执行一次 |
我们通过调用NiXie_Scan函数实现对每一位的扫描,在对应位上显示的数字由NiXie_Buf[i]控制。NiXie_Scan()由原来的NiXie()改写而来:
1 | void NiXie_Scan(unsigned char location,number) |
可以看到,我们去掉了Delay(1);
,同时将P0=0x00;
放到了函数开头,这是因为NiXie_Loop()通定时器实现了Delay的功能,而将P0=0x00;
放到函数开头可以实现函数执行结束后延时的无缝对接。
我们对NiXieTable[]也做了一定修改,具体如下:
1 | unsigned char NiXieTable[]={ |
NiXie_Buf[i]是我们想显示的数字与符号,我们先初始化:
1 | unsigned char NiXie_Buf[9]={0,10,10,10,10,10,10,10,10}; |
NiXie_Buf长度为9是因为NiXie(i,NiXie_Buf[i])中的i∈[1,8],下标范围为0~8方便直接调用。根据NiXieTable[]可知初始化为10表示清空数码管显示。
这样,我只需要再定义一个NiXie_SetBuf()函数用于设定第location位显示的数字为number,在主函数中调用NiXie_SetBuf()函数即可控制数码管显示。
1 | void NiXie_SetBuf(unsigned char location,number){ |
3.主函数编写
首先定义三个变量用于存放分、秒与百分秒:
1 | unsigned char Min,Sec,MinSec; |
再定义一个变量用于控制按键:
1 | unsigned char KeyNum; |
最后定义一个变量用于控制按键1的开始与暂停状态:
1 | unsigned char RunFlag; |
以上变量均为全局变量。
在主函数中,我们需要实现“真正”的计时功能,再次复用定时器中断:
1 | void Timer0_Routine() interrupt 1 |
Sec_Loop()函数用于控制时间的进位,具体代码如下:
1 | void Sec_Loop(){ |
自此,所有的函数组件已经全部搭建完毕,main函数的编写就没有任何困难了:
1 | void main() |