Skip to content

Commit b2ba5fb

Browse files
committed
Delay interrupt acknowledgement, add HALT instruction
1 parent 6a91263 commit b2ba5fb

2 files changed

Lines changed: 156 additions & 25 deletions

File tree

k0emu/processor.py

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def __init__(self, bus=None):
1818
self._init_opcode_map_prefix_0x71()
1919
self._total_cycles = 0
2020
self._inst_cycles = 0
21+
self._interrupt_delayed = False
2122
self.pc = 0
2223

2324
def reset(self):
@@ -27,18 +28,18 @@ def reset(self):
2728
self._inst_cycles = 0
2829

2930
def step(self):
31+
# fetch and execute the instruction
3032
self._inst_cycles = 0
3133
opcode = self._consume_byte()
3234
handler = self._opcode_map_unprefixed.get(opcode, self._opcode_not_implemented)
3335
handler(opcode)
3436
self._total_cycles += self._inst_cycles
3537

38+
# clock peripherals by number of cycles instruction consumed
3639
self.bus.tick(self._inst_cycles)
3740

38-
if self.read_psw() & Flags.IE: # enabled
39-
pending = self.bus.pending_interrupt
40-
if pending is not None:
41-
self._service_interrupt(pending)
41+
# service interrupt
42+
self._service_interrupt()
4243

4344
@property
4445
def total_cycles(self):
@@ -64,20 +65,31 @@ def _bus_write(self, address, value):
6465
self._inst_cycles += 1
6566
self.bus.write(address, value)
6667

67-
def _service_interrupt(self, pending):
68-
if (self.read_psw() & Flags.ISP) and not pending.high_priority:
69-
return # low-priority blocked by high-priority ISR
68+
def _service_interrupt(self):
69+
# according to the programming manual, after EI or RETI, the
70+
# one instruction is executed before acknowledging any interrupt.
71+
if self._interrupt_delayed:
72+
self._interrupt_delayed = False
73+
return
7074

71-
self.bus.acknowledge_interrupt(pending)
75+
psw = self.read_psw()
7276

73-
# Push PSW and PC, clear IE, set ISP if high priority
74-
self._push(self.read_psw())
75-
psw = self.read_psw() & ~Flags.IE
77+
if not (psw & Flags.IE):
78+
return # interrupts must be enabled
79+
if psw & Flags.ISP:
80+
return # must not already be in a high priority interrupt
81+
82+
pending = self.bus.pending_interrupt
83+
if pending is None:
84+
return
85+
86+
self.bus.acknowledge_interrupt(pending)
87+
self._push(psw)
88+
psw &= ~Flags.IE
7689
if pending.high_priority:
7790
psw |= Flags.ISP
7891
self.write_psw(psw)
7992
self._push_word(self.pc)
80-
8193
self.pc = self.read_memory_word(pending.vector_address)
8294

8395
def _init_opcode_map_unprefixed(self):
@@ -455,9 +467,11 @@ def _init_opcode_map_prefix_0x71(self):
455467
# xor1 cy,0fe20h.0 ;71 07 20 saddr
456468
for opcode2 in (0x07, 0x17, 0x27, 0x37, 0x47, 0x57, 0x67, 0x77):
457469
D[opcode2] = self._opcode_0x71_0x07_to_0x77_xor1
470+
# halt ;71 10
471+
D[0x10] = self._opcode_0x71_0x10_halt
472+
458473
self._opcode_map_prefix_0x71 = D
459474

460-
# not implemented
461475
def _opcode_not_implemented(self, opcode):
462476
raise NotImplementedError()
463477

@@ -1705,15 +1719,6 @@ def _opcode_0x61_0x20_to_0x27_addc(self, opcode2):
17051719
self.write_gp_reg(reg, result)
17061720
self._inst_cycles += 2
17071721

1708-
# set1 [hl].0 ;71 82
1709-
def _opcode_0x71_0x82_to_0xf2_set1(self, opcode2):
1710-
bit = _bit(opcode2)
1711-
address = self.read_gp_regpair(RegisterPairs.HL)
1712-
value = self._bus_read(address)
1713-
result = self._operation_set1(value, bit)
1714-
self._bus_write(address, result)
1715-
self._inst_cycles += 2
1716-
17171722
# clr1 0fffeh.0 ;71 0b fe sfr
17181723
def _opcode_0x71_0x0b_to_0x7b_clr1(self, opcode2):
17191724
bit = _bit(opcode2)
@@ -1732,6 +1737,21 @@ def _opcode_0x71_0x0a_to_0x7a_set1(self, opcode2):
17321737
self._bus_write(address, result)
17331738
self._inst_cycles += 1
17341739

1740+
# halt ;71 10
1741+
def _opcode_0x71_0x10_halt(self, opcode):
1742+
while self.bus.pending_interrupt is None:
1743+
self._total_cycles += 1
1744+
self.bus.tick(1)
1745+
1746+
# set1 [hl].0 ;71 82
1747+
def _opcode_0x71_0x82_to_0xf2_set1(self, opcode2):
1748+
bit = _bit(opcode2)
1749+
address = self.read_gp_regpair(RegisterPairs.HL)
1750+
value = self._bus_read(address)
1751+
result = self._operation_set1(value, bit)
1752+
self._bus_write(address, result)
1753+
self._inst_cycles += 2
1754+
17351755
# clr1 [hl].0 ;71 83
17361756
def _opcode2_0x71_0x83_to_0xf3_clr1(self, opcode2):
17371757
bit = _bit(opcode2)
@@ -2124,6 +2144,9 @@ def _opcode_0x0a_to_0x7a_set1(self, opcode):
21242144
value = self._bus_read(address)
21252145
result = self._operation_set1(value, bit)
21262146
self._bus_write(address, result)
2147+
# after EI (set1 PSW.7), interrupt acknowledge must be delayed
2148+
if (address == self.PSW_ADDRESS) and (bit == 7):
2149+
self._interrupt_delayed = True
21272150

21282151
# clr1 0fe20h.0 ;0b 20 saddr
21292152
# clr1 psw.0 ;0b 1e
@@ -2144,6 +2167,7 @@ def _opcode_0xaf(self, opcode):
21442167
def _opcode_0x8f(self, opcode):
21452168
self.pc = self._pop_word()
21462169
self.write_psw(self._pop())
2170+
self._interrupt_delayed = True
21472171
self._inst_cycles += 2
21482172

21492173
# retb ;9f

k0emu/tests/test_processor_interrupts.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from k0emu.devices import MemoryDevice, InterruptControllerDevice
2+
from k0emu.devices import MemoryDevice, InterruptControllerDevice, WatchTimerDevice
33
from k0emu.processor import Processor, Flags
44

55

@@ -66,7 +66,7 @@ def test_low_priority_blocked_when_isp_set(self):
6666
proc.step()
6767
self.assertEqual(proc.pc, 1) # blocked, not vectored
6868

69-
def test_high_priority_not_blocked_when_isp_set(self):
69+
def test_high_priority_blocked_when_isp_set(self):
7070
proc, mem, intc, dev = _make_processor()
7171
mem.write(0, 0x00) # NOP
7272
mem.write(0x0006, 0x00)
@@ -77,7 +77,7 @@ def test_high_priority_not_blocked_when_isp_set(self):
7777
proc.write_psw(Flags.IE | Flags.ISP)
7878
proc.pc = 0
7979
proc.step()
80-
self.assertEqual(proc.pc, 0x1000)
80+
self.assertEqual(proc.pc, 1) # blocked, not vectored
8181

8282

8383
class InterruptEntryTests(unittest.TestCase):
@@ -255,3 +255,110 @@ def test_inttm52_vectors_to_003c(self):
255255
proc.pc = 0
256256
proc.step()
257257
self.assertEqual(proc.pc, 0x2000)
258+
259+
260+
def _make_processor_with_timer():
261+
"""Create a processor with memory, interrupt controller, and watch timer."""
262+
proc = Processor()
263+
mem = MemoryDevice("test_memory", size=0xFF41)
264+
proc.bus.add_device(mem, (0x0000, 0xFF40))
265+
intc = InterruptControllerDevice("intc")
266+
proc.bus.add_device(intc, (0xFFE0, 0xFFEB))
267+
proc.bus.set_interrupt_controller(intc)
268+
wt = WatchTimerDevice("watch_timer")
269+
proc.bus.add_device(wt, (0xFF41, 0xFF41))
270+
intc.connect(wt, wt.INT_PRESCALER, intc.INTWTNI0)
271+
return proc, mem, intc, wt
272+
273+
274+
class HaltTests(unittest.TestCase):
275+
"""Tests for the HALT instruction (0x71 0x10)."""
276+
277+
def test_halt_wakes_on_pending_interrupt(self):
278+
proc, mem, intc, wt = _make_processor_with_timer()
279+
# HALT at address 0
280+
mem.write(0, 0x71)
281+
mem.write(1, 0x10)
282+
# ISR at vector 0x0024 (INTWTNI0) points to 0x2000
283+
mem.write(0x0024, 0x00)
284+
mem.write(0x0025, 0x20)
285+
# Enable watch timer: WTNM00=1, n=0, fw=128 -> interval=2048
286+
wt.write(0, 0x01)
287+
# Unmask INTWTNI0
288+
intc.write(intc.MK1L, intc.read(intc.MK1L) & 0xFE)
289+
proc.write_psw(Flags.IE)
290+
proc.write_sp(0xFE00)
291+
proc.pc = 0
292+
proc.step()
293+
# HALT should have waited for the timer, then serviced the interrupt
294+
self.assertEqual(proc.pc, 0x2000)
295+
296+
def test_halt_advances_cycles(self):
297+
proc, mem, intc, wt = _make_processor_with_timer()
298+
mem.write(0, 0x71)
299+
mem.write(1, 0x10)
300+
mem.write(0x0024, 0x00)
301+
mem.write(0x0025, 0x20)
302+
wt.write(0, 0x01) # interval=2048
303+
intc.write(intc.MK1L, intc.read(intc.MK1L) & 0xFE)
304+
proc.write_psw(Flags.IE)
305+
proc.write_sp(0xFE00)
306+
proc.pc = 0
307+
cycles_before = proc.total_cycles
308+
proc.step()
309+
# Should have waited ~2048 cycles for the timer
310+
elapsed = proc.total_cycles - cycles_before
311+
self.assertGreaterEqual(elapsed, 2048)
312+
313+
def test_halt_does_not_double_tick(self):
314+
proc, mem, intc, wt = _make_processor_with_timer()
315+
mem.write(0, 0x71)
316+
mem.write(1, 0x10)
317+
mem.write(0x0024, 0x00)
318+
mem.write(0x0025, 0x20)
319+
wt.write(0, 0x01) # interval=2048
320+
intc.write(intc.MK1L, intc.read(intc.MK1L) & 0xFE)
321+
proc.write_psw(Flags.IE)
322+
proc.write_sp(0xFE00)
323+
proc.pc = 0
324+
proc.step()
325+
# The timer should have fired exactly once (no double-ticking).
326+
# After firing, the prescaler counter resets to a small remainder.
327+
self.assertLess(wt._prescaler_counter, 100)
328+
329+
def test_halt_with_interrupt_already_pending(self):
330+
proc, mem, intc, wt = _make_processor_with_timer()
331+
mem.write(0, 0x71)
332+
mem.write(1, 0x10)
333+
mem.write(0x0024, 0x00)
334+
mem.write(0x0025, 0x20)
335+
# Set IF flag before HALT
336+
intc.write(intc.IF1L, 0x01) # INTWTNI0 already pending
337+
intc.write(intc.MK1L, intc.read(intc.MK1L) & 0xFE)
338+
proc.write_psw(Flags.IE)
339+
proc.write_sp(0xFE00)
340+
proc.pc = 0
341+
cycles_before = proc.total_cycles
342+
proc.step()
343+
# Should wake almost immediately
344+
self.assertEqual(proc.pc, 0x2000)
345+
elapsed = proc.total_cycles - cycles_before
346+
# Only the HALT fetch cycles + 1 tick to detect the pending interrupt
347+
self.assertLess(elapsed, 20)
348+
349+
def test_halt_resumes_at_next_instruction_when_ie_disabled(self):
350+
proc, mem, intc, wt = _make_processor_with_timer()
351+
# HALT at address 0, NOP at address 2
352+
mem.write(0, 0x71)
353+
mem.write(1, 0x10)
354+
mem.write(2, 0x00) # NOP
355+
# Set IF flag but IE=0 so interrupt won't be serviced
356+
intc.write(intc.IF1L, 0x01)
357+
intc.write(intc.MK1L, intc.read(intc.MK1L) & 0xFE)
358+
proc.write_psw(0) # IE=0
359+
proc.write_sp(0xFE00)
360+
proc.pc = 0
361+
proc.step()
362+
# HALT wakes on the unmasked interrupt request but since IE=0,
363+
# it executes the next instruction instead of vectoring
364+
self.assertEqual(proc.pc, 2)

0 commit comments

Comments
 (0)