701 lines
23 KiB
C++
701 lines
23 KiB
C++
/*
|
|
* Copyright (c) 2021 Arm Limited and Contributors. All rights reserved.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*
|
|
* ============================================================================
|
|
* REACTIVE GAME TEMPLATE - Event-Driven Architecture for RP2350
|
|
* ============================================================================
|
|
*
|
|
* This template provides a clean, reactive architecture for building games
|
|
* and interactive applications on Raspberry Pi Pico with displays.
|
|
*
|
|
* KEY FEATURES:
|
|
* - 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
|
|
* - Modular: Clear separation of input, game logic, and rendering
|
|
*
|
|
* ARCHITECTURE:
|
|
* 1. Interrupt handlers set flags (kept minimal)
|
|
* 2. Main loop processes input events
|
|
* 3. Game logic updates state based on events
|
|
* 4. Screen refreshes only when changes occur
|
|
*
|
|
* HOW TO CREATE YOUR OWN GAME:
|
|
* ============================================================================
|
|
* 1. Modify GameState structure with your game variables
|
|
* 2. Implement game_init() to set initial values
|
|
* 3. Implement game_update() to handle input and update state
|
|
* 4. Implement game_draw() to render your game graphics
|
|
* 5. Adjust GameConfig for your game's needs
|
|
* 6. The reactive loop and input system work automatically!
|
|
* ============================================================================
|
|
*/
|
|
|
|
#include "pico/stdlib.h"
|
|
#include "pico/binary_info.h"
|
|
#include "hardware/sync.h"
|
|
#include "board_config.h" // Board-specific pin configuration
|
|
#include "sd_card.h"
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include "display/low_level_render.h"
|
|
#include "display/low_level_gui.h"
|
|
#include "display/low_level_display.h"
|
|
#include "display/low_level_display_epaper.h"
|
|
#include "display/low_level_touch.h"
|
|
|
|
|
|
// Binary info for RP2350 - ensures proper boot image structure
|
|
bi_decl(bi_program_description("4.0\" TFT ST7796 with Touch and SD Card Demo"));
|
|
bi_decl(bi_program_version_string("0.1"));
|
|
bi_decl(bi_program_build_date_string(__DATE__));
|
|
|
|
// ============================================================================
|
|
// INPUT EVENT STRUCTURES
|
|
// ============================================================================
|
|
|
|
// Input event types
|
|
enum InputType {
|
|
INPUT_NONE = 0,
|
|
INPUT_TOUCH_DOWN,
|
|
INPUT_TOUCH_MOVE,
|
|
INPUT_TOUCH_UP,
|
|
INPUT_BUTTON_0,
|
|
INPUT_BUTTON_1,
|
|
INPUT_GESTURE
|
|
};
|
|
|
|
// Unified input event structure
|
|
struct InputEvent {
|
|
InputType type;
|
|
int16_t x;
|
|
int16_t y;
|
|
uint8_t gesture_code; // For gesture events
|
|
uint8_t button_id; // For button events
|
|
uint8_t pressure; // Touch pressure/weight
|
|
bool valid; // Set to true if event is valid
|
|
};
|
|
|
|
// ============================================================================
|
|
// GAME STATE AND CONFIGURATION
|
|
// ============================================================================
|
|
|
|
// Game state - customize this for your game
|
|
struct GameState {
|
|
// Drawing game state
|
|
int16_t last_x;
|
|
int16_t last_y;
|
|
bool is_drawing;
|
|
|
|
// General game state
|
|
uint32_t score;
|
|
bool game_over;
|
|
uint32_t frame_count;
|
|
|
|
// UI state
|
|
uint8_t progress_value; // Progress bar value (0-100)
|
|
uint8_t focused_button; // Which button has focus (0 or 1)
|
|
uint32_t button1_clicks; // Count clicks on button 1
|
|
uint32_t button2_clicks; // Count clicks on button 2
|
|
|
|
// Statistics
|
|
uint32_t touch_success_count;
|
|
uint32_t touch_fail_count;
|
|
};
|
|
|
|
// Game configuration - adjust these for your game
|
|
struct GameConfig {
|
|
uint32_t touch_debounce_ms; // Touch polling rate
|
|
uint32_t button_debounce_ms; // Button debounce delay
|
|
bool enable_gestures; // Enable gesture recognition
|
|
bool enable_continuous_draw; // Allow continuous drawing while touched
|
|
bool debug_verbose; // Print debug messages
|
|
};
|
|
|
|
// ============================================================================
|
|
// INTERRUPT HANDLERS (Keep these minimal!)
|
|
// ============================================================================
|
|
|
|
// Touch interrupt handling
|
|
volatile bool touch_interrupt_flag = false;
|
|
volatile bool touch_event_down = false;
|
|
LowLevelTouch* touch = nullptr;
|
|
|
|
// Button interrupt handling
|
|
#ifdef BUTTON_KEY0_PIN
|
|
volatile bool button_key0_pressed = false;
|
|
volatile bool button_key1_pressed = false;
|
|
#endif
|
|
|
|
/**
|
|
* @brief Touch interrupt callback handler
|
|
*
|
|
* Called automatically by hardware when INT pin changes state:
|
|
* - Falling edge: Touch detected (INT goes LOW)
|
|
* - Rising edge: Touch released (INT goes HIGH)
|
|
*
|
|
* This runs in interrupt context, so keep it fast - just set a flag
|
|
*
|
|
* @param gpio GPIO pin number that triggered the interrupt
|
|
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
|
|
*/
|
|
void touch_interrupt_handler(uint gpio, uint32_t events) {
|
|
// Set flag to indicate touch event occurred
|
|
// Main loop will handle the actual touch reading
|
|
touch_interrupt_flag = true;
|
|
|
|
// Track which edge triggered (down vs up)
|
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
|
touch_event_down = true;
|
|
}
|
|
if (events & GPIO_IRQ_EDGE_RISE) {
|
|
touch_event_down = false;
|
|
}
|
|
}
|
|
|
|
#ifdef BUTTON_KEY0_PIN
|
|
/**
|
|
* @brief Button interrupt callback handler
|
|
*
|
|
* Called automatically by hardware when button pins change state.
|
|
* Buttons are active LOW (pressed = 0, released = 1) with pull-ups.
|
|
*
|
|
* This runs in interrupt context, so keep it fast - just set flags.
|
|
*
|
|
* @param gpio GPIO pin number that triggered the interrupt
|
|
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
|
|
*/
|
|
void button_interrupt_handler(uint gpio, uint32_t events) {
|
|
// Only respond to falling edge (button press)
|
|
if (events & GPIO_IRQ_EDGE_FALL) {
|
|
if (gpio == BUTTON_KEY0_PIN) {
|
|
button_key0_pressed = true;
|
|
}
|
|
#ifdef BUTTON_KEY1_PIN
|
|
else if (gpio == BUTTON_KEY1_PIN) {
|
|
button_key1_pressed = true;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Screen dimensions and configuration from board_config.h
|
|
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];
|
|
|
|
/**
|
|
* @brief Refresh the screen with the 1-bit buffer
|
|
*
|
|
* Displays work directly with 1-bit monochrome buffers.
|
|
* The display driver internally converts to its native format (RGB565, etc.)
|
|
*
|
|
* @param buffer Pointer to 1-bit framebuffer (width*height/8 bytes)
|
|
* @param display Pointer to display abstraction layer
|
|
*/
|
|
void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) {
|
|
display->draw_buffer(buffer);
|
|
display->refresh();
|
|
}
|
|
|
|
// ============================================================================
|
|
// INPUT PROCESSING
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Get human-readable gesture name
|
|
*
|
|
* @param gesture_code Gesture code from touch controller
|
|
* @return Constant string with gesture name
|
|
*/
|
|
const char* get_gesture_name(uint8_t gesture_code) {
|
|
switch(gesture_code) {
|
|
case 0x10: return "Move Up";
|
|
case 0x14: return "Move Right";
|
|
case 0x18: return "Move Down";
|
|
case 0x1C: return "Move Left";
|
|
case 0x48: return "Zoom In";
|
|
case 0x49: return "Zoom Out";
|
|
default: return "Unknown";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Process touch input and convert to InputEvent
|
|
*
|
|
* Reads touch data from controller and creates appropriate InputEvent.
|
|
* Handles debouncing and filtering internally.
|
|
*
|
|
* @param config Game configuration
|
|
* @param last_time Pointer to last touch time for debouncing
|
|
* @return InputEvent structure (valid=false if no valid input)
|
|
*/
|
|
InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) {
|
|
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
|
|
|
// Check if touch interrupt flag is set
|
|
if (!touch_interrupt_flag) {
|
|
return event; // No touch event
|
|
}
|
|
|
|
// Don't clear the flag yet - we may still be processing continuous touch
|
|
|
|
// Check if touch is active
|
|
if (!touch_event_down) {
|
|
// Touch released
|
|
touch_interrupt_flag = false;
|
|
event.type = INPUT_TOUCH_UP;
|
|
event.valid = true;
|
|
return event;
|
|
}
|
|
|
|
// Touch is down - check debounce timing
|
|
uint32_t now = to_ms_since_boot(get_absolute_time());
|
|
if (now - *last_time < config.touch_debounce_ms) {
|
|
return event; // Too soon, skip
|
|
}
|
|
|
|
// Read touch data
|
|
TouchData touch_data;
|
|
if (!touch || !touch->read_touch(&touch_data)) {
|
|
return event; // Read failed
|
|
}
|
|
|
|
// Populate event structure
|
|
event.x = touch_data.points[0].x;
|
|
event.y = touch_data.points[0].y;
|
|
event.pressure = touch_data.points[0].pressure;
|
|
event.gesture_code = touch_data.gesture;
|
|
event.valid = true;
|
|
|
|
// Determine event type
|
|
if (*last_time == 0) {
|
|
event.type = INPUT_TOUCH_DOWN;
|
|
} else {
|
|
event.type = INPUT_TOUCH_MOVE;
|
|
}
|
|
|
|
// Handle gesture events
|
|
if (config.enable_gestures && touch_data.gesture != 0) {
|
|
event.type = INPUT_GESTURE;
|
|
if (config.debug_verbose) {
|
|
printf("Gesture: 0x%02X (%s)\n", event.gesture_code, get_gesture_name(event.gesture_code));
|
|
}
|
|
}
|
|
|
|
*last_time = now;
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* @brief Process button input and convert to InputEvent
|
|
*
|
|
* Checks button flags and verifies button state with debouncing.
|
|
* Clears flags after processing.
|
|
*
|
|
* @param config Game configuration
|
|
* @return InputEvent structure (valid=false if no valid input)
|
|
*/
|
|
InputEvent process_button_input(const GameConfig& config) {
|
|
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
|
|
|
#ifdef BUTTON_KEY0_PIN
|
|
// Check KEY0
|
|
if (button_key0_pressed) {
|
|
button_key0_pressed = false;
|
|
sleep_ms(config.button_debounce_ms);
|
|
|
|
if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify still pressed
|
|
event.type = INPUT_BUTTON_0;
|
|
event.button_id = 0;
|
|
event.valid = true;
|
|
|
|
if (config.debug_verbose) {
|
|
printf("Button KEY0 action triggered\n");
|
|
}
|
|
return event;
|
|
}
|
|
}
|
|
|
|
#ifdef BUTTON_KEY1_PIN
|
|
// Check KEY1
|
|
if (button_key1_pressed) {
|
|
button_key1_pressed = false;
|
|
sleep_ms(config.button_debounce_ms);
|
|
|
|
if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify still pressed
|
|
event.type = INPUT_BUTTON_1;
|
|
event.button_id = 1;
|
|
event.valid = true;
|
|
|
|
if (config.debug_verbose) {
|
|
printf("Button KEY1 action triggered\n");
|
|
}
|
|
return event;
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
return event;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GAME LOGIC (Customize this section for your game!)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief Initialize game state
|
|
*
|
|
* Called once at startup to set initial game values.
|
|
* Customize this for your game.
|
|
*
|
|
* @param state Pointer to GameState to initialize
|
|
*/
|
|
void game_init(GameState* state) {
|
|
state->last_x = -1;
|
|
state->last_y = -1;
|
|
state->is_drawing = false;
|
|
state->score = 0;
|
|
state->game_over = false;
|
|
state->frame_count = 0;
|
|
state->progress_value = 50; // Start at 50%
|
|
state->focused_button = 0; // Start with first button focused
|
|
state->button1_clicks = 0;
|
|
state->button2_clicks = 0;
|
|
state->touch_success_count = 0;
|
|
state->touch_fail_count = 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Update game state based on input event
|
|
*
|
|
* This is where your game logic goes.
|
|
* Called whenever an input event occurs.
|
|
*
|
|
* @param state Pointer to GameState to update
|
|
* @param input Input event to process
|
|
* @param config Game configuration
|
|
* @param renderer Renderer for drawing operations
|
|
* @return true if screen needs refresh (drawing occurred)
|
|
*/
|
|
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
|
|
bool needs_refresh = false;
|
|
|
|
switch (input.type) {
|
|
case INPUT_TOUCH_DOWN:
|
|
// Start new drawing stroke
|
|
state->last_x = input.x;
|
|
state->last_y = input.y;
|
|
state->is_drawing = true;
|
|
state->touch_success_count++;
|
|
break;
|
|
|
|
case INPUT_TOUCH_MOVE:
|
|
// Continue drawing stroke
|
|
if (config.enable_continuous_draw && state->is_drawing) {
|
|
if (state->last_x >= 0 && state->last_y >= 0) {
|
|
// Draw line from last position
|
|
renderer->draw_line(state->last_x, state->last_y, input.x, input.y, true);
|
|
needs_refresh = true;
|
|
}
|
|
state->last_x = input.x;
|
|
state->last_y = input.y;
|
|
state->touch_success_count++;
|
|
}
|
|
break;
|
|
|
|
case INPUT_TOUCH_UP:
|
|
// End drawing stroke
|
|
state->is_drawing = false;
|
|
state->last_x = -1;
|
|
state->last_y = -1;
|
|
needs_refresh = true; // Final refresh to show complete stroke
|
|
break;
|
|
|
|
case INPUT_BUTTON_0:
|
|
// KEY0: Switch focus between buttons
|
|
state->focused_button = (state->focused_button == 0) ? 1 : 0;
|
|
needs_refresh = true;
|
|
if (config.debug_verbose) {
|
|
printf("Focus switched to button %d\n", state->focused_button);
|
|
}
|
|
break;
|
|
|
|
case INPUT_BUTTON_1:
|
|
// KEY1: Activate the focused button
|
|
if (state->focused_button == 0) {
|
|
state->button1_clicks++;
|
|
if (config.debug_verbose) {
|
|
printf("Button 1 clicked! Total: %d\n", state->button1_clicks);
|
|
}
|
|
} else {
|
|
state->button2_clicks++;
|
|
if (config.debug_verbose) {
|
|
printf("Button 2 clicked! Total: %d\n", state->button2_clicks);
|
|
}
|
|
}
|
|
needs_refresh = true;
|
|
break;
|
|
|
|
case INPUT_GESTURE:
|
|
// Handle gesture
|
|
if (config.debug_verbose) {
|
|
printf("Gesture detected: %s\n", get_gesture_name(input.gesture_code));
|
|
}
|
|
// Add gesture-specific actions here
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
state->frame_count++;
|
|
return needs_refresh;
|
|
}
|
|
|
|
/**
|
|
* @brief Draw game graphics to screen buffer
|
|
*
|
|
* All initial UI drawing operations go here.
|
|
* Called once at startup to create the initial screen.
|
|
*
|
|
* @param state Pointer to current GameState
|
|
* @param renderer Renderer for drawing primitives
|
|
* @param gui GUI system for widgets (optional)
|
|
*/
|
|
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
|
|
// Draw main window
|
|
LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Button Game");
|
|
|
|
// Draw instructions using text
|
|
renderer->set_font(&font_5x5_obj);
|
|
renderer->draw_string(20, 50, "KEY0: Switch Focus", true);
|
|
renderer->draw_string(20, 65, "KEY1: Click Button", true);
|
|
|
|
// Create button labels with click counts
|
|
char btn1_label[30];
|
|
snprintf(btn1_label, sizeof(btn1_label), "BTN 1 (%d)", state->button1_clicks);
|
|
|
|
char btn2_label[30];
|
|
snprintf(btn2_label, sizeof(btn2_label), "BTN 2 (%d)", state->button2_clicks);
|
|
|
|
// Draw Button 1 using GUI button element
|
|
// pressed=true shows it's focused/selected
|
|
gui->draw_button(w1, 10, 90, btn1_label, state->focused_button == 0, true);
|
|
|
|
// Draw Button 2 using GUI button element
|
|
gui->draw_button(w1, 10, 140, btn2_label, state->focused_button == 1, true);
|
|
|
|
// Draw status indicators using GUI elements
|
|
// Show which button is focused
|
|
if (state->focused_button == 0) {
|
|
gui->draw_radio_button(w1, 200, 100, "Active", true);
|
|
} else {
|
|
gui->draw_radio_button(w1, 200, 100, "Active", false);
|
|
}
|
|
|
|
if (state->focused_button == 1) {
|
|
gui->draw_radio_button(w1, 200, 150, "Active", true);
|
|
} else {
|
|
gui->draw_radio_button(w1, 200, 150, "Active", false);
|
|
}
|
|
|
|
// Show total interactions with a status bar
|
|
uint32_t total_clicks = state->button1_clicks + state->button2_clicks;
|
|
int percentage = (total_clicks > 0) ? ((state->button1_clicks * 100) / total_clicks) : 50;
|
|
|
|
char total_str[20];
|
|
snprintf(total_str, sizeof(total_str), "%d", total_clicks);
|
|
gui->draw_status_bar(w1, 10, 200, 270, "TOTAL CLICKS", "BTN1 vs BTN2 Ratio", percentage, total_str);
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN PROGRAM
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
int main()
|
|
{
|
|
// Initialize standard I/O for debugging with timeout
|
|
// This prevents hanging when USB is not connected
|
|
stdio_init_all();
|
|
sleep_ms(5000); // Wait for USB connection (if present)
|
|
|
|
printf("\n=== %s Demo ===\n", BOARD_NAME);
|
|
|
|
// Create display abstraction using factory method
|
|
// The factory handles all board-specific configuration internally
|
|
LowLevelDisplay* display = LowLevelDisplay::create((DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT);
|
|
|
|
if (!display) {
|
|
printf("Failed to create display!\n");
|
|
return -1;
|
|
}
|
|
|
|
printf("Initializing 4.0\" TFT with Touch and SD Card...\n");
|
|
|
|
// Initialize the display
|
|
if (!display->init()) {
|
|
printf("Display initialization failed!\n");
|
|
delete display;
|
|
return -1;
|
|
}
|
|
|
|
// Do a full refresh with white screen first (removes ghosting on e-paper)
|
|
printf("Performing initial full refresh to white...\n");
|
|
display->clear(true); // Clear to white
|
|
|
|
// For e-paper, do a full refresh to ensure clean display
|
|
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
|
|
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
|
|
epaper->full_refresh(); // Full refresh removes ghosting
|
|
printf("Full refresh complete\n");
|
|
} else {
|
|
refresh_screen(bit_buffer, display); // For TFT, just refresh normally
|
|
}
|
|
|
|
// Now clear to black for drawing
|
|
display->clear(false); // Clear to black
|
|
|
|
// Initialize renderer and GUI system
|
|
LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT);
|
|
renderer.set_font(&font_5x5_obj);
|
|
LowLevelGUI gui = LowLevelGUI(&renderer, font_BMplain_obj);
|
|
|
|
// Initialize game configuration
|
|
GameConfig config = {
|
|
.touch_debounce_ms = 10,
|
|
.button_debounce_ms = 50,
|
|
.enable_gestures = true,
|
|
.enable_continuous_draw = true,
|
|
.debug_verbose = false
|
|
};
|
|
|
|
// Initialize game state
|
|
GameState game_state;
|
|
game_init(&game_state);
|
|
|
|
// Draw initial game graphics
|
|
game_draw(&game_state, &renderer, &gui);
|
|
|
|
// Refresh the screen with the rendered GUI
|
|
refresh_screen(bit_buffer, display);
|
|
|
|
// Initialize touch screen using abstraction
|
|
touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT,
|
|
TOUCH_SWAP_XY, TOUCH_INVERT_X, TOUCH_INVERT_Y);
|
|
|
|
if (touch) {
|
|
printf("Touch initialized successfully\n");
|
|
|
|
// Set up interrupt-driven touch detection
|
|
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");
|
|
|
|
// Run communication test if available
|
|
// Note: Commented out as it may hang on some hardware configurations
|
|
printf("\nRunning touch reliability test...\n");
|
|
touch->test_communication();
|
|
printf("...\n");
|
|
} else {
|
|
printf("Touch initialization failed or not configured\n");
|
|
}
|
|
|
|
#ifdef BUTTON_KEY0_PIN
|
|
// Initialize hardware buttons (e-ink board only)
|
|
printf("\nInitializing hardware buttons...\n");
|
|
|
|
// Initialize KEY0 button
|
|
gpio_init(BUTTON_KEY0_PIN);
|
|
gpio_set_dir(BUTTON_KEY0_PIN, GPIO_IN);
|
|
gpio_pull_up(BUTTON_KEY0_PIN); // Active LOW with pull-up
|
|
printf(" KEY0 initialized on GP%d (active LOW)\n", BUTTON_KEY0_PIN);
|
|
|
|
#ifdef BUTTON_KEY1_PIN
|
|
// Initialize KEY1 button
|
|
gpio_init(BUTTON_KEY1_PIN);
|
|
gpio_set_dir(BUTTON_KEY1_PIN, GPIO_IN);
|
|
gpio_pull_up(BUTTON_KEY1_PIN); // Active LOW with pull-up
|
|
printf(" KEY1 initialized on GP%d (active LOW)\n", BUTTON_KEY1_PIN);
|
|
#endif
|
|
|
|
// Enable interrupts on falling edge (button press)
|
|
gpio_set_irq_enabled_with_callback(BUTTON_KEY0_PIN,
|
|
GPIO_IRQ_EDGE_FALL,
|
|
true,
|
|
&button_interrupt_handler);
|
|
#ifdef BUTTON_KEY1_PIN
|
|
gpio_set_irq_enabled(BUTTON_KEY1_PIN, GPIO_IRQ_EDGE_FALL, true);
|
|
#endif
|
|
|
|
printf("Button interrupts enabled (falling edge = press)\n");
|
|
#endif
|
|
|
|
// Test SD card and FatFS
|
|
// if (sd_card_init_with_board_config()) {
|
|
// sd_card_test_fatfs();
|
|
// } else {
|
|
// printf("SD Card initialization failed or no card present\n");
|
|
// }
|
|
|
|
// ========================================================================
|
|
// REACTIVE GAME LOOP
|
|
// ========================================================================
|
|
// The loop sleeps until an interrupt occurs, then:
|
|
// 1. Process input (button or touch)
|
|
// 2. Update game state based on input
|
|
// 3. Redraw only if game_update() indicates changes occurred
|
|
// This is ideal for e-ink displays (minimal refreshes) and power efficiency
|
|
// ========================================================================
|
|
|
|
uint32_t last_touch_time = 0;
|
|
|
|
while (1) {
|
|
// Sleep until interrupt wakes us up (very power efficient!)
|
|
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs
|
|
|
|
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
|
bool needs_refresh = false;
|
|
|
|
// 1. Process button input first (higher priority)
|
|
input = process_button_input(config);
|
|
if (input.valid) {
|
|
needs_refresh = game_update(&game_state, input, config, &renderer);
|
|
}
|
|
|
|
// 2. Process touch input (if no button was pressed)
|
|
if (!input.valid) {
|
|
input = process_touch_input(config, &last_touch_time);
|
|
if (input.valid) {
|
|
needs_refresh = game_update(&game_state, input, config, &renderer);
|
|
}
|
|
}
|
|
|
|
// 3. Redraw and refresh screen only if needed
|
|
if (needs_refresh) {
|
|
// For button presses or touch release, redraw entire UI
|
|
if (input.type == INPUT_BUTTON_0 || input.type == INPUT_BUTTON_1 || input.type == INPUT_TOUCH_UP) {
|
|
// Clear buffer and redraw entire UI with updated state
|
|
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
|
|
game_draw(&game_state, &renderer, &gui);
|
|
}
|
|
refresh_screen(bit_buffer, display);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|