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.
This commit is contained in:
Anachronaut
2026-08-26 11:11:25 -04:00
parent 6b41354f8f
commit c3188ed657
21 changed files with 311 additions and 23 deletions
+45 -1
View File
@@ -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: