diff --git a/src/simudator/cli/cli.py b/src/simudator/cli/cli.py index 0578f640d35540598e04d9638f5caf96fca8da80..7eb5e46ddcf8349ff805b61a6b647575bdbccca0 100644 --- a/src/simudator/cli/cli.py +++ b/src/simudator/cli/cli.py @@ -73,6 +73,13 @@ class CLI: case ["rc"] | ["run_continuously"]: self.processor.run_continuously() + if self.processor.breakpoint_reached: + bp = self.processor.last_breakpoint + print(f"Reached breakpoint: {bp}") + + if self.processor.should_halt(): + print("The processor halted") + case ["r"] | ["resets"]: # self.processor.load_cycle(0) @@ -86,6 +93,13 @@ class CLI: try: for _ in range(int(user_input.split()[1])): self.processor.do_tick() + if self.processor.is_stopped: + break + if self.processor.breakpoint_reached: + bp = self.processor.last_breakpoint + print(f"Reached breakpoint: {bp}") + if self.processor.should_halt(): + print("The processor halted") except ValueError: print("Invalid value") diff --git a/src/simudator/core/processor.py b/src/simudator/core/processor.py index 0199e8edf0f39f3a74e2c49885ac4677cf1a23ef..c1983ca8f6da33b39d5c7bc9fd68255570373c02 100644 --- a/src/simudator/core/processor.py +++ b/src/simudator/core/processor.py @@ -50,8 +50,10 @@ class Processor: def do_tick(self) -> None: """ - Does one clock-cycle of the processor by running each modules - tick function, then handels the following updates. + Simulate one clock cycle of the processor. + + Run each module's tick function, then handle the following updates. + Also check for breakpoints that are reached in this cycle. """ if len(self.module_history) > self.clock - self.removed_cycles: # If a previous stored cycle has been loaded, discard @@ -60,6 +62,7 @@ class Processor: self.module_history = self.module_history[0:self.clock] self.signal_history = self.signal_history[0:self.clock] + self.unstop() self.save_cycle() self.clock += 1 @@ -73,6 +76,10 @@ class Processor: module = self.update_queue.pop(0) module.update_logic() + self.stop_at_breakpoints() + if self.should_halt(): + self.stop() + def get_current_instrution(self) -> str: """ Return the current instruction. Useful for pipeline diagram. @@ -89,31 +96,28 @@ class Processor: raise NotImplemented - def run_continuously(self): + def run_continuously(self) -> None: """ - Runs the processor until it halts. + Run the processor until it halts, is stopped or reaches a breakpoint. """ - while not self.should_halt() and not self.is_stopped: + self.unstop() + while not self.is_stopped: self.do_tick() - # Print all reached breakpoints then exit the loop - self.breakpoint_reached = False - for _, bp in self.breakpoints.items(): - #TODO: Can make this more efficient by only checking enabled breakpoints - if bp.is_break() and bp.is_enabled: - self.breakpoint_reached = True - self.last_breakpoint = bp - - if self.breakpoint_reached: - return - def should_halt(self) -> bool: return False def stop(self) -> None: + """ + Signal to stop the execution of the processor until the processor is + instructed to run continuously or do some ticks again. + """ self.is_stopped = True def unstop(self) -> None: + """ + Reset the stop execution signal. + """ self.is_stopped = False def is_new_instruction(self) -> bool: @@ -125,7 +129,24 @@ class Processor: """ return self.new_instruction - def reset(self) -> None: + def stop_at_breakpoints(self) -> None: + """ + Stop the execution if any breakpoint has been reached during this cycle. + + Also record the breakpoint that was reached. + """ + self.breakpoint_reached = False + for _, bp in self.breakpoints.items(): + #TODO: Can make this more efficient by only checking enabled breakpoints + if bp.is_break() and bp.is_enabled: + self.breakpoint_reached = True + self.last_breakpoint = bp + + if self.breakpoint_reached: + self.stop() + + + def reset(self): """ Resets all values in the processor and then does one propogation to the signals. @@ -242,6 +263,8 @@ class Processor: is given to each module in the function 'load_from_str'. Each module will thus load itself. """ + self.reset() + file = open(file_path) # Ensure we are at the start of the file diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py index 44fd5492220cd4af0528e9b22b310fbe9c077986..baec07a7241f9d018a6a382add85a8572af63546 100644 --- a/src/simudator/gui/gui.py +++ b/src/simudator/gui/gui.py @@ -2,6 +2,7 @@ import ast import json import sys from json import JSONDecodeError +from threading import Thread from qtpy import QtCore, QtWidgets from qtpy.QtCore import QPointF @@ -92,7 +93,7 @@ class GUI(QMainWindow): and added individually. """ - halted_signal = pyqtSignal(int) + cpu_tick_signal = pyqtSignal(int) HALT_MESSAGE_THRESHOLD = 100 def __init__(self, cpu: Processor): @@ -116,7 +117,15 @@ class GUI(QMainWindow): self.threadpool = QtCore.QThreadPool() # Signal to tell gui when cpu has halted - self.halted_signal.connect(self.cpuHaltedFunction) + self.cpu_tick_signal.connect(self.handleCpuTick) + + # Used to set if all values in the gui should be updated each tick + # or only the clock counter + self.update_all_values = False + + # Used to set the update delay + # Useful when watching the values being updated while running + self.update_delay = 0.00 # Used to lock some actions in ui when cpu is running in another thread # Using the cpu's internal status directly could case problems @@ -215,9 +224,22 @@ class GUI(QMainWindow): self.breakpoint_action.setStatusTip("Open breakpoint window.") self.breakpoint_action.triggered.connect(self.openBreakpointWindow) + # Create 'update value' window button + self.update_value_action = QAction("Update values while running", self, checkable=True) + self.update_value_action.setChecked(False) + self.update_value_action.setStatusTip("Toggle value updates while running.") + self.update_value_action.triggered.connect(self.toggle_value_update_on_run) + + # Create 'set delay' window button + self.set_delay_action = QAction("Set update delay", self) + self.set_delay_action.setStatusTip("Sets the delay between each update when the cpu is running.") + self.set_delay_action.triggered.connect(self.set_update_delay) + # Create Tools menu for tool actions actions tools_menu = menu.addMenu("&Tools") tools_menu.addAction(self.breakpoint_action) + tools_menu.addAction(self.update_value_action) + tools_menu.addAction(self.set_delay_action) # Add run button on toolbar arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay) @@ -333,14 +355,24 @@ class GUI(QMainWindow): signal.connect(self.addLambdaBreakpoint) case "UPDATE": signal.connect(self.updateCpuListeners) + case "CLOCKUPDATE": + signal.connect(self.updateCpuClockCycle) def updateCpuListeners(self) -> None: """ - Updates the graphics items in the scene, the clock and the pipeline diagram. + Updates the graphics items in the scene and the clock. Used after the cpu has run or when the user has edited somehting. """ self.cpu_graphics_scene.updateGraphicsItems() + self.updateCpuClockCycle() + + def updateCpuClockCycle(self) -> None: + """ + Update the clock cycle counter. + + Used while the program is running to show the user nothing has crashed. + """ self.clock_label.setText("Clockcycle: " + str(self.cpu.get_clock())) def lambdaBreakpointDialog(self) -> None: @@ -470,10 +502,8 @@ class GUI(QMainWindow): steps = self.jump_value_box.value() self.cpu_running = True self.setDisabledWhenRunning(True) - self.cpu.unstop() - simultaion_thread = RunThread(self.cpu, self.halted_signal, False, steps) - self.threadpool.start(simultaion_thread) - self.updateCpuListeners() + simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay, False, steps) + self.threadpool.start(simulation_thread) def stepAsmToolBarButtonClick(self): """ @@ -504,34 +534,43 @@ class GUI(QMainWindow): # Create own thread for cpu simulation so gui dosent freeze self.cpu_running = True self.setDisabledWhenRunning(True) - self.cpu.unstop() - simultaion_thread = RunThread(self.cpu, self.halted_signal) - self.threadpool.start(simultaion_thread) + simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay) + self.threadpool.start(simulation_thread) @Slot(int) - def cpuHaltedFunction(self, steps: int) -> None: + def handleCpuTick(self, steps: int) -> None: """ - Called from other thread when cpu has halted. Will inform the user and update visuals. + Called from other thread after every cpu tick. + Will inform the user and update visuals. """ - # If a breakpoint halted the program inform thr user - if self.cpu.breakpoint_reached: - self.messageBox("Reached breakpoint: " + self.cpu.last_breakpoint.__str__()) - # Only show halted message for larger steps that take time - # This is done so a user dosent have to close - # the message box after every small step - elif steps > self.HALT_MESSAGE_THRESHOLD: - self.messageBox("The processor halted.") - self.updateCpuListeners() - self.cpu_running = False - self.setDisabledWhenRunning(False) + # Update cpu clock counter every tick + self.updateCpuClockCycle() + + if self.update_all_values: + self.updateCpuListeners() + + # A signal of 0 steps signifies end of execution, i.e. the CPU has + # halted or run the specified amount of ticks + # => Enable the relevant parts of the GUI again + if steps == 0: + self.cpu_running = False + self.setDisabledWhenRunning(False) + self.updateCpuListeners() + + # Inform user of reached break point + if self.cpu.breakpoint_reached: + self.messageBox("Reached breakpoint: " + self.cpu.last_breakpoint.__str__()) + + # Inform user of halt + if self.cpu.should_halt(): + self.messageBox("The processor halted.") def stopToolBarButtonClick(self) -> None: """ Tells the cpu to stop. It will then stop at an appropriate in its own thread. """ self.cpu.stop() - self.updateCpuListeners() # TODO: What is the difference between folderSaveDialog and folderLoadDialog? @@ -795,7 +834,21 @@ class GUI(QMainWindow): self.breakpoint_window = BreakpointWindow(self.cpu) self.breakpoint_window.show() - def showPortNamesBarButtonClick(self) -> None: + def toggle_value_update_on_run(self): + """ + Toggles whether all values or only clock cycle is being updated each tick. + """ + self.update_all_values = not self.update_all_values + + def set_update_delay(self): + """ + Sets the update delay for the visual updates while the cpu is running. + """ + delay, ok = QInputDialog.getDouble(self, "Input Dialog", "Enter a float value:", decimals=5) + if ok: + self.update_delay = delay + + def showPortNamesBarButtonClick(self): """ Toggles showing port names in the graphics scene. """ diff --git a/src/simudator/gui/run_continuously_thread.py b/src/simudator/gui/run_continuously_thread.py index 01d45e46e237c662bc1512260e81f4cba72979c1..1f1ef5beba2c2f1c38d27526480789ef2de62b93 100644 --- a/src/simudator/gui/run_continuously_thread.py +++ b/src/simudator/gui/run_continuously_thread.py @@ -1,33 +1,47 @@ -from qtpy.QtCore import QRunnable +import time +from qtpy.QtCore import QRunnable class RunThread(QRunnable): """ - This class is used to run the cpu on a seperate thread. + This class is used to run the simulated cpu several ticks or continuously on + a seperate thread. This allows the user to interact with the GUI while the + simulation is running. - This allows the user to interact with the GUI will th cpu is running. - When the cpu halts this thread will emit to it's given signal - the GUI can then handel what should happend after execution on its own. + After each cpu tick, this thread will emit to its given QT signal so that + the GUI can update itself and possibly inform the user of when the execution + has halted. """ - def __init__(self, cpu, signal, run_continuously=True, steps=0): + def __init__(self, cpu, signal, delay: float, run_continuously=True, steps=0): super().__init__() self.cpu = cpu self.signal = signal self.run_continuously = run_continuously self.steps = steps + self.delay = delay def run(self): if self.run_continuously: - self.cpu.run_continuously() + self.cpu.unstop() + while not self.cpu.is_stopped: + self.cpu.do_tick() + self.signal.emit(1) + time.sleep(self.delay) else: for _ in range(self.steps): self.cpu.do_tick() + self.signal.emit(1) + time.sleep(self.delay) + + if self.cpu.is_stopped: + break - self.signal.emit(self.steps) + # Signal end of execution as having run 0 ticks + self.signal.emit(0) def run_asm(self): self.cpu.run_asm(self.steps) - self.signal.emit(self.steps) + self.signal.emit(0)