PretoWatch (Part 4)

Last time, we discussed task management within the RTOS system. This time, we focus on user interactions with the system, specifically how I implemented the buttons.

In this project, user interactions and state of the system are closely related because buttons are the only input to the system.

If you’re familiar with handling buttons in electronic hardware, you’ve likely heard about switch bouncing. Switch bouncing is a common issue in mechanical switches where the contacts rapidly open and close multiple times before settling into a stable state when pressed or released. This generates multiple unwanted electrical signals or noise instead of a single clean transition.

To solve this issue, we can use hardware or software debouncing techniques. Since we are running an RTOS and I want to save space on the PCB by avoiding additional components, I’ve decided to implement software debouncing.

Here is the definition of buttonState for the three different buttons:

typedef enum {
	BUTTON_STATE_IDLE, BUTTON_STATE_PRESSED, BUTTON_STATE_LONG_PRESSED
} ButtonState;
volatile ButtonState buttonState[3] = { BUTTON_STATE_IDLE, BUTTON_STATE_IDLE,
		BUTTON_STATE_IDLE };

This initializes an array named buttonState to keep track of the state of each of the three buttons, setting each button’s initial state to BUTTON_STATE_IDLE.

volatile uint32_t lastValidPressTime[3] = { 0, 0, 0 };

The lastValidPressTime array stores the last valid timestamp for each of the three buttons. This allows us to determine whether there has been a valid press or if the button state needs to be reset.

    #define DEBOUNCE_TIME				  50
    #define RESET_IDLE_TIME				 1500
    uint32_t currentTime = osKernelSysTick();
		
		//reset to idle state
		if ((currentTime - lastValidPressTime[0]) >= RESET_IDLE_TIME) {
			buttonState[0] = BUTTON_STATE_IDLE;
		}
		//Debounce button press/release
		if ((currentTime - lastValidPressTime[0]) >= DEBOUNCE_TIME) {
			lastValidPressTime[0] = currentTime;
			//Button in idle, so it is a button press
			if (buttonState[0] == BUTTON_STATE_IDLE) {
				osSignalSet(controlHandle, SIGNAL_SW1_PRESSED);
				buttonState[0] = BUTTON_STATE_PRESSED;
			//Button not in idle, so it is a button release
			} else {
				buttonState[0] = BUTTON_STATE_IDLE;
			}
		}

The side button I am using tends to be inconsistent with bouncing when released. To address this, I’ve added a feature that resets the button state to BUTTON_STATE_IDLE if the last press was 1500ms ago.

I’ve implemented a long press feature only for button 2, which operates similarly to the code above:

		if ((currentTime - lastValidPressTime[1]) >= RESET_IDLE_TIME) {
			buttonState[1] = BUTTON_STATE_IDLE;
		}
		if ((currentTime - lastValidPressTime[1]) >= DEBOUNCE_TIME) {
			lastValidPressTime[1] = currentTime;
			if (buttonState[1] == BUTTON_STATE_IDLE) {
				buttonState[1] = BUTTON_STATE_PRESSED;
				osTimerStart(sw2ButtonTimerHandle, LONG_PRESS_TIME);
			} else {
				if (buttonState[1] == BUTTON_STATE_PRESSED) {
					osSignalSet(controlHandle, SIGNAL_SW2_PRESSED);
					osTimerStop(sw2ButtonTimerHandle);
				}
				buttonState[1] = BUTTON_STATE_IDLE;
			}

		}
		
		void SW2ButtonTimerCallback(void const * argument)
    {
      /* USER CODE BEGIN SW2ButtonTimerCallback */
    	if (HAL_GPIO_ReadPin(SW2_GPIO_Port, SW2_Pin) == GPIO_PIN_RESET) {
    		// Button 2 is still pressed, so it's a long press
    		buttonState[1] = BUTTON_STATE_LONG_PRESSED;
    		osSignalSet(controlHandle, SIGNAL_SW2_PRESSED); // Signal long press
    	}
      /* USER CODE END SW2ButtonTimerCallback */
    }

The only difference is that, since we cannot determine if the current press is a short press or a long press until the user releases the button, the signal for button 2 short press is only set when the user releases the button.

To handle the received signals in the Control Task, we need to wait for the signal event in the task loop and process the corresponding actions based on the received signal. Here’s how you can implement the signal handling for the Control Task:

void StartControlTask(void const * argument)
{
  /* USER CODE BEGIN StartControlTask */
	/* Infinite loop */
	for (;;) {
		osEvent event = osSignalWait(
		SIGNAL_SW1_PRESSED | SIGNAL_SW2_PRESSED | SIGNAL_SW3_PRESSED, 50); // Wait for any button signal

		if (event.status == osEventSignal) {
			if (event.value.signals & SIGNAL_SW1_PRESSED) {
				printf("Button 1 Pressed!\n");
				handleButton1Press();
			}
			if (event.value.signals & SIGNAL_SW2_PRESSED) {
				printf("Button 2 Pressed!\n");
				if (buttonState[1] == BUTTON_STATE_LONG_PRESSED) {
					printf("Button 2 Long Pressed!\n");
					// Handle long press for button 2
					handleButton2LongPress();
				} else {
					printf("Button 2 Short Pressed!\n");
					// Handle short press for button 2
					handleButton2Press();
				}
			}
			if (event.value.signals & SIGNAL_SW3_PRESSED) {
				printf("Button 3 Pressed!\n");
				handleButton3Press();
			}
		}
		// Optionally, you can include a small delay to prevent busy-waiting if the task is not strictly event-driven
		osDelay(50); // Small delay to yield CPU time
	}
  /* USER CODE END StartControlTask */
}

With the button handling now successfully implemented in the project, we have laid a strong foundation for user interaction. In our next installment, we’ll dive into how users can navigate the system using these buttons through a state machine. Stay tuned for more insights into the PretoWatch project!

Leave a Reply

Your email address will not be published. Required fields are marked *