4 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
51 changed files with 5636 additions and 438 deletions

View File

@@ -1 +1 @@
2.58
2.66

View File

@@ -3848,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;cursor:pointer;\" data-sort-col=\"0\" 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=\"1\" 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=\"2\" 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=\"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>");
@@ -3870,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>");
@@ -3891,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>"
@@ -3900,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>");
@@ -3915,7 +3919,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>"
@@ -3962,6 +3966,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
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(de.name).append("\">");
@@ -4163,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");

View File

@@ -1 +1 @@
2.74
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>
@@ -139,6 +140,9 @@
<div class="panel panel-primary">
<div class="panel-heading">
<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>

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.
@@ -604,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.
@@ -753,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).
@@ -817,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 + '">';
@@ -826,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;
}
@@ -1129,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>';
@@ -1166,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);
});
@@ -1218,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
@@ -1230,9 +1247,11 @@ 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.
@@ -1241,8 +1260,10 @@ var ConfigGUI = (function($) {
$tbody.append(renderDrvRomRow({ enable: 0, file: '' }));
});
$container.on('click', '[data-action="remove-drv-rom"]', function() {
if (confirm('Remove this system ROM entry?'))
$(this).closest('tr').remove();
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"]');
@@ -1252,9 +1273,11 @@ var ConfigGUI = (function($) {
// 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.
@@ -1275,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.
@@ -1318,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
@@ -1367,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.
@@ -1862,43 +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);
// 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);
}
});
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

@@ -2,17 +2,16 @@ var lastStatus = 0;
// Backup the entire SD card as a tar download.
function backupSD() {
if (!confirm('Download the entire SD card as a tar archive?\n\nThis may take several minutes depending on the SD card size and connection speed.'))
return;
// 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);
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.
@@ -109,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;
@@ -186,13 +185,13 @@ function uploadFile() {
{
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();
}
}
@@ -238,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.");
});
}
@@ -278,10 +277,120 @@ 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.

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.58
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
@@ -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]);
@@ -845,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;
}
@@ -2172,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));
@@ -2582,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)
@@ -2771,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);
@@ -3171,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);
}
}
@@ -3307,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.
@@ -3319,15 +3331,16 @@ uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
switch (cpu->_Z80.im)
{
// Interrupt Mode 0
// 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:
debugf("INT IM0 not implemented.\r\n");
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
// Interrupt Mode 2 - Fetch the address to branch to from the bus.
case 2:
// Get the vector from the bus, used with I register to form the address of the ISR.
//debugf("ReadInt:%04x, mode:%02x\r\n", address, cpu->_Z80.im);
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
@@ -3337,21 +3350,21 @@ uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
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)
@@ -3365,9 +3378,12 @@ 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)
@@ -3378,15 +3394,16 @@ void __func_in_RAM(Z80CPU_ldra)(void *context)
}
// 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.
@@ -3403,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");
}
}
@@ -2241,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)
{
@@ -2257,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)
@@ -2310,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
@@ -3994,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},
@@ -4012,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},
@@ -4024,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

@@ -319,7 +319,11 @@ uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
celestiteCtrl->mz1r37Ram = NULL;
celestiteCtrl->mz1r37AddrLatch = 0;
celestiteCtrl->mz1r37FileName = NULL;
celestiteCtrl->mz1r37WritePending = false;
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;
@@ -581,14 +585,73 @@ 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.
// Clear write-pending flags periodically (~6s at 3.5MHz) so the next batch
// of writes can queue a new scheduled write. The coalesce timeouts on Core 0
// (2s for R12, 5s for R37) will have completed by then.
// 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;
celestiteCtrl->mz1r37WritePending = 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.
@@ -1244,20 +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, one message per window).
if (celestiteCtrl->mz1r37FileName && celestiteCtrl->requestQueue && !celestiteCtrl->mz1r37WritePending)
// 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;
if (queue_try_add(celestiteCtrl->requestQueue, &msg))
celestiteCtrl->mz1r37WritePending = true;
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

@@ -1298,6 +1298,11 @@ 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;

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>
@@ -408,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.
@@ -416,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.
@@ -446,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

@@ -215,7 +215,13 @@ 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 mz1r37WritePending; // True while a scheduled write is queued/in-flight.
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").

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

@@ -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

@@ -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;

View File

@@ -1 +1 @@
2.579
2.74

View File

@@ -1 +1 @@
2.566
2.723