Refactor main loop touch flow and update docs

This commit is contained in:
Adolfo Reyna
2026-02-18 12:08:23 -05:00
parent be6a217b08
commit a06e0d69fe
4 changed files with 41 additions and 31 deletions

View File

@@ -21,13 +21,13 @@ The project now uses a **clean, event-driven architecture** perfect for building
-**Event-Driven**: Display only updates when input is received
- 🔋 **Power Efficient**: Uses `__wfi()` to sleep between inputs (< 1mA idle)
- 📄 **E-ink Optimized**: Minimizes screen refreshes for e-paper displays
- 🎯 **Interrupt-Driven**: Touch and button handling via hardware interrupts
- 🎯 **Hybrid Input**: IRQ wake-up plus active-touch sampling for smoother drag/move handling
- 🧩 **Modular**: Clear separation of input processing, game logic, and rendering
### Architecture Highlights
```
Interrupt → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep
IRQ/Timer → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep
```
**Before (Polling Loop):**
@@ -45,8 +45,12 @@ while(1) {
**After (Reactive Template):**
```cpp
while(1) {
__wfi(); // Sleep until interrupt
while (1) {
bool stay_awake = pending_refresh || touch_event_down || (last_touch_time != 0);
if (!stay_awake) {
__wfi(); // Sleep until interrupt
if (!has_pending_wake_source()) continue; // Ignore unrelated wake-ups
}
InputEvent input = process_button_input(config);
if (!input.valid) {
@@ -534,7 +538,8 @@ The typical development workflow:
**Interrupt not triggering:**
- Verify `TOUCH_INT_PIN` is correctly defined
- Check INT pin is pulled high (internal pull-up or external resistor)
- Confirm touch controller is configured for interrupt mode
- Confirm touch controller is configured for trigger/interrupt mode
- Touch processing uses hybrid mode: IRQ wake-up plus active-session sampling while touch is down
### SD Card Issues
@@ -579,7 +584,7 @@ The code includes `printf()` statements for debugging touch, display, and SD car
For battery-powered projects:
- E-Paper displays use almost no power when idle
- Use `__wfi()` (Wait For Interrupt) to sleep between inputs
- Use `__wfi()` (Wait For Interrupt) to sleep between event bursts
- Disable TFT backlight when idle
- Lower SPI/I2C baud rates to reduce power consumption
@@ -607,7 +612,8 @@ For battery-powered projects:
### Touch Features
- Multi-touch support (up to 2 points)
- Coordinate transformation
- Touch debouncing
- Hybrid IRQ + active-session sampling
- Touch debouncing with release hysteresis
- Event types (press, lift, contact)
### SD Card Features

View File

@@ -14,7 +14,7 @@
* - Event-driven: Display only updates when input is received
* - Power efficient: Uses __wfi() to sleep between inputs
* - E-ink optimized: Minimizes screen refreshes
* - Interrupt-driven: Touch and button handling via interrupts
* - Hybrid input handling: IRQ wake-up plus active-touch sampling
* - Modular: Clear separation of input, game logic, and rendering
*
* ARCHITECTURE:
@@ -36,7 +36,6 @@
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/sync.h"
#include "pico/multicore.h"
#include "board_config.h" // Board-specific pin configuration
#include "sd_card.h"
@@ -171,7 +170,6 @@ static uint32_t last_interaction_time = 0; // Last time user interacted
static bool is_idle_2min_triggered = false; // Flag for 2min trigger
static bool is_idle_10min_triggered = false; // Flag for 10min trigger
static volatile bool dim_check_flag = false; // Flag set by timer to check dimming
static LowLevelDisplay* global_display = nullptr; // Global display pointer for timer callback
/**
* @brief Update last interaction time and notify display driver
@@ -269,6 +267,18 @@ static inline bool has_pending_wake_source() {
return false;
}
/**
* @brief Returns true when the currently selected game needs frame ticks.
*/
static inline bool game_wants_frame_updates(GameLauncher& launcher) {
if (!launcher.is_game_selected()) {
return false;
}
Game* game = launcher.get_selected_game();
return game && game->wants_frame_updates();
}
/**
* @brief Touch interrupt callback handler
*
@@ -282,6 +292,8 @@ static inline bool has_pending_wake_source() {
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
*/
void touch_interrupt_handler(uint gpio, uint32_t events) {
(void)gpio;
// Track which edge triggered (down vs up).
// Keep ISR minimal: do not log/print from interrupt context.
if (events & GPIO_IRQ_EDGE_FALL) {
@@ -325,9 +337,6 @@ void button_interrupt_handler(uint gpio, uint32_t events) {
const int V_WIDTH = DISPLAY_WIDTH;
const int V_HEIGHT = DISPLAY_HEIGHT;
// Touch indicator settings
#define TOUCH_RADIUS 10
uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8];
/**
@@ -427,7 +436,7 @@ int main()
if (touch) {
printf("Touch initialized successfully\n");
// Set up interrupt-driven touch detection
// Set up touch IRQ wake-up (InputManager handles active-touch sampling)
printf("Setting up touch interrupt callback...\n");
touch->set_interrupt_callback(touch_interrupt_handler);
printf("Touch interrupt enabled on INT pin (falling and rising edges)\n");
@@ -549,10 +558,11 @@ int main()
// Core 0 (this loop): Handles input and game logic - stays responsive
// Core 1: Handles display refresh - can take 1-2 seconds for e-ink
//
// The loop sleeps until an interrupt occurs, then:
// The loop primarily sleeps on __wfi(), and wakes to:
// 1. Process input (button or touch)
// 2. Update game state based on input
// 3. Queue refresh on Core 1 (non-blocking)
// While touch is active or a game needs frame ticks, the loop stays awake.
// This keeps Core 0 responsive even during slow e-ink refreshes
// ========================================================================
@@ -561,8 +571,6 @@ int main()
// Initialize last interaction time to current time
last_interaction_time = to_ms_since_boot(get_absolute_time());
global_display = display;
// Set up repeating alarm to periodically check dimming status
// This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS
add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true);
@@ -612,12 +620,7 @@ int main()
if (touch_event_down) stay_awake = true; // Keep sampling while finger is down
if (last_touch_time != 0) stay_awake = true; // Keep sampling during active touch session
if (launcher.is_game_selected()) {
Game* g = launcher.get_selected_game();
if (g && g->wants_frame_updates()) {
stay_awake = true;
}
}
if (game_wants_frame_updates(launcher)) stay_awake = true;
if (!stay_awake) {
// Sleep until interrupt wakes us up (very power efficient!)
@@ -722,10 +725,10 @@ int main()
}
}
if (launcher.is_game_selected()) {
if (game_wants_frame_updates(launcher)) {
// No input, but check if game wants continuous updates
current_game = launcher.get_selected_game();
if (current_game->wants_frame_updates()) {
if (current_game) {
// Only send frame tick if we're ready to draw the next frame
if (!is_refresh_in_progress()) {
InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true};

View File

@@ -10,7 +10,6 @@
// External interrupt flags from basic1.cpp
extern volatile bool touch_interrupt_flag;
extern volatile bool touch_event_down;
extern volatile bool button_key0_pressed;
extern volatile bool button_key1_pressed;

View File

@@ -37,9 +37,9 @@ public:
InputManager(LowLevelTouch* touch, const GameConfig* config);
/**
* @brief Process touch input from controller
* @param last_time Pointer to last touch timestamp for debouncing
* @return InputEvent (valid=false if no valid input)
* @brief Process touch input using hybrid IRQ + active-session sampling.
* @param last_time Pointer to last touch timestamp (0 means no active touch session)
* @return InputEvent (valid=false if no state change/update is emitted)
*/
InputEvent process_touch_input(uint32_t* last_time);
@@ -97,6 +97,8 @@ public:
private:
LowLevelTouch* touch;
const GameConfig* config;
// Consecutive touch_count==0 samples while a touch session is active.
// Used to suppress false TOUCH_UP events from transient touch controller jitter.
uint8_t no_touch_samples = 0;
// Virtual button regions