355 lines
14 KiB
C++
355 lines
14 KiB
C++
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
//
|
||
// 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 <philip.smart@net2net.org>
|
||
//
|
||
// 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 <http://www.gnu.org/licenses/>.
|
||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
#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 <string>
|
||
#include <unordered_map>
|
||
#include <functional>
|
||
#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<std::string> *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;
|
||
}
|