From 8c918e54fca1142ac8bc0356d453976c9e5f8ca7 Mon Sep 17 00:00:00 2001 From: Arghya Biswas Date: Wed, 8 Apr 2026 20:32:48 +0530 Subject: [PATCH] Added new hardware mode in emulator Signed-off-by: Arghya Biswas --- .gitignore | 1 + Emulator/core/decoder.py | 9 +- Emulator/core/hardware_cpu.py | 470 ++++++++++++++++++++++ Emulator/core/{cpu.py => software_cpu.py} | 43 +- Emulator/gui/main_window.py | 168 +++++++- Emulator/gui/widgets.py | 54 ++- Emulator/main.py | 19 +- 7 files changed, 734 insertions(+), 30 deletions(-) create mode 100644 Emulator/core/hardware_cpu.py rename Emulator/core/{cpu.py => software_cpu.py} (85%) diff --git a/.gitignore b/.gitignore index 20f3684..85d5895 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Compiler/*.bin Microcode/out/ Gen7segDriver/*.bin __pycache__ +.vscode diff --git a/Emulator/core/decoder.py b/Emulator/core/decoder.py index c4d33ed..b89c510 100644 --- a/Emulator/core/decoder.py +++ b/Emulator/core/decoder.py @@ -106,13 +106,14 @@ def _decodeSpecialOpcode(self, instruction, opcode, upperNibble): def _decode4bitOpcode(self, instruction, opcode, upperNibble): opcodeName = self.opcodes[opcode] - # These instructions have SSDD format (source, destination registers) - destReg = upperNibble & 0b11 # Bottom 2 bits - sourceReg = (upperNibble >> 2) & 0b11 # Top 2 bits + # These instructions use DDSS in upper nibble: + # destination in bits [7:6], source in bits [5:4] + destinationReg = (upperNibble >> 2) & 0b11 # Top 2 bits + sourceReg = upperNibble & 0b11 # Bottom 2 bits return (opcodeName, { 'sourceRegister': sourceReg, - 'destinationRegister': destReg + 'destinationRegister': destinationReg }, 1) diff --git a/Emulator/core/hardware_cpu.py b/Emulator/core/hardware_cpu.py new file mode 100644 index 0000000..6011218 --- /dev/null +++ b/Emulator/core/hardware_cpu.py @@ -0,0 +1,470 @@ +import os +import yaml + +from .decoder import InstructionDecoder +from .registers import RegisterFile +from .memory import Memory +from .alu import ALU + + +class HardwareCPU: + DEFAULT_MICROCODE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Microcode', 'out')) + DEFAULT_SEVENSEG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Gen7segDriver', 'decimal_display_segments.bin')) + DEFAULT_CONFIG_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Microcode', 'MicroCodeConfig.yaml')) + + def __init__(self, + microcodeDir=None, + sevenSegPath=None, + configPath=None, + enable_signal_logging=True, + log_callback=None): + self.registers = RegisterFile() + self.memory = Memory() + self.alu = ALU() + self.decoder = InstructionDecoder() + + self.microcodeDir = microcodeDir or self.DEFAULT_MICROCODE_DIR + self.sevenSegPath = sevenSegPath or self.DEFAULT_SEVENSEG_PATH + self.configPath = configPath or self.DEFAULT_CONFIG_PATH + + self.enable_signal_logging = enable_signal_logging + self.log_callback = log_callback if log_callback is not None else print + + self.microcodeBanks = [bytearray(), bytearray(), bytearray()] + self.sevenSegRom = None + self.inputSignalByCode = {} + self.outputSignalByCode = {} + + self.flagSelect = (0, 0, 0) + self.memoryAddress = 0 + self.bus = 0 + self.temp1 = 0 + self.temp2 = 0 + self.pcLowRegister = 0 + self.pcHighRegister = 0 + self.sevenSegmentValue = 0 + self.sevenSegmentPatterns = None + self.outputEnabled = False + self.signedMode = True + + self._loadConfig() + self._loadMicrocodeBanks() + self._loadSevenSegRom() + self.reset() + + def setLogCallback(self, callback): + self.log_callback = callback if callback is not None else print + + def _loadConfig(self): + if not os.path.exists(self.configPath): + raise FileNotFoundError(f"Microcode config not found: {self.configPath}") + + with open(self.configPath, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + self.inputSignalByCode.clear() + self.outputSignalByCode.clear() + + pinConfig = config.get('PinConfig', {}) + for signalName, index in pinConfig.get('InputControl', {}).items(): + if index is None: + continue + self.inputSignalByCode[int(index)] = signalName + + for signalName, index in pinConfig.get('OutputControl', {}).items(): + if index is None: + continue + self.outputSignalByCode[int(index)] = signalName + + def _loadMicrocodeBanks(self): + for chipIndex in range(3): + filename = os.path.join(self.microcodeDir, f"Microcode_{chipIndex}.bin") + if not os.path.exists(filename): + raise FileNotFoundError(f"Microcode bank missing: {filename}") + with open(filename, 'rb') as f: + data = f.read() + if len(data) != 2 ** 15: + raise ValueError(f"Microcode bank {filename} must be 32768 bytes") + self.microcodeBanks[chipIndex] = bytearray(data) + + def _loadSevenSegRom(self): + if not os.path.exists(self.sevenSegPath): + self._generateSevenSegRom() + + with open(self.sevenSegPath, 'rb') as f: + data = f.read() + + if len(data) != 2048: + raise ValueError(f"7-seg ROM must be 2048 bytes: {self.sevenSegPath}") + + self.sevenSegRom = data + + def _generateSevenSegRom(self): + os.makedirs(os.path.dirname(self.sevenSegPath), exist_ok=True) + + segment_map = [ + 0b01111110, # 0 + 0b00001100, # 1 + 0b10110110, # 2 + 0b10011110, # 3 + 0b11001100, # 4 + 0b11011010, # 5 + 0b11111010, # 6 + 0b00001110, # 7 + 0b11111110, # 8 + 0b11011110, # 9 + 0b10000000, # - + 0b00000000 # blank + ] + + eeprom = bytearray(2048) + THOUSAND_ADD = 0b000 << 8 + HUNDRED_ADD = 0b010 << 8 + TEN_ADD = 0b001 << 8 + UNIT_ADD = 0b011 << 8 + SIGN = 0b100 << 8 + + def genReverseNegativeNum(num): + return (num - 1) ^ 0xFF + + for index in range(256): + num = index + unit = num % 10 + ten = (num // 10) % 10 + hundred = (num // 100) % 10 + + eeprom[UNIT_ADD + index] = segment_map[unit] + eeprom[HUNDRED_ADD + index] = segment_map[hundred] if hundred else segment_map[11] + eeprom[TEN_ADD + index] = segment_map[ten] if ten or hundred else segment_map[11] + eeprom[THOUSAND_ADD + index] = segment_map[11] + + for index in range(256): + num = genReverseNegativeNum(index) + unit = num % 10 + ten = (num // 10) % 10 + hundred = (num // 100) % 10 + + eeprom[SIGN + THOUSAND_ADD + index] = segment_map[11] + eeprom[SIGN + TEN_ADD + index] = segment_map[11] + eeprom[SIGN + HUNDRED_ADD + index] = segment_map[11] + eeprom[SIGN + UNIT_ADD + index] = segment_map[unit] + + if ten: + eeprom[SIGN + TEN_ADD + index] = segment_map[ten] + eeprom[SIGN + HUNDRED_ADD + index] = segment_map[10] + else: + eeprom[SIGN + TEN_ADD + index] = segment_map[10] + + if hundred: + eeprom[SIGN + HUNDRED_ADD + index] = segment_map[hundred] + eeprom[SIGN + THOUSAND_ADD + index] = segment_map[10] + + with open(self.sevenSegPath, 'wb') as f: + f.write(bytes(eeprom)) + + def _compute_flag_input(self): + select = self.flagSelect + zero = self.alu.flags['zero'] + carry = self.alu.flags['carry'] + negative = self.alu.flags['negative'] + greater = (not zero) and (not negative) + less = negative + equal = zero + + code = (select[0] << 2) | (select[1] << 1) | select[2] + + if code == 0b000: + return 1 if zero else 0 + if code == 0b001: + return 1 if carry else 0 + if code == 0b010: + return 1 if greater else 0 + if code == 0b011: + return 1 if less else 0 + if code == 0b100: + return 1 if equal else 0 + return 0 + + def _get_microcode_address(self): + flagBit = self._compute_flag_input() + seq = self.sequenceCounter & 0x0F + ir = self.instructionRegister & 0xFF + address = (flagBit << 12) | (seq << 8) | ir + return address + + def _decode_microcode_outputs(self, bank0, bank1, bank2, address): + signals = { + 'PCL': bool(bank0 & 0x80), + 'PCC': bool(bank0 & 0x40), + 'AdSu': bool(bank0 & 0x20), + 'Cin': bool(bank0 & 0x10), + 'SpC': bool(bank0 & 0x08), + 'SpUd': bool(bank2 & 0x80), + 'FlSe0': bool(bank2 & 0x40), + 'FlSe1': bool(bank2 & 0x20), + 'FlSe2': bool(bank2 & 0x10), + 'HLT': bool(bank2 & 0x08), + } + + vi_in_code = (((bank1 >> 7) & 1) << 0) | (((bank1 >> 6) & 1) << 1) | (((bank1 >> 5) & 1) << 2) | (((bank1 >> 4) & 1) << 3) + vi_out_code = (((bank1 >> 3) & 1) << 0) | (((bank1 >> 2) & 1) << 1) | (((bank1 >> 1) & 1) << 2) | (((bank1 >> 0) & 1) << 3) + + signals['virtual_input'] = self.inputSignalByCode.get(vi_in_code) + signals['virtual_output'] = self.outputSignalByCode.get(vi_out_code) + + if signals['virtual_input']: + signals[signals['virtual_input']] = True + if signals['virtual_output']: + signals[signals['virtual_output']] = True + + return signals + + def _get_active_signals(self, signals): + """Return a list of signal names that are HIGH (True)""" + active = [] + exclude_signals = {'RESRV'} # Skip non-real signals + for sig_name, sig_value in signals.items(): + if sig_value is True and sig_name not in exclude_signals: # Only True booleans, skip None or strings and excluded signals + active.append(sig_name) + return sorted(active) + + def _log_signals(self, signals, cycle_num): + """Print active signals for this cycle in a formatted line""" + active = self._get_active_signals(signals) + if active: + signal_str = " ".join(active) + else: + signal_str = "(no signals)" + self.log_callback(f" [Cycle {cycle_num}] {signal_str}") + + def _log_instruction_start(self): + """Print instruction header when starting a new instruction""" + opcode = self.memory.readRom(self.programCounter) + mnemonic, operands, size = self.decoder.decode(opcode) + + # Format the instruction with operands + if mnemonic == 'LDI' or mnemonic == 'LDM' or mnemonic == 'SAV': + reg_name = ['A', 'B', 'C', 'D'][operands.get('register', 0)] + immediate = self.memory.readRom(self.programCounter + 1) + self.log_callback(f"\n{mnemonic} {reg_name} {immediate}:") + elif mnemonic in ['INC', 'DEC', 'NOT', 'CMI']: + reg_name = ['A', 'B', 'C', 'D'][operands.get('register', 0)] + self.log_callback(f"\n{mnemonic} {reg_name}:") + elif mnemonic in ['ADD', 'SUB', 'MOV', 'AND', 'OR', 'XOR', 'CMP']: + src_reg = ['A', 'B', 'C', 'D'][operands.get('sourceRegister', 0)] + dst_reg = ['A', 'B', 'C', 'D'][operands.get('destinationRegister', 0)] + self.log_callback(f"\n{mnemonic} {dst_reg} {src_reg}:") + else: + self.log_callback(f"\n{mnemonic}:") + + def _alu_result(self, adsu, cin): + if adsu: + result, carryOut = self.alu.subtract(self.temp1, self.temp2, cin) + else: + result, carryOut = self.alu.add(self.temp1, self.temp2, cin) + return result, carryOut + + def _compute_bus_value(self, signals): + virtual_output = signals.get('virtual_output') + if virtual_output == 'rAO': + return self.registers.readByName('A') + if virtual_output == 'rBO': + return self.registers.readByName('B') + if virtual_output == 'rCO': + return self.registers.readByName('C') + if virtual_output == 'rDO': + return self.registers.readByName('D') + if virtual_output == 'PCO': + return self.programCounter & 0xFF + if virtual_output == 'RomO': + return self.memory.readRom(self.programCounter) + if virtual_output == 'MeO': + return self.memory.readRam(self.memoryAddress) + if virtual_output == 'AdSuO': + result, _ = self._alu_result(signals['AdSu'], int(signals['Cin'])) + return result + if virtual_output == 'AndO': + return self.temp1 & self.temp2 + if virtual_output == 'OrO': + return self.temp1 | self.temp2 + if virtual_output == 'XorO': + return self.temp1 ^ self.temp2 + + # No output source is driving the bus in this micro-step. + return 0 + + def _apply_control_signals(self, signals): + if signals.get('SpO') or signals.get('Seg7E') or signals.get('SpL'): + self.outputEnabled = True + self.sevenSegmentPatterns = None + + if signals.get('rAI'): + self.registers.writeByName('A', self.bus) + if signals.get('rBI'): + self.registers.writeByName('B', self.bus) + if signals.get('rCI'): + self.registers.writeByName('C', self.bus) + if signals.get('rDI'): + self.registers.writeByName('D', self.bus) + if signals.get('T1I'): + self.temp1 = self.bus & 0xFF + if signals.get('T2I'): + self.temp2 = self.bus & 0xFF + if signals.get('PCLI'): + self.pcLowRegister = self.bus & 0xFF + if signals.get('PCHI'): + self.pcHighRegister = self.bus & 0x07 + if signals.get('IRI'): + self.instructionRegister = self.bus & 0xFF + if signals.get('Seg7E'): + self.sevenSegmentValue = self.registers.readByName('A') + if signals.get('SpL'): + self.sevenSegmentValue = self.bus & 0xFF + + if signals.get('MdI'): + self.memoryAddress = self.bus & 0x0F + + if signals.get('MeI') and signals.get('MdI'): + self.memory.writeRam(self.memoryAddress, self.bus) + + if signals.get('PCL'): + self.programCounter = ((self.pcHighRegister << 8) | self.pcLowRegister) & 0x7FF + + if signals.get('PCC'): + self.programCounter = (self.programCounter + 1) & 0x7FF + + if signals.get('FlgU'): + result, carryOut = self._alu_result(signals['AdSu'], int(signals['Cin'])) + self.alu._updateFlags(result, carryOut) + + if signals.get('Seg7E') or signals.get('SpO') or signals.get('SpL'): + self._update_seven_segment_patterns() + + if signals.get('SpUd'): + self.signedMode = False + else: + self.signedMode = True + + if signals.get('HLT'): + self.halted = True + + def _update_seven_segment_patterns(self): + if not self.sevenSegRom: + return + if self.signedMode and self.sevenSegmentValue > 127: + index = self.sevenSegmentValue & 0xFF + offset = 0x400 + else: + index = self.sevenSegmentValue & 0xFF + offset = 0 + + thousand = self.sevenSegRom[offset + 0 + index] + hundred = self.sevenSegRom[offset + 512 + index] + ten = self.sevenSegRom[offset + 256 + index] + unit = self.sevenSegRom[offset + 768 + index] + self.sevenSegmentPatterns = [thousand, hundred, ten, unit] + + def loadProgram(self, binaryData, startAddress=0): + self.memory.loadRom(binaryData, startAddress) + self.reset() + + def reset(self): + self.registers.reset() + self.memory.resetRam() + self.alu.reset() + self.programCounter = 0 + self.pcLowRegister = 0 + self.pcHighRegister = 0 + self.instructionRegister = 0 + self.sequenceCounter = 0 + self.halted = False + self.running = False + self.flagSelect = (0, 0, 0) + self.memoryAddress = 0 + self.bus = 0 + self.temp1 = 0 + self.temp2 = 0 + self.sevenSegmentValue = 0 + self.sevenSegmentPatterns = None + self.outputEnabled = False + self.instructionCount = 0 + self.cycleCount = 0 + self.currentInstructionStarted = False + + def step(self): + if self.halted: + return False + + address = self._get_microcode_address() + bank0 = self.microcodeBanks[0][address] + bank1 = self.microcodeBanks[1][address] + bank2 = self.microcodeBanks[2][address] + + signals = self._decode_microcode_outputs(bank0, bank1, bank2, address) + + # Log instruction start before logging signals + if self.sequenceCounter == 0 and not self.currentInstructionStarted and self.enable_signal_logging: + self._log_instruction_start() + + if self.enable_signal_logging: + self._log_signals(signals, self.cycleCount) + + bus_value = self._compute_bus_value(signals) + self.bus = bus_value & 0xFF + + self._apply_control_signals(signals) + + next_seq = 0 if signals.get('SqR') else ((self.sequenceCounter + 1) & 0x0F) + if signals.get('SqR') and self.currentInstructionStarted: + self.instructionCount += 1 + self.currentInstructionStarted = False + elif signals.get('HLT') and self.currentInstructionStarted: + self.instructionCount += 1 + self.currentInstructionStarted = False + + if self.sequenceCounter == 0 and not self.currentInstructionStarted: + self.currentInstructionStarted = True + + self.sequenceCounter = next_seq + self.cycleCount += 1 + + self.flagSelect = (int(signals.get('FlSe0', False)), + int(signals.get('FlSe1', False)), + int(signals.get('FlSe2', False))) + + return not self.halted + + def run(self, maxInstructions=10000): + self.running = True + executed = 0 + while self.running and not self.halted and executed < maxInstructions: + if not self.step(): + break + executed += 1 + self.running = False + return executed + + def getState(self): + return { + 'registers': self.registers.getAllRegisters(), + 'executionMode': 'hardware', + 'cycleType': 'micro', + 'pc': self.programCounter, + 'ir': self.instructionRegister, + 'halted': self.halted, + 'alu_flags': self.alu.getFlags(), + 'seven_segment': self.sevenSegmentValue, + 'seven_segment_patterns': self.sevenSegmentPatterns, + 'outputEnabled': self.outputEnabled, + 'signedMode': self.signedMode, + 'instructionCount': self.instructionCount, + 'cycleCount': self.cycleCount, + 'ram': self.memory.getRamDump() + } + + def setSignedMode(self, signedMode): + self.signedMode = signedMode + + def __str__(self): + state = self.getState() + return f"PC:{state['pc']:04X} {self.registers} {self.alu} 7SEG:{state['seven_segment']:02X}" diff --git a/Emulator/core/cpu.py b/Emulator/core/software_cpu.py similarity index 85% rename from Emulator/core/cpu.py rename to Emulator/core/software_cpu.py index efb04ea..ca3d1a6 100644 --- a/Emulator/core/cpu.py +++ b/Emulator/core/software_cpu.py @@ -4,8 +4,8 @@ from .alu import ALU -class CPU8Bit: - def __init__(self): +class SoftwareCPU: + def __init__(self, enable_execution_logging=False, log_callback=None): # Initialize components self.registers = RegisterFile() self.memory = Memory() @@ -26,6 +26,8 @@ def __init__(self): self.outputEnabled = False # Debug/statistics self.instructionCount = 0 self.cycleCount = 0 + self.enableExecutionLogging = enable_execution_logging + self.logCallback = log_callback if log_callback is not None else print def reset(self): @@ -63,12 +65,17 @@ def step(self): if self.halted: return False + pcBefore = self.programCounter + # Fetch instruction instruction = self.fetch() # Decode instruction opcode, operands, size = self.decoder.decode(instruction) + if self.enableExecutionLogging: + self.logCallback(f"{pcBefore:04X}: {self._formatInstructionForLog(opcode, operands)}") + # Execute instruction success = self.execute(opcode, operands, size) @@ -125,8 +132,8 @@ def execute(self, opcode, operands, instructionSize): self.programCounter += instructionSize return False - # Advance PC for non-jump instructions - if opcode not in ['JMP', 'JMZ', 'JNZ', 'JMC', 'JME', 'JNG', 'JML', 'HLT']: + # Advance PC for non-jump instructions (including HLT to match hardware fetch behavior) + if opcode not in ['JMP', 'JMZ', 'JNZ', 'JMC', 'JME', 'JNG', 'JML']: self.programCounter += instructionSize return True @@ -281,6 +288,26 @@ def _getJumpAddress(self): lowByte = self.memory.readRom(self.programCounter + 2) # PC + 2 return (highByte << 8) | lowByte + def _formatInstructionForLog(self, opcode, operands): + if opcode in ['ADD', 'SUB', 'MOV', 'AND', 'OR', 'XOR', 'CMP']: + src = self.decoder.registerName(operands.get('sourceRegister', 0)) + dst = self.decoder.registerName(operands.get('destinationRegister', 0)) + return f"{opcode} {dst} {src}" + + if opcode in ['INC', 'DEC', 'NOT']: + reg = self.decoder.registerName(operands.get('register', 0)) + return f"{opcode} {reg}" + + if opcode in ['LDI', 'LDM', 'SAV', 'CMI']: + reg = self.decoder.registerName(operands.get('register', 0)) + value = self._getNextByte() + return f"{opcode} {reg} {value}" + + if opcode in ['JMP', 'JMZ', 'JNZ', 'JMC', 'JME', 'JNG', 'JML']: + return f"{opcode} {self._getJumpAddress():04X}" + + return opcode + def run(self, maxInstructions = 10000): self.running = True @@ -298,6 +325,8 @@ def run(self, maxInstructions = 10000): def getState(self): return { 'registers' : self.registers.getAllRegisters(), + 'executionMode' : 'software', + 'cycleType' : 'instruction', 'pc' : self.programCounter, 'ir' : self.instructionRegister, 'halted' : self.halted, @@ -314,6 +343,12 @@ def getState(self): def setSignedMode(self, signedMode): self.signedMode = signedMode + def setLogCallback(self, callback): + self.logCallback = callback if callback is not None else print + + def setExecutionLogging(self, enabled): + self.enableExecutionLogging = bool(enabled) + def __str__(self): state = self.getState() diff --git a/Emulator/gui/main_window.py b/Emulator/gui/main_window.py index 4d044e1..de807c5 100644 --- a/Emulator/gui/main_window.py +++ b/Emulator/gui/main_window.py @@ -6,18 +6,82 @@ # Add parent directory to path for imports sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from core.cpu import * +from core.software_cpu import SoftwareCPU +from core.hardware_cpu import HardwareCPU from gui.widgets import * + +class ToolTip: + def __init__(self, widget, text=""): + self.widget = widget + self.text = text + self.tip_window = None + self.widget.bind("", self._show) + self.widget.bind("", self._hide) + + def set_text(self, text): + self.text = text + + def _show(self, event=None): + if self.tip_window is not None or not self.text: + return + + self.tip_window = tw = tk.Toplevel(self.widget) + tw.wm_overrideredirect(True) + + label = tk.Label( + tw, + text=self.text, + justify="left", + background="#FFFDE7", + foreground="#111111", + relief="solid", + borderwidth=1, + font=("Arial", 9), + padx=6, + pady=4 + ) + label.pack() + + # Ensure tooltip stays within visible screen bounds. + tw.update_idletasks() + + screen_w = self.widget.winfo_screenwidth() + screen_h = self.widget.winfo_screenheight() + tip_w = tw.winfo_width() + tip_h = tw.winfo_height() + + x = self.widget.winfo_rootx() + 20 + y = self.widget.winfo_rooty() + self.widget.winfo_height() + 8 + + # If not enough room below, show above the widget. + if y + tip_h > screen_h - 8: + y = self.widget.winfo_rooty() - tip_h - 8 + + # Clamp within screen margins. + x = max(8, min(x, screen_w - tip_w - 8)) + y = max(8, min(y, screen_h - tip_h - 8)) + + tw.wm_geometry(f"+{x}+{y}") + + def _hide(self, event=None): + if self.tip_window is not None: + self.tip_window.destroy() + self.tip_window = None + class EmulatorMainWindow: - def __init__(self): + def __init__(self, mode='software'): self.root = tk.Tk() self.root.title("8-bit Computer Emulator") - self.root.geometry("1000x700") + self.root.geometry("1000x800") self.root.resizable(True, True) # Initialize CPU - self.cpu = CPU8Bit() + self.mode = mode + if self.mode == 'hardware': + self.cpu = HardwareCPU(enable_signal_logging=True, log_callback=self.appendConsole) + else: + self.cpu = SoftwareCPU(enable_execution_logging=True, log_callback=self.appendConsole) # GUI state self.running = False @@ -28,6 +92,9 @@ def __init__(self): self.createWidgets() self.createControls() + # Attach GUI console logger + self.cpu.setLogCallback(self.appendConsole) + # Update display self.updateDisplay() @@ -109,6 +176,7 @@ def createWidgets(self): self.assemblyTextbox.pack(side = "left", fill = "both", expand = True) assemblyCodeScrollbar.pack(side = "right", fill = "y") + self.assemblyTextbox.config(state = "disabled") # Disassembly code display disassemblyCodeFrame = tk.LabelFrame(codeFrame, text = "Disassembly Code", font = ("Arial", 10, "bold")) @@ -119,6 +187,20 @@ def createWidgets(self): self.disassemblyTextbox.pack(side = "left", fill = "both", expand = True) disassemblyCodeScrollbar.pack(side = "right", fill = "y") + self.disassemblyTextbox.config(state = "disabled") + + # Console output + consoleFrame = tk.LabelFrame(centerFrame, text = "Console", font = ("Arial", 10, "bold")) + consoleFrame.pack(fill = "both", expand = True, pady = 10) + + self.consoleTextbox = tk.Text(consoleFrame, width = 10, height = 10, + font = ("Courier", 9), bg = "#111", fg = "#eee") + consoleScrollbar = tk.Scrollbar(consoleFrame, orient = "vertical", command = self.consoleTextbox.yview) + self.consoleTextbox.configure(yscrollcommand = consoleScrollbar.set) + + self.consoleTextbox.pack(side = "left", fill = "both", expand = True) + consoleScrollbar.pack(side = "right", fill = "y") + self.consoleTextbox.config(state = "disabled") def createControls(self): @@ -150,8 +232,19 @@ def createControls(self): speedEntry = tk.Entry(speedFrame, textvariable = self.speedVar, width = 8) speedEntry.pack(side = "left", padx = 2) + # Mode indicator + modeFrame = tk.LabelFrame(controlFrame, text = "Mode") + modeFrame.pack(side = "left", padx = 5) + self.modeIndicator = tk.Label(modeFrame, text = "SOFTWARE", + font = ("Arial", 9, "bold"), + bg = "#1E88E5", fg = "white", + width = 10, relief = "ridge", bd = 2) + self.modeIndicator.pack(side = "left", padx = 2, pady = 2) + self.modeTooltip = ToolTip(self.modeIndicator) + # Status - self.statusLabel = tk.Label(controlFrame, text = "Ready to load program", + self.statusLabel = tk.Label(controlFrame, + text = f"Ready to load program ({self.mode} mode)", relief = "sunken", anchor = "w") self.statusLabel.pack(side = "right", fill = "x", expand = True, padx = 5) @@ -169,6 +262,7 @@ def loadProgram(self): self.cpu.loadProgram(binaryData) self.updateDisplay() + self.appendConsole(f"Loaded program: {os.path.basename(filename)}") # Try to load corresponding assembly file for display asmFile = filename.replace('.bin', '.s') @@ -183,12 +277,25 @@ def loadProgram(self): messagebox.showerror("Error", f"Failed to load program:\n{e}") + def appendConsole(self, text): + self.consoleTextbox.config(state = "normal") + self.consoleTextbox.insert(tk.END, text + "\n") + self.consoleTextbox.see(tk.END) + self.consoleTextbox.config(state = "disabled") + + def clearConsole(self): + self.consoleTextbox.config(state = "normal") + self.consoleTextbox.delete(1.0, tk.END) + self.consoleTextbox.config(state = "disabled") + def loadAssemblyDisplay(self, filename): try: with open(filename, 'r') as f: content = f.read() + self.assemblyTextbox.config(state = "normal") self.assemblyTextbox.delete(1.0, tk.END) self.assemblyTextbox.insert(tk.END, content) + self.assemblyTextbox.config(state = "disabled") except: pass @@ -196,6 +303,7 @@ def loadAssemblyDisplay(self, filename): def showDisassembly(self, binaryData): try: instructions = self.cpu.decoder.decodeProgram(binaryData) + self.disassemblyTextbox.config(state = "normal") self.disassemblyTextbox.delete(1.0, tk.END) self.disassemblyTextbox.insert(tk.END, "; Disassembly\n") @@ -233,10 +341,13 @@ def showDisassembly(self, binaryData): line = f"{addr:04X}: {hex_bytes:<8} {instStr}\n" lines = line + lines self.disassemblyTextbox.insert(tk.END, lines) + self.disassemblyTextbox.config(state = "disabled") except Exception as e: + self.disassemblyTextbox.config(state = "normal") self.disassemblyTextbox.delete(1.0, tk.END) self.disassemblyTextbox.insert(1.0, f"Disassembly failed: {e}") + self.disassemblyTextbox.config(state = "disabled") def toggleRun(self): @@ -244,10 +355,12 @@ def toggleRun(self): self.running = False self.runButton.config(text = "Run", bg = "green") self.statusLabel.config(text = "Stopped") + self.appendConsole("Run stopped") else: self.running = True self.runButton.config(text = "Stop", bg = "red") self.statusLabel.config(text = "Running...") + self.appendConsole(f"Run started ({self.mode} mode)") self.runContinuous() @@ -268,6 +381,7 @@ def runContinuous(self): self.runButton.config(text = "Run", bg = "green") if self.cpu.halted: self.statusLabel.config(text = "Program halted") + self.appendConsole("Program halted") else: self.statusLabel.config(text = "Stopped") @@ -277,6 +391,7 @@ def stepCpu(self): self.cpu.step() self.updateDisplay() self.statusLabel.config(text = f"Stepped - PC: {self.cpu.programCounter:04X}") + self.appendConsole(f"Stepped - PC: {self.cpu.programCounter:04X}") else: self.statusLabel.config(text = "CPU is halted") @@ -287,16 +402,27 @@ def resetCpu(self): self.cpu.reset() self.updateDisplay() self.statusLabel.config(text = "CPU reset") + self.appendConsole("CPU reset") def hardReset(self): self.running = False self.runButton.config(text = "Run", bg = "green") - self.cpu = CPU8Bit() # Create new CPU instance + if self.mode == 'hardware': + self.cpu = HardwareCPU(enable_signal_logging=True, log_callback=self.appendConsole) + else: + self.cpu = SoftwareCPU(enable_execution_logging=True, log_callback=self.appendConsole) + self.cpu.setSignedMode(self.sevenSegDisplay.signedMode) self.updateDisplay() + self.assemblyTextbox.config(state = "normal") self.assemblyTextbox.delete(1.0, tk.END) + self.assemblyTextbox.config(state = "disabled") + self.disassemblyTextbox.config(state = "normal") self.disassemblyTextbox.delete(1.0, tk.END) + self.disassemblyTextbox.config(state = "disabled") self.statusLabel.config(text = "Hard reset - ready to load program") + self.clearConsole() + self.appendConsole("Hard reset - ready to load program") def updateDisplay(self): @@ -307,11 +433,39 @@ def updateDisplay(self): self.flagsDisplay.updateFlags(state['alu_flags']) self.statusDisplay.updateStatus(state) self.memoryDisplay.updateMemory(state['ram']) - self.sevenSegDisplay.setValue(state['seven_segment'], state['outputEnabled']) + if state.get('seven_segment_patterns'): + self.sevenSegDisplay.setPatterns(state['seven_segment_patterns'], state['outputEnabled']) + else: + self.sevenSegDisplay.setValue(state['seven_segment'], state['outputEnabled']) + self._updateModeIndicator(state) # Sync display mode with CPU mode self.sevenSegDisplay.setMode(state['signedMode']) + def _updateModeIndicator(self, state): + mode = state.get('executionMode', self.mode) + if mode == 'hardware': + self.modeIndicator.config(text = "HARDWARE", bg = "#2E7D32") + self.modeTooltip.set_text( + "Hardware mode:\n" + "- Executes generated microcode step-by-step\n" + "- Cycle count = micro-cycles\n" + "- Console shows control-signal logs\n\n" + "How to switch mode:\n" + "Restart emulator with: python main.py -m software" + ) + else: + self.modeIndicator.config(text = "SOFTWARE", bg = "#1E88E5") + self.modeTooltip.set_text( + "Software mode:\n" + "- Executes instruction behavior directly\n" + "- Cycle count = instruction cycles\n" + "- Console shows instruction logs\n\n" + "How to switch mode:\n" + "Restart emulator with: python main.py -m hardware" + ) + + def _on_display_mode_change(self, *args): signedMode = (self.sevenSegDisplay.modeVar.get() == "Signed") self.cpu.setSignedMode(signedMode) diff --git a/Emulator/gui/widgets.py b/Emulator/gui/widgets.py index f08841f..c7b2d05 100644 --- a/Emulator/gui/widgets.py +++ b/Emulator/gui/widgets.py @@ -8,6 +8,7 @@ def __init__(self, parent, **kwargs): self.value = 0 self.enabled = False self.signedMode = True # Default to signed mode (matching assembler default) + self.rawPatterns = None # Create main frame mainFrame = tk.Frame(self) @@ -97,16 +98,23 @@ def drawDisplay(self): digitSpacing = 80 startX = 10 - for i, digitChar in enumerate(digits): - xOffset = startX + (i * digitSpacing) + if self.rawPatterns is not None: + patterns = self.rawPatterns + for i, patternByte in enumerate(patterns): + xOffset = startX + (i * digitSpacing) + pattern = self._patternFromByte(patternByte) + self.drawSingleDigit(xOffset, pattern, onColor, offColor) + else: + for i, digitChar in enumerate(digits): + xOffset = startX + (i * digitSpacing) - if digitChar.isdigit(): - digitNum = int(digitChar) - pattern = self.patterns.get(digitNum, (0,0,0,0,0,0,0)) - else: - pattern = self.patterns.get(digitChar, (0,0,0,0,0,0,0)) + if digitChar.isdigit(): + digitNum = int(digitChar) + pattern = self.patterns.get(digitNum, (0,0,0,0,0,0,0)) + else: + pattern = self.patterns.get(digitChar, (0,0,0,0,0,0,0)) - self.drawSingleDigit(xOffset, pattern, onColor, offColor) + self.drawSingleDigit(xOffset, pattern, onColor, offColor) # Update info label if self.signedMode: @@ -116,6 +124,23 @@ def drawDisplay(self): self.infoLabel.config(text = f"Value: {self.value} (0x{self.value:02X}) [Unsigned]") + def _patternFromByte(self, patternByte): + # Convert 7-segment byte from Gen7segDriver format to a tuple (a,b,c,d,e,f,g) + return ( + bool(patternByte & 0x02), + bool(patternByte & 0x04), + bool(patternByte & 0x08), + bool(patternByte & 0x10), + bool(patternByte & 0x20), + bool(patternByte & 0x40), + bool(patternByte & 0x80) + ) + + def setPatterns(self, patterns, enabled = True): + self.rawPatterns = list(patterns) if patterns is not None else None + self.enabled = enabled + self.drawDisplay() + def drawSingleDigit(self, xOffset, pattern, onColor, offColor): # Segment coordinates relative to xOffset segments = { @@ -136,6 +161,7 @@ def drawSingleDigit(self, xOffset, pattern, onColor, offColor): def setValue(self, value, enabled = True): + self.rawPatterns = None self.value = value & 0xFF # Ensure 8-bit self.enabled = enabled self.drawDisplay() @@ -298,7 +324,8 @@ def __init__(self, parent, **kwargs): tk.Label(statusFrame, textvariable = self.inst_var, font = ("Courier", 9)).grid(row = 1, column = 1, sticky = "w") # Cycle count - tk.Label(statusFrame, text = "Cycles:", font = ("Arial", 9)).grid(row = 2, column = 0, sticky = "e") + self.cycle_label_var = tk.StringVar(value = "Cycles:") + tk.Label(statusFrame, textvariable = self.cycle_label_var, font = ("Arial", 9)).grid(row = 2, column = 0, sticky = "e") self.cycle_var = tk.StringVar(value = "0") tk.Label(statusFrame, textvariable = self.cycle_var, font = ("Courier", 9)).grid(row = 2, column = 1, sticky = "w") @@ -313,5 +340,14 @@ def updateStatus(self, state): self.inst_var.set(str(state['instructionCount'])) self.cycle_var.set(str(state['cycleCount'])) + cycle_type = state.get('cycleType', '') + execution_mode = state.get('executionMode', '') + if execution_mode == 'hardware' or cycle_type == 'micro': + self.cycle_label_var.set("HW Cycles:") + elif execution_mode == 'software' or cycle_type == 'instruction': + self.cycle_label_var.set("SW Cycles:") + else: + self.cycle_label_var.set("Cycles:") + status_text = "HALTED" if state['halted'] else "Ready" self.halted_var.set(status_text) diff --git a/Emulator/main.py b/Emulator/main.py index 1ad3747..5137da2 100644 --- a/Emulator/main.py +++ b/Emulator/main.py @@ -27,6 +27,8 @@ def main(): parser.add_argument("-ng", "--no-gui", action = "store_true", help = "Run without GUI (command line only)") parser.add_argument("-d", "--debug", action = "store_true", help = "Enable debug output") parser.add_argument("-u", "--unsigned", action = "store_true", help = "Start in unsigned mode (0 to 255)") + parser.add_argument("-m", "--mode", choices=["software", "hardware"], default="software", + help = "Emulator mode: software or hardware") args = parser.parse_args() @@ -40,18 +42,23 @@ def main(): return 1 try: - from core.cpu import CPU8Bit - # autoLoadProgram is already imported at the top + if args.mode == 'hardware': + from core.hardware_cpu import HardwareCPU + cpu = HardwareCPU(enable_signal_logging=True) + else: + from core.software_cpu import SoftwareCPU + cpu = SoftwareCPU() - # Create CPU and load program - cpu = CPU8Bit() cpu.setSignedMode(initialSignedMode) programData = autoLoadProgram(args.program) cpu.loadProgram(programData['binaryData']) + executed = cpu.run() print(f"Loaded program: {args.program}") print(f"Mode: {'Signed (-128 to +127)' if initialSignedMode else 'Unsigned (0 to 255)'}") - print("Program executed successfully") + print(f"Execution mode: {args.mode}") + print(f"Executed {executed} instruction cycles") + print(f"Program halted: {cpu.halted}") return 0 except Exception as e: @@ -63,7 +70,7 @@ def main(): try: # Create and start GUI - app = EmulatorMainWindow() + app = EmulatorMainWindow(mode=args.mode) # Set initial display mode app.cpu.setSignedMode(initialSignedMode)