/* * 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 #include #include #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(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; }