///////////////////////////////////////////////////////////////////////////////////////////////////////// // // Name: SDCard.cpp // Created: Jan 2025 // Version: v1.1 // Author(s): Philip Smart // Description: Class definition to encapsulate the Espressif SD Card MMC Interface. // v1.1: All read/write handlers rewritten to use binary IPC SPI frames. // 32-sector FIFO read cache for CPM/RFS sequential access. // Burst read/write (up to IPCF_MAX_SECTORS per SPI transaction). // CRC32 integrity checking (hardware-assisted via esp_rom_crc32_le). // Credits: // Copyright: (c) 2019-2026 Philip Smart // // History: v1.00 Jan 2025 - Initial write. // v1.10 Mar 2025 - Binary IPC, burst, sector cache, CRC32. // // Notes: See Makefile to enable/disable conditional components // ///////////////////////////////////////////////////////////////////////////////////////////////////////// // This source file is free software: you can redistribute it and#or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This source file is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . ///////////////////////////////////////////////////////////////////////////////////////////////////////// #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/spi_slave.h" #include "esp_system.h" #include "esp_log.h" #include "esp_rom_crc.h" #include "sdmmc_cmd.h" #include "driver/sdspi_host.h" #include "driver/spi_common.h" #include "driver/sdmmc_host.h" #include "esp_vfs_fat.h" #include #include #include #include #include #include "IO.h" #include "SDCard.h" #include "ipc_protocol.h" #define SDTAG "SDCARD" // --------------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------------- SDCard::SDCard() { ioBuf = IO_getIoBuf(); rp2350Header = nullptr; cacheHead = 0; memset(sectorCache, 0, sizeof(sectorCache)); } // --------------------------------------------------------------------------- // SD card mount / initialisation (unchanged from v1.0) // --------------------------------------------------------------------------- void SDCard::init() { esp_err_t ret; esp_vfs_fat_sdmmc_mount_config_t sdMountConf; sdmmc_slot_config_t sdSlotConf; sdmmc_host_t sdHost; sdMountConf = {.format_if_mount_failed = false, .max_files = 5, .allocation_unit_size = 16 * 1024, .disk_status_check_enable = true}; ESP_LOGI(SDTAG, "Initializing SD card (SDMMC)"); sdHost = SDMMC_HOST_DEFAULT(); sdSlotConf = SDMMC_SLOT_CONFIG_DEFAULT(); sdSlotConf.width = 4; sdSlotConf.clk = (gpio_num_t) CONFIG_SD_CLK; sdSlotConf.cmd = (gpio_num_t) CONFIG_SD_CMD; sdSlotConf.d0 = (gpio_num_t) CONFIG_SD_DAT0; sdSlotConf.d1 = (gpio_num_t) CONFIG_SD_DAT1; sdSlotConf.d2 = (gpio_num_t) CONFIG_SD_DAT2; sdSlotConf.d3 = (gpio_num_t) CONFIG_SD_DAT3; sdSlotConf.cd = (gpio_num_t) CONFIG_SD_CDDETECT; sdSlotConf.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; ESP_LOGI(SDTAG, "Mounting filesystem"); ret = esp_vfs_fat_sdmmc_mount(sdCardMountPoint.c_str(), &sdHost, &sdSlotConf, &sdMountConf, &sdCard); if (ret != ESP_OK) { if (ret == ESP_FAIL) ESP_LOGE(SDTAG, "Failed to mount filesystem."); else ESP_LOGE(SDTAG, "Failed to initialize card (%s).", esp_err_to_name(ret)); return; } ESP_LOGI(SDTAG, "Filesystem mounted."); sdmmc_card_print_info(stdout, sdCard); } // --------------------------------------------------------------------------- // Recursive directory deletion (unchanged from v1.0) // --------------------------------------------------------------------------- bool SDCard::deleteDir(const std::string &path) { if (path.compare(SD_CARD_MOUNT_POINT) == 0 || path.compare(SD_CARD_MOUNT_POINT "/") == 0 || path.compare("/") == 0) { ESP_LOGE(SDTAG, "Cannot delete root directory: %s", path.c_str()); return false; } DIR *dir = opendir(path.c_str()); if (!dir) { ESP_LOGE(SDTAG, "Failed to open directory: %s", path.c_str()); return false; } struct dirent *entry; while ((entry = readdir(dir)) != nullptr) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; std::string fullPath = path + "/" + entry->d_name; struct stat statbuf; if (stat(fullPath.c_str(), &statbuf) != 0) { ESP_LOGE(SDTAG, "Failed to stat: %s", fullPath.c_str()); closedir(dir); return false; } if (S_ISDIR(statbuf.st_mode)) { if (!deleteDir(fullPath)) { closedir(dir); return false; } } else { if (unlink(fullPath.c_str()) != 0) { ESP_LOGE(SDTAG, "Failed to delete file: %s", fullPath.c_str()); closedir(dir); return false; } } } closedir(dir); if (rmdir(path.c_str()) != 0) { ESP_LOGE(SDTAG, "Failed to delete directory: %s", path.c_str()); return false; } return true; } // --------------------------------------------------------------------------- // Sector cache helpers // --------------------------------------------------------------------------- // Returns cache slot index if hit, -1 if miss. int SDCard::cacheLookup(const char *filename, uint32_t fileOffset) { for (int i = 0; i < SECTOR_CACHE_SIZE; i++) { if (sectorCache[i].valid && sectorCache[i].fileOffset == fileOffset && strncmp(sectorCache[i].filename, filename, IPCF_FILENAME_LEN) == 0) { return i; } } return -1; } // Insert a sector into the cache using FIFO eviction. void SDCard::cacheInsert(const char *filename, uint32_t fileOffset, const uint8_t *data) { t_SectorCacheEntry *e = §orCache[cacheHead]; strncpy(e->filename, filename, IPCF_FILENAME_LEN - 1); e->filename[IPCF_FILENAME_LEN - 1] = '\0'; e->fileOffset = fileOffset; memcpy(e->data, data, IPCF_SECTOR_SIZE); e->valid = true; cacheHead = (cacheHead + 1) % SECTOR_CACHE_SIZE; } // Invalidate any cached entry matching filename + fileOffset (write path). void SDCard::cacheInvalidate(const char *filename, uint32_t fileOffset) { for (int i = 0; i < SECTOR_CACHE_SIZE; i++) { if (sectorCache[i].valid && sectorCache[i].fileOffset == fileOffset && strncmp(sectorCache[i].filename, filename, IPCF_FILENAME_LEN) == 0) { sectorCache[i].valid = false; } } } // --------------------------------------------------------------------------- // Internal helper — build and send an error response frame. // --------------------------------------------------------------------------- void SDCard::sendErrResp(FSPI &fspi, uint8_t status, uint8_t command, TickType_t timeout) { t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = status; hdr.payloadLen = 0; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; fspi.sendBinaryResp(&hdr, NULL, 0, respSize, timeout); } // --------------------------------------------------------------------------- // Internal helper — receive write payload + CRC32 from RP2350. // Returns ESP_OK on success, error code on CRC mismatch or SPI failure. // --------------------------------------------------------------------------- esp_err_t SDCard::receiveWritePayload(FSPI &fspi, uint8_t *buf, uint32_t len, uint32_t expectedCrc) { // The RP2350 sends: payload (len bytes) | CRC32 (4 bytes). // We receive into ipcCmdBuf (DMA-capable) then copy to buf. // ESP32 SPI slave DMA requires transaction size to be a multiple of 4 bytes. // Round up so the DMA does not silently truncate the last CRC byte(s). // The RP2350 pads its T2 DMA to the same rounded-up length (zeros after CRC). uint32_t frameLen = (len + IPCF_CRC_SIZE + 3u) & ~3u; spi_slave_transaction_t t = {}; t.length = frameLen * 8; t.rx_buffer = fspi.ipcCmdBuf; t.tx_buffer = fspi.ipcRespBuf; // zeros (already cleared) esp_err_t ret = spi_slave_transmit(FSPI_HOST, &t, portMAX_DELAY); if (ret != ESP_OK) return ret; // Verify CRC32 of received payload. uint32_t recvCrc; memcpy(&recvCrc, fspi.ipcCmdBuf + len, IPCF_CRC_SIZE); uint32_t calcCrc = esp_rom_crc32_le(0, fspi.ipcCmdBuf, len); if (calcCrc != recvCrc) { ESP_LOGE(SDTAG, "Write payload CRC mismatch: calc=%08lx recv=%08lx (len=%lu)", calcCrc, recvCrc, (unsigned long) len); // Diagnostic: print first 16 received bytes and the CRC bytes. ESP_LOGE(SDTAG, " recv[0..15]: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", fspi.ipcCmdBuf[0], fspi.ipcCmdBuf[1], fspi.ipcCmdBuf[2], fspi.ipcCmdBuf[3], fspi.ipcCmdBuf[4], fspi.ipcCmdBuf[5], fspi.ipcCmdBuf[6], fspi.ipcCmdBuf[7], fspi.ipcCmdBuf[8], fspi.ipcCmdBuf[9], fspi.ipcCmdBuf[10], fspi.ipcCmdBuf[11], fspi.ipcCmdBuf[12], fspi.ipcCmdBuf[13], fspi.ipcCmdBuf[14], fspi.ipcCmdBuf[15]); ESP_LOGE(SDTAG, " crc_bytes @ offset %lu: %02X %02X %02X %02X", (unsigned long) len, fspi.ipcCmdBuf[len + 0], fspi.ipcCmdBuf[len + 1], fspi.ipcCmdBuf[len + 2], fspi.ipcCmdBuf[len + 3]); return ESP_ERR_INVALID_CRC; } memcpy(buf, fspi.ipcCmdBuf, len); return ESP_OK; } // --------------------------------------------------------------------------- // readSectorViaSPI — read one IPCF_SECTOR_SIZE (512-byte) sector. // Checks the read cache first. On a cache miss, reads SECTOR_READAHEAD // consecutive sectors in a single fread() and caches them all. Subsequent // single-sector requests for offsets +512, +1024, ... hit the cache and // cost only one SPI round-trip with no SD card I/O — critical for CPM DIR // and other sequential-access patterns. // --------------------------------------------------------------------------- bool SDCard::readSectorViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t fileOffset = frame.fileOffset; uint8_t command = frame.command; // --- Cache lookup --- int slot = cacheLookup(filename, fileOffset); if (slot >= 0) { // Cache hit — no SD card I/O needed. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = IPCF_SECTOR_SIZE; hdr.sectorCount = 1; hdr.fileOffset = fileOffset; hdr.flags = IPCF_FLAG_LAST_CHUNK; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_SECTOR_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, sectorCache[slot].data, IPCF_SECTOR_SIZE, respSize, portMAX_DELAY); return (ret == ESP_OK); } // --- Cache miss — open file and read ahead --- std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = fopen(filepath.c_str(), "r"); if (!f) { ESP_LOGE(SDTAG, "readSector: open failed (%s)", filepath.c_str()); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } fseek(f, 0L, SEEK_END); int fileSize = ftell(f); if ((int) (fileOffset + IPCF_SECTOR_SIZE) > fileSize) { fclose(f); ESP_LOGE(SDTAG, "readSector: offset %lu out of range (fileSize=%d)", fileOffset, fileSize); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Clamp read-ahead to file boundary. int numAhead = SECTOR_READAHEAD; while (numAhead > 1 && (int) (fileOffset + (uint32_t) numAhead * IPCF_SECTOR_SIZE) > fileSize) numAhead--; // Allocate heap buffer for the read-ahead block. // Fall back to a single-sector stack buffer if heap is tight. uint8_t stackBuf[IPCF_SECTOR_SIZE]; uint8_t *raBuf = (uint8_t *) heap_caps_malloc((size_t) numAhead * IPCF_SECTOR_SIZE, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); uint8_t *readBuf = raBuf ? raBuf : stackBuf; int numToRead = raBuf ? numAhead : 1; fseek(f, (long) fileOffset, SEEK_SET); size_t totalRead = fread(readBuf, 1, (size_t) numToRead * IPCF_SECTOR_SIZE, f); fclose(f); if (totalRead < IPCF_SECTOR_SIZE) { if (raBuf) free(raBuf); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Insert all complete sectors into the cache (requested + read-ahead). int sectorsRead = (int) (totalRead / IPCF_SECTOR_SIZE); for (int i = 0; i < sectorsRead; i++) { uint8_t *sec = readBuf + (size_t) i * IPCF_SECTOR_SIZE; size_t secBytes = totalRead - (size_t) i * IPCF_SECTOR_SIZE; if (secBytes < IPCF_SECTOR_SIZE) memset(sec + secBytes, 0, IPCF_SECTOR_SIZE - secBytes); cacheInsert(filename, fileOffset + (uint32_t) i * IPCF_SECTOR_SIZE, sec); } // Build and send response for the originally requested sector (readBuf[0]). t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = IPCF_SECTOR_SIZE; hdr.sectorCount = 1; hdr.fileOffset = fileOffset; hdr.flags = IPCF_FLAG_LAST_CHUNK; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_SECTOR_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, readBuf, IPCF_SECTOR_SIZE, respSize, portMAX_DELAY); if (raBuf) free(raBuf); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // readBurstViaSPI — read up to IPCF_MAX_SECTORS consecutive sectors. // All sectors returned in a single SPI transaction. Sectors served from // cache where available; any uncached sectors are read from SD. // --------------------------------------------------------------------------- bool SDCard::readBurstViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t fileOffset = frame.fileOffset; int numSectors = (int) frame.sectorCount; uint8_t command = frame.command; if (numSectors < 1) numSectors = 1; if (numSectors > IPCF_MAX_SECTORS) numSectors = IPCF_MAX_SECTORS; // Allocate a DMA-capable burst buffer. uint32_t totalBytes = (uint32_t) numSectors * IPCF_SECTOR_SIZE; uint8_t *burstBuf = (uint8_t *) heap_caps_malloc(totalBytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!burstBuf) { ESP_LOGE(SDTAG, "readBurst: malloc failed (%lu bytes)", totalBytes); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Open file for sectors not in cache. std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = NULL; bool result = true; for (int s = 0; s < numSectors && result; s++) { uint32_t secOffset = fileOffset + (uint32_t) s * IPCF_SECTOR_SIZE; uint8_t *dest = burstBuf + (size_t) s * IPCF_SECTOR_SIZE; int slot = cacheLookup(filename, secOffset); if (slot >= 0) { // Cache hit. memcpy(dest, sectorCache[slot].data, IPCF_SECTOR_SIZE); } else { // Cache miss — open file on first miss. if (!f) { f = fopen(filepath.c_str(), "r"); if (!f) { ESP_LOGE(SDTAG, "readBurst: open failed (%s)", filepath.c_str()); result = false; break; } } fseek(f, (long) secOffset, SEEK_SET); size_t rd = fread(dest, 1, IPCF_SECTOR_SIZE, f); if (rd < IPCF_SECTOR_SIZE) memset(dest + rd, 0, IPCF_SECTOR_SIZE - rd); // Cache this sector. cacheInsert(filename, secOffset, dest); } } if (f) fclose(f); if (!result) { free(burstBuf); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } // Build and send response. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = (uint16_t) totalBytes; hdr.sectorCount = (uint16_t) numSectors; hdr.fileOffset = fileOffset; hdr.flags = IPCF_FLAG_LAST_CHUNK; uint32_t respSize = IPCF_HEADER_SIZE + totalBytes + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, burstBuf, totalBytes, respSize, portMAX_DELAY); free(burstBuf); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // readFileViaSPI — send ONE chunk of a file per command invocation. // Used by RFILE, RFD, RQD, RRF commands. // // The RP2350 drives chunked file transfers by issuing one command per chunk, // advancing fileOffset by the received payloadLen each time. The ESP32 // must NOT loop internally — doing so would collide with the RP2350's next // command SPI transaction. // // Response header carries: // fileOffset = total file size (so RP2350 knows when to stop) // payloadLen = bytes in this chunk // flags = IPCF_FLAG_LAST_CHUNK when this chunk reaches EOF // --------------------------------------------------------------------------- bool SDCard::readFileViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t reqOffset = frame.fileOffset; // start position for this chunk uint8_t command = frame.command; std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = fopen(filepath.c_str(), "r"); if (!f) { ESP_LOGE(SDTAG, "readFile: open failed (%s)", filepath.c_str()); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } fseek(f, 0L, SEEK_END); int fileSize = ftell(f); if (reqOffset >= (uint32_t) fileSize) { fclose(f); // Past EOF — send last-chunk response with zero payload so RP2350 terminates. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = 0; hdr.fileOffset = (uint32_t) fileSize; hdr.flags = IPCF_FLAG_LAST_CHUNK; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY); return true; } fseek(f, (long) reqOffset, SEEK_SET); // Allocate DMA-capable chunk buffer. uint8_t *chunkBuf = (uint8_t *) heap_caps_malloc(IPCF_MAX_PAYLOAD, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!chunkBuf) { fclose(f); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } size_t chunk = fread(chunkBuf, 1, IPCF_MAX_PAYLOAD, f); fclose(f); if (chunk == 0) { free(chunkBuf); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } bool lastChunk = ((int) (reqOffset + chunk) >= fileSize); t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = (uint16_t) chunk; hdr.fileOffset = (uint32_t) fileSize; // carry total size hdr.flags = lastChunk ? IPCF_FLAG_LAST_CHUNK : IPCF_FLAG_MORE_DATA; uint32_t respSize = IPCF_HEADER_SIZE + (uint32_t) chunk + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, chunkBuf, (uint32_t) chunk, respSize, portMAX_DELAY); free(chunkBuf); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // writeSectorViaSPI — receive one sector from RP2350, write to file. // Invalidates any cached copy of the sector. // --------------------------------------------------------------------------- bool SDCard::writeSectorViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t fileOffset = frame.fileOffset; uint8_t command = frame.command; // Use the actual payload length from the T1 header — the WD1773 driver may // send 256-byte sectors (MZ-2Z009E) rather than 512. Fall back to // IPCF_SECTOR_SIZE for backwards compatibility if payloadLen is zero. uint32_t payloadLen = frame.payloadLen > 0 ? frame.payloadLen : IPCF_SECTOR_SIZE; if (payloadLen > IPCF_SECTOR_SIZE) payloadLen = IPCF_SECTOR_SIZE; // Invalidate cached sector before write. cacheInvalidate(filename, fileOffset); // Receive payload from RP2350 (payloadLen + CRC32). uint8_t sectorBuf[IPCF_SECTOR_SIZE]; if (receiveWritePayload(fspi, sectorBuf, payloadLen, 0) != ESP_OK) { sendErrResp(fspi, IPCF_STATUS_CRCFAIL, command); return false; } // Open file and seek to write position. std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = fopen(filepath.c_str(), "r+"); if (!f) { ESP_LOGE(SDTAG, "writeSector: open failed (%s)", filepath.c_str()); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } fseek(f, (long) fileOffset, SEEK_SET); size_t written = fwrite(sectorBuf, 1, payloadLen, f); fflush(f); fclose(f); if (written != payloadLen) { ESP_LOGE(SDTAG, "writeSector: short write %zu/%lu", written, (unsigned long) payloadLen); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Send success response (header + CRC only, no payload). t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = 0; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // writeBurstViaSPI — receive up to IPCF_MAX_SECTORS sectors, write to file. // All sectors received in one SPI transaction. // --------------------------------------------------------------------------- bool SDCard::writeBurstViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t fileOffset = frame.fileOffset; int numSectors = (int) frame.sectorCount; uint8_t command = frame.command; if (numSectors < 1) numSectors = 1; if (numSectors > IPCF_MAX_SECTORS) numSectors = IPCF_MAX_SECTORS; // Use actual payload length from T1 header — sectors may be < 512 bytes. uint32_t totalBytes = frame.payloadLen > 0 ? frame.payloadLen : (uint32_t) numSectors * IPCF_SECTOR_SIZE; if (totalBytes > IPCF_MAX_PAYLOAD) totalBytes = IPCF_MAX_PAYLOAD; // Determine per-sector size for cache invalidation. uint32_t sectorLen = totalBytes / (uint32_t) numSectors; if (sectorLen == 0) sectorLen = IPCF_SECTOR_SIZE; // Invalidate cached sectors in the range. for (int s = 0; s < numSectors; s++) cacheInvalidate(filename, fileOffset + (uint32_t) s * sectorLen); // Allocate DMA-capable receive buffer. uint8_t *burstBuf = (uint8_t *) heap_caps_malloc(totalBytes, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!burstBuf) { sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Receive write payload (totalBytes + CRC32). if (receiveWritePayload(fspi, burstBuf, totalBytes, 0) != ESP_OK) { free(burstBuf); sendErrResp(fspi, IPCF_STATUS_CRCFAIL, command); return false; } // Open file and write all sectors. std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = fopen(filepath.c_str(), "r+"); if (!f) { free(burstBuf); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } fseek(f, (long) fileOffset, SEEK_SET); size_t written = fwrite(burstBuf, 1, totalBytes, f); fflush(f); fclose(f); free(burstBuf); if (written != totalBytes) { ESP_LOGE(SDTAG, "writeBurst: short write %zu/%lu", written, totalBytes); sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Success response. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = 0; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // writeFileViaSPI — receive an entire file from RP2350 in chunks. // Each chunk arrives as: [command frame] [payload + CRC32] pairs. // The file is created/overwritten at the path specified in frame.filename. // --------------------------------------------------------------------------- bool SDCard::writeFileViaSPI(const t_IpcFrameHdr &frame, FSPI &fspi) { const char *filename = frame.filename; uint32_t fileOffset = frame.fileOffset; // byte offset of this chunk uint32_t chunkLen = frame.payloadLen; // bytes to receive uint8_t command = frame.command; bool lastChunk = (frame.flags & IPCF_FLAG_LAST_CHUNK) != 0; // Allocate DMA receive buffer. uint8_t *chunkBuf = (uint8_t *) heap_caps_malloc(chunkLen, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!chunkBuf) { sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Receive payload. if (receiveWritePayload(fspi, chunkBuf, chunkLen, 0) != ESP_OK) { free(chunkBuf); sendErrResp(fspi, IPCF_STATUS_CRCFAIL, command); return false; } // Open for write (first chunk: create/truncate; subsequent chunks: append). std::string filepath = SD_CARD_MOUNT_POINT "/"; filepath.append(filename); FILE *f = fopen(filepath.c_str(), fileOffset == 0 ? "w" : "r+"); if (!f) { free(chunkBuf); sendErrResp(fspi, IPCF_STATUS_NOTFOUND, command); return false; } fseek(f, (long) fileOffset, SEEK_SET); size_t written = fwrite(chunkBuf, 1, chunkLen, f); fflush(f); fclose(f); free(chunkBuf); if (written != chunkLen) { sendErrResp(fspi, IPCF_STATUS_ERR, command); return false; } // Send status response. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = 0; hdr.flags = lastChunk ? IPCF_FLAG_LAST_CHUNK : 0; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // storeRP2350Info — receive the RP2350 partition header sent by INF command. // The payload is already in fspi.ipcCmdBuf (received during command transaction). // Stores a copy in ioBuf for WiFi to access (web interface). // --------------------------------------------------------------------------- bool SDCard::storeRP2350Info(const t_IpcFrameHdr &frame, FSPI &fspi) { // The INF write payload was received in the second SPI transaction by // FSPI_sendBinaryCmd() on the RP2350 side, which means the ESP32's // receiveWritePayload() must be called to get it. uint32_t payloadSize = frame.payloadLen; if (payloadSize == 0 || payloadSize > IPCF_MAX_PAYLOAD) { sendErrResp(fspi, IPCF_STATUS_ERR, frame.command); return false; } uint8_t *infoBuf = (uint8_t *) malloc(payloadSize); if (!infoBuf) { sendErrResp(fspi, IPCF_STATUS_ERR, frame.command); return false; } if (receiveWritePayload(fspi, infoBuf, payloadSize, 0) != ESP_OK) { free(infoBuf); sendErrResp(fspi, IPCF_STATUS_CRCFAIL, frame.command); return false; } // Store partition header + extended core config for WiFi subsystem access. // rp2350Header points to wifiCtrl.run.rp2350FlashHeader. if (rp2350Header && payloadSize >= sizeof(t_FlashPartitionHeader)) { memcpy(rp2350Header, infoBuf, sizeof(t_FlashPartitionHeader)); // Extended payload fields — extract via WiFi's wifiCtrl.run fields directly. // The rp2350Header pointer points to wifiCtrl.run.rp2350FlashHeader, and the // fields after it (rp2350CpuFreq, rp2350PsramFreq, etc.) are at known offsets. // Backwards-compatible: only extract fields present in the payload. if (payloadSize > sizeof(t_FlashPartitionHeader)) { t_FlashInfoPayload *info = (t_FlashInfoPayload *) infoBuf; // Core config fields (cpufreq..psramSize) — present in all extended payloads. const size_t coreExtSize = offsetof(t_FlashInfoPayload, hostClkHz); if (payloadSize >= coreExtSize) { int32_t *extra = (int32_t *) (rp2350Header + 1); extra[0] = info->cpufreq; extra[1] = info->psramfreq; extra[2] = info->voltage; uint32_t *extra32 = (uint32_t *) &extra[3]; extra32[0] = info->flashSize; extra32[1] = info->psramSize; } // Host clock + emulation speed — added after psramSize. if (payloadSize >= offsetof(t_FlashInfoPayload, driverSummary)) { int32_t *extra = (int32_t *) (rp2350Header + 1); uint32_t *extra32 = (uint32_t *) &extra[3]; extra32[2] = info->hostClkHz; // rp2350HostClkHz extra32[3] = info->emulSpeedHz; // rp2350EmulSpeedHz } // Driver summary — last field, variable-length content. uint32_t *extra32base = (uint32_t *) ((int32_t *)(rp2350Header + 1) + 3); char *drvSummary = (char *) &extra32base[4]; // After flashSize, psramSize, hostClkHz, emulSpeedHz if (payloadSize >= sizeof(t_FlashInfoPayload)) { memcpy(drvSummary, info->driverSummary, FLASHHDR_DRIVER_SUMMARY_SIZE); drvSummary[FLASHHDR_DRIVER_SUMMARY_SIZE - 1] = '\0'; } else { drvSummary[0] = '\0'; } } } free(infoBuf); // Send success response. t_IpcFrameHdr hdr = {}; hdr.frameType = IPCF_TYPE_RESPONSE; hdr.command = frame.command; hdr.status = IPCF_STATUS_OK; hdr.payloadLen = 0; uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE; esp_err_t ret = fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY); return (ret == ESP_OK); }