From 85329f13c3346d29743c117b8c50c696120f1c76 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Sat, 29 Aug 2026 22:15:02 -0400 Subject: [PATCH] Take a device's line down when its status port is read A device raises a line and something has to take it down. Two things did: being interrupted, and being woken from WAIT with the Interrupt Flag down - the second because a masked program has nowhere to dispatch to, so nobody else would. There was a third way to learn a device had finished and nothing answered it. The documented idiom reads the status, branches out if the device is already done, and only WAITs otherwise; on a disk quick enough to finish before the first look, which is every disk here, the WAIT is unreachable. The line then stood for the rest of the machine's life. The program that leaves it standing never pays for it - it was masked throughout. The bill arrives at whoever next sets the Interrupt Flag. The boot chain reads the disk to load a program, leaves the line up, and hands over; the loaded program is then interrupted on behalf of a read that finished before it existed, through a vector table with no entry for a device it never touched, and faults on the instruction after its SIF. Found by running Examples/tune.asm through Once. It set up its whole sound and died four bytes before its first note, which is why it was silent rather than wrong - and why it looked like a sound bug for a while. So reading the port that answers a device takes its line down, the same way taking the byte already took the console's down. Disk and screen do it on their status port. And a reset now clears every line, which is the sentence the manual already makes about the vector table: a handler left behind aims an interrupt into a program that is no longer running, and so does a line. testPrograms/diskLineTest.asm pins it - the racy idiom, then SIF with no handler installed anywhere. It faults without the fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/testPrograms/diskLineTest.asm | 69 ++++++++++++++++++++++++++ Source/Emulator/io.c | 23 ++++++++- Source/Emulator/io.h | 6 +++ Source/Emulator/machine.c | 8 +++ Source/Emulator/video.c | 5 ++ SplitBit Programming Manual.md | 37 ++++++++++++++ SplitBit Test Manual.md | 2 +- Tests/expected/diskLineTest.out | 3 ++ Tests/manifest | 7 +++ 9 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 Programs/testPrograms/diskLineTest.asm create mode 100644 Tests/expected/diskLineTest.out diff --git a/Programs/testPrograms/diskLineTest.asm b/Programs/testPrograms/diskLineTest.asm new file mode 100644 index 0000000..8e67b8a --- /dev/null +++ b/Programs/testPrograms/diskLineTest.asm @@ -0,0 +1,69 @@ +; diskLineTest.asm +; A device's line comes down when its status port is read. +; Written by Anachronaut +; +; ---- The idiom with the race in it ---- +; +; This is the shape the manual gives for waiting on a device, and it has a hole. It reads the +; status, branches out if the device has already finished, and only WAITs otherwise. On a disk +; fast enough to finish before the first look - which is every disk here - the WAIT is never +; reached, and WAIT was the only thing that took the line down for a program with no handler. +; +; The line then stood for the rest of the machine's life. Nothing else was going to answer it: +; the program is masked and has nowhere to dispatch to. So the next program to set the +; Interrupt Flag was interrupted on behalf of a read that finished before it was loaded, and +; faulted on the instruction after the SIF. +; +; That is not a story about the disk. It is what happens to any program CosmOS loads, because +; the boot chain reads the disk to load it. Playing Examples/tune.asm through Once is how it +; was found: the program set its whole patch up and died four bytes before its first note. +; +; So reading the status takes the line down, the same way taking the byte takes the console's +; down. This program is the check on that: read the disk without ever waiting, then let +; interrupts in with no handler installed anywhere. If a line were standing, SIF would find it +; and there would be nothing to catch it. + +#Program + +start: + ; Block 0, read. + RSTA + OUTA 0x20 + OUTA 0x21 + INIA 0x01 + OUTA 0x22 + +waitDisk: + INA 0x23 + INIB 0x01 + AND + BRQ readDone ; Already finished, so the WAIT below never runs. + WAIT + BRI waitDisk + +readDone: + ; No handler is installed for anything, and none is installed below either. This is the + ; instruction the fault used to land on. + SIF + + INIA 0d110 + OUTA 0x00 + INIA 0d111 + OUTA 0x00 + INIA 0d32 + OUTA 0x00 + INIA 0d108 + OUTA 0x00 + INIA 0d105 + OUTA 0x00 + INIA 0d110 + OUTA 0x00 + INIA 0d101 + OUTA 0x00 + INIA 0d10 + OUTA 0x00 + HALT + +#Vectors + + Boot start diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 08cc227..7160acc 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -709,6 +709,10 @@ 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 @@ -1156,7 +1160,24 @@ uint8_t InputHandler(uint8_t Address) { break; case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8); case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF); - case DISK_STATUS: return diskStatus; + 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); diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index 69e9179..c55a19e 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -358,6 +358,12 @@ void raiseInterrupt(uint8_t port); void clearInterrupt(uint8_t port); +// Every line down at once, for a reset. The vector table is cleared when the machine starts +// over because a handler left behind would aim an interrupt into a program that is no longer +// running; a LINE left behind does exactly the same thing, and arrives at a program that +// never asked the device for anything. +void clearAllInterrupts(void); + // The lowest numbered port with its line up, or -1 if none of them are. int nextPendingInterrupt(void); diff --git a/Source/Emulator/machine.c b/Source/Emulator/machine.c index 5cd4fee..8227049 100644 --- a/Source/Emulator/machine.c +++ b/Source/Emulator/machine.c @@ -137,6 +137,14 @@ static int machineRestart(Machine *m) { soundReset(); consoleHome(); consoleResetInput(); + // ---- And every line down ---- + // + // The same reasoning that clears the vector table. A handler left behind would aim an + // interrupt into a program that is no longer running; a line left behind arrives at one + // that never asked the device for anything. The devices reset above take their own down, + // and this is the rest of them - the disk in particular, which is not unplugged by a + // reset and keeps whatever it was doing. + clearAllInterrupts(); initializeCPU(&m->cpu, Program, Data); // A machine that had stopped is running again, which is the entire point of asking from // outside: the interesting time to restart something is when it is not going anywhere. diff --git a/Source/Emulator/video.c b/Source/Emulator/video.c index 29f2b40..b9946d2 100644 --- a/Source/Emulator/video.c +++ b/Source/Emulator/video.c @@ -223,7 +223,12 @@ uint8_t videoRead(uint8_t port) { // Looking is what answers it. A frame that has been noticed is not still // waiting to be, and a program polling in a loop would otherwise see the first // frame for ever. + // + // The line goes with the flag, and for the stronger reason: a program that polls + // this port is not going to be the one that answers an interrupt, so a line left + // standing here is one nothing will ever take down. frameWaiting = 0; + clearInterrupt(PORT_VIDEO); return status; } case VIDEO_CONTROL: diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index c53a85e..9d27b81 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -204,6 +204,9 @@ waitForDisk: ready: ``` +Notice that on a device quick enough to finish before the first look, the `WAIT` in that loop +never runs at all. That is fine, and it is why the next section exists. + Test the device, then wait. If the device finishes in the gap between the two, its line is standing when WAIT runs and the wait is skipped rather than slept through. @@ -390,6 +393,40 @@ One last thing. An interrupt arriving while the Stack Pointer is somewhere unusu # When Something Else Wants Attention +## Answering A Line: + +A device raises its line when it has something to say, and something has to take it down +again. There are three things that do, and between them they cover every way a program can +find out that a device is finished. + +**Being interrupted takes it down.** The dispatch does it, before the handler runs, which is +why a handler does not have to and why a handler that forgets does not spin. + +**Being woken from `WAIT` with the Interrupt Flag down takes it down**, because nobody else is +going to. A masked program has nowhere to dispatch to, and a line left standing would be found +by the next `WAIT`, and the one after that, and the program would spin exactly as it did +before while appearing to sleep. + +**And reading the port that answers the device takes it down.** For the console that is the +data port, because taking the byte is what answers the console. For the disk and the screen it +is the status port: the operation finished, and whether it worked is what Status is for. + +That third one is the one to have in mind, because without it the loop above has a hole in it. +A program that polls, finds the device already done and never reaches its `WAIT` has used none +of the first two. The line stands - and it stands for the rest of the machine's life, because +nothing is ever going to come along and answer it. + +**What that costs is not paid by the program that leaves it.** That program never set the +Interrupt Flag; it was masked throughout. The bill arrives later, at whoever does. The boot +chain reads the disk to load a program, leaves the line up, and hands over - and the loaded +program is interrupted on behalf of a read that finished before it existed, through a vector +table that has no entry for a device it never touched. It faults on the instruction after its +`SIF`. `Programs/Examples/tune.asm` is how this was found: run through `Once`, it set up its +whole sound and then died four bytes before playing a note. + +So: a status read is an acknowledgement, and a program that wants to be interrupted by a +device should not poll it. + ## Interrupts: An interrupt is an involuntary transfer of control. A subroutine call is agreed to by the code that makes it, so CALL can leave Q and Data Pointer 3 alone and let a subroutine pass results back through them. An interrupt arrives in code that has never heard of it, where Q and DP3 are ordinary working registers, so it saves everything: diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index ae7db99..f9fa5f7 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 170 tests, of which 108 run, 35 +everything it printed against a file in `Tests/expected`. 171 tests, of which 109 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/diskLineTest.out b/Tests/expected/diskLineTest.out new file mode 100644 index 0000000..b2f25ca --- /dev/null +++ b/Tests/expected/diskLineTest.out @@ -0,0 +1,3 @@ +no line +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 64fc7c6..9f7f64e 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -750,6 +750,13 @@ runOffTest | testPrograms/runOffTest.asm | run | - # that never woke would hang, and a hang is what this catches. Whether it slept or spun is # a question about cycle counts, which settle() strips, so Tests/terminal.sh asks that one. waitTest | testPrograms/waitTest.asm | run | - | 5000000 | disks/sbfs.img@10000 +# The other half of what WAIT is for, and the half that was missing. waitTest checks that a +# masked program which SLEEPS on a device is woken and does not leave the line up; this checks +# the program that never sleeps at all, because the disk finished before the first look. The +# WAIT is unreachable, so nothing but the status read can take the line down. It then sets the +# Interrupt Flag with no handler installed anywhere: a standing line faults there, which is +# how a program loaded by CosmOS used to die on the instruction after its SIF. +diskLineTest | testPrograms/diskLineTest.asm | run | - | 1000000 | disks/sbfs.img printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | -