From 32ac09c92e6e4bb0f32f8c15597a7efdfffc005e Mon Sep 17 00:00:00 2001 From: Philip Smart Date: Thu, 30 Apr 2026 08:58:26 +0100 Subject: [PATCH] Sharp MZ80A updates and performance tuning --- projects/tzpuPico/esp32/filepack_version.txt | 2 +- projects/tzpuPico/esp32/main/SDCard.cpp | 51 +- projects/tzpuPico/esp32/main/WiFi.cpp | 269 ++++++++-- .../esp32/main/include/CommandProcessor.h | 4 +- projects/tzpuPico/esp32/main/include/WiFi.h | 3 + .../tzpuPico/esp32/main/include/flash_ram.h | 6 + projects/tzpuPico/esp32/main/tzpuPico.cpp | 60 ++- projects/tzpuPico/esp32/sdkconfig | 12 +- .../tzpuPico/esp32/sdkconfig.mode_ncm_only | 4 +- .../esp32/sdkconfig.mode_wifi_and_ncm | 4 +- .../tzpuPico/esp32/sdkconfig.mode_wifi_only | 4 +- projects/tzpuPico/esp32/tzpuPico_version.txt | 2 +- projects/tzpuPico/esp32/version.txt | 2 +- projects/tzpuPico/esp32/webserver/config.htm | 8 +- .../tzpuPico/esp32/webserver/filemanager.htm | 6 +- projects/tzpuPico/esp32/webserver/index.htm | 119 ++++- .../esp32/webserver/js/filemanager.js | 77 ++- .../tzpuPico/esp32/webserver/ota-esp32.htm | 8 +- .../tzpuPico/esp32/webserver/ota-rp2350.htm | 8 +- .../tzpuPico/esp32/webserver/personality.htm | 6 +- .../esp32/webserver/webfs_version.txt | 2 +- .../tzpuPico/esp32/webserver/wifimanager.htm | 6 +- projects/tzpuPico/src/ESP.c | 52 +- projects/tzpuPico/src/Z80CPU.c | 500 +++++++++--------- projects/tzpuPico/src/drivers/PIT8253.c | 421 +++++++++++++++ projects/tzpuPico/src/drivers/PPI8255.c | 345 ++++++++++++ projects/tzpuPico/src/drivers/Sharp/MZ-1E05.c | 18 +- projects/tzpuPico/src/drivers/Sharp/MZ-1E14.c | 5 +- projects/tzpuPico/src/drivers/Sharp/MZ-1E19.c | 7 +- projects/tzpuPico/src/drivers/Sharp/MZ-1R12.c | 23 +- projects/tzpuPico/src/drivers/Sharp/MZ-1R18.c | 7 +- projects/tzpuPico/src/drivers/Sharp/MZ700.c | 10 +- projects/tzpuPico/src/drivers/Sharp/MZ80A.c | 233 ++++++-- projects/tzpuPico/src/drivers/Sharp/MZ80AFI.c | 32 +- projects/tzpuPico/src/drivers/Sharp/QDDrive.c | 63 ++- projects/tzpuPico/src/drivers/Sharp/RFS.c | 202 +++++-- projects/tzpuPico/src/drivers/Sharp/TZFS.c | 3 +- projects/tzpuPico/src/drivers/Sharp/WD1773.c | 8 +- projects/tzpuPico/src/include/ESP.h | 3 +- projects/tzpuPico/src/include/FSPI.h | 5 +- projects/tzpuPico/src/include/Z80CPU.h | 26 +- .../tzpuPico/src/include/drivers/PIT8253.h | 71 +++ .../tzpuPico/src/include/drivers/PPI8255.h | 123 +++++ .../src/include/drivers/Sharp/MZ80A.h | 2 + .../tzpuPico/src/include/drivers/Sharp/RFS.h | 20 + projects/tzpuPico/src/include/flash_ram.h | 9 +- projects/tzpuPico/src/include/psram.h | 1 + projects/tzpuPico/src/model/BaseZ80/main.c | 128 ++++- .../tzpuPico/src/model/BaseZ80/version.txt | 2 +- projects/tzpuPico/src/psram.c | 64 ++- projects/tzpuPico/src/z80.pio | 5 +- projects/tzpuPico/version.txt | 2 +- 52 files changed, 2408 insertions(+), 645 deletions(-) create mode 100644 projects/tzpuPico/src/drivers/PIT8253.c create mode 100644 projects/tzpuPico/src/drivers/PPI8255.c create mode 100644 projects/tzpuPico/src/include/drivers/PIT8253.h create mode 100644 projects/tzpuPico/src/include/drivers/PPI8255.h diff --git a/projects/tzpuPico/esp32/filepack_version.txt b/projects/tzpuPico/esp32/filepack_version.txt index 6c3571a..fc249e9 100644 --- a/projects/tzpuPico/esp32/filepack_version.txt +++ b/projects/tzpuPico/esp32/filepack_version.txt @@ -1 +1 @@ -2.07 +2.18 diff --git a/projects/tzpuPico/esp32/main/SDCard.cpp b/projects/tzpuPico/esp32/main/SDCard.cpp index 7b4a778..2bc439c 100644 --- a/projects/tzpuPico/esp32/main/SDCard.cpp +++ b/projects/tzpuPico/esp32/main/SDCard.cpp @@ -814,19 +814,48 @@ bool SDCard::storeRP2350Info(const t_IpcFrameHdr &frame, FSPI &fspi) { memcpy(rp2350Header, infoBuf, sizeof(t_FlashPartitionHeader)); - // Extended payload includes cpufreq/psramfreq/voltage/flashSize/psramSize after the header. - if (payloadSize >= sizeof(t_FlashInfoPayload)) + // Extended payload fields — extract via WiFi's wifiCtrl.run fields directly. + // The rp2350Header pointer points to wifiCtrl.run.rp2350FlashHeader, and the + // fields after it (rp2350CpuFreq, rp2350PsramFreq, etc.) are at known offsets. + // Backwards-compatible: only extract fields present in the payload. + if (payloadSize > sizeof(t_FlashPartitionHeader)) { t_FlashInfoPayload *info = (t_FlashInfoPayload *) infoBuf; - // Store in the fields immediately after rp2350FlashHeader in wifiCtrl.run. - // Layout must match: int32_t cpufreq, psramfreq, voltage; uint32_t flashSize, psramSize; - int32_t *extra = (int32_t *) (rp2350Header + 1); - extra[0] = info->cpufreq; - extra[1] = info->psramfreq; - extra[2] = info->voltage; - uint32_t *extra32 = (uint32_t *) &extra[3]; - extra32[0] = info->flashSize; - extra32[1] = info->psramSize; + + // Core config fields (cpufreq..psramSize) — present in all extended payloads. + const size_t coreExtSize = offsetof(t_FlashInfoPayload, hostClkHz); + if (payloadSize >= coreExtSize) + { + int32_t *extra = (int32_t *) (rp2350Header + 1); + extra[0] = info->cpufreq; + extra[1] = info->psramfreq; + extra[2] = info->voltage; + uint32_t *extra32 = (uint32_t *) &extra[3]; + extra32[0] = info->flashSize; + extra32[1] = info->psramSize; + } + + // Host clock + emulation speed — added after psramSize. + if (payloadSize >= offsetof(t_FlashInfoPayload, driverSummary)) + { + int32_t *extra = (int32_t *) (rp2350Header + 1); + uint32_t *extra32 = (uint32_t *) &extra[3]; + extra32[2] = info->hostClkHz; // rp2350HostClkHz + extra32[3] = info->emulSpeedHz; // rp2350EmulSpeedHz + } + + // Driver summary — last field, variable-length content. + uint32_t *extra32base = (uint32_t *) ((int32_t *)(rp2350Header + 1) + 3); + char *drvSummary = (char *) &extra32base[4]; // After flashSize, psramSize, hostClkHz, emulSpeedHz + if (payloadSize >= sizeof(t_FlashInfoPayload)) + { + memcpy(drvSummary, info->driverSummary, FLASHHDR_DRIVER_SUMMARY_SIZE); + drvSummary[FLASHHDR_DRIVER_SUMMARY_SIZE - 1] = '\0'; + } + else + { + drvSummary[0] = '\0'; + } } } free(infoBuf); diff --git a/projects/tzpuPico/esp32/main/WiFi.cpp b/projects/tzpuPico/esp32/main/WiFi.cpp index aa8f5a8..6553181 100644 --- a/projects/tzpuPico/esp32/main/WiFi.cpp +++ b/projects/tzpuPico/esp32/main/WiFi.cpp @@ -476,39 +476,41 @@ bool WiFi::stringReplace(std::string &str, const std::string &from, const std::s // bool WiFi::setFloppyDiskFile(const std::string ¶m1, int diskNo) { - // Check for out of bounds values. - if (diskNo > WIFI_MAX_FLOPPY_DISK_IMAGES) - return (false); + if (diskNo < 0 || diskNo >= WIFI_MAX_FLOPPY_DISK_IMAGES) + return false; - // Save the floppy disk image name for later use. - this->wifiCtrl.run.floppyDiskImage[diskNo] = param1; - return (true); + if (this->wifiCtrl.run.floppyDiskImage[diskNo] != param1) + { + this->wifiCtrl.run.floppyDiskImage[diskNo] = param1; + ESP_LOGI(WIFITAG, "Floppy %d: %s", diskNo, param1.c_str()); + } + return true; } -// Method to change the name of the quick disk image indexed by 'diskNo'. -// bool WiFi::setQuickDiskFile(const std::string ¶m1, int diskNo) { - // Check for out of bounds values. - if (diskNo > WIFI_MAX_QUICK_DISK_IMAGES) - return (false); + if (diskNo < 0 || diskNo >= WIFI_MAX_QUICK_DISK_IMAGES) + return false; - // Save the floppy disk image name for later use. - this->wifiCtrl.run.quickDiskImage[diskNo] = param1; - return (true); + if (this->wifiCtrl.run.quickDiskImage[diskNo] != param1) + { + this->wifiCtrl.run.quickDiskImage[diskNo] = param1; + ESP_LOGI(WIFITAG, "Quick Disk %d: %s", diskNo, param1.c_str()); + } + return true; } -// Method to change the name of the ramfile backup image indexed by 'ramfileNo'. -// bool WiFi::setRamFile(const std::string ¶m1, int ramfileNo) { - // Check for out of bounds values. - if (ramfileNo > WIFI_MAX_RAMFILE_IMAGES) - return (false); + if (ramfileNo < 0 || ramfileNo >= WIFI_MAX_RAMFILE_IMAGES) + return false; - // Save the floppy disk image name for later use. - this->wifiCtrl.run.ramFileImage[ramfileNo] = param1; - return (true); + if (this->wifiCtrl.run.ramFileImage[ramfileNo] != param1) + { + this->wifiCtrl.run.ramFileImage[ramfileNo] = param1; + ESP_LOGI(WIFITAG, "RAMFILE %d: %s", ramfileNo, param1.c_str()); + } + return true; } // Method to unpack a gzipped and/or tar file. The extension of the srcFile determines the action taken. @@ -739,14 +741,16 @@ esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str) #endif pairs.push_back(keyValue); keyValue.name = "%SK_USBNCMVISIBLE%"; -#if !defined(CONFIG_IF_WIFI_ENABLED) && defined(CONFIG_IF_USB_NCM_ENABLED) +#if defined(CONFIG_IF_USB_NCM_ENABLED) keyValue.value = ""; #else keyValue.value = "style=\"display:none\""; #endif pairs.push_back(keyValue); keyValue.name = "%SK_NETPANELTITLE%"; -#if defined(CONFIG_IF_WIFI_ENABLED) +#if defined(CONFIG_IF_WIFI_ENABLED) && defined(CONFIG_IF_USB_NCM_ENABLED) + keyValue.value = "Network Configuration"; +#elif defined(CONFIG_IF_WIFI_ENABLED) keyValue.value = "WiFi Configuration"; #else keyValue.value = "Network Configuration"; @@ -910,6 +914,23 @@ esp_err_t WiFi::expandVarsAndSend(httpd_req_t *req, std::string str) keyValue.value = (ai && ai->description[0]) ? std::string(ai->description) : "N/A"; pairs.push_back(keyValue); + keyValue.name = "%SK_FWAUTHOR%"; + keyValue.value = (ai && ai->author[0]) ? std::string(ai->author) : "N/A"; + pairs.push_back(keyValue); + + keyValue.name = "%SK_FWLICENSE%"; + keyValue.value = (ai && ai->license[0]) ? std::string(ai->license) : "N/A"; + pairs.push_back(keyValue); + + keyValue.name = "%SK_FWCOPYRIGHT%"; + keyValue.value = (ai && ai->copyright[0]) ? std::string(ai->copyright) : "N/A"; + pairs.push_back(keyValue); + + // Active drivers — populated by RP2350 via INF IPC command. + keyValue.name = "%SK_ACTIVEDRIVERS%"; + keyValue.value = (wifiCtrl.run.rp2350DriverSummary[0]) ? std::string(wifiCtrl.run.rp2350DriverSummary) : "N/A"; + pairs.push_back(keyValue); + keyValue.name = "%SK_CPUCLOCK%"; keyValue.value = (wifiCtrl.run.rp2350CpuFreq > 0) ? std::to_string(wifiCtrl.run.rp2350CpuFreq / 1000000) + " MHz" : "N/A"; @@ -1072,6 +1093,12 @@ esp_err_t WiFi::versionedRename(const std::string &src, const std::string &dst, } // dst is now free — rename src into place. + // If src == dst this is a backup-only operation (the dst was already versioned + // above), so skip the rename — there's nothing to move. + if (src == dst) + { + return ESP_OK; + } if (rename(src.c_str(), dst.c_str()) != 0) { errMsg = "Failed to rename " + src + " -> " + dst; @@ -1109,6 +1136,9 @@ esp_err_t WiFi::expandAndSendFile(httpd_req_t *req, const char *basePath, const inFile.close(); ESP_LOGI(WIFITAG, "expandAndSendFile: %s loaded (%d bytes)", fileName.c_str(), (int)contents.length()); + // Close connection after response — see defaultFileHandler for rationale. + httpd_resp_set_hdr(req, "Connection", "close"); + if (contents.find("%SK_") == std::string::npos) { // No template variables — send entire file as a single HTTP chunk. @@ -1292,6 +1322,15 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) std::string disposition = ""; esp_err_t result = ESP_OK; + // Per-request local variables — the shared wifiCtrl.session struct must NOT be + // used here because multiple requests are handled concurrently by esp_http_server. + // Using the shared session caused a race condition where parallel requests + // overwrote each other's filePath/fileName/gzip fields, leaving some responses + // (typically jquery.min.js) stuck forever in "pending" state. + std::string localFilePath; + std::string localFileName; + bool localGzip = false; + // Retrieve pointer to object in order to access data. WiFi *pThis = (WiFi *) req->user_ctx; @@ -1312,18 +1351,17 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) buf.reset(new char[bufLen]); if (buf != nullptr && httpd_req_get_hdr_value_str(req, "Accept-Encoding", buf.get(), bufLen) == ESP_OK) { - pThis->wifiCtrl.session.gzip = (strstr(buf.get(), "gzip") != NULL); - pThis->wifiCtrl.session.deflate = (strstr(buf.get(), "deflate") != NULL); + localGzip = (strstr(buf.get(), "gzip") != NULL); } } // Look for a filename in the URI and construct the file path returning both. - result = pThis->getPathFromURI(pThis->wifiCtrl.session.filePath, pThis->wifiCtrl.session.fileName, pThis->wifiCtrl.run.basePath, req->uri); + result = pThis->getPathFromURI(localFilePath, localFileName, pThis->wifiCtrl.run.basePath, req->uri); if (result == ESP_FAIL) { if (strlen(req->uri) == 1 && req->uri[0] == '/') { - pThis->wifiCtrl.session.fileName = "/"; + localFileName = "/"; result = ESP_OK; } else @@ -1336,20 +1374,20 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) if (result == ESP_OK) { // See if the provided name matches a static handler, such as root. - if (pThis->wifiCtrl.session.fileName == "/" || pThis->wifiCtrl.session.fileName == "index.html" || pThis->wifiCtrl.session.fileName == "index.htm") + if (localFileName == "/" || localFileName == "index.html" || localFileName == "index.htm") { result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, "index.htm"); } else { // Get details of the file. - if (stat(pThis->wifiCtrl.session.filePath.c_str(), &fileStat) == -1) + if (stat(localFilePath.c_str(), &fileStat) == -1) { - if (pThis->wifiCtrl.session.gzip) + if (localGzip) { - gzipFile = pThis->wifiCtrl.session.filePath + ".gz"; + gzipFile = localFilePath + ".gz"; } - if (pThis->wifiCtrl.session.gzip && stat(gzipFile.c_str(), &fileStat) == -1) + if (localGzip && stat(gzipFile.c_str(), &fileStat) == -1) { result = ESP_FAIL; httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); @@ -1367,22 +1405,22 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) } else { - ESP_LOGI(WIFITAG, "Opened file : %s ", pThis->wifiCtrl.session.filePath.c_str()); + ESP_LOGI(WIFITAG, "Opened file : %s ", localFilePath.c_str()); } if (result == ESP_OK) { // If the file is HTML, JS or CSS then process externally. - if ((pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".html") || pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".htm") || - (pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".js") && !pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".min.js")) || - (pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".css") && !pThis->isFileExt(pThis->wifiCtrl.session.fileName, ".min.css"))) && + if ((pThis->isFileExt(localFileName, ".html") || pThis->isFileExt(localFileName, ".htm") || + (pThis->isFileExt(localFileName, ".js") && !pThis->isFileExt(localFileName, ".min.js")) || + (pThis->isFileExt(localFileName, ".css") && !pThis->isFileExt(localFileName, ".min.css"))) && gzipFile.empty()) { - result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, pThis->wifiCtrl.session.fileName); + result = pThis->expandAndSendFile(req, pThis->wifiCtrl.run.basePath, localFileName); } else { - fd = fopen(gzipFile.empty() ? pThis->wifiCtrl.session.filePath.c_str() : gzipFile.c_str(), "r"); + fd = fopen(gzipFile.empty() ? localFilePath.c_str() : gzipFile.c_str(), "r"); if (!fd) { result = ESP_FAIL; @@ -1393,9 +1431,17 @@ esp_err_t WiFi::defaultFileHandler(httpd_req_t *req) ESP_LOGI(WIFITAG, "Sending %sfile : %s (%ld bytes)...", gzipFile.empty() ? "" : "gzip ", - pThis->wifiCtrl.session.fileName.c_str(), + localFileName.c_str(), fileStat.st_size); - result = pThis->setContentTypeFromFileType(req, pThis->wifiCtrl.session.fileName); + // Close connection after each file response. The server is + // single-threaded — while sending a large file (e.g. 98KB font), + // all other requests are blocked. With keep-alive, the browser + // queues subsequent requests on existing connections that may be + // stalled behind a large transfer, leaving them "pending" forever. + // With Connection:close, the browser opens fresh connections for + // each resource. With 20 sockets available this works well. + httpd_resp_set_hdr(req, "Connection", "close"); + result = pThis->setContentTypeFromFileType(req, localFileName); if (result == ESP_OK) { std::unique_ptr chunk(new char[MAX_CHUNK_SIZE]); @@ -1525,36 +1571,112 @@ esp_err_t WiFi::defaultDataGETHandler(httpd_req_t *req) // Match URI and execute required data retrieval and return. if (uriStr == "wifistatus") { - // Lightweight JSON endpoint for AJAX polling of WiFi signal + system status. - char json[256]; + // JSON endpoint for AJAX polling of WiFi + system + RP2350 status. + // All fields that can change at runtime are included so the dashboard + // updates live without requiring a page refresh. int8_t txPwr = 0; int rssi = 0; bool connected = pThis->wifiCtrl.client.connected; + const char *ip = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP) + ? pThis->wifiCtrl.ap.ip : pThis->wifiCtrl.client.ip; + const char *nm = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP) + ? pThis->wifiCtrl.ap.netmask : pThis->wifiCtrl.client.netmask; + const char *gw = (pThis->wifiCtrl.run.wifiMode == WIFI_CONFIG_AP) + ? pThis->wifiCtrl.ap.gateway : pThis->wifiCtrl.client.gateway; #if defined(CONFIG_IF_WIFI_ENABLED) esp_wifi_get_max_tx_power(&txPwr); wifi_ap_record_t apInfo; if (connected && esp_wifi_sta_get_ap_info(&apInfo) == ESP_OK) rssi = apInfo.rssi; #endif - // Compute uptime string + // Compute uptime string. int64_t us = esp_timer_get_time(); int secs = (int) (us / 1000000); - int days = secs / 86400; - secs %= 86400; - int hrs = secs / 3600; - secs %= 3600; - int mins = secs / 60; - secs %= 60; + int days = secs / 86400; secs %= 86400; + int hrs = secs / 3600; secs %= 3600; + int mins = secs / 60; secs %= 60; char uptimeBuf[32]; if (days > 0) snprintf(uptimeBuf, sizeof(uptimeBuf), "%dd %02d:%02d:%02d", days, hrs, mins, secs); else snprintf(uptimeBuf, sizeof(uptimeBuf), "%02d:%02d:%02d", hrs, mins, secs); - snprintf(json, sizeof(json), - "{\"rssi\":%d,\"txPower\":%.1f,\"connected\":%s,\"uptime\":\"%s\"}", - rssi, txPwr * 0.25f, connected ? "true" : "false", uptimeBuf); + + // RP2350 partition header data (populated by INF IPC from RP2350). + uint8_t activeApp = pThis->wifiCtrl.run.rp2350FlashHeader.activeApp; + bool haveInfo = (pThis->wifiCtrl.run.rp2350FlashHeader.configured == FLASH_APP_CONFIGURED_FLAG && + activeApp >= 1 && activeApp < FLASH_APP_MAX_INSTANCES); + t_FlashPartitionInstance *ai = haveInfo ? &pThis->wifiCtrl.run.rp2350FlashHeader.config[activeApp] : NULL; + + const char *fwVer = (ai && ai->version[0]) ? ai->version : ""; + const char *fwDate = (ai && ai->versionDate[0]) ? ai->versionDate : ""; + const char *persona = (ai && ai->description[0]) ? ai->description : ""; + const char *drvSummary = pThis->wifiCtrl.run.rp2350DriverSummary[0] ? pThis->wifiCtrl.run.rp2350DriverSummary : ""; + int cpuMHz = pThis->wifiCtrl.run.rp2350CpuFreq / 1000000; + int psramMHz = pThis->wifiCtrl.run.rp2350PsramFreq / 1000000; + int flashMB = pThis->wifiCtrl.run.rp2350FlashSize / (1024 * 1024); + int psramMB = pThis->wifiCtrl.run.rp2350PsramSize / (1024 * 1024); + uint32_t hostClkHz = pThis->wifiCtrl.run.rp2350HostClkHz; + uint32_t emulSpeedHz = pThis->wifiCtrl.run.rp2350EmulSpeedHz; + + // Build RP2350 partition table HTML for live update. + std::ostringstream rpParts; + for (int idx = 0; idx < FLASH_APP_MAX_INSTANCES; idx++) + { + const t_FlashPartitionInstance *pi = &pThis->wifiCtrl.run.rp2350FlashHeader.config[idx]; + std::string active = (idx == activeApp && haveInfo) ? "Yes" : "No"; + rpParts << "" + << "" << idx << "" + << "" << to_str(pi->addr, 0, 16) << "" + << "" << to_str(pi->size, 0, 16) << "" + << "" << to_str(pi->chksum, 0, 16) << "" + << "" << active << "" + << "" << pi->license << "" + << "" << pi->author << "" + << "" << pi->description << "" + << "" << pi->version << "" + << "" << pi->versionDate << "" + << "" << pi->copyright << "" + << ""; + } + // Escape the HTML for safe embedding in JSON string value. + std::string rpPartsHtml = rpParts.str(); + std::string rpPartsEsc; + rpPartsEsc.reserve(rpPartsHtml.size() + 64); + for (char c : rpPartsHtml) + { + if (c == '"') rpPartsEsc += "\\\""; + else if (c == '\\') rpPartsEsc += "\\\\"; + else if (c == '\n') rpPartsEsc += "\\n"; + else rpPartsEsc += c; + } + + // Build JSON response using std::string (partition HTML can be large). + std::ostringstream json; + json << "{\"rssi\":" << rssi + << ",\"txPower\":" << (txPwr * 0.25f) + << ",\"connected\":" << (connected ? "true" : "false") + << ",\"uptime\":\"" << uptimeBuf << "\"" + << ",\"ip\":\"" << (ip[0] ? ip : "") << "\"" + << ",\"netmask\":\"" << (nm[0] ? nm : "") << "\"" + << ",\"gateway\":\"" << (gw[0] ? gw : "") << "\"" + << ",\"fwVersion\":\"" << fwVer << "\"" + << ",\"fwDate\":\"" << fwDate << "\"" + << ",\"activePartition\":" << (haveInfo ? std::to_string(activeApp) : "null") + << ",\"persona\":\"" << persona << "\"" + << ",\"drivers\":\"" << drvSummary << "\"" + << ",\"rp2350Clock\":" << (cpuMHz > 0 ? std::to_string(cpuMHz) : "null") + << ",\"psramClock\":" << (psramMHz > 0 ? std::to_string(psramMHz) : "null") + << ",\"rp2350Flash\":" << (flashMB > 0 ? std::to_string(flashMB) : "null") + << ",\"rp2350Psram\":" << (psramMB > 0 ? std::to_string(psramMB) : "null") + << ",\"hostClkHz\":" << (hostClkHz > 0 ? std::to_string(hostClkHz) : "null") + << ",\"emulSpeedHz\":" << (emulSpeedHz > 0 ? std::to_string(emulSpeedHz) : "null") + << ",\"rpPartitions\":\"" << rpPartsEsc << "\"" + << ",\"floppy1\":\"" << (pThis->wifiCtrl.run.floppyDiskImage[0].empty() ? "none" : pThis->wifiCtrl.run.floppyDiskImage[0]) << "\"" + << ",\"floppy2\":\"" << (pThis->wifiCtrl.run.floppyDiskImage[1].empty() ? "none" : pThis->wifiCtrl.run.floppyDiskImage[1]) << "\"" + << ",\"qdisk\":\"" << (pThis->wifiCtrl.run.quickDiskImage[0].empty() ? "none" : pThis->wifiCtrl.run.quickDiskImage[0]) << "\"" + << "}"; httpd_resp_set_type(req, "application/json"); - httpd_resp_sendstr(req, json); + httpd_resp_sendstr(req, json.str().c_str()); return ESP_OK; } else if (uriStr.substr(0, 6) == "rename") @@ -3475,6 +3597,12 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req) htmlStr.append(""); htmlStr.append("\n"); + htmlStr.append("" + "" + "
" + "
" + "" + "
\n"); } else { @@ -3493,6 +3621,12 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req) htmlStr.append(""); htmlStr.append("\n"); + htmlStr.append("" + "" + "
" + "
" + "" + "
\n"); } int entryCnt = 1; @@ -4351,10 +4485,12 @@ bool WiFi::startWebserver(void) // Keep keep-alive enabled (HTTP/1.1 default) so the browser can multiplex // multiple resource requests over one TCP connection. USB NCM at Full Speed // (12 Mbps) is much slower than WiFi, so active transfers hold sockets longer. - // 20 sockets with a 5s idle timeout gives enough headroom for pages that load - // 12+ resources in parallel without LRU-purging active transfers. - config.recv_wait_timeout = 5; - config.send_wait_timeout = 10; + // Short recv_wait_timeout aggressively recycles idle keep-alive connections. + // With 20 sockets available, the browser can always open fresh connections. + // Longer timeouts (5-15s) cause stale keep-alive connections from previous + // page loads to hold sockets, triggering LRU purge of active transfers. + config.recv_wait_timeout = 1; + config.send_wait_timeout = 15; // Setup the required paths and descriptors then register them with the server. const httpd_uri_t dataPOST = {.uri = "/data", .method = HTTP_POST, .handler = defaultDataPOSTHandler, .user_ctx = this}; @@ -5046,6 +5182,14 @@ WiFi::WiFi(bool defaultMode, uint16_t device, NVS *nvs, SDCard *sdcard, cJSON *c strncpy(wifiCtrl.run.fsPath, fsPath, FILE_PATH_MAX); wifiCtrl.run.versionList = versionList; memset(&wifiCtrl.run.rp2350FlashHeader, 0, sizeof(t_FlashPartitionHeader)); + wifiCtrl.run.rp2350CpuFreq = 0; + wifiCtrl.run.rp2350PsramFreq = 0; + wifiCtrl.run.rp2350Voltage = 0; + wifiCtrl.run.rp2350FlashSize = 0; + wifiCtrl.run.rp2350PsramSize = 0; + wifiCtrl.run.rp2350HostClkHz = 0; + wifiCtrl.run.rp2350EmulSpeedHz = 0; + wifiCtrl.run.rp2350DriverSummary[0] = '\0'; // Wire SDCard's rp2350Header pointer directly to our rp2350FlashHeader so // SDCard::storeRP2350Info() can populate it when the INF IPC command arrives. if (sdcard) @@ -5128,6 +5272,15 @@ WiFi::WiFi(bool defaultMode, uint16_t device, NVS *nvs, SDCard *sdcard, cJSON *c wifiConfig.params.txPower = 0; } + // Sync the runtime wifiMode from the persisted config so that template + // expansion shows the correct WiFi mode (AP vs Client) even before + // wifi->run() is called. Without this, pages loaded via USB NCM before + // WiFi connects would show AP config instead of Client config. + if (wifiConfig.params.wifiMode == WIFI_CONFIG_CLIENT || wifiConfig.params.wifiMode == WIFI_CONFIG_AP) + { + wifiCtrl.run.wifiMode = wifiConfig.params.wifiMode; + } + // Process JSON and override any setting specified in config. // Check we have a valid handle. if (config == NULL) diff --git a/projects/tzpuPico/esp32/main/include/CommandProcessor.h b/projects/tzpuPico/esp32/main/include/CommandProcessor.h index 5482d3f..2ea80c0 100644 --- a/projects/tzpuPico/esp32/main/include/CommandProcessor.h +++ b/projects/tzpuPico/esp32/main/include/CommandProcessor.h @@ -155,8 +155,10 @@ class CommandProcessor : public WiFi, FSPI, SDCard // WiFi tracking for floppy/QD: if (frame.command == IPCF_CMD_RFD) wifi.setFloppyDiskFile(std::string(frame.filename), frame.diskNo); - else if (frame.command == IPCF_CMD_RQD || frame.command == IPCF_CMD_RRF) + else if (frame.command == IPCF_CMD_RQD) wifi.setQuickDiskFile(std::string(frame.filename), frame.diskNo); + else if (frame.command == IPCF_CMD_RRF) + wifi.setRamFile(std::string(frame.filename), frame.diskNo); sdcard.readFileViaSPI(frame, fspi); } diff --git a/projects/tzpuPico/esp32/main/include/WiFi.h b/projects/tzpuPico/esp32/main/include/WiFi.h index 5fba879..f6bcad4 100644 --- a/projects/tzpuPico/esp32/main/include/WiFi.h +++ b/projects/tzpuPico/esp32/main/include/WiFi.h @@ -324,6 +324,9 @@ class WiFi int32_t rp2350Voltage; // Core voltage (from INF) uint32_t rp2350FlashSize; // RP2350 Flash size in bytes (from INF) uint32_t rp2350PsramSize; // RP2350 PSRAM size in bytes (from INF) + uint32_t rp2350HostClkHz; // Measured host clock frequency in Hz (from INF) + uint32_t rp2350EmulSpeedHz; // Effective Z80 emulation speed in Hz (from INF) + char rp2350DriverSummary[FLASHHDR_DRIVER_SUMMARY_SIZE]; // Active drivers (from INF) // Active Floppy Disk images. std::string floppyDiskImage[WIFI_MAX_FLOPPY_DISK_IMAGES]; diff --git a/projects/tzpuPico/esp32/main/include/flash_ram.h b/projects/tzpuPico/esp32/main/include/flash_ram.h index 3015cef..77262bc 100644 --- a/projects/tzpuPico/esp32/main/include/flash_ram.h +++ b/projects/tzpuPico/esp32/main/include/flash_ram.h @@ -152,6 +152,9 @@ typedef struct } t_FlashPartitionHeader; // Extended INF payload — partition header + runtime core config. +// Maximum size of the active driver summary string sent via INF IPC. +#define FLASHHDR_DRIVER_SUMMARY_SIZE 128 + // Sent by RP2350 ESP_sendVersionInfo, received by ESP32 storeRP2350Info. typedef struct { @@ -161,6 +164,9 @@ typedef struct int32_t voltage; // Core voltage setting (VREG enum) uint32_t flashSize; // RP2350 Flash size in bytes uint32_t psramSize; // RP2350 PSRAM size in bytes + uint32_t hostClkHz; // Measured host clock frequency in Hz + uint32_t emulSpeedHz; // Effective Z80 emulation speed in Hz + char driverSummary[FLASHHDR_DRIVER_SUMMARY_SIZE]; // Active drivers and interfaces } t_FlashInfoPayload; // Structure to describe a single file stored in ROM. Indexed by its filename (matching a filename appearing in JSON including path) diff --git a/projects/tzpuPico/esp32/main/tzpuPico.cpp b/projects/tzpuPico/esp32/main/tzpuPico.cpp index 546a78a..672a789 100644 --- a/projects/tzpuPico/esp32/main/tzpuPico.cpp +++ b/projects/tzpuPico/esp32/main/tzpuPico.cpp @@ -319,6 +319,7 @@ void buildVersionList(WiFi::t_versionList *versionList, NVS &nvs, SDCard &sdcard #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) { @@ -357,13 +358,16 @@ static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx) // 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. -bool setupUSBConsole(void) +// 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. +void setupUSBTask(void *pvParameters) { bool cdcOk = false; bool ncmOk = false; - // 0. Initialise the TCP/IP stack and event loop early so we can create - // the esp_netif before the host starts probing. + // 0. Initialise the TCP/IP stack and event loop (idempotent / tolerates double-init). esp_netif_init(); esp_event_loop_create_default(); @@ -372,19 +376,18 @@ bool setupUSBConsole(void) tusb_cfg.external_phy = false; esp_err_t ret = tinyusb_driver_install(&tusb_cfg); if (ret != ESP_OK) { - return false; + 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. Force a clean USB disconnect/reconnect cycle. After a software reboot - // (esp_restart), the USB peripheral may not fully reset — the host sees a - // glitch rather than a proper disconnect. macOS then tries to resume the - // previous NCM session state, which fails and leaves the link "inactive". - // By explicitly pulling D+ low (tud_disconnect) and then re-enabling it - // (tud_connect), the host sees a clean device removal followed by a fresh - // enumeration, regardless of whether this was a power cycle or soft reboot. + // 2. Force a clean USB disconnect/reconnect cycle. These delays run in this + // background task so the main task is free to start the CommandProcessor. tud_disconnect(); vTaskDelay(pdMS_TO_TICKS(1500)); tud_connect(); + vTaskDelay(pdMS_TO_TICKS(500)); // 3. Initialise CDC-ACM for serial logging. tinyusb_config_cdcacm_t acm_cfg = {}; @@ -400,8 +403,7 @@ bool setupUSBConsole(void) ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config); ncmOk = (ret == ESP_OK); - // 5. Create the lwIP netif IMMEDIATELY so received packets are handled - // as soon as the host starts probing (ARP, NDP, DHCP). + // 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); @@ -434,19 +436,21 @@ bool setupUSBConsole(void) 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; // DHCP lease time in minutes + 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); } } - // 6. Redirect stdout/stderr/stdin to the TinyUSB CDC-ACM port. + // 6. Redirect console to TinyUSB CDC-ACM. if (cdcOk) { esp_tusb_init_console(TINYUSB_CDC_ACM_0); } - return cdcOk; + ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s)", cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL"); + g_usbReady = true; + vTaskDelete(NULL); } #endif // CONFIG_IF_USB_NCM_ENABLED @@ -811,12 +815,11 @@ extern "C" IO_init(LOGGING_FRAMED); #endif - // Install TinyUSB composite device (CDC-ACM + NCM) and redirect console - // to CDC-ACM as early as possible so all subsequent ESP_LOG output is visible. + // 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) - if (setupUSBConsole()) { - ESP_LOGW(MAINTAG, "USB CDC-ACM console active — log output on TinyUSB serial port."); - } + xTaskCreate(setupUSBTask, "usbSetup", 8192, NULL, 5, NULL); #endif // Phase 1: fast setup — eFUSE, NVS, SPI slave (~60 ms). @@ -937,10 +940,17 @@ extern "C" if (!wifiStarted && loopCount > 100) { #if defined(CONFIG_IF_USB_NCM_ENABLED) - // USB NCM network + lwIP netif were already created in setupUSBConsole(). - // Start the webserver now so USB NCM is immediately browsable. - // The HTTP server binds to INADDR_ANY:80 so it will also serve on - // WiFi once that connection is established. + // 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 diff --git a/projects/tzpuPico/esp32/sdkconfig b/projects/tzpuPico/esp32/sdkconfig index 4edf2e0..675b8ba 100644 --- a/projects/tzpuPico/esp32/sdkconfig +++ b/projects/tzpuPico/esp32/sdkconfig @@ -588,7 +588,13 @@ CONFIG_SD_CDDETECT=21 # # WiFi # -# CONFIG_IF_WIFI_ENABLED is not set +CONFIG_IF_WIFI_ENABLED=y +CONFIG_IF_WIFI_SSID="pZ80" +CONFIG_IF_WIFI_DEFAULT_SSID_PWD="pZ80pZ80" +CONFIG_IF_WIFI_MAX_RETRIES=10 +CONFIG_IF_WIFI_AP_CHANNEL=7 +CONFIG_IF_WIFI_SSID_HIDDEN=0 +CONFIG_IF_WIFI_MAX_CONNECTIONS=5 # end of WiFi # @@ -1951,8 +1957,8 @@ CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP # -CONFIG_LWIP_MAX_ACTIVE_TCP=16 -CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_MAX_ACTIVE_TCP=24 +CONFIG_LWIP_MAX_LISTENING_TCP=24 CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y CONFIG_LWIP_TCP_MAXRTX=12 CONFIG_LWIP_TCP_SYNMAXRTX=12 diff --git a/projects/tzpuPico/esp32/sdkconfig.mode_ncm_only b/projects/tzpuPico/esp32/sdkconfig.mode_ncm_only index 897e21b..34061e7 100644 --- a/projects/tzpuPico/esp32/sdkconfig.mode_ncm_only +++ b/projects/tzpuPico/esp32/sdkconfig.mode_ncm_only @@ -1956,8 +1956,8 @@ CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP # -CONFIG_LWIP_MAX_ACTIVE_TCP=16 -CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_MAX_ACTIVE_TCP=24 +CONFIG_LWIP_MAX_LISTENING_TCP=24 CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y CONFIG_LWIP_TCP_MAXRTX=12 CONFIG_LWIP_TCP_SYNMAXRTX=12 diff --git a/projects/tzpuPico/esp32/sdkconfig.mode_wifi_and_ncm b/projects/tzpuPico/esp32/sdkconfig.mode_wifi_and_ncm index 9ee9bac..056b1be 100644 --- a/projects/tzpuPico/esp32/sdkconfig.mode_wifi_and_ncm +++ b/projects/tzpuPico/esp32/sdkconfig.mode_wifi_and_ncm @@ -1962,8 +1962,8 @@ CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP # -CONFIG_LWIP_MAX_ACTIVE_TCP=16 -CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_MAX_ACTIVE_TCP=24 +CONFIG_LWIP_MAX_LISTENING_TCP=24 CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y CONFIG_LWIP_TCP_MAXRTX=12 CONFIG_LWIP_TCP_SYNMAXRTX=12 diff --git a/projects/tzpuPico/esp32/sdkconfig.mode_wifi_only b/projects/tzpuPico/esp32/sdkconfig.mode_wifi_only index 3463de6..2f5e035 100644 --- a/projects/tzpuPico/esp32/sdkconfig.mode_wifi_only +++ b/projects/tzpuPico/esp32/sdkconfig.mode_wifi_only @@ -1960,8 +1960,8 @@ CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 # # TCP # -CONFIG_LWIP_MAX_ACTIVE_TCP=16 -CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_MAX_ACTIVE_TCP=24 +CONFIG_LWIP_MAX_LISTENING_TCP=24 CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y CONFIG_LWIP_TCP_MAXRTX=12 CONFIG_LWIP_TCP_SYNMAXRTX=12 diff --git a/projects/tzpuPico/esp32/tzpuPico_version.txt b/projects/tzpuPico/esp32/tzpuPico_version.txt index cd5ac03..91972c3 100644 --- a/projects/tzpuPico/esp32/tzpuPico_version.txt +++ b/projects/tzpuPico/esp32/tzpuPico_version.txt @@ -1 +1 @@ -2.0 +2.01 diff --git a/projects/tzpuPico/esp32/version.txt b/projects/tzpuPico/esp32/version.txt index 7dba3a2..e72716a 100644 --- a/projects/tzpuPico/esp32/version.txt +++ b/projects/tzpuPico/esp32/version.txt @@ -1 +1 @@ -2.27 +2.46 diff --git a/projects/tzpuPico/esp32/webserver/config.htm b/projects/tzpuPico/esp32/webserver/config.htm index 72d95b0..0212399 100644 --- a/projects/tzpuPico/esp32/webserver/config.htm +++ b/projects/tzpuPico/esp32/webserver/config.htm @@ -95,9 +95,9 @@ @@ -117,7 +117,7 @@
-

This is the JSON configuration editor to define the %SK_PROCESSOR% and ESP32 configuration. Please read the documentation if you need help.

+

This is the JSON configuration editor to define the %SK_PROCESSOR% and ESP32 configuration.

diff --git a/projects/tzpuPico/esp32/webserver/filemanager.htm b/projects/tzpuPico/esp32/webserver/filemanager.htm index 167b55a..f77dab5 100644 --- a/projects/tzpuPico/esp32/webserver/filemanager.htm +++ b/projects/tzpuPico/esp32/webserver/filemanager.htm @@ -95,9 +95,9 @@ diff --git a/projects/tzpuPico/esp32/webserver/index.htm b/projects/tzpuPico/esp32/webserver/index.htm index 724ee0b..1255647 100644 --- a/projects/tzpuPico/esp32/webserver/index.htm +++ b/projects/tzpuPico/esp32/webserver/index.htm @@ -95,9 +95,9 @@ @@ -115,10 +115,12 @@ + @@ -141,9 +143,9 @@ Password:%SK_APPWD% - IP (AP):%SK_CURRENTIP% - NETMASK:%SK_CURRENTNM% - GATEWAY:%SK_CURRENTGW% + IP (AP):%SK_CURRENTIP% + NETMASK:%SK_CURRENTNM% + GATEWAY:%SK_CURRENTGW% @@ -160,14 +162,14 @@ DHCP:Enabled - IP (assigned):%SK_CURRENTIP% - NETMASK:%SK_CURRENTNM% - GATEWAY:%SK_CURRENTGW% + IP (assigned):%SK_CURRENTIP% + NETMASK:%SK_CURRENTNM% + GATEWAY:%SK_CURRENTGW% - IP (fixed):%SK_CURRENTIP% - NETMASK:%SK_CURRENTNM% - GATEWAY:%SK_CURRENTGW% + IP (fixed):%SK_CURRENTIP% + NETMASK:%SK_CURRENTNM% + GATEWAY:%SK_CURRENTGW% @@ -200,27 +202,23 @@ - + - - - - - + - + - + - + @@ -234,6 +232,41 @@ +
+
+
+
+

Active Personality

+
+
+
Firmware Version:%SK_FWVERSION%Firmware Version:%SK_FWVERSION% ESP32 Version:%SK_ESPVERSION%
Active Partition:%SK_ACTIVEPARTITION%Active Persona:%SK_PERSONA%
RP2350 Clock:%SK_CPUCLOCK%RP2350 Clock:%SK_CPUCLOCK% ESP32 Clock:%SK_ESP32CLOCK%
RP2350 Flash:%SK_RP2350FLASH%RP2350 Flash:%SK_RP2350FLASH% ESP32 Flash:%SK_ESP32FLASH%
RP2350 PSRAM:%SK_RP2350PSRAM%RP2350 PSRAM:%SK_RP2350PSRAM% ESP32 PSRAM:%SK_ESP32PSRAM%
PSRAM Clock:%SK_PSRAMCLOCK%PSRAM Clock:%SK_PSRAMCLOCK% SD Card:%SK_SDCARD%
+ + + + + + + + + + + + + + + + + + + + + +
Personality:%SK_PERSONA%Processor:%SK_PROCESSOR%
Active Partition:%SK_ACTIVEPARTITION%Host Clock:N/A
Emulation Speed:N/ADrivers:%SK_ACTIVEDRIVERS%
Floppy 1:%SK_FLOPPY1%Floppy 2:%SK_FLOPPY2%
Quick Disk:%SK_QDDISK%
+ + + + +
@@ -281,7 +314,7 @@ Copyright - + %SK_RP_PARTITIONS% @@ -309,7 +342,7 @@