【嵌入式底层知识修炼】基于通用消抖算法,拓展通用非阻塞Key按键识别算法


首先学习别人的东西,然后吸收别人的东西,接着应用并改造别人的东西,最后这个东西就属于你自己了

————小白


  如果你对按键Key识别处理算法有下列需求,则可以移植代码进行使用,并留意注意点

  • 算法所占code少于0.5K
  • 能识别最基本的初始化、短按、长按、松开,四种状态
  • 可移植性高,配置容易,Key可裁剪性高
  • 注意:在得到LongPress之前,会得到一个ShortPress

  对于按键处理,很多MCU芯片都自带库,但是当自己遇到一款没有库并且要实现按键处理功能的需求时,往往要自己实现,我们都不建议造轮子,既然已经有了轮子,我们改造便是

01 - 通用消抖算法

  下列为一个通用的消抖算法,以按键Key消抖为例,Key_IO是按键对应的IO口值,Service_10ms()需要放置一个每10ms就执行一次的硬件或软件定时器中,作用是滤去抖动,消抖的时间是DEBOUNCE_TIMES * 10ms,比如下列宏定义为5,则消抖时间为50ms

#define DEBOUNCE_TIMES 	5

uint8_t Key_CurrState, Key_LastState, Key_Counter;
uint8_t Key_GetState(void) {
   
	uint8_t Flag = Key_IO;	//1、Get key IO value
	if (Key_CurrState != Flag) {
   
		Key_CurrState = Flag;
		Key_Counter = DEBOUNCE_TIMES;
	}
	if (Key_LastState != Key_CurrState) {
   
		if (Key_Counter == 0) {
   
			Key_LastState = Key_CurrState; //2、The key state has changed after debounce
		}
	}
	return Key_LastState;
}

void Key_DebounceService_10ms(void) {
   
	if (PluseKey_Counter) {
   
		PluseKey_Counter--;
	}
}

  其中按键Key真正得到改变的识别代码是注释2所在行,也就是在此能够得到Key改变时当前的状态,可以在此进行拓展

02 - Key按键识别需求

  按键Key识别需要识别出Key最基本的4个状态:初始化、短按、长按、松开,其中初始化是指系统开始时,按键Key没有发生任何变化,此时不算为松开状态,故计入初始化状态,再加上嵌入式的资源紧张条件,要求算法所占code不能太多,因此有下列需求:

  • 算法所占code少于0.5K
  • 能识别最基本的初始化、短按、长按、松开,四种状态
  • 可移植性高,配置容易,Key可裁剪性高

03 - 拓展通用非阻塞Key按键算法

  基于通用消抖算法,我们能够针对上列需求,对算法进行拓展,利用好CurrIOStateLastIOState这两个变量即可,前者代表当前状态,后者代表上一次的状态,对于按键Key,我们定义IO口的状态只有两种:1代表按下,0代表松开,于是能有4种组合:

CurrIOState LastIOState 含义
0 0 初始化
0 1 松开
1 0 短按
1 1 长按

  考虑到可移植性,需要把实际按键Key的IO值抽象,同时IO口的状态也有正负逻辑之分,实际可能是0代表按下,1代表松开,最后实际的长按时间也各不相同,或短或长,需要使用者去定义,于是第一个封装好的结构体如下:

typedef uint8_t (*fpGetIOStateFunc)(void);
typedef struct {
   
	uint8_t LongPressTime;
	uint8_t ZeroIsPress;
	fpGetIOStateFunc GetIOFunc;
} KeyInfo_t;

  此结构体由使用者提供配置,具体含义如下:

变量 含义
LongPressTime 按下多久算长按(不包括消抖时间),单位是10ms,如果为0,使用默认值2000ms
ZeroIsPress 按键Key电路的逻辑,若ZeroIsPress==1,代表Key闭合的时候IO的值为0,Key断开时IO的值为1;0则相反
GetIOFunc 得到按键Key电路IO值的函数,返回uint8_t,此函数必须提供,不能为NULL

  随后,把通用消抖算法所用到的变量进行封装,得到另一个结构体,对使用者而言,只关心其中一个State

typedef enum {
   
	KeyState_None,
	KeyState_ShortPress,
	KeyState_LongPress,
	KeyState_Release
<think>好的,我现在需要帮助用户解决如何使用状态机算法实现多按键的问题。首先,我得理解用户的具体需求。用户提到了状态机算法和多个按键处理,可能是在嵌入式系统中应用,比如使用STM32或FPGA。用户提供的引用资料涉及Verilog、中断处理函数和硬件方法,这让我想到需要结合软件和状态机的设计。 首先,我应该回忆按键的基本原理。机械按键在按下和释放时会产生动,导致电平不稳定,通常持续10-50毫秒。的目的是确保只有稳定的按键动作被识别。传统的方法可能有延时检测,但状态机更适合处理多按键和实时系统,因为它不会阻塞其他任务。 接下来,状态机的设计是关键。每个按键需要一个独立的状态机,这样多个按键可以并行处理。状态通常包括空闲、检测按下、确认按下、检测释放、确认释放。每个状态转换需要时间戳来检查动是否结束。 然后,我需要考虑如何管理多个按键的状态。可能需要使用结构体数组来存储每个按键的状态、计时器和当前状态。这样每个按键可以独立处理,互不干扰。 中断处理可能也是一个点。引用中提到中断处理函数和获取时间戳的函数,所以使用外部中断或定时器中断来触发状态检测,或者定时轮询的方式。定时器中断可以周期性地检查按键状态,比如每10ms一次,这样不会占用太多资源。 代码结构方面,可能需要定义状态枚举类型,按键结构体,初始化函数,状态处理函数,以及在中断或主循环中调用处理函数。例如,在STM32中,可以在定时器中断服务例程中遍历所有按键,更新状态。 同时,要处理按键事件,如按下和释放,可能需要回调函数或事件标志,这样主程序可以响应按键事件而不需要不断轮询。 还需要考虑时间管理,比如使用全局时间戳函数来记录状态转换的时间,以便判断是否过了时间。例如,在检测到按键按下后,等待20ms再次确认,如果仍然按下,则视为有效。 硬件连接方面,按键通常上拉,按下时接地,所以检测低电平表示按下。代码中需要读取GPIO的状态,如引用3中的GPIO_ReadInputDataBit函数。 最后,测试和优化。确保每个按键在不同动情况下都能正确识别,并且状态转换正确。可能还需要处理按键按的情况,但用户主要关注,所以可以暂时不考虑。 总结步骤:设计状态机结构,管理多按键的状态,定时检测,处理状态转换,生成事件。结合用户提供的引用中的状态机和中断处理,给出一个具体的代码框架和说明。</think>### 分步解答:状态机算法实现多按键方案 #### **1. 按键动原理与状态机设计** 机械按键在闭合/断开时会产生$10-50ms$的动信号[^3]。状态机通过时间窗口过滤动,核心设计如下: **状态定义**(每个按键独立): - `IDLE`:空闲状态 - `PRESS_DETECT`:检测到按下信号 - `PRESS_CONFIRMED`:确认有效按下 - `RELEASE_DETECT`:检测到释放信号 - `RELEASE_CONFIRMED`:确认完全释放 $$ \begin{cases} \text{状态迁移条件} \\ \Delta t \geq 20ms \quad \text{(时间阈值)} \end{cases} $$ #### **2. 多按键数据结构设计** 使用结构体数组管理每个按键状态: ```c typedef enum { KEY_IDLE, KEY_PRESS_DETECT, KEY_PRESS_CONFIRMED, KEY_RELEASE_DETECT, KEY_RELEASE_CONFIRMED } KeyState; typedef struct { KeyState state; // 当前状态 uint32_t timestamp; // 状态进入时间 uint8_t pin_value; // 当前引脚电平 uint8_t key_id; // 按键编号 } KeyFSM; ``` #### **3. 核心状态机处理函数** ```c #define DEBOUNCE_TIME 20 // 时间20ms void key_fsm_handler(KeyFSM *key) { uint32_t current_time = course_ms(); // 获取全局时间[^2] switch (key->state) { case KEY_IDLE: if (key->pin_value == PRESSED_LEVEL) { // 检测到按下信号 key->state = KEY_PRESS_DETECT; key->timestamp = current_time; } break; case KEY_PRESS_DETECT: if (current_time - key->timestamp >= DEBOUNCE_TIME) { if (key->pin_value == PRESSED_LEVEL) { // 确认有效按下 key->state = KEY_PRESS_CONFIRMED; on_key_pressed(key->key_id); // 触发按下事件 } else { key->state = KEY_IDLE; // 动导致的误触发 } } break; // 其余状态处理类似,补充释放状态检测 case KEY_PRESS_CONFIRMED: if (key->pin_value == RELEASED_LEVEL) { key->state = KEY_RELEASE_DETECT; key->timestamp = current_time; } break; case KEY_RELEASE_DETECT: if (current_time - key->timestamp >= DEBOUNCE_TIME) { if (key->pin_value == RELEASED_LEVEL) { key->state = KEY_RELEASE_CONFIRMED; on_key_released(key->key_id); // 触发释放事件 } else { key->state = KEY_PRESS_CONFIRMED; } } break; case KEY_RELEASE_CONFIRMED: key->state = KEY_IDLE; break; } } ``` #### **4. 多按键扫描实现** 在定时器中断(例如$10ms$周期)中处理所有按键: ```c KeyFSM keys[TOTAL_KEYS]; // 定义所有按键 void TIM3_IRQHandler(void) { // STM32定时器中断示例 if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { for (int i = 0; i < TOTAL_KEYS; i++) { keys[i].pin_value = GPIO_ReadInputDataBit(KEY_PORT, key_pins[i]); // 读取引脚状态 key_fsm_handler(&keys[i]); } TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } } ``` #### **5. 事件处理接口** ```c // 用户自定义的按键回调函数 void on_key_pressed(uint8_t key_id) { printf("Key%d pressed\n", key_id); // 执行具体业务逻辑 } void on_key_released(uint8_t key_id) { printf("Key%d released\n", key_id); } ``` #### **方案优势** 1. **非阻塞设计**:基于定时器中断的状态机扫描不占用主循环资源 2. **精确**:通过时间戳计算严格过滤动区间 3. **扩展性强**:支持任意数量按键,只需增加结构体数组元素
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值