状态机解决按键状态捕获问题(二)
任务介绍
上篇文章中我们通过状态机编程解决了非阻塞式的按键扫描操作,本次我们将会在原有函数的基础上进行功能改进,增加按键的状态尝试捕获按键的双击和长按操作,同时增加扫描按键的数量
代码编写
此次代码是在上一次代码的基础上进行修改的,配置都保持原来的状态,因此在此就不再介绍项目配置的相关操作了,接下来我们照旧先分析一下本次编写代码的思路
代码编写思路
总体思路介绍
由于在本次程序中需要判断按键的长按和双击操作,因此我们需要考虑每次按下的时长以及按键之间的时间间隔的问题,已知我们的按键扫描函数是在基本定时器6的中断服务函数中被调用的,且定时器6的中断每10ms触发一次。为了能够记录时间长短在此我们使用次数来代表时间,程序中按键按下时长超过一秒则说明按键处于长按状态,如果两次按键按下的时间间隔小于300ms则说明此时按键被双击,我们采用宏定义来存储时间信息
宏定义介绍
c++
//扫描周期
#define KEY_SCAN_PERIOD_MS 10
//长按时间判定,单按键按下时间超过1秒钟时说明按键处于长按状态
#define KEY_LONG_MS 1000
//双击时间间隔,当两次按键按下的时间间隔在300ms以内证明按键被双击了
#define KEY_DOUBLE_MS 300
//将时间转换为扫描的次数
#define KEY_LONG_TIMES (KEY_LONG_MS / KEY_SCAN_PERIOD_MS)
#define KEY_DOUBLE_TIMES (KEY_DOUBLE_MS / KEY_SCAN_PERIOD_MS)
上述宏定义成功将时间信息转换为次数信息,便于后续在函数中进行比较,时间转换为次数的操作方法为:函数每10ms被调用一次,则将对应的事件间隔转换为ms单位后除以10,这样就能够获得对应的次数信息
枚举和结构体类型介绍
同时我们创建了新的事件枚举来记录发生的事件,事件的作用是将不同的状态联系起来,它的作用就像是系统里的通讯员,代表着某件有意义的事情刚刚发生了,事件就是大脑经过思考后得出的结果,每次底层状态机检测到电平信号发生变化时机会触发相应的事件,主循环中会根据事件来做出相应的行动,事件枚举的内容如下
c++
typedef enum
{
KEY_EVENT_NONE = 0, //未发生事件
KEY_EVENT_SHORT, //短按事件
KEY_EVENT_LONG, //长按事件
KEY_EVENT_DOUBLE, //双击事件
} KeyEvent;
最底层的按键消抖状态枚举和上一次保持一致
c++
typedef enum
{
KEY_STATE_IDLE = 0, //空闲状态
KEY_STATE_CONFIRM, //确认状态
KEY_STATE_PRESSED, //按下状态
KEY_STATE_WAIT_RELEASE, //等待抬起状态
} KeyState;
最后定义一个结构体来存储一个按键的所有属性,将所有属性封装在一起体现了面向对象的编程思想,便于后续的统一操作和管理,由于当前的按键扫描是非阻塞的(每隔10ms就会进来扫描一次,不会在这里死等,所以我们必须将当前的进度保存下来,等到下一次进来的时候再接着往下走,这个结构体就是我们的存储器,用来存储当前的进度,其内容如下
c++
typedef struct
{
KeyState state;
uint16_t press_times; //记录当前这次按下持续了多少个周期
uint16_t gap_times; //第一次松开按键到第二次按键按键中间经历了多少个周期
uint8_t click_count; //记录在一轮操作中按下的次数
KeyEvent event;
} KeyEx;
在此再介绍一下结构中各个成员的作用:
state:是按键状态枚举型变量,其作用为记录当前走到哪一步了(是刚按下还是在按着又或者是在等待双击)press_times和gap_times:充当秒表,由于不能使用延时函数进行死等操作,因此只能够依靠每次定时器中断进入扫描函数时来给这两个变量自增来积累时间,通过它们系统才能判断出按键是否达到了长按的状态或者松开的时间是否超过了双击的时间间隔来判断是否达到了双击的条件click_count:记录用户是第几次按下按键,用于区分双击和单击
至此程序中涉及到相关状态的全局变量就介绍完毕了,接下来要做的事情就是如何利用上述的这些变量来将整个程序的状态转换关系表示出来
流程转换过程介绍

结合上述流程图我们对整体的状态转换能够有一个大体的了解,接下来我会再详细的介绍一下整体的流程转换逻辑。在当前的项目中,整体的状态转换是由一个后台定时器(TIM6)驱动的,定时器每隔10ms会触发一次中断,调用按键扫描函数,这就好像一个巡逻员,每隔10ms就会去查看一次按键引脚当前的电平状态,并根据当前所处状态 和引脚电平变化来决定下一步去哪,根据图片我们可以梳理出一条清晰的状态转换路径:
-
空闲态
按键初始时保持空闲的状态
- 当前状态:用户没有按下按键,引脚为高电平,所有计数器理论上都是清零的
- 转换条件:一旦扫描函数检测到按键引脚电平变为了低电平,就会触发状态转换
- 动作与去向:此时系统认为按键疑似按下,此时会切换到确认状态,进行按键的软件消抖操作
-
确认态
按键按下时会产生硬件抖动,因此这里需要进行软件消抖
- 当前情况:系统中已经产生了一次低电平,现在相较于第一次产生低电平过去了
10ms,此时再次检测按键引脚电平 - 转换路径A(确认成功):如果此时引脚电平仍旧为低电平,说明不是抖动而是真的按下了,此时切换状态为按下态,并清零时间计数器
- 转换路径B(假按下/抖动):如果引脚变回了高电平,说明刚才产生的电平信号只是一个干扰信号,此时退回空闲状态
- 当前情况:系统中已经产生了一次低电平,现在相较于第一次产生低电平过去了
-
按下态(核心判断状态)
在这个状态下,系统主要用来计算用户按下了多久的时间,每隔
10ms进行一次扫描都会在函数内部对press_times变量进行自增操作,将该变量的值乘以10就可以得到大致的按键按下持续时长- 转换路径A(触发长按):如果用户一直按着不松手,当
press_times的值超过100时则直接产生长按事件,触发完长按事件后此时状态仍然停留在按下态,直到用户松手 - 转化路径B(第一次松开):如果用户松手了
- 情况1:如果刚才已经触发过长按事件了,那么这次松开代表整个动作结束,直接回到空闲态
- 情况2:如果没有触发长按说明这是一次短按,系统会将记录本轮按键按下次数的变量进行自增操作,并切换到等待态,等待看看有没有第二次点击
- 转换路径C(第二次松开):如果这是双击的第二次松开(即全局变量的值已经是1的情况下),此时系统判定双击成立,生成双击事件,随后清理标志位并切换到空闲态
- 转换路径A(触发长按):如果用户一直按着不松手,当
-
等待态(等待双击状态)
这个状态是为了判定当前的按键按下究竟是一次孤立的单击还是双击操作的第一次按键按下,每隔
10ms会扫描一次,每次在扫描函数中都会让gap_times自增以记录按键按下的空闲时间- 转换路径A(超时,确认为单击操作):如果用户再指定时间间隔内没有按下第二次,当
gap_times的值超过了指定的阈值,此时系统会直接生成短按事件,然后回到空闲态 - 转换路径B(超时前按下了按键):如果在
300ms内,按键的引脚电平再次发生了变化,那么此时会切换到确认态为新的按键按下进行消抖处理
- 转换路径A(超时,确认为单击操作):如果用户再指定时间间隔内没有按下第二次,当
简化版状态转换流程
-
单击路线:空闲态 -> 确认态 -> 按下态 -> 等待态 -> (超时) -> 触发单击 -> 空闲态
-
长按路线:空闲态 -> 确认态 -> 按下态 -> (一直按着超时) -> 触发长按 -> 按下态 -> (松开) -> 空闲态
-
双击路线:空闲态 -> 确认态 -> 按下态 -> 等待态 -> (马上又按) -> 确认态 -> 按下态 -> (松手) -> 触发双击 -> 空闲态
按键扫描函数代码
下面就是按键扫描函数的代码实现
c++
void Key_scan_states_one(GPIO_TypeDef *port, uint16_t pin, KeyEx *k)
{
GPIO_PinState level = HAL_GPIO_ReadPin(port, pin);
switch (k->state)
{
case KEY_STATE_IDLE:
k->event = KEY_EVENT_NONE;
k->press_times = 0;
k->gap_times = 0;
k->click_count = 0;
//电平发生变化,说明此时按键疑似按下,接下来需要进步验证
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
break;
//接下来进入验证状态,在验证状态中需要检测按键是否真的已经按下
case KEY_STATE_CONFIRM:
//再次读取信息以确定按键状态
if(level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_PRESSED;
k->press_times = 0;
}
else
{
//此时说明前面的按下是抖动误触,此时将状态返回到空闲态等待下一次电平变化
k->state = KEY_STATE_IDLE;
}
break;
//当确定按键第一次按下后需要记录按下的持续时间以进行下一步操作
case KEY_STATE_PRESSED:
//记录按下状态的持续时间
k->press_times++;
//长按判定,达到指定时间则更改状态为长按状态
if (k->press_times == KEY_LONG_TIMES)
{
k->event = KEY_EVENT_LONG;
}
//状态变化说明按键抬起,表明此时不是长按状态
if (level == GPIO_PIN_SET)
{
//此时进入短按一次状态
if (k->press_times < KEY_LONG_TIMES)
{
k->click_count++;
//如果这是第一次短按则进入等待,尝试等待第二次按键按下
if (k->click_count == 1)
{
//状态更新为等待按键松开
k->state = KEY_STATE_WAIT_RELEASE;
//重置间隔时间
k->gap_times = 0;
}
else
{
//此时说明第二次短按也完成,则判定进入了双击状态
k->event = KEY_EVENT_DOUBLE;
//一轮判断完成,将状态恢复为空闲态
k->state = KEY_STATE_IDLE;
}
}
else
{
//此时已经触发过了长按,当不再短按时直接回到空闲态
k->state = KEY_STATE_IDLE;
}
}
break;
case KEY_STATE_WAIT_RELEASE:
//记录两次按键按下的时间间隔
k->gap_times++;
//按键按下时间超过了双击的间隔时间则进一步判断是否为长按状态
if (k->gap_times > KEY_DOUBLE_TIMES)
{
if (k->click_count == 1 && k->event != KEY_EVENT_LONG)
{
k->event = KEY_EVENT_SHORT;
}
k->state = KEY_STATE_IDLE;
}
else
{
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
}
break;
default:
k->state = KEY_STATE_IDLE;
break;
}
}
当前函数有三个形参:
port:代表按键所在的GPIO端口pin:按键对应的具体引脚k:指向特定按键(KeyEx)结构体的指针
当前按键扫描函数的作用为:读取指定port和pin的当前电平,然后根据传入的档案k中记录的历史状态,推演出这个按键下一步应该进入什么状态,并更新档案k里的定时器和事件
按键扫描函数调用
为了方便函数的统一调用我们将三个按键扫描函数封装到一个函数中即可,封装后的函数如下
c++
void Key_scan_states_aLL(void)
{
Key_scan_states_one(KEY1_GPIO_Port, KEY1_Pin, &key1);
Key_scan_states_one(KEY2_GPIO_Port, KEY2_Pin, &key2);
Key_scan_states_one(KEY3_GPIO_Port, KEY3_Pin, &key3);
}
随后即可在定时器6的中断服务函数中进行调用
c++
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
Key_scan_states_aLL();
}
}
主函数标志位响应
按键扫描函数会将对应的全局变量(标志位)的值进行修改,随后我们需要在主函数中依据标志位值得变化来做出响应得动作,确保我们得按键扫描函数得逻辑能够正常运行,主函数循环中的代码如下
c++
LED_TurnOffALL();
// 处理按键1的事件(控制LED1)
KeyEvent e1 = key1.event;
key1.event = KEY_EVENT_NONE;
switch (e1)
{
case KEY_EVENT_SHORT: // 单击:切换LED1的亮灭
LED_Toggle(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED1
LED_TurnOff(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED1
LED_TurnOn(LED1);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key1 Double Pressed!");
break;
default:
break;
}
// 处理按键2的事件(控制LED2)
KeyEvent e2 = key2.event;
key2.event = KEY_EVENT_NONE;
switch (e2)
{
case KEY_EVENT_SHORT: // 单击:切换LED2的亮灭
LED_Toggle(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED2
LED_TurnOff(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED2
LED_TurnOn(LED2);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key2 Double Pressed!");
break;
default:
break;
}
// 处理按键3的事件(控制LED3)
KeyEvent e3 = key3.event;
key3.event = KEY_EVENT_NONE;
switch (e3)
{
case KEY_EVENT_SHORT: // 单击:切换LED3的亮灭
LED_Toggle(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Pressed!");
break;
case KEY_EVENT_LONG: // 长按:熄灭LED3
LED_TurnOff(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Long Pressed!");
break;
case KEY_EVENT_DOUBLE: // 双击:点亮LED3
LED_TurnOn(LED3);
LCD_Clear(White); // 清屏,背景设为白色
LCD_DisplayStringLine(Line4, (u8 *)" Key3 Double Pressed!");
break;
default:
break;
}
在主循环中程序会不间断得扫描按键全局标志位的值的变化以获取按键的状态,随后根据按键状态的变化来做出响应的动作(在显示屏上显示信息,同时控制对应LED灯的亮灭)
总结
至此通过状态机判断按键状态的程序就编写完成了,本程序仍有许多可以改进的地方,但是本次主要是向大家介绍一下状态机编程的思想以及操作方法,在裸机开发中状态机发挥着极其重要的作用,它允许程序在非阻塞模式下进行状态检测等一系列操作,最后附上按键操作的所有函数实现以供参考
key.h
c++
#ifndef __KEY_H__
#define __KEY_H__
#include "main.h"
#include "LED.h"
typedef enum
{
KEY_STATE_IDLE = 0, //空闲状态
KEY_STATE_CONFIRM, //确认状态
KEY_STATE_PRESSED, //按下状态
KEY_STATE_WAIT_RELEASE, //等待抬起状态
} KeyState;
typedef enum
{
KEY_EVENT_NONE = 0, //未发生事件
KEY_EVENT_SHORT, //短按事件
KEY_EVENT_LONG, //长按事件
KEY_EVENT_DOUBLE, //双击事件
} KeyEvent;
typedef struct
{
KeyState state;
uint16_t press_times; //记录当前这次按下持续了多少个周期
uint16_t gap_times; //第一次松开按键到第二次按键按键中间经历了多少个周期
uint8_t click_count; //记录在一轮操作中按下的次数
KeyEvent event;
} KeyEx;
//扫描周期
#define KEY_SCAN_PERIOD_MS 10
//长按时间判定,单按键按下时间超过1秒钟时说明按键处于长按状态
#define KEY_LONG_MS 1000
//双击时间间隔,当两次按键按下的时间间隔在300ms以内证明按键被双击了
#define KEY_DOUBLE_MS 300
//将时间转换为扫描的次数
#define KEY_LONG_TIMES (KEY_LONG_MS / KEY_SCAN_PERIOD_MS)
#define KEY_DOUBLE_TIMES (KEY_DOUBLE_MS / KEY_SCAN_PERIOD_MS)
// 按键事件标志位,在 Key.c 中定义,这里只做声明
extern volatile uint8_t key1_flag;
extern volatile uint8_t key2_flag;
extern volatile uint8_t key3_flag;
// 三个按键的扩展状态(在 Key.c 中定义),用于在 Bsp_loop 中读取事件
extern KeyEx key1;
extern KeyEx key2;
extern KeyEx key3;
void Key_scan(void);
void Key_scan_ALL(void);
void Key_scan_one(GPIO_TypeDef *port, uint16_t pin, KeyState *state, volatile uint8_t *flag);
void Key_scan_states_one(GPIO_TypeDef *port, uint16_t pin, KeyEx *k);
void Key_scan_states_aLL(void);
#endif
key.c
c++
#include "Key.h"
// 初始化按键为空闲状态,全局标志位只在本文件定义一次
volatile uint8_t key1_flag = 0;
volatile uint8_t key2_flag = 0;
volatile uint8_t key3_flag = 0;
// 三个按键的扩展状态(供其它模块读取事件)
KeyEx key1 = {KEY_STATE_IDLE};
KeyEx key2 = {KEY_STATE_IDLE};
KeyEx key3 = {KEY_STATE_IDLE};
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6)
{
Key_scan_states_aLL();
}
}
void Key_scan(void)
{
//静态变量只会被初始化一次,后续会跳过当前语句
static KeyState key_state = KEY_STATE_IDLE;
GPIO_PinState curr_level = HAL_GPIO_ReadPin(KEY3_GPIO_Port, KEY3_Pin);
switch (key_state)
{
case KEY_STATE_IDLE:
//此时为低电平按键疑似按下
if (curr_level == GPIO_PIN_RESET)
{
key_state = KEY_STATE_CONFIRM;
}
break;
case KEY_STATE_CONFIRM:
//10ms后按键对应引脚仍旧为低电平,说明此时按键确实按下
if (curr_level == GPIO_PIN_RESET)
{
key_state = KEY_STATE_PRESSED;
key1_flag = 1;
}
//10ms后按键引脚变回高电平说明刚才是错误信息
else
{
//再次将按键状态转变为空闲态
key_state = KEY_STATE_IDLE;
}
break;
//按键按下后等待按键松手
case KEY_STATE_PRESSED:
if (curr_level == GPIO_PIN_SET)
{
key_state = KEY_STATE_WAIT_RELEASE;
}
break;
//等待用户松手
case KEY_STATE_WAIT_RELEASE:
//如果电平变化之后10ms之后仍旧不变说明已经彻底松手了
if (curr_level == GPIO_PIN_SET)
{
key_state = KEY_STATE_IDLE;
}
//如果短期内再次回到原来的状态则保持状态位不变
else
{
key_state = KEY_STATE_PRESSED;
}
break;
default:
break;
}
}
void Key_scan_one(GPIO_TypeDef *port, uint16_t pin, KeyState *state, volatile uint8_t *flag)
{
GPIO_PinState curr_level = HAL_GPIO_ReadPin(port, pin);
switch (*state)
{
case KEY_STATE_IDLE:
// 此时为低电平按键疑似按下
if (curr_level == GPIO_PIN_RESET)
{
*state = KEY_STATE_CONFIRM;
}
break;
case KEY_STATE_CONFIRM:
// 10ms后按键对应引脚仍旧为低电平,说明此时按键确实按下
if (curr_level == GPIO_PIN_RESET)
{
*state = KEY_STATE_PRESSED;
*flag = 1;
}
// 10ms后按键引脚变回高电平说明刚才是错误信息
else
{
// 再次将按键状态转变为空闲态
*state = KEY_STATE_IDLE;
}
break;
// 按键按下后等待按键松手
case KEY_STATE_PRESSED:
if (curr_level == GPIO_PIN_SET)
{
*state = KEY_STATE_WAIT_RELEASE;
}
break;
// 等待用户松手
case KEY_STATE_WAIT_RELEASE:
// 如果电平变化之后10ms之后仍旧不变说明已经彻底松手了
if (curr_level == GPIO_PIN_SET)
{
*state = KEY_STATE_IDLE;
}
// 如果短期内再次回到原来的状态则保持状态位不变
else
{
*state = KEY_STATE_PRESSED;
}
break;
default:
*state = KEY_STATE_IDLE;
break;
}
}
void Key_scan_ALL(void)
{
static KeyState key1_state = KEY_STATE_IDLE;
static KeyState key2_state = KEY_STATE_IDLE;
static KeyState key3_state = KEY_STATE_IDLE;
Key_scan_one(KEY1_GPIO_Port, KEY1_Pin, &key1_state, &key1_flag);
Key_scan_one(KEY2_GPIO_Port, KEY2_Pin, &key2_state, &key2_flag);
Key_scan_one(KEY3_GPIO_Port, KEY3_Pin, &key3_state, &key3_flag);
}
void Key_scan_states_one(GPIO_TypeDef *port, uint16_t pin, KeyEx *k)
{
GPIO_PinState level = HAL_GPIO_ReadPin(port, pin);
switch (k->state)
{
case KEY_STATE_IDLE:
k->event = KEY_EVENT_NONE;
k->press_times = 0;
k->gap_times = 0;
k->click_count = 0;
//电平发生变化,说明此时按键疑似按下,接下来需要进步验证
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
break;
//接下来进入验证状态,在验证状态中需要检测按键是否真的已经按下
case KEY_STATE_CONFIRM:
//再次读取信息以确定按键状态
if(level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_PRESSED;
k->press_times = 0;
}
else
{
//此时说明前面的按下是抖动误触,此时将状态返回到空闲态等待下一次电平变化
k->state = KEY_STATE_IDLE;
}
break;
//当确定按键第一次按下后需要记录按下的持续时间以进行下一步操作
case KEY_STATE_PRESSED:
//记录按下状态的持续时间
k->press_times++;
//长按判定,达到指定时间则更改状态为长按状态
if (k->press_times == KEY_LONG_TIMES)
{
k->event = KEY_EVENT_LONG;
}
//状态变化说明按键抬起,表明此时不是长按状态
if (level == GPIO_PIN_SET)
{
//此时进入短按一次状态
if (k->press_times < KEY_LONG_TIMES)
{
k->click_count++;
//如果这是第一次短按则进入等待,尝试等待第二次按键按下
if (k->click_count == 1)
{
//状态更新为等待按键松开
k->state = KEY_STATE_WAIT_RELEASE;
//重置间隔时间
k->gap_times = 0;
}
else
{
//此时说明第二次短按也完成,则判定进入了双击状态
k->event = KEY_EVENT_DOUBLE;
//一轮判断完成,将状态恢复为空闲态
k->state = KEY_STATE_IDLE;
}
}
else
{
//此时已经触发过了长按,当不再短按时直接回到空闲态
k->state = KEY_STATE_IDLE;
}
}
break;
case KEY_STATE_WAIT_RELEASE:
//记录两次按键按下的时间间隔
k->gap_times++;
//按键按下时间超过了双击的间隔时间则进一步判断是否为长按状态
if (k->gap_times > KEY_DOUBLE_TIMES)
{
if (k->click_count == 1 && k->event != KEY_EVENT_LONG)
{
k->event = KEY_EVENT_SHORT;
}
k->state = KEY_STATE_IDLE;
}
else
{
if (level == GPIO_PIN_RESET)
{
k->state = KEY_STATE_CONFIRM;
}
}
break;
default:
k->state = KEY_STATE_IDLE;
break;
}
}
void Key_scan_states_aLL(void)
{
Key_scan_states_one(KEY1_GPIO_Port, KEY1_Pin, &key1);
Key_scan_states_one(KEY2_GPIO_Port, KEY2_Pin, &key2);
Key_scan_states_one(KEY3_GPIO_Port, KEY3_Pin, &key3);
}