Files
pico/projects/tzpuPico/esp32/main/tzpuPico.cpp
Philip Smart 4196e58420 MZ-1500 persona, expansion boards, Celestite LAN, QDF format, virtual mode
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>
2026-05-17 11:34:55 +01:00

1058 lines
43 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: 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 ms1 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