ESCAPE WAS A BUG I LEFT. This machine sends Escape to the console like any other key, and Raylib closes a window on Escape unless it is told not to - so a program reading keys could be ended by one of them, taking whatever was in memory with it. SetExitKey(KEY_NULL), and it is a byte again. F12 is the reset button. A button on the case rather than a key the machine can see: nothing sends a function key to the console, so nothing can be surprised by one. It does what writing MACHINE_RESET does, which is that the machine starts the way it started - the boot chain runs again and finds whatever the disk now says to run. Which is what makes a bare metal program escapable. Once puts a demo in front of the next start and deletes the request before jumping, so a demo that has taken the whole machine is one keypress from the system coming back, instead of closing the window and opening it again. IT HAD TO REACH A MACHINE THAT IS WAITING, and that took two more things. A reset is acted on between instructions, and a machine blocked on a key is part way through one - so the button would have set a flag that nothing ever came along to notice, in exactly the situation a reset button is for. The wait ends now: the console is told its input is over, which it is for a machine about to stop existing. And the reset puts the console's input back - nothing pushed back, no line half gathered, and not at the end of input. That was already wrong before the button existed: a reset after the input ran out left a console that had run out afterwards, so a machine could be restarted once and then never typed at again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
361 lines
18 KiB
C
361 lines
18 KiB
C
// io.h
|
|
// I/O for the SplitBit CPU Emulator
|
|
// Written by Anachronaut
|
|
// 10/16/2024
|
|
|
|
#ifndef IO_H
|
|
#define IO_H
|
|
|
|
#include <stdint.h>
|
|
#include "cpu.h"
|
|
|
|
// ---- Ports ----
|
|
//
|
|
// Which port a device answers on is a property of the machine rather than of any
|
|
// program, so the numbers live here and everything else refers to them by name.
|
|
|
|
// The console answers on three ports. The data port is the machine's oldest promise and
|
|
// does not change: writing sends a byte, reading takes one and waits for it. The other two
|
|
// are additions, so a program written before they existed cannot notice them.
|
|
#define PORT_CONSOLE 0x00
|
|
#define PORT_CONSOLE_TOP 0x06
|
|
#define CONSOLE_DATA 0x00
|
|
#define CONSOLE_STATUS 0x01
|
|
#define CONSOLE_CONTROL 0x02
|
|
|
|
// ---- Where the cursor is, as registers ----
|
|
//
|
|
// Read as well as written, which is the thing an escape sequence cannot do without a query
|
|
// and a parse. A program that wants to put something back where it found it asks.
|
|
#define CONSOLE_CURSOR_ROW 0x03
|
|
#define CONSOLE_CURSOR_COLUMN 0x04
|
|
|
|
// Written, and it happens at once - the same shape as the memory controller's Command port
|
|
// rather than a bit in a register that otherwise holds state.
|
|
#define CONSOLE_COMMAND 0x05
|
|
#define CONSOLE_COMMAND_CLEAR 0x01
|
|
|
|
// ---- What colour to write in ----
|
|
//
|
|
// The attribute given to every cell the console draws from now on. Its low nibble picks one
|
|
// of sixteen ink and paper pairs, and the default palette is arranged so that XOR 8 turns
|
|
// any of them inside out - which is highlighting, and is also how the cursor is drawn.
|
|
#define CONSOLE_ATTRIBUTE 0x06
|
|
|
|
#define PORT_TEST 0x10
|
|
#define PORT_REFUSE 0x11
|
|
#define PORT_MEMORY 0x12
|
|
|
|
// ---- Starting again ----
|
|
//
|
|
// Writing 1 here asks the machine to start over: whatever put the first instruction in
|
|
// memory does it again, and the CPU begins where the boot vector points.
|
|
//
|
|
// A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not.
|
|
// Something that could only be asked for through SWI would be unavailable in exactly the
|
|
// case that most wants it, and a program that owns the whole machine has no system to ask.
|
|
//
|
|
// What it does NOT do is unplug anything. The disk stays attached and its image keeps
|
|
// whatever was written to it, which is what a warm restart means: the machine starts
|
|
// again, the world it starts into does not.
|
|
#define PORT_MACHINE 0x13
|
|
#define MACHINE_RESET 0x01
|
|
|
|
// The disk answers on a block of four ports and interrupts on the first of them. A device
|
|
// that spans more than one port raises its line on its base, which is the rule the
|
|
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
|
#define PORT_DISK 0x20
|
|
#define PORT_DISK_TOP 0x23
|
|
#define DISK_BLOCK_HIGH 0x20
|
|
#define DISK_BLOCK_LOW 0x21
|
|
#define DISK_COMMAND 0x22
|
|
#define DISK_STATUS 0x23
|
|
// ---- The screen ----
|
|
//
|
|
// Sixteen ports, like the controller, and it interrupts on its base the way the disk
|
|
// established for a device that spans more than one. The registers themselves are in
|
|
// video.h, with the memory layout they describe.
|
|
#define PORT_VIDEO 0x30
|
|
#define PORT_VIDEO_TOP 0x3F
|
|
|
|
#define PORT_REGISTRY 0xFF
|
|
|
|
// ---- The console ----
|
|
//
|
|
// Two modes, chosen by the program through the control port. The console starts in LINE
|
|
// mode, which is what the machine has always done: the terminal holds what is typed until
|
|
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
|
|
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
|
|
//
|
|
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
|
|
// program that wants them seen has to send them back out itself. That is not a choice this
|
|
// machine is making; it is what asking the terminal to stop holding a line means, and the
|
|
// editing goes away with it. A program that wants keys is expected to want that.
|
|
//
|
|
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
|
|
// wait, and keeping that in one place means the data port means one thing everywhere. A
|
|
// read that sometimes blocked and sometimes did not, depending on state set somewhere
|
|
// else, is the kind of thing that works until it does not.
|
|
//
|
|
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
|
|
// there is nothing to put into another mode, and the status port answers by asking the
|
|
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
|
|
//
|
|
// A PROGRAM THAT INTERRUPTS ON INPUT MUST NOT BLOCK ON THE DATA PORT. Reading it waits,
|
|
// and the machine executes no instructions while it is waiting, so nothing is serviced
|
|
// and the line the console is about to raise goes nowhere until the read it was meant to
|
|
// replace has already finished. Interrupting and blocking are two answers to the same
|
|
// question and a program wants one of them.
|
|
|
|
// The control port's bits, which are independent of one another. Writing zero asks for
|
|
// line mode with no interrupts, which is how the console starts and what a program that
|
|
// knows nothing of any of this leaves behind it.
|
|
#define CONSOLE_MODE_LINE 0x00
|
|
#define CONSOLE_MODE_KEY 0x01
|
|
|
|
// Asks the console to put its line up when a byte arrives, instead of the program having
|
|
// to come and look. It composes with the mode rather than depending on it: in line mode
|
|
// the terminal still holds what is typed until Return, and then a whole line's worth of
|
|
// bytes arrive at once, each raising the line in turn as the one before it is taken.
|
|
// That is not especially useful, but a control bit that quietly did nothing depending on
|
|
// another control bit would be worse than a burst of interrupts somebody asked for.
|
|
#define CONSOLE_CONTROL_INTERRUPT 0x02
|
|
// Show a cursor where the next character will go. Off when the machine starts, because a
|
|
// machine draws what it is told to and a program painting its own screen does not want one
|
|
// blinking in the middle of it. A system that reads lines from a person turns it on.
|
|
#define CONSOLE_CONTROL_CURSOR 0x04
|
|
|
|
// Set when there is a byte to be had. NOT set at the end of input, although a read would
|
|
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
|
|
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
|
|
// like that now stops when the input does, which is what anybody writing one intends.
|
|
#define CONSOLE_STATUS_READY 0x01
|
|
// Set once input has run out for good. The data port still answers 0xFF, which is what it
|
|
// always did and what every program written before this expects, but 0xFF is also an
|
|
// ordinary byte and this bit is the only thing that can tell the difference.
|
|
#define CONSOLE_STATUS_ENDED 0x02
|
|
// Which mode the console is in, so that a program can put it back the way it found it
|
|
// rather than assuming it knows.
|
|
#define CONSOLE_STATUS_KEYMODE 0x04
|
|
// Whether the console is set to interrupt, for the same reason: everything a program can
|
|
// ask the console to be, it can also ask the console what it currently is.
|
|
#define CONSOLE_STATUS_INTERRUPT 0x08
|
|
// And whether a cursor is being shown, for the same reason as the rest: everything a program
|
|
// can ask the console to be, it can also ask the console what it currently is.
|
|
#define CONSOLE_STATUS_CURSOR 0x10
|
|
|
|
// Puts the terminal back the way it was found. Registered with atexit and called from a
|
|
// handler for every signal that can end this process and be caught, because a machine that
|
|
// stops in key mode and does not undo it leaves the shell that started it unusable, which
|
|
// is a far worse failure than anything the program was doing. atexit alone is not enough:
|
|
// it does not run when a process is killed, and the ending that matters most is SIGHUP,
|
|
// which is what arrives when whatever launched this machine dies and takes the terminal
|
|
// with it.
|
|
void consoleRestore(void);
|
|
|
|
// One byte from the console, waiting if it has to. Everything that reads standard input
|
|
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
|
|
// of its own behind that would make the status port lie about what is waiting.
|
|
uint8_t consoleReadByte(void);
|
|
|
|
// Puts the cursor back in the corner. Called when the machine starts, since the screen is
|
|
// cleared then too and a cursor left where the last program stopped would be a cursor
|
|
// pointing into something that is gone.
|
|
void consoleHome(void);
|
|
|
|
// ---- How a front end with a window feeds the console ----
|
|
//
|
|
// Called while the console has nothing to give. It returns a byte, or one of the two
|
|
// answers below. They have to be told apart: a window with nobody typing yet is the normal
|
|
// case and happens sixty times a second, while a window that has gone is the end of input.
|
|
// One value for both would have made the first keystroke look like a closed machine.
|
|
//
|
|
// Without a hook the console reads standard input, which is what it has always done.
|
|
#define CONSOLE_NOTHING_YET (-1)
|
|
#define CONSOLE_GONE (-2)
|
|
//
|
|
// mayWait says which question is being asked. Zero is the status port looking, and must not
|
|
// present or sleep: a program polling in a loop would otherwise run at the frame rate. One
|
|
// is the data port blocking, where presenting is exactly right, because a machine waiting
|
|
// for a key is still a machine somebody is looking at.
|
|
void consoleSetInputHook(int (*hook)(int mayWait));
|
|
|
|
// ---- Device classes ----
|
|
//
|
|
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
|
// unimplemented port already gives zero, so "nothing there" needs no special case and a
|
|
// machine with no registry at all answers correctly by doing nothing.
|
|
//
|
|
// Classes 0x01 to 0x0F belong to the machine itself. Peripherals start at 0x10.
|
|
|
|
#define DEVICE_NONE 0x00
|
|
#define DEVICE_REGISTRY 0x01
|
|
#define DEVICE_CONSOLE 0x02
|
|
#define DEVICE_CONTROLLER 0x03
|
|
// The machine itself, which is what a reset is asking. In the range kept for the machine
|
|
// rather than among the peripherals, because it is not one: it is not attached to
|
|
// anything and cannot be unplugged.
|
|
#define DEVICE_MACHINE 0x04
|
|
#define DEVICE_TEST 0x10
|
|
#define DEVICE_REFUSE 0x11
|
|
#define DEVICE_MEMORY 0x12
|
|
#define DEVICE_DISK 0x13
|
|
#define DEVICE_VIDEO 0x14
|
|
|
|
// What a device brings besides itself. This means memory that somebody has to register
|
|
// with the controller, so the controller's own bank 2 does not count: it is already there.
|
|
#define DEVICE_FLAG_HAS_MEMORY 0x01
|
|
|
|
// ---- The disk ----
|
|
//
|
|
// Blocks are a page each, so a block number is the whole of a 16 bit address and the
|
|
// arithmetic never needs a multiply. Sixteen megabytes is absurd for this machine, which
|
|
// is the point: there is room for anything a filesystem might want to grow into later.
|
|
|
|
#define DISK_BLOCK_BYTES 256
|
|
|
|
// A fresh image is made the size of Program and Data together, which is a round number
|
|
// for this machine and small enough to read in a hex editor while it is being built.
|
|
#define DISK_DEFAULT_BLOCKS 512
|
|
|
|
#define DISK_COMMAND_READ 0x01
|
|
#define DISK_COMMAND_WRITE 0x02
|
|
|
|
// Set while an operation is still going, and it now really is set: a disk given a latency
|
|
// says busy, takes that many cycles, and finishes then. A program that does not wait gets
|
|
// whatever was in the buffer before, which is what the hardware would give it.
|
|
//
|
|
// It reads clear the whole time when the latency is zero, which is the default and how
|
|
// every test here has always run.
|
|
#define DISK_STATUS_BUSY 0x01
|
|
// Set when the disk cannot be written at all. Unlike the two bits above it, this is not
|
|
// about the last operation: it is a standing property of the medium, readable before
|
|
// anything is attempted. A write protected disk is barred here, in the device, rather
|
|
// than by anything in the filesystem, so writing blocks directly cannot get around it.
|
|
#define DISK_STATUS_PROTECTED 0x04
|
|
|
|
// Set when the last operation did not work: no image, or a block that is not on it.
|
|
// A disk that cannot read a block is an ordinary thing that happens to working programs,
|
|
// so it says so rather than stopping the machine.
|
|
#define DISK_STATUS_ERROR 0x02
|
|
|
|
// ---- Devices that take time ----
|
|
//
|
|
// A real device does not finish inside the instruction that asked it to. It says it is
|
|
// busy, takes as long as it takes, and is done when the machine has run that far - so the
|
|
// emulator needs somewhere to notice that time has passed. That is this: called once per
|
|
// instruction with the machine's clock, it lets any device whose moment has come finish.
|
|
//
|
|
// It is written for the disk and is not about the disk. Anything that will take time - a
|
|
// display that refreshes, a port that waits on the host - wants exactly this shape.
|
|
void deviceTick(unsigned long now);
|
|
|
|
// ---- Time that passed while the machine was stopped ----
|
|
//
|
|
// A console waiting on a key it has not been given has stopped the machine, and time is
|
|
// still going by: the cursor still blinks, a disk still turns. That is exactly what idle
|
|
// cycles are for, and without them the machine's clock froze the moment somebody was asked
|
|
// a question - so the cursor stopped blinking precisely when there was a person looking at
|
|
// it and waiting to type.
|
|
//
|
|
// Returned and cleared, the way the controller's cycles are, and picked up in the same
|
|
// place: after a port access, by the CPU that was stopped.
|
|
unsigned long takeIdleCycles(void);
|
|
|
|
// How many cycles a block read or write takes. Zero means the answer is there before the
|
|
// next instruction is, which is what this machine has always done and what every recorded
|
|
// test assumes.
|
|
void setDiskLatency(unsigned long cycles);
|
|
|
|
// Attaches an image, making one if it is not there. A disk is read only if the host will
|
|
// not let the file be written, or if writeProtect asks for it, which is the emulated
|
|
// equivalent of the tab on the side of a floppy. Returns 1 if it could not attach.
|
|
uint8_t attachDisk(const char *path, uint8_t writeProtect);
|
|
|
|
void detachDisk(void);
|
|
|
|
// How many bytes a device's entry in the registry runs to. Reading past the end gives
|
|
// zero, so the record can grow later without anything already written having to change.
|
|
#define DEVICE_RECORD_BYTES 2
|
|
|
|
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address);
|
|
|
|
uint8_t InputHandler(uint8_t Address);
|
|
|
|
// ---- Interrupt lines ----
|
|
//
|
|
// One line per port. A device puts its line up to ask for attention, and the CPU takes
|
|
// it down when it answers. Which line a device uses is not a choice: a device on port N
|
|
// interrupts on N, which is what saves the machine from needing any arbitration.
|
|
//
|
|
// These belong to the bus rather than to the CPU. Nothing here is saved in a frame, and
|
|
// a program cannot read them except by being interrupted.
|
|
|
|
// Gives every device a moment to notice something the machine did not ask it about.
|
|
// Nothing here runs alongside the CPU: a device that waits on the outside world - the
|
|
// console is the only one so far - is never going to see a keystroke unless something
|
|
// asks it to look, and the CPU calling this between instructions is that something.
|
|
//
|
|
// It is called on every step and gets out of the way immediately when there is nothing to
|
|
// do, because there usually is not. A device that wants attention rarely is a device that
|
|
// must cost nothing when it does not.
|
|
void serviceDevices(void);
|
|
|
|
// Set when something has written MACHINE_RESET, and taken by the loop that acts on it. A
|
|
// request rather than an action, because a device cannot restart the machine from inside
|
|
// the instruction that asked - the CPU is mid-step and its state is not yet consistent.
|
|
int takeResetRequest(void);
|
|
|
|
// ---- The button on the front of the case ----
|
|
//
|
|
// A machine has one, and a window is the case. Writing MACHINE_RESET is how a PROGRAM asks;
|
|
// this is how a person does, without needing a program that is willing to listen - which is
|
|
// the whole point of a reset button and the reason the port exists at all.
|
|
void requestReset(void);
|
|
|
|
// Puts the console's input back to how a machine starts: nothing pushed back, no line half
|
|
// gathered, and NOT at the end of input. Called when the machine starts over, because a
|
|
// console that had run out of input would still have run out afterwards - and a reset that
|
|
// left the keyboard dead would be a reset nobody could use twice.
|
|
void consoleResetInput(void);
|
|
|
|
void raiseInterrupt(uint8_t port);
|
|
|
|
void clearInterrupt(uint8_t port);
|
|
|
|
// The lowest numbered port with its line up, or -1 if none of them are.
|
|
int nextPendingInterrupt(void);
|
|
|
|
// ---- Refusing ----
|
|
//
|
|
// A device can refuse what it was asked to do. Interrupting is a device asking for
|
|
// attention later; refusing is a device saying no to the instruction happening now, so
|
|
// it has to stop the machine where it stands rather than raise a line and let execution
|
|
// carry on past the mistake.
|
|
//
|
|
// The refusal names a software vector, so the cause is known from the entry it arrives
|
|
// through, the same way every other fault on this machine works.
|
|
|
|
void refuseAccess(uint8_t faultVector);
|
|
|
|
// ---- Memory a device brings ----
|
|
//
|
|
// Returns the memory owned by the device on this port, or NULL if it owns none, and
|
|
// fills in how much of it there is. This is how the controller finds out what a device
|
|
// brings when it is told to register a bank.
|
|
//
|
|
// It is a direct look at the machine's device table rather than a conversation through
|
|
// the registry's port. The port protocol remembers which port it was asked about, so a
|
|
// controller that used it would silently lose the place of any enumeration a program had
|
|
// in progress. Same table, two consumers, and only one of them needs the protocol.
|
|
uint8_t *deviceMemory(uint8_t port, uint32_t *capacity);
|
|
|
|
// The vector a device refused with, or 0 if none did. Reading it clears it, because a
|
|
// refusal is answered once.
|
|
uint8_t takeRefusal(void);
|
|
|
|
// Which port did the refusing. Only meaningful alongside a refusal.
|
|
uint8_t refusingPort(void);
|
|
|
|
#endif // IO_H
|