5 Commits

Author SHA1 Message Date
Philip Smart
c0c5201534 Bug fixes 2026-06-04 16:29:21 +01:00
Philip Smart
5a14318018 Added missing MZ80B files 2026-06-04 12:06:04 +01:00
Philip Smart
f5670c66d0 MZ-1E30 SASI HDD interface, IPL Reset, debug shell hooks, XIP cache fix
New MZ-1E30 SASI hard disk interface for MZ-2500:
- SASI.c/h: reusable SASI bus controller with full state machine
  (BUS_FREE/SELECTION/COMMAND/DATA_IN/DATA_OUT/STATUS/MESSAGE phases)
- MZ1E30.c/h: board-level wrapper with ROM (ports A8/A9) and SASI (A4/A5)
- Sector-level I/O via ESP32 SD card IPC (no PSRAM allocation for 22MB images)
- Inline response polling in sasiReadStatus for Z80 tight-loop compatibility
- Supports READ(6), WRITE(6), SEEK(6), TEST_UNIT_READY, REQUEST_SENSE, INQUIRY
- 4 target disks, 256-byte blocks, SuperTurboZ Enhanced ROM compatible

Debug shell hook system:
- Generic driver-registered trace hooks (fdctrace, mmutrace, qdtrace)
- All machine drivers register machineName; FDC/QD interfaces register trace hooks
- Extensible for new machines without modifying dbgsh.c

Web GUI: IPL Reset button, MZ-1E30 interface with noLoadAddr ROM config

Config parser: always store ROM file in romConfig even with romAddrCount=0
(interfaces with port-based ROM access need the filename without loadaddr)

WD1773 XIP cache coherency fix: DMA disk loads bypass XIP cache, leaving
stale calloc zeros. xip_cache_clean_all + invalidate_all after DMA ensures
Core 1 sees fresh PSRAM data. Fixes intermittent ExtDSK misdetection as D88.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-04 12:04:58 +01:00
Philip Smart
756d54a22c MZ-2500 virtual mode, D88 native format, driver callback refactoring
Virtual mode interrupt support:
- Gate array needs physical M1 bus cycles (fetchPhysicalMem in fetchOpcode)
  for internal clocks (8253 timer, interrupt controller) to advance
- MZ2500 driver overrides cpu->_Z80.reti and cpu->_Z80.fetch with custom
  handlers (MZ2500_retiHandler, MZ2500_fetchByte) — zero core changes
- Physical RETI bus sequence (ED 4D plant + M1 fetch) clears IC in_service
- One-shot INT suppression prevents premature interrupts during I=0x02 init
- Physical RETI cleanup at suppression boundary clears accumulated in_service

D88 native format handler (WD1773.c):
- Sparse/contiguous auto-detection for track mapping strategy
- Physical (C,H) → D88 index mapping built from sector headers (Balloon Fight)
- Index-based identity mapping for standard/H-swapped disks (Galaga, CP/M)
- d88PhysMap array with proper allocation/cleanup

Driver architecture cleanup:
- All MZ-2500 interrupt logic in MZ2500.c driver (not Z80CPU.c core)
- Custom RETI/fetchByte set directly on cpu->_Z80 function pointers
- Core Z80CPU.c unchanged from pre-MZ-2500 state for other machines
- Debug shell: psync command, intcount with /INT pin diagnostic
- MZ8BFI: MAX_DISK_DRIVES reduced to 2 for PSRAM heap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 12:19:00 +01:00
Philip Smart
6758c2892c Updates to fix Z80 write cycles, MZ1500 persona and other addtions including updates to ESP32 web interface and fixing NCM reconnection should the host not connect on first try 2026-05-27 10:58:37 +01:00
60 changed files with 7456 additions and 686 deletions

View File

@@ -1 +1 @@
2.49
2.66

View File

@@ -1619,6 +1619,159 @@ esp_err_t WiFi::defaultDataGETHandler(httpd_req_t *req)
return cfgResult;
}
else if (uriStr == "backup")
{
// Stream the entire SD card as a tar archive.
// Uses POSIX/ustar tar format: 512-byte header per file + padded data.
// Streamed via chunked HTTP so memory usage is constant regardless of SD size.
httpd_resp_set_type(req, "application/x-tar");
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"picoZ80_backup.tar\"");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
// Recursive lambda to walk directories and stream tar entries.
// Uses a stack-based approach to avoid deep recursion.
struct TarEntry { std::string path; bool isDir; long size; time_t mtime; };
std::vector<std::string> dirStack;
dirStack.push_back(std::string(pThis->wifiCtrl.run.fsPath));
char hdr[512];
std::unique_ptr<char[]> fbuf(new char[MAX_CHUNK_SIZE]);
if (!fbuf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory allocation failed");
return ESP_FAIL;
}
esp_err_t tarResult = ESP_OK;
int fileCount = 0;
std::string basePath = pThis->wifiCtrl.run.fsPath;
if (basePath.back() != '/') basePath += '/';
while (!dirStack.empty() && tarResult == ESP_OK)
{
std::string curDir = dirStack.back();
dirStack.pop_back();
DIR *d = opendir(curDir.c_str());
if (!d) continue;
// Collect entries first so we can close the dir handle quickly.
std::vector<TarEntry> entries;
struct dirent *ent;
while ((ent = readdir(d)) != NULL)
{
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = curDir + "/" + ent->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) != 0)
continue;
TarEntry te;
te.path = fullPath;
te.isDir = S_ISDIR(st.st_mode);
te.size = te.isDir ? 0 : st.st_size;
te.mtime = st.st_mtime;
entries.push_back(te);
}
closedir(d);
for (const auto &te : entries)
{
if (te.isDir) {
dirStack.push_back(te.path);
}
// Build relative path by stripping the SD mount prefix.
std::string relPath = te.path;
if (relPath.find(basePath) == 0)
relPath = relPath.substr(basePath.length());
if (te.isDir && relPath.back() != '/')
relPath += '/';
// Skip paths that are too long for tar (max 255 = 155 prefix + 100 name).
if (relPath.length() > 255)
continue;
// Build the 512-byte POSIX ustar header.
memset(hdr, 0, 512);
// Split into prefix (first 155 chars of dir) and name (last 100 chars).
std::string tarName, tarPrefix;
if (relPath.length() <= 100) {
tarName = relPath;
} else {
size_t splitAt = relPath.rfind('/', 99);
if (splitAt == std::string::npos) splitAt = 99;
tarPrefix = relPath.substr(0, splitAt);
tarName = relPath.substr(splitAt + 1);
}
strncpy(hdr + 0, tarName.c_str(), 100);
snprintf(hdr + 100, 8, "%07o", te.isDir ? 0755 : 0644); // mode
snprintf(hdr + 108, 8, "%07o", 0); // uid
snprintf(hdr + 116, 8, "%07o", 0); // gid
snprintf(hdr + 124, 12, "%011lo", te.isDir ? 0L : te.size); // size
snprintf(hdr + 136, 12, "%011lo", (long)te.mtime); // mtime
memset(hdr + 148, ' ', 8); // checksum placeholder
hdr[156] = te.isDir ? '5' : '0'; // typeflag
memcpy(hdr + 257, "ustar", 5); // magic
hdr[263] = '0'; hdr[264] = '0'; // version
if (!tarPrefix.empty())
strncpy(hdr + 345, tarPrefix.c_str(), 155);
// Compute header checksum (sum of all bytes, treating checksum field as spaces).
unsigned int cksum = 0;
for (int i = 0; i < 512; i++)
cksum += (unsigned char)hdr[i];
snprintf(hdr + 148, 7, "%06o", cksum);
hdr[155] = '\0';
// Send header.
if (httpd_resp_send_chunk(req, hdr, 512) != ESP_OK) {
tarResult = ESP_FAIL;
break;
}
// Send file data (not for directories).
if (!te.isDir && te.size > 0) {
FILE *f = fopen(te.path.c_str(), "rb");
if (f) {
long remaining = te.size;
while (remaining > 0 && tarResult == ESP_OK) {
size_t toRead = (remaining > MAX_CHUNK_SIZE) ? MAX_CHUNK_SIZE : remaining;
size_t got = fread(fbuf.get(), 1, toRead, f);
if (got > 0) {
if (httpd_resp_send_chunk(req, fbuf.get(), got) != ESP_OK)
tarResult = ESP_FAIL;
remaining -= got;
} else {
break;
}
}
fclose(f);
// Pad to 512-byte boundary.
int pad = (512 - (te.size % 512)) % 512;
if (pad > 0 && tarResult == ESP_OK) {
memset(hdr, 0, pad);
if (httpd_resp_send_chunk(req, hdr, pad) != ESP_OK)
tarResult = ESP_FAIL;
}
}
}
fileCount++;
}
}
// End-of-archive: two 512-byte zero blocks.
if (tarResult == ESP_OK) {
memset(hdr, 0, 512);
httpd_resp_send_chunk(req, hdr, 512);
httpd_resp_send_chunk(req, hdr, 512);
httpd_resp_send_chunk(req, NULL, 0);
ESP_LOGI(WIFITAG, "Backup complete: %d files streamed as tar", fileCount);
}
return tarResult;
}
else if (uriStr == "wifistatus")
{
// JSON endpoint for AJAX polling of WiFi + system + RP2350 status.
@@ -3695,13 +3848,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
else
{
htmlStr.append("<div id=\"filemanager-info\" style=\"font-size:12px;padding-top:8px;padding-bottom:8px;\"><b>").append(sdpath).append("</b></div>");
htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:25px;\">");
htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:4px;\">");
htmlStr.append(" <table class=\"table table-borderless table-sm\" style=\"width:100%\">");
htmlStr.append(" <thead>");
htmlStr.append(" <tr>");
htmlStr.append(" <th style=\"width:50%;min-width:200px\"><b>Name</b></th>");
htmlStr.append(" <th style=\"width:5%\"><b>Type</b></th>");
htmlStr.append(" <th style=\"width:5%\"><b>Size (Bytes)</b></th>");
htmlStr.append(" <th style=\"width:1%;padding:0 6px;text-align:center;white-space:nowrap;\"><input type=\"checkbox\" id=\"selectAll\" onclick=\"toggleSelectAll(this)\" title=\"Select all\"></th>");
htmlStr.append(" <th style=\"min-width:200px;padding-left:2px;cursor:pointer;\" data-sort-col=\"1\" data-sort-type=\"text\" title=\"Click to sort\"><b>Name</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"2\" data-sort-type=\"text\" title=\"Click to sort\"><b>Type</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"3\" data-sort-type=\"num\" title=\"Click to sort\"><b>Size (Bytes)</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"text-align:left;width:10%\"><b>Action</b></th>");
htmlStr.append(" </tr>");
htmlStr.append(" </thead>");
@@ -3717,12 +3871,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
upDir.pop_back();
}
htmlStr.append("<tr>");
htmlStr.append("<td></td>");
htmlStr.append("<td style=\"text-align:center\"><a href=\"").append(upDir).append("\">").append("[up level]").append("</a></td>");
htmlStr.append("<td></td>");
htmlStr.append("<td></td>");
htmlStr.append("<td></td>");
htmlStr.append("</tr>\n");
htmlStr.append("<tr>");
htmlStr.append("<td></td>");
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
@@ -3738,7 +3894,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
"onclick=\"mkdir()\"></button>");
htmlStr.append("</td></tr>\n");
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
"<td colspan=\"4\">"
"<td colspan=\"5\">"
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
@@ -3747,6 +3903,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
else
{
htmlStr.append("<tr>");
htmlStr.append("<td></td>");
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
@@ -3762,31 +3919,58 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
"onclick=\"mkdir()\"></button>");
htmlStr.append("</td></tr>\n");
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
"<td colspan=\"4\">"
"<td colspan=\"5\">"
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
"</div></td></tr>\n");
}
int entryCnt = 1;
std::string fileToStat;
// Collect all directory entries, then sort: directories first, then alphabetical (case-insensitive).
struct t_DirEntry {
std::string name;
bool isDir;
long size;
};
std::vector<t_DirEntry> dirEntries;
while ((entry = readdir(dir)) != NULL)
{
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
fileToStat = sdpath + "/" + entry->d_name;
std::string fileToStat = sdpath + "/" + entry->d_name;
if (stat(fileToStat.c_str(), &entryStat) == -1)
{
ESP_LOGE(WIFITAG, "Failed to subdir stat %s : %s", entrytype, fileToStat.c_str());
ESP_LOGE(WIFITAG, "Failed to stat %s", fileToStat.c_str());
continue;
}
sprintf(entrysize, "%ld", entryStat.st_size);
t_DirEntry de;
de.name = entry->d_name;
de.isDir = (entry->d_type == DT_DIR);
de.size = entryStat.st_size;
dirEntries.push_back(de);
}
closedir(dir);
std::sort(dirEntries.begin(), dirEntries.end(), [](const t_DirEntry &a, const t_DirEntry &b) {
// Directories first, then files. Within each group, case-insensitive alphabetical.
if (a.isDir != b.isDir) return a.isDir > b.isDir;
std::string al = a.name, bl = b.name;
std::transform(al.begin(), al.end(), al.begin(), ::tolower);
std::transform(bl.begin(), bl.end(), bl.begin(), ::tolower);
return al < bl;
});
int entryCnt = 1;
for (const auto &de : dirEntries)
{
entrytype = de.isDir ? "directory" : "file";
sprintf(entrysize, "%ld", de.size);
htmlStr.append("<tr>");
htmlStr.append("<td style=\"padding:0 6px;text-align:center;white-space:nowrap;\"><input type=\"checkbox\" class=\"file-select\" data-name=\"").append(de.name).append("\" data-dir=\"").append(directory).append("\" onchange=\"updateDeleteSelectedBtn()\"></td>");
htmlStr.append("<td>");
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=ren\">");
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("</td>");
@@ -3806,14 +3990,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
// Slot 2: Open Dir (dirs) or Download (files)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
if (entry->d_type == DT_DIR)
if (de.isDir)
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=dir\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory);
if (htmlStr.back() != '/')
htmlStr.append("/");
htmlStr.append(entry->d_name).append("\">");
htmlStr.append(de.name).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"dir\" class=\"fa fa-folder-open-o wm-button-small\" "
"aria-hidden=\"true\" title=\"Open\"></button>");
htmlStr.append("</form>");
@@ -3822,7 +4006,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"/data/download\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" class=\"fa fa-download wm-button-small\" aria-hidden=\"true\" "
"title=\"Download\"></button>");
@@ -3832,15 +4016,15 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
// Slot 3: Edit (text files only, blank otherwise)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
if (entry->d_type != DT_DIR)
if (!de.isDir)
{
std::filesystem::path filePath = entry->d_name;
std::filesystem::path filePath = de.name;
if (filePath.extension() == ".txt" || filePath.extension() == ".htm" || filePath.extension() == ".js" ||
filePath.extension() == ".css" || filePath.extension() == ".json")
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=edit\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"edit\" class=\"fa fa-pencil wm-button-small\" "
"aria-hidden=\"true\" title=\"Edit\"></button>");
@@ -3853,7 +4037,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=copy\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"copy\" class=\"fa fa-copy wm-button-small\" "
"aria-hidden=\"true\" title=\"Copy\"></button>");
@@ -3866,18 +4050,17 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
htmlStr.append("<form method=\"GET\" style=\"display:inline\">");
htmlStr.append("<input type=\"hidden\" name=\"cmd\" value=\"del\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" class=\"fa fa-trash wm-button-small\" "
"aria-hidden=\"true\" title=\"Delete\" onclick=\"return confirmDelete(event, '")
.append(entry->d_name)
.append(de.name)
.append("');\"></button>");
htmlStr.append("</form>");
htmlStr.append("</span>");
htmlStr.append("</td></tr>\n");
entryCnt++;
}
closedir(dir);
htmlStr.append(" </tbody>");
htmlStr.append(" </table>");
@@ -3985,6 +4168,15 @@ esp_err_t WiFi::defaultRebootHandler(httpd_req_t *req)
httpd_resp_set_status(req, "200 OK");
httpd_resp_sendstr(req, "");
}
else if (uriStr.substr(0, 3) == "ipl")
{
// Send command to RP2350 to trigger IPL reset (BST via 8255 PPI PC3).
CP_queueCmd("IPLR");
// Indicate IPL reset request successful.
httpd_resp_set_status(req, "200 OK");
httpd_resp_sendstr(req, "");
}
else
{
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Unknown reboot directive");
@@ -4276,7 +4468,15 @@ esp_err_t WiFi::changeDiskHandler(httpd_req_t *req, enum DRIVETYPES driveType)
.then(arr => {
const tbody = document.getElementById('listing');
tbody.innerHTML = '';
// Sort: ".." first, then directories alphabetically, then files alphabetically.
arr.sort((a, b) => {
if (a.name === '..') return -1;
if (b.name === '..') return 1;
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
arr.forEach(e => {
const tr = document.createElement('tr');
const td = document.createElement('td');

View File

@@ -321,6 +321,26 @@ void buildVersionList(WiFi::t_versionList *versionList, NVS &nvs, SDCard &sdcard
static esp_netif_t *s_usb_ncm_netif = NULL;
static volatile bool g_usbReady = false; // Set by USB task when setup completes.
// NCM connection parameters — read from JSON config "esp32.core" block.
// Defaults are used if the config doesn't specify them.
typedef struct {
bool enabled; // NCM enable/disable (default: true)
int maxRetries; // Max connection cycles: 0=forever, 5-1000 (default: 0)
int retryPeriod; // Delay between retries: 0=increasing backoff, 1-120s fixed (default: 0)
} t_ncmConfig;
static t_ncmConfig g_ncmConfig = { true, 0, 0 };
#endif
// WiFi enable flag — read from JSON config "esp32.core.wifi" (1=enable, 0=disable).
// Defaults to enabled if compiled in, disabled if not compiled in.
#if defined(CONFIG_IF_WIFI_ENABLED)
static bool g_wifiEnabled = true;
#else
static bool g_wifiEnabled = false;
#endif
#if defined(CONFIG_IF_USB_NCM_ENABLED)
static void usb_ncm_l2_free(void *h, void *buffer)
{
free(buffer);
@@ -362,18 +382,29 @@ static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx)
// 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;
// USB NCM connection timing parameters.
static constexpr int USB_NCM_DISCONNECT_INIT_MS = 3000; // Initial disconnect hold time.
static constexpr int USB_NCM_DISCONNECT_MAX_MS = 60000; // Maximum disconnect hold (backoff cap).
static constexpr int USB_NCM_BACKOFF_STEP_MS = 1000; // Increase disconnect by this much each retry.
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500; // Post-connect settle before polling mount.
static constexpr int USB_NCM_POLL_MS = 100; // Polling interval for mount/DHCP checks.
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000; // Max wait for tud_mounted() per attempt.
static constexpr int USB_NCM_LINK_RETRIES = 3; // Link-state UP retries per mount cycle.
static constexpr int USB_NCM_POST_MOUNT_MS = 1500; // Delay after mount for host NCM driver load.
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000; // DHCP wait per link-state attempt.
// Flag set when the DHCP server assigns a lease to the host.
static volatile bool g_ncmDhcpLeased = false;
// Event handler: fired when the DHCP server assigns an IP to the connected host.
static void ncm_dhcp_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
ip_event_ap_staipassigned_t *evt = (ip_event_ap_staipassigned_t *)event_data;
ESP_LOGI(MAINTAG, "USB NCM: DHCP lease assigned to host (IP " IPSTR ")", IP2STR(&evt->ip));
g_ncmDhcpLeased = true;
}
}
void setupUSBTask(void *pvParameters)
{
@@ -384,77 +415,40 @@ void setupUSBTask(void *pvParameters)
esp_netif_init();
esp_event_loop_create_default();
// Register for DHCP server lease events so we know when the host has an IP.
esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED,
&ncm_dhcp_event_handler, NULL);
// 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.
g_usbReady = true;
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++)
// 2. Initialise CDC-ACM for serial logging (independent of NCM connection).
{
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");
}
tinyusb_config_cdcacm_t acm_cfg = {};
ret = tusb_cdc_acm_init(&acm_cfg);
cdcOk = (ret == ESP_OK);
}
if (!mounted)
// 3. Initialise the NCM network class handler.
{
ESP_LOGE(MAINTAG, "USB: host did not enumerate device after %d attempts", USB_NCM_MAX_RETRIES);
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);
}
// 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.
// 4. Create the lwIP netif and DHCP server (persists across reconnect cycles).
if (ncmOk) {
esp_netif_ip_info_t ip_info = {};
ip_info.ip.addr = ipaddr_addr(CONFIG_IF_USB_NCM_IP);
@@ -491,45 +485,136 @@ void setupUSBTask(void *pvParameters)
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.
// 5. 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);
// 6. Signal ready so the main loop can start WiFi and the webserver immediately.
// The NCM connection will continue retrying in the background below.
ESP_LOGI(MAINTAG, "USB init complete (CDC:%s NCM:%s), starting NCM connection loop...",
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL");
g_usbReady = true;
if (!ncmOk || s_usb_ncm_netif == NULL) {
ESP_LOGE(MAINTAG, "USB NCM: init failed, connection loop not started");
vTaskDelete(NULL);
return;
}
// 7. NCM connection loop.
// Reads config from g_ncmConfig (populated by JSON parser after g_usbReady is set).
// Allow a moment for the main loop to parse JSON config before we read g_ncmConfig.
vTaskDelay(pdMS_TO_TICKS(500));
if (!g_ncmConfig.enabled) {
ESP_LOGI(MAINTAG, "USB NCM: disabled by config (ncm=0), connection loop not started");
vTaskDelete(NULL);
return;
}
int maxCycles = g_ncmConfig.maxRetries; // 0=forever, else limit
int fixedDelay = g_ncmConfig.retryPeriod; // 0=backoff, else fixed seconds
ESP_LOGI(MAINTAG, "USB NCM: config retries=%d period=%ds", maxCycles, fixedDelay);
vTaskDelay(pdMS_TO_TICKS(50)); // Let TinyUSB task initialise.
bool warmBoot = tud_mounted();
int disconnectMs = warmBoot ? (USB_NCM_DISCONNECT_INIT_MS + USB_NCM_BACKOFF_STEP_MS)
: USB_NCM_DISCONNECT_INIT_MS;
int cycle = 0;
while (!g_ncmDhcpLeased)
{
cycle++;
// Check retry limit (0 = forever).
if (maxCycles > 0 && cycle > maxCycles) {
ESP_LOGW(MAINTAG, "USB NCM: retry limit reached (%d cycles), giving up", maxCycles);
break;
}
ESP_LOGI(MAINTAG, "USB NCM: connection cycle %d%s (%s, disconnect=%d ms)",
cycle, maxCycles > 0 ? ("/" + std::to_string(maxCycles)).c_str() : "",
(cycle == 1 && warmBoot) ? "warm reboot" : "retry", disconnectMs);
// --- Phase 1: Disconnect/reconnect ---
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(disconnectMs));
tud_connect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
// --- Phase 2: Wait for host to enumerate ---
bool mounted = false;
int waited = 0;
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
{
if (tud_mounted()) {
mounted = true;
ESP_LOGI(MAINTAG, "USB NCM: host enumerated (cycle %d, %d ms)", cycle, waited);
break;
}
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
dhcpWait += USB_NCM_POLL_MS;
waited += USB_NCM_POLL_MS;
}
if (!mounted) {
ESP_LOGW(MAINTAG, "USB NCM: mount timeout (cycle %d)", cycle);
if (fixedDelay > 0) {
disconnectMs = fixedDelay * 1000;
} else {
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
disconnectMs = USB_NCM_DISCONNECT_MAX_MS;
}
continue;
}
// --- Phase 3: Link-state UP with retry ---
ESP_LOGI(MAINTAG, "USB NCM: waiting %d ms for host NCM driver...", USB_NCM_POST_MOUNT_MS);
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POST_MOUNT_MS));
for (int linkAttempt = 1; linkAttempt <= USB_NCM_LINK_RETRIES && !g_ncmDhcpLeased; linkAttempt++)
{
if (linkAttempt > 1) {
tud_network_link_state(0, false);
vTaskDelay(pdMS_TO_TICKS(500));
}
tud_network_link_state(0, true);
ESP_LOGI(MAINTAG, "USB NCM: link UP (cycle %d, link attempt %d/%d)",
cycle, linkAttempt, USB_NCM_LINK_RETRIES);
waited = 0;
while (waited < USB_NCM_DHCP_WAIT_MS && !g_ncmDhcpLeased) {
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
waited += USB_NCM_POLL_MS;
}
if (g_ncmDhcpLeased) {
ESP_LOGI(MAINTAG, "USB NCM: DHCP confirmed (cycle %d, link attempt %d, %d ms)",
cycle, linkAttempt, waited);
}
}
if (!g_ncmDhcpLeased) {
ESP_LOGW(MAINTAG, "USB NCM: no DHCP after link-state retries (cycle %d), full reconnect...", cycle);
if (fixedDelay > 0) {
disconnectMs = fixedDelay * 1000;
} else {
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
disconnectMs = USB_NCM_DISCONNECT_MAX_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;
if (g_ncmDhcpLeased) {
ESP_LOGI(MAINTAG, "USB NCM: connection established after %d cycle(s)", cycle);
}
vTaskDelete(NULL);
}
#endif // CONFIG_IF_USB_NCM_ENABLED
@@ -736,6 +821,60 @@ void setupJSON(NVS &nvs, SDCard &sdcard, FSPI &fspi, cJSON *config)
}
}
// WiFi enable/disable from "esp32.core.wifi" — only effective if WiFi is compiled in.
{
cJSON *wifiEnable = cJSON_GetObjectItem(coreObj, "wifi_enable");
if (cJSON_IsNumber(wifiEnable)) {
#if defined(CONFIG_IF_WIFI_ENABLED)
g_wifiEnabled = (wifiEnable->valueint != 0);
#else
g_wifiEnabled = false; // Not compiled in, always disabled.
#endif
}
ESP_LOGI(MAINTAG, "WiFi config: enabled=%d (compiled=%s)",
g_wifiEnabled,
#if defined(CONFIG_IF_WIFI_ENABLED)
"yes"
#else
"no"
#endif
);
}
// NCM connection parameters from "esp32.core" — only effective if NCM is compiled in.
#if defined(CONFIG_IF_USB_NCM_ENABLED)
{
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
if (cJSON_IsNumber(ncmEnable))
g_ncmConfig.enabled = (ncmEnable->valueint != 0);
cJSON *ncmRetries = cJSON_GetObjectItem(coreObj, "ncmretries");
if (cJSON_IsNumber(ncmRetries)) {
int v = ncmRetries->valueint;
if (v == 0 || (v >= 5 && v <= 1000))
g_ncmConfig.maxRetries = v;
}
cJSON *ncmPeriod = cJSON_GetObjectItem(coreObj, "ncmperiod");
if (cJSON_IsNumber(ncmPeriod)) {
int v = ncmPeriod->valueint;
if (v >= 0 && v <= 120)
g_ncmConfig.retryPeriod = v;
}
ESP_LOGI(MAINTAG, "NCM config: enabled=%d retries=%d period=%d",
g_ncmConfig.enabled, g_ncmConfig.maxRetries, g_ncmConfig.retryPeriod);
}
#else
// NCM not compiled in — if config says enabled, log a note.
{
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
if (cJSON_IsNumber(ncmEnable) && ncmEnable->valueint != 0) {
ESP_LOGW(MAINTAG, "NCM config: enabled in config but NCM not compiled in — ignored");
}
}
#endif
return;
}
@@ -1039,8 +1178,12 @@ extern "C"
// 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();
if (g_wifiEnabled) {
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
wifi->run();
} else {
ESP_LOGW(MAINTAG, "WiFi disabled by config (wifi_enable=0).");
}
#endif
wifiStarted = true;
}

View File

@@ -1 +1 @@
2.66
2.83

View File

@@ -96,6 +96,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -163,6 +163,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -96,6 +96,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>
@@ -138,7 +139,14 @@
<div class="col-lg-12">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory</h3>
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory
<button type="button" id="deleteSelectedBtn" class="btn btn-danger btn-xs" style="float:right;margin-top:-2px;margin-right:5px;display:none;" onclick="deleteSelected()" title="Delete all selected files">
<i class="fa fa-trash"></i> Delete Selected (<span id="deleteSelectedCount">0</span>)
</button>
<button type="button" class="btn btn-default btn-xs" style="float:right;margin-top:-2px;" onclick="backupSD()" title="Download entire SD card as a tar archive">
<i class="fa fa-download"></i> Backup SD
</button>
</h3>
</div>
<div class="panel-body">
<div class="table-responsive">

View File

@@ -96,6 +96,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -4,11 +4,11 @@ function rebootRP2350() {
if (response.ok) {
void(0);
} else {
alert('Reboot failed: ' + response.status);
modalAlert('Reboot failed: ' + response.status);
}
})
.catch(err => {
alert('Error: ' + err);
modalAlert('Error: ' + err);
});
}
@@ -18,11 +18,25 @@ function rebootHost() {
if (response.ok) {
void(0);
} else {
alert('Reboot failed: ' + response.status);
modalAlert('Reboot failed: ' + response.status);
}
})
.catch(err => {
alert('Error: ' + err);
modalAlert('Error: ' + err);
});
}
function rebootIPL() {
fetch('/reboot/ipl')
.then(response => {
if (response.ok) {
void(0);
} else {
modalAlert('IPL Reset failed: ' + response.status);
}
})
.catch(err => {
modalAlert('Error: ' + err);
});
}
@@ -32,10 +46,10 @@ function reloadConfig() {
if (response.ok) {
void(0);
} else {
alert('Reload failed: ' + response.status);
modalAlert('Reload failed: ' + response.status);
}
})
.catch(err => {
alert('Error: ' + err);
modalAlert('Error: ' + err);
});
}

View File

@@ -1,3 +1,55 @@
// ---------------------------------------------------------------------------
// Modal replacements for alert() and confirm().
// Usage: modalAlert('Something happened');
// modalConfirm('Delete this?', function() { /* OK */ });
// modalConfirm('Delete this?', onOk, onCancel);
// ---------------------------------------------------------------------------
function _createModalOverlay() {
var ov = document.createElement('div');
ov.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;' +
'background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
return ov;
}
function _modalBox() {
return 'background:#2c2c2c;border:1px solid #555;border-radius:8px;padding:20px 30px;' +
'max-width:450px;width:90%;color:#eee;font-size:14px;text-align:center;';
}
function _modalBtn(bg, border) {
return 'padding:6px 20px;cursor:pointer;border-radius:4px;margin:0 5px;' +
'background:' + bg + ';color:#fff;border:1px solid ' + border + ';';
}
function modalAlert(message, onClose) {
var ov = _createModalOverlay();
ov.innerHTML = '<div style="' + _modalBox() + '">' +
'<p style="margin-bottom:15px;white-space:pre-wrap;">' + message + '</p>' +
'<button id="_maOk" style="' + _modalBtn('#555','#444') + '">OK</button></div>';
document.body.appendChild(ov);
document.getElementById('_maOk').onclick = function() {
document.body.removeChild(ov);
if (typeof onClose === 'function') onClose();
};
}
function modalConfirm(message, onOk, onCancel) {
var ov = _createModalOverlay();
ov.innerHTML = '<div style="' + _modalBox() + '">' +
'<p style="margin-bottom:15px;white-space:pre-wrap;">' + message + '</p>' +
'<button id="_mcOk" style="' + _modalBtn('#c33','#a22') + '">OK</button>' +
'<button id="_mcNo" style="' + _modalBtn('#555','#444') + '">Cancel</button></div>';
document.body.appendChild(ov);
document.getElementById('_mcOk').onclick = function() {
document.body.removeChild(ov);
if (typeof onOk === 'function') onOk();
};
document.getElementById('_mcNo').onclick = function() {
document.body.removeChild(ov);
if (typeof onCancel === 'function') onCancel();
};
}
$(document).ready(function() {
$('.side-nav a, .side-nav .dropdown-toggle').on('mousedown touchstart', function() {
$(this).blur();

View File

@@ -317,7 +317,7 @@ var ConfigGUI = (function($) {
// -----------------------------------------------------------------------
// Known drivers — must match virtualFuncMap[] in Z80CPU.c.
var knownDrivers = ['MZ700', 'MZ1500', 'MZ80A', 'MZ2000', 'MZ2200'];
var knownDrivers = ['MZ700', 'MZ1500', 'MZ80A', 'MZ80B', 'MZ2000', 'MZ2200', 'MZ2500'];
// ROM constraints per interface: minRom = initial entries, maxRom = hard limit.
var interfaceRomLimits = {
@@ -332,7 +332,8 @@ var ConfigGUI = (function($) {
'MZ-1R23': { minRom: 0, maxRom: 0 },
'MZ-1R37': { minRom: 0, maxRom: 0 },
'PIO-3034': { minRom: 0, maxRom: 0 },
'Celestite': { minRom: 0, maxRom: 0 }
'Celestite': { minRom: 0, maxRom: 0 },
'MZ-1E30': { minRom: 0, maxRom: 1, noLoadAddr: true }
};
// Valid interfaces per driver — derived from interfaceFuncMap[] in each
@@ -341,8 +342,10 @@ var ConfigGUI = (function($) {
'MZ700': ['RFS', 'MZ-1E05', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite'],
'MZ1500': ['RFS', 'MZ-1E05', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite'],
'MZ80A': ['RFS', 'MZ80AFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R37', 'PIO-3034'],
'MZ80B': ['MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite'],
'MZ2000': ['MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite'],
'MZ2200': ['MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite']
'MZ2200': ['MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite'],
'MZ2500': ['MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1E30', 'MZ-1R12', 'MZ-1R18', 'MZ-1R23', 'MZ-1R37', 'PIO-3034', 'Celestite']
};
// Get the ROM limit for a given interface name.
@@ -401,6 +404,13 @@ var ConfigGUI = (function($) {
$tbody.html('<tr><td style="color:#777;">Empty directory</td></tr>');
return;
}
// Sort: ".." first, then directories alphabetically, then files alphabetically.
arr.sort(function(a, b) {
if (a.name === '..') return -1;
if (b.name === '..') return 1;
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
$.each(arr, function(i, entry) {
var $tr = $('<tr>');
var $td = $('<td style="font-size:12px;cursor:pointer;">');
@@ -471,7 +481,7 @@ var ConfigGUI = (function($) {
// Core Settings Renderer
// -----------------------------------------------------------------------
function renderCoreSection(containerId, coreObj, title) {
function renderCoreSection(containerId, coreObj, title, isPartition) {
var id = uid('core');
var html = '<div class="cfg-section">';
html += '<div class="panel panel-info">';
@@ -506,6 +516,18 @@ var ConfigGUI = (function($) {
html += '<span class="help-block col-sm-6" style="font-size:11px;">50 - 166 MHz (warning above 133 MHz)</span>';
html += '</div>';
// z80refresh — only shown for partition cores (not global RP2350 core).
if (isPartition) {
var refreshChk = coreObj.z80refresh ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">Z80 Refresh</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="z80refresh"' + refreshChk + ' title="Enable Z80 DRAM refresh cycles during virtual memory fetches"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Insert DRAM refresh cycles when executing from virtual ROM/RAM (needed when mixing virtual ROM with physical DRAM)</span>';
html += '</div>';
}
html += '</form>';
html += '</div></div></div></div>';
@@ -516,13 +538,17 @@ var ConfigGUI = (function($) {
}
// Collect core settings from the form back into an object.
function collectCore(containerId) {
function collectCore(containerId, isPartition) {
var $el = $('#' + containerId);
return {
var obj = {
voltage: parseFloat($el.find('[data-field="voltage"]').val()) || 1.10,
cpufreq: mhzToFreq($el.find('[data-field="cpufreq"]').val()) || 240000000,
psramfreq: mhzToFreq($el.find('[data-field="psramfreq"]').val()) || 133000000
};
if (isPartition) {
obj.z80refresh = $el.find('[data-field="z80refresh"]').is(':checked') ? 1 : 0;
}
return obj;
}
// -----------------------------------------------------------------------
@@ -581,7 +607,8 @@ var ConfigGUI = (function($) {
});
$('#' + containerId).on('click', '[data-action="remove-row"]', function() {
if (confirm('Remove this memory region?')) $(this).closest('tr').remove();
var row = $(this).closest('tr');
modalConfirm('Remove this memory region?', function() { row.remove(); });
});
// Bind address/size validation: 16-bit, 512-byte aligned, addr+size <= 64K.
@@ -730,7 +757,8 @@ var ConfigGUI = (function($) {
});
$('#' + containerId).on('click', '[data-action="remove-row"]', function() {
if (confirm('Remove this I/O region?')) $(this).closest('tr').remove();
var row = $(this).closest('tr');
modalConfirm('Remove this I/O region?', function() { row.remove(); });
});
// Bind address/size validation: 16-bit, addr+size <= 64K (except 0+0x10000).
@@ -794,7 +822,7 @@ var ConfigGUI = (function($) {
// ROM Entry Renderer (a single ROM file with its load addresses)
// -----------------------------------------------------------------------
function renderRomEntry(rom, romIdx) {
function renderRomEntry(rom, romIdx, hideLoadAddr) {
var romId = uid('rom');
var romChk = rom.enable ? ' checked' : '';
var html = '<div class="panel panel-default" style="margin-bottom:4px; border-left:2px solid #5cb85c;" data-rom-idx="' + romIdx + '">';
@@ -803,12 +831,15 @@ var ConfigGUI = (function($) {
html += '<input type="text" data-field="rom-file" value="' + (rom.file || '') + '" style="width:220px;font-size:11px;" title="ROM image file path on the SD card">';
html += ' <button type="button" class="btn btn-default btn-xs" data-action="browse-file-rom" title="Browse SD card for ROM file" style="padding:0px 4px;font-size:10px;"><i class="fa fa-folder-open"></i></button>';
html += ' <button type="button" class="btn btn-danger btn-xs" data-action="remove-rom" title="Remove this ROM entry" style="padding:0px 4px;font-size:10px;float:right;margin-right:20px;"><i class="fa fa-times"></i></button>';
html += ' <i class="fa fa-chevron-right" style="float:right;margin-top:2px;"></i>';
if (!hideLoadAddr) html += ' <i class="fa fa-chevron-right" style="float:right;margin-top:2px;"></i>';
html += '</div>';
if (!hideLoadAddr) {
html += '<div id="' + romId + '" class="panel-collapse collapse">';
html += '<div class="panel-body" style="padding:6px;">';
html += renderRomLoadAddrs(rom.loadaddr);
html += '</div></div>';
}
html += '</div>';
html += '<div id="' + romId + '" class="panel-collapse collapse">';
html += '<div class="panel-body" style="padding:6px;">';
html += renderRomLoadAddrs(rom.loadaddr);
html += '</div></div></div>';
return html;
}
@@ -988,14 +1019,23 @@ var ConfigGUI = (function($) {
collectValue: function($td) {
return $td.find('[data-field="param-value"]').val() || '';
}
},
ip: {
label: 'IP Address',
renderValue: function(val) {
return '<input type="text" data-field="param-value" value="' + (val || '') + '"'
+ ' placeholder="xxx.xxx.xxx.xxx[:port]"'
+ ' title="IP address of the netfs fileserver with optional port (e.g. 192.168.1.100 or 192.168.1.100:8080)"'
+ ' style="font-size:11px;width:200px;font-family:monospace;">';
},
collectValue: function($td) {
return $td.find('[data-field="param-value"]').val() || '';
}
}
// Future parameter types can be added here, e.g.:
// integer: { label: 'Integer', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
// string: { label: 'String', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
};
// List of type keys for the dropdown, in display order.
var paramTypeOrder = ['file'];
var paramTypeOrder = ['file', 'ip'];
// Detect which param type a config object uses (by checking which known key is present).
function detectParamType(paramObj) {
@@ -1097,10 +1137,11 @@ var ConfigGUI = (function($) {
html += '</div>';
// ROMs - always show section with Add button
var ifRomLimits = getIfRomLimits(iface.name);
html += '<div data-section="roms" style="margin-top:6px;"><b style="font-size:11px;">ROM Files</b>';
if (iface.rom && iface.rom.length > 0) {
for (var r = 0; r < iface.rom.length; r++) {
html += renderRomEntry(iface.rom[r], r);
html += renderRomEntry(iface.rom[r], r, ifRomLimits.noLoadAddr);
}
}
html += '<button type="button" class="btn btn-success btn-xs cfg-btn-add" data-action="add-rom" style="margin-top:4px;" title="Add a ROM file entry"><i class="fa fa-plus"></i> Add ROM</button>';
@@ -1134,12 +1175,19 @@ var ConfigGUI = (function($) {
var $romPanels = $ifPanel.find('[data-rom-idx]');
if ($romPanels.length > 0) {
iface.rom = [];
var ifLimitsForRom = getIfRomLimits(iface.name);
$romPanels.each(function() {
var $romPanel = $(this);
var la = collectLoadAddrs($romPanel);
// Interfaces with noLoadAddr hide the loadaddr UI; emit a default
// disabled entry so the config parser stores the ROM file to flash.
if (la.length === 0 && ifLimitsForRom.noLoadAddr) {
la = [{ enable: 0, position: 0, addr: 0, bank: 0, size: 0, tcycwait: 0, tcycsync: 0 }];
}
var rom = {
file: $romPanel.find('[data-field="rom-file"]').val(),
enable: $romPanel.find('[data-field="rom-enable"]').is(':checked') ? 1 : 0,
loadaddr: collectLoadAddrs($romPanel)
loadaddr: la
};
iface.rom.push(rom);
});
@@ -1186,6 +1234,7 @@ var ConfigGUI = (function($) {
html += '</div></div></div></div>';
var $container = $('#' + containerId);
$container.off(); // Remove all previously delegated handlers to prevent duplicates on re-render.
$container.html(html);
// Bind add-driver handler
@@ -1198,17 +1247,37 @@ var ConfigGUI = (function($) {
// Bind remove-driver handler
$container.on('click', '[data-action="remove-driver"]', function(e) {
e.stopPropagation();
var name = $(this).closest('[data-driver-idx]').find('[data-field="drv-name"]').val() || 'this driver';
if (confirm('Remove driver "' + name + '" and all its interfaces?'))
$(this).closest('[data-driver-idx]').remove();
var $drv = $(this).closest('[data-driver-idx]');
var name = $drv.find('[data-field="drv-name"]').val() || 'this driver';
modalConfirm('Remove driver "' + name + '" and all its interfaces?', function() {
$drv.remove();
});
});
// Bind add/remove/browse for driver-level ROM entries.
$container.on('click', '[data-action="add-drv-rom"]', function() {
var $tbody = $(this).closest('[data-section="drv-roms"]').find('table[data-section="drv-rom"] tbody');
$tbody.append(renderDrvRomRow({ enable: 0, file: '' }));
});
$container.on('click', '[data-action="remove-drv-rom"]', function() {
var row = $(this).closest('tr');
modalConfirm('Remove this system ROM entry?', function() {
row.remove();
});
});
$container.on('click', '[data-action="browse-drv-rom"]', function() {
var $input = $(this).closest('td').find('[data-field="file"]');
openFileBrowser($input);
});
// Bind remove-interface handler
$container.on('click', '[data-action="remove-interface"]', function(e) {
e.stopPropagation();
var ifName = $(this).closest('[data-if-idx]').find('[data-field="if-name"]').first().val() || 'this interface';
if (confirm('Remove interface "' + ifName + '"?'))
$(this).closest('[data-if-idx]').remove();
var $iface = $(this).closest('[data-if-idx]');
var ifName = $iface.find('[data-field="if-name"]').first().val() || 'this interface';
modalConfirm('Remove interface "' + ifName + '"?', function() {
$iface.remove();
});
});
// When driver name changes, rebuild the interface dropdown to show only valid interfaces.
@@ -1229,7 +1298,7 @@ var ConfigGUI = (function($) {
var $controls = $(this).closest('.cfg-add-if-controls');
var $ifSection = $controls.closest('[data-section="interfaces"]');
var ifName = $controls.find('[data-field="new-if-name"]').val();
if (!ifName) { alert('Please select an interface.'); return; }
if (!ifName) { modalAlert('Please select an interface.'); return; }
var nextIdx = $ifSection.find('[data-if-idx]').length;
var limits = getIfRomLimits(ifName);
// Build initial ROM entries based on minRom for this interface.
@@ -1272,18 +1341,19 @@ var ConfigGUI = (function($) {
var currentCount = $romsSection.find('[data-rom-idx]').length;
if (currentCount >= limits.maxRom) return; // button should be disabled, but guard anyway
var newRom = { file: '', enable: 0, loadaddr: [{ enable: 0, position: 0, addr: 0, bank: 0, size: 0, tcycwait: 0, tcycsync: 0 }] };
$(renderRomEntry(newRom, currentCount)).insertBefore($(this));
$(renderRomEntry(newRom, currentCount, limits.noLoadAddr)).insertBefore($(this));
updateAddRomBtn($ifPanel);
});
// Bind remove-rom handler.
$container.on('click', '[data-action="remove-rom"]', function(e) {
e.stopPropagation();
if (confirm('Remove this ROM entry?')) {
var $ifPanel = $(this).closest('[data-if-idx]');
$(this).closest('[data-rom-idx]').remove();
var $ifPanel = $(this).closest('[data-if-idx]');
var $romEntry = $(this).closest('[data-rom-idx]');
modalConfirm('Remove this ROM entry?', function() {
$romEntry.remove();
updateAddRomBtn($ifPanel);
}
});
});
// Bind add-loadaddr handler
@@ -1321,7 +1391,8 @@ var ConfigGUI = (function($) {
// Bind generic remove-row inside drivers
$container.on('click', '.cfg-if-panel [data-action="remove-row"]', function() {
if (confirm('Remove this row?')) $(this).closest('tr').remove();
var row = $(this).closest('tr');
modalConfirm('Remove this row?', function() { row.remove(); });
});
// File browse buttons for ROM files and param (disk image) files.
@@ -1349,6 +1420,18 @@ var ConfigGUI = (function($) {
});
}
// Render a single driver-level ROM row (simple: enable + file, no loadaddr).
function renderDrvRomRow(rom) {
var chk = rom.enable ? ' checked' : '';
var html = '<tr>';
html += '<td style="width:30px;text-align:center;"><input type="checkbox" data-field="enable"' + chk + ' title="Enable this ROM to overwrite the host ROM"></td>';
html += '<td><div class="input-group" style="width:280px;"><input type="text" data-field="file" value="' + (rom.file || '') + '" style="font-size:11px;" title="ROM image file path on the SD card">';
html += '<span class="input-group-btn"><button type="button" class="btn btn-default btn-xs" data-action="browse-drv-rom" title="Browse SD card for ROM file" style="height:22px;padding:1px 6px;"><i class="fa fa-folder-open"></i></button></span></div></td>';
html += '<td style="width:30px;"><button type="button" class="btn btn-danger btn-xs cfg-btn-remove" data-action="remove-drv-rom" title="Remove this ROM entry"><i class="fa fa-times"></i></button></td>';
html += '</tr>';
return html;
}
function renderDriver(driver, drvIdx) {
var drvId = uid('drv');
var chk = driver.enable ? ' checked' : '';
@@ -1383,6 +1466,25 @@ var ConfigGUI = (function($) {
html += '<label style="font-size:12px;" title="Enable or disable this driver"><input type="checkbox" data-field="drv-enable"' + chk + ' title="Enable this driver"> Enabled</label>';
html += '</div>';
// Driver-level ROMs — system ROMs that overwrite the host's ROMs when enabled.
// The driver knows the load layout so no loadaddr is needed, just file + enable.
html += '<div data-section="drv-roms" style="margin-bottom:10px;">';
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">System ROMs</h5>';
html += '<table class="table table-condensed table-bordered cfg-rom-table" data-section="drv-rom" style="width:auto !important;">';
html += '<thead><tr>';
html += '<th title="Enable loading this ROM to overwrite the host ROM">En</th>';
html += '<th title="ROM image file on the SD card">File</th>';
html += '<th></th>';
html += '</tr></thead><tbody>';
if (driver.rom && driver.rom.length > 0) {
for (var dr = 0; dr < driver.rom.length; dr++) {
html += renderDrvRomRow(driver.rom[dr]);
}
}
html += '</tbody></table>';
html += '<button type="button" class="btn btn-success btn-xs cfg-btn-add" data-action="add-drv-rom" title="Add a system ROM file"><i class="fa fa-plus"></i> Add ROM</button>';
html += '</div>';
// Interfaces - always show section with Add button
html += '<div data-section="interfaces">';
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">Interfaces</h5>';
@@ -1415,6 +1517,19 @@ var ConfigGUI = (function($) {
type: $drv.find('[data-field="drv-type"]').first().val()
};
// Collect driver-level ROMs
var drvRoms = [];
$drv.find('table[data-section="drv-rom"] tbody tr').each(function() {
var $tr = $(this);
drvRoms.push({
enable: $tr.find('[data-field="enable"]').is(':checked') ? 1 : 0,
file: $tr.find('[data-field="file"]').val() || ''
});
});
if (drvRoms.length > 0) {
driver.rom = drvRoms;
}
// Collect interfaces
var $ifs = $drv.find('[data-if-idx]');
if ($ifs.length > 0) {
@@ -1460,6 +1575,46 @@ var ConfigGUI = (function($) {
html += '<span class="help-block col-sm-6" style="font-size:11px;">Operating mode</span>';
html += '</div>';
// WiFi settings
html += '<hr style="margin:10px 0;">';
html += '<div style="font-size:12px;font-weight:bold;margin-bottom:8px;">Network Interfaces</div>';
var wifiChk = (espCore.wifi_enable === undefined || espCore.wifi_enable) ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">WiFi Enable</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="wifi_enable"' + wifiChk + ' title="Enable WiFi network interface"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable WiFi (requires reboot). Ignored if WiFi not compiled in.</span>';
html += '</div>';
// NCM (USB Network) settings
var ncmChk = (espCore.ncm === undefined || espCore.ncm) ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Enable</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="ncm"' + ncmChk + ' title="Enable USB NCM network interface"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable USB NCM network (requires reboot)</span>';
html += '</div>';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Retries</label>';
html += '<div class="col-sm-3">';
html += '<input type="number" class="form-control" data-field="ncmretries" min="0" max="1000" value="' + (espCore.ncmretries || 0) + '" title="Max connection retry cycles (0=forever, 5-1000)">';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = retry forever, 5-1000 = max attempts</span>';
html += '</div>';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Retry Period</label>';
html += '<div class="col-sm-3">';
html += '<input type="number" class="form-control" data-field="ncmperiod" min="0" max="120" value="' + (espCore.ncmperiod || 0) + '" title="Delay between retries in seconds (0=increasing backoff, 1-120=fixed)">';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = increasing backoff (1s per retry, max 60s), 1-120 = fixed seconds</span>';
html += '</div>';
html += '</form>';
html += '</div></div></div></div>';
@@ -1469,8 +1624,12 @@ var ConfigGUI = (function($) {
function collectESP32Core(containerId) {
var $el = $('#' + containerId);
return {
device: $el.find('[data-field="device"]').val() || 'Z80',
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0
device: $el.find('[data-field="device"]').val() || 'Z80',
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0,
wifi_enable: $el.find('[data-field="wifi_enable"]').is(':checked') ? 1 : 0,
ncm: $el.find('[data-field="ncm"]').is(':checked') ? 1 : 0,
ncmretries: parseInt($el.find('[data-field="ncmretries"]').val(), 10) || 0,
ncmperiod: parseInt($el.find('[data-field="ncmperiod"]').val(), 10) || 0
};
}
@@ -1587,18 +1746,18 @@ var ConfigGUI = (function($) {
var z80 = rp.z80 || [];
// RP2350 Global Core
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings');
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings', false);
// Partition 1 (z80[1])
var p1 = z80[1] || {};
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings');
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings', true);
renderMemorySection('cfg-p1-memory', p1.memory || []);
renderIOSection('cfg-p1-io', p1.io || []);
renderDriversSection('cfg-p1-drivers', p1.drivers || []);
// Partition 2 (z80[2])
var p2 = z80[2] || {};
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings');
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings', true);
renderMemorySection('cfg-p2-memory', p2.memory || []);
renderIOSection('cfg-p2-io', p2.io || []);
renderDriversSection('cfg-p2-drivers', p2.drivers || []);
@@ -1619,20 +1778,20 @@ var ConfigGUI = (function($) {
function collectConfig() {
var cfg = {
rp2350: {
core: collectCore('cfg-rp2350-core'),
core: collectCore('cfg-rp2350-core', false),
z80: [
// z80[0] = Bootloader (currently empty)
{},
// z80[1] = Partition 1
{
core: collectCore('cfg-p1-core'),
core: collectCore('cfg-p1-core', true),
memory: collectMemory('cfg-p1-memory'),
io: collectIO('cfg-p1-io'),
drivers: collectDrivers('cfg-p1-drivers')
},
// z80[2] = Partition 2
{
core: collectCore('cfg-p2-core'),
core: collectCore('cfg-p2-core', true),
memory: collectMemory('cfg-p2-memory'),
io: collectIO('cfg-p2-io'),
drivers: collectDrivers('cfg-p2-drivers')
@@ -1728,41 +1887,48 @@ var ConfigGUI = (function($) {
var v = parseInt($(this).val(), 10);
if (v > 133) warnings.push('PSRAM Frequency ' + v + ' MHz exceeds 133 MHz — device may not boot');
});
if (warnings.length > 0) {
if (!confirm('Warnings:\n\n' + warnings.join('\n') + '\n\nSave anyway?')) {
return;
}
function doSave() {
var cfg = collectConfig();
var json = JSON.stringify(cfg, null, 2);
$('#cfgSaveBtn').prop('disabled', true);
showMsg('Saving configuration...', 'alert-info');
$.ajax({
url: '/data/config',
type: 'POST',
contentType: 'application/json',
data: json,
success: function(response) {
var msg = '';
if (typeof response === 'string') {
msg = response;
} else if (response && response.message) {
msg = response.message;
} else {
msg = 'Configuration saved successfully.';
}
showMsg(msg, 'alert-success');
$('#cfgSaveBtn').prop('disabled', false);
// Reload the config from the device to reflect the saved state.
setTimeout(function() { fetchConfig(); }, 1000);
},
error: function(jqXHR, textStatus, errorThrown) {
var msg = jqXHR.responseText || 'Failed to save configuration: ' + (errorThrown || textStatus);
showMsg(msg, 'alert-danger');
$('#cfgSaveBtn').prop('disabled', false);
}
});
}
var cfg = collectConfig();
var json = JSON.stringify(cfg, null, 2);
if (warnings.length > 0) {
modalConfirm('Warnings:\n\n' + warnings.join('\n') + '\n\nSave anyway?', function() {
doSave();
});
return;
}
$('#cfgSaveBtn').prop('disabled', true);
showMsg('Saving configuration...', 'alert-info');
$.ajax({
url: '/data/config',
type: 'POST',
contentType: 'application/json',
data: json,
success: function(response) {
var msg = '';
if (typeof response === 'string') {
msg = response;
} else if (response && response.message) {
msg = response.message;
} else {
msg = 'Configuration saved successfully.';
}
showMsg(msg, 'alert-success');
$('#cfgSaveBtn').prop('disabled', false);
},
error: function(jqXHR, textStatus, errorThrown) {
var msg = jqXHR.responseText || 'Failed to save configuration: ' + (errorThrown || textStatus);
showMsg(msg, 'alert-danger');
$('#cfgSaveBtn').prop('disabled', false);
}
});
doSave();
}
// -----------------------------------------------------------------------

View File

@@ -77,13 +77,13 @@ $(document).ready(function () {
const statusDiv = document.getElementById('saveStatus');
if (!editFileInput || !textarea) {
alert("Editor elements not found!");
modalAlert("Editor elements not found!");
return;
}
let fullInput = editFileInput.value.trim();
if (!fullInput) {
alert("No file path/filename specified!");
modalAlert("No file path/filename specified!");
return;
}
@@ -94,7 +94,7 @@ $(document).ready(function () {
.replace(/^[\\/]+/, '');
if (!cleanedPath) {
alert("No valid path after removing SD prefix!");
modalAlert("No valid path after removing SD prefix!");
return;
}
@@ -109,11 +109,11 @@ $(document).ready(function () {
}
if (!filenameOnly) {
alert("No valid filename found!");
modalAlert("No valid filename found!");
return;
}
if (filenameOnly.includes(' ')) {
alert("Filename cannot contain spaces!");
modalAlert("Filename cannot contain spaces!");
return;
}
@@ -190,7 +190,7 @@ $(document).ready(function () {
statusDiv.style.color = "#721c24";
statusDiv.innerHTML = `✗ Save failed: ${err.message}`;
}
alert("Save failed!\n" + err.message); // keep alert as fallback
modalAlert("Save failed!\n" + err.message); // keep alert as fallback
} finally {
// Re-enable controls

View File

@@ -1,5 +1,19 @@
var lastStatus = 0;
// Backup the entire SD card as a tar download.
function backupSD() {
modalConfirm('Download the entire SD card as a tar archive?\n\nThis may take several minutes depending on the SD card size and connection speed.', function() {
// Create a temporary hidden link to trigger the download.
var a = document.createElement('a');
a.href = '/data/backup';
a.download = 'picoZ80_backup.tar';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
}
// Confirm delete with "Don't ask again" checkbox.
// Uses localStorage to persist the preference across sessions.
// To re-enable: localStorage.removeItem('skipDeleteConfirm') in browser console.
@@ -94,19 +108,19 @@ function uploadFile() {
if (fileInput.length == 0)
{
alert("No file selected!");
modalAlert("No file selected!");
} else if (filePath.length == 0)
{
alert("File path on server is not set!");
modalAlert("File path on server is not set!");
} else if (filePath.indexOf(' ') >= 0)
{
alert("File path on server cannot have spaces!");
modalAlert("File path on server cannot have spaces!");
} else if (filePath[filePath.length-1] == '/')
{
alert("File name not specified after path!");
modalAlert("File name not specified after path!");
} else if (fileInput[0].size > MAX_FILE_SIZE)
{
alert("File size must be less than "+MAX_FILE_SIZE_STR+"!");
modalAlert("File size must be less than "+MAX_FILE_SIZE_STR+"!");
} else
{
document.getElementById("newfile").disabled = true;
@@ -126,6 +140,11 @@ function uploadFile() {
var fileSize = file.size;
var xhttp = new XMLHttpRequest();
// Detect if the file is an archive that the server will unpack.
var fileName = file.name.toLowerCase();
var isArchive = fileName.endsWith('.tar') || fileName.endsWith('.gz') ||
fileName.endsWith('.tgz') || fileName.endsWith('.tar.gz');
// Track upload progress.
xhttp.upload.addEventListener("progress", function(e) {
if (e.lengthComputable && progressBar && progressText) {
@@ -143,26 +162,36 @@ function uploadFile() {
}
});
// When upload transfer completes, show "Unpacking..." for archives while
// waiting for the server to finish processing and send its response.
xhttp.upload.addEventListener("load", function() {
if (isArchive && progressBar && progressText) {
progressBar.style.width = "100%";
progressBar.style.background = "#f0ad4e"; // amber/orange
progressText.textContent = "Unpacking archive on device... please wait";
}
});
xhttp.onreadystatechange = function()
{
if (xhttp.readyState == 4)
{
if (xhttp.status == 200)
{
if (progressBar) progressBar.style.width = "100%";
if (progressText) progressText.textContent = "Upload complete!";
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#5cb85c"; }
if (progressText) progressText.textContent = isArchive ? "Unpacked successfully!" : "Upload complete!";
setTimeout(function() { location.reload(); }, 1000);
} else if (xhttp.status == 0)
{
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#d9534f"; }
if (progressText) progressText.textContent = "Upload failed — connection lost";
alert("Upload failed, server closed the connection abruptly!");
modalAlert("Upload failed server closed the connection abruptly!");
location.reload();
} else
{
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#d9534f"; }
if (progressText) progressText.textContent = "Upload failed — error " + xhttp.status;
alert(xhttp.status + " Error!\n" + xhttp.responseText);
modalAlert(xhttp.status + " Error!\n" + xhttp.responseText);
location.reload();
}
}
@@ -208,7 +237,7 @@ function downloadFile(event) {
})
.catch(error => {
console.error("Download failed:", error);
alert("Failed to download the file.");
modalAlert("Failed to download the file.");
});
}
@@ -248,6 +277,201 @@ function mkdir() {
})
.catch(error => {
console.error('Error:', error);
alert('Failed to create directory');
modalAlert('Failed to create directory');
});
}
// Toggle all file-select checkboxes.
function toggleSelectAll(master) {
var checkboxes = document.querySelectorAll('.file-select');
checkboxes.forEach(function(cb) { cb.checked = master.checked; });
updateDeleteSelectedBtn();
}
// Update the "Delete Selected" button visibility and count.
function updateDeleteSelectedBtn() {
var checked = document.querySelectorAll('.file-select:checked');
var btn = document.getElementById('deleteSelectedBtn');
var countSpan = document.getElementById('deleteSelectedCount');
if (btn) {
btn.style.display = checked.length > 0 ? '' : 'none';
}
if (countSpan) {
countSpan.textContent = checked.length;
}
}
// Delete all selected files sequentially using the same modal style as single delete.
function deleteSelected() {
var checked = document.querySelectorAll('.file-select:checked');
if (checked.length === 0) return;
var names = [];
checked.forEach(function(cb) { names.push(cb.getAttribute('data-name')); });
var dir = checked[0].getAttribute('data-dir');
// Build file list HTML (max 15 shown, then "and N more...")
var listHtml = '<div style="text-align:left;max-height:200px;overflow-y:auto;margin:10px 0;' +
'padding:8px 12px;background:#1a1a1a;border-radius:4px;font-size:12px;line-height:1.6;">';
var showCount = Math.min(names.length, 15);
for (var i = 0; i < showCount; i++) {
listHtml += '<div>' + names[i] + '</div>';
}
if (names.length > 15) {
listHtml += '<div style="color:#999;">...and ' + (names.length - 15) + ' more</div>';
}
listHtml += '</div>';
// Show modal overlay matching the single-delete style.
var overlay = document.createElement('div');
overlay.id = 'delConfirmOverlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;' +
'background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
overlay.innerHTML =
'<div style="background:#2c2c2c;border:1px solid #555;border-radius:8px;padding:20px 30px;' +
'max-width:450px;color:#eee;font-size:14px;text-align:center;">' +
'<p style="margin-bottom:5px;">Delete <b>' + names.length + ' selected item(s)</b>?</p>' +
listHtml +
'<button id="delMultiOk" style="margin-right:10px;padding:6px 20px;cursor:pointer;' +
'background:#c33;color:#fff;border:1px solid #a22;border-radius:4px;">Delete All</button>' +
'<button id="delMultiCancel" style="padding:6px 20px;cursor:pointer;' +
'background:#555;color:#fff;border:1px solid #444;border-radius:4px;">Cancel</button></div>';
document.body.appendChild(overlay);
document.getElementById('delMultiOk').onclick = function() {
// Replace modal content with a prominent progress display.
var total = names.length;
overlay.querySelector('div').innerHTML =
'<p style="margin-bottom:12px;color:#eee;font-size:16px;font-weight:bold;">' +
'<i class="fa fa-trash" style="color:#c33;margin-right:8px;"></i>Deleting ' + total + ' item(s)...</p>' +
'<div style="background:#444;border-radius:4px;height:20px;margin:12px 0;overflow:hidden;">' +
'<div id="delMultiBar" style="background:#c33;height:100%;width:0%;transition:width 0.2s;border-radius:4px;"></div></div>' +
'<p id="delMultiCount" style="color:#ccc;font-size:13px;margin:8px 0;">0 / ' + total + '</p>' +
'<p id="delMultiName" style="color:#999;font-size:11px;word-break:break-all;min-height:16px;"></p>';
var idx = 0;
var errors = 0;
function deleteNext() {
if (idx >= total) {
// Show completion briefly before reloading.
var bar = document.getElementById('delMultiBar');
var count = document.getElementById('delMultiCount');
var fname = document.getElementById('delMultiName');
if (bar) bar.style.background = '#5cb85c';
if (count) count.textContent = 'Complete! Deleted ' + (total - errors) + ' of ' + total + ' item(s).';
if (count) count.style.color = '#5cb85c';
if (fname) fname.textContent = errors > 0 ? errors + ' failed' : 'Reloading...';
setTimeout(function() {
document.body.removeChild(overlay);
location.reload();
}, 1200);
return;
}
var pct = Math.round(((idx + 1) / total) * 100);
var bar = document.getElementById('delMultiBar');
var count = document.getElementById('delMultiCount');
var fname = document.getElementById('delMultiName');
if (bar) bar.style.width = pct + '%';
if (count) count.textContent = (idx + 1) + ' / ' + total;
if (fname) fname.textContent = names[idx];
var name = names[idx++];
var url = '?cmd=del&name=' + encodeURIComponent(name) + '&dir=' + encodeURIComponent(dir);
// Use a small timeout to allow the browser to repaint the progress bar.
setTimeout(function() {
fetch(url).then(function() { deleteNext(); }).catch(function() { errors++; deleteNext(); });
}, 50);
}
deleteNext();
};
document.getElementById('delMultiCancel').onclick = function() {
document.body.removeChild(overlay);
};
}
// ---------------------------------------------------------------------------
// Client-side column sorting for the file manager table.
// Clicking a column header toggles ascending/descending sort.
// The first two rows (up-level link + upload controls) are kept at the top;
// only the file/directory data rows are sorted.
// ---------------------------------------------------------------------------
(function() {
// Track current sort state.
var currentCol = -1;
var ascending = true;
document.addEventListener('click', function(e) {
var th = e.target.closest('th[data-sort-col]');
if (!th) return;
var col = parseInt(th.getAttribute('data-sort-col'), 10);
var sortType = th.getAttribute('data-sort-type') || 'text';
var table = th.closest('table');
if (!table) return;
var tbody = table.querySelector('tbody');
if (!tbody) return;
// Toggle direction if clicking the same column, otherwise ascending.
if (col === currentCol) {
ascending = !ascending;
} else {
currentCol = col;
ascending = true;
}
// Collect all rows. The first rows may be [up level] and upload controls —
// identify data rows by having a form with cmd=ren (the rename form).
var allRows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
var fixedRows = [];
var dataRows = [];
allRows.forEach(function(row) {
if (row.querySelector('input[name="oldname"]')) {
dataRows.push(row);
} else {
fixedRows.push(row);
}
});
// Sort data rows.
dataRows.sort(function(a, b) {
var cellA = a.cells[col];
var cellB = b.cells[col];
if (!cellA || !cellB) return 0;
var valA, valB;
if (sortType === 'num') {
// Extract numeric value (from size column text content).
valA = parseFloat(cellA.textContent.replace(/[^0-9.-]/g, '')) || 0;
valB = parseFloat(cellB.textContent.replace(/[^0-9.-]/g, '')) || 0;
} else {
// Text sort: use the input value for column 0 (name), textContent otherwise.
var inputA = cellA.querySelector('input[name="name"]');
var inputB = cellB.querySelector('input[name="name"]');
valA = (inputA ? inputA.value : cellA.textContent).toLowerCase();
valB = (inputB ? inputB.value : cellB.textContent).toLowerCase();
}
if (valA < valB) return ascending ? -1 : 1;
if (valA > valB) return ascending ? 1 : -1;
return 0;
});
// Re-append rows: fixed rows first, then sorted data rows.
fixedRows.forEach(function(row) { tbody.appendChild(row); });
dataRows.forEach(function(row) { tbody.appendChild(row); });
// Update sort indicator icons.
table.querySelectorAll('th[data-sort-col] .fa').forEach(function(icon) {
icon.className = 'fa fa-sort';
icon.style.color = '#999';
});
var icon = th.querySelector('.fa');
if (icon) {
icon.className = ascending ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
icon.style.color = '#333';
}
});
})();

View File

@@ -62,15 +62,15 @@ function reloadConfig() {
.then(response => {
if (response.ok) {
// Optional: show a quick message (non-blocking)
alert('RP2350 config reloaded successfully');
modalAlert('RP2350 config reloaded successfully');
// or better: add a <div id="status"> somewhere and do:
// document.getElementById('status').innerText = 'Reloaded OK';
} else {
alert('Reload failed: ' + response.status);
modalAlert('Reload failed: ' + response.status);
}
})
.catch(err => {
alert('Error: ' + err);
modalAlert('Error: ' + err);
});
}

View File

@@ -97,6 +97,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -97,6 +97,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -95,6 +95,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -1 +1 @@
2.49
2.66

View File

@@ -95,6 +95,7 @@
<li><a href="reboot/esp32"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">ESP32</a></li>
<li><a href="#" onclick="rebootRP2350(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">RP2350B</a></li>
<li><a href="#" onclick="rebootHost(); return false;"><i class="fa fa-rotate-left"></i><span style="padding-left: 10px;">Host</a></li>
<li><a href="#" onclick="rebootIPL(); return false;"><i class="fa fa-power-off"></i><span style="padding-left: 10px;">IPL Reset</a></li>
</ul>
</li>
</ul>

View File

@@ -17,8 +17,10 @@ set(pZ80_drivers_sharp_src
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ700.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ1500.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ80A.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ80B.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ2000.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ2200.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ2500.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/RFS.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/WD1773.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/QDDrive.c
@@ -33,6 +35,8 @@ set(pZ80_drivers_sharp_src
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R37.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/PIO-3034.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/Celestite.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/SASI.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ1E30.c
)
set(pM6502_common_src

View File

@@ -52,8 +52,10 @@
#include "drivers/Sharp/MZ700.h" // Sharp MZ700 Persona driver.
#include "drivers/Sharp/MZ1500.h" // Sharp MZ-1500 Persona driver.
#include "drivers/Sharp/MZ80A.h" // Sharp MZ80A Persona driver.
#include "drivers/Sharp/MZ80B.h" // Sharp MZ-80B Persona driver.
#include "drivers/Sharp/MZ2000.h" // Sharp MZ-2000 Persona driver.
#include "drivers/Sharp/MZ2200.h" // Sharp MZ-2200 Persona driver.
#include "drivers/Sharp/MZ2500.h" // Sharp MZ-2500 Persona driver.
#endif
@@ -105,8 +107,8 @@ volatile uint8_t g_dbgForceIOWait;
// IRQ 0 - SM0 - ADDRESS LOAD
// IRQ 1 - SM1 - DATA LOAD
static uint sm_addr = PIO_SM_0; // 4 words
static uint sm_data = PIO_SM_1; // 6 words
// 10 total words
static uint sm_data = PIO_SM_1; // 7 words
// 11 total words
// PIO 1 state machines.
// IRQ 0 - SM0 - CYCLE START
// IRQ 5 - SM3 - REFRESH
@@ -212,10 +214,14 @@ static const t_VirtualFuncMap virtualFuncMap[] = {
// it sets up devices and memory structure to replicate the MZ-1500 logic.
{"MZ80A", (t_VirtualFunc) MZ80A_Init}, // This virtual function creates a Sharp MZ80A 'persona' whereby
// it sets up devices and memory structure to replicate the MZ80A logic.
{"MZ80B", (t_VirtualFunc) MZ80B_Init}, // This virtual function creates a Sharp MZ-80B 'persona' whereby
// it sets up devices and memory structure to replicate the MZ-80B logic.
{"MZ2000", (t_VirtualFunc) MZ2000_Init}, // This virtual function creates a Sharp MZ-2000 'persona' whereby
// it sets up devices and memory structure to replicate the MZ-2000 logic.
{"MZ2200", (t_VirtualFunc) MZ2200_Init}, // This virtual function creates a Sharp MZ-2200 'persona' whereby
// it sets up devices and memory structure to replicate the MZ-2200 logic.
{"MZ2500", (t_VirtualFunc) MZ2500_Init}, // This virtual function creates a Sharp MZ-2500 'persona' whereby
// it sets up devices and memory structure to replicate the MZ-2500 logic.
#endif
};
static const size_t virtualFuncMapSize = sizeof(virtualFuncMap) / sizeof(virtualFuncMap[0]);
@@ -632,6 +638,45 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
continue;
}
/////////////////////////////
// Driver-level "rom" array
/////////////////////////////
// Optional ROM files at the driver level (not interface level).
// These are system ROMs where the driver knows the load layout (no loadaddr needed).
cJSON *drvROMArr = cJSON_GetObjectItem(driver, "rom");
if (cJSON_IsArray(drvROMArr))
{
size_t drvROMCnt = cJSON_GetArraySize(drvROMArr);
if (drvROMCnt > 0)
{
if (!cpu->_drivers.driver[validDrivers].romConfig)
cpu->_drivers.driver[validDrivers].romConfig = (t_drvROMConfig *) calloc(drvROMCnt, sizeof(t_drvROMConfig));
if (cpu->_drivers.driver[validDrivers].romConfig)
{
int validDrvROMs = 0;
for (size_t ri = 0; ri < drvROMCnt; ri++)
{
cJSON *romItem = cJSON_GetArrayItem(drvROMArr, ri);
if (!cJSON_IsObject(romItem))
continue;
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
if (!cJSON_IsNumber(romEnable) || romEnable->valueint != 1)
continue;
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
continue;
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romFile = strdup(romFile->valuestring);
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddrCount = 0;
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddr = NULL;
validDrvROMs++;
}
cpu->_drivers.driver[validDrivers].romCount = validDrvROMs;
debugf("Z80 Drivers ROM: Driver ROM Maps(%d)\r\n", validDrvROMs);
}
}
}
//////////////
// "if" array
//////////////
@@ -806,13 +851,12 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
validLoadAddrConfigs++;
}
// Only setup an entry if there are valid load address configurations.
if (validLoadAddrConfigs > 0)
{
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].romConfig[validROMConfigs].romFile = strdup(drvROMFile->valuestring);
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].romConfig[validROMConfigs].romAddrCount = validLoadAddrConfigs;
validROMConfigs++;
}
// Always store the ROM file entry — drivers with port-based ROM access
// (e.g. MZ-1E30 SASI) need the filename even with no loadaddr mappings.
// romAddrCount=0 tells the standard loader "nothing to map into Z80 space."
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].romConfig[validROMConfigs].romFile = strdup(drvROMFile->valuestring);
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].romConfig[validROMConfigs].romAddrCount = validLoadAddrConfigs;
validROMConfigs++;
}
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].romCount = validROMConfigs;
}
@@ -981,14 +1025,24 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
continue;
}
// Extract fields
// Extract known key/value pairs. Each param entry has one key
// that identifies the parameter type ("file", "ip", etc.).
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
bool hasValue = false;
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
{
continue;
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
hasValue = true;
}
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
{
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].ip = strdup(ip->valuestring);
hasValue = true;
}
if (!hasValue)
continue;
validParams++;
}
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParamCount = validParams;
@@ -1626,6 +1680,79 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
}
debugf("Z80 Drivers: enable(%d) name(%s) physical(%s)\r\n", enableDriver->valueint, driverName->valuestring, driverIsPhysical->valuestring);
/////////////////////////////
// Driver-level "rom" array
/////////////////////////////
// Driver-level ROM files (no loadaddr — the driver knows the layout).
// Parse and store to flash so Z80CPU_ReadROM can find them at startup.
cJSON *drvROMDrv = cJSON_GetObjectItem(driver, "rom");
if (cJSON_IsArray(drvROMDrv))
{
size_t drvROMDrvCount = cJSON_GetArraySize(drvROMDrv);
for (size_t ri = 0; ri < drvROMDrvCount; ri++)
{
cJSON *romItem = cJSON_GetArrayItem(drvROMDrv, ri);
if (!cJSON_IsObject(romItem))
continue;
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
if (cJSON_IsNumber(romEnable) && romEnable->valueint != 1)
continue;
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
continue;
debugf("Z80 Drivers ROM: file(%s)\r\n", romFile->valuestring);
// Store to flash (same pattern as interface ROMs).
if (appConfig->s.romCount >= (FLASH_MAX_ROM_IMAGES - 1))
{
debugf("Error: No free slots in ROM Header for file:%s\r\n", romFile->valuestring);
continue;
}
int found = -1;
for (int idx = 0; idx < appConfig->s.romCount; idx++)
{
if (strcmp(appConfig->s.ROMS[idx].filename, romFile->valuestring) == 0)
found = idx;
}
if (found >= 0)
{
debugf("Warning: ROM (file=%s) already stored in slot %d, skipping.\r\n", romFile->valuestring, found);
continue;
}
memset((uint8_t *) romBuffer, 0, FLASH_MAX_ROM_IMAGE_SIZE);
watchdog_update();
debugf("Reading driver ROM file:%s\r\n", romFile->valuestring);
int fileSize = ESP_readFile(romFile->valuestring, NULL, NULL, NULL, &romBuffer, FLASH_MAX_ROM_IMAGE_SIZE, false, 0);
watchdog_update();
if (fileSize <= 0)
{
debugf("Error: ROM file '%s' not found or empty\r\n", romFile->valuestring);
continue;
}
if ((romNextPos + fileSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
{
debugf("Error: ROM Image (%s, %dbytes) exceeds available FlashRAM storage\r\n", romFile->valuestring, fileSize);
continue;
}
uint32_t flashSize = ((fileSize % FLASH_SECTOR_SIZE) == 0) ? fileSize : ((fileSize / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
if ((romNextPos + flashSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
{
debugf("Error: ROM Image (%s) rounded up to sector exceeds FlashRAM\r\n", romFile->valuestring);
continue;
}
updateFlashBytes(romNextPos, (uint8_t *) romBuffer, flashSize);
appConfig->s.ROMS[appConfig->s.romCount].size = fileSize;
appConfig->s.ROMS[appConfig->s.romCount].addr = romNextPos;
strcpy(appConfig->s.ROMS[appConfig->s.romCount].filename, romFile->valuestring);
romNextPos += flashSize;
appConfig->s.romCount++;
debugf("Z80 Drivers ROM: stored %d bytes to flash slot %d\r\n", fileSize, appConfig->s.romCount - 1);
}
}
////////////////
// "if" array
////////////////
@@ -2014,16 +2141,24 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
continue;
}
// Extract fields
// Extract known key/value pairs.
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
{
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
}
else if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
{
debugf("Z80 Drivers IF Params: ip(%s)\r\n", ip->valuestring);
}
else
{
debugf("Error: Virtual driver \"param\" item %zu, driver %zu invalid, skipping\r\n", i7, drvConfig);
result = false;
continue;
}
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
}
}
@@ -2042,6 +2177,7 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
//////////////////////////
// Finally, if we encountered no errors, write out the header. Any errors then this config wont be marked active.
watchdog_update();
if (result == true)
updateFlashBytes(FLASH_APP_START_CONFIG_POS + (FLASH_APP_CONFIG_SIZE * (cfgApp - 1)), (uint8_t *) appConfig, sizeof(t_FlashAppConfigHeader));
@@ -2228,7 +2364,11 @@ int Z80CPU_init_pio(void)
}
pio_sm_set_enabled(pio_0, sm_addr, true);
// Data Bus - Setup data on the bus, tri-state on BUSACK.
// Data Bus - Setup data on the bus, tristate after /WR completes.
// After driving data, the SM waits for /WR low then high, ensuring the
// data bus is held for the entire write cycle (including wait states),
// then tristates. This prevents stale data persisting on the physical
// bus in virtual mode where no subsequent bus cycle would clear it.
offset_data = pio_add_program(pio_0, &z80_data_program);
pio_sm_config c_data = z80_data_program_get_default_config(offset_data);
sm_config_set_out_pins(&c_data, Z80_PIN_DATA_0, 8);
@@ -2236,6 +2376,13 @@ int Z80CPU_init_pio(void)
sm_config_set_out_shift(&c_data, true, true, 24); // Shift right, auto pull 24 bits (8 final direction, 8 data, 8 start direction).
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_DATA_0, 8, false); // Set as input initially.
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_BUSACK, 1, false);
// Enable internal pull-ups on D0-D7. A real Z80 has weak internal pull-ups
// on its data bus; when no device is driving, the bus drifts to 0xFF.
// Without pull-ups the RP2350 GPIOs are true high-Z — bus capacitance holds
// the last driven value indefinitely, causing false device detection in
// virtual mode (e.g. IPL FDC probe writes 0xA5, reads it back after 80µs).
for (uint i = Z80_PIN_DATA_0; i < Z80_PIN_DATA_0 + 8; i++)
gpio_pull_up(i);
sm_config_set_clkdiv(&c_data, 1.0f);
pio_sm_clear_fifos(pio_0, sm_data);
pio_sm_restart(pio_0, sm_data);
@@ -2441,6 +2588,7 @@ t_Z80CPU *Z80CPU_init(void)
cpu.dbgVerifyEnabled = false;
cpu.dbgCorruptCount = 0;
cpu.dbgCorruptHead = 0;
memset(&cpu.dbgHooks, 0, sizeof(cpu.dbgHooks));
#endif
// Request Queue: Core 1 → Core 0 (Commands from ALL devices)
@@ -2630,7 +2778,8 @@ void __func_in_RAM(Z80CPU_cpu)(t_Z80CPU *cpu)
{
// Locals.
static int pollCnt = 0;
int pollLimit = (0.250 / (1/cpu->hostClkHz)) / T_STATES_PER_LOOP;
int pollLimit = (int)((0.250 * (double)cpu->hostClkHz) / T_STATES_PER_LOOP);
if (pollLimit < 1) pollLimit = 1;
// Notify core 0 than we are configured and running.
multicore_fifo_push_blocking(1);
@@ -3030,12 +3179,14 @@ uint8_t __func_in_RAM(Z80CPU_fetchOpcode)(void *context, uint16_t address)
Z80CPU_waitPhysicalStates(cpu, 1);
}
// DRAM refresh: when fetching opcodes from virtual memory, the physical M1 cycle
// (which includes a RFSH bus cycle) is not generated. Physical DRAM on the host
// motherboard will decay without a refresh.
// Virtual mode: always generate a physical M1 cycle to keep the host
// gate array's internal clocks running (8253 timer, interrupt controller).
// Without M1 bus activity, /INT is never asserted. The data from the
// physical bus is discarded — PSRAM data is used for execution.
// Interrupt suppression is handled in fetchByte (z80_int gating).
if(cpu->refreshEnable)
{
Z80CPU_refreshDRAM(context, true, address, data);
Z80CPU_fetchPhysicalMem(cpu, address);
}
}
@@ -3166,9 +3317,11 @@ void __func_in_RAM(Z80CPU_writeByte)(void *context, uint16_t address, uint8_t va
}
// Interrupt acknowledge.
// IM0 - Not yet implemented.
// IM0 - Read opcode/operands from the data bus.
// IM1 - Nothing to do, CPU will jump to 0038H
// IM2 - Read vector from data bus, return vector and emulation will process to run correct service routine.
uint32_t intAckCount = 0;
uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
{
// Locals.
@@ -3178,36 +3331,40 @@ uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
switch (cpu->_Z80.im)
{
case 0:
debugf("INT IM0 not implemented.\r\n");
break;
// Interrupt Mode 0 - Fetch instruction opcode from the data bus via INTA cycle.
// The interrupting device places an instruction byte on the bus during /M1+/IORQ.
// For multi-byte instructions, the Z80 library calls inta again for prefix bytes
// (CB/ED/DD/FD) and int_fetch for operand bytes (addresses, displacements).
case 0:
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
case 2:
debugf("ReadInt:%04x, mode:%02x\r\n", address, cpu->_Z80.im);
// Get the vector from the bus, used with I register to form the address of the ISR.
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
// Interrupt Mode 2 - Fetch the address to branch to from the bus.
case 2:
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
case 1:
default:
break;
// Interrupt Mode 1 - Basic branch to 0x0038
case 1:
default:
break;
}
intAckCount++;
return (vector);
}
// Special IM0 fetch to retrieve an instruction off the data bus.
// Currently not implemented.
// IM0 fetch for subsequent bytes of multi-byte instructions injected onto the data bus.
// Called by the Z80 library's im0_fetch trampoline for operand bytes (e.g. the 16-bit
// address in CALL/JP, displacement in JR/DJNZ). On real hardware these are memory read
// M-cycles, but the interrupting device (e.g. Amstrad PCW MPU) drives the data bus
// regardless of cycle type. Using INTA bus cycles here is safe and matches the device
// behaviour — it reads whatever the device puts on the bus.
uint8_t __func_in_RAM(Z80CPU_fetchIntAckIM0)(void *context, uint16_t address)
{
// Locals.
Z_UNUSED(context)
uint8_t opcode = 0;
//t_Z80CPU* cpu = (t_Z80CPU*)context;
//z80_int(&cpu->_Z80, false);
debugf("FetchIntAckIM0 - not yet implemented, addr:%04x\r\n", address);
return (opcode);
t_Z80CPU *cpu = (t_Z80CPU *)context;
return Z80CPU_fetchPhysicalIntVector(cpu, address);
}
uint8_t __func_in_RAM(Z80CPU_nmia)(void *context, uint16_t address)
@@ -3221,28 +3378,32 @@ uint8_t __func_in_RAM(Z80CPU_nmia)(void *context, uint16_t address)
void __func_in_RAM(Z80CPU_ldia)(void *context)
{
// Locals.
Z_UNUSED(context)
debugf("LDIA\r\n");
// I register changed (LD I,A). Don't reset m1DelayCount here — some
// software (CP/M) changes I frequently during normal operation, and
// resetting the delay each time permanently blocks interrupts.
// The delay is one-shot: it protects the initial boot sequence, then
// interrupts are allowed forever regardless of I changes.
}
void __func_in_RAM(Z80CPU_ldra)(void *context)
{
// Locals.
Z_UNUSED(context)
debugf("LDRA\r\n");
//debugf("LDRA\r\n");
}
// Z80 RETI instruction, ie. return from interrupt.
// This is the DEFAULT handler. Drivers that need custom RETI behaviour (e.g.
// physical bus RETI sequence for gate array in_service tracking) directly
// overwrite cpu->_Z80.reti with their own handler in Init.
void __func_in_RAM(Z80CPU_reti)(void *context)
{
// Locals.
t_Z80CPU *cpu = (t_Z80CPU *) context;
// Normally clear the emulation interrupt state, but the Z80 INT line is level active and if the interrupt routine
// hasnt reset the trigger or a new interrupt has come in, then we reinterrupt.
// Update the Z80 INT line — INT is level-sensitive, if the line is still
// active after RETI the interrupt will be re-taken immediately.
z80_int(&cpu->_Z80, (((sio_hw_t *) 0xd0000000)->gpio_hi_in & 2) == 0);
//debugf("RETI\r\n");
}
// Z80 RETN instruction, ie. return from nmi interrupt.
@@ -3259,7 +3420,17 @@ uint8_t __func_in_RAM(Z80CPU_illegal)(void *context, uint8_t opcode)
{
// Locals.
t_Z80CPU *cpu = (t_Z80CPU *) context;
debugf("Illegal Opcode:%02x PC:%04x\r\n", opcode, Z80_PC(cpu->_Z80));
// Only report each unique opcode+PC combination once to avoid flooding
// the debug UART and blocking the debug shell.
static uint8_t lastOpcode = 0xFF;
static uint16_t lastPC = 0xFFFF;
uint16_t pc = Z80_PC(cpu->_Z80);
if (opcode != lastOpcode || pc != lastPC)
{
debugf("Illegal Opcode:%02x PC:%04x\r\n", opcode, pc);
lastOpcode = opcode;
lastPC = pc;
}
return 0x00;
}

View File

@@ -158,8 +158,8 @@ static bool hostAddrReadable(uint32_t addr)
// Flash XIP: 0x10000000 0x10FFFFFF (16 MB)
if (addr >= 0x10000000 && addr <= 0x10FFFFFF)
return true;
// PSRAM QMI: 0x11000000 0x113FFFFF (4 MB)
if (addr >= 0x11000000 && addr <= 0x113FFFFF)
// PSRAM QMI: 0x11000000 0x117FFFFF (8 MB)
if (addr >= 0x11000000 && addr <= 0x117FFFFF)
return true;
// SRAM: 0x20000000 0x20081FFF (520 KB, 10 banks)
if (addr >= 0x20000000 && addr <= 0x20081FFF)
@@ -1088,6 +1088,19 @@ static void cmdDrivers(t_Z80CPU *cpu, int argc, char **argv)
ifc->isPhysical, ifc->romCount, ifc->addrMapCount, ifc->ioMapCount);
}
}
// Show registered debug hooks.
shPuts("\r\nDebug hooks:\r\n");
shPrintf(" Machine: %s\r\n", cpu->dbgHooks.machineName ? cpu->dbgHooks.machineName : "(none)");
shPrintf(" FDC: %s %s\r\n",
cpu->dbgHooks.fdcName ? cpu->dbgHooks.fdcName : "(none)",
cpu->dbgHooks.fdcTraceDump ? "(trace available)" : "");
shPrintf(" Trace: %s %s\r\n",
cpu->dbgHooks.machineTraceName ? cpu->dbgHooks.machineTraceName : "(none)",
cpu->dbgHooks.machineTraceDump ? "(available)" : "");
shPrintf(" Media: %s %s\r\n",
cpu->dbgHooks.qdName ? cpu->dbgHooks.qdName : "(none)",
cpu->dbgHooks.qdTraceDump ? "(trace available)" : "");
}
// ---------------------------------------------------------------------------
@@ -1209,45 +1222,187 @@ static void cmdReset(t_Z80CPU *cpu, int argc, char **argv)
cmdRegs(cpu, 0, NULL);
}
// Forward declarations for FDC debug functions in WD1773.c
extern void WD1773_dumpLog(void);
extern bool fdcDbgEnabled;
// IPL reset — triggers BST mode via 8255 PPI Port C bit 3 (PC3) then resets the Z80.
// This replicates the hardware IPL RESET switch on MZ-80B / MZ-2000 / MZ-2500.
// The 8255 is at I/O 0xE0-0xE3; the control register at 0xE3 supports a bit set/reset
// command (bit 7 = 0): bits 3-1 = bit number, bit 0 = set(1) or reset(0).
// Set PC3: OUT 0xE3, 0x07 (bit_num=3, set)
// Clear PC3: OUT 0xE3, 0x06 (bit_num=3, reset)
// BST is triggered on the falling edge of PC3 (1→0).
static void cmdIpl(t_Z80CPU *cpu, int argc, char **argv)
{
(void) argc;
(void) argv;
if (!cpu)
{
shPuts("CPU not initialised.\r\n");
return;
}
// Clear trace and corruption logs for a clean capture from IPL.
#ifdef INCLUDE_DBGSH
cpu->dbgTraceHead = 0;
cpu->dbgTraceCount = 0;
cpu->dbgCorruptCount = 0;
cpu->dbgCorruptHead = 0;
cpu->dbgBpHit = false;
#endif
// Hold the CPU first if it's running.
if (!cpu->hold || !cpu->holdAck)
{
cpu->hold = true;
int timeout = 3000;
while (!cpu->holdAck && timeout > 0) { sleep_ms(1); timeout--; }
if (!cpu->holdAck)
{
shPuts("CPU hold timeout — cannot IPL reset.\r\n");
cpu->hold = false;
return;
}
}
// Trigger BST via 8255 PPI Port C bit 3 (PC3).
// First ensure PC3 is HIGH (set), then clear it to generate the falling edge.
shPuts("Triggering IPL (BST via 8255 PC3)...\r\n");
Z80CPU_writePhysicalIO(cpu, 0xE3, 0x07); // Set PC3 HIGH
sleep_ms(1);
Z80CPU_writePhysicalIO(cpu, 0xE3, 0x06); // Clear PC3 LOW → BST falling edge
// Allow hardware to latch BST mode before triggering Z80 reset.
sleep_ms(10);
// Force Z80 reset and release Core 1 to process it.
cpu->forceReset = true;
__dmb();
cpu->holdAck = false;
__dmb();
cpu->hold = false;
// CPU is now running — let it boot normally.
shPuts("IPL reset triggered.\r\n");
}
// Forward declarations for FDC debug functions in WD1773.c
static void cmdFdcTrace(t_Z80CPU *cpu, int argc, char **argv)
{
(void) cpu;
if (!cpu || !cpu->dbgHooks.fdcTraceDump)
{
shPuts("No FDC driver active.\r\n");
return;
}
if (argc < 2)
{
shPrintf("FDC trace is %s. Usage: fdctrace <on|off|dump>\r\n", fdcDbgEnabled ? "ON" : "OFF");
bool on = cpu->dbgHooks.fdcTraceEnabled ? *cpu->dbgHooks.fdcTraceEnabled : false;
shPrintf("%s trace is %s. Usage: fdctrace <on|off|dump>\r\n",
cpu->dbgHooks.fdcName ? cpu->dbgHooks.fdcName : "FDC", on ? "ON" : "OFF");
return;
}
if (strcasecmp(argv[1], "dump") == 0)
WD1773_dumpLog();
else
cpu->dbgHooks.fdcTraceDump();
else if (cpu->dbgHooks.fdcTraceEnabled)
{
fdcDbgEnabled = (strcasecmp(argv[1], "on") == 0);
shPrintf("FDC trace %s\r\n", fdcDbgEnabled ? "ON" : "OFF");
*cpu->dbgHooks.fdcTraceEnabled = (strcasecmp(argv[1], "on") == 0);
shPrintf("%s trace %s\r\n",
cpu->dbgHooks.fdcName ? cpu->dbgHooks.fdcName : "FDC",
*cpu->dbgHooks.fdcTraceEnabled ? "ON" : "OFF");
}
}
// Forward declarations for QD debug functions in QDDrive.c
extern void QDDrive_dumpLog(void);
extern bool qdDbgEnabled;
static void cmdMachineTrace(t_Z80CPU *cpu, int argc, char **argv)
{
(void) argc;
(void) argv;
if (!cpu || !cpu->dbgHooks.machineTraceDump)
{
shPuts("No machine trace driver active.\r\n");
return;
}
cpu->dbgHooks.machineTraceDump();
}
// Interrupt counter (in Z80CPU.c).
extern uint32_t intAckCount;
static void cmdIntCount(t_Z80CPU *cpu, int argc, char **argv)
{
(void) argc; (void) argv;
// Also read /INT pin right now.
int intPinNow = (((sio_hw_t *) 0xd0000000)->gpio_hi_in & 2) == 0 ? 1 : 0;
shPrintf("Interrupt ACKs: %lu IFF1=%d IFF2=%d IM=%d I=%02X /INT=%s pinLow=%lu\r\n",
(unsigned long)intAckCount,
cpu ? (int)cpu->_Z80.iff1 : -1,
cpu ? (int)cpu->_Z80.iff2 : -1,
cpu ? (int)cpu->_Z80.im : -1,
cpu ? (int)cpu->_Z80.i : -1,
intPinNow ? "ACTIVE" : "high",
cpu ? (unsigned long)cpu->intPinLowCount : 0);
}
// One-shot physical→PSRAM sync: copy entire 64K from physical bus to PSRAM bank 0.
// Usage: psync [start end] — default 0000-FFFF. Runs while Z80 is held (no AHB contention).
// Includes periodic DRAM refresh and /RESET check (same pattern as cmdDm).
static void cmdPsync(t_Z80CPU *cpu, int argc, char **argv)
{
uint32_t start = 0x0000;
uint32_t end = 0xFFFF;
if (argc >= 3)
{
start = strtoul(argv[1], NULL, 16);
end = strtoul(argv[2], NULL, 16);
}
if (end > 0xFFFF) end = 0xFFFF;
if (start > end) { shPrintf("Invalid range\r\n"); return; }
// If /RESET is LOW, physical bus cycles would hang (host clock may be gated).
if (!gpio_get(38))
{
shPrintf("ERROR: /RESET is LOW — physical bus unavailable\r\n");
return;
}
shPrintf("Syncing physical bus -> PSRAM bank 0 (%04lX-%04lX)...\r\n",
(unsigned long)start, (unsigned long)end);
uint32_t mismatch = 0;
for (uint32_t a = start; a <= end; a++)
{
uint8_t physByte = gpio_get(38) ? Z80CPU_readPhysicalMem(cpu, (uint16_t)a) : 0xFF;
uint8_t psramByte = cpu->_z80PSRAM->RAM[a];
if (physByte != psramByte)
mismatch++;
cpu->_z80PSRAM->RAM[a] = physByte;
// DRAM refresh every 16 bytes to prevent DRAM decay.
if ((a & 0x0F) == 0x0F && gpio_get(38))
Z80CPU_refreshDRAM(cpu, true, 0, 0);
}
shPrintf("Done. %lu bytes synced, %lu mismatches\r\n",
(unsigned long)(end - start + 1), (unsigned long)mismatch);
}
static void cmdQdTrace(t_Z80CPU *cpu, int argc, char **argv)
{
(void) cpu;
if (!cpu || !cpu->dbgHooks.qdTraceDump)
{
shPuts("No QD/media driver active.\r\n");
return;
}
if (argc < 2)
{
shPrintf("QD trace is %s. Usage: qdtrace <on|off|dump>\r\n", qdDbgEnabled ? "ON" : "OFF");
bool on = cpu->dbgHooks.qdTraceEnabled ? *cpu->dbgHooks.qdTraceEnabled : false;
shPrintf("%s trace is %s. Usage: qdtrace <on|off|dump>\r\n",
cpu->dbgHooks.qdName ? cpu->dbgHooks.qdName : "QD", on ? "ON" : "OFF");
return;
}
if (strcasecmp(argv[1], "dump") == 0)
QDDrive_dumpLog();
else
cpu->dbgHooks.qdTraceDump();
else if (cpu->dbgHooks.qdTraceEnabled)
{
qdDbgEnabled = (strcasecmp(argv[1], "on") == 0);
shPrintf("QD trace %s\r\n", qdDbgEnabled ? "ON" : "OFF");
*cpu->dbgHooks.qdTraceEnabled = (strcasecmp(argv[1], "on") == 0);
shPrintf("%s trace %s\r\n",
cpu->dbgHooks.qdName ? cpu->dbgHooks.qdName : "QD",
*cpu->dbgHooks.qdTraceEnabled ? "ON" : "OFF");
}
}
@@ -1434,7 +1589,7 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
if (argc < 5)
{
shPuts("Usage: save <p|pf|v> <filename> <addr> <len>\r\n"
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM bank 0\r\n"
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM (full addr, e.g. 0x27D000 = bank 39)\r\n"
" filename relative to /sdcard/ on ESP32\r\n");
return;
}
@@ -1490,7 +1645,8 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
}
else
{
scratch[i] = cpu->_z80PSRAM->RAM[z80Addr];
// Use full 32-bit address for virtual PSRAM access (supports bank > 0).
scratch[i] = cpu->_z80PSRAM->RAM[(addr + i) % sizeof(cpu->_z80PSRAM->RAM)];
}
}
@@ -2240,9 +2396,10 @@ static void cmdDis(t_Z80CPU *cpu, int argc, char **argv)
// ---------------------------------------------------------------------------
// Command: wm — write memory
// wm <addr> <byte> [byte...] — auto: follows Z80 memory map
// wm p <addr> <byte> [byte...] — force physical host (Z80 bus)
// wm v <addr> <byte> [byte...] — force virtual PSRAM (bits 23:16 = bank)
// wm [p|v|r] <addr> <byte> [byte...] — write bytes
// wm [p|v|r] <addr> fill <val> <len> — fill range with a value
// wm r <addr> test — PSRAM write-read-back test
// Modes: (none)=auto mapped, p=physical Z80 bus, v=virtual PSRAM bank, r=RP2350 address
// ---------------------------------------------------------------------------
static void cmdWm(t_Z80CPU *cpu, int argc, char **argv)
{
@@ -2256,17 +2413,131 @@ static void cmdWm(t_Z80CPU *cpu, int argc, char **argv)
char c = (char) tolower((unsigned char) argv[1][0]);
if (c == 'p') { type = 'p'; argIdx++; }
else if (c == 'v') { type = 'v'; argIdx++; }
else if (c == 'r') { type = 'r'; argIdx++; }
}
if (argc - argIdx < 2)
{
shPuts("Usage: wm [p|v] <addr> <byte> [byte...]\r\n");
shPuts("Usage: wm [p|v|r] <addr> <byte> [byte...]\r\n");
shPuts(" wm [p|v|r] <addr> fill <val> <len> [w|d]\r\n");
shPuts(" wm r <addr> test\r\n");
return;
}
uint32_t fullAddr = parseAddr(argv[argIdx++]);
// Raw RP2350 address: validate range.
if (type == 'r')
{
if (!(fullAddr >= 0x11000000 && fullAddr <= 0x117FFFFF) &&
!(fullAddr >= 0x20000000 && fullAddr <= 0x20FFFFFF))
{
shPrintf("Address %08lX not in writable region (PSRAM 0x11000000-0x117FFFFF, SRAM 0x20000000+)\r\n",
(unsigned long)fullAddr);
return;
}
// Test subcommand: write-read-back with cache invalidation.
if (argIdx < argc && strcmp(argv[argIdx], "test") == 0)
{
volatile uint8_t *p = (volatile uint8_t *)fullAddr;
xip_cache_invalidate_all();
__dsb();
shPrintf("PSRAM test at %08lX:\r\n", (unsigned long)fullAddr);
shPrintf(" Before: %02X %02X %02X %02X\r\n", p[0], p[1], p[2], p[3]);
p[0] = 0xDE; p[1] = 0xAD; p[2] = 0xBE; p[3] = 0xEF;
shPrintf(" After write (cached): %02X %02X %02X %02X\r\n", p[0], p[1], p[2], p[3]);
__dsb();
xip_cache_invalidate_all();
__dsb();
shPrintf(" After cache flush: %02X %02X %02X %02X\r\n", p[0], p[1], p[2], p[3]);
bool ok = (p[0] == 0xDE && p[1] == 0xAD && p[2] == 0xBE && p[3] == 0xEF);
shPrintf(" Result: %s\r\n", ok ? "PASS" : "FAIL");
return;
}
// Fill subcommand for raw address.
if (argIdx < argc && strcmp(argv[argIdx], "fill") == 0)
{
argIdx++;
if (argc - argIdx < 2) { shPuts("Usage: wm r <addr> fill <val> <len>\r\n"); return; }
uint8_t val = (uint8_t)parseAddr(argv[argIdx++]);
uint32_t len = parseAddr(argv[argIdx++]);
memset((void *)fullAddr, val, len);
__dsb();
shPrintf("Filled %lu bytes at %08lX with %02X\r\n",
(unsigned long)len, (unsigned long)fullAddr, val);
return;
}
// Byte write for raw address.
int written = 0;
for (int i = argIdx; i < argc; i++)
{
uint8_t val = (uint8_t) strtoul(argv[i], NULL, 16);
*(volatile uint8_t *)(fullAddr + (uint32_t)written) = val;
written++;
}
__dsb();
shPrintf("Wrote %d byte(s) at %08lX (RP2350).\r\n", written, (unsigned long)fullAddr);
return;
}
// Z80 modes: auto/physical/virtual.
uint16_t addr = (uint16_t)(fullAddr & 0xFFFF);
// Check for fill subcommand.
if (argIdx < argc && strcmp(argv[argIdx], "fill") == 0)
{
argIdx++;
if (argc - argIdx < 2) { shPuts("Usage: wm [p|v] <addr> fill <val> <len> [w|d]\r\n"); return; }
uint32_t value = parseAddr(argv[argIdx++]);
uint32_t len = parseAddr(argv[argIdx++]);
int width = 1;
if (argIdx < argc)
{
char w = (char)tolower((unsigned char)argv[argIdx][0]);
if (w == 'w') width = 2;
else if (w == 'd') width = 4;
}
bool autoHeld = false;
if (!cpu->hold)
{
cpu->hold = true;
int timeout = 3000;
while (!cpu->holdAck && timeout > 0) { sleep_ms(1); timeout--; }
if (cpu->holdAck) autoHeld = true; else cpu->hold = false;
}
uint32_t ramSize = MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE;
uint32_t filled = 0;
for (uint32_t off = 0; off < len; off += (uint32_t)width)
{
for (int b = 0; b < width && (off + (uint32_t)b) < len; b++)
{
uint8_t byte = (uint8_t)((value >> (b * 8)) & 0xFF);
uint16_t wa = (uint16_t)(addr + off + (uint32_t)b);
if (type == 'p')
Z80CPU_writePhysicalMem(cpu, wa, byte);
else if (type == 'v')
{
uint32_t psAddr = (fullAddr & 0x00FF0000) | wa;
cpu->_z80PSRAM->RAM[psAddr % ramSize] = byte;
}
else
memWriteMapped(cpu, wa, byte);
filled++;
}
}
const char *label = (type == 'p') ? "physical" : (type == 'v') ? "virtual" : "mapped";
shPrintf("Filled %lu byte(s) at %04X with %0*lX (%s).\r\n",
(unsigned long)filled, addr, width * 2, (unsigned long)value, label);
if (autoHeld) cpu->hold = false;
return;
}
// Auto-hold for physical or mapped writes.
bool autoHeld = false;
if (!cpu->hold)
@@ -2309,103 +2580,6 @@ static void cmdWm(t_Z80CPU *cpu, int argc, char **argv)
cpu->hold = false;
}
// ---------------------------------------------------------------------------
// Command: fill — fill memory with a constant value
// fill [p|v] <addr> <len> <value> — 8-bit fill
// fill [p|v] <addr> <len> w <value> — 16-bit fill (little-endian)
// fill [p|v] <addr> <len> d <value> — 32-bit fill (little-endian)
// Follows Z80 memory map by default; p/v override as per dm/wm.
// ---------------------------------------------------------------------------
static void cmdFill(t_Z80CPU *cpu, int argc, char **argv)
{
if (!cpu || !cpu->_z80PSRAM) { shPuts("CPU/PSRAM not initialised.\r\n"); return; }
// Parse optional type override.
int argIdx = 1;
char type = 'a';
if (argc >= 2 && argv[1][1] == '\0')
{
char c = (char) tolower((unsigned char) argv[1][0]);
if (c == 'p') { type = 'p'; argIdx++; }
else if (c == 'v') { type = 'v'; argIdx++; }
}
// Need at least: addr len value
if (argc - argIdx < 3)
{
shPuts("Usage: fill [p|v] <addr> <len> [w|d] <value>\r\n");
shPuts(" (no w/d) = 8-bit, w = 16-bit, d = 32-bit\r\n");
return;
}
uint32_t fullAddr = parseAddr(argv[argIdx++]);
uint32_t len = parseAddr(argv[argIdx++]);
uint16_t addr = (uint16_t)(fullAddr & 0xFFFF);
// Check for width specifier.
int width = 1; // bytes per unit
if (argIdx < argc - 1)
{
char w = (char) tolower((unsigned char) argv[argIdx][0]);
if ((w == 'w' || w == 'd') && argv[argIdx][1] == '\0')
{
width = (w == 'w') ? 2 : 4;
argIdx++;
}
}
if (argIdx >= argc)
{
shPuts("Missing fill value.\r\n");
return;
}
uint32_t value = parseAddr(argv[argIdx]);
// Auto-hold.
bool autoHeld = false;
if (!cpu->hold)
{
cpu->hold = true;
int timeout = 3000;
while (!cpu->holdAck && timeout > 0) { sleep_ms(1); timeout--; }
if (cpu->holdAck)
autoHeld = true;
else
cpu->hold = false;
}
// Fill loop.
uint32_t ramSize = MEMORY_PAGE_BANKS * MEMORY_PAGE_SIZE;
uint32_t filled = 0;
for (uint32_t off = 0; off < len; off += (uint32_t) width)
{
for (int b = 0; b < width && (off + (uint32_t) b) < len; b++)
{
uint8_t byte = (uint8_t)((value >> (b * 8)) & 0xFF);
uint16_t wa = (uint16_t)(addr + off + (uint32_t) b);
if (type == 'p')
Z80CPU_writePhysicalMem(cpu, wa, byte);
else if (type == 'v')
{
uint32_t psAddr = (fullAddr & 0x00FF0000) | wa;
cpu->_z80PSRAM->RAM[psAddr % ramSize] = byte;
}
else
memWriteMapped(cpu, wa, byte);
filled++;
}
}
const char *label = (type == 'p') ? "physical" : (type == 'v') ? "virtual" : "mapped";
shPrintf("Filled %lu byte(s) at %04X with %0*lX (%s).\r\n",
(unsigned long) filled, addr, width * 2, (unsigned long) value, label);
if (autoHeld)
cpu->hold = false;
}
// ---------------------------------------------------------------------------
// Command: copy — copy between physical host memory and virtual PSRAM
// copy pv <physAddr> <len> <virtAddr> — physical → virtual
@@ -3993,6 +4167,7 @@ static const t_DbgShCmd cmdTable[] =
{"help", "Show this help", cmdHelp},
{"regs", "Dump Z80 registers", cmdRegs},
{"dm", "Dump memory: dm [p|f|v|r] <addr> [len]", cmdDm},
{"wm", "Write memory: wm <addr> <byte..>|fill|test", cmdWm},
{"cmp", "Compare: cmp [f] <phys> <virt> <len>", cmdCmp},
{"dis", "Disassemble: dis [p|v] [addr] [count]", cmdDis},
{"asm", "Assemble: asm [addr] (interactive)", cmdAsm},
@@ -4011,8 +4186,7 @@ static const t_DbgShCmd cmdTable[] =
{"bp", "Set breakpoint: bp <addr>", cmdBp},
{"bc", "Clear breakpoint: bc <n|*>", cmdBc},
{"bl", "List breakpoints", cmdBl},
{"wm", "Write memory: wm [p|v] <addr> <byte>...", cmdWm},
{"fill", "Fill memory: fill [p|v] <addr> <len> [w|d] <val>", cmdFill},
{"wm", "Write memory: wm [p|v|r] <addr> <byte>|fill|test", cmdWm},
{"copy", "Copy mem: copy <pv|fp|vp> <src> <len> <dst>", cmdCopy},
{"memtest", "Test physical memory: memtest <addr> <len> [pattern]", cmdMemtest},
{"in", "Read I/O port: in <port>", cmdIn},
@@ -4023,7 +4197,11 @@ static const t_DbgShCmd cmdTable[] =
{"iowait", "Force IO wait states: iowait <0-8>", cmdIOwait},
{"corrupt", "Show fetch corruption log: corrupt [clear]", cmdCorrupt},
{"reset", "Force Z80 reset", cmdReset},
{"ipl", "IPL reset (BST via 8255 PC3)", cmdIpl},
{"fdctrace","FDC trace: fdctrace <on|off|dump>", cmdFdcTrace},
{"mmutrace","Machine-specific trace dump (MMU/IO, etc.)", cmdMachineTrace},
{"intcount","Show interrupt ACK count and state", cmdIntCount},
{"psync", "Sync phys->PSRAM: psync [start end]", cmdPsync},
{"qdtrace", "QD trace: qdtrace <on|off|dump>", cmdQdTrace},
{"piodbg", "RP2350 PIO diagnostics: piodbg [clear]", cmdPiodbg},
{"load", "Load file: load <p|v> <file> <addr> [len] [ofs]", cmdLoad},

View File

@@ -114,6 +114,17 @@ static void celestiteProcessSocketCmd(uint8_t sockNum)
*sr = SOCK_CLOSED;
break;
}
// Reset RX state for the new socket (clear stale data from previous connection).
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0 + 1] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RD0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RD0 + 1] = 0;
celestiteCtrl->netRecvPending[sockNum] = false;
// Discard any stale RECV result from previous connection on this socket.
if (celestiteCtrl->netRecvReady && celestiteCtrl->netRecvSock == sockNum)
{
celestiteCtrl->netRecvReady = false;
}
// Queue ESP32 socket creation (fire-and-forget — local state already set).
// Don't set netSockPending so that the next operation (CONNECT/LISTEN)
// can properly track its response.
@@ -163,6 +174,16 @@ static void celestiteProcessSocketCmd(uint8_t sockNum)
*sr = SOCK_CLOSED;
celestiteCtrl->netSockPending[sockNum] = false;
celestiteCtrl->netRecvPending[sockNum] = false;
// Discard any in-flight RECV result for this socket.
// Without this, a stale RECV from a large TCP response (>2KB) survives
// through CLOSE/OPEN and corrupts the next connection's RX buffer.
if (celestiteCtrl->netRecvReady && celestiteCtrl->netRecvSock == sockNum)
{
celestiteCtrl->netRecvReady = false;
}
// Clear RSR so stale data doesn't affect the next OPEN on this socket.
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0 + 1] = 0;
break;
case Sn_CR_SEND:
@@ -220,7 +241,7 @@ static void celestiteW5100Reset(void)
memset(celestiteCtrl->w5100Mem, 0, W5100_MEM_SIZE);
// Set defaults per W5100 datasheet.
celestiteCtrl->w5100Mem[W5100_MR] = W5100_MR_IND; // IDM mode enabled after reset.
celestiteCtrl->w5100Mem[W5100_MR] = W5100_MR_IND | W5100_MR_AI; // IDM mode + auto-increment.
celestiteCtrl->w5100Mem[W5100_RTR0] = 0x07; // Default retry time = 200ms (0x07D0 × 100µs).
celestiteCtrl->w5100Mem[W5100_RTR0 + 1] = 0xD0;
celestiteCtrl->w5100Mem[W5100_RCR] = 0x08; // Default retry count = 8.
@@ -288,64 +309,101 @@ uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
celestiteCtrl->romWriteUnprotected = false;
memset(celestiteCtrl->ufm, 0xFF, CELESTITE_UFM_SIZE);
// Integrated MZ-1R12 CMOS RAM — enabled if params[0].file is set.
// params[0].file = SD card backing file for MZ-1R12 (e.g., "ram/CELESTITE_R12.ram").
// params[1].file = SD card backing file for MZ-1R37 (e.g., "ram/CELESTITE_R37.ram").
// Initialise optional features to defaults.
celestiteCtrl->mz1r12Ram = NULL;
celestiteCtrl->mz1r12Size = 0;
celestiteCtrl->mz1r12Addr = 0;
celestiteCtrl->mz1r12FileName = NULL;
celestiteCtrl->mz1r12WritePending = false;
celestiteCtrl->mz1r12NextReqId = 1;
celestiteCtrl->mz1r37Ram = NULL;
celestiteCtrl->mz1r37AddrLatch = 0;
celestiteCtrl->mz1r37FileName = NULL;
celestiteCtrl->mz1r37SectorWritePending = false;
celestiteCtrl->mz1r37NextReqId = 1;
celestiteCtrl->mz1r37HasDirty = false;
celestiteCtrl->mz1r37FlushPos = 0;
memset(celestiteCtrl->mz1r37DirtyMap, 0, sizeof(celestiteCtrl->mz1r37DirtyMap));
celestiteCtrl->netSrvIP[0] = 0;
celestiteCtrl->netSrvIP[1] = 0;
celestiteCtrl->netSrvIP[2] = 0;
celestiteCtrl->netSrvIP[3] = 0;
celestiteCtrl->netSrvPort = 6800;
celestiteCtrl->cfgIndex = 0;
// Parse params for memory backing files.
if (config->ifParamCount >= 1 && config->ifParam[0].file && config->ifParam[0].file[0] != '\0')
// Parse params by key type — order and count don't matter.
// "file" params: first = MZ-1R12 backing, second = MZ-1R37 backing.
// "ip" param: NET file server address as "a.b.c.d" or "a.b.c.d:port".
int fileIdx = 0;
for (int p = 0; p < config->ifParamCount; p++)
{
// Allocate MZ-1R12 CMOS RAM (32KB, doubler to 64KB via unlock D1h+12h).
// Allocate 64KB up front so doubling doesn't need realloc.
celestiteCtrl->mz1r12Ram = (uint8_t *) calloc(1, CELESTITE_R12_DBL);
celestiteCtrl->mz1r12Size = CELESTITE_R12_SIZE; // 32KB initially.
if (celestiteCtrl->mz1r12Ram)
if (config->ifParam[p].ip && config->ifParam[p].ip[0] != '\0')
{
celestiteCtrl->mz1r12FileName = strdup(config->ifParam[0].file);
debugf("Celestite: MZ-1R12 32KB enabled, file=%s\r\n", celestiteCtrl->mz1r12FileName);
// Queue load from SD card.
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFC; // MZ-1R12 load sentinel.
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r12FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = CELESTITE_R12_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
unsigned int ip0, ip1, ip2, ip3, port;
int matched = sscanf(config->ifParam[p].ip, "%u.%u.%u.%u:%u", &ip0, &ip1, &ip2, &ip3, &port);
if (matched >= 4)
{
celestiteCtrl->netSrvIP[0] = (uint8_t) ip0;
celestiteCtrl->netSrvIP[1] = (uint8_t) ip1;
celestiteCtrl->netSrvIP[2] = (uint8_t) ip2;
celestiteCtrl->netSrvIP[3] = (uint8_t) ip3;
if (matched >= 5)
celestiteCtrl->netSrvPort = (uint16_t) port;
}
debugf("Celestite: NET server=%u.%u.%u.%u:%u\r\n",
celestiteCtrl->netSrvIP[0], celestiteCtrl->netSrvIP[1],
celestiteCtrl->netSrvIP[2], celestiteCtrl->netSrvIP[3],
celestiteCtrl->netSrvPort);
}
}
if (config->ifParamCount >= 2 && config->ifParam[1].file && config->ifParam[1].file[0] != '\0')
{
// Allocate MZ-1R37 EMM (640KB).
celestiteCtrl->mz1r37Ram = (uint8_t *) calloc(1, CELESTITE_R37_SIZE);
if (celestiteCtrl->mz1r37Ram)
else if (config->ifParam[p].file && config->ifParam[p].file[0] != '\0')
{
celestiteCtrl->mz1r37FileName = strdup(config->ifParam[1].file);
debugf("Celestite: MZ-1R37 640KB enabled, file=%s\r\n", celestiteCtrl->mz1r37FileName);
if (fileIdx == 0)
{
// First file param: MZ-1R12 CMOS RAM (32KB, doubler to 64KB via unlock D1h+12h).
// Initialise to 0xFF (empty battery-backed SRAM state). calloc zeros
// would cause the MZ-1500 IPL to think a valid extended ROM is present
// at offset 0x8000 (it checks for non-zero at that address to skip).
celestiteCtrl->mz1r12Ram = (uint8_t *) malloc(CELESTITE_R12_DBL);
if (celestiteCtrl->mz1r12Ram)
memset(celestiteCtrl->mz1r12Ram, 0xFF, CELESTITE_R12_DBL);
celestiteCtrl->mz1r12Size = CELESTITE_R12_SIZE;
if (celestiteCtrl->mz1r12Ram)
{
celestiteCtrl->mz1r12FileName = strdup(config->ifParam[p].file);
debugf("Celestite: MZ-1R12 32KB enabled, file=%s\r\n", celestiteCtrl->mz1r12FileName);
// Queue load from SD card.
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFD; // MZ-1R37 load sentinel.
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFC;
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r12FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = CELESTITE_R12_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
}
}
else if (fileIdx == 1)
{
// Second file param: MZ-1R37 EMM (640KB).
celestiteCtrl->mz1r37Ram = (uint8_t *) calloc(1, CELESTITE_R37_SIZE);
if (celestiteCtrl->mz1r37Ram)
{
celestiteCtrl->mz1r37FileName = strdup(config->ifParam[p].file);
debugf("Celestite: MZ-1R37 640KB enabled, file=%s\r\n", celestiteCtrl->mz1r37FileName);
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFD;
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
}
}
fileIdx++;
}
}
@@ -415,6 +473,10 @@ uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
case 0x69:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_UFM;
break;
case 0x6A:
case 0x6B:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_Cfg;
break;
case 0x6F:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_Unlock;
break;
@@ -523,8 +585,76 @@ uint8_t __func_in_RAM(Celestite_PollCB)(t_Z80CPU *cpu)
// Ping results use the memoReg which is already written by PollCB's ping response handling.
// NET_CFG and socket results are now handled inline in the IDM read handler.
// Periodic recv polling: throttled, only when established sockets exist.
// Clear MZ-1R12 write-pending flag periodically (~6s at 3.5MHz) so the next
// batch of writes can queue a new scheduled write. The coalesce timeout on
// Core 0 (2s) will have completed by then.
celestiteCtrl->pollCounter++;
if ((celestiteCtrl->pollCounter & 0x1FFFFF) == 0)
{
celestiteCtrl->mz1r12WritePending = false;
}
// MZ-1R37 incremental dirty page flush: after writes quiesce (2s), flush
// one 512-byte page per poll cycle via MSG_WRITE_SECTOR.
if (celestiteCtrl->mz1r37HasDirty && celestiteCtrl->mz1r37Ram)
{
// Process any pending sector write response.
if (celestiteCtrl->mz1r37SectorWritePending)
{
t_CoreMsg msg;
while (queue_try_remove(celestiteCtrl->responseQueue, &msg))
{
if (msg.context != celestiteCtrl || msg.type != MSG_WRITE_COMPLETE ||
msg.requestId != celestiteCtrl->mz1r37WriteId)
{
queue_try_add(celestiteCtrl->responseQueue, &msg);
break;
}
celestiteCtrl->mz1r37SectorWritePending = false;
}
}
int64_t elapsed = absolute_time_diff_us(celestiteCtrl->mz1r37LastWrite, get_absolute_time());
if (elapsed >= 2000000 && !celestiteCtrl->mz1r37SectorWritePending)
{
// Scan for next dirty page.
int pageCount = CELESTITE_R37_SIZE / 512;
bool found = false;
for (int i = 0; i < pageCount; i++)
{
int page = (celestiteCtrl->mz1r37FlushPos + i) % pageCount;
if (celestiteCtrl->mz1r37DirtyMap[page >> 3] & (1u << (page & 7)))
{
uint32_t offset = page * 512;
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_WRITE_SECTOR;
msg.context = celestiteCtrl;
msg.requestId = celestiteCtrl->mz1r37NextReqId++;
strncpy(msg.sectorOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.sectorOp.offset = offset;
msg.sectorOp.size = 512;
msg.sectorOp.buffer = celestiteCtrl->mz1r37Ram + offset;
if (queue_try_add(celestiteCtrl->requestQueue, &msg))
{
celestiteCtrl->mz1r37WriteId = msg.requestId;
celestiteCtrl->mz1r37SectorWritePending = true;
celestiteCtrl->mz1r37DirtyMap[page >> 3] &= ~(1u << (page & 7));
celestiteCtrl->mz1r37FlushPos = (page + 1) % pageCount;
}
found = true;
break;
}
}
if (!found)
{
celestiteCtrl->mz1r37HasDirty = false;
celestiteCtrl->mz1r37FlushPos = 0;
}
}
}
// Periodic recv polling: throttled, only when established sockets exist.
if ((celestiteCtrl->pollCounter & 0xFFF) == 0)
{
for (int s = 0; s < W5100_NUM_SOCKETS; s++)
@@ -723,27 +853,51 @@ uint8_t __func_in_RAM(Celestite_IO_DR)(t_Z80CPU *cpu, bool read, uint16_t addr,
__dmb();
uint8_t rsn = celestiteCtrl->netRecvSock;
uint32_t rlen = celestiteCtrl->netRecvLen;
celestiteCtrl->netRecvReady = false;
uint8_t rsr = celestiteCtrl->w5100Mem[rsn < W5100_NUM_SOCKETS ?
W5100_SOCK_BASE + (rsn * W5100_SOCK_SIZE) + Sn_SR : 0];
if (rsn < W5100_NUM_SOCKETS && rlen > 0)
// Only process if socket is valid and still connected.
// Discard RECV results for closed sockets (ghost data from old connection).
if (rsn >= W5100_NUM_SOCKETS || rsr == SOCK_CLOSED)
{
celestiteCtrl->netRecvReady = false;
if (rsn < W5100_NUM_SOCKETS)
celestiteCtrl->netRecvPending[rsn] = false;
}
// Only copy data when the RX buffer is empty (RSR == 0).
// If RSR > 0, leave netRecvReady=true so data is copied on the
// next DR read after the Z80 has consumed the current buffer.
else if (rsn < W5100_NUM_SOCKETS)
{
uint16_t sb = W5100_SOCK_BASE + (rsn * W5100_SOCK_SIZE);
uint16_t rxBase = W5100_RX_BASE + (rsn * 0x0800);
uint16_t rxMask = 0x07FF;
uint16_t rxRd = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RD0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RD0 + 1];
for (uint32_t i = 0; i < rlen && i < 0x0800; i++)
{
uint16_t rxAddr = rxBase + ((rxRd + i) & rxMask);
celestiteCtrl->w5100Mem[rxAddr] = celestiteCtrl->netRecvBuf[i];
}
uint16_t curRsr = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1];
curRsr += (uint16_t)rlen;
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] = (uint8_t)(curRsr >> 8);
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1] = (uint8_t)(curRsr & 0xFF);
celestiteCtrl->w5100Mem[sb + Sn_IR] |= Sn_IR_RECV;
celestiteCtrl->netRecvPending[rsn] = false;
if (curRsr == 0 || rlen == 0)
{
// Buffer empty or empty RECV result — safe to process.
celestiteCtrl->netRecvReady = false;
celestiteCtrl->netRecvPending[rsn] = false;
if (rlen > 0)
{
uint16_t rxBase = W5100_RX_BASE + (rsn * 0x0800);
uint16_t rxMask = 0x07FF;
uint16_t rxRd = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RD0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RD0 + 1];
uint16_t copyLen = (rlen > 0x0800) ? 0x0800 : (uint16_t)rlen;
for (uint16_t i = 0; i < copyLen; i++)
{
uint16_t rxAddr = rxBase + ((rxRd + i) & rxMask);
celestiteCtrl->w5100Mem[rxAddr] = celestiteCtrl->netRecvBuf[i];
}
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] = (uint8_t)(copyLen >> 8);
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1] = (uint8_t)(copyLen & 0xFF);
celestiteCtrl->w5100Mem[sb + Sn_IR] |= Sn_IR_RECV;
}
}
// else: RSR > 0 and rlen > 0 — leave netRecvReady=true,
// data will be copied when RSR reaches 0 (after CMD_RECV).
}
}
@@ -934,6 +1088,43 @@ uint8_t __func_in_RAM(Celestite_IO_UFM)(t_Z80CPU *cpu, bool read, uint16_t addr,
return 0;
}
// ---------------------------------------------------------------------------
// Ports 6Ah/6Bh — Configuration registers (index/data pair).
// Write index to 6Ah, read value from 6Bh. Extensible for future config items.
// Index 0-3: NET file server IP address bytes [0..3].
// Index 4: NET file server port high byte.
// Index 5: NET file server port low byte.
// ---------------------------------------------------------------------------
uint8_t __func_in_RAM(Celestite_IO_Cfg)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
(void)cpu;
uint8_t port = addr & 0xFF;
if (port == 0x6A)
{
// Index register (write only).
if (!read)
celestiteCtrl->cfgIndex = data;
return 0xFF;
}
// Port 0x6B — data register (read only).
if (read)
{
switch (celestiteCtrl->cfgIndex)
{
case 0: return celestiteCtrl->netSrvIP[0];
case 1: return celestiteCtrl->netSrvIP[1];
case 2: return celestiteCtrl->netSrvIP[2];
case 3: return celestiteCtrl->netSrvIP[3];
case 4: return (uint8_t)(celestiteCtrl->netSrvPort >> 8);
case 5: return (uint8_t)(celestiteCtrl->netSrvPort & 0xFF);
default: return 0xFF;
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Port 6Fh — Unlock register (write only, two-byte sequence: D1h then keyword).
// Keywords: 12h=double MZ-1R12, 37h=enable EMM, 05h=unprotect ROM,
@@ -1045,8 +1236,11 @@ uint8_t __func_in_RAM(Celestite_IO_R12)(t_Z80CPU *cpu, bool read, uint16_t addr,
{
celestiteCtrl->mz1r12Ram[celestiteCtrl->mz1r12Addr] = data;
// Schedule SD card write (coalesced, same pattern as standalone MZ-1R12).
if (celestiteCtrl->mz1r12FileName && celestiteCtrl->requestQueue)
// Schedule SD card write (coalesced). Only queue one message
// per coalesce window — the 2s timeout ensures the final state
// is written. Without this guard, every byte write floods the
// inter-core queue and starves other operations.
if (celestiteCtrl->mz1r12FileName && celestiteCtrl->requestQueue && !celestiteCtrl->mz1r12WritePending)
{
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
@@ -1057,8 +1251,8 @@ uint8_t __func_in_RAM(Celestite_IO_R12)(t_Z80CPU *cpu, bool read, uint16_t addr,
msg.fileOp.timeout = 2000; // 2 second coalesce window.
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = celestiteCtrl->mz1r12Size;
queue_try_add(celestiteCtrl->requestQueue, &msg);
celestiteCtrl->mz1r12WritePending = true;
if (queue_try_add(celestiteCtrl->requestQueue, &msg))
celestiteCtrl->mz1r12WritePending = true;
}
}
celestiteCtrl->mz1r12Addr++;
@@ -1113,19 +1307,14 @@ uint8_t __func_in_RAM(Celestite_IO_R37Data)(t_Z80CPU *cpu, bool read, uint16_t a
{
celestiteCtrl->mz1r37Ram[fullAddr] = data;
// Schedule SD card write (coalesced).
if (celestiteCtrl->mz1r37FileName && celestiteCtrl->requestQueue)
// Mark the 512-byte page as dirty. Actual SD write happens
// incrementally in PollCB after writes quiesce (2s timeout).
if (celestiteCtrl->mz1r37FileName)
{
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_WRITE_FILE_SCHEDULED;
msg.context = celestiteCtrl;
msg.requestId = 0;
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.timeout = 5000; // 5 second coalesce window (640KB is large).
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(celestiteCtrl->requestQueue, &msg);
uint32_t page = fullAddr / 512;
celestiteCtrl->mz1r37DirtyMap[page >> 3] |= (1u << (page & 7));
celestiteCtrl->mz1r37HasDirty = true;
celestiteCtrl->mz1r37LastWrite = get_absolute_time();
}
}
return 0;

View File

@@ -74,12 +74,14 @@ uint8_t MZ1E05_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
return (0);
}
// Get size of a disk image.
// Get size of a disk image. D88/ExtDSK files are kept in native format
// and can be larger than raw — allocate 50% headroom.
int diskSize = wd1773_getDiskSize(&mz1e05Ctrl->fdc, MZ_700);
if (diskSize > 0)
int bufSize = diskSize + diskSize / 2;
if (bufSize > 0)
{
// Allocate space for the disk images.
mz1e05Ctrl->disk = (uint8_t *) calloc(MAX_MZ1E05_DISK_DRIVES, diskSize);
mz1e05Ctrl->disk = (uint8_t *) calloc(MAX_MZ1E05_DISK_DRIVES, bufSize);
}
if (mz1e05Ctrl->disk != NULL)
{
@@ -97,7 +99,7 @@ uint8_t MZ1E05_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
mz1e05Ctrl->diskctl = FDC_NO_FILE;
}
}
if (wd1773_init(&mz1e05Ctrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz1e05Ctrl->diskName[0], mz1e05Ctrl->disk, diskSize, MZ_700))
if (wd1773_init(&mz1e05Ctrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz1e05Ctrl->diskName[0], mz1e05Ctrl->disk, bufSize, MZ_700))
{
result = 1;

View File

@@ -122,6 +122,17 @@ uint8_t MZ1E14_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
free(mz1e14Ctrl);
mz1e14Ctrl = NULL;
}
else
{
// Register QD debug shell hooks.
#ifdef INCLUDE_DBGSH
extern void QDDrive_dumpLog(void);
extern bool qdDbgEnabled;
cpu->dbgHooks.qdName = "QD";
cpu->dbgHooks.qdTraceDump = QDDrive_dumpLog;
cpu->dbgHooks.qdTraceEnabled = &qdDbgEnabled;
#endif
}
return (result);
}

View File

@@ -127,6 +127,17 @@ uint8_t MZ1E19_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
free(mz1e19Ctrl);
mz1e19Ctrl = NULL;
}
else
{
// Register QD debug shell hooks.
#ifdef INCLUDE_DBGSH
extern void QDDrive_dumpLog(void);
extern bool qdDbgEnabled;
cpu->dbgHooks.qdName = "QD";
cpu->dbgHooks.qdTraceDump = QDDrive_dumpLog;
cpu->dbgHooks.qdTraceEnabled = &qdDbgEnabled;
#endif
}
return (result);
}

View File

@@ -18,14 +18,15 @@
// LD A, X ; A=data byte
// OUT (C), A ; Write X to address 0xABCDE
//
// The I/O base address is configurable via ioMap in the JSON config:
// "ioMap": [{"srcAddr": 172, "dstAddr": <base>, "size": 2}]
// Default base is 0xAC.
// Persistence: dirty pages (512 bytes each) are tracked via a bitmap.
// After writes quiesce (2s timeout), dirty pages are flushed to SD one
// per poll cycle via MSG_WRITE_SECTOR — no 640KB bulk writes.
//
// Credits:
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// History: May 2026 v1.0 - Initial write.
// Jun 2026 v1.1 - Dirty-page tracking, incremental sector write-back.
//
// Notes: See Makefile to enable/disable conditional components
//
@@ -61,35 +62,130 @@
t_MZ1R37 *mz1r37Ctrl; // Control structure for the MZ-1R37 EMM board.
// -----------------------------------------------------------------------------------------------
// Interface: MZ-1R37 640K EMM
// Description: 640KB expanded memory board with 20-bit address space. No auto-increment.
// Address is formed from a latched upper portion (port ACh) and the B register
// value during data access (port ADh).
// Dirty page helpers
// -----------------------------------------------------------------------------------------------
static inline void dirtyPageSet(uint32_t fullAddr)
{
uint32_t page = fullAddr / MZ1R37_PAGE_SIZE;
mz1r37Ctrl->dirtyMap[page >> 3] |= (1u << (page & 7));
mz1r37Ctrl->hasDirtyPages = true;
mz1r37Ctrl->lastWriteTime = get_absolute_time();
}
static inline void dirtyPageClear(uint32_t page)
{
mz1r37Ctrl->dirtyMap[page >> 3] &= ~(1u << (page & 7));
}
static inline bool dirtyPageTest(uint32_t page)
{
return (mz1r37Ctrl->dirtyMap[page >> 3] & (1u << (page & 7))) != 0;
}
// -----------------------------------------------------------------------------------------------
// Inter-core response processing
// -----------------------------------------------------------------------------------------------
static void MZ1R37_ProcessResponses(void)
{
t_CoreMsg msg;
while (queue_try_remove(mz1r37Ctrl->responseQueue, &msg))
{
if (msg.context != mz1r37Ctrl)
{
queue_try_add(mz1r37Ctrl->responseQueue, &msg);
break;
}
switch (msg.type)
{
case MSG_LOAD_COMPLETE:
mz1r37Ctrl->loadPending = false;
if (msg.response.success)
debugf("MZ-1R37: Backing file loaded (%u bytes)\r\n", msg.response.size);
break;
case MSG_WRITE_COMPLETE:
if (mz1r37Ctrl->sectorWritePending && msg.requestId == mz1r37Ctrl->pendingWriteId)
mz1r37Ctrl->sectorWritePending = false;
break;
default:
break;
}
}
}
// -----------------------------------------------------------------------------------------------
// Flush one dirty page to SD card. Called from PollCB, one page per cycle.
// Returns true if a page was flushed (or is in flight).
// -----------------------------------------------------------------------------------------------
static bool MZ1R37_FlushNextDirtyPage(void)
{
if (!mz1r37Ctrl->ramfileName || !mz1r37Ctrl->requestQueue)
return false;
// Don't queue another if one is still in flight.
if (mz1r37Ctrl->sectorWritePending)
return true;
// Scan from last position to find the next dirty page.
for (int i = 0; i < MZ1R37_PAGE_COUNT; i++)
{
int page = (mz1r37Ctrl->flushScanPos + i) % MZ1R37_PAGE_COUNT;
if (dirtyPageTest(page))
{
uint32_t offset = page * MZ1R37_PAGE_SIZE;
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_WRITE_SECTOR;
msg.context = mz1r37Ctrl;
msg.requestId = mz1r37Ctrl->nextRequestId++;
strncpy(msg.sectorOp.filename, mz1r37Ctrl->ramfileName, MAX_IC_FILENAME_LEN - 1);
msg.sectorOp.offset = offset;
msg.sectorOp.size = MZ1R37_PAGE_SIZE;
msg.sectorOp.buffer = mz1r37Ctrl->ram + offset;
if (queue_try_add(mz1r37Ctrl->requestQueue, &msg))
{
mz1r37Ctrl->pendingWriteId = msg.requestId;
mz1r37Ctrl->sectorWritePending = true;
dirtyPageClear(page);
mz1r37Ctrl->flushScanPos = (page + 1) % MZ1R37_PAGE_COUNT;
return true;
}
return false; // Queue full, try next poll.
}
}
// No dirty pages found — flush complete.
mz1r37Ctrl->hasDirtyPages = false;
mz1r37Ctrl->flushScanPos = 0;
return false;
}
// -----------------------------------------------------------------------------------------------
// Public API
// -----------------------------------------------------------------------------------------------
// Method to initialise the CPU state to support an MZ-1R37 EMM interface.
uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
{
// Locals.
uint8_t result = 0;
// Interface logic only for virtual device. Physical device doesnt need configuration.
if (config->isPhysical)
return (0);
// Only one instance allowed.
if (mz1r37Ctrl != NULL)
return (0);
// Allocate control structure.
mz1r37Ctrl = (t_MZ1R37 *) calloc(1, sizeof(t_MZ1R37));
if (!mz1r37Ctrl)
{
debugf("MZ-1R37: Failed to allocate control structure\r\n");
return (0);
}
// Allocate 640KB RAM.
mz1r37Ctrl->ram = (uint8_t *) calloc(1, MZ1R37_RAM_SIZE);
if (mz1r37Ctrl->ram == NULL)
{
@@ -99,6 +195,10 @@ uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
return (0);
}
mz1r37Ctrl->requestQueue = &cpu->requestQueue;
mz1r37Ctrl->responseQueue = &cpu->responseQueue;
mz1r37Ctrl->nextRequestId = 1;
// Determine I/O base address from ioMap config (default 0xAC).
mz1r37Ctrl->ioBase = MZ1R37_DEFAULT_BASE;
for (int remapIdx = 0; remapIdx < config->ioMapCount; remapIdx++)
@@ -107,7 +207,24 @@ uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
}
mz1r37Ctrl->addrLatch = 0;
// Install I/O handlers at the configured base address.
// Load backing file if configured.
if (config->ifParamCount > 0 && config->ifParam[0].file != NULL)
{
mz1r37Ctrl->ramfileName = strdup(config->ifParam[0].file);
mz1r37Ctrl->loadPending = true;
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = mz1r37Ctrl;
msg.requestId = mz1r37Ctrl->nextRequestId++;
strncpy(msg.fileOp.filename, mz1r37Ctrl->ramfileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = mz1r37Ctrl->ram;
msg.fileOp.size = MZ1R37_RAM_SIZE;
queue_try_add(mz1r37Ctrl->requestQueue, &msg);
}
// Install I/O handlers.
uint8_t base = mz1r37Ctrl->ioBase;
for (int idx = 0; idx < IO_PAGE_SIZE; idx++)
{
@@ -118,33 +235,47 @@ uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1R37_IO_Data;
}
debugf("MZ-1R37: Initialised, base=0x%02X, RAM=%dKB\r\n", base, MZ1R37_RAM_SIZE / 1024);
debugf("MZ-1R37: Initialised, base=0x%02X, RAM=%dKB, file=%s\r\n",
base, MZ1R37_RAM_SIZE / 1024,
mz1r37Ctrl->ramfileName ? mz1r37Ctrl->ramfileName : "(none)");
result = 1;
return (result);
}
// Reset handler.
uint8_t MZ1R37_Reset(t_Z80CPU *cpu)
{
(void)cpu;
if (mz1r37Ctrl)
{
mz1r37Ctrl->addrLatch = 0;
return (0);
}
uint8_t __func_in_RAM(MZ1R37_PollCB)(t_Z80CPU *cpu)
{
(void)cpu;
if (!mz1r37Ctrl)
return (0);
// Process any pending responses (load or sector write completions).
if (mz1r37Ctrl->loadPending || mz1r37Ctrl->sectorWritePending)
MZ1R37_ProcessResponses();
// Flush dirty pages after writes have quiesced.
if (mz1r37Ctrl->hasDirtyPages)
{
int64_t elapsed = absolute_time_diff_us(mz1r37Ctrl->lastWriteTime, get_absolute_time());
if (elapsed >= MZ1R37_WRITE_QUIESCE_US)
{
MZ1R37_FlushNextDirtyPage();
}
}
return (0);
}
// Poll handler — nothing to do for a static RAM board.
uint8_t __func_in_RAM(MZ1R37_PollCB)(t_Z80CPU *cpu)
{
(void)cpu;
return (0);
}
// Task processor — no external tasks currently handled.
uint8_t MZ1R37_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
(void)cpu;
@@ -162,26 +293,18 @@ uint8_t MZ1R37_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *pa
}
// -----------------------------------------------------------------------
// I/O handler for Base+0 (default ACh) — Address latch (write only).
//
// Via OUT (C),A:
// B register (port address MSB) provides address[19:16] (bits 0-3 used).
// A register (data) provides address[15:8].
//
// The latched value is combined with the B register during data access
// to form the full 20-bit address.
// Port Base+0 (default ACh) — Address latch (write only).
// -----------------------------------------------------------------------
uint8_t __func_in_RAM(MZ1R37_IO_AddrLatch)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
(void)cpu;
if (read)
return 0xFF; // Write-only port.
return 0xFF;
if (mz1r37Ctrl)
{
// addr MSB (B register) = address[19:16], data (A register) = address[15:8].
uint8_t addrHi = (uint8_t)((addr >> 8) & 0x0F); // Bits 19:16 (only 4 bits used).
uint8_t addrHi = (uint8_t)((addr >> 8) & 0x0F);
mz1r37Ctrl->addrLatch = ((uint32_t)addrHi << 16) | ((uint32_t)data << 8);
}
@@ -189,14 +312,7 @@ uint8_t __func_in_RAM(MZ1R37_IO_AddrLatch)(t_Z80CPU *cpu, bool read, uint16_t ad
}
// -----------------------------------------------------------------------
// I/O handler for Base+1 (default ADh) — Data read/write.
//
// Via OUT (C),A or IN A,(C):
// B register (port address MSB) provides address[7:0].
// Combined with the latched address[19:8] to form the full 20-bit address.
//
// No auto-increment — each access must set the B register explicitly.
// Reads from addresses >= 640KB (0xA0000) return 0xFF (unpopulated).
// Port Base+1 (default ADh) — Data read/write.
// -----------------------------------------------------------------------
uint8_t __func_in_RAM(MZ1R37_IO_Data)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
@@ -205,15 +321,12 @@ uint8_t __func_in_RAM(MZ1R37_IO_Data)(t_Z80CPU *cpu, bool read, uint16_t addr, u
if (mz1r37Ctrl == NULL)
return 0xFF;
// Form the full 20-bit address: latch[19:8] | B register[7:0].
uint32_t fullAddr = (mz1r37Ctrl->addrLatch & 0xFFF00) | (uint32_t)((addr >> 8) & 0xFF);
if (read)
{
if (fullAddr < MZ1R37_RAM_SIZE)
{
return mz1r37Ctrl->ram[fullAddr];
}
return 0xFF;
}
else
@@ -221,6 +334,7 @@ uint8_t __func_in_RAM(MZ1R37_IO_Data)(t_Z80CPU *cpu, bool read, uint16_t addr, u
if (fullAddr < MZ1R37_RAM_SIZE)
{
mz1r37Ctrl->ram[fullAddr] = data;
dirtyPageSet(fullAddr);
}
return 0;
}

View File

@@ -537,6 +537,88 @@ void MZ1500_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len)
return;
}
// Callbacks for driver-level ROM loading. Each ROM type has its own callback
// that knows where to place the data. The config.json "rom" array has 3 entries:
// rom[0] = Monitor ROM I (4K) → Bank 0 at 0x0000-0x0FFF
// rom[1] = Extended ROM (6K) → Bank 0 at 0xE800-0xFFFF (E000-E7FF is hardware I/O, not enabled by ROM select)
// rom[2] = CG-ROM (4K) → Bank 39 at 0xD000-0xDFFF
// No loadaddr needed — the romIndex determines the destination.
// ROM index constants for driver-level ROM array.
#define MZ1500_ROM_MONITOR 0 // 4K Monitor ROM at 0x0000-0x0FFF.
#define MZ1500_ROM_EXTENDED 1 // 6K Extended ROM at 0xE800-0xFFFF.
#define MZ1500_ROM_CGROM 2 // 4K CG-ROM at Bank 39 0xD000-0xDFFF.
static int mz1500_romLoadIndex = 0;
void MZ1500_readDriverROM(void *ctx, void *cfg, char *buf, int len)
{
t_Z80CPU *cpu = (t_Z80CPU *) ctx;
(void)cfg;
if (cpu == NULL || buf == NULL || len <= 0)
return;
int romIdx = mz1500_romLoadIndex++;
switch (romIdx)
{
case MZ1500_ROM_MONITOR:
{
// Monitor ROM I: 4K → Bank 0 at 0x0000.
uint32_t copyLen = (len > 0x1000) ? 0x1000 : (uint32_t)len;
uint32_t dstAddr = (MZ1500_MEMBANK_0 * MEMORY_PAGE_SIZE) + 0x0000;
for (uint32_t i = 0; i < copyLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[i];
debugf("MZ1500_ROM: Monitor ROM loaded (%d bytes → Bank 0 : 0x0000)\r\n", copyLen);
break;
}
case MZ1500_ROM_EXTENDED:
{
// Extended ROM: 6K → Bank 0 at 0xE800.
// E000-E7FF is hardware I/O (not ROM), so ROM maps E800-FFFF.
// If file is >6K (8K = full E000-FFFF dump), skip first 2K (the E000-E7FF gap).
uint32_t srcOfs = 0;
uint32_t dataLen = (uint32_t)len;
if (dataLen > 0x1800)
{
srcOfs = dataLen - 0x1800; // Skip leading bytes (E000-E7FF region).
dataLen = 0x1800;
}
uint32_t dstAddr = (MZ1500_MEMBANK_0 * MEMORY_PAGE_SIZE) + 0xE800;
for (uint32_t i = 0; i < dataLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[srcOfs + i];
debugf("MZ1500_ROM: Extended ROM loaded (%d bytes, srcOfs=%d → Bank 0 : 0xE800)\r\n", dataLen, srcOfs);
break;
}
case MZ1500_ROM_CGROM:
{
// CG-ROM: 4K → Bank 39 at 0xD000.
uint32_t copyLen = (len > 0x1000) ? 0x1000 : (uint32_t)len;
uint32_t dstAddr = (MZ1500_MEMBANK_CGROM * MEMORY_PAGE_SIZE) + 0xD000;
for (uint32_t i = 0; i < copyLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[i];
debugf("MZ1500_ROM: CG-ROM loaded (%d bytes → PSRAM 0x%06X)\r\n", copyLen, dstAddr);
// Dump at offset 0xA00 where Japanese/MZ-700 ROMs differ (lowercase chars).
if (copyLen > 0xA08)
{
debugf("MZ1500_ROM: CG buf[A00]: %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
(uint8_t)buf[0xA00], (uint8_t)buf[0xA01], (uint8_t)buf[0xA02], (uint8_t)buf[0xA03],
(uint8_t)buf[0xA04], (uint8_t)buf[0xA05], (uint8_t)buf[0xA06], (uint8_t)buf[0xA07]);
debugf("MZ1500_ROM: PSRAM[DA00]: %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
cpu->_z80PSRAM->RAM[dstAddr+0xA00], cpu->_z80PSRAM->RAM[dstAddr+0xA01],
cpu->_z80PSRAM->RAM[dstAddr+0xA02], cpu->_z80PSRAM->RAM[dstAddr+0xA03],
cpu->_z80PSRAM->RAM[dstAddr+0xA04], cpu->_z80PSRAM->RAM[dstAddr+0xA05],
cpu->_z80PSRAM->RAM[dstAddr+0xA06], cpu->_z80PSRAM->RAM[dstAddr+0xA07]);
}
break;
}
default:
debugf("MZ1500_ROM: unexpected ROM index %d, ignoring\r\n", romIdx);
break;
}
}
// Method to write out a stored ROM into the internal emulation RAM as directed by the configuration array.
// Each config element specifies a position in the ROM and the target within the memory.
void MZ1500_readROMData(void *ctx, void *cfg, char *buf, int len)
@@ -651,14 +733,13 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.loDRAMen = true;
}
// E1h: D000-FFFF -> DRAM. Does NOT close PCG bank on MZ-1500 (bank switch happens behind the scenes).
// E1h: D000-EFFF -> DRAM. Does NOT close PCG bank on MZ-1500.
// F000-FFFF excluded — handled by priority logic below (PCG > DRAM > ROM).
// E000-E7FF (8255/8253 hardware I/O) must ALWAYS remain physical — skip blocks 112-115.
if (!MZ1500Ctrl.inhibit && port == 0xe1 && !MZ1500Ctrl.hiDRAMen)
{
// Enable upper 12K block as DRAM, but preserve E000-E7FF as PHYSICAL_HW.
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0xF000 / MEMORY_BLOCK_SIZE); idx++)
{
// Skip E000-E7FF (blocks 112-115) — 8255/8253 must always be on physical bus.
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
MZ1500Ctrl.upmembankPtr[idx - (0xd000 / MEMORY_BLOCK_SIZE)] = cpu->_membankPtr[idx];
@@ -669,7 +750,7 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
if ((MZ1500Ctrl.loDRAMen && port == 0xe2) || port == 0xe4)
{
// Enable lower 4K block as Monitor ROM
// Enable lower 4K block as Monitor ROM.
for (int idx = 0x0000; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
@@ -677,11 +758,11 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.loDRAMen = false;
}
// E3h: D000-FFFF -> VRAM/KEY/TIMER. Does NOT close PCG bank on MZ-1500.
// E3h/E4h: D000-EFFF -> VRAM/KEY/TIMER. Does NOT close PCG bank on MZ-1500.
// F000-FFFF excluded — handled by priority logic below.
if ((!MZ1500Ctrl.inhibit && MZ1500Ctrl.hiDRAMen && port == 0xe3) || (MZ1500Ctrl.hiDRAMen && port == 0xe4))
{
// Restore upper 12K block to Memory mapped I/O, skip E000-E7FF (always physical).
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0xF000 / MEMORY_BLOCK_SIZE); idx++)
{
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
@@ -696,11 +777,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.inhibit = false;
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500 && MZ1500Ctrl.pcgBankOpen)
{
// Restore F000-FFFF from saved mapping.
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
MZ1500Ctrl.pcgBankOpen = false;
}
}
@@ -711,16 +787,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
{
// MZ-1500: Open PCG bank at F000-FFFF.
// Bits 0-1 select: 00=CGROM, 01=PCG blue, 10=PCG red, 11=PCG green.
// Save current F000-FFFF mapping (e.g., FDD ROM) and switch to PHYSICAL
// so reads go to the real CGROM/PCG-RAM on the bus.
if (!MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)] = cpu->_membankPtr[idx];
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (idx * MEMORY_BLOCK_SIZE);
}
}
MZ1500Ctrl.pcgBankSelect = data & 0x03;
MZ1500Ctrl.pcgBankOpen = true;
}
@@ -742,14 +808,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
{
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500)
{
// MZ-1500: Close PCG bank. Restore F000-FFFF to saved mapping (FDD ROM etc).
if (MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
}
MZ1500Ctrl.pcgBankOpen = false;
}
else
@@ -768,6 +826,62 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
}
}
// MZ-1500 mode: compute memory mapping for D000-FFFF from current flags.
// Per manual p.62-63: $E5 affects the entire $D000~$FFFF range.
// E000-E7FF (hardware I/O) is always physical and skipped.
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500)
{
if (MZ1500Ctrl.pcgBankOpen)
{
// $E5 active: D000-FFFF (minus E000-E7FF) changes based on bank select.
if (MZ1500Ctrl.pcgBankSelect == MZ1500_PCG_BANK_CGROM)
{
// Bank=0 (CG-ROM): D000-DFFF → PSRAM Bank 39 at D000 (virtual CG-ROM,
// allows user override via config.json rom[2]).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_CGROM << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else
{
// Bank=1-3 (PCG planes): D000-DFFF → PHYSICAL (writes must hit real PCG RAM
// so the display hardware can read them for video output).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
// E800-FFFF → PHYSICAL when $E5 active (hardware routing).
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else if (MZ1500Ctrl.hiDRAMen)
{
// $E1 active: D000-FFFF (minus E000-E7FF) → DRAM (Bank 1).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24) | (MZ1500_MEMBANK_1 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else
{
// Default ($E3/$E4): D000-DFFF=VRAM, E800-FFFF=ROM (MZ-MON-ROM II in PSRAM).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL_VRAM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
}
return (0);
}
@@ -866,19 +980,23 @@ uint8_t MZ1500_Reset(t_Z80CPU *cpu)
MZ1500Ctrl.loDRAMen = false;
MZ1500Ctrl.hiDRAMen = false;
// Reset MZ-1500-specific PCG state. Restore F000-FFFF if PCG bank was open.
if (MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
}
// Reset MZ-1500-specific PCG state.
MZ1500Ctrl.pcgBankOpen = false;
MZ1500Ctrl.pcgBankSelect = MZ1500_PCG_BANK_CGROM;
MZ1500Ctrl.pcgPriority = 0;
MZ1500Ctrl.pcgPalette = 0;
// Restore D000-FFFF to default state (hardware RESET already resets physical latches).
// D000-DFFF=VRAM, E000-E7FF stays PHYSICAL_HW, E800-FFFF=ROM (PSRAM).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL_VRAM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
// Reset PIT emulation state so it re-initializes on next access.
pitInitialized = false;
@@ -1035,9 +1153,15 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
switch (idx)
{
// 0x0000:0x0FFF = Monitor ROM.
// Stays physical for fetches unless overridden by ROM config.
// Physical ROM is mirrored into PSRAM at init for bank switching reference.
// Virtual mode: runs from PSRAM (mirrored from physical during init,
// or overridden by user ROM via config.json).
case 0 ... 7:
if (!isPhysical)
{
memType = MEMBANK_TYPE_ROM;
waitStates = 1;
tCycSync = true;
}
break;
// 0x1000:0xCFFF = RAM
@@ -1062,7 +1186,7 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
memType = MEMBANK_TYPE_PHYSICAL_HW;
break;
// 0xE800:0xEFFF = Empty
// 0xE800:0xEFFF = MZ-MON-ROM II (part 1).
case 116 ... 119:
if (!isPhysical)
{
@@ -1072,13 +1196,13 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
}
break;
// 0xF000:0xFFFF = Empty
// 0xF000:0xFFFF = MZ-MON-ROM II (part 2, F88C+ unused/0xFF).
case 120 ... 127:
if (!isPhysical)
{
memType = MEMBANK_TYPE_ROM;
waitStates = 1;
tCycSync = true;
memType = MEMBANK_TYPE_ROM;
}
break;
}
@@ -1134,11 +1258,23 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Copy monitor ROM to Bank 0.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0x0000, 0x0000, 0x1000);
// Copy User ROM to Bank 0.
// Copy MZ-MON-ROM II to Bank 0 (E800-FFFF, contiguous IPL ROM).
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0xE800, 0xE800, 0x800);
// Copy Floppy ROM to Bank 0.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0xF000, 0xF000, 0x1000);
// Mirror CG-ROM to Bank 39 at D000-DFFF (4K).
// The CG-ROM is only accessible via $E5 bank=0, which maps it to D000-DFFF.
// Select CG-ROM on the physical bus, copy it, then restore default state.
// User-provided CG-ROM (rom[2] in config.json) overrides this mirror.
Z80CPU_writePhysicalIO(cpu, 0xE5, 0x00); // Select CG-ROM at D000-DFFF.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_CGROM << 16) | 0xD000, 0xD000, 0x1000);
Z80CPU_writePhysicalIO(cpu, 0xE4, 0x00); // Restore default memory map.
// Note: The physical CGROM chip (Japanese) is read directly by the display
// hardware for text-only programs. The virtual CG-ROM in Bank 39 is only
// visible to the Z80 CPU via E5h bank=0. Programs that use PCG will read
// the virtual CG-ROM and program PCG planes, which the display then uses.
// Text-only programs (BASIC, monitor) show physical CGROM characters.
}
// Initialise driver control state variables.
@@ -1162,12 +1298,38 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Install reset handler.
config->resetPtr = MZ1500_Reset;
// Register debug shell hooks for this machine.
#ifdef INCLUDE_DBGSH
cpu->dbgHooks.machineName = "MZ-1500";
#endif
// Install polling loop handler.
config->pollPtr = MZ1500_PollCB;
// Install task processing handler.
config->taskPtr = MZ1500_TaskProcessor;
// Load driver-level ROM(s) if provided in config.json.
// rom[0]=Monitor(4K), rom[1]=Extended(6K), rom[2]=CG-ROM(4K).
// These override the physical ROM mirrors with user-supplied ROM data.
mz1500_romLoadIndex = 0;
debugf("MZ1500_Init: romCount=%d\r\n", config->romCount);
for (int romIdx = 0; romIdx < config->romCount; romIdx++)
{
if (config->romConfig[romIdx].romFile != NULL)
{
debugf("MZ1500_Init: loading ROM '%s'\r\n", config->romConfig[romIdx].romFile);
Z80CPU_ReadROM(appConfig,
config->romConfig[romIdx].romFile,
cpu,
NULL,
MZ1500_readDriverROM,
NULL,
0,
0);
}
}
// Go through each interface and perform necessary configuration.
for (int ifIdx = 0; ifIdx < config->ifCount; ifIdx++)
{

View File

@@ -0,0 +1,265 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: MZ1E30.c
// Created: Jun 2026
// Author(s): Philip Smart
// Description: Z80 CPU DRIVER - MZ-1E30 SASI HDD Interface
// This file contains setup and driver for the Sharp MZ-1E30 SASI hard disk interface
// board for the MZ-2500/2800. Pairs with the reusable SASI controller (SASI.c).
//
// I/O ports:
// 0xA4 : SASI data R/W (commands, data, status, messages)
// 0xA5 : SASI control (W: SEL/RST/DMA/INT) / status (R: REQ/ACK/BSY/MSG/C_D/I_O)
// 0xA8 : ROM upper address latch (W only, D6-D0 = A14-A8)
// 0xA9 : ROM data read (R only, addr bus A7-A0 = lower byte)
//
// The board carries a 32KB IPL ROM that is accessed via ports 0xA8/0xA9
// (not mapped into Z80 address space). The SASI bus connects to up to 4
// target disk drives via ports 0xA4/0xA5.
//
// Credits:
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// License: GNU General Public License v3.0
// See LICENSE or <http://www.gnu.org/licenses/>
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#include "Z80CPU.h"
#define Z80_STATIC
#include "Z80.h"
#include <string.h>
#include <stdlib.h>
#include <pico/printf.h>
#include <pico/stdlib.h>
#include "pico/util/queue.h"
#include "pico/multicore.h"
#include "intercore.h"
#include "drivers/Sharp/MZ.h"
#include "drivers/Sharp/SASI.h"
#include "drivers/Sharp/MZ1E30.h"
#include "debug.h"
t_MZ1E30 *mz1e30Ctrl; // Control structure for the MZ-1E30 SASI Interface.
// -----------------------------------------------------------------------------------------------
// Interface: Sharp MZ-1E30 SASI HDD Interface Board for the MZ-2500/2800.
// Description: A SASI hard disk interface board with IPL ROM for SASI disk access.
// -----------------------------------------------------------------------------------------------
// Method to initialise the CPU state to support an MZ-1E30 SASI Interface.
uint8_t MZ1E30_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
{
// Locals.
uint8_t result = 0;
// First instance, allocate memory.
if (mz1e30Ctrl == NULL)
{
mz1e30Ctrl = (t_MZ1E30 *) calloc(1, sizeof(t_MZ1E30));
if (!mz1e30Ctrl)
{
return (0);
}
}
else
{
// Only one instance of this board allowed.
return (0);
}
mz1e30Ctrl->isPhysical = config->isPhysical;
// Initialize the SASI controller.
if (!sasiInit(&mz1e30Ctrl->sasi, &cpu->requestQueue, &cpu->responseQueue))
{
free(mz1e30Ctrl);
mz1e30Ctrl = NULL;
return (0);
}
// Load ROM from flash if a ROM file is configured.
// The ROM is accessed via I/O ports 0xA8/0xA9 (not mapped into Z80 address space),
// so we load it into a private buffer using Z80CPU_ReadROM from flash storage.
if (config->romCount > 0 && config->romConfig[0].romFile != NULL && cpu->appConfig != NULL)
{
mz1e30Ctrl->rom = (uint8_t *) calloc(1, SASI_ROM_SIZE);
if (mz1e30Ctrl->rom)
{
mz1e30Ctrl->romName = strdup(config->romConfig[0].romFile);
uint32_t romBytes = Z80CPU_ReadROM(cpu->appConfig, mz1e30Ctrl->romName,
NULL, NULL, NULL,
mz1e30Ctrl->rom, SASI_ROM_SIZE, 0);
if (romBytes > 0)
{
mz1e30Ctrl->sasi.romData = mz1e30Ctrl->rom;
mz1e30Ctrl->sasi.romSize = romBytes;
debugf("MZ1E30:ROM loaded %u bytes from flash\r\n", romBytes);
}
else
{
debugf("MZ1E30:ROM '%s' not found in flash\r\n", mz1e30Ctrl->romName);
free(mz1e30Ctrl->rom);
mz1e30Ctrl->rom = NULL;
}
}
}
// Configure disk targets — no RAM allocation needed.
// Sectors are read/written on demand via ESP32 SD card IPC.
// Disk filenames come from ifParam[] entries (up to MAX_MZ1E30_DISK_TARGETS).
for (int idx = 0; idx < MAX_MZ1E30_DISK_TARGETS; idx++)
{
mz1e30Ctrl->diskName[idx] = NULL;
if (idx < config->ifParamCount && config->ifParam[idx].file != NULL)
{
mz1e30Ctrl->diskName[idx] = strdup(config->ifParam[idx].file);
sasiSetTarget(&mz1e30Ctrl->sasi, idx, mz1e30Ctrl->diskName[idx], SASI_STD_DISK_SIZE);
}
}
// Install I/O handlers.
// Physical mode: SASI ports pass through to real hardware, only ROM is virtualised if configured.
// Virtual mode: Both SASI and ROM are handled by our emulation.
for (int idx = 0; idx < IO_PAGE_SIZE; idx++)
{
uint8_t port = idx & 0xFF;
// SASI data and control ports (0xA4-0xA5).
if (!config->isPhysical && (port == 0xA4 || port == 0xA5))
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1E30_IO_SASI;
// ROM ports (0xA8-0xA9) — always virtualised if ROM is loaded.
if (mz1e30Ctrl->rom && (port == 0xA8 || port == 0xA9))
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1E30_IO_ROM;
}
result = 1;
mz1e30Ctrl->diskctl = MZ1E30_HD_FILE_NOT_LOADED;
// Register debug shell hooks.
#ifdef INCLUDE_DBGSH
extern void sasiTraceDump(void);
extern bool sasiDbgEnabled;
cpu->dbgHooks.fdcName = "SASI";
cpu->dbgHooks.fdcTraceDump = sasiTraceDump;
cpu->dbgHooks.fdcTraceEnabled = &sasiDbgEnabled;
#endif
debugf("MZ1E30:INIT phys=%d rom=%s disks=%d\r\n",
config->isPhysical,
mz1e30Ctrl->romName ? mz1e30Ctrl->romName : "(none)",
config->ifParamCount);
return (result);
}
// Reset handler for the MZ-1E30 SASI Interface.
uint8_t MZ1E30_Reset(t_Z80CPU *cpu)
{
if (mz1e30Ctrl)
{
sasiReset(&mz1e30Ctrl->sasi);
}
return (0);
}
// Poll handler for the MZ-1E30 SASI Interface.
uint8_t __func_in_RAM(MZ1E30_PollCB)(t_Z80CPU *cpu)
{
if (mz1e30Ctrl)
{
sasiProcessResponses(&mz1e30Ctrl->sasi);
}
return (0);
}
// Task processor, called by external events to influence/update the driver.
uint8_t MZ1E30_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
uint8_t result = 0;
switch (task)
{
case HARD_DISK_CHANGE:
{
int diskNo = atoi(strtok(param, ","));
char *fileName = strtok(NULL, ",");
if (fileName != NULL && mz1e30Ctrl)
{
sasiChangeDisk(&mz1e30Ctrl->sasi, diskNo, fileName);
}
break;
}
case FLOPPY_DISK_CHANGE:
case QUICK_DISK_CHANGE:
break;
default:
result = 1;
break;
}
return (result);
}
// I/O Handler for SASI ports 0xA4 (data) and 0xA5 (control/status).
uint8_t __func_in_RAM(MZ1E30_IO_SASI)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
uint8_t result = 0;
uint8_t port = addr & 0xFF;
if (port == 0xA4)
{
if (read)
result = sasiReadData(&mz1e30Ctrl->sasi);
else
sasiWriteData(&mz1e30Ctrl->sasi, data);
}
else if (port == 0xA5)
{
if (read)
result = sasiReadStatus(&mz1e30Ctrl->sasi);
else
sasiWriteControl(&mz1e30Ctrl->sasi, data);
}
return (result);
}
// I/O Handler for ROM ports 0xA8 (address latch) and 0xA9 (data read).
uint8_t __func_in_RAM(MZ1E30_IO_ROM)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
uint8_t result = 0;
uint8_t port = addr & 0xFF;
if (port == 0xA8)
{
if (!read)
{
// Write: latch ROM upper address from data bus bits 6-0.
// Hardware also latches addr bus A11:A10 (B register bits 3:2) for ROM chip select.
// Both must be 0 to select this ROM board. In virtual mode we always accept.
sasiWriteRomAddr(&mz1e30Ctrl->sasi, data);
}
// Read from 0xA8 is invalid — return 0xFF.
else
{
result = 0xFF;
}
}
else if (port == 0xA9)
{
if (read)
{
// Read: ROM data. Lower address comes from Z80 address bus A15-A8
// (B register during INDR/INI). Hardware decodes A15-A8 as ROM A7-A0.
result = sasiReadRomData(&mz1e30Ctrl->sasi, (addr >> 8) & 0xFF);
}
// Write to 0xA9 is invalid — ignore.
}
return (result);
}

View File

@@ -1089,12 +1089,20 @@ uint8_t MZ2000_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Install reset handler.
config->resetPtr = MZ2000_Reset;
// Register debug shell hooks for this machine.
#ifdef INCLUDE_DBGSH
cpu->dbgHooks.machineName = "MZ-2000";
#endif
// Install polling loop handler.
config->pollPtr = MZ2000_PollCB;
// Install task processing handler.
config->taskPtr = MZ2000_TaskProcessor;
// Set machine type for shared interfaces (MZ8BFI uses this for FDC geometry).
MZ8BFI_machineType = MZ_2000;
// Go through each interface and perform necessary configuration.
for (int ifIdx = 0; ifIdx < config->ifCount; ifIdx++)
{

View File

@@ -1089,12 +1089,20 @@ uint8_t MZ2200_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Install reset handler.
config->resetPtr = MZ2200_Reset;
// Register debug shell hooks for this machine.
#ifdef INCLUDE_DBGSH
cpu->dbgHooks.machineName = "MZ-2200";
#endif
// Install polling loop handler.
config->pollPtr = MZ2200_PollCB;
// Install task processing handler.
config->taskPtr = MZ2200_TaskProcessor;
// Set machine type for shared interfaces (MZ8BFI uses this for FDC geometry).
MZ8BFI_machineType = MZ_2200;
// Go through each interface and perform necessary configuration.
for (int ifIdx = 0; ifIdx < config->ifCount; ifIdx++)
{

File diff suppressed because it is too large Load Diff

View File

@@ -1015,6 +1015,11 @@ uint8_t MZ700_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig
// Install reset handler.
config->resetPtr = MZ700_Reset;
// Register debug shell hooks for this machine.
#ifdef INCLUDE_DBGSH
cpu->dbgHooks.machineName = "MZ-700";
#endif
// Install polling loop handler.
config->pollPtr = MZ700_PollCB;

View File

@@ -885,6 +885,11 @@ uint8_t MZ80A_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig
// Install reset handler.
config->resetPtr = MZ80A_Reset;
// Register debug shell hooks for this machine.
#ifdef INCLUDE_DBGSH
cpu->dbgHooks.machineName = "MZ-80A";
#endif
// Install polling loop handler.
config->pollPtr = MZ80A_PollCB;

View File

@@ -80,15 +80,13 @@ uint8_t MZ80AFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
return (0);
}
// Get raw disk image size.
// Get raw disk image size. D88/ExtDSK files are kept in native format
// and can be larger than raw — allocate 50% headroom.
int diskSize = wd1773_getDiskSize(&mz80afiCtrl->fdc, MZ_80A);
if (diskSize > 0)
int bufSize = diskSize + diskSize / 2;
if (bufSize > 0)
{
// Allocate space for the disk images. Add overhead for Extended CPC DSK format
// which includes a 256-byte disk header and a 256-byte Track-Info block per track.
int extDskOverhead = 256 + (mz80afiCtrl->fdc.cylinders * mz80afiCtrl->fdc.heads * 256);
int allocSize = diskSize + extDskOverhead;
mz80afiCtrl->disk = (uint8_t *) calloc(MAX_MZ80AFI_DISK_DRIVES, allocSize);
mz80afiCtrl->disk = (uint8_t *) calloc(MAX_MZ80AFI_DISK_DRIVES, bufSize);
}
if (mz80afiCtrl->disk != NULL)
{
@@ -106,9 +104,7 @@ uint8_t MZ80AFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
mz80afiCtrl->diskctl = MZ80AFI_FDC_NO_FILE;
}
}
int extDskOverhead = 256 + (mz80afiCtrl->fdc.cylinders * mz80afiCtrl->fdc.heads * 256);
int allocSize = diskSize + extDskOverhead;
if (wd1773_init(&mz80afiCtrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz80afiCtrl->diskName[0], mz80afiCtrl->disk, allocSize, MZ_80A))
if (wd1773_init(&mz80afiCtrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz80afiCtrl->diskName[0], mz80afiCtrl->disk, bufSize, MZ_80A))
{
result = 1;

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,7 @@
#include "drivers/Sharp/MZ8BFI.h"
t_MZ8BFI *mz8bfiCtrl; // Control structure for the MZ-8BFI Floppy Disk Interface.
int MZ8BFI_machineType = MZ_2000; // Default machine type for FDC geometry.
// -----------------------------------------------------------------------------------------------
// Interface: Sharp MZ-8BFI / E0054PA Floppy Disk Interface Board (MZ-2000).
@@ -91,10 +92,14 @@ uint8_t MZ8BFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
}
// Get size of a raw disk image.
int diskSize = wd1773_getDiskSize(&mz8bfiCtrl->fdc, MZ_2000);
// D88 images are larger than raw (688-byte header + 16-byte per-sector headers).
// Allocate enough to hold a D88 file which is then converted in-place to raw.
int diskSize = wd1773_getDiskSize(&mz8bfiCtrl->fdc, MZ8BFI_machineType);
// D88/ExtDSK files are kept in native format and can be larger than raw.
// D88 overhead: 688-byte header + 16-byte per-sector headers + potential
// non-standard sector counts on copy-protected disks.
// Use 15% over raw as minimum headroom to avoid needing realloc.
int d88Overhead = 688 + mz8bfiCtrl->fdc.cylinders * mz8bfiCtrl->fdc.heads * mz8bfiCtrl->fdc.sectorsPerTrack * 16;
int minHeadroom = diskSize / 7; // ~15%
if (d88Overhead < minHeadroom) d88Overhead = minHeadroom;
int bufSize = diskSize + d88Overhead;
if (bufSize > 0)
{
@@ -117,7 +122,7 @@ uint8_t MZ8BFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
mz8bfiCtrl->diskctl = MZ8BFI_FDC_NO_FILE;
}
}
if (wd1773_init(&mz8bfiCtrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz8bfiCtrl->diskName[0], mz8bfiCtrl->disk, bufSize, MZ_2000))
if (wd1773_init(&mz8bfiCtrl->fdc, &cpu->requestQueue, &cpu->responseQueue, mz8bfiCtrl->diskName[0], mz8bfiCtrl->disk, bufSize, MZ8BFI_machineType))
{
result = 1;
@@ -152,6 +157,17 @@ uint8_t MZ8BFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
free(mz8bfiCtrl);
mz8bfiCtrl = NULL;
}
else
{
// Register FDC debug shell hooks.
#ifdef INCLUDE_DBGSH
extern void WD1773_dumpLog(void);
extern bool fdcDbgEnabled;
cpu->dbgHooks.fdcName = "WD1773";
cpu->dbgHooks.fdcTraceDump = WD1773_dumpLog;
cpu->dbgHooks.fdcTraceEnabled = &fdcDbgEnabled;
#endif
}
return (result);
}
@@ -228,12 +244,13 @@ uint8_t __func_in_RAM(MZ8BFI_IO_DriveSel)(t_Z80CPU *cpu, bool read, uint16_t add
uint8_t __func_in_RAM(MZ8BFI_IO_SideSel)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
// Side select is external TTL logic, NOT through the MB8866 inverting data bus.
wd1773_setHead(&mz8bfiCtrl->fdc, data & 0x01);
WD1773_logSideSel(data);
return (0);
}
// DDEN select (0 = MFM, 1 = FM)
// DDEN select (0 = MFM, 1 = FM) — also through the inverting buffer.
uint8_t __func_in_RAM(MZ8BFI_IO_DDENSel)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
wd1773_setDensity(&mz8bfiCtrl->fdc, data & 0x01);

View File

@@ -0,0 +1,728 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: SASI.c
// Created: Jun 2026
// Author(s): Philip Smart
// Description: SASI Bus Controller Emulation
//
// Reusable SASI (Shugart Associates System Interface) bus controller emulation.
// Implements the SASI bus phase state machine, command decoding, and data transfer
// for virtual hard disk targets. Designed to be paired with a board-level wrapper
// (e.g., MZ1E30.c) that handles I/O port routing and ROM access.
//
// Disk images are NOT loaded into PSRAM (22MB+ per target). Instead, sectors are
// read/written on demand via MSG_READ_SECTOR/MSG_WRITE_SECTOR through the inter-core
// queue to ESP32 SD card. The Z80 spins polling REQ while the sector is fetched
// (~1-10ms round trip), which is well within SASI timeout limits.
//
// Supports: READ(6), WRITE(6), SEEK(6), TEST UNIT READY, REQUEST SENSE, INQUIRY.
// Block size: 256 bytes. Up to 4 targets.
//
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// License: GNU General Public License v3.0
// See LICENSE or <http://www.gnu.org/licenses/>
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#include "Z80CPU.h"
#define Z80_STATIC
#include "Z80.h"
#include <string.h>
#include <stdlib.h>
#include <pico/printf.h>
#include <pico/stdlib.h>
#include "pico/util/queue.h"
#include "intercore.h"
#include "drivers/Sharp/SASI.h"
#include "debug.h"
bool sasiDbgEnabled = false;
// ========================================================================================
// Debug Trace
// ========================================================================================
// Pack a trace entry: [phase(4) | rw(1) | port(3) | data(8) | extra(16)]
static inline void sasiTraceLog(t_SASI *sasi, uint8_t phase, bool isRead, uint8_t port, uint8_t data, uint16_t extra)
{
if (!sasiDbgEnabled)
return;
uint32_t entry = ((uint32_t)(phase & 0x0F) << 28) |
((uint32_t)(isRead ? 1 : 0) << 27) |
((uint32_t)(port & 0x07) << 24) |
((uint32_t)data << 16) |
(uint32_t)extra;
sasi->trace.entries[sasi->trace.head] = entry;
sasi->trace.head = (sasi->trace.head + 1) % SASI_TRACE_SIZE;
if (sasi->trace.count < SASI_TRACE_SIZE)
sasi->trace.count++;
}
// Global pointer for dump function (set during init, like WD1773 pattern).
static t_SASI *g_sasiForTrace = NULL;
static const char *sasiPhaseName(t_SASIPhase phase)
{
switch (phase)
{
case SASI_PHASE_BUS_FREE: return "FREE";
case SASI_PHASE_SELECTION: return "SEL";
case SASI_PHASE_COMMAND: return "CMD";
case SASI_PHASE_DATA_IN: return "DIN";
case SASI_PHASE_DATA_OUT: return "DOUT";
case SASI_PHASE_STATUS_IN: return "STAT";
case SASI_PHASE_MESSAGE_IN: return "MSG";
default: return "???";
}
}
void sasiTraceDump(void)
{
if (!g_sasiForTrace || g_sasiForTrace->trace.count == 0)
{
debugf("SASI: no trace data\r\n");
return;
}
t_SASI *sasi = g_sasiForTrace;
int start = (sasi->trace.count < SASI_TRACE_SIZE)
? 0
: sasi->trace.head;
int count = sasi->trace.count;
debugf("SASI trace (%d entries):\r\n", count);
for (int i = 0; i < count; i++)
{
int idx = (start + i) % SASI_TRACE_SIZE;
uint32_t e = sasi->trace.entries[idx];
uint8_t phase = (e >> 28) & 0x0F;
bool isRead = (e >> 27) & 1;
uint8_t port = (e >> 24) & 0x07;
uint8_t data = (e >> 16) & 0xFF;
uint16_t extra = e & 0xFFFF;
debugf(" %s %s P%d D=%02X x=%04X\r\n",
sasiPhaseName((t_SASIPhase)phase),
isRead ? "RD" : "WR", port, data, extra);
}
}
// ========================================================================================
// Internal Helpers
// ========================================================================================
static uint32_t sasiGetNextRequestId(t_SASI *sasi)
{
return sasi->nextRequestId++;
}
// Return the bus phase signal bits for port 0xA5 read based on current phase.
// MSG(4) | C/D(3) | I/O(2)
static uint8_t sasiPhaseSignals(t_SASIPhase phase)
{
switch (phase)
{
case SASI_PHASE_COMMAND: return SASI_STAT_CD; // C/D=1, I/O=0
case SASI_PHASE_DATA_IN: return SASI_STAT_IO; // C/D=0, I/O=1
case SASI_PHASE_DATA_OUT: return 0; // C/D=0, I/O=0
case SASI_PHASE_STATUS_IN: return SASI_STAT_CD | SASI_STAT_IO; // C/D=1, I/O=1
case SASI_PHASE_MESSAGE_IN: return SASI_STAT_MSG | SASI_STAT_CD | SASI_STAT_IO; // MSG=1, C/D=1, I/O=1
default: return 0;
}
}
// Decode LBA from CDB6: [opcode, LUN(7:5)|LBA2(4:0), LBA1, LBA0, NOB, CTRL]
static uint32_t sasiCdbLBA(const uint8_t *cdb)
{
return ((uint32_t)(cdb[1] & 0x1F) << 16) | ((uint32_t)cdb[2] << 8) | cdb[3];
}
static uint8_t sasiCdbBlockCount(const uint8_t *cdb)
{
return cdb[4];
}
// ========================================================================================
// Sector-Level I/O (on-demand from SD via ESP32)
// ========================================================================================
// Queue an async sector read from SD card for the current LBA.
// Sets readPending=true, REQ stays deasserted until sasiProcessResponses gets the data.
static void sasiQueueReadBlock(t_SASI *sasi)
{
int tid = sasi->selectedTarget;
if (tid < 0 || !sasi->target[tid].ready)
return;
uint32_t offset = sasi->lba * SASI_BLOCK_SIZE;
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_READ_SECTOR;
msg.context = sasi;
msg.requestId = sasiGetNextRequestId(sasi);
strncpy(msg.sectorOp.filename, sasi->target[tid].filename, MAX_IC_FILENAME_LEN - 1);
msg.sectorOp.offset = offset;
msg.sectorOp.size = SASI_BLOCK_SIZE;
msg.sectorOp.buffer = sasi->buffer;
sasi->readPending = true;
sasi->readRequestId = msg.requestId;
sasi->req = false; // Z80 polls status; REQ stays low until sector arrives.
queue_try_add(sasi->requestQueue, &msg);
}
// Queue an async sector write to SD card from the buffer at current LBA.
static void sasiQueueWriteBlock(t_SASI *sasi)
{
int tid = sasi->selectedTarget;
if (tid < 0 || !sasi->target[tid].ready)
return;
uint32_t offset = sasi->lba * SASI_BLOCK_SIZE;
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_WRITE_SECTOR;
msg.context = sasi;
msg.requestId = sasiGetNextRequestId(sasi);
strncpy(msg.sectorOp.filename, sasi->target[tid].filename, MAX_IC_FILENAME_LEN - 1);
msg.sectorOp.offset = offset;
msg.sectorOp.size = SASI_BLOCK_SIZE;
msg.sectorOp.buffer = sasi->buffer;
sasi->writePending = true;
sasi->writeRequestId = msg.requestId;
queue_try_add(sasi->requestQueue, &msg);
}
// ========================================================================================
// Command Processing
// ========================================================================================
// Process a complete CDB and transition to the appropriate phase.
static void sasiProcessCommand(t_SASI *sasi)
{
uint8_t opcode = sasi->cdb[0];
uint32_t lba = sasiCdbLBA(sasi->cdb);
uint8_t nob = sasiCdbBlockCount(sasi->cdb);
int tid = sasi->selectedTarget;
if (sasiDbgEnabled)
debugf("SASI:CMD op=%02X lba=%06X nob=%d tgt=%d\r\n", opcode, lba, nob, tid);
sasi->senseKey = SASI_SENSE_NO_SENSE;
sasi->statusByte = SASI_STATUS_GOOD;
sasi->messageByte = SASI_MSG_COMPLETE;
switch (opcode)
{
case SASI_CMD_TEST_UNIT_READY:
if (tid < 0 || !sasi->target[tid].ready)
{
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_NOT_READY;
}
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
break;
case SASI_CMD_REQUEST_SENSE:
{
memset(sasi->buffer, 0, SASI_BLOCK_SIZE);
sasi->buffer[0] = sasi->senseKey;
sasi->bufferPos = 0;
sasi->bufferSize = 4;
sasi->phase = SASI_PHASE_DATA_IN;
sasi->req = true;
break;
}
case SASI_CMD_READ6:
if (nob == 0) nob = 1;
sasi->lba = lba;
sasi->blocksRemaining = nob;
sasi->totalBlocks = nob;
if (tid < 0 || !sasi->target[tid].ready)
{
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_NOT_READY;
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
else if ((uint64_t)(lba + nob) * SASI_BLOCK_SIZE > sasi->target[tid].diskSize)
{
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_ILLEGAL_REQ;
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
else
{
// Enter DATA_IN phase but with REQ=0 — sector read is async.
sasi->phase = SASI_PHASE_DATA_IN;
sasi->bufferPos = 0;
sasi->bufferSize = SASI_BLOCK_SIZE;
sasiQueueReadBlock(sasi);
// REQ will be asserted by sasiProcessResponses when sector arrives.
}
break;
case SASI_CMD_WRITE6:
if (nob == 0) nob = 1;
sasi->lba = lba;
sasi->blocksRemaining = nob;
sasi->totalBlocks = nob;
if (tid < 0 || !sasi->target[tid].ready)
{
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_NOT_READY;
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
else
{
sasi->bufferPos = 0;
sasi->bufferSize = SASI_BLOCK_SIZE;
sasi->phase = SASI_PHASE_DATA_OUT;
sasi->req = true;
}
break;
case SASI_CMD_SEEK6:
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
break;
case SASI_CMD_INQUIRY:
{
memset(sasi->buffer, 0, SASI_BLOCK_SIZE);
sasi->buffer[0] = 0x00; // Direct access device
sasi->buffer[1] = 0x00; // Not removable
sasi->buffer[2] = 0x01; // SASI compliance
sasi->buffer[3] = 0x00; // Response format
sasi->buffer[4] = 31; // Additional length
memcpy(&sasi->buffer[8], "SHARP ", 8);
memcpy(&sasi->buffer[16], "MZ-1E30 HDD ", 16);
memcpy(&sasi->buffer[32], "1.0 ", 4);
sasi->bufferPos = 0;
sasi->bufferSize = 36;
sasi->phase = SASI_PHASE_DATA_IN;
sasi->req = true;
break;
}
default:
debugf("SASI:CMD unknown op=%02X\r\n", opcode);
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_ILLEGAL_REQ;
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
break;
}
}
// ========================================================================================
// Public API — Bus Operations
// ========================================================================================
bool sasiInit(t_SASI *sasi, queue_t *requestQueue, queue_t *responseQueue)
{
memset(sasi, 0, sizeof(t_SASI));
sasi->requestQueue = requestQueue;
sasi->responseQueue = responseQueue;
sasi->selectedTarget = -1;
sasi->phase = SASI_PHASE_BUS_FREE;
sasi->nextRequestId = 1;
g_sasiForTrace = sasi;
return true;
}
void sasiReset(t_SASI *sasi)
{
sasi->phase = SASI_PHASE_BUS_FREE;
sasi->selectedTarget = -1;
sasi->selAsserted = false;
sasi->rstAsserted = false;
sasi->req = false;
sasi->ack = false;
sasi->bsy = false;
sasi->cdbPos = 0;
sasi->bufferPos = 0;
sasi->bufferSize = 0;
sasi->blocksRemaining = 0;
sasi->readPending = false;
sasi->statusByte = SASI_STATUS_GOOD;
sasi->messageByte = SASI_MSG_COMPLETE;
sasi->senseKey = SASI_SENSE_NO_SENSE;
if (sasiDbgEnabled)
debugf("SASI:RST\r\n");
}
void sasiWriteData(t_SASI *sasi, uint8_t val)
{
sasiTraceLog(sasi, sasi->phase, false, 0xA4 & 0x07, val, 0);
sasi->dataOut = val;
switch (sasi->phase)
{
case SASI_PHASE_BUS_FREE:
case SASI_PHASE_SELECTION:
break;
case SASI_PHASE_COMMAND:
if (sasi->cdbPos < SASI_CDB_SIZE)
{
sasi->cdb[sasi->cdbPos++] = val;
sasi->ack = true;
sasi->req = false;
if (sasi->cdbPos >= SASI_CDB_SIZE)
{
sasi->ack = false;
sasiProcessCommand(sasi);
}
else
{
sasi->ack = false;
sasi->req = true;
}
}
break;
case SASI_PHASE_DATA_OUT:
if (sasi->bufferPos < sasi->bufferSize)
{
sasi->buffer[sasi->bufferPos++] = val;
sasi->ack = true;
sasi->req = false;
sasi->ack = false;
if (sasi->bufferPos >= sasi->bufferSize)
{
// Block complete — queue write to SD card.
sasiQueueWriteBlock(sasi);
sasi->lba++;
sasi->blocksRemaining--;
if (sasi->blocksRemaining > 0)
{
sasi->bufferPos = 0;
sasi->bufferSize = SASI_BLOCK_SIZE;
sasi->req = true;
}
else
{
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
}
else
{
sasi->req = true;
}
}
break;
default:
break;
}
}
uint8_t sasiReadData(t_SASI *sasi)
{
uint8_t result = 0xFF;
switch (sasi->phase)
{
case SASI_PHASE_DATA_IN:
if (sasi->bufferPos < sasi->bufferSize)
{
result = sasi->buffer[sasi->bufferPos++];
sasi->ack = true;
sasi->req = false;
sasi->ack = false;
if (sasi->bufferPos >= sasi->bufferSize)
{
sasi->lba++;
sasi->blocksRemaining--;
if (sasi->blocksRemaining > 0)
{
// Queue next block read — REQ stays low until it arrives.
sasi->bufferPos = 0;
sasi->bufferSize = SASI_BLOCK_SIZE;
sasiQueueReadBlock(sasi);
}
else
{
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
}
else
{
sasi->req = true;
}
}
break;
case SASI_PHASE_STATUS_IN:
result = sasi->statusByte;
sasi->ack = true;
sasi->req = false;
sasi->ack = false;
sasi->phase = SASI_PHASE_MESSAGE_IN;
sasi->req = true;
break;
case SASI_PHASE_MESSAGE_IN:
result = sasi->messageByte;
sasi->ack = true;
sasi->req = false;
sasi->ack = false;
sasi->bsy = false;
sasi->phase = SASI_PHASE_BUS_FREE;
sasi->selectedTarget = -1;
sasi->req = false;
break;
default:
break;
}
sasiTraceLog(sasi, sasi->phase, true, 0xA4 & 0x07, result, 0);
return result;
}
void sasiWriteControl(t_SASI *sasi, uint8_t val)
{
sasiTraceLog(sasi, sasi->phase, false, 0xA5 & 0x07, val, 0);
bool newSel = (val & SASI_CTRL_SEL) != 0;
bool newRst = (val & SASI_CTRL_RST) != 0;
// Handle RST — full bus reset.
if (newRst && !sasi->rstAsserted)
{
sasiReset(sasi);
sasi->rstAsserted = true;
return;
}
if (!newRst && sasi->rstAsserted)
{
sasi->rstAsserted = false;
}
// Handle SEL assertion/deassertion for target selection.
// SASI protocol: Host asserts SEL + target ID on data bus. Target sees its ID
// and asserts BSY (while SEL is still active). Host polls for BSY=1, then
// deasserts SEL. Command phase begins.
if (newSel && !sasi->selAsserted)
{
sasi->selAsserted = true;
sasi->selectedTarget = -1;
for (int i = 0; i < SASI_MAX_TARGETS; i++)
{
if (sasi->dataOut & (1 << i))
{
sasi->selectedTarget = i;
break;
}
}
// Target responds to SEL by asserting BSY immediately (while SEL is still active).
// The Z80 polls status waiting for BSY=1 before deasserting SEL.
if (sasi->selectedTarget >= 0 && sasi->selectedTarget < SASI_MAX_TARGETS &&
sasi->target[sasi->selectedTarget].ready)
{
sasi->bsy = true;
sasi->phase = SASI_PHASE_SELECTION;
if (sasiDbgEnabled)
debugf("SASI:SEL tgt=%d data=%02X → BSY\r\n", sasi->selectedTarget, sasi->dataOut);
}
else
{
if (sasiDbgEnabled)
debugf("SASI:SEL tgt=%d data=%02X → no target\r\n", sasi->selectedTarget, sasi->dataOut);
}
}
else if (!newSel && sasi->selAsserted)
{
// SEL deasserted — if BSY is active, transition to COMMAND phase.
sasi->selAsserted = false;
if (sasi->bsy && sasi->selectedTarget >= 0)
{
sasi->phase = SASI_PHASE_COMMAND;
sasi->cdbPos = 0;
sasi->req = true;
if (sasiDbgEnabled)
debugf("SASI:CMD phase tgt=%d\r\n", sasi->selectedTarget);
}
else
{
// No valid target — remain BUS_FREE.
sasi->selectedTarget = -1;
sasi->phase = SASI_PHASE_BUS_FREE;
}
}
}
uint8_t sasiReadStatus(t_SASI *sasi)
{
// If a sector read is pending, check the response queue inline.
// The Z80 polls status in a tight loop; PollCB only runs between Z80_run batches
// which may not be frequent enough. Checking here ensures the response is picked
// up as soon as it arrives from Core 0's ESP32 SPI transaction.
if (sasi->readPending)
{
sasiProcessResponses(sasi);
}
uint8_t result = 0;
if (sasi->bsy)
result |= SASI_STAT_BSY;
if (sasi->req)
result |= SASI_STAT_REQ;
if (sasi->ack)
result |= SASI_STAT_ACK;
if (sasi->phase != SASI_PHASE_BUS_FREE && sasi->phase != SASI_PHASE_SELECTION)
result |= sasiPhaseSignals(sasi->phase);
sasiTraceLog(sasi, sasi->phase, true, 0xA5 & 0x07, result, 0);
return result;
}
// ========================================================================================
// Target Configuration (no RAM allocation — sector I/O is on-demand)
// ========================================================================================
void sasiSetTarget(t_SASI *sasi, int targetId, const char *filename, uint32_t diskSize)
{
if (targetId < 0 || targetId >= SASI_MAX_TARGETS)
return;
t_SASITarget *tgt = &sasi->target[targetId];
tgt->filename = filename ? strdup(filename) : NULL;
tgt->diskSize = diskSize;
tgt->ready = (filename != NULL && diskSize > 0);
debugf("SASI:TGT %d file=%s size=%u ready=%d\r\n",
targetId, filename ? filename : "(none)", diskSize, tgt->ready);
}
void sasiChangeDisk(t_SASI *sasi, int targetId, const char *newFilename)
{
if (targetId < 0 || targetId >= SASI_MAX_TARGETS)
return;
t_SASITarget *tgt = &sasi->target[targetId];
if (tgt->filename)
{
free(tgt->filename);
tgt->filename = NULL;
}
tgt->ready = false;
if (newFilename)
{
tgt->filename = strdup(newFilename);
tgt->ready = true;
}
}
// ========================================================================================
// Async Response Processing (called from PollCB on Core 1)
// ========================================================================================
void sasiProcessResponses(t_SASI *sasi)
{
t_CoreMsg msg;
while (queue_try_remove(sasi->responseQueue, &msg))
{
// Only process messages targeted at this controller.
if (msg.context != sasi)
{
queue_try_add(sasi->responseQueue, &msg);
break;
}
switch (msg.type)
{
case MSG_READ_COMPLETE:
if (sasi->readPending && sasi->readRequestId == msg.requestId)
{
sasi->readPending = false;
if (msg.response.success)
{
sasi->req = true;
}
else
{
debugf("SASI:RD FAIL lba=%u\r\n", sasi->lba);
sasi->statusByte = SASI_STATUS_CHECK;
sasi->senseKey = SASI_SENSE_NOT_READY;
sasi->phase = SASI_PHASE_STATUS_IN;
sasi->req = true;
}
}
break;
case MSG_WRITE_COMPLETE:
if (sasi->writePending && sasi->writeRequestId == msg.requestId)
{
sasi->writePending = false;
if (!msg.response.success)
debugf("SASI:WR FAIL id=%u\r\n", msg.requestId);
}
break;
default:
break;
}
}
}
// ========================================================================================
// ROM Access
// ========================================================================================
void sasiWriteRomAddr(t_SASI *sasi, uint8_t val)
{
// Data bus bits 6-0 → ROM address A14-A8. Bit 7 is invalid/unused.
sasi->romAddrHigh = val & 0x7F;
sasiTraceLog(sasi, sasi->phase, false, 0, val, (uint16_t)sasi->romAddrHigh << 8);
}
uint8_t sasiReadRomData(t_SASI *sasi, uint8_t addrLow)
{
if (!sasi->romData)
return 0xFF;
uint32_t addr = ((uint32_t)sasi->romAddrHigh << 8) | addrLow;
if (addr >= sasi->romSize)
return 0xFF;
uint8_t result = sasi->romData[addr];
sasiTraceLog(sasi, sasi->phase, true, 1, result, (uint16_t)addr);
return result;
}

View File

@@ -28,11 +28,18 @@
#include "pico/util/queue.h"
#include "pico/multicore.h"
#include "pico/time.h"
#include "hardware/xip_cache.h"
#include "debug.h"
#include "intercore.h"
#include "drivers/Sharp/MZ.h"
#include "drivers/Sharp/WD1773.h"
// DRQ deferral counter: readSector sets to 1, first status read decrements
// to 0 (returns DRQ=0 so software sees the "seeking" state), second read
// finds counter=0 and asserts DRQ. Separate from addressSector which is
// used by READ ADDRESS for sector rotation simulation.
static int fdcDrqDelay = 0;
// FDC I/O trace ring buffer — enabled via debug shell "fdctrace on".
#define FDC_DBG_SZ 64
static struct { uint8_t port; uint8_t rw; uint8_t val; uint8_t st; } fdcDbg[FDC_DBG_SZ];
@@ -42,6 +49,8 @@ bool fdcDbgEnabled = false;
static t_WD1773 *fdcDbgInstance = NULL; // Saved for dump function.
static int wd1773_getD88TrackSectors(t_WD1773 *wd, int track, int head);
static void fdcLog(uint8_t port, uint8_t rw, uint8_t val, uint8_t st)
{
if (!fdcDbgEnabled) return;
@@ -143,8 +152,8 @@ bool wd1773_setDiskSize(t_WD1773 *wd, int machineType)
case MZ_2500:
wd->cylinders = 80;
wd->heads = 2;
wd->sectorsPerTrack = 8;
wd->sectorSize = 512;
wd->sectorsPerTrack = 16;
wd->sectorSize = 256;
wd->densityMfm = true;
break;
default:
@@ -195,6 +204,24 @@ void wd1773_getTrackParams(t_WD1773 *wd, int track, int head, int *sectors, int
}
}
}
if (wd->isD88)
{
int nsec = wd1773_getD88TrackSectors(wd, track, head);
if (nsec > 0)
{
*sectors = nsec;
// Read sector size from first sector header's N field via physical mapping.
int physIdx = track * wd->heads + head;
int mapSize = wd->cylinders * wd->heads;
if (wd->d88PhysMap && physIdx >= 0 && physIdx < mapSize && wd->d88PhysMap[physIdx] >= 0)
{
uint32_t trkOfs = wd->trackOffsets[wd->d88PhysMap[physIdx]];
uint8_t N = wd->diskImage[trkOfs + 3];
*sectorSize = 128 << N;
}
return;
}
}
*sectors = wd->sectorsPerTrack;
*sectorSize = wd->sectorSize;
}
@@ -237,13 +264,25 @@ int wd1773_processDeviceResponses(t_WD1773 *wd, queue_t *responseQueue)
wd->opState.loadPending = false;
wd->diskLoaded = msg.response.success;
if (!msg.response.success)
{
debugf("FDC:LOAD FAILED\r\n");
wd1773_setStatusFlag(wd, WD1773_STATUS_CRC_ERROR);
}
else
{
// The disk image was loaded by Core 0 via SPI DMA, which writes
// directly to PSRAM without updating Core 1's XIP cache.
// xip_cache_invalidate_range does NOT work for PSRAM on RP2350.
// Full cache flush: clean (write-back dirty lines including our
// struct field writes) then invalidate (discard stale image data).
wd->diskSize = msg.response.size;
wd->diskImage = (uint8_t *)((uintptr_t)wd->diskImage | 0x04000000);
xip_cache_clean_all();
xip_cache_invalidate_all();
wd->isExtendedDsk = wd1773_parseExtendedDsk(wd);
if (!wd->isExtendedDsk)
wd1773_parseD88(wd); // Try D88 format (converts in-place to raw).
wd1773_parseD88(wd);
debugf("FDC:LOAD OK size=%d extDsk=%d d88=%d diskImg=%p\r\n", wd->diskSize, wd->isExtendedDsk, wd->isD88, wd->diskImage);
}
wd->intrq = true;
break;
@@ -330,6 +369,7 @@ bool wd1773_parseExtendedDsk(t_WD1773 *wd)
}
wd->diskSize = offset;
debugf("FDC:ExtDSK cyl=%d heads=%d tracks=%d size=%d\r\n", wd->cylinders, wd->heads, wd->totalTracks, offset);
return true;
}
@@ -387,15 +427,14 @@ bool wd1773_parseD88(t_WD1773 *wd)
if (headerDiskSize != fileSize)
return false;
// Save track offset table before raw conversion overwrites the header.
uint32_t trackOfs[164];
// Count tracks from the offset table.
int totalTracks = 0;
for (int i = 0; i < 164; i++)
{
int o = 0x20 + i * 4;
trackOfs[i] = img[o] | (img[o + 1] << 8) | (img[o + 2] << 16) | (img[o + 3] << 24);
if (trackOfs[i] != 0)
totalTracks = i + 1; // Last non-zero entry determines total tracks.
uint32_t tOfs = img[o] | (img[o + 1] << 8) | (img[o + 2] << 16) | (img[o + 3] << 24);
if (tOfs != 0)
totalTracks = i + 1;
}
if (totalTracks == 0)
return false;
@@ -407,55 +446,89 @@ bool wd1773_parseD88(t_WD1773 *wd)
int cylinders = (totalTracks + wd->heads - 1) / wd->heads;
if (cylinders > 0)
wd->cylinders = cylinders;
wd->totalTracks = totalTracks;
int rawSectorSize = wd->sectorSize;
int rawTrackSize = wd->sectorsPerTrack * rawSectorSize;
uint32_t rawDiskSize = (uint32_t)wd->cylinders * wd->heads * rawTrackSize;
// Extract sector data from D88 into raw flat layout.
for (int t = 0; t < totalTracks; t++)
// Allocate and populate track offset array from the D88 header.
// Keep the D88 data in-place — NO raw conversion. Sector lookups
// are done at read time by walking the D88 sector headers.
wd->trackOffsets = calloc(totalTracks, sizeof(uint32_t));
if (!wd->trackOffsets)
return false;
for (int i = 0; i < totalTracks; i++)
{
if (trackOfs[t] == 0 || trackOfs[t] >= fileSize)
continue;
int o = 0x20 + i * 4;
wd->trackOffsets[i] = img[o] | (img[o + 1] << 8) | (img[o + 2] << 16) | (img[o + 3] << 24);
}
// Read sector count from first sector header in this track.
uint8_t *firstHdr = img + trackOfs[t];
uint16_t sectorsInTrack = firstHdr[4] | (firstHdr[5] << 8);
// Build physical (C,H) → D88 track index mapping.
//
// Two strategies:
// 1. INDEX-BASED: d88PhysMap[i] = i (identity). Used for standard/contiguous
// disks where D88 index order matches physical cylinder*heads+head.
// Handles disks with swapped H fields in sector headers (e.g. MS-DOS).
//
// 2. HEADER-BASED: d88PhysMap uses C/H from each track's first sector header.
// Used for sparse/non-standard disks (copy-protected, gaps in track table)
// where D88 indices don't correspond to physical positions (e.g. Balloon Fight).
//
// Detection: if the track offset table has gaps (zero entries between non-zero
// entries), the disk is sparse → use header-based. Otherwise → index-based.
int mapSize = wd->cylinders * wd->heads;
wd->d88PhysMap = malloc(mapSize * sizeof(int));
if (!wd->d88PhysMap)
{
free(wd->trackOffsets);
wd->trackOffsets = NULL;
return false;
}
for (int i = 0; i < mapSize; i++)
wd->d88PhysMap[i] = -1;
uint32_t secOfs = trackOfs[t];
for (int s = 0; s < sectorsInTrack; s++)
// Detect sparse track layout (gaps in the track offset table).
bool isSparse = false;
bool seenNonZero = false;
for (int i = totalTracks - 1; i >= 0; i--)
{
if (wd->trackOffsets[i] != 0)
seenNonZero = true;
else if (seenNonZero)
{
if (secOfs + 16 > fileSize)
break;
uint8_t *hdr = img + secOfs;
uint8_t C = hdr[0];
uint8_t H = hdr[1];
uint8_t R = hdr[2];
// uint8_t N = hdr[3]; // Size code — we use rawSectorSize instead.
uint16_t dataSize = hdr[0x0E] | (hdr[0x0F] << 8);
// Calculate destination in raw flat layout.
uint32_t rawOffset = ((uint32_t)C * wd->heads + H) * rawTrackSize + ((uint32_t)(R - 1)) * rawSectorSize;
uint32_t srcOfs = secOfs + 16;
int copySize = (dataSize < rawSectorSize) ? dataSize : rawSectorSize;
if (R >= 1 && srcOfs + copySize <= fileSize && rawOffset + rawSectorSize <= rawDiskSize)
{
memmove(img + rawOffset, img + srcOfs, copySize);
if (copySize < rawSectorSize)
memset(img + rawOffset + copySize, 0, rawSectorSize - copySize);
}
secOfs += 16 + dataSize;
isSparse = true;
break;
}
}
// Update disk size to reflect the raw format.
wd->diskSize = rawDiskSize;
if (isSparse)
{
// Header-based mapping: read C/H from each track's first sector header.
for (int i = 0; i < totalTracks; i++)
{
uint32_t tOfs = wd->trackOffsets[i];
if (tOfs == 0 || tOfs + 16 > fileSize)
continue;
uint8_t hdrC = img[tOfs];
uint8_t hdrH = img[tOfs + 1];
int physIdx = hdrC * wd->heads + hdrH;
if (physIdx >= 0 && physIdx < mapSize)
wd->d88PhysMap[physIdx] = i;
}
}
else
{
// Index-based mapping: D88 index order is the physical order.
// Sector header C/H may be swapped (e.g. MS-DOS) — ignore them.
for (int i = 0; i < totalTracks && i < mapSize; i++)
{
if (wd->trackOffsets[i] != 0)
wd->d88PhysMap[i] = i;
}
}
debugf("D88: %d tracks (%d cyl x %d heads), %d sec/trk, %d B/sec → %d bytes raw\r\n",
totalTracks, wd->cylinders, wd->heads, wd->sectorsPerTrack, rawSectorSize, rawDiskSize);
wd->isD88 = true;
debugf("D88: %d tracks (%d cyl x %d heads), native format (%s)\r\n",
totalTracks, wd->cylinders, wd->heads,
isSparse ? "header-mapped" : "index-mapped");
return true;
}
@@ -477,10 +550,17 @@ bool wd1773_findSectorInTrack(t_WD1773 *wd, int track, int head, int sectorId, u
uint8_t num = tib[0x15];
uint32_t data = wd->trackOffsets[idx] + 0x100;
// Match on sector ID only. The Track-Info block is already indexed by
// (track * heads + head), so we are searching within the correct physical
// track data. The sector info fields info[0] (C) and info[1] (H) record
// whatever the FDC wrote during formatting — on CP/M disks the H field is
// deliberately swapped relative to the physical head select, causing a
// strict three-field match to fail. Matching on sector ID alone is safe
// because sector IDs are unique within a single track.
for (int s = 0; s < num; s++)
{
uint8_t *info = tib + 0x18 + s * 8;
if (info[2] == sectorId && info[0] == track && info[1] == head)
if (info[2] == sectorId)
{
*dataOffset = data;
*dataLength = (info[7] << 8) | info[6];
@@ -493,6 +573,84 @@ bool wd1773_findSectorInTrack(t_WD1773 *wd, int track, int head, int sectorId, u
return false;
}
/**
* @brief Find a sector in a D88 track by walking the sector headers.
*
* D88 stores per-sector headers (16 bytes each) interleaved with sector data.
* Each header: C(1), H(1), R(1), N(1), nsec(2), density(1), deleted(1),
* status(1), reserved(5), dataSize(2).
* Sector data follows immediately after each header.
*
* @return true if sector found, with dataOffset/dataLength/st1/st2 populated.
*/
bool wd1773_findSectorInD88(t_WD1773 *wd, int track, int head, int sectorId, uint32_t *dataOffset, uint32_t *dataLength, uint8_t *st1, uint8_t *st2)
{
// Use physical mapping: FDC track register = cylinder, head = selected head.
// d88PhysMap translates (cyl*heads+head) to the actual D88 track index.
int physIdx = track * wd->heads + head;
int mapSize = wd->cylinders * wd->heads;
if (!wd->d88PhysMap || physIdx < 0 || physIdx >= mapSize)
return false;
int idx = wd->d88PhysMap[physIdx];
if (idx < 0 || idx >= wd->totalTracks || wd->trackOffsets[idx] == 0)
return false;
uint32_t fileSize = wd->diskSize; // For D88, diskSize = original file size.
uint8_t *img = wd->diskImage;
uint32_t trkOfs = wd->trackOffsets[idx];
if (trkOfs >= fileSize)
return false;
// Read sector count from first sector header.
uint16_t nsec = img[trkOfs + 4] | (img[trkOfs + 5] << 8);
// Walk through all sectors in this track.
uint32_t secOfs = trkOfs;
for (int s = 0; s < nsec; s++)
{
if (secOfs + 16 > fileSize)
break;
uint8_t *hdr = img + secOfs;
uint8_t R = hdr[2];
uint8_t N = hdr[3];
uint16_t dataSize = hdr[0x0E] | (hdr[0x0F] << 8);
if (R == sectorId)
{
*dataOffset = secOfs + 16;
*dataLength = dataSize;
*st1 = hdr[8]; // FDC status from D88 header.
*st2 = 0;
(void)N; // Size code available if needed.
return true;
}
secOfs += 16 + dataSize;
}
return false; // Sector not found → Record Not Found.
}
/**
* @brief Get the number of sectors in a D88 track (for READ ADDRESS rotation).
*/
static int wd1773_getD88TrackSectors(t_WD1773 *wd, int track, int head)
{
// Use physical mapping to find the D88 track index.
int physIdx = track * wd->heads + head;
int mapSize = wd->cylinders * wd->heads;
if (!wd->d88PhysMap || physIdx < 0 || physIdx >= mapSize)
return 0;
int idx = wd->d88PhysMap[physIdx];
if (idx < 0 || idx >= wd->totalTracks || wd->trackOffsets[idx] == 0)
return 0;
uint8_t *img = wd->diskImage;
uint32_t trkOfs = wd->trackOffsets[idx];
if (trkOfs >= wd->diskSize)
return 0;
return img[trkOfs + 4] | (img[trkOfs + 5] << 8);
}
/**
* @brief Initialize FDC and queue disk load
*/
@@ -504,6 +662,7 @@ bool wd1773_init(t_WD1773 *wd, queue_t *req, queue_t *resp, const char *filename
wd->requestQueue = req;
wd->responseQueue = resp;
wd->diskImage = img;
wd->diskBufSize = bufSize;
wd->currentTrack = 0;
wd->currentHead = 0;
wd->direction = 1;
@@ -513,6 +672,7 @@ bool wd1773_init(t_WD1773 *wd, queue_t *req, queue_t *resp, const char *filename
wd->diskLoaded = false;
wd->motorStartTime = get_absolute_time();
wd->lastRotation = get_absolute_time();
wd->lastByteTime = get_absolute_time();
wd->machineType = type;
wd->opState.loadPending = wd->opState.writePending = wd->opState.readPending = false;
wd->filename = strdup(filename);
@@ -549,6 +709,11 @@ void wd1773_cleanup(t_WD1773 *wd)
free(wd->trackOffsets);
wd->trackOffsets = NULL;
}
if (wd->d88PhysMap)
{
free(wd->d88PhysMap);
wd->d88PhysMap = NULL;
}
if (wd->trackSizes)
{
free(wd->trackSizes);
@@ -567,17 +732,23 @@ void wd1773_changeDisk(t_WD1773 *wd, const char *name, int diskNo)
free((void *) wd->filename);
wd->filename = strdup(name);
// Mark the disk as unloaded first — prevents Core 1 Z80 emulation from
// accessing stale FDC state during the load.
// Mark the disk as unloaded and free stale format tracking arrays
// so the parsers start fresh for the new image.
wd->diskLoaded = false;
wd->isExtendedDsk = false;
wd->isD88 = false;
if (wd->trackOffsets) { free(wd->trackOffsets); wd->trackOffsets = NULL; }
if (wd->d88PhysMap) { free(wd->d88PhysMap); wd->d88PhysMap = NULL; }
if (wd->trackSizes) { free(wd->trackSizes); wd->trackSizes = NULL; }
wd->totalTracks = 0;
wd->opState.loadPending = true;
// Queue the load request to Core 0.
t_CoreMsg msg = {.type = MSG_LOAD_FLOPPYDISK, .context = wd};
strncpy(msg.fileOp.filename, name, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.filename[MAX_IC_FILENAME_LEN - 1] = '\0';
msg.fileOp.buffer = wd->diskImage;
msg.fileOp.size = wd->diskSize;
msg.fileOp.buffer = (uint8_t *)((uintptr_t)wd->diskImage & ~0x04000000); // DMA uses cached alias
msg.fileOp.size = wd->diskBufSize;
msg.fileOp.diskNo = diskNo;
// Use non-blocking add — queue_add_blocking from Core 0 to Core 0's
// own queue hangs if the queue is full (nobody else is draining it).
@@ -625,16 +796,30 @@ void wd1773_clearStatusFlags(t_WD1773 *wd)
uint8_t wd1773_getStatus(t_WD1773 *wd)
{
wd1773_processResponses(wd);
uint8_t st = wd->statusFlags & ~WD1773_STATUS_NOT_READY; // Mask out stale NOT_READY — computed live below.
if (wd->busy)
st |= WD1773_STATUS_BUSY;
if (wd->writeProtect)
// Write Protect is only reported for TYPE_I commands and TYPE_II/III
// WRITE commands. For TYPE_II READ commands, bit 6 means "Write Fault"
// (or Record Type on some variants) and must be 0 — otherwise the
// software may reject the read as an error.
if (wd->writeProtect &&
(wd->lastCommandType == TYPE_I ||
wd->currentOperation == OP_WRITE_SECTOR ||
wd->currentOperation == OP_WRITE_TRACK))
{
st |= WD1773_STATUS_WRITE_PROTECT;
}
if (wd->drq)
st |= WD1773_STATUS_DRQ;
bool ready = wd->diskLoaded && wd->motorOn && (absolute_time_diff_us(wd->motorStartTime, get_absolute_time()) >= wd->spinUpUs);
// Virtual FDC: report ready if disk is loaded, regardless of motor state.
// Real floppy motors coast for seconds after power-off; software often
// turns the motor off between operations and expects the drive to still
// respond to commands issued shortly after.
bool ready = wd->diskLoaded;
if (!ready || wd->opState.loadPending || wd->opState.readPending)
st |= WD1773_STATUS_NOT_READY;
@@ -667,9 +852,40 @@ uint8_t __not_in_flash_func(wd1773_read)(t_WD1773 *wd, uint8_t offset)
switch (offset)
{
case 0:
{
wd->intrq = false;
val = wd1773_getStatus(wd);
// After a TYPE_I command completes, busy is left true so that the
// first status read reports BUSY=1 (software polls for command
// acceptance). Now that the status has been read once, clear busy
// and assert INTRQ to signal completion — matching real FDC timing
// where BUSY is visible for a few microseconds after the command.
// Simulate DRQ delay: real FDC asserts DRQ after a seek/rotation
// delay, not immediately. MZ-2500 MS-DOS FDC code has TWO phases:
// Phase 2 (F5DB): loop while inverted-DRQ bit=0 (waits for DRQ CLEAR)
// Phase 3 (F5E1): poll until DRQ active, then read data register
// The first status read after a read command MUST return DRQ=0 so
// Phase 2 can exit. DRQ is then asserted for subsequent reads
// so Phase 3 finds data ready. We use addressSector as a one-shot
// counter: readSector sets it to 1, first status read decrements to 0
// (returning DRQ=0), second status read sees counter=0 and asserts DRQ.
if (wd->busy && !wd->drq && wd->bufferSize > 0 && fdcDrqDelay == 0 &&
(wd->currentOperation == OP_READ_SECTOR || wd->currentOperation == OP_READ_TRACK ||
wd->currentOperation == OP_READ_ADDRESS))
{
wd->drq = true;
wd->lastByteTime = get_absolute_time();
}
if (fdcDrqDelay > 0)
fdcDrqDelay--;
if (wd->busy && wd->lastCommandType == TYPE_I && wd->currentOperation == OP_NONE)
{
wd->busy = false;
wd->intrq = true;
}
break;
}
case 1:
val = wd->trackReg;
break;
@@ -686,6 +902,7 @@ uint8_t __not_in_flash_func(wd1773_read)(t_WD1773 *wd, uint8_t offset)
if (wd->drq)
{
wd->dataReg = wd->buffer[wd->bufferPos++];
wd->lastByteTime = get_absolute_time();
if (wd->bufferPos >= wd->bufferSize)
{
wd->drq = false;
@@ -801,7 +1018,17 @@ void __not_in_flash_func(wd1773_write)(t_WD1773 *wd, uint8_t offset, uint8_t val
*/
void wd1773_executeCommand(t_WD1773 *wd)
{
if (!wd->diskLoaded || !wd->motorOn || absolute_time_diff_us(wd->motorStartTime, get_absolute_time()) < wd->spinUpUs)
uint8_t cmd = wd->command;
{
static const char *typeNames[] = {"?", "I", "II", "III", "IV"};
int ti = wd->lastCommandType;
if (fdcDbgEnabled) debugf("FDC:CMD %02X type=%s T%02d H%d S%02d st=%02X\r\n",
cmd, (ti >= 0 && ti <= 4) ? typeNames[ti] : "?",
wd->trackReg, wd->currentHead, wd->sectorReg,
wd1773_getStatus(wd));
}
if (!wd->diskLoaded)
{
wd1773_setStatusFlag(wd, WD1773_STATUS_NOT_READY);
wd->busy = false;
@@ -810,8 +1037,6 @@ void wd1773_executeCommand(t_WD1773 *wd)
return;
}
uint8_t cmd = wd->command;
switch (wd->lastCommandType)
{
case TYPE_I:
@@ -854,8 +1079,12 @@ void wd1773_executeCommand(t_WD1773 *wd)
if (verify && wd->trackReg != wd->currentTrack)
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
// Leave busy=true briefly so the next status read sees BUSY=1.
// Software (e.g., MZ-80B CP/M IPLPROC) polls for BUSY immediately
// after issuing RESTORE/SEEK; if we clear busy synchronously, the
// poll loop never sees BUSY=1 and reports a timeout error.
// The first status read (wd1773_read port 0, TYPE_I path) will
// clear busy and set intrq, mimicking the real FDC's behaviour.
wd->currentOperation = OP_NONE;
break;
}
@@ -910,12 +1139,41 @@ void wd1773_executeCommand(t_WD1773 *wd)
*/
void wd1773_readSector(t_WD1773 *wd)
{
if (fdcDbgEnabled) debugf("FDC:RD T%02d H%d S%02d (trk=%d)\r\n", wd->trackReg, wd->currentHead, wd->sectorReg, wd->currentTrack);
// D88 native format: look up sector directly from D88 headers.
if (wd->isD88)
{
uint32_t dataOffset, dataLength;
uint8_t st1, st2;
if (!wd1773_findSectorInD88(wd, wd->trackReg, wd->currentHead, wd->sectorReg, &dataOffset, &dataLength, &st1, &st2))
{
if (fdcDbgEnabled) debugf("FDC:RD RNF d88 T%d H%d S%d\r\n", wd->trackReg, wd->currentHead, wd->sectorReg);
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
wd->currentOperation = OP_NONE;
return;
}
int copySize = (dataLength > MAX_SECTOR_SIZE) ? MAX_SECTOR_SIZE : dataLength;
memcpy(wd->buffer, wd->diskImage + dataOffset, copySize);
if (fdcDbgEnabled) debugf("FDC:RD OK d88 off=%06X len=%d\r\n", dataOffset, copySize);
wd->bufferPos = 0;
wd->bufferSize = copySize;
wd->drq = false; // DRQ deferred: first status read returns DRQ=0, second asserts DRQ
fdcDrqDelay = 1;
return;
}
// Raw flat format.
if (!wd->isExtendedDsk)
{
int sectors = wd->sectorsPerTrack;
int sectorSize = wd->sectorSize;
if (wd->sectorReg < 1 || wd->sectorReg > sectors || wd->trackReg >= wd->cylinders)
{
if (fdcDbgEnabled) debugf("FDC:RD RNF raw S%d>%d or T%d>=%d\r\n", wd->sectorReg, sectors, wd->trackReg, wd->cylinders);
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
@@ -926,6 +1184,7 @@ void wd1773_readSector(t_WD1773 *wd)
uint32_t offset = (wd->trackReg * wd->heads + wd->currentHead) * (sectors * sectorSize) + (wd->sectorReg - 1) * sectorSize;
if (offset + sectorSize > wd->diskSize)
{
if (fdcDbgEnabled) debugf("FDC:RD RNF raw off=%06X > diskSz=%d\r\n", offset, wd->diskSize);
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
@@ -934,9 +1193,11 @@ void wd1773_readSector(t_WD1773 *wd)
}
memcpy(wd->buffer, wd->diskImage + offset, sectorSize);
if (fdcDbgEnabled) debugf("FDC:RD OK raw off=%06X len=%d\r\n", offset, sectorSize);
wd->bufferPos = 0;
wd->bufferSize = sectorSize;
wd->drq = true;
wd->drq = false; // DRQ deferred: first status read returns DRQ=0, second asserts DRQ
fdcDrqDelay = 1;
return;
}
@@ -944,6 +1205,7 @@ void wd1773_readSector(t_WD1773 *wd)
uint8_t st1, st2;
if (!wd1773_findSectorInTrack(wd, wd->trackReg, wd->currentHead, wd->sectorReg, &dataOffset, &dataLength, &st1, &st2))
{
if (fdcDbgEnabled) debugf("FDC:RD RNF ext T%d H%d S%d\r\n", wd->trackReg, wd->currentHead, wd->sectorReg);
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
@@ -957,9 +1219,12 @@ void wd1773_readSector(t_WD1773 *wd)
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
memcpy(wd->buffer, wd->diskImage + dataOffset, dataLength);
if (fdcDbgEnabled) debugf("FDC:RD OK off=%06X len=%d b0=%02X\r\n", dataOffset, dataLength, wd->buffer[0]);
wd->bufferPos = 0;
wd->bufferSize = dataLength;
wd->drq = true;
wd->drq = false; // DRQ deferred: first status read returns DRQ=0, second asserts DRQ
fdcDrqDelay = 1;
wd->lastByteTime = get_absolute_time();
}
/**
@@ -967,6 +1232,44 @@ void wd1773_readSector(t_WD1773 *wd)
*/
void wd1773_writeSector(t_WD1773 *wd)
{
if (wd->writeProtect)
{
wd1773_setStatusFlag(wd, WD1773_STATUS_WRITE_PROTECT);
wd->busy = false;
wd->intrq = true;
wd->currentOperation = OP_NONE;
return;
}
// D88 native format: find sector and write data in-place.
if (wd->isD88)
{
uint32_t dataOffset, dataLength;
uint8_t st1, st2;
if (!wd1773_findSectorInD88(wd, wd->trackReg, wd->currentHead, wd->sectorReg, &dataOffset, &dataLength, &st1, &st2))
{
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
wd->currentOperation = OP_NONE;
return;
}
int writeSize = (wd->bufferSize < (int)dataLength) ? wd->bufferSize : (int)dataLength;
memcpy(wd->diskImage + dataOffset, wd->buffer, writeSize);
wd->opState.pendingWriteId = wd1773_getNextRequestId(wd);
wd->opState.writePending = true;
t_CoreMsg msg = {.type = MSG_WRITE_SECTOR, .context = wd, .requestId = wd->opState.pendingWriteId};
snprintf(msg.sectorOp.filename, MAX_IC_FILENAME_LEN, "%s", wd->filename);
msg.sectorOp.offset = dataOffset;
msg.sectorOp.size = writeSize;
msg.sectorOp.buffer = wd->diskImage + dataOffset;
queue_add_blocking(wd->requestQueue, &msg);
return;
}
if (!wd->isExtendedDsk)
{
int sectors = wd->sectorsPerTrack;
@@ -1059,6 +1362,44 @@ void wd1773_writeSector(t_WD1773 *wd)
*/
void wd1773_readTrack(t_WD1773 *wd)
{
// D88 native format: gather all sector data from the D88 track.
if (wd->isD88)
{
int idx = wd->trackReg * wd->heads + wd->currentHead;
if (idx >= wd->totalTracks || wd->trackOffsets[idx] == 0)
{
wd1773_setStatusFlag(wd, WD1773_STATUS_SEEK_ERROR);
wd->busy = false;
wd->intrq = true;
wd->currentOperation = OP_NONE;
return;
}
uint8_t *img = wd->diskImage;
uint32_t trkOfs = wd->trackOffsets[idx];
int nsec = wd1773_getD88TrackSectors(wd, wd->trackReg, wd->currentHead);
uint32_t secOfs = trkOfs;
int total = 0;
for (int s = 0; s < nsec; s++)
{
if (secOfs + 16 > wd->diskSize) break;
uint16_t dSz = img[secOfs + 0x0E] | (img[secOfs + 0x0F] << 8);
int copySize = dSz;
if (total + copySize > MAX_TRACK_SIZE) copySize = MAX_TRACK_SIZE - total;
if (copySize > 0)
memcpy(wd->buffer + total, img + secOfs + 16, copySize);
total += copySize;
secOfs += 16 + dSz;
}
wd->bufferPos = 0;
wd->bufferSize = total;
wd->drq = false; // DRQ deferred
fdcDrqDelay = 1;
return;
}
if (!wd->isExtendedDsk)
{
if (wd->trackReg >= wd->cylinders)
@@ -1087,7 +1428,8 @@ void wd1773_readTrack(t_WD1773 *wd)
memcpy(wd->buffer, wd->diskImage + offset, size);
wd->bufferPos = 0;
wd->bufferSize = size;
wd->drq = true;
wd->drq = false; // DRQ deferred
fdcDrqDelay = 1;
return;
}
@@ -1118,7 +1460,8 @@ void wd1773_readTrack(t_WD1773 *wd)
memcpy(wd->buffer, wd->diskImage + dataStart, dataSize);
wd->bufferPos = 0;
wd->bufferSize = dataSize;
wd->drq = true;
wd->drq = false; // DRQ deferred
fdcDrqDelay = 1;
}
/**
@@ -1218,20 +1561,94 @@ void wd1773_writeTrack(t_WD1773 *wd)
}
/**
* @brief Read Address (fake 6-byte record)
* @brief Read Address (6-byte ID record)
*
* On real hardware, READ ADDRESS reads whichever sector ID field is currently
* passing under the head. We simulate disk rotation by cycling through the
* sectors sequentially. For Extended DSK images the actual C/H/R/N values
* are read from the Track-Info sector descriptors so that non-standard sector
* IDs (e.g. CP/M disks using IDs 11-20) and per-sector size codes are
* reported correctly.
*/
void wd1773_readAddress(t_WD1773 *wd)
{
int sectors, sectorSize;
wd1773_getTrackParams(wd, wd->trackReg, wd->currentHead, &sectors, &sectorSize);
// Simulate disk rotation: advance to the next sector ID field.
// On real hardware, READ ADDRESS reads whichever sector is currently
// passing under the head. We cycle through 1..sectorsPerTrack.
// Advance simulated rotation index.
wd->addressSector++;
if (wd->addressSector < 1 || wd->addressSector > sectors)
wd->addressSector = 1;
if (wd->isExtendedDsk)
{
// Read actual C/H/R/N from the Track-Info sector descriptor.
int idx = wd->trackReg * wd->heads + wd->currentHead;
if (idx < wd->totalTracks && wd->trackSizes[idx] != 0)
{
uint8_t *tib = wd->diskImage + wd->trackOffsets[idx];
int s = wd->addressSector - 1; // 0-based index into sector info
if (s < tib[0x15])
{
uint8_t *info = tib + 0x18 + s * 8;
wd->buffer[0] = info[0]; // C — cylinder from sector ID
wd->buffer[1] = info[1]; // H — head from sector ID
wd->buffer[2] = info[2]; // R — sector ID
wd->buffer[3] = info[3]; // N — size code
wd->buffer[4] = 0; // CRC1
wd->buffer[5] = 0; // CRC2
// Per WD1773 datasheet: C value is written into the Sector Register.
wd->sectorReg = info[0];
wd->bufferPos = 0;
wd->bufferSize = 6;
wd->drq = true;
return;
}
}
}
// D88 native format: read actual C/H/R/N from D88 sector headers.
if (wd->isD88)
{
int idx = wd->trackReg * wd->heads + wd->currentHead;
if (idx < wd->totalTracks && wd->trackOffsets[idx] != 0)
{
uint8_t *img = wd->diskImage;
uint32_t trkOfs = wd->trackOffsets[idx];
int nsec = wd1773_getD88TrackSectors(wd, wd->trackReg, wd->currentHead);
// Re-clamp addressSector for this track's actual sector count.
if (wd->addressSector < 1 || wd->addressSector > nsec)
wd->addressSector = 1;
// Walk to the Nth sector header.
uint32_t secOfs = trkOfs;
for (int s = 0; s < wd->addressSector - 1 && s < nsec; s++)
{
uint16_t dSz = img[secOfs + 0x0E] | (img[secOfs + 0x0F] << 8);
secOfs += 16 + dSz;
}
if (secOfs + 16 <= wd->diskSize)
{
uint8_t *hdr = img + secOfs;
wd->buffer[0] = hdr[0]; // C
wd->buffer[1] = hdr[1]; // H
wd->buffer[2] = hdr[2]; // R
wd->buffer[3] = hdr[3]; // N
wd->buffer[4] = 0; // CRC1
wd->buffer[5] = 0; // CRC2
wd->sectorReg = hdr[0]; // Per WD1773 datasheet.
wd->bufferPos = 0;
wd->bufferSize = 6;
wd->drq = true;
return;
}
}
}
// Raw format or fallback: synthesise ID from assumed 1..N numbering.
wd->buffer[0] = wd->trackReg; // C — cylinder
wd->buffer[1] = wd->currentHead; // H — head
wd->buffer[2] = wd->addressSector; // R — sector ID (simulated rotation)
@@ -1245,5 +1662,6 @@ void wd1773_readAddress(t_WD1773 *wd)
wd->bufferPos = 0;
wd->bufferSize = 6;
wd->drq = true;
wd->drq = false; // DRQ deferred
fdcDrqDelay = 1;
}

View File

@@ -187,6 +187,7 @@ enum Z80CPU_TASK_NAME
FLOPPY_DISK_CHANGE = 0, // Execute a floppy disk change.
QUICK_DISK_CHANGE = 1, // Execute a quick disk change.
RAMFILE_CHANGE = 2, // Execute a RAMFILE backup change.
HARD_DISK_CHANGE = 3, // Execute a hard disk image change.
};
// Forward declare the Z80 CPU structure, needed by the declarations for function prototypes.
@@ -218,9 +219,35 @@ typedef uint8_t (*t_TaskFunc)(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *p
// Function pointer type for memory task processing.
typedef uint8_t *(*t_MemoryTask)(t_Z80CPU *cpu, uint32_t intAddr, uint16_t extAddr, uint16_t size);
// ---------------------------------------------------------------------------
// Debug shell hooks — driver/interface-registered function pointers that let
// machine-specific debug commands (fdctrace, mmutrace, etc.) work with any
// FDC, MMU, or media controller. Drivers call the registration helpers in
// their Init functions; the debug shell dispatches through these hooks.
// ---------------------------------------------------------------------------
typedef struct
{
const char *machineName; // Display name: "MZ-2500", "PCW-8256", "MZ-80K", etc.
// FDC trace hooks (WD1773, UPD765, T3444M, ...)
const char *fdcName; // FDC type name: "WD1773", "uPD765", "T3444M", NULL if none.
void (*fdcTraceDump)(void); // Dump FDC I/O trace ring buffer.
bool *fdcTraceEnabled; // Pointer to the driver's trace-enable flag.
// Machine-specific trace hooks (MMU banking, gate array, ...)
const char *machineTraceName; // Short name for the command, e.g. "MMU/IO" or "GateArray".
void (*machineTraceDump)(void); // Dump machine-specific trace.
// Quick Disk / secondary media hooks
const char *qdName; // Media type name: "QD", NULL if none.
void (*qdTraceDump)(void); // Dump QD trace ring buffer.
bool *qdTraceEnabled; // Pointer to the driver's trace-enable flag.
} t_DbgShellHooks;
// Function pointer type for virtual driver feature processing.
typedef uint8_t *(*t_VirtualFunc)(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig *config, const char *ifName);
// Structure of the internal RP2350 RAM, fast RAM, which is used to initially subdivide the Z80 Address space. Advantage is speed when address space is not complicated.
// 31:24 23:16 15:0
// Z80 Address = 0x0055 -> membankPtr[0x0055 / MEMORY_BLOCK_SIZE] -> <Memory Type> <Bank> <16bit Z80 Address>
@@ -354,9 +381,11 @@ typedef struct
} t_ioReMap;
// Struct for storing interface configuration parameters.
// Each param entry is a key/value pair — the key determines the parameter type.
typedef struct
{
char *file; // Name of a file required by the interface for initialisation.
char *file; // "file" key: name of a file required by the interface.
char *ip; // "ip" key: IP address string, e.g. "192.168.1.210:6800".
} t_ifParam;
// Struct for storing a drivers interface configuration.
@@ -379,6 +408,8 @@ struct t_drvConfig
{
const char *name; // Name of the driver. 1:1 mapping with t_VirtualFuncMap.virtualFuncName
bool isPhysical; // Should the driver use physical mappings (ROM/RAM etc) or virtual?
int romCount; // Number of driver-level ROM's (e.g., system ROM replacement).
t_drvROMConfig *romConfig; // Struct array of driver-level ROM descriptions.
int ifCount; // Number of interfaces stored.
t_drvIFConfig *ifConfig; // Driver interface configuration. Each driver can have multiple optional interfaces.
t_ResetFunc resetPtr; // Pointer to a reset handler for this driver.
@@ -404,6 +435,7 @@ struct Z80CPU
t_drivers _drivers; // Handle to array of structures to specify active drivers and their configuration.
queue_t requestQueue; // Inter core command request queue. Used to dispatch commands from the Z80 to core0 for processing.
queue_t responseQueue; // Inter core command wresponse queue. Used to dispatch responses to commands received from the Z80 on core0.
t_FlashAppConfigHeader *appConfig; // Pointer to flash app config header (set by persona driver, used by interface drivers for ROM access).
bool halt; // Halt state, true = active.
bool refreshEnable; // Enable DRAM refresh during virtual memory fetches. Set via JSON config "z80refresh" parameter.
volatile bool hold; // Hold CPU processing, used by Core 0 when access to physical is needed.
@@ -412,6 +444,8 @@ struct Z80CPU
uint32_t hostClkHz; // Measured host clock frequency in Hz (GPIO35, measured at boot).
uint32_t emulSpeedHz; // Effective emulation speed in Hz (computed from Z80 cycle counter).
volatile uint32_t emulLoopCount; // Incremented by Core 1 after each z80_run() call (32-bit, atomic on ARM).
volatile uint32_t intPinLowCount; // Diagnostic: count of z80_run loops where /INT (GPIO33) was LOW.
uint32_t m1DelayCount; // Virtual mode: T-state counter for delaying M1 bus cycles after EI.
// Pending task from Core 0 — processed by Core 1 in the emulation loop.
// Core 0 writes these, Core 1 reads and clears pendingTask.
@@ -442,6 +476,9 @@ struct Z80CPU
volatile uint32_t dbgCorruptCount; // Total corruptions detected.
volatile uint32_t dbgCorruptHead; // Write index into corruption log.
uint32_t dbgCorrupt[DBG_CORRUPT_SZ]; // Log: [31:16]=PC, [15:8]=fetched, [7:0]=verified.
// Driver-registered debug hooks for machine-specific trace commands.
t_DbgShellHooks dbgHooks;
#endif
};

View File

@@ -22,6 +22,11 @@
// 68h (W) : UFM address register
// 68h (R) : UFM status (bit 7: WP, bit 1: READY, bit 0: BUSY)
// 69h (R/W) : UFM data register
// 6Ah (W) : Config index register (selects config byte)
// 6Bh (R) : Config data register (returns selected byte)
// Index 0-3: NET file server IP[0..3]
// Index 4: NET file server port high byte
// Index 5: NET file server port low byte
// 6Fh (W) : Unlock (write D1h then keyword)
//
// Credits: Board designed by Oh!Ishi (Oh!石)
@@ -210,6 +215,19 @@ typedef struct
uint8_t *mz1r37Ram; // 640KB RAM buffer.
uint32_t mz1r37AddrLatch; // Latched address bits [19:8].
char *mz1r37FileName; // SD card backing file name (NULL = no persistence).
bool mz1r37SectorWritePending; // True while a single sector write is in flight.
uint32_t mz1r37WriteId; // Request ID for pending sector write.
uint32_t mz1r37NextReqId; // Monotonic request ID counter.
uint8_t mz1r37DirtyMap[(CELESTITE_R37_SIZE / 512 + 7) / 8]; // 160-byte dirty bitmap (512B pages).
bool mz1r37HasDirty; // Quick flag: at least one dirty page exists.
absolute_time_t mz1r37LastWrite; // Time of last Z80 write (for quiesce detection).
int mz1r37FlushPos; // Scan position for incremental flush.
// NET file server configuration (read via ports 6Ah/6Bh).
// Parsed from ifParam[2].file = "ip:port" (e.g., "192.168.1.210:6800").
uint8_t netSrvIP[4]; // File server IP address (default 0.0.0.0 = unconfigured).
uint16_t netSrvPort; // File server TCP port (default 6800).
uint8_t cfgIndex; // Config register index (port 6Ah write).
// Network state (Phase 2).
queue_t *requestQueue; // Pointer to inter-core request queue.
@@ -256,5 +274,6 @@ uint8_t Celestite_IO_Unlock(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t dat
uint8_t Celestite_IO_R12(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_R37Latch(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_R37Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_Cfg(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
#endif // CELESTITE_H

View File

@@ -45,17 +45,32 @@
#define MZ1R37_H
// Constants.
#define MZ1R37_RAM_SIZE 655360 // 640KB = 640 × 1024 bytes.
#define MZ1R37_ADDR_MASK 0xFFFFF // 20-bit address space (1MB).
#define MZ1R37_DEFAULT_BASE 0xAC // Default I/O base address.
#define MZ1R37_PORT_COUNT 2 // Number of consecutive I/O ports used.
#define MZ1R37_RAM_SIZE 655360 // 640KB = 640 x 1024 bytes.
#define MZ1R37_ADDR_MASK 0xFFFFF // 20-bit address space (1MB).
#define MZ1R37_DEFAULT_BASE 0xAC // Default I/O base address.
#define MZ1R37_PORT_COUNT 2 // Number of consecutive I/O ports used.
#define MZ1R37_PAGE_SIZE 512 // Dirty tracking granularity (matches SD sector size).
#define MZ1R37_PAGE_COUNT (MZ1R37_RAM_SIZE / MZ1R37_PAGE_SIZE) // 1280 pages.
#define MZ1R37_DIRTY_MAP_SIZE ((MZ1R37_PAGE_COUNT + 7) / 8) // 160 bytes bitmap.
#define MZ1R37_WRITE_QUIESCE_US 2000000 // 2 seconds after last write before flushing dirty pages.
// Struct for the MZ-1R37 EMM board state.
typedef struct
{
uint8_t *ram; // 640KB RAM buffer.
uint32_t addrLatch; // Latched address bits [19:8] (from port ACh write).
uint8_t ioBase; // I/O base address.
uint8_t *ram; // 640KB RAM buffer.
uint32_t addrLatch; // Latched address bits [19:8] (from port ACh write).
uint8_t ioBase; // I/O base address.
char *ramfileName; // Backing file on ESP32 SD card (NULL = no persistence).
queue_t *requestQueue; // Inter-core request queue (to Core 0).
queue_t *responseQueue; // Inter-core response queue (from Core 0).
bool loadPending; // Backing file load in progress.
bool sectorWritePending; // A single sector write is in flight.
uint32_t pendingWriteId; // Request ID for pending sector write.
uint32_t nextRequestId; // Monotonic request ID counter.
uint8_t dirtyMap[MZ1R37_DIRTY_MAP_SIZE]; // Bitmap: 1 bit per 512-byte page.
bool hasDirtyPages; // Quick flag: at least one dirty page exists.
absolute_time_t lastWriteTime; // Time of last Z80 write (for quiesce detection).
int flushScanPos; // Current position in dirty map scan.
} t_MZ1R37;
// Public prototypes.

View File

@@ -69,6 +69,7 @@
// Constants.
#define MZ1500_MEMBANK_0 0 // Primary RAM bank.
#define MZ1500_MEMBANK_1 1 // RAM bank to use for paging in RAM.
#define MZ1500_MEMBANK_CGROM 39 // Bank for CG-ROM storage (D000-DFFF = 4K). Banks 40-63 reserved for RFS.
#define MZ1500_UPPERMEM_BLOCKS 64 // Number of blocks in the upper paged 12K RAM.
// PCG constants.

View File

@@ -0,0 +1,57 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: MZ1E30.h
// Created: Jun 2026
// Author(s): Philip Smart
// Description: Z80 CPU DRIVER - MZ-1E30 SASI HDD Interface
// This file contains setup and driver for the Sharp MZ-1E30 SASI hard disk interface.
//
// I/O ports:
// 0xA4 : SASI data (commands/data write, status/data read)
// 0xA5 : SASI control (write: SEL/RST/DMA/INT) / status (read: REQ/ACK/BSY/MSG/C_D/I_O)
// 0xA8 : ROM upper address latch (write only, D6-D0 = A14-A8)
// 0xA9 : ROM data read (read only, addr bus A7-A0 = lower address)
//
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// License: GNU General Public License v3.0
// See LICENSE or <http://www.gnu.org/licenses/>
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#ifndef MZ1E30_H
#define MZ1E30_H
#include "drivers/Sharp/SASI.h"
// Constants.
#define MAX_MZ1E30_DISK_TARGETS 4 // Up to 4 SASI target disks.
enum t_MZ1E30DiskCtrl
{
MZ1E30_HD_NO_FILE = 0, // No file defined as disk image.
MZ1E30_HD_FILE_NOT_LOADED = 1, // File has not been loaded into RAM.
};
// Struct for the MZ-1E30 SASI HDD Interface.
// Note: Disk images are NOT loaded into RAM — sectors are read/written on demand
// via ESP32 SD card IPC (22MB+ per target is too large for 8MB PSRAM).
typedef struct
{
t_SASI sasi; // SASI Bus Controller instance.
char *diskName[MAX_MZ1E30_DISK_TARGETS]; // Filename of each disk on ESP32 SD card.
uint8_t *rom; // ROM image buffer (32KB).
char *romName; // ROM filename on ESP32 SD card.
enum t_MZ1E30DiskCtrl diskctl; // Disk control state.
bool isPhysical; // True if physical SASI (pass-through).
} t_MZ1E30;
// Public prototypes.
uint8_t MZ1E30_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
uint8_t MZ1E30_Reset(t_Z80CPU *cpu);
uint8_t MZ1E30_PollCB(t_Z80CPU *cpu);
uint8_t MZ1E30_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
uint8_t MZ1E30_IO_SASI(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ1E30_IO_ROM(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
#endif // MZ1E30_H

View File

@@ -0,0 +1,124 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: MZ2500.h
// Created: May 2026
// Author(s): Philip Smart
// Description: Z80 CPU DRIVER - Sharp MZ-2500 (Super MZ)
// This file contains setup and drivers to mimic a Sharp MZ-2500 machine internally to
// increase speed through use of internal RAM and provide virtual expansion drivers.
//
// MZ-2500 I/O map (key ports):
// 0x80-0x8F : Graphic palette (16 entries, IGRB + priority)
// 0xB4-0xB5 : Memory mapping registers (8-page MMU)
// 0xBC-0xBF : Graphics controller (G-CRTC, 25 internal registers)
// 0xC6-0xC7 : Interrupt controller (VBLANK, timer, printer, RTC)
// 0xC8-0xC9 : OPN YM2203 (FM+PSG sound, GPIO for system control)
// 0xCC : RTC RP5C15
// 0xCD : SIO baud rate / address select
// 0xCE : Dictionary ROM bank switching
// 0xCF : PCG / Kanji ROM bank switching
// 0xD8-0xDE : MB8876 FDC (3.5" 640KB drives)
// 0xE0-0xE3 : 8255 PPI (cassette/voice, BST/NST)
// 0xE4-0xE7 : 8253 PIT (31.25 kHz clock)
// 0xE8-0xEB : Z80B PIO (keyboard, compat VRAM)
// 0xEF : Joystick (2 Atari-compatible ports)
// 0xF0-0xF3 : 8253 gate pulse
// 0xF4-0xF7 : CRT controller (expanded, compat mode select)
// 0xFE-0xFF : Printer
//
// MZ-2500 memory banking (ports B4h/B5h):
// 8 x 8K pages, each independently mapped to any memory block.
// Boot (BST): Pages 0-3 = IPL ROM, Pages 4-7 = RAM
// Normal (NST): All pages = RAM (software-controlled banking)
//
// Credits:
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// History: May 2026 v1.0 - Initial write based on MZ2000/MZ80B drivers.
//
// 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/>.
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#ifndef MZ2500_H
#define MZ2500_H
#include "drivers/Sharp/MZ.h"
#include "drivers/Sharp/WD1773.h"
// PSRAM bank constants.
#define MZ2500_MEMBANK_MAIN0 0 // Main RAM 0-63K (MB 00h-07h).
#define MZ2500_MEMBANK_MAIN1 1 // Main RAM 64-127K (MB 08h-0Fh).
#define MZ2500_MEMBANK_IPL 2 // IPL ROM 32K (MB 34h-37h).
#define MZ2500_MEMBANK_EXT0 3 // Extended RAM 0-63K (MB 10h-17h, MZ-1R26).
#define MZ2500_MEMBANK_EXT1 4 // Extended RAM 64-127K (MB 18h-1Fh, MZ-1R26).
// MMU constants.
#define MZ2500_MMU_PAGES 8 // 8 x 8K pages in 64K address space.
#define MZ2500_MMU_PAGE_SIZE 0x2000 // 8K per page.
#define MZ2500_MMU_BLOCKS_PER_PAGE 16 // 8192 / 512 = 16 membankPtr entries per MMU page.
// Memory block ranges (as written to port B5h).
#define MZ2500_MB_MAIN_START 0x00 // Main RAM: 00h-0Fh (128K).
#define MZ2500_MB_MAIN_END 0x0F
#define MZ2500_MB_EXT_START 0x10 // Extended RAM: 10h-1Fh (MZ-1R26, 128K).
#define MZ2500_MB_EXT_END 0x1F
#define MZ2500_MB_GVRAM_START 0x20 // Standard GVRAM: 20h-27h (64K, B/R/G/I planes).
#define MZ2500_MB_GVRAM_END 0x27
#define MZ2500_MB_GVRAME_START 0x28 // Extended GVRAM: 28h-2Fh (MZ-1R27, 64K).
#define MZ2500_MB_GVRAME_END 0x2F
#define MZ2500_MB_RMW_START 0x30 // GVRAM Read-Modify-Write: 30h-33h.
#define MZ2500_MB_RMW_END 0x33
#define MZ2500_MB_IPL_START 0x34 // IPL ROM: 34h-37h (32K).
#define MZ2500_MB_IPL_END 0x37
#define MZ2500_MB_TVRAM 0x38 // Text VRAM: 38h (8K).
#define MZ2500_MB_KANJI_PCG 0x39 // Kanji ROM / PCG: 39h (banked via CFh).
#define MZ2500_MB_DICT 0x3A // Dictionary ROM: 3Ah (banked via CEh).
#define MZ2500_MB_COMMROM_START 0x3C // Communication ROM: 3Ch-3Fh.
#define MZ2500_MB_COMMROM_END 0x3F
// Compatibility mode constants (from CRT register 0Fh MOD bits).
#define MZ2500_MODE_NATIVE 0x00 // MZ-2500 native mode.
#define MZ2500_MODE_MZ2000 0x01 // MZ-2000 compatibility mode.
#define MZ2500_MODE_MZ80B 0x02 // MZ-80B compatibility mode.
#define MZ2500_MODE_FIXED 0x03 // Fixed MZ-2500 mode.
// Private prototypes.
void MZ2500_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len);
void MZ2500_readROMData(void *ctx, void *cfg, char *buf, int len);
uint8_t MZ2500_IO_WT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_Debug(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_MMU(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_PPI(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_PIO(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_CRT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_PIT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_IntCtrl(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_IO_OPN(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ2500_PollCB(t_Z80CPU *cpu);
uint8_t MZ2500_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
uint8_t MZ2500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig *config, const char *ifName);
// Driver override callbacks — set in MZ2500_Init on cpu->_Z80.reti / cpu->_Z80.fetch / cpu->_Z80.inta.
uint8_t MZ2500_fetchByte(void *context, uint16_t address);
uint8_t MZ2500_readIntAck(void *context, uint16_t address);
void MZ2500_retiHandler(void *context);
// Debug trace — dump MMU/IO event ring buffer.
void MZ2500_dumpTrace(void);
#endif // MZ2500_H

View File

@@ -0,0 +1,66 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: MZ80B.h
// Created: May 2026
// Author(s): Philip Smart
// Description: Z80 CPU DRIVER - Sharp MZ-80B
// This file contains setup and drivers to mimic a Sharp MZ-80B machine internally to
// increase speed through use of internal RAM and provide virtual expansion drivers.
// Based on the MZ2000 driver but adapted for the MZ-80B memory map and peripherals.
//
// MZ-80B I/O map (I/O port space, not memory-mapped):
// 0xE0-0xE3 : 8255 PPI (cassette, BST/NST memory mode, LEDs, display reverse)
// 0xE4-0xE7 : 8253 PIT (timers, 31.25 kHz clock)
// 0xE8-0xEB : Z80 PIO (keyboard, VRAM paging)
// 0xF4 : Graphic VRAM page select (GRPH I / GRPH II input/output control)
//
// MZ-80B memory map:
// Boot (BST): 0x0000-0x07FF = IPL ROM (2K), 0x8000-0xFFFF = RAM(I) (32K)
// Normal (NST): 0x0000-0x7FFF = RAM(I), 0x8000-0xFFFF = RAM(II) (optional)
// VRAM (PIO A7=1): 0xD000-0xDFFF = Character VRAM, 0xE000-0xFFFF = Graphics VRAM
// VRAM (PIO A6=1): 0x5000-0x5FFF = Character VRAM, 0x6000-0x7FFF = Graphics VRAM
//
// Credits:
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// History: May 2026 v1.0 - Initial write based on MZ2000 driver.
//
// 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/>.
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#ifndef MZ80B_H
#define MZ80B_H
#include "drivers/Sharp/MZ.h"
#include "drivers/Sharp/WD1773.h"
// Constants.
#define MZ80B_MEMBANK_0 0 // Primary RAM bank (IPL ROM + RAM in boot, swapped RAM in normal).
#define MZ80B_MEMBANK_1 1 // Secondary RAM bank (0x8000-0xFFFF in normal mode).
// Private prototypes.
void MZ80B_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len);
void MZ80B_readROMData(void *ctx, void *cfg, char *buf, int len);
uint8_t MZ80B_IO_Debug(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ80B_IO_PIT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ80B_IO_PPI(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ80B_IO_PIO(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t MZ80B_PollCB(t_Z80CPU *cpu);
uint8_t MZ80B_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
uint8_t MZ80B_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig *config, const char *ifName);
#endif // MZ80B_H

View File

@@ -42,7 +42,7 @@
#include "drivers/Sharp/WD1773.h"
// Constants.
#define MAX_MZ8BFI_DISK_DRIVES 4 // MZ-2000 FDC supports up to 4 drives.
#define MAX_MZ8BFI_DISK_DRIVES 2 // 2 virtual drives — 4 would exhaust 8MB PSRAM heap.
enum t_MZ8BFIDiskCtrl
{
@@ -59,6 +59,10 @@ typedef struct
enum t_MZ8BFIDiskCtrl diskctl; // Disk control.
} t_MZ8BFI;
// Machine type for FDC geometry — set by persona driver before calling MZ8BFI_Init.
// Defaults to MZ_2000 if not set. Values from MZ.h: MZ_80B, MZ_2000, MZ_2200, MZ_2500.
extern int MZ8BFI_machineType;
// Public prototypes.
uint8_t MZ8BFI_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
uint8_t MZ8BFI_Reset(t_Z80CPU *cpu);

View File

@@ -0,0 +1,271 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: SASI.h
// Created: Jun 2026
// Author(s): Philip Smart
// Description: SASI Bus Controller Emulation Header
//
// Register-level emulation of a SASI (Shugart Associates System Interface) bus
// controller. Reusable across different host boards (MZ-1E30, etc.).
// Supports up to 4 targets, 256-byte blocks, async SD I/O via queues.
//
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// License: GNU General Public License v3.0
// See LICENSE or <http://www.gnu.org/licenses/>
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#ifndef SASI_H
#define SASI_H
#include <stdint.h>
#include <stdbool.h>
#include "pico/util/queue.h"
/* ========================================================================================
* Constants
* ======================================================================================== */
#define SASI_MAX_TARGETS 4 // Maximum SASI targets (IDs 0-3)
#define SASI_BLOCK_SIZE 256 // MZ-2500 SASI block size (bytes)
#define SASI_CDB_SIZE 6 // CDB6 command descriptor block
#define SASI_STD_BLOCKS 87648 // Standard disk: 87648 blocks = ~21.4MB
#define SASI_STD_DISK_SIZE (SASI_STD_BLOCKS * SASI_BLOCK_SIZE)
#define SASI_ROM_SIZE 32768 // 32KB IPL ROM (A14-A0 = 15 bits)
/* SASI commands (CDB6 format) */
#define SASI_CMD_TEST_UNIT_READY 0x00
#define SASI_CMD_REQUEST_SENSE 0x03
#define SASI_CMD_READ6 0x08
#define SASI_CMD_WRITE6 0x0A
#define SASI_CMD_SEEK6 0x0B
#define SASI_CMD_INQUIRY 0x12
/* Port 0xA5 write — control output bits */
#define SASI_CTRL_SEL 0x20 // Bit 5: Assert -SEL
#define SASI_CTRL_RST 0x08 // Bit 3: Assert -RST
#define SASI_CTRL_DMA 0x02 // Bit 1: DMA enable (MZ-2800 only)
#define SASI_CTRL_INT 0x01 // Bit 0: Interrupt enable (MZ-2800 only)
/* Port 0xA5 read — status bits */
#define SASI_STAT_REQ 0x80 // Bit 7: -REQ active (target requesting transfer)
#define SASI_STAT_ACK 0x40 // Bit 6: -ACK active (host acknowledging)
#define SASI_STAT_BSY 0x20 // Bit 5: -BSY active (target busy)
#define SASI_STAT_MSG 0x10 // Bit 4: -MSG active (message phase)
#define SASI_STAT_CD 0x08 // Bit 3: -C/D active (1=command, 0=data)
#define SASI_STAT_IO 0x04 // Bit 2: -I/O active (1=input/target→host, 0=output/host→target)
#define SASI_STAT_INT_STATUS 0x01 // Bit 0: Interrupt status (MZ-2800)
/* SASI status byte values (returned in STATUS_IN phase) */
#define SASI_STATUS_GOOD 0x00
#define SASI_STATUS_CHECK 0x02 // Check Condition
/* SASI message byte values (returned in MESSAGE_IN phase) */
#define SASI_MSG_COMPLETE 0x00 // Command Complete
/* SASI sense keys (for REQUEST SENSE) */
#define SASI_SENSE_NO_SENSE 0x00
#define SASI_SENSE_NOT_READY 0x04
#define SASI_SENSE_ILLEGAL_REQ 0x05
/* Debug trace ring buffer size */
#define SASI_TRACE_SIZE 256
/* ========================================================================================
* Data Types
* ======================================================================================== */
/**
* @brief SASI bus phases
*
* Bus signal encoding: MSG(4) | C/D(3) | I/O(2)
* BUS_FREE: all deasserted = 0x00
* COMMAND: C/D=1 = 0x08 (but MSG=0, I/O=0 → host→target)
* DATA_OUT: all 0 = 0x00 (host→target data)
* DATA_IN: I/O=1 = 0x04 (target→host data)
* STATUS_IN: C/D=1, I/O=1 = 0x0C
* MESSAGE_IN: MSG=1, C/D=1, I/O=1 = 0x1C
*/
typedef enum
{
SASI_PHASE_BUS_FREE = 0,
SASI_PHASE_SELECTION,
SASI_PHASE_COMMAND,
SASI_PHASE_DATA_IN,
SASI_PHASE_DATA_OUT,
SASI_PHASE_STATUS_IN,
SASI_PHASE_MESSAGE_IN
} t_SASIPhase;
/**
* @brief Per-target state (one per SASI device ID)
*
* Disk images are NOT loaded into PSRAM (22MB+ per target is too large).
* Instead, sectors are read/written on demand via MSG_READ_SECTOR/MSG_WRITE_SECTOR
* through the inter-core queue to ESP32 SD card.
*/
typedef struct
{
char *filename; // Disk image filename on SD card
uint32_t diskSize; // Total disk size in bytes (0 = not configured)
bool ready; // Target is ready (filename configured)
} t_SASITarget;
/**
* @brief Complete SASI controller state
*/
typedef struct
{
// --- Bus State ---
t_SASIPhase phase; // Current bus phase
bool selAsserted; // Host -SEL output state
bool rstAsserted; // Host -RST output state
bool req; // Target -REQ state
bool ack; // Host -ACK state (auto-generated)
bool bsy; // Target -BSY state
uint8_t dataOut; // Last value written to data port (0xA4)
// --- Target Selection ---
int selectedTarget; // Currently selected target ID (-1 = none)
// --- Command ---
uint8_t cdb[SASI_CDB_SIZE]; // Command Descriptor Block
int cdbPos; // Bytes received in current CDB
// --- Data Transfer ---
uint8_t buffer[SASI_BLOCK_SIZE]; // Transfer buffer (one block)
int bufferPos; // Current byte position in buffer
int bufferSize; // Total bytes to transfer in buffer
uint32_t lba; // Current logical block address
int blocksRemaining; // Blocks left to transfer
int totalBlocks; // Total blocks in current command
// --- Status / Message ---
uint8_t statusByte; // Status for STATUS_IN phase
uint8_t messageByte; // Message for MESSAGE_IN phase
uint8_t senseKey; // Last sense key (for REQUEST SENSE)
// --- Targets ---
t_SASITarget target[SASI_MAX_TARGETS]; // Per-target state
// --- ROM ---
uint8_t *romData; // IPL ROM data buffer (up to 32KB)
uint32_t romSize; // Actual ROM size loaded
uint8_t romAddrHigh; // Latched upper address (A14-A8 via port 0xA8)
// --- Async I/O ---
queue_t *requestQueue; // Queue to Core 0
queue_t *responseQueue; // Queue from Core 0
uint32_t nextRequestId; // Next async request ID
// --- Sector Read (on-demand from SD via ESP32) ---
bool readPending; // Sector read in progress (waiting for Core 0)
uint32_t readRequestId; // ID of pending read request
// --- Sector Write-back ---
bool writePending; // Sector write queued to SD card
uint32_t writeRequestId; // ID of pending write request
// --- Debug Trace ---
struct
{
uint32_t entries[SASI_TRACE_SIZE]; // Packed: [phase(4)|rw(1)|port(3)|data(8)|extra(16)]
int head; // Next write position
int count; // Total entries (saturates at SASI_TRACE_SIZE)
} trace;
} t_SASI;
/* ========================================================================================
* Public Function Prototypes
* ======================================================================================== */
/**
* @brief Initialize the SASI controller
* @param sasi Pointer to controller state
* @param requestQueue Queue to send requests to Core 0
* @param responseQueue Queue to receive responses from Core 0
* @return true on success
*/
bool sasiInit(t_SASI *sasi, queue_t *requestQueue, queue_t *responseQueue);
/**
* @brief Reset the SASI bus to BUS_FREE
*/
void sasiReset(t_SASI *sasi);
/**
* @brief Write to SASI data port (0xA4)
*
* Handles: target ID during SELECTION, CDB bytes during COMMAND, data during DATA_OUT.
* Auto-generates ACK pulse.
*/
void sasiWriteData(t_SASI *sasi, uint8_t val);
/**
* @brief Read from SASI data port (0xA4)
*
* Handles: data during DATA_IN, status during STATUS_IN, message during MESSAGE_IN.
* Auto-generates ACK pulse and advances bus phase.
*/
uint8_t sasiReadData(t_SASI *sasi);
/**
* @brief Write to SASI control port (0xA5)
*
* Handles: SEL assert/deassert, RST.
*/
void sasiWriteControl(t_SASI *sasi, uint8_t val);
/**
* @brief Read from SASI status port (0xA5)
*
* Returns: REQ, ACK, BSY, MSG, C/D, I/O bits reflecting current bus phase.
*/
uint8_t sasiReadStatus(t_SASI *sasi);
/**
* @brief Process async disk image load/write completions from Core 0
*/
void sasiProcessResponses(t_SASI *sasi);
/**
* @brief Configure a disk target (filename only — no RAM allocation needed)
* @param sasi Controller state
* @param targetId Target ID (0-3)
* @param filename Disk image filename on SD card
* @param diskSize Disk size in bytes (e.g. SASI_STD_DISK_SIZE)
*/
void sasiSetTarget(t_SASI *sasi, int targetId, const char *filename, uint32_t diskSize);
/**
* @brief Change disk image for a target (runtime, from web GUI)
*/
void sasiChangeDisk(t_SASI *sasi, int targetId, const char *newFilename);
/**
* @brief Write to ROM address latch (port 0xA8)
*
* Data bits 6-0 → ROM A14-A8. Bus address bits A3:A2 must be 0 (decoded externally).
*/
void sasiWriteRomAddr(t_SASI *sasi, uint8_t val);
/**
* @brief Read ROM data (port 0xA9)
*
* Address = (romAddrHigh << 8) | (bus addr A7-A0).
* The lower 8 bits come from the Z80 address bus, passed as 'addrLow'.
*/
uint8_t sasiReadRomData(t_SASI *sasi, uint8_t addrLow);
/**
* @brief Dump SASI trace ring buffer (debug shell)
*/
void sasiTraceDump(void);
/**
* @brief Debug trace enable flag (toggled by debug shell)
*/
extern bool sasiDbgEnabled;
#endif // SASI_H

View File

@@ -69,7 +69,7 @@
#define MAX_SECTOR_SIZE 1024 // Largest sector size supported
#define MAX_TRACK_SIZE 8192 // Max track data (32×256)
#define MAX_DISK_SIZE (80 * 2 * 8 * 512) // MZ-2500 max
#define MAX_DISK_SIZE (80 * 2 * 16 * 256) // MZ-2500 max (640KB, 3.5" 2DD)
#define ROTATION_US 200000 // 300 RPM = 200ms
#define PULSE_WIDTH_US 2000 // Index pulse duration
#define SPIN_UP_US 500000ULL // Motor spin-up time (default, real-speed Z80 with tCycSync)
@@ -163,13 +163,17 @@ typedef struct
// --- Rotation Simulation ---
int addressSector; // Simulated sector rotation for READ ADDRESS
absolute_time_t lastByteTime; // Time of last data register read/DRQ assert
// --- Format Support ---
uint32_t diskBufSize; // Allocated buffer capacity (bytes)
bool diskLoaded; // Image successfully loaded
bool isExtendedDsk; // true if Extended CPC .DSK
uint32_t *trackOffsets; // Array of track start offsets
uint32_t *trackSizes; // Array of track data sizes
int totalTracks; // cylinders * heads
bool isD88; // true if D88 native format (not converted to raw)
uint32_t *trackOffsets; // Array of track start offsets (ExtDSK and D88)
uint32_t *trackSizes; // Array of track data sizes (ExtDSK only)
int totalTracks; // Total D88 track offset entries
int *d88PhysMap; // D88: maps physical (cyl*heads+head) → D88 track index (-1=empty)
} t_WD1773;
/* ========================================================================================

View File

@@ -57,6 +57,13 @@
#define IPCF_CMD_NET_RECV 0x13 // Receive data from socket (response payload = data)
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
// Network file server commands (BASIC NET: device → ESP32 → PC file server)
#define IPCF_CMD_NETFS_DIR 0x20 // Get directory from file server (response: 32-byte entries)
#define IPCF_CMD_NETFS_INFO 0x21 // Get file info (filename in header, response: atrb+size+addrs)
#define IPCF_CMD_NETFS_READ 0x22 // Read file data (filename+offset in header, response: data)
#define IPCF_CMD_NETFS_WRITE 0x23 // Write file (filename+size in header, payload: data)
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
// ---------------------------------------------------------------------------
// Status codes (t_IpcFrameHdr::status — response frames only)
// ---------------------------------------------------------------------------

View File

@@ -17,6 +17,8 @@ set(APP_COMMON_DEFINES
${model_common_defines}
USE_STDIO
INCLUDE_SHARP_DRIVERS
MZ80B_DEBUG
MZ2500_DEBUG
MZ2000_DEBUG
DEBUG
Z80_WITH_RESET_SIGNAL

View File

@@ -778,6 +778,37 @@ void processSPICommands(const char *cmd)
Z80CPU_ForceHostResetActive(z80CPU, false);
}
// IPL Reset (BST via 8255 PPI PC3), IPLR
if (strcmp(cmd, "IPLR") == 0)
{
debugf("IPL Reset (web GUI)\r\n");
// Hold CPU.
z80CPU->hold = true;
int holdTimeout = 3000;
while (!z80CPU->holdAck && holdTimeout > 0) { sleep_ms(1); holdTimeout--; }
if (!z80CPU->holdAck)
{
debugf("IPL Reset: CPU hold timeout\r\n");
z80CPU->hold = false;
}
else
{
// Trigger BST via 8255 PPI Port C bit 3 (PC3).
Z80CPU_writePhysicalIO(z80CPU, 0xE3, 0x07); // Set PC3 HIGH
sleep_ms(1);
Z80CPU_writePhysicalIO(z80CPU, 0xE3, 0x06); // Clear PC3 LOW → BST falling edge
sleep_ms(10);
// Force Z80 reset and release.
z80CPU->forceReset = true;
__dmb();
z80CPU->holdAck = false;
__dmb();
z80CPU->hold = false;
}
}
// Active partition change. Parameter = 1 or 2.
if (strncmp(cmd, "APRT,", 5) == 0)
{
@@ -879,7 +910,11 @@ void processSPICommands(const char *cmd)
watchdog_update();
}
if (!z80CPU->holdAck)
debugf("CFG: Warning — CPU hold ack timeout, proceeding anyway\r\n");
{
debugf("CFG: Warning — CPU hold ack timeout, force-resetting Core 1\r\n");
multicore_reset_core1();
sleep_ms(10);
}
else
debugf("CFG: CPU held OK\r\n");
}
@@ -891,9 +926,19 @@ void processSPICommands(const char *cmd)
watchdog_update();
debugf("CFG: Processing JSON config...\r\n");
if (!processJSONConfig(cfgApp))
{
debugf("CFG: Error — Processing JSON config failed\r\n");
}
else
{
debugf("CFG: JSON config processed OK\r\n");
// Update the partition table to point to the newly saved config.
// Without this, the next reboot loads from the OLD activeApp partition.
if (changeActivePartition(cfgApp) == 0)
debugf("CFG: Active partition set to %d\r\n", (int) cfgApp);
else
debugf("CFG: Error — failed to set active partition %d\r\n", (int) cfgApp);
}
watchdog_update();
// Flush out all debug messages prior to reboot.
@@ -986,7 +1031,8 @@ void processInterCoreCommands(void)
watchdog_hw->scratch[BOOTP_SCR_SPIDIAG] = (watchdog_hw->scratch[BOOTP_SCR_SPIDIAG] & 0xF0FFFFFF) |
(((uint32_t) qmsg.type & 0x0F) << 24);
plogf("IC:%d ", (int) qmsg.type);
debugf("IC_REQ: type=%d ctx=%p\r\n", (int) qmsg.type, qmsg.context);
if (qmsg.type != MSG_READ_SECTOR && qmsg.type != MSG_WRITE_SECTOR)
debugf("IC_REQ: type=%d ctx=%p\r\n", (int) qmsg.type, qmsg.context);
t_CoreMsg response = {.context = qmsg.context, .requestId = qmsg.requestId, .response.success = false, .response.size = 0};
bool skipResponse = false; // Set true for fire-and-forget operations.
@@ -1010,7 +1056,7 @@ void processInterCoreCommands(void)
watchdog_update();
qmsg.fileOp.buffer = bufBefore; // Reset in case realloc changed it on a failed partial read.
plogf("a%d>", attempt);
bytesXfer = ESP_readFloppyDiskFile(qmsg.fileOp.filename, NULL, NULL, NULL, (uint8_t **)&qmsg.fileOp.buffer, qmsg.fileOp.size, true, 0, qmsg.fileOp.diskNo);
bytesXfer = ESP_readFloppyDiskFile(qmsg.fileOp.filename, NULL, NULL, NULL, (uint8_t **)&qmsg.fileOp.buffer, qmsg.fileOp.size, false, 0, qmsg.fileOp.diskNo);
plogf("r%lu>", bytesXfer);
if (bytesXfer > 0)
break;
@@ -1097,7 +1143,7 @@ void processInterCoreCommands(void)
{
responseType = MSG_LOAD_COMPLETE;
uint8_t *bufBefore = qmsg.fileOp.buffer;
for (int attempt = 0; attempt < 10; attempt++)
for (int attempt = 0; attempt < 3; attempt++)
{
qmsg.fileOp.buffer = bufBefore;
bytesXfer = ESP_readRamFile(qmsg.fileOp.filename, NULL, NULL, NULL, (uint8_t **)&qmsg.fileOp.buffer, qmsg.fileOp.size, true, 0);
@@ -1111,6 +1157,16 @@ void processInterCoreCommands(void)
success = true;
response.response.size = bytesXfer;
}
else
{
// File doesn't exist — treat as fresh uninitialised RAM board.
// Buffer is already pre-filled by the driver (0xFF for Celestite
// MZ-1R12, matching empty battery-backed SRAM). The backing file
// will be created on first save from the Z80.
debugf("RAMFILE: '%s' not found, using pre-filled buffer (%d bytes)\r\n", qmsg.fileOp.filename, (int) qmsg.fileOp.size);
success = true;
response.response.size = qmsg.fileOp.size;
}
}
else
{

View File

@@ -1 +1 @@
2.534
2.74

Binary file not shown.

View File

@@ -0,0 +1,67 @@
A2DSP: equ 14A7H
ATBL: equ 1568H
ATRB: equ 10F0H
CLR: equ 122EH
COMNT: equ 1108H
DBGCR: equ 1563H
DBGLP: equ 155BH
DLY1: equ 143FH
DLY2: equ 1442H
DTADR: equ 1104H
EXADR: equ 1106H
HTCHK: equ 1405H
HTDISP: equ 13BFH
HTEND: equ 140DH
HTNL: equ 13D4H
HTNL2: equ 13E1H
HTNL3: equ 13E6H
HTNL4: equ 13EFH
HTNL5: equ 13F8H
HTNL6: equ 13FEH
HTNXT: equ 1405H
HTTPDN: equ 1419H
HTTPGOT: equ 13A0H
HTTPRD: equ 138DH
HTTPREQ: equ 1548H
HTTPRW: equ 1376H
HTTPWD: equ 1354H
HTTPWR: equ 134BH
LOOPCNT: equ 1566H
MAINLP: equ 1228H
MEND: equ 15E8H
NAME: equ 10F1H
PDECV: equ 145BH
PH1: equ 14CBH
PH2: equ 14D8H
PHEX: equ 14BCH
PHEXV: equ 148CH
PHXN: equ 1499H
PHXN1: equ 14A2H
PNGD: equ 1274H
PNGDN: equ 1292H
PNGGOT: equ 1286H
PNGW: equ 1269H
PSTR: equ 14B4H
PVD1: equ 147CH
PVD10: equ 1467H
PVD100: equ 145EH
PVD10L: equ 1473H
PVD10N: equ 1470H
PVD1N: equ 1485H
PVRAM: equ 144CH
SIZE: equ 1102H
START: equ 1200H
S_BYT: equ 1546H
S_FAIL: equ 1539H
S_HTTP: equ 1523H
S_LOOP: equ 14F5H
S_MS: equ 1543H
S_PASS: equ 1534H
S_PING: equ 14FBH
S_TCP: equ 150CH
S_TITLE: equ 14DBH
S_TOUT: equ 153EH
TCPD: equ 130BH
TCPFL: equ 1312H
TCPOK: equ 131EH
TCPW: equ 12F3H

Binary file not shown.

View File

@@ -0,0 +1,93 @@
A2DSP: equ 170DH
ATBL: equ 1A51H
ATRB: equ 10F0H
BNRMSG: equ 172DH
BYTEMSG: equ 19C0H
COMNT: equ 1108H
CRLF: equ 19F6H
DONEMSG: equ 1A08H
DTADR: equ 1104H
EXADR: equ 1106H
FAILMSG: equ 18D0H
HTTPREQ: equ 1A27H
MEND: equ 1AD1H
MSMSG: equ 1A03H
NAME: equ 10F1H
NETMSG: equ 18D7H
OKMSG: equ 19C9H
PASSMSG: equ 18C9H
PAUSMSG: equ 19D7H
PBANNER: equ 1726H
PD10: equ 16D0H
PD100: equ 16B9H
PD10N: equ 16CDH
PD10S: equ 16C2H
PD1N: equ 16E7H
PD1P: equ 16E2H
PD1S: equ 16D9H
PDEC: equ 16B2H
PHEX: equ 16EEH
PHX1: equ 16FDH
PHX2: equ 170AH
PMSG: equ 171DH
PSTR: equ 16AAH
RESULT: equ 169AH
RPASS: equ 16A3H
RXGMSG: equ 19B6H
RXWMSG: equ 199EH
SCRHDR: equ 1A3AH
SIZE: equ 1102H
START: equ 1200H
T10MSG: equ 1875H
T11MSG: equ 1891H
T12MSG: equ 18ADH
T13DLY: equ 1407H
T13DONE: equ 141AH
T13GOT: equ 1416H
T13MSG: equ 191BH
T13WAIT: equ 13F6H
T14MSG: equ 1937H
T15DLY: equ 1463H
T15DONE: equ 147DH
T15GOT: equ 1472H
T15MSG: equ 1948H
T15WAIT: equ 1458H
T16DLY: equ 14ECH
T16DONE: equ 150BH
T16FAIL: equ 14F3H
T16MSG: equ 1964H
T16OK: equ 14FBH
T16WAIT: equ 14D5H
T17CD: equ 1581H
T17CLR: equ 161BH
T17CONN: equ 158BH
T17CW: equ 1569H
T17DISP: equ 162FH
T17DONE: equ 1685H
T17END: equ 165FH
T17FAIL: equ 167FH
T17GOT: equ 15EDH
T17HALT: equ 167DH
T17LIM: equ 1612H
T17LOK: equ 1614H
T17MSG: equ 1980H
T17NL: equ 1640H
T17NXT: equ 1657H
T17RD: equ 15DDH
T17RW: equ 15C6H
T17WR: equ 1595H
T17WRD: equ 159EH
T1MSG: equ 1779H
T2MSG: equ 1795H
T3DONE: equ 128EH
T3FAIL: equ 1288H
T3MSG: equ 17B1H
T4DONE: equ 12D6H
T4FAIL: equ 12D0H
T4MSG: equ 17CDH
T5MSG: equ 17E9H
T6MSG: equ 1805H
T7MSG: equ 1821H
T8MSG: equ 183DH
T9MSG: equ 1859H
TOMSG: equ 19F9H

View File

@@ -106,16 +106,21 @@ public start_addr:
.wrap
; State machine to output an 8bit byte onto the Z80 Data Bus.
; The state machine sets IRQ 1 to indicate it is waiting, waits for the flag
; to clear then loads the 8bit data byte out of the fifo onto the pins.
; After driving data, waits for /WR to go active (low) then inactive (high),
; indicating the write cycle has completed, then tristates the data bus.
; This prevents stale data from persisting on the physical bus in virtual mode,
; where no subsequent physical bus cycle (M1 fetch / refresh) would otherwise
; clear it. Works with any number of wait states since it tracks the actual
; /WR signal rather than using a fixed timeout.
.program z80_data
.wrap_target
public start_data:
irq set 1
wait 0 irq 1 ; Wait till control starts the sequence.
out pindirs, 8 ; Set direction to output (or input during tri-state BUSRQ).
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7)
wait 0 irq 0 ; Wait till next address change.
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7).
wait 0 gpio Z80_PIN_WR ; Wait for /WR active (low) — write cycle in progress.
wait 1 gpio Z80_PIN_WR ; Wait for /WR inactive (high) — write cycle complete.
out pindirs, 8 ; Set final direction, normally input mode.
.wrap

354
projects/tzpuPico/tools/NetFileServer/netfs.py vendored Executable file
View File

@@ -0,0 +1,354 @@
#!/usr/bin/env python3
"""
NetFS — Binary File Server for Sharp MZ BASIC Network Drive
Serves MZF files from a local directory over TCP using a simple binary protocol
designed for easy Z80 assembly parsing.
Binary Protocol:
All requests start with 1-byte command. All responses start with 1-byte status.
Status: 0x00=OK, 0xFF=error, 0xFE=end of data.
DIR (0x01):
Request: [0x01] [unit:1]
Response: [0x00] [count:1] [entry0:32] ... [entryN:32]
Each entry is 32 bytes: atrb(1) + name(17) + size(2LE) + dtadr(2LE) + exadr(2LE) + pad(8)
Returns up to 63 entries from the directory mapped to unit (1-7). Max 63 (2KB RX buffer).
INFO (0x02):
Request: [0x02] [filename:17]
Response: [0x00] [entry:32] (or [0xFF] if not found)
READ (0x03):
Request: [0x03] [unit:1] [filename:17] [offset:2LE] [length:2LE]
Response: [0x00] [actual_len:2LE] [data:actual_len] (or [0xFF] if not found)
WRITE (0x04):
Request: [0x04] [unit:1] [filename:17] [atrb:1] [size:2LE] [dtadr:2LE] [exadr:2LE] [data:size]
Response: [0x00] (or [0xFF] on error)
CLOSE (0x05):
Request: [0x05]
Response: [0x00] (server closes connection)
DELETE (0x06):
Request: [0x06] [filename:17]
Response: [0x00] (or [0xFF] if not found)
All commands include a unit byte (1-7) selecting the server directory.
Usage:
python3 netfs.py [--port 6800] [--dir ./mzf_files] [--dir2 path] ... [--dir7 path]
--dir maps to NET1: (or NET: with no digit), --dir2 maps to NET2:, etc.
Units without a --dirN argument fall back to --dir.
(c) 2026 Philip Smart <philip.smart@net2net.org>
"""
import argparse
import os
import socket
import struct
import threading
import sys
DEFAULT_PORT = 6800
DEFAULT_DIR = "."
MZF_HEADER_SIZE = 128
# Protocol constants
CMD_DIR = 0x01
CMD_INFO = 0x02
CMD_READ = 0x03
CMD_WRITE = 0x04
CMD_CLOSE = 0x05
CMD_DELETE = 0x06
STATUS_OK = 0x00
STATUS_ERR = 0xFF
STATUS_END = 0xFE
def parse_mzf_header(data):
"""Parse a 128-byte MZF header."""
if len(data) < MZF_HEADER_SIZE:
return None
atrb = data[0]
# Remap RB/BSD/BRD types to BTX (type 2) so BASIC can LOAD them
if atrb in (0x03, 0x04, 0x05):
atrb = 0x02
name_raw = data[1:18]
name = name_raw.rstrip(b'\x00\x0d').decode('ascii', errors='replace')
size = struct.unpack('<H', data[18:20])[0]
dtadr = struct.unpack('<H', data[20:22])[0]
exadr = struct.unpack('<H', data[22:24])[0]
return {
'atrb': atrb, 'name': name, 'name_raw': name_raw,
'size': size, 'dtadr': dtadr, 'exadr': exadr
}
def make_dir_entry(atrb, name_raw, size, dtadr, exadr):
"""Create a 32-byte binary directory entry (standard MZF format).
Z80 handler remaps fields as needed for the target BASIC."""
entry = bytearray(32)
entry[0] = atrb & 0xFF
entry[1:18] = name_raw[:17].ljust(17, b'\x00')
struct.pack_into('<H', entry, 18, size & 0xFFFF)
struct.pack_into('<H', entry, 20, dtadr & 0xFFFF)
struct.pack_into('<H', entry, 22, exadr & 0xFFFF)
return bytes(entry)
def scan_directory(dirpath):
"""Scan directory for MZF files."""
entries = []
for fname in sorted(os.listdir(dirpath)):
fpath = os.path.join(dirpath, fname)
if not os.path.isfile(fpath):
continue
ext = os.path.splitext(fname)[1].lower()
if ext in ('.mzf', '.mzt', '.m12'):
try:
with open(fpath, 'rb') as f:
hdr_data = f.read(MZF_HEADER_SIZE)
hdr = parse_mzf_header(hdr_data)
if hdr:
hdr['filepath'] = fpath
hdr['data_offset'] = MZF_HEADER_SIZE
entries.append(hdr)
except Exception as e:
print(f" Warning: {fname}: {e}")
elif ext in ('.bin', '.rom', '.dat'):
fsize = os.path.getsize(fpath)
name_clean = os.path.splitext(fname)[0][:17]
name_raw = name_clean.encode('ascii', errors='replace').ljust(17, b'\x00')
entries.append({
'atrb': 0x01, 'name': name_clean, 'name_raw': name_raw,
'size': fsize & 0xFFFF, 'dtadr': 0x1200, 'exadr': 0x1200,
'filepath': fpath, 'data_offset': 0
})
return entries
def find_file(entries, name_bytes):
"""Find a file by 17-byte name field (binary match)."""
# Strip trailing nulls/CRs for comparison
search = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip().upper()
for ent in entries:
if ent['name'].strip().upper() == search:
return ent
return None
def handle_client(conn, addr, dirs):
"""Handle a binary protocol client connection.
dirs: dict mapping unit number (1-7) to directory path."""
conn.settimeout(30) # 30s timeout — recovers if host crashes mid-transfer.
print(f"[{addr}] Connected")
try:
while True:
# Read 1-byte command
cmd_data = conn.recv(1)
if not cmd_data:
break
cmd = cmd_data[0]
if cmd == CMD_DIR:
# Read 1-byte unit number.
unit_data = conn.recv(1)
if not unit_data:
break
unit = unit_data[0]
dp = dirs.get(unit)
entries = scan_directory(dp) if dp else []
count = min(len(entries), 63)
resp = bytearray()
resp.append(STATUS_OK)
resp.append(count)
for i in range(count):
ent = entries[i]
resp.extend(make_dir_entry(ent['atrb'], ent['name_raw'], ent['size'], ent['dtadr'], ent['exadr']))
conn.sendall(bytes(resp))
print(f"[{addr}] DIR unit={unit} ({dp or 'not configured'}) → {count}/{len(entries)} entries ({len(resp)} bytes)")
elif cmd == CMD_INFO:
# Read 17-byte filename
name_bytes = conn.recv(17)
if len(name_bytes) < 17:
conn.sendall(bytes([STATUS_ERR]))
continue
# INFO searches unit 1 directory (no unit byte in protocol).
dp = dirs.get(1)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes)
if found:
entry = make_dir_entry(found['atrb'], found['name_raw'], found['size'], found['dtadr'], found['exadr'])
conn.sendall(bytes([STATUS_OK]) + entry)
print(f"[{addr}] INFO '{found['name']}' → size={found['size']}")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] INFO '{name_str}' → NOT FOUND")
elif cmd == CMD_READ:
# [unit:1][name:17][offset:2][length:2] = 22 bytes.
params = conn.recv(22)
if len(params) < 22:
conn.sendall(bytes([STATUS_ERR]))
continue
unit = params[0]
name_bytes = params[1:18]
offset = struct.unpack('<H', params[18:20])[0]
length = struct.unpack('<H', params[20:22])[0]
dp = dirs.get(unit)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes) if entries else None
if found:
with open(found['filepath'], 'rb') as f:
f.seek(found['data_offset'] + offset)
max_read = found['size'] - offset if offset < found['size'] else 0
data = f.read(min(length, max_read))
actual_len = len(data)
resp = bytearray()
resp.append(STATUS_OK)
resp.extend(struct.pack('<H', actual_len))
resp.extend(data)
conn.sendall(bytes(resp))
print(f"[{addr}] READ unit={unit} '{found['name']}' ofs={offset} len={length}{actual_len} bytes")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] READ unit={unit} '{name_str}' → NOT FOUND")
elif cmd == CMD_WRITE:
# [unit:1][name:17][atrb:1][size:2][dtadr:2][exadr:2] = 25 bytes.
params = conn.recv(25)
if len(params) < 25:
conn.sendall(bytes([STATUS_ERR]))
continue
unit = params[0]
name_bytes = params[1:18]
atrb = params[18]
size = struct.unpack('<H', params[19:21])[0]
dtadr = struct.unpack('<H', params[21:23])[0]
exadr = struct.unpack('<H', params[23:25])[0]
dp = dirs.get(unit)
if not dp:
# Drain the data bytes then return error.
remaining = size
while remaining > 0:
chunk = conn.recv(min(remaining, 4096))
if not chunk:
break
remaining -= len(chunk)
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
print(f"[{addr}] WRITE unit={unit} '{name_str}' → unit not configured")
continue
# Read file data
data = b''
remaining = size
while remaining > 0:
chunk = conn.recv(min(remaining, 4096))
if not chunk:
break
data += chunk
remaining -= len(chunk)
# Build MZF file
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
fname = name_str.replace(' ', '_') + '.mzf'
fpath = os.path.join(dp, fname)
hdr = bytearray(MZF_HEADER_SIZE)
hdr[0] = atrb
hdr[1:18] = name_bytes[:17]
struct.pack_into('<H', hdr, 18, size)
struct.pack_into('<H', hdr, 20, dtadr)
struct.pack_into('<H', hdr, 22, exadr)
with open(fpath, 'wb') as f:
f.write(bytes(hdr))
f.write(data)
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] WRITE unit={unit} '{name_str}' atrb={atrb:02X} size={size}{fpath}")
elif cmd == CMD_DELETE:
# Read 17-byte filename (no unit byte — uses unit 1 dir).
name_bytes = conn.recv(17)
if len(name_bytes) < 17:
conn.sendall(bytes([STATUS_ERR]))
continue
dp = dirs.get(1)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes)
if found:
try:
os.remove(found['filepath'])
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] DELETE '{found['name']}'{found['filepath']}")
except Exception as e:
conn.sendall(bytes([STATUS_ERR]))
print(f"[{addr}] DELETE '{found['name']}' FAILED: {e}")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] DELETE '{name_str}' → NOT FOUND")
elif cmd == CMD_CLOSE:
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] CLOSE")
break
else:
conn.sendall(bytes([STATUS_ERR]))
print(f"[{addr}] Unknown cmd: 0x{cmd:02X}")
except (ConnectionResetError, BrokenPipeError):
pass
except socket.timeout:
print(f"[{addr}] Timeout (host crash recovery)")
except Exception as e:
print(f"[{addr}] Error: {e}")
finally:
print(f"[{addr}] Disconnected")
conn.close()
def main():
parser = argparse.ArgumentParser(description="NetFS — Binary File Server for Sharp MZ BASIC")
parser.add_argument('--port', '-p', type=int, default=DEFAULT_PORT, help=f"TCP port (default: {DEFAULT_PORT})")
parser.add_argument('--dir', '-d', type=str, default=DEFAULT_DIR, help="Directory for NET1:/NET: (default: .)")
for i in range(2, 8):
parser.add_argument(f'--dir{i}', type=str, default=None, help=f"Directory for NET{i}:")
args = parser.parse_args()
# Build unit → directory mapping.
dirs = {1: os.path.abspath(args.dir)}
for i in range(2, 8):
d = getattr(args, f'dir{i}', None)
if d:
dirs[i] = os.path.abspath(d)
# Validate all directories exist.
for unit, path in dirs.items():
if not os.path.isdir(path):
print(f"Error: directory '{path}' (NET{unit}:) does not exist")
sys.exit(1)
print(f"NetFS v4.0 — Sharp MZ BASIC Network File Server")
for unit in sorted(dirs):
entries = scan_directory(dirs[unit])
print(f" NET{unit}: {dirs[unit]} ({len(entries)} files)")
print(f"Listening on port {args.port}")
print()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', args.port))
sock.listen(5)
try:
while True:
conn, addr = sock.accept()
t = threading.Thread(target=handle_client, args=(conn, addr, dirs), daemon=True)
t.start()
except KeyboardInterrupt:
print("\nShutting down...")
finally:
sock.close()
if __name__ == '__main__':
main()

View File

@@ -1 +1 @@
2.518
2.723