///////////////////////////////////////////////////////////////////////////////////////////////////////// // // Name: FSPI.cpp // Created: Jan 2025 // Version: v1.1 // Author(s): Philip Smart // Description: Class definition to encapsulate the Espressif FSPI Interface. // v1.1: Fixed critical bug — max_transfer_sz was 32 bytes (silently truncated // all SPI transfers to 32 bytes). Now set to IPCF_MAX_FRAME_SIZE (8260). // Added receiveBinaryCmd() and sendBinaryResp() for binary IPC protocol. // DMA-capable buffers allocated at init(). CRC32 via esp_rom_crc32_le(). // Credits: // Copyright: (c) 2019-2026 Philip Smart // // History: v1.00 Jan 2025 - Initial write. // v1.10 Mar 2025 - Bug fix max_transfer_sz, binary IPC, DMA buffers. // // 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 "esp_system.h" #include "esp_log.h" #include "esp_rom_crc.h" #include "esp_rom_sys.h" #include "sdmmc_cmd.h" #include "driver/sdspi_host.h" #include "driver/spi_common.h" #include "driver/spi_slave.h" #include "esp_vfs_fat.h" #include #include #include #include "FSPI.h" #include "ipc_protocol.h" #define FSPITAG "FSPI" // Declared in IO.cpp — set before esp_restart() so CommandProcessor uses the // 100 ms startup delay (OOB path) instead of the 3500 ms crash-recovery delay. // Safe here because spihost corruption means no T2/T3 is in progress — the // RP2350 never received T3 HS so it will abort its command and retry cleanly. extern uint32_t g_oob_restart_magic; #define OOB_RESTART_MAGIC_FSPI 0xAA55CC33u FSPI::FSPI() { ipcCmdBuf = NULL; ipcRespBuf = NULL; ESP_LOGI(FSPITAG, "SPI Constructor"); } extern "C" { // Called after a transaction is queued and ready for the master. // HS HIGH → tells RP2350 master the ESP32 is ready for this transaction. // IRAM_ATTR is mandatory: this is called from the SPI slave ISR context. // Without it, if the flash cache is disabled when the ISR fires (e.g. during // WiFi or flash operations), accessing this function's code page causes a // "Cache disabled but cached memory region accessed" panic (EXCCAUSE 7). void IRAM_ATTR myPostSetupCb(spi_slave_transaction_t *trans) { (void) trans; gpio_set_level((gpio_num_t) CONFIG_HS, 1); } // Called after the transaction completes (data exchanged). // HS LOW → transaction boundary signalled to RP2350. // IRAM_ATTR required for same reason as myPostSetupCb above. void IRAM_ATTR myPostTransCb(spi_slave_transaction_t *trans) { (void) trans; gpio_set_level((gpio_num_t) CONFIG_HS, 0); } esp_err_t FSPI_init(void) { esp_err_t ret; gpio_config_t ioConf; spi_slave_interface_config_t slvCfg = { .spics_io_num = CONFIG_FSPI_CS0, .flags = 0, .queue_size = 10, .mode = 3, .post_setup_cb = myPostSetupCb, .post_trans_cb = myPostTransCb}; spi_bus_config_t busCfg = { .mosi_io_num = CONFIG_FSPI_MOSI, .miso_io_num = CONFIG_FSPI_MISO, .sclk_io_num = CONFIG_FSPI_CLK, .data2_io_num = -1, .data3_io_num = -1, .data4_io_num = -1, .data5_io_num = -1, .data6_io_num = -1, .data7_io_num = -1, // FIX: was 32 (silently truncated all SPI transfers to 32 bytes). // Must be >= the largest single SPI transaction: // IPCF_MAX_FRAME_SIZE = IPCF_HEADER_SIZE(64) + IPCF_MAX_PAYLOAD(8192) + IPCF_CRC_SIZE(4) = 8260 .max_transfer_sz = IPCF_MAX_FRAME_SIZE, .flags = SPICOMMON_BUSFLAG_SLAVE, .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, .intr_flags = 0, }; ESP_LOGI(FSPITAG, "Configuring SPI slave (max_transfer_sz=%d).", IPCF_MAX_FRAME_SIZE); ret = spi_slave_initialize(FSPI_HOST, &busCfg, &slvCfg, SPI_DMA_CH_AUTO); ESP_ERROR_CHECK(ret); // Handshake GPIO — output, initially LOW (not ready). ioConf.intr_type = GPIO_INTR_DISABLE; ioConf.mode = GPIO_MODE_OUTPUT; ioConf.pin_bit_mask = (1ULL << CONFIG_HS); ioConf.pull_down_en = GPIO_PULLDOWN_DISABLE; ioConf.pull_up_en = GPIO_PULLUP_ENABLE; gpio_config(&ioConf); gpio_set_level((gpio_num_t) CONFIG_HS, 0); ESP_LOGI(FSPITAG, "SPI init complete."); return (ret); } } // Perform hardware + buffer initialisation. bool FSPI::init() { // Allocate DMA-capable buffers for IPC frame protocol. // Only allocate if not already allocated (reinit reuses existing buffers). if (!ipcCmdBuf) ipcCmdBuf = (uint8_t *) heap_caps_malloc(IPCF_MAX_FRAME_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!ipcRespBuf) ipcRespBuf = (uint8_t *) heap_caps_malloc(IPCF_MAX_FRAME_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); if (!ipcCmdBuf || !ipcRespBuf) { ESP_LOGE(FSPITAG, "Failed to allocate DMA buffers."); return false; } memset(ipcCmdBuf, 0, IPCF_MAX_FRAME_SIZE); memset(ipcRespBuf, 0, IPCF_MAX_FRAME_SIZE); esp_err_t ret = FSPI_init(); return (ret == ESP_OK); } // --------------------------------------------------------------------------- // Binary IPC protocol — command reception // --------------------------------------------------------------------------- // Block until the RP2350 master sends a 64-byte command frame, then return it. esp_err_t FSPI::receiveBinaryCmd(uint8_t *cmdBuf, TickType_t timeout) { spi_slave_transaction_t t = {}; t.length = IPCF_HEADER_SIZE * 8; // bits t.rx_buffer = cmdBuf; t.tx_buffer = NULL; // dummy MISO (ESP32 slave sends zeros) // Queue the receive — myPostSetupCb fires → HS HIGH → RP2350 can send. esp_err_t ret = spi_slave_transmit(FSPI_HOST, &t, timeout); // myPostTransCb fires → HS LOW after transfer completes. return ret; } // --------------------------------------------------------------------------- // Binary IPC protocol — response transmission // --------------------------------------------------------------------------- // Build and send a response frame: [hdr 64B] [payload] [CRC32 4B]. // The frame is padded to respSize bytes so the RP2350 can pre-size its DMA. esp_err_t FSPI::sendBinaryResp(const t_IpcFrameHdr *hdr, const uint8_t *payload, uint32_t payloadLen, uint32_t respSize, TickType_t timeout) { if (respSize > IPCF_MAX_FRAME_SIZE) { ESP_LOGE(FSPITAG, "sendBinaryResp: respSize %lu exceeds IPCF_MAX_FRAME_SIZE %u", respSize, IPCF_MAX_FRAME_SIZE); respSize = IPCF_MAX_FRAME_SIZE; } // Build frame into ipcRespBuf: header | payload | crc32 | (pad zeros) memset(ipcRespBuf, 0, respSize); memcpy(ipcRespBuf, hdr, IPCF_HEADER_SIZE); if (payload && payloadLen > 0) memcpy(ipcRespBuf + IPCF_HEADER_SIZE, payload, payloadLen); // CRC32 over header + payload. // esp_rom_crc32_le(0, data, len) == standard IEEE 802.3 CRC32. // The function internally does: crc=~init, process bytes, return ~crc. // Passing init=0 gives ~0=0xFFFFFFFF as the internal start, which is correct. // Do NOT use ~esp_rom_crc32_le(~0u,...): that pre-conditions to ~0xFFFFFFFF=0 // (wrong seed) then the outer ~ undoes the post-condition, giving crc_from_zero. uint32_t crc = esp_rom_crc32_le(0, ipcRespBuf, IPCF_HEADER_SIZE + payloadLen); memcpy(ipcRespBuf + IPCF_HEADER_SIZE + payloadLen, &crc, IPCF_CRC_SIZE); // Round up to a 4-byte DMA boundary. ESP32 SPI slave DMA requires the // transaction size to be a multiple of 4 bytes; if respSize is not aligned // (e.g. a partial file chunk where payloadLen % 4 != 0), the DMA silently // truncates the transfer, potentially dropping the last 1–3 bytes of the CRC. // The extra pad bytes are zeros (ipcRespBuf was memset to 0 above for respSize // bytes, and bytes beyond respSize are never read by the RP2350 — it uses // resp->payloadLen to locate the CRC, which stays at the correct offset). uint32_t wireSize = (respSize + 3u) & ~3u; // wireSize <= IPCF_MAX_FRAME_SIZE because respSize <= IPCF_MAX_FRAME_SIZE // and IPCF_MAX_FRAME_SIZE (8260) is already a multiple of 4. // Stall long enough for the CPU D-cache to write back ipcRespBuf to SRAM // before the GDMA reads it. On ESP32-S3 internal SRAM is D-cache mapped; // CPU writes (memset/memcpy above) reach D-cache immediately but may not // reach physical SRAM for several µs. esp_cache_msync() would be ideal // but logs an unsilenceable error for internal SRAM addresses. // 200 µs at 240 MHz (~48 000 cycles) is sufficient for all dirty cache // lines to be written back naturally, with zero log noise. esp_rom_delay_us(200); spi_slave_transaction_t t = {}; t.length = wireSize * 8; // bits t.tx_buffer = ipcRespBuf; t.rx_buffer = ipcCmdBuf; // absorb NOP bytes clocked by RP2350 master // Queue the send — myPostSetupCb fires → HS HIGH → RP2350 clocks the data out. esp_err_t ret = spi_slave_transmit(FSPI_HOST, &t, timeout); if (ret != ESP_OK) { // spihost[FSPI_HOST] is NULL (corrupted by SPI ISR starvation during RP2350 // SPI pin glitch / CS floating). HS was NEVER raised — RP2350 will time out // on T3 HS and eventually give up. Restart immediately rather than waiting // for the next receiveBinaryCmd() to detect the same failure, which would // leave the RP2350 waiting the full ESP_HANDSHAKE_TIMEOUT (3 s) for T3 HS. // Do NOT restart — log the error and return failure. The CommandProcessor // loop will detect the error via receiveBinaryCmd and keep retrying. // WiFi/web must stay online for firmware updates. ESP_LOGE(FSPITAG, "sendBinaryResp: spi_slave_transmit failed (%s) — SPI degraded.", esp_err_to_name(ret)); } return ret; } // CRC32 — standard IEEE 802.3. Static helper exposed so SDCard can use it. uint32_t FSPI::crc32(const uint8_t *data, size_t len) { return esp_rom_crc32_le(0, data, (uint32_t) len); } // --------------------------------------------------------------------------- // Parameter parsing helper // --------------------------------------------------------------------------- bool FSPI::decodeParam(const char *paramStr, std::vector *paramVec) { char *token = strtok((char *) paramStr, ","); while (token) { while (*token == ' ' || *token == '\t') token++; size_t len = strlen(token); while (len > 0 && (token[len - 1] == ' ' || token[len - 1] == '\t')) len--; paramVec->push_back(std::string(token, len)); token = strtok(NULL, ","); } return true; } // --------------------------------------------------------------------------- // Legacy SPI methods — kept for backward compatibility. // --------------------------------------------------------------------------- esp_err_t FSPI::sendData(const uint8_t *data, size_t length, size_t timeout) { spi_slave_transaction_t t = {}; t.length = length * 8; t.tx_buffer = data; return spi_slave_transmit(FSPI_HOST, &t, (TickType_t) timeout); } esp_err_t FSPI::receiveData(uint8_t *data, size_t length, size_t timeout) { spi_slave_transaction_t t = {}; t.length = length * 8; t.rx_buffer = data; return spi_slave_transmit(FSPI_HOST, &t, (TickType_t) timeout); } bool FSPI::receiveCommand(char *cmd, size_t timeout) { bool result = false; int retries = 5; spi_slave_transaction_t t = {}; t.length = FSPI_CMDMSG_SIZE * 8; t.rx_buffer = cmd; while (gpio_get_level((gpio_num_t) CONFIG_BOOT) == 1 && retries-- > 0) { spi_slave_transmit(FSPI_HOST, &t, (TickType_t) timeout); if (cmd[0] != 0x00) { result = true; break; } } return result; } uint8_t FSPI::getResponse(size_t timeout) { uint8_t response; esp_err_t result; result = receiveData(dataBuf, FSPI_RESP_SIZE, timeout); if (result == ESP_OK && dataBuf[0] == 0xFF && dataBuf[1] == 0xFF) response = dataBuf[2]; else response = FSPI_RESP_NAK; return response; } esp_err_t FSPI::sendResponse(uint8_t resultCode, size_t length, size_t timeout) { for (int idx = 0; idx < FSPI_DATABLOCK_SIZE; idx++) dataBuf[idx] = (uint8_t) idx; dataBuf[0] = dataBuf[1] = 0xFF; dataBuf[2] = resultCode; return sendData(dataBuf, length, timeout); } esp_err_t FSPI::sendACK(size_t timeout) { return sendResponse(FSPI_RESP_ACK, FSPI_RESP_SIZE, timeout); } esp_err_t FSPI::sendNAK(size_t timeout) { return sendResponse(FSPI_RESP_NAK, FSPI_RESP_SIZE, timeout); } esp_err_t FSPI::sendABORT(size_t timeout) { return sendResponse(FSPI_RESP_ABORT, FSPI_RESP_SIZE, timeout); } uint8_t FSPI::calculateChecksum(uint8_t *data, size_t length) { uint8_t checksum = 0; for (size_t i = 0; i < length; i++) checksum ^= data[i]; return checksum; }