Add Core1 refresh recovery and shared SPI arbitration

This commit is contained in:
Adolfo Reyna
2026-02-18 15:43:35 -05:00
parent ebc58d7e4d
commit 3e54466752
5 changed files with 188 additions and 32 deletions

View File

@@ -59,6 +59,7 @@ extern "C" {
#include "lua_game_loader.h"
#include "serial_uploader.h"
#include "scene_stack.h"
#include "shared_spi_bus.h"
// Binary info for RP2350 - ensures proper boot image structure
@@ -75,6 +76,28 @@ volatile bool refresh_requested = false;
volatile bool refresh_in_progress = false;
const uint8_t* volatile refresh_buffer = nullptr;
LowLevelDisplay* volatile refresh_display = nullptr;
volatile uint32_t core1_heartbeat = 0;
void core1_entry();
static void restart_core1_refresh_worker() {
printf("Attempting Core1 restart...\n");
// Stop Core1 and clear in-flight refresh state.
multicore_reset_core1();
refresh_requested = false;
refresh_in_progress = false;
refresh_buffer = nullptr;
refresh_display = nullptr;
core1_heartbeat = 0;
// Recover shared SPI lock state after hard core reset.
shared_spi_bus_force_recover();
multicore_launch_core1(core1_entry);
sleep_ms(20);
printf("Core1 restart complete\n");
}
/**
* @brief Core 1 entry point - handles display refresh operations
@@ -86,19 +109,29 @@ void core1_entry() {
printf("Core 1 started - handling display refreshes\n");
while (1) {
core1_heartbeat++;
// Wait for refresh request
if (refresh_requested && refresh_buffer && refresh_display) {
// refresh_in_progress is already set by Core 0 to lock the buffer
// 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
if (refresh_requested) {
if (refresh_buffer && refresh_display) {
// refresh_in_progress is already set by Core 0 to lock the buffer
// Get local copies for safe access
LowLevelDisplay* display = refresh_display;
const uint8_t* buffer = refresh_buffer;
// Perform refresh with shared SPI bus lock to avoid SD/display collisions.
shared_spi_bus_lock();
display->draw_buffer(buffer);
display->refresh();
shared_spi_bus_unlock();
} else {
// Recovery guard: never leave Core 0 stuck waiting forever if a
// malformed/partial request is observed across cores.
printf("Core1: dropped malformed refresh request\n");
}
// Clear flags in all cases to avoid deadlock on Core 0.
refresh_requested = false;
refresh_in_progress = false; // Unlock buffer for Core 0
}
@@ -466,6 +499,9 @@ int main()
printf("\n=== %s Demo ===\n", BOARD_NAME);
printf("Starting dual-core system...\n");
// Initialize shared SPI lock before SD/display operations start.
shared_spi_bus_init();
// Create display abstraction using factory method
// The factory handles all board-specific configuration internally
@@ -485,11 +521,11 @@ int main()
return -1;
}
// Enable dirty rectangle optimization for ST7796 displays
// Enable dirty/partial refresh optimization for ST7796.
if (display->get_type() == DISPLAY_TYPE_ST7796) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
st7796_display->enable_dirty_rect(true);
printf("Dirty rectangle optimization enabled (4 quadrants: TL/TR/BL/BR split)\n");
printf("Dirty rectangle optimization enabled\n");
}
// Launch Core 1 for display refresh handling
@@ -607,7 +643,7 @@ int main()
// Draw launcher menu
launcher.draw();
// Refresh the screen with the launcher menu (async on Core 1)
// Initial refresh queued on Core 1 (async for all display types in this test mode).
refresh_screen_async(bit_buffer, display);
printf("Initial screen refresh queued on Core 1\n");
@@ -698,6 +734,10 @@ int main()
bool needs_refresh = false; // Track if screen needs redraw
bool dirty_rect_opt_state = (display->get_type() == DISPLAY_TYPE_ST7796);
SceneStack scene_stack;
bool force_sync_tft_refresh = false;
bool core1_restart_attempted = false;
uint32_t last_seen_core1_heartbeat = core1_heartbeat;
uint32_t last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time());
bool swipe_candidate_active = false;
int16_t swipe_start_x = 0;
int16_t swipe_start_y = 0;
@@ -705,6 +745,33 @@ int main()
int16_t swipe_last_y = 0;
while (1) {
// Core1 liveness watchdog:
// If refresh work is pending/in-progress but Core1 heartbeat stops advancing,
// fall back to synchronous TFT refresh on Core0 to avoid frozen UI.
uint32_t hb_now = core1_heartbeat;
uint32_t now_ms = to_ms_since_boot(get_absolute_time());
if (hb_now != last_seen_core1_heartbeat) {
last_seen_core1_heartbeat = hb_now;
last_core1_heartbeat_ms = now_ms;
core1_restart_attempted = false;
} else if (!force_sync_tft_refresh &&
(refresh_in_progress || pending_refresh || needs_refresh) &&
(now_ms - last_core1_heartbeat_ms) > 500) {
if (!core1_restart_attempted) {
core1_restart_attempted = true;
restart_core1_refresh_worker();
last_seen_core1_heartbeat = core1_heartbeat;
last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time());
pending_refresh = true;
} else {
force_sync_tft_refresh = true;
refresh_requested = false;
refresh_in_progress = false;
pending_refresh = true;
printf("Core1 heartbeat stalled after restart; switching TFT refresh to synchronous fallback\n");
}
}
// 0. Process serial uploads (for rapid game iteration)
serial_uploader.process(is_refresh_in_progress());
@@ -933,15 +1000,9 @@ int main()
if (time_since_last_frame >= TARGET_FRAME_TIME_MS) {
// Only draw if Core 1 is finished with the buffer
if (!is_refresh_in_progress()) {
// Update dirty rectangle optimization based on continuous updates
// Keep dirty rectangle optimization enabled for TFT.
if (display->get_type() == DISPLAY_TYPE_ST7796) {
bool wants_opt = false;
if (scene_stack.is(SceneId::GAME)) {
Game* g = launcher.get_selected_game();
if (g && g->wants_frame_updates()) {
wants_opt = true;
}
}
bool wants_opt = true;
if (dirty_rect_opt_state != wants_opt) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
@@ -979,16 +1040,13 @@ int main()
}
}
// TFT fix:
// Run refresh synchronously on Core 0 for ST7796/ST7789.
// We observed intermittent frozen screens with async Core 1 refresh
// after SD/Lua activity even though scene/game logic continued.
// E-paper keeps async refresh because its refresh latency is high.
bool refresh_started = false;
if (display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789) {
if (force_sync_tft_refresh &&
(display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789)) {
refresh_screen(bit_buffer, display);
refresh_started = true;
} else {
// Async refresh test path.
refresh_started = refresh_screen_async(bit_buffer, display);
}