Search code examples
cembeddedstm32halstm32f4

Implementing a single press, long press and a double press function in HAL for STM32


I'm trying to implement a single press, double press and long press function to perform different functions. So far I've understood the logic for a single press and long press but I cant figure out how to detect a double press. As for the code, I've implemented the single press and long press using a counter but the code only stays on the first if condition.

          bool single_press = false;
      bool long_press = false;

      if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13))
      {

          HAL_TIM_Base_Start(&htim2);
          if ((TIM2->CNT == 20) && (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)))
          {
              single_press = true;
              long_press = false;
          }
          else if ((TIM2->CNT == 799) && (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13)))
          {
              single_press = true;
              long_press = true;
          }
          HAL_TIM_Base_Stop(&htim2);
      }

      if (single_press == true && long_press == false)
      {
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 1);
          HAL_Delay(1000);
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, 0);
      }
      else if (single_press == true && long_press == true)
      {
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 1);
          HAL_Delay(1000);
          HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 0);
      }
  }

I'm trying to implement a case where if I press the key for 20 ms (single press) PB0 will go high for a second and if I press the key for 800 ms PB7 will go high for a second. However, on running the program, when I press the button, PB0 goes high regardless of how long I hold the button and PB7 stays low. So I guess I have two questions:

  • How can I edit my code such that for a single press PB0 goes high and for a long press PB7 goes high?
  • How would one implement a double press function?

Thanks!


Solution

  • Do not use delays for a start. All the while you are in the delay, you are not seeing what the button is doing (or doing anything else useful for that matter). Instead, you need to continuously poll (or use interrupts) for the button state and when the state changes, time-stamp it, and make your action decisions based on the timing.

    First of all, you will need a robust button state detection with debouncing. There are a number of approaches. An example:

    bool buttonState()
    {
        static const uint32_t DEBOUNCE_MILLIS = 20 ;
        static bool buttonstate = HAL_GPIO_ReadPin( GPIOC, GPIO_PIN_13 ) == GPIO_PIN_SET ;
        static uint32_t buttonstate_ts = HAL_GetTick() ;
    
        uint32_t now = HAL_GetTick() ;
        if( now - buttonstate_ts > DEBOUNCE_MILLIS )
        {
            if( buttonstate != HAL_GPIO_ReadPin( GPIOC, GPIO_PIN_13 ) == GPIO_PIN_SET )
            {
                buttonstate = !buttonstate ;
                buttonstate_ts = now ;
            }
        }
        return buttonstate ;
    }
    

    So buttonState() always returns immediately - no delays, but re-reading the button is held off for 20ms after a state change to prevent misinterpreting switch bounce as multiple state changes.

    Then you need a button state polling function that detects the timing of button down and button up events. Such that:

         ____________________________
    ____|                            |_____________
         <----long-press min-->
                               ^
                               |_Long press detected
         ______     _____
    ____|      |___|     |_________________________
                         ^
                         |_Double press detected
         ______
    ____|      |___________________________________
               <------->
                  ^    ^
                  |    |_Single press detected
                  |_ Double press gap max.
    

    Note that the single press is detected after too long has passed after button-up for it to be a double press. The following may need some debugging (untested) treat as illustrative:

    typedef enum
    {
        NO_PRESS,
        SINGLE_PRESS,
        LONG_PRESS,
        DOUBLE_PRESS
    } eButtonEvent ;
    
    eButtonEvent getButtonEvent()
    {
        static const uint32_t DOUBLE_GAP_MILLIS_MAX = 250 ;
        static const uint32_t LONG_MILLIS_MIN = 800 ;
        static uint32_t button_down_ts = 0 ;
        static uint32_t button_up_ts = 0 ;
        static bool double_pending = false ;
        static bool long_press_pending = false ;
        static bool button_down = false ; ;
    
        eButtonEvent button_event = NO_PRESS ;
        uint32_t now = HAL_GetTick() ;
    
        // If state changed...
        if( button_down != buttonState() )
        {
            button_down = !button_down ;
            if( button_down )
            {
                // Timestamp button-down
                button_down_ts = now ;
            }
            else
            {
                // Timestamp button-up
                button_up_ts = now ;
    
                // If double decision pending...
                if( double_pending )
                {
                    button_event = DOUBLE_PRESS ;
                    double_pending = false ;
                }
                else
                {
                    double_pending = true ;
                }
    
                // Cancel any long press pending
                long_press_pending = false ;
            }
        }
    
        // If button-up and double-press gap time expired, it was a single press
        if( !button_down && double_pending && now - button_up_ts > DOUBLE_GAP_MILLIS_MAX )
        {
            double_pending = false ;
            button_event = SINGLE_PRESS ;
        }
        // else if button-down for long-press...
        else if( !long_press_pending && button_down && now - button_down_ts > LONG_MILLIS_MIN )
        {
            button_event = LONG_PRESS ;
            long_press_pending = false ;
            double_pending = false ;
    
        }
    
        return button_event ;
    }
    

    Then finally you need to poll for button events frequently:

    int main()
    {
        for(;;)
        {
            // Check for button events
            switch( getButtonEvent() )
            {
                case NO_PRESS :     { ... } break ;
                case SINGLE_PRESS : { ... } break ;
                case LONG_PRESS :   { ... } break ;
                case DOUBLE_PRESS : { ... } break ;
            }
    
            // Do other work...
        }
    }
    

    See how there are no delays, allowing you to check for button events and do other work in real-time. Obviously, the "other work" must also execute without excessive delays otherwise it would mess up your button event timing. So for example to implement your 1-second output on a single press you might have:

                case SINGLE_PRESS :
                {
                    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 1);
                    single_press_ts = now ;
    
                } break ;
    

    then later after the switch/case:

        if( now - single_press_ts > 1000 )
        {
            HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, 0);
        }
    

    If that is a problem then you would need to consider using interrupts for the button event processing - combining it with the de-bounce processing or use an RTOS scheduler and poll for button events in a task.