Files
basic1/.claude/skills/sd_card_best_practices.md
Adolfo Reyna 84b009c33e Add serial upload tool for rapid Lua game iteration
Implements a complete serial upload workflow that allows uploading and
immediately testing Lua games via USB serial connection.

New Components:
- SerialUploader: Receives files via serial, writes to SD card
- upload_game.py: Python tool for sending files from host computer
- Protocol: Text-based with base64 encoding for reliability

Key Features:
- Uploads file to /games folder on SD card
- Overwrites existing files (FA_CREATE_ALWAYS)
- Auto-launches uploaded game immediately
- Proper memory cleanup (prevents Lua state conflicts)

SD Card Fixes:
- Fixed SPI speed management (12.5MHz for SD, 32MHz for display)
- Fixed SD write protocol (poll for data response token)
- Added speed switching wrappers around all FatFS operations
- Cleaned up excessive debug output

Game Launcher Improvements:
- Added clear_games() to prevent duplicate registrations
- Added cleanup in select_game_by_name() to delete old instances
- Added exact match priority in game selection
- LuaGameLoader now has clear_factory_data() for memory cleanup

Integration:
- Added serial_uploader to CMakeLists.txt
- Integrated into main loop in basic1.cpp
- Re-scans games after upload to pick up new files

Documentation:
- UPLOAD_TOOL.md: Usage instructions
- sd_card_best_practices.md: Critical lessons learned

Known Issues:
- Game launch after upload occasionally causes freeze (needs investigation)
- Display may not refresh properly after upload

Usage:
  python upload_game.py games/lua_examples/2048.lua /dev/tty.usbmodem101

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 22:52:57 -05:00

9.2 KiB

SD Card Best Practices for RP2350 + FatFS

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

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

ALWAYS wrap SD card operations with speed switching:

// Save current speed and switch to SD card speed
uint prev_speed = sd_card_set_spi_speed();

// ... SD card operations here ...
// f_open(), f_write(), f_read(), f_readdir(), etc.

// Restore previous speed for display
sd_card_restore_spi_speed(prev_speed);

Why This Matters

Running SD card operations at the wrong SPI speed causes:

  • Unreliable reads/writes
  • Corrupted data
  • FR_DISK_ERR errors from FatFS
  • Hardware-level protocol failures (0xFF data responses)

Where Speed Switching is Already Handled

These functions handle SPI speed internally (you don't need to wrap them):

  • LuaGameLoader::register_all_games()
  • All functions in sd_card.c (low-level operations)

Where You MUST Handle Speed Switching

Any code that calls FatFS functions directly:

  • f_open(), f_close()
  • f_read(), f_write()
  • f_opendir(), f_readdir(), f_closedir()
  • f_stat(), f_mkdir(), f_unlink()
  • f_getfree(), f_sync()

2. SD Card Write Protocol

The Data Response Polling Issue

When writing to SD card with CMD24 (write single block), the data response token may not arrive immediately. You must poll for it:

// After sending data block and CRC:
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
    response = sd_card_transfer(0xFF);
    if (response != 0xFF) {
        break;  // Got the response
    }
}

// Check if data was accepted
if ((response & 0x1F) != SD_DATA_ACCEPTED) {
    // Write failed
}

Why: The SD card may need a few clock cycles before sending the data response token. Reading only once may return 0xFF (no response yet).

Wait for Card Ready After CMD24

After sending the write command and before sending data:

// After CMD24 command:
uint8_t ready_byte;
do {
    ready_byte = sd_card_transfer(0xFF);
    timeout_count++;
    if (timeout_count > 1000) {
        return false;  // Timeout
    }
} while (ready_byte != 0xFF);

This ensures the card is ready to receive the data block.

3. FatFS Best Practices

Always Check Return Codes

FRESULT fr = f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
    printf("ERROR: f_open failed: %d\n", fr);
    // Clean up and return
    return false;
}

Common FatFS error codes:

  • FR_OK (0): Success
  • FR_DISK_ERR (1): Low-level disk error (often SPI speed issue)
  • FR_NOT_READY (3): Card not initialized
  • FR_NO_FILE (4): File not found
  • FR_NO_PATH (5): Path not found
  • FR_EXIST (8): File/directory already exists

Use FA_CREATE_ALWAYS to Overwrite

For rapid iteration (like our serial uploader):

f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);

This overwrites existing files, perfect for development.

Write in Chunks for Large Files

For files larger than 512 bytes, write in chunks:

const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;

while (total_written < total_size) {
    uint32_t chunk_size = min(CHUNK_SIZE, total_size - total_written);
    UINT bytes_written;

    fr = f_write(&fil, buffer + total_written, chunk_size, &bytes_written);
    if (fr != FR_OK || bytes_written != chunk_size) {
        // Handle error
        break;
    }

    total_written += bytes_written;
}

Always Sync and Close

f_sync(&fil);  // Ensure data is written to card
f_close(&fil); // Close file and update directory

Skipping f_sync() can lead to data loss if power is lost.

4. Memory Management

Clean Up After Re-scanning

When re-scanning games (like after upload), clean up old data:

// Clear game launcher entries
game_launcher->clear_games();

// Clear Lua game factory data
LuaGameLoader::clear_factory_data();

// Re-scan
LuaGameLoader::register_all_games(game_launcher);

Why: Without cleanup, you get duplicate registrations and memory leaks.

Delete Old Game Instances Before Creating New Ones

if (selected_game) {
    delete selected_game;
    selected_game = nullptr;
}

// Now create new game
selected_game = factory(width, height, renderer, gui, input_manager);

Critical for Lua games: Each LuaGame has a Lua state. Not cleaning up the old one before creating a new one causes conflicts and freezes.

5. Debugging Tips

Add Targeted Debug Output

When debugging SD operations, add prints at key points:

printf("✓ Wrote %u bytes to %s\n", bytes_written, filepath);

But avoid spamming the console - it slows down operations significantly.

Check Hardware Layer First

If FatFS returns FR_DISK_ERR, the issue is usually at the hardware level:

  1. Check SPI speed (most common issue)
  2. Check SD card write protection
  3. Check physical SD card connection
  4. Verify SD card is properly initialized

Use Root Directory for Testing

When debugging writes, test with root directory first:

FIL test_file;
if (f_open(&test_file, "/test.txt", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) {
    printf("Root write OK\n");
    f_close(&test_file);
    f_unlink("/test.txt");
}

This isolates directory-related issues.

6. Common Pitfalls

DON'T: Forget SPI Speed Management

// BAD - Will fail or be unreliable
f_open(&fil, "/games/test.lua", FA_WRITE);

DO: Always Switch Speeds

// GOOD
uint prev_speed = sd_card_set_spi_speed();
f_open(&fil, "/games/test.lua", FA_WRITE);
// ... operations ...
sd_card_restore_spi_speed(prev_speed);

DON'T: Assume Immediate Data Response

// BAD - May get 0xFF (no response yet)
uint8_t response = sd_card_transfer(0xFF);
if (response != 0x05) {
    // Might incorrectly fail
}

DO: Poll for Data Response

// GOOD - Poll until response arrives
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
    response = sd_card_transfer(0xFF);
    if (response != 0xFF) break;
}

DON'T: Skip Error Checking

// BAD
f_write(&fil, buffer, size, &bytes_written);
f_close(&fil);

DO: Check Every Return Value

// GOOD
if (f_write(&fil, buffer, size, &bytes_written) != FR_OK) {
    printf("Write failed\n");
    f_close(&fil);
    return false;
}

DON'T: Create New Games Without Cleanup

// BAD - Memory leak and Lua state conflicts
selected_game = new LuaGame(...);

DO: Clean Up First

// GOOD
if (selected_game) {
    delete selected_game;
    selected_game = nullptr;
}
selected_game = new LuaGame(...);

7. Serial Upload Pattern

The serial uploader demonstrates the complete pattern:

bool SerialUploader::write_file_to_sd() {
    // 1. Validate input
    if (!file_buffer || bytes_received == 0) return false;

    // 2. Set SD card SPI speed
    uint prev_speed = sd_card_set_spi_speed();

    // 3. Ensure directory exists
    f_mkdir("/games");

    // 4. Open file (overwrite mode for iteration)
    FIL fil;
    FRESULT fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE);
    if (fr != FR_OK) {
        sd_card_restore_spi_speed(prev_speed);
        return false;
    }

    // 5. Write in chunks
    const uint32_t CHUNK_SIZE = 512;
    uint32_t total_written = 0;
    while (total_written < bytes_received) {
        uint32_t chunk = min(CHUNK_SIZE, bytes_received - total_written);
        UINT written;

        fr = f_write(&fil, file_buffer + total_written, chunk, &written);
        if (fr != FR_OK || written != chunk) {
            f_close(&fil);
            sd_card_restore_spi_speed(prev_speed);
            return false;
        }

        total_written += written;
    }

    // 6. Sync and close
    f_sync(&fil);
    f_close(&fil);

    // 7. Restore display SPI speed
    sd_card_restore_spi_speed(prev_speed);

    return true;
}

8. Testing Checklist

When implementing new SD card functionality:

  • SPI speed switching is in place
  • All FatFS return codes are checked
  • Files are properly closed after operations
  • Memory is cleaned up (no leaks)
  • Error messages are informative
  • Tested with both small and large files
  • Tested overwriting existing files
  • Tested with non-existent directories
  • Verified data integrity (read back after write)

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

Following these practices will save hours of debugging SD card issues!