///////////////////////////////////////////////////////////////////////////////////////////////////////// // // 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 // // 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 . ///////////////////////////////////////////////////////////////////////////////////////////////////////// #include #include #include #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