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
+26
View File
@@ -51,6 +51,29 @@ typedef enum {
FAULT_DEVICE_REFUSED // A device refused, and nothing was installed to catch it.
} FaultCause;
// ---- What a CPU is plugged into ----
//
// 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. That is fine for a machine with one processor and wrong
// for a machine with two: A PERIPHERAL CORE'S BUS IS ITS OWN. It sees the devices its own
// device gives it, raises its own interrupt lines, and stalls on its own controller - none
// of which are the host's.
//
// Gathered here rather than threaded through as a bus number, because a bus is a thing a
// device provides, and a device that provides one should hand over the answers rather than
// be looked up by an index somebody has to keep right.
typedef struct {
uint8_t (*out)(uint8_t value, uint8_t port);
uint8_t (*in)(uint8_t port);
// What the memory controller on this bus has just spent moving memory, and what the
// machine spent stopped waiting on a device. Both are taken and cleared.
unsigned long (*takeStall)(void);
unsigned long (*takeIdle)(void);
// The lowest port with its line up, or below zero for none, and putting one down.
int (*nextInterrupt)(void);
void (*clearInterrupt)(uint8_t port);
} Bus;
// The struct containing the CPU registers.
typedef struct {
uint8_t A;
@@ -62,6 +85,9 @@ typedef struct {
uint16_t StackPointer;
uint8_t *Program;
uint8_t *Data;
// Which bus this processor is on. initializeCPU puts the machine's own here, which is
// what every CPU had before there could be more than one.
const Bus *bus;
// ---- What the machine has cost so far ----
//
+18
View File
@@ -653,6 +653,24 @@ int takeResetRequest(void) {
return wanted;
}
// ---- The bus this machine's processor is on ----
//
// Everything a CPU asks of the world outside it, for the world this file is. A peripheral
// core is handed a different one of these by whatever device contains it, which is the whole
// of what a private bus is: not a number to be checked, a different set of answers.
static const Bus theMachinesBus = {
OutputHandler,
InputHandler,
controllerTakeCycles,
takeIdleCycles,
nextPendingInterrupt,
clearInterrupt,
};
const Bus *machineBus(void) {
return &theMachinesBus;
}
void raiseInterrupt(uint8_t port) {
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
}
+4
View File
@@ -330,6 +330,10 @@ void clearInterrupt(uint8_t port);
// The lowest numbered port with its line up, or -1 if none of them are.
int nextPendingInterrupt(void);
// The bus this machine's own processor is on: its devices, its lines, its controller. A
// peripheral core is given a different one by whatever device contains it.
const Bus *machineBus(void);
// ---- Refusing ----
//
// A device can refuse what it was asked to do. Interrupting is a device asking for