Added new hardware mode in emulator#70
Conversation
Signed-off-by: Arghya Biswas <arghya@ialoy.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new “hardware” execution mode to the emulator, allowing programs to run via generated microcode (micro-cycle stepping) in addition to the existing direct/instruction-level software execution path.
Changes:
- Added a
--mode {software,hardware}CLI switch and propagated mode selection into the GUI. - Implemented a new
HardwareCPUthat loads microcode banks + 7-seg ROM and steps micro-cycles with optional signal logging. - Updated GUI widgets/status to display mode, cycle labeling, console output, and hardware 7-seg raw segment patterns.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| Emulator/main.py | Adds CLI --mode and CLI execution output updates; passes mode into GUI. |
| Emulator/gui/widgets.py | Supports rendering raw 7-seg patterns and improves cycle label text based on mode/cycle type. |
| Emulator/gui/main_window.py | Adds mode indicator + console, wires CPU logging, and supports hardware 7-seg patterns in UI. |
| Emulator/core/software_cpu.py | Renames CPU class to SoftwareCPU, adds execution logging hooks, and exposes mode metadata via getState(). |
| Emulator/core/hardware_cpu.py | New microcode-driven CPU implementation (micro-cycle stepping, signal logging, 7-seg ROM patterns). |
| Emulator/core/decoder.py | Adjusts register-field decoding for 4-bit opcodes to match DDSS upper-nibble layout. |
| .gitignore | Ignores .vscode/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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}") |
There was a problem hiding this comment.
In CLI mode, cpu.run() returns a count of executed steps, but that means instruction-cycles in software mode and micro-cycles in hardware mode. Printing this unconditionally as “instruction cycles” is misleading; use args.mode/cpu.getState()['cycleType'] to label the count appropriately (or print both instructionCount and cycleCount for hardware).
There was a problem hiding this comment.
Fixed in commit ba6c9ab. The CLI output now labels the counts appropriately based on mode:
- Hardware mode: prints
Executed N instructions (M micro-cycles)usinggetState()['instructionCount']andgetState()['cycleCount'] - Software mode: prints
Executed N instruction cyclesusinggetState()['instructionCount']
| # Assembly code display | ||
| assemblyCodeFrame = tk.LabelFrame(codeFrame, text = "Assembly Code", font = ("Arial", 10, "bold")) | ||
| assemblyCodeFrame.pack(fill = "both",side = "right", expand = True, pady = 10) | ||
|
|
||
| self.assemblyTextbox = tk.Text(assemblyCodeFrame, width = 10, height = 15, font = ("Courier", 9)) | ||
| assemblyCodeScrollbar = tk.Scrollbar(assemblyCodeFrame, orient = "vertical", command = self.assemblyTextbox.yview) | ||
| self.assemblyTextbox.configure(yscrollcommand = assemblyCodeScrollbar.set) | ||
|
|
||
| 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")) | ||
| disassemblyCodeFrame.pack(fill = "both", side = "right", expand = True, pady = 10) | ||
| self.disassemblyTextbox = tk.Text(disassemblyCodeFrame, width = 10, height = 15, font = ("Courier", 9)) | ||
| disassemblyCodeScrollbar = tk.Scrollbar(disassemblyCodeFrame, orient = "vertical", command = self.disassemblyTextbox.yview) | ||
| self.disassemblyTextbox.configure(yscrollcommand = disassemblyCodeScrollbar.set) | ||
|
|
||
| self.disassemblyTextbox.pack(side = "left", fill = "both", expand = True) | ||
| disassemblyCodeScrollbar.pack(side = "right", fill = "y") | ||
| self.disassemblyTextbox.config(state = "disabled") |
There was a problem hiding this comment.
assemblyTextbox/disassemblyTextbox are set to state="disabled", but Emulator/main.py writes to these widgets directly during startup program load. Writing to a disabled tk.Text raises TclError; either don’t disable by default, or centralize updates via helper methods that temporarily set state="normal" before delete/insert.
| if signals.get('MdI'): | ||
| self.memoryAddress = self.bus & 0x0F | ||
|
|
||
| if signals.get('MeI') and signals.get('MdI'): |
There was a problem hiding this comment.
RAM writes are currently gated on MeI and MdI being asserted in the same micro-cycle. The microcode asserts MdI (address latch) and later asserts MeI (write enable) in different cycles (e.g. InsSAV), so stores will never write. Latch memoryAddress when MdI is high, and perform the write when MeI is high (using the latched address).
| if signals.get('MeI') and signals.get('MdI'): | |
| if signals.get('MeI'): |
| 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 | ||
|
|
There was a problem hiding this comment.
signedMode is forced to True whenever SpUd is not asserted. Since the current microcode never drives SpUd high, this will override setSignedMode(False) and make unsigned mode unusable in hardware mode. Consider only updating signedMode when SpUd is explicitly driven (or treat it as a latched state), and compute 7-seg patterns after the final signedMode for the cycle is known.
| 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('SpUd'): | |
| self.signedMode = False | |
| if signals.get('Seg7E') or signals.get('SpO') or signals.get('SpL'): | |
| self._update_seven_segment_patterns() |
| 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'): |
There was a problem hiding this comment.
self.sevenSegmentPatterns is reset to None every micro-step, so the GUI will usually fall back to setValue() (software segment patterns) instead of rendering the hardware ROM-driven patterns. Keep the last computed patterns until the display value/mode changes (or recompute patterns every step from sevenSegmentValue + signedMode).
No description provided.