diff --git a/.gitignore b/.gitignore
index 31a2ae473b61b58e212f445c2e5511401fc19ff1..dae54c3ada7b570b393b6ec95dc20570e1ef06c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,10 @@ test_save_to_file
 test_save_to_file_layout
 cpu_save_file
 
+# mock files
+mock*
+dummy
+
 *~
 *.autosave
 *.a
diff --git a/dummy b/dummy
new file mode 100644
index 0000000000000000000000000000000000000000..50f62b930aa7bfb53b977188d487bb992fcd103b
--- /dev/null
+++ b/dummy
@@ -0,0 +1,3 @@
+a:
+value:: 1
+
diff --git a/mock_file b/mock_file
new file mode 100644
index 0000000000000000000000000000000000000000..704d1e4125be1a06e285f4c73c2210b690a96287
--- /dev/null
+++ b/mock_file
@@ -0,0 +1,75 @@
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
+a:
+value:: 1
+
diff --git a/mock_file.txt b/mock_file.txt
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/simudator/cli/cli.py b/src/simudator/cli/cli.py
index 8cece34ab1733200d85c52e342092bc5f697c8fc..25017547485eddca37d2b7846442f12c377d712f 100644
--- a/src/simudator/cli/cli.py
+++ b/src/simudator/cli/cli.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 import ast
 from os.path import exists, isdir
 
+from simudator.cli.printing import pretty_print, pretty_print_verbose
 from simudator.core.processor import Processor
 
 HELP_TEXT = """Here is a list of possible commands:
@@ -195,12 +196,12 @@ class CLI:
 
                 case ["pp"] | ["pretty print"]:
                     # pretty print the modules of the processor
-                    self._processor.pretty_print()
+                    pretty_print(self._processor)
 
                 case ["ppv"] | ["pretty print verbose"]:
                     # pretty print the modules of the processor
                     # with all available information
-                    self._processor.pretty_print_verbose()
+                    pretty_print_verbose(self._processor)
 
                 # Breakpoints -------------------------------------------------
                 case ["p", "br"] | ["print", "breaks"]:
diff --git a/src/simudator/cli/printing.py b/src/simudator/cli/printing.py
new file mode 100644
index 0000000000000000000000000000000000000000..42530c15ce3f01555278069070a2a8767fafb0af
--- /dev/null
+++ b/src/simudator/cli/printing.py
@@ -0,0 +1,262 @@
+from simudator.core.module import Module
+from simudator.core.modules.memory import Memory
+from simudator.core.processor import Processor
+from simudator.processor.mia.modules.bus import Bus
+from simudator.processor.mia.modules.micro_memory import MicroMemory
+
+
+def pretty_print(processor: Processor) -> None:
+    """Print the processor state in a readable and compact format."""
+    pretty_print_verbose(processor, processor._ignore_keys)
+
+
+def pretty_print_verbose(
+    processor: Processor, ignore_keys: list[str] | None = None
+) -> None:
+    """
+    Print the most relevant information about each module in a compact and
+    readable format.
+
+    State variables of modules can be ignored with the optional argument.
+
+    Parameters
+    ----------
+    ignore_keys : list[str]
+        List of names of state variables of modules to exclude when
+        printing module states.
+    """
+    # TODO: ignore keys per module and not for all modules
+    memory_modules = []
+    other_modules = []
+
+    if ignore_keys is None:
+        ignore_keys = []
+
+    # TODO: remove isinstance(module, micro_memory)
+    for module in processor.get_modules():
+        if not isinstance(module, Bus):
+            if isinstance(module, Memory) or isinstance(module, MicroMemory):
+                memory_modules.append(module)
+            else:
+                other_modules.append(module)
+
+    # sort the modules by name to ensure that they appear in the
+    # same order
+    memory_modules.sort(key=lambda x: x.name, reverse=True)
+    other_modules.sort(key=lambda x: x.name, reverse=True)
+
+    # specify which keys to ignore from the modules 'get_state'
+    # function
+    # TODO: ignore fields per module, this will ignore 'mask'
+    # for every module
+
+    module_to_line_length = {}
+    # get the longest line length for all other_modules
+    for module in other_modules:
+        # +3 for padding
+        line_len = module.get_longest_line_len(ignore_keys) + 3
+
+        # TODO: what to do if two or more modules has the same name
+        module_to_line_length[module] = line_len
+
+    groups = group_pp_modules(module_to_line_length)
+
+    print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
+
+    # print each group separate
+    for group in groups:
+        pretty_print_names(group)
+
+        # Build a new string containing the information to show
+        # the user
+        for row in range(get_most_fields(group, ignore_keys)):
+            string = ""
+            total_padding = 0
+
+            # Get the information from each module
+            for module in group:
+                module_state = module.get_state()
+                keys = fields_to_list(module)
+
+                # Get the keys that we want to print to the user
+                # If this is not done, the keys we want to print
+                # could end up on an index that is higher than the
+                # number of rows printed and will therefore be missed
+                real_keys = [key for key in keys if key not in ignore_keys]
+
+                # Dont go out of index
+                # Needed since one module might want to print 4
+                # fields and another only 1
+                if row < len(real_keys) and real_keys[row] not in ignore_keys:
+                    string += real_keys[row] + ": " + str(module_state[real_keys[row]])
+
+                # pad the string so each string has the same length
+                total_padding += module_to_line_length[module]
+                string = string.ljust(total_padding)
+                # replace last to chars with a separator and padding
+                string = string[0:-2] + "| "
+
+            print(string)
+        print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
+
+    for memory_module in memory_modules:
+        pretty_print_memory(memory_module)
+
+
+def pretty_print_memory(module: Memory) -> None:
+    """Print a memory module in a compact and readable format.
+
+    Parameters
+    ----------
+    module : Memory
+        Memory module to print the state of.
+    """
+    print(module.name)
+    print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
+
+    longest_line_len = module.get_longest_line_len()
+    # longest_line_len = processor.get_longest_memory_value(module.memory)
+    string = ""
+    last_mem_len = module.get_largest_mem_adr()
+
+    for i, value in enumerate(module.get_state()["memory"]):
+
+        # create a new string containing the address and the value
+        # of the memory address formatted to fit with the largest
+        # address and largest memory value
+        new_row = (str(i).ljust(last_mem_len) + ": " + str(value)).ljust(
+            longest_line_len + last_mem_len + 3
+        ) + "|"
+
+        # only add the string if there is space for it, else
+        # print the string and start a new
+        if len(string + new_row) + 1 > Processor.MAX_LINE_LEN:
+            print(string)
+            string = new_row
+        else:
+            # First iteration string will be
+            # empty and should not be padded
+            if string:
+                string += " " + new_row
+            else:
+                string = new_row
+    print(string)
+    print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
+    print()
+
+
+def pretty_print_names(module_to_line_length: dict[Module, int]) -> None:
+    """
+    Print the name of the modules in one row with formatting.
+
+    Adds spacing between the names so that the longest state variable of
+    each module has space to be printed below the name.
+
+    Parameters
+    ----------
+    module_to_line_length : dict[Module, int]
+        Mapping from module to length of the longest state variable of
+        the module.
+    """
+    name_string = ""
+    total_len = 0
+    for module in module_to_line_length:
+        name_string += module.name
+        total_len += module_to_line_length[module]
+        name_string = name_string.ljust(total_len)
+        name_string = name_string[0:-2] + "| "
+
+    print(name_string)
+
+
+def group_pp_modules(
+    module_to_line_length: dict[Module, int]
+) -> list[dict[Module, int]]:
+    """Group the modules to be pretty printed into groups with a
+    total line length lower than 'Processor.MAX_LINE_LEN'.
+
+    Parameters
+    ----------
+    module_to_line_length : dict[Module, int]
+        Mapping from module to length of the longest state variable of
+        the module (takes as the line length).
+
+    Returns
+    -------
+    list[dict[Module, int]]
+        List of mappings from module to line length, each mapping
+        representing a group of modules for printing.
+    """
+
+    groups = [{}]
+    group_index = 0
+    line_len = 0
+    for module in module_to_line_length:
+
+        # Make sure the line is not to long
+        if line_len + module_to_line_length[module] < Processor.MAX_LINE_LEN:
+            line_len += module_to_line_length[module]
+            groups[group_index][module] = module_to_line_length[module]
+
+        # If it would have been, start a new group
+        else:
+            groups.append({})
+            group_index += 1
+            groups[group_index][module] = module_to_line_length[module]
+            line_len = module_to_line_length[module]
+    return groups
+
+
+def fields_to_list(module: Module, ignore_keys=[]) -> list[str]:
+    """
+    Return a list containing all state variable names, excluding module
+    name, for a module.
+    Optional argument to ignore specific state variables.
+
+    Parameters
+    ----------
+    module : Module
+        Module to get state variable names from.
+    ignore_keys : list[str]
+        List of state variable names to exclude.
+
+    Returns
+    -------
+    list[str]
+        List of state variable names, excluding module name, of a module.
+    """
+    return [
+        key for key in module.get_state() if key != "name" and key not in ignore_keys
+    ]
+
+
+def get_most_fields(modules: dict[Module, int], ignore_keys=None) -> int:
+    """Get the maximum number of state variables among all modules.
+
+    Can optionally ignore keys.
+
+    Parameters
+    ----------
+    modules : dict[Module, int]
+        Mapping from module to length of the longest state variable of
+        the module. Used as a list of modules to take the maximum over.
+    ignore_keys : list[str]
+        State variables to exclude.
+
+    Returns
+    -------
+    int
+        The maximum of the number of module state variables among all
+        modules of the processor.
+    """
+
+    if ignore_keys is None:
+        ignore_keys = []
+
+    fields = 0
+    for module in modules:
+        module_fields = len(fields_to_list(module, ignore_keys))
+        if module_fields > fields:
+            fields = module_fields
+
+    return fields
diff --git a/src/simudator/core/processor.py b/src/simudator/core/processor.py
index 061bec12e280e77fa84bd28235f918e781fec6db..2485c5389b0f328ace9742e30eb5b25a93e485de 100644
--- a/src/simudator/core/processor.py
+++ b/src/simudator/core/processor.py
@@ -7,9 +7,6 @@ from simudator.core.breakpoint_lambda import LambdaBreakpoint
 from simudator.core.breakpoint_memory import MemoryBreakpoint
 from simudator.core.breakpoint_state import StateBreakpoint
 from simudator.core.module import Module
-from simudator.core.modules.memory import Memory
-from simudator.processor.mia.modules.bus import Bus
-from simudator.processor.mia.modules.micro_memory import MicroMemory
 
 from .signal import Signal
 
@@ -19,30 +16,38 @@ class Processor:
     Main class for controlling the processor.
 
     Uses modules and signals to simulate processor components behaviour.
+
+    Attributes
+    ----------
+    is_stopped : bool
+        Used to indicate that the processor has stopped, e.g. from a halt
+        instruction.
+    breakpoint_reached : bool
+        Used to indicate that the processor has reached a breakpoint at
+        the current clock cycle.
+    last_breakpoint : Breakpoint | None
+        Stores the last reached breakpoint if any.
+
     """
 
     __slots__ = (
-        "modules",
-        "signals",
-        "clock",
-        "update_queue",
-        "module_history",
-        "signal_history",
-        "breakpoint_id_counter",
-        "breakpoints",
+        "_modules",
+        "_signals",
+        "_clock",
+        "_update_queue",
+        "_module_history",
+        "_signal_history",
+        "_breakpoint_id_counter",
+        "_breakpoints",
         "breakpoint_reached",
         "last_breakpoint",
-        "cycles_to_save",
-        "removed_cycles",
-        "max_line_len",
-        "line_separator",
+        "_removed_cycles",
         "is_stopped",
-        "new_instruction",
-        "current_instructions",
-        "ignore_keys",
-        "lambdas",
-        "assembly_cycles",
-        "line_separator",
+        "_new_instruction",
+        "_current_instructions",
+        "_ignore_keys",
+        "_lambdas",
+        "_assembly_cycles",
     )
 
     LINE_SEPARATOR = "-"
@@ -51,47 +56,42 @@ class Processor:
 
     def __init__(self) -> None:
         # For simulation
-        self.modules: dict[str, Module] = dict()
-        self.signals = []
-        self.clock = 0
-        self.update_queue = []
-        self.is_stopped = False
+        self._modules: dict[str, Module] = dict()
+        self._signals: list[Signal] = []
+        self._clock: int = 0
+        self._update_queue: list[Module] = []
+        self.is_stopped: bool = False
 
         # Breakpoint handling
-        self.breakpoint_id_counter = 1
-        self.breakpoints: dict[int, Breakpoint] = {}
-        self.breakpoint_reached = False
-        self.last_breakpoint = None
-        self.lambdas: dict[str, Callable[..., bool]] = {}
+        self._breakpoint_id_counter: int = 1
+        self._breakpoints: dict[int, Breakpoint] = {}
+        self.breakpoint_reached: bool = False
+        self.last_breakpoint: Breakpoint | None = None
+        self._lambdas: dict[str, Callable[..., bool]] = {}
 
         # Saving processor state for saving/loading to file and undoing ticks
-        self.removed_cycles = 0
-        self.assembly_cycles = [0]  # Map asm instruction to clock cycle
-        self.module_history: list[dict[str, dict[str, Any]]] = []
-        self.signal_history: list[list] = []  # TODO: Is this needed?
+        self._removed_cycles: int = 0
+        self._assembly_cycles: list[int] = [0]  # Map asm instruction to clock cycle
+        self._module_history: list[dict[str, dict[str, Any]]] = []
+        self._signal_history: list[list] = []  # TODO: Is this needed?
 
         # For showing which instructions are being done and signalling
         # the start of a new one
-        self.new_instruction = False
+        self._new_instruction: bool = False
 
         # It is the responsibility of the CPU to tell the gui:s pipeline
         # diagram what information to be displayed and how to display it.
         # Thus each CPU keeps track of its current instructions together with
         # each instructions position in the pipeline diagram.
-        self.current_instructions: list[tuple[str, int, int]] = []
+        self._current_instructions: list[tuple[str, int, int]] = []
 
         # TODO: keeping track of what pieces of info not to show
         # show not be done at the processor level.
         # Maybe implement a 'get_pretty_print_state' at module
         # level?
-        self.ignore_keys = [
+        self._ignore_keys: list[str] = [
             "bit_length",
             "mask",
-            "increment",
-            "read_from_bus",
-            "read_from_uADR",
-            "decrement_by_one",
-            "bus_id",
         ]
 
     def do_tick(self) -> None:
@@ -101,25 +101,25 @@ class Processor:
         Also check for breakpoints that are reached in this cycle, and save
         clock cycles when new assembly instructions are started.
         """
-        if len(self.module_history) > self.clock - self.removed_cycles:
+        if len(self._module_history) > self._clock - self._removed_cycles:
             # If a previous stored cycle has been loaded, discard
             # all stored cycles from that cycle and onward in
             # the history of saved cycles
-            self.module_history = self.module_history[0 : self.clock]
-            self.signal_history = self.signal_history[0 : self.clock]
+            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
+        self._clock += 1
 
-        for module in self.modules.values():
+        for module in self._modules.values():
             module.update_register()
 
-        for module in self.modules.values():
+        for module in self._modules.values():
             module.output_register()
 
-        while self.update_queue:
-            module = self.update_queue.pop(0)
+        while self._update_queue:
+            module = self._update_queue.pop(0)
             module.update_logic()
 
         self.stop_at_breakpoints()
@@ -129,11 +129,11 @@ class Processor:
         # Set new_instruction and save clock cycle for each
         # new assembly instruction
         if self.is_new_instruction():
-            self.new_instruction = True
-            self.current_instructions = self.get_current_instructions()
-            self.assembly_cycles.append(self.get_clock())
+            self._new_instruction = True
+            self._current_instructions = self.get_current_instructions()
+            self._assembly_cycles.append(self.get_clock())
         else:
-            self.new_instruction = False
+            self._new_instruction = False
 
     def get_current_instructions(self) -> list[tuple[str, int, int]]:
         """Return a list of the current instructions with their positions.
@@ -207,14 +207,14 @@ class Processor:
             Number of assembly instructions to undo.
         """
 
-        current_clock_cycle = self.clock
-        index = len(self.assembly_cycles)
-        saved_cycle = self.assembly_cycles[index - 1]
+        current_clock_cycle = self._clock
+        index = len(self._assembly_cycles)
+        saved_cycle = self._assembly_cycles[index - 1]
 
         # Make sure we are undoing the instruction(s) we are currently on
         while saved_cycle >= current_clock_cycle:
             index -= 1
-            saved_cycle = self.assembly_cycles[index - 1]
+            saved_cycle = self._assembly_cycles[index - 1]
 
         index -= num_instructions
 
@@ -222,7 +222,7 @@ class Processor:
         if index < 0:
             raise IndexError
 
-        clockcycle = self.assembly_cycles[index]
+        clockcycle = self._assembly_cycles[index]
 
         self.load_cycle(clockcycle)
 
@@ -230,7 +230,7 @@ class Processor:
         # load the start state. This is done since we only append clock
         # cycles to the list self.assembly_cycles when we reach a new state
         # that has uPC set to 0, which wont happen when we load a new file.
-        self.assembly_cycles = self.assembly_cycles[: index + 1]
+        self._assembly_cycles = self._assembly_cycles[: index + 1]
 
     def run_continuously(self) -> None:
         """
@@ -275,7 +275,7 @@ class Processor:
         Should be implemented per CPU, return field new_instruction. It is up
         to each processor to set this field to True/False correctly.
         """
-        return self.new_instruction
+        return self._new_instruction
 
     def stop_at_breakpoints(self) -> None:
         """Stop the execution if any breakpoint has been reached during this
@@ -284,7 +284,7 @@ class Processor:
         Also record the breakpoint that was reached.
         """
         self.breakpoint_reached = False
-        for _, bp in self.breakpoints.items():
+        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:
@@ -300,18 +300,18 @@ class Processor:
         This resets all modules and removes any saved states for undoing clock
         cycles. A round of value propagation is done to reset signals too.
         """
-        self.clock = 0
-        self.module_history.clear()
-        self.signal_history.clear()
-        self.removed_cycles = 0
-        self.assembly_cycles = [0]
+        self._clock = 0
+        self._module_history.clear()
+        self._signal_history.clear()
+        self._removed_cycles = 0
+        self._assembly_cycles = [0]
 
-        for module in self.modules.values():
+        for module in self._modules.values():
             module.reset()
             module.output_register()
 
-        while self.update_queue:
-            module = self.update_queue.pop(0)
+        while self._update_queue:
+            module = self._update_queue.pop(0)
             module.update_logic()
 
     def add_modules_to_update(self, module: Module) -> None:
@@ -327,8 +327,8 @@ class Processor:
         Module.update_logic :
             Method for updating a module.
         """
-        if module not in self.update_queue:
-            self.update_queue.append(module)
+        if module not in self._update_queue:
+            self._update_queue.append(module)
 
     def add_module(self, module: Module) -> None:
         """Add module to be simulated by the processor.
@@ -338,7 +338,7 @@ class Processor:
         module : Module
             Module to add.
         """
-        self.modules[module.name] = module
+        self._modules[module.name] = module
 
     def get_module(self, name: str) -> Module:
         """Get module with specific name.
@@ -353,7 +353,7 @@ class Processor:
         Module
             The module with the specified name.
         """
-        return self.modules[name]
+        return self._modules[name]
 
     def get_modules(self) -> list[Module]:
         """Get list of all modules.
@@ -363,7 +363,7 @@ class Processor:
         list[Module]
             List of all modules in the processor.
         """
-        return list(self.modules.values())
+        return list(self._modules.values())
 
     def add_signals(self, signals: list[Signal]) -> None:
         """Add signals to the processor.
@@ -373,7 +373,7 @@ class Processor:
         list[Signal]
             List of signals to add for simulation.
         """
-        self.signals += signals
+        self._signals += signals
 
     def get_clock(self) -> int:
         """Get the current clockcycle number.
@@ -383,7 +383,7 @@ class Processor:
         int
             Current clock cycle number of the processor.
         """
-        return self.clock
+        return self._clock
 
     def set_clock(self, value: int) -> None:
         """Set current clockcycle number.
@@ -393,7 +393,7 @@ class Processor:
         value : int
             Cycle number to set the clock to.
         """
-        self.clock = value
+        self._clock = value
 
     def save_cycle(self) -> None:
         """
@@ -404,13 +404,13 @@ class Processor:
 
         # Only save a specified number of cycles,
         # saving every cycle can easily eat all ram
-        if len(self.module_history) > Processor.CYCLES_TO_SAVE:
-            self.module_history.pop(0)
-            self.removed_cycles += 1
+        if len(self._module_history) > Processor.CYCLES_TO_SAVE:
+            self._module_history.pop(0)
+            self._removed_cycles += 1
 
-        for module in self.modules.values():
+        for module in self._modules.values():
             module_states[module.name] = module.get_state()
-        self.module_history.append(module_states)
+        self._module_history.append(module_states)
 
     def load_cycle(self, cycle: int) -> None:
         """Load the state of all modules as they were at the specified clock
@@ -424,26 +424,26 @@ class Processor:
             Number of the cycle to load.
         """
 
-        cycle_index = cycle - self.removed_cycles
+        cycle_index = cycle - self._removed_cycles
 
         if cycle_index < 0:
             raise ValueError("The cycle to be loaded is not saved")
 
         try:
-            module_states = self.module_history[cycle_index]
+            module_states = self._module_history[cycle_index]
         except IndexError:
             raise IndexError
 
         for module_name, module_state in module_states.items():
-            self.modules[module_name].set_state(module_state)
+            self._modules[module_name].set_state(module_state)
 
-        self.clock = cycle
+        self._clock = cycle
 
-        for module in self.modules.values():
+        for module in self._modules.values():
             module.output_register()
 
-        while self.update_queue:
-            module = self.update_queue.pop(0)
+        while self._update_queue:
+            module = self._update_queue.pop(0)
             module.update_logic()
 
     def load_state_from_file(self, file_path: str) -> None:
@@ -512,265 +512,26 @@ class Processor:
         file.write("")
         file.close()
 
-        for module in self.modules.values():
-            res =module.save_state_to_file(file_path)
+        for module in self._modules.values():
+            res = module.save_state_to_file(file_path)
 
             if not res:
                 return False
-        
-        return True
-
-    def pretty_print(self) -> None:
-        """Print the processor state in a readable and compact format."""
-        self.pretty_print_verbose(self.ignore_keys)
-
-    def pretty_print_verbose(self, ignore_keys=None) -> None:
-        """
-        Print the most relevant information about each module in a compact and
-        readable format.
-
-        State variables of modules can be ignored with the optional argument.
-
-        Parameters
-        ----------
-        ignore_keys : list[str]
-            List of names of state variables of modules to exclude when
-            printing module states.
-        """
-        # TODO: ignore keys per module and not for all modules
-        memory_modules = []
-        other_modules = []
-
-        if ignore_keys is None:
-            ignore_keys = []
-
-        # TODO: remove isinstance(module, micro_memory)
-        for module in self.modules.values():
-            if not isinstance(module, Bus):
-                if isinstance(module, Memory) or isinstance(module, MicroMemory):
-                    memory_modules.append(module)
-                else:
-                    other_modules.append(module)
-
-        # sort the modules by name to ensure that they appear in the
-        # same order
-        memory_modules.sort(key=lambda x: x.name, reverse=True)
-        other_modules.sort(key=lambda x: x.name, reverse=True)
-
-        # specify which keys to ignore from the modules 'get_state'
-        # function
-        # TODO: ignore fields per module, this will ignore 'mask'
-        # for every module
-
-        module_to_line_length = {}
-        # get the longest line length for all other_modules
-        for module in other_modules:
-            # +3 for padding
-            line_len = module.get_longest_line_len(ignore_keys) + 3
-
-            # TODO: what to do if two or more modules has the same name
-            module_to_line_length[module] = line_len
-
-        groups = self.group_pp_modules(module_to_line_length)
-
-        print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
-
-        # print each group separate
-        for group in groups:
-            self.pretty_print_names(group)
-
-            # Build a new string containing the information to show
-            # the user
-            for row in range(self.get_most_fields(group, ignore_keys)):
-                string = ""
-                total_padding = 0
-
-                # Get the information from each module
-                for module in group:
-                    module_state = module.get_state()
-                    keys = self.fields_to_list(module)
-
-                    # Get the keys that we want to print to the user
-                    # If this is not done, the keys we want to print
-                    # could end up on an index that is higher than the
-                    # number of rows printed and will therefore be missed
-                    real_keys = [key for key in keys if key not in ignore_keys]
-
-                    # Dont go out of index
-                    # Needed since one module might want to print 4
-                    # fields and another only 1
-                    if row < len(real_keys) and real_keys[row] not in ignore_keys:
-                        string += (
-                            real_keys[row] + ": " + str(module_state[real_keys[row]])
-                        )
-
-                    # pad the string so each string has the same length
-                    total_padding += module_to_line_length[module]
-                    string = string.ljust(total_padding)
-                    # replace last to chars with a separator and padding
-                    string = string[0:-2] + "| "
-
-                print(string)
-            print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
-
-        for memory_module in memory_modules:
-            self.pretty_print_memory(memory_module)
-
-    def pretty_print_memory(self, module: Memory) -> None:
-        """Print a memory module in a compact and readable format.
-
-        Parameters
-        ----------
-        module : Memory
-            Memory module to print the state of.
-        """
-        print(module.name)
-        print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
-
-        longest_line_len = module.get_longest_line_len()
-        # longest_line_len = self.get_longest_memory_value(module.memory)
-        string = ""
-        last_mem_len = module.get_largest_mem_adr()
-
-        for i, value in enumerate(module.get_state()["memory"]):
-
-            # create a new string containing the address and the value
-            # of the memory address formatted to fit with the largest
-            # address and largest memory value
-            new_row = (str(i).ljust(last_mem_len) + ": " + str(value)).ljust(
-                longest_line_len + last_mem_len + 3
-            ) + "|"
-
-            # only add the string if there is space for it, else
-            # print the string and start a new
-            if len(string + new_row) + 1 > Processor.MAX_LINE_LEN:
-                print(string)
-                string = new_row
-            else:
-                # First iteration string will be
-                # empty and should not be padded
-                if string:
-                    string += " " + new_row
-                else:
-                    string = new_row
-        print(string)
-        print(Processor.LINE_SEPARATOR * Processor.MAX_LINE_LEN)
-        print()
-
-    def pretty_print_names(self, module_to_line_length: dict[Module, int]) -> None:
-        """
-        Print the name of the modules in one row with formatting.
-
-        Adds spacing between the names so that the longest state variable of
-        each module has space to be printed below the name.
-
-        Parameters
-        ----------
-        module_to_line_length : dict[Module, int]
-            Mapping from module to length of the longest state variable of
-            the module.
-        """
-        name_string = ""
-        total_len = 0
-        for module in module_to_line_length:
-            name_string += module.name
-            total_len += module_to_line_length[module]
-            name_string = name_string.ljust(total_len)
-            name_string = name_string[0:-2] + "| "
-
-        print(name_string)
-
-    def group_pp_modules(
-        self, module_to_line_length: dict[Module, int]
-    ) -> list[dict[Module, int]]:
-        """Group the modules to be pretty printed into groups with a
-        total line length lower than 'Processor.MAX_LINE_LEN'.
-
-        Parameters
-        ----------
-        module_to_line_length : dict[Module, int]
-            Mapping from module to length of the longest state variable of
-            the module (takes as the line length).
 
-        Returns
-        -------
-        list[dict[Module, int]]
-            List of mappings from module to line length, each mapping
-            representing a group of modules for printing.
-        """
-
-        groups = [{}]
-        group_index = 0
-        line_len = 0
-        for module in module_to_line_length:
-
-            # Make sure the line is not to long
-            if line_len + module_to_line_length[module] < Processor.MAX_LINE_LEN:
-                line_len += module_to_line_length[module]
-                groups[group_index][module] = module_to_line_length[module]
-
-            # If it would have been, start a new group
-            else:
-                groups.append({})
-                group_index += 1
-                groups[group_index][module] = module_to_line_length[module]
-                line_len = module_to_line_length[module]
-        return groups
+        return True
 
-    def fields_to_list(self, module: Module, ignore_keys=[]) -> list[str]:
+    def get_breakpoints(self) -> dict[int, Breakpoint]:
         """
-        Return a list containing all state variable names, excluding module
-        name, for a module.
-        Optional argument to ignore specific state variables.
-
-        Parameters
-        ----------
-        module : Module
-            Module to get state variable names from.
-        ignore_keys : list[str]
-            List of state variable names to exclude.
+        Get the breakpoints in the processor as a map from breakpoint id to
+        breakpoint.
 
         Returns
         -------
-        list[str]
-            List of state variable names, excluding module name, of a module.
+        dict[int, Breakpoint]
+            Map from breakpoint id to breakpoint, containing all breakpoints
+            in the processor.
         """
-        return [
-            key
-            for key in module.get_state()
-            if key != "name" and key not in ignore_keys
-        ]
-
-    def get_most_fields(self, modules: dict[Module, int], ignore_keys=None) -> int:
-        """Get the maximum number of state variables among all modules.
-
-        Can optionally ignore keys.
-
-        Parameters
-        ----------
-        modules : dict[Module, int]
-            Mapping from module to length of the longest state variable of
-            the module. Used as a list of modules to take the maximum over.
-        ignore_keys : list[str]
-            State variables to exclude.
-
-        Returns
-        -------
-        int
-            The maximum of the number of module state variables among all
-            modules of the processor.
-        """
-
-        if ignore_keys is None:
-            ignore_keys = []
-
-        fields = 0
-        for module in modules:
-            module_fields = len(self.fields_to_list(module, ignore_keys))
-            if module_fields > fields:
-                fields = module_fields
-
-        return fields
+        return self._breakpoints
 
     def add_state_breakpoint(
         self, module_name: str, state_name: str, value: Any
@@ -794,17 +555,17 @@ class Processor:
             exist in the module.
         """
 
-        if module_name not in self.modules:
+        if module_name not in self._modules:
             raise ValueError(f"No module named {module_name}")
 
-        module = self.modules[module_name]
+        module = self._modules[module_name]
 
         if state_name not in module.get_state():
             raise ValueError(f"No state named {state_name} in " f"module {module_name}")
 
         bp = StateBreakpoint(module, state_name, value)
-        self.breakpoints[self.breakpoint_id_counter] = bp
-        self.breakpoint_id_counter += 1
+        self._breakpoints[self._breakpoint_id_counter] = bp
+        self._breakpoint_id_counter += 1
 
     def remove_breakpoint(self, id: int) -> bool:
         """Remove a specific breakpoint.
@@ -814,17 +575,17 @@ class Processor:
         id : int
             ID of the breakpoint to be removed.
         """
-        if id in self.breakpoints:
-            del self.breakpoints[id]
+        if id in self._breakpoints:
+            del self._breakpoints[id]
             return True
         return False
 
     def print_breakpoints(self) -> None:
         """Print all breakpoints of the processor."""
-        if not self.breakpoints:
+        if not self._breakpoints:
             print("There are no breakpoints.")
         else:
-            for bp_id, bp in self.breakpoints.items():
+            for bp_id, bp in self._breakpoints.items():
                 print(f"BP {bp_id}: {bp}")
 
     def add_memory_breakpoint(self, module_name: str, address: int, value: Any) -> None:
@@ -845,16 +606,16 @@ class Processor:
             If the module does not exist or is not a memory.
         """
 
-        if module_name not in self.modules:
+        if module_name not in self._modules:
             raise ValueError(f"No module named {module_name}")
 
-        module = self.modules[module_name]
+        module = self._modules[module_name]
         if "memory" not in module.get_state():
             raise ValueError(f"Module {module_name} is not a memory.")
 
         bp = MemoryBreakpoint(module, address, value)
-        self.breakpoints[self.breakpoint_id_counter] = bp
-        self.breakpoint_id_counter += 1
+        self._breakpoints[self._breakpoint_id_counter] = bp
+        self._breakpoint_id_counter += 1
 
     def get_breakpoint_lambdas(self) -> list[str]:
         """Get all functions available for lambda breakpoints.
@@ -866,7 +627,7 @@ class Processor:
             for adding lambda breakpoints.
         """
 
-        return list(self.lambdas.keys())
+        return list(self._lambdas.keys())
 
     def add_lambda_breakpoint(self, lambda_name: str, **kwargs) -> None:
         """Add a lambda breakpoint to the processor.
@@ -894,16 +655,16 @@ class Processor:
         # modules as arguments without having direct access to them
         for key in kwargs.keys():
             value = kwargs[key]
-            if value in self.modules:
-                kwargs[key] = self.modules[value]
+            if value in self._modules:
+                kwargs[key] = self._modules[value]
 
-        if lambda_name not in self.lambdas:
+        if lambda_name not in self._lambdas:
             raise ValueError(f"No lambda named {lambda_name}.")
 
-        lambda_func = self.lambdas[lambda_name]
+        lambda_func = self._lambdas[lambda_name]
         bp = LambdaBreakpoint(lambda_func, **kwargs)
-        self.breakpoints[self.breakpoint_id_counter] = bp
-        self.breakpoint_id_counter += 1
+        self._breakpoints[self._breakpoint_id_counter] = bp
+        self._breakpoint_id_counter += 1
 
     def set_enabled_breakpoint(self, bp_id: int, is_enabled: bool) -> None:
         """Toggle a breakpoint to enabled or disabled.
@@ -915,4 +676,4 @@ class Processor:
         is_enabled : bool
             ``True`` to enable, ``False`` to disable the breakpoint.
         """
-        self.breakpoints[bp_id].set_enabled(is_enabled)
+        self._breakpoints[bp_id].set_enabled(is_enabled)
diff --git a/src/simudator/gui/breakpoint_window.py b/src/simudator/gui/breakpoint_window.py
index ca19345436f974868d30ba1bdd3539bd02b60068..649364e0b6690d87e959c3161a9df2f72770c008 100644
--- a/src/simudator/gui/breakpoint_window.py
+++ b/src/simudator/gui/breakpoint_window.py
@@ -53,7 +53,7 @@ class BreakpointWindow(QWidget):
         stop_icon = self.style().standardIcon(QStyle.SP_DialogCancelButton)
 
         # Add an list item for each breakpoint in cpu
-        for bp_id, breakpoint in self.cpu.breakpoints.items():
+        for bp_id, breakpoint in self.cpu.get_breakpoints().items():
             bp_str = str(bp_id) + ": " + breakpoint.__str__()
             bp_item = QListWidgetItem(bp_str)
 
@@ -95,7 +95,7 @@ class BreakpointWindow(QWidget):
         if bp_id is None:
             return
 
-        del self.cpu.breakpoints[bp_id]
+        self.cpu.remove_breakpoint(bp_id)
         self.update()
 
     def getSelectedItemId(self) -> int:
@@ -120,4 +120,4 @@ class BreakpointWindow(QWidget):
         # Do nothing if there are no breakpoints or if id is nonetype
         if bp_id is None or bp_id == -1:
             return
-        return self.cpu.breakpoints[bp_id]
+        return self.cpu.get_breakpoints()[bp_id]
diff --git a/src/simudator/gui/cpu_graphics_scene.py b/src/simudator/gui/cpu_graphics_scene.py
index 47292c4ec7552e6f75e0243cbb44009a5bdee77b..a64e417b1dfa6a4b9d834bb031373a2f9364a3f1 100644
--- a/src/simudator/gui/cpu_graphics_scene.py
+++ b/src/simudator/gui/cpu_graphics_scene.py
@@ -1,8 +1,22 @@
-from qtpy.QtWidgets import QGraphicsItem, QGraphicsScene
+import json
+from json.decoder import JSONDecodeError
+
+from qtpy.QtCore import QPointF
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import (
+    QErrorMessage,
+    QFileDialog,
+    QGraphicsItem,
+    QGraphicsScene,
+    QMessageBox,
+)
 
 from simudator.core.processor import Processor
 from simudator.gui.color_scheme import ColorScheme
 from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
+from simudator.gui.orientation import Orientation
+from simudator.gui.port_graphics_item import PortGraphicsItem
 from simudator.gui.signal_graphics_item import SignalGraphicsItem
 
 
@@ -10,69 +24,78 @@ class CpuGraphicsScene(QGraphicsScene):
     """
     This class creates the graphics scene for visualising the processor.
     It takes each module representation as a ModuleGraphicsItem
-    and handels mouse inputs for interacting with these graphicsitems.
+    and handles mouse inputs for interacting with these graphicsitems.
     Can create a default layout on creation and can save/load new layouts.
     """
 
     MODULE_SPACEING = 100
 
-    def __init__(self, cpu: Processor):
+    def __init__(self):
         super().__init__()
-        self.cpu = cpu
-        self.module_graphics_items = dict()
-        self.signal_graphics_items = []
+        self._module_graphics_items = dict()
+        self._signal_graphics_items = []
+        self._error_msg_box = QErrorMessage()
         self.setBackgroundBrush(ColorScheme.Window)
 
-    def resetSignals(self) -> None:
+    def reset_signals(self) -> None:
         """
-        Resets all graphical signals to their default visual representation
+        Reset all graphical signals to their default visual representation
         when initialised.
         """
-        for graphics_Signal in self.signal_graphics_items:
-            graphics_Signal.reset()
+        for graphics_signal in self._signal_graphics_items:
+            graphics_signal.reset()
 
-    def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Takes a ModuleGraphicsItem and adds it to the scene.
-        Will give it a new position according to the deafult layout.
+    def add_module(self, item: ModuleGraphicsItem) -> None:
         """
-        self.module_graphics_items[graphics_item.name] = graphics_item
-        self.placeModuleGraphicsItemDefault(graphics_item)
+        Add a graphical module item to the processor scene at a position
+        according to the default layout.
 
-    def placeModuleGraphicsItemDefault(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Places a module graphics items at a position where the x value
-        is equal to the y value. Used to place all graphics items in a
-        diagonal.
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
+            Graphical module item to add to the scene.
         """
-        placement = len(self.module_graphics_items) * self.MODULE_SPACEING
-        graphics_item.setPos(placement, placement)
-        self.addItem(graphics_item)
+        self._module_graphics_items[item.name] = item
+        self._place_module_default(item)
 
-    def replaceModuleGraphicsItem(
-        self, graphics_item: ModuleGraphicsItem, pos_x: int, pos_y: int
-    ) -> None:
+    def _place_module_default(self, item: ModuleGraphicsItem) -> None:
         """
-        Changes the postions of an existing modules graphics item.
+        Place a graphical module item at a position according to the default
+        layout, i.e. placing all module items along a diagonal.
+
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
         """
-        graphics_item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING)
+        placement = len(self._module_graphics_items) * self.MODULE_SPACEING
+        item.setPos(placement, placement)
+        self.addItem(item)
 
-    def updateGraphicsItems(self):
+    def move_module_to(self, item: ModuleGraphicsItem, pos_x: int, pos_y: int) -> None:
         """
-        Used to update graphicsitems when modules in the processor has chnaged values.
+        Place an existing graphical module item at some position.
+
+        Parameters
+        ----------
+        item : ModuleGraphicsItem
+           Graphical module item to place at a position.
+        pos_x : int
+            Position on x-axis to place the module item at.
+        pos_y : int
+            Position on y-axis to place the module item at.
         """
-        for graphics_item in self.module_graphics_items.values():
-            graphics_item.update()
+        item.setPos(pos_x * self.MODULE_SPACEING, pos_y * self.MODULE_SPACEING)
 
-    def addAllSignals(self) -> None:
+    def add_all_signals(self) -> None:
         """
-        Instantiates signals between all matching ports of all modules.
+        Instantiate graphical signals between all matching ports of all
+        graphical modules.
         """
         # Map which ports that are connected using their signals' names
         # (Every port keeps a reference to a simulation signal which has
         # a name)
         signal_to_ports = {}
-        for module_w in self.module_graphics_items.values():
+        for module_w in self._module_graphics_items.values():
             ports = module_w.getPorts()
             for port in ports:
                 s_name = port.getSignalName()
@@ -88,96 +111,317 @@ class CpuGraphicsScene(QGraphicsScene):
                 port_1 = module_graphics_items[0]
                 port_2 = module_graphics_items[1]
                 signal_w = SignalGraphicsItem(port_1, port_2)
-                self.signal_graphics_items.append(signal_w)
+                self._signal_graphics_items.append(signal_w)
                 self.addItem(signal_w)
                 port_1.moved.connect(signal_w.move)
                 port_2.moved.connect(signal_w.move)
                 port_1.toggled.connect(signal_w.toggleVisibility)
                 port_2.toggled.connect(signal_w.toggleVisibility)
 
-    def getModulesGraphicsItems(self) -> list[ModuleGraphicsItem]:
-        return list(self.module_graphics_items.values())
+    def get_modules(self) -> list[ModuleGraphicsItem]:
+        """
+        Get a list of all graphical module items in the scene.
 
-    def moduleGraphicsItemsDict(self) -> dict[str, ModuleGraphicsItem]:
-        return self.module_graphics_items
+        Returns
+        -------
+        list[ModuleGraphicsItem]
+            List of graphical module items in the scene.
+        """
+        return list(self._module_graphics_items.values())
 
-    def getSignalGraphicsItems(self) -> list[SignalGraphicsItem]:
-        return self.signal_graphics_items
+    def get_signals(self) -> list[SignalGraphicsItem]:
+        """
+        Get a list of all graphical signal items in the scene.
+
+        Returns
+        -------
+        list[SignalGraphicsItem]
+            List of graphical signal items in the scene.
+        """
+        return self._signal_graphics_items
 
     def mousePressEvent(self, event):
         super().mousePressEvent(event)
 
-    def load_layout_from_file(self, file_path: str) -> None:
+    @Slot()
+    def load_layout(self) -> None:
         """
-        Loads a layout for a processor from a saved filed.
+        Prompt the user for a layout file and load the layout from the file.
         """
-        file = open(file_path)
-
-        graphics_item_name = None
-        graphics_item_str = ""
 
-        # Go through each line in the file
-        for line in file.readlines():
-            # If no name currently saved then get name from current line
-            if graphics_item_name is None:
-                graphics_item_name = line.partition(":")[0]
+        # Prompt the user for a file
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
+        path = dialog.getOpenFileName()[0]
 
-            # if already has name get line content
-            else:
-                # if not empty line, then save content
-                if line.strip():
-                    graphics_item_str += line
+        # If no file was selected, do nothing
+        if path == '':
+            return
 
-                # if line is empty then give saved content to name file
-                else:
-                    graphics_item = self.get_module(graphics_item_name)
-                    try:
-                        graphics_item.load_state_from_str(graphics_item_str)
-                    except Exception as exc:
-                        raise exc
-
-                    # set back to empty
-                    graphics_item_str = ""
-                    graphics_item_name = None
+        try:
+            self.load_layout_from_file(path)
 
-        # give module anything that is left
-        if graphics_item_name and graphics_item_str:
-            graphics_item = self.get_module(graphics_item_name)
-            graphics_item.load_state_from_str(graphics_item_str)
+        # Anything goes wrong with loading the selected one,
+        # we dont care about what went wrong
+        except (OSError, JSONDecodeError):
+            self.load_default_layout()
+            self._error_msg_box.showMessage(
+                "Unable to load given file.", "load_layout_err"
+            )
+        else:
+            QMessageBox.information(
+                self.parent(), "SimuDator", "File loaded succesfully."
+            )
 
-    def save_layout_to_file(self, file_path: str) -> None:
+    def _save_layout_to_file(self, file_path: str) -> None:
         """
-        Saves the positions and visibility of graphicsitems in the scene (this
-        includes module and signal representations) to a file.
+        Save the layout of the scene to file.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to the file to save to layout to.
         """
         layout_str = ""
-        for graphics_item in self.module_graphics_items.values():
+        for graphics_item in self._module_graphics_items.values():
             layout_str += graphics_item.save_state_as_str() + "\n"
 
         file = open(file_path, "w")
         file.write(layout_str)
         file.close()
 
-    def setAllSignalsVisibility(self, is_signals_visible: bool) -> None:
-        for item in self.signal_graphics_items:
-            item.setVisible(is_signals_visible)
+    @Slot(bool)
+    def show_all_signals(self, value: bool) -> None:
+        """
+        Set the visibility of all graphical signals in the scene.
 
-    def setPortNamesVisibility(self, is_ports_visible: bool) -> None:
-        for item in self.module_graphics_items.values():
+        Parameters
+        ----------
+        value : bool
+            ``True`` to show all graphical signals, ``False`` to hide them.
+        """
+        for item in self._signal_graphics_items:
+            item.setVisible(value)
+
+    @Slot(bool)
+    def show_port_names(self, value: bool) -> None:
+        """
+        Set the visibility of the names of all ports of graphical modules
+        in the scene.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to show the port names, ``False`` to hide them.
+        """
+        for item in self._module_graphics_items.values():
             for port in item.ports:
-                port.setNameVisibility(is_ports_visible)
+                port.setNameVisibility(value)
 
-    def setLayoutLock(self, is_layout_locked: bool) -> None:
+    def toggle_layout_lock(self, value: bool) -> None:
         """
-        Toggles the layout lock making it so items in the scene can not be moved.
+        Toggle the layout between locked and unlocked. Locked means that
+        nothing can be moved around in the layout.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to lock the layout, ``False`` to unlock it.
         """
 
-        for item in self.module_graphics_items.values():
-            item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked)
-            item.setLocked(is_layout_locked)
+        for item in self._module_graphics_items.values():
+            item.setFlag(QGraphicsItem.ItemIsMovable, not value)
+            item.setLocked(value)
 
-        for item in self.signal_graphics_items:
-            item.setFlag(QGraphicsItem.ItemIsMovable, not is_layout_locked)
+        for item in self._signal_graphics_items:
+            item.setFlag(QGraphicsItem.ItemIsMovable, not value)
 
             # We use this value so lines in the signal can not be moved or edited
-            item.is_locked = is_layout_locked
+            item.is_locked = value
+
+    @Slot()
+    def save_layout(self) -> None:
+        """
+        Prompt the user for a file and save the scene layout to the file.
+
+        This overwrites the previous content of the file.
+        """
+        # Prompt the user for a file
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+        path = dialog.getSaveFileName()[0]
+
+        # Open the given file erasing the previous content
+        with open(path, "w") as fp:
+            graphics_modules_data = {}
+            ports_data = {}
+            graphics_signals_data = {}
+
+            graphics_modules = self.get_modules()
+            for graphics_module in graphics_modules:
+                pos = (graphics_module.x(), graphics_module.y())
+                graphics_modules_data[graphics_module.getName()] = pos
+
+                for port in graphics_module.getPorts():
+                    visibility = port.isVisible()
+                    orientation = int(port.getOrientation())
+                    data = (port.x(), port.y(), orientation, visibility)
+                    ports_data[port.getID()] = data
+
+            for graphics_signal in self.get_signals():
+                visibility = graphics_signal.isVisible()
+                points = []
+                for point in graphics_signal.getPoints():
+                    points.append((point.x(), point.y()))
+                data = (points, visibility)
+                graphics_signals_data[graphics_signal.getID()] = data
+
+            data = (graphics_modules_data, ports_data, graphics_signals_data)
+            json.dump(data, fp)
+
+        fp.close()
+
+    @Slot()
+    def load_default_layout(self) -> None:
+        """
+        Place all graphical modules in the scene according to the default
+        layout, i.e. along a diagonal.
+        """
+        counter = 0
+        for key in self._module_graphics_items:
+            module_graphic = self._module_graphics_items[key]
+            module_graphic.showPorts()
+            counter += 1
+            self.move_module_to(module_graphic, counter, counter)
+        self.reset_signals()
+
+    def load_layout_from_file(self, file_path: str) -> None:
+        """
+        Load a layout from a file.
+
+        If at anypoint this function would error, the default layout
+        is loaded instead.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to a file containing a layout for the processor scene.
+        """
+        graphics_modules = self._module_graphics_items
+        ports = {}
+        graphics_signals = {}
+
+        for graphics_module in graphics_modules.values():
+            for port in graphics_module.getPorts():
+                ports[port.getID()] = port
+
+        for graphics_signal in self.get_signals():
+            graphics_signals[graphics_signal.getID()] = graphics_signal
+
+        # Open the file in 'read-only'
+        with open(file_path, 'rb') as fp:
+            data = json.load(fp)
+            graphics_modules_data = data[0]
+            ports_data = data[1]
+            graphics_signals_data = data[2]
+
+            for g_module_name, g_module_data in graphics_modules_data.items():
+                g_module = graphics_modules[g_module_name]
+                x = g_module_data[0]
+                y = g_module_data[1]
+                self._load_module(g_module, x, y)
+
+            for port_id, port_data in ports_data.items():
+                port = ports[int(port_id)]
+                self._load_port(port, *port_data)
+
+            for g_signal_id, g_signal_data in graphics_signals_data.items():
+                g_signal = graphics_signals[int(g_signal_id)]
+                self._load_signal(g_signal, *g_signal_data)
+
+        fp.close()
+
+    def _load_signal(
+        self,
+        signal: SignalGraphicsItem,
+        signal_points: list[tuple[float, float]],
+        visibility: bool,
+    ) -> None:
+        """
+        Set the positions and visibility of a graphical signal. Helper method
+        for loading a layout from file.
+
+        Parameters
+        ----------
+        signal : SignalGraphicsItem
+            Graphical signal to modify.
+        signal_points : list[tuple[float, float]]
+            List of points for visually drawing the signal as line.
+        visibility : bool
+            Visibility of the signal. ``True`` to show it, ``False`` to hide it.
+        """
+        qpoints = []
+        # Turn points -> QPointF
+        # list[int, int] -> QPointF object
+        for point in signal_points:
+            qpoints.append(QPointF(point[0], point[1]))
+
+        # Set the new points
+        signal.setPoints(qpoints)
+        signal.setVisible(visibility)
+
+    def _load_module(
+        self,
+        module: ModuleGraphicsItem,
+        pos_x: float,
+        pos_y: float,
+    ) -> None:
+        """
+        Set the position of a graphical module in the scene. Helper method
+        for loading a layout from file.
+
+        Parameters
+        ----------
+        module : ModuleGraphicsItem
+            Graphical module of which to set the position.
+        pos_x : float
+            Position on the x-axis in the scene.
+        pos_y : float
+            Position on the y-axis in the scene.
+        """
+        module.setX(pos_x)
+        module.setY(pos_y)
+
+    def _load_port(
+        self,
+        port: PortGraphicsItem,
+        pos_x: float,
+        pos_y: float,
+        orientation: Orientation,
+        visibility: bool,
+    ) -> None:
+        """
+        Set position, orientation and visibility of a port of a graphical
+        module. Helper method for loading a layout from file.
+
+        Parameters
+        ----------
+        port : PortGraphicsItem
+            Port to modify.
+        pos_x : float
+            Position on the x-axis in the scene.
+        pos_y : float
+            Position on the y-axis in the scene.
+        orientation : Orientation
+            Orientation of the port.
+        visibility : bool
+            Visibility of the port. ``True`` to show the port, ``False`` to
+            hide it.
+        """
+        port.setOrientation(orientation)
+        port.setX(pos_x)
+        port.setY(pos_y)
+        port.setVisible(visibility)
diff --git a/src/simudator/gui/custom_toolbar.py b/src/simudator/gui/custom_toolbar.py
deleted file mode 100644
index b55049f2cb0848b4ca1b8b294e734d06fd9a429b..0000000000000000000000000000000000000000
--- a/src/simudator/gui/custom_toolbar.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from qtpy.QtWidgets import QToolBar
-
-
-class CustomToolBar(QToolBar):
-    """
-    A custom implementation of QToolBar that reimplemented contextMenuEvent
-    so that the toolbar is no longer removable.
-    """
-
-    def __init__(self, text):
-        super().__init__(text)
-
-    def contextMenuEvent(self, event):
-        pass
diff --git a/src/simudator/gui/gui.py b/src/simudator/gui/gui.py
index a5a30b14f0e83e811a211e3c7de90780a1056cc6..9b3fdc9cb05e293a70869cc1c078057c23668475 100644
--- a/src/simudator/gui/gui.py
+++ b/src/simudator/gui/gui.py
@@ -1,139 +1,65 @@
 import ast
-import json
 import sys
-from json import JSONDecodeError
-from threading import Thread
 
 from qtpy import QtCore, QtWidgets
-from qtpy.QtCore import QPointF
 from qtpy.QtCore import Signal as pyqtSignal
 from qtpy.QtCore import Slot
 from qtpy.QtWidgets import (
     QAction,
     QApplication,
     QErrorMessage,
-    QFileDialog,
-    QGraphicsView,
-    QInputDialog,
-    QLabel,
     QMainWindow,
     QMessageBox,
-    QSpinBox,
-    QStyle,
-    QWidget,
 )
 
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.gui.breakpoint_window import BreakpointWindow
 from simudator.gui.cpu_graphics_scene import CpuGraphicsScene
-from simudator.gui.custom_toolbar import CustomToolBar
 from simudator.gui.dialogs.lambda_breakpoint_dialog import LambdaBreakpointDialog
+from simudator.gui.menu_bar import MainMenuBar
 from simudator.gui.module_graphics_item.module_graphics_item import ModuleGraphicsItem
-from simudator.gui.orientation import Orientation
 from simudator.gui.pipeline import PipeLine
-from simudator.gui.port_graphics_item import PortGraphicsItem
-from simudator.gui.run_continuously_thread import RunThread
-from simudator.gui.signal_graphics_item import SignalGraphicsItem
+from simudator.gui.processor_handler import ProcessorHandler
 from simudator.gui.signal_viewer import SignalViewer
-
-
-class View(QGraphicsView):
-    """
-    This class views all QGraphicsItems for the user. It takes the
-    QGraphicsScene as input and inherits all funcitonality from the
-    QGraphicsView and overrides the wheelEvent function.
-    This allows the users to navigate the view with their trackpads
-    and zoom in/out with their trackpads + ctrl.
-    """
-
-    def __init__(self, QGraphicsScene):
-        super().__init__(QGraphicsScene)
-        self.scene = QGraphicsScene
-
-    def wheelEvent(self, event):
-        """
-        Default behaviour if ctrl is not pressed, otherwise zoom in/out.
-        """
-        modifiers = QtWidgets.QApplication.keyboardModifiers()
-        if modifiers == QtCore.Qt.ControlModifier:
-            # Factor above 1 zooms in, below zooms out
-            factor = 1.03
-            if event.angleDelta().y() < 0:
-                factor = 0.97
-
-            # If event got triggered due to the x axis, do nothing
-            if event.pixelDelta().x() != 0:
-                return
-
-            view_pos = event.globalPosition()
-            scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
-
-            self.centerOn(scene_pos)
-            self.scale(factor, factor)
-
-            old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
-            new_mapToScene = self.mapToScene(self.viewport().rect().center())
-
-            delta = old_mapToScene - new_mapToScene
-
-            self.centerOn(scene_pos - delta)
-
-        else:
-            # Default behaviour
-            super().wheelEvent(event)
+from simudator.gui.simulation_toolbar import SimulationToolBar
+from simudator.gui.view import View
 
 
 class GUI(QMainWindow):
     """
-    Main gui class. Handles gui windows, toolbar and visualizes modules.
+    Main gui class for visualizing the SimuDator processor simulator.
 
-    This is the main class for the GUI. It handles creating the window for
-    the gui, aswell as the toolbar for controlling the simultaion.
-    It takes a processor and visualizes its modules as boxes with the signals as lines
-    between them. Graphics items for the modules and signals need to be created
-    and added individually.
-    """
+    Graphics items for modules need to be created and added individually using
+    the provided methods. Signals are also not added automatically but need
+    not be created.
 
-    cpu_tick_signal = pyqtSignal(int)
-    HALT_MESSAGE_THRESHOLD = 100
+    Parameters
+    ----------
+    processor : Processor
+        The simulated processor to visualize and control.
+    """
 
-    def __init__(self, cpu: Processor):
+    def __init__(self, processor: Processor):
         super().__init__()
 
-        self.cpu = cpu
+        self._processor = processor
+        self._processor_handler = ProcessorHandler(processor, parent=self)
 
         self.setWindowTitle("SimuDator")
-        self.cpu_graphics_scene = CpuGraphicsScene(cpu)
-        self.graphics_view = View(self.cpu_graphics_scene)
+        self._graphics_scene = CpuGraphicsScene()
+        self._graphics_view = View(self._graphics_scene)
         # self.graphics_view.setDragMode(True)
-        self.moduleActions: dict[str, QAction] = {}
-
-        self.setCentralWidget(self.graphics_view)
-
-        self.errorMessageWidget = QErrorMessage(self)
-
-        self.createToolBar()
-
-        # Threadpool for running cpu and gui concurently
-        self.threadpool = QtCore.QThreadPool()
-
-        # Signal to tell gui when cpu has halted
-        self.cpu_tick_signal.connect(self.handleCpuTick)
+        self._module_actions: dict[str, QAction] = {}
 
-        # Used to set if all values in the gui should be updated each tick
-        # or only the clock counter
-        self.update_all_values = False
+        self.setCentralWidget(self._graphics_view)
 
-        # Used to set the update delay
-        # Useful when watching the values being updated while running
-        self.update_delay = 0.00
+        self._error_msg_widget = QErrorMessage(self)
 
-        # Used to lock some actions in ui when cpu is running in another thread
-        # Using the cpu's internal status directly could case problems
-        self.cpu_running = False
+        self._create_menu_bar()
+        self._create_tool_bar()
 
-        self.pipeline = PipeLine()
+        self._pipeline = PipeLine()
         self.init_pipeline()
 
         # Set Style, THESE ARE TEST AND DONT WORK
@@ -142,196 +68,58 @@ class GUI(QMainWindow):
         self.setAttribute(QtCore.Qt.WA_StyledBackground)
 
         # Add this so we can save the window in scope later, otherwise it disappears
-        self.breakpoint_window = None
+        self._breakpoint_window = None
 
-    def createToolBar(self) -> None:
+    def _create_menu_bar(self) -> None:
         """
-        Creates the toolbar containing file, layout and toolbar buttons.
-
-
-        Creates the toolbar containing the file, layout and toolbar
-        buttons with their respective sub buttons.
+        Create a main menu bar and connect its signals to appropriate slots of
+        other widgets.
         """
 
-        # Create toolbar
-        toolbar = CustomToolBar("Main toolbar")
-        self.addToolBar(toolbar)
-
-        # Create load action
-        self.load_action = QAction("Load", self)
-        self.load_action.setStatusTip("Load processor state from file")
-        self.load_action.triggered.connect(self.loadToolBarButtonClick)
+        self._menu_bar = MainMenuBar(self)
+        self.setMenuBar(self._menu_bar)
 
-        # Create save action
-        self.save_action = QAction("Save", self)
-        self.save_action.setStatusTip("Save processor state to file")
-        self.save_action.triggered.connect(self.saveStateMenuButtonClick)
-
-        self.reset_action = QAction("Reset", self)
-        self.reset_action.setStatusTip("Reset processor")
-        self.reset_action.triggered.connect(self.resetMenuButtonClick)
+        # Connect signals for processor related actions
+        self._menu_bar.load.connect(self._processor_handler.load_state)
+        self._menu_bar.save.connect(self._processor_handler.save_state)
+        self._menu_bar.reset.connect(self._processor_handler.reset)
+        self._menu_bar.update_all_values.connect(
+            self._processor_handler.toggle_value_update_on_run
+        )
+        self._menu_bar.set_delay.connect(self._processor_handler.set_update_delay)
 
-        # Create File menu for load and save
-        menu = self.menuBar()
-        file_menu = menu.addMenu("&File")
-        file_menu.addAction(self.load_action)
-        file_menu.addAction(self.save_action)
-        file_menu.addAction(self.reset_action)
+        # Connect signals for managing the processor layout
+        self._menu_bar.load_layout.connect(self._graphics_scene.load_layout)
+        self._menu_bar.load_default_layout.connect(
+            self._graphics_scene.load_default_layout
+        )
+        self._menu_bar.save_layout.connect(self._graphics_scene.save_layout)
+        self._menu_bar.lock_layout.connect(self._graphics_scene.toggle_layout_lock)
+        self._menu_bar.show_all_signals.connect(self._graphics_scene.show_all_signals)
+        self._menu_bar.show_port_names.connect(self._graphics_scene.show_port_names)
 
-        # create load layout action
-        load_layout_action = QAction("Load layout", self)
-        load_layout_action.setStatusTip("Loads the selected layout from file.")
-        load_layout_action.triggered.connect(self.loadLayoutToolBarButtonClick)
+        # Connect signal for managing breakpoints
+        self._menu_bar.show_breakpoints.connect(self.openBreakpointWindow)
 
-        # create load default layout action
-        load_default_layout_action = QAction("Load default layout", self)
-        load_default_layout_action.setStatusTip("Loads the default layout from file.")
-        load_default_layout_action.triggered.connect(
-            self.loadDefaultLayoutToolBarButtonClick
+        self._processor_handler.running.connect(
+            self._menu_bar.set_disabled_when_running
         )
 
-        # create save layout action
-        save_layout_action = QAction("Save layout", self)
-        save_layout_action.setStatusTip("Saves the current layout to a file.")
-        save_layout_action.triggered.connect(self.saveLayoutToolBarButtonClick)
-
-        # create layout lock action
-        self.lock_layout_action = QAction("Lock layout", self, checkable=True)
-        self.lock_layout_action.setStatusTip("Lock layout so items can't be moved.")
-        self.lock_layout_action.triggered.connect(self.toggleLayoutLockMenuButtonClick)
-
-        # Create show signals actions
-        self.signal_vis_action = QAction("Show signals", self, checkable=True)
-        self.signal_vis_action.setChecked(True)
-        self.signal_vis_action.setStatusTip("Toggle the visibility of signal.")
-        self.signal_vis_action.triggered.connect(self.showSignalsMenuButtonClick)
-
-        # Create show port name actions
-        self.port_vis_action = QAction("Show port names", self, checkable=True)
-        self.port_vis_action.setChecked(True)
-        self.port_vis_action.setStatusTip("Toggle the visibility of port names.")
-        self.port_vis_action.triggered.connect(self.showPortNamesBarButtonClick)
-
-        # Create Layout menu for layout actions
-        layout_menu = menu.addMenu("&Layout")
-        layout_menu.addAction(load_layout_action)
-        layout_menu.addAction(load_default_layout_action)
-        layout_menu.addAction(save_layout_action)
-        layout_menu.addAction(self.lock_layout_action)
-        layout_menu.addAction(self.signal_vis_action)
-        layout_menu.addAction(self.port_vis_action)
-
-        # Create breakpoint window action
-        self.breakpoint_action = QAction("Breakpoints", self)
-        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)
-        self.run_action = QAction(arrow_icon, "Run", self)
-        self.run_action.setStatusTip("Run until halt")
-        self.run_action.triggered.connect(self.runToolBarButtonClick)
-        toolbar.addAction(self.run_action)
-
-        # Add stop button on toolbar
-        stop_icon = self.style().standardIcon(QStyle.SP_MediaStop)
-        self.stop_action = QAction(stop_icon, "Stop", self)
-        self.stop_action.setStatusTip("Stop running")
-        self.stop_action.triggered.connect(self.stopToolBarButtonClick)
-        toolbar.addAction(self.stop_action)
-
-        # Add Asm label
-        self.clock_cycle_label = QLabel("Clock cycle: ", self)
-        toolbar.addWidget(self.clock_cycle_label)
-
-        # Add undo button on toolbar
-        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
-        self.undo_action = QAction(backward_arrow_icon, "Undo", self)
-        self.undo_action.setStatusTip("Undo the last processor tick")
-        self.undo_action.triggered.connect(self.undoToolBarButtonClick)
-        toolbar.addAction(self.undo_action)
-
-        # Add step button on toolbar
-        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
-        self.step_action = QAction(forward_arrow_icon, "Step", self)
-        self.step_action.setStatusTip("Run one clock cycle")
-        self.step_action.triggered.connect(self.stepToolBarButtonClick)
-        toolbar.addAction(self.step_action)
-
-        # Add box for jump value
-        self.jump_value_box = QSpinBox()
-        self.jump_value_box.setMinimum(999999)
-        self.jump_value_box.setMinimum(1)
-        self.jump_value_box.setValue(1)
-        toolbar.addWidget(self.jump_value_box)
-
-        # Add seperator so clock gets better spacing
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-        toolbar.addSeparator()
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-
-        # Add clock counter
-        self.clock_label = QLabel("Clock cycles: " + str(self.cpu.get_clock()), self)
-        toolbar.addWidget(self.clock_label)
-
-        # Add seperator so clock gets better spacing
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-        toolbar.addSeparator()
-        spacing = QWidget()
-        spacing.setFixedWidth(10)
-        toolbar.addWidget(spacing)
-
-        # Add Asm label
-        self.asm_label = QLabel("Assembler Instructions: ", self)
-        toolbar.addWidget(self.asm_label)
-
-        # Add undo asm button on toolbar
-        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
-        self.undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self)
-        self.undo_asm_action.setStatusTip("Undo the last assembler instruction")
-        self.undo_asm_action.triggered.connect(self.undoAsmToolBarButtonClick)
-        toolbar.addAction(self.undo_asm_action)
-
-        # Add step button on toolbar
-        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
-        self.step_asm_action = QAction(forward_arrow_icon, "Step Asm", self)
-        self.step_asm_action.setStatusTip("Run one assembler instruction")
-        self.step_asm_action.triggered.connect(self.stepAsmToolBarButtonClick)
-        toolbar.addAction(self.step_asm_action)
-
-        # Add box for jump value
-        self.asm_jump_value_box = QSpinBox()
-        self.asm_jump_value_box.setMinimum(999999)
-        self.asm_jump_value_box.setMinimum(1)
-        self.asm_jump_value_box.setValue(1)
-        toolbar.addWidget(self.asm_jump_value_box)
+    def _create_tool_bar(self) -> None:
+        """
+        Create a simulation toolbar and connect its signals to appropriate
+        slots of other widgets.
+        """
+        self._toolbar = SimulationToolBar("Main toolbar", self)
+        self.addToolBar(self._toolbar)
+        self._toolbar.run.connect(self._processor_handler.run_simulation)
+        self._toolbar.stop.connect(self._processor_handler.stop_simulation)
+        self._toolbar.step.connect(self._processor_handler.step_cycles)
+        self._toolbar.undo.connect(self._processor_handler.undo_cycles)
+        self._toolbar.step_asm.connect(self._processor_handler.step_asm_instructions)
+        self._toolbar.undo_asm.connect(self._processor_handler.undo_asm_instructions)
+        self._processor_handler.cycle_changed.connect(self._toolbar.update_clock)
+        self._processor_handler.running.connect(self._toolbar.set_disabled_when_running)
 
     def connectModuleActions(self, action_signals: []) -> None:
         """
@@ -359,50 +147,30 @@ class GUI(QMainWindow):
                     # LambdaBPs are not tested since we dont
                     # actually have any modules that make them yet
                     signal.connect(self.addLambdaBreakpoint)
-                case "UPDATE":
-                    signal.connect(self.updateCpuListeners)
-                case "CLOCKUPDATE":
-                    signal.connect(self.updateCpuClockCycle)
 
     def init_pipeline(self) -> None:
         """Initialize the pipeline diagram.
 
         Sets its height, width and instructions.
         """
-        size = self.cpu.get_pipeline_dimensions()
-
-        self.pipeline.set_height(size[0])
-        self.pipeline.set_width(size[1])
-        self.update_pipeline()
-
-        self.pipeline.show()
-
-    def updateCpuListeners(self) -> None:
-        """
-        Updates the graphics items in the scene, the clock, and the pipeline diagram.
+        size = self._processor.get_pipeline_dimensions()
 
-        Used after the cpu has run or when the user has edited somehting.
-        """
-        self.cpu_graphics_scene.updateGraphicsItems()
-        self.updateCpuClockCycle()
-        self.update_pipeline()
-
-    def update_pipeline(self) -> None:
-        self.pipeline.set_instructions(self.cpu.get_current_instructions())
+        self._pipeline.set_height(size[0])
+        self._pipeline.set_width(size[1])
 
-    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()))
+        self._processor_handler.changed_instruction.connect(
+            self._pipeline.set_instructions
+        )
+        # TODO: Find prettier way of making the processor handler emit its
+        # signal for the current processor instructions
+        self._processor_handler._signal_processor_changed()
+        self._pipeline.show()
 
     def lambdaBreakpointDialog(self) -> None:
         """
         Opens dialog window for user to create a breakpoint.
         """
-        lambdas = self.cpu.get_breakpoint_lambdas()
+        lambdas = self._processor.get_breakpoint_lambdas()
         lambda_br_dialog = LambdaBreakpointDialog(lambdas, self)
         lambda_br_dialog.accepted.connect(self.addLambdaBreakpoint)
 
@@ -411,8 +179,8 @@ class GUI(QMainWindow):
         Updates the breakpoint window when new breakpoints are added.
         """
         # Don't do anything if window is closed
-        if self.breakpoint_window is not None:
-            self.breakpoint_window.update()
+        if self._breakpoint_window is not None:
+            self._breakpoint_window.update()
 
     """
     @Slot is used to explicitly mark a python method as a Qt slot
@@ -430,13 +198,13 @@ class GUI(QMainWindow):
         try:
             parsed_value = ast.literal_eval(value)
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
         else:
-            module = self.cpu.get_module(module_name)
+            module = self._processor.get_module(module_name)
             module_state = module.get_state()
             module_state[state] = parsed_value
             module.set_state(module_state)
-            self.cpu_graphics_scene.updateGraphicsItems()
+            self._graphics_scene.update_modules()
 
     @Slot(str, str, str)
     def editMemoryContent(self, module_name: str, adress: str, value: str) -> None:
@@ -449,17 +217,17 @@ class GUI(QMainWindow):
             parsed_adress = int(adress, 16)
             parsed_value = ast.literal_eval(value)
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
         except ValueError:
-            self.errorMessageWidget.showMessage(
+            self._error_msg_widget.showMessage(
                 "You must enter a hexadecimal" "number preceeded by '0x' (e.g." "0xc3)."
             )
         else:
-            module = self.cpu.get_module(module_name)
+            module = self._processor.get_module(module_name)
             module_state = module.get_state()
             module_state['memory'][parsed_adress] = parsed_value
             module.set_state(module_state)
-            self.cpu_graphics_scene.updateGraphicsItems()
+            self._graphics_scene.update_modules()
 
     @Slot(str, str, str)
     def addStateBreakpoint(self, module_name: str, state: str, value: str) -> None:
@@ -468,10 +236,10 @@ class GUI(QMainWindow):
         """
         try:
             parsed_value = ast.literal_eval(value)
-            self.cpu.add_state_breakpoint(module_name, state, parsed_value)
+            self._processor.add_state_breakpoint(module_name, state, parsed_value)
             self.updateBreakpointWindow()
         except (ValueError, SyntaxError) as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
 
     @Slot(str, str)
     def addLambdaBreakpoint(self, lambda_name, kwargs_str) -> None:
@@ -483,10 +251,10 @@ class GUI(QMainWindow):
             for kwarg in kwargs_str.split(' '):
                 key, value = kwarg.split('=')
                 lambda_kwargs[key] = ast.literal_eval(value)
-            self.cpu.add_lambda_breakpoint(lambda_name, **lambda_kwargs)
+            self._processor.add_lambda_breakpoint(lambda_name, **lambda_kwargs)
             self.updateBreakpointWindow()
         except (ValueError, SyntaxError) as e:
-            self.errorMessageWidget.showMessage(str(e))
+            self._error_msg_widget.showMessage(str(e))
 
     @Slot(str, str, str)
     def addMemoryBreakpoint(self, module_name: str, adress: str, value: str) -> None:
@@ -496,498 +264,56 @@ class GUI(QMainWindow):
         try:
             parsed_adress = int(adress)
             parsed_value = int(value)
-            self.cpu.add_memory_breakpoint(module_name, parsed_adress, parsed_value)
+            self._processor.add_memory_breakpoint(
+                module_name, parsed_adress, parsed_value
+            )
             self.updateBreakpointWindow()
         except SyntaxError as e:
-            self.errorMessageWidget.showMessage(str(e))
-
-    def setDisabledWhenRunning(self, is_disable):
-        """
-        Greys out buttons for actions that can't be done while cpu is running.
-        """
-        self.load_action.setDisabled(is_disable)
-        self.save_action.setDisabled(is_disable)
-        self.reset_action.setDisabled(is_disable)
-        self.run_action.setDisabled(is_disable)
-        self.step_action.setDisabled(is_disable)
-        self.undo_action.setDisabled(is_disable)
-
-    def stepToolBarButtonClick(self):
-        """
-        Runs the cpu a specified number of clock cycles according to the jump value box.
-        """
-
-        # Don't do steps if cpu is running
-        if self.cpu_running:
-            return
-
-        steps = self.jump_value_box.value()
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simulation_thread = RunThread(
-            self.cpu, self.cpu_tick_signal, self.update_delay, False, False, steps
-        )
-        self.threadpool.start(simulation_thread)
-
-    def stepAsmToolBarButtonClick(self):
-        """
-        Runs the cpu a specified number of asm instructions according to the jump value box.
-        """
-
-        # Don't do steps if cpu is running
-        if self.cpu_running:
-            return
-
-        steps = self.asm_jump_value_box.value()
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simultaion_thread = RunThread(
-            self.cpu, self.cpu_tick_signal, self.update_delay, False, True, steps
-        )
-        self.threadpool.start(simultaion_thread)
-        self.updateCpuListeners()
-
-    def runToolBarButtonClick(self) -> None:
-        """
-        Runs the cpu until it is stopped by user input, breakpoint or similar.
-        """
-
-        # Don't run if already running
-        if self.cpu_running:
-            return
-
-        # Create own thread for cpu simulation so gui dosent freeze
-        self.cpu_running = True
-        self.setDisabledWhenRunning(True)
-        simulation_thread = RunThread(self.cpu, self.cpu_tick_signal, self.update_delay)
-        self.threadpool.start(simulation_thread)
-
-    @Slot(int)
-    def handleCpuTick(self, steps: int) -> None:
-        """
-        Called from other thread after every cpu tick.
-        Will inform the user and update visuals.
-        """
-
-        # 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()
-
-    def folderSaveDialog(self) -> str:
-        """
-        Open a file explorer in a new window. Return the absolute path to the selected file.
-
-        Can return existing as well as non-existing files.
-        If the selected files does not exist it will be created.
-        """
-        dialog = QFileDialog()
-        dialog.setFileMode(QFileDialog.AnyFile)
-        dialog.setAcceptMode(QFileDialog.AcceptOpen)
-        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
-
-        # getSaveFileName() can returncan return existing file but also create new ones
-        return dialog.getSaveFileName()[0]
-
-    def folderLoadDialog(self) -> str:
-        """
-        Open a file explorer in a new window. Return the absolute path to the selected file.
-
-        Can only return existing files.
-        """
-        dialog = QFileDialog()
-        dialog.setFileMode(QFileDialog.AnyFile)
-        dialog.setAcceptMode(QFileDialog.AcceptOpen)
-        dialog.setDirectory("~/simudator")  # TODO: does this work when exported?
-
-        # getOpenFileName() will only return already existing files
-        return dialog.getOpenFileName()[0]
-
-    def loadToolBarButtonClick(self) -> None:
-        """
-        Loads the processor state from selected file.
-        """
-
-        # not safe to load while cpu is running
-        if self.cpu_running:
-            return
-
-        path = self.folderLoadDialog()
-        try:
-            self.cpu.load_state_from_file(path)
-        except KeyError:
-            self.errorBox("Could not load selected file.")
-        except ValueError:
-            self.errorBox("Selected file was empty.")
-        except FileNotFoundError:
-            # This error is triggered when no file was selected by
-            # by the user. The user should know they did not select a
-            # file and therefore there no need to display a box telling
-            # them that.
-            pass
-            # self.errorBox("No vaild file was selected.")
-        else:
-            self.messageBox("Loaded file successfully.")
-            self.updateCpuListeners()
-
-    def resetMenuButtonClick(self) -> None:
-        """
-        Will reset processor to inital values.
-        """
-
-        # not safe to reset while cpu running
-        if self.cpu_running:
-            return
-
-        answer = QMessageBox.question(
-            self, "Reset Processor", "Are you sure you want to reset the processor?"
-        )
-
-        if answer == QMessageBox.Yes:
-            self.cpu.reset()
-            self.updateCpuListeners()
-
-    def undoNToolBarButtonClick(self) -> None:
-        # TODO: I dont think this is used
-        """
-        Undo zero to N cycles.
-        """
-
-        # Don't try to undo while running
-        if self.cpu_running:
-            return
-
-        cycles, ok = QInputDialog(self).getInt(
-            self,
-            "Input number of cycles to run",
-            "Input number of cycles to run",
-        )
-        if ok:
-            if cycles < 1:
-                self.errorBox("Please input a number larger than 0.")
-                return
-            try:
-                # Make sure we load 0 as lowest
-                self.cpu.load_cycle(max(self.cpu.get_clock() - cycles, 0))
-                self.updateCpuListeners()
-            except (ValueError, IndexError):
-                self.errorBox("Unable to undo the cycle(s).")
-
-    def saveLayoutToolBarButtonClick(self) -> None:
-        """
-        Saves the layout of all the modules in a (somewhat) human readable format.
-
-        This also erases the previous content of the file before saving
-        the data.
-        """
-        path = self.folderSaveDialog()
-
-        # Change file to the selected file if a file was selected
-        if path == '':
-            return
-
-        # Open the given file erasing the previous content
-        with open(path, "w") as fp:
-            graphics_modules_data = {}
-            ports_data = {}
-            graphics_signals_data = {}
-
-            graphics_modules = self.cpu_graphics_scene.getModulesGraphicsItems()
-            for graphics_module in graphics_modules:
-                pos = (graphics_module.x(), graphics_module.y())
-                graphics_modules_data[graphics_module.getName()] = pos
-
-                for port in graphics_module.getPorts():
-                    visibility = port.isVisible()
-                    orientation = int(port.getOrientation())
-                    data = (port.x(), port.y(), orientation, visibility)
-                    ports_data[port.getID()] = data
-
-            for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems():
-                visibility = graphics_signal.isVisible()
-                points = []
-                for point in graphics_signal.getPoints():
-                    points.append((point.x(), point.y()))
-                data = (points, visibility)
-                graphics_signals_data[graphics_signal.getID()] = data
-
-            data = (graphics_modules_data, ports_data, graphics_signals_data)
-            json.dump(data, fp)
-
-        fp.close()
-
-    def loadDefaultLayoutToolBarButtonClick(self) -> None:
-        """
-        Places all the module_graphic objects in a diagonal going down to the right.
-        """
-        counter = 0
-        for key in self.cpu_graphics_scene.module_graphics_items:
-            module_graphic = self.cpu_graphics_scene.module_graphics_items[key]
-            module_graphic.showPorts()
-            counter += 1
-            self.cpu_graphics_scene.replaceModuleGraphicsItem(
-                module_graphic, counter, counter
-            )
-        self.cpu_graphics_scene.resetSignals()
-
-    def loadLayoutFromFile(self, file) -> None:
-        """
-        Loads a layout for the current cpu from the selected file.
-
-        If at anypoint this funciton would error, the default layout
-        is loaded instead.
-        """
-        # TODO: speed(?)
-        graphics_modules = self.cpu_graphics_scene.moduleGraphicsItemsDict()
-        ports = {}
-        graphics_signals = {}
-
-        for graphics_module in graphics_modules.values():
-            for port in graphics_module.getPorts():
-                ports[port.getID()] = port
-
-        for graphics_signal in self.cpu_graphics_scene.getSignalGraphicsItems():
-            graphics_signals[graphics_signal.getID()] = graphics_signal
-
-        # Open the file in 'read-only'
-        with open(file, 'rb') as fp:
-            data = json.load(fp)
-            graphics_modules_data = data[0]
-            ports_data = data[1]
-            graphics_signals_data = data[2]
-
-            for g_module_name, g_module_data in graphics_modules_data.items():
-                g_module = graphics_modules[g_module_name]
-                x = g_module_data[0]
-                y = g_module_data[1]
-                self.loadGraphicsModule(g_module, x, y)
-
-            for port_id, port_data in ports_data.items():
-                port = ports[int(port_id)]
-                self.loadPort(port, *port_data)
-
-            for g_signal_id, g_signal_data in graphics_signals_data.items():
-                g_signal = graphics_signals[int(g_signal_id)]
-                self.loadSignal(g_signal, *g_signal_data)
-
-        fp.close()
-
-    def loadSignal(
-        self,
-        graphic_signal: SignalGraphicsItem,
-        signal_points: list[tuple[float, float]],
-        visibility: bool,
-    ) -> None:
-        """
-        Changes the graphical signal to have the positions given as argument.
-        """
-        qpoints = []
-        # Turn points -> QPointF
-        # list[int, int] -> QPointF object
-        for point in signal_points:
-            qpoints.append(QPointF(point[0], point[1]))
-
-        # Set the new points
-        graphic_signal.setPoints(qpoints)
-        graphic_signal.setVisible(visibility)
-
-    def loadGraphicsModule(
-        self,
-        graphics_module: ModuleGraphicsItem,
-        graphics_module_x: float,
-        graphics_module_y: float,
-    ) -> None:
-        """
-        Changes the positions of graphical modules to the ones given as argument.
-        """
-        graphics_module.setX(graphics_module_x)
-        graphics_module.setY(graphics_module_y)
-
-    def loadPort(
-        self,
-        port: PortGraphicsItem,
-        x: float,
-        y: float,
-        orientation: Orientation,
-        visibility: bool,
-    ) -> None:
-        port.setOrientation(orientation)
-        port.setX(x)
-        port.setY(y)
-        port.setVisible(visibility)
-
-    def saveStateMenuButtonClick(self) -> None:
-        """
-        Save state of cpu to file.
-
-        This erases the previous content of the file before saving the data.
-        """
-
-        # Not safe to save while running
-        if self.cpu_running:
-            return
-
-        path = self.folderSaveDialog()
-
-        # Change file to the selected file if a file was selected
-        if path == '':
-            return
-
-        res = self.cpu.save_state_to_file(path)
-
-        if res:
-            self.messageBox("File saved.")
-        else:
-            self.errorBox("Unable to save.")
+            self._error_msg_widget.showMessage(str(e))
 
+    @Slot()
     def openBreakpointWindow(self) -> None:
         """
         Opens window for editing breakpoints.
         """
-        self.breakpoint_window = BreakpointWindow(self.cpu)
-        self.breakpoint_window.show()
-
-    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.
-        """
-        self.cpu_graphics_scene.setPortNamesVisibility(self.port_vis_action.isChecked())
-
-    def showSignalsMenuButtonClick(self) -> None:
-        """
-        Toggle shoing the signals in the graphics scene.
-        """
-        self.cpu_graphics_scene.setAllSignalsVisibility(
-            self.signal_vis_action.isChecked()
-        )
-
-    def toggleLayoutLockMenuButtonClick(self) -> None:
-        """
-        Toggles so the layout can not be edited.
-        """
-        self.cpu_graphics_scene.setLayoutLock(self.lock_layout_action.isChecked())
-
-    def loadLayoutToolBarButtonClick(self) -> None:
-        """
-        Loads a given layout from a selected file.
-
-        If the layout was
-        unable to load, an error message will pop up informing the user
-        and the default layout will be loaded.
-        """
+        self._breakpoint_window = BreakpointWindow(self._processor)
+        self._breakpoint_window.show()
 
-        path = self.folderLoadDialog()
-
-        # If no file was selected, do nothing
-        if path == '':
-            return
-
-        try:
-            self.loadLayoutFromFile(path)
-
-        # Anything goes wrong with loading the selected one,
-        # we dont care about what went wrong
-        except (OSError, JSONDecodeError):
-            self.loadDefaultLayoutToolBarButtonClick()
-            self.errorBox("Unable to load given file.")
-        else:
-            self.messageBox("File loaded successfully.")
-
-    def errorBox(self, message="Something went wrong.") -> None:
-        """
-        Displays a simple box with the given error message.
-        """
-
-        dlg = QMessageBox(self)
-        dlg.setWindowTitle("Error")
-        dlg.setText(message)
-        dlg.exec()
-
-    def messageBox(self, message="Something happend.") -> None:
+    def messageBox(self, message, title="Message") -> None:
         """
         Displays a simple box with the given message.
         """
-
         dlg = QMessageBox(self)
-        dlg.setWindowTitle("Message")
+        dlg.setWindowTitle(title)
         dlg.setText(message)
         dlg.exec()
 
-    def undoToolBarButtonClick(self) -> None:
+    def add_module_graphics_item(self, item: ModuleGraphicsItem) -> None:
         """
-        Undos as many processor cycles as the number entered in the box.
+        Add a module graphics item to the graphics scene and connect its
+        QT signals and slots.
         """
-        try:
-            steps = self.jump_value_box.value()
-            self.cpu.load_cycle(max(self.cpu.get_clock() - steps, 0))
-            self.updateCpuListeners()
-        except (ValueError, IndexError):
-            self.errorBox("Unable to undo the cycle.")
+        self._graphics_scene.add_module(item)
+        self.connectModuleActions(item.getActionSignals())
+        self._processor_handler.changed.connect(item.update)
 
-    def undoAsmToolBarButtonClick(self) -> None:
+    def add_all_signals(self) -> None:
         """
-        Undos as many processor cycles as the number entered in the box.
+        Add visual representations of all processor signals between all modules
+        added to the GUI.
         """
-        try:
-            steps = self.asm_jump_value_box.value()
-            self.cpu.undo_asm_instruction(steps)
-            self.updateCpuListeners()
-        except (ValueError, IndexError):
-            self.errorBox("Unable to undo the cycle.")
+        self._graphics_scene.add_all_signals()
 
-    def addModuleGraphicsItem(self, graphics_item: ModuleGraphicsItem) -> None:
-        """
-        Adds an item to the graphics scene.
-        """
-        self.cpu_graphics_scene.addModuleGraphicsItem(graphics_item)
-        self.connectModuleActions(graphics_item.getActionSignals())
+    def load_layout(self, file_path: str) -> None:
+        """Load a processor layout from file.
+
+        Parameters
+        ----------
+        file_path : str
+            Path to the file containing a processor layout to load.
 
-    def addAllSignals(self) -> None:
-        """
-        Add signals depending on modules in the graphics scene.
         """
-        self.cpu_graphics_scene.addAllSignals()
+        self._graphics_scene.load_layout_from_file(file_path)
 
     def add_signal_viewer(self, signal: Signal, label: str | None = None) -> None:
         """
@@ -1001,7 +327,8 @@ class GUI(QMainWindow):
             Optional label of the signal viewer.
         """
         viewer = SignalViewer(signal, label)
-        self.cpu_graphics_scene.addItem(viewer)
+        self._processor_handler.changed.connect(viewer.update)
+        self._graphics_scene.addItem(viewer)
 
 
 if __name__ == '__main__':
diff --git a/src/simudator/gui/menu_bar.py b/src/simudator/gui/menu_bar.py
new file mode 100644
index 0000000000000000000000000000000000000000..29183a9df0ef1db0a99dd035a3407b83e93eead9
--- /dev/null
+++ b/src/simudator/gui/menu_bar.py
@@ -0,0 +1,192 @@
+import typing
+
+from PyQt5.QtWidgets import QAction
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QMenuBar, QWidget
+
+
+class MainMenuBar(QMenuBar):
+    """
+    Main menu bar of the simudator GUI. Contains submenus for file I/O,
+    layout managing and miscellaneous tools.
+
+    Parameters
+    ----------
+    parent : QWidget | None
+        Parent of the widget. The widget will be a new window if it is ``None``
+        (default).
+    """
+
+    load = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to load a processor state.
+    """
+
+    save = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to save the processor state.
+    """
+
+    reset = pyqtSignal()
+    """
+    QT signal emitted when the user has request to reset the processor state.
+    """
+
+    load_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to load a processor layout.
+    """
+
+    load_default_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to load the default processor
+    layout.
+    """
+
+    save_layout = pyqtSignal()
+    """
+    QT signal emitted when the user has request to save the processor layout.
+    """
+
+    lock_layout = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has request to toggle the lock of editing
+    the processor layout.
+    """
+
+    show_breakpoints = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to show and manage
+    the processor breakpoints.
+    """
+
+    show_all_signals = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to toggle showing all
+    processor signals.
+    """
+
+    show_port_names = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to toggle showing the names
+    of processor module ports.
+    """
+
+    update_all_values = pyqtSignal(bool)
+    """
+    QT signal emitted when the user has requested to see all values of
+    processor modules update in real time when running the processor.
+    """
+
+    set_delay = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to set the delay between
+    clock cycles when running the processor.
+    """
+
+    def __init__(self, parent: typing.Optional[QWidget] = None) -> None:
+        super().__init__(parent)
+
+        # Create load action
+        self._load_action = QAction("Load", self)
+        self._load_action.setStatusTip("Load processor state from file")
+        self._load_action.triggered.connect(self.load)
+
+        # Create save action
+        self._save_action = QAction("Save", self)
+        self._save_action.setStatusTip("Save processor state to file")
+        self._save_action.triggered.connect(self.save)
+
+        self._reset_action = QAction("Reset", self)
+        self._reset_action.setStatusTip("Reset processor")
+        self._reset_action.triggered.connect(self.reset)
+
+        # Create File menu for load and save
+        file_menu = self.addMenu("&File")
+        file_menu.addAction(self._load_action)
+        file_menu.addAction(self._save_action)
+        file_menu.addAction(self._reset_action)
+
+        # create load layout action
+        load_layout_action = QAction("Load layout", self)
+        load_layout_action.setStatusTip("Loads the selected layout from file.")
+        load_layout_action.triggered.connect(self.load_layout)
+
+        # create load default layout action
+        load_default_layout_action = QAction("Load default layout", self)
+        load_default_layout_action.setStatusTip("Loads the default layout from file.")
+        load_default_layout_action.triggered.connect(self.load_default_layout)
+
+        # create save layout action
+        save_layout_action = QAction("Save layout", self)
+        save_layout_action.setStatusTip("Saves the current layout to a file.")
+        save_layout_action.triggered.connect(self.save_layout)
+
+        # create layout lock action
+        self._lock_layout_action = QAction("Lock layout", self, checkable=True)
+        self._lock_layout_action.setStatusTip("Lock layout so items can't be moved.")
+        self._lock_layout_action.triggered.connect(self.lock_layout)
+
+        # Create show signals actions
+        self._signal_vis_action = QAction("Show signals", self, checkable=True)
+        self._signal_vis_action.setChecked(True)
+        self._signal_vis_action.setStatusTip("Toggle the visibility of signal.")
+        self._signal_vis_action.triggered.connect(self.show_all_signals)
+
+        # Create show port name actions
+        self._port_vis_action = QAction("Show port names", self, checkable=True)
+        self._port_vis_action.setChecked(True)
+        self._port_vis_action.setStatusTip("Toggle the visibility of port names.")
+        self._port_vis_action.triggered.connect(self.show_port_names)
+
+        # Create Layout menu for layout actions
+        layout_menu = self.addMenu("&Layout")
+        layout_menu.addAction(load_layout_action)
+        layout_menu.addAction(load_default_layout_action)
+        layout_menu.addAction(save_layout_action)
+        layout_menu.addAction(self._lock_layout_action)
+        layout_menu.addAction(self._signal_vis_action)
+        layout_menu.addAction(self._port_vis_action)
+
+        # Create breakpoint window action
+        self._breakpoint_action = QAction("Breakpoints", self)
+        self._breakpoint_action.setStatusTip("Open breakpoint window.")
+        self._breakpoint_action.triggered.connect(self.show_breakpoints)
+
+        # 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.update_all_values)
+
+        # 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_delay)
+
+        # Create Tools menu for tool actions actions
+        tools_menu = self.addMenu("&Tools")
+        tools_menu.addAction(self._breakpoint_action)
+        tools_menu.addAction(self._update_value_action)
+        tools_menu.addAction(self._set_delay_action)
+
+    @Slot(bool)
+    def set_disabled_when_running(self, value: bool) -> None:
+        """
+        Toggle the menu bar between enabled and disabled. When disabled,
+        actions which should not be performed while a processor is running
+        are disabled and greyed out.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to disable the menu bar, ``False`` to enable it.
+        """
+        self._load_action.setDisabled(value)
+        self._save_action.setDisabled(value)
+        self._reset_action.setDisabled(value)
diff --git a/src/simudator/gui/module_graphics_item/integer_memory_graphic.py b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py
new file mode 100644
index 0000000000000000000000000000000000000000..a177b6228b5395a5c163bf8d1c371f4e0f0a923a
--- /dev/null
+++ b/src/simudator/gui/module_graphics_item/integer_memory_graphic.py
@@ -0,0 +1,253 @@
+from enum import Enum
+
+from qtpy.QtWidgets import QButtonGroup, QHBoxLayout, QHeaderView, QRadioButton
+
+from simudator.gui.module_graphics_item.memory_graphic import (
+    Memory,
+    MemoryTable,
+    MemoryWindow,
+)
+
+
+class Base(Enum):
+    NONE = 0
+    DECIMAL_SIGNED = 1
+    DECIMAL_UNSIGNED = 2
+    BINARY = 3
+    HEXADECIMAL = 4
+
+
+class ValueTooBig(Exception):
+    """
+    A class representing the error of a user inputting a value that does not fit in the memory.
+
+    Used to differentiate between wrong input using the exception python raises by default (ValueError)
+    and the error of a value that does not fit.
+    """
+
+    pass
+
+
+class IntegerMemoryWindow(MemoryWindow):
+    """
+    A class showing the contents of a memory module in a new window.
+
+    Parameters
+    ----------
+    memory_module: Memory
+        An instance of the Memory base class.
+    bit_length: int
+        An integer specifying the number of bits of each address.
+    """
+
+    def __init__(self, memory_module: Memory, bit_length: int):
+
+        # Do not let parent create edit/view buttons
+        super().__init__(memory_module, False)
+
+        # Remove the memory table the parent created
+        self.layout.removeWidget(self._memory_table)
+
+        # Add our own
+        self._memory_table = IntegerMemoryTable(memory_module, bit_length)
+        self.layout.addWidget(self._memory_table)
+
+        # Create base buttons, they are exclusive by default
+        # so need a seperate QButtonGroup since these four
+        # have nothing to do with the edit/view buttons
+        self.dec_signed_button = QRadioButton("Decimal Signed")
+        self.dec_unsigned_button = QRadioButton("Decimal Unsigned")
+        self.bin_button = QRadioButton("Binary")
+        self.hex_button = QRadioButton("Hexadecimal")
+        self.base_group = QButtonGroup()
+        self.base_group.addButton(self.dec_signed_button, 1)
+        self.base_group.addButton(self.dec_unsigned_button, 2)
+        self.base_group.addButton(self.bin_button, 3)
+        self.base_group.addButton(self.hex_button, 4)
+
+        # Create edit/view buttons, they are exclusive by default
+        self.edit_button = QRadioButton("Edit")
+        self.view_button = QRadioButton("View")
+        self.edit_group = QButtonGroup()
+        self.edit_group.addButton(self.edit_button, 1)
+        self.edit_group.addButton(self.view_button, 2)
+
+        # Connect them to the 'set_base' function
+        self.base_group.buttonClicked.connect(self._set_base)
+        # Connect them to the 'set_edit' function
+        self.edit_group.buttonClicked.connect(self._set_edit)
+
+        # Create a layout that expands horizontally, so that
+        # all buttons appear in one row
+        self.first_button_layout = QHBoxLayout()
+        self.first_button_layout.addWidget(self.dec_signed_button)
+        self.first_button_layout.addWidget(self.dec_unsigned_button)
+        self.first_button_layout.addWidget(self.bin_button)
+        self.first_button_layout.addWidget(self.hex_button)
+        self.layout.addLayout(self.first_button_layout)
+
+        self.second_button_layout = QHBoxLayout()
+        self.second_button_layout.addWidget(self.edit_button)
+        self.second_button_layout.addWidget(self.view_button)
+        self.layout.addLayout(self.second_button_layout)
+
+        # Set the default base to decimal and disable editing
+        # from start
+        self.bin_button.toggle()
+        self.view_button.toggle()
+        self._set_base()
+        self._set_edit()
+
+        # Sets the size of each column to the smallest possible
+        # width that allows all content in each box to be visible
+        self._memory_table.horizontalHeader().setSectionResizeMode(
+            QHeaderView.ResizeToContents
+        )
+
+    def _set_base(self):
+        pressed_button_id = self.base_group.checkedId()
+        if pressed_button_id == 1:
+            base = Base.DECIMAL_SIGNED
+        elif pressed_button_id == 2:
+            base = Base.DECIMAL_UNSIGNED
+        elif pressed_button_id == 3:
+            base = Base.BINARY
+        else:
+            base = Base.HEXADECIMAL
+
+        self._memory_table.set_base(base)
+        self._memory_table.update()
+
+
+class IntegerMemoryTable(MemoryTable):
+    """
+    A class showing the contents of a memory module in a QTableWidget.
+
+    This class assumes that the size of the memory module will remain constant.
+
+    Parameters
+    ----------
+    memory_module: Memory
+        An instance of the Memory base class.
+    bit_length: int
+        An integer specifying the number of bits of each address.
+    column_size: int
+        An integer specifying the number of columns, optional.
+
+    """
+
+    def __init__(self, memory_module: Memory, bit_length: int, column_size=-1):
+        self._base = Base.DECIMAL_UNSIGNED
+        self._bit_length = bit_length
+        super().__init__(memory_module, column_size)
+
+        self.update()
+        # Signal used to detect when the user has edited a cell
+        # This code can only be called after the initial self.update().
+        # When the QTableWidget is initialized it currently fills each cell with empty strings.
+        # It also signals cellChanged for each cell it initializes. Thus _on_cell_changed
+        # is called for each cell and tries to save the cells empty string into the modules memory as
+        # an integer.
+        self.cellChanged.connect(self._on_cell_changed)
+
+    def update(self):
+        """
+        Update the content of this widget to reflect the content of the memory module.
+        """
+        memory_content = self._memory.get_state()["memory"]
+        for i in range(self._memory_size):
+            value = memory_content[i]
+            row = i // self._column_size
+            col = i % self._column_size
+            if self._base == Base.DECIMAL_UNSIGNED or self._base == Base.DECIMAL_SIGNED:
+                value = self._get_dec_from_sign(value)
+            elif self._base == Base.BINARY:
+                value = str(bin(int(value)))[2:]  # remove '0b'
+            else:  # Hex
+                value = str(hex(int(value)))[2:]  # remove '0x'
+            self.set_item(row, col, value)
+
+    def set_base(self, base: Base):
+        """
+        Change what base the content is viewed in.
+
+        Parameters
+        ----------
+        base: Base
+            An instance of the base enum representing what base to change to.
+        """
+        self._base = base
+
+    def _get_dec_from_sign(self, value: int) -> str:
+        if self._base == Base.DECIMAL_UNSIGNED:
+            return str(int(value))
+
+        # Any value above or equal to 'threshold' must have the MSB set to 1
+        threshold = 2 ** (self._bit_length - 1)
+
+        if value < threshold:
+            return str(int(value))
+
+        # If the MSB is set to one subtract its value twice,
+        # once so we dont count the positive value from it,
+        # and the second time so we count it as negative
+        return str(value - (threshold * 2))
+
+    def _on_cell_changed(self, row, col):
+        state = self._memory.get_state()
+
+        item = self.item(row, col)
+        value = item.text()
+        max_value = 2**self._bit_length
+        min_value = -(2 ** (self._bit_length - 1))
+
+        # Turn every value into a positive int
+        # in base 10
+        # This makes converting between bases
+        # much easier since we can assume what base
+        # and format each value is in
+
+        try:
+            if self._base == Base.BINARY:
+                value = int(value, 2)
+            elif self._base == Base.HEXADECIMAL:
+                value = int(value, 16)
+            elif (
+                self._base == Base.DECIMAL_SIGNED or self._base == Base.DECIMAL_UNSIGNED
+            ):
+                value = int(value)
+
+            if value > max_value or value < min_value:
+                raise ValueTooBig
+
+        except ValueError:
+            msg = None
+            if self._base == Base.BINARY:
+                msg = "You must enter a binary number (e.g. 0b101)."
+            elif (
+                self._base == Base.DECIMAL_SIGNED or self._base == Base.DECIMAL_UNSIGNED
+            ):
+                msg = "You must enter a decimal number (e.g. 107)."
+            elif self._base == Base.HEXADECIMAL:
+                msg = "You must enter a hexadecimal number (e.g. 0xc3)."
+            self._errorMessageWidget.showMessage(msg)
+            self.update()
+            return
+
+        except ValueTooBig:
+            self._errorMessageWidget.showMessage(
+                f"Input value does not fit within the bit length of {self._bit_length}."
+            )
+            self.update()
+            return
+
+        # Turn negative signed numbers into their positive unsigned
+        # counter part
+        # Ex 4 bits:
+        # -7 = 1001
+        # -7 + 16 % 16 = 9 = 1001 = -7
+        value = (value + max_value) % max_value
+
+        index = row * 4 + col
+        state['memory'][index] = value
+        self._memory.set_state(state)
diff --git a/src/simudator/gui/module_graphics_item/memory_graphic.py b/src/simudator/gui/module_graphics_item/memory_graphic.py
index 2817042cc772ef63fbbce36395542b585fda8eec..c4a944d213270e0f99330c5dede4d914dfc2a611 100644
--- a/src/simudator/gui/module_graphics_item/memory_graphic.py
+++ b/src/simudator/gui/module_graphics_item/memory_graphic.py
@@ -5,10 +5,17 @@ from qtpy.QtCore import Signal as pyqtSignal
 from qtpy.QtCore import Slot
 from qtpy.QtWidgets import (
     QAction,
+    QButtonGroup,
+    QErrorMessage,
     QGraphicsRectItem,
     QGraphicsSimpleTextItem,
+    QHBoxLayout,
+    QHeaderView,
+    QRadioButton,
     QTableWidget,
     QTableWidgetItem,
+    QVBoxLayout,
+    QWidget,
 )
 
 from simudator.core.modules import Memory
@@ -18,7 +25,66 @@ from simudator.gui.orientation import Orientation
 from simudator.gui.port_graphics_item import PortGraphicsItem
 
 
-class MemoryWindow(QTableWidget):
+class MemoryWindow(QWidget):
+    """
+    A class showing the contents of a memory module in a new window.
+
+    Parameters
+    ----------
+    memory_module: Memory
+        An instance of the Memory base class.
+    edit_buttons: bool
+        A bool deciding whether or not to add buttons to the widget
+        for toggling between setting the memory contents to 
+        editable or not.
+    """
+
+    def __init__(self, memory_module: Memory, edit_buttons=True):
+        super().__init__()
+
+        # Create a layout that expands vertically
+        # so that the buttons can be displayed below the
+        # memory content
+        self.layout = QVBoxLayout(self)
+        self._memory_table = MemoryTable(memory_module)
+        self.layout.addWidget(self._memory_table)
+
+        if edit_buttons:
+            # Create edit/view buttons, they are exclusive by default
+            self.edit_button = QRadioButton("Edit")
+            self.view_button = QRadioButton("View")
+            self.edit_group = QButtonGroup()
+            self.edit_group.addButton(self.edit_button, 1)
+            self.edit_group.addButton(self.view_button, 2)
+
+            # Connect them to the 'set_base' function
+            self.edit_group.buttonClicked.connect(self._set_edit)
+            self.second_button_layout = QHBoxLayout()
+            self.second_button_layout.addWidget(self.edit_button)
+            self.second_button_layout.addWidget(self.view_button)
+            self.layout.addLayout(self.second_button_layout)
+
+            # Set the default base to decimal and disable editing
+            # from start
+            self.view_button.toggle()
+            self._set_edit()
+
+        # Sets the size of each column to the smallest possible
+        # width that allows all content in each box to be visible
+        self._memory_table.horizontalHeader().setSectionResizeMode(
+            QHeaderView.ResizeToContents
+        )
+        self._memory_table.update()
+
+    def _set_edit(self):
+        pressed_button_id = self.edit_group.checkedId()
+        if pressed_button_id == 1:
+            self._memory_table.set_editable(True)
+        if pressed_button_id == 2:
+            self._memory_table.set_editable(False)
+
+
+class MemoryTable(QTableWidget):
     """
     A class showing the contents of a memory module in a QTableWidget.
 
@@ -26,8 +92,10 @@ class MemoryWindow(QTableWidget):
 
     Parameters
     ----------
-    memory_module: An instance of the Memory base class.
-    column_size: An integer specifying the number of columns, optional.
+    memory_module: Memory
+        An instance of the Memory base class.
+    column_size: int
+        An integer specifying the number of columns, optional.
 
     """
 
@@ -38,8 +106,12 @@ class MemoryWindow(QTableWidget):
         self._memory_size = len(self._memory.get_state()["memory"])
         self._set_column_size()
         self.setColumnCount(self._column_size)
-        self.setRowCount(ceil(self._memory_size / self._column_size))
+        rows = ceil(self._memory_size / self._column_size)
+        self.setRowCount(rows)
+        self._init_table_items(self._column_size, rows)
         self.setHorizontalHeaderLabels(["+" + str(i) for i in range(4)])
+        self.set_editable(False)
+        self._errorMessageWidget = QErrorMessage()
 
         vertical_headers = []
         for i in range(0, self._memory_size, self._column_size):
@@ -49,6 +121,14 @@ class MemoryWindow(QTableWidget):
 
         self.update()
 
+        # Signal used to detect when the user has edited a cell
+        # This code can only be called after the initial self.update().
+        # When the QTableWidget is initialized it currently fills each celll with empty strings.
+        # It also signals cellChanged for each cell it initializes. Thus _on_cell_changed
+        # is called for each cell and tries to save the cells empty string into the modules memory as
+        # an integer.
+        self.cellChanged.connect(self._on_cell_changed)
+
     def update(self):
         """
         Update the content of this widget to reflect the content of the memory module.
@@ -58,7 +138,8 @@ class MemoryWindow(QTableWidget):
             value = memory_content[i]
             row = i // self._column_size
             col = i % self._column_size
-            self.set_item(row, col, str(value))
+            value = str(value)
+            self.set_item(row, col, value)
 
     def set_item(self, row: int, col: int, text: str) -> None:
         """Set the text at specified table cell to the given text.
@@ -66,9 +147,9 @@ class MemoryWindow(QTableWidget):
         Parameters
         ----------
         row: int
-             The items row position in the pipeline diagram.
+             The items row position in the table.
         col: int
-             The items column position in the pipeline diagram.
+             The items column position in the table.
         text: str
              The text to be displayed.
         """
@@ -76,6 +157,18 @@ class MemoryWindow(QTableWidget):
 
         self.setItem(row, col, item)
 
+    def set_editable(self, editable: bool) -> None:
+        if editable:
+            self.setEditTriggers(self.AllEditTriggers)
+        else:
+            self.setEditTriggers(self.NoEditTriggers)
+
+    def _init_table_items(self, cols: int, rows: int) -> None:
+        for col in range(cols):
+            for row in range(rows):
+                item = QTableWidgetItem()
+                self.setItem(row, col, item)
+
     def _set_column_size(self) -> None:
         """
         Set the column size to a reasonable value if the size was not given to the constructor.
@@ -97,6 +190,16 @@ class MemoryWindow(QTableWidget):
         self._column_size = 1
         return
 
+    def _on_cell_changed(self, row, col):
+        state = self._memory.get_state()
+        item = self.item(row, col)
+        value = item.text()
+        if value == "":
+            return
+        index = row * 4 + col
+        state['memory'][index] = value
+        self._memory.set_state(state)
+
 
 class MemoryGraphicsItem(ModuleGraphicsItem):
     """
@@ -172,7 +275,7 @@ class MemoryGraphicsItem(ModuleGraphicsItem):
         """
         Create and show a MemoryWindow that displays the contents of the module.
         """
-        self.memory_window = MemoryWindow(self.module)
+        self.memory_window = MemoryWindow(self._module)
         self.memory_window.show()
 
     def shouldIgnoreAction(self, action: QAction) -> bool:
@@ -192,10 +295,6 @@ class MemoryGraphicsItem(ModuleGraphicsItem):
         memory_br_action.triggered.connect(self.memoryBreakpointDialog)
         self.actions.append(memory_br_action)
 
-        edit_memory_action = QAction('Edit memory content', self)
-        edit_memory_action.triggered.connect(self.editMemoryContentDialog)
-        self.actions.append(edit_memory_action)
-
         show_content_action = QAction('Show memory content', self)
         show_content_action.triggered.connect(self.showMemoryContents)
         self.actions.append(show_content_action)
@@ -241,7 +340,8 @@ class MemoryGraphicsItem(ModuleGraphicsItem):
             module_state = self.module.get_state()
             module_state['memory'][parsed_address] = value
             self.module.set_state(module_state)
-            self.update_graphics_signal.emit()
+            self.module_edited.emit()
+            self.update()
 
     def getActionSignals(self) -> []:
         # Do parent signals and then add memory specific signals
diff --git a/src/simudator/gui/module_graphics_item/module_graphics_item.py b/src/simudator/gui/module_graphics_item/module_graphics_item.py
index 30ae67a723d2f6c1fd73596e917d49a19c63b8d6..2860e9e50e343f03d66e29b1927522e2cf8d314e 100644
--- a/src/simudator/gui/module_graphics_item/module_graphics_item.py
+++ b/src/simudator/gui/module_graphics_item/module_graphics_item.py
@@ -6,6 +6,7 @@ from qtpy.QtCore import Slot
 from qtpy.QtGui import QCursor
 from qtpy.QtWidgets import (
     QAction,
+    QErrorMessage,
     QGraphicsItem,
     QGraphicsObject,
     QGraphicsRectItem,
@@ -38,12 +39,16 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem):
     new_state_breakpoint_signal = pyqtSignal(str, str, str)
     update_graphics_signal = pyqtSignal()
 
+    module_edited = pyqtSignal()
+
     def __init__(self, module: Module, name: str = None):
         super().__init__()
 
         # Save module for later updates
         self.module = module
 
+        self.errorMessageWidget = QErrorMessage()
+
         # Use modules name if no name is given
         if name is None:
             self.name = self.module.name
@@ -183,15 +188,38 @@ class ModuleGraphicsItem(QGraphicsObject, QGraphicsItem):
         """
         try:
             parsed_value = ast.literal_eval(value)
+
+            # ast.literal_eval only raises an error the the
+            # parsed value isn't a valid data type.
+            # If the user edits GRX and enters a string "a"
+            # it will crash
+
+            if isinstance(parsed_value, list):
+                for value in parsed_value:
+                    if isinstance(value, str):
+                        raise TypeError
+            if isinstance(parsed_value, str):
+                raise TypeError
+
         except SyntaxError as e:
             self.errorMessageWidget.showMessage(str(e))
+        except ValueError as e:
+            self.errorMessageWidget.showMessage(
+                "Value Error. Unable to parse input. Make sure it is in the correct base."
+            )
+        except TypeError as e:
+            self.errorMessageWidget.showMessage(
+                "Type Error. Unable to parse input. Make sure it is the correct type."
+            )
         else:
             module_state = self.module.get_state()
             module_state[state] = parsed_value
             self.module.set_state(module_state)
             # Since we have changed a value we send a signal to the gui to update
-            self.update_graphics_signal.emit()
+            self.module_edited.emit()
+            self.update()
 
+    @Slot()
     def update(self):
         """
         Update the visuals of the graphics item to match it's module.
diff --git a/src/simudator/gui/pipeline.py b/src/simudator/gui/pipeline.py
index fc9d78012115a011bfc6574414ba6c8a478b6a67..3ba4ffea08234d266b38e1b1526f6803c6059432 100644
--- a/src/simudator/gui/pipeline.py
+++ b/src/simudator/gui/pipeline.py
@@ -1,5 +1,6 @@
 from typing import Any
 
+from qtpy.QtCore import Slot
 from qtpy.QtWidgets import QTableWidget, QTableWidgetItem
 
 
@@ -13,7 +14,9 @@ class PipeLine(QTableWidget):
 
     def __init__(self):
         super().__init__()
+        self.setEditTriggers(self.NoEditTriggers)
 
+    @Slot(list)
     def set_instructions(self, instructions: list[tuple[str, int, int]]) -> None:
         """
         Give the pipeline the current CPU instructions.
diff --git a/src/simudator/gui/processor_handler.py b/src/simudator/gui/processor_handler.py
new file mode 100644
index 0000000000000000000000000000000000000000..91612fd9a6e5af44e70f145e0745729c632d44a7
--- /dev/null
+++ b/src/simudator/gui/processor_handler.py
@@ -0,0 +1,335 @@
+from qtpy import QtCore
+from qtpy.QtCore import QObject, QThreadPool
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QErrorMessage, QFileDialog, QInputDialog, QMessageBox
+
+from simudator.core.processor import Processor
+from simudator.gui.run_continuously_thread import RunThread
+
+
+class ProcessorHandler(QObject):
+    """
+    Wrapper class for interacting with the processor in the GUI.
+
+    Parameters
+    ----------
+    processor : Processor
+    parent : QObject | None
+        Optional parent QObject that takes ownership of this QObject.
+    """
+
+    running = pyqtSignal(bool)
+    """
+    PyQT signal emitted when the processor has started or finished running. 
+    Emits ``True`` when it has started and ``False`` when it has finished.
+    """
+    cycle_changed = pyqtSignal(int)
+    """
+    PyQT signal emitted when the current clock cycle count of the processor 
+    has changed. Emits the current clock cycle count as an ``int``.
+    """
+    changed = pyqtSignal()
+    """
+    PyQT signal emitted when the processor has changed state.
+    """
+    changed_instruction = pyqtSignal(list)
+    """
+    PyQT signal emitted when the processor has changed the current instruction(s)
+    being executed. Emits a list of instructions.
+
+    See Also
+    --------
+    simudator.core.processor.get_current_instructions :
+        Method of the processor used to get instructions that are emitted.
+    """
+
+    def __init__(
+        self,
+        processor: Processor,
+        parent: QObject | None = None,
+    ) -> None:
+        super().__init__(parent)
+        self._processor = processor
+        # Threadpool for running cpu and gui concurrently
+        self._threadpool = QThreadPool.globalInstance()
+
+        # 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
+        self._processor_running = False
+
+        self._error_msg_box = QErrorMessage()
+
+    def _signal_processor_changed(self) -> None:
+        """
+        Emit signals for signifying that the processor has changed its state.
+        """
+        self.cycle_changed.emit(self._processor.get_clock())
+        self.changed.emit()
+        self.changed_instruction.emit(self._processor.get_current_instructions())
+
+    @Slot(int)
+    def step_cycles(self, cycles: int):
+        """
+        Run some number of clock cycles of the processor.
+
+        Parameters
+        ----------
+        Number of clock cycles to perform.
+        """
+
+        # Don't do steps if cpu is running
+        if self._processor_running:
+            return
+
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(
+            self._processor,
+            self._update_delay,
+            False,
+            False,
+            cycles,
+        )
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot(int)
+    def step_asm_instructions(self, instructions: int):
+        """
+        Run some numer of asm instructions.
+
+        Parameters
+        ----------
+        instructions : int
+            Number of asm instructions to perform.
+        """
+
+        # Don't do steps if cpu is running
+        if self._processor_running:
+            return
+
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(
+            self._processor, self._update_delay, False, True, instructions
+        )
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot()
+    def run_simulation(self) -> None:
+        """
+        Run the processor continuously until it is stopped by user input,
+        reaches a breakpoint or halts on its own.
+        """
+
+        # Don't run if already running
+        if self._processor_running:
+            return
+
+        # Create own thread for cpu simulation so gui dosent freeze
+        self._processor_running = True
+        self.running.emit(True)
+        simulation_thread = RunThread(self._processor, self._update_delay)
+        simulation_thread.processor_tick.connect(self.on_processor_tick)
+        self._threadpool.start(simulation_thread)
+
+    @Slot(int)
+    def on_processor_tick(self, steps: int) -> None:
+        """
+        Called from other thread after every cpu tick.
+        Will inform the user and update visuals.
+        """
+
+        # Update cpu clock counter every tick
+        self.cycle_changed.emit(self._processor.get_clock())
+
+        if self._update_all_values:
+            self._signal_processor_changed()
+
+        # 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._processor_running = False
+            self.running.emit(False)
+            self._signal_processor_changed()
+
+            # Inform user of reached break point
+            if self._processor.breakpoint_reached:
+                QMessageBox.information(
+                    self.parent(),
+                    "SimuDator",
+                    f"Reached breakpoint: {str(self._processor.last_breakpoint)}",
+                )
+
+            # Inform user of halt
+            if self._processor.should_halt():
+                QMessageBox.information(
+                    self.parent(), "SimuDator", "The processor halted."
+                )
+
+    @Slot()
+    def stop_simulation(self) -> None:
+        """
+        Stop the processor.
+        """
+        self._processor.stop()
+
+    def get_save_path(self) -> str:
+        """
+        Prompt the user for a file path meant for saving to file. Creates the
+        specified file if it does not exist.
+        """
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+
+        return dialog.getSaveFileName()[0]
+
+    def get_file_path(self) -> str:
+        """
+        Prompt the user for a file path meant for loading the processor state.
+        """
+        dialog = QFileDialog()
+        dialog.setFileMode(QFileDialog.AnyFile)
+        dialog.setAcceptMode(QFileDialog.AcceptOpen)
+
+        return dialog.getOpenFileName()[0]
+
+    @Slot()
+    def load_state(self) -> None:
+        """
+        Load the processor state from selected file.
+        """
+
+        # not safe to load while cpu is running
+        if self._processor_running:
+            return
+
+        path = self.get_file_path()
+        try:
+            self._processor.load_state_from_file(path)
+        except KeyError:
+            self._error_msg_box.showMessage("Could not load selected file.", "load_err")
+        except ValueError:
+            self._error_msg_box.showMessage("Selected file was empty.", "load_empty")
+        except FileNotFoundError:
+            # This error is triggered when no file was selected by
+            # by the user. The user should know they did not select a
+            # file and therefore there no need to display a box telling
+            # them that.
+            # self.messageBox("No valid file was selected.")
+            pass
+        else:
+            QMessageBox.information(None, "SimuDator", "Loaded file successfully.")
+            self._signal_processor_changed()
+
+    @Slot()
+    def reset(self) -> None:
+        """
+        Reset the processor to initial values and signal the processor state
+        change.
+        """
+
+        # not safe to reset while cpu running
+        if self._processor_running:
+            return
+
+        answer = QMessageBox.question(
+            self.parent(),
+            "Reset Processor",
+            "Are you sure you want to reset the processor?",
+        )
+
+        if answer == QMessageBox.Yes:
+            self._processor.reset()
+            self._signal_processor_changed()
+
+    @Slot()
+    def save_state(self) -> None:
+        """
+        Save the state of the processor to file.
+
+        This erases the previous content of the file before saving the data.
+        """
+
+        # Not safe to save while running
+        if self._processor_running:
+            return
+
+        path = self.get_save_path()
+
+        # Change file to the selected file if a file was selected
+        if path == '':
+            return
+
+        res = self._processor.save_state_to_file(path)
+
+        if res:
+            QMessageBox.information(self.parent(), "SimuDator", "File saved.")
+        else:
+            self._error_msg_box.showMessage("Unable to save.", "save_err")
+
+    @Slot(int)
+    def undo_cycles(self, cycles: int) -> None:
+        """
+        Undo some number of processor clock cycles.
+
+        Parameters
+        ----------
+        cycles : int
+            Number of clock cycles to undo.
+        """
+        try:
+            self._processor.load_cycle(max(self._processor.get_clock() - cycles, 0))
+            self._signal_processor_changed()
+        except (ValueError, IndexError):
+            self._error_msg_box.showMessage("Unable to undo the cycle.")
+
+    @Slot(int)
+    def undo_asm_instructions(self, instructions: int) -> None:
+        """
+        Undo some number of asm instructions.
+
+        Parameters
+        ----------
+        instructions : int
+            Number of instructions to undo.
+        """
+        try:
+            self._processor.undo_asm_instruction(instructions)
+            self._signal_processor_changed()
+        except (ValueError, IndexError):
+            self._error_msg_box("Unable to undo the instruction.")
+
+    @Slot()
+    def toggle_value_update_on_run(self):
+        """
+        Toggle whether all values in the GUI related to the processor should be
+        updated after each clock cycle or after having run several cycles at a
+        time / running continuously. The clock cycle counter is always updated
+        after each tick regardless.
+        """
+        self._update_all_values = not self._update_all_values
+
+    @Slot()
+    def set_update_delay(self):
+        """
+        Set the delay waited between each consecutive clock cycle when running
+        several cycles or continuously. Prompts the user for the delay.
+        """
+        delay, ok = QInputDialog.getDouble(
+            None, "Input Dialog", "Enter a float value:", decimals=5
+        )
+        if ok:
+            self._update_delay = delay
diff --git a/src/simudator/gui/run_continuously_thread.py b/src/simudator/gui/run_continuously_thread.py
index 72d0fdcbb58e3352901f21c5fca428a17200f2d4..13368de4f10407cce26c1fb8cf2bb1bc46c10186 100644
--- a/src/simudator/gui/run_continuously_thread.py
+++ b/src/simudator/gui/run_continuously_thread.py
@@ -1,39 +1,54 @@
 import time
 
-from qtpy.QtCore import QRunnable
+from qtpy.QtCore import QObject, QRunnable
+from qtpy.QtCore import Signal as pyqtSignal
+
+
+class SignalHolder(QObject):
+    """
+    Helper class for RunThread as the thread cannot inherit from QObject to
+    have signals directly. Instead, an instance of this class is used to hold
+    signals for the thread.
+    """
+
+    processor_tick = pyqtSignal(int)
+
 
 class RunThread(QRunnable):
     """
-    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 class is used to run the simulated cpu several ticks or continuously
+    on a separate thread. This allows the user to interact with the GUI while
+    the simulation is running.
 
-    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 
+    After each CPU tick, this thread will emit to its 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, delay: float, run_continuously=True, 
-                 step_asm=False, steps=0):
+    def __init__(
+        self, cpu, delay: float, run_continuously=True, step_asm=False, steps=0
+    ):
+
         super().__init__()
         self.cpu = cpu
-        self.signal = signal
         self.run_continuously = run_continuously
         self.steps = steps
         self.delay = delay
         self.step_asm = step_asm
+        self.signals = SignalHolder()
+        self.processor_tick = self.signals.processor_tick
 
     def run(self):
         if self.step_asm:
 
             while self.steps > 0:
                 self.cpu.do_tick()
+                self.processor_tick.emit(1)
 
                 # We only care about asm instructions
                 if self.cpu.is_new_instruction():
                     self.steps -= 1
 
-                self.signal.emit(1)
                 time.sleep(self.delay)
 
                 if self.cpu.is_stopped:
@@ -44,18 +59,17 @@ class RunThread(QRunnable):
                 self.cpu.unstop()
                 while not self.cpu.is_stopped:
                     self.cpu.do_tick()
-                    self.signal.emit(1)
+                    self.processor_tick.emit(1)
                     time.sleep(self.delay)
 
             else:
                 for _ in range(self.steps):
                     self.cpu.do_tick()
-                    self.signal.emit(1)
+                    self.processor_tick.emit(1)
                     time.sleep(self.delay)
 
                     if self.cpu.is_stopped:
                         break
 
-
         # Signal end of execution as having run 0 ticks
-        self.signal.emit(0)
+        self.processor_tick.emit(0)
diff --git a/src/simudator/gui/simulation_toolbar.py b/src/simudator/gui/simulation_toolbar.py
new file mode 100644
index 0000000000000000000000000000000000000000..090627eaae00362cee96f3aa2f61159b6fc9faf7
--- /dev/null
+++ b/src/simudator/gui/simulation_toolbar.py
@@ -0,0 +1,193 @@
+from PyQt5.QtWidgets import QAction
+from qtpy import QtGui
+from qtpy.QtCore import Signal as pyqtSignal
+from qtpy.QtCore import Slot
+from qtpy.QtWidgets import QLabel, QSpinBox, QStyle, QToolBar, QWidget
+
+
+class SimulationToolBar(QToolBar):
+    """
+    A toolbar for basic controls of a processor simulation. This includes
+    running and stopping the processor, stepping and undoing clock cycles and
+    stepping and undoing asm instructions.
+
+    Parameters
+    ----------
+    title : str
+        Title of the toolbar.
+    parent : QWidget | None
+        Parent of the widget. The widget will be a new window if it is ``None``
+        (default).
+    """
+
+    run = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to run the processor
+    continuously.
+    """
+
+    stop = pyqtSignal()
+    """
+    QT signal emitted when the user has requested to stop the processor.
+    """
+
+    step = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to step some clock cycles
+    of the processor.
+    """
+
+    undo = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to undo some clock cycles of
+    the processor.
+    """
+
+    step_asm = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to step some asm instructions
+    of the processor.
+    """
+
+    undo_asm = pyqtSignal(int)
+    """
+    QT signal emitted when the user has requested to undo some asm instructions
+    of the processor.
+    """
+
+    def __init__(self, title, parent: QWidget | None = None):
+        super().__init__(title, parent)
+        # Run continuously button
+        arrow_icon = self.style().standardIcon(QStyle.SP_MediaPlay)
+        self._run_action = QAction(arrow_icon, "Run", self)
+        self._run_action.setStatusTip("Run until halt")
+        self._run_action.triggered.connect(self.run.emit)
+        self.addAction(self._run_action)
+
+        # Stop button
+        stop_icon = self.style().standardIcon(QStyle.SP_MediaStop)
+        self._stop_action = QAction(stop_icon, "Stop", self)
+        self._stop_action.setStatusTip("Stop running")
+        self._stop_action.triggered.connect(self.stop.emit)
+        self.addAction(self._stop_action)
+
+        # Separator and spacing between basic controls and stepping clock cycles
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+        self.addSeparator()
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+
+        # Clock cycle counter
+        self._clock_cycle_label = QLabel("Clock cycles: 0", self)
+        self._clock_cycle_label.setMinimumWidth(150)
+        self.addWidget(self._clock_cycle_label)
+
+        # Undo clock cycle button
+        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
+        self._undo_action = QAction(backward_arrow_icon, "Undo", self)
+        self._undo_action.setStatusTip("Undo the last processor tick")
+        self._undo_action.triggered.connect(self._signal_undo)
+        self.addAction(self._undo_action)
+
+        # Step clock cycle button
+        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
+        self._step_action = QAction(forward_arrow_icon, "Step", self)
+        self._step_action.setStatusTip("Run one clock cycle")
+        self._step_action.triggered.connect(self._signal_step)
+        self.addAction(self._step_action)
+
+        # Spin box for the number of clock cycles to step/undo at a time
+        self._jump_value_box = QSpinBox()
+        self._jump_value_box.setMinimum(999999)
+        self._jump_value_box.setMinimum(1)
+        self._jump_value_box.setValue(1)
+        self.addWidget(self._jump_value_box)
+
+        # Separator and spacing between steping clock cycles and
+        # stepping asm instructions
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+        self.addSeparator()
+        spacing = QWidget()
+        spacing.setFixedWidth(10)
+        self.addWidget(spacing)
+
+        # Asm instruction counter
+        self._asm_label = QLabel("Assembler instructions: ", self)
+        self.addWidget(self._asm_label)
+
+        # Undo asm instruction button
+        backward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekBackward)
+        self._undo_asm_action = QAction(backward_arrow_icon, "Undo Asm", self)
+        self._undo_asm_action.setStatusTip("Undo the last assembler instruction")
+        self._undo_asm_action.triggered.connect(self._signal_undo_asm)
+        self.addAction(self._undo_asm_action)
+
+        # Step asm instruction button
+        forward_arrow_icon = self.style().standardIcon(QStyle.SP_MediaSeekForward)
+        self._step_asm_action = QAction(forward_arrow_icon, "Step Asm", self)
+        self._step_asm_action.setStatusTip("Run one assembler instruction")
+        self._step_asm_action.triggered.connect(self._signal_step_asm)
+        self.addAction(self._step_asm_action)
+
+        # Spin box for the number of asm instructions to step/undo at a time
+        self._asm_jump_value_box = QSpinBox()
+        self._asm_jump_value_box.setMinimum(999999)
+        self._asm_jump_value_box.setMinimum(1)
+        self._asm_jump_value_box.setValue(1)
+        self.addWidget(self._asm_jump_value_box)
+
+    def contextMenuEvent(self, a0: QtGui.QContextMenuEvent | None) -> None:
+        # Override to remove the functionality of hiding the toolbar as there
+        # is currently no way of showing it again
+        pass
+
+    @Slot(int)
+    def update_clock(self, clock_cycle: int) -> None:
+        """
+        Set the displayed number of the clock cycle counter.
+
+        Parameters
+        ----------
+        clock_cycle : int
+            Number of clock cycles to display.
+        """
+        self._clock_cycle_label.setText(f"Clock cycles: {clock_cycle}")
+
+    @Slot(bool)
+    def set_disabled_when_running(self, value: bool) -> None:
+        """
+        Disable relevant parts of the toolbar related to controlling a
+        processor. Intended to be used to disable stepping functionality
+        while the processor is running.
+
+        All buttons except for the stop button are disabled. The disabled
+        buttons are greyed out and cannot be interacted with while the toolbar
+        is disabled.
+
+        Parameters
+        ----------
+        value : bool
+            ``True`` to disable the toolbar, ``False`` to enable it.
+        """
+        self._run_action.setDisabled(value)
+        self._step_action.setDisabled(value)
+        self._undo_action.setDisabled(value)
+        self._step_asm_action.setDisabled(value)
+        self._undo_asm_action.setDisabled(value)
+
+    def _signal_step(self):
+        self.step.emit(self._jump_value_box.value())
+
+    def _signal_undo(self):
+        self.undo.emit(self._jump_value_box.value())
+
+    def _signal_step_asm(self):
+        self.step_asm.emit(self._asm_jump_value_box.value())
+
+    def _signal_undo_asm(self):
+        self.undo_asm.emit(self._asm_jump_value_box.value())
diff --git a/src/simudator/gui/view.py b/src/simudator/gui/view.py
new file mode 100644
index 0000000000000000000000000000000000000000..08e1b3535b69629ce2fa6227024ce9e95e71cb43
--- /dev/null
+++ b/src/simudator/gui/view.py
@@ -0,0 +1,49 @@
+from qtpy import QtCore, QtWidgets
+from qtpy.QtGui import QWheelEvent
+from qtpy.QtWidgets import QGraphicsView
+
+
+class View(QGraphicsView):
+    """
+    This class views all QGraphicsItems for the user. It takes the
+    QGraphicsScene as input and inherits all funcitonality from the
+    QGraphicsView and overrides the wheelEvent function.
+    This allows the users to navigate the view with their trackpads
+    and zoom in/out with their trackpads + ctrl.
+    """
+
+    def __init__(self, QGraphicsScene):
+        super().__init__(QGraphicsScene)
+        self.scene = QGraphicsScene
+
+    def wheelEvent(self, event: QWheelEvent | None) -> None:
+        """
+        Default behaviour if ctrl is not pressed, otherwise zoom in/out.
+        """
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        if modifiers == QtCore.Qt.ControlModifier:
+            # Factor above 1 zooms in, below zooms out
+            factor = 1.03
+            if event.angleDelta().y() < 0:
+                factor = 0.97
+
+            # If event got triggered due to the x axis, do nothing
+            if event.pixelDelta().x() != 0:
+                return
+
+            view_pos = event.globalPosition()
+            scene_pos = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
+
+            self.centerOn(scene_pos)
+            self.scale(factor, factor)
+
+            old_mapToScene = self.mapToScene(int(view_pos.x()), int(view_pos.y()))
+            new_mapToScene = self.mapToScene(self.viewport().rect().center())
+
+            delta = old_mapToScene - new_mapToScene
+
+            self.centerOn(scene_pos - delta)
+
+        else:
+            # Default behaviour
+            super().wheelEvent(event)
diff --git a/src/simudator/processor/mia/gui/mia_alu_graphic.py b/src/simudator/processor/mia/gui/mia_alu_graphic.py
index 737c7325cdaef600f745f4040fe317e782c80887..c5ef0e2e0b807e45fc07bb24a29a66216ffa4e49 100644
--- a/src/simudator/processor/mia/gui/mia_alu_graphic.py
+++ b/src/simudator/processor/mia/gui/mia_alu_graphic.py
@@ -25,12 +25,16 @@ class AluGraphicsItem(ModuleGraphicsItem):
         )
 
         # make port for input a
-        port_a = PortGraphicsItem(self.module.signals["in_input_a"], Orientation.UP, self)
+        port_a = PortGraphicsItem(
+            self.module.signals["in_input_ar"], Orientation.UP, self
+        )
         port_a.setPos(self.RECT_WIDTH * 3 / 4, 0)
         self.ports.append(port_a)
 
         # make port for input b
-        port_b = PortGraphicsItem(self.module.signals["in_input_b"], Orientation.UP, self)
+        port_b = PortGraphicsItem(
+            self.module.signals["in_input_bus"], Orientation.UP, self
+        )
         port_b.setPos(self.RECT_WIDTH / 4, 0)
         self.ports.append(port_b)
 
diff --git a/src/simudator/processor/mia/gui/mia_grx_graphic.py b/src/simudator/processor/mia/gui/mia_grx_graphic.py
index d7d94295ee0adc6f33fc7dc5f1c60d5beaa9bfc5..54dba094d489c8f8493d02cf60550c3cc5fb6c8a 100644
--- a/src/simudator/processor/mia/gui/mia_grx_graphic.py
+++ b/src/simudator/processor/mia/gui/mia_grx_graphic.py
@@ -3,6 +3,7 @@ import math
 from qtpy.QtCore import QPointF
 from qtpy.QtGui import QPolygonF
 from qtpy.QtWidgets import (
+    QErrorMessage,
     QGraphicsLineItem,
     QGraphicsPolygonItem,
     QGraphicsRectItem,
@@ -126,14 +127,18 @@ class GrxGraphicsItem(ModuleGraphicsItem):
         )
 
         # Add ports to and from bus
-        from_bus_port = PortGraphicsItem(self.module.signals["in_input"], Orientation.RIGHT, self)
+        from_bus_port = PortGraphicsItem(
+            self.module.signals["in_input"], Orientation.RIGHT, self
+        )
         from_bus_port.setPos(
             rect_width + self.LINE_LENGTH + self.MUX_WIDTH,
             # Use the buses port margins so the ports align nicely
             mux_height / 2 - BusGraphicsItem.PORT_MARGIN / 2,
         )
 
-        to_bus_port = PortGraphicsItem(self.module.signals["out_content"], Orientation.RIGHT, self)
+        to_bus_port = PortGraphicsItem(
+            self.module.signals["out_content"], Orientation.RIGHT, self
+        )
         to_bus_port.setPos(
             rect_width + self.LINE_LENGTH + self.MUX_WIDTH,
             # Use the buses port margins so the ports align nicely
diff --git a/src/simudator/processor/mia/gui/mia_memory_graphic.py b/src/simudator/processor/mia/gui/mia_memory_graphic.py
index ec29b7717ebc86efe8b0762dc07f3abef6ca34a1..21d61b027d8bc3386cf0ff66eb3809969b47ce0d 100644
--- a/src/simudator/processor/mia/gui/mia_memory_graphic.py
+++ b/src/simudator/processor/mia/gui/mia_memory_graphic.py
@@ -1,17 +1,13 @@
 import ast
 
 from qtpy.QtCore import Slot
-from qtpy.QtWidgets import (
-    QErrorMessage,
-    QGraphicsRectItem,
-    QGraphicsSimpleTextItem,
-    QTextEdit,
-    QVBoxLayout,
-    QWidget,
-)
+from qtpy.QtWidgets import QErrorMessage, QGraphicsRectItem, QGraphicsSimpleTextItem
 
 from simudator.core.modules import Memory
 from simudator.gui.color_scheme import ColorScheme as CS
+from simudator.gui.module_graphics_item.integer_memory_graphic import (
+    IntegerMemoryWindow,
+)
 from simudator.gui.module_graphics_item.memory_graphic import (
     MemoryGraphicsItem,
     MemoryWindow,
@@ -72,7 +68,8 @@ class MiaMemoryGraphicsItem(MemoryGraphicsItem):
         Create and show a MemoryWindow that displays the contents
         of the memory module associated with this graphics item.
         """
-        self.memory_window = MemoryWindow(self.module)
+        bit_length = 16
+        self.memory_window = IntegerMemoryWindow(self.module, bit_length)
         self.memory_window.show()
 
     def memoryBreakpointDialog(self) -> None:
diff --git a/src/simudator/processor/mia/mia.py b/src/simudator/processor/mia/mia.py
index b1a283daa0d51d208afeb6090b5535a48037bfe0..eb89efe3adf5197f7621593bb7c57d7bd787a401 100644
--- a/src/simudator/processor/mia/mia.py
+++ b/src/simudator/processor/mia/mia.py
@@ -38,7 +38,16 @@ class MIA_CPU(Processor):
         super().__init__()
         # Creating all signals
         # Signals follow the naming convention 'to_from'
-        self.max_line_len = 100
+
+        # Add state variable names to skip when pretty printing the MIA
+        # processor in the CLI
+        self._ignore_keys += [
+            "increment",
+            "read_from_bus",
+            "read_from_uADR",
+            "decrement_by_one",
+            "bus_id",
+        ]
 
         # A signal used by all modules connected to the bus
         bus_control = Signal(self, "bus_control", (0, 0))
@@ -50,54 +59,54 @@ class MIA_CPU(Processor):
         # Needed since optional arguments need to be in the correct order
         default_value = 0
 
-        pm_bus = Signal(self, "PM-Bus")
-        bus_pm = Signal(self, "Bus-PM")
-        asr_bus = Signal(self, "asr_bus")
-        pm_asr = Signal(self, "pm_asr")
-        alu_ar = Signal(self, "alu_ar")
-        ar_alu = Signal(self, "ar_alu")
-        bus_ar = Signal(self, "bus_ar")
-        alu_hr = Signal(self, "alu_hr")
-        hr_alu = Signal(self, "hr_alu")
-        bus_hr = Signal(self, "bus_hr")
-        hr_bus = Signal(self, "hr_bus")
-        alu_bus = Signal(self, "alu_bus")
-        alu_uM = Signal(self, "alu_uM")
-        z_alu = Signal(self, "z_alu")
-        n_alu = Signal(self, "n_alu")
-        o_alu = Signal(self, "o_alu")
-        c_alu = Signal(self, "c_alu")
-        grx_bus = Signal(self, "grx_bus")
-        bus_grx = Signal(self, "bus_grx")
-        grx_control = Signal(self, "grx_control")  # used to index registers with GRx
-        m_control = Signal(self, "m_control")  # used to index registers with M
-        s_control = Signal(self, "s_control")  # decides if GRx or M is used
-        pc_uM = Signal(self, "pc_uM")
-        bus_pc = Signal(self, "bus_pc")
-        pc_bus = Signal(self, "pc_bus")
-        pm_bus = Signal(self, "pm_bus")
-        bus_pm = Signal(self, "bus_pm")
+        pm_bus = Signal(self, "PM-Bus", 0)
+        bus_pm = Signal(self, "Bus-PM", 0)
+        asr_bus = Signal(self, "asr_bus", 0)
+        pm_asr = Signal(self, "pm_asr", 0)
+        alu_ar = Signal(self, "alu_ar", 0)
+        ar_alu = Signal(self, "ar_alu", 0)
+        bus_ar = Signal(self, "bus_ar", 0)
+        alu_hr = Signal(self, "alu_hr", 0)
+        hr_alu = Signal(self, "hr_alu", 0)
+        bus_hr = Signal(self, "bus_hr", 0)
+        hr_bus = Signal(self, "hr_bus", 0)
+        alu_bus = Signal(self, "alu_bus", 0)
+        alu_uM = Signal(self, "alu_uM", 0)
+        z_alu = Signal(self, "z_alu", 0)
+        n_alu = Signal(self, "n_alu", 0)
+        o_alu = Signal(self, "o_alu", 0)
+        c_alu = Signal(self, "c_alu", 0)
+        grx_bus = Signal(self, "grx_bus", 0)
+        bus_grx = Signal(self, "bus_grx", 0)
+        grx_control = Signal(self, "grx_control", 0)  # used to index registers with GRx
+        m_control = Signal(self, "m_control", 0)  # used to index registers with M
+        s_control = Signal(self, "s_control", 0)  # decides if GRx or M is used
+        pc_uM = Signal(self, "pc_uM", 0)
+        bus_pc = Signal(self, "bus_pc", 0)
+        pc_bus = Signal(self, "pc_bus", 0)
+        pm_bus = Signal(self, "pm_bus", 0)
+        bus_pm = Signal(self, "bus_pm", 0)
         always_write_signal = Signal(self, value=(999, 0))
-        uPC_k1 = Signal(self, "uPC_k1")
-        k1_ir = Signal(self, "k1_ir")
-        uPC_k2 = Signal(self, "uPC_k2")
-        ir_bus = Signal(self, "ir_bus")
-        bus_ir = Signal(self, "bus_ir")
-        SuPC_uPC = Signal(self, "SuPC_uPC")
-        uPC_SuPC = Signal(self, "uPC_SuPC")
-        uM_uPC = Signal(self, "uM_uPC")
-        uPC_uM_cont = Signal(self, "uPC_uM_cont")
-        uPC_uM = Signal(self, "uPC_uM")
-        bus_uM = Signal(self, "bus_uM")
-        lc_uM = Signal(self, "lc_uM")
-        lc_uM_uADR = Signal(self, "lc_uM_uADR")
-        uM_z = Signal(self, "uM_z")
-        uM_n = Signal(self, "uM_n")
-        uM_c = Signal(self, "uM_c")
-        uM_o = Signal(self, "uM_o")
-        uM_l = Signal(self, "uM_l")
-        lc_bus = Signal(self, "lc_bus")
-        l_lc = Signal(self, "l_lc")
+        uPC_k1 = Signal(self, "uPC_k1", 0)
+        k1_ir = Signal(self, "k1_ir", 0)
+        uPC_k2 = Signal(self, "uPC_k2", 0)
+        ir_bus = Signal(self, "ir_bus", 0)
+        bus_ir = Signal(self, "bus_ir", 0)
+        SuPC_uPC = Signal(self, "SuPC_uPC", 0)
+        uPC_SuPC = Signal(self, "uPC_SuPC", 0)
+        uM_uPC = Signal(self, "uM_uPC", 0)
+        uPC_uM_cont = Signal(self, "uPC_uM_cont", 0)
+        uPC_uM = Signal(self, "uPC_uM", 0)
+        bus_uM = Signal(self, "bus_uM", 0)
+        lc_uM = Signal(self, "lc_uM", 0)
+        lc_uM_uADR = Signal(self, "lc_uM_uADR", 0)
+        uM_z = Signal(self, "uM_z", 0)
+        uM_n = Signal(self, "uM_n", 0)
+        uM_c = Signal(self, "uM_c", 0)
+        uM_o = Signal(self, "uM_o", 0)
+        uM_l = Signal(self, "uM_l", 0)
+        lc_bus = Signal(self, "lc_bus", 0)
+        l_lc = Signal(self, "l_lc", 0)
 
         # ASR specific
         asr_bus_id = 0b111
@@ -112,11 +121,10 @@ class MIA_CPU(Processor):
         # HR specific
         hr_bus_id = 0b101
         hr_bit_length = 16
-        hr = HR(alu_hr, hr_alu, bus_hr, hr_bus, bus_control, hr_bus_id, hr_bit_length)
+        hr = HR(alu_hr, hr_alu, hr_bus, bus_hr, bus_control, hr_bus_id, hr_bit_length)
 
         # ALU specific
         alu = ALU(
-            "ALU",
             alu_ar,
             alu_bus,
             alu_hr,
@@ -127,6 +135,7 @@ class MIA_CPU(Processor):
             n_alu,
             o_alu,
             c_alu,
+            "ALU",
         )
 
         # PC specific
@@ -260,9 +269,9 @@ class MIA_CPU(Processor):
         for module in ALLTHEMODULES:
             self.add_module(module)
 
-        self.micro_memory = uM
+        self._micro_memory = uM
 
-        self.lambdas = {}
+        self._lambdas = {}
 
     def is_new_instruction(self) -> bool:
         return self.get_module("uPC").value == 0
@@ -305,7 +314,7 @@ class MIA_CPU(Processor):
         return (2, 2)
 
     def should_halt(self) -> bool:
-        micro_memory_state = self.micro_memory.get_state()
+        micro_memory_state = self._micro_memory.get_state()
         return micro_memory_state["halt"]
 
     def launch_gui(self):
@@ -334,7 +343,7 @@ class MIA_CPU(Processor):
             (None, None),
             (pc.signals["in_input"], pc.signals["out_content"]),
             (None, None),
-            (alu.signals["in_input_b"], None),
+            (alu.signals["in_input_bus"], None),
             (None, None),
             (None, ar.signals["out_content"]),
             (None, None),
@@ -343,36 +352,36 @@ class MIA_CPU(Processor):
             (grx.signals["in_input"], grx.signals["out_content"]),
         ]
 
-        gui.addModuleGraphicsItem(
+        gui.add_module_graphics_item(
             BusGraphicsItem(self.get_module("Bus"), bus_signal_pairs)
         )
-        gui.addModuleGraphicsItem(uPcGraphicsItem(self.get_module("uPC")))
-        gui.addModuleGraphicsItem(SupcGraphicsItem(self.get_module("SuPC")))
-        gui.addModuleGraphicsItem(PcGraphicsItem(self.get_module("PC")))
-        gui.addModuleGraphicsItem(ArGraphicsItem(self.get_module("AR")))
-        gui.addModuleGraphicsItem(AsrGraphicsItem(self.get_module("ASR")))
-        gui.addModuleGraphicsItem(HrGraphicsItem(self.get_module("HR")))
-        gui.addModuleGraphicsItem(MicroMemoryGraphicsItem(self.get_module("uM")))
-        gui.addModuleGraphicsItem(AluGraphicsItem(self.get_module("ALU")))
-        gui.addModuleGraphicsItem(GrxGraphicsItem(self.get_module("GRx")))
-        gui.addModuleGraphicsItem(IrGraphicsItem(self.get_module("IR")))
+        gui.add_module_graphics_item(uPcGraphicsItem(self.get_module("uPC")))
+        gui.add_module_graphics_item(SupcGraphicsItem(self.get_module("SuPC")))
+        gui.add_module_graphics_item(PcGraphicsItem(self.get_module("PC")))
+        gui.add_module_graphics_item(ArGraphicsItem(self.get_module("AR")))
+        gui.add_module_graphics_item(AsrGraphicsItem(self.get_module("ASR")))
+        gui.add_module_graphics_item(HrGraphicsItem(self.get_module("HR")))
+        gui.add_module_graphics_item(MicroMemoryGraphicsItem(self.get_module("uM")))
+        gui.add_module_graphics_item(AluGraphicsItem(self.get_module("ALU")))
+        gui.add_module_graphics_item(GrxGraphicsItem(self.get_module("GRx")))
+        gui.add_module_graphics_item(IrGraphicsItem(self.get_module("IR")))
 
         flag_modules = ["Z-Flag", "N-Flag", "C-Flag", "O-Flag", "L-Flag", "LC"]
         for name in flag_modules:
             module = self.get_module(name)
             widget = FlagGraphicsItem(module)
-            gui.addModuleGraphicsItem(widget)
+            gui.add_module_graphics_item(widget)
 
         memory_modules = ["PM", "K1", "K2"]
         for name in memory_modules:
             module = self.get_module(name)
             widget = MiaMemoryGraphicsItem(module)
-            gui.addModuleGraphicsItem(widget)
+            gui.add_module_graphics_item(widget)
 
-        gui.addAllSignals()
+        gui.add_all_signals()
 
         gui.show()
-        gui.loadLayoutFromFile("mia_layout")
+        gui.load_layout("mia_layout")
         app.exec()
 
     def launch_cli(self):
diff --git a/src/simudator/processor/mia/modules/alu.py b/src/simudator/processor/mia/modules/alu.py
index 482da580be76ab06e8f941cbf00ef8febd577e5c..30c27eea53e9e1399f980f2f326924e6684bb18c 100644
--- a/src/simudator/processor/mia/modules/alu.py
+++ b/src/simudator/processor/mia/modules/alu.py
@@ -1,70 +1,104 @@
 from simudator.core.module import Module
 from simudator.core.signal import Signal
+from simudator.processor.mia.modules.alu_arithmetics import (
+    add_binary_numbers,
+    arithmetic_right_shift,
+    bin_to_int,
+    binary_and,
+    binary_or,
+    bitwise_not,
+    int_to_bin,
+    left_rotate,
+    logical_left_shift,
+    logical_right_shift,
+)
 
 
 class ALU(Module):
     """
-    Module used for arithmetic logic calculations.
-
-    Takes two input signals that are used to recive values
-    for calculations.
-    Takes one control signal to control what operand should
-    be executed.
-    Takes four flags too give extra infromation from calulations.
-    Will do all calulations as two's complement binary numbers.
+    Module used for arithmetic logic calculations in MIA.
+
+    Parameters
+    ----------
+    input_ar : Signal
+        Input from the AR register, used as the first operand in calculations.
+    input_bus : Signal
+        Input from the bus, used as the second operand in calculations.
+    input_hr : Signal
+        Input from the HR register, used in 32-bit calculations.
+    control : Signal
+        Control signal for choosing ALU operation.
+    output_ar : Signal
+        Output signal to the AR register onto which the operation result is
+        written.
+    output_hr : Signal
+        Output signal to the HR register onto which the lower 16-bits of the
+        result of a 32-bit calculation are written.
+    z_flag : Signal
+        Output for setting the z-flag after a calculation.
+    n_flag : Signal
+        Output for setting the n-flag after a calculation.
+    o_flag : Signal
+        Output for setting the o-flag after a calculation.
+    c_flag : Signal
+        Output for setting the c-flag after a calculation.
+    name : Signal
+        Name of the ALU.
     """
 
-    __slots__ = ("z_value", "n_value", "o_value", "c_value", "hr_value")
+    __slots__ = ("_z_value", "_n_value", "_o_value", "_c_value", "_hr_value")
 
     WORD_LENGTH = 16
 
     def __init__(
         self,
+        input_ar: Signal,
+        input_bus: Signal,
+        input_hr: Signal,
+        control: Signal,
+        output_ar: Signal,
+        output_hr: Signal,
+        z_flag: Signal,
+        n_flag: Signal,
+        o_flag: Signal,
+        c_flag: Signal,
         name: str,
-        input_signal_a: Signal,
-        input_signal_b: Signal,
-        input_signal_hr: Signal,
-        control_signal: Signal,
-        output_signal: Signal,
-        output_signal_hr: Signal,
-        z_signal: Signal,
-        n_signal: Signal,
-        o_signal: Signal,
-        c_signal: Signal,
     ):
 
         signals = {
-            "in_input_a": input_signal_a,
-            "in_input_b": input_signal_b,
-            "in_control": control_signal,
-            "in_hr": input_signal_hr,
-            "out_result": output_signal,
-            "out_hr": output_signal_hr,
-            "out_flag_z": z_signal,
-            "out_flag_n": n_signal,
-            "out_flag_o": o_signal,
-            "out_flag_c": c_signal,
+            "in_input_ar": input_ar,
+            "in_input_bus": input_bus,
+            "in_control": control,
+            "in_hr": input_hr,
+            "out_result": output_ar,
+            "out_hr": output_hr,
+            "out_flag_z": z_flag,
+            "out_flag_n": n_flag,
+            "out_flag_o": o_flag,
+            "out_flag_c": c_flag,
         }
         super().__init__(signals, name)
 
         # flag values
-        self.z_value = 0
-        self.n_value = 0
-        self.o_value = 0
-        self.c_value = 0
+        self._z_value = 0
+        self._n_value = 0
+        self._o_value = 0
+        self._c_value = 0
 
         # internal values
-        self.hr_value = None
+        self._hr_value = None
 
     def update_register(self) -> None:
-        """
-        The alu has no register.
+        """Do nothing.
+
+        The alu has no internal register.
         """
         pass
 
     def output_register(self) -> None:
-        """
-        The alu has no register.
+        """Do nothing.
+
+        The alu has no internal register.
         """
         pass
 
@@ -77,103 +111,131 @@ class ALU(Module):
 
     def update_logic(self) -> None:
         """
-        Will calculate and operation depending on given contorl signal
+        Will calculate and operation depending on given control signal
         and update outputs with result.
         """
 
-        value_a = self.signals["in_input_a"].get_value()
-        value_b = self.signals["in_input_b"].get_value()
+        value_ar = self.signals["in_input_ar"].get_value()
+        value_bus = self.signals["in_input_bus"].get_value()
         value_hr = self.signals["in_hr"].get_value()
-
-        # bitmask control value to get first 4 bits
         control_value = self.signals["in_control"].get_value()
 
-        # If control value is none do nothing
-        if control_value is None:
-            return
-
         # Mask control value
         control_value = control_value & 0b1111
 
         # Define output value
-        output_value = None
+        bin_ar = int_to_bin(value_ar, self.WORD_LENGTH)
+        bin_bus = int_to_bin(value_bus, self.WORD_LENGTH)
+        update_z_n_flags = True
+        update_o_flag = False
+        bin_res = None
+        carry = self._c_value
 
         # Check control value for action
         match control_value:
             case 0b0000:
                 # Do nothing
+                update_z_n_flags = False
                 return
 
             case 0b0001:
                 # set output to only b
-                output_value = value_b
+                update_z_n_flags = False
+                bin_res = bin_bus
 
             case 0b0010:
                 # calculate bitwise not of b
-                output_value = self.ones_complement(value_b)
+                update_z_n_flags = False
+                bin_res = bitwise_not(bin_bus)
 
             case 0b0011:
                 # returns 0
-                output_value = 0
+                bin_res = int_to_bin(0, self.WORD_LENGTH)
 
             case 0b0100:
                 # return sum of a and b
-                output_value = self.add_numbers(value_a, value_b, True)
+                update_o_flag = True
+                bin_res, carry = add_binary_numbers(bin_ar, bin_bus)
 
             case 0b0101:
-                # returns the differense of a and b
-                output_value = self.subtract_numbers(value_a, value_b, True)
+                # returns the difference of a and b
+                update_o_flag = True
+                bin_bus = bitwise_not(bin_bus)
+                bin_res, carry = add_binary_numbers(bin_ar, bin_bus, 1)
 
             case 0b0110:
                 # returns the bitwise AND of a and b
-                output_value = self.and_numbers(value_a, value_b)
+                bin_res = binary_and(bin_ar, bin_bus)
 
             case 0b0111:
                 # returns the bitwise OR of a and b
-                output_value = self.or_numbers(value_a, value_b)
+                bin_res = binary_or(bin_ar, bin_bus)
 
             case 0b1000:
                 # return sum of a and b without changing flags
-                output_value = self.add_numbers(value_a, value_b, False)
+                update_z_n_flags = False
+                update_o_flag = False
+                bin_res, _ = add_binary_numbers(bin_ar, bin_bus)
 
             case 0b1001:
-                # returns the leftshifted a
-                output_value = self.logical_left_shift(value_a)
+                # returns the leftshifted AR
+                bin_res, carry = logical_left_shift(bin_ar)
 
             case 0b1010:
-                # returns the leftshifted a (with HR)
-                output_value = self.logical_left_shift(value_a, value_hr)
+                # returns the leftshifted ARHR
+                long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH)
+                bin_res, carry = logical_left_shift(long_word)
+                self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :])
 
             case 0b1011:
-                # returns the arithmetic righshifted a
-                output_value = self.arithmetic_right_shift(value_a)
+                # returns the arithmetic righshifted AR
+                bin_res, carry = arithmetic_right_shift(bin_ar)
 
             case 0b1100:
-                # returns the arithmetic righshifted a (with HR)
-                output_value = self.arithmetic_right_shift(value_a, value_hr)
+                # returns the arithmetic righshifted ARHR
+                long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH)
+                bin_res, carry = arithmetic_right_shift(long_word)
+                self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :])
 
             case 0b1101:
-                # returns the logically righshifted a
-                output_value = self.logical_right_shift(value_a)
+                # returns the logically righshifted AR
+                bin_res, carry = logical_right_shift(bin_ar)
 
             case 0b1110:
-                # returns a one step rotated left
-                output_value = self.left_rotate(value_a)
+                # returns AR one step rotated left
+                bin_res, carry = left_rotate(bin_ar)
 
             case 0b1111:
-                # returns a one step rotated left (with HR)
-                output_value = self.left_rotate(value_a, value_hr)
+                # returns ARHR one step rotated left
+                long_word = bin_ar + int_to_bin(value_hr, self.WORD_LENGTH)
+                bin_res, carry = left_rotate(long_word)
+                self._hr_value = bin_to_int(bin_res[self.WORD_LENGTH :])
 
             case _:
                 return
 
         # Update output signal and flags
-        self.signals["out_result"].update_value(output_value)
-        self.signals["out_flag_z"].update_value(self.z_value)
-        self.signals["out_flag_n"].update_value(self.n_value)
-        self.signals["out_flag_o"].update_value(self.o_value)
-        self.signals["out_flag_c"].update_value(self.c_value)
-        self.signals["out_hr"].update_value(self.hr_value)
+        self._update_flags(
+            bin_ar,
+            bin_bus,
+            bin_res,
+            carry,
+            update_z_n_flags,
+            update_o_flag,
+        )
+
+        if len(bin_res) == 2 * self.WORD_LENGTH:
+            # In case of a 32-bit calculation, extract the upper 16-bits
+            # that should go into the AR register
+            # (Not done earlier to properly update the flags as a 32-bit number)
+            bin_res = bin_res[: self.WORD_LENGTH]
+        res = bin_to_int(bin_res)
+        self.signals["out_result"].update_value(res)
+        self.signals["out_flag_z"].update_value(self._z_value)
+        self.signals["out_flag_n"].update_value(self._n_value)
+        self.signals["out_flag_o"].update_value(self._o_value)
+        self.signals["out_flag_c"].update_value(self._c_value)
+        self.signals["out_hr"].update_value(self._hr_value)
 
     def print_module(self) -> None:
         print(
@@ -181,496 +243,71 @@ class ALU(Module):
             self.name,
             "\n -----",
             "\n a_value: ",
-            self.signals["in_input_a"].get_value(),
+            self.signals["in_input_ar"].get_value(),
             "\n b_value: ",
-            self.signals["in_input_b"].get_value(),
+            self.signals["in_input_bus"].get_value(),
             "\n control_signal: ",
             self.signals["in_control"].get_value(),
         )
 
     def reset(self) -> None:
-        self.z_value = 0
-        self.n_value = 0
-        self.o_value = 0
-        self.c_value = 0
-        self.hr_value = None
-
-    # ----- ALU operations -----
-
-    def ones_complement(self, number: int) -> int:
-        """
-        Returns the ones complement of an int.
-        """
-        binary_number = self.int_to_bin(number)
-        binary_complement = self.bitwise_not(binary_number)
-        result = self.bin_to_int(binary_complement)
-        return result
-
-    def add_numbers(self, number_a: int, number_b: int, change_flags: bool) -> int:
-        """
-        Sum two numbers as binary numbers.
-
-        Will update flags if asked to
-        """
-
-        # Convert to binary
-        binary_a = self.int_to_bin(number_a)
-        binary_b = self.int_to_bin(number_b)
-
-        # Add the numbers
-        result_tuple = self.add_binary_numbers(binary_a, binary_b)
-
-        # Change flags
-        if change_flags:
-            self.update_flags(binary_a, binary_b, result_tuple[0], result_tuple[1])
-
-        # Convert back to int
-        result = self.bin_to_int(result_tuple[0])
-        return result
-
-    def subtract_numbers(self, number_a: int, number_b: int, change_flags: bool) -> int:
-        """
-        Calculates difference between to two numbers as binary numbers.
-
-        Will update flags if asked to.
-        """
-
-        # Convert to binary
-        binary_a = self.int_to_bin(number_a)
-        # Convert b to a negative number
-        binary_b = self.int_to_negative_bin(number_b)
-
-        # Add the numbers
-        result_tuple = self.add_binary_numbers(binary_a, binary_b)
-
-        # Change flags
-        if change_flags:
-            self.update_flags(binary_a, binary_b, result_tuple[0], result_tuple[1])
-
-        # Convert back to int
-        result = self.bin_to_int(result_tuple[0])
-        return result
-
-    def and_numbers(self, number_a: int, number_b: int) -> int:
-        """
-        Bitwise and two int's and return the result.
-
-        Will update flags.
-        """
-
-        # Convert to binary
-        binary_a = self.int_to_bin(number_a)
-        binary_b = self.int_to_bin(number_b)
-
-        # Calulate and
-        binary_and = self.binary_and(binary_a, binary_b)
-
-        # Update flags, send data only to update z and n flags
-        self.update_flags(None, None, binary_and, None)
-
-        # Convert to int
-        result = self.bin_to_int(binary_and)
-
-        return result
-
-    def or_numbers(self, number_a: int, number_b: int) -> int:
-        """
-        Bitwise or two int's and return the result.
-
-        Will update flags.
-        """
-        # Convert to binary
-        binary_a = self.int_to_bin(number_a)
-        binary_b = self.int_to_bin(number_b)
-
-        # Calulate and
-        binary_or = self.binary_or(binary_a, binary_b)
-
-        # Update flags, send data only to update z and n flags
-        self.update_flags(None, None, binary_or, None)
-
-        # Convert to int
-        result = self.bin_to_int(binary_or)
-
-        return result
-
-    def logical_left_shift(self, number: int, hr_value: int = None) -> int:
-        """
-        Left shifts given number as a binary with word.
-
-        Fills right with 0's after shift.
-        Will update carry with removed bit.
-        """
-
-        # Convert to binary representation
-        binary_number = self.int_to_bin(number)
-
-        new_binary_number = None
-        carry = None
-
-        if hr_value is not None:
-            # get hr as a binary
-            hr_binary = self.int_to_bin(hr_value)
-
-            # do the shift
-            long_word_binary = hr_binary[1:] + binary_number + [0]
-
-            # get hr number
-            new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH])
-
-            # update hr
-            self.hr_value = new_hr_number
-
-            # get new value from binary
-            new_binary_number = long_word_binary[self.WORD_LENGTH :]
-
-            # Update carry with removed bit
-            carry = hr_binary[0]
-
-        else:
-            # Do shift
-            new_binary_number = binary_number[1:] + [0]
-
-            # Update carry with removed bit
-            carry = binary_number[0]
-
-        # Convert shifted to int
-        value = self.bin_to_int(new_binary_number)
-
-        self.update_flags(None, None, new_binary_number, carry)
-
-        return value
-
-    def logical_right_shift(self, number: int) -> int:
-        """
-        Left shifts given number as a binary.
-
-        Fills right with 0's after shift.
-        Will update carry with removed bit.
-        """
-        # Conert to binary
-        binary_number = self.int_to_bin(number)
-
-        # Do shift
-        new_binary_number = [0] + binary_number[:-1]
-
-        # Convert shifted to int
-        value = self.bin_to_int(new_binary_number)
-
-        # Update flags
-        self.update_flags(None, None, new_binary_number, binary_number[-1])
-
-        return value
-
-    def arithmetic_right_shift(self, number: int, hr_value: int = None) -> int:
-        """
-        Right shifts given number as a binary with word length WORD_LENGTH.
-
-        Fills right with most significant bit.
-        Will update carry with removed bit.
-        """
-        # Convert to binary representation
-        binary_number = self.int_to_bin(number)
-
-        new_binary_number = None
-        carry = None
-
-        if hr_value is not None:
-            # get hr as a binary
-            hr_binary = self.int_to_bin(hr_value)
-
-            # do shift
-            long_word_binary = [binary_number[-1]] + hr_binary + binary_number[:-1]
-
-            # get hr number
-            new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH])
-
-            # update hr
-            self.hr_value = new_hr_number
-
-            # get new value from binary
-            new_binary_number = long_word_binary[self.WORD_LENGTH :]
-
-            # Update carry with removed bit
-            carry = binary_number[-1]
+        self._z_value = 0
+        self._n_value = 0
+        self._o_value = 0
+        self._c_value = 0
+        self._hr_value = None
 
-        else:
-            # Do shift
-            new_binary_number = [binary_number[0]] + binary_number[:-1]
-
-            # Update carry with removed bit
-            carry = binary_number[-1]
-
-        # Convert shifted to int
-        value = self.bin_to_int(new_binary_number)
-
-        # Update flags
-        self.update_flags(None, None, new_binary_number, carry)
-
-        return value
-
-    def left_rotate(self, number: int, hr_value: int = None) -> int:
-        """
-        Rotates given number left as a binary with word length WORD_LENGTH.
-
-        Fills right with 0's after shift.
-        """
-
-        # Convert to binary representation
-        binary_number = self.int_to_bin(number)
-
-        new_binary_number = None
-        carry = None
-
-        if hr_value is not None:
-            # get hr as a binary
-            hr_binary = self.int_to_bin(self.signals["in_hr"].get_value())
-
-            long_word_binary = hr_binary[1:] + binary_number + [hr_binary[0]]
-
-            # get hr number
-            new_hr_number = self.bin_to_int(long_word_binary[: self.WORD_LENGTH])
-
-            # update hr
-            self.hr_value = new_hr_number
-
-            # get new value from binary
-            new_binary_number = long_word_binary[self.WORD_LENGTH :]
-
-            # Update carry with removed bit
-            carry = hr_binary[0]
-
-        else:
-            # Do rotation
-            new_binary_number = binary_number[1:] + [binary_number[0]]
-
-            # Update carry with removed bit
-            carry = binary_number[0]
-
-        # Convert shifted to int
-        value = self.bin_to_int(new_binary_number)
-
-        self.update_flags(None, None, new_binary_number, carry)
-
-        return value
-
-    # ----- Converters -----
-
-    def bin_to_int(self, binary_number: [int], word_length: int = None) -> int:
-        """
-        Converts from binary list representation to int.
-
-        """
-
-        # Set default word length if none is specified
-        if word_length is None:
-            word_length = self.WORD_LENGTH
-
-        number = 0
-
-        # Get values for bits and add them
-        for bit_index in range(0, word_length):
-            number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1)
-
-        return number
-
-    def int_to_bin(self, number: int, word_length: int = None) -> [int]:
-        """
-        Converts an int to a list binary representation where
-        each index in the list represents one bit of the word.
-
-        This is done to more easily handle edge cases that occur
-        when doing arithmetic with fixed word lenghts.
-        This version can not handel negative numbers.
-        """
-
-        # Set default word length if none is specified
-        if word_length is None:
-            word_length = self.WORD_LENGTH
-
-        # Get list representation with correct length
-        binary_number = [0 for i in range(word_length)]
-
-        # We can now look at the other bits
-        for bit_index in range(0, word_length):
-            # If number is larger then bit we subtract the
-            # bits decimal value from the number and make the bit 1.
-            if number >= 2 ** (word_length - bit_index - 1):
-                number -= 2 ** (word_length - bit_index - 1)
-                binary_number[bit_index] = 1
-
-        return binary_number
-
-    def int_to_negative_bin(self, number: int) -> [int]:
-        """
-        Returns the negative of a given int as a binary list.
-        """
-
-        binary_number = self.int_to_bin(number)
-        not_binary_number = self.bitwise_not(binary_number)
-        binary_one = [0 for i in range(15)] + [1]
-        result_tuple = self.add_binary_numbers(not_binary_number, binary_one)
-        negativt_binary = result_tuple[0]
-        return negativt_binary
-
-    def int_to_bin_twocomp(self, number: int) -> [int]:
-        """
-        Converts an int to a list binary representation where
-        each index in the list represents one bit of the word.
-
-        This is done to more easily handle edge cases that occur
-        when doing arithmetic with fixed word lenghts.
-        """
-
-        # Get list representation with correct length
-        binary_number = [0 for i in range(self.WORD_LENGTH)]
-
-        # Using twos complement representation
-        # so handel negativ numbers accordingly
-        if number < 0:
-            binary_number[0] = 1
-
-            # We add this so the rest of the caclulation is
-            # the same for nagativ numbers
-            number += 2 ** (self.WORD_LENGTH - 1)
-
-        # We can now look at the other bits
-        for bit_index in range(1, self.WORD_LENGTH):
-            # If number is larger then bit we subtract the
-            # bits decimal value from the number and make the bit 1.
-            if number >= 2 ** (self.WORD_LENGTH - bit_index - 1):
-                number -= 2 ** (self.WORD_LENGTH - bit_index - 1)
-                binary_number[bit_index] = 1
-
-        return binary_number
-
-    def bin_to_int_twocomp(self, binary_number: [int]) -> int:
-        """
-        Converts from two's complement binary list representation to int.
-        """
-        number = 0
-
-        # two's complement representation so most significant
-        # bit will be negativ
-        if binary_number[0] == 1:
-            number = -(2 ** (self.WORD_LENGTH - 1))
-
-        # Get values for other bits
-        for bit_index in range(1, self.WORD_LENGTH):
-            number += binary_number[bit_index] * 2 ** (self.WORD_LENGTH - bit_index - 1)
-
-        return number
-
-    # ----- Internal operations -----
-
-    def update_flags(
+    def _update_flags(
         self,
-        binary_a: list[int],
-        binary_b: list[int],
-        binary_result: list[int],
+        a: list[int],
+        b: list[int],
+        res: list[int],
         carry: int,
+        z_n: bool = True,
+        o: bool = False,
     ) -> None:
-        """
-        Will update the flags depending on inputed values and the result.
-        This function can take none for any input and will update the
-        as well as possible for all flags.
-        """
-
-        if binary_result is not None:
-            # Update z flag if we have result
-            # For this we check if result is zero
-            if self.bin_to_int(binary_result, len(binary_result)) == 0:
-                self.z_value = 1
+        """Update the outputted values to the flags of the ALU depending on
+        the operands and resulting from the ALU operation.
+
+        It is possible to not update specific flags. For convenience, the
+        c-flag is always updated with the supplied carry value.
+
+        Parameters
+        ----------
+        a : list[int]
+            First ALU operand.
+        b : list[int]
+            Second ALU operand.
+        res : list[int]
+            Result of the ALU operation.
+        carry : int
+            Carry bit from the ALU operation.
+        z_n : bool
+            ``True`` if the z- and n-flags should be updated, ``False`` if not.
+        o : bool
+            ``True`` if the o-flag should be updated, ``False`` if not
+        """
+
+        if z_n:
+            # Set the z flag if the result is 0
+            if bin_to_int(res) == 0:
+                self._z_value = 1
             else:
-                self.z_value = 0
+                self._z_value = 0
 
-            # Update n flag if we have result
-            # For this we check if the value is negativ in twos complement
-            if binary_result[0] == 1:
-                self.n_value = 1
+            # Set the n flag if the most significant bit is 1,
+            # i.e. the number is negative if interpreted as two's complement
+            if res[0] == 1:
+                self._n_value = 1
             else:
-                self.n_value = 0
+                self._n_value = 0
 
-            # If we also have given values we can calculate o flag
-            if binary_a is not None and binary_b is not None:
-                # Check if last bit in result is diffrent from both iven values
-                if (binary_a[0] == binary_b[0]) and (binary_a[0] != binary_result[0]):
-                    self.o_value = 1
-                else:
-                    self.o_value = 0
-
-        # Set carry if we have it
-        if carry is not None:
-            self.c_value = carry
-
-    def bitwise_not(self, binary_number: [int]) -> [int]:
-        """
-        Returns bitwise not of given binary number
-        """
-
-        # Make empty binary number for result
-        new_binary_number = [0 for _ in range(self.WORD_LENGTH)]
-
-        # Change each bits value
-        for bit_index in range(self.WORD_LENGTH):
-            if binary_number[bit_index] == 0:
-                new_binary_number[bit_index] = 1
-            elif binary_number[bit_index] == 1:
-                new_binary_number[bit_index] = 0
-
-        return new_binary_number
-
-    def add_binary_numbers(
-        self, binary_a: list[int], binary_b: list[int]
-    ) -> tuple[list[int], int]:
-        """
-        Returns a tuple with the sum of two binary numbers
-        and the carry bit from the calculations
-        """
-
-        # Make new binary number to store results
-        binary_sum = [0 for _ in range(self.WORD_LENGTH)]
-
-        # Need carry for calculation
-        carry = 0
-
-        # Do addition bit by bit and save carry between each loop
-        for bit_index in range(self.WORD_LENGTH - 1, -1, -1):
-            bit_sum = binary_a[bit_index] + binary_b[bit_index] + carry
-            match bit_sum:
-                case 0:
-                    binary_sum[bit_index] = 0
-                    carry = 0
-                case 1:
-                    binary_sum[bit_index] = 1
-                    carry = 0
-                case 2:
-                    binary_sum[bit_index] = 0
-                    carry = 1
-                case 3:
-                    binary_sum[bit_index] = 1
-                    carry = 1
-
-        return (binary_sum, carry)
-
-    def binary_and(self, binary_a: list[int], binary_b: list[int]) -> list[int]:
-        """
-        Calculate and between binary two numbers
-        """
-        binary_and = [0 for _ in range(self.WORD_LENGTH)]
-
-        for binary_index in range(self.WORD_LENGTH):
-            binary_and[binary_index] = binary_a[binary_index] & binary_b[binary_index]
-
-        return binary_and
-
-    def binary_or(self, binary_a: list[int], binary_b: list[int]) -> list[int]:
-        """
-        Calculate or between two binary numbers
-        """
-        binary_or = [0 for _ in range(self.WORD_LENGTH)]
-
-        for binary_index in range(self.WORD_LENGTH):
-            binary_or[binary_index] = binary_a[binary_index] | binary_b[binary_index]
+        if o:
+            # Set the o flag if the MSB is the same for both a and b and this
+            # bit is different to the MSB of the result
+            if (a[0] == b[0]) and (a[0] != res[0]):
+                self._o_value = 1
+            else:
+                self._o_value = 0
 
-        return binary_or
+        self._c_value = carry
diff --git a/src/simudator/processor/mia/modules/alu_arithmetics.py b/src/simudator/processor/mia/modules/alu_arithmetics.py
new file mode 100644
index 0000000000000000000000000000000000000000..f0f00d39c62cfe63f87c9c756d4a30f5061578ec
--- /dev/null
+++ b/src/simudator/processor/mia/modules/alu_arithmetics.py
@@ -0,0 +1,293 @@
+def bin_to_int(binary_number: list[int]) -> int:
+    """
+    Convert from binary bit list representation to integer.
+
+    Parameters
+    ----------
+    binary_number : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    int
+        The binary number converted to an integer.
+    """
+    number = 0
+    word_length = len(binary_number)
+
+    # Get values for bits and add them
+    for bit_index in range(0, word_length):
+        number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1)
+
+    return number
+
+
+def int_to_bin(number: int, word_length: int) -> list[int]:
+    """Convert a non-negative integer to a binary number represented as a list
+    of bits.
+
+    Parameters
+    ----------
+    number : int
+        Non-negative integer to convert to binary.
+    word_length : int
+        Number of bits to represent the binary number.
+
+    Returns
+    -------
+    list[int]
+        List of bits representing the number converted to binary.
+    """
+    # Get list representation with correct length
+    binary_number = [0 for _ in range(word_length)]
+
+    # We can now look at the other bits
+    for bit_index in range(0, word_length):
+        # If number is larger then bit we subtract the
+        # bits decimal value from the number and make the bit 1.
+        if number >= 2 ** (word_length - bit_index - 1):
+            number -= 2 ** (word_length - bit_index - 1)
+            binary_number[bit_index] = 1
+
+    return binary_number
+
+
+def bin_to_int_twocomp(binary_number: list[int]) -> int:
+    """Convert from two's complement binary bit list representation to integer.
+
+    Parameters
+    ----------
+    binary_number : list[int]
+        Binary number in two's complement, represented as a bit list.
+
+    Returns
+    -------
+    int
+        The binary number converted to an integer.
+    """
+    number = 0
+    word_length = len(binary_number)
+
+    # two's complement representation so most significant
+    # bit will be negative
+    if binary_number[0] == 1:
+        number = -(2 ** (word_length - 1))
+
+    # Get values for other bits
+    for bit_index in range(1, word_length):
+        number += binary_number[bit_index] * 2 ** (word_length - bit_index - 1)
+
+    return number
+
+
+def bitwise_not(binary_number: list[int]) -> list[int]:
+    """Perform bitwise not on a binary number.
+
+    Parameters
+    ----------
+    binary_number : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    list[int]
+        Resulting bit-flipped binary number as a list of bits.
+    """
+
+    # Make empty binary number for result
+    new_binary_number = [0 for _ in range(len(binary_number))]
+
+    # Change each bits value
+    for bit_index in range(len(binary_number)):
+        if binary_number[bit_index] == 0:
+            new_binary_number[bit_index] = 1
+        elif binary_number[bit_index] == 1:
+            new_binary_number[bit_index] = 0
+
+    return new_binary_number
+
+
+def add_binary_numbers(
+    a: list[int], b: list[int], carry: int = 0
+) -> tuple[list[int], int]:
+    """Add two binary numbers together.
+
+    Assumes both numbers are represented with the same number of bits,
+    i.e. have the same word length.
+
+    Parameters
+    ----------
+    a : list[int]
+        First operand represented as a list of bits.
+    b : list[int]
+        Second operand represented as a list of bits.
+    carry : int
+        Carry in bit for the least significant bit. Useful for subtraction.
+
+    Returns
+    -------
+    sum : list[int]
+        Resulting binary sum represented as a list of bits.
+    carry : int
+        Carry bit from performing the summation.
+    """
+
+    # Make new binary number to store results
+    word_length = len(a)
+    binary_sum = [0 for _ in range(word_length)]
+
+    # Need carry for calculation
+    # carry = 0
+
+    # Do addition bit by bit and save carry between each loop
+    for bit_index in range(word_length - 1, -1, -1):
+        bit_sum = a[bit_index] + b[bit_index] + carry
+        match bit_sum:
+            case 0:
+                binary_sum[bit_index] = 0
+                carry = 0
+            case 1:
+                binary_sum[bit_index] = 1
+                carry = 0
+            case 2:
+                binary_sum[bit_index] = 0
+                carry = 1
+            case 3:
+                binary_sum[bit_index] = 1
+                carry = 1
+
+    return (binary_sum, carry)
+
+
+def binary_and(a: list[int], b: list[int]) -> list[int]:
+    """
+    Calculate bitwise and between two binary numbers.
+
+    Assumes both numbers are represented with the same number of bits,
+    i.e. have the same word length.
+
+    Parameters
+    ----------
+    a : list[int]
+        First operand represented as a list of bits.
+    b : list[int]
+        Second operand represented as a list of bits.
+
+    Returns
+    -------
+        Resulting binary number as a list of bits.
+    """
+    word_length = len(a)
+    binary_and = [0 for _ in range(word_length)]
+
+    for binary_index in range(word_length):
+        binary_and[binary_index] = a[binary_index] & b[binary_index]
+
+    return binary_and
+
+
+def binary_or(a: list[int], b: list[int]) -> list[int]:
+    """Calculate bitwise or between two binary numbers
+
+    Assumes both numbers are represented with the same number of bits,
+    i.e. have the same word length.
+
+    Parameters
+    ----------
+    a : list[int]
+        First operand represented as a list of bits.
+    b : list[int]
+        Second operand represented as a list of bits.
+
+    Returns
+    -------
+        Resulting binary number as a list of bits.
+    """
+    word_length = len(a)
+    binary_or = [0 for _ in range(word_length)]
+
+    for binary_index in range(word_length):
+        binary_or[binary_index] = a[binary_index] | b[binary_index]
+
+    return binary_or
+
+
+def logical_left_shift(bin_num: list[int]) -> tuple[list[int], int]:
+    """Logically left shift a binary number.
+
+    Parameters
+    ----------
+    bin_num : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    shifted_bin_num : list[int]
+        Resulting shifted binary number as a list of bits.
+    carry : int
+        Carry bit from the left shift, i.e. the bit that is shifted out.
+    """
+    new_bin_num = bin_num[1:] + [0]
+    carry = bin_num[0]
+
+    return new_bin_num, carry
+
+
+def logical_right_shift(bin_num: list[int]) -> tuple[list[int], int]:
+    """Logically right shift a binary number.
+
+    Parameters
+    ----------
+    bin_num : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    shifted_bin_num : list[int]
+        Resulting shifted binary number as a list of bits.
+    carry : int
+        Carry bit from the right shift, i.e. the bit that is shifted out.
+    """
+    new_bin_num = [0] + bin_num[:-1]
+    carry = bin_num[-1]
+    return new_bin_num, carry
+
+
+def arithmetic_right_shift(bin_num: list[int]) -> tuple[list[int], int]:
+    """Arithmetically right shift a binary number.
+
+    Parameters
+    ----------
+    bin_num : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    shifted_bin_num : list[int]
+        Resulting shifted binary number as a list of bits.
+    carry : int
+        Carry bit from the right shift, i.e. the bit that is shifted out.
+    """
+    new_bin_num = [bin_num[0]] + bin_num[:-1]
+    carry = bin_num[-1]
+    return new_bin_num, carry
+
+
+def left_rotate(bin_num: list[int]) -> tuple[list[int], int]:
+    """Rotate the bits of a number to the left.
+
+    Parameters
+    ----------
+    bin_num : list[int]
+        Binary number represented as a list of bits.
+
+    Returns
+    -------
+    rotated_bin_num : list[int]
+        Resulting rotated binary number as a list of bits.
+    carry : int
+        Carry bit from the rotation, i.e. the bit that is shifted "out".
+    """
+    new_bin_num = bin_num[1:] + [bin_num[0]]
+    carry = bin_num[0]
+    return new_bin_num, carry
diff --git a/test/test_core/test_breakpoint.py b/test/test_core/test_breakpoint.py
index 01f115d012ba1a4cf042244d71761be28dd28673..37992bfb0010ebe54ef7c69cbab4f2bcf062905e 100644
--- a/test/test_core/test_breakpoint.py
+++ b/test/test_core/test_breakpoint.py
@@ -5,6 +5,7 @@ from simudator.core.module import Module
 from simudator.core.modules.memory import Memory
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
+from simudator.processor.mia.modules.lc import LC
 
 
 class DummyModule(Module):
@@ -184,3 +185,120 @@ def test_lambda_breakpoint_ref_args():
     dummy.value = value
     assert bp.is_break() == func_dummy_val(dummy, value)
     assert bp.is_break() == (not func_dummy_val(dummy, not value))
+
+
+def test_add_remove_memory_breakpoint():
+    """
+    Test that the cpu properly add/removes memory breakpoints.
+
+    Id counter starts at 1 and is only incremented.
+    """
+    cpu = Processor()
+    in_s = Signal(cpu)
+    out_s = Signal(cpu)
+    ctrl_s = Signal(cpu)
+    adr_s = Signal(cpu)
+    mem = Memory(in_s, out_s, ctrl_s, adr_s, 8)
+    cpu.add_module(mem)
+
+    assert len(cpu._breakpoints) == 0
+    assert cpu._breakpoint_id_counter == 1
+    cpu.add_memory_breakpoint("Memory", 1, 1)
+    assert cpu._breakpoint_id_counter == 2
+    assert len(cpu._breakpoints) == 1
+    assert cpu.remove_breakpoint(1) == True
+    assert cpu._breakpoints == {}
+    assert len(cpu._breakpoints) == 0
+
+
+def test_stop_on_memory_breakpoint():
+    """
+    Test that the cpu properly stops on a memory breakpoint.
+    """
+    cpu = Processor()
+    in_s = Signal(cpu)
+    out_s = Signal(cpu)
+    ctrl_s = Signal(cpu)
+    adr_s = Signal(cpu)
+    mem = Memory(in_s, out_s, ctrl_s, adr_s, 8)
+    cpu.add_module(mem)
+    assert mem._memory == [0 for _ in range(8)]
+    assert cpu._clock == 0
+
+    in_s.update_value(1)
+    ctrl_s.update_value(True)
+    adr_s.update_value(2)
+
+    # Add breakpoint if address 3 reaches value 1
+    cpu.add_memory_breakpoint("Memory", 2, 1)
+    cpu.set_enabled_breakpoint(1, True)
+    cpu.run_continuously()
+    assert mem._memory == [0, 0, 1, 0, 0, 0, 0, 0]
+    assert cpu.is_stopped == True
+    assert cpu._clock == 2
+
+    mem._memory = [0 for _ in range(8)]
+    in_s.update_value(1)
+    ctrl_s.update_value(True)
+    adr_s.update_value(2)
+    cpu.set_enabled_breakpoint(1, False)
+    cpu.do_tick()
+    assert cpu.is_stopped == False
+
+
+def test_add_remove_state_breakpoint():
+    """
+    Test that the cpu properly adds/removes state breakpoints.
+
+    Id counter starts at 1 and is only incremented.
+    """
+    cpu = Processor()
+    s = Signal(cpu)
+    lc = LC(s, s, s, s)
+    cpu.add_module(lc)
+
+    assert len(cpu._breakpoints) == 0
+    assert cpu._breakpoint_id_counter == 1
+    cpu.add_state_breakpoint("LC", "value", 1)
+    assert len(cpu._breakpoints) == 1
+    assert cpu._breakpoint_id_counter == 2
+    assert cpu.remove_breakpoint(1) == True
+    assert cpu._breakpoints == {}
+    assert len(cpu._breakpoints) == 0
+
+
+def test_stop_on_state_breakpoint():
+    """
+    Test that the cpu properly stops on a state breakpoint.
+    """
+    cpu = Processor()
+    mM_control = Signal(cpu)
+
+    # Sets the behaviour of the LC to decrement by one each tick
+    mM_control.set_value(1)
+
+    bus_input = Signal(cpu)
+    l_flag = Signal(cpu)
+    mM_uADR = Signal(cpu)
+    lc = LC(mM_control, bus_input, l_flag, mM_uADR)
+
+    lc._value = 10
+    cpu.add_module(lc)
+
+    cpu.add_state_breakpoint("LC", "value", 5)
+    cpu.run_continuously()
+    assert lc._value == 5
+    assert cpu.is_stopped == True
+
+    cpu.add_state_breakpoint("LC", "value", 2)
+    cpu.run_continuously()
+    assert lc._value == 2
+    assert cpu.is_stopped == True
+
+    # 'Reset' loop counter and turn off previous breakpoint
+    lc._value = 10
+    cpu.set_enabled_breakpoint(1, False)
+    cpu.set_enabled_breakpoint(2, False)
+    for _ in range(10):
+        cpu.do_tick()
+        assert cpu.is_stopped == False
diff --git a/test/test_core/test_reset.py b/test/test_core/test_reset.py
new file mode 100644
index 0000000000000000000000000000000000000000..bec0a7312bed9912a65b0bf9dabd3cf1cb5990d3
--- /dev/null
+++ b/test/test_core/test_reset.py
@@ -0,0 +1,56 @@
+from simudator.core.module import Module
+from simudator.core.modules.memory import Memory
+from simudator.core.processor import Processor
+from simudator.core.signal import Signal
+from simudator.processor.mia.modules.lc import LC
+
+
+class DummyModule(Module):
+    def __init__(self):
+        self.name = "name"
+        self.a = 4
+        self.b = "a"
+        self.c = [1, 2, 3]
+
+    def reset(self):
+        self.name = "dummy"
+        self.a = 0
+        self.b = ""
+        self.c = []
+
+    def get_state(self):
+        return {
+            "name": self.name,
+            "a": self.a,
+            "b": self.b,
+            "c": self.c,
+        }
+
+    def output_register(self):
+        pass
+
+    def update_logic(self):
+        pass
+
+
+def test_reset():
+    cpu = Processor()
+    cpu._signal_history = ["this", "should", "be", "removed"]
+    dummy = DummyModule()
+    cpu._clock = 100
+    cpu._removed_cycles = 1337
+    cpu._module_history.append(dummy.get_state())
+    cpu._signal_history.clear()
+    cpu._removed_cycles = 0
+    cpu._assembly_cycles = [i for i in range(9)]
+    cpu.add_module(dummy)
+    cpu.reset()
+    assert dummy.name == "dummy"
+    assert dummy.a == 0
+    assert dummy.b == ""
+    assert dummy.c == []
+    assert cpu._clock == 0
+    assert cpu._removed_cycles == 0
+    assert cpu._module_history == []
+    assert cpu._signal_history == []
+    assert cpu._assembly_cycles == [0]
diff --git a/test/test_core/test_save_state.py b/test/test_core/test_save_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..16199e77a9b1e2795d0563d55ff97a0c0e046ef7
--- /dev/null
+++ b/test/test_core/test_save_state.py
@@ -0,0 +1,43 @@
+from unittest.mock import mock_open, patch
+
+from simudator.core.module import Module
+from simudator.core.modules.register import Register
+from simudator.core.signal import Signal
+from simudator.processor.simple.simple import SIMPLE_CPU
+
+
+def test_module_save_state():
+    module = Module({})
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = module.name
+    with patch("builtins.open", mock_file):
+        module._helper_save_state_to_file(dummy_file_path, content)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
+
+    module.name = "aaa"
+    content = module.name
+    with patch("builtins.open", mock_file):
+        module._helper_save_state_to_file(dummy_file_path, content)
+        mock_file().write.assert_called_with(content)
+
+
+def test_default_register_save_state():
+    in_sigal = Signal(None)
+    register = Register(in_sigal, None)
+    register.name = "a"
+    register._value = "a"
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = register.name + ":\nvalue:: " + str(register._value) + "\n\n"
+    with patch("builtins.open", mock_file):
+        register.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_alu.py b/test/test_mia/test_alu.py
index 09ebf6405f7bf7f32dcd05a932a828b9b3f8289f..c87caf98f60e602ee9905bc5b1451d72d2f2da6e 100644
--- a/test/test_mia/test_alu.py
+++ b/test/test_mia/test_alu.py
@@ -9,12 +9,11 @@ input_signal_hr = Signal(cpu)
 control_signal = Signal(cpu)
 output_signal = Signal(cpu)
 hr_signal = Signal(cpu)
-z_signal = Signal(cpu)
-n_signal = Signal(cpu)
-o_signal = Signal(cpu)
-c_signal = Signal(cpu)
+z_signal = Signal(cpu, value=0)
+n_signal = Signal(cpu, value=0)
+o_signal = Signal(cpu, value=0)
+c_signal = Signal(cpu, value=0)
 alu = ALU(
-    "alu",
     input_signal_a,
     input_signal_b,
     input_signal_hr,
@@ -25,273 +24,413 @@ alu = ALU(
     n_signal,
     o_signal,
     c_signal,
+    "alu",
 )
 cpu.add_module(alu)
 
 
-def test_int_binary_converter():
-    # test int to binary
-    assert alu.int_to_bin(0) == [0 for i in range(alu.WORD_LENGTH)]
-    assert alu.int_to_bin(1) == [0] * (alu.WORD_LENGTH - 1) + [1]
-    assert alu.int_to_bin(32767) == [0] + [1] * (alu.WORD_LENGTH - 1)
-    assert alu.int_to_bin(9) == [0] * (alu.WORD_LENGTH - 4) + [1, 0, 0, 1]
-
-    # test binary to int
-    assert alu.bin_to_int(alu.int_to_bin(15)) == 15
-    assert alu.bin_to_int(alu.int_to_bin(0)) == 0
-    assert alu.bin_to_int(alu.int_to_bin(2**16)) == 2**16 - 1
-    assert alu.bin_to_int(alu.int_to_bin(3576)) == 3576
-    assert alu.bin_to_int([0 for i in range(alu.WORD_LENGTH)]) == 0
-    assert alu.bin_to_int([0] * (alu.WORD_LENGTH - 1) + [1]) == 1
-    assert alu.bin_to_int([1 for i in range(alu.WORD_LENGTH)]) == 2**16 - 1
-    assert alu.bin_to_int([0] + [1] * (alu.WORD_LENGTH - 1)) == 32767
-    assert alu.bin_to_int([0] * (alu.WORD_LENGTH - 4) + [1, 0, 0, 1]) == 9
-
-
-def test_ones_complement():
-    # test bitwise not
-    assert alu.ones_complement(0) == 2**16 - 1
-    assert alu.ones_complement(1) == 2**16 - 1 - 1
-    assert alu.ones_complement(2) == 2**16 - 1 - 2
-    assert alu.ones_complement(300) == 2**16 - 1 - 300
-
-
-def test_negative_bin():
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(0)) == 0
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(10)) == -10
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(1337)) == -1337
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**15 - 1)) == -(
-        2**15 - 1
+def create_test_case(op_ar, op_bus, op_code, res, z=None, n=None, o=None, c=None):
+    """
+    Create a test case for an ALU operation, optionally checking the flags set.
+
+    Parameters
+    ----------
+    op_ar : int
+        Input value to the ALU from the AR register, i.e. operand A.
+    op_bus : int
+        Input value to the ALU from the bus, i.e. operand B.
+    op_code : int
+        ALU operation code.
+    res: int
+        Result from the ALU operation.
+    z : int | None
+        Expected z-flag value after the operation. Set it to ``None`` if the
+        flag should not be affected by the operation.
+    n : int | None
+        Expected n-flag value after the operation. Set it to ``None`` if the
+        flag should not be affected by the operation.
+    o : int | None
+        Expected o-flag value after the operation. Set it to ``None`` if the
+        flag should not be affected by the operation.
+    c : int | None
+        Expected c-flag value after the operation. Set it to ``None`` if the
+        flag should not be affected by the operation.
+
+    Returns
+    -------
+    tuple
+        Test case for the ALU.
+    """
+    return op_ar, op_bus, op_code, res, z, n, o, c, None, None
+
+
+def create_hr_test_case(op_ar, op_hr, op_code, ar_res, hr_res, z, n, c):
+    """
+    Create a test case for a 32-bit ALU operation, i.e. one using the values
+    of the AR and HR registers together.
+
+    The 32-bit ALU operations always affect the z-, n- and c-flag while
+    not affecting the o-flag. As such, those 3 flags must be provided with
+    expected values.
+
+    Parameters
+    ----------
+    op_ar : int
+        Input value to the ALU from the AR register.
+    op_hr : int
+        Input value to the ALU from the HR register.
+    op_code : int
+        ALU operation code.
+    ar_res: int
+        Result to the AR register from the ALU operation.
+    hr_res: int
+        Result to the HR register from the ALU operation.
+    z : int
+        Expected z-flag value after the operation.
+    n : int
+        Expected n-flag value after the operation.
+    c : int
+        Expected c-flag value after the operation.
+
+    Returns
+    -------
+    tuple
+        Test case for the ALU.
+    """
+    return op_ar, 0, op_code, ar_res, z, n, None, c, op_hr, hr_res
+
+
+def check_test_case(test_case):
+    """
+    Assert that the ALU passes a test case for some operation.
+
+    Parameters
+    ----------
+    test_case : tuple
+        Test case to test the ALU against.
+    """
+    (op_1, op_2, op_code, res, z, n, o, c, hr_in, hr_out) = test_case
+    input_signal_a.update_value(op_1)
+    input_signal_b.update_value(op_2)
+    input_signal_hr.update_value(hr_in)
+    control_signal.update_value(op_code)
+
+    # Flags set to None in the test case should not be affected by the ALU
+    # instruction, so save their values before performing the operation
+    if z is None:
+        z = alu._z_value
+    if n is None:
+        n = alu._n_value
+    if o is None:
+        o = alu._o_value
+    if c is None:
+        c = alu._c_value
+
+    alu.update_logic()
+
+    assert output_signal.get_value() == res
+    assert hr_signal.get_value() == hr_out
+    assert z_signal.get_value() == z
+    assert n_signal.get_value() == n
+    assert o_signal.get_value() == o
+    assert c_signal.get_value() == c
+
+
+def check_test_cases(test_cases):
+    """
+    Assert that the ALU passes some test cases for some operations.
+
+    Note that the ALU is not reset between each test case, meaning that the
+    order of the test cases may matter depending on how they affect the flags.
+
+    Parameters
+    ----------
+    test_case : list[tuple]
+        List of test cases to test the ALU against.
+    """
+    for case in test_cases:
+        check_test_case(case)
+
+
+def test_nop():
+    op_code = 0b0000
+    # Set the flag values of the ALU and make sure that they are not affected
+    alu.reset()
+    alu._z_value = 1
+    alu._n_value = 1
+    alu._o_value = 1
+    alu._c_value = 1
+    z_signal.set_value(1)
+    n_signal.set_value(1)
+    o_signal.set_value(1)
+    c_signal.set_value(1)
+    test_case = create_test_case(10, 5, op_code, None)
+    check_test_case(test_case)
+
+
+def test_read_bus():
+    op_code = 0b0001
+    # Set the flag values of the ALU and make sure that they are not affected
+    alu.reset()
+    alu._z_value = 1
+    alu._n_value = 1
+    alu._o_value = 1
+    alu._c_value = 1
+    test_cases = (
+        create_test_case(0x0000, 0x0000, op_code, 0x0000),
+        create_test_case(0x0000, 0x0174, op_code, 0x0174),
+        create_test_case(0x0000, 0xFFFFF, op_code, 0xFFFF),  # Overflow
+        create_test_case(0x0AEF, 0x0360, op_code, 0x0360),  # Random AR value
+    )
+    check_test_cases(test_cases)
+
+
+def test_bitwise_not():
+    op_code = 0b0010
+    # Set the flag values of the ALU and make sure that they are not affected
+    alu.reset()
+    alu._z_value = 1
+    alu._n_value = 1
+    alu._o_value = 1
+    alu._c_value = 1
+    test_cases = (
+        # Random values in the AR input which should not affect the result
+        create_test_case(0x0000, 0x0000, op_code, 0xFFFF),
+        create_test_case(0x0F38, 0x0004, op_code, 0xFFFB),
+        create_test_case(0x0000, 0xFFFF, op_code, 0x0000),
+        create_test_case(0x2E6D, 0x8531, op_code, 0x7ACE),
+    )
+    check_test_cases(test_cases)
+
+
+def test_reset_ar():
+    op_code = 0b0011
+    # Set the o- and c-flag values of the ALU and make sure that they are
+    # not affected
+    alu.reset()
+    alu._o_value = 1
+    alu._c_value = 1
+    # (Random values in the AR and bus input which should not affect the result)
+    test_case = create_test_case(0xFD12, 0x34B7, op_code, 0, 1, 0)
+    check_test_case(test_case)
+
+
+def test_add():
+    op_code = 0b0100
+    test_cases = (
+        create_test_case(0x0001, 0x0000, op_code, 0x0001, 0, 0, 0, 0),
+        create_test_case(0x0000, 0x0001, op_code, 0x0001, 0, 0, 0, 0),
+        # Test all possible flag combinations
+        create_test_case(0x0004, 0x0008, op_code, 0x000C, 0, 0, 0, 0),
+        create_test_case(0xC000, 0x4001, op_code, 0x0001, 0, 0, 0, 1),
+        create_test_case(0x8000, 0x8001, op_code, 0x0001, 0, 0, 1, 1),
+        create_test_case(0x8000, 0x0001, op_code, 0x8001, 0, 1, 0, 0),
+        create_test_case(0xC000, 0xC000, op_code, 0x8000, 0, 1, 0, 1),
+        create_test_case(0x4000, 0x4001, op_code, 0x8001, 0, 1, 1, 0),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, 0, 0),
+        create_test_case(0x0001, 0xFFFF, op_code, 0x0000, 1, 0, 0, 1),
+        create_test_case(0x8000, 0x8000, op_code, 0x0000, 1, 0, 1, 1),
+    )
+    # Clear flags as they will be affected by the tests
+    alu.reset()
+    check_test_cases(test_cases)
+
+
+def test_subtract():
+    op_code = 0b0101
+    test_cases = (
+        # Test all possible flag combinations
+        create_test_case(0x0001, 0xFFFF, op_code, 0x0002, 0, 0, 0, 0),
+        create_test_case(0x0001, 0x0000, op_code, 0x0001, 0, 0, 0, 1),
+        create_test_case(0x0005, 0x0003, op_code, 0x0002, 0, 0, 0, 1),
+        create_test_case(0x8000, 0x0001, op_code, 0x7FFF, 0, 0, 1, 1),
+        create_test_case(0x0005, 0x000A, op_code, 0xFFFB, 0, 1, 0, 0),
+        create_test_case(0xFFFF, 0x0001, op_code, 0xFFFE, 0, 1, 0, 1),
+        create_test_case(0xA093, 0x0000, op_code, 0xA093, 0, 1, 0, 1),
+        create_test_case(0x4000, 0xBFFF, op_code, 0x8001, 0, 1, 1, 0),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, 0, 1),
+        create_test_case(0x0032, 0x0032, op_code, 0x0000, 1, 0, 0, 1),
+    )
+    alu.reset()
+    check_test_cases(test_cases)
+
+
+def test_and():
+    op_code = 0b0110
+    # Set the o- and c-flag values of the ALU and make sure that they are
+    # not affected
+    alu.reset()
+    alu._o_value = 1
+    alu._c_value = 1
+    test_cases = (
+        create_test_case(0x0F0F, 0xF00F, op_code, 0x000F, 0, 0),
+        create_test_case(0x80DA, 0xF000, op_code, 0x8000, 0, 1),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0),
+        create_test_case(0xFFFF, 0x0000, op_code, 0x0000, 1, 0),
+    )
+    check_test_cases(test_cases)
+
+
+def test_or():
+    op_code = 0b0111
+    # Set the o- and c-flag values of the ALU and make sure that they are
+    # not affected
+    alu.reset()
+    alu._o_value = 1
+    alu._c_value = 1
+    test_cases = (
+        create_test_case(0x0017, 0x0000, op_code, 0x0017, 0, 0),
+        create_test_case(0x0000, 0x0385, op_code, 0x0385, 0, 0),
+        create_test_case(0xAAAA, 0x5555, op_code, 0xFFFF, 0, 1),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0),
+    )
+    check_test_cases(test_cases)
+
+
+def test_add_no_flags():
+    op_code = 0b1000
+    # Set the flag values of the ALU and make sure that they are not affected
+    alu.reset()
+    alu._z_value = 1
+    alu._n_value = 1
+    alu._o_value = 1
+    alu._c_value = 1
+    test_cases = (
+        # Test cases that hit all possible flag combinations during normal add
+        create_test_case(0x0004, 0x0008, op_code, 0x000C),
+        create_test_case(0xC000, 0x4001, op_code, 0x0001),
+        create_test_case(0x8000, 0x8001, op_code, 0x0001),
+        create_test_case(0x8000, 0x0001, op_code, 0x8001),
+        create_test_case(0xC000, 0xC000, op_code, 0x8000),
+        create_test_case(0x4000, 0x4001, op_code, 0x8001),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000),
+        create_test_case(0x0001, 0xFFFF, op_code, 0x0000),
+        create_test_case(0x8000, 0x8000, op_code, 0x0000),
     )
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 1)) == 1
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 10)) == 10
-    assert alu.bin_to_int_twocomp(alu.int_to_negative_bin(2**16 - 4567)) == 4567
-
-
-def test_add_numbers():
-    # test binary sum
-    assert alu.add_numbers(1, 1, False) == 2
-
-    assert alu.add_numbers(1, 1, True) == 2
-    assert alu.z_value == 0
-    assert alu.n_value == 0
-    assert alu.o_value == 0
-    assert alu.c_value == 0
-
-    assert alu.add_numbers(14, 20, False) == 34
-
-    assert alu.add_numbers(0, 0, True) == 0
-    assert alu.z_value == 1
-    assert alu.n_value == 0
-    assert alu.o_value == 0
-    assert alu.c_value == 0
-
-    assert alu.add_numbers(1, 2**16 - 1, True) == 0
-    assert alu.z_value == 1
-    assert alu.n_value == 0
-    assert alu.o_value == 0
-    assert alu.c_value == 1
-
-
-def test_binary_diff():
-    assert alu.subtract_numbers(1, 1, True) == 0
-    assert alu.z_value == 1
-    assert alu.n_value == 0
-    assert alu.o_value == 0
-    assert alu.c_value == 1
-    assert alu.subtract_numbers(0, 1, True) == 65535
-    assert alu.z_value == 0
-    assert alu.n_value == 1
-    assert alu.o_value == 0
-    assert alu.c_value == 0
-    assert alu.subtract_numbers(0xA100, 0xA010, True) == (0xA100 - 0xA010)
-    assert alu.z_value == 0
-    assert alu.n_value == 0
-    assert alu.o_value == 0
-    assert alu.c_value == 1
-    assert alu.subtract_numbers(0xA010, 0xA100, True) == 0xFFFF + 1 + (0xA010 - 0xA100)
-    assert alu.z_value == 0
-    assert alu.n_value == 1
-    assert alu.o_value == 0
-    assert alu.c_value == 0
-    assert alu.subtract_numbers(30, 29, False) == 1
-    assert alu.subtract_numbers(10, 3, False) == 7
-    assert alu.subtract_numbers(4, 2, False) == 2
-    assert alu.subtract_numbers(500, 2, False) == 498
-    assert alu.subtract_numbers(10, 2, False) == 8
-
-
-def test_and_numbers():
-    assert alu.and_numbers(1, 2) == 0
-    assert alu.and_numbers(1, 1) == 1
-    assert alu.and_numbers(3, 1) == 1
-    assert alu.and_numbers(3, 2) == 2
-    assert alu.and_numbers(0, 0) == 0
-    assert alu.and_numbers(20, 0) == 0
-    assert alu.and_numbers(5, 0) == 0
-
-
-def test_or_numbers():
-    assert alu.or_numbers(1, 2) == 3
-    assert alu.or_numbers(1, 1) == 1
-    assert alu.or_numbers(0, 0) == 0
-    assert alu.or_numbers(20, 0) == 20
-
-
-def test_binary_logical_left_shift():
-    assert alu.logical_left_shift(0b01) == 0b10
-    assert alu.c_value == 0
-    assert alu.logical_left_shift(0b11) == 0b110
-    assert alu.c_value == 0
-    assert alu.logical_left_shift(0b0) == 0b0
-    assert alu.c_value == 0
-    assert alu.logical_left_shift(0b1000000000000000) == 0b0
-    assert alu.c_value == 1
-
-
-def test_arithmetic_right_shift():
-    assert alu.arithmetic_right_shift(0) == 0
-    assert alu.c_value == 0
-    assert alu.arithmetic_right_shift(1) == 0
-    assert alu.c_value == 1
-    assert alu.arithmetic_right_shift(2) == 1
-    assert alu.c_value == 0
-    assert alu.arithmetic_right_shift(0b1111111111111111) == 0b1111111111111111
-    assert alu.c_value == 1
-    assert alu.arithmetic_right_shift(0b0111111111111111) == 0b0011111111111111
-    assert alu.c_value == 1
-
-
-def test_logical_right_shift():
-    assert alu.logical_right_shift(0) == 0
-    assert alu.c_value == 0
-    assert alu.logical_right_shift(1) == 0
-    assert alu.c_value == 1
-    assert alu.logical_right_shift(2) == 1
-    assert alu.c_value == 0
+    check_test_cases(test_cases)
+
+
+def test_shift_left():
+    # 16-bit logical left shift, AR only
+    op_code = 0b1001
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_test_case(0x0006, 0x0000, op_code, 0x000C, 0, 0, None, 0),
+        create_test_case(0x0006, 0xF593, op_code, 0x000C, 0, 0, None, 0),
+        create_test_case(0x8001, 0x0000, op_code, 0x0002, 0, 0, None, 1),
+        create_test_case(0x4001, 0x0000, op_code, 0x8002, 0, 1, None, 0),
+        create_test_case(0xC001, 0x0000, op_code, 0x8002, 0, 1, None, 1),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, None, 0),
+        create_test_case(0x0000, 0x0256, op_code, 0x0000, 1, 0, None, 0),
+    )
+    check_test_cases(test_cases)
+
+    # 32-bit logical left shift, ARHR
+    op_code = 0b1010
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_hr_test_case(0x0000, 0x0001, op_code, 0x0000, 0x0002, 0, 0, 0),
+        create_hr_test_case(0x0001, 0x0000, op_code, 0x0002, 0x0000, 0, 0, 0),
+        create_hr_test_case(0x0001, 0x0001, op_code, 0x0002, 0x0002, 0, 0, 0),
+        create_hr_test_case(0x8000, 0x8000, op_code, 0x0001, 0x0000, 0, 0, 1),
+        create_hr_test_case(0x4000, 0x8000, op_code, 0x8001, 0x0000, 0, 1, 0),
+        create_hr_test_case(0xC000, 0x8000, op_code, 0x8001, 0x0000, 0, 1, 1),
+        create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0),
+        create_hr_test_case(0x8000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 1),
+    )
+    check_test_cases(test_cases)
+
+
+def test_arithmetic_shift_right():
+    # 16-bit arithmetic right shift, AR only
+    op_code = 0b1011
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_test_case(0x0002, 0x0000, op_code, 0x0001, 0, 0, None, 0),
+        create_test_case(0x0002, 0x0934, op_code, 0x0001, 0, 0, None, 0),
+        create_test_case(0x0003, 0x0000, op_code, 0x0001, 0, 0, None, 1),
+        create_test_case(0x8000, 0x0000, op_code, 0xC000, 0, 1, None, 0),
+        create_test_case(0x8001, 0x0000, op_code, 0xC000, 0, 1, None, 1),
+        create_test_case(0x0000, 0x0000, op_code, 0x0000, 1, 0, None, 0),
+        create_test_case(0x0001, 0x0000, op_code, 0x0000, 1, 0, None, 1),
+    )
+    check_test_cases(test_cases)
+
+    # 32-bit arithmetic right shift, ARHR
+    op_code = 0b1100
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        # Focus on AR part only
+        create_hr_test_case(0x0002, 0x0000, op_code, 0x0001, 0x0000, 0, 0, 0),
+        # Focus on HR part only
+        create_hr_test_case(0x0000, 0x0002, op_code, 0x0000, 0x0001, 0, 0, 0),
+        # Both AR and HR are non-zero
+        create_hr_test_case(0x400A, 0x0F06, op_code, 0x2005, 0x0783, 0, 0, 0),
+        create_hr_test_case(0x000B, 0x0006, op_code, 0x0005, 0x8003, 0, 0, 0),
+        # Check all possible combinations of flags
+        create_hr_test_case(0x0003, 0x0000, op_code, 0x0001, 0x8000, 0, 0, 0),
+        create_hr_test_case(0x0003, 0x0003, op_code, 0x0001, 0x8001, 0, 0, 1),
+        create_hr_test_case(0x8000, 0x0000, op_code, 0xC000, 0x0000, 0, 1, 0),
+        create_hr_test_case(0x800A, 0x8006, op_code, 0xC005, 0x4003, 0, 1, 0),
+        create_hr_test_case(0xC005, 0x0003, op_code, 0xE002, 0x8001, 0, 1, 1),
+        create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0),
+        create_hr_test_case(0x0000, 0x0001, op_code, 0x0000, 0x0000, 1, 0, 1),
+    )
+    check_test_cases(test_cases)
+
+
+def test_logical_shift_right():
+    op_code = 0b1101
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_test_case(0x0002, 0x0000, op_code, 0x0001, 0, 0, None, 0),
+        create_test_case(0x8002, 0x9654, op_code, 0x4001, 0, 0, None, 0),
+        create_test_case(0x0005, 0xA634, op_code, 0x0002, 0, 0, None, 1),
+        create_test_case(0x8001, 0x0000, op_code, 0x4000, 0, 0, None, 1),
+        create_test_case(0x0000, 0x45F2, op_code, 0x0000, 1, 0, None, 0),
+        create_test_case(0x0001, 0x3D9E, op_code, 0x0000, 1, 0, None, 1),
+    )
+    check_test_cases(test_cases)
 
 
 def test_rotate_left():
-    assert alu.left_rotate(0) == 0
-    assert alu.c_value == 0
-    assert alu.left_rotate(1) == 2
-
-
-def test_signals():
-    # Test just out
-    control_signal.update_value(1)
-    input_signal_b.update_value(15)
-    cpu.do_tick()
-    assert output_signal.get_value() == 15
-
-    # Test do nothing
-    control_signal.update_value(0)
-    cpu.do_tick()
-    assert output_signal.get_value() == 15
-
-    # Test sum
-    control_signal.update_value(4)
-    input_signal_b.update_value(1)
-    input_signal_a.update_value(1)
-    cpu.do_tick()
-    assert output_signal.get_value() == 2
-
-    input_signal_b.update_value(5)
-    input_signal_a.update_value(7)
-    control_signal.update_value(4)
-    cpu.do_tick()
-    assert output_signal.get_value() == 12
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert o_signal.get_value() == 0
-    assert c_signal.get_value() == 0
-
-    # test diff
-    input_signal_b.update_value(7)
-    input_signal_a.update_value(5)
-    control_signal.update_value(5)
-    cpu.do_tick()
-    assert output_signal.get_value() == 2**16 - 2
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 1
-    assert o_signal.get_value() == 0
-    assert c_signal.get_value() == 0
-
-    # test diff
-    input_signal_a.update_value(10)
-    input_signal_b.update_value(2)
-    control_signal.update_value(5)
-    cpu.do_tick()
-    assert output_signal.get_value() == 8
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert o_signal.get_value() == 0
-    assert c_signal.get_value() == 1
-
-
-def test_hr_signal():
-    # test left shift
-    input_signal_a.update_value(0b1000000000000000)
-    input_signal_hr.update_value(0b0000000000000000)
-    control_signal.update_value(0b1010)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0
-    assert hr_signal.get_value() == 1
-    assert z_signal.get_value() == 1
-    assert n_signal.get_value() == 0
-    assert c_signal.get_value() == 0
-
-    # test left shift
-    input_signal_a.update_value(0b1000000000000001)
-    input_signal_hr.update_value(0b1000000000000010)
-    control_signal.update_value(0b1010)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0b0000000000000010
-    assert hr_signal.get_value() == 0b000000000000101
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert c_signal.get_value() == 1
-
-    # test arithmetic right shift
-    input_signal_a.update_value(0b1000000000000000)
-    input_signal_hr.update_value(0b0000000000000000)
-    control_signal.update_value(0b1100)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0b0100000000000000
-    assert hr_signal.get_value() == 0b0000000000000000
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert c_signal.get_value() == 0
-
-    # test arithmetic right shift
-    input_signal_a.update_value(0b0000000110000001)
-    input_signal_hr.update_value(0b1000000000000001)
-    control_signal.update_value(0b1100)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0b1000000011000000
-    assert hr_signal.get_value() == 0b1100000000000000
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 1
-    assert c_signal.get_value() == 1
-
-    # test left rotate
-    input_signal_a.update_value(0b1000000000000000)
-    input_signal_hr.update_value(0b1000000000000000)
-    control_signal.update_value(0b1111)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0b0000000000000001
-    assert hr_signal.get_value() == 0b0000000000000001
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert c_signal.get_value() == 1
-
-    # test left rotate
-    input_signal_a.update_value(0b0000000110000001)
-    input_signal_hr.update_value(0b0100000000000001)
-    control_signal.update_value(0b1111)
-    cpu.do_tick()
-    assert output_signal.get_value() == 0b0000001100000010
-    assert hr_signal.get_value() == 0b1000000000000010
-    assert z_signal.get_value() == 0
-    assert n_signal.get_value() == 0
-    assert c_signal.get_value() == 0
+    # 16-bit left rotate, AR input only
+    op_code = 0b1110
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_test_case(0x0001, 0x0000, op_code, 0x0002, 0, 0, None, 0),
+        create_test_case(0x0008, 0x2971, op_code, 0x0010, 0, 0, None, 0),
+        create_test_case(0x8001, 0x0952, op_code, 0x0003, 0, 0, None, 1),
+        create_test_case(0x5020, 0xAECD, op_code, 0xA040, 0, 1, None, 0),
+        create_test_case(0xC001, 0xF408, op_code, 0x8003, 0, 1, None, 1),
+        create_test_case(0x0000, 0x1987, op_code, 0x0000, 1, 0, None, 0),
+    )
+    check_test_cases(test_cases)
+
+    # 32-bit left rotate, ARHR input
+    op_code = 0b1111
+    # Set the o-flag value of the ALU and make sure that it is not affected
+    alu.reset()
+    alu._o_value = 1
+    test_cases = (
+        create_hr_test_case(0x0410, 0x0000, op_code, 0x0820, 0x0000, 0, 0, 0),
+        create_hr_test_case(0x0000, 0x0410, op_code, 0x0000, 0x0820, 0, 0, 0),
+        create_hr_test_case(0x0080, 0x8410, op_code, 0x0101, 0x0820, 0, 0, 0),
+        create_hr_test_case(0xA000, 0x8001, op_code, 0x4001, 0x0003, 0, 0, 1),
+        create_hr_test_case(0x7001, 0xC001, op_code, 0xE003, 0x8002, 0, 1, 0),
+        create_hr_test_case(0xE021, 0xC001, op_code, 0xC043, 0x8003, 0, 1, 1),
+        create_hr_test_case(0x0000, 0x0000, op_code, 0x0000, 0x0000, 1, 0, 0),
+    )
+    check_test_cases(test_cases)
diff --git a/test/test_mia/test_alu_arithmetics.py b/test/test_mia/test_alu_arithmetics.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ee910d5f41a13d28854b9712073999b034b0be9
--- /dev/null
+++ b/test/test_mia/test_alu_arithmetics.py
@@ -0,0 +1,240 @@
+from simudator.processor.mia.modules.alu import ALU
+from simudator.processor.mia.modules.alu_arithmetics import (
+    add_binary_numbers,
+    arithmetic_right_shift,
+    bin_to_int,
+    bin_to_int_twocomp,
+    binary_and,
+    binary_or,
+    bitwise_not,
+    int_to_bin,
+    left_rotate,
+    logical_left_shift,
+    logical_right_shift,
+)
+
+
+def create_unary_test_case(op, res, carry=None):
+    """
+    Create a test case for a unary arithmetic function.
+
+    Parameters
+    ----------
+    op : int
+        Operand to the arithmetic function.
+    res : int
+        Expected result from the arithmetic function.
+    carry : int
+        Expected carry out from the function if any. Set it to ``None`` to
+        signify that the function does not return a carry out bit.
+
+    Returns
+    -------
+    tuple[int, int, int]
+        Test case for a unary arithmetic function.
+    """
+    return op, res, carry
+
+
+def check_unary_test(test_case, fun):
+    """
+    Assert that a unary arithmetic function passes a test case.
+
+    Parameters
+    ----------
+    test_case : tuple[int, int, int]
+        Test case for the unary arithmetic function.
+    fun : callable
+        Unary arithmetic function to test. Should operate on binary numbers
+        represented as lists of bits.
+        ``fun(list[int]) -> list[int] | tuple[list[int], int]``
+    """
+    (op, res, carry) = test_case
+    bin_op = int_to_bin(op, ALU.WORD_LENGTH)
+    bin_res = int_to_bin(res, ALU.WORD_LENGTH)
+    if carry is None:
+        assert fun(bin_op) == bin_res
+    else:
+        assert fun(bin_op) == (bin_res, carry)
+
+
+def check_unary_tests(test_cases, fun):
+    """
+    Assert that a unary arithmetic function passes some test cases.
+
+    Parameters
+    ----------
+    test_cases : list[tuple[int, int, int]]
+        List of test cases for the unary arithmetic function.
+    fun : callable
+        Unary arithmetic function to test. Should operate on binary numbers
+        represented as lists of bits.
+        ``fun(list[int]) -> list[int] | tuple[list[int], int]``
+    """
+    for case in test_cases:
+        check_unary_test(case, fun)
+
+
+def create_binary_test_case(op_1, op_2, res, carry=None):
+    """
+    Create a test case for a binary arithmetic function.
+
+    Parameters
+    ----------
+    op_1 : int
+        First operand to the arithmetic function.
+    op_2 : int
+        Second operand to the arithmetic function.
+    res : int
+        Expected result from the arithmetic function.
+    carry : int
+        Expected carry out from the function if any. Set it to ``None`` to
+        signify that the function does not return a carry out bit.
+
+    Returns
+    -------
+    tuple[int, int, int, int]
+        Test case for a binary arithmetic function.
+    """
+    return op_1, op_2, res, carry
+
+
+def check_binary_test(test_case, fun):
+    """
+    Assert that a binary arithmetic function passes a test case.
+
+    Parameters
+    ----------
+    test_case : tuple[int, int, int, int]
+        Test case for the binary arithmetic function.
+    fun : callable
+        Binary arithmetic function to test. Should operate on binary numbers
+        represented as lists of bits.
+        ``fun(list[int], list[int]) -> list[int] | tuple[list[int], int]``
+    """
+    (op_1, op_2, res, carry) = test_case
+    bin_op_1 = int_to_bin(op_1, ALU.WORD_LENGTH)
+    bin_op_2 = int_to_bin(op_2, ALU.WORD_LENGTH)
+    bin_res = int_to_bin(res, ALU.WORD_LENGTH)
+    if carry is None:
+        assert fun(bin_op_1, bin_op_2) == bin_res
+    else:
+        assert fun(bin_op_1, bin_op_2) == (bin_res, carry)
+
+
+def check_binary_tests(test_cases, fun):
+    """
+    Assert that a binary arithmetic function passes some test cases.
+
+    Parameters
+    ----------
+    test_case : list[tuple[int, int, int, int]]
+        List of test cases for the binary arithmetic function.
+    fun : callable
+        Binary arithmetic function to test. Should operate on binary numbers
+        represented as lists of bits.
+        ``fun(list[int], list[int]) -> list[int] | tuple[list[int], int]``
+    """
+    for case in test_cases:
+        check_binary_test(case, fun)
+
+
+def test_int_binary_converter():
+    # test int to binary
+    word_length = ALU.WORD_LENGTH
+    assert int_to_bin(0, word_length) == [0 for _ in range(word_length)]
+    assert int_to_bin(1, word_length) == [0] * (word_length - 1) + [1]
+    assert int_to_bin(32767, word_length) == [0] + [1] * (word_length - 1)
+    assert int_to_bin(9, word_length) == [0] * (word_length - 4) + [1, 0, 0, 1]
+
+    # test binary to int
+    assert bin_to_int(int_to_bin(15, word_length)) == 15
+    assert bin_to_int(int_to_bin(0, word_length)) == 0
+    assert bin_to_int(int_to_bin(2**16, word_length)) == 2**16 - 1
+    assert bin_to_int(int_to_bin(3576, word_length)) == 3576
+    assert bin_to_int([0 for _ in range(word_length)]) == 0
+    assert bin_to_int([0] * (word_length - 1) + [1]) == 1
+    assert bin_to_int([1 for _ in range(word_length)]) == 2**16 - 1
+    assert bin_to_int([0] + [1] * (word_length - 1)) == 32767
+    assert bin_to_int([0] * (word_length - 4) + [1, 0, 0, 1]) == 9
+
+
+def test_bitwise_not():
+    test_cases = (
+        create_unary_test_case(0, 2**16 - 1),
+        create_unary_test_case(1, 2**16 - 1 - 1),
+        create_unary_test_case(2, 2**16 - 1 - 2),
+        create_unary_test_case(300, 2**16 - 1 - 300),
+    )
+    check_unary_tests(test_cases, bitwise_not)
+
+
+def test_add():
+    test_cases = (
+        create_binary_test_case(0b01, 0b10, 0b11, 0),
+        create_binary_test_case(0b00101, 0b10101, 0b11010, 0),
+        create_binary_test_case(0, 0, 0, 0),
+        create_binary_test_case(1, 2**16 - 1, 0, 1),
+    )
+    check_binary_tests(test_cases, add_binary_numbers)
+
+
+def test_and():
+    test_cases = (
+        create_binary_test_case(0b01, 0b10, 0b00),
+        create_binary_test_case(0b1, 0b1, 0b1),
+        create_binary_test_case(0b11111, 0b00000, 0b00000),
+        create_binary_test_case(0b11111, 0b10101, 0b10101),
+    )
+    check_binary_tests(test_cases, binary_and)
+
+
+def test_or():
+    test_cases = (
+        create_binary_test_case(0b01, 0b10, 0b11),
+        create_binary_test_case(0b000, 0b000, 0b00),
+        create_binary_test_case(0b11111, 0b00000, 0b11111),
+        create_binary_test_case(0b01010, 0b10101, 0b11111),
+        create_binary_test_case(0b01110, 0b10001, 0b11111),
+    )
+    check_binary_tests(test_cases, binary_or)
+
+
+def test_logical_left_shift():
+    test_cases = (
+        create_unary_test_case(0b01, 0b10, 0),
+        create_unary_test_case(0b11, 0b110, 0),
+        create_unary_test_case(0b0, 0b0, 0),
+        create_unary_test_case(0b1000000000000000, 0b0, 1),
+    )
+    check_unary_tests(test_cases, logical_left_shift)
+
+
+def test_arithmetic_right_shift():
+    test_cases = (
+        create_unary_test_case(0b0, 0b0, 0),
+        create_unary_test_case(0b1, 0b0, 1),
+        create_unary_test_case(0b10, 0b1, 0),
+        create_unary_test_case(0b1111111111111111, 0b1111111111111111, 1),
+        create_unary_test_case(0b0111111111111110, 0b0011111111111111, 0),
+    )
+    check_unary_tests(test_cases, arithmetic_right_shift)
+
+
+def test_logical_right_shift():
+    test_cases = (
+        create_unary_test_case(0b0, 0b0, 0),
+        create_unary_test_case(0b1, 0b0, 1),
+        create_unary_test_case(0b10, 0b01, 0),
+        create_unary_test_case(0b1101, 0b0110, 1),
+    )
+    check_unary_tests(test_cases, logical_right_shift)
+
+
+def test_rotate_left():
+    test_cases = (
+        create_unary_test_case(0b1000000000000000, 0b0000000000000001, 1),
+        create_unary_test_case(0b0, 0b0, 0),
+        create_unary_test_case(0b01, 0b10, 0),
+    )
+    check_unary_tests(test_cases, left_rotate)
diff --git a/test/test_mia/test_ir.py b/test/test_mia/test_ir.py
index 113a19c1cd885e960b6ea3641626136e764a0e7d..ef4a45f2f8611106f18cd71c540ee456475f0f3a 100644
--- a/test/test_mia/test_ir.py
+++ b/test/test_mia/test_ir.py
@@ -1,3 +1,5 @@
+from unittest.mock import mock_open, patch
+
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.ir import IR
@@ -24,3 +26,27 @@ def test_ir():
     assert m_s.get_value() == (instruction >> 8) & 0b11
     assert grx_s.get_value() == (instruction >> 10) & 0b11
     assert op_s.get_value() == (instruction >> 12) & 0b1111
+
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    ir = IR(
+        s,
+        s,
+        s,
+        s,
+        s,
+        s,
+    )
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = ir.name + ":\n"
+    content += "Instruction: " + hex(ir.instruction)[2:] + "\n\n"
+    with patch("builtins.open", mock_file):
+        ir.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_lc.py b/test/test_mia/test_lc.py
index f0877605fbe8436aaf81888c566f3f2e259bb5c6..56714f5ce12021172adc146e70f34d20cedab6c7 100644
--- a/test/test_mia/test_lc.py
+++ b/test/test_mia/test_lc.py
@@ -1,3 +1,5 @@
+from unittest.mock import mock_open, patch
+
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.lc import LC
@@ -212,3 +214,20 @@ def test_get_state():
     state = lc.get_state()
     assert state["name"] == "LC"
     assert state["value"] == 10
+
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    lc = LC(s, s, s, s)
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = lc.name + ":\n"
+    content += "value: " + hex(lc._value)[2:] + "\n\n"
+    with patch("builtins.open", mock_file):
+        lc.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_mia_grx.py b/test/test_mia/test_mia_grx.py
index 7740e2a673c800c3264a2fd7303d0ee75de70cae..69e9d41009dd83821bc6a68adc6ebd2963c509bc 100644
--- a/test/test_mia/test_mia_grx.py
+++ b/test/test_mia/test_mia_grx.py
@@ -1,6 +1,7 @@
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.mia_grx import GRX
+from unittest.mock import patch, mock_open
 
 
 def test_default_name():
@@ -145,3 +146,24 @@ def test_set_value_from_bus():
         error = True
 
     assert error is True
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    grx = GRX(s, s, s, s, s, s, 1)
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = grx.name + ":\n"
+    for index, value in enumerate(grx.registers):
+        content += str(hex(index)[2:]) + ": "
+        content += str(hex(value)[2:])
+        content += "\n"
+
+    content += "\n"
+    with patch("builtins.open", mock_file):
+        grx.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_micro_memory.py b/test/test_mia/test_micro_memory.py
index efa544c6c1028b8c19aa757a43c783eed365f896..8cfe4206ff751966dfe54cf561dbdd775ebce165 100644
--- a/test/test_mia/test_micro_memory.py
+++ b/test/test_mia/test_micro_memory.py
@@ -1,6 +1,7 @@
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.micro_memory import MicroMemory
+from unittest.mock import patch, mock_open
 
 
 def test_alu_field():
@@ -393,3 +394,26 @@ def test_seq_field():
     # uPC should be reset to 0, corresponding to control signal 011
     assert upc_control_s.get_value() == 0b011
     # TODO: Test for some sort of HALT signal
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    uM = MicroMemory(s,s,s,s,s,s,s,s,s,s,s,s,s,s,s)
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = uM.name + ":\n"
+    for index, value in enumerate(uM.memory):
+        content += (
+            str(hex(index)[2:].rjust(uM.MEMORY_ADDRESS_PADDING, "0")) + ": "
+        )
+        content += str(hex(value)[2:].rjust(uM.MEMORY_VALUE_PADDING, "0"))
+        content += "\n"
+
+    content += "\n"
+    with patch("builtins.open", mock_file):
+        uM.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_micro_pc.py b/test/test_mia/test_micro_pc.py
index 1772b552331e538153f4170570d8c7b4a23320b7..91073e409c9da4566227df1ac29c45e329e21153 100644
--- a/test/test_mia/test_micro_pc.py
+++ b/test/test_mia/test_micro_pc.py
@@ -1,6 +1,7 @@
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.micro_pc import MicroPC
+from unittest.mock import patch, mock_open
 
 
 def test_upc():
@@ -69,3 +70,19 @@ def test_upc():
     assert upc.value == f_um.get_value()
     assert t_supc.get_value() == temp
     assert t_um.get_value() == upc.value
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    upc = MicroPC(s,s,s,s,s,s,s)
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = upc.name + ":\n"
+    content += "value: " + hex(upc.value)[2:] + "\n\n"
+    with patch("builtins.open", mock_file):
+        upc.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)
diff --git a/test/test_mia/test_pc.py b/test/test_mia/test_pc.py
index 48d216cfd83ef37ae7cfea4a73efd9e54d3f06a3..5c417e37f034da909c8aa5031851ef3b85fe5073 100644
--- a/test/test_mia/test_pc.py
+++ b/test/test_mia/test_pc.py
@@ -1,6 +1,7 @@
 from simudator.core.processor import Processor
 from simudator.core.signal import Signal
 from simudator.processor.mia.modules.pc import PC
+from unittest.mock import patch, mock_open
 
 
 def test_default_name():
@@ -198,3 +199,19 @@ def test_set_state():
     assert pc.name == "pc"
     assert pc.value == 23
     assert pc.increase_by_one is True
+
+def test_save_state():
+    cpu = Processor()
+    s = Signal(cpu)
+    pc = PC(s,s,s,s,0)
+
+    dummy_file_path = "dummy"
+
+    mock_file = mock_open()
+
+    content = pc.name + ":\n"
+    content += "value: " + hex(pc.value)[2:] + "\n\n"
+    with patch("builtins.open", mock_file):
+        pc.save_state_to_file(dummy_file_path)
+        mock_file.assert_called_once_with(dummy_file_path, "a")
+        mock_file().write.assert_called_once_with(content)