m68000-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- m68000_python/__init__.py +73 -0
- m68000_python/__main__.py +112 -0
- m68000_python/_alu.py +656 -0
- m68000_python/_bcd.py +137 -0
- m68000_python/_bits.py +100 -0
- m68000_python/_control.py +208 -0
- m68000_python/_core.py +545 -0
- m68000_python/_dispatch.py +243 -0
- m68000_python/_ea.py +177 -0
- m68000_python/_flags.py +152 -0
- m68000_python/_loads.py +405 -0
- m68000_python/_shifts.py +168 -0
- m68000_python/_system.py +314 -0
- m68000_python/conformance.py +796 -0
- m68000_python/console.py +254 -0
- m68000_python/cpu.py +373 -0
- m68000_python/debug.py +361 -0
- m68000_python/disasm.py +354 -0
- m68000_python/py.typed +0 -0
- m68000_python/state.py +76 -0
- m68000_python/trace.py +296 -0
- m68000_python-0.1.0.dist-info/METADATA +379 -0
- m68000_python-0.1.0.dist-info/RECORD +25 -0
- m68000_python-0.1.0.dist-info/WHEEL +4 -0
- m68000_python-0.1.0.dist-info/licenses/LICENSE +21 -0
m68000_python/console.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""A line-command debugger over :class:`~m68000_python.debug.DebugSession`.
|
|
2
|
+
|
|
3
|
+
``CommandDebugger(session).execute("step 3")`` returns the lines a terminal
|
|
4
|
+
would print; ``interact(stdin, stdout)`` is the prompt loop that
|
|
5
|
+
``python -m m68000_python`` runs. Numbers are decimal, or hexadecimal with a
|
|
6
|
+
``$`` or ``0x`` prefix, the rule of every console in this family.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
|
|
12
|
+
from m68000_python.debug import BoundaryKind, DebugSession, StepRecord
|
|
13
|
+
from m68000_python.disasm import disassemble
|
|
14
|
+
|
|
15
|
+
HELP = """\
|
|
16
|
+
registers | r show the registers
|
|
17
|
+
step [n] | s [n] run n boundaries (default 1), showing each
|
|
18
|
+
run [n] | c [n] run until a breakpoint, watchpoint, STOP or n steps (default 1,000,000)
|
|
19
|
+
break ADDR | b ADDR stop before the instruction at ADDR
|
|
20
|
+
delete ADDR remove a breakpoint
|
|
21
|
+
watch ADDR [r|w|rw] stop after a step that touches ADDR
|
|
22
|
+
unwatch ADDR remove a watchpoint
|
|
23
|
+
disassemble [ADDR] [N] N instructions from ADDR (default: PC, 8) | d
|
|
24
|
+
memory ADDR [N] N bytes from ADDR (default 64) | m
|
|
25
|
+
set REG VALUE set d0-d7, a0-a7, usp, ssp, sr or pc
|
|
26
|
+
ipl LEVEL set the interrupt level (0-7)
|
|
27
|
+
history [N] the last N boundaries (default 10)
|
|
28
|
+
help this text
|
|
29
|
+
quit | q leave"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CommandError(Exception):
|
|
33
|
+
"""A command that could not be run; the message says why."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class CommandResult:
|
|
38
|
+
"""What one command printed, and whether it asked to leave."""
|
|
39
|
+
|
|
40
|
+
lines: tuple[str, ...] = ()
|
|
41
|
+
quit: bool = False
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if type(self.lines) is not tuple or not all(type(line) is str for line in self.lines):
|
|
45
|
+
raise ValueError("lines must be a tuple of strings")
|
|
46
|
+
if type(self.quit) is not bool:
|
|
47
|
+
raise ValueError("quit must be a bool")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_number(text: str, name: str = "number", *, maximum: int = 0xFFFFFFFF) -> int:
|
|
51
|
+
"""Parse a decimal number, or a hexadecimal one written ``$1F`` or ``0x1F``."""
|
|
52
|
+
body = text.strip()
|
|
53
|
+
try:
|
|
54
|
+
if body.startswith("$"):
|
|
55
|
+
value = int(body[1:], 16)
|
|
56
|
+
elif body[:2].lower() == "0x":
|
|
57
|
+
value = int(body[2:], 16)
|
|
58
|
+
else:
|
|
59
|
+
value = int(body, 10)
|
|
60
|
+
except ValueError:
|
|
61
|
+
raise CommandError(f"{name}: {text!r} is not a number") from None
|
|
62
|
+
if not 0 <= value <= maximum:
|
|
63
|
+
raise CommandError(f"{name}: {text} is out of range 0..{maximum:#x}")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CommandDebugger:
|
|
68
|
+
"""Execute debugger commands against a session."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, session: DebugSession) -> None:
|
|
71
|
+
self.session = session
|
|
72
|
+
|
|
73
|
+
# -- views ---------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def registers(self) -> list[str]:
|
|
76
|
+
state = self.session.target.capture_state()
|
|
77
|
+
d = " ".join(f"D{i}={value:08X}" for i, value in enumerate(state.d))
|
|
78
|
+
a = " ".join(f"A{i}={value:08X}" for i, value in enumerate((*state.a, state.a7)))
|
|
79
|
+
sr = state.sr
|
|
80
|
+
flags = "".join(
|
|
81
|
+
c if sr & bit else "."
|
|
82
|
+
for c, bit in zip("TSXNZVC", (0x8000, 0x2000, 16, 8, 4, 2, 1), strict=True)
|
|
83
|
+
)
|
|
84
|
+
status = f"PC={state.pc & 0xFFFFFF:06X} SR={sr:04X} {flags} I={(sr >> 8) & 7}"
|
|
85
|
+
extra = f"USP={state.usp:08X} SSP={state.ssp:08X} IPL={state.ipl} clock={state.clock}"
|
|
86
|
+
if state.stopped:
|
|
87
|
+
extra += " STOPPED"
|
|
88
|
+
if state.halted:
|
|
89
|
+
extra += " HALTED"
|
|
90
|
+
return [d[:47], d[48:], a[:47], a[48:], status, extra]
|
|
91
|
+
|
|
92
|
+
def _result(self, lines: list[str]) -> CommandResult:
|
|
93
|
+
return CommandResult(tuple(lines))
|
|
94
|
+
|
|
95
|
+
def _describe(self, record: StepRecord) -> str:
|
|
96
|
+
if record.kind is BoundaryKind.INSTRUCTION:
|
|
97
|
+
text = (
|
|
98
|
+
record.instruction.text
|
|
99
|
+
if record.instruction
|
|
100
|
+
else f"(opcode {record.before.ir:04X})"
|
|
101
|
+
)
|
|
102
|
+
return f"{record.before.pc & 0xFFFFFF:06X} {text:40} {record.cycles:4} clocks"
|
|
103
|
+
return f"{record.before.pc & 0xFFFFFF:06X} <{record.kind.value}> {record.cycles} clocks"
|
|
104
|
+
|
|
105
|
+
def disassemble(self, address: int | None, count: int) -> list[str]:
|
|
106
|
+
peek = self.session.peek_word
|
|
107
|
+
if peek is None:
|
|
108
|
+
raise CommandError("the session has no peek_word: nothing to disassemble from")
|
|
109
|
+
if address is None:
|
|
110
|
+
address = self.session.target.capture_state().pc & 0xFFFFFF
|
|
111
|
+
lines = []
|
|
112
|
+
for _ in range(count):
|
|
113
|
+
instruction = disassemble(peek, address)
|
|
114
|
+
words = " ".join(f"{word:04X}" for word in instruction.words)
|
|
115
|
+
lines.append(f"{address:06X} {words:24} {instruction.text}")
|
|
116
|
+
address = (address + instruction.length) & 0xFFFFFF
|
|
117
|
+
return lines
|
|
118
|
+
|
|
119
|
+
def memory(self, address: int, count: int) -> list[str]:
|
|
120
|
+
peek = self.session.peek_word
|
|
121
|
+
if peek is None:
|
|
122
|
+
raise CommandError("the session has no peek_word: no memory to show")
|
|
123
|
+
lines = []
|
|
124
|
+
for row in range(address & ~1, address + count, 16):
|
|
125
|
+
words = [peek((row + i) & 0xFFFFFF) for i in range(0, 16, 2)]
|
|
126
|
+
data = b"".join(word.to_bytes(2, "big") for word in words)
|
|
127
|
+
text = "".join(chr(b) if 32 <= b < 127 else "." for b in data)
|
|
128
|
+
lines.append(f"{row & 0xFFFFFF:06X} {data.hex(' ')} {text}")
|
|
129
|
+
return lines
|
|
130
|
+
|
|
131
|
+
# -- the command language --------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def execute(self, line: str) -> CommandResult:
|
|
134
|
+
words = line.split()
|
|
135
|
+
if not words:
|
|
136
|
+
return CommandResult()
|
|
137
|
+
command, arguments = words[0].lower(), words[1:]
|
|
138
|
+
session = self.session
|
|
139
|
+
if command in ("quit", "q", "exit"):
|
|
140
|
+
return CommandResult(quit=True)
|
|
141
|
+
if command == "help":
|
|
142
|
+
return self._result(HELP.splitlines())
|
|
143
|
+
if command in ("registers", "r", "regs"):
|
|
144
|
+
return self._result(self.registers())
|
|
145
|
+
if command in ("step", "s"):
|
|
146
|
+
count = parse_number(arguments[0], "count") if arguments else 1
|
|
147
|
+
if count == 0:
|
|
148
|
+
raise CommandError("count must be positive")
|
|
149
|
+
return self._result([self._describe(session.step()) for _ in range(count)])
|
|
150
|
+
if command in ("run", "c", "continue"):
|
|
151
|
+
count = parse_number(arguments[0], "count") if arguments else 1_000_000
|
|
152
|
+
if count == 0:
|
|
153
|
+
raise CommandError("count must be positive")
|
|
154
|
+
result = session.run(max_steps=count)
|
|
155
|
+
lines = [
|
|
156
|
+
f"{result.reason.value} after {result.steps:,} steps, {result.cycles:,} clocks"
|
|
157
|
+
]
|
|
158
|
+
lines += [
|
|
159
|
+
f" {kind} {address:06X} = {value:X}" for kind, address, value, _ in result.hits
|
|
160
|
+
]
|
|
161
|
+
return self._result(lines + self.registers())
|
|
162
|
+
if command in ("break", "b"):
|
|
163
|
+
session.add_breakpoint(
|
|
164
|
+
parse_number(self._one(arguments, "address"), "address", maximum=0xFFFFFF)
|
|
165
|
+
)
|
|
166
|
+
return self._result(
|
|
167
|
+
[f"breakpoints: {', '.join(f'{a:06X}' for a in sorted(session.breakpoints))}"]
|
|
168
|
+
)
|
|
169
|
+
if command == "delete":
|
|
170
|
+
session.remove_breakpoint(
|
|
171
|
+
parse_number(self._one(arguments, "address"), "address", maximum=0xFFFFFF)
|
|
172
|
+
)
|
|
173
|
+
return CommandResult()
|
|
174
|
+
if command == "watch":
|
|
175
|
+
if not arguments:
|
|
176
|
+
raise CommandError("watch needs an address")
|
|
177
|
+
kind = arguments[1] if len(arguments) > 1 else "rw"
|
|
178
|
+
try:
|
|
179
|
+
session.add_watchpoint(
|
|
180
|
+
parse_number(arguments[0], "address", maximum=0xFFFFFF), kind
|
|
181
|
+
)
|
|
182
|
+
except ValueError as exc:
|
|
183
|
+
raise CommandError(str(exc)) from None
|
|
184
|
+
return CommandResult()
|
|
185
|
+
if command == "unwatch":
|
|
186
|
+
session.remove_watchpoint(
|
|
187
|
+
parse_number(self._one(arguments, "address"), "address", maximum=0xFFFFFF)
|
|
188
|
+
)
|
|
189
|
+
return CommandResult()
|
|
190
|
+
if command in ("disassemble", "d", "dis"):
|
|
191
|
+
address = parse_number(arguments[0], "address", maximum=0xFFFFFF) if arguments else None
|
|
192
|
+
count = parse_number(arguments[1], "count") if len(arguments) > 1 else 8
|
|
193
|
+
return self._result(self.disassemble(address, count))
|
|
194
|
+
if command in ("memory", "m"):
|
|
195
|
+
address = parse_number(self._one(arguments[:1], "address"), "address", maximum=0xFFFFFF)
|
|
196
|
+
count = parse_number(arguments[1], "count", maximum=4096) if len(arguments) > 1 else 64
|
|
197
|
+
return self._result(self.memory(address, count))
|
|
198
|
+
if command == "set":
|
|
199
|
+
if len(arguments) != 2:
|
|
200
|
+
raise CommandError("set needs a register and a value")
|
|
201
|
+
self._set(arguments[0].lower(), parse_number(arguments[1], arguments[0]))
|
|
202
|
+
return self._result(self.registers())
|
|
203
|
+
if command == "ipl":
|
|
204
|
+
session.cpu.set_ipl(parse_number(self._one(arguments, "level"), "level", maximum=7))
|
|
205
|
+
return CommandResult()
|
|
206
|
+
if command == "history":
|
|
207
|
+
count = parse_number(arguments[0], "count") if arguments else 10
|
|
208
|
+
records = list(session.iter_history())[-count:] if count else []
|
|
209
|
+
return self._result([self._describe(record) for record in records])
|
|
210
|
+
raise CommandError(f"unknown command {command!r} (try help)")
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _one(arguments: list[str], name: str) -> str:
|
|
214
|
+
if len(arguments) != 1:
|
|
215
|
+
raise CommandError(f"expected one {name}")
|
|
216
|
+
return arguments[0]
|
|
217
|
+
|
|
218
|
+
def _set(self, name: str, value: int) -> None:
|
|
219
|
+
cpu = self.session.cpu
|
|
220
|
+
if len(name) == 2 and name[0] in "da" and name[1] in "01234567":
|
|
221
|
+
cpu.R[(0 if name[0] == "d" else 8) + int(name[1])] = value
|
|
222
|
+
elif name == "usp":
|
|
223
|
+
cpu.usp = value
|
|
224
|
+
elif name == "ssp":
|
|
225
|
+
cpu.ssp = value
|
|
226
|
+
elif name == "sr":
|
|
227
|
+
if value > 0xFFFF:
|
|
228
|
+
raise CommandError("sr is 16 bits")
|
|
229
|
+
cpu.set_sr(value)
|
|
230
|
+
elif name == "pc":
|
|
231
|
+
cpu.set_pc(value)
|
|
232
|
+
else:
|
|
233
|
+
raise CommandError(f"no register {name!r}")
|
|
234
|
+
|
|
235
|
+
def interact(self, stdin: TextIO, stdout: TextIO) -> None:
|
|
236
|
+
"""Read commands from ``stdin`` until ``quit`` or end of input."""
|
|
237
|
+
while True:
|
|
238
|
+
stdout.write("m68000> ")
|
|
239
|
+
stdout.flush()
|
|
240
|
+
line = stdin.readline()
|
|
241
|
+
if not line:
|
|
242
|
+
return
|
|
243
|
+
try:
|
|
244
|
+
result = self.execute(line)
|
|
245
|
+
except CommandError as exc:
|
|
246
|
+
stdout.write(f"error: {exc}\n")
|
|
247
|
+
continue
|
|
248
|
+
for text in result.lines:
|
|
249
|
+
stdout.write(text + "\n")
|
|
250
|
+
if result.quit:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = ["CommandDebugger", "CommandError", "CommandResult", "parse_number"]
|
m68000_python/cpu.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""The public MC68000 CPU class.
|
|
2
|
+
|
|
3
|
+
The host owns memory and every device. It passes four callables in --
|
|
4
|
+
``read_byte(address)``, ``read_word(address)``, ``write_byte(address, value)``
|
|
5
|
+
and ``write_word(address, value)`` over a 24-bit address space -- calls
|
|
6
|
+
:meth:`M68000CPU.step`, and adds the returned clock count to its own clock.
|
|
7
|
+
Interrupts are a level the host sets with :meth:`M68000CPU.set_ipl`; the
|
|
8
|
+
optional ``acknowledge(level)`` callable answers the interrupt-acknowledge
|
|
9
|
+
cycle with a vector number, :data:`AUTOVECTOR` or :data:`SPURIOUS`. See
|
|
10
|
+
README.md, "The embedding contract".
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from m68000_python._alu import ALUMixin
|
|
16
|
+
from m68000_python._bcd import BCDMixin
|
|
17
|
+
from m68000_python._bits import BitsMixin
|
|
18
|
+
from m68000_python._control import ControlMixin
|
|
19
|
+
from m68000_python._core import (
|
|
20
|
+
AUTOVECTOR,
|
|
21
|
+
IPL_MASK,
|
|
22
|
+
MASK24,
|
|
23
|
+
SPURIOUS,
|
|
24
|
+
VECTOR_AUTOVECTOR_BASE,
|
|
25
|
+
VECTOR_SPURIOUS,
|
|
26
|
+
VECTOR_TRACE,
|
|
27
|
+
VECTOR_UNINITIALIZED,
|
|
28
|
+
Acknowledge,
|
|
29
|
+
CoreMixin,
|
|
30
|
+
GroupZero,
|
|
31
|
+
ReadFunction,
|
|
32
|
+
S,
|
|
33
|
+
T,
|
|
34
|
+
WriteFunction,
|
|
35
|
+
)
|
|
36
|
+
from m68000_python._dispatch import Handler, build_table
|
|
37
|
+
from m68000_python._ea import EAMixin
|
|
38
|
+
from m68000_python._flags import FlagsMixin
|
|
39
|
+
from m68000_python._loads import LoadsMixin
|
|
40
|
+
from m68000_python._shifts import ShiftsMixin
|
|
41
|
+
from m68000_python._system import SystemMixin
|
|
42
|
+
from m68000_python.state import CPUState
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class M68000CPU(
|
|
46
|
+
ALUMixin,
|
|
47
|
+
BCDMixin,
|
|
48
|
+
BitsMixin,
|
|
49
|
+
ControlMixin,
|
|
50
|
+
LoadsMixin,
|
|
51
|
+
ShiftsMixin,
|
|
52
|
+
SystemMixin,
|
|
53
|
+
EAMixin,
|
|
54
|
+
FlagsMixin,
|
|
55
|
+
CoreMixin,
|
|
56
|
+
):
|
|
57
|
+
"""A Motorola MC68000 instruction core (also the MC68HC000 and MC68EC000).
|
|
58
|
+
|
|
59
|
+
Registers: ``R`` is a list of sixteen 32-bit values, D0-D7 then A0-A7,
|
|
60
|
+
where A7 is the stack pointer of the current mode; ``usp`` and ``ssp``
|
|
61
|
+
name the two stack pointers whatever the mode. ``SR`` is the 16-bit
|
|
62
|
+
status register. ``PC`` is the address of the next instruction; the
|
|
63
|
+
prefetch queue behind it is ``ir`` (that instruction's first word) and
|
|
64
|
+
``irc`` (the word after it), read from memory ahead of execution as the
|
|
65
|
+
chip does (docs/start-here.md, "Prefetch").
|
|
66
|
+
|
|
67
|
+
Keyword options, each costing nothing unless used:
|
|
68
|
+
|
|
69
|
+
* ``acknowledge(level)``: the interrupt-acknowledge cycle. Return a
|
|
70
|
+
vector number (0-255), :data:`AUTOVECTOR`, or :data:`SPURIOUS`. Left
|
|
71
|
+
out, every interrupt is autovectored, as on most arcade boards.
|
|
72
|
+
* ``function_codes=True``: every bus call gets ``fc=`` (UM Table 3-2):
|
|
73
|
+
1/2 user data/program, 5/6 supervisor data/program, 7 CPU space.
|
|
74
|
+
* ``tas_write(address, value)``: the write half of TAS's read-modify-write
|
|
75
|
+
cycle, for a bus that treats it differently (the Genesis drops it;
|
|
76
|
+
docs/undocumented-behavior.md). Defaults to ``write_byte``.
|
|
77
|
+
* ``address_error(address, write, fc)``: told about the access an address
|
|
78
|
+
error aborted, which never reaches the bus (UM 6.3.10).
|
|
79
|
+
* ``reset_devices()``: called when the RESET instruction pulses the RESET
|
|
80
|
+
line (PRM 6-83); the processor itself is not reset.
|
|
81
|
+
|
|
82
|
+
A host raises :class:`BusError` from any bus callable to assert BERR.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
_table: list[Handler]
|
|
86
|
+
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
read_byte: ReadFunction,
|
|
90
|
+
read_word: ReadFunction,
|
|
91
|
+
write_byte: WriteFunction,
|
|
92
|
+
write_word: WriteFunction,
|
|
93
|
+
*,
|
|
94
|
+
acknowledge: Acknowledge | None = None,
|
|
95
|
+
function_codes: bool = False,
|
|
96
|
+
tas_write: WriteFunction | None = None,
|
|
97
|
+
address_error: Callable[[int, bool, int], None] | None = None,
|
|
98
|
+
reset_devices: Callable[[], None] | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
for name, value in (("acknowledge", acknowledge), ("tas_write", tas_write),
|
|
101
|
+
("address_error", address_error),
|
|
102
|
+
("reset_devices", reset_devices)): # fmt: skip
|
|
103
|
+
if value is not None and not callable(value):
|
|
104
|
+
raise TypeError(f"{name} must be callable or None")
|
|
105
|
+
if type(function_codes) is not bool:
|
|
106
|
+
raise TypeError("function_codes must be a bool")
|
|
107
|
+
self._init_core(
|
|
108
|
+
read_byte,
|
|
109
|
+
read_word,
|
|
110
|
+
write_byte,
|
|
111
|
+
write_word,
|
|
112
|
+
acknowledge,
|
|
113
|
+
function_codes,
|
|
114
|
+
tas_write,
|
|
115
|
+
address_error,
|
|
116
|
+
reset_devices,
|
|
117
|
+
)
|
|
118
|
+
cls = type(self)
|
|
119
|
+
if "_table" not in cls.__dict__:
|
|
120
|
+
cls._table = build_table(cls)
|
|
121
|
+
|
|
122
|
+
# -- state capture and restore (docs/cpu-state.md) --------------------------
|
|
123
|
+
|
|
124
|
+
def capture_state(self) -> CPUState:
|
|
125
|
+
"""Return an immutable snapshot of all CPU-owned state; no host reads."""
|
|
126
|
+
R = self.R
|
|
127
|
+
return CPUState(
|
|
128
|
+
d=tuple(R[0:8]),
|
|
129
|
+
a=tuple(R[8:15]),
|
|
130
|
+
usp=self.usp,
|
|
131
|
+
ssp=self.ssp,
|
|
132
|
+
sr=self.SR,
|
|
133
|
+
pc=(self._pc - 4) & 0xFFFFFFFF,
|
|
134
|
+
ir=self.ir,
|
|
135
|
+
irc=self.irc,
|
|
136
|
+
ipl=self.ipl,
|
|
137
|
+
nmi_edge=self._nmi_edge,
|
|
138
|
+
trace_pending=self._trace_pending,
|
|
139
|
+
stopped=self.stopped,
|
|
140
|
+
halted=self.halted,
|
|
141
|
+
clock=self.clock,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def restore_state(self, state: CPUState) -> None:
|
|
145
|
+
"""Restore a captured state without touching the host's memory or devices."""
|
|
146
|
+
if type(state) is not CPUState:
|
|
147
|
+
raise TypeError("state must be a CPUState")
|
|
148
|
+
R = self.R
|
|
149
|
+
R[0:8] = state.d
|
|
150
|
+
R[8:15] = state.a
|
|
151
|
+
self.SR = state.sr
|
|
152
|
+
if state.sr & S:
|
|
153
|
+
R[15], self._other_sp = state.ssp, state.usp
|
|
154
|
+
else:
|
|
155
|
+
R[15], self._other_sp = state.usp, state.ssp
|
|
156
|
+
self._pc = (state.pc + 4) & 0xFFFFFFFF
|
|
157
|
+
self.ir = state.ir
|
|
158
|
+
self.irc = state.irc
|
|
159
|
+
self.ipl = state.ipl
|
|
160
|
+
self._nmi_edge = state.nmi_edge
|
|
161
|
+
self._trace_pending = state.trace_pending
|
|
162
|
+
self.stopped = state.stopped
|
|
163
|
+
self.halted = state.halted
|
|
164
|
+
self.clock = state.clock
|
|
165
|
+
|
|
166
|
+
# -- the program counter as a programmer sees it --------------------------
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def PC(self) -> int:
|
|
170
|
+
"""The address of the next instruction (the queue reads 4 bytes ahead)."""
|
|
171
|
+
return (self._pc - 4) & 0xFFFFFFFF
|
|
172
|
+
|
|
173
|
+
def set_pc(self, address: int) -> None:
|
|
174
|
+
"""Start executing at ``address``: refill the prefetch queue from it.
|
|
175
|
+
|
|
176
|
+
Two program reads, as a jump does; no clocks are counted. The host
|
|
177
|
+
uses this instead of a reset when it loads a program itself. An odd
|
|
178
|
+
address is refused: instructions live at even addresses (UM 6.3.10),
|
|
179
|
+
and a jump to an odd one is an address error, not a start.
|
|
180
|
+
"""
|
|
181
|
+
if address & 1:
|
|
182
|
+
raise ValueError("an instruction address must be even")
|
|
183
|
+
address &= 0xFFFFFFFF
|
|
184
|
+
self.ir = self._read_program(address & MASK24)
|
|
185
|
+
self.irc = self._read_program((address + 2) & MASK24)
|
|
186
|
+
self._pc = (address + 4) & 0xFFFFFFFF
|
|
187
|
+
self.stopped = False
|
|
188
|
+
|
|
189
|
+
# -- external signals -----------------------------------------------------
|
|
190
|
+
|
|
191
|
+
def set_sr(self, value: int) -> None:
|
|
192
|
+
"""Write SR as an instruction would: A7 follows S to the other stack pointer.
|
|
193
|
+
|
|
194
|
+
Assigning ``SR`` directly changes the bits and nothing else, which is
|
|
195
|
+
what a state restore wants and a host usually does not.
|
|
196
|
+
"""
|
|
197
|
+
self._set_sr(value)
|
|
198
|
+
|
|
199
|
+
def _next_pc(self) -> int:
|
|
200
|
+
"""The address execution resumes at: what an interrupt or trace stacks.
|
|
201
|
+
|
|
202
|
+
At a boundary the queue has read 4 bytes ahead. STOP leaves the queue
|
|
203
|
+
as it was (its immediate still in IRC, corpus T3), so a stopped CPU
|
|
204
|
+
resumes at the prefetch address itself: the instruction after STOP.
|
|
205
|
+
"""
|
|
206
|
+
return self._pc if self.stopped else (self._pc - 4) & 0xFFFFFFFF
|
|
207
|
+
|
|
208
|
+
def set_ipl(self, level: int) -> None:
|
|
209
|
+
"""Set the interrupt level on IPL2-IPL0 (0 = none, 7 = non-maskable).
|
|
210
|
+
|
|
211
|
+
Level 7 is edge-triggered: a change to 7 from below is taken once
|
|
212
|
+
even with the mask at 7 (UM 6.3.2).
|
|
213
|
+
"""
|
|
214
|
+
if not 0 <= level <= 7:
|
|
215
|
+
raise ValueError("IPL level must be 0-7")
|
|
216
|
+
if level == 7 and self.ipl != 7:
|
|
217
|
+
self._nmi_edge = True
|
|
218
|
+
self.ipl = level
|
|
219
|
+
|
|
220
|
+
def reset(self) -> int:
|
|
221
|
+
"""The reset exception (UM 6.3.1): S set, T clear, mask 7, SSP and PC from 0 and 4.
|
|
222
|
+
|
|
223
|
+
Nothing is pushed. A fault while fetching the vectors or the first
|
|
224
|
+
instruction (an odd initial PC) halts the processor as a double bus
|
|
225
|
+
fault (UM 5.4.4). Returns the clocks spent, 40 as UM Table 8-14
|
|
226
|
+
prints: 14 internal, the four vector reads, and the two-read refill
|
|
227
|
+
of the queue with its 2 idle clocks, as every other exception entry
|
|
228
|
+
refills it. The 14 are Nuked-MD's: its gate-level 68000 reads the
|
|
229
|
+
SSP vector 14 clocks after RESET is released (docs/claims.md).
|
|
230
|
+
"""
|
|
231
|
+
self._cycles = 0
|
|
232
|
+
self.halted = False
|
|
233
|
+
self.stopped = False
|
|
234
|
+
self._trace_pending = False
|
|
235
|
+
self._nmi_edge = False
|
|
236
|
+
self._set_sr((self.SR | S | IPL_MASK) & ~T)
|
|
237
|
+
self._cycles += 14
|
|
238
|
+
try:
|
|
239
|
+
self.R[15] = (self._read_program_word(0) << 16) | self._read_program_word(2)
|
|
240
|
+
pc = (self._read_program_word(4) << 16) | self._read_program_word(6)
|
|
241
|
+
self._fault_pc = pc
|
|
242
|
+
self._jump_idle(pc)
|
|
243
|
+
except GroupZero:
|
|
244
|
+
# An address or bus error during the reset sequence (an odd initial
|
|
245
|
+
# PC, BERR on a vector) is a double bus fault: halt (UM 5.4.4).
|
|
246
|
+
self.halted = True
|
|
247
|
+
self.clock += self._cycles
|
|
248
|
+
return self._cycles
|
|
249
|
+
|
|
250
|
+
# -- execution ------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def step_clocks(self) -> int:
|
|
254
|
+
"""Clocks the step in progress has spent so far (read-only).
|
|
255
|
+
|
|
256
|
+
Read from inside a bus callback, it counts the access being made as
|
|
257
|
+
complete: that access occupies clocks ``step_clocks - 4`` to
|
|
258
|
+
``step_clocks`` of the step, so ``clock + step_clocks - 4`` is when it
|
|
259
|
+
began on the host's running count. A host that stalls the CPU (a
|
|
260
|
+
wait state, a device holding the bus) uses it to place the stall at
|
|
261
|
+
the right clock; the core itself does not model wait states, so the
|
|
262
|
+
host adds its own stall clocks to the ``step()`` total. Inside an
|
|
263
|
+
``acknowledge`` callback the acknowledge cycle's own four clocks are
|
|
264
|
+
not yet counted. Between steps it is the last ``step()``'s (or
|
|
265
|
+
``reset()``'s) total.
|
|
266
|
+
"""
|
|
267
|
+
return self._cycles
|
|
268
|
+
|
|
269
|
+
def step(self) -> int:
|
|
270
|
+
"""Run one instruction or one exception entry; return its clock count.
|
|
271
|
+
|
|
272
|
+
At an instruction boundary, in this order: a halted CPU idles; a
|
|
273
|
+
trace exception left by the previous instruction is taken (UM 6.3.8:
|
|
274
|
+
trace outranks interrupts); an interrupt above the mask, or a level
|
|
275
|
+
7 edge, is taken (UM 6.3.2); a stopped CPU idles; otherwise the
|
|
276
|
+
instruction in IR runs.
|
|
277
|
+
"""
|
|
278
|
+
if self.halted:
|
|
279
|
+
self._cycles = 4
|
|
280
|
+
self.clock += 4
|
|
281
|
+
return 4
|
|
282
|
+
self._cycles = 0
|
|
283
|
+
try:
|
|
284
|
+
level = self.ipl
|
|
285
|
+
if self._trace_pending:
|
|
286
|
+
self._trace_pending = False
|
|
287
|
+
self._opcode = self.ir
|
|
288
|
+
self._exception(VECTOR_TRACE, self._next_pc())
|
|
289
|
+
self.stopped = False
|
|
290
|
+
elif level and (level > (self.SR >> 8) & 7 or (level == 7 and self._nmi_edge)):
|
|
291
|
+
self._interrupt(level)
|
|
292
|
+
elif self.stopped:
|
|
293
|
+
self._cycles = 4
|
|
294
|
+
else:
|
|
295
|
+
opcode = self._opcode = self.ir
|
|
296
|
+
self._fault_pc = self._pc - 2
|
|
297
|
+
traced = self.SR & T
|
|
298
|
+
if traced:
|
|
299
|
+
self._untraced = False
|
|
300
|
+
self._table[opcode](self, opcode)
|
|
301
|
+
if traced and not self._untraced:
|
|
302
|
+
# Trace follows an instruction that completed, as its own
|
|
303
|
+
# boundary: the next step takes it (UM 6.3.8). An illegal
|
|
304
|
+
# or privileged instruction was never executed and is not
|
|
305
|
+
# traced (_system._not_executed). The corpus's final
|
|
306
|
+
# states are captured before the trace (its issue #2).
|
|
307
|
+
self._trace_pending = True
|
|
308
|
+
except GroupZero as fault:
|
|
309
|
+
self._group_zero(fault)
|
|
310
|
+
self.clock += self._cycles
|
|
311
|
+
return self._cycles
|
|
312
|
+
|
|
313
|
+
def _interrupt(self, level: int) -> None:
|
|
314
|
+
"""Interrupt entry (UM 6.3.2-6.3.4): acknowledge, stack, vector.
|
|
315
|
+
|
|
316
|
+
The mask rises to the accepted level; the acknowledge cycle names the
|
|
317
|
+
vector, or asks for the autovector (24 + level), or reports spurious
|
|
318
|
+
(vector 24). A vector number outside 0-255 is an uninitialized
|
|
319
|
+
vector (15).
|
|
320
|
+
"""
|
|
321
|
+
if level == 7:
|
|
322
|
+
self._nmi_edge = False
|
|
323
|
+
self._opcode = self.ir
|
|
324
|
+
pc = self._next_pc() # the instruction the interrupt came before
|
|
325
|
+
self.stopped = False
|
|
326
|
+
# Three internal steps: SR copied, S set and T cleared, the mask
|
|
327
|
+
# raised to the level being taken (UM 6.3.2; MAME 0.285's order).
|
|
328
|
+
self._cycles += 6
|
|
329
|
+
saved = self.SR
|
|
330
|
+
self._set_sr(((saved | S) & ~T & ~IPL_MASK) | (level << 8))
|
|
331
|
+
self._processing_exception = True
|
|
332
|
+
sp = self.R[15]
|
|
333
|
+
self._write_word(sp - 2, pc)
|
|
334
|
+
# The acknowledge cycle comes between the first push and the rest.
|
|
335
|
+
answer = AUTOVECTOR if self._acknowledge is None else self._acknowledge(level)
|
|
336
|
+
self._cycles += 4
|
|
337
|
+
if answer == AUTOVECTOR:
|
|
338
|
+
vector = VECTOR_AUTOVECTOR_BASE + level
|
|
339
|
+
# VPA: the cycle waits for the E clock (CLK/10) as the manual
|
|
340
|
+
# describes (UM 5.1.4, 6.3.2); the phase is the host's running
|
|
341
|
+
# clock count, ``clock``, as MAME 0.285's vpa_sync takes it.
|
|
342
|
+
self._cycles += self._e_clock_wait()
|
|
343
|
+
elif answer == SPURIOUS:
|
|
344
|
+
vector = VECTOR_SPURIOUS
|
|
345
|
+
elif 0 <= answer <= 255:
|
|
346
|
+
vector = answer
|
|
347
|
+
else:
|
|
348
|
+
vector = VECTOR_UNINITIALIZED
|
|
349
|
+
self._cycles += 4
|
|
350
|
+
sp = (sp - 6) & 0xFFFFFFFF
|
|
351
|
+
self.R[15] = sp
|
|
352
|
+
self._write_word(sp, saved)
|
|
353
|
+
self._write_word(sp + 2, pc >> 16)
|
|
354
|
+
target = self._read_vector(vector)
|
|
355
|
+
self._jump_idle(target)
|
|
356
|
+
self._processing_exception = False
|
|
357
|
+
|
|
358
|
+
def _e_clock_wait(self) -> int:
|
|
359
|
+
"""Clocks an autovectored acknowledge waits for the E clock.
|
|
360
|
+
|
|
361
|
+
MAME 0.285 (m68000.cpp, ``vpa_sync`` and ``vpa_after``): with t the
|
|
362
|
+
clock count at the start of the cycle, the transfer is aligned to the
|
|
363
|
+
next E-clock period boundary, one period later when fewer than 3
|
|
364
|
+
clocks remain, and one clock is added after it. The MAME lockstep
|
|
365
|
+
on System 16B agrees at every phase (docs/validation.md, rung 6).
|
|
366
|
+
"""
|
|
367
|
+
now = self.clock + self._cycles - 4
|
|
368
|
+
phase = now % 10
|
|
369
|
+
self.last_acknowledge_phase = phase
|
|
370
|
+
return ((10 - phase) if phase < 7 else (20 - phase)) + 1
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
__all__ = ["AUTOVECTOR", "M68000CPU", "SPURIOUS"]
|