// emulator.c // SplitBit Emulator // Small 8-Bit Harvard Architecture CPU // Written by Anachronaut // 10/15/2024 #include #include #include #include "cpu.h" #include "controller.h" #include "io.h" #include "utility.h" #include #include #include // nanoseconds per second #define NS_PER_SEC 1000000000LL #define CYCLE_RATE 1000000 typedef struct { long long cycles_per_sec; // e.g. 1000000 for 1 MHz long long accumulator_ns; // unspent nanoseconds struct timespec prev; } CycleTimer; static inline long long timespec_diff_ns(struct timespec a, struct timespec b) { return (a.tv_sec - b.tv_sec) * NS_PER_SEC + (a.tv_nsec - b.tv_nsec); } void cycle_timer_init(CycleTimer *t, long long cycles_per_sec) { t->cycles_per_sec = cycles_per_sec; t->accumulator_ns = 0; clock_gettime(CLOCK_MONOTONIC, &t->prev); } // Call once per host frame. Returns how many SplitBit cycles to execute. int cycle_timer_tick(CycleTimer *t) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); long long elapsed = timespec_diff_ns(now, t->prev); t->prev = now; // optional: clamp to avoid spiral-of-death on hitches if (elapsed > NS_PER_SEC / 10) elapsed = NS_PER_SEC / 10; t->accumulator_ns += elapsed; long long period_ns = NS_PER_SEC / t->cycles_per_sec; int cycles = (int)(t->accumulator_ns / period_ns); t->accumulator_ns %= period_ns; return cycles; } // How many cycles to run between glances at the wall clock. In fast mode there is // no clock to keep pace with, so run a large batch before looking up. #define FAST_BATCH 65536 unsigned long cycleCount = 0; char *programFile = NULL; // Memory Banks: uint8_t Program[0x10000], Data[0x10000]; // How the run is reported. The idle half is mentioned only when there is one, so that // every program written before WAIT existed prints exactly the line it always did. // // THE TWO ARE NOT THE SAME KIND OF TIME. A bus cycle is the machine using memory; an idle // cycle is the machine stopped in a WAIT while a device catches up. Added together they // are elapsed time, which is what a cycle limit measures; told apart they say whether a // program was working or waiting. static void reportCycles(const CPURegisters *cpu, unsigned long cycleCount) { if (cpu->idleCycles > 0) { printf("Execution halted after %lu cycles, %lu of them waiting.\n", cycleCount, cpu->idleCycles); } else { printf("Execution halted after %lu cycles.\n", cycleCount); } } int main (int argc, char *argv[]) { EmulatorOptions options; uint8_t result = parseOptions(argc, argv, &options); if (result == OPTIONS_HELP) { // The user asked for help and got it, which is not a failure. return 0; } else if (result == OPTIONS_ERROR) { // Bad command line, don't execute. return 1; } if (optind < argc) { programFile = argv[optind]; optind++; } else { fprintf(stderr, "Error: No boot image specified.\n"); printHelp(argv[0]); return 1; } if (optind < argc) { fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]); return 1; } if (loadFile(programFile, Program, Data)) { fprintf(stderr, "Error: Couldn't read file: %s\n", programFile); return 1; } if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) { return 1; } CPURegisters cpu; // The controller has to know where the memories are before anything can reach // them through it. Banks 0 and 1 are those two arrays. initializeController(Program, Data); initializeCPU(&cpu, Program, Data); if(options.debug) { printRegisters(&cpu, Program, Data); } CycleTimer timer; setDiskLatency(options.diskCycles); cycle_timer_init(&timer, CYCLE_RATE); uint8_t limitReached = 0; while (!(cpu.Status & STATUS_HALT) && !limitReached) { if (options.debug) { // Wait before advancing, not after, so that a keypress is what moves the // machine on rather than something that happens once it already has. // Through the console rather than getchar, so that everything reading standard // input reads it the same way and the console's pushback stays the only place // a byte can be sitting. consoleReadByte(); } int cycles; if (options.debug) { // Debug mode advances one instruction per keypress, so the wall clock // has no say in how many cycles to run. cycles = 1; } else if (options.fast) { cycles = FAST_BATCH; } else { cycles = cycle_timer_tick(&timer); } // ---- Spending a budget of cycles, not running a count of instructions ---- // // An instruction costs what it touches, so a batch is finished when the cycles are // gone rather than after so many steps. In debug mode the budget is one, and any // instruction costs at least the fetch of its own opcode, so one step still runs. for (long spent = 0; spent < cycles; ) { // Both kinds of cycle, because both are time passing. A step that waits // spends no bus at all, and a budget measured only in bus cycles would never // be spent - the machine would sit inside one batch forever and the device it // was waiting for would never be given a moment to finish. unsigned long before = cpu.busCycles + cpu.idleCycles; stepCPU(&cpu); unsigned long took = (cpu.busCycles + cpu.idleCycles) - before; spent += (long)took; cycleCount += took; // Time has passed, so anything waiting on it may be finished. deviceTick(cycleCount); if (cpu.Status & STATUS_HALT) { // We've halted. break; } if (options.cycles && cycleCount >= options.cycles) { limitReached = 1; break; } } if (options.debug) { printRegisters(&cpu, Program, Data); printf("Cycle: %lu\n", cycleCount); } } detachDisk(); if (limitReached) { printf("Execution stopped after %lu cycles. (cycle limit reached)\n", cycleCount); } else if (cpu.Status & STATUS_FAULT) { // The Program Counter is still pointing at whatever the CPU could not get past. reportCycles(&cpu, cycleCount); if (cpu.Fault == FAULT_NO_HANDLER) { fprintf(stderr, "Fault: Software vector %u, dispatched from Program Address 0x%04X, has no handler installed.\n", cpu.FaultVector, cpu.ProgramCounter); } else if (cpu.Fault == FAULT_DEVICE_REFUSED) { fprintf(stderr, "Fault: The device on port %u refused the access at Program Address 0x%04X, and nothing is installed to deal with it.\n", cpu.FaultVector, cpu.ProgramCounter); } else if (cpu.Fault == FAULT_NO_DEVICE_HANDLER) { fprintf(stderr, "Fault: The device on port %u interrupted at Program Address 0x%04X, and hardware vector %u has no handler installed.\n", cpu.FaultVector, cpu.ProgramCounter, cpu.FaultVector); } else { fprintf(stderr, "Fault: 0x%02X at Program Address 0x%04X is not an instruction.\n", Program[cpu.ProgramCounter], cpu.ProgramCounter); } return 1; } else { reportCycles(&cpu, cycleCount); } return 0; }