CosmOS's line editor looks for 0x08, which is what Voyager's keyboard
sends. A POSIX terminal sends its own erase character instead, and on
most of them that is 0x7F.
It stayed hidden while the terminal was doing the editing: canonical
mode consumes the erase character itself and hands over a finished
line. Key mode turns ICANON off, which is the point of it, so from the
day the shell started editing its own line - 7360374 - Backspace worked
in Voyager and did nothing at all in the console-only emulator. The
Programming Manual already claimed the console normalised Backspace on a
terminal the way it normalises the arrow keys; the code did not.
The erase character is read from the terminal's own VERASE rather than
assumed to be 0x7F, because some terminals are set to 0x08 and a person
who has moved their erase key has said where it is. Forward Delete is
untouched and stays 0x86: two keys, two values.
Only when standard input really is a terminal. A file or a pipe holding
0x7F holds a byte somebody wrote, not a key somebody pressed.
Three checks in terminal.sh under a pseudo-terminal whose VERASE is set
on the slave side: erase at 0x7F arrives as Backspace, erase at 0x08
still does, and a 0x7F read from a file is left alone. The last one is
what fails on the obvious wrong fix of translating 0x7F unconditionally,
which was verified with break.sh along with the fix's removal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1711 lines
72 KiB
C
1711 lines
72 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 "pad.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;
|
|
case '\t': {
|
|
// ---- A tab moves the cursor and marks nothing ----
|
|
//
|
|
// The byte reaches standard output either way, so anything reading the serial
|
|
// line has always seen tabs and a host terminal has always laid them out. The
|
|
// screen dropped them, which is why a tab separated file - an assembler symbol
|
|
// table, say - came out of Type or More as its fields run together.
|
|
//
|
|
// MOVING RATHER THAN WRITING SPACES is what a terminal does, and the difference
|
|
// shows the moment anything is already on the line: a tab that wrote blanks
|
|
// would rub out what it passed over.
|
|
//
|
|
// Eight columns is the conventional stop and the one everything that prints a
|
|
// tab assumes. Reaching or passing the right edge wraps, which is what an
|
|
// ordinary character does at the edge - the alternative, stopping in the last
|
|
// column, invents a rule that only tabs obey.
|
|
int stop = (cursorColumn / CONSOLE_TAB_WIDTH + 1) * CONSOLE_TAB_WIDTH;
|
|
if (stop >= videoColumns()) {
|
|
consoleNewLine();
|
|
} else {
|
|
cursorColumn = (uint8_t)stop;
|
|
}
|
|
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.
|
|
//
|
|
// THE CONSOLE'S OWN KEYS GO THE SAME WAY, from above rather than below: this is the
|
|
// gatherer, which is what line mode IS behind a window, and line mode delivers
|
|
// characters. An arrow key pressed while something else is collecting the line
|
|
// arrived too late to move anything.
|
|
if (got < 0x20 || (got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST) ||
|
|
consoleLineLength >= CONSOLE_LINE_BYTES - 1) {
|
|
continue;
|
|
}
|
|
consoleLine[consoleLineLength++] = (unsigned char)got;
|
|
consoleDraw((uint8_t)got);
|
|
}
|
|
}
|
|
|
|
void consoleSetInputHook(int (*hook)(int mayWait)) {
|
|
inputHook = hook;
|
|
}
|
|
|
|
// ---- Reading standard input, in one place ----
|
|
//
|
|
// The blocking read and the poll behind the status port each reached for read() themselves,
|
|
// which was fine while one byte from the host was one byte for the program. It stopped
|
|
// being fine the moment a key could arrive as several: a sequence half taken by one path
|
|
// and half by the other is not a key, it is two pieces of rubbish. So there is one way in
|
|
// now, and the translation below sits on top of it.
|
|
//
|
|
// Answers a byte, CONSOLE_NOTHING_YET when nothing is waiting and it was told not to wait,
|
|
// or CONSOLE_GONE at the end of input. Deliberately the same three answers a front end's
|
|
// hook gives, so that everything above this treats a terminal and a window alike.
|
|
static int consoleFromInput(int mayWait) {
|
|
if (!mayWait) {
|
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
|
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
|
return CONSOLE_NOTHING_YET;
|
|
}
|
|
}
|
|
unsigned char byte;
|
|
for (;;) {
|
|
const ssize_t got = read(STDIN_FILENO, &byte, 1);
|
|
if (got == 1) {
|
|
return byte;
|
|
}
|
|
if (got == 0) {
|
|
return CONSOLE_GONE;
|
|
}
|
|
if (errno != EINTR) {
|
|
return CONSOLE_GONE;
|
|
}
|
|
// Interrupted before anything arrived, so ask again.
|
|
}
|
|
}
|
|
|
|
// ---- What a terminal sends for a key that is not a character ----
|
|
//
|
|
// ESC [ A and its neighbours. A window hands over the key somebody pressed; a terminal
|
|
// hands over the sequence it was taught to send decades ago, and something has to turn one
|
|
// into the other. It happens here because the console is already the thing that turns a
|
|
// window's Enter key into a newline, and because doing this once in C is a great deal
|
|
// better than doing it in every program that wants an arrow key.
|
|
//
|
|
// ONLY WHEN THERE IS A TERMINAL. Nothing else sends these: a file or a pipe holds exactly
|
|
// the bytes somebody put in it, and translating there would mean an escape byte followed by
|
|
// a bracket could never be read back as what it is. It also keeps the timing problem below
|
|
// out of every test this machine has, which is the larger gain - a test writes the key
|
|
// values themselves and no terminal is involved in reading them.
|
|
//
|
|
// THE TIMING PROBLEM, which only the terminal case has: pressing Escape and pressing Up
|
|
// both begin with 0x1B, and the difference is that Up is followed immediately by more. So
|
|
// after an escape the console waits a moment to see whether anything is. A keyboard
|
|
// delivers a whole sequence in one go, so the wait is only ever spent when somebody really
|
|
// did press Escape, and it is far shorter than the gap between two keystrokes.
|
|
#define CONSOLE_ESCAPE_WAIT_MS 30
|
|
|
|
static int consoleTerminalKnown = 0; // isatty has been asked.
|
|
static int consoleHasTerminal = 0; // and this is what it said.
|
|
|
|
static int consoleTranslatingKeys(void) {
|
|
if (!consoleTerminalKnown) {
|
|
consoleHasTerminal = isatty(STDIN_FILENO);
|
|
consoleTerminalKnown = 1;
|
|
}
|
|
return consoleHasTerminal;
|
|
}
|
|
|
|
// A byte taken while deciding what an escape was, which turned out not to belong to it.
|
|
// One is enough: it is only ever the byte immediately after an escape that can be both
|
|
// part of a sequence and an ordinary character.
|
|
static int consoleHeldByte = -1;
|
|
|
|
// The next byte of a sequence, or below zero if the terminal has stopped talking. The wait
|
|
// is what tells a sequence from a keypress, so this is the one read in the console that
|
|
// times out rather than blocking.
|
|
static int consoleSequenceByte(void) {
|
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
|
if (poll(&waiting, 1, CONSOLE_ESCAPE_WAIT_MS) <= 0 || (waiting.revents & POLLIN) == 0) {
|
|
return -1;
|
|
}
|
|
return consoleFromInput(1);
|
|
}
|
|
|
|
// An escape has just been read. Answers the key it began, 0x1B if it was the Escape key
|
|
// itself, or below zero for a sequence this console has no key for.
|
|
static int consoleKeyFromEscape(void) {
|
|
const int intro = consoleSequenceByte();
|
|
if (intro < 0) {
|
|
// Nothing followed it, so somebody pressed Escape.
|
|
return 0x1B;
|
|
}
|
|
if (intro != '[' && intro != 'O') {
|
|
// An escape and then something else, close enough together to look like one thing.
|
|
// It was two: the escape is delivered now and the other byte waits its turn rather
|
|
// than being dropped, because it is an ordinary character somebody typed.
|
|
consoleHeldByte = intro;
|
|
return 0x1B;
|
|
}
|
|
// ESC [ 3 ~ carries a number and ESC [ A does not, and both end in a byte that says
|
|
// which kind it was. So the digits are collected and the ending decides.
|
|
int number = 0;
|
|
int final = consoleSequenceByte();
|
|
while (final >= '0' && final <= '9') {
|
|
number = number * 10 + (final - '0');
|
|
final = consoleSequenceByte();
|
|
}
|
|
switch (final) {
|
|
case 'A': return CONSOLE_KEY_UP;
|
|
case 'B': return CONSOLE_KEY_DOWN;
|
|
case 'C': return CONSOLE_KEY_RIGHT;
|
|
case 'D': return CONSOLE_KEY_LEFT;
|
|
// Terminals disagree about Home and End more than about anything else here, so both
|
|
// spellings of each are taken: the lettered one, and the numbered one that the
|
|
// terminals which do not use letters send instead.
|
|
case 'H': return CONSOLE_KEY_HOME;
|
|
case 'F': return CONSOLE_KEY_END;
|
|
case '~':
|
|
switch (number) {
|
|
case 1: case 7: return CONSOLE_KEY_HOME;
|
|
case 4: case 8: return CONSOLE_KEY_END;
|
|
case 3: return CONSOLE_KEY_DELETE;
|
|
default: break;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
// A sequence this console has no key for. THE WHOLE OF IT GOES rather than the bytes
|
|
// being handed on, because it is a control sequence and not text: a program given the
|
|
// tail of one would put a bracket and a letter into whatever it was reading, which is
|
|
// the exact fault this whole translation exists to end.
|
|
return -1;
|
|
}
|
|
|
|
// One key from standard input: a byte as it arrived, or one of the console's own key values
|
|
// where a terminal sent a sequence meaning one.
|
|
// ---- The terminal's erase key is not the one this machine knows ----
|
|
//
|
|
// SplitBit's Backspace is 0x08. That is what Voyager's keyboard sends and what CosmOS's line
|
|
// editor looks for. A POSIX terminal sends whatever ITS erase character is, and on most of
|
|
// them that is 0x7F.
|
|
//
|
|
// It went unnoticed for as long as the terminal was doing the editing: in canonical mode the
|
|
// tty eats its own erase character and hands over a finished line. Key mode turns ICANON off,
|
|
// which is the point of it, and from then on the byte arrives raw and means nothing to the
|
|
// editor. So Backspace worked in Voyager and stopped working in the console-only emulator on
|
|
// the day the shell started editing the line itself.
|
|
//
|
|
// VERASE IS ASKED RATHER THAN 0x7F ASSUMED, because the terminal is what knows: it was saved
|
|
// on the way into raw mode, some terminals really are set to 0x08, and a person who has moved
|
|
// their erase key somewhere else has said where it is.
|
|
//
|
|
// ONLY WHEN STANDARD INPUT IS A TERMINAL. A file or a pipe holding 0x7F holds a byte that
|
|
// somebody wrote, not a key somebody pressed, and rewriting it would corrupt input that has
|
|
// nothing to do with terminals. consoleTerminalSaved is exactly that question: it is only
|
|
// ever set after an isatty succeeded.
|
|
static int consoleEraseNormalized(int got) {
|
|
if (!consoleTerminalSaved || got < 0) {
|
|
return got;
|
|
}
|
|
const cc_t erase = consoleSavedTerminal.c_cc[VERASE];
|
|
// A terminal with no erase key at all, which says so this way, has nothing to translate.
|
|
if (erase == _POSIX_VDISABLE) {
|
|
return got;
|
|
}
|
|
return got == (int)erase ? 0x08 : got;
|
|
}
|
|
|
|
static int consoleKeyFromInput(int mayWait) {
|
|
for (;;) {
|
|
int got;
|
|
if (consoleHeldByte >= 0) {
|
|
// Already normalized on the way in, and doing it twice would be wrong the day
|
|
// 0x08 is somebody's erase character: it is its own answer.
|
|
got = consoleHeldByte;
|
|
consoleHeldByte = -1;
|
|
} else {
|
|
got = consoleEraseNormalized(consoleFromInput(mayWait));
|
|
}
|
|
if (got < 0) {
|
|
return got;
|
|
}
|
|
if (got == 0x1B && consoleTranslatingKeys()) {
|
|
got = consoleKeyFromEscape();
|
|
}
|
|
// Nothing to hand over: either that sequence meant nothing here, or it meant a key
|
|
// and line mode does not deliver keys. Both are the same answer to a caller - there
|
|
// is still no byte - so a blocking read asks again and a poll says so and leaves.
|
|
const int undeliverable =
|
|
!consoleKeyMode && got >= CONSOLE_KEY_FIRST && got <= CONSOLE_KEY_LAST;
|
|
if (got < 0 || undeliverable) {
|
|
if (!mayWait) {
|
|
// ---- A LOOK MUST NOT CONSUME WHAT IT CANNOT REPORT ----
|
|
//
|
|
// This is the status port asking, and either way it has no byte to report.
|
|
// But a key that line mode will not deliver is not the same as a key that is
|
|
// gone: THE MODE CAN CHANGE. A program that polls and then asks for key mode
|
|
// - which is exactly what the shell does before it reads a line - would find
|
|
// that the first key it was reaching for had been swallowed by the looking.
|
|
//
|
|
// So it is held rather than dropped, and delivered as soon as something is
|
|
// willing to take it. The blocking read below drops it instead, and must:
|
|
// that read IS the delivery, line mode genuinely has no use for the key, and
|
|
// a byte held there would be met again forever.
|
|
if (undeliverable) {
|
|
consoleHeldByte = got;
|
|
}
|
|
return CONSOLE_NOTHING_YET;
|
|
}
|
|
continue;
|
|
}
|
|
return got;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
const int got = consoleKeyFromInput(1);
|
|
if (got >= 0) {
|
|
return (uint8_t)got;
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
const int got = consoleKeyFromInput(0);
|
|
if (got >= 0) {
|
|
consolePushback = got;
|
|
} else if (got == CONSOLE_GONE) {
|
|
consoleEnded = 1;
|
|
} else {
|
|
// Nothing waiting, which is the ordinary answer and not news.
|
|
return;
|
|
}
|
|
// 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.
|
|
|
|
// ---- What belongs to a drive, and what belongs to the controller ----
|
|
//
|
|
// A disk is write protected and has a size; a controller has a block register, a status and
|
|
// one buffer. So these three are per drive and everything below is not - which is the same
|
|
// division a real controller makes, and the reason the buffer holding whichever drive was
|
|
// last read is correct rather than a shortcut.
|
|
static FILE *diskImage[DISK_DRIVE_COUNT];
|
|
static uint32_t diskBlockCount[DISK_DRIVE_COUNT];
|
|
static uint8_t diskProtected[DISK_DRIVE_COUNT];
|
|
|
|
// A drive whose blocks are memory. Everything else about it is a drive: it selects, it reads
|
|
// and writes, it has a size, and a filesystem on it is a filesystem. What it does not have is
|
|
// a file behind it, so it comes up as zeroes and goes away when the machine does.
|
|
static uint8_t *diskMemory[DISK_DRIVE_COUNT];
|
|
|
|
// Which one the registers refer to, and how many are plugged in at all.
|
|
static uint8_t diskDrive = 0;
|
|
static uint8_t diskDrives = 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 void diskSettle(void);
|
|
|
|
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;
|
|
|
|
// Attaching gives the next free drive number, so the order they are named on the command
|
|
// line is the order the machine has them in.
|
|
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
|
|
if (diskDrives >= DISK_DRIVE_COUNT) {
|
|
fprintf(stderr, "Error: This machine has %d drives.\n", DISK_DRIVE_COUNT);
|
|
return 1;
|
|
}
|
|
const uint8_t at = diskDrives;
|
|
diskProtected[at] = writeProtect ? 1 : 0;
|
|
diskImage[at] = fopen(path, "r+b");
|
|
if (diskImage[at] == 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[at] = fopen(path, "rb");
|
|
if (diskImage[at] != NULL) {
|
|
diskProtected[at] = 1;
|
|
}
|
|
}
|
|
if (diskImage[at] == NULL) {
|
|
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
|
|
// block should read as.
|
|
diskImage[at] = fopen(path, "w+b");
|
|
if (diskImage[at] == 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[at]) != DISK_BLOCK_BYTES) {
|
|
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
|
|
fclose(diskImage[at]);
|
|
diskImage[at] = NULL;
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
if (fseek(diskImage[at], 0, SEEK_END) != 0) {
|
|
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
|
|
fclose(diskImage[at]);
|
|
diskImage[at] = NULL;
|
|
return 1;
|
|
}
|
|
long size = ftell(diskImage[at]);
|
|
// A part written block at the end is not a block, so it is not counted.
|
|
diskBlockCount[at] = (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.
|
|
// The protect bit is a standing property of the drive now selected, so it reads true
|
|
// before anything has been asked of it rather than only after a write is turned away.
|
|
diskDrives++;
|
|
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
|
return 0;
|
|
}
|
|
|
|
uint8_t attachRamDisk(uint32_t blocks) {
|
|
if (diskDrives >= DISK_DRIVE_COUNT) {
|
|
fprintf(stderr, "Error: This machine has %d drives.\n", DISK_DRIVE_COUNT);
|
|
return 1;
|
|
}
|
|
if (blocks == 0) {
|
|
fprintf(stderr, "Error: A disk of no blocks is not a disk.\n");
|
|
return 1;
|
|
}
|
|
const uint8_t at = diskDrives;
|
|
diskMemory[at] = calloc(blocks, DISK_BLOCK_BYTES);
|
|
if (diskMemory[at] == NULL) {
|
|
fprintf(stderr, "Error: Couldn't make a %u block disk in memory.\n", blocks);
|
|
return 1;
|
|
}
|
|
diskBlockCount[at] = blocks;
|
|
diskProtected[at] = 0;
|
|
diskDrives++;
|
|
return 0;
|
|
}
|
|
|
|
void detachDisk(void) {
|
|
for (int at = 0; at < DISK_DRIVE_COUNT; at++) {
|
|
if (diskImage[at] != NULL) {
|
|
fclose(diskImage[at]);
|
|
diskImage[at] = NULL;
|
|
}
|
|
free(diskMemory[at]);
|
|
diskMemory[at] = NULL;
|
|
}
|
|
diskDrives = 0;
|
|
diskDrive = 0;
|
|
}
|
|
|
|
// 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[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
|
if (command == DISK_COMMAND_WRITE && diskProtected[diskDrive]) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
if (diskMemory[diskDrive] != NULL) {
|
|
// A drive made of memory. The same block, the same 256 bytes, and no seek: what
|
|
// makes this worth having is that a program cannot tell except by how fast it was.
|
|
if (diskBlock >= diskBlockCount[diskDrive]) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
uint8_t *at = diskMemory[diskDrive] + (size_t)diskBlock * DISK_BLOCK_BYTES;
|
|
if (command == DISK_COMMAND_WRITE) {
|
|
memcpy(at, diskBuffer, DISK_BLOCK_BYTES);
|
|
} else {
|
|
memcpy(diskBuffer, at, DISK_BLOCK_BYTES);
|
|
}
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
if (diskImage[diskDrive] == NULL || diskBlock >= diskBlockCount[diskDrive]) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
raiseInterrupt(PORT_DISK);
|
|
return;
|
|
}
|
|
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
|
if (fseek(diskImage[diskDrive], 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.
|
|
// ---- Finishing what a drive was in the middle of ----
|
|
//
|
|
// A transfer waits for the clock, so at any moment one may be owed. Changing drives with one
|
|
// outstanding would run it against the disk that is arriving instead of the one that asked,
|
|
// so the drive register calls this first and the transfer happens now.
|
|
//
|
|
// The waiting is what is given up, not the work. A program that changes drives without
|
|
// looking at the status bit has not lost anything it had asked for.
|
|
static void diskSettle(void) {
|
|
if (diskPending) {
|
|
const uint8_t command = diskPending;
|
|
diskPending = 0;
|
|
diskTransfer(command);
|
|
}
|
|
}
|
|
|
|
static void diskTransfer(uint8_t command) {
|
|
size_t moved = 0;
|
|
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
|
if (fseek(diskImage[diskDrive], offset, SEEK_SET) != 0) {
|
|
diskStatus |= DISK_STATUS_ERROR;
|
|
} else if (command == DISK_COMMAND_READ) {
|
|
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
|
} else {
|
|
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
|
fflush(diskImage[diskDrive]);
|
|
}
|
|
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);
|
|
// And the timer, which is the only beat a program can choose for itself.
|
|
timerTick(now);
|
|
// And the pads, whose recordings step on a frame so that a level read twice in one is
|
|
// the same level both times.
|
|
padTick(now);
|
|
if (diskPending && now >= diskReadyAt) {
|
|
diskSettle();
|
|
}
|
|
}
|
|
|
|
// ---- A beat a program sets for itself ----
|
|
//
|
|
// COUNTED IN CYCLES, which is this machine's unit of time everywhere else: it is what the
|
|
// cost model counts and what a frame is measured in. A timer counting anything else would be
|
|
// a second thing to remember, and a prescaler would buy range that twenty four bits already
|
|
// covers - one cycle at one end and sixteen point seven seconds at the other, with 120 beats
|
|
// a minute sitting at 500,000 in the middle of it.
|
|
static uint32_t timerPeriod = 0;
|
|
static uint32_t timerLeft = 0;
|
|
static uint8_t timerControl = 0;
|
|
static uint8_t timerTicked = 0;
|
|
static unsigned long timerLast = 0;
|
|
|
|
void timerReset(void) {
|
|
timerPeriod = 0;
|
|
timerLeft = 0;
|
|
timerControl = 0;
|
|
timerTicked = 0;
|
|
timerLast = 0;
|
|
clearInterrupt(PORT_TIMER);
|
|
}
|
|
|
|
// ---- Caught up rather than counted ----
|
|
//
|
|
// The same shape as the screen's frame: the machine runs in batches, so more than one period
|
|
// can pass between two looks. What is owed is worked out from how far the clock moved rather
|
|
// than by being told once per cycle, and several periods at once still mean one tick - a
|
|
// missed one is missed, which is what missing one is.
|
|
void timerTick(unsigned long now) {
|
|
const unsigned long moved = now - timerLast;
|
|
timerLast = now;
|
|
if (!(timerControl & TIMER_CONTROL_RUN) || timerPeriod == 0) {
|
|
return;
|
|
}
|
|
if ((unsigned long)timerLeft > moved) {
|
|
timerLeft -= (uint32_t)moved;
|
|
return;
|
|
}
|
|
|
|
timerTicked = 1;
|
|
if (timerControl & TIMER_CONTROL_INTERRUPT) {
|
|
raiseInterrupt(PORT_TIMER);
|
|
}
|
|
if (timerControl & TIMER_CONTROL_REPEAT) {
|
|
// What is left over carries into the next period, so a timer asked for 1,000 cycles
|
|
// gets a tick every 1,000 and not every 1,000 plus however late anybody looked.
|
|
const unsigned long over = moved - timerLeft;
|
|
timerLeft = timerPeriod - (uint32_t)(over % timerPeriod);
|
|
} else {
|
|
timerControl = (uint8_t)(timerControl & ~TIMER_CONTROL_RUN);
|
|
timerLeft = 0;
|
|
}
|
|
}
|
|
|
|
static uint8_t timerWrite(uint8_t value, uint8_t port) {
|
|
switch (port) {
|
|
case TIMER_CONTROL: {
|
|
const uint8_t wasRunning = timerControl & TIMER_CONTROL_RUN;
|
|
timerControl = value;
|
|
if ((value & TIMER_CONTROL_RUN) && !wasRunning) {
|
|
// Starting loads the period. Asking it to run while it already is does not,
|
|
// so a program that sets the interrupt bit half way through a period does not
|
|
// silently move the beat it was keeping.
|
|
timerLeft = timerPeriod;
|
|
}
|
|
if (!(value & TIMER_CONTROL_INTERRUPT)) {
|
|
// Asking to stop being interrupted takes down whatever was already asked for,
|
|
// the same as the screen and for the same reason.
|
|
clearInterrupt(PORT_TIMER);
|
|
}
|
|
}
|
|
break;
|
|
case TIMER_PERIOD_HIGH:
|
|
timerPeriod = (timerPeriod & 0x0000FFFFu) | ((uint32_t)value << 16);
|
|
break;
|
|
case TIMER_PERIOD_MID:
|
|
timerPeriod = (timerPeriod & 0x00FF00FFu) | ((uint32_t)value << 8);
|
|
break;
|
|
case TIMER_PERIOD_LOW:
|
|
timerPeriod = (timerPeriod & 0x00FFFF00u) | value;
|
|
break;
|
|
default: break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static uint8_t timerRead(uint8_t port) {
|
|
switch (port) {
|
|
case TIMER_STATUS: {
|
|
uint8_t status = 0;
|
|
if (timerTicked) status |= TIMER_STATUS_TICKED;
|
|
if (timerControl & TIMER_CONTROL_RUN) status |= TIMER_STATUS_RUNNING;
|
|
if (timerControl & TIMER_CONTROL_INTERRUPT) status |= TIMER_STATUS_INTERRUPT;
|
|
// Looking is what answers it, the same as every other status port here: a beat
|
|
// that has been noticed is not still waiting to be, and the line goes down with
|
|
// the flag because a program that polls is not one that will answer a handler.
|
|
timerTicked = 0;
|
|
clearInterrupt(PORT_TIMER);
|
|
return status;
|
|
}
|
|
case TIMER_CONTROL: return timerControl;
|
|
case TIMER_PERIOD_HIGH: return (uint8_t)(timerPeriod >> 16);
|
|
case TIMER_PERIOD_MID: return (uint8_t)(timerPeriod >> 8);
|
|
case TIMER_PERIOD_LOW: return (uint8_t)timerPeriod;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
// ---- 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 || port == VIDEO_SCREEN0 || port == VIDEO_SCREEN1) {
|
|
// Two banks: the atlas of tiles and colours on the base port, and the map or the
|
|
// bitmap on its own. A program blits the part that changed and the rest stays as it
|
|
// was, which is the whole reason the screen is memory rather than a window onto a
|
|
// port - and having two means a picture costs the map and not the font.
|
|
return videoMemory(port, 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_TIMER, DEVICE_TIMER, 0 },
|
|
{ PORT_PAD, DEVICE_PAD, 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 - with one difference, because
|
|
// this device owns TWO banks. Forwarding the whole block to the base record used to
|
|
// say that all sixteen ports brought memory, which was harmless only while nobody
|
|
// believed it: a program that enumerated the block and registered everything
|
|
// claiming memory would have faulted on the fourteen that have none.
|
|
//
|
|
// So the block answers honestly. The screen port says it brings memory because it
|
|
// does, and the rest of the block says it does not.
|
|
static const DeviceRecord videoScreen0Record =
|
|
{ VIDEO_SCREEN0, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
|
static const DeviceRecord videoScreen1Record =
|
|
{ VIDEO_SCREEN1, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
|
static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 };
|
|
if (port == VIDEO_SCREEN0) { return &videoScreen0Record; }
|
|
if (port == VIDEO_SCREEN1) { return &videoScreen1Record; }
|
|
return &videoPlainRecord;
|
|
}
|
|
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
|
return deviceOnPort(PORT_SOUND);
|
|
}
|
|
if (port > PORT_TIMER && port <= PORT_TIMER_TOP) {
|
|
return deviceOnPort(PORT_TIMER);
|
|
}
|
|
// Sixteen ports, one device: four pads, a port saying which are there, and eleven kept
|
|
// for the analogue axes that are not built.
|
|
if (port > PORT_PAD && port <= PORT_PAD_TOP) {
|
|
return deviceOnPort(PORT_PAD);
|
|
}
|
|
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);
|
|
}
|
|
if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) {
|
|
return timerWrite(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);
|
|
// ---- And the cursor goes home ----
|
|
//
|
|
// A screen with nothing on it and a cursor half way down it is not a cleared
|
|
// screen: the next thing written lands where the last thing happened to
|
|
// leave off, which is a position that no longer means anything because
|
|
// whatever gave it meaning has just been erased.
|
|
//
|
|
// The attribute is deliberately NOT reset. Clearing is about what is on the
|
|
// screen and not about how the next thing will be drawn - a program that
|
|
// chose a colour and then cleared still wants that colour, the same as on a
|
|
// terminal, where 2J does not touch the graphics state either.
|
|
cursorRow = 0;
|
|
cursorColumn = 0;
|
|
consoleCursorMoved();
|
|
// 2J empties it and H is what puts the cursor at the top. A terminal given
|
|
// only the first does exactly what this device did before this comment.
|
|
consoleTellTerminal("\033[2J\033[H");
|
|
}
|
|
// 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 DISK_DRIVE:
|
|
// ---- Choosing which disk the registers mean ----
|
|
//
|
|
// Whatever the drive was doing is collected first. A controller told to change
|
|
// drives in the middle of a transfer has no good answer, and the transfer it was
|
|
// part way through belongs to the drive being left.
|
|
//
|
|
// A number past the end selects nothing rather than wrapping to drive 0. Wrapping
|
|
// would mean a program asking for a drive that is not there quietly reading the
|
|
// one that is, which is the same shape of fault as the bank number Grid took: it
|
|
// succeeds, and the wrong disk answers. So the selection stands and every read of
|
|
// it says so.
|
|
diskSettle();
|
|
if (DataByte < DISK_DRIVE_COUNT) {
|
|
diskDrive = DataByte;
|
|
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
|
}
|
|
break;
|
|
case DISK_DRIVES:
|
|
case DISK_FLAGS:
|
|
case DISK_SIZE_HIGH:
|
|
case DISK_SIZE_LOW:
|
|
// Read only: how many drives there are, and what kind each one is, are facts
|
|
// about the machine rather than instructions to it.
|
|
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);
|
|
}
|
|
if (Address >= PORT_PAD && Address <= PORT_PAD_TOP) {
|
|
return padRead(Address);
|
|
}
|
|
if (Address >= PORT_TIMER && Address <= PORT_TIMER_TOP) {
|
|
return timerRead(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_DRIVE: return diskDrive;
|
|
case DISK_DRIVES: return diskDrives;
|
|
case DISK_FLAGS:
|
|
// What the selected drive IS. Volatile means its contents do not survive the
|
|
// machine stopping, which is the one thing a system cannot find out by looking.
|
|
return (uint8_t)(diskMemory[diskDrive] != NULL ? DISK_FLAG_VOLATILE : 0);
|
|
case DISK_SIZE_HIGH: return (uint8_t)(diskBlockCount[diskDrive] >> 8);
|
|
case DISK_SIZE_LOW: return (uint8_t)(diskBlockCount[diskDrive] & 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;
|
|
}
|
|
}
|