初学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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…