Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c5201534 | ||
|
|
5a14318018 | ||
|
|
f5670c66d0 | ||
|
|
756d54a22c | ||
|
|
6758c2892c | ||
|
|
cc040f7f49 | ||
|
|
4196e58420 |
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
@@ -1 +1 @@
|
||||
2.43
|
||||
2.66
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
#include "CommandProcessor.h"
|
||||
#include "ipc_protocol.h"
|
||||
#include "esp_attr.h" // RTC_NOINIT_ATTR
|
||||
#include "esp_timer.h"
|
||||
#include "esp_rom_crc.h" // esp_rom_crc32_le for network payload CRC
|
||||
|
||||
// Declared in IO.cpp — set before esp_restart() in the OOB handler so we know
|
||||
// this SW reset was clean (no mid-transaction state) and can use the fast path.
|
||||
@@ -414,6 +416,404 @@ void CommandProcessor::cmdNop(const t_IpcFrameHdr &frame)
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Network command handlers (Celestite W5100 emulation).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void CommandProcessor::cmdNetCfg(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
// Return ESP32's WiFi STA network configuration.
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
// Get the default STA interface.
|
||||
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
if (!netif)
|
||||
{
|
||||
hdr.status = IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t ipInfo;
|
||||
esp_netif_get_ip_info(netif, &ipInfo);
|
||||
uint8_t mac[6];
|
||||
esp_netif_get_mac(netif, mac);
|
||||
|
||||
// Pack: IP[4] + GW[4] + Subnet[4] + MAC[6] = 18 bytes.
|
||||
uint8_t payload[18];
|
||||
memcpy(payload, &ipInfo.ip.addr, 4);
|
||||
memcpy(payload + 4, &ipInfo.gw.addr, 4);
|
||||
memcpy(payload + 8, &ipInfo.netmask.addr, 4);
|
||||
memcpy(payload + 12, mac, 6);
|
||||
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = 18;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + 18 + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, payload, 18, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void CommandProcessor::cmdNetSocket(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
uint8_t sockNum = frame.diskNo;
|
||||
uint8_t op = (uint8_t)(frame.sectorCount & 0xFF);
|
||||
uint8_t protocol = (uint8_t)frame.filename[2];
|
||||
uint16_t port = ((uint8_t)frame.filename[0] << 8) | (uint8_t)frame.filename[1];
|
||||
uint32_t ipAddr = frame.fileOffset;
|
||||
|
||||
if (sockNum >= 4)
|
||||
{
|
||||
hdr.status = IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t resultStatus = 0x00; // SOCK_CLOSED by default.
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case 0x01: // OPEN
|
||||
{
|
||||
int type = (protocol == 1) ? SOCK_STREAM : SOCK_DGRAM;
|
||||
int fd = socket(AF_INET, type, 0);
|
||||
if (fd >= 0)
|
||||
{
|
||||
netSockFd[sockNum] = fd;
|
||||
resultStatus = (protocol == 1) ? 0x13 : 0x22; // SOCK_INIT or SOCK_UDP.
|
||||
ESP_LOGI("NET", "Socket %d opened: fd=%d proto=%d", sockNum, fd, protocol);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("NET", "Socket %d open failed: errno=%d", sockNum, errno);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x04: // CONNECT
|
||||
{
|
||||
int fd = netSockFd[sockNum];
|
||||
if (fd < 0)
|
||||
break;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = ipAddr; // Already in network byte order.
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
ESP_LOGI("NET", "Socket %d connecting to %d.%d.%d.%d:%d",
|
||||
(int)sockNum, (int)(ipAddr & 0xFF), (int)((ipAddr >> 8) & 0xFF), (int)((ipAddr >> 16) & 0xFF), (int)((ipAddr >> 24) & 0xFF), (int)port);
|
||||
|
||||
// Non-blocking connect with select() timeout to avoid blocking the SPI command loop.
|
||||
int flags = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (ret == 0)
|
||||
{
|
||||
// Immediate connect (localhost or cached).
|
||||
resultStatus = 0x17; // SOCK_ESTABLISHED.
|
||||
ESP_LOGI("NET", "Socket %d connected (immediate)", sockNum);
|
||||
}
|
||||
else if (errno == EINPROGRESS)
|
||||
{
|
||||
// Wait for connection with 5-second timeout via select().
|
||||
fd_set wfds;
|
||||
FD_ZERO(&wfds);
|
||||
FD_SET(fd, &wfds);
|
||||
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
|
||||
int sel = select(fd + 1, NULL, &wfds, NULL, &tv);
|
||||
if (sel > 0)
|
||||
{
|
||||
// Check if connect succeeded.
|
||||
int err = 0;
|
||||
socklen_t errLen = sizeof(err);
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen);
|
||||
if (err == 0)
|
||||
{
|
||||
resultStatus = 0x17; // SOCK_ESTABLISHED.
|
||||
ESP_LOGI("NET", "Socket %d connected", sockNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
resultStatus = 0x00;
|
||||
ESP_LOGE("NET", "Socket %d connect error: %d", sockNum, err);
|
||||
close(fd);
|
||||
netSockFd[sockNum] = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultStatus = 0x00; // Timeout or error.
|
||||
ESP_LOGE("NET", "Socket %d connect timeout", sockNum);
|
||||
close(fd);
|
||||
netSockFd[sockNum] = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resultStatus = 0x00; // SOCK_CLOSED on failure.
|
||||
ESP_LOGE("NET", "Socket %d connect failed: errno=%d", sockNum, errno);
|
||||
close(fd);
|
||||
netSockFd[sockNum] = -1;
|
||||
}
|
||||
|
||||
// Restore blocking mode if still open.
|
||||
if (netSockFd[sockNum] >= 0)
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
break;
|
||||
}
|
||||
case 0x02: // LISTEN
|
||||
{
|
||||
int fd = netSockFd[sockNum];
|
||||
if (fd < 0)
|
||||
break;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0 &&
|
||||
listen(fd, 1) == 0)
|
||||
{
|
||||
resultStatus = 0x14; // SOCK_LISTEN.
|
||||
ESP_LOGI("NET", "Socket %d listening on port %d", sockNum, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("NET", "Socket %d listen failed: errno=%d", sockNum, errno);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x10: // CLOSE
|
||||
case 0x08: // DISCON
|
||||
{
|
||||
int fd = netSockFd[sockNum];
|
||||
if (fd >= 0)
|
||||
{
|
||||
if (op == 0x08)
|
||||
shutdown(fd, SHUT_WR);
|
||||
close(fd);
|
||||
netSockFd[sockNum] = -1;
|
||||
ESP_LOGI("NET", "Socket %d closed", sockNum);
|
||||
}
|
||||
resultStatus = 0x00; // SOCK_CLOSED.
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW("NET", "Unknown socket op: 0x%02X", op);
|
||||
break;
|
||||
}
|
||||
|
||||
// Send 1-byte response with new socket status.
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = 1;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + 1 + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, &resultStatus, 1, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void CommandProcessor::cmdNetSend(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
uint8_t sockNum = frame.diskNo;
|
||||
if (sockNum >= 4 || netSockFd[sockNum] < 0)
|
||||
{
|
||||
hdr.status = IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Receive the T2 write payload (data to send over network).
|
||||
// Inline T2 receive: same logic as SDCard::receiveWritePayload but without private access.
|
||||
uint32_t txLen = frame.payloadLen;
|
||||
bool gotPayload = false;
|
||||
if (txLen > 0 && txLen <= IPCF_MAX_PAYLOAD)
|
||||
{
|
||||
uint32_t frameLen = (txLen + IPCF_CRC_SIZE + 3u) & ~3u;
|
||||
spi_slave_transaction_t t2 = {};
|
||||
t2.length = frameLen * 8;
|
||||
t2.rx_buffer = fspi.ipcCmdBuf;
|
||||
t2.tx_buffer = fspi.ipcRespBuf;
|
||||
if (spi_slave_transmit(FSPI_HOST, &t2, portMAX_DELAY) == ESP_OK)
|
||||
{
|
||||
// Verify CRC32.
|
||||
uint32_t recvCrc, calcCrc;
|
||||
memcpy(&recvCrc, fspi.ipcCmdBuf + txLen, IPCF_CRC_SIZE);
|
||||
calcCrc = esp_rom_crc32_le(0, fspi.ipcCmdBuf, txLen);
|
||||
if (calcCrc == recvCrc)
|
||||
gotPayload = true;
|
||||
else
|
||||
ESP_LOGE("NET", "Send payload CRC mismatch: calc=%08lx recv=%08lx", (unsigned long)calcCrc, (unsigned long)recvCrc);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t sent = 0;
|
||||
if (gotPayload && txLen > 0)
|
||||
{
|
||||
sent = send(netSockFd[sockNum], fspi.ipcCmdBuf, txLen, 0);
|
||||
if (sent < 0)
|
||||
{
|
||||
ESP_LOGE("NET", "Socket %d send failed: errno=%d", sockNum, errno);
|
||||
sent = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGI("NET", "Socket %d sent %d bytes", sockNum, (int)sent);
|
||||
}
|
||||
}
|
||||
|
||||
// Response: header + CRC only (no payload).
|
||||
// FSPI_sendBinaryCmd expects write-command responses to have no payload
|
||||
// (it calculates T3 size as IPCF_HEADER_SIZE + CRC_SIZE for write ops).
|
||||
// Including a payload causes CRC offset mismatch.
|
||||
hdr.status = (sent > 0) ? IPCF_STATUS_OK : IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void CommandProcessor::cmdNetRecv(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
uint8_t sockNum = frame.diskNo;
|
||||
if (sockNum >= 4 || netSockFd[sockNum] < 0)
|
||||
{
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = 0;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set non-blocking for poll.
|
||||
int flags = fcntl(netSockFd[sockNum], F_GETFL, 0);
|
||||
fcntl(netSockFd[sockNum], F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
static uint8_t recvBuf[IPCF_MAX_PAYLOAD];
|
||||
uint16_t reqSize = frame.sectorCount;
|
||||
if (reqSize == 0 || reqSize > IPCF_MAX_PAYLOAD)
|
||||
reqSize = IPCF_MAX_PAYLOAD;
|
||||
|
||||
int recvd = recv(netSockFd[sockNum], recvBuf, reqSize, 0);
|
||||
|
||||
// Restore blocking mode.
|
||||
fcntl(netSockFd[sockNum], F_SETFL, flags);
|
||||
|
||||
if (recvd <= 0)
|
||||
{
|
||||
// No data or error — return empty response.
|
||||
// Check if connection was closed by peer.
|
||||
if (recvd == 0)
|
||||
{
|
||||
ESP_LOGI("NET", "Socket %d: peer closed connection", sockNum);
|
||||
}
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = 0;
|
||||
// Use flags to signal peer close: flags bit 0 = peer disconnected.
|
||||
if (recvd == 0)
|
||||
hdr.flags = 0x04; // Signal peer close.
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI("NET", "Socket %d recv %d bytes", sockNum, recvd);
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = (uint16_t)recvd;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + recvd + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, recvBuf, recvd, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void CommandProcessor::cmdNetPing(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
uint32_t targetIp = frame.fileOffset;
|
||||
|
||||
// Use lwIP raw socket for ICMP echo.
|
||||
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
|
||||
uint32_t rtt = 0xFFFFFFFF; // Timeout sentinel.
|
||||
|
||||
if (sock >= 0)
|
||||
{
|
||||
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = targetIp;
|
||||
|
||||
// Build ICMP echo request.
|
||||
uint8_t icmpPkt[64];
|
||||
memset(icmpPkt, 0, sizeof(icmpPkt));
|
||||
icmpPkt[0] = 8; // Type: Echo Request.
|
||||
icmpPkt[1] = 0; // Code: 0.
|
||||
icmpPkt[4] = 0x12; // ID high.
|
||||
icmpPkt[5] = 0x34; // ID low.
|
||||
icmpPkt[6] = 0; // Seq high.
|
||||
icmpPkt[7] = 1; // Seq low.
|
||||
// Checksum.
|
||||
uint32_t sum = 0;
|
||||
for (int i = 0; i < 64; i += 2)
|
||||
sum += (icmpPkt[i] << 8) | icmpPkt[i + 1];
|
||||
while (sum >> 16)
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
uint16_t cksum = ~sum;
|
||||
icmpPkt[2] = (uint8_t)(cksum >> 8);
|
||||
icmpPkt[3] = (uint8_t)(cksum & 0xFF);
|
||||
|
||||
int64_t t0 = esp_timer_get_time();
|
||||
int sent = sendto(sock, icmpPkt, 64, 0, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (sent > 0)
|
||||
{
|
||||
uint8_t pingRecvBuf[128];
|
||||
struct sockaddr_in from;
|
||||
socklen_t fromLen = sizeof(from);
|
||||
int pingRecvd = recvfrom(sock, pingRecvBuf, sizeof(pingRecvBuf), 0, (struct sockaddr *)&from, &fromLen);
|
||||
if (pingRecvd > 0)
|
||||
{
|
||||
int64_t t1 = esp_timer_get_time();
|
||||
rtt = (uint32_t)((t1 - t0) / 1000); // Convert us to ms.
|
||||
ESP_LOGI("NET", "Ping %d.%d.%d.%d: RTT=%d ms",
|
||||
(int)(targetIp & 0xFF), (int)((targetIp >> 8) & 0xFF), (int)((targetIp >> 16) & 0xFF), (int)((targetIp >> 24) & 0xFF), (int)rtt);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW("NET", "Ping timeout");
|
||||
}
|
||||
}
|
||||
close(sock);
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE("NET", "Cannot create ICMP socket: errno=%d", errno);
|
||||
}
|
||||
|
||||
// Response: no payload (FSPI expects write-style response for non-sector commands).
|
||||
// RTT encoded in sectorCount field (16-bit, ms). 0xFFFF = timeout.
|
||||
hdr.status = (rtt < 0xFFFFFFFF) ? IPCF_STATUS_OK : IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
hdr.sectorCount = (uint16_t)(rtt < 0xFFFF ? rtt : 0xFFFF);
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// start — spawn the waitForCommand task.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -471,6 +871,24 @@ void CommandProcessor::processCommand(const t_IpcFrameHdr &frame)
|
||||
case IPCF_CMD_INF:
|
||||
cmdReadInfo(frame);
|
||||
break;
|
||||
case IPCF_CMD_DIR:
|
||||
cmdReadDir(frame);
|
||||
break;
|
||||
case IPCF_CMD_NET_CFG:
|
||||
cmdNetCfg(frame);
|
||||
break;
|
||||
case IPCF_CMD_NET_SOCK:
|
||||
cmdNetSocket(frame);
|
||||
break;
|
||||
case IPCF_CMD_NET_SEND:
|
||||
cmdNetSend(frame);
|
||||
break;
|
||||
case IPCF_CMD_NET_RECV:
|
||||
cmdNetRecv(frame);
|
||||
break;
|
||||
case IPCF_CMD_NET_PING:
|
||||
cmdNetPing(frame);
|
||||
break;
|
||||
default:
|
||||
cmdUnknown(frame);
|
||||
break;
|
||||
|
||||
@@ -1619,6 +1619,159 @@ esp_err_t WiFi::defaultDataGETHandler(httpd_req_t *req)
|
||||
|
||||
return cfgResult;
|
||||
}
|
||||
else if (uriStr == "backup")
|
||||
{
|
||||
// Stream the entire SD card as a tar archive.
|
||||
// Uses POSIX/ustar tar format: 512-byte header per file + padded data.
|
||||
// Streamed via chunked HTTP so memory usage is constant regardless of SD size.
|
||||
httpd_resp_set_type(req, "application/x-tar");
|
||||
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"picoZ80_backup.tar\"");
|
||||
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
|
||||
|
||||
// Recursive lambda to walk directories and stream tar entries.
|
||||
// Uses a stack-based approach to avoid deep recursion.
|
||||
struct TarEntry { std::string path; bool isDir; long size; time_t mtime; };
|
||||
std::vector<std::string> dirStack;
|
||||
dirStack.push_back(std::string(pThis->wifiCtrl.run.fsPath));
|
||||
|
||||
char hdr[512];
|
||||
std::unique_ptr<char[]> fbuf(new char[MAX_CHUNK_SIZE]);
|
||||
if (!fbuf) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory allocation failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t tarResult = ESP_OK;
|
||||
int fileCount = 0;
|
||||
std::string basePath = pThis->wifiCtrl.run.fsPath;
|
||||
if (basePath.back() != '/') basePath += '/';
|
||||
|
||||
while (!dirStack.empty() && tarResult == ESP_OK)
|
||||
{
|
||||
std::string curDir = dirStack.back();
|
||||
dirStack.pop_back();
|
||||
|
||||
DIR *d = opendir(curDir.c_str());
|
||||
if (!d) continue;
|
||||
|
||||
// Collect entries first so we can close the dir handle quickly.
|
||||
std::vector<TarEntry> entries;
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL)
|
||||
{
|
||||
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
|
||||
continue;
|
||||
std::string fullPath = curDir + "/" + ent->d_name;
|
||||
struct stat st;
|
||||
if (stat(fullPath.c_str(), &st) != 0)
|
||||
continue;
|
||||
TarEntry te;
|
||||
te.path = fullPath;
|
||||
te.isDir = S_ISDIR(st.st_mode);
|
||||
te.size = te.isDir ? 0 : st.st_size;
|
||||
te.mtime = st.st_mtime;
|
||||
entries.push_back(te);
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
for (const auto &te : entries)
|
||||
{
|
||||
if (te.isDir) {
|
||||
dirStack.push_back(te.path);
|
||||
}
|
||||
|
||||
// Build relative path by stripping the SD mount prefix.
|
||||
std::string relPath = te.path;
|
||||
if (relPath.find(basePath) == 0)
|
||||
relPath = relPath.substr(basePath.length());
|
||||
if (te.isDir && relPath.back() != '/')
|
||||
relPath += '/';
|
||||
|
||||
// Skip paths that are too long for tar (max 255 = 155 prefix + 100 name).
|
||||
if (relPath.length() > 255)
|
||||
continue;
|
||||
|
||||
// Build the 512-byte POSIX ustar header.
|
||||
memset(hdr, 0, 512);
|
||||
|
||||
// Split into prefix (first 155 chars of dir) and name (last 100 chars).
|
||||
std::string tarName, tarPrefix;
|
||||
if (relPath.length() <= 100) {
|
||||
tarName = relPath;
|
||||
} else {
|
||||
size_t splitAt = relPath.rfind('/', 99);
|
||||
if (splitAt == std::string::npos) splitAt = 99;
|
||||
tarPrefix = relPath.substr(0, splitAt);
|
||||
tarName = relPath.substr(splitAt + 1);
|
||||
}
|
||||
strncpy(hdr + 0, tarName.c_str(), 100);
|
||||
snprintf(hdr + 100, 8, "%07o", te.isDir ? 0755 : 0644); // mode
|
||||
snprintf(hdr + 108, 8, "%07o", 0); // uid
|
||||
snprintf(hdr + 116, 8, "%07o", 0); // gid
|
||||
snprintf(hdr + 124, 12, "%011lo", te.isDir ? 0L : te.size); // size
|
||||
snprintf(hdr + 136, 12, "%011lo", (long)te.mtime); // mtime
|
||||
memset(hdr + 148, ' ', 8); // checksum placeholder
|
||||
hdr[156] = te.isDir ? '5' : '0'; // typeflag
|
||||
memcpy(hdr + 257, "ustar", 5); // magic
|
||||
hdr[263] = '0'; hdr[264] = '0'; // version
|
||||
if (!tarPrefix.empty())
|
||||
strncpy(hdr + 345, tarPrefix.c_str(), 155);
|
||||
|
||||
// Compute header checksum (sum of all bytes, treating checksum field as spaces).
|
||||
unsigned int cksum = 0;
|
||||
for (int i = 0; i < 512; i++)
|
||||
cksum += (unsigned char)hdr[i];
|
||||
snprintf(hdr + 148, 7, "%06o", cksum);
|
||||
hdr[155] = '\0';
|
||||
|
||||
// Send header.
|
||||
if (httpd_resp_send_chunk(req, hdr, 512) != ESP_OK) {
|
||||
tarResult = ESP_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
// Send file data (not for directories).
|
||||
if (!te.isDir && te.size > 0) {
|
||||
FILE *f = fopen(te.path.c_str(), "rb");
|
||||
if (f) {
|
||||
long remaining = te.size;
|
||||
while (remaining > 0 && tarResult == ESP_OK) {
|
||||
size_t toRead = (remaining > MAX_CHUNK_SIZE) ? MAX_CHUNK_SIZE : remaining;
|
||||
size_t got = fread(fbuf.get(), 1, toRead, f);
|
||||
if (got > 0) {
|
||||
if (httpd_resp_send_chunk(req, fbuf.get(), got) != ESP_OK)
|
||||
tarResult = ESP_FAIL;
|
||||
remaining -= got;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
// Pad to 512-byte boundary.
|
||||
int pad = (512 - (te.size % 512)) % 512;
|
||||
if (pad > 0 && tarResult == ESP_OK) {
|
||||
memset(hdr, 0, pad);
|
||||
if (httpd_resp_send_chunk(req, hdr, pad) != ESP_OK)
|
||||
tarResult = ESP_FAIL;
|
||||
}
|
||||
}
|
||||
}
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// End-of-archive: two 512-byte zero blocks.
|
||||
if (tarResult == ESP_OK) {
|
||||
memset(hdr, 0, 512);
|
||||
httpd_resp_send_chunk(req, hdr, 512);
|
||||
httpd_resp_send_chunk(req, hdr, 512);
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
ESP_LOGI(WIFITAG, "Backup complete: %d files streamed as tar", fileCount);
|
||||
}
|
||||
|
||||
return tarResult;
|
||||
}
|
||||
else if (uriStr == "wifistatus")
|
||||
{
|
||||
// JSON endpoint for AJAX polling of WiFi + system + RP2350 status.
|
||||
@@ -3695,13 +3848,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
else
|
||||
{
|
||||
htmlStr.append("<div id=\"filemanager-info\" style=\"font-size:12px;padding-top:8px;padding-bottom:8px;\"><b>").append(sdpath).append("</b></div>");
|
||||
htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:25px;\">");
|
||||
htmlStr.append("<div id=\"filemanager-area\" style=\"padding-left:4px;\">");
|
||||
htmlStr.append(" <table class=\"table table-borderless table-sm\" style=\"width:100%\">");
|
||||
htmlStr.append(" <thead>");
|
||||
htmlStr.append(" <tr>");
|
||||
htmlStr.append(" <th style=\"width:50%;min-width:200px\"><b>Name</b></th>");
|
||||
htmlStr.append(" <th style=\"width:5%\"><b>Type</b></th>");
|
||||
htmlStr.append(" <th style=\"width:5%\"><b>Size (Bytes)</b></th>");
|
||||
htmlStr.append(" <th style=\"width:1%;padding:0 6px;text-align:center;white-space:nowrap;\"><input type=\"checkbox\" id=\"selectAll\" onclick=\"toggleSelectAll(this)\" title=\"Select all\"></th>");
|
||||
htmlStr.append(" <th style=\"min-width:200px;padding-left:2px;cursor:pointer;\" data-sort-col=\"1\" data-sort-type=\"text\" title=\"Click to sort\"><b>Name</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
|
||||
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"2\" data-sort-type=\"text\" title=\"Click to sort\"><b>Type</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
|
||||
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"3\" data-sort-type=\"num\" title=\"Click to sort\"><b>Size (Bytes)</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
|
||||
htmlStr.append(" <th style=\"text-align:left;width:10%\"><b>Action</b></th>");
|
||||
htmlStr.append(" </tr>");
|
||||
htmlStr.append(" </thead>");
|
||||
@@ -3717,12 +3871,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
upDir.pop_back();
|
||||
}
|
||||
htmlStr.append("<tr>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("<td style=\"text-align:center\"><a href=\"").append(upDir).append("\">").append("[up level]").append("</a></td>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("</tr>\n");
|
||||
htmlStr.append("<tr>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
|
||||
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
|
||||
@@ -3738,7 +3894,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
"onclick=\"mkdir()\"></button>");
|
||||
htmlStr.append("</td></tr>\n");
|
||||
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
|
||||
"<td colspan=\"4\">"
|
||||
"<td colspan=\"5\">"
|
||||
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
|
||||
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
|
||||
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
|
||||
@@ -3747,6 +3903,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
else
|
||||
{
|
||||
htmlStr.append("<tr>");
|
||||
htmlStr.append("<td></td>");
|
||||
htmlStr.append("<form action=\"/data/upload\" method=\"POST\" id=\"fileupload\">");
|
||||
htmlStr.append("<input type=\"hidden\" id=\"basepath\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<td><input id=\"filepath\" type=\"text\" style=\"width:100%;\"></td>");
|
||||
@@ -3762,31 +3919,58 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
"onclick=\"mkdir()\"></button>");
|
||||
htmlStr.append("</td></tr>\n");
|
||||
htmlStr.append("<tr id=\"uploadProgressRow\" style=\"display:none;\">"
|
||||
"<td colspan=\"4\">"
|
||||
"<td colspan=\"5\">"
|
||||
"<div style=\"background:#444;border-radius:4px;height:22px;width:100%;position:relative;\">"
|
||||
"<div id=\"uploadProgressBar\" style=\"background:#5cb85c;height:100%;border-radius:4px;width:0%;transition:width 0.2s;\"></div>"
|
||||
"<span id=\"uploadProgressText\" style=\"position:absolute;top:0;left:0;width:100%;text-align:center;line-height:22px;color:#fff;font-size:12px;\"></span>"
|
||||
"</div></td></tr>\n");
|
||||
}
|
||||
|
||||
int entryCnt = 1;
|
||||
std::string fileToStat;
|
||||
// Collect all directory entries, then sort: directories first, then alphabetical (case-insensitive).
|
||||
struct t_DirEntry {
|
||||
std::string name;
|
||||
bool isDir;
|
||||
long size;
|
||||
};
|
||||
std::vector<t_DirEntry> dirEntries;
|
||||
|
||||
while ((entry = readdir(dir)) != NULL)
|
||||
{
|
||||
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
|
||||
fileToStat = sdpath + "/" + entry->d_name;
|
||||
std::string fileToStat = sdpath + "/" + entry->d_name;
|
||||
if (stat(fileToStat.c_str(), &entryStat) == -1)
|
||||
{
|
||||
ESP_LOGE(WIFITAG, "Failed to subdir stat %s : %s", entrytype, fileToStat.c_str());
|
||||
ESP_LOGE(WIFITAG, "Failed to stat %s", fileToStat.c_str());
|
||||
continue;
|
||||
}
|
||||
sprintf(entrysize, "%ld", entryStat.st_size);
|
||||
t_DirEntry de;
|
||||
de.name = entry->d_name;
|
||||
de.isDir = (entry->d_type == DT_DIR);
|
||||
de.size = entryStat.st_size;
|
||||
dirEntries.push_back(de);
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
std::sort(dirEntries.begin(), dirEntries.end(), [](const t_DirEntry &a, const t_DirEntry &b) {
|
||||
// Directories first, then files. Within each group, case-insensitive alphabetical.
|
||||
if (a.isDir != b.isDir) return a.isDir > b.isDir;
|
||||
std::string al = a.name, bl = b.name;
|
||||
std::transform(al.begin(), al.end(), al.begin(), ::tolower);
|
||||
std::transform(bl.begin(), bl.end(), bl.begin(), ::tolower);
|
||||
return al < bl;
|
||||
});
|
||||
|
||||
int entryCnt = 1;
|
||||
for (const auto &de : dirEntries)
|
||||
{
|
||||
entrytype = de.isDir ? "directory" : "file";
|
||||
sprintf(entrysize, "%ld", de.size);
|
||||
|
||||
htmlStr.append("<tr>");
|
||||
htmlStr.append("<td style=\"padding:0 6px;text-align:center;white-space:nowrap;\"><input type=\"checkbox\" class=\"file-select\" data-name=\"").append(de.name).append("\" data-dir=\"").append(directory).append("\" onchange=\"updateDeleteSelectedBtn()\"></td>");
|
||||
htmlStr.append("<td>");
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=ren\">");
|
||||
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("</td>");
|
||||
@@ -3806,14 +3990,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
|
||||
// Slot 2: Open Dir (dirs) or Download (files)
|
||||
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
||||
if (entry->d_type == DT_DIR)
|
||||
if (de.isDir)
|
||||
{
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=dir\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory);
|
||||
if (htmlStr.back() != '/')
|
||||
htmlStr.append("/");
|
||||
htmlStr.append(entry->d_name).append("\">");
|
||||
htmlStr.append(de.name).append("\">");
|
||||
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"dir\" class=\"fa fa-folder-open-o wm-button-small\" "
|
||||
"aria-hidden=\"true\" title=\"Open\"></button>");
|
||||
htmlStr.append("</form>");
|
||||
@@ -3822,7 +4006,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
{
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"/data/download\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<button type=\"submit\" class=\"fa fa-download wm-button-small\" aria-hidden=\"true\" "
|
||||
"title=\"Download\"></button>");
|
||||
@@ -3832,15 +4016,15 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
|
||||
// Slot 3: Edit (text files only, blank otherwise)
|
||||
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
||||
if (entry->d_type != DT_DIR)
|
||||
if (!de.isDir)
|
||||
{
|
||||
std::filesystem::path filePath = entry->d_name;
|
||||
std::filesystem::path filePath = de.name;
|
||||
if (filePath.extension() == ".txt" || filePath.extension() == ".htm" || filePath.extension() == ".js" ||
|
||||
filePath.extension() == ".css" || filePath.extension() == ".json")
|
||||
{
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=edit\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"edit\" class=\"fa fa-pencil wm-button-small\" "
|
||||
"aria-hidden=\"true\" title=\"Edit\"></button>");
|
||||
@@ -3853,7 +4037,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=copy\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"copy\" class=\"fa fa-copy wm-button-small\" "
|
||||
"aria-hidden=\"true\" title=\"Copy\"></button>");
|
||||
@@ -3866,18 +4050,17 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
|
||||
htmlStr.append("<form method=\"GET\" style=\"display:inline\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"cmd\" value=\"del\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
|
||||
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
|
||||
htmlStr.append("<button type=\"submit\" class=\"fa fa-trash wm-button-small\" "
|
||||
"aria-hidden=\"true\" title=\"Delete\" onclick=\"return confirmDelete(event, '")
|
||||
.append(entry->d_name)
|
||||
.append(de.name)
|
||||
.append("');\"></button>");
|
||||
htmlStr.append("</form>");
|
||||
htmlStr.append("</span>");
|
||||
htmlStr.append("</td></tr>\n");
|
||||
entryCnt++;
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
htmlStr.append(" </tbody>");
|
||||
htmlStr.append(" </table>");
|
||||
@@ -3985,6 +4168,15 @@ esp_err_t WiFi::defaultRebootHandler(httpd_req_t *req)
|
||||
httpd_resp_set_status(req, "200 OK");
|
||||
httpd_resp_sendstr(req, "");
|
||||
}
|
||||
else if (uriStr.substr(0, 3) == "ipl")
|
||||
{
|
||||
// Send command to RP2350 to trigger IPL reset (BST via 8255 PPI PC3).
|
||||
CP_queueCmd("IPLR");
|
||||
|
||||
// Indicate IPL reset request successful.
|
||||
httpd_resp_set_status(req, "200 OK");
|
||||
httpd_resp_sendstr(req, "");
|
||||
}
|
||||
else
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Unknown reboot directive");
|
||||
@@ -4276,7 +4468,15 @@ esp_err_t WiFi::changeDiskHandler(httpd_req_t *req, enum DRIVETYPES driveType)
|
||||
.then(arr => {
|
||||
const tbody = document.getElementById('listing');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
|
||||
// Sort: ".." first, then directories alphabetically, then files alphabetically.
|
||||
arr.sort((a, b) => {
|
||||
if (a.name === '..') return -1;
|
||||
if (b.name === '..') return 1;
|
||||
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
|
||||
arr.forEach(e => {
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
@@ -56,6 +58,13 @@
|
||||
#include "esp_vfs_fat.h"
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include "lwip/sockets.h"
|
||||
#include "esp_netif.h"
|
||||
#include "FSPI.h"
|
||||
#include "WiFi.h"
|
||||
#include "cJSON.h"
|
||||
@@ -85,6 +94,7 @@ class CommandProcessor : public WiFi, FSPI, SDCard
|
||||
CommandProcessor(WiFi &wifi, FSPI &fspi, SDCard &sdcard, cJSON *config) : wifi(wifi), fspi(fspi), sdcard(sdcard)
|
||||
{
|
||||
(void) config;
|
||||
for (int i = 0; i < 4; i++) netSockFd[i] = -1;
|
||||
}
|
||||
|
||||
const char *getClassName(const std::string &prettyFunction)
|
||||
@@ -176,6 +186,65 @@ class CommandProcessor : public WiFi, FSPI, SDCard
|
||||
}
|
||||
}
|
||||
|
||||
void cmdReadDir(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
// Build a directory listing as text and send back as payload.
|
||||
// The path is in frame.filename (relative to SD card mount point).
|
||||
char fullPath[256];
|
||||
if (frame.filename[0] == '\0')
|
||||
snprintf(fullPath, sizeof(fullPath), "%s", SD_CARD_MOUNT_POINT);
|
||||
else
|
||||
snprintf(fullPath, sizeof(fullPath), "%s/%s", SD_CARD_MOUNT_POINT, frame.filename);
|
||||
|
||||
// Remove trailing slash if present (except for mount root).
|
||||
size_t plen = strlen(fullPath);
|
||||
if (plen > strlen(SD_CARD_MOUNT_POINT) && fullPath[plen - 1] == '/')
|
||||
fullPath[plen - 1] = '\0';
|
||||
|
||||
ESP_LOGI("CMDPROC", "DIR: '%s' → '%s'", frame.filename, fullPath);
|
||||
DIR *dir = opendir(fullPath);
|
||||
t_IpcFrameHdr hdr = {};
|
||||
hdr.frameType = IPCF_TYPE_RESPONSE;
|
||||
hdr.command = frame.command;
|
||||
|
||||
if (!dir)
|
||||
{
|
||||
hdr.status = IPCF_STATUS_ERR;
|
||||
hdr.payloadLen = 0;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build listing into a buffer. Use the DMA TX buffer scratch area.
|
||||
static char dirBuf[IPCF_MAX_PAYLOAD];
|
||||
int pos = 0;
|
||||
struct dirent *entry;
|
||||
|
||||
while ((entry = readdir(dir)) != NULL && pos < (int) sizeof(dirBuf) - 80)
|
||||
{
|
||||
// Stat the entry for size and type.
|
||||
char entryPath[512];
|
||||
struct stat st;
|
||||
snprintf(entryPath, sizeof(entryPath), "%.255s/%.255s", fullPath, entry->d_name);
|
||||
bool isDir = (entry->d_type == DT_DIR);
|
||||
long sz = 0;
|
||||
if (!isDir && stat(entryPath, &st) == 0)
|
||||
sz = (long) st.st_size;
|
||||
|
||||
if (isDir)
|
||||
pos += snprintf(dirBuf + pos, sizeof(dirBuf) - pos, " <DIR> %s\n", entry->d_name);
|
||||
else
|
||||
pos += snprintf(dirBuf + pos, sizeof(dirBuf) - pos, " %7ld %s\n", sz, entry->d_name);
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
hdr.status = IPCF_STATUS_OK;
|
||||
hdr.payloadLen = (uint16_t) pos;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + pos + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, (uint8_t *) dirBuf, pos, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
void cmdUnknown(const t_IpcFrameHdr &frame)
|
||||
{
|
||||
ESP_LOGE("CMDPROC", "Unknown binary cmd opcode: %02X", frame.command);
|
||||
@@ -188,5 +257,14 @@ class CommandProcessor : public WiFi, FSPI, SDCard
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
|
||||
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
|
||||
}
|
||||
|
||||
// Network command handlers (Celestite W5100 emulation).
|
||||
void cmdNetCfg(const t_IpcFrameHdr &frame);
|
||||
void cmdNetSocket(const t_IpcFrameHdr &frame);
|
||||
void cmdNetSend(const t_IpcFrameHdr &frame);
|
||||
void cmdNetRecv(const t_IpcFrameHdr &frame);
|
||||
void cmdNetPing(const t_IpcFrameHdr &frame);
|
||||
|
||||
int netSockFd[4]; // BSD socket file descriptors, one per W5100 socket.
|
||||
};
|
||||
#endif // CMDPROC_H
|
||||
|
||||
@@ -48,6 +48,14 @@
|
||||
#define IPCF_CMD_RFD 0x08 // Read floppy disk image file
|
||||
#define IPCF_CMD_RQD 0x09 // Read quick-disk image file
|
||||
#define IPCF_CMD_RRF 0x0A // Read RAM-file backup image
|
||||
#define IPCF_CMD_DIR 0x0B // Read directory listing (returns text in payload)
|
||||
|
||||
// Network commands (Celestite W5100 emulation)
|
||||
#define IPCF_CMD_NET_CFG 0x10 // Get ESP32 network config (IP, gateway, subnet, MAC)
|
||||
#define IPCF_CMD_NET_SOCK 0x11 // Socket operation (open/connect/listen/close/discon)
|
||||
#define IPCF_CMD_NET_SEND 0x12 // Send data to socket (payload = data to send)
|
||||
#define IPCF_CMD_NET_RECV 0x13 // Receive data from socket (response payload = data)
|
||||
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status codes (t_IpcFrameHdr::status — response frames only)
|
||||
|
||||
@@ -321,6 +321,26 @@ void buildVersionList(WiFi::t_versionList *versionList, NVS &nvs, SDCard &sdcard
|
||||
static esp_netif_t *s_usb_ncm_netif = NULL;
|
||||
static volatile bool g_usbReady = false; // Set by USB task when setup completes.
|
||||
|
||||
// NCM connection parameters — read from JSON config "esp32.core" block.
|
||||
// Defaults are used if the config doesn't specify them.
|
||||
typedef struct {
|
||||
bool enabled; // NCM enable/disable (default: true)
|
||||
int maxRetries; // Max connection cycles: 0=forever, 5-1000 (default: 0)
|
||||
int retryPeriod; // Delay between retries: 0=increasing backoff, 1-120s fixed (default: 0)
|
||||
} t_ncmConfig;
|
||||
static t_ncmConfig g_ncmConfig = { true, 0, 0 };
|
||||
#endif
|
||||
|
||||
// WiFi enable flag — read from JSON config "esp32.core.wifi" (1=enable, 0=disable).
|
||||
// Defaults to enabled if compiled in, disabled if not compiled in.
|
||||
#if defined(CONFIG_IF_WIFI_ENABLED)
|
||||
static bool g_wifiEnabled = true;
|
||||
#else
|
||||
static bool g_wifiEnabled = false;
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||||
|
||||
static void usb_ncm_l2_free(void *h, void *buffer)
|
||||
{
|
||||
free(buffer);
|
||||
@@ -362,6 +382,30 @@ static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx)
|
||||
// CommandProcessor (SPI slave) without waiting for USB delays. The 2-second
|
||||
// disconnect/reconnect cycle that macOS needs would otherwise block SPI
|
||||
// slave init, causing the RP2350's first sector reads to fail.
|
||||
// USB NCM connection timing parameters.
|
||||
static constexpr int USB_NCM_DISCONNECT_INIT_MS = 3000; // Initial disconnect hold time.
|
||||
static constexpr int USB_NCM_DISCONNECT_MAX_MS = 60000; // Maximum disconnect hold (backoff cap).
|
||||
static constexpr int USB_NCM_BACKOFF_STEP_MS = 1000; // Increase disconnect by this much each retry.
|
||||
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500; // Post-connect settle before polling mount.
|
||||
static constexpr int USB_NCM_POLL_MS = 100; // Polling interval for mount/DHCP checks.
|
||||
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000; // Max wait for tud_mounted() per attempt.
|
||||
static constexpr int USB_NCM_LINK_RETRIES = 3; // Link-state UP retries per mount cycle.
|
||||
static constexpr int USB_NCM_POST_MOUNT_MS = 1500; // Delay after mount for host NCM driver load.
|
||||
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000; // DHCP wait per link-state attempt.
|
||||
// Flag set when the DHCP server assigns a lease to the host.
|
||||
static volatile bool g_ncmDhcpLeased = false;
|
||||
|
||||
// Event handler: fired when the DHCP server assigns an IP to the connected host.
|
||||
static void ncm_dhcp_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
ip_event_ap_staipassigned_t *evt = (ip_event_ap_staipassigned_t *)event_data;
|
||||
ESP_LOGI(MAINTAG, "USB NCM: DHCP lease assigned to host (IP " IPSTR ")", IP2STR(&evt->ip));
|
||||
g_ncmDhcpLeased = true;
|
||||
}
|
||||
}
|
||||
|
||||
void setupUSBTask(void *pvParameters)
|
||||
{
|
||||
bool cdcOk = false;
|
||||
@@ -371,43 +415,40 @@ void setupUSBTask(void *pvParameters)
|
||||
esp_netif_init();
|
||||
esp_event_loop_create_default();
|
||||
|
||||
// Register for DHCP server lease events so we know when the host has an IP.
|
||||
esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED,
|
||||
&ncm_dhcp_event_handler, NULL);
|
||||
|
||||
// 1. Install TinyUSB driver on the OTG peripheral (GPIO 19/20).
|
||||
tinyusb_config_t tusb_cfg = {};
|
||||
tusb_cfg.external_phy = false;
|
||||
esp_err_t ret = tinyusb_driver_install(&tusb_cfg);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(MAINTAG, "USB: TinyUSB driver install failed");
|
||||
g_usbReady = true; // Signal ready (even on failure) so main loop doesn't wait forever.
|
||||
g_usbReady = true;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Force a clean USB disconnect/reconnect cycle. These delays run in this
|
||||
// background task so the main task is free to start the CommandProcessor.
|
||||
// The disconnect duration must be long enough for the host OS to fully
|
||||
// tear down its NCM driver; the post-connect wait allows the host to
|
||||
// enumerate the device and start its DHCP client. Shorter timings cause
|
||||
// intermittent failures where the host never brings the link up.
|
||||
tud_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
tud_connect();
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
// 2. Initialise CDC-ACM for serial logging (independent of NCM connection).
|
||||
{
|
||||
tinyusb_config_cdcacm_t acm_cfg = {};
|
||||
ret = tusb_cdc_acm_init(&acm_cfg);
|
||||
cdcOk = (ret == ESP_OK);
|
||||
}
|
||||
|
||||
// 3. Initialise CDC-ACM for serial logging.
|
||||
tinyusb_config_cdcacm_t acm_cfg = {};
|
||||
ret = tusb_cdc_acm_init(&acm_cfg);
|
||||
cdcOk = (ret == ESP_OK);
|
||||
// 3. Initialise the NCM network class handler.
|
||||
{
|
||||
tinyusb_net_config_t net_config = {};
|
||||
net_config.mac_addr[0] = 0x02; net_config.mac_addr[1] = 0x02;
|
||||
net_config.mac_addr[2] = 0x11; net_config.mac_addr[3] = 0x22;
|
||||
net_config.mac_addr[4] = 0x33; net_config.mac_addr[5] = 0x01;
|
||||
net_config.on_recv_callback = usb_ncm_recv_callback;
|
||||
ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config);
|
||||
ncmOk = (ret == ESP_OK);
|
||||
}
|
||||
|
||||
// 4. Initialise the NCM network class handler.
|
||||
tinyusb_net_config_t net_config = {};
|
||||
net_config.mac_addr[0] = 0x02; net_config.mac_addr[1] = 0x02;
|
||||
net_config.mac_addr[2] = 0x11; net_config.mac_addr[3] = 0x22;
|
||||
net_config.mac_addr[4] = 0x33; net_config.mac_addr[5] = 0x01;
|
||||
net_config.on_recv_callback = usb_ncm_recv_callback;
|
||||
ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config);
|
||||
ncmOk = (ret == ESP_OK);
|
||||
|
||||
// 5. Create the lwIP netif so received packets are handled immediately.
|
||||
// 4. Create the lwIP netif and DHCP server (persists across reconnect cycles).
|
||||
if (ncmOk) {
|
||||
esp_netif_ip_info_t ip_info = {};
|
||||
ip_info.ip.addr = ipaddr_addr(CONFIG_IF_USB_NCM_IP);
|
||||
@@ -447,21 +488,133 @@ void setupUSBTask(void *pvParameters)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Redirect console to TinyUSB CDC-ACM.
|
||||
// 5. Redirect console to TinyUSB CDC-ACM.
|
||||
if (cdcOk) {
|
||||
esp_tusb_init_console(TINYUSB_CDC_ACM_0);
|
||||
}
|
||||
|
||||
// 7. Allow time for the host to complete DHCP and bring the link up before
|
||||
// signalling ready. Without this the webserver may start before the host
|
||||
// has an IP address, causing the first connection attempt to fail.
|
||||
if (ncmOk) {
|
||||
ESP_LOGI(MAINTAG, "USB NCM netif up — waiting for host DHCP...");
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
// 6. Signal ready so the main loop can start WiFi and the webserver immediately.
|
||||
// The NCM connection will continue retrying in the background below.
|
||||
ESP_LOGI(MAINTAG, "USB init complete (CDC:%s NCM:%s), starting NCM connection loop...",
|
||||
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL");
|
||||
g_usbReady = true;
|
||||
|
||||
if (!ncmOk || s_usb_ncm_netif == NULL) {
|
||||
ESP_LOGE(MAINTAG, "USB NCM: init failed, connection loop not started");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s)", cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL");
|
||||
g_usbReady = true;
|
||||
// 7. NCM connection loop.
|
||||
// Reads config from g_ncmConfig (populated by JSON parser after g_usbReady is set).
|
||||
// Allow a moment for the main loop to parse JSON config before we read g_ncmConfig.
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
if (!g_ncmConfig.enabled) {
|
||||
ESP_LOGI(MAINTAG, "USB NCM: disabled by config (ncm=0), connection loop not started");
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
int maxCycles = g_ncmConfig.maxRetries; // 0=forever, else limit
|
||||
int fixedDelay = g_ncmConfig.retryPeriod; // 0=backoff, else fixed seconds
|
||||
ESP_LOGI(MAINTAG, "USB NCM: config retries=%d period=%ds", maxCycles, fixedDelay);
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(50)); // Let TinyUSB task initialise.
|
||||
bool warmBoot = tud_mounted();
|
||||
int disconnectMs = warmBoot ? (USB_NCM_DISCONNECT_INIT_MS + USB_NCM_BACKOFF_STEP_MS)
|
||||
: USB_NCM_DISCONNECT_INIT_MS;
|
||||
int cycle = 0;
|
||||
|
||||
while (!g_ncmDhcpLeased)
|
||||
{
|
||||
cycle++;
|
||||
|
||||
// Check retry limit (0 = forever).
|
||||
if (maxCycles > 0 && cycle > maxCycles) {
|
||||
ESP_LOGW(MAINTAG, "USB NCM: retry limit reached (%d cycles), giving up", maxCycles);
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGI(MAINTAG, "USB NCM: connection cycle %d%s (%s, disconnect=%d ms)",
|
||||
cycle, maxCycles > 0 ? ("/" + std::to_string(maxCycles)).c_str() : "",
|
||||
(cycle == 1 && warmBoot) ? "warm reboot" : "retry", disconnectMs);
|
||||
|
||||
// --- Phase 1: Disconnect/reconnect ---
|
||||
tud_disconnect();
|
||||
vTaskDelay(pdMS_TO_TICKS(disconnectMs));
|
||||
|
||||
tud_connect();
|
||||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
|
||||
|
||||
// --- Phase 2: Wait for host to enumerate ---
|
||||
bool mounted = false;
|
||||
int waited = 0;
|
||||
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
|
||||
{
|
||||
if (tud_mounted()) {
|
||||
mounted = true;
|
||||
ESP_LOGI(MAINTAG, "USB NCM: host enumerated (cycle %d, %d ms)", cycle, waited);
|
||||
break;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
|
||||
waited += USB_NCM_POLL_MS;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
ESP_LOGW(MAINTAG, "USB NCM: mount timeout (cycle %d)", cycle);
|
||||
if (fixedDelay > 0) {
|
||||
disconnectMs = fixedDelay * 1000;
|
||||
} else {
|
||||
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
|
||||
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
|
||||
disconnectMs = USB_NCM_DISCONNECT_MAX_MS;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Phase 3: Link-state UP with retry ---
|
||||
ESP_LOGI(MAINTAG, "USB NCM: waiting %d ms for host NCM driver...", USB_NCM_POST_MOUNT_MS);
|
||||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POST_MOUNT_MS));
|
||||
|
||||
for (int linkAttempt = 1; linkAttempt <= USB_NCM_LINK_RETRIES && !g_ncmDhcpLeased; linkAttempt++)
|
||||
{
|
||||
if (linkAttempt > 1) {
|
||||
tud_network_link_state(0, false);
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
}
|
||||
|
||||
tud_network_link_state(0, true);
|
||||
ESP_LOGI(MAINTAG, "USB NCM: link UP (cycle %d, link attempt %d/%d)",
|
||||
cycle, linkAttempt, USB_NCM_LINK_RETRIES);
|
||||
|
||||
waited = 0;
|
||||
while (waited < USB_NCM_DHCP_WAIT_MS && !g_ncmDhcpLeased) {
|
||||
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
|
||||
waited += USB_NCM_POLL_MS;
|
||||
}
|
||||
|
||||
if (g_ncmDhcpLeased) {
|
||||
ESP_LOGI(MAINTAG, "USB NCM: DHCP confirmed (cycle %d, link attempt %d, %d ms)",
|
||||
cycle, linkAttempt, waited);
|
||||
}
|
||||
}
|
||||
|
||||
if (!g_ncmDhcpLeased) {
|
||||
ESP_LOGW(MAINTAG, "USB NCM: no DHCP after link-state retries (cycle %d), full reconnect...", cycle);
|
||||
if (fixedDelay > 0) {
|
||||
disconnectMs = fixedDelay * 1000;
|
||||
} else {
|
||||
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
|
||||
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
|
||||
disconnectMs = USB_NCM_DISCONNECT_MAX_MS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_ncmDhcpLeased) {
|
||||
ESP_LOGI(MAINTAG, "USB NCM: connection established after %d cycle(s)", cycle);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
#endif // CONFIG_IF_USB_NCM_ENABLED
|
||||
@@ -668,6 +821,60 @@ void setupJSON(NVS &nvs, SDCard &sdcard, FSPI &fspi, cJSON *config)
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi enable/disable from "esp32.core.wifi" — only effective if WiFi is compiled in.
|
||||
{
|
||||
cJSON *wifiEnable = cJSON_GetObjectItem(coreObj, "wifi_enable");
|
||||
if (cJSON_IsNumber(wifiEnable)) {
|
||||
#if defined(CONFIG_IF_WIFI_ENABLED)
|
||||
g_wifiEnabled = (wifiEnable->valueint != 0);
|
||||
#else
|
||||
g_wifiEnabled = false; // Not compiled in, always disabled.
|
||||
#endif
|
||||
}
|
||||
ESP_LOGI(MAINTAG, "WiFi config: enabled=%d (compiled=%s)",
|
||||
g_wifiEnabled,
|
||||
#if defined(CONFIG_IF_WIFI_ENABLED)
|
||||
"yes"
|
||||
#else
|
||||
"no"
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
// NCM connection parameters from "esp32.core" — only effective if NCM is compiled in.
|
||||
#if defined(CONFIG_IF_USB_NCM_ENABLED)
|
||||
{
|
||||
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
|
||||
if (cJSON_IsNumber(ncmEnable))
|
||||
g_ncmConfig.enabled = (ncmEnable->valueint != 0);
|
||||
|
||||
cJSON *ncmRetries = cJSON_GetObjectItem(coreObj, "ncmretries");
|
||||
if (cJSON_IsNumber(ncmRetries)) {
|
||||
int v = ncmRetries->valueint;
|
||||
if (v == 0 || (v >= 5 && v <= 1000))
|
||||
g_ncmConfig.maxRetries = v;
|
||||
}
|
||||
|
||||
cJSON *ncmPeriod = cJSON_GetObjectItem(coreObj, "ncmperiod");
|
||||
if (cJSON_IsNumber(ncmPeriod)) {
|
||||
int v = ncmPeriod->valueint;
|
||||
if (v >= 0 && v <= 120)
|
||||
g_ncmConfig.retryPeriod = v;
|
||||
}
|
||||
|
||||
ESP_LOGI(MAINTAG, "NCM config: enabled=%d retries=%d period=%d",
|
||||
g_ncmConfig.enabled, g_ncmConfig.maxRetries, g_ncmConfig.retryPeriod);
|
||||
}
|
||||
#else
|
||||
// NCM not compiled in — if config says enabled, log a note.
|
||||
{
|
||||
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
|
||||
if (cJSON_IsNumber(ncmEnable) && ncmEnable->valueint != 0) {
|
||||
ESP_LOGW(MAINTAG, "NCM config: enabled in config but NCM not compiled in — ignored");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -971,8 +1178,12 @@ extern "C"
|
||||
// Start WiFi (AP or Client mode). When USB NCM has already started
|
||||
// the webserver, the WiFi event handlers detect that server != NULL
|
||||
// and skip the redundant startWebserver() call.
|
||||
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
|
||||
wifi->run();
|
||||
if (g_wifiEnabled) {
|
||||
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
|
||||
wifi->run();
|
||||
} else {
|
||||
ESP_LOGW(MAINTAG, "WiFi disabled by config (wifi_enable=0).");
|
||||
}
|
||||
#endif
|
||||
wifiStarted = true;
|
||||
}
|
||||
|
||||
2
projects/tzpuPico/esp32/version.txt
vendored
2
projects/tzpuPico/esp32/version.txt
vendored
@@ -1 +1 @@
|
||||
2.54
|
||||
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>
|
||||
@@ -138,7 +139,14 @@
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory</h3>
|
||||
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory
|
||||
<button type="button" id="deleteSelectedBtn" class="btn btn-danger btn-xs" style="float:right;margin-top:-2px;margin-right:5px;display:none;" onclick="deleteSelected()" title="Delete all selected files">
|
||||
<i class="fa fa-trash"></i> Delete Selected (<span id="deleteSelectedCount">0</span>)
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-xs" style="float:right;margin-top:-2px;" onclick="backupSD()" title="Download entire SD card as a tar archive">
|
||||
<i class="fa fa-download"></i> Backup SD
|
||||
</button>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
|
||||
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();
|
||||
|
||||
365
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
365
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
@@ -316,30 +316,36 @@ var ConfigGUI = (function($) {
|
||||
// Driver and Interface Definitions
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
var knownDrivers = ['MZ700', 'MZ80A', 'MZ80B', 'MZ2000', 'MZ1500', 'MZ2200', 'MZ2500'];
|
||||
// Known drivers — must match virtualFuncMap[] in Z80CPU.c.
|
||||
var knownDrivers = ['MZ700', 'MZ1500', 'MZ80A', 'MZ80B', 'MZ2000', 'MZ2200', 'MZ2500'];
|
||||
|
||||
// ROM constraints per interface: minRom = initial entries, maxRom = hard limit.
|
||||
var interfaceRomLimits = {
|
||||
'RFS': { minRom: 2, maxRom: 4 },
|
||||
'TZFS': { minRom: 1, maxRom: 1 },
|
||||
'MZ-1E05': { minRom: 1, maxRom: 1 },
|
||||
'MZ-1E14': { minRom: 1, maxRom: 1 },
|
||||
'MZ-1E19': { minRom: 0, maxRom: 1 },
|
||||
'MZ-1R12': { minRom: 1, maxRom: 1 },
|
||||
'MZ-1R18': { minRom: 0, maxRom: 0 },
|
||||
'MZ80AFI': { minRom: 1, maxRom: 1 },
|
||||
'MZ-8BFI': { minRom: 0, maxRom: 0 }
|
||||
'MZ-8BFI': { minRom: 0, maxRom: 0 },
|
||||
'MZ-1R23': { minRom: 0, maxRom: 0 },
|
||||
'MZ-1R37': { minRom: 0, maxRom: 0 },
|
||||
'PIO-3034': { minRom: 0, maxRom: 0 },
|
||||
'Celestite': { minRom: 0, maxRom: 0 },
|
||||
'MZ-1E30': { minRom: 0, maxRom: 1, noLoadAddr: true }
|
||||
};
|
||||
|
||||
// Valid interfaces per driver, derived from firmware source.
|
||||
// Valid interfaces per driver — derived from interfaceFuncMap[] in each
|
||||
// driver source file (src/drivers/Sharp/MZ*.c).
|
||||
var driverInterfaces = {
|
||||
'MZ700': ['RFS', 'MZ-1E05', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ80A': ['RFS', 'MZ80AFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ80B': ['RFS', 'MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ2000': ['RFS', 'MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ1500': ['RFS', 'MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ2200': ['RFS', 'MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18'],
|
||||
'MZ2500': ['RFS', 'MZ-8BFI', 'MZ-1E14', 'MZ-1E19', 'MZ-1R12', 'MZ-1R18']
|
||||
'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'],
|
||||
'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.
|
||||
@@ -398,6 +404,13 @@ var ConfigGUI = (function($) {
|
||||
$tbody.html('<tr><td style="color:#777;">Empty directory</td></tr>');
|
||||
return;
|
||||
}
|
||||
// Sort: ".." first, then directories alphabetically, then files alphabetically.
|
||||
arr.sort(function(a, b) {
|
||||
if (a.name === '..') return -1;
|
||||
if (b.name === '..') return 1;
|
||||
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
});
|
||||
$.each(arr, function(i, entry) {
|
||||
var $tr = $('<tr>');
|
||||
var $td = $('<td style="font-size:12px;cursor:pointer;">');
|
||||
@@ -468,7 +481,7 @@ var ConfigGUI = (function($) {
|
||||
// Core Settings Renderer
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function renderCoreSection(containerId, coreObj, title) {
|
||||
function renderCoreSection(containerId, coreObj, title, isPartition) {
|
||||
var id = uid('core');
|
||||
var html = '<div class="cfg-section">';
|
||||
html += '<div class="panel panel-info">';
|
||||
@@ -503,6 +516,18 @@ var ConfigGUI = (function($) {
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">50 - 166 MHz (warning above 133 MHz)</span>';
|
||||
html += '</div>';
|
||||
|
||||
// z80refresh — only shown for partition cores (not global RP2350 core).
|
||||
if (isPartition) {
|
||||
var refreshChk = coreObj.z80refresh ? ' checked' : '';
|
||||
html += '<div class="form-group">';
|
||||
html += '<label class="col-sm-3 control-label">Z80 Refresh</label>';
|
||||
html += '<div class="col-sm-3">';
|
||||
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="z80refresh"' + refreshChk + ' title="Enable Z80 DRAM refresh cycles during virtual memory fetches"> Enable</div>';
|
||||
html += '</div>';
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">Insert DRAM refresh cycles when executing from virtual ROM/RAM (needed when mixing virtual ROM with physical DRAM)</span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</form>';
|
||||
html += '</div></div></div></div>';
|
||||
|
||||
@@ -513,13 +538,17 @@ var ConfigGUI = (function($) {
|
||||
}
|
||||
|
||||
// Collect core settings from the form back into an object.
|
||||
function collectCore(containerId) {
|
||||
function collectCore(containerId, isPartition) {
|
||||
var $el = $('#' + containerId);
|
||||
return {
|
||||
var obj = {
|
||||
voltage: parseFloat($el.find('[data-field="voltage"]').val()) || 1.10,
|
||||
cpufreq: mhzToFreq($el.find('[data-field="cpufreq"]').val()) || 240000000,
|
||||
psramfreq: mhzToFreq($el.find('[data-field="psramfreq"]').val()) || 133000000
|
||||
};
|
||||
if (isPartition) {
|
||||
obj.z80refresh = $el.find('[data-field="z80refresh"]').is(':checked') ? 1 : 0;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -578,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.
|
||||
@@ -727,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).
|
||||
@@ -791,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 + '">';
|
||||
@@ -800,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;
|
||||
}
|
||||
|
||||
@@ -958,29 +992,50 @@ var ConfigGUI = (function($) {
|
||||
// Param (disk/file) Renderer (for interface param)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Per-interface parameter descriptions: paramHints[interfaceName][paramIndex] = hint text.
|
||||
// Shown as placeholder/title on the param value input to guide the user.
|
||||
var paramHints = {
|
||||
'Celestite': {
|
||||
0: 'MZ-1R12 RAM backing file (e.g. ram/CELESTITE_R12.ram)',
|
||||
1: 'MZ-1R37 EMM backing file (e.g. ram/CELESTITE_R37.ram)'
|
||||
},
|
||||
'MZ-1R12': { 0: 'RAM backing file (e.g. ram/RAMBOARD1.ram)' },
|
||||
'MZ-1R23': { 0: 'Kanji ROM file', 1: 'Dictionary ROM file' }
|
||||
};
|
||||
|
||||
// Known parameter types and how to render their value editor.
|
||||
// Each entry: { label, renderValue(val), collectValue($td) }
|
||||
// New parameter types can be added here as the firmware evolves.
|
||||
var paramTypes = {
|
||||
file: {
|
||||
label: 'File',
|
||||
renderValue: function(val) {
|
||||
renderValue: function(val, hint) {
|
||||
var ph = hint ? ' placeholder="' + hint + '" title="' + hint + '"' : '';
|
||||
return '<div class="input-group" style="width:260px;">'
|
||||
+ '<input type="text" data-field="param-value" value="' + (val || '') + '" style="font-size:11px;">'
|
||||
+ '<input type="text" data-field="param-value" value="' + (val || '') + '"' + ph + ' style="font-size:11px;">'
|
||||
+ '<span class="input-group-btn"><button type="button" class="btn btn-default btn-xs" data-action="browse-file-param" title="Browse" style="height:22px;padding:1px 6px;">'
|
||||
+ '<i class="fa fa-folder-open"></i></button></span></div>';
|
||||
},
|
||||
collectValue: function($td) {
|
||||
return $td.find('[data-field="param-value"]').val() || '';
|
||||
}
|
||||
},
|
||||
ip: {
|
||||
label: 'IP Address',
|
||||
renderValue: function(val) {
|
||||
return '<input type="text" data-field="param-value" value="' + (val || '') + '"'
|
||||
+ ' placeholder="xxx.xxx.xxx.xxx[:port]"'
|
||||
+ ' title="IP address of the netfs fileserver with optional port (e.g. 192.168.1.100 or 192.168.1.100:8080)"'
|
||||
+ ' style="font-size:11px;width:200px;font-family:monospace;">';
|
||||
},
|
||||
collectValue: function($td) {
|
||||
return $td.find('[data-field="param-value"]').val() || '';
|
||||
}
|
||||
}
|
||||
// Future parameter types can be added here, e.g.:
|
||||
// integer: { label: 'Integer', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
|
||||
// string: { label: 'String', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
|
||||
};
|
||||
|
||||
// List of type keys for the dropdown, in display order.
|
||||
var paramTypeOrder = ['file'];
|
||||
var paramTypeOrder = ['file', 'ip'];
|
||||
|
||||
// Detect which param type a config object uses (by checking which known key is present).
|
||||
function detectParamType(paramObj) {
|
||||
@@ -991,23 +1046,23 @@ var ConfigGUI = (function($) {
|
||||
return paramTypeOrder[0]; // default
|
||||
}
|
||||
|
||||
function renderParamTable(params) {
|
||||
function renderParamTable(params, ifName) {
|
||||
if (!params || params.length === 0) return '';
|
||||
var html = '<table class="table table-condensed table-bordered cfg-param-table" data-section="param">';
|
||||
html += '<thead><tr><th title="Enable this parameter">En</th><th title="Parameter type">Type</th><th title="Parameter value">Value</th><th></th></tr></thead>';
|
||||
html += '<tbody>';
|
||||
for (var i = 0; i < params.length; i++) {
|
||||
html += renderParamRow(params[i]);
|
||||
html += renderParamRow(params[i], ifName, i);
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// Always-visible param section with Add button (even when no params exist).
|
||||
function renderParamSection(params) {
|
||||
function renderParamSection(params, ifName) {
|
||||
var html = '<div data-section="params-wrapper" style="margin-top:6px;"><b style="font-size:11px;">Parameters</b>';
|
||||
if (params && params.length > 0) {
|
||||
html += renderParamTable(params);
|
||||
html += renderParamTable(params, ifName);
|
||||
} else {
|
||||
// Empty table ready to receive rows
|
||||
html += '<table class="table table-condensed table-bordered cfg-param-table" data-section="param">';
|
||||
@@ -1019,10 +1074,12 @@ var ConfigGUI = (function($) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderParamRow(paramObj) {
|
||||
function renderParamRow(paramObj, ifName, paramIdx) {
|
||||
var pType = detectParamType(paramObj);
|
||||
var pValue = paramObj[pType] || '';
|
||||
var chk = paramObj.enable ? ' checked' : '';
|
||||
// Look up per-interface hint for this param index.
|
||||
var hint = (ifName && paramHints[ifName] && paramHints[ifName][paramIdx]) ? paramHints[ifName][paramIdx] : '';
|
||||
var html = '<tr data-param-type="' + pType + '">';
|
||||
html += '<td style="width:30px;"><input type="checkbox" data-field="enable"' + chk + ' title="Enable this parameter"></td>';
|
||||
html += '<td style="width:80px;"><select data-field="param-type" style="font-size:11px;width:75px;" title="Parameter type">';
|
||||
@@ -1032,7 +1089,7 @@ var ConfigGUI = (function($) {
|
||||
html += '<option value="' + key + '"' + (key === pType ? ' selected' : '') + '>' + pt.label + '</option>';
|
||||
}
|
||||
html += '</select></td>';
|
||||
html += '<td class="cfg-param-value-cell">' + paramTypes[pType].renderValue(pValue) + '</td>';
|
||||
html += '<td class="cfg-param-value-cell">' + paramTypes[pType].renderValue(pValue, hint) + '</td>';
|
||||
html += '<td style="width:30px;"><button type="button" class="btn btn-danger btn-xs cfg-btn-remove" data-action="remove-row"><i class="fa fa-times"></i></button></td>';
|
||||
html += '</tr>';
|
||||
return html;
|
||||
@@ -1080,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>';
|
||||
@@ -1099,8 +1157,8 @@ var ConfigGUI = (function($) {
|
||||
html += renderIOMapTable(iface.iomap);
|
||||
}
|
||||
|
||||
// Params - always show section with Add button
|
||||
html += renderParamSection(iface.param);
|
||||
// Params - always show section with Add button (pass interface name for hints).
|
||||
html += renderParamSection(iface.param, iface.name);
|
||||
|
||||
html += '</div></div></div>';
|
||||
return html;
|
||||
@@ -1117,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);
|
||||
});
|
||||
@@ -1169,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
|
||||
@@ -1181,17 +1247,37 @@ var ConfigGUI = (function($) {
|
||||
// Bind remove-driver handler
|
||||
$container.on('click', '[data-action="remove-driver"]', function(e) {
|
||||
e.stopPropagation();
|
||||
var name = $(this).closest('[data-driver-idx]').find('[data-field="drv-name"]').val() || 'this driver';
|
||||
if (confirm('Remove driver "' + name + '" and all its interfaces?'))
|
||||
$(this).closest('[data-driver-idx]').remove();
|
||||
var $drv = $(this).closest('[data-driver-idx]');
|
||||
var name = $drv.find('[data-field="drv-name"]').val() || 'this driver';
|
||||
modalConfirm('Remove driver "' + name + '" and all its interfaces?', function() {
|
||||
$drv.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Bind add/remove/browse for driver-level ROM entries.
|
||||
$container.on('click', '[data-action="add-drv-rom"]', function() {
|
||||
var $tbody = $(this).closest('[data-section="drv-roms"]').find('table[data-section="drv-rom"] tbody');
|
||||
$tbody.append(renderDrvRomRow({ enable: 0, file: '' }));
|
||||
});
|
||||
$container.on('click', '[data-action="remove-drv-rom"]', function() {
|
||||
var row = $(this).closest('tr');
|
||||
modalConfirm('Remove this system ROM entry?', function() {
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
$container.on('click', '[data-action="browse-drv-rom"]', function() {
|
||||
var $input = $(this).closest('td').find('[data-field="file"]');
|
||||
openFileBrowser($input);
|
||||
});
|
||||
|
||||
// Bind remove-interface handler
|
||||
$container.on('click', '[data-action="remove-interface"]', function(e) {
|
||||
e.stopPropagation();
|
||||
var ifName = $(this).closest('[data-if-idx]').find('[data-field="if-name"]').first().val() || 'this interface';
|
||||
if (confirm('Remove interface "' + ifName + '"?'))
|
||||
$(this).closest('[data-if-idx]').remove();
|
||||
var $iface = $(this).closest('[data-if-idx]');
|
||||
var ifName = $iface.find('[data-field="if-name"]').first().val() || 'this interface';
|
||||
modalConfirm('Remove interface "' + ifName + '"?', function() {
|
||||
$iface.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// When driver name changes, rebuild the interface dropdown to show only valid interfaces.
|
||||
@@ -1212,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.
|
||||
@@ -1255,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
|
||||
@@ -1304,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.
|
||||
@@ -1332,6 +1420,18 @@ var ConfigGUI = (function($) {
|
||||
});
|
||||
}
|
||||
|
||||
// Render a single driver-level ROM row (simple: enable + file, no loadaddr).
|
||||
function renderDrvRomRow(rom) {
|
||||
var chk = rom.enable ? ' checked' : '';
|
||||
var html = '<tr>';
|
||||
html += '<td style="width:30px;text-align:center;"><input type="checkbox" data-field="enable"' + chk + ' title="Enable this ROM to overwrite the host ROM"></td>';
|
||||
html += '<td><div class="input-group" style="width:280px;"><input type="text" data-field="file" value="' + (rom.file || '') + '" style="font-size:11px;" title="ROM image file path on the SD card">';
|
||||
html += '<span class="input-group-btn"><button type="button" class="btn btn-default btn-xs" data-action="browse-drv-rom" title="Browse SD card for ROM file" style="height:22px;padding:1px 6px;"><i class="fa fa-folder-open"></i></button></span></div></td>';
|
||||
html += '<td style="width:30px;"><button type="button" class="btn btn-danger btn-xs cfg-btn-remove" data-action="remove-drv-rom" title="Remove this ROM entry"><i class="fa fa-times"></i></button></td>';
|
||||
html += '</tr>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderDriver(driver, drvIdx) {
|
||||
var drvId = uid('drv');
|
||||
var chk = driver.enable ? ' checked' : '';
|
||||
@@ -1366,6 +1466,25 @@ var ConfigGUI = (function($) {
|
||||
html += '<label style="font-size:12px;" title="Enable or disable this driver"><input type="checkbox" data-field="drv-enable"' + chk + ' title="Enable this driver"> Enabled</label>';
|
||||
html += '</div>';
|
||||
|
||||
// Driver-level ROMs — system ROMs that overwrite the host's ROMs when enabled.
|
||||
// The driver knows the load layout so no loadaddr is needed, just file + enable.
|
||||
html += '<div data-section="drv-roms" style="margin-bottom:10px;">';
|
||||
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">System ROMs</h5>';
|
||||
html += '<table class="table table-condensed table-bordered cfg-rom-table" data-section="drv-rom" style="width:auto !important;">';
|
||||
html += '<thead><tr>';
|
||||
html += '<th title="Enable loading this ROM to overwrite the host ROM">En</th>';
|
||||
html += '<th title="ROM image file on the SD card">File</th>';
|
||||
html += '<th></th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
if (driver.rom && driver.rom.length > 0) {
|
||||
for (var dr = 0; dr < driver.rom.length; dr++) {
|
||||
html += renderDrvRomRow(driver.rom[dr]);
|
||||
}
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
html += '<button type="button" class="btn btn-success btn-xs cfg-btn-add" data-action="add-drv-rom" title="Add a system ROM file"><i class="fa fa-plus"></i> Add ROM</button>';
|
||||
html += '</div>';
|
||||
|
||||
// Interfaces - always show section with Add button
|
||||
html += '<div data-section="interfaces">';
|
||||
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">Interfaces</h5>';
|
||||
@@ -1398,6 +1517,19 @@ var ConfigGUI = (function($) {
|
||||
type: $drv.find('[data-field="drv-type"]').first().val()
|
||||
};
|
||||
|
||||
// Collect driver-level ROMs
|
||||
var drvRoms = [];
|
||||
$drv.find('table[data-section="drv-rom"] tbody tr').each(function() {
|
||||
var $tr = $(this);
|
||||
drvRoms.push({
|
||||
enable: $tr.find('[data-field="enable"]').is(':checked') ? 1 : 0,
|
||||
file: $tr.find('[data-field="file"]').val() || ''
|
||||
});
|
||||
});
|
||||
if (drvRoms.length > 0) {
|
||||
driver.rom = drvRoms;
|
||||
}
|
||||
|
||||
// Collect interfaces
|
||||
var $ifs = $drv.find('[data-if-idx]');
|
||||
if ($ifs.length > 0) {
|
||||
@@ -1443,6 +1575,46 @@ var ConfigGUI = (function($) {
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">Operating mode</span>';
|
||||
html += '</div>';
|
||||
|
||||
// WiFi settings
|
||||
html += '<hr style="margin:10px 0;">';
|
||||
html += '<div style="font-size:12px;font-weight:bold;margin-bottom:8px;">Network Interfaces</div>';
|
||||
|
||||
var wifiChk = (espCore.wifi_enable === undefined || espCore.wifi_enable) ? ' checked' : '';
|
||||
html += '<div class="form-group">';
|
||||
html += '<label class="col-sm-3 control-label">WiFi Enable</label>';
|
||||
html += '<div class="col-sm-3">';
|
||||
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="wifi_enable"' + wifiChk + ' title="Enable WiFi network interface"> Enable</div>';
|
||||
html += '</div>';
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable WiFi (requires reboot). Ignored if WiFi not compiled in.</span>';
|
||||
html += '</div>';
|
||||
|
||||
// NCM (USB Network) settings
|
||||
|
||||
var ncmChk = (espCore.ncm === undefined || espCore.ncm) ? ' checked' : '';
|
||||
html += '<div class="form-group">';
|
||||
html += '<label class="col-sm-3 control-label">NCM Enable</label>';
|
||||
html += '<div class="col-sm-3">';
|
||||
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="ncm"' + ncmChk + ' title="Enable USB NCM network interface"> Enable</div>';
|
||||
html += '</div>';
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable USB NCM network (requires reboot)</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="form-group">';
|
||||
html += '<label class="col-sm-3 control-label">NCM Retries</label>';
|
||||
html += '<div class="col-sm-3">';
|
||||
html += '<input type="number" class="form-control" data-field="ncmretries" min="0" max="1000" value="' + (espCore.ncmretries || 0) + '" title="Max connection retry cycles (0=forever, 5-1000)">';
|
||||
html += '</div>';
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = retry forever, 5-1000 = max attempts</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="form-group">';
|
||||
html += '<label class="col-sm-3 control-label">NCM Retry Period</label>';
|
||||
html += '<div class="col-sm-3">';
|
||||
html += '<input type="number" class="form-control" data-field="ncmperiod" min="0" max="120" value="' + (espCore.ncmperiod || 0) + '" title="Delay between retries in seconds (0=increasing backoff, 1-120=fixed)">';
|
||||
html += '</div>';
|
||||
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = increasing backoff (1s per retry, max 60s), 1-120 = fixed seconds</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div></div></div></div>';
|
||||
|
||||
@@ -1452,8 +1624,12 @@ var ConfigGUI = (function($) {
|
||||
function collectESP32Core(containerId) {
|
||||
var $el = $('#' + containerId);
|
||||
return {
|
||||
device: $el.find('[data-field="device"]').val() || 'Z80',
|
||||
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0
|
||||
device: $el.find('[data-field="device"]').val() || 'Z80',
|
||||
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0,
|
||||
wifi_enable: $el.find('[data-field="wifi_enable"]').is(':checked') ? 1 : 0,
|
||||
ncm: $el.find('[data-field="ncm"]').is(':checked') ? 1 : 0,
|
||||
ncmretries: parseInt($el.find('[data-field="ncmretries"]').val(), 10) || 0,
|
||||
ncmperiod: parseInt($el.find('[data-field="ncmperiod"]').val(), 10) || 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1570,18 +1746,18 @@ var ConfigGUI = (function($) {
|
||||
var z80 = rp.z80 || [];
|
||||
|
||||
// RP2350 Global Core
|
||||
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings');
|
||||
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings', false);
|
||||
|
||||
// Partition 1 (z80[1])
|
||||
var p1 = z80[1] || {};
|
||||
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings');
|
||||
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings', true);
|
||||
renderMemorySection('cfg-p1-memory', p1.memory || []);
|
||||
renderIOSection('cfg-p1-io', p1.io || []);
|
||||
renderDriversSection('cfg-p1-drivers', p1.drivers || []);
|
||||
|
||||
// Partition 2 (z80[2])
|
||||
var p2 = z80[2] || {};
|
||||
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings');
|
||||
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings', true);
|
||||
renderMemorySection('cfg-p2-memory', p2.memory || []);
|
||||
renderIOSection('cfg-p2-io', p2.io || []);
|
||||
renderDriversSection('cfg-p2-drivers', p2.drivers || []);
|
||||
@@ -1602,20 +1778,20 @@ var ConfigGUI = (function($) {
|
||||
function collectConfig() {
|
||||
var cfg = {
|
||||
rp2350: {
|
||||
core: collectCore('cfg-rp2350-core'),
|
||||
core: collectCore('cfg-rp2350-core', false),
|
||||
z80: [
|
||||
// z80[0] = Bootloader (currently empty)
|
||||
{},
|
||||
// z80[1] = Partition 1
|
||||
{
|
||||
core: collectCore('cfg-p1-core'),
|
||||
core: collectCore('cfg-p1-core', true),
|
||||
memory: collectMemory('cfg-p1-memory'),
|
||||
io: collectIO('cfg-p1-io'),
|
||||
drivers: collectDrivers('cfg-p1-drivers')
|
||||
},
|
||||
// z80[2] = Partition 2
|
||||
{
|
||||
core: collectCore('cfg-p2-core'),
|
||||
core: collectCore('cfg-p2-core', true),
|
||||
memory: collectMemory('cfg-p2-memory'),
|
||||
io: collectIO('cfg-p2-io'),
|
||||
drivers: collectDrivers('cfg-p2-drivers')
|
||||
@@ -1711,41 +1887,48 @@ var ConfigGUI = (function($) {
|
||||
var v = parseInt($(this).val(), 10);
|
||||
if (v > 133) warnings.push('PSRAM Frequency ' + v + ' MHz exceeds 133 MHz — device may not boot');
|
||||
});
|
||||
if (warnings.length > 0) {
|
||||
if (!confirm('Warnings:\n\n' + warnings.join('\n') + '\n\nSave anyway?')) {
|
||||
return;
|
||||
}
|
||||
function doSave() {
|
||||
var cfg = collectConfig();
|
||||
var json = JSON.stringify(cfg, null, 2);
|
||||
|
||||
$('#cfgSaveBtn').prop('disabled', true);
|
||||
showMsg('Saving configuration...', 'alert-info');
|
||||
|
||||
$.ajax({
|
||||
url: '/data/config',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: json,
|
||||
success: function(response) {
|
||||
var msg = '';
|
||||
if (typeof response === 'string') {
|
||||
msg = response;
|
||||
} else if (response && response.message) {
|
||||
msg = response.message;
|
||||
} else {
|
||||
msg = 'Configuration saved successfully.';
|
||||
}
|
||||
showMsg(msg, 'alert-success');
|
||||
$('#cfgSaveBtn').prop('disabled', false);
|
||||
// Reload the config from the device to reflect the saved state.
|
||||
setTimeout(function() { fetchConfig(); }, 1000);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var msg = jqXHR.responseText || 'Failed to save configuration: ' + (errorThrown || textStatus);
|
||||
showMsg(msg, 'alert-danger');
|
||||
$('#cfgSaveBtn').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var cfg = collectConfig();
|
||||
var json = JSON.stringify(cfg, null, 2);
|
||||
if (warnings.length > 0) {
|
||||
modalConfirm('Warnings:\n\n' + warnings.join('\n') + '\n\nSave anyway?', function() {
|
||||
doSave();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#cfgSaveBtn').prop('disabled', true);
|
||||
showMsg('Saving configuration...', 'alert-info');
|
||||
|
||||
$.ajax({
|
||||
url: '/data/config',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: json,
|
||||
success: function(response) {
|
||||
var msg = '';
|
||||
if (typeof response === 'string') {
|
||||
msg = response;
|
||||
} else if (response && response.message) {
|
||||
msg = response.message;
|
||||
} else {
|
||||
msg = 'Configuration saved successfully.';
|
||||
}
|
||||
showMsg(msg, 'alert-success');
|
||||
$('#cfgSaveBtn').prop('disabled', false);
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
var msg = jqXHR.responseText || 'Failed to save configuration: ' + (errorThrown || textStatus);
|
||||
showMsg(msg, 'alert-danger');
|
||||
$('#cfgSaveBtn').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
doSave();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
246
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
246
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
@@ -1,5 +1,19 @@
|
||||
var lastStatus = 0;
|
||||
|
||||
// Backup the entire SD card as a tar download.
|
||||
function backupSD() {
|
||||
modalConfirm('Download the entire SD card as a tar archive?\n\nThis may take several minutes depending on the SD card size and connection speed.', function() {
|
||||
// Create a temporary hidden link to trigger the download.
|
||||
var a = document.createElement('a');
|
||||
a.href = '/data/backup';
|
||||
a.download = 'picoZ80_backup.tar';
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm delete with "Don't ask again" checkbox.
|
||||
// Uses localStorage to persist the preference across sessions.
|
||||
// To re-enable: localStorage.removeItem('skipDeleteConfirm') in browser console.
|
||||
@@ -94,19 +108,19 @@ function uploadFile() {
|
||||
|
||||
if (fileInput.length == 0)
|
||||
{
|
||||
alert("No file selected!");
|
||||
modalAlert("No file selected!");
|
||||
} else if (filePath.length == 0)
|
||||
{
|
||||
alert("File path on server is not set!");
|
||||
modalAlert("File path on server is not set!");
|
||||
} else if (filePath.indexOf(' ') >= 0)
|
||||
{
|
||||
alert("File path on server cannot have spaces!");
|
||||
modalAlert("File path on server cannot have spaces!");
|
||||
} else if (filePath[filePath.length-1] == '/')
|
||||
{
|
||||
alert("File name not specified after path!");
|
||||
modalAlert("File name not specified after path!");
|
||||
} else if (fileInput[0].size > MAX_FILE_SIZE)
|
||||
{
|
||||
alert("File size must be less than "+MAX_FILE_SIZE_STR+"!");
|
||||
modalAlert("File size must be less than "+MAX_FILE_SIZE_STR+"!");
|
||||
} else
|
||||
{
|
||||
document.getElementById("newfile").disabled = true;
|
||||
@@ -126,6 +140,11 @@ function uploadFile() {
|
||||
var fileSize = file.size;
|
||||
var xhttp = new XMLHttpRequest();
|
||||
|
||||
// Detect if the file is an archive that the server will unpack.
|
||||
var fileName = file.name.toLowerCase();
|
||||
var isArchive = fileName.endsWith('.tar') || fileName.endsWith('.gz') ||
|
||||
fileName.endsWith('.tgz') || fileName.endsWith('.tar.gz');
|
||||
|
||||
// Track upload progress.
|
||||
xhttp.upload.addEventListener("progress", function(e) {
|
||||
if (e.lengthComputable && progressBar && progressText) {
|
||||
@@ -143,26 +162,36 @@ function uploadFile() {
|
||||
}
|
||||
});
|
||||
|
||||
// When upload transfer completes, show "Unpacking..." for archives while
|
||||
// waiting for the server to finish processing and send its response.
|
||||
xhttp.upload.addEventListener("load", function() {
|
||||
if (isArchive && progressBar && progressText) {
|
||||
progressBar.style.width = "100%";
|
||||
progressBar.style.background = "#f0ad4e"; // amber/orange
|
||||
progressText.textContent = "Unpacking archive on device... please wait";
|
||||
}
|
||||
});
|
||||
|
||||
xhttp.onreadystatechange = function()
|
||||
{
|
||||
if (xhttp.readyState == 4)
|
||||
{
|
||||
if (xhttp.status == 200)
|
||||
{
|
||||
if (progressBar) progressBar.style.width = "100%";
|
||||
if (progressText) progressText.textContent = "Upload complete!";
|
||||
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#5cb85c"; }
|
||||
if (progressText) progressText.textContent = isArchive ? "Unpacked successfully!" : "Upload complete!";
|
||||
setTimeout(function() { location.reload(); }, 1000);
|
||||
} else if (xhttp.status == 0)
|
||||
{
|
||||
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#d9534f"; }
|
||||
if (progressText) progressText.textContent = "Upload failed — connection lost";
|
||||
alert("Upload failed, server closed the connection abruptly!");
|
||||
modalAlert("Upload failed — server closed the connection abruptly!");
|
||||
location.reload();
|
||||
} else
|
||||
{
|
||||
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#d9534f"; }
|
||||
if (progressText) progressText.textContent = "Upload failed — error " + xhttp.status;
|
||||
alert(xhttp.status + " Error!\n" + xhttp.responseText);
|
||||
modalAlert(xhttp.status + " Error!\n" + xhttp.responseText);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
@@ -208,7 +237,7 @@ function downloadFile(event) {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Download failed:", error);
|
||||
alert("Failed to download the file.");
|
||||
modalAlert("Failed to download the file.");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,6 +277,201 @@ function mkdir() {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to create directory');
|
||||
modalAlert('Failed to create directory');
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle all file-select checkboxes.
|
||||
function toggleSelectAll(master) {
|
||||
var checkboxes = document.querySelectorAll('.file-select');
|
||||
checkboxes.forEach(function(cb) { cb.checked = master.checked; });
|
||||
updateDeleteSelectedBtn();
|
||||
}
|
||||
|
||||
// Update the "Delete Selected" button visibility and count.
|
||||
function updateDeleteSelectedBtn() {
|
||||
var checked = document.querySelectorAll('.file-select:checked');
|
||||
var btn = document.getElementById('deleteSelectedBtn');
|
||||
var countSpan = document.getElementById('deleteSelectedCount');
|
||||
if (btn) {
|
||||
btn.style.display = checked.length > 0 ? '' : 'none';
|
||||
}
|
||||
if (countSpan) {
|
||||
countSpan.textContent = checked.length;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all selected files sequentially using the same modal style as single delete.
|
||||
function deleteSelected() {
|
||||
var checked = document.querySelectorAll('.file-select:checked');
|
||||
if (checked.length === 0) return;
|
||||
|
||||
var names = [];
|
||||
checked.forEach(function(cb) { names.push(cb.getAttribute('data-name')); });
|
||||
|
||||
var dir = checked[0].getAttribute('data-dir');
|
||||
|
||||
// Build file list HTML (max 15 shown, then "and N more...")
|
||||
var listHtml = '<div style="text-align:left;max-height:200px;overflow-y:auto;margin:10px 0;' +
|
||||
'padding:8px 12px;background:#1a1a1a;border-radius:4px;font-size:12px;line-height:1.6;">';
|
||||
var showCount = Math.min(names.length, 15);
|
||||
for (var i = 0; i < showCount; i++) {
|
||||
listHtml += '<div>' + names[i] + '</div>';
|
||||
}
|
||||
if (names.length > 15) {
|
||||
listHtml += '<div style="color:#999;">...and ' + (names.length - 15) + ' more</div>';
|
||||
}
|
||||
listHtml += '</div>';
|
||||
|
||||
// Show modal overlay matching the single-delete style.
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'delConfirmOverlay';
|
||||
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;' +
|
||||
'background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
||||
overlay.innerHTML =
|
||||
'<div style="background:#2c2c2c;border:1px solid #555;border-radius:8px;padding:20px 30px;' +
|
||||
'max-width:450px;color:#eee;font-size:14px;text-align:center;">' +
|
||||
'<p style="margin-bottom:5px;">Delete <b>' + names.length + ' selected item(s)</b>?</p>' +
|
||||
listHtml +
|
||||
'<button id="delMultiOk" style="margin-right:10px;padding:6px 20px;cursor:pointer;' +
|
||||
'background:#c33;color:#fff;border:1px solid #a22;border-radius:4px;">Delete All</button>' +
|
||||
'<button id="delMultiCancel" style="padding:6px 20px;cursor:pointer;' +
|
||||
'background:#555;color:#fff;border:1px solid #444;border-radius:4px;">Cancel</button></div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
document.getElementById('delMultiOk').onclick = function() {
|
||||
// Replace modal content with a prominent progress display.
|
||||
var total = names.length;
|
||||
overlay.querySelector('div').innerHTML =
|
||||
'<p style="margin-bottom:12px;color:#eee;font-size:16px;font-weight:bold;">' +
|
||||
'<i class="fa fa-trash" style="color:#c33;margin-right:8px;"></i>Deleting ' + total + ' item(s)...</p>' +
|
||||
'<div style="background:#444;border-radius:4px;height:20px;margin:12px 0;overflow:hidden;">' +
|
||||
'<div id="delMultiBar" style="background:#c33;height:100%;width:0%;transition:width 0.2s;border-radius:4px;"></div></div>' +
|
||||
'<p id="delMultiCount" style="color:#ccc;font-size:13px;margin:8px 0;">0 / ' + total + '</p>' +
|
||||
'<p id="delMultiName" style="color:#999;font-size:11px;word-break:break-all;min-height:16px;"></p>';
|
||||
|
||||
var idx = 0;
|
||||
var errors = 0;
|
||||
function deleteNext() {
|
||||
if (idx >= total) {
|
||||
// Show completion briefly before reloading.
|
||||
var bar = document.getElementById('delMultiBar');
|
||||
var count = document.getElementById('delMultiCount');
|
||||
var fname = document.getElementById('delMultiName');
|
||||
if (bar) bar.style.background = '#5cb85c';
|
||||
if (count) count.textContent = 'Complete! Deleted ' + (total - errors) + ' of ' + total + ' item(s).';
|
||||
if (count) count.style.color = '#5cb85c';
|
||||
if (fname) fname.textContent = errors > 0 ? errors + ' failed' : 'Reloading...';
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(overlay);
|
||||
location.reload();
|
||||
}, 1200);
|
||||
return;
|
||||
}
|
||||
var pct = Math.round(((idx + 1) / total) * 100);
|
||||
var bar = document.getElementById('delMultiBar');
|
||||
var count = document.getElementById('delMultiCount');
|
||||
var fname = document.getElementById('delMultiName');
|
||||
if (bar) bar.style.width = pct + '%';
|
||||
if (count) count.textContent = (idx + 1) + ' / ' + total;
|
||||
if (fname) fname.textContent = names[idx];
|
||||
|
||||
var name = names[idx++];
|
||||
var url = '?cmd=del&name=' + encodeURIComponent(name) + '&dir=' + encodeURIComponent(dir);
|
||||
// Use a small timeout to allow the browser to repaint the progress bar.
|
||||
setTimeout(function() {
|
||||
fetch(url).then(function() { deleteNext(); }).catch(function() { errors++; deleteNext(); });
|
||||
}, 50);
|
||||
}
|
||||
deleteNext();
|
||||
};
|
||||
|
||||
document.getElementById('delMultiCancel').onclick = function() {
|
||||
document.body.removeChild(overlay);
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client-side column sorting for the file manager table.
|
||||
// Clicking a column header toggles ascending/descending sort.
|
||||
// The first two rows (up-level link + upload controls) are kept at the top;
|
||||
// only the file/directory data rows are sorted.
|
||||
// ---------------------------------------------------------------------------
|
||||
(function() {
|
||||
// Track current sort state.
|
||||
var currentCol = -1;
|
||||
var ascending = true;
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var th = e.target.closest('th[data-sort-col]');
|
||||
if (!th) return;
|
||||
|
||||
var col = parseInt(th.getAttribute('data-sort-col'), 10);
|
||||
var sortType = th.getAttribute('data-sort-type') || 'text';
|
||||
var table = th.closest('table');
|
||||
if (!table) return;
|
||||
|
||||
var tbody = table.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
// Toggle direction if clicking the same column, otherwise ascending.
|
||||
if (col === currentCol) {
|
||||
ascending = !ascending;
|
||||
} else {
|
||||
currentCol = col;
|
||||
ascending = true;
|
||||
}
|
||||
|
||||
// Collect all rows. The first rows may be [up level] and upload controls —
|
||||
// identify data rows by having a form with cmd=ren (the rename form).
|
||||
var allRows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
|
||||
var fixedRows = [];
|
||||
var dataRows = [];
|
||||
allRows.forEach(function(row) {
|
||||
if (row.querySelector('input[name="oldname"]')) {
|
||||
dataRows.push(row);
|
||||
} else {
|
||||
fixedRows.push(row);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort data rows.
|
||||
dataRows.sort(function(a, b) {
|
||||
var cellA = a.cells[col];
|
||||
var cellB = b.cells[col];
|
||||
if (!cellA || !cellB) return 0;
|
||||
|
||||
var valA, valB;
|
||||
if (sortType === 'num') {
|
||||
// Extract numeric value (from size column text content).
|
||||
valA = parseFloat(cellA.textContent.replace(/[^0-9.-]/g, '')) || 0;
|
||||
valB = parseFloat(cellB.textContent.replace(/[^0-9.-]/g, '')) || 0;
|
||||
} else {
|
||||
// Text sort: use the input value for column 0 (name), textContent otherwise.
|
||||
var inputA = cellA.querySelector('input[name="name"]');
|
||||
var inputB = cellB.querySelector('input[name="name"]');
|
||||
valA = (inputA ? inputA.value : cellA.textContent).toLowerCase();
|
||||
valB = (inputB ? inputB.value : cellB.textContent).toLowerCase();
|
||||
}
|
||||
|
||||
if (valA < valB) return ascending ? -1 : 1;
|
||||
if (valA > valB) return ascending ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Re-append rows: fixed rows first, then sorted data rows.
|
||||
fixedRows.forEach(function(row) { tbody.appendChild(row); });
|
||||
dataRows.forEach(function(row) { tbody.appendChild(row); });
|
||||
|
||||
// Update sort indicator icons.
|
||||
table.querySelectorAll('th[data-sort-col] .fa').forEach(function(icon) {
|
||||
icon.className = 'fa fa-sort';
|
||||
icon.style.color = '#999';
|
||||
});
|
||||
var icon = th.querySelector('.fa');
|
||||
if (icon) {
|
||||
icon.className = ascending ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
|
||||
icon.style.color = '#333';
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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.43
|
||||
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>
|
||||
|
||||
10
projects/tzpuPico/src/CMakeLists.txt
vendored
10
projects/tzpuPico/src/CMakeLists.txt
vendored
@@ -15,8 +15,12 @@ set(pZ80_common_src
|
||||
|
||||
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
|
||||
@@ -27,6 +31,12 @@ set(pZ80_drivers_sharp_src
|
||||
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1E19.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R12.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R18.c
|
||||
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ-1R23.c
|
||||
${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
|
||||
|
||||
@@ -462,6 +462,42 @@ int ESP_readBurstSectors(const char *fileName, int filePos, uint8_t *memLocation
|
||||
return (int) payloadLen;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ESP_readDir — read a directory listing from the ESP32 SD card.
|
||||
// path: directory path relative to /sdcard/ (e.g., "roms" or "dsk/MZ2000")
|
||||
// buf: output buffer for null-terminated text listing
|
||||
// bufLen: size of buf
|
||||
// Returns: bytes written to buf (excluding null), or -1 on error.
|
||||
// ---------------------------------------------------------------------------
|
||||
int ESP_readDir(const char *path, char *buf, int bufLen)
|
||||
{
|
||||
uint32_t payloadLen = 0;
|
||||
// Pass path as filename — empty string sends "/" for root.
|
||||
const char *sendPath = (path && path[0]) ? path : "/";
|
||||
|
||||
// Retry up to 3 times — first SPI command after idle can fail.
|
||||
uint8_t *payload = NULL;
|
||||
for (int retry = 0; retry < 3 && !payload; retry++)
|
||||
{
|
||||
payloadLen = 0;
|
||||
payload = FSPI_sendBinaryCmd(
|
||||
IPCF_CMD_DIR, sendPath, 0, 0, 0, (uint8_t) retry, NULL, 0, &payloadLen);
|
||||
}
|
||||
|
||||
if (!payload || payloadLen == 0)
|
||||
return -1;
|
||||
|
||||
t_IpcFrameHdr *resp = (t_IpcFrameHdr *)(payload - IPCF_HEADER_SIZE);
|
||||
if (resp->status != IPCF_STATUS_OK)
|
||||
return -1;
|
||||
|
||||
int copyLen = ((int) payloadLen < (bufLen - 1))
|
||||
? (int) payloadLen : (bufLen - 1);
|
||||
memcpy(buf, payload, copyLen);
|
||||
buf[copyLen] = '\0';
|
||||
return copyLen;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ESP_writeFile — write an entire buffer to a file on the ESP32 SD card.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -613,3 +649,101 @@ float ESP_version(void)
|
||||
{
|
||||
return ESP_VERSION;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Network functions for Celestite W5100 emulation (Phase 2).
|
||||
// These are called from Core 0 (main loop) to send IPC commands to ESP32.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool ESP_netCfg(uint8_t *ipOut, uint8_t *gwOut, uint8_t *subnetOut, uint8_t *macOut)
|
||||
{
|
||||
uint32_t payloadLen = 0;
|
||||
uint8_t *payload = FSPI_sendBinaryCmd(IPCF_CMD_NET_CFG, "", 0, 0, 0, gIpcSeq++, NULL, 0, &payloadLen);
|
||||
if (payload == NULL || payloadLen < 18)
|
||||
return false;
|
||||
// Response payload: IP[4] + GW[4] + Subnet[4] + MAC[6] = 18 bytes.
|
||||
if (ipOut) memcpy(ipOut, payload, 4);
|
||||
if (gwOut) memcpy(gwOut, payload + 4, 4);
|
||||
if (subnetOut) memcpy(subnetOut, payload + 8, 4);
|
||||
if (macOut) memcpy(macOut, payload + 12, 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP_netSocket(uint8_t sockNum, uint8_t operation, uint8_t protocol, uint32_t ipAddr, uint16_t port, uint8_t *statusOut)
|
||||
{
|
||||
// Pack params into IPC frame fields:
|
||||
// diskNo=sockNum, sectorCount=operation, fileOffset=ipAddr
|
||||
// filename[0..1]=port (big-endian), filename[2]=protocol
|
||||
char params[IPCF_FILENAME_LEN];
|
||||
memset(params, 0, sizeof(params));
|
||||
params[0] = (char)(port >> 8);
|
||||
params[1] = (char)(port & 0xFF);
|
||||
params[2] = (char)protocol;
|
||||
|
||||
uint32_t payloadLen = 0;
|
||||
uint8_t *payload = FSPI_sendBinaryCmd(IPCF_CMD_NET_SOCK, params, ipAddr, operation, sockNum, gIpcSeq++, NULL, 0, &payloadLen);
|
||||
if (payload == NULL)
|
||||
return false;
|
||||
// Response payload: 1 byte = new socket status.
|
||||
if (statusOut && payloadLen >= 1)
|
||||
*statusOut = payload[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP_netSend(uint8_t sockNum, uint8_t *data, uint32_t len, uint32_t *sentOut)
|
||||
{
|
||||
if (data == NULL || len == 0)
|
||||
return false;
|
||||
// Cap at max payload.
|
||||
if (len > IPCF_MAX_PAYLOAD)
|
||||
len = IPCF_MAX_PAYLOAD;
|
||||
|
||||
uint32_t payloadLen = 0;
|
||||
uint8_t *payload = FSPI_sendBinaryCmd(IPCF_CMD_NET_SEND, "", 0, 0, sockNum, gIpcSeq++, data, len, &payloadLen);
|
||||
if (payload == NULL)
|
||||
return false;
|
||||
// Response payload: 4 bytes = bytes sent (little-endian).
|
||||
if (sentOut && payloadLen >= 4)
|
||||
{
|
||||
*sentOut = payload[0] | (payload[1] << 8) | (payload[2] << 16) | (payload[3] << 24);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP_netRecv(uint8_t sockNum, uint8_t *buf, uint32_t bufSize, uint32_t *recvdOut)
|
||||
{
|
||||
if (buf == NULL || bufSize == 0)
|
||||
return false;
|
||||
|
||||
// Request recv: sectorCount = requested size (capped at max payload).
|
||||
uint16_t reqSize = (uint16_t)(bufSize > IPCF_MAX_PAYLOAD ? IPCF_MAX_PAYLOAD : bufSize);
|
||||
|
||||
uint32_t payloadLen = 0;
|
||||
uint8_t *payload = FSPI_sendBinaryCmd(IPCF_CMD_NET_RECV, "", 0, reqSize, sockNum, gIpcSeq++, NULL, 0, &payloadLen);
|
||||
if (payload == NULL)
|
||||
return false;
|
||||
// Response payload = received data.
|
||||
if (payloadLen > 0 && payloadLen <= bufSize)
|
||||
{
|
||||
memcpy(buf, payload, payloadLen);
|
||||
}
|
||||
if (recvdOut)
|
||||
*recvdOut = payloadLen;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP_netPing(uint32_t ipAddr, uint32_t *rttOut)
|
||||
{
|
||||
uint32_t payloadLen = 0;
|
||||
// fileOffset carries the target IP.
|
||||
uint8_t *payload = FSPI_sendBinaryCmd(IPCF_CMD_NET_PING, "", ipAddr, 0, 0, gIpcSeq++, NULL, 0, &payloadLen);
|
||||
if (payload == NULL)
|
||||
return false;
|
||||
// RTT encoded in response header sectorCount field (no payload).
|
||||
t_IpcFrameHdr *resp = (t_IpcFrameHdr *)(payload - IPCF_HEADER_SIZE);
|
||||
if (rttOut)
|
||||
{
|
||||
*rttOut = resp->sectorCount; // RTT in ms, 0xFFFF = timeout.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -423,8 +423,8 @@ uint8_t *FSPI_sendBinaryCmd(uint8_t opcode,
|
||||
cmd->fileOffset = fileOffset;
|
||||
cmd->diskNo = diskNo;
|
||||
cmd->payloadLen = (uint16_t) (writeData ? writeLen : 0);
|
||||
if (filename && filename[0])
|
||||
strncpy(cmd->filename, filename, IPCF_FILENAME_LEN - 1);
|
||||
if (filename)
|
||||
memcpy(cmd->filename, filename, IPCF_FILENAME_LEN);
|
||||
|
||||
// === Transaction 1: Send 64-byte command header ===
|
||||
// Flush any stale RX FIFO bytes left by a previous failed transaction.
|
||||
@@ -490,7 +490,12 @@ uint8_t *FSPI_sendBinaryCmd(uint8_t opcode,
|
||||
// read commands: header + sectorCount × 512 + CRC32
|
||||
// write commands: header + CRC32 (just status, no payload returned)
|
||||
// INF command: header + CRC32
|
||||
uint32_t respPayload = (writeData == NULL && sectorCount > 0) ? (uint32_t) sectorCount * IPCF_SECTOR_SIZE : 0;
|
||||
// DIR command: header + variable payload + CRC32 (use max frame size)
|
||||
uint32_t respPayload;
|
||||
if (opcode == IPCF_CMD_DIR)
|
||||
respPayload = IPCF_MAX_PAYLOAD; // variable-length — allocate max
|
||||
else
|
||||
respPayload = (writeData == NULL && sectorCount > 0) ? (uint32_t) sectorCount * IPCF_SECTOR_SIZE : 0;
|
||||
uint32_t respSize = IPCF_HEADER_SIZE + respPayload + IPCF_CRC_SIZE;
|
||||
if (respSize > IPCF_MAX_FRAME_SIZE)
|
||||
respSize = IPCF_MAX_FRAME_SIZE;
|
||||
|
||||
@@ -50,8 +50,12 @@
|
||||
// Include Device drivers.
|
||||
#ifdef INCLUDE_SHARP_DRIVERS
|
||||
#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
|
||||
|
||||
|
||||
@@ -103,8 +107,8 @@ volatile uint8_t g_dbgForceIOWait;
|
||||
// IRQ 0 - SM0 - ADDRESS LOAD
|
||||
// IRQ 1 - SM1 - DATA LOAD
|
||||
static uint sm_addr = PIO_SM_0; // 4 words
|
||||
static uint sm_data = PIO_SM_1; // 6 words
|
||||
// 10 total words
|
||||
static uint sm_data = PIO_SM_1; // 7 words
|
||||
// 11 total words
|
||||
// PIO 1 state machines.
|
||||
// IRQ 0 - SM0 - CYCLE START
|
||||
// IRQ 5 - SM3 - REFRESH
|
||||
@@ -206,10 +210,18 @@ static const t_VirtualFuncMap virtualFuncMap[] = {
|
||||
#ifdef INCLUDE_SHARP_DRIVERS
|
||||
{"MZ700", (t_VirtualFunc) MZ700_Init}, // This virtual function creates a Sharp MZ700 'persona' whereby
|
||||
// it sets up devices and memory structure to replicate the MZ700 logic.
|
||||
{"MZ1500", (t_VirtualFunc) MZ1500_Init}, // This virtual function creates a Sharp MZ-1500 'persona' whereby
|
||||
// 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]);
|
||||
@@ -626,6 +638,45 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// Driver-level "rom" array
|
||||
/////////////////////////////
|
||||
|
||||
// Optional ROM files at the driver level (not interface level).
|
||||
// These are system ROMs where the driver knows the load layout (no loadaddr needed).
|
||||
cJSON *drvROMArr = cJSON_GetObjectItem(driver, "rom");
|
||||
if (cJSON_IsArray(drvROMArr))
|
||||
{
|
||||
size_t drvROMCnt = cJSON_GetArraySize(drvROMArr);
|
||||
if (drvROMCnt > 0)
|
||||
{
|
||||
if (!cpu->_drivers.driver[validDrivers].romConfig)
|
||||
cpu->_drivers.driver[validDrivers].romConfig = (t_drvROMConfig *) calloc(drvROMCnt, sizeof(t_drvROMConfig));
|
||||
if (cpu->_drivers.driver[validDrivers].romConfig)
|
||||
{
|
||||
int validDrvROMs = 0;
|
||||
for (size_t ri = 0; ri < drvROMCnt; ri++)
|
||||
{
|
||||
cJSON *romItem = cJSON_GetArrayItem(drvROMArr, ri);
|
||||
if (!cJSON_IsObject(romItem))
|
||||
continue;
|
||||
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
|
||||
if (!cJSON_IsNumber(romEnable) || romEnable->valueint != 1)
|
||||
continue;
|
||||
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
|
||||
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
|
||||
continue;
|
||||
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romFile = strdup(romFile->valuestring);
|
||||
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddrCount = 0;
|
||||
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddr = NULL;
|
||||
validDrvROMs++;
|
||||
}
|
||||
cpu->_drivers.driver[validDrivers].romCount = validDrvROMs;
|
||||
debugf("Z80 Drivers ROM: Driver ROM Maps(%d)\r\n", validDrvROMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////
|
||||
// "if" array
|
||||
//////////////
|
||||
@@ -800,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;
|
||||
}
|
||||
@@ -975,14 +1025,24 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
// Extract known key/value pairs. Each param entry has one key
|
||||
// that identifies the parameter type ("file", "ip", etc.).
|
||||
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
|
||||
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
|
||||
|
||||
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
|
||||
bool hasValue = false;
|
||||
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
|
||||
{
|
||||
continue;
|
||||
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
|
||||
hasValue = true;
|
||||
}
|
||||
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
|
||||
if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
|
||||
{
|
||||
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].ip = strdup(ip->valuestring);
|
||||
hasValue = true;
|
||||
}
|
||||
if (!hasValue)
|
||||
continue;
|
||||
validParams++;
|
||||
}
|
||||
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParamCount = validParams;
|
||||
@@ -1620,6 +1680,79 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
|
||||
}
|
||||
debugf("Z80 Drivers: enable(%d) name(%s) physical(%s)\r\n", enableDriver->valueint, driverName->valuestring, driverIsPhysical->valuestring);
|
||||
|
||||
/////////////////////////////
|
||||
// Driver-level "rom" array
|
||||
/////////////////////////////
|
||||
|
||||
// Driver-level ROM files (no loadaddr — the driver knows the layout).
|
||||
// Parse and store to flash so Z80CPU_ReadROM can find them at startup.
|
||||
cJSON *drvROMDrv = cJSON_GetObjectItem(driver, "rom");
|
||||
if (cJSON_IsArray(drvROMDrv))
|
||||
{
|
||||
size_t drvROMDrvCount = cJSON_GetArraySize(drvROMDrv);
|
||||
for (size_t ri = 0; ri < drvROMDrvCount; ri++)
|
||||
{
|
||||
cJSON *romItem = cJSON_GetArrayItem(drvROMDrv, ri);
|
||||
if (!cJSON_IsObject(romItem))
|
||||
continue;
|
||||
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
|
||||
if (cJSON_IsNumber(romEnable) && romEnable->valueint != 1)
|
||||
continue;
|
||||
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
|
||||
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
|
||||
continue;
|
||||
|
||||
debugf("Z80 Drivers ROM: file(%s)\r\n", romFile->valuestring);
|
||||
|
||||
// Store to flash (same pattern as interface ROMs).
|
||||
if (appConfig->s.romCount >= (FLASH_MAX_ROM_IMAGES - 1))
|
||||
{
|
||||
debugf("Error: No free slots in ROM Header for file:%s\r\n", romFile->valuestring);
|
||||
continue;
|
||||
}
|
||||
int found = -1;
|
||||
for (int idx = 0; idx < appConfig->s.romCount; idx++)
|
||||
{
|
||||
if (strcmp(appConfig->s.ROMS[idx].filename, romFile->valuestring) == 0)
|
||||
found = idx;
|
||||
}
|
||||
if (found >= 0)
|
||||
{
|
||||
debugf("Warning: ROM (file=%s) already stored in slot %d, skipping.\r\n", romFile->valuestring, found);
|
||||
continue;
|
||||
}
|
||||
|
||||
memset((uint8_t *) romBuffer, 0, FLASH_MAX_ROM_IMAGE_SIZE);
|
||||
watchdog_update();
|
||||
debugf("Reading driver ROM file:%s\r\n", romFile->valuestring);
|
||||
int fileSize = ESP_readFile(romFile->valuestring, NULL, NULL, NULL, &romBuffer, FLASH_MAX_ROM_IMAGE_SIZE, false, 0);
|
||||
watchdog_update();
|
||||
if (fileSize <= 0)
|
||||
{
|
||||
debugf("Error: ROM file '%s' not found or empty\r\n", romFile->valuestring);
|
||||
continue;
|
||||
}
|
||||
if ((romNextPos + fileSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
|
||||
{
|
||||
debugf("Error: ROM Image (%s, %dbytes) exceeds available FlashRAM storage\r\n", romFile->valuestring, fileSize);
|
||||
continue;
|
||||
}
|
||||
uint32_t flashSize = ((fileSize % FLASH_SECTOR_SIZE) == 0) ? fileSize : ((fileSize / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
|
||||
if ((romNextPos + flashSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
|
||||
{
|
||||
debugf("Error: ROM Image (%s) rounded up to sector exceeds FlashRAM\r\n", romFile->valuestring);
|
||||
continue;
|
||||
}
|
||||
updateFlashBytes(romNextPos, (uint8_t *) romBuffer, flashSize);
|
||||
appConfig->s.ROMS[appConfig->s.romCount].size = fileSize;
|
||||
appConfig->s.ROMS[appConfig->s.romCount].addr = romNextPos;
|
||||
strcpy(appConfig->s.ROMS[appConfig->s.romCount].filename, romFile->valuestring);
|
||||
romNextPos += flashSize;
|
||||
appConfig->s.romCount++;
|
||||
debugf("Z80 Drivers ROM: stored %d bytes to flash slot %d\r\n", fileSize, appConfig->s.romCount - 1);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////
|
||||
// "if" array
|
||||
////////////////
|
||||
@@ -2008,16 +2141,24 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
// Extract known key/value pairs.
|
||||
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
|
||||
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
|
||||
|
||||
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
|
||||
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
|
||||
{
|
||||
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
|
||||
}
|
||||
else if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
|
||||
{
|
||||
debugf("Z80 Drivers IF Params: ip(%s)\r\n", ip->valuestring);
|
||||
}
|
||||
else
|
||||
{
|
||||
debugf("Error: Virtual driver \"param\" item %zu, driver %zu invalid, skipping\r\n", i7, drvConfig);
|
||||
result = false;
|
||||
continue;
|
||||
}
|
||||
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2036,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));
|
||||
|
||||
@@ -2222,7 +2364,11 @@ int Z80CPU_init_pio(void)
|
||||
}
|
||||
pio_sm_set_enabled(pio_0, sm_addr, true);
|
||||
|
||||
// Data Bus - Setup data on the bus, tri-state on BUSACK.
|
||||
// Data Bus - Setup data on the bus, tristate after /WR completes.
|
||||
// After driving data, the SM waits for /WR low then high, ensuring the
|
||||
// data bus is held for the entire write cycle (including wait states),
|
||||
// then tristates. This prevents stale data persisting on the physical
|
||||
// bus in virtual mode where no subsequent bus cycle would clear it.
|
||||
offset_data = pio_add_program(pio_0, &z80_data_program);
|
||||
pio_sm_config c_data = z80_data_program_get_default_config(offset_data);
|
||||
sm_config_set_out_pins(&c_data, Z80_PIN_DATA_0, 8);
|
||||
@@ -2230,6 +2376,13 @@ int Z80CPU_init_pio(void)
|
||||
sm_config_set_out_shift(&c_data, true, true, 24); // Shift right, auto pull 24 bits (8 final direction, 8 data, 8 start direction).
|
||||
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_DATA_0, 8, false); // Set as input initially.
|
||||
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_BUSACK, 1, false);
|
||||
// Enable internal pull-ups on D0-D7. A real Z80 has weak internal pull-ups
|
||||
// on its data bus; when no device is driving, the bus drifts to 0xFF.
|
||||
// Without pull-ups the RP2350 GPIOs are true high-Z — bus capacitance holds
|
||||
// the last driven value indefinitely, causing false device detection in
|
||||
// virtual mode (e.g. IPL FDC probe writes 0xA5, reads it back after 80µs).
|
||||
for (uint i = Z80_PIN_DATA_0; i < Z80_PIN_DATA_0 + 8; i++)
|
||||
gpio_pull_up(i);
|
||||
sm_config_set_clkdiv(&c_data, 1.0f);
|
||||
pio_sm_clear_fifos(pio_0, sm_data);
|
||||
pio_sm_restart(pio_0, sm_data);
|
||||
@@ -2435,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)
|
||||
@@ -2560,7 +2714,7 @@ void __func_in_RAM(Z80CPU_task)(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char
|
||||
|
||||
// Method to reset the Z80 CPU.
|
||||
// This method is directly invoked to reset the Z80 Emulation or invoked via the hardware reset line going active low.
|
||||
void __func_in_RAM(Z80CPU_reset)(t_Z80CPU *cpu)
|
||||
void __func_in_RAM(Z80CPU_reset)(t_Z80CPU *cpu, bool physicalReset)
|
||||
{
|
||||
#ifdef DEBUG_PIO
|
||||
debugf("Resetting Z80\n");
|
||||
@@ -2569,21 +2723,24 @@ void __func_in_RAM(Z80CPU_reset)(t_Z80CPU *cpu)
|
||||
// Reset the CPU emulation.
|
||||
z80_instant_reset(&cpu->_Z80);
|
||||
|
||||
// Call driver reset handlers first — they return 1 to allow hardware reset
|
||||
// (default), or 0 to suppress it. Results are AND'd: if ANY driver returns 0,
|
||||
// the hardware reset is suppressed. Drivers running in physical mode on hosts
|
||||
// that generate their own /RESET (e.g., MZ-2000 NST) return 0 to prevent the
|
||||
// picoZ80 from driving /RESET and interfering with the host's reset sequence.
|
||||
uint8_t doHwReset = 1;
|
||||
// Call driver reset handlers to reset driver state. For physical resets,
|
||||
// drivers return 1 to allow hardware reset or 0 to suppress it (e.g., MZ-2000
|
||||
// in physical mode returns 0 because the gate array manages /RESET for
|
||||
// NST/BST transitions). For software resets (web page reboot, dbgsh reset
|
||||
// command), always drive /RESET regardless of driver opinion.
|
||||
uint8_t doHwReset = physicalReset ? 0 : 1;
|
||||
for (int idx = 0; idx < cpu->_drivers.drvCount; idx++)
|
||||
{
|
||||
if (cpu->_drivers.driver[idx].resetPtr != NULL)
|
||||
{
|
||||
doHwReset &= cpu->_drivers.driver[idx].resetPtr(cpu);
|
||||
uint8_t drvResult = cpu->_drivers.driver[idx].resetPtr(cpu);
|
||||
if (physicalReset)
|
||||
doHwReset |= drvResult; // OR: any driver requesting HW reset enables it
|
||||
}
|
||||
}
|
||||
|
||||
// Only drive /RESET on the bus if all drivers agree.
|
||||
// Drive /RESET on the bus for software resets, or if a driver requests it
|
||||
// for physical resets.
|
||||
if (doHwReset)
|
||||
Z80CPU_ForceHostResetActive(cpu, true);
|
||||
|
||||
@@ -2621,12 +2778,16 @@ 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);
|
||||
|
||||
sleep_ms(2000);
|
||||
// This is a setup delay, basically to allow the ESP32 to come online and serve any floppy/qd images. If
|
||||
// immediate Z80 start is required and your not worried about the Floppy/QD being available immediately,
|
||||
// remote this delay.
|
||||
sleep_ms(250);
|
||||
|
||||
// Loop forever, running Z80 instructions and processing external signals.
|
||||
while (1)
|
||||
@@ -2776,7 +2937,7 @@ void __func_in_RAM(Z80CPU_cpu)(t_Z80CPU *cpu)
|
||||
cpu->_Z80.sp.uint16_value);
|
||||
#endif
|
||||
|
||||
Z80CPU_reset(cpu);
|
||||
Z80CPU_reset(cpu, physicalReset);
|
||||
cpu->forceReset = false;
|
||||
|
||||
if (physicalReset)
|
||||
@@ -3018,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3154,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.
|
||||
@@ -3166,36 +3331,40 @@ uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
|
||||
|
||||
switch (cpu->_Z80.im)
|
||||
{
|
||||
case 0:
|
||||
debugf("INT IM0 not implemented.\r\n");
|
||||
break;
|
||||
// Interrupt Mode 0 - Fetch instruction opcode from the data bus via INTA cycle.
|
||||
// The interrupting device places an instruction byte on the bus during /M1+/IORQ.
|
||||
// For multi-byte instructions, the Z80 library calls inta again for prefix bytes
|
||||
// (CB/ED/DD/FD) and int_fetch for operand bytes (addresses, displacements).
|
||||
case 0:
|
||||
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
debugf("ReadInt:%04x, mode:%02x\r\n", address, cpu->_Z80.im);
|
||||
// Get the vector from the bus, used with I register to form the address of the ISR.
|
||||
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
|
||||
break;
|
||||
// Interrupt Mode 2 - Fetch the address to branch to from the bus.
|
||||
case 2:
|
||||
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
default:
|
||||
break;
|
||||
// Interrupt Mode 1 - Basic branch to 0x0038
|
||||
case 1:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
intAckCount++;
|
||||
|
||||
return (vector);
|
||||
}
|
||||
|
||||
// Special IM0 fetch to retrieve an instruction off the data bus.
|
||||
// Currently not implemented.
|
||||
// IM0 fetch for subsequent bytes of multi-byte instructions injected onto the data bus.
|
||||
// Called by the Z80 library's im0_fetch trampoline for operand bytes (e.g. the 16-bit
|
||||
// address in CALL/JP, displacement in JR/DJNZ). On real hardware these are memory read
|
||||
// M-cycles, but the interrupting device (e.g. Amstrad PCW MPU) drives the data bus
|
||||
// regardless of cycle type. Using INTA bus cycles here is safe and matches the device
|
||||
// behaviour — it reads whatever the device puts on the bus.
|
||||
uint8_t __func_in_RAM(Z80CPU_fetchIntAckIM0)(void *context, uint16_t address)
|
||||
{
|
||||
// Locals.
|
||||
Z_UNUSED(context)
|
||||
uint8_t opcode = 0;
|
||||
//t_Z80CPU* cpu = (t_Z80CPU*)context;
|
||||
//z80_int(&cpu->_Z80, false);
|
||||
|
||||
debugf("FetchIntAckIM0 - not yet implemented, addr:%04x\r\n", address);
|
||||
return (opcode);
|
||||
t_Z80CPU *cpu = (t_Z80CPU *)context;
|
||||
return Z80CPU_fetchPhysicalIntVector(cpu, address);
|
||||
}
|
||||
|
||||
uint8_t __func_in_RAM(Z80CPU_nmia)(void *context, uint16_t address)
|
||||
@@ -3209,28 +3378,32 @@ uint8_t __func_in_RAM(Z80CPU_nmia)(void *context, uint16_t address)
|
||||
|
||||
void __func_in_RAM(Z80CPU_ldia)(void *context)
|
||||
{
|
||||
// Locals.
|
||||
Z_UNUSED(context)
|
||||
debugf("LDIA\r\n");
|
||||
// I register changed (LD I,A). Don't reset m1DelayCount here — some
|
||||
// software (CP/M) changes I frequently during normal operation, and
|
||||
// resetting the delay each time permanently blocks interrupts.
|
||||
// The delay is one-shot: it protects the initial boot sequence, then
|
||||
// interrupts are allowed forever regardless of I changes.
|
||||
}
|
||||
|
||||
void __func_in_RAM(Z80CPU_ldra)(void *context)
|
||||
{
|
||||
// Locals.
|
||||
Z_UNUSED(context)
|
||||
debugf("LDRA\r\n");
|
||||
//debugf("LDRA\r\n");
|
||||
}
|
||||
|
||||
// Z80 RETI instruction, ie. return from interrupt.
|
||||
// This is the DEFAULT handler. Drivers that need custom RETI behaviour (e.g.
|
||||
// physical bus RETI sequence for gate array in_service tracking) directly
|
||||
// overwrite cpu->_Z80.reti with their own handler in Init.
|
||||
void __func_in_RAM(Z80CPU_reti)(void *context)
|
||||
{
|
||||
// Locals.
|
||||
t_Z80CPU *cpu = (t_Z80CPU *) context;
|
||||
|
||||
// Normally clear the emulation interrupt state, but the Z80 INT line is level active and if the interrupt routine
|
||||
// hasnt reset the trigger or a new interrupt has come in, then we reinterrupt.
|
||||
// Update the Z80 INT line — INT is level-sensitive, if the line is still
|
||||
// active after RETI the interrupt will be re-taken immediately.
|
||||
z80_int(&cpu->_Z80, (((sio_hw_t *) 0xd0000000)->gpio_hi_in & 2) == 0);
|
||||
//debugf("RETI\r\n");
|
||||
}
|
||||
|
||||
// Z80 RETN instruction, ie. return from nmi interrupt.
|
||||
@@ -3247,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1327,26 +1482,28 @@ static void cmdSaveHist(t_Z80CPU *cpu, int argc, char **argv)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command: load — load a file from SD card into memory
|
||||
// load <p|v> <filename> <addr> <len> [fileofs]
|
||||
// load <p|v> <filename> <addr> [len] [fileofs]
|
||||
// p = physical bus write, v = virtual PSRAM (bank 0)
|
||||
// filename is relative to /sdcard/ on the ESP32
|
||||
// len is optional — if omitted, loads the entire file (up to 64KB)
|
||||
// ---------------------------------------------------------------------------
|
||||
static void cmdLoad(t_Z80CPU *cpu, int argc, char **argv)
|
||||
{
|
||||
if (!cpu) { shPuts("CPU not initialised.\r\n"); return; }
|
||||
|
||||
if (argc < 5)
|
||||
if (argc < 4)
|
||||
{
|
||||
shPuts("Usage: load <p|v> <filename> <addr> <len> [fileofs]\r\n"
|
||||
shPuts("Usage: load <p|v> <filename> <addr> [len] [fileofs]\r\n"
|
||||
" p = physical bus write, v = virtual PSRAM bank 0\r\n"
|
||||
" filename relative to /sdcard/ on ESP32\r\n");
|
||||
" filename relative to /sdcard/ on ESP32\r\n"
|
||||
" len optional — omit to load entire file (max 64KB)\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
bool physical = (argv[1][0] == 'p' || argv[1][0] == 'P');
|
||||
const char *fileName = argv[2];
|
||||
uint32_t addr = (uint32_t) strtoul(argv[3], NULL, 0);
|
||||
uint32_t len = (uint32_t) strtoul(argv[4], NULL, 0);
|
||||
uint32_t len = (argc >= 5) ? (uint32_t) strtoul(argv[4], NULL, 0) : 0x10000;
|
||||
int fileOfs = (argc >= 6) ? (int) strtoul(argv[5], NULL, 0) : 0;
|
||||
|
||||
if (len == 0 || len > 0x100000)
|
||||
@@ -1432,7 +1589,7 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
|
||||
if (argc < 5)
|
||||
{
|
||||
shPuts("Usage: save <p|pf|v> <filename> <addr> <len>\r\n"
|
||||
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM bank 0\r\n"
|
||||
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM (full addr, e.g. 0x27D000 = bank 39)\r\n"
|
||||
" filename relative to /sdcard/ on ESP32\r\n");
|
||||
return;
|
||||
}
|
||||
@@ -1488,7 +1645,8 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
|
||||
}
|
||||
else
|
||||
{
|
||||
scratch[i] = cpu->_z80PSRAM->RAM[z80Addr];
|
||||
// Use full 32-bit address for virtual PSRAM access (supports bank > 0).
|
||||
scratch[i] = cpu->_z80PSRAM->RAM[(addr + i) % sizeof(cpu->_z80PSRAM->RAM)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1511,6 +1669,51 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
|
||||
// Phase 2 — In-Circuit Emulation commands
|
||||
// ===========================================================================
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Command: dir — list SD card directory
|
||||
// dir — list root directory
|
||||
// dir <path> — list given path (relative to /sdcard/)
|
||||
// ---------------------------------------------------------------------------
|
||||
static void cmdDir(t_Z80CPU *cpu, int argc, char **argv)
|
||||
{
|
||||
(void) cpu;
|
||||
const char *path = (argc >= 2) ? argv[1] : "";
|
||||
|
||||
// Strip leading '/' — the ESP32 prepends /sdcard/.
|
||||
while (*path == '/')
|
||||
path++;
|
||||
|
||||
static char dirBuf[4096];
|
||||
int len = ESP_readDir(path, dirBuf, sizeof(dirBuf));
|
||||
|
||||
if (len < 0)
|
||||
{
|
||||
shPrintf("Failed to read directory '%s'.\r\n", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
shPrintf("Directory '%s' is empty.\r\n", path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Output line by line with \r\n conversion.
|
||||
shPrintf("Directory: /%s\r\n", path);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
if (dirBuf[i] == '\n')
|
||||
shPuts("\r\n");
|
||||
else
|
||||
shWrite(&dirBuf[i], 1);
|
||||
|
||||
if ((i & 0xFF) == 0xFF)
|
||||
shFlush();
|
||||
}
|
||||
if (len > 0 && dirBuf[len - 1] != '\n')
|
||||
shPuts("\r\n");
|
||||
}
|
||||
|
||||
// Helper: ensure CPU is held before debug operations that inspect state.
|
||||
static bool ensureHeld(t_Z80CPU *cpu)
|
||||
{
|
||||
@@ -2193,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)
|
||||
{
|
||||
@@ -2209,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)
|
||||
@@ -2262,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
|
||||
@@ -3946,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},
|
||||
@@ -3964,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},
|
||||
@@ -3976,11 +4197,16 @@ 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},
|
||||
{"load", "Load file: load <p|v> <file> <addr> [len] [ofs]", cmdLoad},
|
||||
{"save", "Save mem: save <p|pf|v> <file> <addr> <len>", cmdSaveFile},
|
||||
{"dir", "List SD card directory: dir [path]", cmdDir},
|
||||
{"echo", "Toggle character echo [on|off]", cmdEcho},
|
||||
{"hist", "Show command history: hist [n]", cmdHist},
|
||||
{"savehst", "Force-save history to ESP32 now", cmdSaveHist},
|
||||
|
||||
1322
projects/tzpuPico/src/drivers/Sharp/Celestite.c
Normal file
1322
projects/tzpuPico/src/drivers/Sharp/Celestite.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
379
projects/tzpuPico/src/drivers/Sharp/MZ-1R23.c
Normal file
379
projects/tzpuPico/src/drivers/Sharp/MZ-1R23.c
Normal file
@@ -0,0 +1,379 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ-1R23.c
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - MZ-1R23 Kanji ROM / MZ-1R24 Dictionary ROM Board
|
||||
// This file contains setup and driver to emulate the MZ-1R23 Kanji ROM board and
|
||||
// the MZ-1R24 Dictionary ROM board. The two boards share the same I/O interface
|
||||
// (B8h control, B9h address/data) and can coexist — the KANJI bit in the control
|
||||
// register selects which ROM is active.
|
||||
//
|
||||
// ROM files are loaded from the ESP32 SD card at init time. The JSON config
|
||||
// specifies filenames via ifParam entries:
|
||||
// params[0].file = kanji ROM filename (e.g. "MZ-1R23.ROM")
|
||||
// params[1].file = dictionary ROM filename (optional, e.g. "MZ-1R24.ROM")
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write.
|
||||
//
|
||||
// Notes: See Makefile to enable/disable conditional components
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// This source file is free software: you can redistribute it and#or modify
|
||||
// it under the terms of the GNU General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This source file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "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/MZ-1R23.h"
|
||||
#include "debug.h"
|
||||
|
||||
t_MZ1R23 *mz1r23Ctrl; // Control structure for the MZ-1R23/MZ-1R24 board.
|
||||
|
||||
// Reverse bits in a byte (for ENDIAN mode).
|
||||
static inline uint8_t reverseBits(uint8_t b)
|
||||
{
|
||||
b = (uint8_t)(((b & 0xF0) >> 4) | ((b & 0x0F) << 4));
|
||||
b = (uint8_t)(((b & 0xCC) >> 2) | ((b & 0x33) << 2));
|
||||
b = (uint8_t)(((b & 0xAA) >> 1) | ((b & 0x55) << 1));
|
||||
return b;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Interface: Sharp MZ-1R23 Kanji ROM Board / MZ-1R24 Dictionary ROM Board
|
||||
// Description: Read-only ROM boards accessed via I/O ports B8h (control) and B9h (address/data).
|
||||
// Kanji ROM provides 16x16 JIS kanji bitmaps (32 bytes per character).
|
||||
// Dictionary ROM provides up to 4 × 64KB banks of data.
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
// Method to initialise the CPU state to support an MZ-1R23/MZ-1R24 interface.
|
||||
uint8_t MZ1R23_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 (mz1r23Ctrl != NULL)
|
||||
return (0);
|
||||
|
||||
// Allocate control structure.
|
||||
mz1r23Ctrl = (t_MZ1R23 *) calloc(1, sizeof(t_MZ1R23));
|
||||
if (!mz1r23Ctrl)
|
||||
{
|
||||
debugf("MZ-1R23: Failed to allocate control structure\r\n");
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Initialise state.
|
||||
mz1r23Ctrl->ctrlReg = MZ1R23_CTRL_KANJI; // Default to Kanji mode.
|
||||
mz1r23Ctrl->readAddr = 0;
|
||||
|
||||
// Load ROM files from SD card if specified in the configuration.
|
||||
// params[0] = kanji ROM file, params[1] = dictionary ROM file (optional).
|
||||
for (int paramIdx = 0; paramIdx < config->ifParamCount && paramIdx < 2; paramIdx++)
|
||||
{
|
||||
if (config->ifParam[paramIdx].file == NULL || config->ifParam[paramIdx].file[0] == '\0')
|
||||
continue;
|
||||
|
||||
if (paramIdx == 0)
|
||||
{
|
||||
// Kanji ROM.
|
||||
mz1r23Ctrl->kanjiRom = (uint8_t *) calloc(1, MZ1R23_KANJI_ROM_MAX);
|
||||
if (mz1r23Ctrl->kanjiRom == NULL)
|
||||
{
|
||||
debugf("MZ-1R23: Failed to allocate kanji ROM buffer (%d bytes)\r\n", MZ1R23_KANJI_ROM_MAX);
|
||||
continue;
|
||||
}
|
||||
mz1r23Ctrl->kanjiRomSize = MZ1R23_KANJI_ROM_MAX;
|
||||
mz1r23Ctrl->kanjiFileName = strdup(config->ifParam[paramIdx].file);
|
||||
mz1r23Ctrl->kanjiLoadCtl = MZ1R23_NOT_LOADED;
|
||||
mz1r23Ctrl->kanjiLoadPending = true;
|
||||
|
||||
// Queue inter-core load request.
|
||||
t_CoreMsg msg = {.type = MSG_LOAD_RAMFILE, .context = mz1r23Ctrl, .requestId = 0};
|
||||
strncpy(msg.fileOp.filename, mz1r23Ctrl->kanjiFileName, MAX_IC_FILENAME_LEN - 1);
|
||||
msg.fileOp.buffer = mz1r23Ctrl->kanjiRom;
|
||||
msg.fileOp.size = MZ1R23_KANJI_ROM_MAX;
|
||||
queue_try_add(&cpu->requestQueue, &msg);
|
||||
|
||||
debugf("MZ-1R23: Queued kanji ROM load: %s\r\n", mz1r23Ctrl->kanjiFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dictionary ROM.
|
||||
mz1r23Ctrl->dictRom = (uint8_t *) calloc(1, MZ1R23_DICT_ROM_MAX);
|
||||
if (mz1r23Ctrl->dictRom == NULL)
|
||||
{
|
||||
debugf("MZ-1R24: Failed to allocate dictionary ROM buffer (%d bytes)\r\n", MZ1R23_DICT_ROM_MAX);
|
||||
continue;
|
||||
}
|
||||
mz1r23Ctrl->dictRomSize = MZ1R23_DICT_ROM_MAX;
|
||||
mz1r23Ctrl->dictFileName = strdup(config->ifParam[paramIdx].file);
|
||||
mz1r23Ctrl->dictLoadCtl = MZ1R23_NOT_LOADED;
|
||||
mz1r23Ctrl->dictLoadPending = true;
|
||||
|
||||
// Queue inter-core load request.
|
||||
t_CoreMsg msg = {.type = MSG_LOAD_RAMFILE, .context = mz1r23Ctrl, .requestId = 1};
|
||||
strncpy(msg.fileOp.filename, mz1r23Ctrl->dictFileName, MAX_IC_FILENAME_LEN - 1);
|
||||
msg.fileOp.buffer = mz1r23Ctrl->dictRom;
|
||||
msg.fileOp.size = MZ1R23_DICT_ROM_MAX;
|
||||
queue_try_add(&cpu->requestQueue, &msg);
|
||||
|
||||
debugf("MZ-1R24: Queued dictionary ROM load: %s\r\n", mz1r23Ctrl->dictFileName);
|
||||
}
|
||||
}
|
||||
|
||||
// Install I/O handlers.
|
||||
for (int idx = 0; idx < IO_PAGE_SIZE; idx++)
|
||||
{
|
||||
// B8h — Control register (write only).
|
||||
if ((idx & 0x00ff) == 0xB8)
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1R23_IO_Ctrl;
|
||||
// B9h — Address write / Data read.
|
||||
if ((idx & 0x00ff) == 0xB9)
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1R23_IO_Data;
|
||||
}
|
||||
|
||||
debugf("MZ-1R23: Initialised (kanji=%s, dict=%s)\r\n",
|
||||
mz1r23Ctrl->kanjiFileName ? mz1r23Ctrl->kanjiFileName : "none",
|
||||
mz1r23Ctrl->dictFileName ? mz1r23Ctrl->dictFileName : "none");
|
||||
|
||||
result = 1;
|
||||
return (result);
|
||||
}
|
||||
|
||||
// Reset handler.
|
||||
uint8_t MZ1R23_Reset(t_Z80CPU *cpu)
|
||||
{
|
||||
(void)cpu;
|
||||
|
||||
if (mz1r23Ctrl)
|
||||
{
|
||||
mz1r23Ctrl->ctrlReg = MZ1R23_CTRL_KANJI;
|
||||
mz1r23Ctrl->readAddr = 0;
|
||||
}
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Poll handler — drain any pending inter-core load responses.
|
||||
uint8_t __func_in_RAM(MZ1R23_PollCB)(t_Z80CPU *cpu)
|
||||
{
|
||||
if (mz1r23Ctrl && (mz1r23Ctrl->kanjiLoadPending || mz1r23Ctrl->dictLoadPending))
|
||||
{
|
||||
MZ1R23_ProcessQueueResponses(cpu);
|
||||
}
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Process inter-core queue responses for pending ROM loads.
|
||||
int MZ1R23_ProcessQueueResponses(t_Z80CPU *cpu)
|
||||
{
|
||||
int processedCount = 0;
|
||||
|
||||
if (cpu == NULL || mz1r23Ctrl == NULL)
|
||||
return (-1);
|
||||
|
||||
while (true)
|
||||
{
|
||||
t_CoreMsg response;
|
||||
if (!queue_try_remove(&cpu->responseQueue, &response))
|
||||
break;
|
||||
|
||||
// If the response is not for us, push it back.
|
||||
if (response.context != (void *) mz1r23Ctrl)
|
||||
{
|
||||
queue_add_blocking(&cpu->responseQueue, &response);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (response.type)
|
||||
{
|
||||
case MSG_LOAD_COMPLETE:
|
||||
if (response.requestId == 0)
|
||||
{
|
||||
// Kanji ROM load complete.
|
||||
mz1r23Ctrl->kanjiLoadPending = false;
|
||||
if (response.response.success)
|
||||
{
|
||||
mz1r23Ctrl->kanjiLoadCtl = MZ1R23_LOADED;
|
||||
debugf("MZ-1R23: Kanji ROM loaded (%d bytes)\r\n", (int)response.response.size);
|
||||
if (response.response.size < mz1r23Ctrl->kanjiRomSize)
|
||||
mz1r23Ctrl->kanjiRomSize = response.response.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
debugf("MZ-1R23: Kanji ROM load FAILED\r\n");
|
||||
}
|
||||
}
|
||||
else if (response.requestId == 1)
|
||||
{
|
||||
// Dictionary ROM load complete.
|
||||
mz1r23Ctrl->dictLoadPending = false;
|
||||
if (response.response.success)
|
||||
{
|
||||
mz1r23Ctrl->dictLoadCtl = MZ1R23_LOADED;
|
||||
debugf("MZ-1R24: Dictionary ROM loaded (%d bytes)\r\n", (int)response.response.size);
|
||||
if (response.response.size < mz1r23Ctrl->dictRomSize)
|
||||
mz1r23Ctrl->dictRomSize = response.response.size;
|
||||
}
|
||||
else
|
||||
{
|
||||
debugf("MZ-1R24: Dictionary ROM load FAILED\r\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
processedCount++;
|
||||
}
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
// Task processor — no external tasks currently handled.
|
||||
uint8_t MZ1R23_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)param;
|
||||
uint8_t result = 0;
|
||||
|
||||
switch (task)
|
||||
{
|
||||
default:
|
||||
result = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return (result);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for B8h — Control register (write only).
|
||||
// Bit 7: KANJI (1=Kanji ROM, 0=Dictionary ROM)
|
||||
// Bit 6: ENDIAN (1=bit-reversed byte order)
|
||||
// Bits 0-1: BANK (dictionary ROM bank, 00-11)
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(MZ1R23_IO_Ctrl)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
if (read)
|
||||
return 0xFF; // Write-only register.
|
||||
|
||||
if (mz1r23Ctrl)
|
||||
{
|
||||
mz1r23Ctrl->ctrlReg = data;
|
||||
mz1r23Ctrl->readAddr = 0; // Changing mode resets the read address.
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for B9h — Address write / Data read.
|
||||
//
|
||||
// Write (via OUT (C),A):
|
||||
// The Z80 B register provides A15-A8 (upper address byte) in the port
|
||||
// address MSB, and the A register provides D7-D0 (lower byte) as data.
|
||||
// - Kanji mode: the 16-bit value is the kanji pattern number.
|
||||
// Internal read address is set to patternNumber × 32.
|
||||
// - Dictionary mode: the 16-bit value is a raw byte address within
|
||||
// the selected bank.
|
||||
//
|
||||
// Read:
|
||||
// Returns the byte at the current read address, then auto-increments.
|
||||
// In Kanji mode, the 32 bytes of a character are read in order:
|
||||
// upper-left (8), lower-left (8), upper-right (8), lower-right (8).
|
||||
// If ENDIAN bit is set, each byte is bit-reversed before returning.
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(MZ1R23_IO_Data)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
|
||||
if (mz1r23Ctrl == NULL)
|
||||
return 0xFF;
|
||||
|
||||
if (read)
|
||||
{
|
||||
// Data read with auto-increment.
|
||||
uint8_t result = 0xFF;
|
||||
|
||||
if (mz1r23Ctrl->ctrlReg & MZ1R23_CTRL_KANJI)
|
||||
{
|
||||
// Kanji ROM read.
|
||||
if (mz1r23Ctrl->kanjiRom && mz1r23Ctrl->readAddr < mz1r23Ctrl->kanjiRomSize)
|
||||
{
|
||||
result = mz1r23Ctrl->kanjiRom[mz1r23Ctrl->readAddr];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dictionary ROM read.
|
||||
uint8_t bank = mz1r23Ctrl->ctrlReg & MZ1R23_CTRL_BANK_MASK;
|
||||
uint32_t dictAddr = (uint32_t)bank * MZ1R23_DICT_BANK_SIZE + mz1r23Ctrl->readAddr;
|
||||
if (mz1r23Ctrl->dictRom && dictAddr < mz1r23Ctrl->dictRomSize)
|
||||
{
|
||||
result = mz1r23Ctrl->dictRom[dictAddr];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply ENDIAN bit-reversal if set.
|
||||
if (mz1r23Ctrl->ctrlReg & MZ1R23_CTRL_ENDIAN)
|
||||
{
|
||||
result = reverseBits(result);
|
||||
}
|
||||
|
||||
mz1r23Ctrl->readAddr++;
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Address write via OUT (C),A.
|
||||
// Upper address byte comes from B register (port address MSB).
|
||||
// Lower address byte comes from A register (data).
|
||||
uint16_t addrVal = (uint16_t)((addr >> 8) & 0xFF) << 8 | data;
|
||||
|
||||
if (mz1r23Ctrl->ctrlReg & MZ1R23_CTRL_KANJI)
|
||||
{
|
||||
// Kanji mode: addrVal is the pattern number.
|
||||
// Internal byte address = pattern number × 32.
|
||||
mz1r23Ctrl->readAddr = (uint32_t)addrVal * MZ1R23_PATTERN_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dictionary mode: addrVal is a raw byte address within the bank.
|
||||
mz1r23Ctrl->readAddr = addrVal;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
341
projects/tzpuPico/src/drivers/Sharp/MZ-1R37.c
Normal file
341
projects/tzpuPico/src/drivers/Sharp/MZ-1R37.c
Normal file
@@ -0,0 +1,341 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ-1R37.c
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - MZ-1R37 640K EMM
|
||||
// This file contains setup and driver to emulate the MZ-1R37 640KB Expanded Memory
|
||||
// Manager board.
|
||||
//
|
||||
// Unlike the PIO-3034, this board has NO auto-increment. All addressing is done
|
||||
// through the Z80 B register (port address MSB) combined with data byte:
|
||||
//
|
||||
// To write byte X to EMM address 0xABCDE:
|
||||
// LD BC, 0x0AAC ; B=0x0A (addr[19:16]), C=0xAC (port)
|
||||
// LD A, 0xBC ; A=0xBC (addr[15:8])
|
||||
// OUT (C), A ; Latch addr[19:8] = 0x0ABC
|
||||
// LD BC, 0xDEAD ; B=0xDE (addr[7:0]), C=0xAD (port)
|
||||
// LD A, X ; A=data byte
|
||||
// OUT (C), A ; Write X to address 0xABCDE
|
||||
//
|
||||
// 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
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// This source file is free software: you can redistribute it and#or modify
|
||||
// it under the terms of the GNU General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This source file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "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/MZ-1R37.h"
|
||||
#include "debug.h"
|
||||
|
||||
t_MZ1R37 *mz1r37Ctrl; // Control structure for the MZ-1R37 EMM board.
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// 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
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
|
||||
{
|
||||
uint8_t result = 0;
|
||||
|
||||
if (config->isPhysical)
|
||||
return (0);
|
||||
|
||||
if (mz1r37Ctrl != NULL)
|
||||
return (0);
|
||||
|
||||
mz1r37Ctrl = (t_MZ1R37 *) calloc(1, sizeof(t_MZ1R37));
|
||||
if (!mz1r37Ctrl)
|
||||
return (0);
|
||||
|
||||
mz1r37Ctrl->ram = (uint8_t *) calloc(1, MZ1R37_RAM_SIZE);
|
||||
if (mz1r37Ctrl->ram == NULL)
|
||||
{
|
||||
debugf("MZ-1R37: Failed to allocate %d bytes RAM\r\n", MZ1R37_RAM_SIZE);
|
||||
free(mz1r37Ctrl);
|
||||
mz1r37Ctrl = NULL;
|
||||
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++)
|
||||
{
|
||||
mz1r37Ctrl->ioBase = (uint8_t)config->ioMap[remapIdx].dstAddr;
|
||||
}
|
||||
mz1r37Ctrl->addrLatch = 0;
|
||||
|
||||
// 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++)
|
||||
{
|
||||
uint8_t port = idx & 0xFF;
|
||||
if (port == base)
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1R37_IO_AddrLatch;
|
||||
else if (port == (uint8_t)(base + 1))
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) MZ1R37_IO_Data;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
uint8_t MZ1R37_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)param;
|
||||
uint8_t result = 0;
|
||||
|
||||
switch (task)
|
||||
{
|
||||
default:
|
||||
result = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return (result);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
if (mz1r37Ctrl)
|
||||
{
|
||||
uint8_t addrHi = (uint8_t)((addr >> 8) & 0x0F);
|
||||
mz1r37Ctrl->addrLatch = ((uint32_t)addrHi << 16) | ((uint32_t)data << 8);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 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)
|
||||
{
|
||||
(void)cpu;
|
||||
|
||||
if (mz1r37Ctrl == NULL)
|
||||
return 0xFF;
|
||||
|
||||
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
|
||||
{
|
||||
if (fullAddr < MZ1R37_RAM_SIZE)
|
||||
{
|
||||
mz1r37Ctrl->ram[fullAddr] = data;
|
||||
dirtyPageSet(fullAddr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
1376
projects/tzpuPico/src/drivers/Sharp/MZ1500.c
Normal file
1376
projects/tzpuPico/src/drivers/Sharp/MZ1500.c
Normal file
File diff suppressed because it is too large
Load Diff
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);
|
||||
}
|
||||
@@ -51,7 +51,6 @@
|
||||
#include "intercore.h"
|
||||
#include "debug.h"
|
||||
#include "drivers/Sharp/MZ.h"
|
||||
#include "drivers/Sharp/RFS.h"
|
||||
#include "drivers/Sharp/WD1773.h"
|
||||
#include "drivers/Sharp/QDDrive.h"
|
||||
#include "drivers/Sharp/MZ8BFI.h"
|
||||
@@ -59,17 +58,24 @@
|
||||
#include "drivers/Sharp/MZ-1E19.h"
|
||||
#include "drivers/Sharp/MZ-1R12.h"
|
||||
#include "drivers/Sharp/MZ-1R18.h"
|
||||
#include "drivers/Sharp/MZ-1R23.h"
|
||||
#include "drivers/Sharp/MZ-1R37.h"
|
||||
#include "drivers/Sharp/PIO-3034.h"
|
||||
#include "drivers/Sharp/Celestite.h"
|
||||
#include "drivers/Sharp/MZ2000.h"
|
||||
|
||||
// Map to contain interface name against method to initialise it.
|
||||
static t_InterfaceFuncMap interfaceFuncMap[] = {
|
||||
{"RFS", false, RFS_Init, RFS_Reset, RFS_PollCB, RFS_TaskProcessor},
|
||||
{"MZ-8BFI", false, MZ8BFI_Init, MZ8BFI_Reset, MZ8BFI_PollCB, MZ8BFI_TaskProcessor},
|
||||
{"E0054PA", false, MZ8BFI_Init, MZ8BFI_Reset, MZ8BFI_PollCB, MZ8BFI_TaskProcessor},
|
||||
{"MZ-1E14", false, MZ1E14_Init, MZ1E14_Reset, MZ1E14_PollCB, MZ1E14_TaskProcessor},
|
||||
{"MZ-1E19", false, MZ1E19_Init, MZ1E19_Reset, MZ1E19_PollCB, MZ1E19_TaskProcessor},
|
||||
{"MZ-1R12", false, MZ1R12_Init, MZ1R12_Reset, MZ1R12_PollCB, MZ1R12_TaskProcessor},
|
||||
{"MZ-1R18", false, MZ1R18_Init, MZ1R18_Reset, MZ1R18_PollCB, MZ1R18_TaskProcessor},
|
||||
{"MZ-1R23", false, MZ1R23_Init, MZ1R23_Reset, MZ1R23_PollCB, MZ1R23_TaskProcessor},
|
||||
{"MZ-1R37", false, MZ1R37_Init, MZ1R37_Reset, MZ1R37_PollCB, MZ1R37_TaskProcessor},
|
||||
{"PIO-3034", false, PIO3034_Init, PIO3034_Reset, PIO3034_PollCB, PIO3034_TaskProcessor},
|
||||
{"Celestite", false, Celestite_Init, Celestite_Reset, Celestite_PollCB, Celestite_TaskProcessor},
|
||||
};
|
||||
static const size_t interfaceFuncMapSize = sizeof(interfaceFuncMap) / sizeof(interfaceFuncMap[0]);
|
||||
|
||||
@@ -83,9 +89,6 @@ static bool mz2000VramEnabled = false; // PIO A bit 7: VRAM paged into add
|
||||
static bool mz2000CharMode = false; // PIO A bit 6: H=Character VRAM, L=Graphics VRAM.
|
||||
static uint32_t mz2000RamBlocks[32]; // Saved RAM membankPtr for blocks 96-127 (0xC000-0xFFFF).
|
||||
|
||||
// RFS control structure — kept for compatibility with RFS interface driver.
|
||||
extern t_RFSCtrl *rfsCtrl;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Comprehensive Intel 8253 PIT (Programmable Interval Timer) emulation.
|
||||
// The MZ-2000 uses an 8253 PIT at I/O ports 0xE4-0xE7 clocked at ~4 MHz.
|
||||
@@ -876,7 +879,7 @@ uint8_t MZ2000_Reset(t_Z80CPU *cpu)
|
||||
// Call interface reset handlers if board installed.
|
||||
for (size_t i = 0; i < interfaceFuncMapSize; i++)
|
||||
{
|
||||
debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
//debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
if (interfaceFuncMap[i].active)
|
||||
{
|
||||
result |= interfaceFuncMap[i].resetFuncPtr(cpu);
|
||||
@@ -1086,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++)
|
||||
{
|
||||
@@ -1128,9 +1139,6 @@ uint8_t MZ2000_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
|
||||
}
|
||||
}
|
||||
|
||||
// When any interface uses physical hardware, RFS bank switching will change
|
||||
// blocks from PHYSICAL to RAM type. The Z80 then fetches from virtual RAM
|
||||
// without generating physical M1/RFSH cycles, causing host DRAM to decay.
|
||||
// Force refreshEnable so Z80CPU_refreshDRAM is called during virtual fetches.
|
||||
if (config->isPhysical)
|
||||
{
|
||||
|
||||
1168
projects/tzpuPico/src/drivers/Sharp/MZ2200.c
Normal file
1168
projects/tzpuPico/src/drivers/Sharp/MZ2200.c
Normal file
File diff suppressed because it is too large
Load Diff
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
@@ -48,6 +48,10 @@
|
||||
#include "drivers/Sharp/MZ-1E19.h"
|
||||
#include "drivers/Sharp/MZ-1R12.h"
|
||||
#include "drivers/Sharp/MZ-1R18.h"
|
||||
#include "drivers/Sharp/MZ-1R23.h"
|
||||
#include "drivers/Sharp/MZ-1R37.h"
|
||||
#include "drivers/Sharp/PIO-3034.h"
|
||||
#include "drivers/Sharp/Celestite.h"
|
||||
#include "drivers/Sharp/MZ700.h"
|
||||
|
||||
// Map to contain interface name against method to initialise it.
|
||||
@@ -58,6 +62,10 @@ static t_InterfaceFuncMap interfaceFuncMap[] = {
|
||||
{"MZ-1E19", false, MZ1E19_Init, MZ1E19_Reset, MZ1E19_PollCB, MZ1E19_TaskProcessor},
|
||||
{"MZ-1R12", false, MZ1R12_Init, MZ1R12_Reset, MZ1R12_PollCB, MZ1R12_TaskProcessor},
|
||||
{"MZ-1R18", false, MZ1R18_Init, MZ1R18_Reset, MZ1R18_PollCB, MZ1R18_TaskProcessor},
|
||||
{"MZ-1R23", false, MZ1R23_Init, MZ1R23_Reset, MZ1R23_PollCB, MZ1R23_TaskProcessor},
|
||||
{"MZ-1R37", false, MZ1R37_Init, MZ1R37_Reset, MZ1R37_PollCB, MZ1R37_TaskProcessor},
|
||||
{"PIO-3034", false, PIO3034_Init, PIO3034_Reset, PIO3034_PollCB, PIO3034_TaskProcessor},
|
||||
{"Celestite", false, Celestite_Init, Celestite_Reset, Celestite_PollCB, Celestite_TaskProcessor},
|
||||
};
|
||||
static const size_t interfaceFuncMapSize = sizeof(interfaceFuncMap) / sizeof(interfaceFuncMap[MZ700_MEMBANK_0]);
|
||||
|
||||
@@ -783,7 +791,7 @@ uint8_t MZ700_Reset(t_Z80CPU *cpu)
|
||||
// Call interface reset handlers if board installed.
|
||||
for (size_t i = 0; i < interfaceFuncMapSize; i++)
|
||||
{
|
||||
debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
//debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
if (interfaceFuncMap[i].active)
|
||||
{
|
||||
result |= interfaceFuncMap[i].resetFuncPtr(cpu);
|
||||
@@ -1007,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;
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
#include "drivers/Sharp/MZ-1E19.h"
|
||||
#include "drivers/Sharp/MZ-1R12.h"
|
||||
#include "drivers/Sharp/MZ-1R18.h"
|
||||
#include "drivers/Sharp/MZ-1R37.h"
|
||||
#include "drivers/Sharp/PIO-3034.h"
|
||||
#include "drivers/Sharp/MZ80A.h"
|
||||
|
||||
// Map to contain interface name against method to initialise it.
|
||||
@@ -59,6 +61,8 @@ static t_InterfaceFuncMap interfaceFuncMap[] = {
|
||||
{"MZ-1E19", false, MZ1E19_Init, MZ1E19_Reset, MZ1E19_PollCB, MZ1E19_TaskProcessor},
|
||||
{"MZ-1R12", false, MZ1R12_Init, MZ1R12_Reset, MZ1R12_PollCB, MZ1R12_TaskProcessor},
|
||||
{"MZ-1R18", false, MZ1R18_Init, MZ1R18_Reset, MZ1R18_PollCB, MZ1R18_TaskProcessor},
|
||||
{"MZ-1R37", false, MZ1R37_Init, MZ1R37_Reset, MZ1R37_PollCB, MZ1R37_TaskProcessor},
|
||||
{"PIO-3034", false, PIO3034_Init, PIO3034_Reset, PIO3034_PollCB, PIO3034_TaskProcessor},
|
||||
};
|
||||
static const size_t interfaceFuncMapSize = sizeof(interfaceFuncMap) / sizeof(interfaceFuncMap[0]);
|
||||
|
||||
@@ -688,7 +692,7 @@ uint8_t MZ80A_Reset(t_Z80CPU *cpu)
|
||||
// RFS_Reset will swap-back _membankPtr and clear rfsCtrl->memSwitch if MEMSW was active.
|
||||
for (size_t i = 0; i < interfaceFuncMapSize; i++)
|
||||
{
|
||||
debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
//debugf("IF_RST[%d]: %s active=%d\r\n", i, interfaceFuncMap[i].interfaceFuncName, interfaceFuncMap[i].active);
|
||||
if (interfaceFuncMap[i].active)
|
||||
{
|
||||
result |= interfaceFuncMap[i].resetFuncPtr(cpu);
|
||||
@@ -881,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);
|
||||
|
||||
250
projects/tzpuPico/src/drivers/Sharp/PIO-3034.c
Normal file
250
projects/tzpuPico/src/drivers/Sharp/PIO-3034.c
Normal file
@@ -0,0 +1,250 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: PIO-3034.c
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - PIO-3034 320K EMM (IO DATA)
|
||||
// This file contains setup and driver to emulate the PIO-3034 320KB Expanded Memory
|
||||
// Manager board by IO DATA.
|
||||
//
|
||||
// The board provides 320KB of RAM accessed via a 19-bit address counter and a data
|
||||
// port with auto-increment. The I/O base address is selectable via DIP switch on
|
||||
// real hardware; here it is configured via ioMap in the JSON config:
|
||||
// "ioMap": [{"srcAddr": 0, "dstAddr": <base>, "size": 4}]
|
||||
// Default base is 0x00 (as used by HuBASIC).
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write.
|
||||
//
|
||||
// Notes: See Makefile to enable/disable conditional components
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// This source file is free software: you can redistribute it and#or modify
|
||||
// it under the terms of the GNU General Public License as published
|
||||
// by the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This source file is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "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/PIO-3034.h"
|
||||
#include "debug.h"
|
||||
|
||||
t_PIO3034 *pio3034Ctrl; // Control structure for the PIO-3034 EMM board.
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Interface: IO DATA PIO-3034 320K EMM
|
||||
// Description: 320KB expanded memory board with 19-bit address counter and auto-increment data
|
||||
// port. Base I/O address is configurable (default 0x00).
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
// Method to initialise the CPU state to support a PIO-3034 EMM interface.
|
||||
uint8_t PIO3034_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 (pio3034Ctrl != NULL)
|
||||
return (0);
|
||||
|
||||
// Allocate control structure.
|
||||
pio3034Ctrl = (t_PIO3034 *) calloc(1, sizeof(t_PIO3034));
|
||||
if (!pio3034Ctrl)
|
||||
{
|
||||
debugf("PIO-3034: Failed to allocate control structure\r\n");
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Allocate 320KB RAM.
|
||||
pio3034Ctrl->ram = (uint8_t *) calloc(1, PIO3034_RAM_SIZE);
|
||||
if (pio3034Ctrl->ram == NULL)
|
||||
{
|
||||
debugf("PIO-3034: Failed to allocate %d bytes RAM\r\n", PIO3034_RAM_SIZE);
|
||||
free(pio3034Ctrl);
|
||||
pio3034Ctrl = NULL;
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Determine I/O base address from ioMap config (default 0x00).
|
||||
pio3034Ctrl->ioBase = PIO3034_DEFAULT_BASE;
|
||||
for (int remapIdx = 0; remapIdx < config->ioMapCount; remapIdx++)
|
||||
{
|
||||
pio3034Ctrl->ioBase = (uint8_t)config->ioMap[remapIdx].dstAddr;
|
||||
}
|
||||
pio3034Ctrl->addr = 0;
|
||||
|
||||
// Install I/O handlers at the configured base address.
|
||||
uint8_t base = pio3034Ctrl->ioBase;
|
||||
for (int idx = 0; idx < IO_PAGE_SIZE; idx++)
|
||||
{
|
||||
uint8_t port = idx & 0xFF;
|
||||
if (port == base)
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) PIO3034_IO_AddrLo;
|
||||
else if (port == (uint8_t)(base + 1))
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) PIO3034_IO_AddrMid;
|
||||
else if (port == (uint8_t)(base + 2))
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) PIO3034_IO_AddrHi;
|
||||
else if (port == (uint8_t)(base + 3))
|
||||
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) PIO3034_IO_Data;
|
||||
}
|
||||
|
||||
debugf("PIO-3034: Initialised, base=0x%02X, RAM=%dKB\r\n", base, PIO3034_RAM_SIZE / 1024);
|
||||
|
||||
result = 1;
|
||||
return (result);
|
||||
}
|
||||
|
||||
// Reset handler.
|
||||
uint8_t PIO3034_Reset(t_Z80CPU *cpu)
|
||||
{
|
||||
(void)cpu;
|
||||
|
||||
if (pio3034Ctrl)
|
||||
{
|
||||
pio3034Ctrl->addr = 0;
|
||||
}
|
||||
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Poll handler — nothing to do for a static RAM board.
|
||||
uint8_t __func_in_RAM(PIO3034_PollCB)(t_Z80CPU *cpu)
|
||||
{
|
||||
(void)cpu;
|
||||
return (0);
|
||||
}
|
||||
|
||||
// Task processor — no external tasks currently handled.
|
||||
uint8_t PIO3034_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)param;
|
||||
uint8_t result = 0;
|
||||
|
||||
switch (task)
|
||||
{
|
||||
default:
|
||||
result = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
return (result);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for Base+0 — Address counter bits [7:0] (write only).
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(PIO3034_IO_AddrLo)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
if (read)
|
||||
return 0xFF;
|
||||
|
||||
if (pio3034Ctrl)
|
||||
{
|
||||
pio3034Ctrl->addr = (pio3034Ctrl->addr & 0x7FF00) | (uint32_t)data;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for Base+1 — Address counter bits [15:8] (write only).
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(PIO3034_IO_AddrMid)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
if (read)
|
||||
return 0xFF;
|
||||
|
||||
if (pio3034Ctrl)
|
||||
{
|
||||
pio3034Ctrl->addr = (pio3034Ctrl->addr & 0x700FF) | ((uint32_t)data << 8);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for Base+2 — Address counter bits [18:16] (write only).
|
||||
// Only bits 0-2 are significant (19-bit address space).
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(PIO3034_IO_AddrHi)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
if (read)
|
||||
return 0xFF;
|
||||
|
||||
if (pio3034Ctrl)
|
||||
{
|
||||
pio3034Ctrl->addr = (pio3034Ctrl->addr & 0x0FFFF) | (((uint32_t)data & 0x07) << 16);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// I/O handler for Base+3 — Data read/write with address auto-increment.
|
||||
// Reads from addresses >= 320KB (0x50000) return 0xFF (unpopulated).
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t __func_in_RAM(PIO3034_IO_Data)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
|
||||
{
|
||||
(void)cpu;
|
||||
(void)addr;
|
||||
|
||||
if (pio3034Ctrl == NULL)
|
||||
return 0xFF;
|
||||
|
||||
if (read)
|
||||
{
|
||||
uint8_t result = 0xFF;
|
||||
|
||||
if (pio3034Ctrl->addr < PIO3034_RAM_SIZE)
|
||||
{
|
||||
result = pio3034Ctrl->ram[pio3034Ctrl->addr];
|
||||
}
|
||||
|
||||
pio3034Ctrl->addr = (pio3034Ctrl->addr + 1) & PIO3034_ADDR_MASK;
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (pio3034Ctrl->addr < PIO3034_RAM_SIZE)
|
||||
{
|
||||
pio3034Ctrl->ram[pio3034Ctrl->addr] = data;
|
||||
}
|
||||
|
||||
pio3034Ctrl->addr = (pio3034Ctrl->addr + 1) & PIO3034_ADDR_MASK;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -217,21 +217,26 @@ void __not_in_flash_func(qdSioWriteControl)(t_QdDrive *qd, t_sioChannel *ch, uin
|
||||
uint32_t pos = qd->currentPosition;
|
||||
bool found = false;
|
||||
|
||||
if (qd->sync16bit)
|
||||
// Hunt for sync pattern: look for the mark byte (0x00) followed by sync char (0x16).
|
||||
// The 0x00 mark always precedes a new block's sync preamble in both compact and
|
||||
// QDF formats. This ensures we skip past any trailing sync bytes from the previous
|
||||
// block (which are preceded by CRC bytes, not 0x00).
|
||||
{
|
||||
while (pos + 1 < qd->diskSize)
|
||||
{
|
||||
if (qd->diskImage[pos] == qd->syncChar1 && qd->diskImage[pos + 1] == qd->syncChar2)
|
||||
if (qd->diskImage[pos] == QD_MARK_FLAG && qd->diskImage[pos + 1] == qd->syncChar1)
|
||||
{
|
||||
qd->currentPosition = pos + 2;
|
||||
qd->currentPosition = pos + 2; // Past the 00 16
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!found)
|
||||
{
|
||||
// Fallback: scan for just sync char (for edge cases at track boundaries).
|
||||
pos = qd->currentPosition;
|
||||
while (pos < qd->diskSize)
|
||||
{
|
||||
if (qd->diskImage[pos] == qd->syncChar1)
|
||||
@@ -252,8 +257,9 @@ void __not_in_flash_func(qdSioWriteControl)(t_QdDrive *qd, t_sioChannel *ch, uin
|
||||
qd->inBlock = true;
|
||||
qd->bytesReadInBlock = 0;
|
||||
qd->chA.rr[1] &= ~0x40;
|
||||
// debugf("QD: SYNC FOUND pos=%d byte=%02X\r\n", qd->currentPosition,
|
||||
// qd->currentPosition < qd->diskSize ? qd->diskImage[qd->currentPosition] : 0xFF);
|
||||
// debugf("QD: SYNC@%d nxt=%02X s16=%d ds=%d\r\n", qd->currentPosition,
|
||||
// qd->currentPosition < qd->diskSize ? qd->diskImage[qd->currentPosition] : 0xFF,
|
||||
// qd->sync16bit, qd->diskSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -330,10 +336,13 @@ uint32_t qdGetNextRequestId(t_QdDrive *qd)
|
||||
return id;
|
||||
}
|
||||
|
||||
// Get size of the QD Image.
|
||||
// Get size of the QD Image buffer. Must be large enough for the Japanese
|
||||
// QDF format (81936 bytes = 16-byte header + 81920 bytes track data).
|
||||
int qdGetDiskSize(t_QdDrive *qd, int machineType)
|
||||
{
|
||||
return MAX_QD_SIZE;
|
||||
(void)qd;
|
||||
(void)machineType;
|
||||
return QDF_FILE_SIZE; // 81936 — large enough for both compact QD and QDF formats.
|
||||
}
|
||||
|
||||
// Process intercore message response.
|
||||
@@ -366,8 +375,36 @@ int __not_in_flash_func(qdProcessDeviceResponses)(t_QdDrive *qd, queue_t *respon
|
||||
case MSG_LOAD_COMPLETE:
|
||||
qd->opState.loadPending = false;
|
||||
qd->diskLoaded = response.response.success;
|
||||
if (!response.response.success)
|
||||
if (response.response.success)
|
||||
{
|
||||
// Detect QDF format: 16-byte header "-QD format-" + 81920 bytes track data.
|
||||
// If found, adjust diskImage pointer past the header (no memmove needed).
|
||||
// Data is always loaded to diskBuf; diskImage points to track start.
|
||||
qd->diskImage = qd->diskBuf; // Reset to base.
|
||||
if (response.response.size >= QDF_FILE_SIZE &&
|
||||
memcmp(qd->diskBuf, QDF_SIGNATURE, 11) == 0)
|
||||
{
|
||||
qd->diskImage = qd->diskBuf + QDF_HEADER_SIZE;
|
||||
qd->diskSize = MAX_QDF_SIZE;
|
||||
debugf("QD: QDF format detected, diskSize=%lu\r\n", (unsigned long)qd->diskSize);
|
||||
}
|
||||
else if (response.response.size <= MAX_QD_FORMAT_SIZE)
|
||||
{
|
||||
// Standard compact QD format.
|
||||
qd->diskSize = (response.response.size > MAX_QD_SIZE) ? response.response.size : MAX_QD_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown size — use what we got, cap at buffer.
|
||||
qd->diskSize = (response.response.size > MAX_QDF_SIZE) ? MAX_QDF_SIZE : response.response.size;
|
||||
debugf("QD: Non-standard size=%lu\r\n", (unsigned long)qd->diskSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qdSetStatusFlag(qd, QD_CRC_ERROR_BIT);
|
||||
}
|
||||
qd->currentPosition = 0;
|
||||
qd->intrq = true;
|
||||
break;
|
||||
|
||||
@@ -458,31 +495,23 @@ void __not_in_flash_func(qdUpdateStatus)(t_QdDrive *qd)
|
||||
qd->chA.rr[0] |= 0x40; // EOM
|
||||
}
|
||||
|
||||
// Sync stripping — only strip leading sync bytes before the first data byte of a block.
|
||||
// Real Z80 SIO only strips sync chars at the start of a message, not mid-data.
|
||||
if (qd->rxEnable && !qd->huntActive && qd->inBlock && qd->currentOperation == QD_OP_READ && qd->currentPosition < qd->diskSize && qd->inhibit
|
||||
&& qd->bytesReadInBlock == 0)
|
||||
// Sync stripping — strip ALL leading sync bytes after hunt finds sync, before first data byte.
|
||||
// Real Z80 SIO automatically strips sync characters after entering sync mode.
|
||||
// QDF format tracks have long sync preambles (9+ bytes of 0x16 before the A5 flag).
|
||||
// Always strip individual sync bytes regardless of 16-bit mode — handles any count correctly.
|
||||
if (qd->rxEnable && !qd->huntActive && qd->inBlock && qd->currentOperation == QD_OP_READ
|
||||
&& qd->currentPosition < qd->diskSize && qd->bytesReadInBlock == 0)
|
||||
{
|
||||
uint32_t startPos = qd->currentPosition;
|
||||
if (qd->sync16bit)
|
||||
uint32_t stripStart = qd->currentPosition;
|
||||
while (qd->currentPosition < qd->diskSize &&
|
||||
(qd->diskImage[qd->currentPosition] == qd->syncChar1 ||
|
||||
qd->diskImage[qd->currentPosition] == qd->syncChar2))
|
||||
{
|
||||
while (qd->currentPosition + 1 < qd->diskSize && qd->diskImage[qd->currentPosition] == qd->syncChar1 &&
|
||||
qd->diskImage[qd->currentPosition + 1] == qd->syncChar2)
|
||||
{
|
||||
qd->currentPosition += 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (qd->currentPosition < qd->diskSize && qd->diskImage[qd->currentPosition] == qd->syncChar1)
|
||||
{
|
||||
qd->currentPosition++;
|
||||
}
|
||||
}
|
||||
if (qd->currentPosition != startPos)
|
||||
{
|
||||
// debugf("QD: SYNC_STRIP %d→%d\r\n", startPos, qd->currentPosition);
|
||||
qd->currentPosition++;
|
||||
}
|
||||
// if (qd->currentPosition != stripStart)
|
||||
// debugf("QD: STRIP %d->%d nxt=%02X\r\n", stripStart, qd->currentPosition,
|
||||
// qd->currentPosition < qd->diskSize ? qd->diskImage[qd->currentPosition] : 0xFF);
|
||||
}
|
||||
|
||||
qd->drq = false;
|
||||
@@ -589,6 +618,8 @@ bool qdInit(t_QdDrive *qd, queue_t *requestQueue, queue_t *responseQueue, const
|
||||
qd->requestQueue = requestQueue;
|
||||
qd->responseQueue = responseQueue;
|
||||
qd->diskImage = diskImage;
|
||||
qd->diskBuf = diskImage; // Save original base for reloads.
|
||||
qd->diskBufSize = bufferSize;
|
||||
qd->currentPosition = 0;
|
||||
qd->motorOn = false;
|
||||
qd->diskLoaded = false;
|
||||
@@ -598,7 +629,7 @@ bool qdInit(t_QdDrive *qd, queue_t *requestQueue, queue_t *responseQueue, const
|
||||
qd->opState.loadPending = false;
|
||||
qd->opState.writePending = false;
|
||||
qd->filename = (filename != NULL) ? strdup(filename) : NULL;
|
||||
qd->diskSize = MAX_QD_SIZE;
|
||||
qd->diskSize = MAX_QD_SIZE; // Default; updated to MAX_QDF_SIZE on QDF format detection.
|
||||
qd->machineType = machineType;
|
||||
qd->chA.pointer = 0;
|
||||
qd->chB.pointer = 0;
|
||||
@@ -628,8 +659,8 @@ bool qdInit(t_QdDrive *qd, queue_t *requestQueue, queue_t *responseQueue, const
|
||||
qd->opState.loadPending = true;
|
||||
t_CoreMsg msg = {.type = MSG_LOAD_QUICKDISK, .context = qd};
|
||||
strncpy(msg.fileOp.filename, filename, MAX_IC_FILENAME_LEN - 1);
|
||||
msg.fileOp.buffer = diskImage;
|
||||
msg.fileOp.size = qd->diskSize;
|
||||
msg.fileOp.buffer = qd->diskBuf;
|
||||
msg.fileOp.size = qd->diskBufSize;
|
||||
msg.fileOp.diskNo = 0;
|
||||
queue_try_add(qd->requestQueue, &msg);
|
||||
}
|
||||
@@ -651,11 +682,15 @@ void qdChangeDisk(t_QdDrive *qd, const char *newFilename, int diskNo)
|
||||
qd->currentPosition = 0;
|
||||
qd->formatFill = false;
|
||||
|
||||
// Reset diskImage to buffer base (in case it was offset for QDF header).
|
||||
qd->diskImage = qd->diskBuf;
|
||||
qd->diskSize = MAX_QD_SIZE;
|
||||
|
||||
// Only 1 QD drive can be configured at a time, so set diskNo to 0 for all change requests.
|
||||
t_CoreMsg msg = {.type = MSG_LOAD_QUICKDISK, .context = qd};
|
||||
strncpy(msg.fileOp.filename, newFilename, MAX_IC_FILENAME_LEN - 1);
|
||||
msg.fileOp.buffer = qd->diskImage;
|
||||
msg.fileOp.size = qd->diskSize;
|
||||
msg.fileOp.buffer = qd->diskBuf; // Always load to base buffer.
|
||||
msg.fileOp.size = qd->diskBufSize;
|
||||
msg.fileOp.diskNo = 0;
|
||||
queue_try_add(qd->requestQueue, &msg);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
8
projects/tzpuPico/src/include/ESP.h
vendored
8
projects/tzpuPico/src/include/ESP.h
vendored
@@ -119,6 +119,7 @@ int ESP_readFile_(const char *fileName,
|
||||
int ESP_readSector(const char *fileName, int filePos, uint8_t *memLocation, int secLen);
|
||||
bool ESP_writeFile(const char *fileName, uint8_t *memLocation, int maxLen);
|
||||
bool ESP_writeSector(const char *fileName, int filePos, uint8_t *memLocation, int secLen);
|
||||
int ESP_readDir(const char *path, char *buf, int bufLen);
|
||||
|
||||
// Burst sector I/O — transfers up to IPCF_MAX_SECTORS sectors in a single SPI transaction.
|
||||
// Returns bytes copied into memLocation (numSectors × IPCF_SECTOR_SIZE on success, 0 on error).
|
||||
@@ -126,6 +127,13 @@ int ESP_readBurstSectors(const char *fileName, int filePos, uint8_t *memLocation
|
||||
// Returns true on success.
|
||||
bool ESP_writeBurstSectors(const char *fileName, int filePos, uint8_t *memLocation, int numSectors);
|
||||
|
||||
// Network functions (Celestite W5100 emulation — Phase 2).
|
||||
bool ESP_netCfg(uint8_t *ipOut, uint8_t *gwOut, uint8_t *subnetOut, uint8_t *macOut);
|
||||
bool ESP_netSocket(uint8_t sockNum, uint8_t operation, uint8_t protocol, uint32_t ipAddr, uint16_t port, uint8_t *statusOut);
|
||||
bool ESP_netSend(uint8_t sockNum, uint8_t *data, uint32_t len, uint32_t *sentOut);
|
||||
bool ESP_netRecv(uint8_t sockNum, uint8_t *buf, uint32_t bufSize, uint32_t *recvdOut);
|
||||
bool ESP_netPing(uint32_t ipAddr, uint32_t *rttOut);
|
||||
|
||||
cJSON *ESP_readConfig(const char *fileName);
|
||||
const char *ESP_getClassName(void);
|
||||
void ESP_replaceExt(char *fileName, size_t fileNameLen, const char *newExt);
|
||||
|
||||
41
projects/tzpuPico/src/include/Z80CPU.h
vendored
41
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>
|
||||
@@ -354,9 +381,11 @@ typedef struct
|
||||
} t_ioReMap;
|
||||
|
||||
// Struct for storing interface configuration parameters.
|
||||
// Each param entry is a key/value pair — the key determines the parameter type.
|
||||
typedef struct
|
||||
{
|
||||
char *file; // Name of a file required by the interface for initialisation.
|
||||
char *file; // "file" key: name of a file required by the interface.
|
||||
char *ip; // "ip" key: IP address string, e.g. "192.168.1.210:6800".
|
||||
} t_ifParam;
|
||||
|
||||
// Struct for storing a drivers interface configuration.
|
||||
@@ -379,6 +408,8 @@ struct t_drvConfig
|
||||
{
|
||||
const char *name; // Name of the driver. 1:1 mapping with t_VirtualFuncMap.virtualFuncName
|
||||
bool isPhysical; // Should the driver use physical mappings (ROM/RAM etc) or virtual?
|
||||
int romCount; // Number of driver-level ROM's (e.g., system ROM replacement).
|
||||
t_drvROMConfig *romConfig; // Struct array of driver-level ROM descriptions.
|
||||
int ifCount; // Number of interfaces stored.
|
||||
t_drvIFConfig *ifConfig; // Driver interface configuration. Each driver can have multiple optional interfaces.
|
||||
t_ResetFunc resetPtr; // Pointer to a reset handler for this driver.
|
||||
@@ -404,6 +435,7 @@ struct Z80CPU
|
||||
t_drivers _drivers; // Handle to array of structures to specify active drivers and their configuration.
|
||||
queue_t requestQueue; // Inter core command request queue. Used to dispatch commands from the Z80 to core0 for processing.
|
||||
queue_t responseQueue; // Inter core command wresponse queue. Used to dispatch responses to commands received from the Z80 on core0.
|
||||
t_FlashAppConfigHeader *appConfig; // Pointer to flash app config header (set by persona driver, used by interface drivers for ROM access).
|
||||
bool halt; // Halt state, true = active.
|
||||
bool refreshEnable; // Enable DRAM refresh during virtual memory fetches. Set via JSON config "z80refresh" parameter.
|
||||
volatile bool hold; // Hold CPU processing, used by Core 0 when access to physical is needed.
|
||||
@@ -412,6 +444,8 @@ struct Z80CPU
|
||||
uint32_t hostClkHz; // Measured host clock frequency in Hz (GPIO35, measured at boot).
|
||||
uint32_t emulSpeedHz; // Effective emulation speed in Hz (computed from Z80 cycle counter).
|
||||
volatile uint32_t emulLoopCount; // Incremented by Core 1 after each z80_run() call (32-bit, atomic on ARM).
|
||||
volatile uint32_t intPinLowCount; // Diagnostic: count of z80_run loops where /INT (GPIO33) was LOW.
|
||||
uint32_t m1DelayCount; // Virtual mode: T-state counter for delaying M1 bus cycles after EI.
|
||||
|
||||
// Pending task from Core 0 — processed by Core 1 in the emulation loop.
|
||||
// Core 0 writes these, Core 1 reads and clears pendingTask.
|
||||
@@ -442,6 +476,9 @@ struct Z80CPU
|
||||
volatile uint32_t dbgCorruptCount; // Total corruptions detected.
|
||||
volatile uint32_t dbgCorruptHead; // Write index into corruption log.
|
||||
uint32_t dbgCorrupt[DBG_CORRUPT_SZ]; // Log: [31:16]=PC, [15:8]=fetched, [7:0]=verified.
|
||||
|
||||
// Driver-registered debug hooks for machine-specific trace commands.
|
||||
t_DbgShellHooks dbgHooks;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -487,7 +524,7 @@ void Z80CPU_deinit(t_Z80CPU *cpu);
|
||||
bool Z80CPU_configFromJSON(t_Z80CPU *cpu, int cfgAppNo);
|
||||
bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, char *json);
|
||||
void Z80CPU_ForceHostResetActive(t_Z80CPU *cpu, bool resetHardware);
|
||||
void Z80CPU_reset(t_Z80CPU *cpu);
|
||||
void Z80CPU_reset(t_Z80CPU *cpu, bool physicalReset);
|
||||
void Z80CPU_cpu_init(t_Z80CPU *cpu);
|
||||
void Z80CPU_cpu(t_Z80CPU *cpu);
|
||||
|
||||
|
||||
279
projects/tzpuPico/src/include/drivers/Sharp/Celestite.h
vendored
Normal file
279
projects/tzpuPico/src/include/drivers/Sharp/Celestite.h
vendored
Normal file
@@ -0,0 +1,279 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: Celestite.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - Celestite LAN / Memory Composite Board
|
||||
// Emulates the Celestite board (designed by Oh!Ishi) which provides:
|
||||
// - Wiznet W5100 hardwired TCP/IP Ethernet controller (IDM access)
|
||||
// - Interrupt controller
|
||||
// - UFM (User Flash Memory) for MAC address / config persistence
|
||||
// - Board unlock features (MZ-1R12 doubling, ROM write-protect, EMM)
|
||||
//
|
||||
// I/O ports (active at 0x60-0x6F):
|
||||
// 60h (W) : W5100 MR (Mode Register)
|
||||
// 61h (W) : W5100 IDM_AR0 (internal address high byte)
|
||||
// 62h (W) : W5100 IDM_AR1 (internal address low byte)
|
||||
// 63h (R/W) : W5100 IDM_DR (data read/write, auto-increment)
|
||||
// 64h (R/W) : Interrupt vector
|
||||
// 65h (W) : Interrupt control (bit 0: ENABLE)
|
||||
// 65h (R) : Interrupt status (bit 7: input, bit 6: ack, bit 0: enable)
|
||||
// 66h (R/W) : Memo register
|
||||
// 68h (W) : UFM address register
|
||||
// 68h (R) : UFM status (bit 7: WP, bit 1: READY, bit 0: BUSY)
|
||||
// 69h (R/W) : UFM data register
|
||||
// 6Ah (W) : Config index register (selects config byte)
|
||||
// 6Bh (R) : Config data register (returns selected byte)
|
||||
// Index 0-3: NET file server IP[0..3]
|
||||
// Index 4: NET file server port high byte
|
||||
// Index 5: NET file server port low byte
|
||||
// 6Fh (W) : Unlock (write D1h then keyword)
|
||||
//
|
||||
// Credits: Board designed by Oh!Ishi (Oh!石)
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Phase 1: Register file + IDM access (no networking).
|
||||
//
|
||||
// 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 CELESTITE_H
|
||||
#define CELESTITE_H
|
||||
|
||||
// Need IPCF_MAX_PAYLOAD for receive buffer sizing.
|
||||
#include "ipc_protocol.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// W5100 Internal Address Map
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Common registers (0x0000-0x002F).
|
||||
#define W5100_MR 0x0000 // Mode Register (RST, WOL, PB, PPPoE, AI, IND).
|
||||
#define W5100_GAR0 0x0001 // Gateway Address [0-3].
|
||||
#define W5100_SUBR0 0x0005 // Subnet Mask [0-3].
|
||||
#define W5100_SHAR0 0x0009 // Source Hardware (MAC) Address [0-5].
|
||||
#define W5100_SIPR0 0x000F // Source IP Address [0-3].
|
||||
#define W5100_IR 0x0015 // Interrupt Register.
|
||||
#define W5100_IMR 0x0016 // Interrupt Mask Register.
|
||||
#define W5100_RTR0 0x0017 // Retry Time [0-1] (100us units).
|
||||
#define W5100_RCR 0x0019 // Retry Count.
|
||||
#define W5100_RMSR 0x001A // RX Memory Size (per socket).
|
||||
#define W5100_TMSR 0x001B // TX Memory Size (per socket).
|
||||
|
||||
// MR bit definitions.
|
||||
#define W5100_MR_RST 0x80 // Software reset.
|
||||
#define W5100_MR_PB 0x10 // Ping block.
|
||||
#define W5100_MR_AI 0x02 // Auto-increment IDM address.
|
||||
#define W5100_MR_IND 0x01 // Indirect bus mode (always set for IDM).
|
||||
|
||||
// Socket register base addresses (4 sockets × 256 bytes each).
|
||||
#define W5100_SOCK_BASE 0x0400
|
||||
#define W5100_SOCK_SIZE 0x0100
|
||||
#define W5100_NUM_SOCKETS 4
|
||||
|
||||
// Socket register offsets (relative to socket base).
|
||||
#define Sn_MR 0x00 // Socket Mode.
|
||||
#define Sn_CR 0x01 // Socket Command.
|
||||
#define Sn_IR 0x02 // Socket Interrupt.
|
||||
#define Sn_SR 0x03 // Socket Status.
|
||||
#define Sn_PORT0 0x04 // Source Port [0-1].
|
||||
#define Sn_DHAR0 0x06 // Dest Hardware Addr [0-5].
|
||||
#define Sn_DIPR0 0x0C // Dest IP [0-3].
|
||||
#define Sn_DPORT0 0x10 // Dest Port [0-1].
|
||||
#define Sn_MSS0 0x12 // Max Segment Size [0-1].
|
||||
#define Sn_PROTO 0x14 // IP Protocol (IPRAW mode).
|
||||
#define Sn_TOS 0x15 // Type of Service.
|
||||
#define Sn_TTL 0x16 // Time to Live.
|
||||
#define Sn_TX_FSR0 0x20 // TX Free Size [0-1].
|
||||
#define Sn_TX_RD0 0x22 // TX Read Pointer [0-1].
|
||||
#define Sn_TX_WR0 0x24 // TX Write Pointer [0-1].
|
||||
#define Sn_RX_RSR0 0x26 // RX Received Size [0-1].
|
||||
#define Sn_RX_RD0 0x28 // RX Read Pointer [0-1].
|
||||
|
||||
// Socket Mode (Sn_MR) protocol field (bits 3-0).
|
||||
#define Sn_MR_TCP 0x01
|
||||
#define Sn_MR_UDP 0x02
|
||||
#define Sn_MR_IPRAW 0x03
|
||||
#define Sn_MR_MACRAW 0x04
|
||||
#define Sn_MR_PROTO_MASK 0x0F
|
||||
|
||||
// Socket Command (Sn_CR) values.
|
||||
#define Sn_CR_OPEN 0x01
|
||||
#define Sn_CR_LISTEN 0x02
|
||||
#define Sn_CR_CONNECT 0x04
|
||||
#define Sn_CR_DISCON 0x08
|
||||
#define Sn_CR_CLOSE 0x10
|
||||
#define Sn_CR_SEND 0x20
|
||||
#define Sn_CR_SEND_MAC 0x21
|
||||
#define Sn_CR_SEND_KEEP 0x22
|
||||
#define Sn_CR_RECV 0x40
|
||||
|
||||
// Socket Status (Sn_SR) values.
|
||||
#define SOCK_CLOSED 0x00
|
||||
#define SOCK_INIT 0x13
|
||||
#define SOCK_LISTEN 0x14
|
||||
#define SOCK_SYNSENT 0x15
|
||||
#define SOCK_SYNRECV 0x16
|
||||
#define SOCK_ESTABLISHED 0x17
|
||||
#define SOCK_FIN_WAIT 0x18
|
||||
#define SOCK_CLOSING 0x1A
|
||||
#define SOCK_TIME_WAIT 0x1B
|
||||
#define SOCK_CLOSE_WAIT 0x1C
|
||||
#define SOCK_LAST_ACK 0x1D
|
||||
#define SOCK_UDP 0x22
|
||||
#define SOCK_IPRAW 0x32
|
||||
#define SOCK_MACRAW 0x42
|
||||
|
||||
// Socket Interrupt (Sn_IR) bits.
|
||||
#define Sn_IR_CON 0x01
|
||||
#define Sn_IR_DISCON 0x02
|
||||
#define Sn_IR_RECV 0x04
|
||||
#define Sn_IR_TIMEOUT 0x08
|
||||
#define Sn_IR_SEND_OK 0x10
|
||||
|
||||
// TX/RX buffer memory.
|
||||
#define W5100_TX_BASE 0x4000 // TX buffer start.
|
||||
#define W5100_TX_SIZE 0x2000 // 8KB total TX.
|
||||
#define W5100_RX_BASE 0x6000 // RX buffer start.
|
||||
#define W5100_RX_SIZE 0x2000 // 8KB total RX.
|
||||
|
||||
// Total W5100 address space we emulate.
|
||||
#define W5100_MEM_SIZE 0x8000 // 32KB covers all registers + buffers.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Celestite board-level registers (outside W5100 address space)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define CELESTITE_UFM_SIZE 256 // UFM storage size in bytes.
|
||||
#define CELESTITE_R12_SIZE 32768 // MZ-1R12 base RAM size (32KB).
|
||||
#define CELESTITE_R12_DBL 65536 // MZ-1R12 doubled RAM size (64KB).
|
||||
#define CELESTITE_R37_SIZE 655360 // MZ-1R37 EMM size (640KB).
|
||||
#define CELESTITE_R37_MASK 0xFFFFF // 20-bit address mask.
|
||||
|
||||
// Unlock register state machine.
|
||||
typedef enum
|
||||
{
|
||||
UNLOCK_IDLE = 0, // Waiting for D1h prefix.
|
||||
UNLOCK_WAIT_KEYWORD // D1h received, waiting for keyword byte.
|
||||
} t_CelestiteUnlockState;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main control structure
|
||||
// ---------------------------------------------------------------------------
|
||||
typedef struct
|
||||
{
|
||||
// W5100 emulated address space (registers + TX/RX buffers).
|
||||
uint8_t *w5100Mem; // 32KB W5100 register/buffer memory.
|
||||
uint16_t idmAddr; // Current IDM address (set by AR0/AR1).
|
||||
|
||||
// Celestite board-level registers.
|
||||
uint8_t intVector; // Interrupt vector (port 64h).
|
||||
uint8_t intControl; // Interrupt control (port 65h write).
|
||||
uint8_t intStatus; // Interrupt status (port 65h read).
|
||||
uint8_t memoReg; // Memo register (port 66h).
|
||||
|
||||
// UFM (User Flash Memory).
|
||||
uint8_t ufm[CELESTITE_UFM_SIZE]; // 256-byte UFM storage.
|
||||
uint8_t ufmAddr; // UFM address register.
|
||||
bool ufmWriteProtect; // UFM write-protection state (true=protected).
|
||||
bool ufmBusy; // UFM busy flag.
|
||||
|
||||
// Unlock register state machine.
|
||||
t_CelestiteUnlockState unlockState;
|
||||
|
||||
// Board feature flags (set by unlock keywords).
|
||||
bool romWriteUnprotected; // Expansion/FD ROM write-protect removed (keyword 05h).
|
||||
|
||||
// Integrated MZ-1R12 CMOS Battery Backed RAM (32KB, doubler to 64KB).
|
||||
// Ports: F8h (W=set high addr, R=reset to 0), F9h (W=set low addr, R=read+autoinc), FAh (W=write+autoinc).
|
||||
// Enabled by params[0].file in JSON config. SD-card backed for persistence.
|
||||
uint8_t *mz1r12Ram; // 64KB buffer (32KB used initially, 64KB when doubled).
|
||||
uint32_t mz1r12Size; // Current active RAM size (32768 or 65536).
|
||||
uint16_t mz1r12Addr; // 16-bit address pointer (auto-increments on read/write).
|
||||
bool mz1r12Doubled; // True after unlock keyword 12h doubles from 32K to 64K.
|
||||
char *mz1r12FileName; // SD card backing file name (NULL = no persistence).
|
||||
bool mz1r12WritePending; // True while a scheduled write is in progress.
|
||||
uint32_t mz1r12NextReqId; // Monotonic request ID for write coalescing.
|
||||
|
||||
// Integrated MZ-1R37 640K EMM.
|
||||
// Ports: ACh (W=address latch via OUT(C),A), ADh (R/W=data via OUT(C),A / IN A,(C)).
|
||||
// No auto-increment. 20-bit address: latch[19:8] | B_register[7:0].
|
||||
// Enabled by params[1].file in JSON config. SD-card backed for persistence.
|
||||
uint8_t *mz1r37Ram; // 640KB RAM buffer.
|
||||
uint32_t mz1r37AddrLatch; // Latched address bits [19:8].
|
||||
char *mz1r37FileName; // SD card backing file name (NULL = no persistence).
|
||||
bool mz1r37SectorWritePending; // True while a single sector write is in flight.
|
||||
uint32_t mz1r37WriteId; // Request ID for pending sector write.
|
||||
uint32_t mz1r37NextReqId; // Monotonic request ID counter.
|
||||
uint8_t mz1r37DirtyMap[(CELESTITE_R37_SIZE / 512 + 7) / 8]; // 160-byte dirty bitmap (512B pages).
|
||||
bool mz1r37HasDirty; // Quick flag: at least one dirty page exists.
|
||||
absolute_time_t mz1r37LastWrite; // Time of last Z80 write (for quiesce detection).
|
||||
int mz1r37FlushPos; // Scan position for incremental flush.
|
||||
|
||||
// NET file server configuration (read via ports 6Ah/6Bh).
|
||||
// Parsed from ifParam[2].file = "ip:port" (e.g., "192.168.1.210:6800").
|
||||
uint8_t netSrvIP[4]; // File server IP address (default 0.0.0.0 = unconfigured).
|
||||
uint16_t netSrvPort; // File server TCP port (default 6800).
|
||||
uint8_t cfgIndex; // Config register index (port 6Ah write).
|
||||
|
||||
// Network state (Phase 2).
|
||||
queue_t *requestQueue; // Pointer to inter-core request queue.
|
||||
queue_t *responseQueue; // Pointer to inter-core response queue.
|
||||
volatile bool netActive; // Fast-path guard: true when ANY network op is pending.
|
||||
bool netCfgPending; // True while NET_CFG request is in flight.
|
||||
bool netCfgDone; // True once ESP32 network config has been loaded.
|
||||
bool netSockPending[W5100_NUM_SOCKETS]; // True while a socket op is in flight per socket.
|
||||
bool netRecvPending[W5100_NUM_SOCKETS]; // True while a RECV poll is in flight per socket.
|
||||
uint8_t netRecvBuf[IPCF_MAX_PAYLOAD]; // Shared receive buffer for incoming data.
|
||||
uint32_t pollCounter; // Throttle periodic recv polling.
|
||||
|
||||
// Direct shared results from Core 0 (bypasses responseQueue for reliability).
|
||||
// Socket operation result:
|
||||
volatile bool netResultReady; // Core 0 sets true when a socket op result is available.
|
||||
volatile uint8_t netResultSock; // Socket number for the result.
|
||||
volatile uint8_t netResultStatus; // Socket status from ESP32.
|
||||
volatile bool netResultSuccess; // Operation success flag.
|
||||
volatile uint8_t netResultOp; // Operation that completed (Sn_CR_CONNECT etc).
|
||||
// Recv data result:
|
||||
volatile bool netRecvReady; // Core 0 sets true when recv data is available.
|
||||
volatile uint8_t netRecvSock; // Socket number for recv data.
|
||||
volatile uint32_t netRecvLen; // Length of received data in netRecvBuf.
|
||||
// NET_CFG result:
|
||||
volatile bool netCfgReady; // Core 0 sets true when NET_CFG result is available.
|
||||
volatile bool netCfgSuccess; // True if config was retrieved successfully.
|
||||
uint8_t netCfgData[18]; // IP[4]+GW[4]+Subnet[4]+MAC[6] from ESP32.
|
||||
} t_Celestite;
|
||||
|
||||
// Public prototypes.
|
||||
uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
|
||||
uint8_t Celestite_Reset(t_Z80CPU *cpu);
|
||||
uint8_t Celestite_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t Celestite_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t Celestite_IO_MR(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_AR0(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_AR1(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_DR(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_Int(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_Memo(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_Ping(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_UFM(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_Unlock(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_R12(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_R37Latch(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_R37Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t Celestite_IO_Cfg(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
|
||||
#endif // CELESTITE_H
|
||||
103
projects/tzpuPico/src/include/drivers/Sharp/MZ-1R23.h
vendored
Normal file
103
projects/tzpuPico/src/include/drivers/Sharp/MZ-1R23.h
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ-1R23.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - MZ-1R23 Kanji ROM / MZ-1R24 Dictionary ROM Board
|
||||
// This file contains setup and driver to emulate the MZ-1R23 Kanji ROM board and
|
||||
// the MZ-1R24 Dictionary ROM board.
|
||||
//
|
||||
// I/O ports:
|
||||
// B8h (W): Control register
|
||||
// Bit 7: KANJI — 1=Kanji ROM, 0=Dictionary ROM
|
||||
// Bit 6: ENDIAN — 0=Bit7 left/Bit0 right, 1=Bit0 left/Bit7 right
|
||||
// Bits 0-1: BANK — Dictionary ROM bank select (00-11)
|
||||
//
|
||||
// B9h (W): Address set via OUT (C),A — upper byte from B register (A15-A8),
|
||||
// lower byte from A register (D7-D0).
|
||||
// Kanji mode: sets kanji pattern number; internal byte address = pattern * 32.
|
||||
// Dict mode: sets raw byte address within selected bank.
|
||||
//
|
||||
// B9h (R): Data read with auto-increment.
|
||||
// Kanji: pattern bytes in order upper-left, lower-left, upper-right, lower-right
|
||||
// (four 8x8 quadrants of a 16x16 character, 8 bytes per quadrant = 32 bytes total).
|
||||
//
|
||||
// Kanji pattern number = (JisH - 0x21 - ((JisH > 0x30) ? 0x08 : 0x00)) * 94
|
||||
// + (JisL - 0x21)
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write.
|
||||
//
|
||||
// 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 MZ1R23_H
|
||||
#define MZ1R23_H
|
||||
|
||||
// Constants.
|
||||
#define MZ1R23_KANJI_ROM_MAX 131072 // 128KB kanji ROM (4 × HN61256 32KB, ~4096 JIS patterns).
|
||||
#define MZ1R23_DICT_ROM_MAX 262144 // 256KB dictionary ROM (8 × HN61256 32KB = 4 × 64KB banks).
|
||||
#define MZ1R23_DICT_BANK_SIZE 65536 // 64KB per dictionary bank.
|
||||
#define MZ1R23_PATTERN_SIZE 32 // 32 bytes per 16x16 kanji pattern (4 quadrants × 8 bytes).
|
||||
|
||||
// Control register (B8h) bit definitions.
|
||||
#define MZ1R23_CTRL_KANJI 0x80 // Bit 7: 1=Kanji ROM, 0=Dictionary ROM.
|
||||
#define MZ1R23_CTRL_ENDIAN 0x40 // Bit 6: 1=Bit-reversed byte order.
|
||||
#define MZ1R23_CTRL_BANK_MASK 0x03 // Bits 0-1: Dictionary ROM bank select.
|
||||
|
||||
// Load state.
|
||||
enum t_MZ1R23LoadCtrl
|
||||
{
|
||||
MZ1R23_NO_FILE = 0, // No file specified.
|
||||
MZ1R23_NOT_LOADED = 1, // File specified but not yet loaded.
|
||||
MZ1R23_LOADED = 2 // ROM loaded and ready.
|
||||
};
|
||||
|
||||
// Struct for the MZ-1R23/MZ-1R24 board state.
|
||||
typedef struct
|
||||
{
|
||||
// Control registers.
|
||||
uint8_t ctrlReg; // B8h control register value.
|
||||
uint32_t readAddr; // Current byte-level read address (auto-increments on read).
|
||||
|
||||
// Kanji ROM.
|
||||
uint8_t *kanjiRom; // Kanji ROM data buffer.
|
||||
uint32_t kanjiRomSize; // Actual size of loaded kanji ROM.
|
||||
char *kanjiFileName; // Filename on SD card.
|
||||
enum t_MZ1R23LoadCtrl kanjiLoadCtl; // Load state.
|
||||
bool kanjiLoadPending; // True while inter-core load is in progress.
|
||||
|
||||
// Dictionary ROM (optional).
|
||||
uint8_t *dictRom; // Dictionary ROM data buffer (NULL if not present).
|
||||
uint32_t dictRomSize; // Actual size of loaded dictionary ROM.
|
||||
char *dictFileName; // Filename on SD card.
|
||||
enum t_MZ1R23LoadCtrl dictLoadCtl; // Load state.
|
||||
bool dictLoadPending; // True while inter-core load is in progress.
|
||||
} t_MZ1R23;
|
||||
|
||||
// Public prototypes.
|
||||
uint8_t MZ1R23_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
|
||||
uint8_t MZ1R23_Reset(t_Z80CPU *cpu);
|
||||
uint8_t MZ1R23_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t MZ1R23_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t MZ1R23_IO_Ctrl(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1R23_IO_Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
int MZ1R23_ProcessQueueResponses(t_Z80CPU *cpu);
|
||||
|
||||
#endif // MZ1R23_H
|
||||
84
projects/tzpuPico/src/include/drivers/Sharp/MZ-1R37.h
vendored
Normal file
84
projects/tzpuPico/src/include/drivers/Sharp/MZ-1R37.h
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ-1R37.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - MZ-1R37 640K EMM
|
||||
// This file contains setup and driver to emulate the MZ-1R37 640KB Expanded Memory
|
||||
// Manager board.
|
||||
//
|
||||
// I/O ports (no auto-increment — all addressing via OUT (C),A / IN A,(C)):
|
||||
// ACh (W): Address latch — sets address[19:8].
|
||||
// Bits from OUT (C),A: B register = address[19:16] (upper nibble)
|
||||
// A register = address[15:8] (mid byte)
|
||||
// ADh (W): Data write — address[7:0] from B register, data from A register.
|
||||
// ADh (R): Data read — address[7:0] from B register, data returned in A register.
|
||||
//
|
||||
// 20-bit address space (1MB), 640KB physically populated.
|
||||
// Addresses above 640KB (0xA0000) return 0xFF on read.
|
||||
//
|
||||
// Note from I/O map: "There is no automatic address increment."
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write.
|
||||
//
|
||||
// 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 MZ1R37_H
|
||||
#define MZ1R37_H
|
||||
|
||||
// Constants.
|
||||
#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.
|
||||
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.
|
||||
uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
|
||||
uint8_t MZ1R37_Reset(t_Z80CPU *cpu);
|
||||
uint8_t MZ1R37_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t MZ1R37_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t MZ1R37_IO_AddrLatch(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1R37_IO_Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
|
||||
#endif // MZ1R37_H
|
||||
135
projects/tzpuPico/src/include/drivers/Sharp/MZ1500.h
vendored
Normal file
135
projects/tzpuPico/src/include/drivers/Sharp/MZ1500.h
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ1500.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - Sharp MZ-1500
|
||||
// This file contains setup and drivers to mimic a Sharp MZ-1500 machine internally to
|
||||
// increase speed through use of internal RAM and provide virtual expansion drivers.
|
||||
// The MZ-1500 is a superset of the MZ-700 with an inbuilt QD drive instead of CMT,
|
||||
// a PCG (Programmable Character Generator), stereo PSG sound (SN76489AN), and a
|
||||
// Z80PIO-based printer interface replacing the MZ-700's simple latch.
|
||||
// Dip switches select MZ-700 mode or MZ-1500 mode at boot.
|
||||
//
|
||||
// MZ-1500 memory map:
|
||||
// 0000-0FFF : Monitor ROM (or DRAM via bank switching)
|
||||
// 1000-CFFF : DRAM
|
||||
// D000-D3FF : Text VRAM
|
||||
// D400-D7FF : PCG VRAM 1 (MZ-1500 only, PCGN[7:0])
|
||||
// D800-DBFF : Attribute VRAM (FC, BC, ATB)
|
||||
// DC00-DFFF : PCG VRAM 2 (MZ-1500 only, PCGN[9:8] + PCGE)
|
||||
// E000-E003 : 8255 PPI
|
||||
// E004-E007 : 8253 PIT
|
||||
// E008 : LS367 (joystick, HBLK, TEMPO) / GATE
|
||||
// E009 : PSG (simultaneous L+R write, MZ-1500 only)
|
||||
// E010-E012 : PCG-RAM control (MZ-1500 only)
|
||||
// E800-EFFF : User ROM / expansion ROM
|
||||
// F000-FFFF : PCG-RAM/CG-ROM bank (MZ-1500) or FDD ROM (MZ-700)
|
||||
//
|
||||
// MZ-1500 I/O ports (beyond MZ-700):
|
||||
// E5h (W) : PCG bank open + select (bits 0-1: 00=CGROM, 01=blue, 10=red, 11=green)
|
||||
// E6h (W) : PCG bank close
|
||||
// E9h (W) : PSG SN76489AN simultaneous (both L+R channels)
|
||||
// F0h (W) : Text/PCG priority + PCG display enable
|
||||
// F1h (W) : Palette
|
||||
// F2h (W) : PSG SN76489AN left channel
|
||||
// F3h (W) : PSG SN76489AN right channel
|
||||
// F4h-F7h : QD I/F via Z80A-SIO (inbuilt, same as MZ-1E19)
|
||||
// FCh-FFh : Printer via Z80PIO (replaces MZ-700 simple printer latch)
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write based on MZ-700 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 MZ1500_H
|
||||
#define MZ1500_H
|
||||
|
||||
#include "drivers/Sharp/MZ.h"
|
||||
#include "drivers/Sharp/WD1773.h"
|
||||
#include "drivers/Sharp/QDDrive.h"
|
||||
|
||||
// Constants.
|
||||
#define MZ1500_MEMBANK_0 0 // Primary RAM bank.
|
||||
#define MZ1500_MEMBANK_1 1 // RAM bank to use for paging in RAM.
|
||||
#define MZ1500_MEMBANK_CGROM 39 // Bank for CG-ROM storage (D000-DFFF = 4K). Banks 40-63 reserved for RFS.
|
||||
#define MZ1500_UPPERMEM_BLOCKS 64 // Number of blocks in the upper paged 12K RAM.
|
||||
|
||||
// PCG constants.
|
||||
#define MZ1500_PCG_BANK_CGROM 0 // CG-ROM (character generator ROM).
|
||||
#define MZ1500_PCG_BANK_BLUE 1 // PCG blue colour plane.
|
||||
#define MZ1500_PCG_BANK_RED 2 // PCG red colour plane.
|
||||
#define MZ1500_PCG_BANK_GREEN 3 // PCG green colour plane.
|
||||
#define MZ1500_PCG_BANK_BLOCKS 16 // Blocks for F000-FFFF (16 × 512B = 8KB).
|
||||
|
||||
// Machine sub-mode — selectable by dip switch on real hardware.
|
||||
typedef enum
|
||||
{
|
||||
MZ1500_MODE_MZ700 = 0, // Running in MZ-700 compatibility mode.
|
||||
MZ1500_MODE_MZ1500 = 1 // Running in native MZ-1500 mode.
|
||||
} t_MZ1500Mode;
|
||||
|
||||
// Saved memioPtr entry for DRAM/MMIO switching.
|
||||
// On real hardware, virtual devices (RFS etc.) at 0xE800-0xEFFF are not on the bus when DRAM is
|
||||
// selected. We save and NULL their memioPtr entries on E1, restore on E3/E4.
|
||||
#define MZ1500_MAX_SAVED_MEMIO 32
|
||||
#ifndef SAVED_MEMIO_ENTRY_DEFINED
|
||||
#define SAVED_MEMIO_ENTRY_DEFINED
|
||||
typedef struct
|
||||
{
|
||||
uint16_t addr;
|
||||
t_MemoryFunc func;
|
||||
} t_savedMemioEntry;
|
||||
#endif
|
||||
|
||||
// Struct for controlling the virtual MZ-1500 state.
|
||||
typedef struct
|
||||
{
|
||||
t_MZ1500Mode mode; // MZ-700 or MZ-1500 sub-mode.
|
||||
uint8_t regCtrl; // Control register.
|
||||
bool loDRAMen; // Lower bank 0000:0FFF DRAM enabled, else monitor.
|
||||
bool hiDRAMen; // Higher bank D000:FFFF DRAM enabled, else memory mapped I/O.
|
||||
bool inhibit; // Inhibit access to upper 12K address range.
|
||||
uint32_t upmembankPtr[MZ1500_UPPERMEM_BLOCKS]; // 12K mirror pointers for bank switching.
|
||||
t_savedMemioEntry upMemio[MZ1500_MAX_SAVED_MEMIO]; // Saved memioPtr entries for DRAM switching.
|
||||
uint8_t upMemioCount; // Number of saved memioPtr entries.
|
||||
|
||||
// MZ-1500-specific PCG state.
|
||||
bool pcgBankOpen; // True when PCG bank is mapped at F000-FFFF.
|
||||
uint8_t pcgBankSelect; // Active PCG bank (0=CGROM, 1=blue, 2=red, 3=green).
|
||||
uint8_t pcgPriority; // Text/PCG priority register (F0h).
|
||||
uint8_t pcgPalette; // Palette register (F1h).
|
||||
uint32_t pcgSavedBankPtr[MZ1500_PCG_BANK_BLOCKS]; // Saved F000-FFFF bank ptrs when PCG bank is open.
|
||||
} t_MZ1500Ctrl;
|
||||
|
||||
// Private prototypes.
|
||||
void MZ1500_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len);
|
||||
void MZ1500_readROMData(void *ctx, void *cfg, char *buf, int len);
|
||||
uint8_t MZ1500_IO_MemoryBankPorts(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_IO_SIO_NotPresent(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_IO_PIT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_IO_PSG(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_IO_PCG(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_IO_Debug(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ1500_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t MZ1500_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig *config, const char *ifName);
|
||||
|
||||
#endif // MZ1500_H
|
||||
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
|
||||
67
projects/tzpuPico/src/include/drivers/Sharp/MZ2200.h
vendored
Normal file
67
projects/tzpuPico/src/include/drivers/Sharp/MZ2200.h
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: MZ2200.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - Sharp MZ-2200
|
||||
// This file contains setup and drivers to mimic a Sharp MZ-2200 machine internally to
|
||||
// increase speed through use of internal RAM and provide virtual expansion drivers.
|
||||
// Based on the MZ80A driver but adapted for the MZ-2200 memory map and peripherals.
|
||||
//
|
||||
// MZ-2200 I/O map (I/O port space, not memory-mapped):
|
||||
// 0xD8-0xDE : MB8866 FDC
|
||||
// 0xE0-0xE3 : 8255 PPI (cassette, BST/NST memory mode)
|
||||
// 0xE4-0xE7 : 8253 PIT (timers)
|
||||
// 0xE8-0xEB : Z80 PIO (keyboard, VRAM paging)
|
||||
// 0xF4-0xF7 : Colour CRT / Graphics VRAM bank select
|
||||
//
|
||||
// MZ-2200 memory map:
|
||||
// Boot (BST): 0x0000-0x7FFF = IPL ROM, 0x8000-0xFFFF = RAM
|
||||
// Normal (NST): 0x0000-0xFFFF = RAM (0x8000 content swapped to 0x0000)
|
||||
// VRAM (PIO A7=1): 0xD000-0xD7FF = Character VRAM (A6=1)
|
||||
// 0xC000-0xFFFF = Graphics VRAM (A6=0, bank via port 0xF7)
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write based on MZ80A 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 MZ2200_H
|
||||
#define MZ2200_H
|
||||
|
||||
#include "drivers/Sharp/MZ.h"
|
||||
#include "drivers/Sharp/WD1773.h"
|
||||
|
||||
// Constants.
|
||||
#define MZ2200_MEMBANK_0 0 // Primary RAM bank (IPL ROM + RAM in boot, swapped RAM in normal).
|
||||
#define MZ2200_MEMBANK_1 1 // Secondary RAM bank (0x8000-0xFFFF in normal mode).
|
||||
|
||||
// Private prototypes.
|
||||
void MZ2200_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len);
|
||||
void MZ2200_readROMData(void *ctx, void *cfg, char *buf, int len);
|
||||
uint8_t MZ2200_IO_Debug(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ2200_IO_PIT(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ2200_IO_PPI(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ2200_IO_PIO(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t MZ2200_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t MZ2200_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t MZ2200_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfig *config, const char *ifName);
|
||||
|
||||
#endif // MZ2200_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
|
||||
@@ -41,11 +41,14 @@
|
||||
// On real hardware, virtual devices (RFS etc.) at 0xE800-0xEFFF are not on the bus when DRAM is
|
||||
// selected. We save and NULL their memioPtr entries on E1, restore on E3/E4.
|
||||
#define MZ700_MAX_SAVED_MEMIO 32
|
||||
#ifndef SAVED_MEMIO_ENTRY_DEFINED
|
||||
#define SAVED_MEMIO_ENTRY_DEFINED
|
||||
typedef struct
|
||||
{
|
||||
uint16_t addr;
|
||||
t_MemoryFunc func;
|
||||
} t_savedMemioEntry;
|
||||
#endif
|
||||
|
||||
// Struct for controlling the virtual MZ700 state.
|
||||
typedef struct
|
||||
|
||||
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);
|
||||
|
||||
69
projects/tzpuPico/src/include/drivers/Sharp/PIO-3034.h
vendored
Normal file
69
projects/tzpuPico/src/include/drivers/Sharp/PIO-3034.h
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Name: PIO-3034.h
|
||||
// Created: May 2026
|
||||
// Author(s): Philip Smart
|
||||
// Description: Z80 CPU DRIVER - PIO-3034 320K EMM (IO DATA)
|
||||
// This file contains setup and driver to emulate the PIO-3034 320KB Expanded Memory
|
||||
// Manager board by IO DATA.
|
||||
//
|
||||
// I/O ports (base address selectable via DIP switch, default 00h):
|
||||
// Base+0 (W): Address counter bits [7:0]
|
||||
// Base+1 (W): Address counter bits [15:8]
|
||||
// Base+2 (W): Address counter bits [18:16]
|
||||
// Base+3 (R): Data read, address counter auto-increment
|
||||
// Base+3 (W): Data write, address counter auto-increment
|
||||
//
|
||||
// 19-bit address space (512KB), 320KB physically populated.
|
||||
// Addresses above 320KB (0x50000) return 0xFF on read.
|
||||
//
|
||||
// Credits:
|
||||
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
|
||||
//
|
||||
// History: May 2026 v1.0 - Initial write.
|
||||
//
|
||||
// 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 PIO3034_H
|
||||
#define PIO3034_H
|
||||
|
||||
// Constants.
|
||||
#define PIO3034_RAM_SIZE 327680 // 320KB = 320 × 1024 bytes.
|
||||
#define PIO3034_ADDR_MASK 0x7FFFF // 19-bit address space (512KB).
|
||||
#define PIO3034_DEFAULT_BASE 0x00 // Default I/O base address.
|
||||
#define PIO3034_PORT_COUNT 4 // Number of consecutive I/O ports used.
|
||||
|
||||
// Struct for the PIO-3034 EMM board state.
|
||||
typedef struct
|
||||
{
|
||||
uint8_t *ram; // 320KB RAM buffer.
|
||||
uint32_t addr; // 19-bit address counter.
|
||||
uint8_t ioBase; // I/O base address (DIP-switch selectable).
|
||||
} t_PIO3034;
|
||||
|
||||
// Public prototypes.
|
||||
uint8_t PIO3034_Init(t_Z80CPU *cpu, t_drvIFConfig *config);
|
||||
uint8_t PIO3034_Reset(t_Z80CPU *cpu);
|
||||
uint8_t PIO3034_PollCB(t_Z80CPU *cpu);
|
||||
uint8_t PIO3034_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param);
|
||||
uint8_t PIO3034_IO_AddrLo(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t PIO3034_IO_AddrMid(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t PIO3034_IO_AddrHi(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
uint8_t PIO3034_IO_Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
|
||||
|
||||
#endif // PIO3034_H
|
||||
@@ -52,8 +52,12 @@
|
||||
// Constants
|
||||
|
||||
// Sharp QD values
|
||||
#define MAX_QD_SIZE 61455U // Standard QD disk image size (~60KB)
|
||||
#define MAX_QD_FORMAT_SIZE 64139U // Formatted size of a QD.
|
||||
#define MAX_QD_SIZE 61455U // Standard QD disk image size (~60KB, compact format)
|
||||
#define MAX_QD_FORMAT_SIZE 64139U // Formatted size of a QD (compact).
|
||||
#define MAX_QDF_SIZE 81920U // Japanese standard QDF format (full formatted track, 80KB)
|
||||
#define QDF_HEADER_SIZE 16U // QDF file header size ("-QD format-" + padding)
|
||||
#define QDF_FILE_SIZE (QDF_HEADER_SIZE + MAX_QDF_SIZE) // Total QDF file size (81936 bytes)
|
||||
#define QDF_SIGNATURE "-QD format-" // QDF file header signature (11 bytes)
|
||||
#ifndef ROTATION_US
|
||||
#define ROTATION_US 200000ULL // 300 RPM spiral track rotation period
|
||||
#endif
|
||||
@@ -137,8 +141,10 @@ typedef struct
|
||||
uint32_t bytesWritten; // Bytes written in current block
|
||||
|
||||
// Disk & file system
|
||||
uint8_t *diskImage; // Pointer to 61455-byte disk buffer
|
||||
uint32_t diskSize; // Size of disk image (always 61455)
|
||||
uint8_t *diskImage; // Pointer to active disk data (may be offset from diskBuf for QDF).
|
||||
uint8_t *diskBuf; // Original allocated buffer base (used for loads).
|
||||
uint32_t diskBufSize; // Allocated buffer size.
|
||||
uint32_t diskSize; // Active disk image size.
|
||||
char *filename; // Current disk filename (for SD save/load)
|
||||
|
||||
// Inter-core communication
|
||||
|
||||
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;
|
||||
|
||||
/* ========================================================================================
|
||||
|
||||
22
projects/tzpuPico/src/include/intercore.h
vendored
22
projects/tzpuPico/src/include/intercore.h
vendored
@@ -50,7 +50,15 @@ typedef enum
|
||||
MSG_LOAD_COMPLETE, // Previous Load request completed.
|
||||
MSG_WRITE_COMPLETE, // Previous Write request completed.
|
||||
MSG_WRITE_OP_SCHEDULED, // Previous Scheduled Write request completed.
|
||||
MSG_READ_COMPLETE // Previous Read request completed.
|
||||
MSG_READ_COMPLETE, // Previous Read request completed.
|
||||
|
||||
// Network operations (Celestite W5100 emulation)
|
||||
MSG_NET_SOCKET, // Socket operation request (open/connect/listen/close)
|
||||
MSG_NET_SEND, // Send data to socket
|
||||
MSG_NET_RECV, // Receive data from socket (poll)
|
||||
MSG_NET_CFG, // Get network config from ESP32
|
||||
MSG_NET_PING, // ICMP ping request
|
||||
MSG_NET_COMPLETE // Network operation complete (response)
|
||||
} t_MsgType;
|
||||
|
||||
typedef struct
|
||||
@@ -86,6 +94,18 @@ typedef struct
|
||||
bool success;
|
||||
uint32_t size;
|
||||
} response; // For completions
|
||||
struct
|
||||
{
|
||||
uint8_t sockNum; // Socket number (0-3)
|
||||
uint8_t operation; // Socket operation (W5100 Sn_CR value)
|
||||
uint8_t protocol; // Protocol (1=TCP, 2=UDP)
|
||||
uint8_t status; // Result status / new socket state
|
||||
uint32_t ipAddr; // IP address (network byte order)
|
||||
uint16_t port; // Port number (host byte order)
|
||||
void *buffer; // Data buffer for send/recv
|
||||
uint32_t size; // Data size
|
||||
uint32_t rtt; // Ping RTT in ms
|
||||
} netOp; // For MSG_NET_* operations
|
||||
};
|
||||
} t_CoreMsg;
|
||||
|
||||
|
||||
15
projects/tzpuPico/src/include/ipc_protocol.h
vendored
15
projects/tzpuPico/src/include/ipc_protocol.h
vendored
@@ -48,6 +48,21 @@
|
||||
#define IPCF_CMD_RFD 0x08 // Read floppy disk image file
|
||||
#define IPCF_CMD_RQD 0x09 // Read quick-disk image file
|
||||
#define IPCF_CMD_RRF 0x0A // Read RAM-file backup image
|
||||
#define IPCF_CMD_DIR 0x0B // Read directory listing (returns text in payload)
|
||||
|
||||
// Network commands (Celestite W5100 emulation)
|
||||
#define IPCF_CMD_NET_CFG 0x10 // Get ESP32 network config (IP, gateway, subnet, MAC)
|
||||
#define IPCF_CMD_NET_SOCK 0x11 // Socket operation (open/connect/listen/close/discon)
|
||||
#define IPCF_CMD_NET_SEND 0x12 // Send data to socket (payload = data to send)
|
||||
#define IPCF_CMD_NET_RECV 0x13 // Receive data from socket (response payload = data)
|
||||
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
|
||||
|
||||
// Network file server commands (BASIC NET: device → ESP32 → PC file server)
|
||||
#define IPCF_CMD_NETFS_DIR 0x20 // Get directory from file server (response: 32-byte entries)
|
||||
#define IPCF_CMD_NETFS_INFO 0x21 // Get file info (filename in header, response: atrb+size+addrs)
|
||||
#define IPCF_CMD_NETFS_READ 0x22 // Read file data (filename+offset in header, response: data)
|
||||
#define IPCF_CMD_NETFS_WRITE 0x23 // Write file (filename+size in header, payload: data)
|
||||
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status codes (t_IpcFrameHdr::status — response frames only)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
#include "Z80CPU.h"
|
||||
#include "FSPI.h"
|
||||
#include "ESP.h"
|
||||
#include "drivers/Sharp/Celestite.h"
|
||||
extern t_Celestite *celestiteCtrl; // Defined in Celestite.c, used for direct shared results.
|
||||
#include "rp2350.h"
|
||||
#include "flash_ram.h"
|
||||
#include "usb_bridge.h"
|
||||
@@ -776,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)
|
||||
{
|
||||
@@ -877,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");
|
||||
}
|
||||
@@ -889,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.
|
||||
@@ -984,8 +1031,10 @@ 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.
|
||||
|
||||
response.requestId = qmsg.requestId;
|
||||
response.response.success = false;
|
||||
@@ -1007,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;
|
||||
@@ -1094,7 +1143,7 @@ void processInterCoreCommands(void)
|
||||
{
|
||||
responseType = MSG_LOAD_COMPLETE;
|
||||
uint8_t *bufBefore = qmsg.fileOp.buffer;
|
||||
for (int attempt = 0; attempt < 10; attempt++)
|
||||
for (int attempt = 0; attempt < 3; attempt++)
|
||||
{
|
||||
qmsg.fileOp.buffer = bufBefore;
|
||||
bytesXfer = ESP_readRamFile(qmsg.fileOp.filename, NULL, NULL, NULL, (uint8_t **)&qmsg.fileOp.buffer, qmsg.fileOp.size, true, 0);
|
||||
@@ -1108,6 +1157,16 @@ void processInterCoreCommands(void)
|
||||
success = true;
|
||||
response.response.size = bytesXfer;
|
||||
}
|
||||
else
|
||||
{
|
||||
// File doesn't exist — treat as fresh uninitialised RAM board.
|
||||
// Buffer is already pre-filled by the driver (0xFF for Celestite
|
||||
// MZ-1R12, matching empty battery-backed SRAM). The backing file
|
||||
// will be created on first save from the Z80.
|
||||
debugf("RAMFILE: '%s' not found, using pre-filled buffer (%d bytes)\r\n", qmsg.fileOp.filename, (int) qmsg.fileOp.size);
|
||||
success = true;
|
||||
response.response.size = qmsg.fileOp.size;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1232,6 +1291,104 @@ void processInterCoreCommands(void)
|
||||
responseType = MSG_LOAD_COMPLETE;
|
||||
success = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Network operations (Celestite W5100 emulation).
|
||||
if (qmsg.type == MSG_NET_CFG)
|
||||
{
|
||||
// Write NET_CFG result to shared struct (bypasses responseQueue).
|
||||
uint8_t ipBuf[4], gwBuf[4], subBuf[4], macBuf[6];
|
||||
if (celestiteCtrl && ESP_netCfg(ipBuf, gwBuf, subBuf, macBuf))
|
||||
{
|
||||
memcpy(celestiteCtrl->netCfgData, ipBuf, 4);
|
||||
memcpy(celestiteCtrl->netCfgData + 4, gwBuf, 4);
|
||||
memcpy(celestiteCtrl->netCfgData + 8, subBuf, 4);
|
||||
memcpy(celestiteCtrl->netCfgData + 12, macBuf, 6);
|
||||
celestiteCtrl->netCfgSuccess = true;
|
||||
__dmb();
|
||||
celestiteCtrl->netCfgReady = true;
|
||||
debugf("NET_CFG: IP=%d.%d.%d.%d GW=%d.%d.%d.%d\r\n",
|
||||
ipBuf[0], ipBuf[1], ipBuf[2], ipBuf[3],
|
||||
gwBuf[0], gwBuf[1], gwBuf[2], gwBuf[3]);
|
||||
}
|
||||
else if (celestiteCtrl)
|
||||
{
|
||||
celestiteCtrl->netCfgSuccess = false;
|
||||
celestiteCtrl->netCfgReady = true;
|
||||
}
|
||||
skipResponse = true;
|
||||
}
|
||||
else if (qmsg.type == MSG_NET_SOCKET)
|
||||
{
|
||||
uint8_t sockStatus = 0;
|
||||
uint8_t op = qmsg.netOp.operation;
|
||||
if (ESP_netSocket(qmsg.netOp.sockNum, op, qmsg.netOp.protocol,
|
||||
qmsg.netOp.ipAddr, qmsg.netOp.port, &sockStatus))
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
debugf("NET_SOCK: s=%d op=%d status=0x%02x\r\n",
|
||||
(int)qmsg.netOp.sockNum, (int)op, (int)sockStatus);
|
||||
// Write result directly to shared struct (bypasses responseQueue).
|
||||
// Include drivers/Sharp/Celestite.h provides t_Celestite*.
|
||||
{
|
||||
if (celestiteCtrl)
|
||||
{
|
||||
celestiteCtrl->netResultSock = qmsg.netOp.sockNum;
|
||||
celestiteCtrl->netResultStatus = sockStatus;
|
||||
celestiteCtrl->netResultSuccess = success;
|
||||
celestiteCtrl->netResultOp = op;
|
||||
__dmb(); // Ensure all fields written before setting flag.
|
||||
celestiteCtrl->netResultReady = true;
|
||||
}
|
||||
}
|
||||
skipResponse = true; // Don't also push to queue.
|
||||
}
|
||||
else if (qmsg.type == MSG_NET_SEND)
|
||||
{
|
||||
uint32_t sent = 0;
|
||||
ESP_netSend(qmsg.netOp.sockNum, (uint8_t *)qmsg.netOp.buffer,
|
||||
qmsg.netOp.size, &sent);
|
||||
debugf("NET_SEND: s=%d len=%lu sent=%lu\r\n",
|
||||
(int)qmsg.netOp.sockNum, (unsigned long)qmsg.netOp.size, (unsigned long)sent);
|
||||
skipResponse = true; // Fire-and-forget.
|
||||
}
|
||||
else if (qmsg.type == MSG_NET_RECV)
|
||||
{
|
||||
uint32_t recvd = 0;
|
||||
ESP_netRecv(qmsg.netOp.sockNum, (uint8_t *)qmsg.netOp.buffer,
|
||||
qmsg.netOp.size, &recvd);
|
||||
if (celestiteCtrl)
|
||||
{
|
||||
if (recvd > 0)
|
||||
{
|
||||
// Data available — signal to IDM handler.
|
||||
celestiteCtrl->netRecvLen = recvd;
|
||||
celestiteCtrl->netRecvSock = qmsg.netOp.sockNum;
|
||||
__dmb();
|
||||
celestiteCtrl->netRecvReady = true;
|
||||
debugf("NET_RECV: s=%d len=%lu\r\n", (int)qmsg.netOp.sockNum, (unsigned long)recvd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No data yet — clear pending so next RX_RSR poll retries.
|
||||
celestiteCtrl->netRecvPending[qmsg.netOp.sockNum] = false;
|
||||
}
|
||||
}
|
||||
skipResponse = true;
|
||||
}
|
||||
else if (qmsg.type == MSG_NET_PING)
|
||||
{
|
||||
uint32_t rtt = 0xFFFFFFFF;
|
||||
if (celestiteCtrl && ESP_netPing(qmsg.netOp.ipAddr, &rtt))
|
||||
{
|
||||
// Write RTT directly to memoReg (read by Z80 via port 67h).
|
||||
celestiteCtrl->memoReg = (rtt < 255) ? (uint8_t)rtt : 0xFE;
|
||||
debugf("PING: RTT=%d ms\r\n", (int)rtt);
|
||||
}
|
||||
skipResponse = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,7 +1400,7 @@ void processInterCoreCommands(void)
|
||||
// No response for scheduled events as they are rapid and many. Scheduled events are for storing
|
||||
// data where the host writes adhoc so it cuts down on physical writes, only when the host has stopped
|
||||
// writing is the data flushed.
|
||||
if (qmsg.type != MSG_WRITE_FILE_SCHEDULED)
|
||||
if (qmsg.type != MSG_WRITE_FILE_SCHEDULED && !skipResponse)
|
||||
{
|
||||
bootStage(BOOTP_IC_RESPONSE);
|
||||
response.type = responseType;
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.491
|
||||
2.74
|
||||
|
||||
541
projects/tzpuPico/src/test/Celestite/celestite_stress.asm
Normal file
541
projects/tzpuPico/src/test/Celestite/celestite_stress.asm
Normal file
@@ -0,0 +1,541 @@
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- Celestite LAN Board — Network Stress Test
|
||||
;- All output on MZ-1500 screen (VRAM D000-D3FF). Loops continuously.
|
||||
;- Also mirrors summary to debug port (50h).
|
||||
;- D800-DFFF colour attributes are NOT touched.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
|
||||
PORT_DBG EQU 050h
|
||||
PORT_MR EQU 060h
|
||||
PORT_AR0 EQU 061h
|
||||
PORT_AR1 EQU 062h
|
||||
PORT_DR EQU 063h
|
||||
PORT_PING EQU 067h
|
||||
|
||||
CMD_OPEN EQU 001h
|
||||
CMD_CLOSE EQU 010h
|
||||
ST_ESTAB EQU 017h
|
||||
ST_CLOSED EQU 000h
|
||||
|
||||
TPSTART EQU 010F0h
|
||||
MEMSTART EQU 01200h
|
||||
VRAM EQU 0D000h
|
||||
VL EQU 40
|
||||
|
||||
ORG TPSTART
|
||||
ATRB: DB 001h
|
||||
NAME: DB "CELESTITE STRESS", 0Dh
|
||||
SIZE: DW MEND - MEMSTART
|
||||
DTADR: DW MEMSTART
|
||||
EXADR: DW MEMSTART
|
||||
COMNT: DS 104
|
||||
|
||||
ORG MEMSTART
|
||||
|
||||
START: LD SP, 01180h
|
||||
;- Init W5100.
|
||||
LD A, 080h
|
||||
OUT (PORT_MR), A
|
||||
LD A, 003h
|
||||
OUT (PORT_MR), A
|
||||
;- Write gateway IP to GAR.
|
||||
XOR A
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 192
|
||||
OUT (PORT_DR), A
|
||||
LD A, 168
|
||||
OUT (PORT_DR), A
|
||||
LD A, 1
|
||||
OUT (PORT_DR), A
|
||||
LD A, 1
|
||||
OUT (PORT_DR), A
|
||||
|
||||
LD HL, 0
|
||||
LD (LOOPCNT), HL
|
||||
|
||||
;- ============================================================
|
||||
MAINLP: ;- Clear text VRAM only (D000-D3E7, 1000 bytes). Leave D800 colours alone.
|
||||
LD HL, VRAM
|
||||
LD BC, 1000
|
||||
CLR: LD (HL), 000h
|
||||
INC HL
|
||||
DEC BC
|
||||
LD A, B
|
||||
OR C
|
||||
JR NZ, CLR
|
||||
|
||||
LD HL, (LOOPCNT)
|
||||
INC HL
|
||||
LD (LOOPCNT), HL
|
||||
|
||||
;- Line 0: Title.
|
||||
LD HL, VRAM
|
||||
LD DE, S_TITLE
|
||||
CALL PVRAM
|
||||
|
||||
;- Line 1: Loop count.
|
||||
LD HL, VRAM + VL
|
||||
LD DE, S_LOOP
|
||||
CALL PVRAM
|
||||
LD A, (LOOPCNT+1)
|
||||
CALL PHEXV
|
||||
LD A, (LOOPCNT)
|
||||
CALL PHEXV
|
||||
|
||||
;- ---- PING (line 3) ----
|
||||
LD HL, VRAM + VL*3
|
||||
LD DE, S_PING
|
||||
CALL PVRAM
|
||||
XOR A
|
||||
OUT (PORT_PING), A
|
||||
LD B, 250
|
||||
PNGW: PUSH BC
|
||||
IN A, (PORT_PING)
|
||||
POP BC
|
||||
CP 0FFh
|
||||
JR NZ, PNGGOT
|
||||
LD DE, 12000
|
||||
PNGD: DEC DE
|
||||
LD A, D
|
||||
OR E
|
||||
JR NZ, PNGD
|
||||
DJNZ PNGW
|
||||
LD HL, VRAM + VL*3 + 28
|
||||
LD DE, S_TOUT
|
||||
CALL PVRAM
|
||||
JR PNGDN
|
||||
PNGGOT: LD HL, VRAM + VL*3 + 28
|
||||
CALL PDECV
|
||||
LD DE, S_MS
|
||||
CALL PVRAM
|
||||
PNGDN:
|
||||
|
||||
;- ---- TCP CONNECT (line 4) ----
|
||||
LD HL, VRAM + VL*4
|
||||
LD DE, S_TCP
|
||||
CALL PVRAM
|
||||
;- Close.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, CMD_CLOSE
|
||||
OUT (PORT_DR), A
|
||||
;- Mode = TCP.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
XOR A
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 001h
|
||||
OUT (PORT_DR), A
|
||||
;- Dest IP.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 00Ch
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 192
|
||||
OUT (PORT_DR), A
|
||||
LD A, 168
|
||||
OUT (PORT_DR), A
|
||||
LD A, 1
|
||||
OUT (PORT_DR), A
|
||||
LD A, 1
|
||||
OUT (PORT_DR), A
|
||||
;- Port 80.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 010h
|
||||
OUT (PORT_AR1), A
|
||||
XOR A
|
||||
OUT (PORT_DR), A
|
||||
LD A, 80
|
||||
OUT (PORT_DR), A
|
||||
;- OPEN + CONNECT.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, CMD_OPEN
|
||||
OUT (PORT_DR), A
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 004h
|
||||
OUT (PORT_DR), A
|
||||
;- Wait.
|
||||
LD B, 250
|
||||
TCPW: PUSH BC
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 003h
|
||||
OUT (PORT_AR1), A
|
||||
IN A, (PORT_DR)
|
||||
POP BC
|
||||
CP ST_ESTAB
|
||||
JR Z, TCPOK
|
||||
CP ST_CLOSED
|
||||
JP Z, TCPFL
|
||||
LD DE, 20000
|
||||
TCPD: DEC DE
|
||||
LD A, D
|
||||
OR E
|
||||
JR NZ, TCPD
|
||||
DJNZ TCPW
|
||||
TCPFL: LD HL, VRAM + VL*4 + 28
|
||||
LD DE, S_FAIL
|
||||
CALL PVRAM
|
||||
JP HTTPDN
|
||||
TCPOK: LD HL, VRAM + VL*4 + 28
|
||||
LD DE, S_PASS
|
||||
CALL PVRAM
|
||||
|
||||
;- ---- HTTP GET (line 5) ----
|
||||
LD HL, VRAM + VL*5
|
||||
LD DE, S_HTTP
|
||||
CALL PVRAM
|
||||
;- Reset TX pointers (TX_RD=0, TX_WR=0 at 0x0422-0x0425).
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 022h
|
||||
OUT (PORT_AR1), A
|
||||
XOR A
|
||||
OUT (PORT_DR), A ; TX_RD high = 0.
|
||||
OUT (PORT_DR), A ; TX_RD low = 0.
|
||||
OUT (PORT_DR), A ; TX_WR high = 0.
|
||||
OUT (PORT_DR), A ; TX_WR low = 0.
|
||||
;- Write request to TX buffer.
|
||||
LD A, 040h
|
||||
OUT (PORT_AR0), A
|
||||
XOR A
|
||||
OUT (PORT_AR1), A
|
||||
LD HL, HTTPREQ
|
||||
HTTPWR: LD A, (HL)
|
||||
OR A
|
||||
JR Z, HTTPWD
|
||||
OUT (PORT_DR), A
|
||||
INC HL
|
||||
JR HTTPWR
|
||||
HTTPWD: LD DE, HTTPREQ
|
||||
OR A
|
||||
SBC HL, DE
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 024h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, H
|
||||
OUT (PORT_DR), A
|
||||
LD A, L
|
||||
OUT (PORT_DR), A
|
||||
;- SEND.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 020h
|
||||
OUT (PORT_DR), A
|
||||
;- Poll RX_RSR.
|
||||
LD B, 250
|
||||
HTTPRW: PUSH BC
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 026h
|
||||
OUT (PORT_AR1), A
|
||||
IN A, (PORT_DR)
|
||||
LD H, A
|
||||
IN A, (PORT_DR)
|
||||
LD L, A
|
||||
POP BC
|
||||
LD A, H
|
||||
OR L
|
||||
JR NZ, HTTPGOT
|
||||
LD DE, 20000
|
||||
HTTPRD: DEC DE
|
||||
LD A, D
|
||||
OR E
|
||||
JR NZ, HTTPRD
|
||||
DJNZ HTTPRW
|
||||
LD HL, VRAM + VL*5 + 28
|
||||
LD DE, S_TOUT
|
||||
CALL PVRAM
|
||||
JP HTTPDN
|
||||
|
||||
HTTPGOT: ;- Show hex count on line 5.
|
||||
PUSH HL
|
||||
LD HL, VRAM + VL*5 + 28
|
||||
POP DE
|
||||
LD A, D
|
||||
CALL PHEXV
|
||||
LD A, E
|
||||
CALL PHEXV
|
||||
LD DE, S_BYT
|
||||
CALL PVRAM
|
||||
|
||||
;- Read HTTP response and display on screen lines 7-24 (18 lines).
|
||||
LD A, 060h
|
||||
OUT (PORT_AR0), A
|
||||
XOR A
|
||||
OUT (PORT_AR1), A
|
||||
LD HL, VRAM + VL*7
|
||||
LD C, 200 ; Max bytes to display.
|
||||
HTDISP: IN A, (PORT_DR)
|
||||
CP 00Dh
|
||||
JR Z, HTNXT ; Skip CR.
|
||||
CP 00Ah
|
||||
JR Z, HTNL ; LF = new line.
|
||||
;- Convert and write to VRAM.
|
||||
PUSH HL
|
||||
PUSH DE
|
||||
CALL A2DSP
|
||||
POP DE
|
||||
POP HL
|
||||
LD (HL), A
|
||||
INC HL
|
||||
JR HTCHK
|
||||
HTNL: ;- Advance to next 40-char line. Round HL up to next VL boundary.
|
||||
;- New HL = ((HL - VRAM + VL) / VL) * VL + VRAM.
|
||||
PUSH DE
|
||||
LD DE, VRAM
|
||||
OR A
|
||||
SBC HL, DE ; HL = offset from VRAM.
|
||||
LD A, L
|
||||
ADD A, VL
|
||||
JR NC, HTNL2
|
||||
INC H
|
||||
HTNL2: LD L, A
|
||||
;- Round down to VL boundary: offset = (offset / 40) * 40.
|
||||
;- Approximate: clear low bits. 40 = 8*5, not power of 2. Use division.
|
||||
;- Simple: subtract (offset mod 40). Use repeated subtraction.
|
||||
LD A, L
|
||||
LD E, A
|
||||
LD A, H
|
||||
LD D, A
|
||||
HTNL3: LD A, D
|
||||
OR A
|
||||
JR NZ, HTNL4
|
||||
LD A, E
|
||||
CP VL
|
||||
JR C, HTNL5
|
||||
HTNL4: LD A, E
|
||||
SUB VL
|
||||
LD E, A
|
||||
JR NC, HTNL3
|
||||
DEC D
|
||||
JR HTNL3
|
||||
HTNL5: ;- E = offset mod 40. Subtract from HL.
|
||||
LD A, L
|
||||
SUB E
|
||||
LD L, A
|
||||
JR NC, HTNL6
|
||||
DEC H
|
||||
HTNL6: LD DE, VRAM
|
||||
ADD HL, DE
|
||||
POP DE
|
||||
JR HTCHK
|
||||
HTNXT: ;- Just consumed CR — skip, don't advance.
|
||||
HTCHK: ;- Bounds check.
|
||||
LD A, H
|
||||
CP 0D4h
|
||||
JR NC, HTEND
|
||||
DEC C
|
||||
JR NZ, HTDISP
|
||||
HTEND:
|
||||
;- RECV confirm.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, 040h
|
||||
OUT (PORT_DR), A
|
||||
|
||||
HTTPDN: ;- Close socket.
|
||||
LD A, 004h
|
||||
OUT (PORT_AR0), A
|
||||
LD A, 001h
|
||||
OUT (PORT_AR1), A
|
||||
LD A, CMD_CLOSE
|
||||
OUT (PORT_DR), A
|
||||
|
||||
;- Debug port summary.
|
||||
LD HL, DBGLP
|
||||
CALL PSTR
|
||||
LD A, (LOOPCNT+1)
|
||||
CALL PHEX
|
||||
LD A, (LOOPCNT)
|
||||
CALL PHEX
|
||||
LD HL, DBGCR
|
||||
CALL PSTR
|
||||
|
||||
;- Delay ~1 second at 3.54MHz (2 × 65535 × 26T ≈ 0.96s).
|
||||
LD B, 2
|
||||
DLY1: LD DE, 0FFFFh
|
||||
DLY2: DEC DE
|
||||
LD A, D
|
||||
OR E
|
||||
JR NZ, DLY2
|
||||
DJNZ DLY1
|
||||
|
||||
JP MAINLP
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- PVRAM: Write null-terminated ASCII string at DE to VRAM at HL. Returns HL advanced.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
PVRAM: LD A, (DE)
|
||||
OR A
|
||||
RET Z
|
||||
PUSH HL
|
||||
PUSH DE
|
||||
CALL A2DSP
|
||||
POP DE
|
||||
POP HL
|
||||
LD (HL), A
|
||||
INC HL
|
||||
INC DE
|
||||
JR PVRAM
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- PDECV: Print A as decimal to VRAM at HL. Advances HL.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
PDECV: PUSH BC
|
||||
LD B, 0
|
||||
PVD100: CP 100
|
||||
JR C, PVD10
|
||||
SUB 100
|
||||
INC B
|
||||
JR PVD100
|
||||
PVD10: PUSH AF
|
||||
LD A, B
|
||||
OR A
|
||||
JR Z, PVD10N
|
||||
ADD A, 020h
|
||||
LD (HL), A
|
||||
INC HL
|
||||
PVD10N: POP AF
|
||||
LD B, 0
|
||||
PVD10L: CP 10
|
||||
JR C, PVD1
|
||||
SUB 10
|
||||
INC B
|
||||
JR PVD10L
|
||||
PVD1: PUSH AF
|
||||
LD A, B
|
||||
OR A
|
||||
JR Z, PVD1N
|
||||
ADD A, 020h
|
||||
LD (HL), A
|
||||
INC HL
|
||||
PVD1N: POP AF
|
||||
ADD A, 020h
|
||||
LD (HL), A
|
||||
INC HL
|
||||
POP BC
|
||||
RET
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- PHEXV: Print A as 2-digit hex to VRAM at HL.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
PHEXV: PUSH AF
|
||||
RRCA
|
||||
RRCA
|
||||
RRCA
|
||||
RRCA
|
||||
AND 00Fh
|
||||
CALL PHXN
|
||||
POP AF
|
||||
AND 00Fh
|
||||
PHXN: CP 00Ah
|
||||
JR C, PHXN1
|
||||
ADD A, 001h - 00Ah
|
||||
LD (HL), A
|
||||
INC HL
|
||||
RET
|
||||
PHXN1: ADD A, 020h
|
||||
LD (HL), A
|
||||
INC HL
|
||||
RET
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- A2DSP: Convert ASCII in A to MZ display code via table.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
A2DSP: PUSH HL
|
||||
AND 07Fh
|
||||
LD L, A
|
||||
LD H, 0
|
||||
LD DE, ATBL
|
||||
ADD HL, DE
|
||||
LD A, (HL)
|
||||
POP HL
|
||||
RET
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- Debug port helpers.
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
PSTR: LD A, (HL)
|
||||
OR A
|
||||
RET Z
|
||||
OUT (PORT_DBG), A
|
||||
INC HL
|
||||
JR PSTR
|
||||
|
||||
PHEX: PUSH AF
|
||||
RRCA
|
||||
RRCA
|
||||
RRCA
|
||||
RRCA
|
||||
AND 00Fh
|
||||
ADD A, 030h
|
||||
CP 03Ah
|
||||
JR C, PH1
|
||||
ADD A, 007h
|
||||
PH1: OUT (PORT_DBG), A
|
||||
POP AF
|
||||
AND 00Fh
|
||||
ADD A, 030h
|
||||
CP 03Ah
|
||||
JR C, PH2
|
||||
ADD A, 007h
|
||||
PH2: OUT (PORT_DBG), A
|
||||
RET
|
||||
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
;- Strings (ASCII, null-terminated).
|
||||
;--------------------------------------------------------------------------------------------------------
|
||||
S_TITLE: DB "CELESTITE NET STRESS TEST", 0
|
||||
S_LOOP: DB "LOOP ", 0
|
||||
S_PING: DB "PING 192.168.1.1", 0
|
||||
S_TCP: DB "TCP CONNECT GATEWAY 80", 0
|
||||
S_HTTP: DB "HTTP GET GATEWAY", 0
|
||||
S_PASS: DB "PASS", 0
|
||||
S_FAIL: DB "FAIL", 0
|
||||
S_TOUT: DB "TOUT", 0
|
||||
S_MS: DB "MS", 0
|
||||
S_BYT: DB "H", 0
|
||||
|
||||
HTTPREQ: DB "GET / HTTP/1.0", 0Dh, 0Ah, 0Dh, 0Ah, 00h
|
||||
|
||||
DBGLP: DB 0Dh, 0Ah, "Loop:", 0
|
||||
DBGCR: DB 0Dh, 0Ah, 0
|
||||
|
||||
LOOPCNT: DW 0
|
||||
|
||||
;- ASCII to MZ display code table (128 bytes).
|
||||
ATBL: DB 0CCh,0E0h,0F2h,0F3h,0CEh,0CFh,0F6h,0F7h
|
||||
DB 0F8h,0F9h,0CDh,0FBh,0FCh,0CDh,0FEh,0FFh
|
||||
DB 0E1h,0C1h,0C2h,0C3h,0C4h,0C5h,0C6h,0E2h
|
||||
DB 0E3h,0E4h,0E5h,0E6h,0EBh,0EEh,0EFh,0F4h
|
||||
DB 000h,061h,062h,063h,064h,065h,066h,067h
|
||||
DB 068h,069h,06Bh,06Ah,02Fh,02Ah,02Eh,02Dh
|
||||
DB 020h,021h,022h,023h,024h,025h,026h,027h
|
||||
DB 028h,029h,04Fh,02Ch,051h,02Bh,057h,049h
|
||||
DB 055h,001h,002h,003h,004h,005h,006h,007h
|
||||
DB 008h,009h,00Ah,00Bh,00Ch,00Dh,00Eh,00Fh
|
||||
DB 010h,011h,012h,013h,014h,015h,016h,017h
|
||||
DB 018h,019h,01Ah,052h,059h,054h,050h,045h
|
||||
DB 000h,001h,002h,003h,004h,005h,006h,007h
|
||||
DB 008h,009h,00Ah,00Bh,00Ch,00Dh,00Eh,00Fh
|
||||
DB 010h,011h,012h,013h,014h,015h,016h,017h
|
||||
DB 018h,019h,01Ah,053h,000h,058h,000h,000h
|
||||
|
||||
MEND:
|
||||
BIN
projects/tzpuPico/src/test/Celestite/celestite_stress.mzf
vendored
Normal file
BIN
projects/tzpuPico/src/test/Celestite/celestite_stress.mzf
vendored
Normal file
Binary file not shown.
67
projects/tzpuPico/src/test/Celestite/celestite_stress.sym
vendored
Normal file
67
projects/tzpuPico/src/test/Celestite/celestite_stress.sym
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
A2DSP: equ 14A7H
|
||||
ATBL: equ 1568H
|
||||
ATRB: equ 10F0H
|
||||
CLR: equ 122EH
|
||||
COMNT: equ 1108H
|
||||
DBGCR: equ 1563H
|
||||
DBGLP: equ 155BH
|
||||
DLY1: equ 143FH
|
||||
DLY2: equ 1442H
|
||||
DTADR: equ 1104H
|
||||
EXADR: equ 1106H
|
||||
HTCHK: equ 1405H
|
||||
HTDISP: equ 13BFH
|
||||
HTEND: equ 140DH
|
||||
HTNL: equ 13D4H
|
||||
HTNL2: equ 13E1H
|
||||
HTNL3: equ 13E6H
|
||||
HTNL4: equ 13EFH
|
||||
HTNL5: equ 13F8H
|
||||
HTNL6: equ 13FEH
|
||||
HTNXT: equ 1405H
|
||||
HTTPDN: equ 1419H
|
||||
HTTPGOT: equ 13A0H
|
||||
HTTPRD: equ 138DH
|
||||
HTTPREQ: equ 1548H
|
||||
HTTPRW: equ 1376H
|
||||
HTTPWD: equ 1354H
|
||||
HTTPWR: equ 134BH
|
||||
LOOPCNT: equ 1566H
|
||||
MAINLP: equ 1228H
|
||||
MEND: equ 15E8H
|
||||
NAME: equ 10F1H
|
||||
PDECV: equ 145BH
|
||||
PH1: equ 14CBH
|
||||
PH2: equ 14D8H
|
||||
PHEX: equ 14BCH
|
||||
PHEXV: equ 148CH
|
||||
PHXN: equ 1499H
|
||||
PHXN1: equ 14A2H
|
||||
PNGD: equ 1274H
|
||||
PNGDN: equ 1292H
|
||||
PNGGOT: equ 1286H
|
||||
PNGW: equ 1269H
|
||||
PSTR: equ 14B4H
|
||||
PVD1: equ 147CH
|
||||
PVD10: equ 1467H
|
||||
PVD100: equ 145EH
|
||||
PVD10L: equ 1473H
|
||||
PVD10N: equ 1470H
|
||||
PVD1N: equ 1485H
|
||||
PVRAM: equ 144CH
|
||||
SIZE: equ 1102H
|
||||
START: equ 1200H
|
||||
S_BYT: equ 1546H
|
||||
S_FAIL: equ 1539H
|
||||
S_HTTP: equ 1523H
|
||||
S_LOOP: equ 14F5H
|
||||
S_MS: equ 1543H
|
||||
S_PASS: equ 1534H
|
||||
S_PING: equ 14FBH
|
||||
S_TCP: equ 150CH
|
||||
S_TITLE: equ 14DBH
|
||||
S_TOUT: equ 153EH
|
||||
TCPD: equ 130BH
|
||||
TCPFL: equ 1312H
|
||||
TCPOK: equ 131EH
|
||||
TCPW: equ 12F3H
|
||||
1137
projects/tzpuPico/src/test/Celestite/celestite_test.asm
Normal file
1137
projects/tzpuPico/src/test/Celestite/celestite_test.asm
Normal file
File diff suppressed because it is too large
Load Diff
BIN
projects/tzpuPico/src/test/Celestite/celestite_test.mzf
vendored
Normal file
BIN
projects/tzpuPico/src/test/Celestite/celestite_test.mzf
vendored
Normal file
Binary file not shown.
93
projects/tzpuPico/src/test/Celestite/celestite_test.sym
vendored
Normal file
93
projects/tzpuPico/src/test/Celestite/celestite_test.sym
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
A2DSP: equ 170DH
|
||||
ATBL: equ 1A51H
|
||||
ATRB: equ 10F0H
|
||||
BNRMSG: equ 172DH
|
||||
BYTEMSG: equ 19C0H
|
||||
COMNT: equ 1108H
|
||||
CRLF: equ 19F6H
|
||||
DONEMSG: equ 1A08H
|
||||
DTADR: equ 1104H
|
||||
EXADR: equ 1106H
|
||||
FAILMSG: equ 18D0H
|
||||
HTTPREQ: equ 1A27H
|
||||
MEND: equ 1AD1H
|
||||
MSMSG: equ 1A03H
|
||||
NAME: equ 10F1H
|
||||
NETMSG: equ 18D7H
|
||||
OKMSG: equ 19C9H
|
||||
PASSMSG: equ 18C9H
|
||||
PAUSMSG: equ 19D7H
|
||||
PBANNER: equ 1726H
|
||||
PD10: equ 16D0H
|
||||
PD100: equ 16B9H
|
||||
PD10N: equ 16CDH
|
||||
PD10S: equ 16C2H
|
||||
PD1N: equ 16E7H
|
||||
PD1P: equ 16E2H
|
||||
PD1S: equ 16D9H
|
||||
PDEC: equ 16B2H
|
||||
PHEX: equ 16EEH
|
||||
PHX1: equ 16FDH
|
||||
PHX2: equ 170AH
|
||||
PMSG: equ 171DH
|
||||
PSTR: equ 16AAH
|
||||
RESULT: equ 169AH
|
||||
RPASS: equ 16A3H
|
||||
RXGMSG: equ 19B6H
|
||||
RXWMSG: equ 199EH
|
||||
SCRHDR: equ 1A3AH
|
||||
SIZE: equ 1102H
|
||||
START: equ 1200H
|
||||
T10MSG: equ 1875H
|
||||
T11MSG: equ 1891H
|
||||
T12MSG: equ 18ADH
|
||||
T13DLY: equ 1407H
|
||||
T13DONE: equ 141AH
|
||||
T13GOT: equ 1416H
|
||||
T13MSG: equ 191BH
|
||||
T13WAIT: equ 13F6H
|
||||
T14MSG: equ 1937H
|
||||
T15DLY: equ 1463H
|
||||
T15DONE: equ 147DH
|
||||
T15GOT: equ 1472H
|
||||
T15MSG: equ 1948H
|
||||
T15WAIT: equ 1458H
|
||||
T16DLY: equ 14ECH
|
||||
T16DONE: equ 150BH
|
||||
T16FAIL: equ 14F3H
|
||||
T16MSG: equ 1964H
|
||||
T16OK: equ 14FBH
|
||||
T16WAIT: equ 14D5H
|
||||
T17CD: equ 1581H
|
||||
T17CLR: equ 161BH
|
||||
T17CONN: equ 158BH
|
||||
T17CW: equ 1569H
|
||||
T17DISP: equ 162FH
|
||||
T17DONE: equ 1685H
|
||||
T17END: equ 165FH
|
||||
T17FAIL: equ 167FH
|
||||
T17GOT: equ 15EDH
|
||||
T17HALT: equ 167DH
|
||||
T17LIM: equ 1612H
|
||||
T17LOK: equ 1614H
|
||||
T17MSG: equ 1980H
|
||||
T17NL: equ 1640H
|
||||
T17NXT: equ 1657H
|
||||
T17RD: equ 15DDH
|
||||
T17RW: equ 15C6H
|
||||
T17WR: equ 1595H
|
||||
T17WRD: equ 159EH
|
||||
T1MSG: equ 1779H
|
||||
T2MSG: equ 1795H
|
||||
T3DONE: equ 128EH
|
||||
T3FAIL: equ 1288H
|
||||
T3MSG: equ 17B1H
|
||||
T4DONE: equ 12D6H
|
||||
T4FAIL: equ 12D0H
|
||||
T4MSG: equ 17CDH
|
||||
T5MSG: equ 17E9H
|
||||
T6MSG: equ 1805H
|
||||
T7MSG: equ 1821H
|
||||
T8MSG: equ 183DH
|
||||
T9MSG: equ 1859H
|
||||
TOMSG: equ 19F9H
|
||||
13
projects/tzpuPico/src/z80.pio
vendored
13
projects/tzpuPico/src/z80.pio
vendored
@@ -106,16 +106,21 @@ public start_addr:
|
||||
.wrap
|
||||
|
||||
; State machine to output an 8bit byte onto the Z80 Data Bus.
|
||||
; The state machine sets IRQ 1 to indicate it is waiting, waits for the flag
|
||||
; to clear then loads the 8bit data byte out of the fifo onto the pins.
|
||||
; After driving data, waits for /WR to go active (low) then inactive (high),
|
||||
; indicating the write cycle has completed, then tristates the data bus.
|
||||
; This prevents stale data from persisting on the physical bus in virtual mode,
|
||||
; where no subsequent physical bus cycle (M1 fetch / refresh) would otherwise
|
||||
; clear it. Works with any number of wait states since it tracks the actual
|
||||
; /WR signal rather than using a fixed timeout.
|
||||
.program z80_data
|
||||
.wrap_target
|
||||
public start_data:
|
||||
irq set 1
|
||||
wait 0 irq 1 ; Wait till control starts the sequence.
|
||||
out pindirs, 8 ; Set direction to output (or input during tri-state BUSRQ).
|
||||
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7)
|
||||
wait 0 irq 0 ; Wait till next address change.
|
||||
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7).
|
||||
wait 0 gpio Z80_PIN_WR ; Wait for /WR active (low) — write cycle in progress.
|
||||
wait 1 gpio Z80_PIN_WR ; Wait for /WR inactive (high) — write cycle complete.
|
||||
out pindirs, 8 ; Set final direction, normally input mode.
|
||||
.wrap
|
||||
|
||||
|
||||
BIN
projects/tzpuPico/tools/MZQD/MZQDTool
vendored
BIN
projects/tzpuPico/tools/MZQD/MZQDTool
vendored
Binary file not shown.
@@ -40,6 +40,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define QDSIZE 61455
|
||||
#define QDFSIZE 81920 /* Japanese QDF format track size (80KB) */
|
||||
#define QDFHDRSIZE 16 /* QDF file header size */
|
||||
#define QDFTOTALSIZE (QDFHDRSIZE + QDFSIZE) /* 81936 bytes total */
|
||||
#define QDFSIGNATURE "-QD format-"
|
||||
#define MAXQDFILETYPES 12
|
||||
#define DEFAULTQDFILE "MZ700.qd"
|
||||
#define CMTHDRSIZE 128 /* MZF/CMT file header size */
|
||||
@@ -71,8 +75,9 @@ static const char *QDFileTypes[MAXQDFILETYPES] = {
|
||||
"???", "LIB", "???", "???", "SYS", "GR "
|
||||
};
|
||||
|
||||
static uint8_t QDArray[QDSIZE];
|
||||
static uint8_t QDArray[QDFSIZE]; /* Large enough for both formats */
|
||||
static char qdFileName[256] = DEFAULTQDFILE;
|
||||
static int qdfFormat = 0; /* 1 = output in Japanese QDF format */
|
||||
|
||||
/* ------- Low-level QD block I/O helpers ------- */
|
||||
|
||||
@@ -98,7 +103,17 @@ static int save_qd(void)
|
||||
fprintf(stderr, "ERROR: Cannot write '%s'\n", qdFileName);
|
||||
return 0;
|
||||
}
|
||||
fwrite(QDArray, QDSIZE, 1, f);
|
||||
if (qdfFormat) {
|
||||
/* Write QDF header + full 81920 byte track */
|
||||
uint8_t hdr[QDFHDRSIZE];
|
||||
memset(hdr, 0xFF, QDFHDRSIZE);
|
||||
memcpy(hdr, QDFSIGNATURE, strlen(QDFSIGNATURE));
|
||||
fwrite(hdr, QDFHDRSIZE, 1, f);
|
||||
fwrite(QDArray, QDFSIZE, 1, f);
|
||||
printf(" Output: QDF format (%d bytes)\n", QDFTOTALSIZE);
|
||||
} else {
|
||||
fwrite(QDArray, QDSIZE, 1, f);
|
||||
}
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
@@ -110,6 +125,27 @@ static int load_qd(void)
|
||||
fprintf(stderr, "ERROR: Cannot open '%s'\n", qdFileName);
|
||||
return 0;
|
||||
}
|
||||
/* Check file size to detect format */
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (sz == QDFTOTALSIZE) {
|
||||
/* Likely QDF format — check header signature */
|
||||
uint8_t hdr[QDFHDRSIZE];
|
||||
fread(hdr, QDFHDRSIZE, 1, f);
|
||||
if (memcmp(hdr, QDFSIGNATURE, strlen(QDFSIGNATURE)) == 0) {
|
||||
printf(" Input: QDF format detected (%ld bytes)\n", sz);
|
||||
memset(QDArray, 0, sizeof(QDArray));
|
||||
fread(QDArray, QDFSIZE, 1, f);
|
||||
fclose(f);
|
||||
return 1;
|
||||
}
|
||||
/* Not QDF — rewind and read as raw */
|
||||
fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
memset(QDArray, 0, sizeof(QDArray));
|
||||
fread(QDArray, QDSIZE, 1, f);
|
||||
fclose(f);
|
||||
return 1;
|
||||
@@ -315,12 +351,14 @@ static void AddFileToQD(const char *mzfFileName)
|
||||
static void usage(void)
|
||||
{
|
||||
printf("Usage:\n");
|
||||
printf(" MZQDTool format [-o disk.qd] Format empty QD image\n");
|
||||
printf(" MZQDTool dir [-o disk.qd] List QD directory\n");
|
||||
printf(" MZQDTool add <file.mzf> [-o disk.qd] Add MZF file to QD\n");
|
||||
printf(" MZQDTool format [-o disk.qd] [-j] Format empty QD image\n");
|
||||
printf(" MZQDTool dir [-o disk.qd] List QD directory\n");
|
||||
printf(" MZQDTool add <file.mzf> [-o disk.qd] [-j] Add MZF file to QD\n");
|
||||
printf(" MZQDTool convert [-o out.qdf] [-j] <input> Convert between formats\n");
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" -o <filename> QD image file (default: %s)\n", DEFAULTQDFILE);
|
||||
printf(" -j Output in Japanese QDF format (81936 bytes with header)\n");
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
@@ -333,12 +371,21 @@ int main(int argc, char *argv[])
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Parse -o option from any position */
|
||||
/* Parse options from any position */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp("-j", argv[i]) == 0) {
|
||||
qdfFormat = 1;
|
||||
/* Remove from argv */
|
||||
for (int j = i; j < argc - 1; j++)
|
||||
argv[j] = argv[j + 1];
|
||||
argc--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
for (int i = 1; i < argc - 1; i++) {
|
||||
if (strcmp("-o", argv[i]) == 0) {
|
||||
strncpy(qdFileName, argv[i + 1], sizeof(qdFileName) - 1);
|
||||
qdFileName[sizeof(qdFileName) - 1] = '\0';
|
||||
/* Remove -o and its argument from argv for command parsing */
|
||||
for (int j = i; j < argc - 2; j++)
|
||||
argv[j] = argv[j + 2];
|
||||
argc -= 2;
|
||||
@@ -356,6 +403,39 @@ int main(int argc, char *argv[])
|
||||
return 2;
|
||||
}
|
||||
AddFileToQD(argv[2]);
|
||||
} else if (strcmp("convert", argv[1]) == 0) {
|
||||
/* Convert between formats. Only compact→QDF is supported (adds header + pads to 80KB).
|
||||
* QDF→compact is NOT supported because the track layouts are structurally different:
|
||||
* compact has data at offset 0, QDF has ~4KB of gap before data starts. */
|
||||
if (argc < 3) {
|
||||
fprintf(stderr, "ERROR: No input file specified\n");
|
||||
return 2;
|
||||
}
|
||||
char outFile[256];
|
||||
strncpy(outFile, qdFileName, sizeof(outFile) - 1);
|
||||
strncpy(qdFileName, argv[2], sizeof(qdFileName) - 1);
|
||||
|
||||
/* Check if input is QDF */
|
||||
FILE *chk = fopen(qdFileName, "rb");
|
||||
if (chk) {
|
||||
uint8_t sig[16];
|
||||
fread(sig, 1, 16, chk);
|
||||
fclose(chk);
|
||||
if (memcmp(sig, QDFSIGNATURE, strlen(QDFSIGNATURE)) == 0 && !qdfFormat) {
|
||||
fprintf(stderr, "ERROR: Cannot convert QDF to compact — track layouts are incompatible.\n");
|
||||
fprintf(stderr, " The picoZ80 QD driver loads QDF files directly. No conversion needed.\n");
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!load_qd()) return 1;
|
||||
strncpy(qdFileName, outFile, sizeof(qdFileName) - 1);
|
||||
if (!qdfFormat) {
|
||||
fprintf(stderr, "ERROR: Only compact→QDF conversion is supported (use -j flag).\n");
|
||||
return 2;
|
||||
}
|
||||
if (!save_qd()) return 1;
|
||||
printf(" Converted: %s -> %s (QDF)\n", argv[2], qdFileName);
|
||||
} else {
|
||||
fprintf(stderr, "ERROR: Unknown command '%s'\n", argv[1]);
|
||||
usage();
|
||||
|
||||
354
projects/tzpuPico/tools/NetFileServer/netfs.py
vendored
Executable file
354
projects/tzpuPico/tools/NetFileServer/netfs.py
vendored
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
NetFS — Binary File Server for Sharp MZ BASIC Network Drive
|
||||
|
||||
Serves MZF files from a local directory over TCP using a simple binary protocol
|
||||
designed for easy Z80 assembly parsing.
|
||||
|
||||
Binary Protocol:
|
||||
All requests start with 1-byte command. All responses start with 1-byte status.
|
||||
Status: 0x00=OK, 0xFF=error, 0xFE=end of data.
|
||||
|
||||
DIR (0x01):
|
||||
Request: [0x01] [unit:1]
|
||||
Response: [0x00] [count:1] [entry0:32] ... [entryN:32]
|
||||
Each entry is 32 bytes: atrb(1) + name(17) + size(2LE) + dtadr(2LE) + exadr(2LE) + pad(8)
|
||||
Returns up to 63 entries from the directory mapped to unit (1-7). Max 63 (2KB RX buffer).
|
||||
|
||||
INFO (0x02):
|
||||
Request: [0x02] [filename:17]
|
||||
Response: [0x00] [entry:32] (or [0xFF] if not found)
|
||||
|
||||
READ (0x03):
|
||||
Request: [0x03] [unit:1] [filename:17] [offset:2LE] [length:2LE]
|
||||
Response: [0x00] [actual_len:2LE] [data:actual_len] (or [0xFF] if not found)
|
||||
|
||||
WRITE (0x04):
|
||||
Request: [0x04] [unit:1] [filename:17] [atrb:1] [size:2LE] [dtadr:2LE] [exadr:2LE] [data:size]
|
||||
Response: [0x00] (or [0xFF] on error)
|
||||
|
||||
CLOSE (0x05):
|
||||
Request: [0x05]
|
||||
Response: [0x00] (server closes connection)
|
||||
|
||||
DELETE (0x06):
|
||||
Request: [0x06] [filename:17]
|
||||
Response: [0x00] (or [0xFF] if not found)
|
||||
|
||||
All commands include a unit byte (1-7) selecting the server directory.
|
||||
|
||||
Usage:
|
||||
python3 netfs.py [--port 6800] [--dir ./mzf_files] [--dir2 path] ... [--dir7 path]
|
||||
|
||||
--dir maps to NET1: (or NET: with no digit), --dir2 maps to NET2:, etc.
|
||||
Units without a --dirN argument fall back to --dir.
|
||||
|
||||
(c) 2026 Philip Smart <philip.smart@net2net.org>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import sys
|
||||
|
||||
DEFAULT_PORT = 6800
|
||||
DEFAULT_DIR = "."
|
||||
MZF_HEADER_SIZE = 128
|
||||
|
||||
# Protocol constants
|
||||
CMD_DIR = 0x01
|
||||
CMD_INFO = 0x02
|
||||
CMD_READ = 0x03
|
||||
CMD_WRITE = 0x04
|
||||
CMD_CLOSE = 0x05
|
||||
CMD_DELETE = 0x06
|
||||
STATUS_OK = 0x00
|
||||
STATUS_ERR = 0xFF
|
||||
STATUS_END = 0xFE
|
||||
|
||||
def parse_mzf_header(data):
|
||||
"""Parse a 128-byte MZF header."""
|
||||
if len(data) < MZF_HEADER_SIZE:
|
||||
return None
|
||||
atrb = data[0]
|
||||
# Remap RB/BSD/BRD types to BTX (type 2) so BASIC can LOAD them
|
||||
if atrb in (0x03, 0x04, 0x05):
|
||||
atrb = 0x02
|
||||
name_raw = data[1:18]
|
||||
name = name_raw.rstrip(b'\x00\x0d').decode('ascii', errors='replace')
|
||||
size = struct.unpack('<H', data[18:20])[0]
|
||||
dtadr = struct.unpack('<H', data[20:22])[0]
|
||||
exadr = struct.unpack('<H', data[22:24])[0]
|
||||
return {
|
||||
'atrb': atrb, 'name': name, 'name_raw': name_raw,
|
||||
'size': size, 'dtadr': dtadr, 'exadr': exadr
|
||||
}
|
||||
|
||||
def make_dir_entry(atrb, name_raw, size, dtadr, exadr):
|
||||
"""Create a 32-byte binary directory entry (standard MZF format).
|
||||
Z80 handler remaps fields as needed for the target BASIC."""
|
||||
entry = bytearray(32)
|
||||
entry[0] = atrb & 0xFF
|
||||
entry[1:18] = name_raw[:17].ljust(17, b'\x00')
|
||||
struct.pack_into('<H', entry, 18, size & 0xFFFF)
|
||||
struct.pack_into('<H', entry, 20, dtadr & 0xFFFF)
|
||||
struct.pack_into('<H', entry, 22, exadr & 0xFFFF)
|
||||
return bytes(entry)
|
||||
|
||||
def scan_directory(dirpath):
|
||||
"""Scan directory for MZF files."""
|
||||
entries = []
|
||||
for fname in sorted(os.listdir(dirpath)):
|
||||
fpath = os.path.join(dirpath, fname)
|
||||
if not os.path.isfile(fpath):
|
||||
continue
|
||||
ext = os.path.splitext(fname)[1].lower()
|
||||
if ext in ('.mzf', '.mzt', '.m12'):
|
||||
try:
|
||||
with open(fpath, 'rb') as f:
|
||||
hdr_data = f.read(MZF_HEADER_SIZE)
|
||||
hdr = parse_mzf_header(hdr_data)
|
||||
if hdr:
|
||||
hdr['filepath'] = fpath
|
||||
hdr['data_offset'] = MZF_HEADER_SIZE
|
||||
entries.append(hdr)
|
||||
except Exception as e:
|
||||
print(f" Warning: {fname}: {e}")
|
||||
elif ext in ('.bin', '.rom', '.dat'):
|
||||
fsize = os.path.getsize(fpath)
|
||||
name_clean = os.path.splitext(fname)[0][:17]
|
||||
name_raw = name_clean.encode('ascii', errors='replace').ljust(17, b'\x00')
|
||||
entries.append({
|
||||
'atrb': 0x01, 'name': name_clean, 'name_raw': name_raw,
|
||||
'size': fsize & 0xFFFF, 'dtadr': 0x1200, 'exadr': 0x1200,
|
||||
'filepath': fpath, 'data_offset': 0
|
||||
})
|
||||
return entries
|
||||
|
||||
def find_file(entries, name_bytes):
|
||||
"""Find a file by 17-byte name field (binary match)."""
|
||||
# Strip trailing nulls/CRs for comparison
|
||||
search = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip().upper()
|
||||
for ent in entries:
|
||||
if ent['name'].strip().upper() == search:
|
||||
return ent
|
||||
return None
|
||||
|
||||
def handle_client(conn, addr, dirs):
|
||||
"""Handle a binary protocol client connection.
|
||||
dirs: dict mapping unit number (1-7) to directory path."""
|
||||
conn.settimeout(30) # 30s timeout — recovers if host crashes mid-transfer.
|
||||
print(f"[{addr}] Connected")
|
||||
try:
|
||||
while True:
|
||||
# Read 1-byte command
|
||||
cmd_data = conn.recv(1)
|
||||
if not cmd_data:
|
||||
break
|
||||
cmd = cmd_data[0]
|
||||
|
||||
if cmd == CMD_DIR:
|
||||
# Read 1-byte unit number.
|
||||
unit_data = conn.recv(1)
|
||||
if not unit_data:
|
||||
break
|
||||
unit = unit_data[0]
|
||||
dp = dirs.get(unit)
|
||||
entries = scan_directory(dp) if dp else []
|
||||
count = min(len(entries), 63)
|
||||
resp = bytearray()
|
||||
resp.append(STATUS_OK)
|
||||
resp.append(count)
|
||||
for i in range(count):
|
||||
ent = entries[i]
|
||||
resp.extend(make_dir_entry(ent['atrb'], ent['name_raw'], ent['size'], ent['dtadr'], ent['exadr']))
|
||||
conn.sendall(bytes(resp))
|
||||
print(f"[{addr}] DIR unit={unit} ({dp or 'not configured'}) → {count}/{len(entries)} entries ({len(resp)} bytes)")
|
||||
|
||||
elif cmd == CMD_INFO:
|
||||
# Read 17-byte filename
|
||||
name_bytes = conn.recv(17)
|
||||
if len(name_bytes) < 17:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
continue
|
||||
# INFO searches unit 1 directory (no unit byte in protocol).
|
||||
dp = dirs.get(1)
|
||||
entries = scan_directory(dp) if dp else []
|
||||
found = find_file(entries, name_bytes)
|
||||
if found:
|
||||
entry = make_dir_entry(found['atrb'], found['name_raw'], found['size'], found['dtadr'], found['exadr'])
|
||||
conn.sendall(bytes([STATUS_OK]) + entry)
|
||||
print(f"[{addr}] INFO '{found['name']}' → size={found['size']}")
|
||||
else:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
|
||||
print(f"[{addr}] INFO '{name_str}' → NOT FOUND")
|
||||
|
||||
elif cmd == CMD_READ:
|
||||
# [unit:1][name:17][offset:2][length:2] = 22 bytes.
|
||||
params = conn.recv(22)
|
||||
if len(params) < 22:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
continue
|
||||
unit = params[0]
|
||||
name_bytes = params[1:18]
|
||||
offset = struct.unpack('<H', params[18:20])[0]
|
||||
length = struct.unpack('<H', params[20:22])[0]
|
||||
dp = dirs.get(unit)
|
||||
entries = scan_directory(dp) if dp else []
|
||||
found = find_file(entries, name_bytes) if entries else None
|
||||
if found:
|
||||
with open(found['filepath'], 'rb') as f:
|
||||
f.seek(found['data_offset'] + offset)
|
||||
max_read = found['size'] - offset if offset < found['size'] else 0
|
||||
data = f.read(min(length, max_read))
|
||||
actual_len = len(data)
|
||||
resp = bytearray()
|
||||
resp.append(STATUS_OK)
|
||||
resp.extend(struct.pack('<H', actual_len))
|
||||
resp.extend(data)
|
||||
conn.sendall(bytes(resp))
|
||||
print(f"[{addr}] READ unit={unit} '{found['name']}' ofs={offset} len={length} → {actual_len} bytes")
|
||||
else:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
|
||||
print(f"[{addr}] READ unit={unit} '{name_str}' → NOT FOUND")
|
||||
|
||||
elif cmd == CMD_WRITE:
|
||||
# [unit:1][name:17][atrb:1][size:2][dtadr:2][exadr:2] = 25 bytes.
|
||||
params = conn.recv(25)
|
||||
if len(params) < 25:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
continue
|
||||
unit = params[0]
|
||||
name_bytes = params[1:18]
|
||||
atrb = params[18]
|
||||
size = struct.unpack('<H', params[19:21])[0]
|
||||
dtadr = struct.unpack('<H', params[21:23])[0]
|
||||
exadr = struct.unpack('<H', params[23:25])[0]
|
||||
dp = dirs.get(unit)
|
||||
if not dp:
|
||||
# Drain the data bytes then return error.
|
||||
remaining = size
|
||||
while remaining > 0:
|
||||
chunk = conn.recv(min(remaining, 4096))
|
||||
if not chunk:
|
||||
break
|
||||
remaining -= len(chunk)
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
|
||||
print(f"[{addr}] WRITE unit={unit} '{name_str}' → unit not configured")
|
||||
continue
|
||||
# Read file data
|
||||
data = b''
|
||||
remaining = size
|
||||
while remaining > 0:
|
||||
chunk = conn.recv(min(remaining, 4096))
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
remaining -= len(chunk)
|
||||
# Build MZF file
|
||||
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
|
||||
fname = name_str.replace(' ', '_') + '.mzf'
|
||||
fpath = os.path.join(dp, fname)
|
||||
hdr = bytearray(MZF_HEADER_SIZE)
|
||||
hdr[0] = atrb
|
||||
hdr[1:18] = name_bytes[:17]
|
||||
struct.pack_into('<H', hdr, 18, size)
|
||||
struct.pack_into('<H', hdr, 20, dtadr)
|
||||
struct.pack_into('<H', hdr, 22, exadr)
|
||||
with open(fpath, 'wb') as f:
|
||||
f.write(bytes(hdr))
|
||||
f.write(data)
|
||||
conn.sendall(bytes([STATUS_OK]))
|
||||
print(f"[{addr}] WRITE unit={unit} '{name_str}' atrb={atrb:02X} size={size} → {fpath}")
|
||||
|
||||
elif cmd == CMD_DELETE:
|
||||
# Read 17-byte filename (no unit byte — uses unit 1 dir).
|
||||
name_bytes = conn.recv(17)
|
||||
if len(name_bytes) < 17:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
continue
|
||||
dp = dirs.get(1)
|
||||
entries = scan_directory(dp) if dp else []
|
||||
found = find_file(entries, name_bytes)
|
||||
if found:
|
||||
try:
|
||||
os.remove(found['filepath'])
|
||||
conn.sendall(bytes([STATUS_OK]))
|
||||
print(f"[{addr}] DELETE '{found['name']}' → {found['filepath']}")
|
||||
except Exception as e:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
print(f"[{addr}] DELETE '{found['name']}' FAILED: {e}")
|
||||
else:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
|
||||
print(f"[{addr}] DELETE '{name_str}' → NOT FOUND")
|
||||
|
||||
elif cmd == CMD_CLOSE:
|
||||
conn.sendall(bytes([STATUS_OK]))
|
||||
print(f"[{addr}] CLOSE")
|
||||
break
|
||||
|
||||
else:
|
||||
conn.sendall(bytes([STATUS_ERR]))
|
||||
print(f"[{addr}] Unknown cmd: 0x{cmd:02X}")
|
||||
|
||||
except (ConnectionResetError, BrokenPipeError):
|
||||
pass
|
||||
except socket.timeout:
|
||||
print(f"[{addr}] Timeout (host crash recovery)")
|
||||
except Exception as e:
|
||||
print(f"[{addr}] Error: {e}")
|
||||
finally:
|
||||
print(f"[{addr}] Disconnected")
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="NetFS — Binary File Server for Sharp MZ BASIC")
|
||||
parser.add_argument('--port', '-p', type=int, default=DEFAULT_PORT, help=f"TCP port (default: {DEFAULT_PORT})")
|
||||
parser.add_argument('--dir', '-d', type=str, default=DEFAULT_DIR, help="Directory for NET1:/NET: (default: .)")
|
||||
for i in range(2, 8):
|
||||
parser.add_argument(f'--dir{i}', type=str, default=None, help=f"Directory for NET{i}:")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Build unit → directory mapping.
|
||||
dirs = {1: os.path.abspath(args.dir)}
|
||||
for i in range(2, 8):
|
||||
d = getattr(args, f'dir{i}', None)
|
||||
if d:
|
||||
dirs[i] = os.path.abspath(d)
|
||||
|
||||
# Validate all directories exist.
|
||||
for unit, path in dirs.items():
|
||||
if not os.path.isdir(path):
|
||||
print(f"Error: directory '{path}' (NET{unit}:) does not exist")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"NetFS v4.0 — Sharp MZ BASIC Network File Server")
|
||||
for unit in sorted(dirs):
|
||||
entries = scan_directory(dirs[unit])
|
||||
print(f" NET{unit}: {dirs[unit]} ({len(entries)} files)")
|
||||
print(f"Listening on port {args.port}")
|
||||
print()
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(('0.0.0.0', args.port))
|
||||
sock.listen(5)
|
||||
|
||||
try:
|
||||
while True:
|
||||
conn, addr = sock.accept()
|
||||
t = threading.Thread(target=handle_client, args=(conn, addr, dirs), daemon=True)
|
||||
t.start()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
2
projects/tzpuPico/version.txt
vendored
2
projects/tzpuPico/version.txt
vendored
@@ -1 +1 @@
|
||||
2.472
|
||||
2.723
|
||||
|
||||
Reference in New Issue
Block a user