硬件平臺單片機、ARM等等 編譯環境MDK5.0 硬件工具配合MCU 主要文件無 作 者@量子CPU(747764222) 本節我們將學習讓你惱火的按鍵,主要是從下面4個方面進行講解: 1.按鍵觸發方式迫在眉睫 2.按鍵掃描 3.中斷掃描 4.按鍵狀態機制 一、按鍵觸發方迫在眉睫 如果你在公司上班,抑或你在學校,當你做的項目任務進度比較多的時候,你還在使用按鍵掃描,也許老板會說你SB,老師說你無能。這就是現實,單片機和ST序列的目前還是單核的,怎么可能一直在做掃描,你大才小用了吧。你把精華浪費在做無聊的事情上面,現在你評價一下你自己吧,我不多說。O(∩_∩)O哈哈~ 親,你的按鍵還活著么?你的按鍵會苦惱你么?如果你還在考慮按鍵用掃描方式,那可能有一天要被人罵了,如果想要面子,又讓你的按鍵有活力。Follow me !!! 你不會按鍵的三種方式,你媽知道么?(*^__^*) 嘻嘻…… 下面以單個獨立按鍵來講解,矩陣按鍵可以思想是一樣的。 二、按鍵掃描方式 按鍵掃描的方式,這應該是大家比較熟悉的,因為大部分單片機入門的時候,碰到按鍵的時候,都是通過掃描方式來實現。 按鍵掃描的原理:CPU需要不停的工作,來判斷IO口是否被拉低或者置高,效率比較低。按鍵掃描主要是處理消抖。 STM32為例講解 /*************************************** * 函數描述:按鍵初始化函數 * 輸入參數:No * 返 回 值:No * 說 明:初始化按鍵 * 修改記錄: ****************************************/ void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //按鍵PA0 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; //輸入模式 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_2; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉輸入 GPIO_Init(GPIOA, &GPIO_InitStruct); } /*************************************** * 函數描述:按鍵掃描函數 * 輸入參數:No * 返 回 值:No * 說 明:掃描按鍵 * 修改記錄: ****************************************/ uint8_t KEY_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin) { if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0 ) //檢測是否有按鍵按下 { Delay(10000); //延時消抖 if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0 ) { while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == 0); //等待按鍵釋放 */ return 0 ; } else return 1; } else eturn 1; } /*************************************** * 函數描述:部分主函數 * 輸入參數:No * 返 回 值:No * 說 明: * 修改記錄: ****************************************/ while(1) { if( KEY_Scan (GPIOA,GPIO_Pin_0) ==0)//判定按鍵是否按下 { GPIO_WriteBit(GPIOC, GPIO_Pin_9, (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_9))));//反轉led2燈 } } 這就是按鍵掃描,至于缺點不言而喻。這不就是苦逼的CPU的么?O(∩_∩)O哈哈~http://bbs.ickey.cn/group-topic-id-13062.html 三、中斷掃描 在你對單片機比較熟悉的時候,在你考慮多任務的時候,你應該被按鍵掃描煩死了,這時候,你絞盡腦汁,終于想到了—————————— 中斷掃描。 中斷掃描的原理:中斷控制效率很高,一旦系統IO口出現上升或者下降沿電平就會觸發執行中斷內的程序。這樣MCU就無需一直在掃描,這樣你就可以干其他的時候,而且不會覺得按鍵不靈活。 同樣以STM32為例講解: 說明:STM32用IO口外部中斷的一般步驟: 1.初始化IO口為輸入; 2.開啟IO口時鐘,設置 IO 口與中斷線的映射關系; 3.初始化線上中斷,設置觸發條件等; 4.配置中斷分組(NVIC),并使能中斷; 5.編寫中斷服務函數。 /*************************************** * 函數描述:按鍵初始化函數 * 輸入參數:No * 返 回 值:No * 說 明:按鍵初始化 * 修改記錄: ****************************************/ void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //按鍵PA0 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN; //輸入模式 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_Level_2; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_DOWN; //下拉輸入 GPIO_Init(GPIOA, &GPIO_InitStruct); } /*************************************** * 函數描述:外部中斷初始化 * 輸入參數:No * 返 回 值:No * 說 明: * 修改記錄: ****************************************/ void EXTI_KEY_Init(void) { EXTI_InitTypeDef EXTI_InitStruct; NVIC_InitTypeDef NVIC_InitStruct; KEY_Init(); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); //使能系統時鐘配置 SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0); //連接EXTI0給GPIOA0 SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0); //配置GPIO與中斷線的映射關系 EXTI_InitStruct.EXTI_Line = EXTI_Line0; //中斷線標號0 EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; //外部中斷模式 EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿中斷 EXTI_InitStruct.EXTI_LineCmd = ENABLE; //中斷線使能 EXTI_Init(&EXTI_InitStruct); NVIC_InitStruct.NVIC_IRQChannel = EXTI0_1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPriority = 0x00; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct); } /*************************************** * 函數描述:外部中斷0服務程序 * 輸入參數:No * 返 回 值:No * 說 明: * 修改記錄: ****************************************/ void EXTI0_1_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line0) != RESET) //判斷線0上的中斷是否發生,可以理解為標志位 { /* Toggle LED1and LED2 */ GPIO_WriteBit(GPIOC, GPIO_Pin_8, (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_8)))); GPIO_WriteBit(GPIOC, GPIO_Pin_9, (BitAction)((1-GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_9)))); /* Clear the EXTI line 0 pending bit */ EXTI_ClearITPendingBit(EXTI_Line0);//清除LINE0上的中斷標志位 } } /*************************************** * 函數描述:主函數 * 輸入參數:No * 返 回 值:No * 說 明: * 修改記錄: ****************************************/ int main(void) { SystemInit(); //系統初始化 LED_Init(); //LED燈初始化 GPIO_ResetBits(GPIOC,GPIO_Pin_8); KEY_Init(); //按鍵初始化 EXTI_KEY_Init(); //外部中斷初始化 while(1) { } } 按鍵中斷的有點是不是不言而喻!!!這不就拯救了苦逼的CPU么?O(∩_∩)O哈哈~ http://bbs.ickey.cn/group-topic-id-13062.html 四、按鍵狀態機制 網上看見一片講狀態機制很經典的文章。借用并轉之!!! 首先按鍵程序進入初始狀態S1,在這個狀態下,檢測按鍵是否按下,如果有按下,則進入按鍵消抖狀態2,在下一次執行按鍵程序時候,直接由按鍵消抖狀態進入按鍵按下狀態3,在此狀態下檢測按鍵是否按下,如果沒有按鍵按下,則返回初始狀態S1,如果有則可以返回鍵值,同時進入長按狀態S4,在長按狀態下每次進入按鍵程序時候對按鍵時間計數,當計數值超過設定閾值時候,則表明長按事件發生,同時進入按鍵連_發狀態S5。如果按鍵鍵值為空鍵,則返回按鍵釋放狀態S6,否則繼續停留在本狀態。在按鍵連_發狀態下,如果按鍵鍵值為空鍵則返回按鍵釋放狀態S6,如果按鍵時間計數超過連_發閾值,則返回連_發按鍵值,清零時間計數后繼續停留在本狀態。 看了這么多,也許你已經有一個模糊的概念了,下面讓我們趁熱打鐵,一起來動手編寫按鍵驅動程序吧。 下面是我使用的硬件的連接圖。 硬件連接很簡單,四個獨立按鍵分別接在P3^0------P3^3四個I/O上面。 因為51單片機I/O口內部結構的限制,在讀取外部引腳狀態的時候,需要向端口寫1.在51單片機復位后,不需要進行此操作也可以進行讀取外部引腳的操作。因此,在按鍵的端口沒有復用的情況下,可以省略此步驟。而對于其它一些真正雙向I/O口的單片機來說,將引腳設置成輸入狀態,是必不可少的一個步驟。 下面的程序代碼初始化引腳為輸入。 void KeyInit(void) { io_key_1 = 1 ; io_key_2 = 1 ; io_key_3 = 1 ; io_key_4 = 1 ; } 根據按鍵硬件連接定義按鍵鍵值 #define KEY_VALUE_1 0x0e #define KEY_VALUE_2 0x0d #define KEY_VALUE_3 0x0b #define KEY_VALUE_4 0x07 #define KEY_NULL 0x0f 下面我們來編寫按鍵的硬件驅動程序。 根據第一章所描述的按鍵檢測原理,我們可以很容易的得出如下的代碼: static uint8 KeyScan(void) { if(io_key_1 == 0)return KEY_VALUE_1 ; if(io_key_2 == 0)return KEY_VALUE_2 ; if(io_key_3 == 0)return KEY_VALUE_3 ; if(io_key_4 == 0)return KEY_VALUE_4 ; return KEY_NULL ; } 其中io_key_1等是我們按鍵端口的定義,如下所示: sbit io_key_1 = P3^0 ; sbit io_key_2 = P3^1 ; sbit io_key_3 = P3^2 ; sbit io_key_4 = P3^3 ; KeyScan()作為底層按鍵的驅動程序,為上層按鍵掃描提供一個接口,這樣我們編寫的上層按鍵掃描函數可以幾乎不用修改就可以拿到我們的其它程序中去使用,使得程序復用性大大提高。同時,通過有意識的將與底層硬件連接緊密的程序和與硬件無關的代碼分開寫,使得程序結構層次清晰,可移植性也更好。對于單片機類的程序而言,能夠做到函數級別的代碼重用已經足夠了。 在編寫我們的上層按鍵掃描函數之前,需要先完成一些宏定義。 //定義長按鍵的TICK數,以及連_發間隔的TICK數 #define KEY_LONG_PERIOD 100 #define KEY_CONTINUE_PERIOD 25 //定義按鍵返回值狀態(按下,長按,連_發,釋放) #define KEY_DOWN 0x80 #define KEY_LONG 0x40 #define KEY_CONTINUE 0x20 #define KEY_UP 0x10 //定義按鍵狀態 #define KEY_STATE_INIT 0 #define KEY_STATE_WOBBLE 1 #define KEY_STATE_PRESS 2 #define KEY_STATE_LONG 3 #define KEY_STATE_CONTINUE 4 #define KEY_STATE_RELEASE 5 接著我們開始編寫完整的上層按鍵掃描函數,按鍵的短按,長按,連按,釋放等等狀態的判斷均是在此函數中完成。對照狀態流程轉移圖,然后再看下面的函數代碼,可以更容易的去理解函數的執行流程。完整的函數代碼如下: void GetKey(uint8 *pKeyValue) { static uint8 s_u8KeyState = KEY_STATE_INIT ; static uint8 s_u8KeyTimeCount = 0 ; static uint8 s_u8LastKey = KEY_NULL ; //保存按鍵釋放時候的鍵值 uint8 KeyTemp = KEY_NULL ; KeyTemp = KeyScan() ; //獲取鍵值 switch(s_u8KeyState) { case KEY_STATE_INIT : { if(KEY_NULL != (KeyTemp)) { s_u8KeyState = KEY_STATE_WOBBLE ; %3 |