// io.h // I/O for the SplitBit CPU Emulator // Written by Anachronaut // 10/16/2024 #ifndef IO_H #define IO_H #include #include "cpu.h" #include "controller.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); // Whether one is waiting, without taking it. For anything that has to behave differently // while a restart is on its way but is not the thing that performs it. int resetIsPending(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); // ---- The lines on one bus ---- // // One bit a port, so a device can ask for attention without anything having to poll it. // Eight ports to the byte, low bit first. // // A MACHINE WITH TWO PROCESSORS HAS TWO SETS OF THESE. A peripheral core's devices raise // lines on the core's bus and the host's devices on the host's, and they are not the same // lines: one array indexed by port for the whole machine would have let each see the other's, // so a disk finishing would interrupt a sound core and a sound core's tick would interrupt // the shell. Both would arrive at a handler for something else entirely. #define INTERRUPT_LINE_BYTES 32 typedef struct { uint8_t bits[INTERRUPT_LINE_BYTES]; } InterruptLines; void linesRaise(InterruptLines *lines, uint8_t port); void linesClear(InterruptLines *lines, uint8_t port); // The lowest numbered port with its line up, or below zero if none has. A scan rather than // a priority encoder, so there is no arbitration to explain and a programmer can work out // what happens next by reading the port numbers. int linesNext(const InterruptLines *lines); // The machine's own controller, which is what every device on this bus means when it moves // memory. A peripheral core's device holds its own. Controller *machineController(void); // The machine's own lines, which is what every device on this bus means. 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); // The bus this machine's own processor is on: its devices, its lines, its controller. A // peripheral core is given a different one by whatever device contains it. const Bus *machineBus(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