Fix SPI bus contention crash on serial game upload

Serial uploader was crashing the Pico when launching games because
it accessed SD card (SPI) while Core 1 was refreshing display (also SPI).
Display and SD card share the same SPI bus and cannot be accessed
simultaneously.

Split game launch into prepare and execute phases:
- prepare: Re-scan games directory (safe, SD access done immediately)
- execute: Load Lua script from SD (deferred until display is idle)

Main loop now checks !is_refresh_in_progress() before completing
launch, preventing SPI conflicts.

Also updated SD card best practices skill to document SPI bus
contention as the #1 most critical issue to avoid.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adolfo Reyna
2026-02-12 23:25:31 -05:00
parent f8fb04db1b
commit 518bc054c4
5 changed files with 158 additions and 24 deletions

View File

@@ -2,13 +2,99 @@
This document captures critical best practices for working with SD card operations in this project, based on lessons learned during development.
## 1. SPI Speed Management
## 1. SPI Bus Contention (CRITICAL)
### ⚠️ CRITICAL: Display and SD Card Share the Same SPI Bus
**The display and SD card use the same SPI bus and CANNOT be accessed simultaneously.** Attempting to do so will cause the Pico to crash or behave unpredictably.
### Real-World Example: Game Launch Crash
The serial uploader originally crashed when launching games because:
1. `SerialUploader::launch_game()` writes file to SD (SPI)
2. Immediately calls `select_game_by_name()`
3. `LuaGame::load_script()` reads from SD (SPI)
4. **Meanwhile**, Core 1 is refreshing the display (also SPI)
5. **CRASH** due to simultaneous SPI access
### Solution: Wait for Display to Be Idle
Before any SD card operation that isn't already protected, ensure no display refresh is in progress:
```cpp
// In main loop:
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
// Safe to launch now - no SPI conflict with display
bool game_launched = serial_uploader.complete_launch();
// ...
}
```
### Key Patterns for Avoiding Contention
**Pattern 1: Check `is_refresh_in_progress()` before SD operations**
```cpp
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 2: Split operations into "prepare" and "execute" phases**
```cpp
// Phase 1: Prepare (safe, no SD access)
void prepare_operation() {
state = READY_TO_EXECUTE;
}
// Phase 2: Execute (only when !is_refresh_in_progress())
bool execute_operation() {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations here
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 3: Keep main loop responsive**
```cpp
while (1) {
// Check if we need to do SD operation
if (needs_sd_operation && !is_refresh_in_progress()) {
perform_sd_operation();
}
// Don't sleep if waiting for SD operation window
bool stay_awake = pending_refresh || needs_sd_operation;
if (!stay_awake) {
__wfi(); // Sleep until interrupt
}
}
```
### When This Matters Most
- **Game loading**: Reading Lua scripts from SD during game launch
- **Serial uploads**: Writing files and immediately loading them
- **Save/load operations**: Writing game state to SD
- **Directory scanning**: Re-scanning games while display is active
### The Core Architecture
This project uses **dual-core display refresh**:
- **Core 0**: Main logic, input processing, game updates, SD card operations
- **Core 1**: Display refresh (writes framebuffer to display via SPI)
Core 1 runs asynchronously, so you must explicitly check `is_refresh_in_progress()` before SD operations.
## 2. SPI Speed Management
### Critical Rule: Always Set SD Card Speed Before Operations
The display and SD card share the same SPI bus but operate at different speeds:
- **Display**: 32 MHz
- **SD Card**: 12.5 MHz
- **Display**: 32 MHz (fast)
- **SD Card**: 12.5 MHz (slower, more reliable)
**ALWAYS wrap SD card operations with speed switching:**
@@ -220,6 +306,29 @@ This isolates directory-related issues.
## 6. Common Pitfalls
### ❌ DON'T: Access SD Card During Display Refresh (MOST CRITICAL)
```cpp
// BAD - Will crash the Pico!
void launch_game() {
scan_games(); // Reads SD card
selected_game = create_game(); // Reads Lua script from SD
}
// Called directly without checking if display is refreshing
```
### ✅ DO: Wait for Display to Be Idle
```cpp
// GOOD - Wait for safe window
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
scan_games();
selected_game = create_game();
sd_card_restore_spi_speed(prev_speed);
}
```
### ❌ DON'T: Forget SPI Speed Management
```cpp
@@ -346,10 +455,11 @@ bool SerialUploader::write_file_to_sd() {
}
```
## 8. Testing Checklist
## 9. Testing Checklist
When implementing new SD card functionality:
- [ ] **SPI bus contention checked** - verify `!is_refresh_in_progress()` before SD operations
- [ ] SPI speed switching is in place
- [ ] All FatFS return codes are checked
- [ ] Files are properly closed after operations
@@ -363,11 +473,12 @@ When implementing new SD card functionality:
## Summary
The most important rules:
1. **Always manage SPI speed** around FatFS operations
2. **Poll for SD card responses** - don't assume immediate response
3. **Check error codes** on every operation
4. **Clean up memory** before creating new game instances
5. **Write in chunks** for large files
6. **Sync before closing** to ensure data is written
1. **⚠️ CRITICAL: Avoid SPI bus contention** - Check `!is_refresh_in_progress()` before SD operations (display and SD share SPI bus)
2. **Always manage SPI speed** around FatFS operations
3. **Poll for SD card responses** - don't assume immediate response
4. **Check error codes** on every operation
5. **Clean up memory** before creating new game instances
6. **Write in chunks** for large files
7. **Sync before closing** to ensure data is written
Following these practices will save hours of debugging SD card issues!