Give the CPU a bus, so that there can be more than one

The first piece of the peripheral core, and it changes no behaviour: the machine still has
exactly one processor, and every one of the 169 tests still passes. What changes is that the
code has stopped assuming so.

FIVE THINGS A CPU ASKS OF THE WORLD OUTSIDE ITSELF, and every one of them was a call to a
function there was exactly one of: the port handlers, what the controller spent moving memory,
what was spent stopped waiting on a device, and the two that work the interrupt lines. Fine
for a machine with one processor and wrong for a machine with two, because A PERIPHERAL CORE'S
BUS IS ITS OWN - it sees the devices its own device gives it, raises its own lines, and stalls
on its own controller, none of which are the host's.

They are gathered into a Bus that the CPU holds a pointer to, rather than threaded through as
a bus number, because a bus is something a device PROVIDES. A device that provides one should
hand over the answers rather than be looked up by an index somebody else has to keep right.
initializeCPU puts the machine's own there, which is what every processor was on when there
could only be one, so nothing at any call site moved.

And shiftRegister is a local now. It always was one in effect - written and read inside a
single instruction and never carried to the next - but it sat at file scope, where a second
processor would have shared it. Two cores taking each other's shift halfway through an SHL is
a poor thing to discover later, and it cost two braces to make impossible.

Still to come on this rung: the interrupt bitmap, which is one file-scope array indexed by
port for the whole machine; a controller each; and the interleaving rule, which has to be
written into the manual as machine behaviour rather than left as something the emulator does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-29 15:11:54 -04:00
co-authored by Claude Opus 5
parent f2e26c1852
commit dae3455da0
4 changed files with 75 additions and 14 deletions
+27 -14
View File
@@ -8,8 +8,6 @@
#include "controller.h"
#include "../Assembler/assembly.h" // For the vector table layout, which both tools share.
uint16_t shiftRegister;
// Reads one entry out of a vector table. Most significant byte first, matching the
// branch instructions and both file formats.
// ---- Every touch of memory, and what it costs ----
@@ -38,20 +36,20 @@ static inline void writeData(CPURegisters *cpu, uint16_t at, uint8_t value) {
// otherwise.
static inline void portOut(CPURegisters *cpu, uint8_t value, uint8_t port) {
cpu->busCycles++;
OutputHandler(value, port);
cpu->bus->out(value, port);
// And whatever memory that made the controller move. The machine waits for it, which
// is the conservative reading: a blit stalls the program that asked for one.
cpu->busCycles += controllerTakeCycles();
cpu->busCycles += cpu->bus->takeStall();
}
static inline uint8_t portIn(CPURegisters *cpu, uint8_t port) {
cpu->busCycles++;
uint8_t value = InputHandler(port);
cpu->busCycles += controllerTakeCycles();
uint8_t value = cpu->bus->in(port);
cpu->busCycles += cpu->bus->takeStall();
// And whatever time went by while the device kept the machine waiting. Idle rather than
// bus, because a machine stopped on a port is not using memory - the same distinction
// WAIT makes, arrived at from the other direction.
cpu->idleCycles += takeIdleCycles();
cpu->idleCycles += cpu->bus->takeIdle();
return value;
}
@@ -131,6 +129,9 @@ static uint8_t answerRefusal(CPURegisters *cpu, uint16_t site) {
}
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
// The machine's own, which is what every processor here was on when there could only be
// one. Anything that wants a processor somewhere else changes this afterwards.
cpu->bus = machineBus();
cpu->A = 0;
cpu->B = 0;
cpu->Q = 0;
@@ -249,17 +250,29 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
break;
case 0x17:
// SHL - Shift AB left.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
//
// A local, and it always was one in effect: written and read inside this one
// instruction and never carried to the next. It sat at file scope until there
// was a second processor to share it with, which is a poor time to find out.
{
uint16_t shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister << 1) | (shiftRegister >> 15);
cpu->A = shiftRegister >> 8;
cpu->B = shiftRegister & 0xFF;
}
break;
case 0x18:
// SHR - Shift AB right.
shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
//
// A local, and it always was one in effect: written and read inside this one
// instruction and never carried to the next. It sat at file scope until there
// was a second processor to share it with, which is a poor time to find out.
{
uint16_t shiftRegister = ((uint16_t)cpu->A << 8) | cpu->B;
shiftRegister = (shiftRegister >> 1) | (shiftRegister << 15);
cpu->A = shiftRegister >> 8;
cpu->B = shiftRegister & 0xFF;
}
break;
//
// 1x - Branch Operations:
@@ -825,7 +838,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// 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) {
if (cpu->bus->nextInterrupt() < 0) {
cpu->Waiting = 1;
}
break;
@@ -858,7 +871,7 @@ void stepCPU(CPURegisters *cpu) {
// 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) {
if (cpu->bus->nextInterrupt() < 0) {
cpu->idleCycles++;
return;
}
@@ -872,7 +885,7 @@ void stepCPU(CPURegisters *cpu) {
// 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());
cpu->bus->clearInterrupt((uint8_t)cpu->bus->nextInterrupt());
}
}
// A device asking for attention is answered between instructions and never
@@ -882,9 +895,9 @@ void stepCPU(CPURegisters *cpu) {
// A line that is up while the Interrupt Flag is clear stays up. Masking holds a
// device off; it does not lose what the device was asking for.
if (cpu->Status & STATUS_INTERRUPT) {
int port = nextPendingInterrupt();
int port = cpu->bus->nextInterrupt();
if (port >= 0) {
clearInterrupt((uint8_t)port);
cpu->bus->clearInterrupt((uint8_t)port);
if (enterInterrupt(cpu, HARDWARE_VECTOR_BASE, (uint8_t)port, cpu->ProgramCounter)) {
// The device asked and nobody was listening. enterInterrupt has
// already stopped the machine; correct the cause, because the empty