Skip to content

Commit

Permalink
Refactor emulator to use new JmpInstruction
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanY3G committed Feb 16, 2025
1 parent 39887ac commit a336d09
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 28 deletions.
35 changes: 30 additions & 5 deletions pioemu/decoding/instruction_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,45 @@
# limitations under the License.
from pioemu.instruction import JmpInstruction

# TODO - Add documentation


class InstructionDecoder:
"""
Decodes state-machine opcodes into higher-level representations.
"""

def __init__(self, side_set_count: int = 0):
"""
Parameters
----------
side_set_count (int): Number of bits of side-set information encoded into opcodes.
"""
self.side_set_count = side_set_count
self.bits_for_delay = 5 - self.side_set_count
self.delay_mask = (1 << self.bits_for_delay) - 1

def decode(self, opcode: int) -> JmpInstruction | None:
"""
Decodes an opcode into an object representing the instruction and its parameters.
Parameters:
opcode (int): The opcode to decode.
Returns:
Instruction: Representation of the given opcode or None when invalid/not supported
"""

if (opcode >> 13) & 7 == 0:
return self._decode_jmp(opcode)

return None

def _decode_jmp(self, opcode: int) -> JmpInstruction:
target_address = opcode & 0x1F
condition = (opcode >> 5) & 7

delay = (opcode >> 8) & self.delay_mask
side_set = opcode >> 8 + self.bits_for_delay
delay_cycles = (opcode >> 8) & self.delay_mask
side_set_value = opcode >> 8 + self.bits_for_delay

return JmpInstruction(target_address, condition, delay, side_set)
return JmpInstruction(
opcode, target_address, condition, delay_cycles, side_set_value
)
26 changes: 18 additions & 8 deletions pioemu/emulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from dataclasses import replace
from typing import Callable, Generator, List, Tuple

from .instruction import Emulation, ProgramCounterAdvance
from .decoding.instruction_decoder import InstructionDecoder as NewInstructionDecoder
from .instruction import Emulation, JmpInstruction, ProgramCounterAdvance
from .instruction_decoder import InstructionDecoder
from .shift_register import ShiftRegister
from .state import State
Expand Down Expand Up @@ -107,7 +108,9 @@ def emulate(
ShiftRegister.shift_right if shift_osr_right else ShiftRegister.shift_left
)

instruction_decoder = InstructionDecoder(
new_instruction_decoder = NewInstructionDecoder(side_set_count)

old_instruction_decoder = InstructionDecoder(
shift_isr_method, shift_osr_method, jmp_pin
)

Expand All @@ -133,7 +136,13 @@ def emulate(
opcode, side_set_count
)

emulation = instruction_decoder.decode(opcode)
instruction = new_instruction_decoder.decode(opcode)

emulation = (
old_instruction_decoder.create_emulation(instruction)
if instruction
else old_instruction_decoder.decode(opcode)
)

if emulation is None:
return
Expand Down Expand Up @@ -185,7 +194,7 @@ def emulate(
)

current_state = _apply_delay_value(
opcode, condition_met, delay_value, current_state
instruction, condition_met, delay_value, current_state
)

current_state = replace(current_state, clock=current_state.clock + 1)
Expand Down Expand Up @@ -252,11 +261,12 @@ def _advance_program_counter(


def _apply_delay_value(
opcode: int, condition_met: bool, delay_value: int, state: State
instruction: JmpInstruction | None,
condition_met: bool,
delay_value: int,
state: State,
) -> State:
jump_instruction = (opcode >> 13) == 0

if jump_instruction or condition_met:
if isinstance(instruction, JmpInstruction) or condition_met:
return replace(state, clock=state.clock + delay_value)

return state
Expand Down
10 changes: 10 additions & 0 deletions pioemu/instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ class ProgramCounterAdvance(Enum):
NEVER = auto()


@dataclass(frozen=True)
class JmpInstruction:
opcode: int
target_address: int
condition: int # TODO: Use an enumeration instead of an integer?
delay_cycles: int
side_set_value: int


@dataclass(frozen=True)
class Emulation:
condition: Callable[[State], bool]
emulate: Callable[[State], State | None]
program_counter_advance: ProgramCounterAdvance
instruction: JmpInstruction | None = None
39 changes: 29 additions & 10 deletions pioemu/instruction_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
)
from .instruction import (
Emulation,
JmpInstruction,
ProgramCounterAdvance,
)
from .instructions.pull import (
Expand Down Expand Up @@ -170,6 +171,22 @@ def __init__(
None,
]

def create_emulation(self, instruction: JmpInstruction) -> Emulation | None:
"""
Returns an emulation for the given instruction.
Parameters:
instruction (Instruction): The instruction to be emulated.
Returns:
Emulation: Emulation for the given instruction or None when invalid/not supported
"""

if isinstance(instruction, JmpInstruction):
return self._decode_jmp(instruction)

return None

def decode(self, opcode: int) -> Emulation | None:
"""
Decodes the given opcode and returns a callable which emulates it.
Expand All @@ -184,18 +201,20 @@ def decode(self, opcode: int) -> Emulation | None:
decoding_function = self.decoding_functions[(opcode >> 13) & 7]
return decoding_function(opcode)

def _decode_jmp(self, opcode: int) -> Emulation | None:
address = opcode & 0x1F
condition = self.jmp_conditions[(opcode >> 5) & 7]
def _decode_jmp(self, instruction: JmpInstruction) -> Emulation | None:
condition = self.jmp_conditions[instruction.condition]

if condition is not None:
return Emulation(
condition,
partial(write_to_program_counter, supplies_value(address)),
ProgramCounterAdvance.WHEN_CONDITION_NOT_MET,
)
if condition is None:
return None

return None
return Emulation(
condition,
partial(
write_to_program_counter, supplies_value(instruction.target_address)
),
ProgramCounterAdvance.WHEN_CONDITION_NOT_MET,
instruction,
)

def _decode_mov(self, opcode: int) -> Emulation:
read_from_source = self.mov_sources[opcode & 7]
Expand Down
18 changes: 13 additions & 5 deletions tests/decoding/test_instruction_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
import pytest
from pioemu.decoding.instruction_decoder import InstructionDecoder
from pioemu.instruction import JmpInstruction
from tests.opcodes import Opcodes


def test_none_returned_for_unsupported_opcodes():
instruction = InstructionDecoder().decode(Opcodes.nop())

assert instruction is None


@pytest.mark.parametrize(
"opcode, side_set_count, expected_target_address, expected_condition, expected_delay, expected_side_set",
"opcode, side_set_count, expected_target_address, expected_condition, expected_delay_cycles, expected_side_set_value",
[
pytest.param(0x1A20, 3, 0, 1, 2, 6, id="jmp !x 0 side 0b110 [2]"),
pytest.param(0x1440, 2, 0, 2, 4, 2, id="jmp x-- 0 side 0b10 [4]"),
Expand All @@ -41,15 +48,16 @@ def test_decoding_of_jmp_instruction(
side_set_count: int,
expected_target_address: int,
expected_condition: int,
expected_delay: int,
expected_side_set: int,
expected_delay_cycles: int,
expected_side_set_value: int,
):
instruction_decoder = InstructionDecoder(side_set_count)
instruction = instruction_decoder.decode(opcode)

assert isinstance(instruction, JmpInstruction)

assert instruction.opcode == opcode
assert instruction.target_address == expected_target_address
assert instruction.condition == expected_condition
assert instruction.delay == expected_delay
assert instruction.side_set == expected_side_set
assert instruction.delay_cycles == expected_delay_cycles
assert instruction.side_set_value == expected_side_set_value

0 comments on commit a336d09

Please sign in to comment.