Interrupt on keypress mode

This commit is contained in:
Anachronaut
2026-08-17 16:02:44 -04:00
parent 91c9d49d1b
commit 08624925fe
11 changed files with 365 additions and 18 deletions
+5
View File
@@ -674,6 +674,11 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
void stepCPU(CPURegisters *cpu) {
if (!(cpu->Status & STATUS_HALT)) {
// Devices get their moment before the lines are read, and unconditionally: a
// device is entitled to notice something whether or not the CPU is currently
// willing to be interrupted about it. Masking decides when a request is answered,
// not whether the outside world is allowed to have happened.
serviceDevices();
// A device asking for attention is answered between instructions and never
// inside one, so the address that goes into the frame is always the start of an
// instruction and RETI always lands somewhere meaningful.
+84 -7
View File
@@ -26,6 +26,7 @@
static int consoleKeyMode = 0;
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;
@@ -35,6 +36,11 @@ void consoleRestore(void) {
consoleTerminalSaved = 0;
}
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);
}
// Restores the terminal and then dies the way it would have died anyway, so that the
@@ -45,8 +51,7 @@ static void consoleSignalHandler(int signalNumber) {
raise(signalNumber);
}
static void consoleSetMode(uint8_t mode) {
int wantKeys = (mode & CONSOLE_MODE_KEY) != 0;
static void consoleSetMode(int wantKeys) {
if (wantKeys == consoleKeyMode) {
return;
}
@@ -79,6 +84,35 @@ static void consoleSetMode(uint8_t mode) {
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}
// 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);
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
@@ -92,6 +126,11 @@ static void consoleShowWhatIsWritten(void) {
}
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;
@@ -150,10 +189,47 @@ static void consoleFetch(void) {
}
// 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 = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0;
uint8_t status = 0;
if (consoleKeyMode) {
status |= CONSOLE_STATUS_KEYMODE;
}
if (consoleInterrupts) {
status |= CONSOLE_STATUS_INTERRUPT;
}
consoleFetch();
if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
@@ -395,7 +471,7 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
}
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 that would raise a line if the console ever did.
// 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) {
@@ -440,7 +516,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case CONSOLE_CONTROL: consoleSetMode(DataByte); break;
case CONSOLE_CONTROL: consoleSetControl(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.
@@ -495,8 +571,9 @@ uint8_t InputHandler(uint8_t Address) {
break;
case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CONTROL:
// Write only. Reading it gives zero rather than the mode, because the mode is
// a bit in the status port and one fact wants one place to live.
// 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);
+30
View File
@@ -58,10 +58,27 @@
// 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
// 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
@@ -74,6 +91,9 @@
// 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
// Puts the terminal back the way it was found. Registered with atexit and called from the
// signal handlers, because a machine that stops in key mode and does not undo it leaves
@@ -161,6 +181,16 @@ uint8_t InputHandler(uint8_t Address);
// 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);
void raiseInterrupt(uint8_t port);
void clearInterrupt(uint8_t port);