Add hw_button driver (PC2 active-low, 20ms debounce) with gesture detection: - Single short press + 500ms quiet -> BTN_EVENT_PARK - SHORT+SHORT+LONG combo (within 3s) -> BTN_EVENT_REARM_COMBO New BALANCE_PARKED state: PID frozen, motors off, quick re-arm via button combo without the 3-second arm interlock required from DISARMED. FC_BTN (0x404) CAN frame sent to Orin on each event: event_id 1=PARKED, 2=UNPARKED, 3=UNPARK_FAILED (pitch > 20 deg) Includes 11 unit tests (1016 assertions) exercising debounce, bounce rejection, short/long classification, sequence detection, and timeout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
4.7 KiB
C
180 lines
4.7 KiB
C
/* hw_button.c — hardware button debounce + gesture detection (Issue #682)
|
|
*
|
|
* Debounce FSM:
|
|
* IDLE → (raw press detected) → DEBOUNCING
|
|
* DEBOUNCING → (still pressed after BTN_DEBOUNCE_MS) → HELD
|
|
* HELD → (released) → classify press type, back to IDLE
|
|
*
|
|
* Press types:
|
|
* SHORT held < BTN_LONG_MIN_MS from confirmed start
|
|
* LONG held >= BTN_LONG_MIN_MS
|
|
*
|
|
* Sequence detection (operates on classified presses):
|
|
* Buffer up to 3 presses. Recognised patterns:
|
|
* [SHORT, SHORT, LONG] -> BTN_EVENT_REARM_COMBO (fires on LONG release)
|
|
* [SHORT] + BTN_COMMIT_MS timeout -> BTN_EVENT_PARK
|
|
* Sequence reset after BTN_SEQ_TIMEOUT_MS from first press.
|
|
*/
|
|
|
|
#include "hw_button.h"
|
|
#include "config.h"
|
|
|
|
#ifndef TEST_HOST
|
|
#include "stm32f7xx_hal.h"
|
|
#endif
|
|
|
|
/* ---- Timing defaults (override in config.h) ---- */
|
|
#ifndef BTN_DEBOUNCE_MS
|
|
#define BTN_DEBOUNCE_MS 20u
|
|
#endif
|
|
#ifndef BTN_LONG_MIN_MS
|
|
#define BTN_LONG_MIN_MS 1500u
|
|
#endif
|
|
#ifndef BTN_COMMIT_MS
|
|
#define BTN_COMMIT_MS 500u
|
|
#endif
|
|
#ifndef BTN_SEQ_TIMEOUT_MS
|
|
#define BTN_SEQ_TIMEOUT_MS 3000u
|
|
#endif
|
|
|
|
/* ---- Press type ---- */
|
|
typedef enum {
|
|
_PT_SHORT = 1u,
|
|
_PT_LONG = 2u,
|
|
} _press_type_t;
|
|
|
|
/* ---- Debounce state ---- */
|
|
typedef enum {
|
|
_BTN_IDLE,
|
|
_BTN_DEBOUNCING,
|
|
_BTN_HELD,
|
|
} _btn_state_t;
|
|
|
|
static _btn_state_t s_state = _BTN_IDLE;
|
|
static uint32_t s_trans_ms = 0u; /* timestamp of last FSM transition */
|
|
static bool s_pressed = false;
|
|
|
|
/* ---- Sequence buffer ---- */
|
|
#define _SEQ_MAX 3u
|
|
static _press_type_t s_seq[_SEQ_MAX];
|
|
static uint8_t s_seq_len = 0u;
|
|
static uint32_t s_seq_first_ms = 0u;
|
|
static uint32_t s_seq_last_ms = 0u;
|
|
|
|
/* ---- GPIO read ---- */
|
|
#ifdef TEST_HOST
|
|
static bool s_test_raw = false;
|
|
void hw_button_inject(bool pressed) { s_test_raw = pressed; }
|
|
static bool _read_raw(void) { return s_test_raw; }
|
|
#else
|
|
static bool _read_raw(void)
|
|
{
|
|
return HAL_GPIO_ReadPin(BTN_PORT, BTN_PIN) == GPIO_PIN_RESET; /* active-low */
|
|
}
|
|
#endif
|
|
|
|
void hw_button_init(void)
|
|
{
|
|
#ifndef TEST_HOST
|
|
__HAL_RCC_GPIOC_CLK_ENABLE(); /* BTN_PORT assumed GPIOC; adjust if needed */
|
|
GPIO_InitTypeDef g = {0};
|
|
g.Pin = BTN_PIN;
|
|
g.Mode = GPIO_MODE_INPUT;
|
|
g.Pull = GPIO_PULLUP;
|
|
HAL_GPIO_Init(BTN_PORT, &g);
|
|
#endif
|
|
s_state = _BTN_IDLE;
|
|
s_seq_len = 0u;
|
|
s_pressed = false;
|
|
}
|
|
|
|
bool hw_button_is_pressed(void)
|
|
{
|
|
return s_pressed;
|
|
}
|
|
|
|
/* Record a classified press into the sequence buffer and check for patterns. */
|
|
static hw_btn_event_t _record_press(_press_type_t pt, uint32_t now_ms)
|
|
{
|
|
if (s_seq_len == 0u) {
|
|
s_seq_first_ms = now_ms;
|
|
}
|
|
s_seq_last_ms = now_ms;
|
|
|
|
if (s_seq_len < _SEQ_MAX) {
|
|
s_seq[s_seq_len++] = pt;
|
|
}
|
|
|
|
/* Check REARM_COMBO: SHORT + SHORT + LONG */
|
|
if (s_seq_len == 3u &&
|
|
s_seq[0] == _PT_SHORT &&
|
|
s_seq[1] == _PT_SHORT &&
|
|
s_seq[2] == _PT_LONG) {
|
|
s_seq_len = 0u;
|
|
return BTN_EVENT_REARM_COMBO;
|
|
}
|
|
|
|
return BTN_EVENT_NONE;
|
|
}
|
|
|
|
hw_btn_event_t hw_button_tick(uint32_t now_ms)
|
|
{
|
|
bool raw = _read_raw();
|
|
|
|
/* ---- Debounce FSM ---- */
|
|
switch (s_state) {
|
|
case _BTN_IDLE:
|
|
if (raw) {
|
|
s_state = _BTN_DEBOUNCING;
|
|
s_trans_ms = now_ms;
|
|
}
|
|
break;
|
|
|
|
case _BTN_DEBOUNCING:
|
|
if (!raw) {
|
|
/* Released before debounce elapsed — bounce, ignore */
|
|
s_state = _BTN_IDLE;
|
|
} else if ((now_ms - s_trans_ms) >= BTN_DEBOUNCE_MS) {
|
|
s_state = _BTN_HELD;
|
|
s_trans_ms = now_ms; /* record confirmed press-start time */
|
|
s_pressed = true;
|
|
}
|
|
break;
|
|
|
|
case _BTN_HELD:
|
|
if (!raw) {
|
|
s_pressed = false;
|
|
uint32_t held_ms = now_ms - s_trans_ms;
|
|
_press_type_t pt = (held_ms >= BTN_LONG_MIN_MS) ? _PT_LONG : _PT_SHORT;
|
|
s_state = _BTN_IDLE;
|
|
hw_btn_event_t ev = _record_press(pt, now_ms);
|
|
if (ev != BTN_EVENT_NONE) {
|
|
return ev;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
/* ---- Sequence timeout / commit check (only when not currently held) ---- */
|
|
if (s_state == _BTN_IDLE && s_seq_len > 0u) {
|
|
uint32_t since_first = now_ms - s_seq_first_ms;
|
|
uint32_t since_last = now_ms - s_seq_last_ms;
|
|
|
|
/* Whole sequence window expired — abandon */
|
|
if (since_first >= BTN_SEQ_TIMEOUT_MS) {
|
|
s_seq_len = 0u;
|
|
return BTN_EVENT_NONE;
|
|
}
|
|
|
|
/* Single short press + BTN_COMMIT_MS of quiet -> PARK */
|
|
if (s_seq_len == 1u &&
|
|
s_seq[0] == _PT_SHORT &&
|
|
since_last >= BTN_COMMIT_MS) {
|
|
s_seq_len = 0u;
|
|
return BTN_EVENT_PARK;
|
|
}
|
|
}
|
|
|
|
return BTN_EVENT_NONE;
|
|
}
|