// 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 #include #include #include #include #include #include #include // ---- 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 || port == VIDEO_SCREEN) { // 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_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 videoScreenRecord = { VIDEO_SCREEN, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY }; static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 }; return (port == VIDEO_SCREEN) ? &videoScreenRecord : &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); } 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; } }