// cpu.h // SplitBit CPU Emulator Core // Written by Anachronaut // 10/16/2024 #ifndef CPU_H #define CPU_H #include // 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