2 Commits

Author SHA1 Message Date
Philip Smart
cc040f7f49 Celstite test programs 2026-05-17 11:43:12 +01:00
Philip Smart
4196e58420 MZ-1500 persona, expansion boards, Celestite LAN, QDF format, virtual mode
New drivers:
- MZ-1500 persona driver with MZ-700/MZ-1500 mode switch, PCG bank switching,
  PSG stubs, physical I/O forwarding for virtual mode
- MZ-2200 persona driver (based on MZ-2000)
- MZ-1R23/MZ-1R24 Kanji ROM / Dictionary ROM board (B8h/B9h IDM)
- MZ-1R37 640KB EMM (ACh/ADh, no auto-increment, 20-bit addressing)
- PIO-3034 320KB EMM (configurable base, 19-bit, auto-increment)
- Celestite LAN/Memory composite board:
  - W5100 TCP/IP via ESP32 WiFi (connect, send, recv, ping)
  - Integrated MZ-1R12 32/64KB CMOS RAM with SD persistence
  - Integrated MZ-1R37 640KB EMM with SD persistence
  - UFM flash, unlock state machine, interrupt controller

Celestite networking (Phase 2):
- New IPC commands: NET_CFG, NET_SOCK, NET_SEND, NET_RECV, NET_PING
- ESP32 BSD socket handlers with non-blocking connect and recv
- Shared volatile struct for cross-core results (bypasses responseQueue)
- Inline IDM read check for socket status and recv data
- Z80 test programs: celestite_test.asm (17 tests), celestite_stress.asm (loop)

MZ-1500 virtual mode fixes:
- I/O writes forwarded to physical hardware (PSG, bank switching, PCG)
- E000-E7FF always stays physical during bank switching
- PCG bank (F000-FFFF) properly remapped to PHYSICAL when open

QDF format support:
- Japanese standard QD format auto-detected on load (81936 bytes)
- Hunt pattern changed to 00+16 (mark+sync) for inter-block gap handling
- Sync stripping handles long preambles (9+ bytes)
- MZQDTool updated with -j flag and format conversion

Other:
- Debug shell load command: len parameter now optional
- FSPI filename field: memcpy instead of strncpy for binary data
- Interface availability expanded across MZ-700/1500/80A/2000/2200
- Web GUI: param hints for Celestite, updated driver interface lists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-17 11:34:55 +01:00
42 changed files with 7934 additions and 124 deletions

View File

@@ -1 +1 @@
2.43
2.49

View File

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

View File

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

View File

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

View File

@@ -362,6 +362,19 @@ static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx)
// CommandProcessor (SPI slave) without waiting for USB delays. The 2-second
// disconnect/reconnect cycle that macOS needs would otherwise block SPI
// slave init, causing the RP2350's first sector reads to fail.
// Maximum number of disconnect/connect cycles to attempt before giving up.
static constexpr int USB_NCM_MAX_RETRIES = 3;
// Time to hold the bus disconnected so the host tears down its NCM driver.
static constexpr int USB_NCM_DISCONNECT_MS = 2500;
// Time to wait after tud_connect() for the host to begin enumeration.
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500;
// Polling interval while waiting for tud_mounted().
static constexpr int USB_NCM_POLL_MS = 100;
// Maximum time to wait for tud_mounted() after a connect before retrying.
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000;
// Time to wait after mount for the host DHCP client to obtain its IP.
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000;
void setupUSBTask(void *pvParameters)
{
bool cdcOk = false;
@@ -382,16 +395,50 @@ void setupUSBTask(void *pvParameters)
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. Disconnect/reconnect with retry loop. macOS in particular can fail to
// enumerate an NCM device on the first attempt after a long power-off period
// because stale USB driver state persists in the kernel. Rather than using
// fixed blind delays, we poll tud_mounted() to confirm the host has actually
// completed enumeration, and retry the full cycle if it hasn't.
bool mounted = false;
for (int attempt = 1; attempt <= USB_NCM_MAX_RETRIES && !mounted; attempt++)
{
ESP_LOGI(MAINTAG, "USB: disconnect/connect attempt %d/%d", attempt, USB_NCM_MAX_RETRIES);
// Disconnect — hold long enough for the host to fully tear down.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_DISCONNECT_MS));
// Reconnect and let the bus settle.
tud_connect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
// Poll tud_mounted() — the host has completed enumeration when this returns true.
int waited = 0;
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
{
if (tud_mounted())
{
mounted = true;
ESP_LOGI(MAINTAG, "USB: host enumerated device after %d ms (attempt %d)", waited, attempt);
break;
}
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
waited += USB_NCM_POLL_MS;
}
if (!mounted)
{
ESP_LOGW(MAINTAG, "USB: mount timeout after %d ms (attempt %d), %s",
USB_NCM_MOUNT_TIMEOUT_MS, attempt,
attempt < USB_NCM_MAX_RETRIES ? "retrying..." : "giving up");
}
}
if (!mounted)
{
ESP_LOGE(MAINTAG, "USB: host did not enumerate device after %d attempts", USB_NCM_MAX_RETRIES);
}
// 3. Initialise CDC-ACM for serial logging.
tinyusb_config_cdcacm_t acm_cfg = {};
@@ -444,6 +491,15 @@ void setupUSBTask(void *pvParameters)
esp_netif_dhcps_option(s_usb_ncm_netif, ESP_NETIF_OP_SET,
ESP_NETIF_IP_ADDRESS_LEASE_TIME, &lease_opt, sizeof(lease_opt));
esp_netif_action_start(s_usb_ncm_netif, 0, 0, 0);
// Notify the USB host that the NCM network link is up. Without this
// the host never receives a ConnectionSpeedChange notification and will
// not start its DHCP client, leaving the interface in "no IP" state.
if (mounted) {
vTaskDelay(pdMS_TO_TICKS(200)); // Let netif settle before signalling host.
tud_network_link_state(0, true);
ESP_LOGI(MAINTAG, "USB NCM: link state set to UP");
}
}
}
@@ -452,15 +508,27 @@ void setupUSBTask(void *pvParameters)
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));
// 7. Wait for the host to complete DHCP and bring the network link up.
// Poll esp_netif_is_netif_up() rather than using a blind delay so we
// proceed as soon as the link is ready (or time out gracefully).
if (ncmOk && mounted && s_usb_ncm_netif != NULL) {
ESP_LOGI(MAINTAG, "USB NCM: waiting for host DHCP...");
int dhcpWait = 0;
while (dhcpWait < USB_NCM_DHCP_WAIT_MS) {
if (esp_netif_is_netif_up(s_usb_ncm_netif)) {
ESP_LOGI(MAINTAG, "USB NCM: netif is up after %d ms", dhcpWait);
break;
}
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
dhcpWait += USB_NCM_POLL_MS;
}
// Allow a little extra time for the host DHCP client to finish even
// after the interface reports up.
vTaskDelay(pdMS_TO_TICKS(500));
}
ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s)", cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL");
ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s mounted:%s)",
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL", mounted ? "YES" : "NO");
g_usbReady = true;
vTaskDelete(NULL);
}

View File

@@ -1 +1 @@
2.54
2.66

View File

@@ -316,30 +316,33 @@ 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', 'MZ2000', 'MZ2200'];
// 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 }
};
// 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'],
'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']
};
// Get the ROM limit for a given interface name.
@@ -958,15 +961,27 @@ 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>';
},
@@ -991,23 +1006,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 +1034,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 +1049,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;
@@ -1099,8 +1116,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;

View File

@@ -1 +1 @@
2.43
2.49

View File

@@ -15,8 +15,10 @@ 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/MZ2000.c
${CMAKE_CURRENT_LIST_DIR}/drivers/Sharp/MZ2200.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 +29,10 @@ 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
)
set(pM6502_common_src

View File

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

View File

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

View File

@@ -50,8 +50,10 @@
// 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/MZ2000.h" // Sharp MZ-2000 Persona driver.
#include "drivers/Sharp/MZ2200.h" // Sharp MZ-2200 Persona driver.
#endif
@@ -206,10 +208,14 @@ 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.
{"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.
#endif
};
static const size_t virtualFuncMapSize = sizeof(virtualFuncMap) / sizeof(virtualFuncMap[0]);
@@ -2560,7 +2566,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 +2575,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);
@@ -2626,7 +2635,10 @@ void __func_in_RAM(Z80CPU_cpu)(t_Z80CPU *cpu)
// 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 +2788,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)

View File

@@ -1327,26 +1327,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)
@@ -1511,6 +1513,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)
{
@@ -3979,8 +4026,9 @@ static const t_DbgShCmd cmdTable[] =
{"fdctrace","FDC trace: fdctrace <on|off|dump>", cmdFdcTrace},
{"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},

File diff suppressed because it is too large Load Diff

View 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;
}
}

View File

@@ -0,0 +1,227 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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
//
// The I/O base address is configurable via ioMap in the JSON config:
// "ioMap": [{"srcAddr": 172, "dstAddr": <base>, "size": 2}]
// Default base is 0xAC.
//
// 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-1R37.h"
#include "debug.h"
t_MZ1R37 *mz1r37Ctrl; // Control structure for the MZ-1R37 EMM board.
// -----------------------------------------------------------------------------------------------
// Interface: MZ-1R37 640K EMM
// Description: 640KB expanded memory board with 20-bit address space. No auto-increment.
// Address is formed from a latched upper portion (port ACh) and the B register
// value during data access (port ADh).
// -----------------------------------------------------------------------------------------------
// Method to initialise the CPU state to support an MZ-1R37 EMM interface.
uint8_t MZ1R37_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
{
// Locals.
uint8_t result = 0;
// Interface logic only for virtual device. Physical device doesnt need configuration.
if (config->isPhysical)
return (0);
// Only one instance allowed.
if (mz1r37Ctrl != NULL)
return (0);
// Allocate control structure.
mz1r37Ctrl = (t_MZ1R37 *) calloc(1, sizeof(t_MZ1R37));
if (!mz1r37Ctrl)
{
debugf("MZ-1R37: Failed to allocate control structure\r\n");
return (0);
}
// Allocate 640KB RAM.
mz1r37Ctrl->ram = (uint8_t *) calloc(1, MZ1R37_RAM_SIZE);
if (mz1r37Ctrl->ram == NULL)
{
debugf("MZ-1R37: Failed to allocate %d bytes RAM\r\n", MZ1R37_RAM_SIZE);
free(mz1r37Ctrl);
mz1r37Ctrl = NULL;
return (0);
}
// 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;
// Install I/O handlers at the configured base address.
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\r\n", base, MZ1R37_RAM_SIZE / 1024);
result = 1;
return (result);
}
// Reset handler.
uint8_t MZ1R37_Reset(t_Z80CPU *cpu)
{
(void)cpu;
if (mz1r37Ctrl)
{
mz1r37Ctrl->addrLatch = 0;
}
return (0);
}
// Poll handler — nothing to do for a static RAM board.
uint8_t __func_in_RAM(MZ1R37_PollCB)(t_Z80CPU *cpu)
{
(void)cpu;
return (0);
}
// Task processor — no external tasks currently handled.
uint8_t MZ1R37_TaskProcessor(t_Z80CPU *cpu, enum Z80CPU_TASK_NAME task, char *param)
{
(void)cpu;
(void)param;
uint8_t result = 0;
switch (task)
{
default:
result = 1;
break;
}
return (result);
}
// -----------------------------------------------------------------------
// I/O handler for Base+0 (default ACh) — Address latch (write only).
//
// Via OUT (C),A:
// B register (port address MSB) provides address[19:16] (bits 0-3 used).
// A register (data) provides address[15:8].
//
// The latched value is combined with the B register during data access
// to form the full 20-bit address.
// -----------------------------------------------------------------------
uint8_t __func_in_RAM(MZ1R37_IO_AddrLatch)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
(void)cpu;
if (read)
return 0xFF; // Write-only port.
if (mz1r37Ctrl)
{
// addr MSB (B register) = address[19:16], data (A register) = address[15:8].
uint8_t addrHi = (uint8_t)((addr >> 8) & 0x0F); // Bits 19:16 (only 4 bits used).
mz1r37Ctrl->addrLatch = ((uint32_t)addrHi << 16) | ((uint32_t)data << 8);
}
return 0;
}
// -----------------------------------------------------------------------
// I/O handler for Base+1 (default ADh) — Data read/write.
//
// Via OUT (C),A or IN A,(C):
// B register (port address MSB) provides address[7:0].
// Combined with the latched address[19:8] to form the full 20-bit address.
//
// No auto-increment — each access must set the B register explicitly.
// Reads from addresses >= 640KB (0xA0000) return 0xFF (unpopulated).
// -----------------------------------------------------------------------
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;
// Form the full 20-bit address: latch[19:8] | B register[7:0].
uint32_t fullAddr = (mz1r37Ctrl->addrLatch & 0xFFF00) | (uint32_t)((addr >> 8) & 0xFF);
if (read)
{
if (fullAddr < MZ1R37_RAM_SIZE)
{
return mz1r37Ctrl->ram[fullAddr];
}
return 0xFF;
}
else
{
if (fullAddr < MZ1R37_RAM_SIZE)
{
mz1r37Ctrl->ram[fullAddr] = data;
}
return 0;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
@@ -1128,9 +1131,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)
{

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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;
}
}

View File

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

View File

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

View File

@@ -487,7 +487,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);

View File

@@ -0,0 +1,260 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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
// 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).
// 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);
#endif // CELESTITE_H

View 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

View File

@@ -0,0 +1,69 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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 × 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.
// 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.
} 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

View File

@@ -0,0 +1,134 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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_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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View File

@@ -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"
@@ -986,6 +988,7 @@ void processInterCoreCommands(void)
plogf("IC:%d ", (int) qmsg.type);
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;
@@ -1232,6 +1235,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 +1344,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;

View File

@@ -1 +1 @@
2.491
2.534

View 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:

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

View File

@@ -1 +1 @@
2.472
2.518