多按鍵狀態機的實現


1. 簡單按鍵檢測

記得開始學習單片機的時候,寫的按鍵掃描是這樣的:

if(KEY1 == 0)
{
    delay_ms(20);
    if(KEY1 == 0)
    {
        while(KEY1 == 0);
        // 按鍵按下處理代碼
    }
}

一看,有個20ms消除抖動時間,就是說我要在這里死等20ms,還有等待按鍵釋放,我就是不放,你能怎么樣?沒辦法只能做超時。那我想做長按1s呢?細思極恐,對於實際項目上的應用來說是很糟糕的事情,這不僅會拖慢你整個系統,還會出現,多個按鍵有時檢測不到的問題。有沒有更好的辦法來實現呢?答案是肯定的,想想,如果這個20ms的延時用定時器來做,不就可以了嗎!!!

2. 狀態機

首先我們得了解什么是狀態機?這個當然是問度娘了!!!
狀態機可歸納為4個要素,即現態、條件、動作、次態。這樣的歸納,主要是出於對狀態機的內在因果關系的考慮。"現態"和"條件"是,"動作"和"次態"是。詳解如下:
現態:是指當前所處的狀態。
條件:當一個條件被滿足,將會觸發一個動作,或者執行一次狀態的遷移。
動作:條件滿足后執行的動作。動作執行完畢后,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足后,也可以不執行任何動作,直接遷移到新狀態。
次態:條件滿足后要遷往的新狀態。"次態"是相對於"現態"而言的,"次態"一旦被激活,就轉變成新的"現態"了。
有了理論的支撐,有沒有發現,狀態機這種機制,其實可以運行到很多場景的啦,不僅僅局限於按鍵。

3. 按鍵枚舉和結構體

按鍵的狀態可以分為:未按、按下、長按、抬起四種。當然你也可以按自己需求去分。廢話不多說,直接上代碼分析。

typedef enum _KEY_STATUS_LIST
{
	KEY_NULL = 0x00,
	KEY_SURE = 0x01,
	KEY_UP   = 0x02,
	KEY_DOWN = 0x04,
	KEY_LONG = 0x08,
}KEY_STATUS_LIST;

我這里定義了一個按鍵狀態的枚舉,包含5個元素,KEY_NULL表示無動作,KEY_SURE表示確認狀態,KEY_UP表示按鍵抬起,
KEY_DOWN 表示按鍵按下,KEY_LONG表示長按。

typedef enum _KEY_LIST
{
	KEY0,
	KEY1,
	KEY2,
	KEY_NUM,
}KEY_LIST;

再來一個枚舉,這里列出你的按鍵,KEY_NUM可以自動統計你按鍵個數,這里KEY_NUM的值為3,至於為什么我就不多說了,自己問度娘。為什么這里要用枚舉?這里拋個磚,繼續往下看。

typedef struct _KEY_COMPONENTS
{
    uint8_t KEY_SHIELD;       //按鍵屏蔽0:屏蔽,1:不屏蔽
	uint8_t KEY_COUNT;        //按鍵長按計數
    uint8_t KEY_LEVEL;        //虛擬當前IO電平,按下1,抬起0
    uint8_t KEY_DOWN_LEVEL;   //按下時IO實際的電平
    uint8_t KEY_STATUS;       //按鍵狀態
    uint8_t KEY_EVENT;        //按鍵事件
    uint8_t (*READ_PIN)(void);//讀IO電平函數
}KEY_COMPONENTS;
extern KEY_COMPONENTS Key_Buf[KEY_NUM];

KEY_SHIELD按鍵屏蔽用的,0表示按鍵不使用,1表示使用;KEY_COUNT長按計數器,好比秒表,按開始然后開始計數了;KEY_LEVEL虛擬按鍵按下的電平,KEY_DOWN_LEVEL,實際按鍵按下的IO電平,這兩個變量,主要是為了函數封裝進行統一,比如你一個按鍵按下高電平,一個按下低電平,我不管這么多,反正我就和你KEY_DOWN_LEVEL值進行比較,相等我就認為你按下,然后把KEY_LEVEL置位,相反就清零;KEY_STATUS就是我們說的按鍵狀態了,它負責記錄某一時刻按鍵狀態;KEY_EVENT表示按鍵事件,我這里分了3個事件,有按下、抬起和長按。(*READ_PIN)是一個函數指針變量,需要把你讀IO的函數接口給它。
最后別忘了,用這個結構體定義變量(這里只是聲明哦),有幾個按鍵就定義幾個結構類型變量。發現沒有我們這里用到了KEY_NUM好處一,按鍵增加也不需要改動。

4. 按鍵IO函數

首先得讀IO口的電平吧。

static uint8_t KEY0_ReadPin(void)
{
    return _KEY0;
}

static uint8_t KEY1_ReadPin(void)
{
    return _KEY1;
}

static uint8_t KEY2_ReadPin(void)
{
    return _KEY2;
}

這個很簡單,就是把你的IO口電平返回來給我就可以了。可以根據自己單片機去實現。有了這幾個函數不就可以定義的結構體類型變量了嗎。

KEY_COMPONENTS Key_Buf[KEY_NUM] = {
{1,0,0,0,KEY_NULL,KEY_NULL,KEY0_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY1_ReadPin},
{1,0,0,0,KEY_NULL,KEY_NULL,KEY2_ReadPin},
};

這個就不多說了,對着上面結構體說明看就知道了,我這里按鍵按下的都是低電平。
真正的按鍵IO電平獲取函數在這里

static void Get_Key_Level(void)
{
    uint8_t i;
    
    for(i = 0;i < KEY_NUM;i++)
    {
        if(Key_Buf[i].KEY_SHIELD == 0)
            continue;
        if(Key_Buf[i].READ_PIN() == Key_Buf[i].KEY_DOWN_LEVEL)
            Key_Buf[i].KEY_LEVEL = 1;
        else
            Key_Buf[i].KEY_LEVEL = 0;
    }
}

這個函數主要是實現封裝,兩步走,先判斷按鍵是否使能,每一個按鍵IO電平。如果我添加按鍵這里要改動嗎?完全不需要動。

5. 按鍵狀態機

重點來了,准備了那么多,終於可以上按鍵狀態機代碼實現了。

void ReadKeyStatus(void)
{
    uint8_t i;
	
    Get_Key_Level();
	
    for(i = 0;i < KEY_NUM;i++)
    {
        switch(Key_Buf[i].KEY_STATUS)
        {
            //狀態0:沒有按鍵按下
            case KEY_NULL:
                if(Key_Buf[i].KEY_LEVEL == 1)//有按鍵按下
                {
                    Key_Buf[i].KEY_STATUS = KEY_SURE;//轉入狀態1
					Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //狀態1:按鍵按下確認
            case KEY_SURE:
                if(Key_Buf[i].KEY_LEVEL == 1)//確認和上次相同
                {
                    Key_Buf[i].KEY_STATUS = KEY_DOWN;//轉入狀態2
					Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
                    Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //狀態2:按鍵按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
					Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
            //狀態3:按鍵連續按下
            case KEY_LONG:
                if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
					Key_Buf[i].KEY_EVENT = KEY_NULL;
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
                {
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
                    Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;
        }
	}
}

這個函數就是獲取當前所有按鍵狀態,每20ms(用定時器定時)調用一次,就可以了。

            //狀態0:沒有按鍵按下
            case KEY_NULL:
                if(Key_Buf[i].KEY_LEVEL == 1)//有按鍵按下
                {
                    Key_Buf[i].KEY_STATUS = KEY_SURE;//轉入狀態1
					Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

這里拿一個按鍵做說明,首先進來先獲取按鍵IO電平,按鍵開始狀態從KEY_NULL開始(現態),此時按鍵按下(條件),轉入KEY_SURE次態),第一步完成退出;

            //狀態1:按鍵按下確認
            case KEY_SURE:
                if(Key_Buf[i].KEY_LEVEL == 1)//確認和上次相同
                {
                    Key_Buf[i].KEY_STATUS = KEY_DOWN;//轉入狀態2
					Key_Buf[i].KEY_EVENT = KEY_DOWN;//按下事件
                    Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

定時器到20ms,剛好去抖動,進來就從新的狀態KEY_SURE開始了,再判斷當前按鍵是否還是按下的,如果沒有按下,那就返回KEY_NULL,說明上次是干擾,如果按鍵是按下的,那就進入真正按下的狀態KEY_DOWN,同時我們給KEY_EVENT事件賦值,標識我觸發了按下事件,KEY_COUNT清零為長按計數做准備,此時退出,你就可以在外面判斷事件這個變量決定是否要執行什么任務(動作);

            //狀態2:按鍵按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
					Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

又過20ms進來,這次是從狀態KEY_DOWN開始,判斷按鍵是否釋放,如果釋放就轉入狀態KEY_NULL,同時標記事件為KEY_UP,如果沒被釋放,我們會進行計數,同時清空數據標志,其它不變,因為我們的條件沒有滿足,不進行狀態遷移,需要注意每次進來沒有變化就清空事件,不然出去你判斷的標記又觸發動作了;

            //狀態2:按鍵按下
            case KEY_DOWN:
                if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
                {
                    Key_Buf[i].KEY_STATUS = KEY_LONG;//轉入狀態3
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
					Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

同樣20ms后進來,假設長按,還是從KEY_DOWN開始,計數值累加,當累加到設定值,比如25時,也就是500ms,滿足長按條件,遷移到長按狀態KEY_LONG,標記事件為KEY_LONG

            //狀態3:按鍵連續按下
            case KEY_LONG:
                if(Key_Buf[i].KEY_LEVEL != 1)//按鍵釋放,端口高電平
                {
                    Key_Buf[i].KEY_STATUS = KEY_NULL;//轉入狀態0
                    Key_Buf[i].KEY_EVENT = KEY_UP;//松開事件
					Key_Buf[i].KEY_EVENT = KEY_NULL;
                }
                else if((Key_Buf[i].KEY_LEVEL == 1) 
                && (++Key_Buf[i].KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超過KEY_LONG_DOWN_DELAY沒有釋放
                {
                    Key_Buf[i].KEY_EVENT = KEY_LONG;//長按事件
                    Key_Buf[i].KEY_COUNT = 0;//計數器清零
                }
                else
                {
                    Key_Buf[i].KEY_EVENT = KEY_NULL;//空事件
                }
                break;

20ms后,進入狀態KEY_LONG,一樣判斷是否釋放,釋放就進入KEY_NULL狀態,標記松開事件,否則繼續判斷是否為長按。我這里做的是一直按下,每500ms就返回一個長按事件。
這就是整狀態機實現,這個函數也不需要我們修改,按鍵增加或減少根本不影響我這個函數的實現,這就是我前面做一堆枚舉、結構體、函數封裝的好處,好處二

6. 按鍵處理函數

前面我們做了一堆標記事件,就是給我們主函數做處理的,下面是我的按鍵處理,放在主循環里就可以了。

void Task_KEY_Scan(void)
{
	ReadKeyStatus();
	
	if(Key_Buf[KEY0].KEY_EVENT == KEY_UP)
	{
		printf("KEY0 Down\n");
	}
	else if(Key_Buf[KEY0].KEY_EVENT == KEY_LONG)
	{
		printf("KEY0 Long Down\n");
	}
	
	if(Key_Buf[KEY1].KEY_EVENT == KEY_UP)
	{
		printf("KEY1 Down\n");
	}
	else if(Key_Buf[KEY1].KEY_EVENT == KEY_LONG)
	{
		printf("KEY1 Long Down\n");
	}
	
	if(Key_Buf[KEY2].KEY_EVENT == KEY_UP)
	{
		printf("KEY2 Down\n");
	}
	else if(Key_Buf[KEY2].KEY_EVENT == KEY_LONG)
	{
		printf("KEY2 Long Down\n");
	}
}

這個就比較簡單,就一直循環判斷每個事件的標記,你需要哪種事件就做對比,出現這個標記就執行你的代碼。有沒有發現,我這里可以清楚的知道是哪個按鍵標記的事件,這就是那個枚舉的終極用處,好處三
到這里就結束了,有什么不足的歡迎大家指出留言下方,有好的建議也可以提出,大家共同學習。好東西分享給大家,后面也會更新一些實用的東西給大家,喜歡就點關注哦!!!_


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM