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.
@@ -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"]