嵌入式设备中按键的硬件消抖, 软件消抖和按键消息处理
2022-06-20 来源:eefocus
按键
按钮就是一种配备了弹性装置的双状态开关: 连通和断开. 由于弹性部件的作用, 大部分时间按钮是断开的. 从电路角度看, 按钮扮演的角色就是开路和短路. 按钮在嵌入式设备中是常见组件, 通常情况下, 一个按钮需要有一个弱上拉或下拉电阻, 对于STM32而言, GPIO口已经自带了弱上拉电阻, 可以在程序中设置是否使用, STC系列的MCU, 要看具体型号和具体的IO口, 例如经典的stc89c51/stc89c52, P0口就是漏极开路的双向IO口, 使用时当电流流出需外接上拉电阻.
将按钮连接到MCU通常有两种方式, 一种是低电平有效, 另一种是高电平有效, 在低电平有效的电路中, 当按钮按下时, 将在引脚上读取到逻辑0, 按钮释放后读取的是1; 在高电平有效电路中则正好相反.
上拉/下拉电阻阻值选取
如果将一个IO口等价为一个电容, 那么低电平有效的等价电路为下图
如果电阻太小, 电流过大可能会损坏元件, 一般这个阻值在几K到几十K欧. 阻值的大小受GPIO的逻辑转换时间限制, 对于STM32, IO口电容为5pF, 上拉电阻可以为10K欧.
按键抖动效应 The bounce effect
按钮在按下和释放时都有可能产生抖动效应, 会导致过程中产生多次短路与开路之间的切换, 对于这个问题, 需要从硬件和软件方面来解决:
硬件上, 低通滤除抖动
软件上, 增加第一次检测到动作后的 dead time
硬件处理
硬件消除抖动(debouncing)是需要优先考虑的方法, 比软件方式更稳定和高效. 可以通过在GPIO口和按键之间添加一个低通滤波电路实现.
实现低通滤波最简单的电路就是 RC滤波. 其阻值和容值怎么计算呢? 取决于抖动的容忍频率. 可以使用以下计算式
低通频率不能太低, 否则会滤除正常的操作, 在正常情况下, 一个人不太可能以100赫兹的频率去按按键, 所以
将低通频率设为10KHz, 对应的就是160欧的电阻和100nF的电容, 或1K欧电阻和16nF电容
将低通频率设为1KHz, 对应的就是1K欧电阻和160nF电容
将低通频率设为100Hz, 对应的就是10K欧电阻和160nF电容
下面的电路中, 使用了10KR电阻和100nF(104)电容作为硬件防抖处理
软件处理
软件处理分两种情况, 如果仅仅需要检测短按, 是比较简单的, 声明一个volatile static a变量用于表示按键状态, 声明一个static uint8_t b变量用于计数, 每个循环的检测中, 低电平(假定按下为低电平)b加1, 当b值计数到达一个阈值时表示按钮按下, 将a置位, 当循环中检测到高电平时将a和b都清零.
如果需要检测短按和长按, 就需要三个变量, 除了上面的a和b以外, 再增加一个循环计数c. 检测的每个循环中, 先按检测短按的方式, 做短按判断, 另外再通过第三个变量记录短按的次数, 当达到预设的长按判断的次数阈值时, 判断为长按. 要注意的是
短按的置位要由按钮释放触发
长按的置位由按钮按下触发
长按释放时, 要避免判断为短按
下面是一段实际应用中的代码, 会在一个间隔10ms的定时器中调用, 其中
KEY1 为按键对应的IO口, 例如P01
debounce[0] 为按键1对应的防抖延时计数器
k1_pressed 当判断按键1为按下时置位, 全局使用
switchcount[0] 按键1对应的长按键计数器, SW_CNTMAX为判断阈值
k1_long_pressed 当判断按键1为长按时置位, 全局使用
event 按键事件, 全局使用
void read_key1(void)
{
//未按下时, KEY1处于高电平, 因此debounce为0xFF
debounce[0] = (debounce[0] << 1) | KEY1;
if (debounce[0] == 0x00) { // 8次检测都为0, 按下置位
k1_pressed = 1;
if (!k1_long_pressed) { // 如果长按未置位, 计数加1
switchcount[0]++;
}
} else { // 按键已松开或未按下
if (k1_pressed) {
if (!k1_long_pressed) {
// 如果短按已置位, 但是长按未置位, 按短按发出系统消息
event = EV_K1_SHORT;
}
// 清理状态和计数器
k1_pressed = 0;
k1_long_pressed = 0;
switchcount[0] = 0;
}
}
if (switchcount[0] > SW_CNTMAX) {
// 如果长按计数器已经达到阈值, 长按置位(避免松开时发出短按消息), 发出长按系统消息
k1_long_pressed = 1;
switchcount[0] = 0;
event = EV_K1_LONG;
}
}
按键消息处理
按键的系统消息是通过状态机模型进行处理的, 在每个按键处理循环中,
清除全局消息
根据当前的按键状态, 判断长按和短按对应的下一个状态
下一个循环, 会跳到对应的按键状态, 再去判断下一个状态
根据按键状态决定当前的显示模式
void main(void)
{
//...
while (true)
{
while (!loop_gate); // wait for open every 100ms
loop_gate = 0; // close gate
ev = event;
event = EV_NONE;
switch (kmode)
{
case K_DISP_SEC:
dmode = D_DISP_SEC;
if (ev == EV_K2_SHORT) {
kmode = K_DISP_ALARM;
m_timeout = TIMEOUT_SHORT;
}
break;
//...
case K_NORMAL:
default:
dmode = D_NORMAL;
if (ev == EV_K1_SHORT) {
kmode = K_DISP_DATE;
m_timeout = TIMEOUT_SHORT;
} else if (ev == EV_ALARM) {
kmode = K_BUZZ_ALARM;
m_timeout = TIMEOUT_LONG;
}
else if (ev == EV_K1_LONG)
kmode = K_SET_MINUTE;
else if (ev == EV_K2_SHORT)
kmode = K_DISP_SEC;
else if (ev == EV_K2_LONG)
kmode = K_SET_ALARM_MINUTE;
}
//...
}
}
参考
按键防抖的硬件和软件处理 https://www.playembedded.org/blog/buttons-stm32/
例子中的完整代码 HML_FwLib_STC12/blob/master/example/others/ds12c887/alarm_clock.c