Files
basic1/basic1.cpp
2026-01-30 21:39:09 -05:00

504 lines
18 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 "pico/multicore.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_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"
// 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(5000); // 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<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 = 20,
.enable_gestures = true,
.enable_continuous_draw = true,
.debug_verbose = false
};
// Create InputManager for processing inputs
InputManager input_manager(touch, &config);
// Create GameLauncher
GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui);
// Register available games
launcher.register_game("Tic-Tac-Toe", "Classic 2-player game",
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g) -> Game* {
return new TicTacToeGame(w, h, r, g);
});
launcher.register_game("Demo Game", "Simple test game",
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g) -> Game* {
return new DemoGame(w, h, r, g);
});
// 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");
// 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 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);
}
// 3. Process input based on current state
if (input.valid) {
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 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;
}
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;
}
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;
}