/* * 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 "pico/multicore.h" #include "board_config.h" // Board-specific pin configuration #include "sd_card.h" #include #include #include #include "display/low_level_render.h" #include "display/low_level_display.h" #include "display/low_level_display_epaper.h" #include "display/low_level_touch.h" #include "input_manager.h" #include "game.h" #include "game_launcher.h" #include "tic_tac_toe.h" #include "demo_game.h" #include "monopoly_game.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__)); // ============================================================================ // DUAL-CORE DISPLAY REFRESH SYSTEM // ============================================================================ // Shared variables for core communication volatile bool refresh_requested = false; volatile bool refresh_in_progress = false; const uint8_t* volatile refresh_buffer = nullptr; LowLevelDisplay* volatile refresh_display = nullptr; /** * @brief Core 1 entry point - handles display refresh operations * * Runs on the second core, waiting for refresh requests. * This keeps Core 0 responsive while display updates happen in background. */ void core1_entry() { printf("Core 1 started - handling display refreshes\n"); while (1) { // Wait for refresh request if (refresh_requested && refresh_buffer && refresh_display) { refresh_in_progress = true; // Get local copies for safe access LowLevelDisplay* display = refresh_display; const uint8_t* buffer = refresh_buffer; // Perform the refresh operation (may be slow for e-ink) display->draw_buffer(buffer); display->refresh(); // Clear flags refresh_requested = false; refresh_in_progress = false; } // Small delay to avoid busy-waiting sleep_us(100); } } /** * @brief Request a screen refresh (non-blocking) * * Queues the refresh on Core 1, keeping Core 0 responsive. * * @param buffer Pointer to 1-bit framebuffer * @param display Pointer to display abstraction * @return true if refresh started, false if already in progress */ bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) { // Check if Core 1 is busy with previous refresh if (refresh_in_progress) { // Still refreshing previous frame, skip this one return false; } // Queue refresh on Core 1 refresh_buffer = buffer; refresh_display = display; refresh_requested = true; return true; } /** * @brief Check if a refresh is currently in progress * @return true if Core 1 is still refreshing */ bool is_refresh_in_progress() { return refresh_in_progress; } // ============================================================================ // GAME CONFIGURATION // ============================================================================ // 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; printf("INT: FALL\n"); } if (events & GPIO_IRQ_EDGE_RISE) { touch_event_down = false; printf("INT: RISE\n"); } } #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 // ============================================================================ // ============================================================================ // 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(100); // Wait for USB connection (if present) printf("\n=== %s Demo ===\n", BOARD_NAME); printf("Starting dual-core system...\n"); // 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 display...\n"); // Initialize the display if (!display->init()) { printf("Display initialization failed!\n"); delete display; return -1; } // Launch Core 1 for display refresh handling printf("Launching Core 1 for display refresh...\n"); multicore_launch_core1(core1_entry); sleep_ms(100); // Give Core 1 time to start // 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(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_homespun_obj); LowLevelGUI gui = LowLevelGUI(&renderer, font_homespun_obj); // Initialize touch screen using abstraction FIRST (before InputManager needs it) 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"); } // Initialize game configuration GameConfig config = { .touch_debounce_ms = 10, .button_debounce_ms = 20, .enable_gestures = true, .enable_continuous_draw = true, .debug_verbose = false }; // Create InputManager for processing inputs (touch must be initialized first!) InputManager input_manager(touch, &config); // Create GameLauncher GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager); // Register available games launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { return new TicTacToeGame(w, h, r, g, im); }); launcher.register_game("Monopoly", "Classic property trading game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { // For Feather TFT (480x320), reduce width to 430 to make room for sidebar buttons uint16_t game_w = (w == 480) ? 430 : w; return new MonopolyGame(game_w, h, r, g, im); }); launcher.register_game("Demo Game", "Simple test game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { return new DemoGame(w, h, r, g, im); }); // Draw launcher menu launcher.draw(); // Refresh the screen with the launcher menu (async on Core 1) refresh_screen_async(bit_buffer, display); printf("Initial screen refresh queued on Core 1\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 WITH DUAL-CORE REFRESH // ======================================================================== // 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: // 1. Process input (button or touch) // 2. Update game state based on input // 3. Queue refresh on Core 1 (non-blocking) // This keeps Core 0 responsive even during slow e-ink refreshes // ======================================================================== uint32_t last_touch_time = 0; bool pending_refresh = false; // Track if we have a pending refresh printf("\nEntering reactive game loop (Core 0 - input & logic)\n"); printf("Display refreshes handled by Core 1\n\n"); Game* current_game = nullptr; uint32_t game_start_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 = input_manager.process_button_input(); // 2. Process touch input (if no button was pressed) if (!input.valid) { input = input_manager.process_touch_input(&last_touch_time); // if debugging enabled, print touch event if (input.valid && config.debug_verbose) { printf("Touch Event: type=%d, x=%d, y=%d, pressure=%d, gesture=0x%02X\n", input.type, input.x, input.y, input.pressure, input.gesture_code); } } // 3. Process input based on current state if (input.valid) { // if debugging enabled, print input event if (config.debug_verbose) { printf("Input Event: type=%d, x=%d, y=%d, gesture=0x%02X, button=%d, pressure=%d\n", input.type, input.x, input.y, input.gesture_code, input.button_id, input.pressure); } if (launcher.is_game_selected()) { // In game mode - process game input current_game = launcher.get_selected_game(); needs_refresh = current_game->update(input); // Check if game wants to exit if (current_game->wants_to_exit()) { printf("Game requested exit - returning to launcher\n"); launcher.reset(); needs_refresh = true; // Force full clear for clean transition display->clear(false); if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); } } // Check if player wants to exit (hold for 2+ seconds or special gesture) // For now, we'll add a simple long-press detection if (input.type == INPUT_TOUCH_DOWN) { // Record start time on first touch if (game_start_time == 0) { game_start_time = to_ms_since_boot(get_absolute_time()); } } else if (input.type == INPUT_TOUCH_UP) { uint32_t now = to_ms_since_boot(get_absolute_time()); if (game_start_time > 0 && (now - game_start_time) > 2000) { // Long press detected - return to menu printf("Long press detected - returning to launcher\n"); launcher.reset(); needs_refresh = true; // Force full clear for clean transition display->clear(false); if (display->get_type() == DISPLAY_TYPE_EPAPER) { LowLevelDisplayEPaper* epaper = static_cast(display); epaper->full_refresh(); } } game_start_time = 0; } } else { // In launcher mode - process menu input bool game_selected = launcher.update(input); if (game_selected) { printf("Game launched successfully\n"); game_start_time = 0; // Force full clear for clean transition to game display->clear(false); // if (display->get_type() == DISPLAY_TYPE_EPAPER) { // LowLevelDisplayEPaper* epaper = static_cast(display); // epaper->full_refresh(); // } } needs_refresh = true; } } // 4. Redraw and queue async refresh on Core 1 if (needs_refresh || pending_refresh) { // Clear buffer and redraw entire UI with updated state memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); if (launcher.is_game_selected()) { current_game = launcher.get_selected_game(); current_game->draw(); } else { launcher.draw(); } // Request async refresh (non-blocking - handled by Core 1) bool refresh_started = refresh_screen_async(bit_buffer, display); if (refresh_started) { pending_refresh = false; // Refresh queued successfully } else { pending_refresh = true; // Core 1 busy, retry next iteration if (config.debug_verbose) { printf("Refresh pending - Core 1 still busy\n"); } } // Core 0 continues immediately, Core 1 handles the refresh } } return 0; }