[参考设计] 非阻塞按键扫描实现

[复制链接]
272|15
磨砂 发表于 2026-8-3 21:39 | 显示全部楼层 |阅读模式
一、基于systick实现获取系统运行时间
1.按键抖动问题

697126a6d8909a3187.png

          注意:在IO口检测到的电压值小于等于0.3VDD(1V左右)是判定为低电平,当电压值大于等于0.7VDD(2.3V左右)时判定为高电平。

          按键抖动:指按下机械按键或开关时,由于金属触点的弹性碰撞,电路在极短时间内产生多次通断的物理现象。

        按键抖动产生的原因:机械按键内部由两个金属触点组成。按下时,触点不会立刻稳定闭合,而是会像弹球一样快速弹跳数次(通常在 1~50 毫秒内),导致电压信号来回跳变。

        如果不处理抖动,每次按键可能被数字电路(如单片机)解读为多次按下。

2.解决方案
2.1 硬件消抖
        硬件消抖是通过在电路中添加无源或有源器件,在信号进入数字电路(如MCU的GPIO)之前就把抖动滤除。

323976a6d88fbb1205.png

        在按键两侧并联一个100nF滤波电容可以起到把抖动干扰滤除。

2.2 软件消抖
        通过时间窗机制实现软件消抖。

3556a6d88f60a2a8.png

阻塞式消抖

834636a6d88f191584.png

        初学者一般会采用阻塞式软件延时方式,但这种方式会导致CPU无法执行其他业务功能模块程序,极大浪费了CPU的执行时间,不能用在实际的项目中。

        优化后应当采用定时器方式实现时间窗机制从而实现非阻塞式的按键扫描。

2.3 SysTick系统滴答定时器

194656a6d88ea94d92.png

       SysTick(系统滴答定时器) 是 ARM Cortex-M 系列处理器内核内置的一个 24 位递减定时器,属于 NVIC(嵌套向量中断控制器 )的一部分。它的存在让所有 Cortex-M 芯片都有一个统一的内核定时代基础,不依赖芯片厂商外设。

499556a6d88e467950.png

        SysTick时钟源有两钟,一种是AHB时钟,也就是内核时钟,还有一种是内核时钟八分频。计数寄存器可以保存计数值,当计数值从高向低递减为0时,会自动从重装载寄存器获取初始值。

①SysTick控制寄存器

318486a6d88e098efc.png

②SysTick计数寄存器(24 位递减计数器)

219536a6d88dada2ec.png

③SysTick重装载寄存器

573436a6d88d531a24.png

2.4 基于systick实现获取系统运行时间代码实现
systick.c

/**
* @file    key_driver.c
* @brief   基于状态机实现非阻塞按键检测驱动
*
* @Details 本驱动实现了一种非阻塞式的按键检测方案,基于有限状态机(FSM)架构。
*          支持以下按键事件检测:
*          - 单击 (Single Click)
*          - 双击 (Double Click)
*          - 三击 (Triple Click)
*          - 长按 (Long Press, >1秒)
*
*          核心设计思想:
*          1. 使用状态机将按键检测逻辑结构化,避免复杂的if-else嵌套
*          2. 基于SysTick定时器(1ms精度)实现时间管理,完全不占用CPU空转等待
*          3. 消抖窗口10ms,连击间隔窗口300ms,长按判定窗口1000ms
*
* ============================================================================
*                           状态机流程图 (State Machine)
* ============================================================================
*
*                          ┌──────────────────────────────────────────────────┐
*                          │                                                 │
*                          ▼                                                 │
*     ┌──────────────────────────────────────────────────────────┐           │
*     │                   KEY_STATE_RELEASE                      │           │
*     │                    (按键释放/空闲态)                      │           │
*     │                                                          │           │
*     │  [A] GPIO==低电平(按下) ─────────────────────────────────►│           │
*     │      记录 IoChangeSysTime = 当前时间                      │           │
*     │      keyState → DEBOUNCING                               │           │
*     │                          ┌─────────────────────────────┐ │           │
*     │  [B] 距离releaseTime     │                             │ │           │
*     │      超过INTERVAL_TIME   │  这是"延迟返回"机制:         │ │           │
*     │      且clickNum==1 ────►│  按键释放后不立即返回码值,    │ │           │
*     │      返回 单击码值       │  而是等待INTERVAL_TIME窗口,  │ │           │
*     │      clickNum清零        │  用于区分单击/双击/三击       │ │           │
*     │                          └─────────────────────────────┘ │           │
*     │  [C] 距离releaseTime     ┌─────────────────────────────┐ │           │
*     │      超过INTERVAL_TIME   │  同理,等待窗口内没有再按下   │ │           │
*     │      且clickNum==2 ────►│  才确认是双击                  │ │           │
*     │      返回 双击码值       └─────────────────────────────┘ │           │
*     │                                                          │           │
*     │  [D] 距离releaseTime     ┌─────────────────────────────┐ │           │
*     │      超过INTERVAL_TIME   │  同理确认三击                  │ │           │
*     │      且clickNum==3 ────►│                                │ │
*     │      返回 三击码值       └─────────────────────────────┘ │           │
*     └──────────┬──────────────────────▲────────────────────────┘           │
*                │                      │                                    │
*     [A]按下    │                      │ [X] 抖动/干扰: 释放                 │
*                ▼                      │ (假按下,回到释放态)               │
*     ┌──────────────────────────────────────────────────────┐               │
*     │              KEY_STATE_DEBOUNCING                    │               │
*     │                 (按键消抖态)                          │               │
*     │                                                      │               │
*     │  等待消抖时间窗口 DEBOUNCING_TIME (10ms)               │───────────────┘
*     │                                                      │
*     │  [Y] 等待时间不足10ms → 直接break,下次再判断         │
*     │                                                      │
*     │  [Z] 10ms后GPIO仍为低电平 → 确认按下                   │
*     │      keyState → SHORT_PRESSED                        │
*     └──────────────────┬───────────────────────────────────┘
*                        │
*     [Z] 确认按下        │
*                        ▼
*     ┌──────────────────────────────────────────────────────┐
*     │            KEY_STATE_SHORT_PRESSED                   │
*     │               (短按确认态)                            │
*     │                                                      │
*     │  [E] GPIO==高电平(释放) ─────────────────────────────►│
*     │      clickNum++    (点击次数+1)                       │
*     │      releaseTime = 当前时间                           │
*     │      keyState → RELEASE                              │
*     │                                                      │
*     │  [F] 按住不放 && 距离IoChangeSysTime                   │
*     │      超过LONGPRESS_TIME(1000ms) ─────────────────────►│
*     │      keyState → LONG_PRESSED                         │
*     │      (注:此处不返回码值,等释放时才返回)              │
*     └──────────┬───────────────────┬───────────────────────┘
*                │                   │
*     [E] 释放   │                   │ [F] 持续按住>1s
*                ▼                   ▼
*     ┌──────────────┐    ┌──────────────────────────────────┐
*     │   RELEASE    │    │       KEY_STATE_LONG_PRESSED      │
*     │ (回去等连击)  │    │          (长按态)                  │
*     └──────────────┘    │                                  │
*                         │  [G] GPIO==高电平(释放) ──────────►│
*                         │      clickNum = 0  (清零)         │
*                         │      keyState → RELEASE           │
*                         │      返回 长按码值                 │
*                         └──────────────┬───────────────────┘
*                                        │
*                         [G] 释放       │
*                                        ▼
*                              ┌──────────────┐
*                              │   RELEASE    │
*                              └──────────────┘
*
* ============================================================================
*                        时序示意图 (以双击为例)
* ============================================================================
*
*     按下        释放      按下        释放              超时返回双击码值
*      │           │         │           │                       │
*      ▼           ▼         ▼           ▼                       ▼
* ────┐┌─────────┐┌───────────────────┐┌──────────────────────────────
*     └┘         └┘                   └┘
*     │←─10ms──→│                      │
*      消抖窗口                         │
*     │         │←─────300ms──────────→│
*               连击间隔判定窗口
*                                     │←──────300ms───────────────────→│
*                                      释放后等待,确认没有第3次按下
*
* ============================================================================
*                          按键码值定义表
* ============================================================================
*
*  按键    单击    双击    三击    长按
*  KEY1     1      11     21      31
*  KEY2     2      12     22      32
*  KEY3     3      13     23      33
*  KEY4     4      14     24      34
*
*  码值计算公式:按键索引(index)取 0~3 分别对应 KEY1~KEY4
*    单击 = index + 1
*    双击 = index + 11
*    三击 = index + 21
*    长按 = index + 31
*
* ============================================================================
*                          关键时间参数
* ============================================================================
*
*  DEBOUNCING_TIME  = 10ms    消抖时间窗口:滤除机械触点抖动
*  LONGPRESS_TIME   = 1000ms  长按判定时间:按住超过此时间为长按
*  INTERVAL_TIME    = 300ms   连击间隔时间:两次释放之间超过此时间确认连击次数
*/

#include <stdint.h>
#include "gd32f30x.h"
#include "systick.h"
#include "key_driver.h"

/**
* @brief GPIO引脚配置结构体
*
* 将每个按键的RCU时钟、GPIO端口、引脚号封装在一起,
* 便于通过数组统一管理和遍历。
*/
typedef struct
{
    rcu_periph_enum rcu;   /**< RCU外设时钟,如 RCU_GPIOA */
    uint32_t port;          /**< GPIO端口基地址,如 GPIOA */
    uint32_t pin;           /**< GPIO引脚号,如 GPIO_PIN_0 */
} key_GPIO_t;

/**
* @brief 按键硬件引脚映射表
*
* 定义了4个按键对应的GPIO硬件连接关系。
* 添加新按键只需在此数组中增加一行即可。
*
* KEY1 → PA0   (WKUP按键,通常位于开发板)
* KEY2 → PG13
* KEY3 → PG14
* KEY4 → PG15
*/
static key_GPIO_t g_gpioList[] =
{
    {RCU_GPIOA, GPIOA, GPIO_PIN_0},   // PA0  → KEY1
    {RCU_GPIOG, GPIOG, GPIO_PIN_13},  // PG13 → KEY2
    {RCU_GPIOG, GPIOG, GPIO_PIN_14},  // PG14 → KEY3
    {RCU_GPIOG, GPIOG, GPIO_PIN_15}   // PG15 → KEY4
};

/** @brief 自动计算按键数量,方便后续遍历 */
#define KEY_NUM_MAX (sizeof(g_gpioList) / sizeof(g_gpioList[0]))

/**
* @brief 按键硬件初始化
*
* 遍历所有按键,依次:
* 1. 使能对应GPIO端口的RCU外设时钟
* 2. 将引脚配置为浮空输入模式(按键按下时接地,读取低电平)
*
* @NOTE  必须在使用任何GPIO之前调用此函数
* @note  浮空输入模式:按键未按下时引脚悬空,读取电平不确定;
*        实际应用中建议使用上拉输入(GPIO_MODE_IPU),确保未按下时读到高电平
*/
void KeyDrvInit(void)
{
    for (uint8_t i = 0; i < KEY_NUM_MAX; i++)
    {
        /* 使能GPIO端口时钟——没有时钟,GPIO外设不工作 */
        rcu_periph_clock_enable(g_gpioList.rcu);

        /* 配置为浮空输入,2MHz速度(输入模式速度参数实际意义不大) */
        gpio_init(g_gpioList.port, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_2MHZ, g_gpioList.pin);
    }
}

/*===========================================================================
*                         状态机实现非阻塞按键检测
*
*  设计要点:
*  1. 整个扫描函数无阻塞(while/for死等),每次调用立即返回
*  2. 所有时间判断基于 SysTick 系统运行时间(1ms精度),不依赖于延时函数
*  3. 需要在主循环或定时器中周期性调用 GetKeyVal() 驱动状态机运转
*=========================================================================== */

/**
* @brief 按键状态枚举
*
* 状态机仅包含4个状态,简洁清晰:
* - RELEASE:      按键处于空闲/释放状态,等待按下事件
* - DEBOUNCING:   检测到电平变化,正在等待消抖时间窗通过
* - SHORT_PRESSED:消抖通过,确认按键被按下,等待释放或超时(长按)
* - LONG_PRESSED: 按住超过1秒,已经确认为长按,等待释放
*/
typedef enum
{
    KEY_STATE_RELEASE,       /**< 释放态:按键空闲,等待被按下 */
    KEY_STATE_DEBOUNCING,    /**< 消抖态:检测到按下,正在进行10ms消抖 */
    KEY_STATE_SHORT_PRESSED, /**< 短按确认态:消抖通过,判断是短按还是长按 */
    KEY_STATE_LONG_PRESSED   /**< 长按态:已确认为长按,等待释放后返回码值 */
} KEY_STATE;

/**
* @brief 每个按键的运行时信息结构体
*
* 每个按键独立维护自己的状态和计时信息,互不干扰。
* 由于是全局静态变量,数据保存在.bss段,启动时自动清零。
*/
typedef struct
{
    KEY_STATE keyState;          /**< 按键当前所处状态 */
    uint64_t  IoChangeSysTime;   /**< GPIO电平变化时刻的系统时间(ms),用于消抖计时和长按判断的起点 */
    uint64_t  releaseTime;       /**< 按键释放时刻的系统时间(ms),用于连击间隔判断的起点 */
    uint8_t   keyClickNum;       /**< 连击计数器:记录连续按下了几次(1=单击, 2=双击, 3=三击) */
} KeyInfo_t;

/** @brief 4个按键各自的状态信息数组,索引与g_gpioList对应 */
static KeyInfo_t g_keyInfo[KEY_NUM_MAX];

/*===========================================================================
*                         时间阈值定义 (单位: ms)
*=========================================================================== */

/**
* @brief 消抖时间窗口
*
* 机械按键在按下/释放瞬间会产生抖动(触点弹跳),
* 表现为几毫秒到十几毫秒内电平反复跳变。
* 10ms是常见的消抖窗口,足以覆盖绝大多数机械按键的抖动期。
*/
#define DEBOUNCING_TIME   (10)

/**
* @brief 长按判定时间
*
* 按键保持按下状态超过此时间(1秒)即判定为长按。
* 计时起点是消抖通过、进入SHORT_PRESSED状态的时刻(IoChangeSysTime)。
*/
#define LONGPRESS_TIME    (1000)

/**
* @brief 连击间隔判定时间
*
* 当按键释放后,在此时间窗口(300ms)内:
* - 如果再次按下 → 说明是连击(双击/三击)
* - 如果超时未按下 → 连击结束,根据keyClickNum返回对应的单击/双击/三击码值
*
* 300ms是一个经验值,兼顾响应速度与误触防范。
*/
#define INTERVAL_TIME     (300)

/**
* @brief 单个按键的状态机扫描函数
*
* 这是整个驱动最核心的函数,实现了完整的按键状态机。
*
* 调用时机:由 GetKeyVal() 在循环中依次对每个按键调用。
* 调用频率:建议不低于 100Hz(即每10ms至少调用一次),
*           否则可能错过消抖窗口内的电平采样。
*
* @param  keyIndex  按键索引 (0~3),对应 g_gpioList 和 g_keyInfo 的下标
* @return 按键码值,编码规则如下:
*         - 0                    : 无事件(最常见返回值)
*         - keyIndex + 1  (1~4)  : 单击
*         - keyIndex + 11 (11~14): 双击
*         - keyIndex + 21 (21~24): 三击
*         - keyIndex + 31 (31~34): 长按
*
* ============================================================================
* 状态转移详解:
*
* [RELEASE 态]
*   条件A: GPIO读到低电平(RESET=按下)
*     → 动作: 记录 IoChangeSysTime = 当前时间
*     → 转移: keyState → DEBOUNCING
*     → 说明: 检测到按下事件,进入消抖流程,无返回值
*
*   条件B: 距离上次释放超过 INTERVAL_TIME 且 clickNum==1
*     → 动作: clickNum 清零
*     → 返回: 单击码值 (keyIndex+1)
*     → 说明: 释放后等待了300ms没有再次按下,确认为单击
*
*   条件C: 距离上次释放超过 INTERVAL_TIME 且 clickNum==2
*     → 动作: clickNum 清零
*     → 返回: 双击码值 (keyIndex+11)
*     → 说明: 释放后等待了300ms没有第3次按下,确认为双击
*
*   条件D: 距离上次释放超过 INTERVAL_TIME 且 clickNum==3
*     → 动作: clickNum 清零
*     → 返回: 三击码值 (keyIndex+21)
*     → 说明: 释放后等待了300ms没有第4次按下,确认为三击
*
* [DEBOUNCING 态]
*   条件X: 距离IoChangeSysTime不足10ms
*     → 动作: 无 (直接break)
*     → 说明: 消抖窗口未满,继续等待,下次扫描再判断
*
*   条件Y: 10ms等待期满,GPIO仍为低电平(RESET)
*     → 转移: keyState → SHORT_PRESSED
*     → 说明: 消抖通过,确认按键真的被按下了
*
*   条件Z: 10ms等待期满,GPIO已恢复高电平(!= RESET)
*     → 转移: keyState → RELEASE
*     → 说明: 之前的低电平只是抖动/干扰,不是真正的按下
*
* [SHORT_PRESSED 态]
*   条件E: GPIO读到高电平(!= RESET,即按键释放)
*     → 动作: clickNum++ (连击计数+1), 记录 releaseTime = 当前时间
*     → 转移: keyState → RELEASE
*     → 说明: 按键释放了,回到释放态等待连击或超时返回
*
*   条件F: 距离IoChangeSysTime超过1000ms 且 GPIO仍为低电平
*     → 转移: keyState → LONG_PRESSED
*     → 说明: 按住超过1秒,升级为长按
*
* [LONG_PRESSED 态]
*   条件G: GPIO读到高电平(!= RESET,即释放)
*     → 动作: clickNum = 0 (清零,长按后不参与连击)
*     → 转移: keyState → RELEASE
*     → 返回: 长按码值 (keyIndex+31)
*     → 说明: 长按释放,返回长按码值。注意与短按不同——
*             短按释放时不立即返回码值(等连击窗口),
*             长按释放时立即返回码值(长按不参与连击逻辑)
* ============================================================================
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
    /* 读取当前GPIO电平:低电平(RESET)=按键按下(接地导通),高电平(SET)=按键释放 */
    FlagStatus keyIoVal = gpio_input_bit_get(g_gpioList[keyIndex].port, g_gpioList[keyIndex].pin);

    switch (g_keyInfo[keyIndex].keyState)
    {
        /*------------------------------------------------------------------
         * 状态1: RELEASE —— 按键空闲,等待按下
         * 这是状态机的"大本营",大部分时间停留在此状态
         *------------------------------------------------------------------*/
        case KEY_STATE_RELEASE:

            /* [条件A] 检测到按键按下(低电平) */
            if (keyIoVal == RESET)
            {
                /* 记录电平变化时间戳,作为消抖计时和长按计时的起点 */
                g_keyInfo[keyIndex].keyState = KEY_STATE_DEBOUNCING;
                g_keyInfo[keyIndex].IoChangeSysTime = GetSysRunTime();
            }
            /* [条件B] 释放后等待超时 + 之前只按了1次 → 确认为单击 */
            else if ((GetSysRunTime() - g_keyInfo[keyIndex].releaseTime >= INTERVAL_TIME)
                     && (g_keyInfo[keyIndex].keyClickNum == 1))
            {
                g_keyInfo[keyIndex].keyClickNum = 0;
                return (keyIndex + 1);   // 返回单击码值: KEY1→1, KEY2→2, KEY3→3, KEY4→4
            }
            /* [条件C] 释放后等待超时 + 之前按了2次 → 确认为双击 */
            else if ((GetSysRunTime() - g_keyInfo[keyIndex].releaseTime >= INTERVAL_TIME)
                     && (g_keyInfo[keyIndex].keyClickNum == 2))
            {
                g_keyInfo[keyIndex].keyClickNum = 0;
                return (keyIndex + 11);  // 返回双击码值: KEY1→11, KEY2→12, KEY3→13, KEY4→14
            }
            /* [条件D] 释放后等待超时 + 之前按了3次 → 确认为三击 */
            else if ((GetSysRunTime() - g_keyInfo[keyIndex].releaseTime >= INTERVAL_TIME)
                     && (g_keyInfo[keyIndex].keyClickNum == 3))
            {
                g_keyInfo[keyIndex].keyClickNum = 0;
                return (keyIndex + 21);  // 返回三击码值: KEY1→21, KEY2→22, KEY3→23, KEY4→24
            }
            break;

        /*------------------------------------------------------------------
         * 状态2: DEBOUNCING —— 消抖等待
         * 等待10ms消抖窗口,然后根据电平决定是真按下还是干扰
         *------------------------------------------------------------------*/
        case KEY_STATE_DEBOUNCING:

            /* [条件X] 消抖窗口尚未满10ms,不做任何判断,直接退出 */
            if (GetSysRunTime() - g_keyInfo[keyIndex].IoChangeSysTime < DEBOUNCING_TIME)
            {
                break;  // 什么都不做,等下次扫描再来检查
            }

            /* [条件Y] 10ms后仍然是低电平 → 消抖通过,是真按下! */
            if (keyIoVal == RESET)
            {
                g_keyInfo[keyIndex].keyState = KEY_STATE_SHORT_PRESSED;
                /* 注意:IoChangeSysTime 保持不变,用于后续长按计时的起点 */
            }
            /* [条件Z] 10ms后恢复高电平 → 是抖动/干扰,回到释放态 */
            else
            {
                g_keyInfo[keyIndex].keyState = KEY_STATE_RELEASE;
            }
            break;

        /*------------------------------------------------------------------
         * 状态3: SHORT_PRESSED —— 短按已确认,等待释放或超时升级为长按
         * 这是状态机的"决策中枢":区分短按释放 vs 持续按住(长按)
         *------------------------------------------------------------------*/
        case KEY_STATE_SHORT_PRESSED:

            /* [条件E] 按键释放(电平恢复高) */
            if (keyIoVal != RESET)
            {
                g_keyInfo[keyIndex].keyState = KEY_STATE_RELEASE;
                g_keyInfo[keyIndex].keyClickNum++;               // 连击计数递增
                g_keyInfo[keyIndex].releaseTime = GetSysRunTime(); // 记录释放时间,用于连击窗口判断
                /*
                 * 注意:这里不立即返回码值!
                 * 因为可能是连击(双击/三击),需要回到RELEASE态等待300ms窗口:
                 * - 如果300ms内再次按下 → clickNum继续递增,走双击/三击逻辑
                 * - 如果300ms内没有再次按下 → 在RELEASE态的条件B/C/D中返回对应码值
                 */
            }
            /* [条件F] 仍然按住,且已超过长按阈值1秒 → 升级为长按 */
            else
            {
                if ((GetSysRunTime() - g_keyInfo[keyIndex].IoChangeSysTime) > LONGPRESS_TIME)
                {
                    g_keyInfo[keyIndex].keyState = KEY_STATE_LONG_PRESSED;
                    /* 切换到长按态等待释放,此时不返回码值 */
                }
            }
            break;

        /*------------------------------------------------------------------
         * 状态4: LONG_PRESSED —— 长按已确认,等待释放
         * 长按释放时立即返回码值,不参与连击逻辑
         *------------------------------------------------------------------*/
        case KEY_STATE_LONG_PRESSED:

            /* [条件G] 长按终于释放了 */
            if (keyIoVal != RESET)
            {
                g_keyInfo[keyIndex].keyState = KEY_STATE_RELEASE;
                g_keyInfo[keyIndex].keyClickNum = 0;              // 清零连击计数(长按不参与连击)
                return (keyIndex + 31);  // 返回长按码值: KEY1→31, KEY2→32, KEY3→33, KEY4→34
            }
            /*
             * 如果还没释放,继续等在LONG_PRESSED态,什么也不做
             */
            break;

        /* 理论上不会执行到这里,防御性编程 */
        default:
            break;
    }

    /* 返回0表示本次扫描无按键事件产生 */
    return 0;
}

/**
* @brief 获取按键码值(对外接口)
*
* 这是应用层唯一需要调用的按键接口函数。
* 内部遍历所有4个按键,调用KeyScan()进行状态机扫描。
*
* 设计意图:
* - 屏蔽底层状态机实现细节
* - 应用层只需关心收到的按键码值,无需了解是短按/长按/多击
* - 单次调用只返回一个按键事件(优先级: KEY1 > KEY2 > KEY3 > KEY4)
*
* @Attention 必须在主循环或定时器中周期性调用(建议≥100Hz),
*            否则状态机无法正常运转,可能漏掉按键事件。
*
* @return 按键码值
*         - 0    : 当前无按键事件
*         - 1~4  : 单击 (KEY1~KEY4)
*         - 11~14: 双击
*         - 21~24: 三击
*         - 31~34: 长按
*/
uint8_t GetKeyVal(void)
{
    uint8_t res = 0;

    /* 依次扫描 KEY1 → KEY2 → KEY3 → KEY4 */
    for (uint8_t i = 0; i < KEY_NUM_MAX; i++)
    {
        res = KeyScan(i);
        /* 一旦某个按键有事件产生,立即返回(优先级机制) */
        if (res != 0)
        {
            return res;
        }
    }

    /* 所有按键都没有事件 */
    return 0;
}


#ifndef __KEY_DRIVER_H__
#define __KEY_DRIVER_H__

#include <stdint.h>

#define KEY1_SINGLE_SHORT_PRESSED        (1)
#define KEY1_DOUBLE_SHORT_PRESSED        (11)
#define KEY1_TRIPLE_SHORT_PRESSED        (21)
#define KEY1_LONG_PRESS                            (31)

#define KEY2_SINGLE_SHORT_PRESSED        (2)
#define KEY2_DOUBLE_SHORT_PRESSED        (12)
#define KEY2_TRIPLE_SHORT_PRESSED        (22)
#define KEY2_LONG_PRESS                            (32)

#define KEY3_SINGLE_SHORT_PRESSED        (3)
#define KEY3_DOUBLE_SHORT_PRESSED        (13)
#define KEY3_TRIPLE_SHORT_PRESSED        (23)
#define KEY3_LONG_PRESS                            (33)

#define KEY4_SINGLE_SHORT_PRESSED        (4)
#define KEY4_DOUBLE_SHORT_PRESSED        (14)
#define KEY4_TRIPLE_SHORT_PRESSED        (24)
#define KEY4_LONG_PRESS                            (34)

/**
***********************************************************
* @brief 按键硬件初始化
* @param
* @return
***********************************************************
*/
void KeyDrvInit(void);

/**
***********************************************************
* @brief 获取按键码值,屏蔽底层实现,应用只关心按键值,不需要关心是短按/长按还是多连击
* @param
* @return 四个按键码值,短按0x01 0x02 0x03,0x04
***********************************************************
*/
uint8_t GetKeyVal(void);

#endif /*__KEY_DRIVER_H__ */

105046a6d88bfb2558.png

   ticks:两次 SysTick 中断之间的计数值(即重装载值)。SysTick 定时器是一个 24 位递减计数器,因此 ticks 的有效范围是 1 ~ 0x00FFFFFF(即 1 ~ 16,777,215)。

159946a6d88bc84b90.png

        注意:NVIC_SetPriority函数是内核异常优先级设置,设置为4位抢占优先级,范围是0~15。

二、基于状态机和SysTick系统滴答定时器实现非阻塞按键扫描
1.按键扫描的软件架构

895456a6d88b3420ca.png

2.状态机编程思想介绍
        状态机(State Machine)思想,简单来说,就是把事物看作由有限个“状态”组成,并且这些状态在特定“事件”触发下,按照严格规则进行“转移”。        

        核心三要素:状态、事件、转移。

        ①状态(State):系统在生命周期中,一个稳定、可被识别的状况或模式。比如电灯的“亮”和“灭”,订单的“待支付”、“已发货”、“已完成”。在任一时刻,系统必须且只能处于一个确定的状态。

        ②事件(Event):会触发状态转移的外部或内部“刺激”。比如按下开关、支付成功、倒计时归零。注意,不是所有事件都会引发转移,取决于当前状态。

        ③转移(Transition):系统在某个状态下,接收到某个事件后,从当前状态切换到下一个状态的过程。它定义了规则:“如果在状态A,发生了事件X,那么就执行动作Y,并转移到状态B”。

        以按键扫描为例:

889526a6d88aa8b772.png

————————————————
版权声明:本文为CSDN博主「原来是小志同学呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2402_88673453/article/details/162573287

xiaoqizi 发表于 2026-8-6 20:57 | 显示全部楼层
利用硬件定时器产生周期性中断,在中断服务函数中调用按键扫描函数
木木guainv 发表于 2026-8-6 23:11 | 显示全部楼层
摒弃简单的延时消抖,采用状态机来记录按键的当前状态
Jiangxiaopi 发表于 2026-8-7 08:48 | 显示全部楼层
事件触发机制:扫描函数只负责更新按键状态并记录事件,主循环通过查询事件标志位来执行对应的业务逻辑,实现硬件检测与业务处理的完全解耦
荣陶陶 发表于 2026-8-7 10:57 | 显示全部楼层
使能对应 GPIO 端口的时钟,并将按键引脚配置为输入模式
Zhiniaocun 发表于 2026-8-7 13:14 | 显示全部楼层
根据外部硬件电路,IO引脚需要配置为内部上拉或下拉输入。
Zuocidian 发表于 2026-8-7 15:44 | 显示全部楼层
定义按键的状态枚举和触发的事件类型,用于状态机流转和主循环的事件处理
Puchou 发表于 2026-8-7 18:01 | 显示全部楼层
非阻塞扫描函数需要被定时器中断以固定的周期调用。通过读取引脚电平并结合计数器,完成消抖和状态判定。

Xiashiqi 发表于 2026-8-7 20:13 | 显示全部楼层
零阻塞:主循环完全释放,系统可无缝接入 RTOS 或处理复杂的通信协议。
小海师 发表于 2026-8-7 22:33 | 显示全部楼层
// 全局事件变量volatile key_event_t current_key_event = KEY_EVENT_NONE;// 定时器中断服务函数 (示例)void TIMER1_IRQHandler(void){    if(RESET != timer_interrupt_flag_get(TIMER1, TIMER_INT_FLAG_UP)){        timer_interrupt_flag_clear(TIMER1, TIMER_INT_FLAG_UP);                // 周期性调用非阻塞扫描        key_scan_non_blocking(&current_key_event);    }}// 主循环int main(void){    system_init();    key_gpio_init();    timer_interrupt_init(); // 配置10ms定时中断        while(1)    {        // 主循环不阻塞,仅轮询事件标志        if(current_key_event != KEY_EVENT_NONE)        {            switch(current_key_event)            {                case KEY_EVENT_SHORT:                    // 执行短按业务逻辑                    break;                case KEY_EVENT_LONG:                    // 执行长按业务逻辑                    break;                default:                    break;            }            current_key_event = KEY_EVENT_NONE; // 清除事件标志        }                // 主循环可以毫无阻碍地执行其他实时任务        other_real_time_tasks();     }}
Haizangwang 发表于 2026-8-8 09:05 | 显示全部楼层
只需在状态机中增加状态分支,即可轻松实现“双击”、“长按+短按组合”等复杂的交互逻辑
捉虫天师 发表于 2026-8-7 22:23 | 显示全部楼层
不原地循环等待按键电平变化,仅在主循环轮询引脚电平,搭配定时消抖、状态机记录按键阶段,每次轮询只做短时判断,判断结束立刻交出 CPU 执行其他业务;按键按下、松开事件触发时执行对应逻辑,全程不占用主循环阻塞时长。
班杰明 发表于 2026-8-13 14:28 | 显示全部楼层
用SysTick定时器实现非阻塞扫描,关键是要合理设置中断周期,避免中断频率过高或过低。实际项目中,我一般将中断周期设置为10-50ms,这样既能有效消抖,又不会影响其他任务执行。
EchoInSilence 发表于 2026-8-21 12:27 | 显示全部楼层
状态机加定时器中断,实现非阻塞消抖,效果不错。
风之呢喃 发表于 2026-8-22 13:19 | 显示全部楼层
用SysTick定时器实现非阻塞扫描,关键在于设置合理的超时时间,避免因抖动导致误判。记得检查SysTick的时钟源和分频设置,确保定时精度。
SophiaOP 发表于 2026-8-23 09:52 | 显示全部楼层
用SysTick定时器,在中断中扫描按键,判断稳定后记录状态,主循环处理按键事件。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

230

主题

4738

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部
0