STM32定时任务的解耦设计与按键事件处理

在本篇文章中,我将介绍一种基于定时器的解耦设计方法,通过函数指针和定时器的结合,使得按键事件的处理和定时任务管理能够灵活、解耦地工作。这个方案不仅能够提高代码的可维护性,还能有效避免不必要的模块依赖,提升系统的可扩展性。

1. 定时任务管理的解耦设计

在裸机环境下,我们通常使用定时器来周期性地执行任务。但是,如果每个定时器都需要绑定到一个特定的函数,代码中就容易产生硬编码的依赖关系,导致耦合度过高。为了解决这一问题,我们可以使用函数指针的方式来解耦定时任务与具体的业务逻辑。

在实现中,首先定义一个任务函数指针类型,接着为每个定时器(例如 TIM1、TIM2)分别绑定对应的任务。通过这种方式,我们可以轻松地为不同的定时器绑定不同的任务,无需直接修改定时器中断处理函数。

例如,下面是一个定时任务管理的框架:和之前定时器内容差不多,只不过这一次把定时器输入放在了外面
基于 STM32 定时器的动态任务调度与函数指针应用

// 定义定时器任务函数指针类型
typedef void (*TimerTask_t)(void);

// 定时器初始化并绑定任务
void MY_timer_init(TIM_HandleTypeDef *htim, TimerTask_t task);

// 定时器中断回调,执行绑定的任务
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);

2. 按键事件处理的解耦设计

在按键事件处理中,通常我们需要根据按键的按下和释放情况,判断是单击、双击还是长按。这些操作需要时间控制,而在裸机环境下,使用 HAL_GetTick() 来获取时间戳会与任务调度冲突。因此,我的设计目标是通过定时器回调来管理按键状态,而避免直接依赖 HAL_GetTick()。

计数器在按键事件中的核心作用

在按键事件处理中,按键的每一次按下和释放都需要通过一系列的状态转换来识别。为确保按键状态稳定、可靠,必须处理去抖动问题。而双击事件则需要在一定的时间窗口内等待第二次按下,以判断是否为有效的双击。

为了实现这一目标,去抖动计数器和双击等待计数器分别管理按键的按下去抖动和双击事件的时间窗口。每次定时器中断时,计数器会递减,并根据计数器的状态决定按键事件的状态转换和回调触发。

1. 去抖动计数器

按键的按下和释放通常会由于机械结构产生抖动(即按键状态在短时间内反复变化)。如果不处理抖动,可能会误判为多个按键事件。去抖动计数器的作用就是在按键状态变化时进行时间延迟,在一段稳定的时间内确认按键状态是否稳定,避免误触发。

例如,在按键按下后,我们会启动去抖动计数器,当计数器倒计时结束时,我们才认为按键状态稳定,可以进行状态切换

2. 双击等待计数器

双击事件是指在短时间内两次按下同一个按键。如果第二次按下的时间间隔超过预设的双击等待时间,我们将识别为单击。因此,双击等待计数器的作用是判断两次按键按下之间的时间是否符合双击事件的要求。

当第一次按下释放后,启动双击等待计数器,等待第二次按下。如果在规定时间内没有检测到第二次按下,就触发单击事件;如果检测到第二次按下,则触发双击事件。

按键事件管理的核心代码

通过去抖动计数器和双击等待计数器,我们能够精确地控制按键状态的切换和事件触发。以下是按键结构体和按键状态管理的核心代码:

// 按键结构体
typedef struct {
    GPIO_TypeDef *GPIOx;          // 按键GPIO端口
    uint16_t GPIO_Pin;            // 按键GPIO引脚
    KeyTask_t SingleClick;        // 单击事件回调
    KeyTask_t DoubleClick;        // 双击事件回调
    KeyState state;               // 当前按键状态
    uint16_t debounce_cnt;        // 去抖动计数器
    uint16_t double_wait_cnt;     // 双击等待计数器
    uint8_t click_valid;          // 是否为有效单击
} KEY_TypeDef;

// 按键状态检测与任务回调
void KEY_CheckStatus(KEY_TypeDef *key);
void KEY_TimerCallback(void);

按键状态机设计

按键的状态机由多个状态和计数器控制,每次定时器触发时,按键状态机会检查当前按键的状态,并根据去抖动计数器和双击等待计数器的值来决定是否切换状态和触发事件。
例如,按键按下后的去抖动处理:

switch(key->state) {
    case KEY_STATE_IDLE:
        if(current_state) {
            key->state = KEY_STATE_DEBOUNCE_PRESS;
            key->debounce_cnt = DEBOUNCE_TIME;  // 启动去抖动计数器
        }
        break;

    case KEY_STATE_DEBOUNCE_PRESS:
        if(!current_state) {
            key->state = KEY_STATE_IDLE;
        } else if(--key->debounce_cnt == 0) {
            key->state = KEY_STATE_PRESSED;  // 去抖动完成,进入按键按下状态
        }
        break;
    
    // 其他状态处理...
}

双击检测与回调

对于双击事件,处理方式是当按键按下并释放后,启动双击等待计数器,检查第二次按下是否在规定的时间内发生:

case KEY_STATE_DEBOUNCE_RELEASE:
    if(current_state) {
        key->state = KEY_STATE_WAIT_DOUBLE;
        key->double_wait_cnt = DOUBLE_CLICK_TIMEOUT;  // 启动双击等待计数器
    }
    break;

case KEY_STATE_WAIT_DOUBLE:
    if(current_state) {
        key->state = KEY_STATE_DEBOUNCE_PRESS_2;  // 检测到第二次按下
        key->debounce_cnt = DEBOUNCE_TIME;
    } else if(--key->double_wait_cnt == 0) {
        if(key->SingleClick) key->SingleClick();  // 超时,触发单击
        key->state = KEY_STATE_IDLE;
    }
    break;

4. 设计的优势

  • 高精度的去抖动和双击判断:通过计数器精确控制去抖动时间和双击时间窗口,避免了不必要的误触发。
  • 避免 HAL_GetTick() 依赖:没有全局时间戳的干扰,定时器回调触发的周期性检查使得事件管理更加高效。
  • 状态切换控制:每个状态都有明确的计时和条件,保证了按键事件的准确性和可靠性。
  • 总结

    通过定时器任务的解耦设计与按键事件处理的模块化管理,我们能够轻松实现灵活的按键事件识别,并且保持系统的高效性和可扩展性。通过将定时器与按键事件的处理解耦,避免了传统设计中的紧密耦合,使得代码更加清晰、易于维护,适应各种不同的需求。

    希望这篇文章能够为你的嵌入式开发提供一些灵感,帮助你更好地设计系统,减少复杂度,提高代码的可复用性。

    双击事件的处理是最具挑战性的部分,主要因为它涉及到等待事件的处理。而单击事件相对简单,处理起来并不复杂。值得一提的是,当我开始使用任务指针来管理事件时,我发现可以将许多功能打包起来,方便在不同的地方进行调用。这种设计方式让我有了一种类似C++的面向对象编程的感觉,代码变得更加灵活和可扩展。

    我也觉得双击这一边我写得很繁琐了,应该会有更简单更有拓展的状态机,一时间我也写不出来,就先用暴力的办法。

    简化状态机的关键是解耦事件处理、使用更简单的时间管理方式以及增加灵活的扩展性。可以通过以下几种方法来降低状态机的复杂性并提高代码的灵活性:

  • 事件驱动与状态管理分离; 使用时间片管理状态; 分层状态机与任务协作; 基于状态表的状态机设计; 使用嵌套计时器回调管理多事件;
    将状态和事件封装成对象(类似C++面向对象思想)
  • 作者:Hi_sl

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32定时任务的解耦设计与按键事件处理

    发表回复