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
+48
View File
@@ -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.