A device raises a line and something has to take it down. Two things did: being interrupted, and being woken from WAIT with the Interrupt Flag down - the second because a masked program has nowhere to dispatch to, so nobody else would. There was a third way to learn a device had finished and nothing answered it. The documented idiom reads the status, branches out if the device is already done, and only WAITs otherwise; on a disk quick enough to finish before the first look, which is every disk here, the WAIT is unreachable. The line then stood for the rest of the machine's life. The program that leaves it standing never pays for it - it was masked throughout. The bill arrives at whoever next sets the Interrupt Flag. The boot chain reads the disk to load a program, leaves the line up, and hands over; the loaded program is then interrupted on behalf of a read that finished before it existed, through a vector table with no entry for a device it never touched, and faults on the instruction after its SIF. Found by running Examples/tune.asm through Once. It set up its whole sound and died four bytes before its first note, which is why it was silent rather than wrong - and why it looked like a sound bug for a while. So reading the port that answers a device takes its line down, the same way taking the byte already took the console's down. Disk and screen do it on their status port. And a reset now clears every line, which is the sentence the manual already makes about the vector table: a handler left behind aims an interrupt into a program that is no longer running, and so does a line. testPrograms/diskLineTest.asm pins it - the racy idiom, then SIF with no handler installed anywhere. It faults without the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1197 lines
48 KiB
C
1197 lines
48 KiB
C
// io.c
|
|
// I/O for the SplitBit CPU Emulator
|
|
// Written by Anachronaut
|
|
// 10/16/2024
|
|
|
|
#include "io.h"
|
|
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
|
#include "controller.h"
|
|
#include "video.h"
|
|
#include "sound.h"
|
|
#include "font.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#include <poll.h>
|
|
|
|
// ---- The console ----
|
|
//
|
|
// The console owns its own reading rather than going through getchar. stdio keeps a
|
|
// buffer, and the status port asks the operating system what is waiting; those two
|
|
// disagree the moment stdio has read ahead, and the status port would then swear nothing
|
|
// was there while a read returned instantly. One byte of pushback here is enough, because
|
|
// nothing needs to look further ahead than the byte it is about to take.
|
|
|
|
static int consoleKeyMode = 0;
|
|
|
|
static int cursorColumn = 0;
|
|
static int cursorRow = 0;
|
|
// What every cell the console draws is given. Zero is the pair the machine wakes up in,
|
|
// grey on black.
|
|
static uint8_t consoleAttribute = 0;
|
|
static int consoleCursorShown = 0;
|
|
|
|
// The device draws the cursor, so it has to be told where the console put it. Called
|
|
// wherever the cursor moves, which is every one of the few places that move it.
|
|
static void consoleCursorMoved(void) {
|
|
videoSetCursor(cursorRow, cursorColumn, consoleCursorShown);
|
|
}
|
|
|
|
static int consoleEnded = 0;
|
|
static int consolePushback = -1; // A byte already taken from the host, or -1.
|
|
static int consoleInterrupts = 0; // Whether an arriving byte puts the line up.
|
|
static struct termios consoleSavedTerminal;
|
|
static int consoleTerminalSaved = 0; // There is a copy of how the terminal was found.
|
|
static int consoleTerminalRaw = 0; // The terminal is currently in this machine's mode.
|
|
static int consoleGuardsInstalled = 0; // The handlers below are in place.
|
|
|
|
// Hands the terminal back exactly as it was found, WITHOUT forgetting what the program
|
|
// asked for. Separate from consoleRestore because the two are wanted in different places:
|
|
// a machine that is stopping wants both, and a machine that is being suspended wants only
|
|
// this, since it is going to carry on wanting keys when it is resumed.
|
|
static void consoleReleaseTerminal(void) {
|
|
if (consoleTerminalRaw) {
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &consoleSavedTerminal);
|
|
consoleTerminalRaw = 0;
|
|
}
|
|
}
|
|
|
|
static void consoleTakeTerminal(void) {
|
|
if (consoleTerminalRaw || !consoleTerminalSaved) {
|
|
return;
|
|
}
|
|
struct termios raw = consoleSavedTerminal;
|
|
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
|
|
raw.c_cc[VMIN] = 1;
|
|
raw.c_cc[VTIME] = 0;
|
|
if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == 0) {
|
|
consoleTerminalRaw = 1;
|
|
}
|
|
}
|
|
|
|
void consoleRestore(void) {
|
|
consoleReleaseTerminal();
|
|
consoleKeyMode = 0;
|
|
// Whatever the console was in the middle of asking for is withdrawn along with the
|
|
// mode. A line left standing here would be answered by whatever ran next, which had
|
|
// nothing to do with it and never asked to be interrupted.
|
|
consoleInterrupts = 0;
|
|
clearInterrupt(PORT_CONSOLE);
|
|
}
|
|
|
|
// ---- Giving the terminal back whatever happens ----
|
|
//
|
|
// A machine that stops in key mode and does not undo it leaves the shell that started it
|
|
// with no echo and no line editing, which is a far worse failure than anything the program
|
|
// was doing, and one the user has no obvious way to connect to this program.
|
|
//
|
|
// atexit covers stopping on purpose and nothing else. It does NOT run when a process is
|
|
// killed by a signal, so every way of dying that matters has to be caught and undone by
|
|
// hand. The list below is every signal whose default action ends the process and which can
|
|
// be caught at all - SIGKILL and SIGSTOP cannot, and nothing can be done about those.
|
|
//
|
|
// SIGHUP is on the list for a specific reason, learned the hard way: it is what arrives
|
|
// when the terminal or the session that started this machine goes away, which is exactly
|
|
// what happens when whatever launched it crashes. Handling INT and TERM and stopping there
|
|
// covers the polite endings and misses the one that actually leaves a broken terminal
|
|
// behind.
|
|
|
|
// Puts the terminal back and then dies the way it would have died anyway, so that whatever
|
|
// is waiting sees the signal it expected rather than a machine that exited quietly.
|
|
static void consoleFatalSignal(int signalNumber) {
|
|
consoleReleaseTerminal();
|
|
signal(signalNumber, SIG_DFL);
|
|
raise(signalNumber);
|
|
}
|
|
|
|
static void consoleContinueSignal(int signalNumber);
|
|
|
|
// Suspending is not dying, so the terminal goes back but the mode is remembered. Whoever
|
|
// gets the terminal next is entitled to find it as they left it, and this machine is
|
|
// entitled to have its keys again when it is resumed.
|
|
static void consoleStopSignal(int signalNumber) {
|
|
consoleReleaseTerminal();
|
|
signal(SIGCONT, consoleContinueSignal);
|
|
signal(signalNumber, SIG_DFL);
|
|
raise(signalNumber);
|
|
}
|
|
|
|
static void consoleContinueSignal(int signalNumber) {
|
|
(void)signalNumber;
|
|
signal(SIGTSTP, consoleStopSignal);
|
|
signal(SIGCONT, consoleContinueSignal);
|
|
if (consoleKeyMode) {
|
|
consoleTakeTerminal();
|
|
}
|
|
}
|
|
|
|
static void consoleInstallGuards(void) {
|
|
if (consoleGuardsInstalled) {
|
|
return;
|
|
}
|
|
consoleGuardsInstalled = 1;
|
|
// Installed on the first use of key mode rather than at startup, so a run that never
|
|
// asks for it installs nothing at all.
|
|
atexit(consoleRestore);
|
|
|
|
static const int fatal[] = {
|
|
SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGABRT, SIGFPE,
|
|
SIGBUS, SIGSEGV, SIGPIPE, SIGALRM, SIGTERM
|
|
};
|
|
for (size_t i = 0; i < sizeof(fatal) / sizeof(fatal[0]); i++) {
|
|
signal(fatal[i], consoleFatalSignal);
|
|
}
|
|
signal(SIGTSTP, consoleStopSignal);
|
|
signal(SIGCONT, consoleContinueSignal);
|
|
}
|
|
|
|
static void consoleSetMode(int wantKeys) {
|
|
if (wantKeys == consoleKeyMode) {
|
|
return;
|
|
}
|
|
if (!wantKeys) {
|
|
consoleRestore();
|
|
return;
|
|
}
|
|
// Nothing to configure when input is not a terminal, but the mode is still recorded:
|
|
// a program asking the status port what mode it is in should be told what it asked
|
|
// for, whether or not there was a terminal to carry it out on.
|
|
consoleKeyMode = 1;
|
|
if (!isatty(STDIN_FILENO)) {
|
|
return;
|
|
}
|
|
if (!consoleTerminalSaved) {
|
|
if (tcgetattr(STDIN_FILENO, &consoleSavedTerminal) != 0) {
|
|
return;
|
|
}
|
|
consoleTerminalSaved = 1;
|
|
}
|
|
consoleInstallGuards();
|
|
consoleTakeTerminal();
|
|
}
|
|
|
|
// Puts the line up if the console has something to say and has been asked to say it.
|
|
// Called wherever news arrives and wherever a program declares it wants to hear news, so
|
|
// that enabling interrupts while a byte is already waiting is not a way to miss it.
|
|
static void consoleAnnounce(void) {
|
|
if (consoleInterrupts && (consolePushback >= 0 || consoleEnded)) {
|
|
raiseInterrupt(PORT_CONSOLE);
|
|
}
|
|
}
|
|
|
|
// The whole control port in one write. The two bits are independent, so both are read out
|
|
// of the byte and applied, and neither is inferred from the other.
|
|
static void consoleSetControl(uint8_t control) {
|
|
// The mode goes first because turning key mode off restores the terminal, and that
|
|
// withdraws any standing request along with it. Setting the interrupt bit afterwards
|
|
// means one write can ask for line mode and interrupts together, which is an ordinary
|
|
// thing to want and would otherwise be undone in the same breath as it was asked for.
|
|
consoleSetMode((control & CONSOLE_MODE_KEY) != 0);
|
|
consoleCursorShown = (control & CONSOLE_CONTROL_CURSOR) != 0;
|
|
consoleCursorMoved();
|
|
|
|
int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0;
|
|
if (!wantInterrupts) {
|
|
// Asking to stop being interrupted takes down whatever was already asked for. A
|
|
// request that outlived the setting that made it would arrive at a program that
|
|
// had just said it did not want it.
|
|
clearInterrupt(PORT_CONSOLE);
|
|
}
|
|
consoleInterrupts = wantInterrupts;
|
|
consoleAnnounce();
|
|
}
|
|
|
|
// Everything already written is put where it can be seen before the machine asks the host
|
|
// anything. Standard output is line buffered on a terminal, so a prompt with no newline
|
|
// after it - "> " is exactly that, and exactly why this matters - would sit in the buffer
|
|
// while the machine waited for an answer to a question nobody had been shown.
|
|
//
|
|
// getchar used to do this by accident, because reading through stdio flushes the line
|
|
// buffered streams first. Reading with read() does not, so what was a side effect of the
|
|
// old way is done deliberately here.
|
|
static void consoleShowWhatIsWritten(void) {
|
|
fflush(stdout);
|
|
}
|
|
|
|
// ---- The console draws as well as it speaks ----
|
|
//
|
|
// THE VOYAGER'S CONSOLE IS A DISPLAY CONTROLLER: it takes a byte stream and puts glyphs on
|
|
// a screen, keeps a cursor, and scrolls. That is an ordinary kind of chip - it is what a
|
|
// video terminal's character generator did - and it is why CosmOS needs no changes at all
|
|
// to run in a window. It already writes bytes to the console.
|
|
//
|
|
// It also keeps writing to standard output, and that is deliberate rather than an
|
|
// oversight. A machine with a screen AND a serial line is completely ordinary, the emulator's
|
|
// standard output is that serial line, and having both is what lets the whole test suite
|
|
// hold Voyager to the same recorded results as SplitBit. It is also what makes --screen
|
|
// work on the plain machine: there is one console, and it drives everything it has.
|
|
|
|
void consoleHome(void) {
|
|
cursorColumn = 0;
|
|
cursorRow = 0;
|
|
consoleAttribute = 0;
|
|
consoleCursorShown = 0;
|
|
consoleCursorMoved();
|
|
}
|
|
|
|
static void consoleNewLine(void) {
|
|
cursorColumn = 0;
|
|
if (cursorRow + 1 < videoRows()) {
|
|
cursorRow++;
|
|
} else {
|
|
// At the bottom, the screen moves under the cursor rather than the cursor moving
|
|
// off the screen. One port write in the device, and no memory moves at all.
|
|
videoScrollUp();
|
|
}
|
|
}
|
|
|
|
// Clears the screen, or from the cursor to the end of it. Reached through the console's
|
|
// Command port rather than through an escape sequence: this machine talks to its devices in
|
|
// registers, and a screen it can address directly needs no protocol to reach it.
|
|
static void consoleClearScreen(int fromCursor) {
|
|
const int rows = videoRows();
|
|
const int columns = videoColumns();
|
|
for (int row = fromCursor ? cursorRow : 0; row < rows; row++) {
|
|
const int from = (fromCursor && row == cursorRow) ? cursorColumn : 0;
|
|
for (int column = from; column < columns; column++) {
|
|
videoPutCell(row, column, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Driving a terminal on the other end of the serial line ----
|
|
//
|
|
// The machine speaks registers. A HOST TERMINAL SPEAKS ANSI, and bridging to the host is the
|
|
// emulator's job - the same job it does reading standard input. So the escapes are GENERATED
|
|
// here, outbound, for the set this device chooses, rather than parsed inbound as though the
|
|
// machine were a terminal itself.
|
|
//
|
|
// That is the whole difference in shape. Parsing means accepting an open protocol somebody
|
|
// else defines and putting a state machine in the hardware. Generating means one device
|
|
// knowing how to talk to one kind of host, in one direction, for exactly the things it can
|
|
// be asked to do - and the set cannot grow behind our backs, because we are the ones saying
|
|
// it.
|
|
static void consoleTellTerminal(const char *sequence) {
|
|
for (const char *at = sequence; *at != '\0'; at++) {
|
|
putchar(*at);
|
|
}
|
|
}
|
|
|
|
// ---- Told once, and only when it matters ----
|
|
//
|
|
// A terminal cares where the cursor is at the moment something is about to be drawn there,
|
|
// not at the moment a register was written. Announcing on every register write sent two
|
|
// sequences for one move, because setting a row and a column is two writes. So a write only
|
|
// marks it, and the next character sends it.
|
|
static int cursorTold = 1;
|
|
|
|
static void consoleSayCursor(void) {
|
|
if (cursorTold) {
|
|
return;
|
|
}
|
|
cursorTold = 1;
|
|
// Room for the widest this can be, and then some. The compiler cannot see that a cursor
|
|
// is bounded by the screen, and a warning about a buffer is not worth being clever over.
|
|
char sequence[32];
|
|
snprintf(sequence, sizeof(sequence), "\033[%d;%dH", cursorRow + 1, cursorColumn + 1);
|
|
consoleTellTerminal(sequence);
|
|
}
|
|
|
|
static void consoleDraw(uint8_t byte) {
|
|
// ---- Nowhere to put a glyph ----
|
|
//
|
|
// A console is a display controller, and a display controller draws characters on a
|
|
// character screen. In bitmap mode there is not one - the memory it would write into is
|
|
// somebody's picture - so it draws nothing and says everything down the serial line
|
|
// instead, which is where it was always going as well.
|
|
//
|
|
// The alternative is what a machine with shared video memory really does, which is
|
|
// scribble. That is honest and useless: nobody can read the marks and they ruin the
|
|
// picture, and a program that has taken the screen has not stopped wanting to print.
|
|
if (videoTextRows() == 0) {
|
|
return;
|
|
}
|
|
switch (byte) {
|
|
case '\n':
|
|
consoleNewLine();
|
|
consoleCursorMoved();
|
|
return;
|
|
case '\r':
|
|
cursorColumn = 0;
|
|
consoleCursorMoved();
|
|
return;
|
|
case 0x08: // Backspace, which CosmOS sends when a line is being edited.
|
|
if (cursorColumn > 0) {
|
|
cursorColumn--;
|
|
videoPutCell(cursorRow, cursorColumn, 0, consoleAttribute);
|
|
consoleCursorMoved();
|
|
}
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
// Anything below the font's first character has no glyph and no agreed meaning here.
|
|
// Drawing a box for it would put marks on the screen that nothing asked for.
|
|
if (byte < CONSOLE_FONT_FIRST) {
|
|
return;
|
|
}
|
|
videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST),
|
|
consoleAttribute);
|
|
if (++cursorColumn >= videoColumns()) {
|
|
consoleNewLine();
|
|
}
|
|
consoleCursorMoved();
|
|
}
|
|
|
|
// ---- Waiting for a key when there is no terminal to wait on ----
|
|
//
|
|
// A window has no standard input, and a machine that blocked on it inside a frame would
|
|
// stop drawing and stop answering. So a front end with a window installs a hook: called
|
|
// while the console has nothing, it gets to keep the window alive and hands back a byte
|
|
// when one is typed, or -1 to say the window has gone.
|
|
static int (*inputHook)(int mayWait) = NULL;
|
|
|
|
// About one frame, which is how long a hook that presents takes to come back. It does not
|
|
// have to be exact - nothing is being measured, and the only thing downstream of it is a
|
|
// cursor blinking at somebody who is thinking about what to type.
|
|
#define CONSOLE_WAIT_CYCLES 16667
|
|
|
|
static unsigned long idleCycles = 0;
|
|
|
|
// The machine's clock as the devices last heard it. Kept here rather than passed about,
|
|
// because a device that has to know how long it has been waiting has to know what time it
|
|
// is now.
|
|
static unsigned long deviceNow = 0;
|
|
|
|
// A frame has gone by with nobody typing. TWO THINGS FOLLOW FROM THAT, and only doing the
|
|
// first is what left the cursor frozen: the CPU is told afterwards that it was stopped for a
|
|
// while, which is what idle cycles are - and the DEVICES are told now, because they are
|
|
// still running. A display controller blinking a cursor does not stop because the processor
|
|
// is waiting on a key, and neither does a disk finishing a read.
|
|
static void consoleWaited(void) {
|
|
idleCycles += CONSOLE_WAIT_CYCLES;
|
|
deviceTick(deviceNow + CONSOLE_WAIT_CYCLES);
|
|
}
|
|
|
|
unsigned long takeIdleCycles(void) {
|
|
const unsigned long taken = idleCycles;
|
|
idleCycles = 0;
|
|
return taken;
|
|
}
|
|
|
|
// ---- Line editing, which the terminal used to do ----
|
|
//
|
|
// A TERMINAL IN LINE MODE DOES NOT HAND A PROGRAM EVERY KEYSTROKE. It collects a line,
|
|
// rubs out a backspace, and delivers the finished thing when Return is pressed. CosmOS has
|
|
// always relied on that, and behind a window there is no terminal to do it - so the raw
|
|
// backspace reached the shell, which put 0x08 in its command buffer and then could not find
|
|
// a command by that name. Correcting a typo made the line unrecognisable while looking
|
|
// perfectly right on screen.
|
|
//
|
|
// So the console does it, because behind a window the console IS the terminal. In key mode
|
|
// it does not: a program in key mode asked for every keystroke as it happens, which is the
|
|
// whole point of key mode.
|
|
#define CONSOLE_LINE_BYTES 256
|
|
static unsigned char consoleLine[CONSOLE_LINE_BYTES];
|
|
static int consoleLineLength = 0;
|
|
static int consoleLineAt = 0;
|
|
|
|
// Collects until Return, echoing as it goes, and leaves the line to be handed out a byte at
|
|
// a time. Returns 0 if the window closed while it was waiting.
|
|
static int consoleGatherLine(void) {
|
|
consoleLineAt = 0;
|
|
consoleLineLength = 0;
|
|
for (;;) {
|
|
const int got = inputHook(1);
|
|
if (got == CONSOLE_GONE) {
|
|
return 0;
|
|
}
|
|
if (got < 0) {
|
|
// Nobody has typed yet, and the hook spent a frame keeping the window alive.
|
|
consoleWaited();
|
|
continue;
|
|
}
|
|
if (got == 0x08) {
|
|
// Nothing to rub out at the start of a line, and rubbing out past it would eat
|
|
// the prompt, which belongs to whoever printed it.
|
|
if (consoleLineLength > 0) {
|
|
consoleLineLength--;
|
|
consoleDraw(0x08);
|
|
}
|
|
continue;
|
|
}
|
|
if (got == '\n' || got == '\r') {
|
|
consoleLine[consoleLineLength++] = '\n';
|
|
consoleDraw('\n');
|
|
return 1;
|
|
}
|
|
// No room, or nothing a line is made of. A control byte that means something to a
|
|
// terminal means nothing here yet, and putting it in the line would only hand a
|
|
// program something it cannot use.
|
|
if (got < 0x20 || consoleLineLength >= CONSOLE_LINE_BYTES - 1) {
|
|
continue;
|
|
}
|
|
consoleLine[consoleLineLength++] = (unsigned char)got;
|
|
consoleDraw((uint8_t)got);
|
|
}
|
|
}
|
|
|
|
void consoleSetInputHook(int (*hook)(int mayWait)) {
|
|
inputHook = hook;
|
|
}
|
|
|
|
uint8_t consoleReadByte(void) {
|
|
// Taking the byte answers whatever the console was asking about, so the line comes
|
|
// down here as well as when the CPU acknowledges it. Otherwise a program that reads
|
|
// the data port with the Interrupt Flag down would be interrupted afterwards on
|
|
// behalf of a byte it already has, and find nothing waiting when it looked.
|
|
clearInterrupt(PORT_CONSOLE);
|
|
if (consolePushback >= 0) {
|
|
uint8_t byte = (uint8_t)consolePushback;
|
|
consolePushback = -1;
|
|
return byte;
|
|
}
|
|
consoleShowWhatIsWritten();
|
|
if (inputHook != NULL) {
|
|
if (!consoleKeyMode) {
|
|
// A line already gathered is handed out a byte at a time, which is what the
|
|
// program is asking for. Only when it runs out is another one collected.
|
|
if (consoleLineAt >= consoleLineLength && !consoleGatherLine()) {
|
|
consoleEnded = 1;
|
|
return 0xFF;
|
|
}
|
|
return consoleLine[consoleLineAt++];
|
|
}
|
|
for (;;) {
|
|
const int got = inputHook(1);
|
|
if (got >= 0) {
|
|
// Nothing echoes in key mode: a program that asked for every keystroke as it
|
|
// happens is drawing its own screen, and marks it did not make would be in
|
|
// the way.
|
|
return (uint8_t)got;
|
|
}
|
|
if (got == CONSOLE_GONE) {
|
|
// The window has closed, which is this machine's end of input the way a
|
|
// closed pipe is the other one's.
|
|
consoleEnded = 1;
|
|
return 0xFF;
|
|
}
|
|
// Nothing typed yet. The hook kept the window alive, which took a frame, and a
|
|
// frame of waiting is a frame of time passing.
|
|
consoleWaited();
|
|
}
|
|
}
|
|
unsigned char byte;
|
|
for (;;) {
|
|
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
|
if (got == 1) {
|
|
return byte;
|
|
}
|
|
if (got == 0) {
|
|
// End of input. Still 0xFF, which is what getchar's EOF became when this was
|
|
// the only answer available, so nothing written against the old behaviour
|
|
// changes. The ENDED bit is the new way to know it was not a real byte.
|
|
consoleEnded = 1;
|
|
return 0xFF;
|
|
}
|
|
if (errno != EINTR) {
|
|
consoleEnded = 1;
|
|
return 0xFF;
|
|
}
|
|
// Interrupted before anything arrived, so ask again.
|
|
}
|
|
}
|
|
|
|
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
|
|
// into the pushback and the next read of the data port hands it over, so nothing is lost
|
|
// and no program can tell that it was fetched early.
|
|
//
|
|
// Fetching it early is what makes the answer worth having. The operating system will say a
|
|
// pipe is readable when what is waiting is the end of it, so asking without reading can
|
|
// only report that SOMETHING is there. Reading settles which: a byte, or the end. Without
|
|
// this, ENDED could not go up until a program had already read the 0xFF that stands for
|
|
// it, and every program would have to swallow one imaginary byte to find out there were
|
|
// none.
|
|
static void consoleFetch(void) {
|
|
if (consolePushback >= 0 || consoleEnded) {
|
|
return;
|
|
}
|
|
// Flushed here too. A program that draws something and then polls rather than reads is
|
|
// just as entitled to have the drawing appear, and it never reaches the read that
|
|
// would otherwise have flushed for it.
|
|
consoleShowWhatIsWritten();
|
|
// ---- Where a byte comes from when there is no standard input ----
|
|
//
|
|
// A window's keys arrive through the hook, and THE STATUS PORT HAS TO ASK IT TOO. It did
|
|
// not, so a program polling READY in a window was asking a standard input nobody was
|
|
// typing at: Snake saw no keys at all, and would suddenly see one if the terminal behind
|
|
// the window happened to be focused. Asked without waiting, because a poll is a poll -
|
|
// the front end presents a frame when the console genuinely blocks, not when it looks.
|
|
if (inputHook != NULL) {
|
|
if (!consoleKeyMode) {
|
|
// READY means there is a byte to be had, and in line mode there is one only
|
|
// while a gathered line is still being handed out. A poll must not take a key
|
|
// from under the gatherer, and half a line is not a line.
|
|
if (consoleLineAt < consoleLineLength) {
|
|
consolePushback = consoleLine[consoleLineAt++];
|
|
consoleAnnounce();
|
|
}
|
|
return;
|
|
}
|
|
const int got = inputHook(0);
|
|
if (got >= 0) {
|
|
consolePushback = got;
|
|
consoleAnnounce();
|
|
} else if (got == CONSOLE_GONE) {
|
|
consoleEnded = 1;
|
|
}
|
|
return;
|
|
}
|
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
|
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
|
return;
|
|
}
|
|
unsigned char byte;
|
|
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
|
if (got == 1) {
|
|
consolePushback = byte;
|
|
} else if (got == 0) {
|
|
consoleEnded = 1;
|
|
}
|
|
// A read that failed for any other reason is left alone: the next attempt asks again,
|
|
// and an interrupted poll is not news.
|
|
//
|
|
// Anything that was news puts the line up. This is the only place a byte arrives from
|
|
// the outside world, so it is the only place that has to, and it raises AT MOST ONCE
|
|
// PER BYTE for free: the pushback holds one, and while it is full there is nothing to
|
|
// fetch and so nothing to announce. A handler that does not read what it was called
|
|
// about is simply not called again, the way a receive register with one byte in it
|
|
// stops asking. The end of input announces itself once for the same reason - it is
|
|
// discovered once, and every later look leaves before it gets here.
|
|
consoleAnnounce();
|
|
}
|
|
|
|
// How many instructions the machine runs between glances at the console. Nothing here
|
|
// happens alongside the CPU, so noticing a keystroke costs a system call, and asking on
|
|
// every instruction costs more than executing one: a poll is about 150ns against roughly
|
|
// 9ns for an instruction at full tilt, so it would slow the machine by nearly twenty
|
|
// times. At the emulated clock this stride is a quarter of a millisecond between glances,
|
|
// which no one typing has ever been able to tell from immediately.
|
|
#define CONSOLE_SERVICE_STRIDE 256
|
|
|
|
void serviceDevices(void) {
|
|
// The common case is a machine nobody is interrupting, and it costs one test.
|
|
if (!consoleInterrupts) {
|
|
return;
|
|
}
|
|
static unsigned int untilNextGlance = 0;
|
|
if (untilNextGlance > 0) {
|
|
untilNextGlance--;
|
|
return;
|
|
}
|
|
untilNextGlance = CONSOLE_SERVICE_STRIDE - 1;
|
|
consoleFetch();
|
|
}
|
|
|
|
static uint8_t consoleStatus(void) {
|
|
uint8_t status = 0;
|
|
if (consoleKeyMode) {
|
|
status |= CONSOLE_STATUS_KEYMODE;
|
|
}
|
|
if (consoleInterrupts) {
|
|
status |= CONSOLE_STATUS_INTERRUPT;
|
|
}
|
|
if (consoleCursorShown) {
|
|
status |= CONSOLE_STATUS_CURSOR;
|
|
}
|
|
consoleFetch();
|
|
if (consoleEnded) {
|
|
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
|
|
// "there is a byte to be had", and at the end of input there is not; what a read
|
|
// returns then is 0xFF standing in for nothing. A program looping while READY
|
|
// stops on its own at the end, which is the behaviour worth having, and one that
|
|
// wants to know why asks ENDED.
|
|
return status | CONSOLE_STATUS_ENDED;
|
|
}
|
|
if (consolePushback >= 0) {
|
|
status |= CONSOLE_STATUS_READY;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
// The machine's own lines. A peripheral core's device keeps its own set, which is the
|
|
// entire reason these are a struct rather than an array sitting here.
|
|
static InterruptLines machineLines;
|
|
|
|
// And its own controller, for the same reason. Everything on this bus that moves memory means
|
|
// this one.
|
|
static Controller theMachinesController;
|
|
|
|
Controller *machineController(void) {
|
|
return &theMachinesController;
|
|
}
|
|
|
|
static uint8_t machineControllerWrite(uint8_t value, uint8_t port) {
|
|
return controllerWrite(&theMachinesController, value, port);
|
|
}
|
|
|
|
static uint8_t machineControllerRead(uint8_t port) {
|
|
return controllerRead(&theMachinesController, port);
|
|
}
|
|
|
|
static unsigned long machineControllerCycles(void) {
|
|
return controllerTakeCycles(&theMachinesController);
|
|
}
|
|
|
|
// Whether somebody has asked the machine to start over, and taking that request away.
|
|
static int resetWanted = 0;
|
|
|
|
void requestReset(void) {
|
|
resetWanted = 1;
|
|
}
|
|
|
|
void consoleResetInput(void) {
|
|
consolePushback = -1;
|
|
consoleEnded = 0;
|
|
consoleLineLength = 0;
|
|
consoleLineAt = 0;
|
|
clearInterrupt(PORT_CONSOLE);
|
|
}
|
|
|
|
int resetIsPending(void) {
|
|
return resetWanted;
|
|
}
|
|
|
|
int takeResetRequest(void) {
|
|
int wanted = resetWanted;
|
|
resetWanted = 0;
|
|
return wanted;
|
|
}
|
|
|
|
// ---- The bus this machine's processor is on ----
|
|
//
|
|
// Everything a CPU asks of the world outside it, for the world this file is. A peripheral
|
|
// core is handed a different one of these by whatever device contains it, which is the whole
|
|
// of what a private bus is: not a number to be checked, a different set of answers.
|
|
static const Bus theMachinesBus = {
|
|
OutputHandler,
|
|
InputHandler,
|
|
machineControllerCycles,
|
|
takeIdleCycles,
|
|
nextPendingInterrupt,
|
|
clearInterrupt,
|
|
};
|
|
|
|
const Bus *machineBus(void) {
|
|
return &theMachinesBus;
|
|
}
|
|
|
|
void linesRaise(InterruptLines *lines, uint8_t port) {
|
|
lines->bits[port >> 3] |= (uint8_t)(1u << (port & 7));
|
|
}
|
|
|
|
void linesClear(InterruptLines *lines, uint8_t port) {
|
|
lines->bits[port >> 3] &= (uint8_t)~(1u << (port & 7));
|
|
}
|
|
|
|
// The machine's own, which is what every device in this file means when it asks for
|
|
// attention. Wrappers rather than a change at every call site, because every one of those
|
|
// devices really is on this bus and saying so twenty times would not make it truer.
|
|
void raiseInterrupt(uint8_t port) {
|
|
linesRaise(&machineLines, port);
|
|
}
|
|
|
|
void clearInterrupt(uint8_t port) {
|
|
linesClear(&machineLines, port);
|
|
}
|
|
|
|
void clearAllInterrupts(void) {
|
|
memset(&machineLines, 0, sizeof(machineLines));
|
|
}
|
|
|
|
int linesNext(const InterruptLines *lines) {
|
|
// Lowest numbered port wins. This is a scan rather than a priority encoder, which
|
|
// means there is no arbitration to explain and a programmer can work out what
|
|
// happens next by reading the port numbers.
|
|
for (int group = 0; group < INTERRUPT_LINE_BYTES; group++) {
|
|
if (lines->bits[group] == 0) {
|
|
continue;
|
|
}
|
|
for (int bit = 0; bit < 8; bit++) {
|
|
if (lines->bits[group] & (1u << bit)) {
|
|
return group * 8 + bit;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int nextPendingInterrupt(void) {
|
|
return linesNext(&machineLines);
|
|
}
|
|
|
|
// ---- Refusing ----
|
|
//
|
|
// Set when a device will not do what it was asked, and read by the CPU immediately
|
|
// after the instruction that asked. It is not a queue: an instruction does one thing to
|
|
// one port, so there is only ever one refusal outstanding.
|
|
|
|
static uint8_t refusedVector = 0;
|
|
static uint8_t refusedPort = 0;
|
|
|
|
void refuseAccess(uint8_t faultVector) {
|
|
refusedVector = faultVector;
|
|
}
|
|
|
|
uint8_t takeRefusal(void) {
|
|
uint8_t vector = refusedVector;
|
|
refusedVector = 0;
|
|
return vector;
|
|
}
|
|
|
|
uint8_t refusingPort(void) {
|
|
return refusedPort;
|
|
}
|
|
|
|
// ---- The disk ----
|
|
//
|
|
// A block device and nothing more. It knows numbered blocks and has never heard of a
|
|
// file, which is the whole point: a filesystem is software this machine will run, not
|
|
// something the host does on its behalf. A disk that understood filenames would be the
|
|
// emulator doing the work and the machine pretending it had.
|
|
|
|
static FILE *diskImage = NULL;
|
|
static uint32_t diskBlockCount = 0;
|
|
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
|
|
|
// ---- A disk that takes time ----
|
|
//
|
|
// The command is checked at once, because a refusal is not work: asking for a block that
|
|
// is not there, or writing to a protected disk, fails before any head moves. What takes
|
|
// time is the transfer, so that is remembered here and done when the machine has run far
|
|
// enough - and until then the buffer holds the block BEFORE this one, which is exactly
|
|
// what a program that ignores the busy bit deserves to read.
|
|
// The machine's clock as devices see it, which the emulator advances as the CPU spends
|
|
// cycles. A device says when it will be finished in these, and is believed.
|
|
static void diskTransfer(uint8_t command);
|
|
|
|
static unsigned long diskLatency = 0;
|
|
static unsigned long diskReadyAt = 0;
|
|
static uint8_t diskPending = 0;
|
|
static uint16_t diskBlock = 0;
|
|
static uint8_t diskStatus = 0;
|
|
static uint8_t diskProtected = 0;
|
|
|
|
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
|
|
diskProtected = writeProtect ? 1 : 0;
|
|
diskImage = fopen(path, "r+b");
|
|
if (diskImage == NULL) {
|
|
// It may be there and simply not writable, which is a read only disk rather than
|
|
// a missing one. Try that before deciding to make a new one.
|
|
diskImage = fopen(path, "rb");
|
|
if (diskImage != NULL) {
|
|
diskProtected = 1;
|
|
}
|
|
}
|
|
if (diskImage == NULL) {
|
|
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
|
|
// block should read as.
|
|
diskImage = fopen(path, "w+b");
|
|
if (diskImage == NULL) {
|
|
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
|
|
return 1;
|
|
}
|
|
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
|
|
for (uint32_t i = 0; i < DISK_DEFAULT_BLOCKS; i++) {
|
|
if (fwrite(empty, 1, DISK_BLOCK_BYTES, diskImage) != DISK_BLOCK_BYTES) {
|
|
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
|
|
fclose(diskImage);
|
|
diskImage = NULL;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
if (fseek(diskImage, 0, SEEK_END) != 0) {
|
|
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
|
|
fclose(diskImage);
|
|
diskImage = NULL;
|
|
return 1;
|
|
}
|
|
long size = ftell(diskImage);
|
|
// A part written block at the end is not a block, so it is not counted.
|
|
diskBlockCount = (size > 0) ? (uint32_t)(size / DISK_BLOCK_BYTES) : 0;
|
|
// The protect bit is a standing property, so it reads true before anything has been
|
|
// asked of the disk rather than only after a write has been turned away.
|
|
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
|
return 0;
|
|
}
|
|
|
|
void detachDisk(void) {
|
|
if (diskImage != NULL) {
|
|
fclose(diskImage);
|
|
diskImage = NULL;
|
|
}
|
|
}
|
|
|
|
// Reads or writes the block the block registers name. The line goes up either way: the
|
|
// operation finished, and whether it worked is what Status is for.
|
|
static void diskCommand(uint8_t command) {
|
|
// The protect bit describes the disk rather than the operation, so it survives.
|
|
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
|
if (command == DISK_COMMAND_WRITE && diskProtected) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
if (diskImage == NULL || diskBlock >= diskBlockCount) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
|
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
if (command != DISK_COMMAND_READ && command != DISK_COMMAND_WRITE) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
if (diskLatency == 0) {
|
|
diskTransfer(command);
|
|
return;
|
|
}
|
|
// It is going to take a while. Say so, and remember what to do when it is over.
|
|
diskStatus |= DISK_STATUS_BUSY;
|
|
diskPending = command;
|
|
diskReadyAt = deviceNow + diskLatency;
|
|
}
|
|
|
|
// The transfer itself, whenever it happens to happen. The seek is done here rather than at
|
|
// the command, because nothing else may touch the image in between and doing it twice is
|
|
// the same answer.
|
|
static void diskTransfer(uint8_t command) {
|
|
size_t moved = 0;
|
|
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
|
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
} else if (command == DISK_COMMAND_READ) {
|
|
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
|
} else {
|
|
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
|
fflush(diskImage);
|
|
}
|
|
if (moved != DISK_BLOCK_BYTES) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
}
|
|
diskStatus &= (uint8_t)~DISK_STATUS_BUSY;
|
|
raiseInterrupt(PORT_DISK);
|
|
}
|
|
|
|
void setDiskLatency(unsigned long cycles) {
|
|
diskLatency = cycles;
|
|
}
|
|
|
|
void deviceTick(unsigned long now) {
|
|
deviceNow = now;
|
|
// The screen blinks its cursor on the machine's own clock rather than the host's, so the
|
|
// picture is the same at the same cycle count however fast anything ran.
|
|
videoTick(now);
|
|
// And the sound, which makes whatever samples are due by now. On the machine's clock,
|
|
// so the same program makes the same sound in the same cycles.
|
|
soundTick(now);
|
|
if (diskPending && now >= diskReadyAt) {
|
|
uint8_t command = diskPending;
|
|
diskPending = 0;
|
|
diskTransfer(command);
|
|
}
|
|
}
|
|
|
|
// ---- A device that brings memory ----
|
|
//
|
|
// The simplest thing that owns a bank. Writing to its port fills its memory with the
|
|
// byte written, which stands in for a disk controller reading a sector: the CPU asks for
|
|
// something and the memory it owns then holds the answer. The waiting is taken out so a
|
|
// test runs the same way every time.
|
|
|
|
#define DEVICE_MEMORY_BYTES 256
|
|
|
|
static uint8_t deviceMemoryBlock[DEVICE_MEMORY_BYTES];
|
|
|
|
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
|
if (port == PORT_MEMORY) {
|
|
*capacity = DEVICE_MEMORY_BYTES;
|
|
return deviceMemoryBlock;
|
|
}
|
|
if (port == PORT_DISK) {
|
|
// The disk's buffer is one block. Reading fills it and writing takes what is in
|
|
// it, and the only way to reach it is to register it as a bank and go through the
|
|
// controller.
|
|
*capacity = DISK_BLOCK_BYTES;
|
|
return diskBuffer;
|
|
}
|
|
if (port == PORT_VIDEO) {
|
|
// Tiles, the map and the palette, in one bank. A program blits the part that
|
|
// changed and the rest stays as it was, which is the whole reason the screen is a
|
|
// bank rather than a window onto a port.
|
|
return videoMemory(capacity);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// ---- The bus registry ----
|
|
//
|
|
// What is plugged into this machine. The table is fixed when the machine is built: a
|
|
// program cannot write to it, because writing would only let a program lie to itself
|
|
// about what hardware exists. Which routine handles a device is a different question,
|
|
// and the vector table already answers it.
|
|
//
|
|
// Nothing here touches the device being asked about. That matters more than it looks:
|
|
// reading a port is a real operation, and asking the console what it is by reading it
|
|
// would take a character off standard input and block waiting for one.
|
|
|
|
typedef struct {
|
|
uint8_t port;
|
|
uint8_t deviceClass;
|
|
uint8_t flags;
|
|
} DeviceRecord;
|
|
|
|
static const DeviceRecord deviceTable[] = {
|
|
{ PORT_CONSOLE, DEVICE_CONSOLE, 0 },
|
|
{ PORT_TEST, DEVICE_TEST, 0 },
|
|
{ PORT_REFUSE, DEVICE_REFUSE, 0 },
|
|
{ PORT_MACHINE, DEVICE_MACHINE, 0 },
|
|
{ PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY },
|
|
{ PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY },
|
|
{ PORT_VIDEO, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY },
|
|
{ PORT_SOUND, DEVICE_SOUND, 0 },
|
|
{ PORT_REGISTRY, DEVICE_REGISTRY, 0 },
|
|
};
|
|
static const int deviceCount = (int)(sizeof(deviceTable) / sizeof(deviceTable[0]));
|
|
|
|
// Which port the registry is currently being asked about, and how far through that
|
|
// port's record it has been read. Selecting a port starts the record again.
|
|
static uint8_t registrySelected = 0;
|
|
static uint8_t registryCursor = 0;
|
|
|
|
static const DeviceRecord controllerRecord = { CONTROLLER_PORT_BASE, DEVICE_CONTROLLER, 0 };
|
|
|
|
static const DeviceRecord *deviceOnPort(uint8_t port) {
|
|
// The controller answers on a block of ports rather than one, so every port in the
|
|
// block reports it. Its own memory is bank 2, which is already registered, so it
|
|
// does not set the flag that means "this brings memory somebody has to register".
|
|
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
|
|
return &controllerRecord;
|
|
}
|
|
if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) {
|
|
// The status and control ports are the same device as the data port, which is the
|
|
// one in the table and the one the console raises its line on.
|
|
return deviceOnPort(PORT_CONSOLE);
|
|
}
|
|
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
|
|
// The base port is in the table proper, since that is the one that owns the
|
|
// memory and raises the line. The rest of the block reports the same device.
|
|
return deviceOnPort(PORT_DISK);
|
|
}
|
|
if (port > PORT_VIDEO && port <= PORT_VIDEO_TOP) {
|
|
// Sixteen ports, one device, and the same rule again.
|
|
return deviceOnPort(PORT_VIDEO);
|
|
}
|
|
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
|
return deviceOnPort(PORT_SOUND);
|
|
}
|
|
for (int i = 0; i < deviceCount; i++) {
|
|
if (deviceTable[i].port == port) {
|
|
return &deviceTable[i];
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// One byte of the selected port's record. Everything about a port that is not there
|
|
// reads as zero, which is the same answer an absent registry would give.
|
|
static uint8_t readRegistry(void) {
|
|
const DeviceRecord *device = deviceOnPort(registrySelected);
|
|
uint8_t answer = 0;
|
|
if (device != NULL && registryCursor < DEVICE_RECORD_BYTES) {
|
|
answer = (registryCursor == 0) ? device->deviceClass : device->flags;
|
|
}
|
|
if (registryCursor < DEVICE_RECORD_BYTES) {
|
|
registryCursor++;
|
|
}
|
|
return answer;
|
|
}
|
|
|
|
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
|
// Whichever port is being talked to is the one that would be doing any refusing.
|
|
refusedPort = Address;
|
|
// The controller answers on a block of ports, which is a range rather than a list.
|
|
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
|
|
return machineControllerWrite(DataByte, Address);
|
|
}
|
|
// And so does the screen.
|
|
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
|
|
return videoWrite(DataByte, Address);
|
|
}
|
|
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
|
|
return soundWrite(DataByte, Address);
|
|
}
|
|
// This function sends the DataByte to the appropriate place based on the Port Address.
|
|
switch(Address) {
|
|
case CONSOLE_DATA:
|
|
// Both, always. The screen because this machine has one, and standard output
|
|
// because the serial line is how everything that is not a person reads it.
|
|
//
|
|
// The terminal is told where the cursor went first, if it has not been told
|
|
// since it was moved. Here rather than at the move, so setting a row and a
|
|
// column costs one sequence rather than two.
|
|
consoleSayCursor();
|
|
consoleDraw(DataByte);
|
|
putchar(DataByte);
|
|
break;
|
|
case CONSOLE_CONTROL: consoleSetControl(DataByte); break;
|
|
case CONSOLE_CURSOR_ROW:
|
|
// Clamped rather than refused. A cursor asked to go off the screen has an
|
|
// obvious place to be, and stopping the machine over one is a poor trade.
|
|
cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1;
|
|
cursorTold = 0;
|
|
consoleCursorMoved();
|
|
break;
|
|
case CONSOLE_CURSOR_COLUMN:
|
|
cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1;
|
|
cursorTold = 0;
|
|
consoleCursorMoved();
|
|
break;
|
|
case CONSOLE_COMMAND:
|
|
if (DataByte == CONSOLE_COMMAND_CLEAR) {
|
|
consoleClearScreen(0);
|
|
consoleTellTerminal("\033[2J");
|
|
}
|
|
// Anything else does nothing. A command block reserved for later should be
|
|
// quiet rather than fatal, the same as the screen's spare registers.
|
|
break;
|
|
case CONSOLE_ATTRIBUTE: consoleAttribute = DataByte; break;
|
|
case CONSOLE_STATUS:
|
|
// Read only. A device saying how it is does not take instructions through the
|
|
// same hole, so a write here is ignored rather than meaning something.
|
|
break;
|
|
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
|
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
|
case DISK_COMMAND: diskCommand(DataByte); break;
|
|
case PORT_MACHINE:
|
|
// Asked for here and acted on between instructions, because a device cannot
|
|
// restart the machine from inside the instruction that asked: the CPU is part
|
|
// way through a step and its state is not yet anything a reset could leave
|
|
// consistently behind.
|
|
if (DataByte == MACHINE_RESET) {
|
|
resetWanted = 1;
|
|
}
|
|
break;
|
|
case PORT_MEMORY:
|
|
// Fills the memory this device owns with the byte written. Nothing is
|
|
// reachable from here: to get at it, register it as a bank and go through
|
|
// the controller, which is the only thing that can reach a device's memory.
|
|
memset(deviceMemoryBlock, DataByte, DEVICE_MEMORY_BYTES);
|
|
break;
|
|
case PORT_REFUSE:
|
|
// A device that refuses everything. It exists so that a device's ability to
|
|
// stop the CPU can be tested before anything depends on it, and so that the
|
|
// path stays tested once the memory controller is the only real user.
|
|
refuseAccess(VECTOR_GUARD_VIOLATION);
|
|
break;
|
|
case PORT_REGISTRY:
|
|
// Names the port the registry is being asked about. This is the only thing
|
|
// that can be written to the registry, and it changes nothing about the
|
|
// machine: it selects a question, it does not give an answer.
|
|
registrySelected = DataByte;
|
|
registryCursor = 0;
|
|
break;
|
|
case PORT_TEST:
|
|
// A test device, and about the simplest one that can exist: writing to it
|
|
// puts its own line up. It stands in for the shape a real device has, where
|
|
// the CPU asks for something and is interrupted once the answer is ready,
|
|
// with the waiting taken out so that a test runs the same way every time.
|
|
// The byte written is ignored; only the asking matters.
|
|
raiseInterrupt(PORT_TEST);
|
|
break;
|
|
default:
|
|
// Writes to unused Output Ports are ignored.
|
|
return 1;
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint8_t InputHandler(uint8_t Address) {
|
|
refusedPort = Address;
|
|
if (Address >= CONTROLLER_PORT_BASE && Address <= CONTROLLER_PORT_TOP) {
|
|
return machineControllerRead(Address);
|
|
}
|
|
if (Address >= PORT_VIDEO && Address <= PORT_VIDEO_TOP) {
|
|
return videoRead(Address);
|
|
}
|
|
if (Address >= PORT_SOUND && Address <= PORT_SOUND_TOP) {
|
|
return soundRead(Address);
|
|
}
|
|
switch(Address) {
|
|
case CONSOLE_DATA:
|
|
// If data is sent here, it should be read from STDIN.
|
|
return consoleReadByte();
|
|
break;
|
|
case CONSOLE_STATUS: return consoleStatus();
|
|
case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow;
|
|
case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn;
|
|
case CONSOLE_ATTRIBUTE: return consoleAttribute;
|
|
case CONSOLE_COMMAND:
|
|
// Write only. What it did is visible in the cursor and on the screen.
|
|
return 0;
|
|
break;
|
|
case CONSOLE_CONTROL:
|
|
// Write only. Reading it gives zero rather than what was last written, because
|
|
// everything it sets is reported by the status port and one fact wants one
|
|
// place to live.
|
|
return 0;
|
|
break;
|
|
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
|
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
|
case DISK_STATUS:
|
|
// ---- Looking is what answers it ----
|
|
//
|
|
// The console takes its line down when the byte is read, because taking the byte
|
|
// is what answers the console. The disk's answer is this port: the operation
|
|
// finished, and whether it worked is what Status is for. So reading it takes the
|
|
// line down, the same way.
|
|
//
|
|
// WITHOUT THIS THE ORDINARY IDIOM LEAVES A LINE STANDING. The documented shape of
|
|
// waiting for a device reads the status, branches out if the device is already
|
|
// done, and only WAITs otherwise - so on a disk fast enough to finish before the
|
|
// first look, which is every disk here, the WAIT that would have taken the line
|
|
// down is never reached. Nothing else was going to answer it either: the program
|
|
// is masked and has no handler. The line then stands for the rest of the
|
|
// machine's life, and the next program to set the Interrupt Flag is interrupted
|
|
// on behalf of a read that finished before it was loaded.
|
|
clearInterrupt(PORT_DISK);
|
|
return diskStatus;
|
|
case PORT_REFUSE:
|
|
// Refuses reads as well, so both directions are covered.
|
|
refuseAccess(VECTOR_GUARD_VIOLATION);
|
|
return 0;
|
|
break;
|
|
case PORT_REGISTRY:
|
|
// One byte of the selected port's record, then the next, and zero once the
|
|
// record has run out.
|
|
return readRegistry();
|
|
break;
|
|
default:
|
|
// Reading from an unused port is ignored.
|
|
return 0;
|
|
break;
|
|
}
|
|
}
|