Files
SplitBit-Emulator/Source/Emulator/cpu.h
T
AnachronautandClaude Opus 5 dae3455da0 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
2026-08-29 15:11:54 -04:00

129 lines
6.2 KiB
C

// cpu.h
// SplitBit CPU Emulator Core
// Written by Anachronaut
// 10/16/2024
#ifndef CPU_H
#define CPU_H
#include <stdint.h>
// How many Data Pointers the CPU has. The instructions that name one take a full
// byte to do it, so the encoding would allow up to 256. The limit here is the size
// of the register file and the cost of saving pointers across a CALL, not the
// instruction format. Must be a power of two, so that the selector can be masked
// down to a valid pointer.
#define DATA_POINTERS 4
// How many Data Pointers survive a CALL. The low numbered pointers are saved and
// restored around a subroutine; the rest are left alone, so a subroutine can use
// one to hand a pointer back to its caller the way Q hands back a byte. This is
// deliberately independent of DATA_POINTERS: adding more pointers should not make
// every CALL more expensive.
#define PRESERVED_DATA_POINTERS 3
#if PRESERVED_DATA_POINTERS > DATA_POINTERS
#error "Cannot preserve more Data Pointers than the CPU has."
#endif
// The bits of the Status register that mean something.
#define STATUS_CARRY 0x01 // An arithmetic result carried out of, or borrowed into, a byte.
#define STATUS_FAULT 0x02 // The CPU met a byte it could not decode, and stopped.
#define STATUS_INTERRUPT 0x04 // Hardware interrupts are enabled. Nothing reads this yet.
#define STATUS_HALT 0x80 // Execution has stopped, either from HALT or from a fault.
// What an interrupt puts on the Stack: the resume address, every Data Pointer, and
// every register the CPU has. The CALL frame leaves Q and DP3 alone, but that is a
// convention between a caller and the subroutine it called. An interrupt arrives in
// code that never agreed to give anything up, so it saves the lot.
#define INTERRUPT_FRAME_BYTES (2 + DATA_POINTERS * 2 + 4)
// Why the CPU stopped, when the Fault Flag is set. This is not something a program can
// read, and it is deliberately not a register: when a handler is installed, the vector
// it arrived through already says what happened, which is why the ISA has no fault
// cause. This exists for the case where nothing is installed and the machine is dead,
// so that whatever examines the wreckage can say something better than "it stopped".
typedef enum {
FAULT_NONE = 0,
FAULT_BAD_OPCODE, // A byte that does not decode to an instruction.
FAULT_NO_HANDLER, // Dispatched through a software vector with nothing in it.
FAULT_NO_DEVICE_HANDLER, // A device interrupted, and its vector was empty.
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;
uint8_t B;
uint8_t Q;
uint8_t Status;
uint16_t ProgramCounter;
uint16_t DataPointer[DATA_POINTERS];
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 ----
//
// ONE BUS ACCESS IS ONE CYCLE, and every access goes through it: fetching an opcode,
// fetching the bytes after it, reading or writing Data Memory, pushing or popping the
// Stack, and reaching a device port. Nothing is overlapped - no fetching the next
// instruction while this one finishes - because that is a thing hardware may or may
// not do and this is the model to design against before deciding.
//
// It replaces counting instructions. Counting instructions said an RSTA and a SETD
// 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.
} CPURegisters;
uint8_t executeOperation(uint8_t instruction, CPURegisters *cpu);
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory);
void stepCPU(CPURegisters *cpu);
#endif // CPU_H