完整教程下載地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980
第19章 STM32H7的GPIO應用之按鍵FIFO
本章教程為大家介紹STM32H7的GPIO應用之按鍵FIFO,這個方案已經在實際項目中千錘百煉,比較實用。
19.1 初學者重要提示
19.2 按鍵硬件檢測
19.3 按鍵FIFO的驅動設計
19.4 按鍵板級支持包(bsp_key.c)
19.5 按鍵FIFO驅動移植和使用
19.6 實驗例程設計框架
19.7 實驗例程說明(MDK)
19.8 實驗例程說明(IAR)
19.9 總計
19.1 初學者重要提示
- 學習本章節前,務必保證已經學習了第15,16和17章。
- 按鍵FIFO驅動擴展和移植更簡單,組合鍵也更好用。支持按下、彈起、長按和組合鍵。
19.2 按鍵硬件設計
V7開發板有三個獨立按鍵和一個五向搖桿,下面是三個獨立按鍵的原理圖:
注意,K1(S1)、K2(S2)和K3(S3)按鍵的上拉電阻是接在5V電壓上,因為這三個按鍵被復用為PS/2鍵盤鼠標接口,而PS/2是需要5V供電的(注,V5和V6開發板做了PS/2復用,而V7沒有使用,這里只是為了兼容之前的板子)。實際測試,K1、K2、K3按鍵和PS/2鍵盤是可以同時工作的。
下面是五向搖桿的原理圖:
通過這個硬件設計,有如下兩個知識點為大家做介紹:
19.2.1 硬件設計
按鍵和CPU之間串聯的電阻起保護作用。按鍵肯定是存在機械抖動的,開發板上面的硬件沒有做硬件濾波處理,即使設計了硬件濾波電路,軟件上還是需要進行濾波。
- 保護GPIO,避免軟件錯誤將IO設置為輸出,如果設置為低電平還好,如果設置輸出的是高電平,按鍵按下會直接跟GND(低電平)連接,從而損壞MCU。
- 保護電阻也起到按鍵隔離作用,這些GPIO可以直接用於其它實驗。
19.2.2 GPIO內部結構分析按鍵
詳細的GPIO模式介紹,請參考第15章的15.3小節,本章僅介紹輸入模式。下面我們通過一張圖來簡單介紹GPIO的結構。
紅色的線條是GPIO輸入通道的信號流向,作為按鍵檢測IO,這些需要配置為浮空輸入。按鍵已經做了5V上拉,因此GPIO內部的上下拉電阻都選擇關閉狀態。
19.3 按鍵FIFO的驅動設計
bsp_key按鍵驅動程序用於掃描獨立按鍵,具有軟件濾波機制,采用FIFO機制保存鍵值。可以檢測如下事件:
- 按鍵按下。
- 按鍵彈起。
- 長按鍵。
- 長按時自動連發。
我們將按鍵驅動分為兩個部分來介紹,一部分是FIFO的實現,一部分是按鍵檢測的實現。
bsp_key.c 文件包含按鍵檢測和按鍵FIFO的實現代碼。
bsp.c 文件會調用bsp_InitKey()初始化函數。
bsp.c 文件會調用bsp_KeyScan按鍵掃描函數。
bsp_timer.c 中的Systick中斷服務程序調用 bsp_RunPer10ms。
中斷程序和主程序通過FIFO接口函數進行信息傳遞。
函數調用關系圖:
19.3.1 按鍵FIFO的原理
FIFO是First Input First Output的縮寫,先入先出隊列。我們這里以5個字節的FIFO空間進行說明。Write變量表示寫位置,Read變量表示讀位置。初始狀態時,Read = Write = 0。
我們依次按下按鍵K1,K2,那么FIFO中的數據變為:
如果Write!= Read,則我們認為有新的按鍵事件。
我們通過函數bsp_GetKey讀取一個按鍵值進行處理后,Read變量變為1。Write變量不變。
我們繼續通過函數bsp_GetKey讀取3個按鍵值進行處理后,Read變量變為4。此時Read = Write = 4。兩個變量已經相等,表示已經沒有新的按鍵事件需要處理。
有一點要特別的注意,如果FIFO空間寫滿了,Write會被重新賦值為0,也就是重新從第一個字節空間填數據進去,如果這個地址空間的數據還沒有被及時讀取出來,那么會被后來的數據覆蓋掉,這點要引起大家的注意。我們的驅動程序開辟了10個字節的FIFO緩沖區,對於一般的應用足夠了。
設計按鍵FIFO主要有三個方面的好處:
- 可靠地記錄每一個按鍵事件,避免遺漏按鍵事件。特別是需要實現按鍵的按下、長按、自動連發、彈起等事件時。
- 讀取按鍵的函數可以設計為非阻塞的,不需要等待按鍵抖動濾波處理完畢。
- 按鍵FIFO程序在嘀嗒定時器中定期的執行檢測,不需要在主程序中一直做檢測,這樣可以有效地降低系統資源消耗。
19.3.2 按鍵FIFO的實現
在bsp_key.h 中定了結構體類型KEY_FIFO_T。這只是類型聲明,並沒有分配變量空間。
#define KEY_FIFO_SIZE 10 typedef struct { uint8_t Buf[KEY_FIFO_SIZE]; /* 鍵值緩沖區 */ uint8_t Read; /* 緩沖區讀指針1 */ uint8_t Write; /* 緩沖區寫指針 */ uint8_t Read2; /* 緩沖區讀指針2 */ }KEY_FIFO_T;
在bsp_key.c 中定義s_tKey結構變量, 此時編譯器會分配一組變量空間。
static KEY_FIFO_T s_tKey; /* 按鍵FIFO變量,結構體 */
一般情況下,只需要一個寫指針Write和一個讀指針Read。在某些情況下,可能有兩個任務都需要訪問按鍵緩沖區,為了避免鍵值被其中一個任務取空,我們添加了第2個讀指針Read2。出廠程序在bsp_Idle()函數中實現的按K1K2組合鍵截屏的功能就使用的第2個讀指針。
當檢測到按鍵事件發生后,可以調用 bsp_PutKey函數將鍵值壓入FIFO。下面的代碼是函數的實現:
/* ********************************************************************************************************* * 函 數 名: bsp_PutKey * 功能說明: 將1個鍵值壓入按鍵FIFO緩沖區。可用於模擬一個按鍵。 * 形 參: _KeyCode : 按鍵代碼 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_PutKey(uint8_t _KeyCode) { s_tKey.Buf[s_tKey.Write] = _KeyCode; if (++s_tKey.Write >= KEY_FIFO_SIZE) { s_tKey.Write = 0; } }
這個bsp_PutKey函數除了被按鍵檢測函數調用外,還可以被其他底層驅動調用。比如紅外遙控器的按鍵檢測,也共用了同一個按鍵FIFO。遙控器的按鍵代碼和主板實體按鍵的鍵值統一編碼,保持鍵值唯一即可實現兩套按鍵同時控制程序的功能。
應用程序讀取FIFO中的鍵值,是通過bsp_GetKey函數和bsp_GetKey2函數實現的。我們來看下這兩個函數的實現:
/* ********************************************************************************************************* * 函 數 名: bsp_GetKey * 功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。 * 形 參: 無 * 返 回 值: 按鍵代碼 ********************************************************************************************************* */ uint8_t bsp_GetKey(void) { uint8_t ret; if (s_tKey.Read == s_tKey.Write) { return KEY_NONE; } else { ret = s_tKey.Buf[s_tKey.Read]; if (++s_tKey.Read >= KEY_FIFO_SIZE) { s_tKey.Read = 0; } return ret; } } /* ********************************************************************************************************* * 函 數 名: bsp_GetKey2 * 功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。獨立的讀指針。 * 形 參: 無 * 返 回 值: 按鍵代碼 ********************************************************************************************************* */ uint8_t bsp_GetKey2(void) { uint8_t ret; if (s_tKey.Read2 == s_tKey.Write) { return KEY_NONE; } else { ret = s_tKey.Buf[s_tKey.Read2]; if (++s_tKey.Read2 >= KEY_FIFO_SIZE) { s_tKey.Read2 = 0; } return ret; } }
返回值KEY_NONE = 0, 表示按鍵緩沖區為空,所有的按鍵時間已經處理完畢。按鍵的鍵值定義在 bsp_key.h文件,下面是具體內容:
typedef enum { KEY_NONE = 0, /* 0 表示按鍵事件 */ KEY_1_DOWN, /* 1鍵按下 */ KEY_1_UP, /* 1鍵彈起 */ KEY_1_LONG, /* 1鍵長按 */ KEY_2_DOWN, /* 2鍵按下 */ KEY_2_UP, /* 2鍵彈起 */ KEY_2_LONG, /* 2鍵長按 */ KEY_3_DOWN, /* 3鍵按下 */ KEY_3_UP, /* 3鍵彈起 */ KEY_3_LONG, /* 3鍵長按 */ KEY_4_DOWN, /* 4鍵按下 */ KEY_4_UP, /* 4鍵彈起 */ KEY_4_LONG, /* 4鍵長按 */ KEY_5_DOWN, /* 5鍵按下 */ KEY_5_UP, /* 5鍵彈起 */ KEY_5_LONG, /* 5鍵長按 */ KEY_6_DOWN, /* 6鍵按下 */ KEY_6_UP, /* 6鍵彈起 */ KEY_6_LONG, /* 6鍵長按 */ KEY_7_DOWN, /* 7鍵按下 */ KEY_7_UP, /* 7鍵彈起 */ KEY_7_LONG, /* 7鍵長按 */ KEY_8_DOWN, /* 8鍵按下 */ KEY_8_UP, /* 8鍵彈起 */ KEY_8_LONG, /* 8鍵長按 */ /* 組合鍵 */ KEY_9_DOWN, /* 9鍵按下 */ KEY_9_UP, /* 9鍵彈起 */ KEY_9_LONG, /* 9鍵長按 */ KEY_10_DOWN, /* 10鍵按下 */ KEY_10_UP, /* 10鍵彈起 */ KEY_10_LONG, /* 10鍵長按 */ }KEY_ENUM;
必須按次序定義每個鍵的按下、彈起和長按事件,即每個按鍵對象(組合鍵也算1個)占用3個數值。我們推薦使用枚舉enum, 不用#define的原因:
- 便於新增鍵值,方便調整順序。
- 使用{ } 將一組相關的定義封裝起來便於理解。
- 編譯器可幫我們避免鍵值重復。
我們來看紅外遙控器的鍵值定義,在bsp_ir_decode.h文件。因為遙控器按鍵和主板按鍵共用同一個FIFO,因此在這里我們先貼出這段定義代碼,讓大家有個初步印象。
/* 定義紅外遙控器按鍵代碼, 和bsp_key.h 的物理按鍵代碼統一編碼 */ typedef enum { IR_KEY_STRAT = 0x80, IR_KEY_POWER = IR_KEY_STRAT + 0x45, IR_KEY_MENU = IR_KEY_STRAT + 0x47, IR_KEY_TEST = IR_KEY_STRAT + 0x44, IR_KEY_UP = IR_KEY_STRAT + 0x40, IR_KEY_RETURN = IR_KEY_STRAT + 0x43, IR_KEY_LEFT = IR_KEY_STRAT + 0x07, IR_KEY_OK = IR_KEY_STRAT + 0x15, IR_KEY_RIGHT = IR_KEY_STRAT + 0x09, IR_KEY_0 = IR_KEY_STRAT + 0x16, IR_KEY_DOWN = IR_KEY_STRAT + 0x19, IR_KEY_C = IR_KEY_STRAT + 0x0D, IR_KEY_1 = IR_KEY_STRAT + 0x0C, IR_KEY_2 = IR_KEY_STRAT + 0x18, IR_KEY_3 = IR_KEY_STRAT + 0x5E, IR_KEY_4 = IR_KEY_STRAT + 0x08, IR_KEY_5 = IR_KEY_STRAT + 0x1C, IR_KEY_6 = IR_KEY_STRAT + 0x5A, IR_KEY_7 = IR_KEY_STRAT + 0x42, IR_KEY_8 = IR_KEY_STRAT + 0x52, IR_KEY_9 = IR_KEY_STRAT + 0x4A, }IR_KEY_E;
我們下面來看一段簡單的應用。這個應用的功能是:主板K1鍵控制LED1指示燈;遙控器的POWER鍵和MENU鍵控制LED2指示燈。
#include "bsp.h" int main(void) { uint8_t ucKeyCode; bsp_Init(); IRD_StartWork(); /* 啟動紅外解碼 */ while(1) { bsp_Idle(); /* 處理按鍵事件 */ ucKeyCode = bsp_GetKey(); if (ucKeyCode > 0) { /* 有鍵按下 */ switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ bsp_LedOn(1); /* 點亮LED1 */ break; case KEY_UP_K1: /* K1鍵彈起 */ bsp_LedOff(1); /* 熄滅LED1 */ break; case IR_KEY_POWER: /* 遙控器POWER鍵按下 */ bsp_LedOn(1); /* 點亮LED2 */ break; case IR_KEY_MENU: /* 遙控器MENU鍵按下 */ bsp_LedOff(1); /* 熄滅LED2 */ break; case MSG_485_RX: /* 通信程序的發來的消息 */ /* 執行通信程序的指令 */ break; default: break; } } } }
看到這里,想必你已經意識到bsp_PutKey函數的強大之處了,可以將不相關的硬件輸入設備統一為一個相同的接口函數。
在上面的應用程序中,我們特意添加了一段紅色的代碼來解說更高級的用法。485通信程序收到有效的命令后通過 bsp_PutKey(MSG_485_RX)函數可以通知APP應用程序進行進一步加工處理(比如顯示接收成功)。這是一種非常好的任務間信息傳遞方式,它不會破壞程序結構。不必新增全局變量來做這種事情,你只需要添加一個鍵值代碼。
對於簡單的程序,可以借用按鍵FIFO來進行少量的信息傳遞。對於復雜的應用,我們推薦使用bsp_msg專門來做這種任務間的通信。因為bsp_msg除了傳遞消息代碼外,還可以傳遞參數結構。
19.3.3 按鍵檢測程序分析
在bsp_key.h 中定了結構體類型KEY_T。
#define KEY_COUNT 10 /* 按鍵個數, 8個獨立建 + 2個組合鍵 */ typedef struct { /* 下面是一個函數指針,指向判斷按鍵手否按下的函數 */ uint8_t (*IsKeyDownFunc)(void); /* 按鍵按下的判斷函數,1表示按下 */ uint8_t Count; /* 濾波器計數器 */ uint16_t LongCount; /* 長按計數器 */ uint16_t LongTime; /* 按鍵按下持續時間, 0表示不檢測長按 */ uint8_t State; /* 按鍵當前狀態(按下還是彈起) */ uint8_t RepeatSpeed; /* 連續按鍵周期 */ uint8_t RepeatCount; /* 連續按鍵計數器 */ }KEY_T;
在bsp_key.c 中定義s_tBtn結構體數組變量。
static KEY_T s_tBtn[KEY_COUNT]; static KEY_FIFO_T s_tKey; /* 按鍵FIFO變量,結構體 */
每個按鍵對象都分配一個結構體變量,這些結構體變量以數組的形式存在將便於我們簡化程序代碼行數。
使用函數指針IsKeyDownFunc可以將每個按鍵的檢測以及組合鍵的檢測代碼進行統一管理。
因為函數指針必須先賦值,才能被作為函數執行。因此在定時掃描按鍵之前,必須先執行一段初始化函數來設置每個按鍵的函數指針和參數。這個函數是 void bsp_InitKey(void)。它由bsp_Init()調用。
/* ********************************************************************************************************* * 函 數 名: bsp_InitKey * 功能說明: 初始化按鍵. 該函數被 bsp_Init() 調用。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_InitKey(void) { bsp_InitKeyVar(); /* 初始化按鍵變量 */ bsp_InitKeyHard(); /* 初始化按鍵硬件 */ }
下面是bsp_InitKeyVar函數的定義:
/* ********************************************************************************************************* * 函 數 名: bsp_InitKeyVar * 功能說明: 初始化按鍵變量 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void bsp_InitKeyVar(void) { uint8_t i; /* 對按鍵FIFO讀寫指針清零 */ s_tKey.Read = 0; s_tKey.Write = 0; s_tKey.Read2 = 0; /* 給每個按鍵結構體成員變量賦一組缺省值 */ for (i = 0; i < KEY_COUNT; i++) { s_tBtn[i].LongTime = KEY_LONG_TIME; /* 長按時間 0 表示不檢測長按鍵事件 */ s_tBtn[i].Count = KEY_FILTER_TIME / 2; /* 計數器設置為濾波時間的一半 */ s_tBtn[i].State = 0; /* 按鍵缺省狀態,0為未按下 */ s_tBtn[i].RepeatSpeed = 0; /* 按鍵連發的速度,0表示不支持連發 */ s_tBtn[i].RepeatCount = 0; /* 連發計數器 */ } /* 如果需要單獨更改某個按鍵的參數,可以在此單獨重新賦值 */ /* 搖桿上下左右,支持長按1秒后,自動連發 */ bsp_SetKeyParam(KID_JOY_U, 100, 6); bsp_SetKeyParam(KID_JOY_D, 100, 6); bsp_SetKeyParam(KID_JOY_L, 100, 6); bsp_SetKeyParam(KID_JOY_R, 100, 6); }
注意一下 Count 這個成員變量,沒有設置為0。為了避免主板上電的瞬間,檢測到一個無效的按鍵按下或彈起事件。我們將這個濾波計數器的初值設置為正常值的1/2。bsp_key.h中定義了濾波時間和長按時間。
/* 按鍵濾波時間50ms, 單位10ms。 只有連續檢測到50ms狀態不變才認為有效,包括彈起和按下兩種事件 即使按鍵電路不做硬件濾波,該濾波機制也可以保證可靠地檢測到按鍵事件 */ #define KEY_FILTER_TIME 5 #define KEY_LONG_TIME 100 /* 單位10ms, 持續1秒,認為長按事件 */
uint8_t KeyPinActive(uint8_t _id)(會調用函數KeyPinActive判斷狀態)函數就是最底層的GPIO輸入狀態判斷函數。
/* ********************************************************************************************************* * 函 數 名: KeyPinActive * 功能說明: 判斷按鍵是否按下 * 形 參: 無 * 返 回 值: 返回值1 表示按下(導通),0表示未按下(釋放) ********************************************************************************************************* */ static uint8_t KeyPinActive(uint8_t _id) { uint8_t level; if ((s_gpio_list[_id].gpio->IDR & s_gpio_list[_id].pin) == 0) { level = 0; } else { level = 1; } if (level == s_gpio_list[_id].ActiveLevel) { return 1; } else { return 0; } } /* ********************************************************************************************************* * 函 數 名: IsKeyDownFunc * 功能說明: 判斷按鍵是否按下。單鍵和組合鍵區分。單鍵事件不允許有其他鍵按下。 * 形 參: 無 * 返 回 值: 返回值1 表示按下(導通),0表示未按下(釋放) ********************************************************************************************************* */ static uint8_t IsKeyDownFunc(uint8_t _id) { /* 實體單鍵 */ if (_id < HARD_KEY_NUM) { uint8_t i; uint8_t count = 0; uint8_t save = 255; /* 判斷有幾個鍵按下 */ for (i = 0; i < HARD_KEY_NUM; i++) { if (KeyPinActive(i)) { count++; save = i; } } if (count == 1 && save == _id) { return 1; /* 只有1個鍵按下時才有效 */ } return 0; } /* 組合鍵 K1K2 */ if (_id == HARD_KEY_NUM + 0) { if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)) { return 1; } else { return 0; } } /* 組合鍵 K2K3 */ if (_id == HARD_KEY_NUM + 1) { if (KeyPinActive(KID_K2) && KeyPinActive(KID_K3)) { return 1; } else { return 0; } } return 0; }
在使用GPIO之前,我們必須對GPIO進行配置,比如打開GPIO時鍾,設置GPIO輸入輸出方向,設置上下拉電阻。下面是配置GPIO的代碼,也就是bsp_InitKeyHard()函數:
/* ********************************************************************************************************* * 函 數 名: bsp_InitKeyHard * 功能說明: 配置按鍵對應的GPIO * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void bsp_InitKeyHard(void) { GPIO_InitTypeDef gpio_init; uint8_t i; /* 第1步:打開GPIO時鍾 */ ALL_KEY_GPIO_CLK_ENABLE(); /* 第2步:配置所有的按鍵GPIO為浮動輸入模式(實際上CPU復位后就是輸入狀態) */ gpio_init.Mode = GPIO_MODE_INPUT; /* 設置輸入 */ gpio_init.Pull = GPIO_NOPULL; /* 上下拉電阻不使能 */ gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* GPIO速度等級 */ for (i = 0; i < HARD_KEY_NUM; i++) { gpio_init.Pin = s_gpio_list[i].pin; HAL_GPIO_Init(s_gpio_list[i].gpio, &gpio_init); } }
我們再來看看按鍵是如何執行掃描檢測的。
按鍵掃描函數bsp_KeyScan10ms ()每隔10ms被執行一次。bsp_RunPer10ms函數在systick中斷服務程序中執行。
void bsp_RunPer10ms(void) { bsp_KeyScan10ms(); /* 掃描按鍵 */ }
bsp_KeyScan10ms ()函數的實現如下:
/* ********************************************************************************************************* * 函 數 名: bsp_KeyScan10ms * 功能說明: 掃描所有按鍵。非阻塞,被systick中斷周期性的調用,10ms一次 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_KeyScan10ms(void) { uint8_t i; for (i = 0; i < KEY_COUNT; i++) { bsp_DetectKey(i); } }
每隔10ms所有的按鍵GPIO均會被掃描檢測一次。bsp_DetectKey函數實現如下:
/* ********************************************************************************************************* * 函 數 名: bsp_DetectKey * 功能說明: 檢測一個按鍵。非阻塞狀態,必須被周期性的調用。 * 形 參: IO的id, 從0開始編碼 * 返 回 值: 無 ********************************************************************************************************* */ static void bsp_DetectKey(uint8_t i) { KEY_T *pBtn; pBtn = &s_tBtn[i]; if (IsKeyDownFunc(i)) { if (pBtn->Count < KEY_FILTER_TIME) { pBtn->Count = KEY_FILTER_TIME; } else if(pBtn->Count < 2 * KEY_FILTER_TIME) { pBtn->Count++; } else { if (pBtn->State == 0) { pBtn->State = 1; /* 發送按鈕按下的消息 */ bsp_PutKey((uint8_t)(3 * i + 1)); } if (pBtn->LongTime > 0) { if (pBtn->LongCount < pBtn->LongTime) { /* 發送按鈕持續按下的消息 */ if (++pBtn->LongCount == pBtn->LongTime) { /* 鍵值放入按鍵FIFO */ bsp_PutKey((uint8_t)(3 * i + 3)); } } else { if (pBtn->RepeatSpeed > 0) { if (++pBtn->RepeatCount >= pBtn->RepeatSpeed) { pBtn->RepeatCount = 0; /* 常按鍵后,每隔RepeatSpeed * 10ms發送1個按鍵 */ bsp_PutKey((uint8_t)(3 * i + 1)); } } } } } } else { if(pBtn->Count > KEY_FILTER_TIME) { pBtn->Count = KEY_FILTER_TIME; } else if(pBtn->Count != 0) { pBtn->Count--; } else { if (pBtn->State == 1) { pBtn->State = 0; /* 發送按鈕彈起的消息 */ bsp_PutKey((uint8_t)(3 * i + 2)); } } pBtn->LongCount = 0; pBtn->RepeatCount = 0; } }
對於初學者,這個函數看起來比較吃力,我們拆分進行分析。
pBtn = &s_tBtn[i];
讀取相應按鍵的結構體地址,程序里面每個按鍵都有自己的結構體。
static KEY_T s_tBtn[KEY_COUNT]; if (IsKeyDownFunc(i)) { 這個里面執行的是按鍵按下的處理 } else { 這個里面執行的是按鍵松手的處理或者按鍵沒有按下的處理 }
執行函數IsKeyDownFunc(i)做按鍵狀態判斷。
/* ********************************************************************************** 下面這個if語句主要是用於按鍵濾波前給Count設置一個初值,前面說按鍵初始化的時候 已經設置了Count = KEY_FILTER_TIME/2 ********************************************************************************** */ if (pBtn->Count < KEY_FILTER_TIME) { pBtn->Count = KEY_FILTER_TIME; } /* ********************************************************************************** 這里實現KEY_FILTER_TIME時間長度的延遲 ********************************************************************************** */ else if(pBtn->Count < 2 * KEY_FILTER_TIME) { pBtn->Count++; } /* ********************************************************************************** 這里實現KEY_FILTER_TIME時間長度的延遲 ********************************************************************************** */ else { /* ********************************************************************************** 這個State變量是有其實際意義的,如果按鍵按下了,這里就將其設置為1,如果沒有按下這個 變量的值就會一直是0,這樣設置的目的可以有效的防止一種情況的出現:比如按鍵K1在某個 時刻檢測到了按鍵有按下,那么它就會做進一步的濾波處理,但是在濾波的過程中,這個按鍵 按下的狀態消失了,這個時候就會進入到上面第二步else語句里面,然后再做按鍵松手檢測濾波 ,濾波結束后判斷這個State變量,如果前面就沒有檢測到按下,這里就不會記錄按鍵彈起。 ********************************************************************************** */ if (pBtn->State == 0) { pBtn->State = 1; /* 發送按鈕按下的消息 */ bsp_PutKey((uint8_t)(3 * i + 1)); } if (pBtn->LongTime > 0) { if (pBtn->LongCount < pBtn->LongTime) { /* 發送按鈕持續按下的消息 */ if (++pBtn->LongCount == pBtn->LongTime) { /* 鍵值放入按鍵FIFO */ bsp_PutKey((uint8_t)(3 * i + 3)); } } else { if (pBtn->RepeatSpeed > 0) { if (++pBtn->RepeatCount >= pBtn->RepeatSpeed) { pBtn->RepeatCount = 0; /* 長按鍵后,每隔10ms發送1個按鍵 */ bsp_PutKey((uint8_t)(3 * i + 1)); } } } } }
19.3.4 按鍵檢測采用中斷方式還是查詢方式
檢測按鍵有中斷方式和GPIO查詢方式兩種。我們推薦大家用GPIO查詢方式。
從裸機的角度分析
中斷方式:中斷方式可以快速地檢測到按鍵按下,並執行相應的按鍵程序,但實際情況是由於按鍵的機械抖動特性,在程序進入中斷后必須進行濾波處理才能判定是否有效的按鍵事件。如果每個按鍵都是獨立的接一個IO引腳,需要我們給每個IO都設置一個中斷,程序中過多的中斷會影響系統的穩定性。中斷方式跨平台移植困難。
查詢方式:查詢方式有一個最大的缺點就是需要程序定期的去執行查詢,耗費一定的系統資源。實際上耗費不了多大的系統資源,因為這種查詢方式也只是查詢按鍵是否按下,按鍵事件的執行還是在主程序里面實現。
從OS的角度分析
中斷方式:在OS中要盡可能少用中斷方式,因為在RTOS中過多的使用中斷會影響系統的穩定性和可預見性(搶占式調度的OS基本沒有可預見性)。只有比較重要的事件處理需要用中斷的方式。
查詢方式:對於用戶按鍵推薦使用這種查詢方式來實現,現在的OS基本都帶有CPU利用率的功能,這個按鍵FIFO占用的還是很小的,基本都在1%以下。
19.4 按鍵板級支持包(bsp_key.c)
按鍵驅動文件bsp_key.c主要實現了如下幾個API:
- KeyPinActive
- IsKeyDownFunc
- bsp_InitKey
- bsp_InitKeyHard
- bsp_InitKeyVar
- bsp_PutKey
- bsp_GetKey
- bsp_GetKey2
- bsp_GetKeyState
- bsp_SetKeyParam
- bsp_ClearKey
- bsp_DetectKey
- bsp_DetectFastIO
- bsp_KeyScan10ms
- bsp_KeyScan1ms
所有這些函數在本章的19.3小節都進行了詳細講解,本小節主要是把需要用戶調用的三個函數做個說明。
19.4.1 函數bsp_InitKeyHard
函數原型:
/* ********************************************************************************************************* * 函 數 名: bsp_InitKey * 功能說明: 初始化按鍵. 該函數被 bsp_Init() 調用。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_InitKey(void) { bsp_InitKeyVar(); /* 初始化按鍵變量 */ bsp_InitKeyHard(); /* 初始化按鍵硬件 */ }
函數描述:
此函數主要用於按鍵的初始化。
使用舉例:
底層驅動初始化直接在bsp.c文件的函數bsp_Init里面調用即可。
19.4.2 函數bsp_GetKey
函數原型:
/* ********************************************************************************************************* * 函 數 名: bsp_GetKey * 功能說明: 從按鍵FIFO緩沖區讀取一個鍵值。 * 形 參: 無 * 返 回 值: 按鍵代碼 ********************************************************************************************************* */ uint8_t bsp_GetKey(void) { uint8_t ret; if (s_tKey.Read == s_tKey.Write) { return KEY_NONE; } else { ret = s_tKey.Buf[s_tKey.Read]; if (++s_tKey.Read >= KEY_FIFO_SIZE) { s_tKey.Read = 0; } return ret; } }
函數描述:
此函數用於從FIFO中讀取鍵值。
使用舉例:
調用此函數前,務必優先調用函數bsp_InitKey進行初始化。
/* ********************************************************************************************************* * 函 數 名: main * 功能說明: c程序入口 * 形 參: 無 * 返 回 值: 錯誤代碼(無需處理) ********************************************************************************************************* */ int main(void) { uint8_t ucKeyCode; /* 按鍵代碼 */ bsp_Init(); /* 硬件初始化 */ /* 進入主程序循環體 */ while (1) { /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */ ucKeyCode = bsp_GetKey(); /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ printf("K1鍵按下\r\n"); break; case KEY_DOWN_K2: /* K2鍵按下 */ printf("K2鍵按下\r\n"); break; default: /* 其它的鍵值不處理 */ break; } } } }
19.4.3 函數bsp_KeyScan10ms
函數原型:
/* ********************************************************************************************************* * 函 數 名: bsp_KeyScan10ms * 功能說明: 掃描所有按鍵。非阻塞,被systick中斷周期性的調用,10ms一次 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_KeyScan10ms(void) { uint8_t i; for (i = 0; i < KEY_COUNT; i++) { bsp_DetectKey(i); } }
函數描述:
此函數是按鍵的主處理函數,用於檢測和存儲按下、松手、長按等狀態。
使用舉例:
調用此函數前,務必優先調用函數bsp_InitKey進行初始化。
另外,此函數需要周期性調用,每10ms調用一次。
- 如果是裸機使用,將此函數放在bsp.c文件的bsp_RunPer10ms函數里面即可,這個函數是由滴答定時器調用的,也就是說,大家要使用按鍵,定時器的初始化函數bsp_InitTimer一定要調用。
- 如果是RTOS使用,需要開啟一個10ms為周期的任務調用函數bsp_KeyScan10ms。
19.5 按鍵FIFO驅動移植和使用
按鍵移植步驟如下:
- 第1步:復制bsp_key.c和bsp_key.c到自己的工程。
- 第2步:根據自己使用的獨立按鍵個數和組合鍵個數,修改幾個地方。
#define HARD_KEY_NUM 8 /* 實體按鍵個數 */ #define KEY_COUNT (HARD_KEY_NUM + 2) /* 8個獨立建 + 2個組合按鍵 */
- 第3步:根據使用的引腳時鍾,修改下面函數:
/* 使能GPIO時鍾 */ #define ALL_KEY_GPIO_CLK_ENABLE() { \ __HAL_RCC_GPIOB_CLK_ENABLE(); \ __HAL_RCC_GPIOC_CLK_ENABLE(); \ __HAL_RCC_GPIOG_CLK_ENABLE(); \ __HAL_RCC_GPIOH_CLK_ENABLE(); \ __HAL_RCC_GPIOI_CLK_ENABLE(); \ };
- 第4步:根據使用的具體引腳,修改如下函數,第3列參數低電平表示按下或者高電平表示按下:
/* GPIO和PIN定義 */ static const X_GPIO_T s_gpio_list[HARD_KEY_NUM] = { {GPIOI, GPIO_PIN_8, 0}, /* K1 */ {GPIOC, GPIO_PIN_13, 0}, /* K2 */ {GPIOH, GPIO_PIN_4, 0}, /* K3 */ {GPIOG, GPIO_PIN_2, 0}, /* JOY_U */ {GPIOB, GPIO_PIN_0, 0}, /* JOY_D */ {GPIOG, GPIO_PIN_3, 0}, /* JOY_L */ {GPIOG, GPIO_PIN_7, 0}, /* JOY_R */ {GPIOI, GPIO_PIN_11, 0}, /* JOY_OK */ };
- 第5步:根據使用的組合鍵個數,在函數IsKeyDownFunc里面添加相應個數的函數:
/* 組合鍵 K1K2 */ if (_id == HARD_KEY_NUM + 0) { if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)) { return 1; } else { return 0; } }
第2行ID表示HARD_KEY_NUM + 0的組合鍵,HARD_KEY_NUM + 1表示下一個組合鍵,以此類推。
另外就是,函數KeyPinActive的參數是表示檢測哪兩個按鍵,設置0的時候表示第4步里面的第1組按鍵,設置為1表示第2組按鍵,以此類推。
- 第6步:主要用到HAL庫的GPIO驅動文件,簡單省事些可以添加所有HAL庫.C源文件進來。
- 第7步:移植完整,應用方法看本章節配套例子即可。
特別注意,別忘了每10ms調用一次按鍵檢測函數bsp_KeyScan10ms。
19.6 實驗例程設計框架
通過程序設計框架,讓大家先對配套例程有一個全面的認識,然后再理解細節,本次實驗例程的設計框架如下:
1、 第1階段,上電啟動階段:
這部分在第14章進行了詳細說明。
2、 第2階段,進入main函數:
- 第1部分,硬件初始化,主要是MPU、Cache、HAL庫、系統時鍾、滴答定時器、按鍵等。
- 第2部分,應用程序設計部分,實現了一個按鍵應用。
- 第3部分,按鍵掃描程序每10ms在滴答定時中斷執行一次。
19.7 實驗例程說明(MDK)
配套例子:
V7-002_按鍵檢測(軟件濾波,FIFO機制)
實驗目的:
- 學習按鍵的按下,彈起,長按和組合鍵的實現。
實驗內容:
- 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2。
實驗操作:
- 3個獨立按鍵和5向搖桿按下時均有串口消息打印。
- 5向搖桿的左鍵和右鍵長按時,會有連發的串口消息。
- 獨立按鍵K1和K2按鍵按下,串口打印消息。
上電后串口打印的信息:
波特率 115200,數據位 8,奇偶校驗位無,停止位 1
程序設計:
系統棧大小分配:
RAM空間用的DTCM:
硬件外設初始化
硬件外設的初始化是在 bsp.c 文件實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾: - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。 - 設置NVIV優先級分組為4。 */ HAL_Init(); /* 配置系統時鍾到400MHz - 切換使用HSE。 - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。 - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。
/* ********************************************************************************************************* * 函 數 名: MPU_Config * 功能說明: 配置MPU * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void MPU_Config( void ) { MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } /* ********************************************************************************************************* * 函 數 名: CPU_CACHE_Enable * 功能說明: 使能L1 Cache * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void CPU_CACHE_Enable(void) { /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache(); }
每10ms調用一次按鍵檢測:
按鍵檢測是在滴答定時器中斷里面實現,每10ms執行一次檢測。
/* ********************************************************************************************************* * 函 數 名: bsp_RunPer10ms * 功能說明: 該函數每隔10ms被Systick中斷調用1次。詳見 bsp_timer.c的定時中斷服務程序。一些處理時間要求 * 不嚴格的任務可以放在此函數。比如:按鍵掃描、蜂鳴器鳴叫控制等。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_RunPer10ms(void) { bsp_KeyScan10ms(); }
主功能:
主功能的實現主要分為兩部分:
- 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2
- 按鍵消息的讀取,檢測到按下后,做串口打印。
/* ********************************************************************************************************* * 函 數 名: main * 功能說明: c程序入口 * 形 參: 無 * 返 回 值: 錯誤代碼(無需處理) ********************************************************************************************************* */ int main(void) { uint8_t ucKeyCode; /* 按鍵代碼 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名稱和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重裝的定時器 */ /* 進入主程序循環體 */ while (1) { bsp_Idle(); /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 進來一次 */ bsp_LedToggle(2); } /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */ ucKeyCode = bsp_GetKey(); /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ printf("K1鍵按下\r\n"); break; case KEY_UP_K1: /* K1鍵彈起 */ printf("K1鍵彈起\r\n"); break; case KEY_DOWN_K2: /* K2鍵按下 */ printf("K2鍵按下\r\n"); break; case KEY_UP_K2: /* K2鍵彈起 */ printf("K2鍵彈起\r\n"); break; case KEY_DOWN_K3: /* K3鍵按下 */ printf("K3鍵按下\r\n"); break; case KEY_UP_K3: /* K3鍵彈起 */ printf("K3鍵彈起\r\n"); break; case JOY_DOWN_U: /* 搖桿UP鍵按下 */ printf("搖桿上鍵按下\r\n"); break; case JOY_DOWN_D: /* 搖桿DOWN鍵按下 */ printf("搖桿下鍵按下\r\n"); break; case JOY_DOWN_L: /* 搖桿LEFT鍵按下 */ printf("搖桿左鍵按下\r\n"); break; case JOY_LONG_L: /* 搖桿LEFT鍵長按 */ printf("搖桿左鍵長按\r\n"); break; case JOY_DOWN_R: /* 搖桿RIGHT鍵按下 */ printf("搖桿右鍵按下\r\n"); break; case JOY_LONG_R: /* 搖桿RIGHT鍵長按 */ printf("搖桿右鍵長按\r\n"); break; case JOY_DOWN_OK: /* 搖桿OK鍵按下 */ printf("搖桿OK鍵按下\r\n"); break; case JOY_UP_OK: /* 搖桿OK鍵彈起 */ printf("搖桿OK鍵彈起\r\n"); break; case SYS_DOWN_K1K2: /* 搖桿OK鍵彈起 */ printf("K1和K2組合鍵按下\r\n"); break; default: /* 其它的鍵值不處理 */ break; } } } }
19.8 實驗例程說明(IAR)
配套例子:
V7-002_按鍵檢測(軟件濾波,FIFO機制)
實驗目的:
- 學習按鍵的按下,彈起,長按和組合鍵的實現。
實驗內容:
- 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2。
實驗操作:
- 3個獨立按鍵和5向搖桿按下時均有串口消息打印。
- 5向搖桿的左鍵和右鍵長按時,會有連發的串口消息。
- 獨立按鍵K1和K2按鍵按下,串口打印消息。
上電后串口打印的信息:
波特率 115200,數據位 8,奇偶校驗位無,停止位 1
程序設計:
系統棧大小分配:
RAM空間用的DTCM:
硬件外設初始化
硬件外設的初始化是在 bsp.c 文件實現:
/* ********************************************************************************************************* * 函 數 名: bsp_Init * 功能說明: 初始化所有的硬件設備。該函數配置CPU寄存器和外設的寄存器並初始化一些全局變量。只需要調用一次 * 形 參:無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_Init(void) { /* 配置MPU */ MPU_Config(); /* 使能L1 Cache */ CPU_CACHE_Enable(); /* STM32H7xx HAL 庫初始化,此時系統用的還是H7自帶的64MHz,HSI時鍾: - 調用函數HAL_InitTick,初始化滴答時鍾中斷1ms。 - 設置NVIV優先級分組為4。 */ HAL_Init(); /* 配置系統時鍾到400MHz - 切換使用HSE。 - 此函數會更新全局變量SystemCoreClock,並重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用於代碼執行時間測量,MDK5.25及其以上版本才支持,IAR不支持。 - 默認不開啟,如果要使能此選項,務必看V7開發板用戶手冊第xx章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder並開啟 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按鍵初始化,要放在滴答定時器之前,因為按鈕檢測是通過滴答定時器掃描 */ bsp_InitTimer(); /* 初始化滴答定時器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化FMC總線74HC574擴展IO. 必須在 bsp_InitLed()前執行 */ bsp_InitLed(); /* 初始化LED */ }
MPU配置和Cache配置:
數據Cache和指令Cache都開啟。配置了AXI SRAM區(本例子未用到AXI SRAM)和FMC的擴展IO區。
/* ********************************************************************************************************* * 函 數 名: MPU_Config * 功能說明: 配置MPU * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void MPU_Config( void ) { MPU_Region_InitTypeDef MPU_InitStruct; /* 禁止 MPU */ HAL_MPU_Disable(); /* 配置AXI SRAM的MPU屬性為Write back, Read allocate,Write allocate */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x24000000; MPU_InitStruct.Size = MPU_REGION_SIZE_512KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /* 配置FMC擴展IO的MPU屬性為Device或者Strongly Ordered */ MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x60000000; MPU_InitStruct.Size = ARM_MPU_REGION_SIZE_64KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.Number = MPU_REGION_NUMBER1; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.SubRegionDisable = 0x00; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); /*使能 MPU */ HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); } /* ********************************************************************************************************* * 函 數 名: CPU_CACHE_Enable * 功能說明: 使能L1 Cache * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ static void CPU_CACHE_Enable(void) { /* 使能 I-Cache */ SCB_EnableICache(); /* 使能 D-Cache */ SCB_EnableDCache(); }
每10ms調用一次按鍵檢測:
按鍵檢測是在滴答定時器中斷里面實現,每10ms執行一次檢測。
/* ********************************************************************************************************* * 函 數 名: bsp_RunPer10ms * 功能說明: 該函數每隔10ms被Systick中斷調用1次。詳見 bsp_timer.c的定時中斷服務程序。一些處理時間要求 * 不嚴格的任務可以放在此函數。比如:按鍵掃描、蜂鳴器鳴叫控制等。 * 形 參: 無 * 返 回 值: 無 ********************************************************************************************************* */ void bsp_RunPer10ms(void) { bsp_KeyScan10ms(); }
主功能:
主功能的實現主要分為兩部分:
- 啟動一個自動重裝軟件定時器,每100ms翻轉一次LED2
- 按鍵消息的讀取,檢測到按下后,做串口打印。
/* ********************************************************************************************************* * 函 數 名: main * 功能說明: c程序入口 * 形 參: 無 * 返 回 值: 錯誤代碼(無需處理) ********************************************************************************************************* */ int main(void) { uint8_t ucKeyCode; /* 按鍵代碼 */ bsp_Init(); /* 硬件初始化 */ PrintfLogo(); /* 打印例程名稱和版本等信息 */ PrintfHelp(); /* 打印操作提示 */ bsp_StartAutoTimer(0, 100); /* 啟動1個100ms的自動重裝的定時器 */ /* 進入主程序循環體 */ while (1) { bsp_Idle(); /* 這個函數在bsp.c文件。用戶可以修改這個函數實現CPU休眠和喂狗 */ /* 判斷定時器超時時間 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 進來一次 */ bsp_LedToggle(2); } /* 按鍵濾波和檢測由后台systick中斷服務程序實現,我們只需要調用bsp_GetKey讀取鍵值即可。 */ ucKeyCode = bsp_GetKey(); /* 讀取鍵值, 無鍵按下時返回 KEY_NONE = 0 */ if (ucKeyCode != KEY_NONE) { switch (ucKeyCode) { case KEY_DOWN_K1: /* K1鍵按下 */ printf("K1鍵按下\r\n"); break; case KEY_UP_K1: /* K1鍵彈起 */ printf("K1鍵彈起\r\n"); break; case KEY_DOWN_K2: /* K2鍵按下 */ printf("K2鍵按下\r\n"); break; case KEY_UP_K2: /* K2鍵彈起 */ printf("K2鍵彈起\r\n"); break; case KEY_DOWN_K3: /* K3鍵按下 */ printf("K3鍵按下\r\n"); break; case KEY_UP_K3: /* K3鍵彈起 */ printf("K3鍵彈起\r\n"); break; case JOY_DOWN_U: /* 搖桿UP鍵按下 */ printf("搖桿上鍵按下\r\n"); break; case JOY_DOWN_D: /* 搖桿DOWN鍵按下 */ printf("搖桿下鍵按下\r\n"); break; case JOY_DOWN_L: /* 搖桿LEFT鍵按下 */ printf("搖桿左鍵按下\r\n"); break; case JOY_LONG_L: /* 搖桿LEFT鍵長按 */ printf("搖桿左鍵長按\r\n"); break; case JOY_DOWN_R: /* 搖桿RIGHT鍵按下 */ printf("搖桿右鍵按下\r\n"); break; case JOY_LONG_R: /* 搖桿RIGHT鍵長按 */ printf("搖桿右鍵長按\r\n"); break; case JOY_DOWN_OK: /* 搖桿OK鍵按下 */ printf("搖桿OK鍵按下\r\n"); break; case JOY_UP_OK: /* 搖桿OK鍵彈起 */ printf("搖桿OK鍵彈起\r\n"); break; case SYS_DOWN_K1K2: /* 搖桿OK鍵彈起 */ printf("K1和K2組合鍵按下\r\n"); break; default: /* 其它的鍵值不處理 */ break; } } } }
19.9 總結
這個方案在實際項目中已經經過千錘百煉,大家可以放心使用。建議熟練掌握其用法。