Files
pico/projects/tzpuPico/esp32/main/FSPI.cpp
2026-03-24 22:22:37 +00:00

355 lines
14 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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 13 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;
}