Implementing Non-blocking Button Input Management on STM32

Developing responsive user interfaces on embedded systems requires decoupling hardware interaction from the main execution loop. In an STM32 environment, specifically using an STM32F103C8T6, direct register manipulation allows for lightweight input handling without the overhead of heavy abstraction layers. The following approach demonstrates how to manage button debouncing, multi-press detection, and state machine transitions using a non-blocking task scheduler.

Task Scheduling with SysTick

Rather than blocking the main loop with delay functions, utilize the SysTick timer to drive a task scheduler. This allows multiple concurrent background operations—such as input polling—to run at predefined intervals.

#define MAX_SCHEDULED_TASKS 8
static void (*task_queue[MAX_SCHEDULED_TASKS])(void);

void SysTick_Handler(void) {
    for (uint8_t i = 0; i < MAX_SCHEDULED_TASKS; i++) {
        if (task_queue[i]) task_queue[i]();
    }
}

void Register_Task(void (*func)(void)) {
    for (uint8_t i = 0; i < MAX_SCHEDULED_TASKS; i++) {
        if (task_queue[i] == 0) {
            task_queue[i] = func;
            break;
        }
    }
}

State Machine Logic for Button Events

An input device typically transitions through stages: Idle, Debouncing (Falling), Pressed, and Debouncing (Raising). By sampling the GPIO pin every 10ms, we can determine the current state and trigger specific events such as single clicks, double clicks, or long-press holds with out stalling the processor.

enum ButtonStatus {
    STATE_RELEASED,
    STATE_FALLING,
    STATE_PRESSED,
    STATE_RAISING
};

static void Scan_Input_Task(void) {
    static uint8_t debounce_timer = 0;
    static enum ButtonStatus current_state = STATE_RELEASED;
    
    // Sampling logic
    bool active_level = (GPIOA->IDR & GPIO_IDR_IDR0) != 0;

    switch (current_state) {
        case STATE_RELEASED:
            if (active_level) current_state = STATE_FALLING;
            break;
        case STATE_FALLING:
            if (active_level) {
                push_event(EVENT_PRESS);
                current_state = STATE_PRESSED;
            } else {
                current_state = STATE_RELEASED;
            }
            break;
        case STATE_PRESSED:
            if (!active_level) current_state = STATE_RAISING;
            break;
        case STATE_RAISING:
            if (!active_level) {
                push_event(EVENT_RELEASE);
                current_state = STATE_RELEASED;
            } else {
                current_state = STATE_PRESSED;
            }
            break;
    }
}

Queue-based Message Handling

To allow the main application logic to consume events asynchronously, impleemnt a circular ring buffer. When the input task identifies a valid state change (e.g., a long press or double click), it pushes an identifier onto the queue. The main loop simply checks this queue during its idle cycles.

while (1) {
    uint8_t event;
    if (fetch_event(&event)) {
        if (event == EVENT_HOLD) {
            toggle_led_indicator();
        } else if (event == EVENT_CLICK) {
            perform_action();
        }
    }
}

This architecture ensures that the system remains responsive, allows for easily scaling to multiple buttons or matrix keyboards by using bitwise flags, and maintains strict separation between hardware-specific sampling and high-level application logic.

Tags: STM32 Embedded Systems c programming Firmware

Posted on Sun, 17 May 2026 11:36:30 +0000 by neuromancer