按键
目前有2种方式做按键检测:
- 外部中断。
- GPIO定时扫描。
硬件电路
一般电路设计都是引脚带上拉电阻,然后接一个按键再接一个小电阻接地。
一开始我不是很明白,如果用外部中断来做的话,就只能捕获下降沿触发了,如果我要上升沿触发呢?
这个单片机是没有内置下拉电阻的,所以只能设计成VDD接小电阻然后接按键,IO外置一个大的下拉电阻,比起上面那种设计多了一个外部电阻不是很合适。
外部中断按键检测
HT66F3195
有两个外部中断引脚,如果懒得在引脚图上面一个一个找,直接在数据手册里面的“输入/输出端口”->“引脚共用功能”的最后一块内容里面找。
这个意思也很简单了,比如INT0可以在PB0上,如果PB0用作其他用途,就换成PA1来使,但是同时只能用一个,PB0和PA1是不能同时都为INT0外部中断功能的。很多有几年开发经验的人,也经常把这个漏掉,如果用封装成库的话,就会大大减少这种低级错误的发生了,因为一个模块的大部分内容直接被我从数据手册里面搬到头文件里封装好了😏
程序配置
INT.h
#ifndef _INT_H_
#define _INT_H_
#define INT_0_Cmd(x) _int0e = x
#define INT_1_Cmd(x) _int1e = x
#define INT_0_IntAddress 0x04
#define INT_1_IntAddress 0x24
#define INT_0_Flag _int0f
#define INT_1_Flag _int1f
#define INT_0_FlagReset() _int0f = 0
#define INT_1_FlagReset() _int1f = 0
#define INT_0_Source(x) _int0ps = x
typedef enum
{
INT_0_PB0 = 0,
INT_0_PA1,
}INT_0_Source_T;
#define INT_1_Source(x) _int1ps = x
typedef enum
{
INT_1_PB1 = 0,
INT_1_PA2,
}INT_1_Source_T;
#define INT_0_Trigger(x) _integ = (_integ&(~(REG_2_BIT<<0)))|(x<<0)
#define INT_1_Trigger(x) _integ = (_integ&(~(REG_2_BIT<<2)))|(x<<2)
typedef enum
{
INT_Disable = 0,
INT_Edge_Up,
INT_Edge_Down,
INT_Edge_Both,
}INT_Trigger_T;
void INT_0_Init();
void INT_1_Init();
#endif
对应的按键任务结构体:
typedef struct
{
unsigned char status_after;
unsigned char status_before;
unsigned char debounce_cnt;
unsigned char trigger_condition; /* 0x01:按下,0xFF:松开,0x00:无 */
}Key_Task;
那个trigger_condition
目前只是预留两种状态,后续可能会加上一些常见的操作,比如双击,长按这种。
模块初始化
void INT_1_Init()
{
/* 引脚功能复用 */
GPIOA_2_Mode(IO_Mode1);
GPIOA_2_Pullup(Enable);
GPIOA_2_Control(IO_Input);
/* 模块属性 */
INT_1_Source(INT_1_PA2);
INT_1_Trigger(INT_Edge_Down);
INT_1_Cmd(Enable);
}
外部中断没有太复杂的配置,没有特定的功能复用,跟普通IO口一样使用,但是要配置成输入状态,至于要不要开上拉电阻要看外部设计,由于这边做的是按键实验,所以要把上拉打开。以下是数据手册的原话。
外部中断引脚和普通 I/O 口共用。
该引脚必须通过设置端口控制寄存器,将该引脚设置为输入口。
还有一点忘记说了,因为外部中断模块跟其他模块不太一样,其他模块配置的流程基本是:模块使能,模块中断使能这两个核心步骤。
外部中断只有中断使能,但是我库里面写的直接Cmd,内容其实就是中断使能,打开这个之后有外部中断触发就能进对应的中断函数了。
中断程序
DEFINE_ISR(external_int1, INT_1_IntAddress)
{
INT_1_FlagReset();
GPIOA_6_Control(IO_Output);
GPIOA_6_Toggle();
}
我这边只是通过按键翻转一个LED。说来也奇怪,跟其他单片机不太一样,我的按键是使用那种665的黑色微动按键那种,用合泰的单片机好像外部中断不用消抖,多次测试也没有出现异常状况,不过还是建议要进行按键消抖操作的。
所以理论上进入中断还是应该是开始给一个变量赋值,注意不是在中断里面死循环延时,在外面的时基里面进行倒计时,如果再判断对应的状态进行对应的操作。
上面的配置其实也能用,但是有风险而且写法也不规范。下面就写一个正常一点的操作。
模块初始化
Key_Task INT_Key;
void INT_1_Init()
{
/* 引脚功能复用 */
GPIOA_2_Mode(IO_Mode1);
GPIOA_2_Pullup(Enable);
GPIOA_2_Control(IO_Input);
/* 模块属性 */
INT_1_Source(INT_1_PA2);
INT_1_Trigger(INT_Edge_Both);
INT_1_Cmd(Enable);
INT_Key.status_after = 0;
INT_Key.status_before = 0;
INT_Key.debounce_cnt = 0;
INT_Key.trigger_condition = 0;
}
这边我把那个触发边沿改成双沿触发,用外部中断的检测来完成按下和松开的动作捕捉。
中断程序
DEFINE_ISR(external_int1, INT_1_IntAddress)
{
INT_1_FlagReset();
INT_Key.status_after = !!(GPIOA_Get_Status() & Pin2);
INT_Key.debounce_cnt = 2;
}
这里只做两件事情:
- 记录触发时候的电平。
- 给消抖倒计时变量赋值,跳出中断之后开始延时。
按键消抖
if(TB0.FragmentFlag & Fragment_10ms_Mask)
{
TB0.FragmentFlag ^= Fragment_10ms_Mask;
if(INT_Key.debounce_cnt != 0)
{
INT_Key.debounce_cnt -= 1;
if(INT_Key.debounce_cnt == 0)
{
INT_Key.status_before = !!(GPIOA_Get_Status() & Pin2);
if(INT_Key.status_after == INT_Key.status_before)
{
if(INT_Key.status_before == 0)
{
INT_Key.trigger_condition = 0x01; /* 按键按下触发标志位 */
}
else
{
INT_Key.trigger_condition = 0xFF; /* 按键松开触发标志位 */
}
}
}
}
}
这里用的是10ms级别的时基碎片,等2个回合也就是20ms。
按键有效触发操作
if(TB0.FragmentFlag & Fragment_1ms_Mask)
{
TB0.FragmentFlag ^= Fragment_1ms_Mask;
if(INT_Key.trigger_condition != 0)
{
if(INT_Key.trigger_condition == 0x01)
{
/* 按键按下操作 */
GPIOA_6_Control(IO_Output);
GPIOA_6_Toggle();
}
else if(INT_Key.trigger_condition == 0xFF)
{
/* 按键松开操作 */
}
INT_Key.trigger_condition = 0;
}
}
有效触发检测操作的时间必须要比消抖频率快,所以这里直接放在1ms的时基碎片里面,其实放主函数里面也可以,那样速度会更快。
先判断完是什么状态然后再清掉,如果先清掉的话就判断不了了。
这里同样是把一个IO口翻转,这边接一个LED出来也行,方便观察现象。
GPIO按键检测
程序配置
KEY.h
#ifndef _KEY_H_
#define _KEY_H_
/*-------------------宏定义封装------------------- */
#define ALL_MICRO_KEY_NUM 2
#define KEY_DEBOUNCE_TIME 10
#define KEY_1 0
#define KEY_2 1
/*-------------------函数声明------------------- */
void Key_GPIO_Init();
void Key_Scan();
void Key_Trigger_Func();
/*-------------------变量声明------------------- */
typedef struct
{
unsigned char status_after;
unsigned char status_before;
unsigned char debounce_cnt;
unsigned char trigger_condition; /* 0x01:按下,0xFF:松开,0x00:无 */
}Key_Task;
extern Key_Task Micro_Key[];
extern Key_Task INT_Key;
extern const unsigned char Micro_Key_Map[ALL_MICRO_KEY_NUM][3];
#endif
初始化
const unsigned char Micro_Key_Map[][3] =
{
{KEY_1, GPIOB, Pin4},
{KEY_2, GPIOA, Pin2},
};
Key_Task Micro_Key[ALL_MICRO_KEY_NUM];
void Key_GPIO_Init()
{
unsigned char i;
/* PB4 - GPIO_Key */
GPIOB_4_Control(IO_Input);
GPIOB_4_Pullup(Enable);
GPIOB_4_Mode(IO_Mode1);
/* PA2 - GPIO_Key */
GPIOA_2_Control(IO_Input);
GPIOA_2_Pullup(Enable);
GPIOA_2_Mode(IO_Mode1);
for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
{
Micro_Key[i].status_after = 0;
Micro_Key[i].status_before = 0;
Micro_Key[i].debounce_cnt = 0;
Micro_Key[i].trigger_condition = 0;
}
}
这里暂时只演示一个按键的写法,手头上有虽说有一个按键矩阵模块,但是这个做法并不是针对矩阵做的,原理其实差不多只是逻辑部分要根据硬件电路做一点变动。
按键检测
void Key_Scan()
{
unsigned char i;
for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
{
Micro_Key[i].status_before = GPIO_Map_Read(Micro_Key_Map[i][GPIO_Map_Group],Micro_Key_Map[i][GPIO_Map_Pin]);
if(Micro_Key[i].status_before == IO_Error)
continue ;
if(Micro_Key[i].status_before != Micro_Key[i].status_after) /* 如果前后两次状态不一样则开始消抖,另外也要记录当前的电平状态 */
{
Micro_Key[i].debounce_cnt = 2;
Micro_Key[i].status_after = Micro_Key[i].status_before;
}
else if(Micro_Key[i].debounce_cnt != 0) /* 消抖 */
{
Micro_Key[i].debounce_cnt -= 1;
if(Micro_Key[i].debounce_cnt == 0) /* 消抖完成 */
{
Micro_Key[i].status_before = GPIO_Map_Read(Micro_Key_Map[i][GPIO_Map_Group],Micro_Key_Map[i][GPIO_Map_Pin]);
if(Micro_Key[i].status_before == Micro_Key[i].status_after) /* 如果消抖之后电平状态跟消抖之前状态一样,则说明按键有效触发 */
{
if(Micro_Key[i].status_before == 0)
{
Micro_Key[i].trigger_condition = 0x01; /* 按键按下 */
}
else
{
Micro_Key[i].trigger_condition = 0xFF; /* 按键松开 */
}
}
}
}
}
}
这个扫描时间要配合里面的消抖时间,这里设定为2,消抖时间20ms的话,这个函数需要放到10ms时基碎片里面。
if(TB0.FragmentFlag & Fragment_10ms_Mask)
{
TB0.FragmentFlag ^= Fragment_10ms_Mask;
Key_Scan();
}
按键触发功能
if(TB0.FragmentFlag & Fragment_1ms_Mask)
{
TB0.FragmentFlag ^= Fragment_1ms_Mask;
Key_Trigger_Func();
}
void Key_Trigger_Func()
{
unsigned char i;
for(i = 0; i < ALL_MICRO_KEY_NUM; i++)
{
if(i == KEY_1)
{
if(Micro_Key[KEY_1].trigger_condition == 0x01)
{
}
else if(Micro_Key[KEY_1].trigger_condition == 0xFF)
{
GPIOA_6_Control(IO_Output);
GPIOA_6_Toggle();
}
Micro_Key[KEY_1].trigger_condition = 0;
}
else if(i == KEY_2)
{
if(Micro_Key[KEY_2].trigger_condition == 0x01)
{
GPIOA_6_Control(IO_Output);
GPIOA_6_Toggle();
}
else if(Micro_Key[KEY_2].trigger_condition == 0xFF)
{
}
Micro_Key[KEY_2].trigger_condition = 0;
}
else
{
if(Micro_Key[i].trigger_condition != 0x00)
Micro_Key[i].trigger_condition = 0;
}
}
}
按键对应触发的功能还是需要尽可能快地反应,这个可以放在1ms的时基碎片里面。
需要做出反应的按键,要配置好对应的分支,目前只需要处理按下和松开即可。其余按键如果不需要做出反应也要清除对应的标志位。
这样子写的差不多了,后来想想这样子写法其实也是有一个缺陷,如果其他地方有调用ms级别的延时,刚好在这段延时时间里面触发了按键,可能就没有来得及检测,直接错过了。我也没有办法针对这种情况进行修改,无论是GPIO的按键还是外部中断按键,碰到长延时都会这样,所以就尽可能在开发中不要使用长延时功能😕