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>
374 lines
9.2 KiB
Markdown
374 lines
9.2 KiB
Markdown
# 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:**
|
|
|
|
```cpp
|
|
// 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:
|
|
|
|
```cpp
|
|
// 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:
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
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):
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
// BAD - Will fail or be unreliable
|
|
f_open(&fil, "/games/test.lua", FA_WRITE);
|
|
```
|
|
|
|
### ✅ DO: Always Switch Speeds
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// BAD
|
|
f_write(&fil, buffer, size, &bytes_written);
|
|
f_close(&fil);
|
|
```
|
|
|
|
### ✅ DO: Check Every Return Value
|
|
|
|
```cpp
|
|
// 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
|
|
|
|
```cpp
|
|
// BAD - Memory leak and Lua state conflicts
|
|
selected_game = new LuaGame(...);
|
|
```
|
|
|
|
### ✅ DO: Clean Up First
|
|
|
|
```cpp
|
|
// 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:
|
|
|
|
```cpp
|
|
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!
|