tic tac toe works on touch and eink

This commit is contained in:
Adolfo Reyna
2026-01-29 17:16:59 -05:00
parent 372895fa08
commit 435a5caa56
7 changed files with 450 additions and 145 deletions

View File

@@ -88,6 +88,7 @@ target_include_directories(basic1 PRIVATE
target_link_libraries(basic1 target_link_libraries(basic1
hardware_spi hardware_spi
hardware_i2c hardware_i2c
pico_multicore
m m
) )

View File

@@ -37,6 +37,7 @@
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "pico/binary_info.h" #include "pico/binary_info.h"
#include "hardware/sync.h" #include "hardware/sync.h"
#include "pico/multicore.h"
#include "board_config.h" // Board-specific pin configuration #include "board_config.h" // Board-specific pin configuration
#include "sd_card.h" #include "sd_card.h"
#include <stdlib.h> #include <stdlib.h>
@@ -54,6 +55,80 @@ 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_version_string("0.1"));
bi_decl(bi_program_build_date_string(__DATE__)); 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;
}
// ============================================================================ // ============================================================================
// INPUT EVENT STRUCTURES // INPUT EVENT STRUCTURES
// ============================================================================ // ============================================================================
@@ -86,25 +161,19 @@ struct InputEvent {
// Game state - customize this for your game // Game state - customize this for your game
struct GameState { struct GameState {
// Drawing game state // Tic-Tac-Toe game state
int16_t last_x; uint8_t board[3][3]; // 0=empty, 1=X, 2=O
int16_t last_y; uint8_t current_player; // 1=X, 2=O
bool is_drawing; uint8_t winner; // 0=none, 1=X wins, 2=O wins, 3=tie
uint8_t selected_row; // Currently selected cell
// General game state uint8_t selected_col;
uint32_t score;
bool game_over; bool game_over;
uint32_t frame_count;
// UI state // Game statistics
uint8_t progress_value; // Progress bar value (0-100) uint32_t x_wins;
uint8_t focused_button; // Which button has focus (0 or 1) uint32_t o_wins;
uint32_t button1_clicks; // Count clicks on button 1 uint32_t ties;
uint32_t button2_clicks; // Count clicks on button 2 uint32_t total_moves;
// Statistics
uint32_t touch_success_count;
uint32_t touch_fail_count;
}; };
// Game configuration - adjust these for your game // Game configuration - adjust these for your game
@@ -151,9 +220,11 @@ void touch_interrupt_handler(uint gpio, uint32_t events) {
// Track which edge triggered (down vs up) // Track which edge triggered (down vs up)
if (events & GPIO_IRQ_EDGE_FALL) { if (events & GPIO_IRQ_EDGE_FALL) {
touch_event_down = true; touch_event_down = true;
printf("INT: FALL\n");
} }
if (events & GPIO_IRQ_EDGE_RISE) { if (events & GPIO_IRQ_EDGE_RISE) {
touch_event_down = false; touch_event_down = false;
printf("INT: RISE\n");
} }
} }
@@ -247,14 +318,18 @@ InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) {
return event; // No touch event return event; // No touch event
} }
printf("Processing touch: flag=%d, event_down=%d\n", touch_interrupt_flag, touch_event_down);
// Don't clear the flag yet - we may still be processing continuous touch // Don't clear the flag yet - we may still be processing continuous touch
// Check if touch is active // Check if touch is active
if (!touch_event_down) { if (!touch_event_down) {
// Touch released // Touch released - reset timing for next touch
touch_interrupt_flag = false; touch_interrupt_flag = false;
*last_time = 0; // Reset so next touch is treated as new touch-down
event.type = INPUT_TOUCH_UP; event.type = INPUT_TOUCH_UP;
event.valid = true; event.valid = true;
printf("Touch UP\n");
return event; return event;
} }
@@ -267,9 +342,18 @@ InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) {
// Read touch data // Read touch data
TouchData touch_data; TouchData touch_data;
if (!touch || !touch->read_touch(&touch_data)) { if (!touch || !touch->read_touch(&touch_data)) {
// Clear flag even if read failed to prevent getting stuck
touch_interrupt_flag = false;
printf("Touch read FAILED\n");
return event; // Read failed return event; // Read failed
} }
// Clear the interrupt flag after successfully reading touch data
// This allows the next touch interrupt to be detected
touch_interrupt_flag = false;
printf("Touch DOWN at (%d,%d)\n", touch_data.points[0].x, touch_data.points[0].y);
// Populate event structure // Populate event structure
event.x = touch_data.points[0].x; event.x = touch_data.points[0].x;
event.y = touch_data.points[0].y; event.y = touch_data.points[0].y;
@@ -353,6 +437,63 @@ InputEvent process_button_input(const GameConfig& config) {
// GAME LOGIC (Customize this section for your game!) // GAME LOGIC (Customize this section for your game!)
// ============================================================================ // ============================================================================
// Game board dimensions (used for both drawing and touch detection)
const int BOARD_SIZE = 200;
const int CELL_SIZE = BOARD_SIZE / 3;
const int BOARD_Y = 80; // Y position below title
/**
* @brief Check if there's a winner on the board
* @return 0=no winner, 1=X wins, 2=O wins, 3=tie
*/
uint8_t check_winner(const GameState* state) {
// Check rows
for (int row = 0; row < 3; row++) {
if (state->board[row][0] != 0 &&
state->board[row][0] == state->board[row][1] &&
state->board[row][1] == state->board[row][2]) {
return state->board[row][0];
}
}
// Check columns
for (int col = 0; col < 3; col++) {
if (state->board[0][col] != 0 &&
state->board[0][col] == state->board[1][col] &&
state->board[1][col] == state->board[2][col]) {
return state->board[0][col];
}
}
// Check diagonals
if (state->board[0][0] != 0 &&
state->board[0][0] == state->board[1][1] &&
state->board[1][1] == state->board[2][2]) {
return state->board[0][0];
}
if (state->board[0][2] != 0 &&
state->board[0][2] == state->board[1][1] &&
state->board[1][1] == state->board[2][0]) {
return state->board[0][2];
}
// Check for tie (board full)
bool board_full = true;
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
if (state->board[row][col] == 0) {
board_full = false;
break;
}
}
if (!board_full) break;
}
if (board_full) return 3; // Tie
return 0; // No winner yet
}
/** /**
* @brief Initialize game state * @brief Initialize game state
* *
@@ -362,18 +503,22 @@ InputEvent process_button_input(const GameConfig& config) {
* @param state Pointer to GameState to initialize * @param state Pointer to GameState to initialize
*/ */
void game_init(GameState* state) { void game_init(GameState* state) {
state->last_x = -1; // Clear the board
state->last_y = -1; for (int row = 0; row < 3; row++) {
state->is_drawing = false; for (int col = 0; col < 3; col++) {
state->score = 0; state->board[row][col] = 0;
}
}
state->current_player = 1; // X starts
state->winner = 0;
state->selected_row = 1; // Start in center
state->selected_col = 1;
state->game_over = false; state->game_over = false;
state->frame_count = 0; state->total_moves = 0;
state->progress_value = 50; // Start at 50%
state->focused_button = 0; // Start with first button focused // Keep win statistics across games
state->button1_clicks = 0; // state->x_wins, state->o_wins, state->ties remain unchanged
state->button2_clicks = 0;
state->touch_success_count = 0;
state->touch_fail_count = 0;
} }
/** /**
@@ -392,74 +537,124 @@ bool game_update(GameState* state, const InputEvent& input, const GameConfig& co
bool needs_refresh = false; bool needs_refresh = false;
switch (input.type) { switch (input.type) {
case INPUT_TOUCH_DOWN: case INPUT_TOUCH_DOWN: {
// Start new drawing stroke // If game is over, restart on touch
state->last_x = input.x; if (state->game_over) {
state->last_y = input.y; game_init(state);
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; needs_refresh = true;
}
state->last_x = input.x;
state->last_y = input.y;
state->touch_success_count++;
}
break; break;
}
case INPUT_TOUCH_UP: printf("Touch down at (%d,%d)\n", input.x, input.y);
// End drawing stroke
state->is_drawing = false; // Calculate board position (must match game_draw!)
state->last_x = -1; int board_x = V_WIDTH - BOARD_SIZE - 20;
state->last_y = -1;
needs_refresh = true; // Final refresh to show complete stroke // Check if touch is within board
if (input.x >= board_x && input.x < board_x + BOARD_SIZE &&
input.y >= BOARD_Y && input.y < BOARD_Y + BOARD_SIZE) {
int touched_col = (input.x - board_x) / CELL_SIZE;
int touched_row = (input.y - BOARD_Y) / CELL_SIZE;
// Clamp to valid range (safety check)
if (touched_row >= 0 && touched_row < 3 && touched_col >= 0 && touched_col < 3) {
// Place piece if cell is empty
if (state->board[touched_row][touched_col] == 0) {
state->board[touched_row][touched_col] = state->current_player;
state->total_moves++;
// Check for winner
state->winner = check_winner(state);
if (state->winner != 0) {
state->game_over = true;
if (state->winner == 1) state->x_wins++;
else if (state->winner == 2) state->o_wins++;
else if (state->winner == 3) state->ties++;
} else {
// Switch player
state->current_player = (state->current_player == 1) ? 2 : 1;
}
needs_refresh = true;
if (config.debug_verbose) {
printf("Touch at [%d,%d] (pixel %d,%d) by player %d\n",
touched_row, touched_col, input.x, input.y,
state->current_player == 1 ? 2 : 1);
}
} else if (config.debug_verbose) {
printf("Cell [%d,%d] already occupied\n", touched_row, touched_col);
}
}
} else if (config.debug_verbose) {
printf("Touch outside board: %d,%d\n", input.x, input.y);
}
break; break;
}
case INPUT_BUTTON_0: case INPUT_BUTTON_0:
// KEY0: Switch focus between buttons // KEY0: Move selection (for button-only boards)
state->focused_button = (state->focused_button == 0) ? 1 : 0; if (!state->game_over) {
// Move to next cell, skipping occupied ones
int attempts = 0;
do {
state->selected_col++;
if (state->selected_col > 2) {
state->selected_col = 0;
state->selected_row++;
if (state->selected_row > 2) {
state->selected_row = 0;
}
}
attempts++;
// If we've tried all 9 cells, stop (game might be full)
if (attempts >= 9) break;
} while (state->board[state->selected_row][state->selected_col] != 0);
needs_refresh = true; needs_refresh = true;
if (config.debug_verbose) { if (config.debug_verbose) {
printf("Focus switched to button %d\n", state->focused_button); printf("Selection moved to [%d,%d]\n", state->selected_row, state->selected_col);
}
} else {
// Restart game
game_init(state);
needs_refresh = true;
} }
break; break;
case INPUT_BUTTON_1: case INPUT_BUTTON_1:
// KEY1: Activate the focused button // KEY1: Place piece at selected position
if (state->focused_button == 0) { if (!state->game_over) {
state->button1_clicks++; if (state->board[state->selected_row][state->selected_col] == 0) {
if (config.debug_verbose) { state->board[state->selected_row][state->selected_col] = state->current_player;
printf("Button 1 clicked! Total: %d\n", state->button1_clicks); state->total_moves++;
}
} 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: // Check for winner
// Handle gesture state->winner = check_winner(state);
if (config.debug_verbose) { if (state->winner != 0) {
printf("Gesture detected: %s\n", get_gesture_name(input.gesture_code)); state->game_over = true;
if (state->winner == 1) state->x_wins++;
else if (state->winner == 2) state->o_wins++;
else if (state->winner == 3) state->ties++;
} else {
// Switch player
state->current_player = (state->current_player == 1) ? 2 : 1;
}
needs_refresh = true;
if (config.debug_verbose) {
printf("Piece placed at [%d,%d]\n", state->selected_row, state->selected_col);
}
}
} }
// Add gesture-specific actions here
break; break;
default: default:
break; break;
} }
state->frame_count++;
return needs_refresh; return needs_refresh;
} }
@@ -475,48 +670,99 @@ bool game_update(GameState* state, const InputEvent& input, const GameConfig& co
*/ */
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) { void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
// Draw main window // Draw main window
LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Button Game"); LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Tic-Tac-Toe");
// Draw instructions using text
renderer->set_font(&font_5x5_obj); 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 // Draw current player or game result
char btn1_label[30]; if (state->game_over) {
snprintf(btn1_label, sizeof(btn1_label), "BTN 1 (%d)", state->button1_clicks); if (state->winner == 1) {
renderer->draw_string(20, 40, "X WINS!", true);
char btn2_label[30]; } else if (state->winner == 2) {
snprintf(btn2_label, sizeof(btn2_label), "BTN 2 (%d)", state->button2_clicks); renderer->draw_string(20, 40, "O WINS!", true);
// 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 { } else {
gui->draw_radio_button(w1, 200, 100, "Active", false); renderer->draw_string(20, 40, "TIE GAME!", true);
}
renderer->draw_string(20, 55, "Touch or KEY0 to restart", true);
} else {
char turn_text[30];
snprintf(turn_text, sizeof(turn_text), "Turn: %s", state->current_player == 1 ? "X" : "O");
renderer->draw_string(20, 40, turn_text, true);
renderer->draw_string(20, 55, "Touch cell or use keys", true);
} }
if (state->focused_button == 1) { // Draw game board (use same layout as touch detection!)
gui->draw_radio_button(w1, 200, 150, "Active", true); int board_x = V_WIDTH - BOARD_SIZE - 20;
// Draw current turn indicator (large, on left side)
if (!state->game_over) {
int indicator_x = 60;
int indicator_y = V_HEIGHT / 2;
int piece_offset = CELL_SIZE / 4;
if (state->current_player == 1) {
// Draw large X
renderer->draw_line(indicator_x - piece_offset, indicator_y - piece_offset,
indicator_x + piece_offset, indicator_y + piece_offset, true, 4);
renderer->draw_line(indicator_x + piece_offset, indicator_y - piece_offset,
indicator_x - piece_offset, indicator_y + piece_offset, true, 4);
} else { } else {
gui->draw_radio_button(w1, 200, 150, "Active", false); // Draw large O
int piece_radius = CELL_SIZE / 4;
renderer->draw_circle(indicator_x, indicator_y, piece_radius, true);
renderer->draw_circle(indicator_x, indicator_y, piece_radius - 1, true);
renderer->draw_circle(indicator_x, indicator_y, piece_radius - 2, true);
renderer->draw_circle(indicator_x, indicator_y, piece_radius - 3, true);
}
}
// Draw grid lines
for (int i = 1; i < 3; i++) {
// Vertical lines
renderer->draw_line(board_x + i * CELL_SIZE, BOARD_Y,
board_x + i * CELL_SIZE, BOARD_Y + BOARD_SIZE, true, 2);
// Horizontal lines
renderer->draw_line(board_x, BOARD_Y + i * CELL_SIZE,
board_x + BOARD_SIZE, BOARD_Y + i * CELL_SIZE, true, 2);
} }
// Show total interactions with a status bar // Draw outer border
uint32_t total_clicks = state->button1_clicks + state->button2_clicks; renderer->draw_rectangle(board_x, BOARD_Y, BOARD_SIZE, BOARD_SIZE, true, 3);
int percentage = (total_clicks > 0) ? ((state->button1_clicks * 100) / total_clicks) : 50;
char total_str[20]; // Draw X's and O's
snprintf(total_str, sizeof(total_str), "%d", total_clicks); for (int row = 0; row < 3; row++) {
gui->draw_status_bar(w1, 10, 200, 270, "TOTAL CLICKS", "BTN1 vs BTN2 Ratio", percentage, total_str); for (int col = 0; col < 3; col++) {
int cell_x = board_x + col * CELL_SIZE;
int cell_y = BOARD_Y + row * CELL_SIZE;
int center_x = cell_x + CELL_SIZE / 2;
int center_y = cell_y + CELL_SIZE / 2;
// Highlight selected cell (for button navigation)
if (!state->game_over && row == state->selected_row && col == state->selected_col) {
renderer->draw_rectangle(cell_x + 5, cell_y + 5, CELL_SIZE - 10, CELL_SIZE - 10, true, 1);
}
if (state->board[row][col] == 1) {
// Draw X
int offset = CELL_SIZE / 4;
renderer->draw_line(center_x - offset, center_y - offset,
center_x + offset, center_y + offset, true, 3);
renderer->draw_line(center_x + offset, center_y - offset,
center_x - offset, center_y + offset, true, 3);
} else if (state->board[row][col] == 2) {
// Draw O
int radius = CELL_SIZE / 4;
renderer->draw_circle(center_x, center_y, radius, true);
renderer->draw_circle(center_x, center_y, radius - 1, true);
renderer->draw_circle(center_x, center_y, radius - 2, true);
}
}
}
// Draw statistics at bottom
char stats[60];
snprintf(stats, sizeof(stats), "X:%d O:%d Tie:%d Moves:%d",
state->x_wins, state->o_wins, state->ties, state->total_moves);
renderer->draw_string(20, V_HEIGHT - 40, stats, true);
} }
// ============================================================================ // ============================================================================
@@ -534,6 +780,7 @@ int main()
sleep_ms(5000); // Wait for USB connection (if present) sleep_ms(5000); // Wait for USB connection (if present)
printf("\n=== %s Demo ===\n", BOARD_NAME); printf("\n=== %s Demo ===\n", BOARD_NAME);
printf("Starting dual-core system...\n");
// Create display abstraction using factory method // Create display abstraction using factory method
// The factory handles all board-specific configuration internally // The factory handles all board-specific configuration internally
@@ -544,7 +791,7 @@ int main()
return -1; return -1;
} }
printf("Initializing 4.0\" TFT with Touch and SD Card...\n"); printf("Initializing display...\n");
// Initialize the display // Initialize the display
if (!display->init()) { if (!display->init()) {
@@ -553,6 +800,11 @@ int main()
return -1; 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) // Do a full refresh with white screen first (removes ghosting on e-paper)
printf("Performing initial full refresh to white...\n"); printf("Performing initial full refresh to white...\n");
display->clear(true); // Clear to white display->clear(true); // Clear to white
@@ -577,7 +829,7 @@ int main()
// Initialize game configuration // Initialize game configuration
GameConfig config = { GameConfig config = {
.touch_debounce_ms = 10, .touch_debounce_ms = 10,
.button_debounce_ms = 50, .button_debounce_ms = 20,
.enable_gestures = true, .enable_gestures = true,
.enable_continuous_draw = true, .enable_continuous_draw = true,
.debug_verbose = false .debug_verbose = false
@@ -585,13 +837,18 @@ int main()
// Initialize game state // Initialize game state
GameState game_state; GameState game_state;
// Initialize statistics (persists across game restarts)
game_state.x_wins = 0;
game_state.o_wins = 0;
game_state.ties = 0;
game_init(&game_state); game_init(&game_state);
// Draw initial game graphics // Draw initial game graphics
game_draw(&game_state, &renderer, &gui); game_draw(&game_state, &renderer, &gui);
// Refresh the screen with the rendered GUI // Refresh the screen with the rendered GUI (async on Core 1)
refresh_screen(bit_buffer, display); refresh_screen_async(bit_buffer, display);
printf("Initial screen refresh queued on Core 1\n");
// Initialize touch screen using abstraction // Initialize touch screen using abstraction
touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT, touch = LowLevelTouch::create((TouchType)TOUCH_TYPE_SELECTED, V_WIDTH, V_HEIGHT,
@@ -652,16 +909,23 @@ int main()
// } // }
// ======================================================================== // ========================================================================
// REACTIVE GAME LOOP // 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: // The loop sleeps until an interrupt occurs, then:
// 1. Process input (button or touch) // 1. Process input (button or touch)
// 2. Update game state based on input // 2. Update game state based on input
// 3. Redraw only if game_update() indicates changes occurred // 3. Queue refresh on Core 1 (non-blocking)
// This is ideal for e-ink displays (minimal refreshes) and power efficiency // This keeps Core 0 responsive even during slow e-ink refreshes
// ======================================================================== // ========================================================================
uint32_t last_touch_time = 0; 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");
while (1) { while (1) {
// Sleep until interrupt wakes us up (very power efficient!) // Sleep until interrupt wakes us up (very power efficient!)
@@ -684,15 +948,25 @@ int main()
} }
} }
// 3. Redraw and refresh screen only if needed // 3. Redraw and queue async refresh on Core 1
if (needs_refresh) { if (needs_refresh || pending_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 // Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
game_draw(&game_state, &renderer, &gui); game_draw(&game_state, &renderer, &gui);
// 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");
} }
refresh_screen(bit_buffer, display); }
// Core 0 continues immediately, Core 1 handles the refresh
} }
} }

View File

@@ -16,9 +16,9 @@
// ============================================================================ // ============================================================================
// ---- SELECT YOUR BOARD HERE ---- // ---- SELECT YOUR BOARD HERE ----
// #define BOARD_FEATHER_TFT // Feather RP2350 + 4.0" TFT ST7796 #define BOARD_FEATHER_TFT // Feather RP2350 + 4.0" TFT ST7796
// #define BOARD_PICO2_TFT // Pico 2 + 4.0" TFT ST7796 // #define BOARD_PICO2_TFT // Pico 2 + 4.0" TFT ST7796
#define BOARD_PICO2_EINK // Pico 2 + E-Ink Display //#define BOARD_PICO2_EINK // Pico 2 + E-Ink Display
// -------------------------------- // --------------------------------
// ============================================================================ // ============================================================================

View File

@@ -1,15 +1,19 @@
#include "low_level_display_st7789.h" #include "low_level_display_st7789.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
// Note: This is a placeholder implementation // Note: This is a placeholder implementation
// You'll need to add the actual ST7789 driver code to lib/st7789/ // You'll need to add the actual ST7789 driver code to lib/st7789/
LowLevelDisplayST7789::LowLevelDisplayST7789(const st7789_config* cfg, int w, int h) LowLevelDisplayST7789::LowLevelDisplayST7789(const st7789_config* cfg, int w, int h)
: config(cfg), width(w), height(h), initialized(false) { : config(cfg), width(w), height(h), initialized(false), rgb_buffer(nullptr) {
} }
LowLevelDisplayST7789::~LowLevelDisplayST7789() { LowLevelDisplayST7789::~LowLevelDisplayST7789() {
// Cleanup if needed if (rgb_buffer) {
free(rgb_buffer);
rgb_buffer = nullptr;
}
} }
bool LowLevelDisplayST7789::init() { bool LowLevelDisplayST7789::init() {
@@ -19,6 +23,15 @@ bool LowLevelDisplayST7789::init() {
// TODO: Implement ST7789 initialization // TODO: Implement ST7789 initialization
// st7789_init(config, width, height); // st7789_init(config, width, height);
//
// TODO: Allocate RGB565 buffer once (reused for all draw operations)
// size_t buffer_size = width * height * sizeof(uint16_t);
// rgb_buffer = (uint16_t *)malloc(buffer_size);
// if (!rgb_buffer) {
// printf("Error: Failed to allocate %zu bytes for RGB buffer\n", buffer_size);
// return false;
// }
printf("ST7789 display initialized: %dx%d (stub)\n", width, height); printf("ST7789 display initialized: %dx%d (stub)\n", width, height);
initialized = true; initialized = true;
return false; // Return false until actual driver is implemented return false; // Return false until actual driver is implemented
@@ -36,6 +49,18 @@ void LowLevelDisplayST7789::draw_pixel(int x, int y, bool white) {
void LowLevelDisplayST7789::draw_buffer(const uint8_t* bit_buffer) { void LowLevelDisplayST7789::draw_buffer(const uint8_t* bit_buffer) {
// TODO: Implement - convert 1-bit to RGB565 and write // TODO: Implement - convert 1-bit to RGB565 and write
// IMPORTANT: Use the persistent rgb_buffer member, NOT malloc/free!
// Example:
// if (!bit_buffer || !rgb_buffer) return;
// for (int y = 0; y < height; y++) {
// for (int x = 0; x < width; x++) {
// int byte_index = (y * width + x) / 8;
// int bit_index = 7 - (x % 8);
// bool pixel_white = (bit_buffer[byte_index] >> bit_index) & 0x01;
// rgb_buffer[y * width + x] = pixel_white ? COLOR_WHITE : COLOR_BLACK;
// }
// }
// st7789_write(rgb_buffer, width * height);
(void)bit_buffer; (void)bit_buffer;
} }

View File

@@ -20,6 +20,7 @@ private:
int width; int width;
int height; int height;
bool initialized; bool initialized;
uint16_t* rgb_buffer; // Persistent buffer for 1-bit to RGB565 conversion
public: public:
LowLevelDisplayST7789(const st7789_config* cfg, int w, int h); LowLevelDisplayST7789(const st7789_config* cfg, int w, int h);

View File

@@ -7,11 +7,14 @@
#define COLOR_WHITE 0xFFFF #define COLOR_WHITE 0xFFFF
LowLevelDisplayST7796::LowLevelDisplayST7796(const st7796_config* cfg, int w, int h) LowLevelDisplayST7796::LowLevelDisplayST7796(const st7796_config* cfg, int w, int h)
: config(cfg), width(w), height(h), initialized(false) { : config(cfg), width(w), height(h), initialized(false), rgb_buffer(nullptr) {
} }
LowLevelDisplayST7796::~LowLevelDisplayST7796() { LowLevelDisplayST7796::~LowLevelDisplayST7796() {
// Cleanup if needed if (rgb_buffer) {
free(rgb_buffer);
rgb_buffer = nullptr;
}
} }
bool LowLevelDisplayST7796::init() { bool LowLevelDisplayST7796::init() {
@@ -20,8 +23,17 @@ bool LowLevelDisplayST7796::init() {
} }
st7796_init(config, width, height); st7796_init(config, width, height);
// Allocate RGB565 buffer once (reused for all draw operations)
size_t buffer_size = width * height * sizeof(uint16_t);
rgb_buffer = (uint16_t *)malloc(buffer_size);
if (!rgb_buffer) {
printf("Error: Failed to allocate %zu bytes for RGB buffer\n", buffer_size);
return false;
}
printf("ST7796 display initialized: %dx%d (RGB buffer: %zu bytes)\n", width, height, buffer_size);
initialized = true; initialized = true;
printf("ST7796 display initialized: %dx%d\n", width, height);
return true; return true;
} }
@@ -34,16 +46,9 @@ void LowLevelDisplayST7796::draw_pixel(int x, int y, bool white) {
} }
void LowLevelDisplayST7796::draw_buffer(const uint8_t* bit_buffer) { void LowLevelDisplayST7796::draw_buffer(const uint8_t* bit_buffer) {
if (!bit_buffer) return; if (!bit_buffer || !rgb_buffer) return;
// Allocate RGB565 buffer for entire screen // Convert 1-bit buffer to RGB565 using persistent buffer
uint16_t *rgb_buffer = (uint16_t *)malloc(width * height * sizeof(uint16_t));
if (!rgb_buffer) {
printf("Error: Failed to allocate RGB buffer\n");
return;
}
// Convert 1-bit buffer to RGB565
for (int y = 0; y < height; y++) { for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
int byte_index = (y * width + x) / 8; int byte_index = (y * width + x) / 8;
@@ -56,8 +61,6 @@ void LowLevelDisplayST7796::draw_buffer(const uint8_t* bit_buffer) {
// Draw entire buffer at once // Draw entire buffer at once
st7796_set_cursor(0, 0); st7796_set_cursor(0, 0);
st7796_write(rgb_buffer, width * height); st7796_write(rgb_buffer, width * height);
free(rgb_buffer);
} }
void LowLevelDisplayST7796::refresh() { void LowLevelDisplayST7796::refresh() {

View File

@@ -10,6 +10,7 @@ private:
int width; int width;
int height; int height;
bool initialized; bool initialized;
uint16_t* rgb_buffer; // Persistent buffer for 1-bit to RGB565 conversion
public: public:
LowLevelDisplayST7796(const st7796_config* cfg, int w, int h); LowLevelDisplayST7796(const st7796_config* cfg, int w, int h);