From c3188ed6578f61d6c7f62eb0900c9ec508e69128 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Wed, 26 Aug 2026 11:11:25 -0400 Subject: [PATCH] Seventy becomes seventy one: a machine that can wait HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a halted machine does not execute, service devices, or take an interrupt - and that has to stay true, because every test ends with a halt and "halted" is how a program says it has finished. The consequence was that SplitBit had no way to wait at all. Every wait was a spin, and a spin is bus traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles, after read-ahead had already hidden three quarters of the latency. WAIT is 0xFE, one byte, no operands, sitting under HALT where the instruction that almost stops the machine belongs. Three decisions in it: - A line already standing means there is nothing to wait for, so WAIT does nothing. That is what makes test-then-wait race-free. - Any line ends the wait, masked or not, so a program can sleep on a device it has no handler for and read its status afterwards. Masking says who answers a request, not whether it happened. - A line that wakes the CPU without being dispatched is taken down by the WAIT. Left standing it would be found by the next WAIT, which would return at once - the program would spin exactly as before while looking as though it slept. Waiting is NOT a Status bit, and that is the trap avoided rather than a gap: Status rides into the interrupt frame and comes back out, so a machine interrupted mid-wait would return from its handler still waiting, and wait again for what it had already been given. An internal field instead. Idle cycles are counted apart from bus cycles and the halt line says so when there are any, which is what makes the difference observable at all - with the line-clearing removed the total moves by ONE cycle, 20,100 against 20,099, and only the idle half changes, halving to 9,976. A test on totals could never have seen it. Tests/terminal.sh asks that question, being the file for things a recorded output cannot see, and fails with the clear removed while "both reads finished" still passes. Three collisions, all found by building it: - 0xFE was the assembler's "not an instruction" sentinel. getOpcode now answers a negative NOT_AN_OPCODE, which is outside the range of every possible answer instead of inside the unused part of it. - 0xFE was also what faultTest and faultResumeTest executed to provoke a fault. They now use 0xFD and say why, because they did not fail when it became an instruction - they HUNG, having started sleeping instead. - Keys.asm has had a label called "wait" for a year, and mnemonics are matched uppercased. What that reported was "Branch without label" at the BRQ thirty lines away. The assembler now refuses a label that is already an instruction, at the label, by name; every instruction added takes a word out of the space of label names, so this will happen again. --- Programs/CosmOS/Apps/Keys.asm | 6 ++- Programs/CosmOS/Assembler/table.asm | 3 +- Programs/CosmOS/Source/cosmos.asm | 3 +- Programs/testPrograms/faultResumeTest.asm | 9 +++- Programs/testPrograms/faultTest.asm | 9 +++- Programs/testPrograms/waitTest.asm | 59 +++++++++++++++++++++++ README.md | 2 +- Source/Assembler/Assm-util.c | 6 +-- Source/Assembler/assembly.c | 10 +++- Source/Assembler/assembly.h | 6 ++- Source/Assembler/secondPass.c | 28 +++++++++++ Source/Emulator/cpu.c | 48 ++++++++++++++++++ Source/Emulator/cpu.h | 13 +++++ Source/Emulator/emulator.c | 28 +++++++++-- SplitBit Programming Manual.md | 46 +++++++++++++++++- Tests/docs.sh | 2 +- Tests/expected/faultTest.out | 2 +- Tests/expected/waitTest.out | 3 ++ Tests/manifest | 7 +++ Tests/run.sh | 3 +- Tests/terminal.sh | 41 ++++++++++++++++ 21 files changed, 311 insertions(+), 23 deletions(-) create mode 100644 Programs/testPrograms/waitTest.asm create mode 100644 Tests/expected/waitTest.out diff --git a/Programs/CosmOS/Apps/Keys.asm b/Programs/CosmOS/Apps/Keys.asm index 672060a..6a886b8 100644 --- a/Programs/CosmOS/Apps/Keys.asm +++ b/Programs/CosmOS/Apps/Keys.asm @@ -39,14 +39,16 @@ start: OUTA 0x02 SIF -wait: +; Called spin rather than wait because WAIT is an instruction now, and a label may not +; be one. Which is the joke of it: this loop is exactly what WAIT exists to replace. +spin: ; This loop is the point. It never touches the console, so every character that appears ; below was put there by something that interrupted it. SETD.3 Stopping LDA.3 RSTB OR - BRQ wait + BRQ spin CALL newLine RSTA diff --git a/Programs/CosmOS/Assembler/table.asm b/Programs/CosmOS/Assembler/table.asm index c95791c..0e55995 100644 --- a/Programs/CosmOS/Assembler/table.asm +++ b/Programs/CosmOS/Assembler/table.asm @@ -37,7 +37,7 @@ AsmShapeSelectors: 0d0 0d0 0d0 0d1 0d1 0d1 0d2 AsmInstructionCount: - 0d70 + 0d71 AsmInstructions: 0x00 0d0 "ADD " @@ -109,4 +109,5 @@ AsmInstructions: 0xE0 0d2 "INA " 0xE1 0d2 "INB " 0xF0 0d0 "NOP " + 0xFE 0d0 "WAIT" 0xFF 0d0 "HALT" diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index dbb79f8..c9dc41a 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -3753,7 +3753,7 @@ ShapeLength: 0d1 0d3 0d2 0d2 0d3 0d4 0d3 InstructionCount: - 0d70 + 0d71 ; ---- The instruction table ---- ; @@ -3830,6 +3830,7 @@ Instructions: 0xE0 0d2 "INA " 0xE1 0d2 "INB " 0xF0 0d0 "NOP " + 0xFE 0d0 "WAIT" 0xFF 0d0 "HALT" DumpBytes: #Reserve 0d16 diff --git a/Programs/testPrograms/faultResumeTest.asm b/Programs/testPrograms/faultResumeTest.asm index 795f972..040e659 100644 --- a/Programs/testPrograms/faultResumeTest.asm +++ b/Programs/testPrograms/faultResumeTest.asm @@ -5,7 +5,12 @@ ; the same byte again, which is why this handler moves the saved address on by one ; first. MVSD is what lets it reach the frame at all. ; -; 0xFE is not an instruction. Writing it as a literal is the only way past the +; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for +; a long time, and then 0xFE became WAIT - so the test stopped faulting and started +; SLEEPING. It hung instead of failing, which is the worst way for a test to notice +; that the thing it was testing had moved. +; +; 0xFD is not an instruction. Writing it as a literal is the only way past the ; assembler, which is what makes a program containing one buildable. ; ; Correct output is: @@ -20,7 +25,7 @@ start: INIA 0x0A OUTA 0x00 - 0xFE ; Not an instruction. The handler steps over this. + 0xFD ; Not an instruction. The handler steps over this. INIA 0d75 ; 'K'. Reached only because the handler moved the address on. OUTA 0x00 diff --git a/Programs/testPrograms/faultTest.asm b/Programs/testPrograms/faultTest.asm index 03c11e0..e9e9f88 100644 --- a/Programs/testPrograms/faultTest.asm +++ b/Programs/testPrograms/faultTest.asm @@ -1,6 +1,11 @@ ; Tests that the CPU stops when it meets a byte it cannot decode. ; -; 0xFE is not an instruction. Placing it in the Program Segment as a literal gets +; CHOSEN AWAY FROM HALT, because the bytes next to it get used. This said 0xFE for +; a long time, and then 0xFE became WAIT - so the test stopped faulting and started +; SLEEPING. It hung instead of failing, which is the worst way for a test to notice +; that the thing it was testing had moved. +; +; 0xFD is not an instruction. Placing it in the Program Segment as a literal gets ; it past the assembler, which is the only way to build a program containing one. ; ; The CPU should raise the Fault Flag, halt, and leave the Program Counter pointing @@ -11,5 +16,5 @@ start: INIA 0d65 ; Something harmless first, so the fault is not at address zero. - 0xFE ; Not an instruction. + 0xFD ; Not an instruction. HALT ; Never reached. diff --git a/Programs/testPrograms/waitTest.asm b/Programs/testPrograms/waitTest.asm new file mode 100644 index 0000000..619d527 --- /dev/null +++ b/Programs/testPrograms/waitTest.asm @@ -0,0 +1,59 @@ +; waitTest.asm +; WAIT, on a disk slow enough to be worth waiting for. +; +; Two reads, waited for rather than spun on, with INTERRUPTS MASKED and no handler +; installed anywhere. That is the shape this instruction exists for: a program sleeps on a +; device it has no handler for and reads the device's status when it wakes. +; +; THE SECOND READ IS THE TEST. A line that wakes the CPU without being dispatched has to +; be taken down by the WAIT itself; left standing, it is found by the next WAIT, which +; returns at once, and the program spins for the whole of the second read while looking +; exactly as though it slept. Both reads print, so a hang fails here - but a program that +; spun would print the same two characters, and what tells them apart is the emulator's +; count of idle cycles against bus cycles. Tests/terminal.sh is where that is checked, +; because settle() strips cycle counts out of every recorded output including this one. +; +; Written by Anachronaut + +#Program +start: + CIF + INIA 0d3 + OUTA 0xE3 + INIA 0x20 + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 + RSTA + OUTA 0x20 + RSTA + OUTA 0x21 + INIA 0x01 + OUTA 0x22 + CALL waitDisk + INIA 0x31 ; "1" + OUTA 0x00 + RSTA + OUTA 0x20 + INIA 0x01 + OUTA 0x21 ; Block 1, a different one. + INIA 0x01 + OUTA 0x22 + CALL waitDisk + INIA 0x32 ; "2" + OUTA 0x00 + INIA 0x0A + OUTA 0x00 + HALT + +waitDisk: + INA 0x23 + INIB 0x01 + AND + BRQ waitDone + WAIT + BRI waitDisk +waitDone: + RET +#Vectors + Boot start diff --git a/README.md b/README.md index 0e6e69f..d16091e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ wrote Asm.sbx: program 7533, data 4099, labels 555 ## The Machine: - **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists. -- **Its own instruction set**, 70 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself. +- **Its own instruction set**, 71 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself. - **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save. - **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told. - **A memory controller** that reads and writes Program Memory, moves blocks between banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program. diff --git a/Source/Assembler/Assm-util.c b/Source/Assembler/Assm-util.c index 921dde8..936c7b1 100644 --- a/Source/Assembler/Assm-util.c +++ b/Source/Assembler/Assm-util.c @@ -122,11 +122,11 @@ int checkIfInstruction(intermediateElement *currentElement) { dot = strchr(dot, '.'); } - uint8_t opcode = getOpcode(token); - if (opcode == 0xFE) { - // It's not an instruction. + int found = getOpcode(token); + if (found == NOT_AN_OPCODE) { return 0; } + uint8_t opcode = (uint8_t)found; int selectorsWanted = dataPointerOperands(opcode); if (selectorsGiven > selectorsWanted) { diff --git a/Source/Assembler/assembly.c b/Source/Assembler/assembly.c index c462c7d..ba0b7a5 100644 --- a/Source/Assembler/assembly.c +++ b/Source/Assembler/assembly.c @@ -92,6 +92,7 @@ Instruction instruction_set[] = { {0xE1, "INB"}, // Special Operations: {0xF0, "NOP"}, + {0xFE, "WAIT"}, {0xFF, "HALT"} }; @@ -139,13 +140,18 @@ int dataPointerOperands(uint8_t opcode) { } } -uint8_t getOpcode(char* mnemonic) { +int getOpcode(char* mnemonic) { for (int i = 0; i < num_instructions; i++) { if (strcmp(instruction_set[i].mnemonic, mnemonic) == 0) { return instruction_set[i].opcode; } } - return 0xFE; // FE is an unused instruction, we'll use it to indicate an error. + // NOT_AN_OPCODE, and it is negative on purpose. This used to answer 0xFE on the + // grounds that 0xFE was unused - which was true until WAIT was given that opcode, at + // which point the assembler would have read WAIT as a word it did not recognise. A + // sentinel picked from the unused half of a range stops being a sentinel the moment + // somebody uses the range, so this one is outside the range altogether. + return NOT_AN_OPCODE; } diff --git a/Source/Assembler/assembly.h b/Source/Assembler/assembly.h index 10f99f4..50141de 100644 --- a/Source/Assembler/assembly.h +++ b/Source/Assembler/assembly.h @@ -119,7 +119,11 @@ const char* getMnemonic(uint8_t opcode); -uint8_t getOpcode(char* mnemonic); +// The opcode a mnemonic assembles to, or NOT_AN_OPCODE if the word is not one. The +// return is an int rather than a byte so that the answer "no" cannot be confused with any +// of the 256 answers "yes" - see the note in getOpcode. +#define NOT_AN_OPCODE (-1) +int getOpcode(char* mnemonic); // How many Data Pointer selector bytes follow the given opcode. Never more than two. #define MAX_DATA_POINTER_OPERANDS 2 diff --git a/Source/Assembler/secondPass.c b/Source/Assembler/secondPass.c index 38a124a..5e2147b 100644 --- a/Source/Assembler/secondPass.c +++ b/Source/Assembler/secondPass.c @@ -81,6 +81,34 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName, cleanedLabel[len - 1] = '\0'; // Remove the colon } + // A NAME THAT IS ALREADY AN INSTRUCTION IS REFUSED, and the error is here rather + // than at the branch that could not find it. Mnemonics are matched with the token + // uppercased, so a label called "wait" and the instruction WAIT are the same word + // - and when WAIT was added, Keys.asm had been using that label for a year. What + // it reported was "Branch without label" at the BRQ, thirty lines from the cause + // and naming nothing that had changed. + // + // This will happen again. Every instruction added takes a word out of the space + // of label names, so the check belongs where the name is claimed. + { + char upper[8]; + int n = 0; + for (; cleanedLabel[n] != '\0' && n < (int)sizeof(upper) - 1; n++) { + upper[n] = (char)toupper((unsigned char)cleanedLabel[n]); + } + upper[n] = '\0'; + // Only a name short enough to BE a mnemonic can collide with one, and the + // longest is four characters. A longer name is truncated by the loop above + // and would not match anything, which is the right answer. + if (cleanedLabel[n] == '\0' && getOpcode(upper) != NOT_AN_OPCODE) { + fprintf(stderr, RED "Error: \"%s\" is an instruction, so it cannot also be" + " a label.\n" RESET, cleanedLabel); + printf("File: %s at line %d.\n", fileName, lineNumber); + free(cleanedLabel); + exit(1); + } + } + // A name may only be defined once. Without this check a reference quietly // resolves to whichever definition came first, so a typo or a name that two // libraries both happen to use is very hard to track down. diff --git a/Source/Emulator/cpu.c b/Source/Emulator/cpu.c index d8f1262..e0188b1 100644 --- a/Source/Emulator/cpu.c +++ b/Source/Emulator/cpu.c @@ -143,6 +143,9 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor cpu->StackPointer = 0xFFFF; cpu->Program = programMemory; cpu->Data = dataMemory; + cpu->busCycles = 0; + cpu->idleCycles = 0; + cpu->Waiting = 0; cpu->Fault = FAULT_NONE; cpu->FaultVector = 0; } @@ -759,6 +762,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) { case 0xF0: // NOP - Do nothing. break; + case 0xFE: + // WAIT - Stop fetching until a device asks for attention. + // + // NOT A HALT. The machine is still clocked and devices still run; what stops + // is the CPU's use of the bus. HALT is how a program says it has finished and + // must stay that way, so this is a separate instruction rather than a gentler + // HALT - and its state is a field of its own rather than a Status bit, for + // the reason cpu.h gives. + // + // A LINE ALREADY UP MEANS THERE IS NOTHING TO WAIT FOR, and that is what + // makes the ordinary idiom race-free: a program tests its device, finds it + // busy, and waits. If the device finished in between, the line is standing + // and this does nothing at all rather than sleeping through the answer. + if (nextPendingInterrupt() < 0) { + cpu->Waiting = 1; + } + break; case 0xFF: // HALT - Set the Halt Bit of the Status Register. cpu->Status |= STATUS_HALT; @@ -777,6 +797,34 @@ void stepCPU(CPURegisters *cpu) { // willing to be interrupted about it. Masking decides when a request is answered, // not whether the outside world is allowed to have happened. serviceDevices(); + // ---- Stopped in a WAIT ---- + // + // Nothing is fetched and nothing is executed. A clock still passes, because a + // device that takes time has to be able to reach the end of it, and it is charged + // to idle rather than to the bus: the CPU is not using memory. + // + // A LINE OF ANY KIND ENDS THE WAIT, masked or not. Masking says who answers a + // request, not whether it happened - so a program can sleep on a device it has no + // handler for and simply read its status afterwards, which is the whole reason + // this is worth having and is what the filesystem does with it. + if (cpu->Waiting) { + if (nextPendingInterrupt() < 0) { + cpu->idleCycles++; + return; + } + cpu->Waiting = 0; + // Woken while masked, so nobody is going to answer this line and take it + // down. IT HAS TO BE TAKEN DOWN HERE. Left standing it would be found by the + // next WAIT, which would return at once, and by the one after that - the + // program would spin exactly as it did before while appearing to sleep. + // + // Unmasked, the dispatch below takes it down instead, and a line with no + // handler faults there the way it always has. Waiting changes what the CPU + // does between instructions; it does not change interrupt policy. + if (!(cpu->Status & STATUS_INTERRUPT)) { + clearInterrupt((uint8_t)nextPendingInterrupt()); + } + } // 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. diff --git a/Source/Emulator/cpu.h b/Source/Emulator/cpu.h index f4671ac..a3791d7 100644 --- a/Source/Emulator/cpu.h +++ b/Source/Emulator/cpu.h @@ -75,6 +75,19 @@ typedef struct { // cost the same, and that a CALL moving ten bytes of Stack cost what a branch costs, // which is not true of any machine anybody could build. unsigned long busCycles; + // ---- And what it has cost while doing nothing ---- + // + // Clocks spent inside WAIT, where the CPU is stopped and the bus is idle. They are + // counted because time still has to pass - a device that takes a while has to be able + // to finish - and they are counted SEPARATELY because they are not the same thing as + // work. A machine waiting on a disk is not using memory, and charging it as though it + // were is exactly the sort of dishonest number the bus count was built to replace. + unsigned long idleCycles; + // Whether the CPU is stopped in a WAIT, which is not a Status bit and must not become + // one: the Status register rides into the interrupt frame and comes back out of it, so + // a machine interrupted while waiting would return from the handler still waiting, and + // wait again for the thing it had already been given. + uint8_t Waiting; // Set alongside the Fault Flag, and read only by whatever reports the stop. uint8_t Fault; // A FaultCause. uint8_t FaultVector; // Which vector was empty, when Fault is FAULT_NO_HANDLER. diff --git a/Source/Emulator/emulator.c b/Source/Emulator/emulator.c index 9d9b382..2313120 100644 --- a/Source/Emulator/emulator.c +++ b/Source/Emulator/emulator.c @@ -65,6 +65,22 @@ char *programFile = NULL; // Memory Banks: uint8_t Program[0x10000], Data[0x10000]; +// How the run is reported. The idle half is mentioned only when there is one, so that +// every program written before WAIT existed prints exactly the line it always did. +// +// THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle +// cycle is the machine stopped in a WAIT while a device catches up. Added together they +// are elapsed time, which is what a cycle limit measures; told apart they say whether a +// program was working or waiting. +static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) { + if (cpu->idleCycles > 0) { + printf("Execution halted after %lu cycles, %lu of them waiting.\n", + cycleCount, cpu->idleCycles); + } else { + printf("Execution halted after %lu cycles.\n", cycleCount); + } +} + int main (int argc, char *argv[]) { EmulatorOptions options; uint8_t result = parseOptions(argc, argv, &options); @@ -133,9 +149,13 @@ int main (int argc, char *argv[]) { // gone rather than after so many steps. In debug mode the budget is one, and any // instruction costs at least the fetch of its own opcode, so one step still runs. for (long spent = 0; spent < cycles; ) { - unsigned long before = cpu.busCycles; + // Both kinds of cycle, because both are time passing. A step that waits + // spends no bus at all, and a budget measured only in bus cycles would never + // be spent - the machine would sit inside one batch forever and the device it + // was waiting for would never be given a moment to finish. + unsigned long before = cpu.busCycles + cpu.idleCycles; stepCPU(&cpu); - unsigned long took = cpu.busCycles - before; + unsigned long took = (cpu.busCycles + cpu.idleCycles) - before; spent += (long)took; cycleCount += took; // Time has passed, so anything waiting on it may be finished. @@ -159,7 +179,7 @@ int main (int argc, char *argv[]) { printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount); } else if (cpu.Status & STATUS_FAULT) { // The Program Counter is still pointing at whatever the CPU could not get past. - printf("Execution halted after %lu cycles.\n", cycleCount); + reportCycles(&cpu, cycleCount); if (cpu.Fault == FAULT_NO_HANDLER) { fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n", cpu.FaultVector, cpu.ProgramCounter); @@ -175,7 +195,7 @@ int main (int argc, char *argv[]) { } return 1; } else { - printf("Execution halted after %lu cycles.\n", cycleCount); + reportCycles(&cpu, cycleCount); } return 0; } diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index c2111d9..2acdbb0 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -46,6 +46,7 @@ It has ten registers: - Bit 1 is the Fault Flag. It is set when the CPU cannot get past something and no handler was installed to deal with it: a byte that is not an instruction, or a dispatch through an empty vector. See Faults. - Bit 2 is the Interrupt Flag. It is set by SIF and cleared by CIF. While it is set the CPU answers devices asking for attention; while it is clear they wait. Arriving at a handler clears it, and RETI restores it along with the rest of the Status register. See Hardware Interrupts. - Bit 7 is the Halt Flag. It is set by the HALT instruction, and by a fault. + - There is no Wait Flag. The CPU stopped in a WAIT is stopped in a way no program can see, precisely because this register is saved and restored across an interrupt and a wait must not be. See WAIT under Special Operations. Each condition has a branch both ways round, so a loop that carries on while something is not zero is one instruction rather than a branch over an unconditional one. Before these existed a quarter of every conditional branch in the corpus was written backwards and padded out, and each of those needed a label invented only to be jumped past. @@ -165,12 +166,55 @@ LDD and STD are how a program follows an address it has stored, rather than one | E0 | INA | 2 | Writes the value of an Input to A. The input port is specified by the next byte of Program Memory. | | E1 | INB | 2 | Writes the value of an Input to B. The input port is specified by the next byte of Program Memory. | -### Special Operations: 2 Instructions +### Special Operations: 3 Instructions | Hex Code | Mnemonic | Bytes | Description | | -- | ---- | -- | -- | | F0 | NOP | 1 | Perform no Operation, increment the Program Counter. | +| FE | WAIT | 1 | Stops fetching until a device asks for attention. | | FF | HALT | 1 | Stops CPU Execution. | +WAIT is not a gentler HALT and the two are not interchangeable. HALT stops the machine and +is how a program says it has finished; WAIT stops only the CPU's use of the bus. The clock +runs, devices run, and the moment any of them raises a line the CPU carries on with the +instruction after the WAIT. + +**A line that is already up means there is nothing to wait for**, and WAIT does nothing at +all. That is what makes the ordinary shape of it safe: + +```asm +waitForDisk: + INA 0x23 ; The status port. + INIB 0x01 + AND + BRQ ready ; Not busy, so there is nothing to wait for. + WAIT + BRI waitForDisk +ready: +``` + +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. + +**Any line ends the wait, whether or not the Interrupt Flag is set.** Masking decides who +answers a request, not whether it happened - so a program can sleep on a device it has no +handler for at all and simply read the device's status afterwards, which is what the loop +above does. If the flag *is* set, everything happens as it always does: the handler runs +and RETI comes back to the instruction after the WAIT. + +A line that wakes the CPU without being dispatched to a handler is taken down by the WAIT +itself. Otherwise it would still be standing at the next WAIT, which would return at once, +and at the one after that - the program would spin exactly as it would have without the +instruction, while looking as though it slept. + +**Waiting is not a bit in the Status register**, and that is deliberate rather than an +oversight. Status is pushed into the interrupt frame and restored by RETI, so a machine +that took an interrupt while waiting would come back from the handler still waiting, and +wait again for the thing it had already been given. + +Nothing wakes a WAIT that no device will ever interrupt. That is a program saying it has +nothing to do until something happens, and if nothing can happen it waits for ever, the +same way an unconditional branch to itself loops for ever. + # Making It Do Something ## Making It Print Something: diff --git a/Tests/docs.sh b/Tests/docs.sh index efa74ab..894089b 100755 --- a/Tests/docs.sh +++ b/Tests/docs.sh @@ -138,7 +138,7 @@ words = {12: "Twelve", 13: "Thirteen", 14: "Fourteen", 15: "Fifteen", 16: "Sixte 17: "Seventeen", 18: "Eighteen", 19: "Nineteen", 20: "Twenty", 21: "Twenty one", 22: "Twenty two", 23: "Twenty three", 24: "Twenty four", 25: "Twenty five", 26: "Twenty six"} -selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("uint8_t getOpcode")] +selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("int getOpcode")] taking = len(re.findall(r'^\s*case 0x[0-9A-Fa-f]{2}:', selectors, re.M)) said = re.search(r'^([A-Z][a-z]+(?: [a-z]+)?) instructions work through a Data Pointer\.', pm, re.M) diff --git a/Tests/expected/faultTest.out b/Tests/expected/faultTest.out index d2b436d..818b06e 100644 --- a/Tests/expected/faultTest.out +++ b/Tests/expected/faultTest.out @@ -1,3 +1,3 @@ -Fault: 0xFE at Program Address 0x0002 is not an instruction. +Fault: 0xFD at Program Address 0x0002 is not an instruction. Execution halted. [exit 1] diff --git a/Tests/expected/waitTest.out b/Tests/expected/waitTest.out new file mode 100644 index 0000000..a3d7529 --- /dev/null +++ b/Tests/expected/waitTest.out @@ -0,0 +1,3 @@ +12 +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index c9ecf6c..75320e2 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -560,6 +560,13 @@ stringKeyword | testPrograms/stringKeyword.asm | run | - # version of RCAL that quietly did what CALL does would pass a test that only checked it # returned to the right place. rawCallAndOffsets | testPrograms/rawCallAndOffsets.asm | run | - | - + +# WAIT, the seventy first instruction, on a disk given ten thousand cycles of latency. Two +# reads with interrupts masked and no handler anywhere: the machine sleeps through both and +# reads the status port when it wakes. What is recorded here is that it FINISHES - a WAIT +# 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 printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | - diff --git a/Tests/run.sh b/Tests/run.sh index 1daacb9..188261b 100755 --- a/Tests/run.sh +++ b/Tests/run.sh @@ -103,7 +103,8 @@ settle() { # exists to remove was sitting inside a line rather than at the head of one, and # survived. replCalculator is the one that does that, and it was the only test to churn # when the machine started charging for memory instead of counting instructions. - sed -i -E 's/Execution halted after [0-9]+ cycles\./Execution halted./; + sed -i -E 's/Execution halted after [0-9]+ cycles, [0-9]+ of them waiting\./Execution halted./; + s/Execution halted after [0-9]+ cycles\./Execution halted./; s/Execution stopped after [0-9]+ cycles\. \(cycle limit reached\)/Execution stopped. (cycle limit reached)/' "$1" } diff --git a/Tests/terminal.sh b/Tests/terminal.sh index a7e9863..40bce14 100755 --- a/Tests/terminal.sh +++ b/Tests/terminal.sh @@ -60,11 +60,21 @@ ASM "$ROOT/Assembler" "$BUILD/prompt.asm" -o "$BUILD/prompt.bin" >/dev/null 2>&1 || { echo "Could not assemble the terminal test programs."; exit 1; } +# And the one that waits. Assembled from the repository rather than written inline, because +# it is a real test program that run.sh also runs - there it proves the machine wakes up at +# all, and here it proves it was asleep. +"$ROOT/Assembler" "$ROOT/Programs/testPrograms/waitTest.asm" -o "$BUILD/waitTest.bin" \ + >/dev/null 2>&1 || { echo "Could not assemble waitTest."; exit 1; } +"$ROOT/SplitDisk" format "$BUILD/wait.img" 32 1 >/dev/null 2>&1 || { + echo "Could not make the disk waitTest reads."; exit 1; } + python3 - "$ROOT" "$BUILD" <<'PY' import os import pty import select import signal +import re +import subprocess import sys import time @@ -249,6 +259,37 @@ try: except FileNotFoundError: report(False, "suspending and resuming", "could not measure") +# ---- Waiting is not the same as spinning ---- +# +# settle() strips cycle counts out of every recorded output, so no test in run.sh can see +# the difference between a machine that slept through a slow disk and one that spun on it. +# The two print the same characters and take the same elapsed time. What separates them is +# WHICH KIND of cycle went by, and that is only visible here. +# +# waitTest reads two blocks from a disk given ten thousand cycles of latency, with +# interrupts masked and no handler installed. Nearly all of the run should be idle. With +# the line-clearing removed from WAIT the total barely moves - it was 20,100 against +# 20,099 - and the idle count halves, because the second wait finds the first wait's line +# still standing and returns at once. +waitProgram = os.path.join(build, "waitTest.bin") +waitDisk = os.path.join(build, "wait.img") +if os.path.exists(waitProgram) and os.path.exists(waitDisk): + run = subprocess.run([emulator, waitProgram, "--fast", "--cycles", "5000000", + "--disk", waitDisk, "--disk-cycles", "10000"], + capture_output=True, text=True) + got = re.search(r"halted after (\d+) cycles, (\d+) of them waiting", run.stdout) + if not got: + report(False, "a slow disk is waited for", "no idle cycles were reported at all") + else: + total, idle = int(got.group(1)), int(got.group(2)) + report("12" in run.stdout, "both reads finished", "%d cycles" % total) + # Two waits of ten thousand cycles each. Sleeping through both puts idle over + # nine tenths of the run; spinning through either drops it to about half. + report(idle > total * 0.9, "and the machine slept rather than spun", + "%d of %d idle (%.0f%%)" % (idle, total, 100.0 * idle / total)) +else: + report(False, "a slow disk is waited for", "waitTest.bin or the disk is missing") + print() if problems: print("The terminal does not survive everything it should:")