osReadLine goes through the shell's editor now, so anything that asks the system for a line gets arrows, Home, End and Delete. The editor is a program, and a word typed with two letters the wrong way round can be put right without starting the line again. IT DOES NOT GET THE HISTORY, and that is the interesting half. Edit would otherwise fill the history with the text of somebody's document, and pressing Up in the middle of writing one would put "dir" into it. The history belongs to the thing whose lines are commands. Two entry points rather than a flag the caller sets first, so a caller cannot forget which it wanted. And the console is put back the way it was FOUND rather than the way the shell likes it. A program that had asked for key mode and then read a line through the system used to be handed back a console in line mode having asked for nothing of the sort. The status port reports all three things the control port can ask for, in the same order two bits along, so one shift turns what the console IS into what to write to make it that again. Which uncovered a real fault in the console. READING THE STATUS PORT WAS EATING A KEY: in line mode the poll consumed an arrow key and dropped it, so a program that looked and then asked for key mode - exactly what reading a line now does - found the first key it was reaching for already gone. A look must not consume what it cannot report, because the mode can change. It is held now and delivered as soon as something will take it. A blocking read still discards it, and must: that read IS the delivery, and a byte held there would be met again forever. Four recordings gained a program's echo, and cosmosEdit's went from "> : : : : > : : > 1: alpha" to a session you can read. VERIFIED THE SAME WAY AS BEFORE: with only the program side of the echo silenced, all 192 tests pass against the recordings as they were before this commit, so the echo is the whole of what changed. cosmosEditService is the new test and it checks both halves at once. Inside Edit, Left/Delete/Left puts "alpah" right. Up and Down do nothing there - were a program's line walking the shell's history, the next line would come out as the echo command from the top of the file instead of the word. And one press of Up back at the prompt finds the command typed before Edit was started, which is the proof that nothing the editor read went into the history at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1624 lines
67 KiB
C
1624 lines
67 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.
|
|
//
|
|
// 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.
|
|
static int consoleKeyFromInput(int mayWait) {
|
|
for (;;) {
|
|
int got;
|
|
if (consoleHeldByte >= 0) {
|
|
got = consoleHeldByte;
|
|
consoleHeldByte = -1;
|
|
} else {
|
|
got = 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);
|
|
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) {
|
|
// 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_TIMER, DEVICE_TIMER, 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);
|
|
}
|
|
if (port > PORT_TIMER && port <= PORT_TIMER_TOP) {
|
|
return deviceOnPort(PORT_TIMER);
|
|
}
|
|
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_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;
|
|
}
|
|
}
|