New drivers: - MZ-1500 persona driver with MZ-700/MZ-1500 mode switch, PCG bank switching, PSG stubs, physical I/O forwarding for virtual mode - MZ-2200 persona driver (based on MZ-2000) - MZ-1R23/MZ-1R24 Kanji ROM / Dictionary ROM board (B8h/B9h IDM) - MZ-1R37 640KB EMM (ACh/ADh, no auto-increment, 20-bit addressing) - PIO-3034 320KB EMM (configurable base, 19-bit, auto-increment) - Celestite LAN/Memory composite board: - W5100 TCP/IP via ESP32 WiFi (connect, send, recv, ping) - Integrated MZ-1R12 32/64KB CMOS RAM with SD persistence - Integrated MZ-1R37 640KB EMM with SD persistence - UFM flash, unlock state machine, interrupt controller Celestite networking (Phase 2): - New IPC commands: NET_CFG, NET_SOCK, NET_SEND, NET_RECV, NET_PING - ESP32 BSD socket handlers with non-blocking connect and recv - Shared volatile struct for cross-core results (bypasses responseQueue) - Inline IDM read check for socket status and recv data - Z80 test programs: celestite_test.asm (17 tests), celestite_stress.asm (loop) MZ-1500 virtual mode fixes: - I/O writes forwarded to physical hardware (PSG, bank switching, PCG) - E000-E7FF always stays physical during bank switching - PCG bank (F000-FFFF) properly remapped to PHYSICAL when open QDF format support: - Japanese standard QD format auto-detected on load (81936 bytes) - Hunt pattern changed to 00+16 (mark+sync) for inter-block gap handling - Sync stripping handles long preambles (9+ bytes) - MZQDTool updated with -j flag and format conversion Other: - Debug shell load command: len parameter now optional - FSPI filename field: memcpy instead of strncpy for binary data - Interface availability expanded across MZ-700/1500/80A/2000/2200 - Web GUI: param hints for Celestite, updated driver interface lists Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1058 lines
43 KiB
C++
1058 lines
43 KiB
C++
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||
//
|
||
// Name: tzpuPico.cpp
|
||
// Created: Sep 2024
|
||
// Version: v1.0
|
||
// Author(s): Philip Smart
|
||
// Description: This source file contains the application logic to interface the RP2350 MPU with
|
||
// Bluetooth, WiFi, SD card services and custom interfaces.
|
||
//
|
||
// Please see the individual classes (singleton obiects) for a specific host logic.
|
||
//
|
||
// The application is configured via the Kconfig system. Use 'idf.py menuconfig' to
|
||
// configure.
|
||
// 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 <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <unistd.h>
|
||
#include <fstream>
|
||
#include <sstream>
|
||
#include <iostream>
|
||
#include <vector>
|
||
#include <iterator>
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#include "freertos/queue.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"
|
||
#include "NVS.h"
|
||
#include "WiFi.h"
|
||
#include "SDCard.h"
|
||
#include "cJSON.h"
|
||
#include "CommandProcessor.h"
|
||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
#include "tinyusb.h"
|
||
#include "tinyusb_net.h"
|
||
#include "tusb_cdc_acm.h"
|
||
#include "tusb_console.h"
|
||
#include "esp_netif.h"
|
||
#include "esp_mac.h"
|
||
#include "lwip/esp_netif_net_stack.h"
|
||
#endif
|
||
|
||
// Defined in CommandProcessor.cpp — ESP32→RP2350 reverse command dispatch.
|
||
extern void CP_queueCmd(const char *cmd); // async: fire and forget
|
||
extern bool CP_sendCmd(const char *cmd, uint32_t timeoutMs); // sync: wait for delivery
|
||
extern bool CP_initialSpiDone(void); // true once INF is processed
|
||
#include "tzpuPico.h"
|
||
|
||
//////////////////////////////////////////////////////////////////////////
|
||
// Important:
|
||
//
|
||
// All configuration is performed via the 'idf.py menuconfig' command.
|
||
// The file 'sdkconfig' contains the configured parameter defines.
|
||
//////////////////////////////////////////////////////////////////////////
|
||
|
||
// Configuration.
|
||
static t_tzpuPicoConfig tzpuPicoConfig;
|
||
|
||
// Overloads for the EFUSE Custom MAC definitions. Limited Efuse space and Custom MAC not needed in eFuse in this design
|
||
// so we overload with custom flags.
|
||
// 0-7 Reserved
|
||
// 8-15 defined as Base configuration and enhanced set 1.
|
||
static const esp_efuse_desc_t ENABLE_BT[] = {
|
||
{EFUSE_BLK3, 8, 1},
|
||
};
|
||
const esp_efuse_desc_t *ESP_EFUSE_ENABLE_BT[] = {&ENABLE_BT[0], NULL};
|
||
|
||
// Map of name to internal device value. This map is used for setting the personality of the ESP32 co-processor
|
||
// according to the emulated device.
|
||
static const t_StringValuePair deviceTypeMap[] = {
|
||
{TARGET_DEVICE_NAME_Z80, TARGET_DEVICE_Z80},
|
||
{TARGET_DEVICE_NAME_6502, TARGET_DEVICE_6502},
|
||
{TARGET_DEVICE_NAME_6512, TARGET_DEVICE_6512},
|
||
};
|
||
static const size_t deviceTypeMapSize = sizeof(deviceTypeMap) / sizeof(deviceTypeMap[0]);
|
||
|
||
// Method to check the efuse coding scheme is disabled. For this project it should be disabled.
|
||
bool checkEFUSE(void)
|
||
{
|
||
// Locals.
|
||
bool result = false;
|
||
size_t secureVersion = 0;
|
||
|
||
// Check the efuse coding scheme, should be NONE and the security version should be 0 for this project.
|
||
esp_efuse_coding_scheme_t coding_scheme = esp_efuse_get_coding_scheme(EFUSE_BLK3);
|
||
if (coding_scheme == EFUSE_CODING_SCHEME_NONE)
|
||
{
|
||
ESP_ERROR_CHECK(esp_efuse_read_field_cnt(ESP_EFUSE_SECURE_VERSION, &secureVersion));
|
||
if (secureVersion == 0)
|
||
{
|
||
result = true;
|
||
}
|
||
}
|
||
|
||
// True = efuse present and correct, false = not recognised.
|
||
return (result);
|
||
}
|
||
|
||
// Method to read out the stored configuration from EFUSE into the configuration structure
|
||
// for later appraisal.
|
||
bool readEFUSE(t_EFUSE &tzpuPicoEfuses)
|
||
{
|
||
// Locals.
|
||
bool result = true;
|
||
|
||
// Manually read each fuse value into the given structure, any failures treat as a complete failure.
|
||
result = esp_efuse_read_field_blob(ESP_EFUSE_HARDWARE_REVISION, &tzpuPicoEfuses.hardwareRevision, 16) == ESP_OK ? result : false;
|
||
tzpuPicoEfuses.hardwareRevision = __builtin_bswap16(tzpuPicoEfuses.hardwareRevision);
|
||
result = esp_efuse_read_field_blob(ESP_EFUSE_SERIAL_NO, &tzpuPicoEfuses.serialNo, 16) == ESP_OK ? result : false;
|
||
tzpuPicoEfuses.serialNo = __builtin_bswap16(tzpuPicoEfuses.serialNo);
|
||
result = esp_efuse_read_field_blob(ESP_EFUSE_BUILD_DATE, &tzpuPicoEfuses.buildDate, 24) == ESP_OK ? result : false;
|
||
result = esp_efuse_read_field_blob(ESP_EFUSE_DISABLE_RESTRICTIONS, &tzpuPicoEfuses.disableRestrictions, 1) == ESP_OK ? result : false;
|
||
result = esp_efuse_read_field_blob(ESP_EFUSE_ENABLE_BT, &tzpuPicoEfuses.enableBluetooth, 1) == ESP_OK ? result : false;
|
||
|
||
// Return true = successful read, false = failed to read efuse or values.
|
||
return (result);
|
||
}
|
||
|
||
// Method to write the configuration to one-time programmable FlashRAM EFuses. This setting persists for the life of the tzpuPico
|
||
// and so minimal information is stored which cant be wiped, everything else uses reprogrammable FlashRAM via NVS.
|
||
bool writeEFUSE(t_EFUSE &tzpuPicoEfuses)
|
||
{
|
||
// Locals.
|
||
bool result = true;
|
||
#ifdef CONFIG_EFUSE_VIRTUAL
|
||
|
||
// Write out the configuration structure member at a time.
|
||
result = esp_efuse_write_field_blob(ESP_EFUSE_HARDWARE_REVISION, &tzpuPicoEfuses.hardwareRevision, 16) == ESP_OK ? result : false;
|
||
result = esp_efuse_write_field_blob(ESP_EFUSE_SERIAL_NO, &tzpuPicoEfuses.serialNo, 16) == ESP_OK ? result : false;
|
||
result = esp_efuse_write_field_blob(ESP_EFUSE_BUILD_DATE, &tzpuPicoEfuses.buildDate, 24) == ESP_OK ? result : false;
|
||
result = esp_efuse_write_field_blob(ESP_EFUSE_DISABLE_RESTRICTIONS, &tzpuPicoEfuses.disableRestrictions, 1) == ESP_OK ? result : false;
|
||
result = esp_efuse_write_field_blob(ESP_EFUSE_ENABLE_BT, &tzpuPicoEfuses.enableBluetooth, 1) == ESP_OK ? result : false;
|
||
|
||
#endif // CONFIG_EFUSE_VIRTUAL
|
||
// Return true for success, false for 1 or more failures.
|
||
return (result);
|
||
}
|
||
|
||
// Method to return the application version number.
|
||
float version(void)
|
||
{
|
||
esp_app_desc_t runningAppInfo;
|
||
const esp_partition_t *runningApp;
|
||
double runningVersion = 0.00;
|
||
|
||
// Get details of the running application, specifically the version number.
|
||
runningApp = esp_ota_get_running_partition();
|
||
if (runningApp == NULL)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Cannot obtain running application information.");
|
||
}
|
||
else
|
||
{
|
||
// Get information on the running application image.
|
||
if (esp_ota_get_partition_description(runningApp, &runningAppInfo) == ESP_OK)
|
||
{
|
||
runningVersion = atof(runningAppInfo.version);
|
||
}
|
||
else
|
||
{
|
||
ESP_LOGE(MAINTAG, "Cannot obtain running application partition information.");
|
||
}
|
||
}
|
||
|
||
return (runningVersion);
|
||
}
|
||
|
||
// Method to startup the WiFi interface.
|
||
// Starting the WiFi method requires no Bluetooth or running host interface threads. It is started after a fresh boot. This is necessary due to the ESP IDF
|
||
// and hardware antenna constraints.
|
||
//
|
||
// Split into two phases to allow the CommandProcessor (SPI slave) to start as early as
|
||
// possible, so RP2350 commands are serviced before WiFi version-list SD reads complete:
|
||
//
|
||
// initWiFi() — allocates an empty version list and creates the WiFi object.
|
||
// Fast (~50 ms: NVS read + JSON parse + temp-dir stat).
|
||
// Call this BEFORE CommandProcessor::start().
|
||
//
|
||
// buildVersionList() — reads version files from SD card and populates the version list
|
||
// in-place. WiFi already holds the pointer so it sees the update
|
||
// automatically; no setter call needed.
|
||
// Call this AFTER CommandProcessor::start() — SPI is live by then.
|
||
//
|
||
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
|
||
WiFi *initWiFi(NVS &nvs, SDCard &sdcard, cJSON *esp32Config, bool defaultMode, uint16_t device, WiFi::t_versionList **versionListOut)
|
||
{
|
||
// Allocate an empty version list. WiFi stores this pointer directly; when
|
||
// buildVersionList() populates it in-place, WiFi sees the updated data automatically.
|
||
WiFi::t_versionList *versionList = new WiFi::t_versionList;
|
||
memset(versionList, 0, sizeof(WiFi::t_versionList));
|
||
*versionListOut = versionList;
|
||
|
||
return new WiFi(defaultMode, device, &nvs, &sdcard, esp32Config, SD_CARD_MOUNT_POINT, versionList);
|
||
}
|
||
|
||
void buildVersionList(WiFi::t_versionList *versionList, NVS &nvs, SDCard &sdcard)
|
||
{
|
||
std::istringstream list(TZPUPICO_MODULES);
|
||
std::vector<std::string> modules{std::istream_iterator<std::string>{list}, std::istream_iterator<std::string>{}};
|
||
for (int idx = 0; idx < (int) modules.size() && idx < WiFi::OBJECT_VERSION_LIST_MAX; idx++, versionList->elements = idx)
|
||
{
|
||
versionList->item[idx] = new WiFi::t_versionItem;
|
||
versionList->item[idx]->object = modules[idx];
|
||
if (modules[idx].compare("esp32") == 0)
|
||
{
|
||
// Read the ESP32 firmware version.
|
||
versionList->item[idx]->version = version();
|
||
}
|
||
else if (modules[idx].compare("tzpuPico") == 0)
|
||
{
|
||
// Look on the filesystem for the version file and read the first line contents as the tzpuPico project version number.
|
||
std::string ver = "0.00";
|
||
std::stringstream fqfn;
|
||
fqfn << SD_CARD_MOUNT_POINT << "/" << TZPUPICO_VERSION_FILE;
|
||
std::ifstream inFile;
|
||
inFile.open(fqfn.str());
|
||
if (inFile.is_open())
|
||
{
|
||
std::getline(inFile, ver);
|
||
}
|
||
inFile.close();
|
||
versionList->item[idx]->version = std::stof(ver);
|
||
}
|
||
else if (modules[idx].compare("NVS") == 0)
|
||
{
|
||
versionList->item[idx]->version = nvs.version();
|
||
}
|
||
else if (modules[idx].compare("WiFi") == 0)
|
||
{
|
||
WiFi *wifiIf = new WiFi();
|
||
versionList->item[idx]->version = wifiIf->version();
|
||
std::destroy_at(wifiIf);
|
||
}
|
||
else if (modules[idx].compare("FilePack") == 0)
|
||
{
|
||
// Look on the filesystem for the filepack version file and read the first line contents as the version number.
|
||
std::string ver = "0.00";
|
||
std::stringstream fqfn;
|
||
fqfn << SD_CARD_MOUNT_POINT << "/" << WiFi::FILEPACK_VERSION_FILE;
|
||
std::ifstream inFile;
|
||
inFile.open(fqfn.str());
|
||
if (inFile.is_open())
|
||
{
|
||
std::getline(inFile, ver);
|
||
}
|
||
inFile.close();
|
||
versionList->item[idx]->version = std::stof(ver);
|
||
}
|
||
else if (modules[idx].compare("WebFS") == 0)
|
||
{
|
||
// Look on the webfs filesystem for the version file and read the first line contents as the version number.
|
||
std::string ver = "0.00";
|
||
std::stringstream fqfn;
|
||
fqfn << SD_CARD_MOUNT_POINT << WiFi::WIFI_WEBFS_PATH << "/" << WiFi::WEBFS_VERSION_FILE;
|
||
std::ifstream inFile;
|
||
inFile.open(fqfn.str());
|
||
if (inFile.is_open())
|
||
{
|
||
std::getline(inFile, ver);
|
||
}
|
||
inFile.close();
|
||
versionList->item[idx]->version = std::stof(ver);
|
||
}
|
||
else
|
||
{
|
||
ESP_LOGE(MAINTAG, "Unknown class name in module configuration list:%s", modules[idx].c_str());
|
||
}
|
||
}
|
||
}
|
||
#endif // CONFIG_IF_WIFI_ENABLED || CONFIG_IF_USB_NCM_ENABLED
|
||
|
||
// USB NCM network interface setup.
|
||
// Creates a USB Ethernet (CDC-NCM) adapter on the ESP32-S3 USB OTG port (GPIO 19/20).
|
||
// The host PC sees a network adapter and can browse to the configured IP for configuration.
|
||
// This provides the same browser-based interface as WiFi but over USB, without requiring
|
||
// FCC/RED certification as no intentional RF emission is involved.
|
||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
|
||
static esp_netif_t *s_usb_ncm_netif = NULL;
|
||
static volatile bool g_usbReady = false; // Set by USB task when setup completes.
|
||
|
||
static void usb_ncm_l2_free(void *h, void *buffer)
|
||
{
|
||
free(buffer);
|
||
}
|
||
|
||
static esp_err_t usb_ncm_netif_transmit(void *h, void *buffer, size_t len)
|
||
{
|
||
if (tinyusb_net_send_sync(buffer, len, NULL, pdMS_TO_TICKS(1000)) != ESP_OK) {
|
||
ESP_LOGE(MAINTAG, "USB NCM: failed to send buffer");
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx)
|
||
{
|
||
if (s_usb_ncm_netif) {
|
||
void *buf_copy = malloc(len);
|
||
if (!buf_copy) {
|
||
return ESP_ERR_NO_MEM;
|
||
}
|
||
memcpy(buf_copy, buffer, len);
|
||
return esp_netif_receive(s_usb_ncm_netif, buf_copy, len, NULL);
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
// Phase 0: Install TinyUSB composite device (CDC-ACM + CDC-NCM), create the
|
||
// lwIP network interface, and redirect console to the CDC-ACM serial port.
|
||
//
|
||
// CRITICAL: The lwIP netif MUST be created before the host finishes USB
|
||
// enumeration and starts probing the NCM link (ARP/NDP). If the netif isn't
|
||
// ready, usb_ncm_recv_callback() drops all packets (s_usb_ncm_netif == NULL),
|
||
// the host gets no ARP replies, and marks the link as inactive.
|
||
//
|
||
// For this reason, esp_netif_init() and esp_event_loop_create_default() are
|
||
// called here (both are idempotent / tolerate double-init) so the netif can
|
||
// be created immediately after the TinyUSB NCM class is initialized.
|
||
// USB setup task — runs asynchronously so the main task can start the
|
||
// CommandProcessor (SPI slave) without waiting for USB delays. The 2-second
|
||
// disconnect/reconnect cycle that macOS needs would otherwise block SPI
|
||
// slave init, causing the RP2350's first sector reads to fail.
|
||
// Maximum number of disconnect/connect cycles to attempt before giving up.
|
||
static constexpr int USB_NCM_MAX_RETRIES = 3;
|
||
// Time to hold the bus disconnected so the host tears down its NCM driver.
|
||
static constexpr int USB_NCM_DISCONNECT_MS = 2500;
|
||
// Time to wait after tud_connect() for the host to begin enumeration.
|
||
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500;
|
||
// Polling interval while waiting for tud_mounted().
|
||
static constexpr int USB_NCM_POLL_MS = 100;
|
||
// Maximum time to wait for tud_mounted() after a connect before retrying.
|
||
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000;
|
||
// Time to wait after mount for the host DHCP client to obtain its IP.
|
||
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000;
|
||
|
||
void setupUSBTask(void *pvParameters)
|
||
{
|
||
bool cdcOk = false;
|
||
bool ncmOk = false;
|
||
|
||
// 0. Initialise the TCP/IP stack and event loop (idempotent / tolerates double-init).
|
||
esp_netif_init();
|
||
esp_event_loop_create_default();
|
||
|
||
// 1. Install TinyUSB driver on the OTG peripheral (GPIO 19/20).
|
||
tinyusb_config_t tusb_cfg = {};
|
||
tusb_cfg.external_phy = false;
|
||
esp_err_t ret = tinyusb_driver_install(&tusb_cfg);
|
||
if (ret != ESP_OK) {
|
||
ESP_LOGE(MAINTAG, "USB: TinyUSB driver install failed");
|
||
g_usbReady = true; // Signal ready (even on failure) so main loop doesn't wait forever.
|
||
vTaskDelete(NULL);
|
||
return;
|
||
}
|
||
|
||
// 2. Disconnect/reconnect with retry loop. macOS in particular can fail to
|
||
// enumerate an NCM device on the first attempt after a long power-off period
|
||
// because stale USB driver state persists in the kernel. Rather than using
|
||
// fixed blind delays, we poll tud_mounted() to confirm the host has actually
|
||
// completed enumeration, and retry the full cycle if it hasn't.
|
||
bool mounted = false;
|
||
for (int attempt = 1; attempt <= USB_NCM_MAX_RETRIES && !mounted; attempt++)
|
||
{
|
||
ESP_LOGI(MAINTAG, "USB: disconnect/connect attempt %d/%d", attempt, USB_NCM_MAX_RETRIES);
|
||
|
||
// Disconnect — hold long enough for the host to fully tear down.
|
||
tud_disconnect();
|
||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_DISCONNECT_MS));
|
||
|
||
// Reconnect and let the bus settle.
|
||
tud_connect();
|
||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
|
||
|
||
// Poll tud_mounted() — the host has completed enumeration when this returns true.
|
||
int waited = 0;
|
||
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
|
||
{
|
||
if (tud_mounted())
|
||
{
|
||
mounted = true;
|
||
ESP_LOGI(MAINTAG, "USB: host enumerated device after %d ms (attempt %d)", waited, attempt);
|
||
break;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
|
||
waited += USB_NCM_POLL_MS;
|
||
}
|
||
|
||
if (!mounted)
|
||
{
|
||
ESP_LOGW(MAINTAG, "USB: mount timeout after %d ms (attempt %d), %s",
|
||
USB_NCM_MOUNT_TIMEOUT_MS, attempt,
|
||
attempt < USB_NCM_MAX_RETRIES ? "retrying..." : "giving up");
|
||
}
|
||
}
|
||
|
||
if (!mounted)
|
||
{
|
||
ESP_LOGE(MAINTAG, "USB: host did not enumerate device after %d attempts", USB_NCM_MAX_RETRIES);
|
||
}
|
||
|
||
// 3. Initialise CDC-ACM for serial logging.
|
||
tinyusb_config_cdcacm_t acm_cfg = {};
|
||
ret = tusb_cdc_acm_init(&acm_cfg);
|
||
cdcOk = (ret == ESP_OK);
|
||
|
||
// 4. Initialise the NCM network class handler.
|
||
tinyusb_net_config_t net_config = {};
|
||
net_config.mac_addr[0] = 0x02; net_config.mac_addr[1] = 0x02;
|
||
net_config.mac_addr[2] = 0x11; net_config.mac_addr[3] = 0x22;
|
||
net_config.mac_addr[4] = 0x33; net_config.mac_addr[5] = 0x01;
|
||
net_config.on_recv_callback = usb_ncm_recv_callback;
|
||
ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config);
|
||
ncmOk = (ret == ESP_OK);
|
||
|
||
// 5. Create the lwIP netif so received packets are handled immediately.
|
||
if (ncmOk) {
|
||
esp_netif_ip_info_t ip_info = {};
|
||
ip_info.ip.addr = ipaddr_addr(CONFIG_IF_USB_NCM_IP);
|
||
ip_info.netmask.addr = ipaddr_addr(CONFIG_IF_USB_NCM_NETMASK);
|
||
ip_info.gw.addr = ip_info.ip.addr;
|
||
|
||
uint8_t lwip_mac[6] = {0x02, 0x02, 0x11, 0x22, 0x33, 0x02};
|
||
|
||
esp_netif_inherent_config_t base_cfg = {};
|
||
base_cfg.flags = (esp_netif_flags_t)(ESP_NETIF_DHCP_SERVER | ESP_NETIF_FLAG_AUTOUP);
|
||
base_cfg.ip_info = &ip_info;
|
||
base_cfg.if_key = "usbncm";
|
||
base_cfg.if_desc = "USB NCM network interface";
|
||
base_cfg.route_prio = 10;
|
||
|
||
esp_netif_driver_ifconfig_t driver_cfg = {};
|
||
driver_cfg.handle = (void *)1;
|
||
driver_cfg.transmit = usb_ncm_netif_transmit;
|
||
driver_cfg.driver_free_rx_buffer = usb_ncm_l2_free;
|
||
|
||
struct esp_netif_netstack_config lwip_netif_config = {};
|
||
lwip_netif_config.lwip.init_fn = ethernetif_init;
|
||
lwip_netif_config.lwip.input_fn = ethernetif_input;
|
||
|
||
esp_netif_config_t cfg = {};
|
||
cfg.base = &base_cfg;
|
||
cfg.driver = &driver_cfg;
|
||
cfg.stack = &lwip_netif_config;
|
||
|
||
s_usb_ncm_netif = esp_netif_new(&cfg);
|
||
if (s_usb_ncm_netif != NULL) {
|
||
esp_netif_set_mac(s_usb_ncm_netif, lwip_mac);
|
||
uint32_t lease_opt = 120;
|
||
esp_netif_dhcps_option(s_usb_ncm_netif, ESP_NETIF_OP_SET,
|
||
ESP_NETIF_IP_ADDRESS_LEASE_TIME, &lease_opt, sizeof(lease_opt));
|
||
esp_netif_action_start(s_usb_ncm_netif, 0, 0, 0);
|
||
|
||
// Notify the USB host that the NCM network link is up. Without this
|
||
// the host never receives a ConnectionSpeedChange notification and will
|
||
// not start its DHCP client, leaving the interface in "no IP" state.
|
||
if (mounted) {
|
||
vTaskDelay(pdMS_TO_TICKS(200)); // Let netif settle before signalling host.
|
||
tud_network_link_state(0, true);
|
||
ESP_LOGI(MAINTAG, "USB NCM: link state set to UP");
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6. Redirect console to TinyUSB CDC-ACM.
|
||
if (cdcOk) {
|
||
esp_tusb_init_console(TINYUSB_CDC_ACM_0);
|
||
}
|
||
|
||
// 7. Wait for the host to complete DHCP and bring the network link up.
|
||
// Poll esp_netif_is_netif_up() rather than using a blind delay so we
|
||
// proceed as soon as the link is ready (or time out gracefully).
|
||
if (ncmOk && mounted && s_usb_ncm_netif != NULL) {
|
||
ESP_LOGI(MAINTAG, "USB NCM: waiting for host DHCP...");
|
||
int dhcpWait = 0;
|
||
while (dhcpWait < USB_NCM_DHCP_WAIT_MS) {
|
||
if (esp_netif_is_netif_up(s_usb_ncm_netif)) {
|
||
ESP_LOGI(MAINTAG, "USB NCM: netif is up after %d ms", dhcpWait);
|
||
break;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
|
||
dhcpWait += USB_NCM_POLL_MS;
|
||
}
|
||
// Allow a little extra time for the host DHCP client to finish even
|
||
// after the interface reports up.
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
}
|
||
|
||
ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s mounted:%s)",
|
||
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL", mounted ? "YES" : "NO");
|
||
g_usbReady = true;
|
||
vTaskDelete(NULL);
|
||
}
|
||
#endif // CONFIG_IF_USB_NCM_ENABLED
|
||
|
||
// Setup phase 1 — fast path: eFUSE, NVS, SPI slave.
|
||
// Called before CommandProcessor::start() so the SPI slave is listening as
|
||
// early as possible (~60 ms after power-on). The SD card is NOT initialised
|
||
// here — that happens in setupSDCard() after CommandProcessor has started.
|
||
//
|
||
// Why the split matters: the RP2350 RFS driver sends an automatic intercore
|
||
// sector-0 read during Z80 SD card initialisation (~500 ms after power-on).
|
||
// Core 0 waits up to ESP_HANDSHAKE_TIMEOUT (3 s) for the ESP32 HS signal.
|
||
// The Z80's own SD-command timeout is much shorter (~500 ms–1 s), so if the
|
||
// ESP32 is not yet listening when the first request arrives the Z80 times out,
|
||
// SD init fails, and the user must manually retry (IC).
|
||
// By starting the SPI slave before sdcard.init(), the ESP32 is ready to respond
|
||
// well before the Z80's first SD request.
|
||
//
|
||
void setupEarly(NVS &nvs, FSPI &fspi)
|
||
{
|
||
// Locals.
|
||
bool eFuseInvalid = false;
|
||
t_EFUSE tzpuPicoEfuses;
|
||
|
||
// Check the efuse and retrieve configured values for later appraisal.
|
||
if (checkEFUSE() == false)
|
||
{
|
||
eFuseInvalid = true;
|
||
}
|
||
memset((void *) &tzpuPicoEfuses, 0x00, sizeof(t_EFUSE));
|
||
if (readEFUSE(tzpuPicoEfuses) == true)
|
||
{
|
||
// If the hw revision, build date and/or serial number havent been set, ie. an unconfigured ESP32 eFuse, obsfucate it.
|
||
if (tzpuPicoEfuses.hardwareRevision == 0)
|
||
{
|
||
tzpuPicoEfuses.hardwareRevision = 1300;
|
||
}
|
||
if (tzpuPicoEfuses.buildDate[0] == 0)
|
||
{
|
||
tzpuPicoEfuses.buildDate[0] = 1;
|
||
tzpuPicoEfuses.buildDate[1] = 6;
|
||
tzpuPicoEfuses.buildDate[2] = 22;
|
||
}
|
||
if (tzpuPicoEfuses.serialNo == 0)
|
||
{
|
||
tzpuPicoEfuses.serialNo = (uint16_t) ((rand() * 65534) + 1);
|
||
}
|
||
// Bug in Efuse programming workaround.
|
||
if (tzpuPicoEfuses.buildDate[0] == 31 && tzpuPicoEfuses.buildDate[1] == 6)
|
||
{
|
||
tzpuPicoEfuses.buildDate[0] = 1;
|
||
}
|
||
ESP_LOGW(MAINTAG,
|
||
"EFUSE:Hardware Rev=%f, Build Date:%d/%d/%d, Serial Number:%05d %s%s",
|
||
((float) tzpuPicoEfuses.hardwareRevision) / 1000,
|
||
tzpuPicoEfuses.buildDate[0],
|
||
tzpuPicoEfuses.buildDate[1],
|
||
tzpuPicoEfuses.buildDate[2],
|
||
tzpuPicoEfuses.serialNo,
|
||
tzpuPicoEfuses.disableRestrictions == true ? "disableRestrictions" : " ",
|
||
tzpuPicoEfuses.enableBluetooth == true ? "enableBluetooth" : " ");
|
||
}
|
||
else
|
||
{
|
||
eFuseInvalid = true;
|
||
ESP_LOGW(MAINTAG, "EFUSE not programmed/readable.");
|
||
}
|
||
#if defined(CONFIG_DISABLE_FEATURE_SECURITY)
|
||
tzpuPicoEfuses.disableRestrictions = true;
|
||
#endif
|
||
|
||
// Initialize NVS — loads tzpuPicoConfig (bootMode, deviceMode) used by WiFi init.
|
||
nvs.init();
|
||
|
||
if (nvs.open(TZPUPICO_NAME) == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Error opening NVS handle with key (%s)!\n", TZPUPICO_NAME);
|
||
}
|
||
|
||
if (nvs.retrieveData(TZPUPICO_NAME, &tzpuPicoConfig, sizeof(t_tzpuPicoConfig)) == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "tzpuPico configuration set to default, no valid config found in NVS.");
|
||
tzpuPicoConfig.params.bootMode = 0;
|
||
tzpuPicoConfig.params.deviceMode = 0;
|
||
|
||
if (nvs.persistData(TZPUPICO_NAME, &tzpuPicoConfig, sizeof(t_tzpuPicoConfig)) == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Persisting Default tzpuPico configuration data failed, check NVS setup.");
|
||
}
|
||
else if (nvs.commitData() == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
||
}
|
||
}
|
||
|
||
// Drive HS (handshake) LOW now so RP2350 sees "not ready" and waits.
|
||
// The SPI slave ISR is NOT registered here — that happens in
|
||
// CommandProcessor::waitForCommand() after the startup delay, by which
|
||
// time the RP2350 has finished its own FSPI_init() and is driving CS HIGH.
|
||
// Registering the ISR early (before RP2350 drives CS) causes ISR starvation:
|
||
// RP2350 GPIO 45 (CS) floats while PIO state-machine signals on the Z80 bus
|
||
// capacitively couple noise into it, triggering the SPI ISR at MHz rates and
|
||
// corrupting spihost[SPI2_HOST] in BSS memory.
|
||
{
|
||
gpio_config_t hsCfg = {};
|
||
hsCfg.pin_bit_mask = (1ULL << CONFIG_HS);
|
||
hsCfg.mode = GPIO_MODE_OUTPUT;
|
||
hsCfg.pull_up_en = GPIO_PULLUP_ENABLE;
|
||
hsCfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||
hsCfg.intr_type = GPIO_INTR_DISABLE;
|
||
gpio_config(&hsCfg);
|
||
gpio_set_level((gpio_num_t) CONFIG_HS, 0);
|
||
}
|
||
}
|
||
|
||
// Setup phase 2 — slow path: SD card.
|
||
// Called AFTER CommandProcessor::start() while the SPI slave is already
|
||
// listening. If a sector read arrives before this completes it will return
|
||
// IPCF_STATUS_NOTFOUND (fopen fails on an unmounted fs), which the RP2350
|
||
// treats as "SD busy" and retries — well within the Z80's SD timeout.
|
||
//
|
||
void setupSDCard(SDCard &sdcard)
|
||
{
|
||
sdcard.init();
|
||
ESP_LOGW(MAINTAG, "Free Heap (%d)", xPortGetFreeHeapSize());
|
||
}
|
||
|
||
// Method to setup any internal options based on the JSON configuration.
|
||
void setupJSON(NVS &nvs, SDCard &sdcard, FSPI &fspi, cJSON *config)
|
||
{
|
||
// Locals.
|
||
//
|
||
uint16_t deviceValue = 0xFFFF;
|
||
|
||
// Get the core object.
|
||
cJSON *coreObj = cJSON_GetObjectItem(config, "core");
|
||
if (!cJSON_IsObject(coreObj))
|
||
{
|
||
ESP_LOGE(MAINTAG, "Error: 'core' is not an object\n");
|
||
return;
|
||
}
|
||
|
||
// Extract fields
|
||
cJSON *device = cJSON_GetObjectItem(coreObj, "device");
|
||
cJSON *bootMode = cJSON_GetObjectItem(coreObj, "mode");
|
||
|
||
// No values, then exit.
|
||
if (!cJSON_IsString(device) && !cJSON_IsNumber(bootMode))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Validate device is a string.
|
||
if (!cJSON_IsString(device))
|
||
{
|
||
ESP_LOGE(MAINTAG, "Error: 'core:device' is not a string\n");
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
// Iterate through the lookup table
|
||
for (size_t i = 0; i < deviceTypeMapSize; i++)
|
||
{
|
||
const char *constant = deviceTypeMap[i].constant;
|
||
size_t constantLen = strlen(constant);
|
||
// Compare case-insensitively and ensure exact match
|
||
if (strncasecmp(device->valuestring, constant, constantLen) == 0 && strlen(device->valuestring) == constantLen)
|
||
{
|
||
deviceValue = deviceTypeMap[i].value;
|
||
}
|
||
}
|
||
|
||
// No match found, error.
|
||
if (deviceValue == 0xFFFF)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Error: 'core:device' value is not valid\n");
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Check override flag, exit if not valid.
|
||
if (!cJSON_IsNumber(bootMode) || bootMode->valueint < 0 || bootMode->valueint > 1)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Error: 'core:mode' is not numeric, it should be 1 for AP, 0 for client.\n");
|
||
return;
|
||
}
|
||
|
||
// If the parameters have changed, update run values and persist.
|
||
if (deviceValue != tzpuPicoConfig.params.deviceMode || bootMode->valueint != tzpuPicoConfig.params.bootMode)
|
||
{
|
||
// Save to internal parameters.
|
||
tzpuPicoConfig.params.deviceMode = deviceValue;
|
||
tzpuPicoConfig.params.bootMode = bootMode->valueint;
|
||
|
||
// Persist the data for next time.
|
||
if (nvs.persistData(TZPUPICO_NAME, &tzpuPicoConfig, sizeof(t_tzpuPicoConfig)) == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Persisting JSON configured tzpuPico configuration data failed, check NVS setup.");
|
||
}
|
||
// No other updates so make a commit here to ensure data is flushed and written.
|
||
else if (nvs.commitData() == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
// Method to read the configuration parameters from the json config file. Parameters used to
|
||
// setup or override internal settings at user control.
|
||
cJSON *readJSONConfig(const char *filename, bool *rp2350ConfigChanged)
|
||
{
|
||
// Lcoals.
|
||
char *buffer;
|
||
std::string configFile = std::string(SD_CARD_MOUNT_POINT) + "/" + filename;
|
||
std::string checksumFile = configFile + ".chk";
|
||
|
||
ESP_LOGI(MAINTAG, "Config File: %s", configFile.c_str());
|
||
|
||
// Open the JSON config file and store into buffer.
|
||
std::ifstream inFile(configFile);
|
||
if (inFile.is_open())
|
||
{
|
||
// Get size of file.
|
||
inFile.seekg(0, std::ios::end);
|
||
std::streamsize size = inFile.tellg();
|
||
inFile.seekg(0, std::ios::beg);
|
||
|
||
// Create a temporary buffer to store the JSON text.
|
||
buffer = (char *) malloc(size + 1);
|
||
if (!buffer)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Failed to allocate buffer (of size %d) for JSON config.", size);
|
||
return (NULL);
|
||
}
|
||
inFile.read(buffer, size);
|
||
buffer[size] = 0x00;
|
||
inFile.close();
|
||
|
||
// Get checksum of buffer to determine if the config has changed.
|
||
int chkSum = 0;
|
||
for (int idx = 0; idx < size; idx++)
|
||
{
|
||
chkSum += (int) buffer[idx];
|
||
}
|
||
ESP_LOGI(MAINTAG, "File: %s, Checksum: %d", configFile.c_str(), chkSum);
|
||
|
||
// Get current checksum, to detect if the file has changed.
|
||
std::string chkSumBuf = "0";
|
||
std::ifstream chkFile(checksumFile);
|
||
if (chkFile.is_open())
|
||
{
|
||
std::getline(chkFile, chkSumBuf);
|
||
ESP_LOGI(MAINTAG, "Read of previous Checksum value: %s", chkSumBuf.c_str());
|
||
inFile.close();
|
||
}
|
||
// If the checksum is invalid or changed set flag to indicate condition.
|
||
if (std::stoi(chkSumBuf, nullptr, 10) != chkSum)
|
||
{
|
||
*rp2350ConfigChanged = true;
|
||
|
||
// Update the checksum file for next time.
|
||
std::ofstream outFile(checksumFile);
|
||
if (outFile.is_open())
|
||
{
|
||
outFile << chkSum << std::endl;
|
||
ESP_LOGI(MAINTAG, "Wrote checksum %d to: %s", chkSum, checksumFile.c_str());
|
||
outFile.close();
|
||
}
|
||
else
|
||
{
|
||
ESP_LOGE(MAINTAG, "FAILED to open checksum file for writing: %s", checksumFile.c_str());
|
||
}
|
||
}
|
||
|
||
// Parse JSON, it contains config for both rp2350 and esp32. Free up text based JSON buffer once parsed.
|
||
cJSON *root = cJSON_Parse(buffer);
|
||
free(buffer);
|
||
if (!root)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Failed to parse JSON: %s", cJSON_GetErrorPtr());
|
||
return (NULL);
|
||
}
|
||
|
||
// Detach the esp32 configuration as we dont want to waste memory holding the rp2350 config.
|
||
cJSON *esp32 = cJSON_DetachItemFromObject(root, "esp32");
|
||
if (!esp32)
|
||
{
|
||
ESP_LOGE(MAINTAG, "Failed to detach 'esp32' object from JSON config: %s", cJSON_GetErrorPtr());
|
||
return (NULL);
|
||
}
|
||
|
||
// Remove the original root containing rp2350 config.
|
||
cJSON_Delete(root);
|
||
return (esp32);
|
||
}
|
||
else
|
||
{
|
||
ESP_LOGE(MAINTAG, "Failed to open JSON config file: %s", configFile.c_str());
|
||
return (NULL);
|
||
}
|
||
}
|
||
|
||
#ifdef __cplusplus
|
||
extern "C"
|
||
{
|
||
#endif
|
||
|
||
// ESP-IDF Application entry point.
|
||
//
|
||
void app_main()
|
||
{
|
||
// Log the reset reason FIRST — critical for diagnosing unexpected reboots.
|
||
esp_reset_reason_t rstReason = esp_reset_reason();
|
||
const char *rstName;
|
||
switch (rstReason)
|
||
{
|
||
case ESP_RST_POWERON: rstName = "POWERON"; break;
|
||
case ESP_RST_EXT: rstName = "EXT_PIN"; break;
|
||
case ESP_RST_SW: rstName = "SW_RESET"; break;
|
||
case ESP_RST_PANIC: rstName = "PANIC"; break;
|
||
case ESP_RST_INT_WDT: rstName = "INT_WDT"; break;
|
||
case ESP_RST_TASK_WDT: rstName = "TASK_WDT"; break;
|
||
case ESP_RST_WDT: rstName = "OTHER_WDT"; break;
|
||
case ESP_RST_BROWNOUT: rstName = "BROWNOUT"; break;
|
||
default: rstName = "UNKNOWN"; break;
|
||
}
|
||
ESP_LOGW(MAINTAG, "=== ESP32 BOOT === reset reason: %d (%s)", (int) rstReason, rstName);
|
||
|
||
// Locals.
|
||
static NVS nvs;
|
||
static SDCard sdcard;
|
||
static FSPI fspi;
|
||
static cJSON *esp32Config;
|
||
bool rp2350ConfigChanged = false;
|
||
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
WiFi *wifi;
|
||
#endif
|
||
static int loopCount = 0;
|
||
static bool wifiStarted = false;
|
||
static RTC_NOINIT_ATTR int powerOn = 1;
|
||
|
||
// On power on, reset the rp2350. Temp code as hardware sequencing via RC needs to be used.
|
||
if (powerOn)
|
||
{
|
||
// gpio_set_direction((gpio_num_t)CONFIG_RP2350_RUN, GPIO_MODE_OUTPUT);
|
||
// gpio_set_level((gpio_num_t)CONFIG_RP2350_RUN, 0);
|
||
// vTaskDelay(100);
|
||
// gpio_set_direction((gpio_num_t)CONFIG_RP2350_RUN, GPIO_MODE_INPUT);
|
||
// gpio_set_level((gpio_num_t)CONFIG_RP2350_RUN, 1);
|
||
powerOn = 0;
|
||
}
|
||
|
||
// Initialise IO helpers and wrappers (not GPIO but C based IO such as stream, stdin, stdout etc).
|
||
//
|
||
ESP_LOGW(MAINTAG, "Initialising IO.");
|
||
#if defined(CONFIG_USE_ESP32_USB_OUTPUT)
|
||
IO_init(LOGGING_NORMAL);
|
||
#elif defined(CONFIG_USE_RP2350_OUTPUT)
|
||
IO_init(LOGGING_FRAMED);
|
||
#else
|
||
IO_init(LOGGING_FRAMED);
|
||
#endif
|
||
|
||
// Launch USB setup (TinyUSB + CDC + NCM + netif) as a background task.
|
||
// The 2-second disconnect/reconnect cycle runs asynchronously so the main
|
||
// task can proceed immediately to start the CommandProcessor (SPI slave).
|
||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
xTaskCreate(setupUSBTask, "usbSetup", 8192, NULL, 5, NULL);
|
||
#endif
|
||
|
||
// Phase 1: fast setup — eFUSE, NVS, SPI slave (~60 ms).
|
||
// NVS loads tzpuPicoConfig (bootMode, deviceMode) needed by initWiFi().
|
||
ESP_LOGW(MAINTAG, "Initialising hardware (phase 1: NVS + SPI).");
|
||
setupEarly(nvs, fspi);
|
||
|
||
// Create WiFi/Web object immediately (NVS credentials, empty version list, ~10 ms).
|
||
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
ESP_LOGW(MAINTAG, "Initialising Web interface object.");
|
||
WiFi::t_versionList *versionList;
|
||
wifi = initWiFi(nvs, sdcard, NULL, tzpuPicoConfig.params.bootMode, tzpuPicoConfig.params.deviceMode, &versionList);
|
||
#endif
|
||
|
||
// Start CommandProcessor — SPI slave is NOW listening for RP2350 commands.
|
||
// Total time from power-on to here: ~170 ms (60 ms setup + 10 ms WiFi + 100 ms task delay).
|
||
// The RP2350 RFS driver sends an automatic sector-0 read during Z80 SD card init
|
||
// (~500 ms after power-on). With the SPI slave already up, the ESP32 will respond
|
||
// in time — either with the sector (SD ready) or with an immediate NOTFOUND error
|
||
// (SD still initialising), which the Z80 treats as "SD busy" and retries.
|
||
// Previously ESP_HANDSHAKE_TIMEOUT (3 s) caused the RP2350 to hold the Z80 SD
|
||
// command pending for 3 s while the Z80's own shorter timeout had already expired.
|
||
ESP_LOGW(MAINTAG, "Starting Command Processor");
|
||
CommandProcessor processor(*wifi, fspi, sdcard, esp32Config); // esp32Config NULL, OK for sector ops
|
||
processor.start();
|
||
|
||
// Phase 2: slow setup — SD card (~400 ms), runs concurrently with CommandProcessor.
|
||
// Any sector reads that arrive during sdcard.init() will fail immediately with
|
||
// NOTFOUND (fopen on unmounted fs) and will be retried by the RP2350.
|
||
ESP_LOGW(MAINTAG, "Initialising hardware (phase 2: SD card).");
|
||
setupSDCard(sdcard);
|
||
|
||
// JSON config and version list — SD is now mounted.
|
||
esp32Config = readJSONConfig(TZPUPICO_CONFIG_FILE, &rp2350ConfigChanged);
|
||
setupJSON(nvs, sdcard, fspi, esp32Config);
|
||
|
||
// Change to logging into a file now that SD is online.
|
||
//IO_setLogMode(LOGGING_FILE);
|
||
|
||
// Populate the version list in-place — WiFi holds the pointer and sees it automatically.
|
||
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
ESP_LOGW(MAINTAG, "Building version list.");
|
||
buildVersionList(versionList, nvs, sdcard);
|
||
#endif
|
||
|
||
// Loop waiting on callback events and action accordingly.
|
||
while (true)
|
||
{
|
||
// Change in boot mode requires persisting and reboot.
|
||
//
|
||
if ((tzpuPicoConfig.params.bootMode & 0xff00) != 0)
|
||
{
|
||
// Set boot mode to wifi, save and restart.
|
||
//
|
||
ESP_LOGW(MAINTAG, "Persisting WiFi mode.");
|
||
tzpuPicoConfig.params.bootMode &= 0x00ff;
|
||
if (nvs.persistData(TZPUPICO_NAME, &tzpuPicoConfig, sizeof(t_tzpuPicoConfig)) == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Persisting tzpuPico configuration data failed, updates will not persist in future power cycles.");
|
||
}
|
||
else
|
||
// Few other updates so make a commit here to ensure data is flushed and written.
|
||
if (nvs.commitData() == false)
|
||
{
|
||
ESP_LOGW(MAINTAG, "NVS Commit writes operation failed, some previous writes may not persist in future power cycles.");
|
||
}
|
||
|
||
// Restart and the tzpuPico will come up in Wifi mode.
|
||
esp_restart();
|
||
}
|
||
|
||
// Piggy backing off the bootMode is a flag to indicate NVS flash erase and reboot.
|
||
//
|
||
if ((tzpuPicoConfig.params.bootMode & 0xff00) != 0 && (tzpuPicoConfig.params.bootMode & 0x00ff) == 255)
|
||
{
|
||
// Close out NVS and erase.
|
||
nvs.eraseAll();
|
||
|
||
// Need to reboot as the in-memory config still holds the old settings.
|
||
esp_restart();
|
||
}
|
||
|
||
// Check to see if a reboot is requested by user via web interface.
|
||
#if defined(CONFIG_IF_WIFI_ENABLED) || defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
if (wifi->doReboot() == true)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Web interface reboot requested..");
|
||
esp_restart();
|
||
}
|
||
#endif
|
||
|
||
// Check to see if a reboot is requested by rp2350.
|
||
if (IO_doReboot() == true)
|
||
{
|
||
ESP_LOGW(MAINTAG, "IO reboot requested..");
|
||
esp_restart();
|
||
}
|
||
|
||
// INF command disabled: the D-cache coherency issue causes the 68-byte T3
|
||
// response to arrive as zeros, and the exchange adds instability during
|
||
// startup. RP2350 version info for the web interface is non-critical.
|
||
|
||
// If the rp2350 JSON config changes, send a command to the rp2350 to force a re-read.
|
||
// Uses the SPI IPC reverse-command queue — the RP2350 picks it up on its next
|
||
// NOP poll (~100 ms). CFG0 means "reload config for the currently active app".
|
||
if (rp2350ConfigChanged == true)
|
||
{
|
||
ESP_LOGW(MAINTAG, "Request RP2350 Update Config via SPI IPC");
|
||
rp2350ConfigChanged = false;
|
||
CP_queueCmd("CFG0");
|
||
}
|
||
|
||
// Start network interfaces ~1 s after the main loop begins (loopCount > 100
|
||
// at 10 ms/iter). The short delay gives the SPI slave (initialized at the end
|
||
// of the CommandProcessor startup delay) a moment to settle and handle the
|
||
// RP2350's first sector reads before the network stack comes up.
|
||
++loopCount;
|
||
if (!wifiStarted && loopCount > 100)
|
||
{
|
||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||
// Wait for the async USB setup task to complete before starting
|
||
// the webserver. This is non-blocking in the sense that the SPI
|
||
// slave and SD card are already running while we wait here.
|
||
if (!g_usbReady)
|
||
{
|
||
// USB task still running — skip this iteration, try next loop.
|
||
++loopCount;
|
||
vTaskDelay(10);
|
||
continue;
|
||
}
|
||
// USB NCM network + lwIP netif are now ready.
|
||
ESP_LOGW(MAINTAG, "Starting webserver on USB NCM.");
|
||
wifi->startWebserver();
|
||
#endif
|
||
|
||
#if defined(CONFIG_IF_WIFI_ENABLED)
|
||
// Start WiFi (AP or Client mode). When USB NCM has already started
|
||
// the webserver, the WiFi event handlers detect that server != NULL
|
||
// and skip the redundant startWebserver() call.
|
||
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
|
||
wifi->run();
|
||
#endif
|
||
wifiStarted = true;
|
||
}
|
||
|
||
// Sleep, not much to be done other than look at event flags.
|
||
vTaskDelay(10);
|
||
}
|
||
|
||
// Lost in space.... this thread is no longer required!
|
||
}
|
||
|
||
#ifdef __cplusplus
|
||
}
|
||
#endif
|