Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c5201534 | ||
|
|
5a14318018 | ||
|
|
f5670c66d0 | ||
|
|
756d54a22c |
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
@@ -1 +1 @@
|
||||
2.58
|
||||
2.66
|
||||
|
||||
@@ -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");
|
||||
|
||||
2
projects/tzpuPico/esp32/version.txt
vendored
2
projects/tzpuPico/esp32/version.txt
vendored
@@ -1 +1 @@
|
||||
2.74
|
||||
2.83
|
||||
|
||||
1
projects/tzpuPico/esp32/webserver/config.htm
vendored
1
projects/tzpuPico/esp32/webserver/config.htm
vendored
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
1
projects/tzpuPico/esp32/webserver/index.htm
vendored
1
projects/tzpuPico/esp32/webserver/index.htm
vendored
@@ -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>
|
||||
|
||||
26
projects/tzpuPico/esp32/webserver/js/actions.js
vendored
26
projects/tzpuPico/esp32/webserver/js/actions.js
vendored
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
52
projects/tzpuPico/esp32/webserver/js/common.js
vendored
52
projects/tzpuPico/esp32/webserver/js/common.js
vendored
@@ -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();
|
||||
|
||||
154
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
154
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
@@ -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();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
12
projects/tzpuPico/esp32/webserver/js/editor.js
vendored
12
projects/tzpuPico/esp32/webserver/js/editor.js
vendored
@@ -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
|
||||
|
||||
149
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
149
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.58
|
||||
2.66
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
projects/tzpuPico/src/CMakeLists.txt
vendored
4
projects/tzpuPico/src/CMakeLists.txt
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
265
projects/tzpuPico/src/drivers/Sharp/MZ1E30.c
Normal file
265
projects/tzpuPico/src/drivers/Sharp/MZ1E30.c
Normal 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);
|
||||
}
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
1247
projects/tzpuPico/src/drivers/Sharp/MZ2500.c
Normal file
1247
projects/tzpuPico/src/drivers/Sharp/MZ2500.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
1235
projects/tzpuPico/src/drivers/Sharp/MZ80B.c
Normal file
1235
projects/tzpuPico/src/drivers/Sharp/MZ80B.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
728
projects/tzpuPico/src/drivers/Sharp/SASI.c
Normal file
728
projects/tzpuPico/src/drivers/Sharp/SASI.c
Normal 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;
|
||||
}
|
||||
@@ -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, §ors, §orSize);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
33
projects/tzpuPico/src/include/Z80CPU.h
vendored
33
projects/tzpuPico/src/include/Z80CPU.h
vendored
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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.
|
||||
|
||||
57
projects/tzpuPico/src/include/drivers/Sharp/MZ1E30.h
vendored
Normal file
57
projects/tzpuPico/src/include/drivers/Sharp/MZ1E30.h
vendored
Normal 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
|
||||
124
projects/tzpuPico/src/include/drivers/Sharp/MZ2500.h
vendored
Normal file
124
projects/tzpuPico/src/include/drivers/Sharp/MZ2500.h
vendored
Normal 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
|
||||
66
projects/tzpuPico/src/include/drivers/Sharp/MZ80B.h
vendored
Normal file
66
projects/tzpuPico/src/include/drivers/Sharp/MZ80B.h
vendored
Normal 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
|
||||
@@ -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);
|
||||
|
||||
271
projects/tzpuPico/src/include/drivers/Sharp/SASI.h
vendored
Normal file
271
projects/tzpuPico/src/include/drivers/Sharp/SASI.h
vendored
Normal 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
|
||||
@@ -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;
|
||||
|
||||
/* ========================================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.579
|
||||
2.74
|
||||
|
||||
2
projects/tzpuPico/version.txt
vendored
2
projects/tzpuPico/version.txt
vendored
@@ -1 +1 @@
|
||||
2.566
|
||||
2.723
|
||||
|
||||
Reference in New Issue
Block a user