Interrupt system implemented, some new programs.

This commit is contained in:
Anachronaut
2026-08-15 00:44:13 -04:00
parent 638b68b25c
commit 6d1966d500
79 changed files with 2778 additions and 88 deletions
+45 -1
View File
@@ -101,6 +101,49 @@ static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
return loadSegment(file, Memory, length);
}
// Reads the Vector Segment, which is optional and last. A file that simply ends here
// was written before vectors existed, and an empty table is exactly right for it: every
// entry reads as zero, which means no handler, and the boot vector reading zero means
// the program starts at 0x0000 the way it always did.
//
// Each entry says where in Program Memory the vector sits and where its handler is, so
// installing one is a write straight into the vector table.
static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
int first = fgetc(file);
if (first == EOF) {
return 0;
}
ungetc(first, file);
char found[SEGMENT_MARKER_LENGTH + 1];
if (readMarker(file, "VEC", SEGMENT_MARKER_LENGTH, found)) {
fprintf(stderr, "Error: Expected a \"VEC\" segment here, found \"%s\".\n", found);
return 1;
}
uint32_t length;
if (readNumber(file, SEGMENT_LENGTH_BYTES, "the vector segment length", &length)) {
return 1;
}
if (length % VECTOR_ENTRY_FILE_BYTES != 0) {
fprintf(stderr, "Error: The vector segment is %u bytes, which is not a whole number of vectors.\n", length);
return 1;
}
for (uint32_t i = 0; i < length / VECTOR_ENTRY_FILE_BYTES; i++) {
uint32_t slot, handler;
if (readNumber(file, 2, "a vector address", &slot)
|| readNumber(file, 2, "a handler address", &handler)) {
return 1;
}
if (slot < SOFTWARE_VECTOR_BASE) {
fprintf(stderr, "Error: This binary puts a vector at 0x%04X, which is below the vector table.\n", slot);
return 1;
}
Program[slot] = (handler >> 8) & 0xFF;
Program[(uint16_t)(slot + 1)] = handler & 0xFF;
}
return 0;
}
uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
FILE *file = fopen(path, "rb");
if (file == NULL) {
@@ -111,7 +154,8 @@ uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) {
// here means there is one exit, and so only one place that has to close the file.
uint8_t failed = readFileHeader(file)
|| readSegment(file, "PRG", Program)
|| readSegment(file, "DAT", Data);
|| readSegment(file, "DAT", Data)
|| readVectorSegment(file, Program);
fclose(file);
return failed;
}
+213 -20
View File
@@ -5,15 +5,78 @@
#include "cpu.h"
#include "io.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 the binary format.
static uint16_t readVector(const uint8_t *programMemory, uint16_t base, uint8_t index) {
uint16_t address = base + (uint16_t)index * VECTOR_ENTRY_BYTES;
return ((uint16_t)programMemory[address] << 8) | (uint16_t)programMemory[address + 1];
}
// Builds an interrupt frame and dispatches through a vector. The resume address is the
// address execution should carry on from once the handler returns, and it goes into the
// frame as a real address so that a handler can read it and make sense of it.
//
// Returns 0 if it dispatched. If the vector is empty there is nothing to dispatch to, so
// it raises a fault and returns 1 rather than jumping to the bottom of Program Memory
// and running whatever happens to be there.
//
// Note that a zero entry means "no handler" to everything that dispatches, including the
// two entries the CPU treats as start addresses when it reads them at reset. The
// exemption belongs to that one read, not to the entries themselves.
static uint8_t enterInterrupt(CPURegisters *cpu, uint16_t base, uint8_t index, uint16_t resumeAddress) {
uint16_t handler = readVector(cpu->Program, base, index);
if (handler == 0x0000) {
cpu->Fault = FAULT_NO_HANDLER;
cpu->FaultVector = index;
cpu->Status |= STATUS_FAULT;
cpu->Status |= STATUS_HALT;
return 1;
}
// Order mirrors genericCall exactly: low byte then high byte, lowest numbered Data
// Pointer first, so that anything walking the Stack sees a familiar shape.
cpu->Data[cpu->StackPointer] = resumeAddress & 0xFF;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (resumeAddress >> 8) & 0xFF;
cpu->StackPointer--;
for (int i = 0; i < DATA_POINTERS; i++) {
cpu->Data[cpu->StackPointer] = cpu->DataPointer[i] & 0xFF;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = (cpu->DataPointer[i] >> 8) & 0xFF;
cpu->StackPointer--;
}
cpu->Data[cpu->StackPointer] = cpu->B;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->A;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->Q;
cpu->StackPointer--;
cpu->Data[cpu->StackPointer] = cpu->Status;
cpu->StackPointer--;
// A handler runs with hardware interrupts held off unless it says otherwise, so an
// interrupt cannot arrive inside the handler for another one and grow the Stack
// without bound. The old setting rode into the frame inside the Status register, so
// RETI puts it back without anything having to remember it separately.
cpu->Status &= ~STATUS_INTERRUPT;
// The Program Counter is stepped after every instruction, so land one short of the
// handler and let that step land on its first byte. genericBranch does the same.
cpu->ProgramCounter = handler - 1;
return 0;
}
void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemory) {
cpu->A = 0;
cpu->B = 0;
cpu->Q = 0;
cpu->Status = 0;
cpu->ProgramCounter = 0x0000;
// Execution begins wherever the boot vector points. It is a start address rather
// than a handler, so a zero there is not "nothing installed" but the address
// 0x0000, which is where a program carrying no vector table of its own begins.
// That is what lets everything written before the table existed still run.
cpu->ProgramCounter = readVector(programMemory, SOFTWARE_VECTOR_BASE, VECTOR_BOOT);
// Every Data Pointer starts at the bottom of Data Memory.
for (int i = 0; i < DATA_POINTERS; i++) {
cpu->DataPointer[i] = 0x0000;
@@ -21,6 +84,8 @@ void initializeCPU(CPURegisters *cpu, uint8_t *programMemory, uint8_t *dataMemor
cpu->StackPointer = 0xFFFF;
cpu->Program = programMemory;
cpu->Data = dataMemory;
cpu->Fault = FAULT_NONE;
cpu->FaultVector = 0;
}
void genericBranch(CPURegisters *cpu){
@@ -70,25 +135,29 @@ uint16_t *selectDataPointer(CPURegisters *cpu) {
}
uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// ADD and SUB share this. It is declared here rather than after a case label
// because a label may only be followed by a statement in ISO C, and a
// declaration is not one.
uint16_t result;
switch(Instruction) {
// 0x - Arithmetic and Logic Operations.
case 0x00:
// ADD - A + B + Carry -> Q
uint16_t result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & 0x01);
result = (uint16_t)cpu->A + (uint16_t)cpu->B + (cpu->Status & STATUS_CARRY);
if (result > 255) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->Q = result & 0xFF;
break;
case 0x01:
// SUB - A - B - Carry -> Q
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & 0x01);
result = (uint16_t)cpu->A - (uint16_t)cpu->B - (cpu->Status & STATUS_CARRY);
if (result > 255) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->Q = result & 0xFF;
break;
@@ -162,16 +231,68 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
break;
case 0x14:
// BRC - Do an immediate branch if the Carry Flag is set.
if (cpu->Status & 0x01) {
if (cpu->Status & STATUS_CARRY) {
genericBranch(cpu);
} else {
cpu->ProgramCounter+=2;
}
break;
case 0x15: {
// BRD - Branch to the address held in a Data Pointer.
// This is the only branch whose destination is not written into the
// program, which is what makes a table of addresses something a program
// can dispatch through rather than only read.
uint16_t destination = *selectDataPointer(cpu);
// stepCPU adds one after every instruction, so aim one short.
cpu->ProgramCounter = destination - 1;
}
break;
case 0x17:
// CALL - Push the Program Counter to the Stack, and perform an immediate branch.
genericCall(cpu);
break;
case 0x18: {
// SWI - Software Interrupt. The byte after the opcode names the vector.
// Never masked: this is an instruction the program deliberately ran, not
// something a device asked for.
uint16_t site = cpu->ProgramCounter;
cpu->ProgramCounter++;
uint8_t vector = cpu->Program[cpu->ProgramCounter];
// Execution resumes after the operand, which the Program Counter is sitting
// on, so the resume address is one further on than that.
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, vector, cpu->ProgramCounter + 1)) {
// No handler. Leave the Program Counter on the SWI itself rather than
// its operand, so the report names the instruction that failed.
cpu->ProgramCounter = site - 1;
}
} break;
case 0x19: {
// RETI - Return from an interrupt. Pops the frame in the exact reverse of
// the order enterInterrupt pushed it.
cpu->StackPointer++;
cpu->Status = cpu->Data[cpu->StackPointer];
cpu->StackPointer++;
cpu->Q = cpu->Data[cpu->StackPointer];
cpu->StackPointer++;
cpu->A = cpu->Data[cpu->StackPointer];
cpu->StackPointer++;
cpu->B = cpu->Data[cpu->StackPointer];
for (int i = DATA_POINTERS - 1; i >= 0; i--) {
cpu->StackPointer++;
cpu->DataPointer[i] = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
cpu->StackPointer++;
cpu->DataPointer[i] |= (uint16_t)cpu->Data[cpu->StackPointer];
}
uint16_t resumeAddress;
cpu->StackPointer++;
resumeAddress = (uint16_t)cpu->Data[cpu->StackPointer] << 8;
cpu->StackPointer++;
resumeAddress = resumeAddress | (uint16_t)cpu->Data[cpu->StackPointer];
// The frame holds the address to carry on from. The Program Counter is
// stepped after every instruction, so land one short of it. RET does the
// same job with its +2, for the same reason.
cpu->ProgramCounter = resumeAddress - 1;
} break;
case 0x1F:
// RET - Return from subroutine, restore the registers and set the Program Counter to the Return Address.
// Pop A from the Stack.
@@ -213,9 +334,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// INCA - Add 1 to A.
// Set the Carry Flag if the register overflows.
if (cpu->A == 0xFF) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->A++;
break;
@@ -223,9 +344,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// INCB - Add 1 to B.
// Set the Carry Flag if the register overflows.
if (cpu->B == 0xFF) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->B++;
break;
@@ -233,9 +354,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// DECA - Subtract 1 from A.
// Set the Carry Flag if the register underflows.
if (cpu->A == 0x00) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->A--;
break;
@@ -243,9 +364,9 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
// DECB - Subtract 1 from B.
// Set the Carry Flag if the register underflows.
if (cpu->B == 0x00) {
cpu->Status |= 0x01;
cpu->Status |= STATUS_CARRY;
} else {
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
}
cpu->B--;
break;
@@ -261,7 +382,25 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
break;
case 0x28:
// CCF - Clear the Carry Flag.
cpu->Status &= ~0x01;
cpu->Status &= ~STATUS_CARRY;
break;
case 0x29:
// MVQA - Copy Q into A.
cpu->A = cpu->Q;
break;
case 0x2A:
// MVQB - Copy Q into B.
cpu->B = cpu->Q;
break;
case 0x2B:
// SIF - Set the Interrupt Flag, enabling hardware interrupts.
cpu->Status |= STATUS_INTERRUPT;
break;
case 0x2C:
// CIF - Clear the Interrupt Flag, disabling hardware interrupts.
// Software interrupts and faults are delivered either way, so this
// only ever holds off a device.
cpu->Status &= ~STATUS_INTERRUPT;
break;
//
// 3x - Stack Operations:
@@ -390,6 +529,18 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
cpu->Data[(uint16_t)(address + 1)] = value & 0xFF;
}
break;
case 0x4C: {
// MVSD - Copy the Stack Pointer into the selected Data Pointer.
//
// The Stack Pointer still cannot be written, so this does not let a program
// move the Stack. It lets a program find it, which is what reading anything
// already on the Stack requires. An interrupt handler needs this to reach
// its own frame, and so does anything that wants to walk back through the
// calls that led to where it is.
uint16_t *target = selectDataPointer(cpu);
*target = cpu->StackPointer;
}
break;
//
// Dx - Output Operations:
//
@@ -429,7 +580,7 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
break;
case 0xFF:
// HALT - Set the Halt Bit of the Status Register.
cpu->Status |= 0x80;
cpu->Status |= STATUS_HALT;
break;
default:
// Unknown Instruction.
@@ -439,9 +590,51 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
}
void stepCPU(CPURegisters *cpu) {
if (!(cpu->Status & 0x80)) {
if (!(cpu->Status & STATUS_HALT)) {
// A device asking for attention is answered between instructions and never
// inside one, so the address that goes into the frame is always the start of an
// instruction and RETI always lands somewhere meaningful.
//
// 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();
if (port >= 0) {
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
// entry is in the hardware table rather than the software one.
cpu->Fault = FAULT_NO_DEVICE_HANDLER;
return;
}
// Entering the handler is what this cycle did, so no instruction runs.
// The step puts the Program Counter on the handler's first byte, the
// same way it does everywhere else.
cpu->ProgramCounter++;
return;
}
}
// The CPU is not halted, so do a cycle.
executeOperation(cpu->Program[cpu->ProgramCounter], cpu);
if (executeOperation(cpu->Program[cpu->ProgramCounter], cpu)) {
// Nothing decodes that byte. Hand it to the fault vector, which gets the
// address of the offending byte itself rather than the one after it, so
// that a handler can read the byte that failed and say what it was.
//
// A handler returning with a bare RETI will therefore meet the same byte
// again. That is the documented behaviour: resuming past a fault means
// deciding where to resume, which is the handler's business and not the
// CPU's guess.
uint16_t faultingAddress = cpu->ProgramCounter;
if (enterInterrupt(cpu, SOFTWARE_VECTOR_BASE, VECTOR_INVALID_OPCODE, faultingAddress)) {
// Nothing is installed, so stop where we are. The Program Counter is
// still on the offending byte, which is what the report wants. The
// cause is the byte, not the empty vector, so say so.
cpu->Fault = FAULT_BAD_OPCODE;
return;
}
// Dispatched. Fall through, so the step below lands on the handler.
}
cpu->ProgramCounter++;
}
}
+27
View File
@@ -26,6 +26,30 @@
#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.
} FaultCause;
// The struct containing the CPU registers.
typedef struct {
uint8_t A;
@@ -37,6 +61,9 @@ typedef struct {
uint16_t StackPointer;
uint8_t *Program;
uint8_t *Data;
// 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);
+21 -3
View File
@@ -99,7 +99,12 @@ int main (int argc, char *argv[]) {
cycle_timer_init(&timer, CYCLE_RATE);
uint8_t limitReached = 0;
while (!(cpu.Status & 0x80) && !limitReached) {
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.
getchar();
}
int cycles;
if (options.debug) {
// Debug mode advances one instruction per keypress, so the wall clock
@@ -113,7 +118,7 @@ int main (int argc, char *argv[]) {
for (int i = 0; i < cycles; i++) {
stepCPU(&cpu);
cycleCount++;
if (cpu.Status & 0x80) {
if (cpu.Status & STATUS_HALT) {
// We've halted.
break;
}
@@ -123,13 +128,26 @@ int main (int argc, char *argv[]) {
}
}
if (options.debug) {
getchar();
printRegisters(&cpu, Program, Data);
printf("Cycle: %lu\n", cycleCount);
}
}
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.
printf("Execution halted after %lu cycles.\n", 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_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 {
printf("Execution halted after %lu cycles.\n", cycleCount);
}
+39
View File
@@ -6,6 +6,37 @@
#include "io.h"
#include <stdio.h>
// One bit per port, so a device can ask for attention without anything having to poll
// it. Eight ports to the byte, low bit first.
#define INTERRUPT_LINE_BYTES 32
static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES];
void raiseInterrupt(uint8_t port) {
pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7));
}
void clearInterrupt(uint8_t port) {
pendingInterrupts[port >> 3] &= (uint8_t)~(1u << (port & 7));
}
int nextPendingInterrupt(void) {
// Lowest numbered port wins. This is a scan rather than a priority encoder, which
// means there is no arbitration to explain and a programmer can work out what
// happens next by reading the port numbers.
for (int group = 0; group < INTERRUPT_LINE_BYTES; group++) {
if (pendingInterrupts[group] == 0) {
continue;
}
for (int bit = 0; bit < 8; bit++) {
if (pendingInterrupts[group] & (1u << bit)) {
return group * 8 + bit;
}
}
}
return -1;
}
uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// This function sends the DataByte to the appropriate place based on the Port Address.
switch(Address) {
@@ -15,6 +46,14 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case 0x10:
// A test device, and about the simplest one that can exist: writing to it
// puts its own line up. It stands in for the shape a real device has, where
// the CPU asks for something and is interrupted once the answer is ready,
// with the waiting taken out so that a test runs the same way every time.
// The byte written is ignored; only the asking matters.
raiseInterrupt(0x10);
break;
default:
// Writes to unused Output Ports are ignored.
return 1;
+16
View File
@@ -13,4 +13,20 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address);
uint8_t InputHandler(uint8_t Address);
// ---- Interrupt lines ----
//
// One line per port. A device puts its line up to ask for attention, and the CPU takes
// it down when it answers. Which line a device uses is not a choice: a device on port N
// interrupts on N, which is what saves the machine from needing any arbitration.
//
// These belong to the bus rather than to the CPU. Nothing here is saved in a frame, and
// a program cannot read them except by being interrupted.
void raiseInterrupt(uint8_t port);
void clearInterrupt(uint8_t port);
// The lowest numbered port with its line up, or -1 if none of them are.
int nextPendingInterrupt(void);
#endif // IO_H
+23 -3
View File
@@ -68,11 +68,31 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
return OPTIONS_OK;
}
// Writes a byte out as eight binary digits, most significant first. printf's %b is
// a recent addition to C and not available everywhere, so this does it by hand.
// The buffer must have room for nine characters.
static void formatBinary(uint8_t value, char *out) {
for (int i = 0; i < 8; i++) {
out[i] = (value & (0x80 >> i)) ? '1' : '0';
}
out[8] = '\0';
}
void printRegisters(CPURegisters *cpu, uint8_t *Program, uint8_t *Data) {
char status[9];
formatBinary(cpu->Status, status);
printf("***** CPU Registers *****\n");
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%08b\n", cpu->A, cpu->B, cpu->Q, cpu->Status);
printf("A: 0x%02X\tB: 0x%02X\tQ: 0x%02X\tStatus: 0b%s\n", cpu->A, cpu->B, cpu->Q, status);
printf("Program Counter: 0x%04X Current Instruction: 0x%02X (%s)\n", cpu->ProgramCounter, Program[cpu->ProgramCounter],getMnemonic(Program[cpu->ProgramCounter]));
printf(" Data Pointer: 0x%04X Current Data Value: 0x%02X\n", cpu->DataPointer[0], Data[cpu->DataPointer[0]]);
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer, Data[cpu->StackPointer+1], Data[cpu->StackPointer+2]);
for (int i = 0; i < DATA_POINTERS; i++) {
printf(" Data Pointer %d: 0x%04X Current Data Value: 0x%02X%s\n",
i, cpu->DataPointer[i], Data[cpu->DataPointer[i]],
i >= PRESERVED_DATA_POINTERS ? " (volatile)" : "");
}
// The two casts keep these inside Data Memory. The Stack Pointer starts at the
// very top, so without them the display would read off the end of the array
// before a single byte has been pushed.
printf(" Stack Pointer: 0x%04X Current Value: (0x%02X) (0x%02X)\n", cpu->StackPointer,
Data[(uint16_t)(cpu->StackPointer + 1)], Data[(uint16_t)(cpu->StackPointer + 2)]);
}