diff --git a/.claude/skills/sd_card_best_practices.md b/.claude/skills/sd_card_best_practices.md new file mode 100644 index 0000000..a98e455 --- /dev/null +++ b/.claude/skills/sd_card_best_practices.md @@ -0,0 +1,373 @@ +# 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! diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a424f3..86fa4cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,10 +44,11 @@ file(GLOB LUA_SOURCES "${CMAKE_CURRENT_LIST_DIR}/lib/lua/*.c" ) -add_executable(basic1 - basic1.cpp +add_executable(basic1 + basic1.cpp lib/input_manager.cpp lib/game_launcher.cpp + lib/serial_uploader.cpp games/tic_tac_toe.cpp games/demo_game.cpp games/monopoly/monopoly_game.cpp diff --git a/UPLOAD_TOOL.md b/UPLOAD_TOOL.md new file mode 100644 index 0000000..c960fbd --- /dev/null +++ b/UPLOAD_TOOL.md @@ -0,0 +1,151 @@ +# Lua Game Upload Tool + +Rapidly upload and execute Lua games on your RP2350 via USB serial for quick iteration during development! + +## Features + +- Upload Lua game files directly to the SD card via USB serial +- Automatically launches the uploaded game immediately +- No need to manually swap SD cards or restart the device +- Perfect for rapid game development and testing + +## Requirements + +### Computer Side +- Python 3.x +- pyserial library: `pip install pyserial` + +### RP2350 Side +- Firmware must be built with serial uploader support (already included) +- SD card inserted and formatted (FAT32) +- USB connection to computer + +## Usage + +### 1. Connect Your Device + +Connect your RP2350 to your computer via USB. The device will appear as a serial port: +- **Linux/Mac**: Usually `/dev/ttyACM0` or `/dev/ttyUSB0` +- **Windows**: Usually `COM3`, `COM4`, etc. + +### 2. Upload a Lua Game + +Basic usage: +```bash +python upload_game.py my_game.lua +``` + +Specify a custom serial port: +```bash +python upload_game.py my_game.lua /dev/ttyACM0 # Linux/Mac +python upload_game.py my_game.lua COM3 # Windows +``` + +The script will: +1. Read your Lua file +2. Upload it to the RP2350 via serial +3. Save it to `/games/.lua` on the SD card +4. Automatically launch the game! + +### 3. View Available Serial Ports + +Run the script without arguments to see available ports: +```bash +python upload_game.py +``` + +## Example Workflow + +```bash +# Edit your game +vim snake.lua + +# Upload and test (the game starts immediately!) +python upload_game.py snake.lua + +# Make changes +vim snake.lua + +# Upload again (instantly replaces and restarts) +python upload_game.py snake.lua +``` + +## Protocol Details + +The tool uses a simple text-based protocol over USB serial: + +``` +UPLOAD + +END +``` + +**Response:** +``` +OK +LAUNCHED +``` + +## Troubleshooting + +### "No serial port found" +- Make sure your device is connected via USB +- Check if the device appears in your system (use `ls /dev/tty*` on Linux/Mac) +- On Linux, you may need to add your user to the `dialout` group: `sudo usermod -a -G dialout $USER` + +### "Permission denied" +On Linux/Mac, you may need permissions to access the serial port: +```bash +sudo chmod 666 /dev/ttyACM0 # Quick fix +# OR +sudo usermod -a -G dialout $USER && newgrp dialout # Permanent fix +``` + +### "Upload failed" or "ERROR" messages +- Make sure the SD card is properly inserted and formatted (FAT32) +- Check that there's enough space on the SD card +- Verify the Lua file is valid (syntax errors will appear when game launches) + +### Game doesn't appear or launch +- Check the serial output from the RP2350 for error messages +- Ensure the Lua file has proper metadata comments at the top: + ```lua + -- NAME: My Game + -- DESCRIPTION: A fun game + ``` + +## Tips for Rapid Development + +1. **Use a serial terminal** alongside uploads to see debug output: + ```bash + screen /dev/ttyACM0 115200 + ``` + +2. **Create a watch script** to auto-upload on file changes: + ```bash + while inotifywait -e modify my_game.lua; do + python upload_game.py my_game.lua + done + ``` + +3. **Use printf() in Lua** for debugging: + ```lua + function update(event) + print("Event type: " .. event.type) + -- your code here + end + ``` + +## File Size Limits + +- Maximum file size: 64 KB +- This should be plenty for most Lua games +- If you need more, consider splitting assets or code + +## Related Files + +- `lib/serial_uploader.h` - C++ header for serial upload handler +- `lib/serial_uploader.cpp` - C++ implementation +- `upload_game.py` - Python upload script + +Happy game development! 🎮 diff --git a/basic1.cpp b/basic1.cpp index 1db4447..4302afd 100644 --- a/basic1.cpp +++ b/basic1.cpp @@ -58,6 +58,7 @@ extern "C" { #include "demo_game.h" #include "monopoly_game.h" #include "lua_game_loader.h" +#include "serial_uploader.h" // Binary info for RP2350 - ensures proper boot image structure @@ -436,7 +437,11 @@ int main() // Create GameLauncher GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager); - + + // Create SerialUploader for rapid game iteration + SerialUploader serial_uploader(&launcher); + printf("Serial uploader initialized\n"); + // Register available games launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { @@ -563,11 +568,22 @@ int main() const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms uint32_t last_frame_time = 0; + bool needs_refresh = false; // Track if screen needs redraw + while (1) { + // 0. Process serial uploads (for rapid game iteration) + bool game_launched_via_serial = serial_uploader.process(); + if (game_launched_via_serial) { + // A new game was uploaded and launched - trigger redraw + needs_refresh = true; + current_game = launcher.get_selected_game(); + // Note: game is already initialized by select_game_by_name() + } + // Determine if we should sleep or stay awake for updates bool stay_awake = false; if (pending_refresh) stay_awake = true; - + if (launcher.is_game_selected()) { Game* g = launcher.get_selected_game(); if (g && g->wants_frame_updates()) { @@ -579,9 +595,8 @@ int main() // Sleep until interrupt wakes us up (very power efficient!) __wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs } - + InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false}; - bool needs_refresh = false; // 1. Process button input first (higher priority) input = input_manager.process_button_input(); diff --git a/diskio_sdcard.c b/diskio_sdcard.c index 79eec45..5809132 100644 --- a/diskio_sdcard.c +++ b/diskio_sdcard.c @@ -6,6 +6,8 @@ #include "diskio.h" #include "sd_card.h" #include +#include +#include /* Definitions of physical drive number for each drive */ #define DEV_SD 0 /* SD card */ @@ -19,9 +21,9 @@ DSTATUS disk_status ( ) { if (pdrv != DEV_SD) return STA_NOINIT; - + // Assume SD card is always initialized after sd_card_init() is called - return 0; // OK + return 0; // OK - not write protected, not removed } /*-----------------------------------------------------------------------*/ @@ -74,13 +76,16 @@ DRESULT disk_write ( ) { if (pdrv != DEV_SD) return RES_PARERR; - + for (UINT i = 0; i < count; i++) { - if (!sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512)))) { + bool result = sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512))); + + if (!result) { + printf("ERROR disk_write failed at sector %lu\n", (unsigned long)(sector + i)); return RES_ERROR; } } - + return RES_OK; } @@ -97,15 +102,15 @@ DRESULT disk_ioctl ( ) { if (pdrv != DEV_SD) return RES_PARERR; - + DRESULT res = RES_ERROR; - + switch (cmd) { case CTRL_SYNC: // Complete pending write process (if needed) res = RES_OK; break; - + case GET_SECTOR_COUNT: // Get number of sectors on the disk (DWORD) { @@ -118,22 +123,27 @@ DRESULT disk_ioctl ( } } break; - + case GET_SECTOR_SIZE: // Get sector size (WORD) *(WORD*)buff = 512; res = RES_OK; break; - + case GET_BLOCK_SIZE: // Get erase block size in unit of sector (DWORD) *(DWORD*)buff = 1; // Single sector erase res = RES_OK; break; - + + case CTRL_TRIM: + // Inform device that data on the block of sectors is no longer used (optional) + res = RES_OK; + break; + default: res = RES_PARERR; } - + return res; } diff --git a/games/lua_examples/2048.lua b/games/lua_examples/2048.lua index 89da5fd..4715eb8 100644 --- a/games/lua_examples/2048.lua +++ b/games/lua_examples/2048.lua @@ -124,6 +124,7 @@ function draw() if state == STATE_MENU then renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2) + renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 30, "Welcome Adolfo2!", true, 2) elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then -- Draw grid @@ -138,14 +139,14 @@ function draw() renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false) else -- Filled tile - renderer.rect(tile_x, tile_y, tile_size, tile_size, true, true) + renderer.rect(tile_x+2, tile_y+2, tile_size-4, tile_size-4, true, true) -- Draw value (simplified) local text = tostring(value) if string.len(text) <= 2 then - renderer.text_scaled(tile_x + 2, tile_y + 2, text, false, 2) + renderer.text_scaled(tile_x + tile_size / 2 - 4, tile_y + tile_size / 2, text, false, 2) else - renderer.text_scaled(tile_x + 1, tile_y + 2, text, false, 2) + renderer.text_scaled(tile_x + tile_size / 2 - 8, tile_y + tile_size / 2, text, false, 2) end end end diff --git a/games/lua_game_loader.cpp b/games/lua_game_loader.cpp index 24cb7fc..44d04b1 100644 --- a/games/lua_game_loader.cpp +++ b/games/lua_game_loader.cpp @@ -23,6 +23,15 @@ struct LuaGameFactoryData { static std::vector factory_data_list; +void LuaGameLoader::clear_factory_data() { + // Delete all factory data and clear the vector + for (LuaGameFactoryData* data : factory_data_list) { + delete data; + } + factory_data_list.clear(); + printf("LuaGameLoader: Cleared all factory data\n"); +} + // Factory wrapper that captures script path static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, diff --git a/games/lua_game_loader.h b/games/lua_game_loader.h index a174fe7..0019965 100644 --- a/games/lua_game_loader.h +++ b/games/lua_game_loader.h @@ -20,6 +20,11 @@ public: */ static int register_all_games(GameLauncher* launcher); + /** + * @brief Clear all factory data (useful before re-scanning) + */ + static void clear_factory_data(); + private: /** * @brief Parse metadata from Lua script comments diff --git a/lib/game_launcher.cpp b/lib/game_launcher.cpp index 38150ed..646a258 100644 --- a/lib/game_launcher.cpp +++ b/lib/game_launcher.cpp @@ -8,6 +8,7 @@ #include "display/low_level_render.h" #include "display/low_level_gui.h" #include +#include GameLauncher::GameLauncher(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) @@ -212,6 +213,72 @@ void GameLauncher::reset() { printf("Launcher reset - returning to menu\n"); } +void GameLauncher::clear_games() { + // Clean up currently selected game first + if (selected_game) { + delete selected_game; + selected_game = nullptr; + } + + // Clear the games vector + games.clear(); + selected_index = 0; + current_page = 0; + printf("Launcher: Cleared all registered games\n"); +} + +bool GameLauncher::select_game_by_name(const char* name) { + // Clean up old game first if one exists + if (selected_game) { + printf("Cleaning up previous game...\n"); + delete selected_game; + selected_game = nullptr; + } + + // First pass: search for exact match (prefer exact matches) + for (size_t i = 0; i < games.size(); i++) { + if (strcmp(games[i].name, name) == 0) { + printf("Found exact match: %s\n", games[i].name); + + // Create and initialize the game + selected_game = games[i].factory(width, height, renderer, gui, input_manager); + if (selected_game) { + printf("Game instance created, initializing...\n"); + selected_game->init(); + selected_index = i; + current_page = i / GAMES_PER_PAGE; + return true; + } else { + printf("Failed to create game instance\n"); + return false; + } + } + } + + // Second pass: search in reverse order for partial match (newest files first) + for (int i = games.size() - 1; i >= 0; i--) { + if (strstr(games[i].name, name) != nullptr) { + printf("Found partial match: %s (searching for: %s)\n", games[i].name, name); + + // Create and initialize the game + selected_game = games[i].factory(width, height, renderer, gui, input_manager); + if (selected_game) { + printf("Game instance created, initializing...\n"); + selected_game->init(); + selected_index = i; + current_page = i / GAMES_PER_PAGE; + return true; + } else { + printf("Failed to create game instance\n"); + return false; + } + } + } + + printf("Game not found: %s\n", name); + return false; +} + int GameLauncher::get_total_pages() const { if (games.empty()) return 1; return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE; diff --git a/lib/game_launcher.h b/lib/game_launcher.h index be9d24e..c625a03 100644 --- a/lib/game_launcher.h +++ b/lib/game_launcher.h @@ -78,13 +78,25 @@ public: * @brief Reset launcher to show menu again */ void reset(); - + + /** + * @brief Clear all registered games (useful before re-scanning) + */ + void clear_games(); + /** * @brief Check if a game is currently selected * @return true if game selected, false if in menu */ bool is_game_selected() const { return selected_game != nullptr; } + /** + * @brief Select a game by name (for programmatic launching) + * @param name Game name to select (partial match supported) + * @return true if game was found and launched, false otherwise + */ + bool select_game_by_name(const char* name); + private: uint16_t width; uint16_t height; diff --git a/lib/sd_card/sd_card.c b/lib/sd_card/sd_card.c index 1af4a03..ebc896f 100644 --- a/lib/sd_card/sd_card.c +++ b/lib/sd_card/sd_card.c @@ -323,47 +323,69 @@ bool sd_card_read_blocks(uint32_t block_addr, uint32_t num_blocks, uint8_t *buff } bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) { - if (!g_card_info.initialized || buffer == NULL) return false; - + if (!g_card_info.initialized || buffer == NULL) { + return false; + } + // For non-SDHC cards, convert block address to byte address if (g_card_info.type != SD_CARD_TYPE_SDHC) { block_addr *= SD_BLOCK_SIZE; } - + sd_card_select(); - + // Send write command uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr); + if (r1 != SD_R1_READY) { sd_card_deselect(); return false; } - + + // Wait for card to be ready to receive data + uint32_t timeout_count = 0; + uint8_t ready_byte; + do { + ready_byte = sd_card_transfer(0xFF); + timeout_count++; + if (timeout_count > 1000) { + sd_card_deselect(); + return false; + } + } while (ready_byte != 0xFF); + // Send start token sd_card_transfer(SD_START_TOKEN); - + // Write data block for (int i = 0; i < SD_BLOCK_SIZE; i++) { sd_card_transfer(buffer[i]); } - + // Send dummy CRC (2 bytes) sd_card_transfer(0xFF); sd_card_transfer(0xFF); - - // Check data response - uint8_t response = sd_card_transfer(0xFF); + + // Check data response - may need to read several bytes before getting response + uint8_t response = 0xFF; + for (int i = 0; i < 10; i++) { + response = sd_card_transfer(0xFF); + if (response != 0xFF) { + break; + } + } + if ((response & 0x1F) != SD_DATA_ACCEPTED) { sd_card_deselect(); return false; } - + // Wait for card to finish writing if (!sd_card_wait_ready(500)) { sd_card_deselect(); return false; } - + sd_card_deselect(); return true; } diff --git a/lib/serial_uploader.cpp b/lib/serial_uploader.cpp new file mode 100644 index 0000000..1225b3e --- /dev/null +++ b/lib/serial_uploader.cpp @@ -0,0 +1,321 @@ +#include "serial_uploader.h" +#include "game_launcher.h" +#include "lua_game_loader.h" +#include "sd_card.h" +#include "pico/stdlib.h" +#include +#include +#include + +// Maximum file size: 64KB (should be plenty for Lua games) +#define MAX_FILE_SIZE (64 * 1024) + +SerialUploader::SerialUploader(GameLauncher* launcher) + : state(IDLE) + , game_launcher(launcher) + , file_size(0) + , bytes_received(0) + , file_buffer(nullptr) + , base64_index(0) +{ + filename[0] = '\0'; + last_uploaded_name[0] = '\0'; +} + +void SerialUploader::reset() { + state = IDLE; + file_size = 0; + bytes_received = 0; + base64_index = 0; + filename[0] = '\0'; + + if (file_buffer) { + delete[] file_buffer; + file_buffer = nullptr; + } +} + +uint8_t SerialUploader::decode_base64_char(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return 0; +} + +void SerialUploader::decode_base64_block(const char* input, uint8_t* output) { + uint32_t combined = (decode_base64_char(input[0]) << 18) | + (decode_base64_char(input[1]) << 12) | + (decode_base64_char(input[2]) << 6) | + decode_base64_char(input[3]); + + output[0] = (combined >> 16) & 0xFF; + output[1] = (combined >> 8) & 0xFF; + output[2] = combined & 0xFF; +} + +bool SerialUploader::write_file_to_sd() { + if (bytes_received == 0) { + printf("ERROR No data received!\n"); + return false; + } + + if (!file_buffer) { + printf("ERROR File buffer is NULL!\n"); + return false; + } + + // Set SPI speed to SD card speed (12.5 MHz) and save previous speed + uint prev_speed = sd_card_set_spi_speed(); + + // Ensure /games directory exists + FRESULT fr = f_mkdir("/games"); + if (fr != FR_OK && fr != FR_EXIST) { + printf("ERROR Failed to create /games directory: %d\n", fr); + // Try to continue anyway in case it already exists + } + + // Build file path - overwrite if file exists + char filepath[128]; + snprintf(filepath, sizeof(filepath), "/games/%s", filename); + + FIL fil; + fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE); + + if (fr != FR_OK) { + printf("ERROR Failed to open file for writing: %d\n", fr); + sd_card_restore_spi_speed(prev_speed); + return false; + } + + // Write data in chunks (more reliable for large files) + const uint32_t CHUNK_SIZE = 512; + uint32_t total_written = 0; + + while (total_written < bytes_received) { + uint32_t chunk = (bytes_received - total_written > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_received - total_written); + + UINT bytes_written = 0; + fr = f_write(&fil, file_buffer + total_written, chunk, &bytes_written); + + if (fr != FR_OK) { + printf("ERROR f_write failed at byte %u: error %d\n", total_written, fr); + f_close(&fil); + sd_card_restore_spi_speed(prev_speed); + return false; + } + + if (bytes_written != chunk) { + printf("ERROR Partial write: wrote %u/%u bytes at offset %u\n", bytes_written, chunk, total_written); + f_close(&fil); + sd_card_restore_spi_speed(prev_speed); + return false; + } + + total_written += bytes_written; + } + + // Sync to ensure data is written to SD card + fr = f_sync(&fil); + if (fr != FR_OK) { + printf("ERROR f_sync failed: %d\n", fr); + } + + f_close(&fil); + + printf("✓ Wrote %u bytes to %s\n", total_written, filepath); + + // Restore display SPI speed + sd_card_restore_spi_speed(prev_speed); + + return true; +} + +void SerialUploader::launch_game() { + // Extract base game name (remove .lua extension) + strncpy(last_uploaded_name, filename, sizeof(last_uploaded_name) - 1); + last_uploaded_name[sizeof(last_uploaded_name) - 1] = '\0'; + + // Remove .lua extension + char* ext = strstr(last_uploaded_name, ".lua"); + if (ext) { + *ext = '\0'; + } + + printf("Re-scanning Lua games to pick up new file...\n"); + + // Clear existing games before re-scanning (prevents duplicates and memory leaks) + game_launcher->clear_games(); + LuaGameLoader::clear_factory_data(); + + // Re-scan Lua games to pick up the newly uploaded file + // Note: LuaGameLoader::register_all_games handles SPI speed internally + int new_games = LuaGameLoader::register_all_games(game_launcher); + printf("Found %d Lua games after re-scan\n", new_games); + + // Now try to launch the newly uploaded game by name + printf("Attempting to launch game: %s\n", last_uploaded_name); + bool launched = game_launcher->select_game_by_name(last_uploaded_name); + + if (launched) { + printf("LAUNCHED %s\n", last_uploaded_name); + } else { + printf("ERROR Failed to launch game: %s\n", last_uploaded_name); + } +} + +bool SerialUploader::process() { + if (state == IDLE) { + // Check for "UPLOAD" command + int c = getchar_timeout_us(0); + if (c == PICO_ERROR_TIMEOUT) return false; + + if (c == 'U') { + // Check for "UPLOAD " + static char cmd_buffer[8]; + cmd_buffer[0] = c; + int idx = 1; + + // Read "PLOAD " + for (int i = 0; i < 6; i++) { + c = getchar_timeout_us(100000); // 100ms timeout + if (c == PICO_ERROR_TIMEOUT) return false; + cmd_buffer[idx++] = c; + } + cmd_buffer[idx] = '\0'; + + if (strcmp(cmd_buffer, "UPLOAD ") == 0) { + state = RECEIVING_FILENAME; + filename[0] = '\0'; + return false; + } + } + } + + if (state == RECEIVING_FILENAME) { + // Read filename until space + int idx = strlen(filename); + while (idx < sizeof(filename) - 1) { + int c = getchar_timeout_us(100000); + if (c == PICO_ERROR_TIMEOUT) break; + + if (c == ' ') { + filename[idx] = '\0'; + state = RECEIVING_SIZE; + file_size = 0; + return false; + } + + filename[idx++] = (char)c; + } + } + + if (state == RECEIVING_SIZE) { + // Read file size until newline + char size_buffer[16]; + int idx = 0; + + while (idx < sizeof(size_buffer) - 1) { + int c = getchar_timeout_us(100000); + if (c == PICO_ERROR_TIMEOUT) break; + + if (c == '\n' || c == '\r') { + size_buffer[idx] = '\0'; + file_size = atoi(size_buffer); + + if (file_size == 0 || file_size > MAX_FILE_SIZE) { + printf("ERROR Invalid file size: %u\n", file_size); + reset(); + return false; + } + + // Allocate buffer for decoded data + // file_size is the ORIGINAL (decoded) size, so allocate exactly that + file_buffer = new uint8_t[file_size]; + + if (!file_buffer) { + printf("ERROR Failed to allocate %u bytes for file buffer\n", file_size); + reset(); + return false; + } + + bytes_received = 0; + base64_index = 0; + + state = RECEIVING_DATA; + printf("Receiving %u bytes for %s...\n", file_size, filename); + return false; + } + + size_buffer[idx++] = (char)c; + } + } + + if (state == RECEIVING_DATA) { + // Read base64 data until "END" + while (true) { + int c = getchar_timeout_us(1000); + if (c == PICO_ERROR_TIMEOUT) break; + + // Check for "END" marker + static char end_check[4] = {0, 0, 0, 0}; + end_check[0] = end_check[1]; + end_check[1] = end_check[2]; + end_check[2] = end_check[3]; + end_check[3] = (char)c; + + if (end_check[0] == '\n' && end_check[1] == 'E' && end_check[2] == 'N' && end_check[3] == 'D') { + state = WRITING_FILE; + return false; + } + + // Skip whitespace + if (isspace(c)) continue; + + // Accumulate base64 characters + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=') { + + base64_buffer[base64_index++] = (char)c; + + // Decode every 4 characters + if (base64_index == 4) { + uint8_t decoded[3]; + decode_base64_block(base64_buffer, decoded); + + // Handle padding + int decoded_count = 3; + if (base64_buffer[2] == '=') decoded_count = 1; + else if (base64_buffer[3] == '=') decoded_count = 2; + + // Copy decoded bytes + for (int i = 0; i < decoded_count; i++) { + if (bytes_received < file_size) { + file_buffer[bytes_received++] = decoded[i]; + } + } + + base64_index = 0; + } + } + } + } + + if (state == WRITING_FILE) { + if (write_file_to_sd()) { + state = LAUNCHING_GAME; + } else { + reset(); + } + return false; + } + + if (state == LAUNCHING_GAME) { + launch_game(); + reset(); + return true; // Signal that game was launched + } + + return false; +} diff --git a/lib/serial_uploader.h b/lib/serial_uploader.h new file mode 100644 index 0000000..185b308 --- /dev/null +++ b/lib/serial_uploader.h @@ -0,0 +1,52 @@ +#ifndef SERIAL_UPLOADER_H +#define SERIAL_UPLOADER_H + +#include +#include "ff.h" + +class GameLauncher; + +class SerialUploader { +public: + SerialUploader(GameLauncher* launcher); + + // Process incoming serial data (call this frequently in main loop) + // Returns true if a game was launched + bool process(); + + // Get the filename of the last uploaded game (without .lua extension) + const char* get_last_uploaded_filename() const { return last_uploaded_name; } + +private: + enum State { + IDLE, + RECEIVING_FILENAME, + RECEIVING_SIZE, + RECEIVING_DATA, + WRITING_FILE, + LAUNCHING_GAME + }; + + State state; + GameLauncher* game_launcher; + + // Upload state + char filename[64]; + char last_uploaded_name[64]; // Game name without .lua extension + uint32_t file_size; + uint32_t bytes_received; + uint8_t* file_buffer; + + // Base64 decoding buffer + char base64_buffer[4]; + int base64_index; + + // Helper methods + void reset(); + bool write_file_to_sd(); + void launch_game(); + uint8_t decode_base64_char(char c); + void decode_base64_block(const char* input, uint8_t* output); +}; + +#endif // SERIAL_UPLOADER_H diff --git a/upload_game.py b/upload_game.py new file mode 100755 index 0000000..bdec7fa --- /dev/null +++ b/upload_game.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Lua Game Uploader for RP2350 +Rapidly upload and execute Lua games via USB serial for quick iteration. + +Usage: + python upload_game.py [serial_port] + +Example: + python upload_game.py my_game.lua /dev/ttyACM0 + python upload_game.py my_game.lua COM3 +""" + +import sys +import os +import base64 +import serial +import time +from pathlib import Path + +def find_serial_port(): + """Auto-detect the RP2350 serial port""" + import serial.tools.list_ports + + # Look for Pico/RP2350 devices + ports = list(serial.tools.list_ports.comports()) + for port in ports: + # Common VID/PID for Raspberry Pi Pico + if 'Pico' in port.description or \ + 'RP2350' in port.description or \ + 'RP2040' in port.description or \ + (port.vid == 0x2E8A): # Raspberry Pi VID + return port.device + + # Fallback: return first available port + if ports: + print(f"Warning: Could not find RP2350, using first available port: {ports[0].device}") + return ports[0].device + + return None + +def upload_game(lua_file, serial_port=None): + """Upload a Lua game file to the RP2350 and execute it""" + + # Check if file exists + if not os.path.exists(lua_file): + print(f"Error: File '{lua_file}' not found") + return False + + # Read the file + with open(lua_file, 'rb') as f: + file_data = f.read() + + file_size = len(file_data) + filename = os.path.basename(lua_file) + + # Ensure filename has .lua extension + if not filename.endswith('.lua'): + filename += '.lua' + + print(f"Uploading {filename} ({file_size} bytes)...") + + # Auto-detect serial port if not provided + if serial_port is None: + serial_port = find_serial_port() + if serial_port is None: + print("Error: No serial port found. Please specify manually.") + return False + print(f"Using serial port: {serial_port}") + + try: + # Open serial connection + ser = serial.Serial(serial_port, 115200, timeout=5) + time.sleep(0.1) # Wait for connection to stabilize + + # Encode file data as base64 + base64_data = base64.b64encode(file_data).decode('ascii') + + # Send upload command + command = f"UPLOAD {filename} {file_size}\n" + print(f"Sending command: {command.strip()}") + ser.write(command.encode('ascii')) + + # Send base64-encoded file data + print("Sending file data...") + ser.write(base64_data.encode('ascii')) + ser.write(b'\n') + + # Send END marker + ser.write(b'END\n') + print("Upload complete, waiting for confirmation...") + + # Read response from device + start_time = time.time() + response_lines = [] + while time.time() - start_time < 10: # 10 second timeout + if ser.in_waiting > 0: + line = ser.readline().decode('utf-8', errors='ignore').strip() + if line: + print(f" << {line}") + response_lines.append(line) + + # Check for success + if line.startswith('OK'): + print(f"✓ File written successfully!") + + if line.startswith('LAUNCHED'): + game_name = line.split(' ', 1)[1] if ' ' in line else filename + print(f"✓ Game '{game_name}' launched!") + ser.close() + return True + + # Check for error + if line.startswith('ERROR'): + print(f"✗ Upload failed: {line}") + ser.close() + return False + + print("Warning: No response received from device") + ser.close() + return False + + except serial.SerialException as e: + print(f"Serial error: {e}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + +def main(): + if len(sys.argv) < 2: + print(__doc__) + print("\nAvailable serial ports:") + try: + import serial.tools.list_ports + for port in serial.tools.list_ports.comports(): + print(f" {port.device}: {port.description}") + except ImportError: + print(" (install pyserial to see available ports)") + sys.exit(1) + + lua_file = sys.argv[1] + serial_port = sys.argv[2] if len(sys.argv) > 2 else None + + success = upload_game(lua_file, serial_port) + sys.exit(0 if success else 1) + +if __name__ == '__main__': + main()