blob: 4dbb07ad19df8f9ff6efa5175a501c07f1181d7b [file] [log] [blame] [raw]
/* altairz80_sio.c: MITS Altair serial I/O card
Copyright (c) 2002-2007, Peter Schorn
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
PETER SCHORN BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name of Peter Schorn shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in this Software without prior written authorization from Peter Schorn.
Based on work by Charles E Owen (c) 1997
These functions support a simulated MITS 2SIO interface card.
The card had two physical I/O ports which could be connected
to any serial I/O device that would connect to a current loop,
RS232, or TTY interface. Available baud rates were jumper
selectable for each port from 110 to 9600.
All I/O is via programmed I/O. Each device has a status port
and a data port. A write to the status port can select
some options for the device (0x03 will reset the port).
A read of the status port gets the port status:
+---+---+---+---+---+---+---+---+
| X | X | X | X | X | X | O | I |
+---+---+---+---+---+---+---+---+
I - A 1 in this bit position means a character has been received
on the data port and is ready to be read.
O - A 1 in this bit means the port is ready to receive a character
on the data port and transmit it out over the serial line.
A read to the data port gets the buffered character, a write
to the data port writes the character to the device.
*/
#include <ctype.h>
#include "altairz80_defs.h"
#include "sim_sock.h"
#include "sim_tmxr.h"
#include <time.h>
#include <assert.h>
#if UNIX_PLATFORM
#include <glob.h>
#elif defined (_WIN32)
#include <windows.h>
#endif
#define UNIT_V_ANSI (UNIT_V_UF + 0) /* ANSI mode, strip bit 8 on output */
#define UNIT_ANSI (1 << UNIT_V_ANSI)
#define UNIT_V_UPPER (UNIT_V_UF + 1) /* upper case mode */
#define UNIT_UPPER (1 << UNIT_V_UPPER)
#define UNIT_V_BS (UNIT_V_UF + 2) /* map delete to backspace */
#define UNIT_BS (1 << UNIT_V_BS)
#define UNIT_V_SIO_VERBOSE (UNIT_V_UF + 3) /* verbose mode, i.e. show error messages */
#define UNIT_SIO_VERBOSE (1 << UNIT_V_SIO_VERBOSE)
#define UNIT_V_MAP (UNIT_V_UF + 4) /* mapping mode on */
#define UNIT_MAP (1 << UNIT_V_MAP)
#define UNIT_V_BELL (UNIT_V_UF + 5) /* ^G (bell character) rings bell */
#define UNIT_BELL (1 << UNIT_V_BELL)
#define UNIT_V_SIMH_VERBOSE (UNIT_V_UF + 0) /* verbose mode for SIMH pseudo device */
#define UNIT_SIMH_VERBOSE (1 << UNIT_V_SIMH_VERBOSE)
#define UNIT_V_SIMH_TIMERON (UNIT_V_UF + 1) /* SIMH pseudo device timer generate interrupts */
#define UNIT_SIMH_TIMERON (1 << UNIT_V_SIMH_VERBOSE)
#define TERMINALS 4 /* lines per mux */
#define SIO_CAN_READ 0x01 /* bit 0 is set iff character available */
#define SIO_CAN_WRITE 0x02 /* bit 1 is set iff character can be sent */
#define SIO_RESET 0x03 /* Command to reset SIO */
#define BACKSPACE_CHAR 0x08 /* backspace character */
#define DELETE_CHAR 0x7f /* delete character */
#define CONTROLC_CHAR 0x03 /* control C character */
#define CONTROLG_CHAR 0x07 /* control G char., rings bell when displayed */
#define CONTROLZ_CHAR 0x1a /* control Z character */
static t_stat sio_set_verbose (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat simh_dev_set_timeron (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat simh_dev_set_timeroff (UNIT *uptr, int32 value, char *cptr, void *desc);
static t_stat sio_reset(DEVICE *dptr);
static t_stat sio_attach(UNIT *uptr, char *cptr);
static t_stat sio_detach(UNIT *uptr);
static t_stat ptr_reset(DEVICE *dptr);
static t_stat ptp_reset(DEVICE *dptr);
static t_stat simh_dev_reset(DEVICE *dptr);
static t_stat simh_svc(UNIT *uptr);
int32 nulldev (const int32 port, const int32 io, const int32 data);
int32 sr_dev (const int32 port, const int32 io, const int32 data);
int32 simh_dev (const int32 port, const int32 io, const int32 data);
int32 sio0d (const int32 port, const int32 io, const int32 data);
int32 sio0s (const int32 port, const int32 io, const int32 data);
int32 sio1d (const int32 port, const int32 io, const int32 data);
int32 sio1s (const int32 port, const int32 io, const int32 data);
void printMessage(void);
extern int32 getBankSelect(void);
extern void setBankSelect(const int32 b);
extern uint32 getCommon(void);
extern uint8 GetBYTEWrapper(const uint32 Addr);
extern t_bool rtc_avail;
extern FILE *sim_log;
extern int32 PCX;
extern int32 sim_switches;
extern const char *scp_error_messages[];
extern int32 SR;
extern UNIT cpu_unit;
extern volatile int32 stop_cpu;
extern int32 sim_interval;
/* SIMH pseudo device status registers */
/* ZSDOS clock definitions */
static time_t ClockZSDOSDelta = 0; /* delta between real clock and Altair clock */
static int32 setClockZSDOSPos = 0; /* determines state for receiving address of parameter block */
static int32 setClockZSDOSAdr = 0; /* address in M of 6 byte parameter block for setting time */
static int32 getClockZSDOSPos = 0; /* determines state for sending clock information */
/* CPM3 clock definitions */
static time_t ClockCPM3Delta = 0; /* delta between real clock and Altair clock */
static int32 setClockCPM3Pos = 0; /* determines state for receiving address of parameter block */
static int32 setClockCPM3Adr = 0; /* address in M of 5 byte parameter block for setting time */
static int32 getClockCPM3Pos = 0; /* determines state for sending clock information */
static int32 daysCPM3SinceOrg = 0; /* days since 1 Jan 1978 */
/* interrupt related */
static uint32 timeOfNextInterrupt; /* time when next interrupt is scheduled */
int32 timerInterrupt = FALSE; /* timer interrupt pending */
int32 timerInterruptHandler = 0x0fc00; /* default address of interrupt handling routine */
static int32 setTimerInterruptAdrPos= 0; /* determines state for receiving timerInterruptHandler */
static int32 timerDelta = 100; /* interrupt every 100 ms */
static int32 setTimerDeltaPos = 0; /* determines state for receiving timerDelta */
/* stop watch and timer related */
static uint32 stopWatchDelta = 0; /* stores elapsed time of stop watch */
static int32 getStopWatchDeltaPos = 0; /* determines the state for receiving stopWatchDelta */
static uint32 stopWatchNow = 0; /* stores starting time of stop watch */
static int32 markTimeSP = 0; /* stack pointer for timer stack */
/* default time in microseconds to sleep for SIMHSleepCmd */
#if defined (_WIN32)
static uint32 SIMHSleep = 1000; /* Sleep uses milliseconds */
#elif defined (__MWERKS__) && defined (macintosh)
static uint32 SIMHSleep = 0; /* No sleep on Macintosh OS9 */
#else
static uint32 SIMHSleep = 100; /* on other platforms 100 micro seconds is good enough */
#endif
/* miscellaneous */
static int32 versionPos = 0; /* determines state for sending device identifier */
static int32 lastCPMStatus = 0; /* result of last attachCPM command */
static int32 lastCommand = 0; /* most recent command processed on port 0xfeh */
static int32 getCommonPos = 0; /* determines state for sending the 'common' register */
/* Support for wild card expansion */
#if UNIX_PLATFORM
static glob_t globS;
static uint32 globPosNameList = 0;
static int32 globPosName = 0;
static int32 globValid = FALSE;
static int32 globError = 0;
#elif defined (_WIN32)
static WIN32_FIND_DATA FindFileData;
static HANDLE hFind = INVALID_HANDLE_VALUE;
static int32 globFinished = FALSE;
static int32 globValid = FALSE;
static int32 globPosName = 0;
#endif
/* SIO status registers */
static int32 warnLevelSIO = 3; /* display at most 'warnLevelSIO' times the same warning */
static int32 warnUnattachedPTP = 0; /* display a warning message if < warnLevel and SIO set to
VERBOSE and output to PTP without an attached file */
static int32 warnUnattachedPTR = 0; /* display a warning message if < warnLevel and SIO set to
VERBOSE and attempt to read from PTR without an attached file */
static int32 warnPTREOF = 0; /* display a warning message if < warnLevel and SIO set to
VERBOSE and attempt to read from PTR past EOF */
static int32 warnUnassignedPort = 0; /* display a warning message if < warnLevel and SIO set to
VERBOSE and attempt to perform IN or OUT on an unassigned PORT */
static TMLN TerminalLines[TERMINALS] = { /* four terminals */
{ 0 }
};
static TMXR altairTMXR = { /* mux descriptor */
TERMINALS, 0, 0, TerminalLines
};
static UNIT sio_unit = {
UDATA (NULL, UNIT_ATTABLE + UNIT_MAP, 0),
0, /* wait = 0 */
FALSE, /* u3 = FALSE, no character available in buffer */
FALSE, /* u4 = FALSE, terminal input is not attached to a file */
FALSE, /* u5 = FALSE, terminal input has not yet reached EOF */
0 /* u6 = 0, not used */
};
static REG sio_reg[] = {
{ DRDATA (SIOWLEV, warnLevelSIO, 32) },
{ DRDATA (WRNUPTP, warnUnattachedPTP, 32) },
{ DRDATA (WRNUPTR, warnUnattachedPTR, 32) },
{ DRDATA (WRNPTRE, warnPTREOF, 32) },
{ DRDATA (WRUPORT, warnUnassignedPort, 32) },
{ HRDATA (FILEATT, sio_unit.u4, 8), REG_RO }, /* TRUE iff terminal input is attached to a file */
{ HRDATA (FILEEOF, sio_unit.u5, 8), REG_RO }, /* TRUE iff terminal input file has reached EOF */
{ HRDATA (TSTATUS, sio_unit.u3, 8) }, /* TRUE iff a character available in sio_unit.buf */
{ DRDATA (TBUFFER, sio_unit.buf, 8) }, /* input buffer for one character */
{ NULL }
};
static MTAB sio_mod[] = {
{ UNIT_ANSI, 0, "TTY", "TTY", NULL }, /* keep bit 8 as is for output */
{ UNIT_ANSI, UNIT_ANSI, "ANSI", "ANSI", NULL }, /* set bit 8 to 0 before output */
{ UNIT_UPPER, 0, "ALL", "ALL", NULL }, /* do not change case of input characters */
{ UNIT_UPPER, UNIT_UPPER, "UPPER", "UPPER", NULL }, /* change input characters to upper case */
{ UNIT_BS, 0, "BS", "BS", NULL }, /* map delete to backspace */
{ UNIT_BS, UNIT_BS, "DEL", "DEL", NULL }, /* map backspace to delete */
{ UNIT_SIO_VERBOSE, 0, "QUIET", "QUIET", NULL }, /* quiet, no error messages */
{ UNIT_SIO_VERBOSE, UNIT_SIO_VERBOSE, "VERBOSE", "VERBOSE", &sio_set_verbose },
/* verbose, display warning messages */
{ UNIT_MAP, 0, "NOMAP", "NOMAP", NULL }, /* disable character mapping */
{ UNIT_MAP, UNIT_MAP, "MAP", "MAP", NULL }, /* enable all character mapping */
{ UNIT_BELL, 0, "BELL", "BELL", NULL }, /* enable bell character */
{ UNIT_BELL, UNIT_BELL, "NOBELL", "NOBELL", NULL }, /* suppress ringing the bell */
{ 0 }
};
DEVICE sio_dev = {
"SIO", &sio_unit, sio_reg, sio_mod,
1, 10, 31, 1, 8, 8,
NULL, NULL, &sio_reset,
NULL, &sio_attach, &sio_detach,
NULL, 0, 0,
NULL, NULL, NULL };
static UNIT ptr_unit = {
UDATA (NULL, UNIT_SEQ + UNIT_ATTABLE + UNIT_ROABLE, 0)
};
static REG ptr_reg[] = {
{ HRDATA (STAT, ptr_unit.u3, 8) },
{ NULL }
};
DEVICE ptr_dev = {
"PTR", &ptr_unit, ptr_reg, NULL,
1, 10, 31, 1, 8, 8,
NULL, NULL, &ptr_reset,
NULL, NULL, NULL,
NULL, 0, 0,
NULL, NULL, NULL
};
static UNIT ptp_unit = {
UDATA (NULL, UNIT_SEQ + UNIT_ATTABLE, 0)
};
DEVICE ptp_dev = {
"PTP", &ptp_unit, NULL, NULL,
1, 10, 31, 1, 8, 8,
NULL, NULL, &ptp_reset,
NULL, NULL, NULL,
NULL, 0, 0,
NULL, NULL, NULL
};
/* Synthetic device SIMH for communication
between Altair and SIMH environment using port 0xfe */
static UNIT simh_unit = {
UDATA (&simh_svc, 0, 0), KBD_POLL_WAIT
};
static REG simh_reg[] = {
{ DRDATA (CZD, ClockZSDOSDelta, 32) },
{ DRDATA (SCZP, setClockZSDOSPos, 8), REG_RO },
{ HRDATA (SCZA, setClockZSDOSAdr, 16), REG_RO },
{ DRDATA (GCZP, getClockZSDOSPos, 8), REG_RO },
{ DRDATA (CC3D, ClockCPM3Delta, 32) },
{ DRDATA (SC3DP, setClockCPM3Pos, 8), REG_RO },
{ HRDATA (SC3DA, setClockCPM3Adr, 16), REG_RO },
{ DRDATA (GC3DP, getClockCPM3Pos, 8), REG_RO },
{ DRDATA (D3DO, daysCPM3SinceOrg, 32), REG_RO },
{ DRDATA (TOFNI, timeOfNextInterrupt, 32), REG_RO },
{ DRDATA (TIMI, timerInterrupt, 3) },
{ HRDATA (TIMH, timerInterruptHandler, 16) },
{ DRDATA (STIAP, setTimerInterruptAdrPos,8), REG_RO },
{ DRDATA (TIMD, timerDelta, 32) },
{ DRDATA (STDP, setTimerDeltaPos, 8), REG_RO },
{ DRDATA (SLEEP, SIMHSleep, 32) },
{ DRDATA (STPDT, stopWatchDelta, 32), REG_RO },
{ DRDATA (STPOS, getStopWatchDeltaPos, 8), REG_RO },
{ DRDATA (STPNW, stopWatchNow, 32), REG_RO },
{ DRDATA (MTSP, markTimeSP, 8), REG_RO },
{ DRDATA (VPOS, versionPos, 8), REG_RO },
{ DRDATA (LCPMS, lastCPMStatus, 8), REG_RO },
{ DRDATA (LCMD, lastCommand, 8), REG_RO },
{ DRDATA (CPOS, getCommonPos, 8), REG_RO },
{ NULL }
};
static MTAB simh_mod[] = {
/* quiet, no warning messages */
{ UNIT_SIMH_VERBOSE, 0, "QUIET", "QUIET", NULL },
/* verbose, display warning messages */
{ UNIT_SIMH_VERBOSE, UNIT_SIMH_VERBOSE, "VERBOSE", "VERBOSE", NULL },
/* timer generated interrupts are off */
{ UNIT_SIMH_TIMERON, 0, "TIMEROFF", "TIMEROFF", &simh_dev_set_timeroff },
/* timer generated interrupts are on */
{ UNIT_SIMH_TIMERON, UNIT_SIMH_TIMERON, "TIMERON", "TIMERON", &simh_dev_set_timeron },
{ 0 }
};
DEVICE simh_device = {
"SIMH", &simh_unit, simh_reg, simh_mod,
1, 10, 31, 1, 16, 4,
NULL, NULL, &simh_dev_reset,
NULL, NULL, NULL,
NULL, 0, 0,
NULL, NULL, NULL
};
char messageBuffer[256] = { 0 };
void printMessage(void) {
printf(messageBuffer);
#if UNIX_PLATFORM
/* need to make sure that carriage return is executed - ttrunstate() of scp_tty.c
has disabled \n translation */
printf("\r\n");
#else
printf("\n");
#endif
if (sim_log) {
fprintf(sim_log, messageBuffer);
fprintf(sim_log,"\n");
}
}
static void resetSIOWarningFlags(void) {
warnUnattachedPTP = warnUnattachedPTR = warnPTREOF = warnUnassignedPort = 0;
}
static t_stat sio_set_verbose(UNIT *uptr, int32 value, char *cptr, void *desc) {
resetSIOWarningFlags();
return SCPE_OK;
}
static t_stat sio_attach(UNIT *uptr, char *cptr) {
t_stat r = SCPE_IERR;
sio_unit.u3 = FALSE; /* no character in terminal input buffer */
get_uint(cptr, 10, 65535, &r); /* attempt to get port, discard result */
if (r == SCPE_OK) { /* string can be interpreted as port number */
sio_unit.u4 = FALSE; /* terminal input is not attached to a file */
return tmxr_attach(&altairTMXR, uptr, cptr); /* attach mux */
}
sio_unit.u4 = TRUE; /* terminal input is attached to a file */
sio_unit.u5 = FALSE; /* EOF not yet reached */
return attach_unit(uptr, cptr);
}
static t_stat sio_detach(UNIT *uptr) {
sio_unit.u3 = FALSE; /* no character in terminal input buffer */
if (sio_unit.u4) { /* is terminal input attached to a file? */
sio_unit.u4 = FALSE; /* not anymore, detach */
return detach_unit(uptr);
}
return tmxr_detach(&altairTMXR, uptr);
}
static void pollConnection(void) {
if (sio_unit.flags & UNIT_ATT) {
int32 temp = tmxr_poll_conn(&altairTMXR); /* poll connection */
if (temp >= 0)
TerminalLines[temp].rcve = 1; /* enable receive */
tmxr_poll_rx(&altairTMXR); /* poll input */
tmxr_poll_tx(&altairTMXR); /* poll output */
}
}
/* reset routines */
static t_stat sio_reset(DEVICE *dptr) {
int32 i;
sio_unit.u3 = FALSE; /* no character in terminal input buffer */
resetSIOWarningFlags();
if (sio_unit.u4) { /* is terminal input attached to a file? */
rewind(sio_unit.fileref); /* yes, rewind input */
sio_unit.u5 = FALSE; /* EOF not yet reached */
}
else if (sio_unit.flags & UNIT_ATT)
for (i = 0; i < TERMINALS; i++)
if (TerminalLines[i].conn)
tmxr_reset_ln(&TerminalLines[i]);
return SCPE_OK;
}
static t_stat ptr_reset(DEVICE *dptr) {
resetSIOWarningFlags();
ptr_unit.u3 = FALSE; /* End Of File not yet reached */
if (ptr_unit.flags & UNIT_ATT) /* attached? */
rewind(ptr_unit.fileref);
return SCPE_OK;
}
static t_stat ptp_reset(DEVICE *dptr) {
resetSIOWarningFlags();
return SCPE_OK;
}
/* I/O instruction handlers, called from the CPU module when an
IN or OUT instruction is issued.
Each function is passed an 'io' flag, where 0 means a read from
the port, and 1 means a write to the port. On input, the actual
input is passed as the return value, on output, 'data' is written
to the device.
Port 1 controls console I/O. We distinguish three cases:
1) SIO attached to a file (i.e. input taken from a file )
2) SIO attached to a port (i.e. Telnet console I/O )
3) SIO not attached to a port (i.e. "regular" console I/O )
*/
int32 sio0s(const int32 port, const int32 io, const int32 data) {
int32 ti = 0, ch;
pollConnection();
if (port != 0x10) { /* 0x10 is default port with ti == 0 */
if (port == 0x14) ti = 1;
else if (port == 0x16) ti = 2;
else if (port == 0x18) ti = 3;
else assert(FALSE);
}
if (io == 0) { /* IN */
if (sio_unit.u4) { /* attached to a file? */
if (sio_unit.u5) /* EOF reached? */
sio_detach(&sio_unit); /* detach file and switch to keyboard input */
else return SIO_CAN_READ | SIO_CAN_WRITE;
}
if (sio_unit.flags & UNIT_ATT) { /* attached to a port? */
return (tmxr_rqln(&TerminalLines[ti]) ? SIO_CAN_READ : 0x00) |
/* read possible if character available */
(TerminalLines[ti].conn && TerminalLines[ti].xmte ? SIO_CAN_WRITE : 0x00);
/* write possible if connected and transmit
enabled */
}
if (sio_unit.u3) /* character available? */
return SIO_CAN_READ | SIO_CAN_WRITE;
ch = sim_poll_kbd(); /* no, try to get a character */
if (ch) { /* character available? */
if (ch == SCPE_STOP) { /* stop CPU in case ^E (default) was typed */
stop_cpu = TRUE;
sim_interval = 0; /* detect stop condition as soon as possible*/
return SIO_CAN_WRITE; /* do not consume stop character */
}
sio_unit.u3 = TRUE; /* indicate character available */
sio_unit.buf = ch; /* store character in buffer */
return SIO_CAN_READ | SIO_CAN_WRITE;
}
return SIO_CAN_WRITE;
} /* OUT follows */
if (data == SIO_RESET) /* reset command */
sio_unit.u3 = FALSE; /* indicate that no character is available */
return 0x00; /* ignored since OUT */
}
static int32 mapCharacter(int32 ch) {
ch &= 0xff;
if (sio_unit.flags & UNIT_MAP) {
if (sio_unit.flags & UNIT_BS) {
if (ch == BACKSPACE_CHAR)
return DELETE_CHAR;
}
else if (ch == DELETE_CHAR)
return BACKSPACE_CHAR;
if (sio_unit.flags & UNIT_UPPER)
return toupper(ch);
}
return ch;
}
int32 sio0d(const int32 port, const int32 io, const int32 data) {
int32 ti = 0, ch;
pollConnection();
if (port != 0x11) { /* 0x11 is default port with ti == 0 */
if (port == 0x15) ti = 1;
else if (port == 0x17) ti = 2;
else if (port == 0x19) ti = 3;
else assert(FALSE);
}
if (io == 0) { /* IN */
if (sio_unit.u4) { /* attached to a file? */
if (sio_unit.u5) { /* EOF reached? */
sio_detach(&sio_unit); /* detach file and switch to keyboard input */
return CONTROLC_CHAR; /* this time return ^C after all */
}
if ((ch = getc(sio_unit.fileref)) == EOF) { /* end of file? */
sio_unit.u5 = TRUE; /* terminal input file has reached EOF */
return CONTROLC_CHAR; /* result is ^C (= CP/M interrupt) */
}
return mapCharacter(ch); /* return mapped character */
}
if (sio_unit.flags & UNIT_ATT)
return mapCharacter(tmxr_getc_ln(&TerminalLines[ti]));
sio_unit.u3 = FALSE; /* no character is available any more */
return mapCharacter(sio_unit.buf); /* return previous character */
} /* OUT follows */
ch = sio_unit.flags & UNIT_ANSI ? data & 0x7f : data; /* clear highest bit in ANSI mode */
if ((ch != CONTROLG_CHAR) || !(sio_unit.flags & UNIT_BELL)) {
if ((sio_unit.flags & UNIT_ATT) && (!sio_unit.u4)) /* attached to a port and not to a file */
tmxr_putc_ln(&TerminalLines[ti], ch); /* status ignored */
else
sim_putchar(ch);
}
return 0x00; /* ignored since OUT */
}
/* PTR/PTP status port */
int32 sio1s(const int32 port, const int32 io, const int32 data) {
if (io == 0) { /* IN */
/* reset I bit iff PTR unit not attached or
no more data available. O bit is always
set since write always possible. */
if ((ptr_unit.flags & UNIT_ATT) == 0) { /* PTR is not attached */
if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTR < warnLevelSIO)) {
warnUnattachedPTR++;
/*06*/ MESSAGE_1("Attempt to test status of unattached PTR. 0x02 returned.");
}
return SIO_CAN_WRITE;
}
/* if EOF then SIO_CAN_WRITE else
(SIO_CAN_WRITE and SIO_CAN_READ) */
return ptr_unit.u3 ? SIO_CAN_WRITE : (SIO_CAN_READ | SIO_CAN_WRITE);
} /* OUT follows */
if (data == SIO_RESET)
ptr_unit.u3 = FALSE; /* reset EOF indicator */
return 0x00; /* ignored since OUT */
}
/* PTR/PTP data port */
int32 sio1d(const int32 port, const int32 io, const int32 data) {
int32 ch;
if (io == 0) { /* IN */
if (ptr_unit.u3) { /* EOF reached, no more data available */
if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnPTREOF < warnLevelSIO)) {
warnPTREOF++;
/*07*/ MESSAGE_1("PTR attempted to read past EOF. 0x00 returned.");
}
return 0x00;
}
if ((ptr_unit.flags & UNIT_ATT) == 0) { /* not attached */
if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTR < warnLevelSIO)) {
warnUnattachedPTR++;
/*08*/ MESSAGE_1("Attempt to read from unattached PTR. 0x00 returned.");
}
return 0x00;
}
if ((ch = getc(ptr_unit.fileref)) == EOF) { /* end of file? */
ptr_unit.u3 = TRUE; /* remember EOF reached */
return CONTROLZ_CHAR; /* ^Z denotes end of text file in CP/M */
}
return ch & 0xff;
} /* OUT follows */
if (ptp_unit.flags & UNIT_ATT) /* unit must be attached */
putc(data, ptp_unit.fileref);
/* else ignore data */
else if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnattachedPTP < warnLevelSIO)) {
warnUnattachedPTP++;
/*09*/ MESSAGE_2("Attempt to output '0x%02x' to unattached PTP - ignored.", data);
}
return 0x00; /* ignored since OUT */
}
int32 nulldev(const int32 port, const int32 io, const int32 data) {
if ((sio_unit.flags & UNIT_SIO_VERBOSE) && (warnUnassignedPort < warnLevelSIO)) {
warnUnassignedPort++;
if (io == 0) {
MESSAGE_2("Unassigned IN(%2xh) - ignored.", port);
}
else {
MESSAGE_3("Unassigned OUT(%2xh) -> %2xh - ignored.", port, data);
}
}
return io == 0 ? 0xff : 0;
}
int32 sr_dev(const int32 port, const int32 io, const int32 data) {
return io == 0 ? SR : 0;
}
static int32 toBCD(const int32 x) {
return (x / 10) * 16 + (x % 10);
}
static int32 fromBCD(const int32 x) {
return 10 * ((0xf0 & x) >> 4) + (0x0f & x);
}
/* Z80 or 8080 programs communicate with the SIMH pseudo device via port 0xfe.
The following principles apply:
1) For commands that do not require parameters and do not return results
ld a,<cmd>
out (0feh),a
Special case is the reset command which needs to be send 128 times to make
sure that the internal state is properly reset.
2) For commands that require parameters and do not return results
ld a,<cmd>
out (0feh),a
ld a,<p1>
out (0feh),a
ld a,<p2>
out (0feh),a
...
Note: The calling program must send all parameter bytes. Otherwise
the pseudo device is left in an undefined state.
3) For commands that do not require parameters and return results
ld a,<cmd>
out (0feh),a
in a,(0feh) ; <A> contains first byte of result
in a,(0feh) ; <A> contains second byte of result
...
Note: The calling program must request all bytes of the result. Otherwise
the pseudo device is left in an undefined state.
4) Commands requiring parameters and returning results do not exist currently.
*/
enum simhPseudoDeviceCommands { /* do not change order or remove commands, add only at the end */
printTimeCmd, /* 0 print the current time in milliseconds */
startTimerCmd, /* 1 start a new timer on the top of the timer stack */
stopTimerCmd, /* 2 stop timer on top of timer stack and show time difference */
resetPTRCmd, /* 3 reset the PTR device */
attachPTRCmd, /* 4 attach the PTR device */
detachPTRCmd, /* 5 detach the PTR device */
getSIMHVersionCmd, /* 6 get the current version of the SIMH pseudo device */
getClockZSDOSCmd, /* 7 get the current time in ZSDOS format */
setClockZSDOSCmd, /* 8 set the current time in ZSDOS format */
getClockCPM3Cmd, /* 9 get the current time in CP/M 3 format */
setClockCPM3Cmd, /* 10 set the current time in CP/M 3 format */
getBankSelectCmd, /* 11 get the selected bank */
setBankSelectCmd, /* 12 set the selected bank */
getCommonCmd, /* 13 get the base address of the common memory segment */
resetSIMHInterfaceCmd, /* 14 reset the SIMH pseudo device */
showTimerCmd, /* 15 show time difference to timer on top of stack */
attachPTPCmd, /* 16 attach PTP to the file with name at beginning of CP/M command line*/
detachPTPCmd, /* 17 detach PTP */
hasBankedMemoryCmd, /* 18 determines whether machine has banked memory */
setZ80CPUCmd, /* 19 set the CPU to a Z80 */
set8080CPUCmd, /* 20 set the CPU to an 8080 */
startTimerInterruptsCmd, /* 21 start timer interrupts */
stopTimerInterruptsCmd, /* 22 stop timer interrupts */
setTimerDeltaCmd, /* 23 set the timer interval in which interrupts occur */
setTimerInterruptAdrCmd, /* 24 set the address to call by timer interrupts */
resetStopWatchCmd, /* 25 reset the millisecond stop watch */
readStopWatchCmd, /* 26 read the millisecond stop watch */
SIMHSleepCmd, /* 27 let SIMH sleep for SIMHSleep microseconds */
getHostOSPathSeparator, /* 28 obtain the file path separator of the OS under which SIMH runs */
getHostFilenames /* 29 perform wildcard expansion and obtain list of file names */
};
#define CPM_COMMAND_LINE_LENGTH 128
#define TIMER_STACK_LIMIT 10 /* stack depth of timer stack */
static uint32 markTime[TIMER_STACK_LIMIT]; /* timer stack */
static struct tm currentTime;
static int32 currentTimeValid = FALSE;
static char version[] = "SIMH003";
static t_stat simh_dev_reset(DEVICE *dptr) {
currentTimeValid = FALSE;
ClockZSDOSDelta = 0;
setClockZSDOSPos = 0;
getClockZSDOSPos = 0;
ClockCPM3Delta = 0;
setClockCPM3Pos = 0;
getClockCPM3Pos = 0;
getStopWatchDeltaPos = 0;
getCommonPos = 0;
setTimerDeltaPos = 0;
setTimerInterruptAdrPos = 0;
markTimeSP = 0;
versionPos = 0;
lastCommand = 0;
lastCPMStatus = SCPE_OK;
timerInterrupt = FALSE;
if (simh_unit.flags & UNIT_SIMH_TIMERON)
simh_dev_set_timeron(NULL, 0, NULL, NULL);
return SCPE_OK;
}
static void warnNoRealTimeClock(void) {
if (simh_unit.flags & UNIT_SIMH_VERBOSE)
printf("Sorry - no real time clock available.\n");
}
static t_stat simh_dev_set_timeron(UNIT *uptr, int32 value, char *cptr, void *desc) {
if (rtc_avail) {
timeOfNextInterrupt = sim_os_msec() + timerDelta;
return sim_activate(&simh_unit, simh_unit.wait); /* activate unit */
}
warnNoRealTimeClock();
return SCPE_ARG;
}
static t_stat simh_dev_set_timeroff(UNIT *uptr, int32 value, char *cptr, void *desc) {
timerInterrupt = FALSE;
sim_cancel(&simh_unit);
return SCPE_OK;
}
static t_stat simh_svc(UNIT *uptr) {
uint32 n = sim_os_msec();
if (n >= timeOfNextInterrupt) {
timerInterrupt = TRUE;
timeOfNextInterrupt += timerDelta;
if (n >= timeOfNextInterrupt) /* time of next interrupt is not in the future */
timeOfNextInterrupt = n + timerDelta; /* make sure it is in the future! */
}
if (simh_unit.flags & UNIT_SIMH_TIMERON)
sim_activate(&simh_unit, simh_unit.wait); /* activate unit */
return SCPE_OK;
}
static char cpmCommandLine[CPM_COMMAND_LINE_LENGTH];
static void createCPMCommandLine(void) {
int32 i, len = (GetBYTEWrapper(0x80) & 0x7f); /* 0x80 contains length of command line, discard first char */
for (i = 0; i < len - 1; i++)
cpmCommandLine[i] = (char)GetBYTEWrapper(0x82 + i); /* the first char, typically ' ', is discarded */
cpmCommandLine[i] = 0; /* make C string */
}
/* The CP/M command line is used as the name of a file and UNIT* uptr is attached to it. */
static void attachCPM(UNIT *uptr) {
createCPMCommandLine();
if (uptr == &ptr_unit)
sim_switches = SWMASK('R');
else if (uptr == &ptp_unit)
sim_switches = SWMASK('W') | SWMASK('C'); /* 'C' option makes sure that file is properly truncated
if it had existed before */
lastCPMStatus = attach_unit(uptr, cpmCommandLine);
if ((lastCPMStatus != SCPE_OK) && (simh_unit.flags & UNIT_SIMH_VERBOSE)) {
MESSAGE_3("Cannot open '%s' (%s).", cpmCommandLine, scp_error_messages[lastCPMStatus - SCPE_BASE]);
/* must keep curly braces as messageX is a macro with two statements */
}
}
/* setClockZSDOSAdr points to 6 byte block in M: YY MM DD HH MM SS in BCD notation */
static void setClockZSDOS(void) {
struct tm newTime;
int32 year = fromBCD(GetBYTEWrapper(setClockZSDOSAdr));
newTime.tm_year = year < 50 ? year + 100 : year;
newTime.tm_mon = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 1)) - 1;
newTime.tm_mday = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 2));
newTime.tm_hour = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 3));
newTime.tm_min = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 4));
newTime.tm_sec = fromBCD(GetBYTEWrapper(setClockZSDOSAdr + 5));
ClockZSDOSDelta = mktime(&newTime) - time(NULL);
}
#define SECONDS_PER_MINUTE 60
#define SECONDS_PER_HOUR (60 * SECONDS_PER_MINUTE)
#define SECONDS_PER_DAY (24 * SECONDS_PER_HOUR)
static time_t mkCPM3Origin(void) {
struct tm date;
date.tm_year = 77;
date.tm_mon = 11;
date.tm_mday = 31;
date.tm_hour = 0;
date.tm_min = 0;
date.tm_sec = 0;
return mktime(&date);
}
/* setClockCPM3Adr points to 5 byte block in M:
0 - 1 int16: days since 31 Dec 77
2 BCD byte: HH
3 BCD byte: MM
4 BCD byte: SS */
static void setClockCPM3(void) {
ClockCPM3Delta = mkCPM3Origin() +
(GetBYTEWrapper(setClockCPM3Adr) + GetBYTEWrapper(setClockCPM3Adr + 1) * 256) * SECONDS_PER_DAY +
fromBCD(GetBYTEWrapper(setClockCPM3Adr + 2)) * SECONDS_PER_HOUR +
fromBCD(GetBYTEWrapper(setClockCPM3Adr + 3)) * SECONDS_PER_MINUTE +
fromBCD(GetBYTEWrapper(setClockCPM3Adr + 4)) - time(NULL);
}
static int32 simh_in(const int32 port) {
int32 result = 0;
switch(lastCommand) {
case getHostFilenames:
#if UNIX_PLATFORM
if (globValid) {
if (globPosNameList < globS.gl_pathc) {
if (!(result = globS.gl_pathv[globPosNameList][globPosName++])) {
globPosNameList++;
globPosName = 0;
}
}
else {
globValid = FALSE;
lastCommand = 0;
globfree(&globS);
}
}
#elif defined (_WIN32)
if (globValid) {
if (globFinished) {
globValid = FALSE;
}
else if (!(result = FindFileData.cFileName[globPosName++])) {
globPosName = 0;
if (!FindNextFile(hFind, &FindFileData)) {
globFinished = TRUE;
FindClose(hFind);
hFind = INVALID_HANDLE_VALUE;
}
}
}
#else
lastCommand = 0;
#endif
break;
case attachPTRCmd:
case attachPTPCmd:
result = lastCPMStatus;
lastCommand = 0;
break;
case getClockZSDOSCmd:
if (currentTimeValid) {
switch(getClockZSDOSPos) {
case 0:
result = toBCD(currentTime.tm_year > 99 ?
currentTime.tm_year - 100 : currentTime.tm_year);
getClockZSDOSPos = 1;
break;
case 1:
result = toBCD(currentTime.tm_mon + 1);
getClockZSDOSPos = 2;
break;
case 2:
result = toBCD(currentTime.tm_mday);
getClockZSDOSPos = 3;
break;
case 3:
result = toBCD(currentTime.tm_hour);
getClockZSDOSPos = 4;
break;
case 4:
result = toBCD(currentTime.tm_min);
getClockZSDOSPos = 5;
break;
case 5:
result = toBCD(currentTime.tm_sec);
getClockZSDOSPos = lastCommand = 0;
break;
}
}
else {
result = getClockZSDOSPos = lastCommand = 0;
}
break;
case getClockCPM3Cmd:
if (currentTimeValid) {
switch(getClockCPM3Pos) {
case 0:
result = daysCPM3SinceOrg & 0xff;
getClockCPM3Pos = 1;
break;
case 1:
result = (daysCPM3SinceOrg >> 8) & 0xff;
getClockCPM3Pos = 2;
break;
case 2:
result = toBCD(currentTime.tm_hour);
getClockCPM3Pos = 3;
break;
case 3:
result = toBCD(currentTime.tm_min);
getClockCPM3Pos = 4;
break;
case 4:
result = toBCD(currentTime.tm_sec);
getClockCPM3Pos = lastCommand = 0;
break;
}
}
else {
result = getClockCPM3Pos = lastCommand = 0;
}
break;
case getSIMHVersionCmd:
result = version[versionPos++];
if (result == 0) {
versionPos = lastCommand = 0;
}
break;
case getBankSelectCmd:
if (cpu_unit.flags & UNIT_BANKED) {
result = getBankSelect();
}
else {
result = 0;
if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_1("Get selected bank ignored for non-banked memory.");
}
}
lastCommand = 0;
break;
case getCommonCmd:
if (getCommonPos == 0) {
result = getCommon() & 0xff;
getCommonPos = 1;
}
else {
result = (getCommon() >> 8) & 0xff;
getCommonPos = lastCommand = 0;
}
break;
case hasBankedMemoryCmd:
result = cpu_unit.flags & UNIT_BANKED ? MAXBANKS : 0;
lastCommand = 0;
break;
case readStopWatchCmd:
if (getStopWatchDeltaPos == 0) {
result = stopWatchDelta & 0xff;
getStopWatchDeltaPos = 1;
}
else {
result = (stopWatchDelta >> 8) & 0xff;
getStopWatchDeltaPos = lastCommand = 0;
}
break;
case getHostOSPathSeparator:
#if defined (__MWERKS__) && defined (macintosh)
result = ':'; /* colon on Macintosh OS 9 */
#elif defined (_WIN32)
result = '\\'; /* back slash in Windows */
#else
result = '/'; /* slash in UNIX */
#endif
break;
default:
if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_2("Undefined IN from SIMH pseudo device on port %03xh ignored.",
port);
}
result = lastCommand = 0;
}
return result;
}
static int32 simh_out(const int32 port, const int32 data) {
time_t now;
switch(lastCommand) {
case setClockZSDOSCmd:
if (setClockZSDOSPos == 0) {
setClockZSDOSAdr = data;
setClockZSDOSPos = 1;
}
else {
setClockZSDOSAdr |= (data << 8);
setClockZSDOS();
setClockZSDOSPos = lastCommand = 0;
}
break;
case setClockCPM3Cmd:
if (setClockCPM3Pos == 0) {
setClockCPM3Adr = data;
setClockCPM3Pos = 1;
}
else {
setClockCPM3Adr |= (data << 8);
setClockCPM3();
setClockCPM3Pos = lastCommand = 0;
}
break;
case setBankSelectCmd:
if (cpu_unit.flags & UNIT_BANKED) {
setBankSelect(data & BANKMASK);
}
else if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_2("Set selected bank to %i ignored for non-banked memory.", data & 3);
}
lastCommand = 0;
break;
case setTimerDeltaCmd:
if (setTimerDeltaPos == 0) {
timerDelta = data;
setTimerDeltaPos = 1;
}
else {
timerDelta |= (data << 8);
setTimerDeltaPos = lastCommand = 0;
}
break;
case setTimerInterruptAdrCmd:
if (setTimerInterruptAdrPos == 0) {
timerInterruptHandler = data;
setTimerInterruptAdrPos = 1;
}
else {
timerInterruptHandler |= (data << 8);
setTimerInterruptAdrPos = lastCommand = 0;
}
break;
default:
lastCommand = data;
switch(data) {
case getHostFilenames:
#if UNIX_PLATFORM
if (!globValid) {
globValid = TRUE;
globPosNameList = globPosName = 0;
createCPMCommandLine();
globError = glob(cpmCommandLine, GLOB_ERR, NULL, &globS);
if (globError) {
if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_3("Cannot expand '%s'. Error is %i.", cpmCommandLine, globError);
}
globfree(&globS);
globValid = FALSE;
}
}
#elif defined (_WIN32)
if (!globValid) {
globValid = TRUE;
globPosName = 0;
globFinished = FALSE;
createCPMCommandLine();
hFind = FindFirstFile(cpmCommandLine, &FindFileData);
if (hFind == INVALID_HANDLE_VALUE) {
if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_3("Cannot expand '%s'. Error is %lu.", cpmCommandLine, GetLastError());
}
globValid = FALSE;
}
}
#endif
break;
case SIMHSleepCmd:
#if defined (_WIN32)
if ((SIMHSleep / 1000) && !sio_unit.u4) /* time to sleep and SIO not attached to a file */
Sleep(SIMHSleep / 1000);
#else
if (SIMHSleep && !sio_unit.u4) /* time to sleep and SIO not attached to a file */
usleep(SIMHSleep);
#endif
break;
case printTimeCmd: /* print time */
if (rtc_avail) {
MESSAGE_2("Current time in milliseconds = %d.", sim_os_msec());
}
else {
warnNoRealTimeClock();
}
break;
case startTimerCmd: /* create a new timer on top of stack */
if (rtc_avail) {
if (markTimeSP < TIMER_STACK_LIMIT) {
markTime[markTimeSP++] = sim_os_msec();
}
else {
MESSAGE_1("Timer stack overflow.");
}
}
else {
warnNoRealTimeClock();
}
break;
case stopTimerCmd: /* stop timer on top of stack and show time difference */
if (rtc_avail) {
if (markTimeSP > 0) {
uint32 delta = sim_os_msec() - markTime[--markTimeSP];
MESSAGE_2("Timer stopped. Elapsed time in milliseconds = %d.", delta);
}
else {
MESSAGE_1("No timer active.");
}
}
else {
warnNoRealTimeClock();
}
break;
case resetPTRCmd: /* reset ptr device */
ptr_reset(NULL);
break;
case attachPTRCmd: /* attach ptr to the file with name at beginning of CP/M command line */
attachCPM(&ptr_unit);
break;
case detachPTRCmd: /* detach ptr */
detach_unit(&ptr_unit);
break;
case getSIMHVersionCmd:
versionPos = 0;
break;
case getClockZSDOSCmd:
time(&now);
now += ClockZSDOSDelta;
currentTime = *localtime(&now);
currentTimeValid = TRUE;
getClockZSDOSPos = 0;
break;
case setClockZSDOSCmd:
setClockZSDOSPos = 0;
break;
case getClockCPM3Cmd:
time(&now);
now += ClockCPM3Delta;
currentTime = *localtime(&now);
currentTimeValid = TRUE;
daysCPM3SinceOrg = (int32) ((now - mkCPM3Origin()) / SECONDS_PER_DAY);
getClockCPM3Pos = 0;
break;
case setClockCPM3Cmd:
setClockCPM3Pos = 0;
break;
case getBankSelectCmd:
case setBankSelectCmd:
case getCommonCmd:
case hasBankedMemoryCmd:
case getHostOSPathSeparator:
break;
case resetSIMHInterfaceCmd:
markTimeSP = 0;
lastCommand = 0;
#if UNIX_PLATFORM
if (globValid) {
globValid = FALSE;
globfree(&globS);
}
#elif defined (_WIN32)
if (globValid) {
globValid = FALSE;
if (hFind != INVALID_HANDLE_VALUE) {
FindClose(hFind);
}
}
#endif
break;
case showTimerCmd: /* show time difference to timer on top of stack */
if (rtc_avail) {
if (markTimeSP > 0) {
uint32 delta = sim_os_msec() - markTime[markTimeSP - 1];
MESSAGE_2("Timer running. Elapsed in milliseconds = %d.", delta);
}
else {
MESSAGE_1("No timer active.");
}
}
else {
warnNoRealTimeClock();
}
break;
case attachPTPCmd: /* attach ptp to the file with name at beginning of CP/M command line */
attachCPM(&ptp_unit);
break;
case detachPTPCmd: /* detach ptp */
detach_unit(&ptp_unit);
break;
case setZ80CPUCmd:
cpu_unit.flags |= UNIT_CHIP;
break;
case set8080CPUCmd:
cpu_unit.flags &= ~UNIT_CHIP;
break;
case startTimerInterruptsCmd:
if (simh_dev_set_timeron(NULL, 0, NULL, NULL) == SCPE_OK) {
timerInterrupt = FALSE;
simh_unit.flags |= UNIT_SIMH_TIMERON;
}
break;
case stopTimerInterruptsCmd:
simh_unit.flags &= ~UNIT_SIMH_TIMERON;
simh_dev_set_timeroff(NULL, 0, NULL, NULL);
break;
case setTimerDeltaCmd:
setTimerDeltaPos = 0;
break;
case setTimerInterruptAdrCmd:
setTimerInterruptAdrPos = 0;
break;
case resetStopWatchCmd:
stopWatchNow = rtc_avail ? sim_os_msec() : 0;
break;
case readStopWatchCmd:
getStopWatchDeltaPos = 0;
stopWatchDelta = rtc_avail ? sim_os_msec() - stopWatchNow : 0;
break;
default:
if (simh_unit.flags & UNIT_SIMH_VERBOSE) {
MESSAGE_3("Unknown command (%i) to SIMH pseudo device on port %03xh ignored.",
data, port);
}
}
}
return 0x00; /* ignored, since OUT */
}
/* port 0xfe is a device for communication SIMH <--> Altair machine */
int32 simh_dev(const int32 port, const int32 io, const int32 data) {
return io == 0 ? simh_in(port) : simh_out(port, data);
}