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>
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_ERRerrors 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): SuccessFR_DISK_ERR (1): Low-level disk error (often SPI speed issue)FR_NOT_READY (3): Card not initializedFR_NO_FILE (4): File not foundFR_NO_PATH (5): Path not foundFR_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:
- Check SPI speed (most common issue)
- Check SD card write protection
- Check physical SD card connection
- 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:
- Always manage SPI speed around FatFS operations
- Poll for SD card responses - don't assume immediate response
- Check error codes on every operation
- Clean up memory before creating new game instances
- Write in chunks for large files
- Sync before closing to ensure data is written
Following these practices will save hours of debugging SD card issues!