Files
2026-03-24 22:22:37 +00:00

646 lines
24 KiB
C++

/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: IO.c
// Created: Sep 2024
// Version: v1.0
// Author(s): Philip Smart
// Description: This source file contains wrappers and extensions to standard ESP32 services and
// hardware, specifically stdin, stdout etc. It is a CPP file albeit its contents are C,
// this is due to an issue with IDF whene it throws assertions when including FreeRTOS.h
// when compiling with gcc rather than g++.
// IDF has quite a few issues when using UART0, hence wrappers and work arounds.
// Credits:
// Copyright: (c) 2026 Philip Smart <philip.smart@net2net.org>
//
// History: Sep 2024 - Initial write based on logic from the tzpuPico project.
//
// 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 <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "esp_attr.h" // RTC_NOINIT_ATTR
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_app_format.h"
#include "esp_ota_ops.h"
#include "esp_system.h"
#include "esp_efuse.h"
#include "hal/gpio_hal.h"
#include "esp_efuse_table.h"
#include "esp_efuse_custom_table.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "sdkconfig.h"
#include "esp_vfs_fat.h"
#include "esp_vfs.h"
#include "sdmmc_cmd.h"
#include "driver/sdmmc_host.h"
#include "IO.h"
#ifdef __cplusplus
extern "C"
{
#endif
//////////////////////////////////////////////////////////////////////////
// Important:
//
// All configuration is performed via the 'idf.py menuconfig' command.
// The file 'sdkconfig' contains the configured parameter defines.
//////////////////////////////////////////////////////////////////////////
// RTC NOINIT memory — survives software resets.
// Set to OOB_RESTART_MAGIC before esp_restart() in the OOB handler so
// CommandProcessor can detect an OOB-triggered restart and use the fast
// 100 ms startup delay instead of the 3500 ms SPI-crash-recovery delay.
// On a power-on reset this will contain garbage — the magic value guards
// against the (vanishingly unlikely) case that garbage matches the constant.
#define OOB_RESTART_MAGIC 0xAA55CC33u
RTC_NOINIT_ATTR uint32_t g_oob_restart_magic;
// Flag set by OOB handler to tell CommandProcessor to reinitialize the SPI slave.
// The RP2350 sends OOB RESET_INPUT via UART after every reset, then sends a SPI
// NOP to unblock the ESP32 from spi_slave_transmit. The CommandProcessor checks
// this flag after each transaction and reinitializes if set.
volatile bool g_spi_clear_requested = false;
// Module globals.
static t_cmdFrame cmdFrame;
static t_Logger logger;
static char ioBuf[MAX_OOB_MSG_SIZE]; // The ioBuf is used to construct responses or commands to be sent to the rp2350.
#if defined(CONFIG_USE_RP2350_OUTPUT)
static SemaphoreHandle_t logMutex;
// VFS operations structure
static esp_vfs_t customVFS = {
.flags = ESP_VFS_FLAG_DEFAULT,
.write = IO_rp2350CustomWrite,
.read = NULL,
.open = NULL,
.close = NULL,
.fstat = NULL,
// Add more NULLs to silence -Wmissing-field-initializers if needed
};
#endif
// UART 0 Configuration if running in NORMAL mode, ie. debug goes to USB.
#if defined(CONFIG_USE_ESP32_USB_OUTPUT)
uart_config_t uartConfig = {
.baud_rate = 115200 * 4,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
#endif
// Getter to return address of temporary storage for forming Response or Command messages.
char *IO_getIoBuf(void)
{
return (ioBuf);
}
// Custom VFS write handler (no void* ctx)
#if defined(CONFIG_USE_RP2350_OUTPUT)
ssize_t IO_rp2350CustomWrite(int fd, const void *data, size_t size)
{
if (fd == STDOUT_FILENO)
{
// Process the data
for (size_t i = 0; i < size; i++)
{
// Use VFS to write to the UART.
// esp_vfs_write(_REENT, STDOUT_FILENO, &((const char *)data)[i], 1);
putchar((int) ((const char *) data)[i]);
}
return size;
}
return -1; // Unsupported fd
}
#endif
// Direct output handler, put string to stdout (uart0) no formatting, framing etc.
ssize_t IO_rp2350WriteString(const void *data, size_t size)
{
// Process the data
#if defined(CONFIG_USE_RP2350_OUTPUT)
for (size_t i = 0; i < size; i++)
{
putchar((int) ((const char *) data)[i]);
}
#endif
#if defined(CONFIG_USE_ESP32_USB_OUTPUT)
uart_write_bytes(UART_NUM_0, (const char *) data, size);
//ESP_LOGI(IOTAG, "Written %dbytes to UART0", size);
#endif
return size;
}
// Method to handle log output data enroute to the UART. As the ESP32 is behind an RP2350 which itself uses USB
// to create virtual COM ports, we frame the log data which in turn is detected, processed by the rp2350 and then routed
// to a virtual COM port.
#if defined(CONFIG_USE_RP2350_OUTPUT)
int IO_rp2350WriteLog(const char *fmt, va_list args)
{
// Locals.
static bool inFrame = false;
int retCount = 0;
// If not configured, exit.
if (logger.logBuffer == NULL)
return 0;
// Guard with a mutex as multiple threads can write to log at same time.
if (xSemaphoreTake(logMutex, portMAX_DELAY) == pdTRUE)
{
// In frame mode, put an STX and ETX around message, allows rp2350 to detect and process as needed.
if (logger.logMode == LOGGING_FRAMED || logger.logMode == LOGGING_FRAMED_VIEW || logger.logMode == LOGGING_FILE)
{
// If not inside a buffer frame, then add STX to start of frame and subsequent calls, until EOL are tagged onto end of buffer.
if (inFrame == false)
{
logger.logBuffer[logger.logPos++] = (logger.logMode == LOGGING_FRAMED || logger.logMode == LOGGING_FILE ? LOG_STX : 0x5b);
inFrame = true;
}
// Add new string to end of buffer then scan for EOL. If EOL found, add ETX and send.
retCount = vsnprintf(logger.logLine, MAX_LOG_LINE_SIZE, fmt, args);
for (int idx = 0; idx < retCount; idx++)
{
// Filter out CR's.
if (logger.logLine[idx] == 0x0d)
{
// End of buffer and a new frame open, close.
if (logger.logPos == 1 && idx == retCount - 1)
{
logger.logPos = 0;
inFrame = false;
}
}
else
// If the EOL terminator is found, then complete the frame and start a new one.
if (logger.logLine[idx] == 0x0a || logger.logPos == (MAX_LOG_BUFFER_SIZE - 3))
{
if (logger.logMode == LOGGING_FILE && logger.logFile != NULL)
{
// Write without frame characters.
fwrite(&logger.logBuffer[1], 1, (logger.logPos - 1), logger.logFile);
}
// Add ETX.
logger.logBuffer[logger.logPos++] = (logger.logMode == LOGGING_FRAMED || logger.logMode == LOGGING_FILE ? LOG_ETX : 0x5d);
// In framed view mode, terminate EOL.
if (logger.logMode == LOGGING_FRAMED_VIEW)
{
logger.logBuffer[logger.logPos] = 0x0a;
logger.logBuffer[logger.logPos + 1] = 0x0d;
logger.logPos = idx + 2;
}
// Send to stdout (hits my_vfs_write via /dev/uart/0)
fwrite(logger.logBuffer, 1, logger.logPos, stdout);
logger.logPos = 0;
// End of buffer, then close frame else start new frame.
if (idx == retCount - 1)
{
inFrame = false;
}
else
{
logger.logBuffer[logger.logPos++] = (logger.logMode == LOGGING_FRAMED || logger.logMode == LOGGING_FILE ? LOG_STX : 0x5b);
}
}
else
{
logger.logBuffer[logger.logPos++] = logger.logLine[idx];
}
}
}
else
// Normal mode, just apply original logic.
if (logger.logMode == LOGGING_NORMAL)
{
// Write directly to stdout.
retCount = vprintf(fmt, args);
// If logmode has changed then ensure buffer is reset.
if (logger.logPos > 0)
{
logger.logPos = 0;
inFrame = false;
}
}
else
{
// If logmode has changed then ensure buffer is reset.
if (logger.logPos > 0)
{
logger.logPos = 0;
inFrame = false;
}
}
// Release semaphore.
xSemaphoreGive(logMutex);
}
return retCount;
}
#endif
// Task to read stdin and push to queue
void IO_stdinReaderTask(void *pvParameters)
{
// Locals.
bool qFull = false;
uint32_t oobCommand = 0x00000000;
ESP_LOGI(IOTAG, "Starting stdin task");
while (1)
{
// If an input queue has been opened, we read the chars from stdin (RP2350) and buffer them for further processing.
if (cmdFrame.inputQueue != NULL)
{
char buf;
int rxChar = EOF;
#if defined(CONFIG_USE_RP2350_OUTPUT)
rxChar = getchar();
buf = (char) rxChar;
if (rxChar != EOF)
#elif defined(CONFIG_USE_ESP32_USB_OUTPUT)
size_t bytesWaiting;
uart_get_buffered_data_len(UART_NUM_0, &bytesWaiting);
if (bytesWaiting > 0)
{
rxChar = uart_read_bytes(UART_NUM_0, &buf, 1, 1);
}
if (rxChar == 1)
#endif
{
// Assemble out of band control commands.
oobCommand <<= 8;
oobCommand = (oobCommand & 0xFFFFFF00) | (uint8_t) buf;
// Process any recognised Out Of Band commands, if non recognised, default to queuing the char.
switch (oobCommand)
{
// The RP2350 sends IO_OOB_CMD_RESET_INPUT (0xAA5555AA) on startup, providing
// ~3 seconds of advance warning before it issues its first SPI command.
// During RP2350 reset the SPI SCK/CS pins float, causing the ESP32 SPI slave
// ISR to fire at MHz rates. This starves the FreeRTOS timer ISR and corrupts
// internal FreeRTOS state (e.g. s_timer_task → 0x39300000, invalid), leading to
// a "Cache disabled but cached memory region accessed" crash in
// vTaskGenericNotifyGiveFromISR when the timer ISR fires with the corrupted
// task handle.
//
// Fix: call spi_slave_free() HERE, while spihost is still valid, to properly
// deregister the SPI slave ISR *before* the SPI pins start glitching.
// Then restart ESP32 cleanly. The 3-second window between this OOB command
// and the RP2350's first SPI attempt gives us enough time to restart safely.
case IO_OOB_CMD_RESET_INPUT:
oobCommand = 0x00000000;
xQueueReset(cmdFrame.inputQueue);
ESP_LOGW(IOTAG,
"Clearing STDIN Q:%d,%d,%d,%s — restarting ESP32 cleanly.",
cmdFrame.inFrame,
cmdFrame.frameLength,
cmdFrame.frameReceived,
cmdFrame.frameBuffer);
cmdFrame.inFrame = false;
cmdFrame.frameLength = 0;
cmdFrame.frameReceived = false;
// Flag this restart as OOB-triggered so CommandProcessor uses
// the 100 ms startup delay (not 3500 ms SPI-crash-recovery delay).
// There is no mid-transaction state to recover from — the SPI slave
// is about to be freed cleanly below.
// Do NOT restart. Instead, signal CommandProcessor to
// reinitialize the SPI slave. The RP2350 will follow up
// with a NOP flush to unblock spi_slave_transmit.
ESP_LOGW(IOTAG, "OOB RESET_INPUT — requesting SPI slave reinit.");
g_spi_clear_requested = true;
break;
// Hard reboot of the ESP32 required?
case IO_OOB_CMD_RESET_MCU:
cmdFrame.reboot = true;
ESP_LOGW(IOTAG, "RESET MCU.");
break;
default:
// Queue the input char, it will either be recognised as part of a frame or taken by
// a stdin sink.
if (xQueueSend(cmdFrame.inputQueue, &buf, 10 / portTICK_PERIOD_MS) != pdTRUE)
{
if (!qFull)
{
ESP_LOGW(IOTAG, "Q full, drop:%c", buf);
qFull = true;
}
}
else
{
if (qFull)
{
ESP_LOGW(IOTAG, "Q emptying...");
qFull = false;
}
else
{
//ESP_LOGW(IOTAG, "(%c)", buf);
}
}
break;
}
}
}
vTaskDelay(1); // Yield
//vTaskDelay(10 / portTICK_PERIOD_MS); // Yield
}
}
// Custom getchar wrapper (framed or singular byte mode)
int IO_rp2350GetChar(bool framed)
{
// Locals.
int rxChar = EOF;
char buf;
// If no data in queue and not blocking, return EOF
if (cmdFrame.inputQueue != NULL && xQueueReceive(cmdFrame.inputQueue, &buf, 0) != pdTRUE)
{
return EOF;
}
else
// If no queue, read directly from stdin.
if (cmdFrame.inputQueue == NULL)
{
#if defined(CONFIG_USE_RP2350_OUTPUT)
rxChar = getchar();
buf = (uint8_t) rxChar;
// No character, exit.
if (rxChar == EOF)
return (EOF);
#elif defined(CONFIG_USE_ESP32_USB_OUTPUT)
size_t bytesWaiting;
uart_get_buffered_data_len(UART_NUM_0, &bytesWaiting);
if (bytesWaiting > 0)
{
rxChar = uart_read_bytes(UART_NUM_0, &buf, 1, 1);
}
// No character, exit.
if (rxChar == 0 || rxChar == EOF)
return (EOF);
ESP_LOGI(IOTAG, "Rx: %c (0x%x),%d", buf, buf, framed);
#endif
}
if (framed)
{
// Frame detection mode
if (buf == CMD_STX)
{
cmdFrame.inFrame = true;
cmdFrame.frameLength = 0;
cmdFrame.frameReceived = false;
//ESP_LOGI(IOTAG, "STX detected from RP2350");
return IO_rp2350GetChar(true); // Next char
}
if (cmdFrame.inFrame)
{
if (buf == CMD_ETX)
{
cmdFrame.inFrame = false;
cmdFrame.frameReceived = true;
//ESP_LOGI(IOTAG, "ETX detected, command received:%s, %d bytes", cmdFrame.frameBuffer, cmdFrame.frameLength);
return EOF;
}
if (cmdFrame.frameLength < FRAME_BUFFER_SIZE - 1)
{
cmdFrame.frameBuffer[cmdFrame.frameLength++] = buf;
cmdFrame.frameBuffer[cmdFrame.frameLength] = '\0';
}
else
{
ESP_LOGW(IOTAG, "Frame buffer overflow");
cmdFrame.inFrame = false;
cmdFrame.frameLength = 0;
}
return IO_rp2350GetChar(true); // Next char
}
// If the char hasnt been consumed, push back as it will be needed by stdin.
if (cmdFrame.inputQueue != NULL && xQueueSendToFront(cmdFrame.inputQueue, &buf, 10 / portTICK_PERIOD_MS) != pdTRUE)
{
ESP_LOGW(IOTAG, "Q full on push back, drop:%c", buf);
}
}
//ESP_LOGW(IOTAG, "Processed uart byte:%c, framed:%d", buf, framed);
// Non-framed mode: return the character directly
return ((int) buf);
}
// Retrieve framed command from RP2350
bool IO_rp2350GetCmdFrame(char *buffer, int max_len, int *out_len)
{
// If a frame hasnt been assembled, then try to build one if data waiting.
IO_rp2350GetChar(true);
// No frame, exit.
if (!cmdFrame.frameReceived)
{
return false;
}
int len = cmdFrame.frameLength;
if (len > max_len - 1)
{
len = max_len - 1;
}
memcpy(buffer, cmdFrame.frameBuffer, len);
memset(&buffer[len], 0x00, max_len - len);
*out_len = len;
cmdFrame.frameReceived = false;
//ESP_LOGI(IOTAG, "Command retrieved: %s", buffer);
return (true);
}
// Send framed response to RP2350
void IO_rp2350SendResponseFrame(const char *response)
{
#if defined(CONFIG_USE_RP2350_OUTPUT)
printf("%c%s%c", RSP_STX, response, RSP_ETX);
#elif defined(CONFIG_USE_ESP32_USB_OUTPUT)
char tmpbuf[256];
sprintf(tmpbuf, "%c%s%c", RSP_STX, response, RSP_ETX);
uart_write_bytes(UART_NUM_0, tmpbuf, strlen(tmpbuf));
//ESP_LOGI(IOTAG, "SendResponse: (%s)(%d) ", tmpbuf, strlen(tmpbuf));
#endif
}
// Send command to RP2350.
void IO_rp2350SendCommand(const char *cmd)
{
#if defined(CONFIG_USE_RP2350_OUTPUT)
printf("%c%s%c", CMD_STX, cmd, CMD_ETX);
#elif defined(CONFIG_USE_ESP32_USB_OUTPUT)
char tmpbuf[256];
sprintf(tmpbuf, "%c%s%c", CMD_STX, cmd, CMD_ETX);
uart_write_bytes(UART_NUM_0, tmpbuf, strlen(tmpbuf));
ESP_LOGI(IOTAG, "SendCommand: (%s)(%d) ", tmpbuf, strlen(tmpbuf));
#endif
}
// Change the logging mode for this ESP32.
bool IO_setLogMode(t_LogMode newLogMode)
{
// Locals
bool result = false;
t_LogMode updatedLogMode = newLogMode;
if (newLogMode < LOGGING_OFF || newLogMode > LOGGING_NORMAL)
return (result);
if (logger.logMode != LOGGING_FILE && logger.logFile != NULL)
{
fclose(logger.logFile);
logger.logFile = NULL;
}
if (logger.logMode == LOGGING_FILE && logger.logFile == NULL)
{
logger.logFile = fopen(IO_DEFAULT_LOG_FILE, "a");
if (logger.logFile == NULL)
{
ESP_LOGE(IOTAG, "Failed to open logging file:%s", IO_DEFAULT_LOG_FILE);
updatedLogMode = LOGGING_FRAMED;
}
else
{
result = true;
}
}
logger.logMode = updatedLogMode;
return (result);
}
// If a reboot has been signalled, then report back so the primary thread can coordinate reboots.
bool IO_doReboot(void)
{
return (cmdFrame.reboot);
}
// Initialisation.
bool IO_init(t_LogMode initialLogMode)
{
// Locals.
bool result = false;
// Initialise control for receiving frames.
cmdFrame.inFrame = false;
cmdFrame.frameLength = 0;
cmdFrame.frameReceived = false;
cmdFrame.reboot = false;
#if defined(CONFIG_USE_ESP32_USB_OUTPUT)
ESP_LOGI(IOTAG, "Initialising IO as independant USB for logging and UART for RP2350 comms.");
if (initialLogMode == LOGGING_NORMAL)
{
//uart_driver_install(UART_NUM_0, UART0_MAX_BUFFER_SIZE * 2, 0, 0, NULL, 0);
uart_driver_install(UART_NUM_0, UART0_MAX_BUFFER_SIZE * 2, UART0_MAX_BUFFER_SIZE, 0, NULL, 0);
uart_param_config(UART_NUM_0, &uartConfig);
uart_set_pin(UART_NUM_0, CONFIG_UART0_TX_PIN, CONFIG_UART0_RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
}
#endif
// Create input queue, to store stdin chars.
cmdFrame.inputQueue = xQueueCreate(INPUT_QUEUE_SIZE, sizeof(char));
if (cmdFrame.inputQueue != NULL)
{
// Start task to read stdin.
xTaskCreate(IO_stdinReaderTask, "IO_stdinReaderTask", 8192, NULL, 5, NULL);
}
#if defined(CONFIG_USE_RP2350_OUTPUT)
// Initialise logger mutex.
logMutex = xSemaphoreCreateMutex();
if (logMutex == NULL)
{
ESP_LOGE(IOTAG, "Failed to create logger Mutex.");
}
// Set a custom logging and input function, if memory permits, to allow us to control the log output and stdin input.
ESP_LOGI(IOTAG, "Initialising IO as logging and RP2350 comms routed through UART0 to RP2350.");
if ((logger.logBuffer = (char *) malloc(MAX_LOG_BUFFER_SIZE)) != NULL && (logger.logLine = (char *) malloc(MAX_LOG_LINE_SIZE)) != NULL)
{
logger.logMode = initialLogMode;
logger.logPos = 0;
// Register VFS for stdout only
esp_vfs_unregister("/dev/uart/0");
esp_err_t err = esp_vfs_register("/dev/uart/0", &customVFS, NULL);
if (err != ESP_OK)
{
ESP_LOGE(IOTAG, "VFS register failed: %d", err);
// Release memory as we cant use the extended stdin/stdout processing.
if (logger.logBuffer)
free(logger.logBuffer);
if (logger.logLine)
free(logger.logLine);
logger.logBuffer = logger.logLine = NULL;
}
else
{
ESP_LOGI(IOTAG, "VFS registered for stdout");
// Override vprintf used by the logger.
esp_log_set_vprintf(IO_rp2350WriteLog);
// Extended logger online.
result = true;
}
}
#endif
return (result);
}
#ifdef __cplusplus
}
#endif