初學STM32全功能按鍵非阻塞式實現和強化

????????其實筆者以前學51的時候按鍵功能就包含非阻塞式的,而且還包括矩陣按鍵的非組塞式按鍵實現。開關的長短鍵功能筆者在之前的51博文中筆者自己嘗試寫過,功能是有了但寫的其實很混亂,幾乎沒有移植的價值。這次江科大剛好出了新的教程,又重新學習了一下。剛好學到江科大關于串口通信部分,了解了狀態機的形式,思路相比以前突然打開了,以前自己寫代碼總覺得狀態標志是個很好的參數,因為他可以幫助區分工作流程的各個狀態。

? ? ? ? 這次江科大的代碼沒了注釋筆者自己注釋了一下。

key.c

#include "stm32f10x.h"                  // Device header
#include "Key.h"/*映射區還有一些在頭文件Key.h文件里*/
#define KEY_PRESSED				1 //按鍵按下
#define KEY_UNPRESSED			0//按鍵松開#define KEY_TIME_DOUBLE			200
#define KEY_TIME_LONG			2000
#define KEY_TIME_REPEAT			100
/* Key_Flag bit6~bit0 分別代表REPEAT(bit6)、Long、Double、Single、UP、Down、HOLD,bit7是空位   */
uint8_t Key_Flag[KEY_COUNT];//定義全局變量標志位每個標志位互相獨立,不同的標志位代表不同的事件void Key_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);/*按鍵使能GPIO初始化,常態是高電平按下是低電平*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);/*按鍵使能GPIO初始化,常態是低電平按下是高電平*/GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);
}uint8_t Key_GetState(uint8_t n) //檢測當前按鍵的電平,并返回相應的電平信息
{if (n == KEY_1){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return KEY_PRESSED;}}else if (n == KEY_2){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return KEY_PRESSED;}}else if (n == KEY_3){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1){return KEY_PRESSED;}}else if (n == KEY_4){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1){return KEY_PRESSED;}}return KEY_UNPRESSED;
}
/*** 函    數:開關狀態檢測函數* 參    數:uint8_t n 指定開關編號,可在 KEY_1  KEY_2 KEY_3 KEY_4選擇如需擴展可在KEY.h中添加,KEY_1的值是0 KEY_2的值是1,看頭文件的定義* 參    數:uint8_t Flag指定開關的狀態可在下面參數中現在
//                 KEY_HOLD :按住開關
//                 KEY_DOWN :按下開關
//                 KEY_UP   :開關彈起 這三項一般不作為參數檢測KEY_SINGLE :單擊KEY_DOUBLE :雙擊KEY_LONG   : 長按KEY_REPEAT : 重復,即一直按住。 后4項狀態是互斥的但是和前三項可以
* 返 回 值:如果開關狀態是例舉(uint8_t Flag)的狀態則返回1,如果不是則返回0
* 注意事項:開關狀態Key_Flag[n]不一定和uint8_t Flag*/
uint8_t Key_Check(uint8_t n, uint8_t Flag)//開關狀態檢測
{if (Key_Flag[n] & Flag)//開關的7個狀態FLAG是互斥,一個狀態只占一位比如單擊 0000 1000,只有開關確實是處于單擊狀態開關的結果才是非0的{if (Flag != KEY_HOLD) //這個函數就是保證不清除bit0位置的狀態{Key_Flag[n] &= ~Flag; //相應的bit控制位清0,其它位保持}return 1;}return 0;
}void Key_Tick(void)
{static uint8_t Count, i;//定義靜態變量i和Countstatic uint8_t CurrState[KEY_COUNT], PrevState[KEY_COUNT];//定義現態和前態static uint8_t S[KEY_COUNT];//定義狀態static uint16_t Time[KEY_COUNT];//for (i = 0; i < KEY_COUNT; i ++)//KEY_COUNT的值在Key.H中定義了{if (Time[i] > 0){Time[i] --; //對應開關的時間減1}}Count ++;if (Count >= 20)//每20ms進入一次這個函數{Count = 0;for (i = 0; i < KEY_COUNT; i ++)//歷遍所有的開關狀態{PrevState[i] = CurrState[i];//當Key_GetState(i)獲得新狀態時代表著CurrState里面的狀態就是前態了CurrState[i] = Key_GetState(i);//把開關狀態賦值給現態if (CurrState[i] == KEY_PRESSED)//如果檢測到開關按下則標志位HOLD置1{Key_Flag[i] |= KEY_HOLD; //KEY_HOLD = 0000 0001則bit 0 置1其它位保持。}else//如果沒檢測到開關按下則標志位HOLD置0{Key_Flag[i] &= ~KEY_HOLD;//,~KEY_HOLD = 1111 1110 則bit 0置0其它位保持。}if (CurrState[i] == KEY_PRESSED && PrevState[i] == KEY_UNPRESSED)//如果現態是按下前態是沒有按下{Key_Flag[i] |= KEY_DOWN;   // KEY_DOWN = 0000 0010 則bit 1置1其它位保持}if (CurrState[i] == KEY_UNPRESSED && PrevState[i] == KEY_PRESSED)//如果現態是彈起前態是按下{Key_Flag[i] |= KEY_UP;  // KEY_UP = 0000 0100則bit 2 置1其它位保持}if (S[i] == 0) //如果對應開關處于空閑狀態{if (CurrState[i] == KEY_PRESSED) //如果現態的開關狀態時按下{Time[i] = KEY_TIME_LONG; //對應開關的時間設置為2000即長按檢測S[i] = 1;//開關由0態進入1態}}else if (S[i] == 1) //如果對應開關處于狀態1{if (CurrState[i] == KEY_UNPRESSED)//如果現態開關狀態是彈起{Time[i] = KEY_TIME_DOUBLE; //對應開關時間設置為200S[i] = 2;//開關由1態進入2態}else if (Time[i] == 0) //如果對應開關時間為0{Time[i] = KEY_TIME_REPEAT; //開關時間設置為100Key_Flag[i] |= KEY_LONG;//KEY_LONG = 0010 0000,則對應標志位bit5 置1其它位保持S[i] = 4;  //開關由1態進人4態,}}else if (S[i] == 2) //如果對應開關處于2態{if (CurrState[i] == KEY_PRESSED)//如果檢測到開關處于按住狀態{Key_Flag[i] |= KEY_DOUBLE;//對應開關設置標志位 KEY_DOUBLE = 0001 0000,bit4 置1其它位保持S[i] = 3;//開關進入狀態3,說明按鍵已然雙擊}else if (Time[i] == 0)//否則的話檢測對應開關的時間是否為0{Key_Flag[i] |= KEY_SINGLE; //對應開關設置標志位 KEY_SINGLE = 0000 1000,bit3 置1其它位保持S[i] = 0;  //開關狀態由2態回到狀態0}}else if (S[i] == 3)//如果對應開關處于狀態3{if (CurrState[i] == KEY_UNPRESSED)//如果對應開關現態是彈起{S[i] = 0;//開關由3態回到狀態0}}else if (S[i] == 4)//如果對應開關處于狀態4{if (CurrState[i] == KEY_UNPRESSED)//如果對應開關現態是彈起{S[i] = 0;//開關由4態回到狀態0}else if (Time[i] == 0)//如果對應時間是0{Time[i] = KEY_TIME_REPEAT; //對應開關設置為重復按鍵時間100Key_Flag[i] |= KEY_REPEAT; //對應開關標志位設置為 KEY_REPEAT = 0100 0000 bit6 置1其它位保持S[i] = 4;//開關已然處于4態}}}}
}

添一下編程思路


當然這篇也不是來分享注釋的,縱觀江科大的代碼,在開關穩態的判斷上稍顯簡陋。因此筆者擴展了一下代碼,1)現在代碼對穩態有了更強的判斷 2)對數組的邊界進行了判斷,防止超過邊界程序不工作。

????????對應代碼移植的注意點:1)確定KEY_COUNT的值以降低資源的消耗不一定要一直設置為4,用幾個設置幾個就行? 2)添加了按鍵開關宏定義? ?3)添加了GPIO口宏定義?

在這幾個宏定義里修改參數無需在模塊中修改即可拿來使用。一般來說雙擊是很少用的功能,關于如果屏蔽這個功能,宏定義中#define KEY_TIME_DOUBLE? ?1原先的200改成1,程序就不會檢測到雙擊,就只剩單擊和長按功能了。

筆者的穩態加強判斷代碼:

/*有歷史的開關狀態判定*/
uint16_t Key_SteadyState(uint8_t n) 
{if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}
if (n == KEY_1){if (Keybuf[KEY_1] == 0xFFFF){return KEY_PRESSED;}}if (n == KEY_2){if (Keybuf[KEY_2] == 0xFFFF){return KEY_PRESSED;}}if (n == KEY_3){if (Keybuf[KEY_3] == 0xFFFF){return KEY_PRESSED;}}   if (n == KEY_4){if (Keybuf[KEY_4] == 0xFFFF){return KEY_PRESSED;}} return KEY_UNPRESSED;
}

????????這也是筆者學51時候宋老師給出的編程思路,按下開關獲得的狀態是1,每1ms進入中斷一次并移位一次把結果存入緩沖值Keybuf[i],因此只要判斷Keybuf的值就能知道過去的16ms是不是處于穩態。如果keybuf[i] =0x0000,則說明前16ms開關一直松開,如果keybuf[i] = 0xffff,則說明開關一直按著。當然本案的代碼是只要其中有1位沒置1,那么進行狀態判斷的時候就認為開關不是按著而是彈開.即對按著有更強的要求,反過來對松開是弱要求(在過去的16ms中如果電平有跳動就判斷松開)因為現在的程序的寫法是兩態判斷。考慮到每20ms才進行一次開關狀態判斷,而不是開關穩態發生變化就馬上進行功能判斷,在響應上會有一些延遲,對應響應要求快速的地方,該方案也許還要調整,因此要結合實際的應用場景選擇合適方案。

key.c

#include "stm32f10x.h"                  // Device header
#include "Key.h"
/*映射區*/#define KEY_PRESSED				1 //按鍵按下
#define KEY_UNPRESSED			0//按鍵松開/*開關閾值時間*/
#define KEY_TIME_DOUBLE			200//把該處的值調低比如設置為1那么程序就不可能檢測到雙擊,變相的屏蔽了雙擊功能
#define KEY_TIME_LONG			2000
#define KEY_TIME_REPEAT			100
/*GPIO口宏定義開關KEY_1 KEY_2都是GPIOB,第二組KEY_3 KEY_4也是GPIOB*/
#define GPIO_KEY_1 GPIO_Pin_1  
#define GPIO_KEY_2 GPIO_Pin_11
#define GPIOX_KEY1_KEY2     GPIOB
#define GPIO_KEY_3 GPIO_Pin_13
#define GPIO_KEY_4 GPIO_Pin_15
#define GPIOX_KEY3_KEY4     GPIOB/* Key_Flag bit6~bit0 分別代表REPEAT(bit6)、Long、Double、Single、UP、Down、HOLD,bit7是空位   */
uint8_t Key_Flag[KEY_COUNT] = {0};//定義全局變量標志位每個標志位互相獨立,不同的標志位代表不同的事件
uint16_t Keybuf[KEY_COUNT] = {0x0000};void Key_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);/*按鍵使能GPIO初始化,常態是高電平按下是低電平*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_KEY_1 | GPIO_KEY_2;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOX_KEY1_KEY2, &GPIO_InitStructure);/*按鍵使能GPIO初始化,常態是低電平按下是高電平*/GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;GPIO_InitStructure.GPIO_Pin = GPIO_KEY_3 | GPIO_KEY_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOX_KEY3_KEY4, &GPIO_InitStructure);
}
/*檢測當前按鍵的電平,并返回相應的電平信息即開關狀態*/
uint16_t Key_GetState(uint8_t n) 
{if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}if (n == KEY_1){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return KEY_PRESSED;}}else if (n == KEY_2){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return KEY_PRESSED;}}else if (n == KEY_3){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1){return KEY_PRESSED;}}else if (n == KEY_4){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1){return KEY_PRESSED;}}return KEY_UNPRESSED;
}
/*有歷史的開關狀態判定*/
uint16_t Key_SteadyState(uint8_t n) 
{if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}
if (n == KEY_1){if (Keybuf[KEY_1] == 0xFFFF){return KEY_PRESSED;}}if (n == KEY_2){if (Keybuf[KEY_2] == 0xFFFF){return KEY_PRESSED;}}if (n == KEY_3){if (Keybuf[KEY_3] == 0xFFFF){return KEY_PRESSED;}}   if (n == KEY_4){if (Keybuf[KEY_4] == 0xFFFF){return KEY_PRESSED;}} return KEY_UNPRESSED;
}/*** 函    數:開關狀態檢測函數* 參    數:uint8_t n 指定開關編號,可在 KEY_1  KEY_2 KEY_3 KEY_4選擇如需擴展可在KEY.h中添加,KEY_1的值是0 KEY_2的值是1,看頭文件的定義* 參    數:uint8_t Flag指定開關的狀態可在下面參數中選擇
//                 KEY_HOLD :按住開關
//                 KEY_DOWN :按下開關
//                 KEY_UP   :開關彈起 這三項一般不作為參數檢測KEY_SINGLE :單擊KEY_DOUBLE :雙擊KEY_LONG   : 長按KEY_REPEAT : 重復,即一直按住。 后4項狀態是互斥的但是和前三項可以共存
* 返 回 值:如果開關狀態是例舉(uint8_t Flag)的狀態則返回1,如果不是則返回0
* 注意事項:開關狀態Key_Flag[n]不一定和uint8_t Flag相同*/
uint8_t Key_Check(uint8_t n, uint8_t Flag)//開關狀態檢測
{/* 添加邊界檢查:確保 n 在有效索引范圍內 */if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}if (Key_Flag[n] & Flag)//開關的7個狀態FLAG一個狀態只占一位比如單擊 0000 1000,只有開關確實是處于單擊狀態開關的結果才是非0的{if (Flag != KEY_HOLD) //這個函數就是保證不清除bit0位置的狀態{Key_Flag[n] &= ~Flag; //相應的bit控制位清0,其它位保持}return 1;}return 0;
}void Key_Tick(void)
{static uint8_t Count, i;//定義靜態變量i和Countstatic uint8_t CurrState[KEY_COUNT], PrevState[KEY_COUNT];//定義現態和前態static uint8_t S[KEY_COUNT] ={0};//定義狀態static uint16_t Time[KEY_COUNT] = {0};//定義開關時間for (i = 0; i < KEY_COUNT; i ++)//遍歷所有的開關,KEY_COUNT的值在Key.H中定義了{Keybuf[i] = (Keybuf[i] << 1) | Key_GetState(i) ;//當前的開關狀態賦值給狀態監控數組if (Time[i] > 0){Time[i] --; //對應開關的時間減1}}Count ++;if (Count >= 20)//每20ms進入一次這個函數{Count = 0;for (i = 0; i < KEY_COUNT; i ++)//歷遍所有的開關狀態{PrevState[i] = CurrState[i];//當Key_GetState(i)獲得新狀態時代表著CurrState里面的狀態就是前態了CurrState[i] =Key_SteadyState(i)  ;//把穩定的開關狀態賦值給現態if (CurrState[i] == KEY_PRESSED)//如果檢測到開關按下則標志位HOLD置1{Key_Flag[i] |= KEY_HOLD; //KEY_HOLD = 0000 0001則bit 0 置1其它位保持。}else//如果沒檢測到開關按下則標志位HOLD置0{Key_Flag[i] &= ~KEY_HOLD;//,~KEY_HOLD = 1111 1110 則bit 0置0其它位保持。}if (CurrState[i] == KEY_PRESSED && PrevState[i] == KEY_UNPRESSED)//如果現態是按下前態是沒有按下{Key_Flag[i] |= KEY_DOWN;   // KEY_DOWN = 0000 0010 則bit 1置1其它位保持}if (CurrState[i] == KEY_UNPRESSED && PrevState[i] == KEY_PRESSED)//如果現態是彈起前態是按下{Key_Flag[i] |= KEY_UP;  // KEY_UP = 0000 0100則bit 2 置1其它位保持}if (S[i] == 0) //如果對應開關處于空閑狀態{if (CurrState[i] == KEY_PRESSED) //如果現態的開關狀態時按下{Time[i] = KEY_TIME_LONG; //對應開關的時間設置為2000即長按檢測S[i] = 1;//開關由0態進入1態}}else if (S[i] == 1) //如果對應開關處于狀態1{if (CurrState[i] == KEY_UNPRESSED)//如果現態開關狀態是彈起{Time[i] = KEY_TIME_DOUBLE; //對應開關時間設置為200S[i] = 2;//開關由1態進入2態}else if (Time[i] == 0) //如果對應開關時間為0{Time[i] = KEY_TIME_REPEAT; //開關時間設置為100Key_Flag[i] |= KEY_LONG;//KEY_LONG = 0010 0000,則對應標志位bit5 置1其它位保持S[i] = 4;  //開關由1態進人4態,}}else if (S[i] == 2) //如果對應開關處于2態{if (CurrState[i] == KEY_PRESSED)//如果檢測到開關處于按住狀態{Key_Flag[i] |= KEY_DOUBLE;//對應開關設置標志位 KEY_DOUBLE = 0001 0000,bit4 置1其它位保持S[i] = 3;//開關進入狀態3,說明按鍵已然雙擊}else if (Time[i] == 0)//否則的話檢測對應開關的時間是否為0{Key_Flag[i] |= KEY_SINGLE; //對應開關設置標志位 KEY_SINGLE = 0000 1000,bit3 置1其它位保持S[i] = 0;  //開關狀態由2態回到狀態0}}else if (S[i] == 3)//如果對應開關處于狀態3{if (CurrState[i] == KEY_UNPRESSED)//如果對應開關現態是彈起{S[i] = 0;//開關由3態回到狀態0}}else if (S[i] == 4)//如果對應開關處于狀態4{if (CurrState[i] == KEY_UNPRESSED)//如果對應開關現態是彈起{S[i] = 0;//開關由4態回到狀態0}else if (Time[i] == 0)//如果對應時間是0{Time[i] = KEY_TIME_REPEAT; //對應開關設置為重復按鍵時間100Key_Flag[i] |= KEY_REPEAT; //對應開關標志位設置為 KEY_REPEAT = 0100 0000 bit6 置1其它位保持S[i] = 4;//回到狀態4}}}}
}

key.h

#ifndef __KEY_H
#define __KEY_H/*使能開關的個數,根據工程的需要更該參數*/
#define KEY_COUNT				4
/*開關命名索引,宏命令取的值與for循環的i有關且是一一對應的因此取值要連續不能隨意錯位,跳過某個數取值的方式,比如0,4,1,3這種順序是不允許的*/
#define KEY_1					0
#define KEY_2					1
#define KEY_3					2
#define KEY_4					3
/* 參數修改區,按工程需要重命名開關名字*/
#define Key_Safe          KEY_1   // 安全開關
#define Key_Start         KEY_2   // 啟動開關
#define Key_Stop          KEY_3   // 停止開關
#define Key_Reset         KEY_4   // 復位開關#define KEY_HOLD				0x01 //0000 0001
#define KEY_DOWN				0x02 //0000 0010
#define KEY_UP					0x04 //0000 0100
#define KEY_SINGLE				0x08 //0000 1000
#define KEY_DOUBLE				0x10 //0001 0000
#define KEY_LONG				0x20 //0010 0000
#define KEY_REPEAT				0x40 //0100 0000void Key_Init(void);
uint8_t Key_Check(uint8_t n, uint8_t Flag);
void Key_Tick(void);#endif

從邏輯上來講后面的程序對穩態的判斷更嚴格,不同場景的使用要求可以按需調整,比如穩態判斷采用8位的,那么時間就是 8ms,count的值改小一點。

貼一個對開關穩態強要求的即在原先的方案上再稍微調整一下

#define KEY_PRESSED				1 //按鍵按下
#define KEY_UNPRESSED			0//按鍵松開
#define KEY_Unsteadystate      2//開關不穩定/*有歷史的開關狀態判定*/
uint16_t Key_SteadyState(uint8_t n) 
{if (n >= KEY_COUNT) {return KEY_Unsteadystate;  // 索引越界時也歸于開關處于不穩定狀態}if (Keybuf[n] == 0xFFFF) {return KEY_PRESSED;}else if (Keybuf[n] == 0x0000) {return KEY_UNPRESSED;}return KEY_Unsteadystate;
}

當然如果你要用這個開關三態的函數的話后續的函數也要調整一下

if (CurrState[i] == KEY_PRESSED && PrevState[i] != KEY_PRESSED)//如果現態是按下前態是沒有按下{Key_Flag[i] |= KEY_DOWN;   // KEY_DOWN = 0000 0010 則bit 1置1其它位保持}if (CurrState[i] == KEY_UNPRESSED && PrevState[i] != KEY_UNPRESSED)//如果現態是彈起前態是按下{Key_Flag[i] |= KEY_UP;  // KEY_UP = 0000 0100則bit 2 置1其它位保持}

基于此筆者又修改了程序,現在開關不在是20ms進行一次狀態判斷,而是程序檢測到開關穩態發生變化的時候,立刻就進入開關動作函數進行工作。

key.c

#include "stm32f10x.h"                  // Device header
#include "Key.h"
/*映射區*/#define KEY_PRESSED				1 //按鍵按下
#define KEY_UNPRESSED			0//按鍵松開
#define KEY_Unsteadystate      2//開關不穩定/*開關閾值時間*/
#define KEY_TIME_DOUBLE			200//把該處的值調低比如設置為1那么程序就不可能檢測到雙擊,變相的屏蔽了雙擊功能
#define KEY_TIME_LONG			2000
#define KEY_TIME_REPEAT			100
/*GPIO口宏定義開關KEY_1 KEY_2都是GPIOB,第二組KEY_3 KEY_4也是GPIOB*/
#define GPIO_KEY_1 GPIO_Pin_1  
#define GPIO_KEY_2 GPIO_Pin_11
#define GPIOX_KEY1_KEY2     GPIOB
#define GPIO_KEY_3 GPIO_Pin_13
#define GPIO_KEY_4 GPIO_Pin_15
#define GPIOX_KEY3_KEY4     GPIOB/* Key_Flag bit6~bit0 分別代表REPEAT(bit6)、Long、Double、Single、UP、Down、HOLD,bit7是空位   */
uint8_t Key_Flag[KEY_COUNT] = {0};//定義全局變量標志位每個標志位互相獨立,不同的標志位代表不同的事件
uint16_t Keybuf[KEY_COUNT] = {0x0000};//開關穩態指示void Key_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);/*按鍵使能GPIO初始化,常態是高電平按下是低電平*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_KEY_1 | GPIO_KEY_2;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOX_KEY1_KEY2, &GPIO_InitStructure);/*按鍵使能GPIO初始化,常態是低電平按下是高電平*/GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;GPIO_InitStructure.GPIO_Pin = GPIO_KEY_3 | GPIO_KEY_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOX_KEY3_KEY4, &GPIO_InitStructure);
}
/*檢測當前按鍵的電平,并返回相應的電平信息即開關狀態*/
uint16_t Key_GetState(uint8_t n) 
{if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}if (n == KEY_1){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return KEY_PRESSED;}}else if (n == KEY_2){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return KEY_PRESSED;}}else if (n == KEY_3){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 1){return KEY_PRESSED;}}else if (n == KEY_4){if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_15) == 1){return KEY_PRESSED;}}return KEY_UNPRESSED;
}
/*有歷史的開關狀態判定*/
uint16_t Key_Get_SteadyState(uint8_t n) 
{if (n >= KEY_COUNT) {return KEY_Unsteadystate;  // 索引越界時,歸于開關處于穩定狀態}if (Keybuf[n] == 0xFFFF) {return KEY_PRESSED;}else if (Keybuf[n] == 0x0000) {return KEY_UNPRESSED;}return KEY_Unsteadystate;
}/*** 函    數:開關狀態檢測函數* 參    數:uint8_t n 指定開關編號,可在 KEY_1  KEY_2 KEY_3 KEY_4選擇如需擴展可在KEY.h中添加,KEY_1的值是0 KEY_2的值是1,看頭文件的定義* 參    數:uint8_t Flag指定開關的狀態可在下面參數中選擇
//                 KEY_HOLD :按住開關
//                 KEY_DOWN :按下開關
//                 KEY_UP   :開關彈起 這三項一般不作為參數檢測KEY_SINGLE :單擊KEY_DOUBLE :雙擊KEY_LONG   : 長按KEY_REPEAT : 重復,即一直按住。 后4項狀態是互斥的但是和前三項可以共存
* 返 回 值:如果開關狀態是例舉(uint8_t Flag)的狀態則返回1,如果不是則返回0
* 注意事項:開關狀態Key_Flag[n]不一定和uint8_t Flag相同*/
uint8_t Key_Check(uint8_t n, uint8_t Flag)//開關狀態檢測
{/* 添加邊界檢查:確保 n 在有效索引范圍內 */if (n >= KEY_COUNT) {return 0;  // 索引越界時直接返回0,不執行后續操作}if (Key_Flag[n] & Flag)//開關的7個狀態FLAG一個狀態只占一位比如單擊 0000 1000,只有開關確實是處于單擊狀態開關的結果才是非0的{if (Flag != KEY_HOLD) //這個函數就是保證不清除bit0位置的狀態{Key_Flag[n] &= ~Flag; //相應的bit控制位清0,其它位保持}return 1;}return 0;
}void Key_Tick(void)
{static uint8_t  i;//定義靜態變量i和Countstatic uint8_t CurrState[KEY_COUNT] ={0}, PrevState[KEY_COUNT]={0};//定義現態和前態static uint8_t S[KEY_COUNT] ={0};//定義狀態static uint16_t Time[KEY_COUNT] = {0};//定義開關時間for (i = 0; i < KEY_COUNT; i ++)//遍歷所有的開關,KEY_COUNT的值在Key.H中定義了{Keybuf[i] = (Keybuf[i] << 1) | Key_GetState(i) ;//當前的開關狀態賦值給狀態監控數組PrevState[i] = CurrState[i];//當Key_GetState(i)獲得新狀態時代表著CurrState里面的狀態就是前態了CurrState[i] =Key_Get_SteadyState(i)   ;//把穩定的開關狀態賦值給現態if (Time[i] > 0){Time[i] --; //對應開關的時間減1}}for (i = 0; i < KEY_COUNT; i ++)//歷遍所有的開關狀態{if (CurrState[i] == KEY_PRESSED)//如果檢測到開關按下則標志位HOLD置1{Key_Flag[i] |= KEY_HOLD; //KEY_HOLD = 0000 0001則bit 0 置1其它位保持。}else//如果沒檢測到開關按下則標志位HOLD置0{Key_Flag[i] &= ~KEY_HOLD;//,~KEY_HOLD = 1111 1110 則bit 0置0其它位保持。}if (CurrState[i] == KEY_PRESSED && PrevState[i] != KEY_PRESSED)//如果現態是按下前態是沒有按下{Key_Flag[i] |= KEY_DOWN;   // KEY_DOWN = 0000 0010 則bit 1置1其它位保持}if (CurrState[i] == KEY_UNPRESSED && PrevState[i] != KEY_UNPRESSED)//如果現態是彈起前態是按下{Key_Flag[i] |= KEY_UP;  // KEY_UP = 0000 0100則bit 2 置1其它位保持}if(PrevState[i] != CurrState[i] && CurrState[i]!= KEY_Unsteadystate )//進入這個函數說明開關有動作從1個穩態到另一個穩態{if(S[i] == 2 )//判斷函數是狀態2,說明函數從長按和重復按鍵狀態退出{S[i] = 0;//開關從2態恢復到0態,Time[i] = 0; //計數時間復位}else if (S[i] == 0) //如果對應開關處于空閑狀態{if (CurrState[i] == KEY_PRESSED) //如果現態的開關狀態時按下{Time[i] = KEY_TIME_LONG; //對應開關的時間設置為2000即長按檢測S[i] = 1;//開關由0態進入1態}}else if(S[i] == 1)//判斷函數是狀態1說明開關進入了短鍵模式,至于是單擊還是雙擊看后續判斷{                  if(Time[i] > 0)//即開關閾值時間沒到達長按設置的時間狀態就發生了變化{S[i] = 3; //進入3態,即進入短鍵判斷模式Time[i] = KEY_TIME_DOUBLE; //設置單擊還是雙擊的閾值時間                  }                                   }else  if(S[i] == 3){if(Time[i] > 0) //在狀態3的情況下又進入了函數并且還沒到達閾值時間說明發生了雙擊{Key_Flag[i] |= KEY_DOUBLE;  //雙擊S[i] = 0;//回到狀態0                 }else{                  }                }                                                }else //進入這里說明開關穩態沒有發生變化{if(S[i] == 1 && Time[i] == 0)//到達長按閾值時間{Key_Flag[i] |= KEY_LONG; //長按Time[i] = KEY_TIME_REPEAT;S[i] = 2;//開關進入狀態2}else if(S[i]== 2)//進入重復按著的情況{if(Time[i] == 0)//100ms的重復又到了0{Key_Flag[i] |= KEY_REPEAT; //設置重復按鍵標志Time[i] = KEY_TIME_REPEAT;//設置重復按鍵時間}}else if(S[i]==3 &&Time[i] == 0 )//在3態的情況下閾值時間到達了0,說明沒有雙擊{Key_Flag[i] |= KEY_SINGLE; //設置單擊按鍵標志S[i] = 0; //開關動作結束,回到狀態0}}}}

?然后是程序的流程圖

經過簡單的測試,江科大原先留下的測試程序都能正常工作。

整個程序開關其實進行了3次穩態變化判斷:1)按鍵彈起------>按鍵按下---------->按鍵再彈起------->按鍵再按下


開關狀態key_single Key_double Key_long Key_repeat Key_Up Key_Down這些開關狀態都需要及時在主函數中使用Key_Check函數清零,否則狀態位會一直保持。但是是Key_HOLD 這個標志位的狀態會隨著開關動作一直變化。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/87325.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/87325.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/87325.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【網絡原理】網絡原理簡單認識 —— 內含網絡通信基礎、五元組、網絡協議(OSI 七層協議、TCP/IP 五層(或四層)協議)、封裝和分用

目錄 1. 網絡互連 1.1 局域網LAN 1.2 廣域網WAN 2 網絡通信基礎 2.1 IP地址 2.2 端口號 2.3 網絡協議 3. 五元組 4. 協議分層 4.1 OSI 七層網絡模型 4.2 TCP/IP 五層&#xff08;或四層&#xff09;網絡模型 4.3 網絡設備所在分層(經典筆試題) 5. 網絡數據傳輸的基…

嵌入式之硬件學習(三)通信方式、串口通信

目錄 一、通信種類 1、并行通信 2、串行通信 3、單工模式(Simplex Communication) 4、半雙工通信(Half-Duplex Communication) 5、全雙工通信(Full-Duplex Communication) 6、串行的異步通信與同步通信 &#xff08;1&#xff09;異步通信 &#xff08;2&#xff09;同…

【微信小程序】3、SpringBoot整合WxJava發送訂閱消息

1、創建消息模板 在公共模板庫里面選擇符合自己業務場景的消息模板&#xff0c;例如&#xff1a; 每個消息模板最多選擇5項&#xff0c;可根據自己業務需求自行選擇&#xff0c;順序也可以自己決定。提交后&#xff0c;我們就得到了屬于自己的消息模板ID 2、文檔閱讀 官方文…

Flask 快速精通:從入門到實戰的輕量級 Web 框架指南

Flask 作為 Python 生態中最受歡迎的輕量級 Web 框架&#xff0c;以其簡潔靈活的設計理念贏得了開發者的青睞。本文將系統梳理 Flask 的核心概念與實戰技巧&#xff0c;幫助你快速掌握這一強大框架。 一、Flask 框架概述 1.1 輕量級框架的核心特性 Flask 誕生于 2010 年&…

Python爬取豆瓣短評并生成詞云分析

一、項目概述 本項目的目標是爬取豆瓣上某部電影的短評數據&#xff0c;并生成詞云進行情感分析。我們將使用Python編程語言&#xff0c;借助爬蟲技術獲取數據&#xff0c;并利用自然語言處理和數據可視化工具進行分析。具體步驟包括&#xff1a; 爬取豆瓣短評數據。數據清洗…

Controller Area Network (CAN) 通信機制簡介

目錄 1. CAN 概述 2. 物理結構與傳輸機制 3. 消息格式與仲裁機制 4. 錯誤檢測與總線狀態 5. 工業用 CAN 接口 6. 本講總結 1. CAN 概述 CAN&#xff08;Controller Area Network&#xff09;是由德國博世&#xff08;Bosch&#xff09;公司于 1983 年提出的串行通信協議…

我有一個想法

我有一個想法 我想為家鄉做點事情&#xff0c;但是又不知道從哪里開始。 也許為家鄉的教育做點事情是比較靠譜的。 于是&#xff0c;我就想到了&#xff0c;是不是可以在高中學校&#xff0c;設立一個“鴻鵠”獎學金&#xff1f; 這個獎學金怎么使用呢&#xff1f; 在每年9月份…

【Pandas】pandas DataFrame stack

Pandas2.2 DataFrame Reshaping sorting transposing 方法描述DataFrame.droplevel(level[, axis])用于**從 DataFrame 的索引&#xff08;行或列&#xff09;中刪除指定層級&#xff08;level&#xff09;**的方法DataFrame.pivot(*, columns[, index, values])用于重塑 Dat…

Java 自動關閉資源語法糖 - try-with-resources

文章目錄 Java 自動關閉資源語法糖 - try-with-resources前言優勢1、自動資源管理2、處理多重資源3、異常處理更健壯4、適用條件 總結 Java 自動關閉資源語法糖 - try-with-resources 前言 日常開發中&#xff0c;我們經常會看到如下代碼&#xff1a; try (InputStream is …

MyBatis中的動態SQL是什么?

大家好&#xff0c;我是鋒哥。今天分享關于【MyBatis中的動態SQL是什么&#xff1f;】面試題。希望對大家有幫助&#xff1b; MyBatis中的動態SQL是什么&#xff1f; 超硬核AI學習資料&#xff0c;現在永久免費了&#xff01; MyBatis中的動態SQL指的是根據不同的條件&#x…

【Java反射】如何新增對象中的屬性,與JavaScript中的直接添加屬性有什么區別?

問&#xff1a; Object obj new Object(); //獲取一個類的class對象 Class<?> objClass Object.class; try { //通過newInstance方法創建一個新的屬性 Field newField Field.class.newInstance(); newField.setAccessible(true); newField.set(obj, “index”); }ca…

java spring boot Swagger安裝及使用

https://springdoc.org/ 可能原因分析 &#x1f50d; 原因 1&#xff1a;SpringFox 版本與 Spring Boot 版本不兼容 ? SpringFox 3.0.0 不完全兼容 Spring Boot 2.6 及更高版本&#xff0c;可能導致 NullPointerException。 Spring Boot 3.x 完全不支持 SpringFox&#xff0c…

電商云倉/前置倉的物流高效監控、管理、預警系統,快遞鳥DMS

在電商行業蓬勃發展的當下&#xff0c;電商云倉和前置倉作為物流配送體系的關鍵環節&#xff0c;其高效運作直接影響著消費者體驗與企業競爭力。快遞鳥 DMS 物流交付管理平臺&#xff0c;以其卓越的物流監控、管理及預警功能&#xff0c;成為電商企業優化云倉和前置倉物流管理的…

HarmonyOS Next深度解析:高德定位SDK高效集成與實戰指南

HarmoyOS Next 實現高德定位SDK 注&#xff1a;作者采用版本為 HarmonyOS 5.0.0 Release SDK和DevEco Studio 5.0.0 Release。 1.獲取本地AppID&#xff1a; 在index.pages的abountToAppear( ) 方法中獲取appID、并打印在Log日志&#xff0c;即可在程序運行時獲取本地項目的…

【技術】記一次 Docker 中的 ES 數據遷移,使用 Reindex API

記一次 Docker 中的 ES 數據遷移&#xff0c;使用 Reindex API 環境背景需求背景開始遷移確認老 ES 的訪問地址在新 ES 中創建索引的 Mapping (選配)在新 ES 中配置老 ES 的地址開始遷移數據數據驗證 首先聲明&#xff0c;是因為環境限制&#xff0c;沒有辦法使用同步工具&…

yii2基礎版本安裝記錄,實錄有點亂看標題即可

因為使用php 安裝的是docker環境所有進入到容器安裝ridh_mfe_api 為掛載目錄 Nginx及PHP掛載配置 因為使用php 安裝的是docker環境所有進入到容器安裝 ridh_mfe_api 為掛載目錄 進入容器 % docker exec -it php sh /var/www/html # ls index.html index.php composer crea…

前端跨域解決方案(3):CORS

1 CORS 核心 CORS&#xff08;Cross-Origin Resource Sharing&#xff09;&#xff0c;即跨域資源共享&#xff0c;是目前最主流的跨域方案&#xff0c;它通過服務器返回的特殊 HTTP 頭&#xff0c;允許瀏覽器放行跨域請求。與傳統的 JSONP 相比&#xff0c;CORS 具有明顯的優…

SpringBoot源碼解析(十五):spring-boot-autoconfigure.jar的模塊化設計

前言 SpringBoot的自動配置是其革命性特性的核心&#xff0c;而spring-boot-autoconfigure.jar則是這一機制的物理載體。本文將深入剖析這個JAR包的模塊化設計哲學&#xff0c;從包結構劃分、條件注解體系到自動配置加載機制&#xff0c;全方位解析SpringBoot如何通過精妙的模…

學習筆記九:docker容器日志問題

docker容器日志問題 背景如何處理日志問題主要通過日志輪詢方式處理。修改 Docker 配置日志快速清理 背景 Docker 默認使用的是 json-file 日志驅動。日志會一直寫&#xff0c;一直寫&#xff0c;沒有限制、沒有輪轉、沒有清理&#xff01; 日志默認位置&#xff1a; /var/lib…

低成本同屏方案:電腦 + 路由器實現 50 臺安卓平板實時同屏

引言 在教育機構、小型培訓場景或企業簡易會議中&#xff0c;常面臨以最低成本實現多設備同屏的需求。本文針對 "電腦 路由器 50 臺安卓平板" 的極簡硬件組合&#xff0c;詳細剖析實時同屏的實現路徑&#xff0c;從問題分析到技術落地提供全流程解決方案&#xff0…