当前位置: 首页 > news >正文

按键及消抖

方法一:延时阻塞

key.c:

#include "key.h"
#include "delay.h"//初始化GPIO
void key_init(void)
{GPIO_InitTypeDef gpio_initstruct;//打开时钟__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能GPIOA时钟//调用GPIO初始化函数gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;          // 两个KEY对应的引脚gpio_initstruct.Mode = GPIO_MODE_INPUT;                 // 输入gpio_initstruct.Pull = GPIO_PULLUP;                     // 默认上拉,要结合实际电路,如果按下拉低接口,输入一个低电平,那这里就是上拉输入gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;           // 高速HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}//按键扫描函数
uint8_t key_scan(void)
{//检测按键是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判断按键是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//如果确实是按下的状态,等待按键松开while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);//返回按键值return 1;}}//检测按键是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判断按键是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//如果确实是按下的状态,等待按键松开while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);//返回按键值return 2;}}//返回默认值return 0;
}

key_scan() 里用 延时消抖(delay_ms(10))属于 “时间消抖法”,优点是简单,缺点是:

  • 延时是阻塞的,CPU 在等待时不能干别的事;

  • 多个按键时效率更低;

  • 延时时间设置不好会影响响应速度。

实际上,按键消抖方法有很多,除了“延时消抖”,常见的还有:

🔹 1. 状态机消抖法(非阻塞,推荐)

用一个状态机记录按键状态(未按下、按下确认、按住、释放确认),每次扫描时更新状态。

  • 扫描周期(比如 5ms 或 10ms)由 定时器中断RTOS 任务来保证。

  • 只要稳定几次扫描结果一致,才认为按下/松开。

优点:非阻塞,不用 delay,系统实时性好。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"   // 确保包含 HAL 头
#include <stdint.h>/* ========================= 配置区域 ========================= */
/* 扫描周期与确认次数:* - 建议每 5~10ms 调用一次 key_scan()(可在主循环里用 HAL_GetTick() 节拍控制,或用定时器/RTOS任务)* - 连续 N 次一致才确认状态变化:N * SCAN_TICKS_MS 即为有效消抖时间*/
#define SCAN_TICKS_MS        5      // 每次扫描的理想间隔(毫秒)
#define CONFIRM_COUNT        3      // 连续 3 次一致才确认(约 15ms 消抖)/* 按键引脚(按你的原工程保持 A0/A1 上拉,低电平=按下) */
#define KEY0_PORT            GPIOA
#define KEY0_PIN             GPIO_PIN_0
#define KEY1_PORT            GPIOA
#define KEY1_PIN             GPIO_PIN_1/* 物理读取:返回 1=按下(低电平),0=松开(高电平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ========================= 状态机定义 =========================* 我们用 4 态模型:*   IDLE(稳定松开)*   PRESS_CHECK(检测到可能按下,进入确认期)*   PRESSED(稳定按下)*   RELEASE_CHECK(检测到可能松开,进入确认期)* 时序:IDLE→PRESS_CHECK→PRESSED→RELEASE_CHECK→IDLE* 只有在 “PRESSED → RELEASE_CHECK → IDLE” 完成时,才认为一次完整按键发生并返回键值。*/
typedef enum {KEY_IDLE = 0,          // 稳定松开KEY_PRESS_CHECK,       // 按下确认中KEY_PRESSED,           // 稳定按下KEY_RELEASE_CHECK      // 松开确认中
} key_state_t;typedef struct {key_state_t state;     // 当前状态uint8_t     cnt;       // 连续一致计数
} key_fsm_t;/* 两个按键的状态机 */
static key_fsm_t key0 = { KEY_IDLE, 0 };
static key_fsm_t key1 = { KEY_IDLE, 0 };/* 节拍控制:保证 key_scan() 按 SCAN_TICKS_MS 的节奏运行(防止被过于频繁调用导致“计时加快”) */
static uint32_t s_last_tick = 0;/* ========================= 用户接口实现 ========================= *//* 初始化GPIO:与原逻辑一致(上拉输入,低电平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();  // 使能 GPIOA 时钟GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;   // 两个 KEY 引脚gpio_initstruct.Mode  = GPIO_MODE_INPUT;           // 输入模式gpio_initstruct.Pull  = GPIO_PULLUP;               // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;       // 输入模式速度无关,LOW 即可HAL_GPIO_Init(GPIOA, &gpio_initstruct);// 状态机复位key0.state = KEY_IDLE;  key0.cnt = 0;key1.state = KEY_IDLE;  key1.cnt = 0;s_last_tick = HAL_GetTick();
}/* 内部:推进单个键的状态机;返回1表示“本次完成一次按键动作(按下并松开确认)”,否则0 */
static uint8_t step_fsm(key_fsm_t *k, uint8_t raw_press)
{switch (k->state){case KEY_IDLE:  // 稳定松开if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt   = 1;}break;case KEY_PRESS_CHECK:  // 按下确认中if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_PRESSED;   // 确认按下成立k->cnt   = 0;}} else {// 摇回去k->state = KEY_IDLE;k->cnt   = 0;}break;case KEY_PRESSED:  // 稳定按下if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt   = 1;}break;case KEY_RELEASE_CHECK:  // 松开确认中if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE;      // 确认松开成立k->cnt   = 0;return 1;                 // ——一次完整按键动作完成(与原版功能一致:松开后返回)}} else {// 又按下了,回到按下稳定k->state = KEY_PRESSED;k->cnt   = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}return 0;
}/* 非阻塞按键扫描:* - 需被“周期性地”调用(推荐每 5~10ms 一次)* - 返回:0=无;1=KEY0 完成一次按键;2=KEY1 完成一次按键* - 行为与原版一致:只有“按下→松开”完整动作完成才返回键值*/
uint8_t key_scan(void)
{/* 简单节拍器:若调用过快则不推进(防止消抖时间被缩短) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {return 0;}s_last_tick = now;/* 读取原始电平(低电平=按下) */uint8_t k0_raw = key0_raw();uint8_t k1_raw = key1_raw();/* 推进两个键的状态机;谁先完成谁先返回 */if (step_fsm(&key0, k0_raw)) return 1;if (step_fsm(&key1, k1_raw)) return 2;return 0;
}

使用建议

  • while(1) 主循环里,每次循环都调用一次 key_scan() 即可;内部已按 SCAN_TICKS_MS 自节拍。

  • 若你在定时器中断/RTOS里有固定 5ms 节拍,也可以去掉内部节拍器逻辑,直接每 5ms 调一次 key_scan()(把顶部节拍相关代码移除即可)。

  • 调参:

    • SCAN_TICKS_MS:扫描周期;

    • CONFIRM_COUNT:确认次数;有效消抖时间约为 SCAN_TICKS_MS × CONFIRM_COUNT(默认 ≈ 15ms)。

如果还想要按下事件立即上报(而不是等松开),可以在 KEY_PRESSED 的进入处(PRESS_CHECK 确认完成时)返回一个“PRESS 事件”;并在 RELEASE_CHECK 确认完成时返回“RELEASE 事件”。

🔹 2. 定时器中断消抖

  • 使用定时器中断(如 1ms)周期性采样按键。

  • 若某引脚状态连续稳定一段时间(如 20ms),再确认按下或松开。

👉 思路类似状态机法,但触发源换成了硬件定时器。

  • key_init():初始化 GPIO + 配置并启动 TIM3 的 1 ms 中断采样。

  • key_scan():非阻塞,读取由中断产生的“完整按键事件”(按下→松开)并返回 1/2/0

思路:在 TIM3 1 ms中断里做采样与消抖的状态机(PRESS_CHECK / RELEASE_CHECK),当“按下并确认”再“松开并确认”完成时,置一个事件标志。主循环调用 key_scan() 只需读标志即可,无 delay、无 while 等待

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ===================== 用户可调参数 ===================== */
/* 采样周期:1ms(由 TIM3 产生) */
#define SAMPLE_PERIOD_MS      1/* 消抖确认时间:例如 20ms(按下与松开都采用该门限) */
#define STABLE_MS             20
#define CONFIRM_TICKS         (STABLE_MS / SAMPLE_PERIOD_MS)  // =20/* 两个按键:PA0 / PA1,上拉输入,低电平=按下 */
#define KEY0_PORT             GPIOA
#define KEY0_PIN              GPIO_PIN_0
#define KEY1_PORT             GPIOA
#define KEY1_PIN              GPIO_PIN_1/* 物理采样:返回 1=按下(低电平),0=松开(高电平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== 定时器句柄 ===================== */
static TIM_HandleTypeDef htim3;/* ===================== 状态机与事件 ===================== */
/* 4 态状态机:*  IDLE(稳定松开) → PRESS_CHECK(按下确认) → PRESSED(稳定按下) → RELEASE_CHECK(松开确认) → IDLE*  只有完成 PRESSED→RELEASE_CHECK→IDLE 时才记为“一次完整按键”。*/
typedef enum {KEY_IDLE = 0,        // 稳定松开KEY_PRESS_CHECK,     // 按下确认中KEY_PRESSED,         // 稳定按下KEY_RELEASE_CHECK    // 松开确认中
} key_state_t;typedef struct {key_state_t state;   // 当前状态uint8_t     cnt;     // 连续一致的计数(用于确认)
} key_fsm_t;/* 两个键的状态机实例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 中断里产生的“完整按键事件”标志(按下→松开完成) */
static volatile uint8_t key0_event = 0;  // 置1表示有一次完整的 KEY0 事件待取
static volatile uint8_t key1_event = 0;  // 置1表示有一次完整的 KEY1 事件待取/* ===================== 内部:推进一个键的状态机(在中断里调用) ===================== */
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, volatile uint8_t *event_flag)
{switch (k->state){case KEY_IDLE:  /* 稳定松开 -> 看到“按下”则进入确认 */if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt   = 1;}break;case KEY_PRESS_CHECK:  /* 按下确认:连续 CONFIRM_TICKS 次都“按下”才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_PRESSED;    // 确认“稳定按下”k->cnt   = 0;}} else {/* 中途又松开:判定失败,回到 IDLE */k->state = KEY_IDLE;k->cnt   = 0;}break;case KEY_PRESSED:  /* 稳定按下 -> 观察到“松开”则进入确认 */if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt   = 1;}break;case KEY_RELEASE_CHECK:  /* 松开确认:连续 CONFIRM_TICKS 次都“松开”才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_IDLE;       // 确认“稳定松开”k->cnt   = 0;*event_flag = 1;           // ——一次完整按键(按下→松开)完成,置事件}} else {/* 中途又按下:回到稳定按下 */k->state = KEY_PRESSED;k->cnt   = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== TIM3 初始化:1kHz(1ms)中断 =====================* 典型 F1 时钟:SYSCLK=72MHz, APB1=36MHz,但定时器时钟在 APB1 分频!=1 时倍频到 72MHz。* 这里配置:Prescaler=7200-1 → 分频 7200,计数频率 10kHz;*          Period   =10-1   → 计满 10 次产生更新 → 1kHz 中断(1ms)。* 若你的时钟不同,请按实际修改。* ====================================================================== */
static void TIM3_Init_1ms(void)
{__HAL_RCC_TIM3_CLK_ENABLE();htim3.Instance = TIM3;htim3.Init.Prescaler         = 7200 - 1;   // 72MHz / 7200 = 10kHzhtim3.Init.CounterMode       = TIM_COUNTERMODE_UP;htim3.Init.Period            = 10 - 1;     // 10kHz / 10 = 1kHz → 1mshtim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;HAL_TIM_Base_Init(&htim3);HAL_TIM_Base_Start_IT(&htim3);/* 使能 NVIC 中断 */HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);HAL_NVIC_EnableIRQ(TIM3_IRQn);
}/* ===================== 用户接口:GPIO + 定时器初始化 ===================== */
void key_init(void)
{/* GPIO:上拉输入,低电平=按下(保持与原工程一致) */__HAL_RCC_GPIOA_CLK_ENABLE();GPIO_InitTypeDef io = {0};io.Pin   = GPIO_PIN_0 | GPIO_PIN_1;io.Mode  = GPIO_MODE_INPUT;io.Pull  = GPIO_PULLUP;io.Speed = GPIO_SPEED_FREQ_LOW;  // 输入模式速度无关HAL_GPIO_Init(GPIOA, &io);/* 状态机与事件清零 */key0_fsm.state = KEY_IDLE;  key0_fsm.cnt = 0;  key0_event = 0;key1_fsm.state = KEY_IDLE;  key1_fsm.cnt = 0;  key1_event = 0;/* 启动 TIM3 1ms 周期中断,进行消抖采样与判定 */TIM3_Init_1ms();
}/* ===================== 中断服务:1ms 采样与消抖 ===================== */
/* HAL 的更新回调:由 TIM3_IRQHandler → HAL_TIM_IRQHandler → 触发此回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM3){/* 读取原始电平(低电平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 推进两个键的状态机;事件标志在状态机内部置位 */key_fsm_step(&key0_fsm, r0, &key0_event);key_fsm_step(&key1_fsm, r1, &key1_event);}
}/* TIM3 IRQHandler(放在 stm32f1xx_it.c 里更规范;若没该文件也可暂放此处) */
void TIM3_IRQHandler(void)
{HAL_TIM_IRQHandler(&htim3);
}/* ===================== 用户接口:查询一次事件 ===================== */
/* 行为与原版保持一致:* - 返回 1:KEY0 完成一次“按下→松开”* - 返回 2:KEY1 完成一次“按下→松开”* - 返回 0:无新事件* 非阻塞:不含 delay,不会卡主循环。*/
uint8_t key_scan(void)
{if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

使用提示

  • 保持原来的主循环不变,周期性调用 key_scan() 即可(现在 不会阻塞 主循环)。

  • 如果在工程里使用了 SysTick 1 ms,也可以改为在 SysTick_Handler 里调用一个 Key_ISR_1ms() 来推进状态机,思路完全一样;这里用 TIM3 是为了不影响 SysTick

  • 想调整消抖时间:把 STABLE_MS 改为你需要的值(例如 10/15/30 ms),1 ms 采样会自动按 CONFIRM_TICKS 计算确认门限。

🔹 3. 计数滤波法

  • 每次扫描时对按键状态计数:

    • 检测到按下 → count++

    • 检测到松开 → count--

  • count 超过上限(如 >3)时确认“按下”,小于下限(如 <0)时确认“松开”。

👉 优点:实现简单,兼顾滤波和消抖。

  • key_init():初始化 GPIO(上拉输入,低电平按下)。

  • key_scan()非阻塞,建议每 5 ms 调用一次;当且仅当完成“一次按下→松开”的完整动作时返回 1/2,否则返回 0(与原代码保持一致,但不再 delay/while 卡住主循环)。

计数滤波法要点:

  • 每次扫描读一次原始电平(按下=1,松开=0)。

  • 计数器 cnt:按下则 +1,松开则 -1,并在 [0..CNT_MAX] 内饱和。

  • cnt >= TH_ON 认为“稳定按下”;当 cnt <= TH_OFF 认为“稳定松开”(带回滞避免抖动来回翻转)。

  • 只有从稳定按下转到稳定松开时,才认定完成一次“点击”,置事件供 key_scan() 返回(与原先“按下后等待松开再返回”的行为一致)。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ====================== 可调参数 ====================== */
/* 建议每 5 ms 调用一次 key_scan()(主循环或定时器节拍) */
#define SCAN_TICKS_MS   5/* 计数滤波参数(积分 + 回滞):* - CNT_MAX:计数上限(越大滤波越强但响应越慢)* - TH_ON  :达到/超过该值判定“稳定按下”* - TH_OFF :小于/等于该值判定“稳定松开”(回滞阈值,应小于 TH_ON)* 例如:CNT_MAX=10,TH_ON=7,TH_OFF=3 → 近似相当于 ~20~40ms 的抗抖(视抖动形态)*/
#define CNT_MAX   10
#define TH_ON     7
#define TH_OFF    3/* ====================== 硬件引脚 ====================== */
/* 与原工程保持一致:PA0 / PA1 上拉输入,低电平=按下 */
#define KEY0_PORT   GPIOA
#define KEY0_PIN    GPIO_PIN_0
#define KEY1_PORT   GPIOA
#define KEY1_PIN    GPIO_PIN_1/* 原始采样:返回 1=按下(低电平),0=松开(高电平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 计数滤波 + 事件 ====================== */
/* 稳定状态标记:0=稳定按下,1=稳定松开(用 1 表示松开更直观) */
typedef struct {uint8_t cnt;       // 0..CNT_MAX 的积分计数uint8_t state;     // 当前稳定状态:1=松开,0=按下
} key_counter_t;static key_counter_t key0 = { CNT_MAX, 1 };  // 初始偏向“松开”
static key_counter_t key1 = { CNT_MAX, 1 };static volatile uint8_t key0_event = 0;      // 完整“按下→松开”事件
static volatile uint8_t key1_event = 0;static uint32_t s_last_tick = 0;             // 内部节拍器,确保按 SCAN_TICKS_MS 推进/* 推进单键的计数滤波与事件判定:* - raw_press: 1=按下,0=松开* - 当从“稳定按下(0)”转变为“稳定松开(1)”时,置 *click_flag = 1*/
static void key_counter_step(key_counter_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 计数积分:按下(+1) / 松开(-1),并饱和在 [0..CNT_MAX] */if (raw_press) {if (k->cnt < CNT_MAX) k->cnt++;} else {if (k->cnt > 0)       k->cnt--;}/* 2) 回滞阈值判定,更新稳定状态 */if (k->cnt >= TH_ON) {/* 稳定按下 */if (k->state != 0) {k->state = 0;               // 进入“稳定按下”/* 此处可产生“PRESS 事件”(如果你需要立即上报按下) */}} else if (k->cnt <= TH_OFF) {/* 稳定松开 */if (k->state != 1) {/* 仅当从按下(0)回到松开(1)时,认为一次点击完成 */if (k->state == 0) {*click_flag = 1;        // ——一次“按下→松开”完成}k->state = 1;               // 进入“稳定松开”}}
}/* ====================== 用户接口实现 ====================== *//* 初始化GPIO:与原工程一致(上拉输入,低电平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();  // 使能 GPIOAGPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;  // 两个 KEYgpio_initstruct.Mode  = GPIO_MODE_INPUT;          // 输入gpio_initstruct.Pull  = GPIO_PULLUP;              // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;      // 输入速度无关HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化计数与状态、事件与节拍 */key0.cnt = CNT_MAX; key0.state = 1; key0_event = 0;key1.cnt = CNT_MAX; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞扫描:* - 建议每 5 ms 调用一次(内部也做了节拍限速,过快调用会直接返回 0)* - 返回:1=KEY0 完成一次“按下→松开”;2=KEY1 完成一次“按下→松开”;0=无事件* - 与原实现“按下后等待松开再返回”的功能一致,但不阻塞。*/
uint8_t key_scan(void)
{/* 简单节拍器:确保按 SCAN_TICKS_MS 推进一次滤波(防止被高频多次调用而缩短“有效消抖时间”) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 没到节拍,不推进滤波 */goto _check_event_and_return;}s_last_tick = now;/* 一次节拍:读取原始电平并推进两个键的计数滤波 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_counter_step(&key0, r0, &key0_event);key_counter_step(&key1, r1, &key1_event);_check_event_and_return:/* 谁先完成一次点击谁先返回;保证与原代码返回语义一致 */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

说明与调参建议

  • 非阻塞:没有 delay_ms()while(),主循环实时性更好。

  • 调用频率:建议每 5 ms 调用一次 key_scan();若你已有固定节拍(比如用定时器/RTOS),也可以移除内部节拍器,固定周期调用即可。

  • 消抖强度

    • 增大 CNT_MAXTH_ON 可增强抗抖(响应更慢);

    • 减小 TH_OFF 可增加回滞,避免临界抖动来回切换;

    • 一般保持 TH_OFF < TH_ON,如示例 TH_ON=7,TH_OFF=3

  • 事件时机:当前代码仅在从稳定按下→稳定松开时上报事件(与原行为一致)。如果你需要“按下就上报(PRESS)/松开再上报(RELEASE)”,可在 k->state 切换处各自置不同事件枚举即可。 

🔹 4. 硬件RC电路消抖

在按键硬件电路上加:

  • 电阻 + 电容(RC电路),滤掉机械抖动;

  • 或者加 施密特触发器(如 74HC14)来增强边沿稳定性。

👉 优点:软件不需要处理消抖,反应更快。缺点是需要增加硬件成本。

🔹 5. 软件滤波算法

  • 滑动平均:保存最近 N 次按键采样结果,取多数值作为当前状态;

  • 数字滤波:如 IIR、FIR 滤波,减少抖动影响。

  • 接口与语义保持不变:key_init() 初始化;key_scan() 只有在一次“按下→松开”完整动作结束时,返回 1/2(否则返回 0),与原阻塞版行为一致;

  • 不再使用 delay_ms() / while(...),建议每 5 ms 调用一次 key_scan()(也可用定时器/RTOS固定节拍调用)。

算法说明(软件滤波)

  • 维护每个键最近 N=8 次采样的“按下位历史(1=按下,0=松开)”hist

  • 计算窗口内“按下”的个数 sum = popcount(hist)

  • 多数判决 + 回滞阈值

    • sum >= MAJ_ON(6) → 判为“稳定按下”;

    • sum <= MAJ_OFF(2) → 判为“稳定松开”;

  • 只有当“稳定按下→稳定松开”完成时,置一次点击事件key_scan() 返回键值;

  • 回滞 (MAJ_OFF < MAJ_ON) 可避免在临界抖动时来回翻转。

仍然可以把 SCAN_TICKS_MSWIN_BITSMAJ_ONMAJ_OFF 调一下,达到你想要的响应/抗抖平衡。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"   // HAL 头文件
#include <stdint.h>
// #include "delay.h"         // 本实现不再使用延时,可删/* ======================== 可调参数 ======================== */
/* 建议每 5 ms 调用一次 key_scan()(主循环节拍或定时器/RTOS) */
#define SCAN_TICKS_MS   5/* 滑动窗口参数(软件滤波) */
#define WIN_BITS        8    /* 窗口长度:最近 8 次采样 */
#define MAJ_ON          6    /* 多数判决:>=6/8 认为“稳定按下” */
#define MAJ_OFF         2    /* 多数判决:<=2/8 认为“稳定松开”(回滞,应当 < MAJ_ON) *//* ======================== 硬件引脚 ======================== */
/* 与原工程保持一致:PA0/PA1 上拉输入,低电平=按下 */
#define KEY0_PORT   GPIOA
#define KEY0_PIN    GPIO_PIN_0
#define KEY1_PORT   GPIOA
#define KEY1_PIN    GPIO_PIN_1/* 读取原始电平:返回 1=按下(低电平),0=松开(高电平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 位历史 + 事件 ====================== */
/* 稳定状态语义:0=稳定按下,1=稳定松开(用 1 表示松开更直观) */
typedef struct {uint8_t  hist;      // 最近 WIN_BITS 次的“按下=1/松开=0”历史(这里是 8 位)uint8_t  state;     // 当前稳定状态:1=松开,0=按下
} key_swf_t;static key_swf_t key0 = { 0x00, 1 };  // 初始历史按 0(=松开),稳定状态设为松开
static key_swf_t key1 = { 0x00, 1 };/* 完整“按下→松开”点击事件(由滤波判定后置位,key_scan() 读取后清零) */
static volatile uint8_t key0_event = 0;
static volatile uint8_t key1_event = 0;/* 内部节拍器,限制推进频率(防止被高频调用导致“有效消抖时间”变短) */
static uint32_t s_last_tick = 0;/* 高效 8 位 popcount(计算 hist 内 1 的个数) */
static inline uint8_t popcount8(uint8_t x)
{x = x - ((x >> 1) & 0x55);x = (x & 0x33) + ((x >> 2) & 0x33);return (uint8_t)(((x + (x >> 4)) & 0x0F));
}/* 推进一个键的滑动窗口滤波与事件判定* raw_press: 1=按下,0=松开* 过程:*   1) hist 左移一位,最低位写入 raw_press(按下=1/松开=0)*   2) 统计 1 的个数 sum = popcount(hist)*   3) 多数判决 + 回滞:sum >= MAJ_ON → 稳定按下;sum <= MAJ_OFF → 稳定松开*   4) 当“稳定按下→稳定松开”发生时,置 click_flag=1(生成一次点击事件)*/
static void key_swf_step(key_swf_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 移位引入当前样本 */k->hist = (uint8_t)((k->hist << 1) | (raw_press ? 1u : 0u));/* 2) 多数判决 */uint8_t sum = popcount8(k->hist);if (sum >= MAJ_ON) {/* 判为“稳定按下” */if (k->state != 0) {k->state = 0;         // 进入稳定按下// 如需“按下立即上报”可在此处产生 PRESS 事件}} else if (sum <= MAJ_OFF) {/* 判为“稳定松开” */if (k->state != 1) {/* 只有从按下(0)回到松开(1)时,认为一次点击完成 */if (k->state == 0) {*click_flag = 1;  // ——一次“按下→松开”完成}k->state = 1;         // 进入稳定松开}}
}/* ======================== 用户接口 ======================== *//* 初始化 GPIO:与原始代码一致(上拉输入、低电平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能 GPIOA 时钟GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;        // 两个 KEY 引脚gpio_initstruct.Mode  = GPIO_MODE_INPUT;                // 输入gpio_initstruct.Pull  = GPIO_PULLUP;                    // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;            // 输入模式速度无关HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化滤波与事件、节拍 */key0.hist = 0x00; key0.state = 1; key0_event = 0;key1.hist = 0x00; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞扫描(软件滤波版本)* - 建议每 5 ms 调用一次;内部也做了 5 ms 的节拍限速* - 返回:1=KEY0 完成一次“按下→松开”;2=KEY1 完成一次“按下→松开”;0=无事件* - 与原代码“按下后等待松开再返回”的语义一致,但不再阻塞主循环*/
uint8_t key_scan(void)
{/* 节拍器:确保按固定间隔推进一次滤波(防止被过快调用而降低消抖时间) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 未到节拍,仅检查是否已有事件 */goto _check_event_and_return;}s_last_tick = now;/* 采样 + 推进两个键的滤波器 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_swf_step(&key0, r0, &key0_event);key_swf_step(&key1, r1, &key1_event);_check_event_and_return:/* 谁先完成一次点击谁先返回;保证与原代码返回时机一致(在“松开确认”时返回) */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

使用与调参建议

  • 调用频率:保持每 5 ms 调用一次 key_scan()。若你已有固定节拍,可以去掉内部节拍器,固定周期调用。

  • 窗口与阈值

    • 增大 WIN_BITSMAJ_ON,或减小 MAJ_OFF → 抗抖更强但响应更慢;

    • 减小 WIN_BITS,或降低 MAJ_ON、提高 MAJ_OFF → 响应更快但抗抖变弱;

    • 一般保持 MAJ_OFF < MAJ_ON 形成回滞,避免边界抖动来回判定。

🔹 6. 操作系统事件消抖

如果在 FreeRTOSRT-Thread 中:

  • 按键扫描任务用固定周期(比如 10ms);

  • 信号量/消息队列通知主任务。
    这样消抖逻辑集中在一个“键盘驱动任务”里。

  • key_init():初始化 GPIO,并创建一个“按键扫描任务”,每隔 10ms 采样,做去抖状态机;当完成“按下→松开”一次完整点击时,把键值(1 或 2)投递到消息队列

  • key_scan()非阻塞,从队列里取一次事件,有就返回 1/2,没有返回 0

  • 不再使用 delay_ms()while(...)delay.h 可保留但未使用。

算法:状态机 + 固定周期采样(10ms)。当同一状态连续 N 次(这里 N=2 ⇒ 约 20ms)保持一致,才确认“稳定按下/稳定松开”。当从“稳定按下”过渡到“稳定松开”时,判定一次完整点击,并将键值通过 FreeRTOS 队列发给主循环。

key.c:

#include "key.h"
#include "delay.h"              // 本实现已不再使用 delay,可保留以兼容旧工程
#include "stm32f1xx_hal.h"/* ===== FreeRTOS 头文件(事件/任务/定时) ===== */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdint.h>/* ===================== 用户可调参数 ===================== */
/* 扫描周期:10ms(按键任务每 10ms 运行一次) */
#define SCAN_TICKS_MS        10/* 去抖确认次数:例如 2 次(2*10ms = 20ms),按下与松开都用该门限 */
#define CONFIRM_COUNT        2/* 队列容量:能缓存这么多个“完整点击事件” */
#define KEY_EVENT_QUEUE_LEN  8/* 硬件引脚:与原工程保持一致(PA0, PA1 上拉输入,低电平=按下) */
#define KEY0_PORT            GPIOA
#define KEY0_PIN             GPIO_PIN_0
#define KEY1_PORT            GPIOA
#define KEY1_PIN             GPIO_PIN_1/* 物理采样:返回 1=按下(低电平),0=松开(高电平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== OS 对象 ===================== */
static QueueHandle_t s_keyQueue = NULL;    /* 用于把“完整点击事件”发给主循环 */
static TaskHandle_t  s_keyTask  = NULL;    /* 按键扫描任务 *//* ===================== 去抖状态机 ===================== */
/* 4 态:*   IDLE(稳定松开) → PRESS_CHECK(按下确认) → PRESSED(稳定按下)*        → RELEASE_CHECK(松开确认) → IDLE* 只有通过 PRESSED→RELEASE_CHECK→IDLE,才认为一次“按下→松开”的完整点击。*/
typedef enum {KEY_IDLE = 0,        // 稳定松开KEY_PRESS_CHECK,     // 按下确认中KEY_PRESSED,         // 稳定按下KEY_RELEASE_CHECK    // 松开确认中
} key_state_t;typedef struct {key_state_t state;   // 当前状态uint8_t     cnt;     // 连续一致计数(用于确认)
} key_fsm_t;/* 两个键的状态机实例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 推进单键状态机:* raw_press: 1=按下, 0=松开* 完成一次“按下→松开”时,通过队列投递键值 event_val(1 或 2)*/
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, uint8_t event_val)
{switch (k->state){case KEY_IDLE:  /* 稳定松开 → 看到“按下”则进入确认 */if (raw_press) { k->state = KEY_PRESS_CHECK; k->cnt = 1; }break;case KEY_PRESS_CHECK:  /* 按下确认:连续 CONFIRM_COUNT 次均为按下才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) { k->state = KEY_PRESSED; k->cnt = 0; }} else {k->state = KEY_IDLE; k->cnt = 0;  /* 回退 */}break;case KEY_PRESSED:  /* 稳定按下 → 观察到“松开”进入确认 */if (!raw_press) { k->state = KEY_RELEASE_CHECK; k->cnt = 1; }break;case KEY_RELEASE_CHECK:  /* 松开确认:连续 CONFIRM_COUNT 次均为松开才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE; k->cnt = 0;/* ——一次完整点击完成:发事件给主循环 —— */if (s_keyQueue) { uint8_t v = event_val; xQueueSend(s_keyQueue, &v, 0); }}} else {k->state = KEY_PRESSED; k->cnt = 0;  /* 回退 */}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== 按键扫描任务 =====================* 周期:每 10ms 运行一次* 逻辑:读取原始电平 → 推进两个键的状态机 → 若形成“完整点击”,将键值投递到队列*/
static void KeyScanTask(void *arg)
{(void)arg;for (;;){/* 1) 采样两键原始电平(低电平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 2) 推进两路状态机;完成点击时由内部发队列 */key_fsm_step(&key0_fsm, r0, 1);  // 键0 → 事件值 1key_fsm_step(&key1_fsm, r1, 2);  // 键1 → 事件值 2/* 3) 固定周期休眠(10ms) */vTaskDelay(pdMS_TO_TICKS(SCAN_TICKS_MS));}
}/* ===================== 用户接口:初始化 ===================== */
void key_init(void)
{/* 1) GPIO 初始化(与原工程保持一致) */__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能 GPIOA 时钟GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;        // 两个 KEY 引脚gpio_initstruct.Mode  = GPIO_MODE_INPUT;                // 输入gpio_initstruct.Pull  = GPIO_PULLUP;                    // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;            // 输入模式速度无关HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 2) 状态机复位 */key0_fsm.state = KEY_IDLE; key0_fsm.cnt = 0;key1_fsm.state = KEY_IDLE; key1_fsm.cnt = 0;/* 3) 创建事件队列(缓存多个“完整点击”) */s_keyQueue = xQueueCreate(KEY_EVENT_QUEUE_LEN, sizeof(uint8_t));/* 4) 创建按键扫描任务(优先级略高于空闲任务即可) */xTaskCreate(KeyScanTask, "key_scan", 256 /*栈字(4B)*/, NULL,tskIDLE_PRIORITY + 1, &s_keyTask);/* 注意:请确保在 main() 里调用 vTaskStartScheduler() 启动调度器 */
}/* ===================== 用户接口:获取一次事件 ===================== */
/* 行为保持与原代码一致:* - 返回 1:KEY0 完成一次“按下→松开”* - 返回 2:KEY1 完成一次“按下→松开”* - 返回 0:当前无事件* 非阻塞:0 超时读取队列*/
uint8_t key_scan(void)
{if (!s_keyQueue) return 0;uint8_t v = 0;if (xQueueReceive(s_keyQueue, &v, 0) == pdPASS) {return v;       // 1 或 2}return 0;
}

使用说明

  • 在系统初始化流程中调用 key_init(),随后启动 RTOS:vTaskStartScheduler()

  • 主循环或你的业务任务里,像以前一样周期性调用 key_scan()

    • 返回 1/2 代表相应按键完成一次按下→松开

    • 返回 0 表示暂无事件;

  • 调参

    • SCAN_TICKS_MS 控制扫描周期;

    • CONFIRM_COUNT 控制按下/松开确认次数(SCAN_TICKS_MS * CONFIRM_COUNT ≈ 去抖时间,默认 ≈ 20ms)。

如果更想用软件定时器而不是任务,也很容易:把 KeyScanTask 换成 xTimerCreate(..., pdMS_TO_TICKS(10), pdTRUE, ...) 的回调里做同样的状态机推进与 xQueueSend 即可,API 保持不变。

总结

  • 简单项目:延时法足够。

  • 稍复杂的单片机项目:状态机法 / 计数法(非阻塞)。

  • 追求稳定 + 硬件条件允许:RC 滤波 + 软件确认最佳。

  • RTOS 系统:定时扫描 + 消息队列更优雅。

http://www.dtcms.com/a/335209.html

相关文章:

  • C++ 最短路Dijkstra
  • [Python]PTA:for 求奇数分之一序列前N项和
  • 安卓开发选择题
  • CUDA 编程笔记:CUDA延迟隐藏
  • 通配符 重定向 管道符
  • Java 中重载与重写的全面解析(更新版)
  • 在浏览器端使用 xml2js 遇到的报错及解决方法
  • BM25算法和传统的TF-IDF算法的区别
  • 改进版的QGIS 的(属性查询) identify featrues 功能
  • 算法题Day2
  • 计组-间接寻址
  • 抽象代数 · 代数结构 | 群、环、域、向量空间
  • 【QT】常⽤控件详解(八) Qt窗⼝ 菜单栏 工具栏 状态栏 浮动窗口 五种内置对话框
  • Oracle数据库文件管理与空间问题解决指南
  • Java中 23 种设计模式介绍,附带示例
  • IO流-打印流
  • leetcode hot100数组:缺失的第一个正数
  • 洛谷B3924 [GESP202312 二级] 小杨的H字矩阵
  • 洛谷B3865 [GESP202309 二级] 小杨的 X 字矩阵(举一反三)
  • CSDN部分内容改为视频转到B站-清单
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘opencv-python’问题
  • Cloudflare Tunnel 使用SAAS回源加速配置教程
  • 配置 Docker 镜像加速,解决 docker pull 拉取镜像失败、docker search 查询镜像失败等问题
  • Agent中的memory
  • 异构数据库兼容力测评:KingbaseES 与 MySQL 的语法・功能・性能全场景验证解析
  • MySQL性能优化:10个关键参数调整指南
  • ISO27001 高阶架构 之 支持 -2
  • 概率论基础教程第3章条件概率与独立性(三)
  • 从频繁告警到平稳发布:服务冷启动 CPU 风暴优化实践00
  • implement libwhich for Windows