diff --git a/build_test.go b/build_test.go index 1a9de9f..b1eed96 100644 --- a/build_test.go +++ b/build_test.go @@ -61,6 +61,7 @@ func TestBuildWithINT(t *testing.T) { type dummyNMI struct{} func (dummyNMI) CheckNMI() bool { return false } +func (dummyNMI) ReturnNMI() {} func TestBuildWithNMI(t *testing.T) { v := dummyNMI{} diff --git a/cpu.go b/cpu.go index 834c47b..3f7ed24 100644 --- a/cpu.go +++ b/cpu.go @@ -40,25 +40,34 @@ type im0data struct { start uint16 end uint16 data []uint8 + + base Memory } -func newIm0data(pc uint16, d []uint8) *im0data { +func newIm0data(pc uint16, d []uint8, base Memory) *im0data { return &im0data{ start: pc, end: pc + uint16(len(d)-1), data: d, + base: base, } } func (im0 *im0data) Get(addr uint16) uint8 { if addr < im0.start || addr > im0.end { - return 0 + // delegate to base Memory for out of range. + return im0.base.Get(addr) } return im0.data[addr-im0.start] } func (im0 *im0data) Set(addr uint16, value uint8) { - // invalid opepration, nothing to do. + if addr >= im0.start && addr <= im0.end { + // invalid opepration, nothing to do. + return + } + // delegate to base Memory for out of range. + im0.base.Set(addr, value) } func (cpu *CPU) failf(msg string, args ...interface{}) { @@ -218,7 +227,7 @@ func (cpu *CPU) tryInterrupt() bool { return true } // check maskable interrupt. - if cpu.INT == nil { + if !cpu.IFF1 || cpu.INT == nil { return false } d := cpu.INT.CheckINT() @@ -231,6 +240,7 @@ func (cpu *CPU) tryInterrupt() bool { cpu.SP -= 2 cpu.writeU16(cpu.SP, cpu.PC) cpu.PC = 0x0038 + cpu.IFF1 = false return true case 2: if n := len(d); n != 1 { @@ -242,7 +252,9 @@ func (cpu *CPU) tryInterrupt() bool { cpu.HALT = false cpu.SP -= 2 cpu.writeU16(cpu.SP, cpu.PC) - cpu.PC = toU16(d[0]&0xfe, cpu.IR.Hi) + // The LSB of interruption data is ignored in IM 2 + cpu.PC = cpu.readU16(toU16(d[0]&0xfe, cpu.IR.Hi)) + cpu.IFF1 = false return true } // interrupt with IM 0 @@ -252,9 +264,10 @@ func (cpu *CPU) tryInterrupt() bool { } cpu.HALT = false savedMemory := cpu.Memory - cpu.Memory = newIm0data(cpu.PC, d) + cpu.Memory = newIm0data(cpu.PC, d, savedMemory) cpu.executeOne() cpu.Memory = savedMemory + cpu.IFF1 = false return true } diff --git a/cpu_test.go b/cpu_test.go index ffc4699..689a69e 100644 --- a/cpu_test.go +++ b/cpu_test.go @@ -1,7 +1,10 @@ package z80 import ( + "context" + "fmt" "testing" + "time" "github.com/google/go-cmp/cmp" ) @@ -106,3 +109,357 @@ type tReg struct { Label string Code int } + +type tINT struct { + data []uint8 + reti bool +} + +func (tint *tINT) interrupt(d ...uint8) { + tint.data = d + tint.reti = false +} + +func (tint *tINT) CheckINT() []uint8 { + v := tint.data + if v != nil { + tint.data = nil + } + return v +} + +func (tint *tINT) ReturnINT() { + tint.reti = true +} + +func testIM0(t *testing.T, n uint8) { + var ( + addr uint16 = uint16(n) * 8 + code uint8 = 0xC7 + n*0x08 + ) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + tint := &tINT{} + cpu := &CPU{ + States: States{SPR: SPR{PC: 0x0100}, IM: 1}, + Memory: MapMemory{}. + // HALT + Put(0x0000, 0x76). + // EI ; RETI + Put(addr, + 0xfb, + 0xed, 0x4d). + // IM 0 ; EI ; HALT ; HALT (for return) + Put(0x0100, + 0xed, 0x46, + 0xfb, + 0x76, + 0x76, + ), + IO: &tForbiddenIO{}, + INT: tint, + } + + // Start the program and HALT at 0x0102 + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0103 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x103, cpu.PC) + } + if cpu.IM != 0 { + t.Fatalf("unexpected interrupt mode: want=0 got=%d", cpu.IM) + } + + // Interrupt with IM 0 + tint.interrupt(code) + cpu.Step() + if cpu.PC != addr { + t.Fatalf("RST 38H not work: want=%04X got=%04X", addr, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 is true, unexpectedly") + } + + // Return from the interruption. + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0104 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x104, cpu.PC) + } + if !cpu.IFF1 { + t.Fatal("IFF1 is false, unexpectedly") + } + if !tint.reti { + t.Fatalf("RETI is not processed, unexpectedly") + } +} + +func TestInterruptIM0(t *testing.T) { + for i := 0; i < 8; i++ { + t.Run(fmt.Sprintf("RST %02XH", i*8), func(t *testing.T) { + testIM0(t, uint8(i)) + }) + } +} + +func TestInterruptIM1(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + tint := &tINT{} + cpu := &CPU{ + States: States{SPR: SPR{PC: 0x0100}, IM: 0}, + Memory: MapMemory{}. + // HALT + Put(0x0000, 0x76). + // EI ; RETI + Put(0x0038, + 0xfb, + 0xed, 0x4d). + // IM 1 ; EI ; HALT + Put(0x0100, + 0xed, 0x56, + 0xfb, + 0x76, + ), + IO: &tForbiddenIO{}, + INT: tint, + } + + // Start the program and HALT at 0x0102 + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0103 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x103, cpu.PC) + } + if cpu.IM != 1 { + t.Fatalf("unexpected interrupt mode: want=1 got=%d", cpu.IM) + } + + // Interrupt with IM 1: with dummy data + tint.interrupt(0) + cpu.Step() + if cpu.PC != 0x0038 { + t.Fatalf("IM 1 interruption not work: want=%04X got=%04X", 0x0038, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 is true, unexpectedly") + } + + // Return from the interruption. + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0103 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x103, cpu.PC) + } + if !cpu.IFF1 { + t.Fatal("IFF1 is false, unexpectedly") + } + if !tint.reti { + t.Fatalf("RETI is not processed, unexpectedly") + } +} + +func testIM2(t *testing.T, addr uint16) { + hi := uint8(addr >> 8) + lo := uint8(addr) + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + tint := &tINT{} + cpu := &CPU{ + States: States{SPR: SPR{PC: 0x0100}, IM: 0}, + Memory: MapMemory{}. + // HALT + Put(0x0000, 0x76). + // EI ; RETI + Put(0x00C0, + 0xfb, 0xed, 0x4d). + // IM 2 ; LD A, 20H ; LD I, A ; IM 1 ; EI ; HALT + Put(0x0100, + 0xed, 0x5e, + 0x3e, hi, + 0xed, 0x47, + 0xfb, + 0x76, + ). + // vector + Put(addr-2, 0, 0, 0xc0, 0x00, 0, 0), + IO: &tForbiddenIO{}, + INT: tint, + } + + // Start the program and HALT at 0x0102 + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0107 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x107, cpu.PC) + } + if cpu.IM != 2 { + t.Fatalf("unexpected interrupt mode: want=2 got=%d", cpu.IM) + } + + // Interrupt with IM 1: with dummy empty byte array. + tint.interrupt(lo) + cpu.Step() + if cpu.PC != 0x00C0 { + t.Fatalf("IM 2 interruption not work: want=%04X got=%04X", 0x00C0, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 is true, unexpectedly") + } + + // Return from the interruption. + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0107 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x107, cpu.PC) + } + if !cpu.IFF1 { + t.Fatal("IFF1 is false, unexpectedly") + } + if !tint.reti { + t.Fatalf("RETI is not processed, unexpectedly") + } +} + +func TestInterruptIM2(t *testing.T) { + for _, addr := range []uint16{ + 0x3000, + 0x3004, + 0x3080, + 0x4080, + 0x40c0, + // odd values are invalid because of restriction of IM 2 + } { + t.Run(fmt.Sprintf("IM 2 with %04X", addr), func(t *testing.T) { + testIM2(t, addr) + }) + } +} + +type tNMI struct { + data bool + retn bool +} + +func (tnmi *tNMI) interrupt() { + tnmi.data = true + tnmi.retn = false +} + +func (tnmi *tNMI) CheckNMI() bool { + v := tnmi.data + if v { + tnmi.data = false + } + return v +} + +func (tnmi *tNMI) ReturnNMI() { + tnmi.retn = true +} + +func testNMI(t *testing.T, iff1 bool) (*CPU, *tINT) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + var tint tINT + var tnmi tNMI + var diOrEi uint8 = 0xf3 // DI + if iff1 { + diOrEi = 0xfb + } + cpu := &CPU{ + States: States{SPR: SPR{PC: 0x0100}, IM: 0}, + Memory: MapMemory{}. + // HALT + Put(0x0000, 0x76). + Put(0x0038, 0x76). + // RETN + Put(0x0066, + 0xed, 0x45). + // IM 1 ; DI ; HALT + Put(0x0100, + 0xed, 0x56, + diOrEi, + 0x76, + ), + IO: &tForbiddenIO{}, + INT: &tint, + NMI: &tnmi, + } + + // Start the program and HALT at 0x0102 + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0103 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x103, cpu.PC) + } + if cpu.IM != 1 { + t.Fatalf("unexpected interrupt mode: want=%d got=%d", 1, cpu.IM) + } + + // Interrupt with NMI + tnmi.interrupt() + cpu.Step() + if cpu.PC != 0x0066 { + t.Fatalf("NMI interruption not work: want=%04X got=%04X", 0x0066, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 should be false in NMI") + } + + // Return from the interruption. + if err := cpu.Run(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cpu.PC != 0x0103 { + t.Fatalf("unexpected PC: want=%04X got=%04X", 0x103, cpu.PC) + } + if cpu.IFF1 != iff1 { + t.Fatalf("unexpected IFF1 after NMI: want=%t got=%t", iff1, cpu.IFF1) + } + if !tnmi.retn { + t.Fatalf("RETN is not processed, unexpectedly") + } + + return cpu, &tint +} + +func TestInterruptNMI(t *testing.T) { + t.Run("DI", func(t *testing.T) { + cpu, tint := testNMI(t, false) + // Try to interrupt with IM 1: should be failed. + tint.interrupt(0) + cpu.Step() + if cpu.PC != 0x0103 { + t.Fatalf("IM 1 interruption should be failed: want=%04X got=%04X", 0x0103, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 is true, unexpectedly") + } + }) + + t.Run("EI", func(t *testing.T) { + cpu, tint := testNMI(t, true) + // Try to interrupt with IM 1: should be succeeded + tint.interrupt(0) + cpu.Step() + if cpu.PC != 0x0038 { + t.Fatalf("IM 1 interruption should be failed: want=%04X got=%04X", 0x0038, cpu.PC) + } + if cpu.IFF1 { + t.Fatal("IFF1 is true, unexpectedly") + } + }) +} diff --git a/op_callret.go b/op_callret.go index 16fcec6..5fddb3e 100644 --- a/op_callret.go +++ b/op_callret.go @@ -20,6 +20,9 @@ func oopRETN(cpu *CPU) { cpu.IFF1 = cpu.IFF2 cpu.InNMI = false + if cpu.NMI != nil { + cpu.NMI.ReturnNMI() + } } func oopRET(cpu *CPU) { diff --git a/z80.go b/z80.go index f80f862..6da41a6 100644 --- a/z80.go +++ b/z80.go @@ -98,6 +98,9 @@ type INT interface { type NMI interface { // CheckNMI should return true if non-maskable interruption made. CheckNMI() bool + + // ReturnNMI is called when "RETN" op is executed. + ReturnNMI() } // InterruptMonitor monitors interruptions.