一、基于systick实现获取系统运行时间1.按键抖动问题注意在IO口检测到的电压值小于等于0.3VDD1V左右是判定为低电平当电压值大于等于0.7VDD2.3V左右时判定为高电平。按键抖动指按下机械按键或开关时由于金属触点的弹性碰撞电路在极短时间内产生多次通断的物理现象。按键抖动产生的原因机械按键内部由两个金属触点组成。按下时触点不会立刻稳定闭合而是会像弹球一样快速弹跳数次通常在1~50 毫秒内导致电压信号来回跳变。如果不处理抖动每次按键可能被数字电路如单片机解读为多次按下。2.解决方案2.1 硬件消抖硬件消抖是通过在电路中添加无源或有源器件在信号进入数字电路如MCU的GPIO之前就把抖动滤除。在按键两侧并联一个100nF滤波电容可以起到把抖动干扰滤除。2.2 软件消抖通过时间窗机制实现软件消抖。阻塞式消抖初学者一般会采用阻塞式软件延时方式但这种方式会导致CPU无法执行其他业务功能模块程序极大浪费了CPU的执行时间不能用在实际的项目中。优化后应当采用定时器方式实现时间窗机制从而实现非阻塞式的按键扫描。2.3SysTick系统滴答定时器SysTick系统滴答定时器是 ARM Cortex-M 系列处理器内核内置的一个24 位递减定时器属于 NVIC嵌套向量中断控制器的一部分。它的存在让所有 Cortex-M 芯片都有一个统一的内核定时代基础不依赖芯片厂商外设。SysTick时钟源有两钟一种是AHB时钟也就是内核时钟还有一种是内核时钟八分频。计数寄存器可以保存计数值当计数值从高向低递减为0时会自动从重装载寄存器获取初始值。①SysTick控制寄存器②SysTick计数寄存器24 位递减计数器③SysTick重装载寄存器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 │ 这是延迟返回机制 │ │ │ * │ 且clickNum1 ────►│ 按键释放后不立即返回码值 │ │ │ * │ 返回 单击码值 │ 而是等待INTERVAL_TIME窗口 │ │ │ * │ clickNum清零 │ 用于区分单击/双击/三击 │ │ │ * │ └─────────────────────────────┘ │ │ * │ [C] 距离releaseTime ┌─────────────────────────────┐ │ │ * │ 超过INTERVAL_TIME │ 同理等待窗口内没有再按下 │ │ │ * │ 且clickNum2 ────►│ 才确认是双击 │ │ │ * │ 返回 双击码值 └─────────────────────────────┘ │ │ * │ │ │ * │ [D] 距离releaseTime ┌─────────────────────────────┐ │ │ * │ 超过INTERVAL_TIME │ 同理确认三击 │ │ │ * │ 且clickNum3 ────►│ │ │ * │ 返回 三击码值 └─────────────────────────────┘ │ │ * └──────────┬──────────────────────▲────────────────────────┘ │ * │ │ │ * [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[i].rcu); /* 配置为浮空输入2MHz速度输入模式速度参数实际意义不大 */ gpio_init(g_gpioList[i].port, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_2MHZ, g_gpioList[i].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 且 clickNum1 * → 动作: clickNum 清零 * → 返回: 单击码值 (keyIndex1) * → 说明: 释放后等待了300ms没有再次按下确认为单击 * * 条件C: 距离上次释放超过 INTERVAL_TIME 且 clickNum2 * → 动作: clickNum 清零 * → 返回: 双击码值 (keyIndex11) * → 说明: 释放后等待了300ms没有第3次按下确认为双击 * * 条件D: 距离上次释放超过 INTERVAL_TIME 且 clickNum3 * → 动作: clickNum 清零 * → 返回: 三击码值 (keyIndex21) * → 说明: 释放后等待了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 * → 返回: 长按码值 (keyIndex31) * → 说明: 长按释放返回长按码值。注意与短按不同—— * 短按释放时不立即返回码值等连击窗口 * 长按释放时立即返回码值长按不参与连击逻辑 * */ 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 0x030x04 *********************************************************** */ uint8_t GetKeyVal(void); #endif /*__KEY_DRIVER_H__ */ticks两次 SysTick 中断之间的计数值即重装载值。SysTick 定时器是一个 24 位递减计数器因此ticks的有效范围是 1 ~ 0x00FFFFFF即 1 ~ 16,777,215。注意NVIC_SetPriority函数是内核异常优先级设置设置为4位抢占优先级范围是0~15。二、基于状态机和SysTick系统滴答定时器实现非阻塞按键扫描1.按键扫描的软件架构2.状态机编程思想介绍状态机State Machine思想简单来说就是把事物看作由有限个“状态”组成并且这些状态在特定“事件”触发下按照严格规则进行“转移”。核心三要素状态、事件、转移。①状态State系统在生命周期中一个稳定、可被识别的状况或模式。比如电灯的“亮”和“灭”订单的“待支付”、“已发货”、“已完成”。在任一时刻系统必须且只能处于一个确定的状态。②事件Event会触发状态转移的外部或内部“刺激”。比如按下开关、支付成功、倒计时归零。注意不是所有事件都会引发转移取决于当前状态。③转移Transition系统在某个状态下接收到某个事件后从当前状态切换到下一个状态的过程。它定义了规则“如果在状态A发生了事件X那么就执行动作Y并转移到状态B”。以按键扫描为例