first public release
This commit is contained in:
52
alu16_flags_test.go
Normal file
52
alu16_flags_test.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Verify 16-bit ADD/ADC/SBC HL,rr flags, XY from high byte, and MEMPTR=HL+1
|
||||||
|
func TestADD_HL_rr_FlagsAndMEMPTR(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetHL(0x0FFF)
|
||||||
|
cpu.SetBC(0x0001)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x09) // ADD HL,BC
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x1000), "ADD HL,BC result")
|
||||||
|
assertFlag(t, cpu, FLAG_H, true, "H set on carry from bit11")
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N cleared")
|
||||||
|
assertFlag(t, cpu, FLAG_C, false, "C not set")
|
||||||
|
// X/Y from high byte of result (0x10 -> both X and Y clear)
|
||||||
|
assertFlag(t, cpu, FLAG_Y, false, "Y from high byte should be clear for 0x10")
|
||||||
|
assertFlag(t, cpu, FLAG_X, false, "X from high byte should be clear for 0x10")
|
||||||
|
assertEq(t, cpu.MEMPTR, uint16(0x1000-0x0001+1), "MEMPTR=oldHL+1")
|
||||||
|
|
||||||
|
// ADC HL,DE with carry in
|
||||||
|
cpu.SetHL(0x7FFF)
|
||||||
|
cpu.SetDE(0x0000)
|
||||||
|
cpu.SetFlag(FLAG_C, true)
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xED, 0x5A) // ADC HL,DE
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x8000), "ADC HL,DE result")
|
||||||
|
assertFlag(t, cpu, FLAG_S, true, "S from result high bit")
|
||||||
|
assertFlag(t, cpu, FLAG_Z, false, "Z")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "PV overflow on 7FFF+0+1")
|
||||||
|
assertFlag(t, cpu, FLAG_C, false, "C")
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N")
|
||||||
|
assertFlag(t, cpu, FLAG_X, false, "X from high byte 0x80")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, false, "Y from high byte 0x80")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSBC_HL_rr_FlagsAndMEMPTR(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetHL(0x8000)
|
||||||
|
cpu.SP = 0x0001
|
||||||
|
cpu.SetFlag(FLAG_C, true)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0x72) // SBC HL,SP
|
||||||
|
mustStep(t, cpu)
|
||||||
|
// 0x8000 - 0x0001 - 1 = 0x7FFE
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x7FFE), "SBC HL,SP result")
|
||||||
|
assertFlag(t, cpu, FLAG_N, true, "N set on subtract")
|
||||||
|
assertFlag(t, cpu, FLAG_C, false, "No borrow overall")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "Overflow: negative - positive -> positive")
|
||||||
|
// XY from high byte 0x7F (both X and Y set)
|
||||||
|
assertFlag(t, cpu, FLAG_X, true, "X from 0x7F")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, true, "Y from 0x7F")
|
||||||
|
}
|
||||||
35
alu_rotate_accu_basics_test.go
Normal file
35
alu_rotate_accu_basics_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RLCA/RRCA/RLA/RRA: verify they don't change S/Z/PV and check cycles.
|
||||||
|
func TestRotatesOnA_Basics(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.A = 0x81
|
||||||
|
cpu.F = 0xFF // start with flags set so we can see what's cleared
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x07, // RLCA
|
||||||
|
0x0F, // RRCA
|
||||||
|
0x17, // RLA
|
||||||
|
0x1F, // RRA
|
||||||
|
)
|
||||||
|
// RLCA
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "RLCA cycles")
|
||||||
|
// RRCA
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "RRCA cycles")
|
||||||
|
// RLA
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "RLA cycles")
|
||||||
|
// RRA
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "RRA cycles")
|
||||||
|
// For these ops, S/Z/PV are unaffected (per your core they are cleared back where needed).
|
||||||
|
// Quick sanity: ensure no unexpected setting of Z just by rotates
|
||||||
|
// HUMAN : here is bug. A is FF, so Z is true, if nobody touch it, if still true, but test want false
|
||||||
|
//assertFlag(t, cpu, FLAG_Z, false, "Rotates shouldn't set Z spuriously")
|
||||||
|
assertFlag(t, cpu, FLAG_Z, true, "Rotates shouldn't set Z spuriously")
|
||||||
|
}
|
||||||
62
alu_test.go
Normal file
62
alu_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Test ADD A,n sets all flags correctly including undocumented X/Y.
|
||||||
|
func TestADD_A_n_Flags(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// 3E 7F = LD A,0x7F; C6 01 = ADD A,0x01
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x3E, 0x7F, 0xC6, 0x01)
|
||||||
|
mustStep(t, cpu) // LD A,7F
|
||||||
|
c := mustStep(t, cpu) // ADD A,01
|
||||||
|
|
||||||
|
assertEq(t, cpu.A, byte(0x80), "A after ADD")
|
||||||
|
assertFlag(t, cpu, FLAG_S, true, "S")
|
||||||
|
assertFlag(t, cpu, FLAG_Z, false, "Z")
|
||||||
|
assertFlag(t, cpu, FLAG_H, true, "H half-carry")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "P/V overflow")
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N")
|
||||||
|
assertFlag(t, cpu, FLAG_C, false, "C")
|
||||||
|
// X and Y from result
|
||||||
|
assertFlag(t, cpu, FLAG_X, (cpu.A&FLAG_X) != 0, "X from result")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (cpu.A&FLAG_Y) != 0, "Y from result")
|
||||||
|
|
||||||
|
// Quick timing smoke-check (not strict because conditional)
|
||||||
|
assertEq(t, c, 7, "cycles for ADD A,n should be 7")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CP n sets X/Y from the OPERAND per implementation fixes.
|
||||||
|
func TestCP_n_XYFromOperand(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// A=0x20; FE 08 = CP 0x08 -> result 0x18 (not stored). X/Y should copy from operand 0x08.
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x3E, 0x20, 0xFE, 0x08)
|
||||||
|
mustStep(t, cpu) // LD A,20
|
||||||
|
mustStep(t, cpu) // CP 08
|
||||||
|
|
||||||
|
// For CP, X/Y come from the operand (0x08 has X=1, Y=0).
|
||||||
|
assertFlag(t, cpu, FLAG_X, true, "CP X from operand")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, false, "CP Y from operand")
|
||||||
|
// N must be set for CP
|
||||||
|
assertFlag(t, cpu, FLAG_N, true, "N set for CP")
|
||||||
|
}
|
||||||
|
|
||||||
|
// INC/DEC r affect flags and preserve C; X/Y come from result.
|
||||||
|
func TestINC_DEC_r_Flags(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Set C flag first to ensure INC/DEC don't change it.
|
||||||
|
cpu.SetFlag(FLAG_C, true)
|
||||||
|
|
||||||
|
// 06 7F = LD B,7F; 04 = INC B; 05 = DEC B
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x06, 0x7F, 0x04, 0x05)
|
||||||
|
mustStep(t, cpu) // LD B,7F
|
||||||
|
|
||||||
|
mustStep(t, cpu) // INC B -> 0x80
|
||||||
|
assertEq(t, cpu.B, byte(0x80), "INC B result")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "INC overflow at 7F->80")
|
||||||
|
assertFlag(t, cpu, FLAG_C, true, "C preserved across INC")
|
||||||
|
|
||||||
|
mustStep(t, cpu) // DEC B -> 0x7F
|
||||||
|
assertEq(t, cpu.B, byte(0x7F), "DEC B result")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "DEC overflow at 80->7F")
|
||||||
|
assertFlag(t, cpu, FLAG_C, true, "C preserved across DEC")
|
||||||
|
}
|
||||||
33
bit_ixiy_xy_wz_test.go
Normal file
33
bit_ixiy_xy_wz_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// BIT n,(IX+d)/(IY+d): X/Y must come from MEMPTR high byte, cycles 20 for BIT on indexed (no write-back).
|
||||||
|
func TestBIT_IXIY_Displacement_FlagsAndTiming(t *testing.T) {
|
||||||
|
// IX case
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.IX = 0x3000
|
||||||
|
mem.WriteByte(0x3005, 0x80) // bit 7 set
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xDD, 0xCB, 0x05, 0x7E) // BIT 7,(IX+5)
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
// Z=0, S=1 for bit7 set
|
||||||
|
assertFlag(t, cpu, FLAG_Z, false, "BIT Z")
|
||||||
|
assertFlag(t, cpu, FLAG_S, true, "BIT S for bit7")
|
||||||
|
// X/Y from MEMPTR high byte
|
||||||
|
memptrHi := byte(cpu.MEMPTR >> 8)
|
||||||
|
assertFlag(t, cpu, FLAG_X, (memptrHi&FLAG_X) != 0, "X from MEMPTR high (IX)")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (memptrHi&FLAG_Y) != 0, "Y from MEMPTR high (IX)")
|
||||||
|
assertEq(t, c, 20, "cycles for DDCB BIT (IX+d) should be 20")
|
||||||
|
|
||||||
|
// IY case
|
||||||
|
cpu, mem, _ = testCPU()
|
||||||
|
cpu.IY = 0x4000
|
||||||
|
mem.WriteByte(0x4002, 0x01) // bit 0 set
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xFD, 0xCB, 0x02, 0x46) // BIT 0,(IY+2)
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertFlag(t, cpu, FLAG_Z, false, "BIT Z (IY)")
|
||||||
|
memptrHi = byte(cpu.MEMPTR >> 8)
|
||||||
|
assertFlag(t, cpu, FLAG_X, (memptrHi&FLAG_X) != 0, "X from MEMPTR high (IY)")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (memptrHi&FLAG_Y) != 0, "Y from MEMPTR high (IY)")
|
||||||
|
assertEq(t, c, 20, "cycles for FDCB BIT (IY+d) should be 20")
|
||||||
|
}
|
||||||
19
bit_xy_test.go
Normal file
19
bit_xy_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Highlights blind spot: BIT n,r should update X/Y from the operand (register form).
|
||||||
|
// Current implementation only fixes the (HL) form, so this test will fail until fixed.
|
||||||
|
func TestBIT_Reg_SetsXYFromOperand(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Load: LD B,0x28 (0010_1000: bit3=1 -> X=1, bit5=1 -> Y=1)
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x06, 0x28, // LD B,28h
|
||||||
|
0xCB, 0x40, // BIT 0,B (arbitrary bit test that does not force Z)
|
||||||
|
)
|
||||||
|
mustStep(t, cpu) // LD
|
||||||
|
mustStep(t, cpu) // BIT 0,B
|
||||||
|
// Expect X/Y to mirror operand (B) on BIT n,r.
|
||||||
|
assertFlag(t, cpu, FLAG_X, true, "BIT n,r should set X from tested register")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, true, "BIT n,r should set Y from tested register")
|
||||||
|
}
|
||||||
36
block_io_flag_derivations_test.go
Normal file
36
block_io_flag_derivations_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// INI/IND/OUTI/OUTD flag sampling: PV mirrors B!=0; N/H follow documented "k = (result + L)" rules.
|
||||||
|
// We do a light assertion on PV and N to avoid over-constraining; your core implements the full rules.
|
||||||
|
func TestINI_OUTI_FlagBehavior_Smoke(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
// Prepare a simple pattern
|
||||||
|
cpu.SetBC(0x0134) // B=1 count, C=port
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
io.inVals[0x0134] = 0x7F
|
||||||
|
|
||||||
|
// INI: read from port into (HL), HL++, B--, PV mirrors B!=0
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xA2) // INI
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if mem.ReadByte(0x4000) != 0x7F {
|
||||||
|
t.Fatalf("INI did not store input into memory")
|
||||||
|
}
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x4001), "HL++ after INI")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0x0034), "B-- after INI")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, false, "PV reflects B!=0 (now zero)")
|
||||||
|
|
||||||
|
// Re-arm for OUTI with two bytes to exercise PV true then false
|
||||||
|
cpu, mem, _ = testCPU()
|
||||||
|
cpu.SetBC(0x0234) // B=2
|
||||||
|
cpu.SetHL(0x5000)
|
||||||
|
mem.WriteByte(0x5000, 0x80) // MSB set to check N behavior via 'k' rule implementation
|
||||||
|
mem.WriteByte(0x5001, 0x00)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xA3) // OUTI (first iteration)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "PV true when B!=0")
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xED, 0xA3) // OUTI (second iteration)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertFlag(t, cpu, FLAG_PV, false, "PV false when B==0")
|
||||||
|
}
|
||||||
44
block_io_outi_otir_tests.go
Normal file
44
block_io_outi_otir_tests.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// OUTI/OTIR: verify port, data, register updates, and timing totals.
|
||||||
|
func TestOUTI_Basic(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
cpu.SetBC(0x1234)
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
mem.WriteByte(0x4000, 0x5A)
|
||||||
|
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xA3) // OUTI
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
if v, ok := io.lastOut[0x1234]; !ok || v != 0x5A {
|
||||||
|
t.Fatalf("OUTI wrote %02X to %04X (ok=%v)", v, 0x1234, ok)
|
||||||
|
}
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x4001), "HL++ after OUTI")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0x1134), "B-- after OUTI")
|
||||||
|
assertEq(t, c, 16, "OUTI cycles 16")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOTIR_TwoBytes_CycleSum(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
cpu.SetBC(0x0034) // only B is count; C is port low
|
||||||
|
cpu.SetHL(0x5000)
|
||||||
|
mem.WriteByte(0x5000, 0x01)
|
||||||
|
mem.WriteByte(0x5001, 0x02)
|
||||||
|
// set upper port byte via B after first OUTI: but OUTI decrements B.
|
||||||
|
// We'll fix the port by using C only (0x0034) and ignore high byte changes.
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xB3) // OTIR
|
||||||
|
total := 0
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
total += mustStep(t, cpu)
|
||||||
|
if cpu.GetBC() == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Timing: 21 + 16 = 37 for two bytes
|
||||||
|
if total != 37 {
|
||||||
|
t.Fatalf("OTIR total cycles got %d want 37", total)
|
||||||
|
}
|
||||||
|
// Last write should be second byte to port 0x0034 (C-only; high byte varies via B but test harness stores by full BC)
|
||||||
|
_ = io // Can't assert exact port with lastOut map if high byte changes; presence is enough here.
|
||||||
|
}
|
||||||
454
cb_opcodes.go
Normal file
454
cb_opcodes.go
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// ExecuteCBOpcode executes a CB-prefixed opcode and returns the number of T-states used
|
||||||
|
func (cpu *CPU) ExecuteCBOpcode(opcode byte) int {
|
||||||
|
// Handle rotate and shift instructions (0x00-0x3F)
|
||||||
|
if opcode <= 0x3F {
|
||||||
|
// Determine operation type from opcode bits 3-5
|
||||||
|
opType := (opcode >> 3) & 0x07
|
||||||
|
// Determine register from opcode bits 0-2
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Handle (HL) special case
|
||||||
|
if reg == 6 {
|
||||||
|
addr := cpu.GetHL()
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
switch opType {
|
||||||
|
case 0: // RLC
|
||||||
|
result := cpu.rlc(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 1: // RRC
|
||||||
|
result := cpu.rrc(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 2: // RL
|
||||||
|
result := cpu.rl(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 3: // RR
|
||||||
|
result := cpu.rr(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 4: // SLA
|
||||||
|
result := cpu.sla(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 5: // SRA
|
||||||
|
result := cpu.sra(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 6: // SLL (Undocumented)
|
||||||
|
result := cpu.sll(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
case 7: // SRL
|
||||||
|
result := cpu.srl(value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle regular registers
|
||||||
|
switch opType {
|
||||||
|
case 0: // RLC
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.rlc(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.rlc(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.rlc(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.rlc(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.rlc(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.rlc(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.rlc(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 1: // RRC
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.rrc(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.rrc(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.rrc(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.rrc(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.rrc(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.rrc(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.rrc(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 2: // RL
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.rl(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.rl(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.rl(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.rl(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.rl(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.rl(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.rl(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 3: // RR
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.rr(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.rr(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.rr(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.rr(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.rr(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.rr(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.rr(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 4: // SLA
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.sla(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.sla(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.sla(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.sla(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.sla(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.sla(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.sla(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 5: // SRA
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.sra(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.sra(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.sra(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.sra(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.sra(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.sra(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.sra(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 6: // SLL (Undocumented)
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.sll(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.sll(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.sll(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.sll(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.sll(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.sll(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.sll(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
case 7: // SRL
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.srl(cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.srl(cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.srl(cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.srl(cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.srl(cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.srl(cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.srl(cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bit test instructions (0x40-0x7F)
|
||||||
|
if opcode >= 0x40 && opcode <= 0x7F {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Handle (HL) special case
|
||||||
|
if reg == 6 {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
cpu.bitMem(bitNum, value, byte(cpu.MEMPTR>>8))
|
||||||
|
return 12
|
||||||
|
} else {
|
||||||
|
// Handle regular registers
|
||||||
|
var regValue byte
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
regValue = cpu.B
|
||||||
|
case 1:
|
||||||
|
regValue = cpu.C
|
||||||
|
case 2:
|
||||||
|
regValue = cpu.D
|
||||||
|
case 3:
|
||||||
|
regValue = cpu.E
|
||||||
|
case 4:
|
||||||
|
regValue = cpu.H
|
||||||
|
case 5:
|
||||||
|
regValue = cpu.L
|
||||||
|
case 7:
|
||||||
|
regValue = cpu.A
|
||||||
|
}
|
||||||
|
cpu.bit(bitNum, regValue)
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reset bit instructions (0x80-0xBF)
|
||||||
|
if opcode >= 0x80 && opcode <= 0xBF {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Handle (HL) special case
|
||||||
|
if reg == 6 {
|
||||||
|
addr := cpu.GetHL()
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
result := cpu.res(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
} else {
|
||||||
|
// Handle regular registers
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.res(bitNum, cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.res(bitNum, cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.res(bitNum, cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.res(bitNum, cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.res(bitNum, cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.res(bitNum, cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.res(bitNum, cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle set bit instructions (0xC0-0xFF)
|
||||||
|
if opcode >= 0xC0 {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Handle (HL) special case
|
||||||
|
if reg == 6 {
|
||||||
|
addr := cpu.GetHL()
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
result := cpu.set(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
return 15
|
||||||
|
} else {
|
||||||
|
// Handle regular registers
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = cpu.set(bitNum, cpu.B)
|
||||||
|
case 1:
|
||||||
|
cpu.C = cpu.set(bitNum, cpu.C)
|
||||||
|
case 2:
|
||||||
|
cpu.D = cpu.set(bitNum, cpu.D)
|
||||||
|
case 3:
|
||||||
|
cpu.E = cpu.set(bitNum, cpu.E)
|
||||||
|
case 4:
|
||||||
|
cpu.H = cpu.set(bitNum, cpu.H)
|
||||||
|
case 5:
|
||||||
|
cpu.L = cpu.set(bitNum, cpu.L)
|
||||||
|
case 7:
|
||||||
|
cpu.A = cpu.set(bitNum, cpu.A)
|
||||||
|
}
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unimplemented opcode
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
|
||||||
|
// rlc rotates a byte left circular
|
||||||
|
func (cpu *CPU) rlc(value byte) byte {
|
||||||
|
result := (value << 1) | (value >> 7)
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x80) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// rrc rotates a byte right circular
|
||||||
|
func (cpu *CPU) rrc(value byte) byte {
|
||||||
|
result := (value >> 1) | (value << 7)
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x01) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// rl rotates a byte left through carry
|
||||||
|
func (cpu *CPU) rl(value byte) byte {
|
||||||
|
oldCarry := cpu.GetFlag(FLAG_C)
|
||||||
|
result := (value << 1)
|
||||||
|
if oldCarry {
|
||||||
|
result |= 0x01
|
||||||
|
}
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x80) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// rr rotates a byte right through carry
|
||||||
|
func (cpu *CPU) rr(value byte) byte {
|
||||||
|
oldCarry := cpu.GetFlag(FLAG_C)
|
||||||
|
result := (value >> 1)
|
||||||
|
if oldCarry {
|
||||||
|
result |= 0x80
|
||||||
|
}
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x01) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sla shifts a byte left arithmetic
|
||||||
|
func (cpu *CPU) sla(value byte) byte {
|
||||||
|
result := value << 1
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x80) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sra shifts a byte right arithmetic
|
||||||
|
func (cpu *CPU) sra(value byte) byte {
|
||||||
|
result := (value >> 1) | (value & 0x80)
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x01) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// sll shifts a byte left logical (Undocumented)
|
||||||
|
func (cpu *CPU) sll(value byte) byte {
|
||||||
|
result := (value << 1) | 0x01
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x80) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// srl shifts a byte right logical
|
||||||
|
func (cpu *CPU) srl(value byte) byte {
|
||||||
|
result := value >> 1
|
||||||
|
cpu.SetFlagState(FLAG_C, (value&0x01) != 0)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.UpdateSZXYPVFlags(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// bit tests a bit in a byte
|
||||||
|
func (cpu *CPU) bit(bitNum uint, value byte) {
|
||||||
|
mask := byte(1 << bitNum)
|
||||||
|
result := value & mask
|
||||||
|
cpu.SetFlagState(FLAG_Z, result == 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (value&(1<<5)) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_X, (value&(1<<3)) != 0)
|
||||||
|
cpu.SetFlag(FLAG_H, true)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
if result == 0 {
|
||||||
|
cpu.SetFlag(FLAG_PV, true)
|
||||||
|
cpu.ClearFlag(FLAG_S)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(FLAG_PV)
|
||||||
|
// For BIT 7, S flag is set to the value of bit 7
|
||||||
|
if bitNum == 7 {
|
||||||
|
cpu.SetFlagState(FLAG_S, (value&0x80) != 0)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(FLAG_S)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// res resets a bit in a byte
|
||||||
|
func (cpu *CPU) res(bitNum uint, value byte) byte {
|
||||||
|
mask := byte(^(1 << bitNum))
|
||||||
|
return value & mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitMem tests a bit in a byte for memory references
|
||||||
|
func (cpu *CPU) bitMem(bitNum uint, value byte, addrHi byte) {
|
||||||
|
mask := byte(1 << bitNum)
|
||||||
|
result := value & mask
|
||||||
|
cpu.SetFlagState(FLAG_Z, result == 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (addrHi&(1<<5)) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_X, (addrHi&(1<<3)) != 0)
|
||||||
|
cpu.SetFlag(FLAG_H, true)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
if result == 0 {
|
||||||
|
cpu.SetFlag(FLAG_PV, true)
|
||||||
|
cpu.ClearFlag(FLAG_S)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(FLAG_PV)
|
||||||
|
// For BIT 7, S flag is set to the value of bit 7
|
||||||
|
if bitNum == 7 {
|
||||||
|
cpu.SetFlagState(FLAG_S, (value&0x80) != 0)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(FLAG_S)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set sets a bit in a byte
|
||||||
|
func (cpu *CPU) set(bitNum uint, value byte) byte {
|
||||||
|
mask := byte(1 << bitNum)
|
||||||
|
return value | mask
|
||||||
|
}
|
||||||
34
cb_prefix_test.go
Normal file
34
cb_prefix_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// BIT n,(HL) must set X/Y from MEMPTR high byte (implementation note in prefix_cb.go).
|
||||||
|
func TestBIT_HL_XYFromMEMPTRHigh(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Put value at 0x4000, set HL=0x4000, then CB 7E = BIT 7,(HL)
|
||||||
|
mem.WriteByte(0x4000, 0x80) // bit 7 set
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xCB, 0x7E)
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
|
||||||
|
// For BIT 7,(HL), Z=0, S=1, PV mirrors Z, H=1, N=0.
|
||||||
|
assertFlag(t, cpu, FLAG_Z, false, "BIT Z")
|
||||||
|
// In our implementation, executeCB overrides X/Y with MEMPTR high.
|
||||||
|
memptrHi := byte(cpu.MEMPTR >> 8)
|
||||||
|
assertFlag(t, cpu, FLAG_X, (memptrHi&FLAG_X) != 0, "X from MEMPTR high")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (memptrHi&FLAG_Y) != 0, "Y from MEMPTR high")
|
||||||
|
|
||||||
|
assertEq(t, c, 12, "cycles for BIT n,(HL)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RLC (HL): verify write-back and timing 15 cycles.
|
||||||
|
func TestRLC_HL_WriteBackAndTiming(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetHL(0x2000)
|
||||||
|
mem.WriteByte(0x2000, 0x81) // 1000 0001 -> RLC -> 0000 0011, C=1
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xCB, 0x06) // RLC (HL)
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, mem.ReadByte(0x2000), byte(0x03), "RLC(HL) write-back")
|
||||||
|
assertFlag(t, cpu, FLAG_C, true, "C set after RLC")
|
||||||
|
assertEq(t, c, 15, "cycles for RLC (HL)")
|
||||||
|
}
|
||||||
34
cpir_test.go
Normal file
34
cpir_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// CPIR/CPDR repeat-cycle accounting: repeating step is 21 cycles, final match step is 16.
|
||||||
|
// We run steps until Z=1 or BC=0 and assert total cycles and final state.
|
||||||
|
func TestCPIR_CycleProfile_AndBehavior(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Setup HL points to 0x4000: [0xAA, 0x55]; A=0x55; BC=2
|
||||||
|
mem.WriteByte(0x4000, 0xAA)
|
||||||
|
mem.WriteByte(0x4001, 0x55)
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
cpu.SetBC(2)
|
||||||
|
cpu.A = 0x55
|
||||||
|
|
||||||
|
// ED B1 = CPIR
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xB1)
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
for {
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
total += c
|
||||||
|
if cpu.GetFlag(FLAG_Z) || cpu.GetBC() == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After CPIR completes: should stop with Z=1, HL=0x4002, BC=0
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x4002), "HL after CPIR")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0), "BC after CPIR")
|
||||||
|
assertFlag(t, cpu, FLAG_Z, true, "Z set when match found")
|
||||||
|
// Timing: 21 (first repeat) + 16 (final) = 37
|
||||||
|
assertEq(t, total, 37, "CPIR total cycles for one repeat + final")
|
||||||
|
}
|
||||||
58
daa_test.go
Normal file
58
daa_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// A focused set of DAA edge cases (after ADD and after SUB).
|
||||||
|
// These are well-known tricky corners and good regression targets.
|
||||||
|
func TestDAA_AfterAdd_LowerNibbleOverflow(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// A=09h; ADD A,01h => 0Ah; DAA => 10h, C=0, H adjusted, N=0
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x3E, 0x09, // LD A,09
|
||||||
|
0xC6, 0x01, // ADD A,01
|
||||||
|
0x27, // DAA
|
||||||
|
)
|
||||||
|
mustStep(t, cpu) // LD
|
||||||
|
mustStep(t, cpu) // ADD
|
||||||
|
mustStep(t, cpu) // DAA
|
||||||
|
if cpu.A != 0x10 {
|
||||||
|
t.Errorf("DAA after 09+01 => got A=%02X want 10", cpu.A)
|
||||||
|
}
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N cleared after DAA on addition")
|
||||||
|
assertFlag(t, cpu, FLAG_C, false, "C should be 0 for 10h here")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAA_AfterAdd_UpperAdjustSetsCarry(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// A=0x90; ADD A,0x90 => 0x20 (with carry in BCD terms) ; DAA should add 0x60 -> A=0x80, C=1
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x3E, 0x90, // LD A,90
|
||||||
|
0xC6, 0x90, // ADD A,90 -> A=0x20 (binary), needs +0x60
|
||||||
|
0x27, // DAA
|
||||||
|
)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.A != 0x80 {
|
||||||
|
t.Errorf("DAA after 90+90 => got A=%02X want 80", cpu.A)
|
||||||
|
}
|
||||||
|
assertFlag(t, cpu, FLAG_C, true, "DAA should set C when adding 0x60")
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N cleared on add-style DAA")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDAA_AfterSub_HexToDecimalBorrow(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// A=0x10; SUB 0x01 => 0x0F; N=1; DAA should subtract 0x06 -> A=0x09 (BCD: 10 - 1 = 09)
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x3E, 0x10, // LD A,10
|
||||||
|
0xD6, 0x01, // SUB 01
|
||||||
|
0x27, // DAA
|
||||||
|
)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.A != 0x09 {
|
||||||
|
t.Errorf("DAA after 10-01 => got A=%02X want 09", cpu.A)
|
||||||
|
}
|
||||||
|
assertFlag(t, cpu, FLAG_N, true, "N remains set for subtraction DAA path")
|
||||||
|
}
|
||||||
61
dd_fd_prefix_test.go
Normal file
61
dd_fd_prefix_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Test LD r,(IX+d) and LD (IX+d),r timing and behavior.
|
||||||
|
func TestIndexedLoadsIX(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.IX = 0x3000
|
||||||
|
mem.WriteByte(0x3005, 0xAB)
|
||||||
|
|
||||||
|
// DD 46 05 = LD B,(IX+5)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xDD, 0x46, 0x05)
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.B, byte(0xAB), "LD B,(IX+5)")
|
||||||
|
assertEq(t, c, 19, "cycles for LD r,(IX+d)")
|
||||||
|
|
||||||
|
// DD 70 05 = LD (IX+5),B
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xDD, 0x70, 0x05)
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, mem.ReadByte(0x3005), cpu.B, "LD (IX+5),B")
|
||||||
|
assertEq(t, c, 19, "cycles for LD (IX+d),r")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undocumented IXH/IXL access: LD IXH,n; LD IXL,n; ADD A,IXH; XOR IXL
|
||||||
|
func TestIXHIXL_Undocumented(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// DD 26 12 = LD IXH,12; DD 2E 34 = LD IXL,34
|
||||||
|
// DD 84 = ADD A,IXH (ADD A,H with DD prefix)
|
||||||
|
// DD AE = XOR (HL)?? No, for XOR r it's 0xAE for (HL); use DD A5 = AND L ?
|
||||||
|
// We'll do: LD A,0x01; DD 84 (ADD A,IXH) -> 0x13; DD B5 isn't valid; use DD A5 for AND IXL via "AND L" with DD -> IXL.
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0xDD, 0x26, 0x12,
|
||||||
|
0xDD, 0x2E, 0x34,
|
||||||
|
0x3E, 0x01,
|
||||||
|
0xDD, 0x84, // ADD A,IXH
|
||||||
|
0xDD, 0xA5, // AND IXL (AND L with DD prefix)
|
||||||
|
)
|
||||||
|
mustStep(t, cpu) // LD IXH,12
|
||||||
|
mustStep(t, cpu) // LD IXL,34
|
||||||
|
mustStep(t, cpu) // LD A,01
|
||||||
|
mustStep(t, cpu) // ADD A,IXH -> 0x13
|
||||||
|
assertEq(t, cpu.A, byte(0x13), "ADD A,IXH")
|
||||||
|
mustStep(t, cpu) // AND IXL (0x34) -> 0x10
|
||||||
|
assertEq(t, cpu.A, byte(0x10), "AND IXL")
|
||||||
|
// XY flags come from A after logical ops per implementation
|
||||||
|
assertFlag(t, cpu, FLAG_X, (cpu.A&FLAG_X) != 0, "X from A after AND")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (cpu.A&FLAG_Y) != 0, "Y from A after AND")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IY mirrors IX tests.
|
||||||
|
func TestIndexedLoadsIY(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.IY = 0x4000
|
||||||
|
mem.WriteByte(0x4002, 0x55)
|
||||||
|
|
||||||
|
// FD 4E 02 = LD C,(IY+2)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xFD, 0x4E, 0x02)
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.C, byte(0x55), "LD C,(IY+2)")
|
||||||
|
assertEq(t, c, 19, "cycles for LD r,(IY+d)")
|
||||||
|
}
|
||||||
381
dd_opcodes.go
Normal file
381
dd_opcodes.go
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// ExecuteDDOpcode executes a DD-prefixed opcode and returns the number of T-states used
|
||||||
|
func (cpu *CPU) ExecuteDDOpcode(opcode byte) int {
|
||||||
|
switch opcode {
|
||||||
|
// Load instructions
|
||||||
|
case 0x09: // ADD IX, BC
|
||||||
|
oldIX := cpu.IX
|
||||||
|
result := cpu.add16IX(cpu.IX, cpu.GetBC())
|
||||||
|
cpu.MEMPTR = oldIX + 1
|
||||||
|
cpu.IX = result
|
||||||
|
return 15
|
||||||
|
case 0x19: // ADD IX, DE
|
||||||
|
oldIX := cpu.IX
|
||||||
|
result := cpu.add16IX(cpu.IX, cpu.GetDE())
|
||||||
|
cpu.MEMPTR = oldIX + 1
|
||||||
|
cpu.IX = result
|
||||||
|
return 15
|
||||||
|
case 0x21: // LD IX, nn
|
||||||
|
cpu.IX = cpu.ReadImmediateWord()
|
||||||
|
return 14
|
||||||
|
case 0x22: // LD (nn), IX
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.IX)
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x23: // INC IX
|
||||||
|
cpu.IX++
|
||||||
|
return 10
|
||||||
|
case 0x24: // INC IXH
|
||||||
|
cpu.SetIXH(cpu.inc8(cpu.GetIXH()))
|
||||||
|
return 8
|
||||||
|
case 0x25: // DEC IXH
|
||||||
|
cpu.SetIXH(cpu.dec8(cpu.GetIXH()))
|
||||||
|
return 8
|
||||||
|
case 0x26: // LD IXH, n
|
||||||
|
cpu.SetIXH(cpu.ReadImmediateByte())
|
||||||
|
return 11
|
||||||
|
case 0x29: // ADD IX, IX
|
||||||
|
oldIX := cpu.IX
|
||||||
|
result := cpu.add16IX(cpu.IX, cpu.IX)
|
||||||
|
cpu.MEMPTR = oldIX + 1
|
||||||
|
cpu.IX = result
|
||||||
|
return 15
|
||||||
|
case 0x2A: // LD IX, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.IX = cpu.Memory.ReadWord(addr)
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x2B: // DEC IX
|
||||||
|
cpu.IX--
|
||||||
|
return 10
|
||||||
|
case 0x2C: // INC IXL
|
||||||
|
cpu.SetIXL(cpu.inc8(cpu.GetIXL()))
|
||||||
|
return 8
|
||||||
|
case 0x2D: // DEC IXL
|
||||||
|
cpu.SetIXL(cpu.dec8(cpu.GetIXL()))
|
||||||
|
return 8
|
||||||
|
case 0x2E: // LD IXL, n
|
||||||
|
cpu.SetIXL(cpu.ReadImmediateByte())
|
||||||
|
return 11
|
||||||
|
case 0x34: // INC (IX+d)
|
||||||
|
return cpu.executeIncDecIndexed(true)
|
||||||
|
case 0x35: // DEC (IX+d)
|
||||||
|
return cpu.executeIncDecIndexed(false)
|
||||||
|
case 0x36: // LD (IX+d), n
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
value := cpu.ReadImmediateByte()
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
cpu.Memory.WriteByte(addr, value)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
case 0x39: // ADD IX, SP
|
||||||
|
oldIX := cpu.IX
|
||||||
|
result := cpu.add16IX(cpu.IX, cpu.SP)
|
||||||
|
cpu.MEMPTR = oldIX + 1
|
||||||
|
cpu.IX = result
|
||||||
|
return 15
|
||||||
|
case 0x40: // LD B,B
|
||||||
|
return 8
|
||||||
|
|
||||||
|
// Load register from IX register
|
||||||
|
case 0x44: // LD B, IXH
|
||||||
|
cpu.B = cpu.GetIXH()
|
||||||
|
return 8
|
||||||
|
case 0x45: // LD B, IXL
|
||||||
|
cpu.B = cpu.GetIXL()
|
||||||
|
return 8
|
||||||
|
case 0x46: // LD B, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(0)
|
||||||
|
case 0x4C: // LD C, IXH
|
||||||
|
cpu.C = cpu.GetIXH()
|
||||||
|
return 8
|
||||||
|
case 0x4D: // LD C, IXL
|
||||||
|
cpu.C = cpu.GetIXL()
|
||||||
|
return 8
|
||||||
|
case 0x4E: // LD C, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(1)
|
||||||
|
case 0x54: // LD D, IXH
|
||||||
|
cpu.D = cpu.GetIXH()
|
||||||
|
return 8
|
||||||
|
case 0x55: // LD D, IXL
|
||||||
|
cpu.D = cpu.GetIXL()
|
||||||
|
return 8
|
||||||
|
case 0x56: // LD D, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(2)
|
||||||
|
case 0x5C: // LD E, IXH
|
||||||
|
cpu.E = cpu.GetIXH()
|
||||||
|
return 8
|
||||||
|
case 0x5D: // LD E, IXL
|
||||||
|
cpu.E = cpu.GetIXL()
|
||||||
|
return 8
|
||||||
|
case 0x5E: // LD E, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(3)
|
||||||
|
case 0x60: // LD IXH, B
|
||||||
|
cpu.SetIXH(cpu.B)
|
||||||
|
return 8
|
||||||
|
case 0x61: // LD IXH, C
|
||||||
|
cpu.SetIXH(cpu.C)
|
||||||
|
return 8
|
||||||
|
case 0x62: // LD IXH, D
|
||||||
|
cpu.SetIXH(cpu.D)
|
||||||
|
return 8
|
||||||
|
case 0x63: // LD IXH, E
|
||||||
|
cpu.SetIXH(cpu.E)
|
||||||
|
return 8
|
||||||
|
case 0x64: // LD IXH, IXH
|
||||||
|
// No operation needed
|
||||||
|
return 8
|
||||||
|
case 0x65: // LD IXH, IXL
|
||||||
|
cpu.SetIXH(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0x66: // LD H, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(4)
|
||||||
|
case 0x67: // LD IXH, A
|
||||||
|
cpu.SetIXH(cpu.A)
|
||||||
|
return 8
|
||||||
|
case 0x68: // LD IXL, B
|
||||||
|
cpu.SetIXL(cpu.B)
|
||||||
|
return 8
|
||||||
|
case 0x69: // LD IXL, C
|
||||||
|
cpu.SetIXL(cpu.C)
|
||||||
|
return 8
|
||||||
|
case 0x6A: // LD IXL, D
|
||||||
|
cpu.SetIXL(cpu.D)
|
||||||
|
return 8
|
||||||
|
case 0x6B: // LD IXL, E
|
||||||
|
cpu.SetIXL(cpu.E)
|
||||||
|
return 8
|
||||||
|
case 0x6C: // LD IXL, IXH
|
||||||
|
cpu.SetIXL(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0x6D: // LD IXL, IXL
|
||||||
|
// No operation needed
|
||||||
|
return 8
|
||||||
|
case 0x6E: // LD L, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(5)
|
||||||
|
case 0x6F: // LD IXL, A
|
||||||
|
cpu.SetIXL(cpu.A)
|
||||||
|
return 8
|
||||||
|
case 0x70: // LD (IX+d), B
|
||||||
|
return cpu.executeStoreToIndexed(cpu.B)
|
||||||
|
case 0x71: // LD (IX+d), C
|
||||||
|
return cpu.executeStoreToIndexed(cpu.C)
|
||||||
|
case 0x72: // LD (IX+d), D
|
||||||
|
return cpu.executeStoreToIndexed(cpu.D)
|
||||||
|
case 0x73: // LD (IX+d), E
|
||||||
|
return cpu.executeStoreToIndexed(cpu.E)
|
||||||
|
case 0x74: // LD (IX+d), H
|
||||||
|
return cpu.executeStoreToIndexed(cpu.H)
|
||||||
|
case 0x75: // LD (IX+d), L
|
||||||
|
return cpu.executeStoreToIndexed(cpu.L)
|
||||||
|
case 0x77: // LD (IX+d), A
|
||||||
|
return cpu.executeStoreToIndexed(cpu.A)
|
||||||
|
case 0x7C: // LD A, IXH
|
||||||
|
cpu.A = cpu.GetIXH()
|
||||||
|
return 8
|
||||||
|
case 0x7D: // LD A, IXL
|
||||||
|
cpu.A = cpu.GetIXL()
|
||||||
|
return 8
|
||||||
|
case 0x7E: // LD A, (IX+d)
|
||||||
|
return cpu.executeLoadFromIndexed(7)
|
||||||
|
|
||||||
|
// Arithmetic and logic instructions
|
||||||
|
case 0x84: // ADD A, IXH
|
||||||
|
cpu.add8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0x85: // ADD A, IXL
|
||||||
|
cpu.add8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0x86: // ADD A, (IX+d)
|
||||||
|
return cpu.executeALUIndexed(0)
|
||||||
|
case 0x8C: // ADC A, IXH
|
||||||
|
cpu.adc8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0x8D: // ADC A, IXL
|
||||||
|
cpu.adc8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0x8E: // ADC A, (IX+d)
|
||||||
|
return cpu.executeALUIndexed(1)
|
||||||
|
case 0x94: // SUB IXH
|
||||||
|
cpu.sub8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0x95: // SUB IXL
|
||||||
|
cpu.sub8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0x96: // SUB (IX+d)
|
||||||
|
return cpu.executeALUIndexed(2)
|
||||||
|
case 0x9C: // SBC A, IXH
|
||||||
|
cpu.sbc8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0x9D: // SBC A, IXL
|
||||||
|
cpu.sbc8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0x9E: // SBC A, (IX+d)
|
||||||
|
return cpu.executeALUIndexed(3)
|
||||||
|
case 0xA4: // AND IXH
|
||||||
|
cpu.and8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0xA5: // AND IXL
|
||||||
|
cpu.and8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0xA6: // AND (IX+d)
|
||||||
|
return cpu.executeALUIndexed(4)
|
||||||
|
case 0xAC: // XOR IXH
|
||||||
|
cpu.xor8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0xAD: // XOR IXL
|
||||||
|
cpu.xor8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0xAE: // XOR (IX+d)
|
||||||
|
return cpu.executeALUIndexed(5)
|
||||||
|
case 0xB4: // OR IXH
|
||||||
|
cpu.or8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0xB5: // OR IXL
|
||||||
|
cpu.or8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0xB6: // OR (IX+d)
|
||||||
|
return cpu.executeALUIndexed(6)
|
||||||
|
case 0xBC: // CP IXH
|
||||||
|
cpu.cp8(cpu.GetIXH())
|
||||||
|
return 8
|
||||||
|
case 0xBD: // CP IXL
|
||||||
|
cpu.cp8(cpu.GetIXL())
|
||||||
|
return 8
|
||||||
|
case 0xBE: // CP (IX+d)
|
||||||
|
return cpu.executeALUIndexed(7)
|
||||||
|
|
||||||
|
// POP and PUSH instructions
|
||||||
|
case 0xE1: // POP IX
|
||||||
|
cpu.IX = cpu.Pop()
|
||||||
|
return 14
|
||||||
|
case 0xE3: // EX (SP), IX
|
||||||
|
temp := cpu.Memory.ReadWord(cpu.SP)
|
||||||
|
cpu.Memory.WriteWord(cpu.SP, cpu.IX)
|
||||||
|
cpu.IX = temp
|
||||||
|
cpu.MEMPTR = temp
|
||||||
|
return 23
|
||||||
|
case 0xE5: // PUSH IX
|
||||||
|
cpu.Push(cpu.IX)
|
||||||
|
return 15
|
||||||
|
case 0xE9: // JP (IX)
|
||||||
|
cpu.PC = cpu.IX
|
||||||
|
return 8
|
||||||
|
case 0xF9: // LD SP, IX
|
||||||
|
cpu.SP = cpu.IX
|
||||||
|
return 10
|
||||||
|
|
||||||
|
// Handle DD CB prefix (IX with displacement and CB operations)
|
||||||
|
case 0xCB: // DD CB prefix
|
||||||
|
return cpu.executeDDCBOpcode()
|
||||||
|
|
||||||
|
case 0xfd:
|
||||||
|
return 8
|
||||||
|
case 0x00: // Extended NOP (undocumented)
|
||||||
|
// DD 00 is an undocumented instruction that acts as an extended NOP
|
||||||
|
// It consumes the DD prefix and the 00 opcode but executes as a NOP
|
||||||
|
// Takes 8 cycles total (4 for DD prefix fetch + 4 for 00 opcode fetch)
|
||||||
|
return 8
|
||||||
|
case 0xdd:
|
||||||
|
return 8
|
||||||
|
default:
|
||||||
|
return cpu.ExecuteOpcode(opcode)
|
||||||
|
//panic(fmt.Sprintf("DD unexpected code %x", opcode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeIncDecIndexed handles INC/DEC (IX+d) instructions
|
||||||
|
func (cpu *CPU) executeIncDecIndexed(isInc bool) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
var result byte
|
||||||
|
if isInc {
|
||||||
|
result = cpu.inc8(value)
|
||||||
|
} else {
|
||||||
|
result = cpu.dec8(value)
|
||||||
|
}
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeLoadFromIndexed handles LD r, (IX+d) instructions
|
||||||
|
func (cpu *CPU) executeLoadFromIndexed(reg byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = value
|
||||||
|
case 1:
|
||||||
|
cpu.C = value
|
||||||
|
case 2:
|
||||||
|
cpu.D = value
|
||||||
|
case 3:
|
||||||
|
cpu.E = value
|
||||||
|
case 4:
|
||||||
|
cpu.H = value
|
||||||
|
case 5:
|
||||||
|
cpu.L = value
|
||||||
|
case 7:
|
||||||
|
cpu.A = value
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeStoreToIndexed handles LD (IX+d), r instructions
|
||||||
|
func (cpu *CPU) executeStoreToIndexed(value byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
cpu.Memory.WriteByte(addr, value)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeALUIndexed handles ALU operations with (IX+d) operand
|
||||||
|
func (cpu *CPU) executeALUIndexed(opType byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
switch opType {
|
||||||
|
case 0: // ADD
|
||||||
|
cpu.add8(value)
|
||||||
|
case 1: // ADC
|
||||||
|
cpu.adc8(value)
|
||||||
|
case 2: // SUB
|
||||||
|
cpu.sub8(value)
|
||||||
|
case 3: // SBC
|
||||||
|
cpu.sbc8(value)
|
||||||
|
case 4: // AND
|
||||||
|
cpu.and8(value)
|
||||||
|
case 5: // XOR
|
||||||
|
cpu.xor8(value)
|
||||||
|
case 6: // OR
|
||||||
|
cpu.or8(value)
|
||||||
|
case 7: // CP
|
||||||
|
cpu.cp8(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// add16IX adds two 16-bit values for IX register and updates flags
|
||||||
|
func (cpu *CPU) add16IX(a, b uint16) uint16 {
|
||||||
|
result := a + b
|
||||||
|
cpu.SetFlagState(FLAG_C, result < a)
|
||||||
|
cpu.SetFlagState(FLAG_H, (a&0x0FFF)+(b&0x0FFF) > 0x0FFF)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
// For IX operations, we update X and Y flags from high byte of result
|
||||||
|
cpu.UpdateFlags3and5FromAddress(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
172
ddcb_opcodes.go
Normal file
172
ddcb_opcodes.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// ExecuteDDCBOpcode executes a DD CB prefixed opcode
|
||||||
|
// This is handled in dd_opcodes.go in the executeDDCBOpcode function
|
||||||
|
// This file exists to satisfy the requirement of separating opcodes by prefix
|
||||||
|
// executeDDCBOpcode executes a DD CB prefixed opcode
|
||||||
|
func (cpu *CPU) executeDDCBOpcode() int {
|
||||||
|
// For DD CB prefixed instructions, R should be incremented by 2 total
|
||||||
|
// We've already incremented R once for the DD prefix and once for the CB prefix
|
||||||
|
// So we need to adjust by -1 to get the correct total increment of 2
|
||||||
|
originalR := cpu.R
|
||||||
|
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
opcode := cpu.ReadOpcode()
|
||||||
|
|
||||||
|
// Adjust R register - DD CB instructions should increment R by 2 total
|
||||||
|
// We've already incremented twice (DD and CB), so we need to subtract 1
|
||||||
|
// to get the correct total of 2 increments
|
||||||
|
cpu.R = originalR
|
||||||
|
|
||||||
|
addr := uint16(int32(cpu.IX) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
// Handle rotate and shift instructions (0x00-0x3F)
|
||||||
|
if opcode <= 0x3F {
|
||||||
|
return cpu.executeRotateShiftIndexed(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bit test instructions (0x40-0x7F)
|
||||||
|
if opcode >= 0x40 && opcode <= 0x7F {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
cpu.bitMem(bitNum, value, byte(addr>>8))
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reset bit instructions (0x80-0xBF)
|
||||||
|
if opcode >= 0x80 && opcode <= 0xBF {
|
||||||
|
return cpu.executeResetBitIndexed(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle set bit instructions (0xC0-0xFF)
|
||||||
|
if opcode >= 0xC0 {
|
||||||
|
return cpu.executeSetBitIndexed(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unimplemented opcode
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeRotateShiftIndexed handles rotate and shift instructions for indexed addressing
|
||||||
|
func (cpu *CPU) executeRotateShiftIndexed(opcode byte, addr uint16, value byte) int {
|
||||||
|
// Determine operation type from opcode bits 3-5
|
||||||
|
opType := (opcode >> 3) & 0x07
|
||||||
|
// Determine register from opcode bits 0-2
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Perform the operation
|
||||||
|
var result byte
|
||||||
|
switch opType {
|
||||||
|
case 0: // RLC
|
||||||
|
result = cpu.rlc(value)
|
||||||
|
case 1: // RRC
|
||||||
|
result = cpu.rrc(value)
|
||||||
|
case 2: // RL
|
||||||
|
result = cpu.rl(value)
|
||||||
|
case 3: // RR
|
||||||
|
result = cpu.rr(value)
|
||||||
|
case 4: // SLA
|
||||||
|
result = cpu.sla(value)
|
||||||
|
case 5: // SRA
|
||||||
|
result = cpu.sra(value)
|
||||||
|
case 6: // SLL (Undocumented)
|
||||||
|
result = cpu.sll(value)
|
||||||
|
case 7: // SRL
|
||||||
|
result = cpu.srl(value)
|
||||||
|
default:
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store result in memory
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeResetBitIndexed handles reset bit instructions for indexed addressing
|
||||||
|
func (cpu *CPU) executeResetBitIndexed(opcode byte, addr uint16, value byte) int {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
result := cpu.res(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeSetBitIndexed handles set bit instructions for indexed addressing
|
||||||
|
func (cpu *CPU) executeSetBitIndexed(opcode byte, addr uint16, value byte) int {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
result := cpu.set(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 23
|
||||||
|
}
|
||||||
36
ed_in_out_test.go
Normal file
36
ed_in_out_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Improved test: verify OUT (C),r writes to the CURRENT BC port after a prior IN changes B.
|
||||||
|
func TestED_IN_OUT_PortAndValue(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
// Arrange: BC=0x1234, IN will load 0x80 into B -> BC becomes 0x8034.
|
||||||
|
cpu.SetBC(0x1234)
|
||||||
|
io.inVals[0x1234] = 0x80
|
||||||
|
|
||||||
|
// ED 40 = IN B,(C); ED 41 = OUT (C),B
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0x40, 0xED, 0x41)
|
||||||
|
|
||||||
|
// IN B,(C)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.B != 0x80 {
|
||||||
|
t.Fatalf("IN B,(C) expected B=0x80, got %02X", cpu.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OUT (C),B should use *current* BC (0x8034) and write B (0x80)
|
||||||
|
mustStep(t, cpu)
|
||||||
|
port := cpu.GetBC()
|
||||||
|
val, ok := io.lastOut[port]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("OUT (C),B wrote nothing to port %04X", port)
|
||||||
|
}
|
||||||
|
if val != cpu.B {
|
||||||
|
t.Fatalf("OUT (C),B wrote %02X, want %02X", val, cpu.B)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also verify MEMPTR behavior matches spec for IN/OUT: MEMPTR = BC + 1
|
||||||
|
if cpu.MEMPTR != (cpu.GetBC() + 1) {
|
||||||
|
t.Fatalf("MEMPTR after OUT should be BC+1: got %04X want %04X", cpu.MEMPTR, cpu.GetBC()+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
809
ed_opcodes.go
Normal file
809
ed_opcodes.go
Normal file
@@ -0,0 +1,809 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ExecuteEDOpcode executes an ED-prefixed opcode and returns the number of T-states used
|
||||||
|
func (cpu *CPU) ExecuteEDOpcode(opcode byte) int {
|
||||||
|
switch opcode {
|
||||||
|
// Block transfer instructions
|
||||||
|
case 0xA0: // LDI
|
||||||
|
cpu.ldi()
|
||||||
|
return 16
|
||||||
|
case 0xA1: // CPI
|
||||||
|
cpu.cpi()
|
||||||
|
return 16
|
||||||
|
case 0xA2: // INI
|
||||||
|
cpu.ini()
|
||||||
|
return 16
|
||||||
|
case 0xA3: // OUTI
|
||||||
|
cpu.outi()
|
||||||
|
return 16
|
||||||
|
case 0xA8: // LDD
|
||||||
|
cpu.ldd()
|
||||||
|
return 16
|
||||||
|
case 0xA9: // CPD
|
||||||
|
cpu.cpd()
|
||||||
|
return 16
|
||||||
|
case 0xAA: // IND
|
||||||
|
cpu.ind()
|
||||||
|
return 16
|
||||||
|
case 0xAB: // OUTD
|
||||||
|
cpu.outd()
|
||||||
|
return 16
|
||||||
|
case 0xB0: // LDIR
|
||||||
|
return cpu.ldir()
|
||||||
|
case 0xB1: // CPIR
|
||||||
|
return cpu.cpir()
|
||||||
|
case 0xB2: // INIR
|
||||||
|
return cpu.inir()
|
||||||
|
case 0xB3: // OTIR
|
||||||
|
return cpu.otir()
|
||||||
|
case 0xB8: // LDDR
|
||||||
|
return cpu.lddr()
|
||||||
|
case 0xB9: // CPDR
|
||||||
|
return cpu.cpdr()
|
||||||
|
case 0xBA: // INDR
|
||||||
|
return cpu.indr()
|
||||||
|
case 0xBB: // OTDR
|
||||||
|
return cpu.otdr()
|
||||||
|
|
||||||
|
// 8-bit load instructions
|
||||||
|
case 0x40: // IN B, (C)
|
||||||
|
return cpu.executeIN(0)
|
||||||
|
case 0x41: // OUT (C), B
|
||||||
|
return cpu.executeOUT(0)
|
||||||
|
case 0x42: // SBC HL, BC
|
||||||
|
result := cpu.sbc16WithMEMPTR(cpu.GetHL(), cpu.GetBC())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x43: // LD (nn), BC
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.GetBC())
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x44, 0x4C, 0x54, 0x5C, 0x64, 0x6C, 0x74, 0x7C: // NEG (various undocumented versions)
|
||||||
|
cpu.neg()
|
||||||
|
return 8
|
||||||
|
case 0x45, 0x55, 0x5D, 0x65, 0x6D, 0x75, 0x7D: // RETN (various undocumented versions)
|
||||||
|
cpu.retn()
|
||||||
|
return 14
|
||||||
|
case 0x46, 0x4E, 0x66: // IM 0 (various undocumented versions)
|
||||||
|
cpu.IM = 0
|
||||||
|
return 8
|
||||||
|
case 0x47: // LD I, A
|
||||||
|
cpu.I = cpu.A
|
||||||
|
return 9
|
||||||
|
case 0x48: // IN C, (C)
|
||||||
|
return cpu.executeIN(1)
|
||||||
|
case 0x49: // OUT (C), C
|
||||||
|
return cpu.executeOUT(1)
|
||||||
|
case 0x4A: // ADC HL, BC
|
||||||
|
result := cpu.adc16WithMEMPTR(cpu.GetHL(), cpu.GetBC())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x4B: // LD BC, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.SetBC(cpu.Memory.ReadWord(addr))
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x4D: // RETI
|
||||||
|
cpu.reti()
|
||||||
|
return 14
|
||||||
|
case 0x4F: // LD R, A
|
||||||
|
// R register is only 7 bits, bit 7 remains unchanged
|
||||||
|
//cpu.R = (cpu.R & 0x80) | (cpu.A & 0x7F)
|
||||||
|
cpu.R = cpu.A // fix zen80 tests
|
||||||
|
return 9
|
||||||
|
case 0x50: // IN D, (C)
|
||||||
|
return cpu.executeIN(2)
|
||||||
|
case 0x51: // OUT (C), D
|
||||||
|
return cpu.executeOUT(2)
|
||||||
|
case 0x52: // SBC HL, DE
|
||||||
|
result := cpu.sbc16WithMEMPTR(cpu.GetHL(), cpu.GetDE())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x53: // LD (nn), DE
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.GetDE())
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x56, 0x76: // IM 1 (various undocumented versions)
|
||||||
|
cpu.IM = 1
|
||||||
|
return 8
|
||||||
|
case 0x57: // LD A, I
|
||||||
|
cpu.ldAI()
|
||||||
|
return 9
|
||||||
|
case 0x58: // IN E, (C)
|
||||||
|
return cpu.executeIN(3)
|
||||||
|
case 0x59: // OUT (C), E
|
||||||
|
return cpu.executeOUT(3)
|
||||||
|
case 0x5A: // ADC HL, DE
|
||||||
|
result := cpu.adc16WithMEMPTR(cpu.GetHL(), cpu.GetDE())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x5B: // LD DE, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.SetDE(cpu.Memory.ReadWord(addr))
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x5E, 0x7E: // IM 2 (various undocumented versions)
|
||||||
|
cpu.IM = 2
|
||||||
|
return 8
|
||||||
|
case 0x5F: // LD A, R
|
||||||
|
cpu.ldAR()
|
||||||
|
return 9
|
||||||
|
case 0x60: // IN H, (C)
|
||||||
|
return cpu.executeIN(4)
|
||||||
|
case 0x61: // OUT (C), H
|
||||||
|
return cpu.executeOUT(4)
|
||||||
|
case 0x62: // SBC HL, HL
|
||||||
|
result := cpu.sbc16WithMEMPTR(cpu.GetHL(), cpu.GetHL())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x63: // LD (nn), HL
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.GetHL())
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x67: // RRD
|
||||||
|
cpu.rrd()
|
||||||
|
return 18
|
||||||
|
case 0x68: // IN L, (C)
|
||||||
|
return cpu.executeIN(5)
|
||||||
|
case 0x69: // OUT (C), L
|
||||||
|
return cpu.executeOUT(5)
|
||||||
|
case 0x6A: // ADC HL, HL
|
||||||
|
result := cpu.adc16WithMEMPTR(cpu.GetHL(), cpu.GetHL())
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x6B: // LD HL, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.SetHL(cpu.Memory.ReadWord(addr))
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x6F: // RLD
|
||||||
|
cpu.rld()
|
||||||
|
return 18
|
||||||
|
case 0x70: // IN (C) (Undocumented - input to dummy register)
|
||||||
|
bc := cpu.GetBC() // Save BC before doing anything
|
||||||
|
value := cpu.inC()
|
||||||
|
cpu.UpdateSZXYFlags(value)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
// Set PV flag based on parity of the result
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.parity(value))
|
||||||
|
// MEMPTR = BC + 1 (using the original BC value)
|
||||||
|
cpu.MEMPTR = bc + 1
|
||||||
|
return 12
|
||||||
|
case 0x71: // OUT (C), 0 (Undocumented)
|
||||||
|
cpu.outC(0)
|
||||||
|
// MEMPTR = BC + 1
|
||||||
|
cpu.MEMPTR = cpu.GetBC() + 1
|
||||||
|
return 12
|
||||||
|
case 0x72: // SBC HL, SP
|
||||||
|
result := cpu.sbc16WithMEMPTR(cpu.GetHL(), cpu.SP)
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x73: // LD (nn), SP
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.SP)
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x78: // IN A, (C)
|
||||||
|
return cpu.executeIN(7)
|
||||||
|
case 0x79: // OUT (C), A
|
||||||
|
return cpu.executeOUT(7)
|
||||||
|
case 0x7A: // ADC HL, SP
|
||||||
|
result := cpu.adc16WithMEMPTR(cpu.GetHL(), cpu.SP)
|
||||||
|
cpu.SetHL(result)
|
||||||
|
return 15
|
||||||
|
case 0x7B: // LD SP, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.SP = cpu.Memory.ReadWord(addr)
|
||||||
|
// MEMPTR = addr + 1
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x80: // endefined NOP
|
||||||
|
return 8
|
||||||
|
case 0x6e:
|
||||||
|
return 8
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("ED unexpected code %x", opcode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeIN handles the IN r, (C) instructions
|
||||||
|
func (cpu *CPU) executeIN(reg byte) int {
|
||||||
|
bc := cpu.GetBC()
|
||||||
|
value := cpu.inC()
|
||||||
|
|
||||||
|
// Update flags
|
||||||
|
cpu.UpdateSZXYFlags(value)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
// Set PV flag based on parity of the result
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.parity(value))
|
||||||
|
// MEMPTR = BC + 1 (using the original BC value)
|
||||||
|
cpu.MEMPTR = bc + 1
|
||||||
|
|
||||||
|
// Set the appropriate register
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = value
|
||||||
|
case 1:
|
||||||
|
cpu.C = value
|
||||||
|
case 2:
|
||||||
|
cpu.D = value
|
||||||
|
case 3:
|
||||||
|
cpu.E = value
|
||||||
|
case 4:
|
||||||
|
cpu.H = value
|
||||||
|
case 5:
|
||||||
|
cpu.L = value
|
||||||
|
case 7:
|
||||||
|
cpu.A = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeOUT handles the OUT (C), r instructions
|
||||||
|
func (cpu *CPU) executeOUT(reg byte) int {
|
||||||
|
var value byte
|
||||||
|
|
||||||
|
// Get the appropriate register value
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
value = cpu.B
|
||||||
|
case 1:
|
||||||
|
value = cpu.C
|
||||||
|
case 2:
|
||||||
|
value = cpu.D
|
||||||
|
case 3:
|
||||||
|
value = cpu.E
|
||||||
|
case 4:
|
||||||
|
value = cpu.H
|
||||||
|
case 5:
|
||||||
|
value = cpu.L
|
||||||
|
case 7:
|
||||||
|
value = cpu.A
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.outC(value)
|
||||||
|
// MEMPTR = BC + 1
|
||||||
|
cpu.MEMPTR = cpu.GetBC() + 1
|
||||||
|
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
|
||||||
|
// ldi loads byte from (HL) to (DE), increments pointers, decrements BC
|
||||||
|
func (cpu *CPU) ldi() {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
cpu.Memory.WriteByte(cpu.GetDE(), value)
|
||||||
|
|
||||||
|
cpu.SetDE(cpu.GetDE() + 1)
|
||||||
|
cpu.SetHL(cpu.GetHL() + 1)
|
||||||
|
cpu.SetBC(cpu.GetBC() - 1)
|
||||||
|
|
||||||
|
// FIXED: Calculate X and Y flags FIRST, preserving S, Z, C
|
||||||
|
n := value + cpu.A
|
||||||
|
cpu.F = (cpu.F & (FLAG_S | FLAG_Z | FLAG_C)) | (n & FLAG_X) | ((n & 0x02) << 4)
|
||||||
|
|
||||||
|
// THEN set the other flags
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.GetBC() != 0)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpi compares A with (HL), increments HL, decrements BC
|
||||||
|
func (cpu *CPU) cpi() {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
result := cpu.A - value
|
||||||
|
|
||||||
|
cpu.SetHL(cpu.GetHL() + 1)
|
||||||
|
cpu.SetBC(cpu.GetBC() - 1)
|
||||||
|
|
||||||
|
cpu.SetFlag(FLAG_N, true)
|
||||||
|
cpu.UpdateSZFlags(result)
|
||||||
|
|
||||||
|
// Set H flag if borrow from bit 4
|
||||||
|
cpu.SetFlagState(FLAG_H, (cpu.A&0x0F) < (value&0x0F))
|
||||||
|
|
||||||
|
// For CPI, F3 and F5 flags come from (A - (HL) - H_flag)
|
||||||
|
// where H_flag is the half-carry flag AFTER the instruction
|
||||||
|
temp := result - boolToByte(cpu.GetFlag(FLAG_H))
|
||||||
|
cpu.SetFlagState(FLAG_3, (temp&0x08) != 0) // Bit 3
|
||||||
|
cpu.SetFlagState(FLAG_5, (temp&0x02) != 0) // Bit 1
|
||||||
|
|
||||||
|
if cpu.GetBC() != 0 {
|
||||||
|
cpu.SetFlag(FLAG_PV, true)
|
||||||
|
} else {
|
||||||
|
cpu.ClearFlag(FLAG_PV)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set MEMPTR = PC - 1
|
||||||
|
cpu.MEMPTR = cpu.PC - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ini inputs byte to (HL), increments HL, decrements B
|
||||||
|
func (cpu *CPU) ini() {
|
||||||
|
value := cpu.IO.ReadPort(uint16(cpu.C) | (uint16(cpu.B) << 8))
|
||||||
|
cpu.Memory.WriteByte(cpu.GetHL(), value)
|
||||||
|
cpu.SetHL(cpu.GetHL() + 1)
|
||||||
|
origbc := cpu.GetBC()
|
||||||
|
cpu.B--
|
||||||
|
|
||||||
|
// Enhanced: Accurate flag calculation for INI
|
||||||
|
k := int(value) + int((cpu.C+1)&0xFF)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_Z, cpu.B == 0)
|
||||||
|
cpu.SetFlagState(FLAG_S, cpu.B&0x80 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_N, (value&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, k > 0xFF)
|
||||||
|
cpu.SetFlagState(FLAG_C, k > 0xFF)
|
||||||
|
// P/V flag is parity of ((k & 0x07) XOR B)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.parity(uint8(k&0x07)^cpu.B))
|
||||||
|
// X and Y flags from B register
|
||||||
|
cpu.F = (cpu.F & 0xD7) | (cpu.B & (FLAG_3 | FLAG_5))
|
||||||
|
|
||||||
|
cpu.MEMPTR = origbc + 1
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to calculate parity
|
||||||
|
func parity(val uint8) bool {
|
||||||
|
count := 0
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
if val&(1<<i) != 0 {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count%2 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// outi outputs byte from (HL) to port, increments HL, decrements B
|
||||||
|
func (cpu *CPU) outi() {
|
||||||
|
val := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
cpu.B--
|
||||||
|
cpu.IO.WritePort(cpu.GetBC(), val)
|
||||||
|
cpu.SetHL(cpu.GetHL() + 1)
|
||||||
|
|
||||||
|
// Enhanced: Accurate flag calculation for OUTI
|
||||||
|
// Note: Use L after HL increment
|
||||||
|
k := int(val) + int(cpu.L)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_Z, cpu.B == 0)
|
||||||
|
cpu.SetFlagState(FLAG_S, cpu.B&0x80 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_N, (val&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, k > 0xFF)
|
||||||
|
cpu.SetFlagState(FLAG_C, k > 0xFF)
|
||||||
|
// P/V flag is parity of ((k & 0x07) XOR B)
|
||||||
|
pvVal := uint8(k&0x07) ^ cpu.B
|
||||||
|
cpu.SetFlagState(FLAG_PV, parity(pvVal))
|
||||||
|
// X and Y flags from B register
|
||||||
|
cpu.F = (cpu.F & 0xD7) | (cpu.B & (FLAG_X | FLAG_Y))
|
||||||
|
|
||||||
|
cpu.MEMPTR = cpu.GetBC() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) ldd() {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
cpu.Memory.WriteByte(cpu.GetDE(), value)
|
||||||
|
cpu.SetHL(cpu.GetHL() - 1)
|
||||||
|
cpu.SetDE(cpu.GetDE() - 1)
|
||||||
|
cpu.SetBC(cpu.GetBC() - 1)
|
||||||
|
|
||||||
|
// FIXED: Calculate X and Y flags FIRST, preserving S, Z, C
|
||||||
|
n := value + cpu.A
|
||||||
|
cpu.F = (cpu.F & (FLAG_S | FLAG_Z | FLAG_C)) | (n & FLAG_X) | ((n & 0x02) << 4)
|
||||||
|
|
||||||
|
// THEN set the other flags
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.GetBC() != 0)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpd compares A with (HL), decrements HL, decrements BC
|
||||||
|
func (cpu *CPU) cpd() {
|
||||||
|
// HUMAN:Working for fuse test, but failed on zexall
|
||||||
|
// value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
// result := cpu.A - value
|
||||||
|
|
||||||
|
// cpu.SetHL(cpu.GetHL() - 1)
|
||||||
|
// cpu.SetBC(cpu.GetBC() - 1)
|
||||||
|
|
||||||
|
// cpu.SetFlag(FLAG_N, true)
|
||||||
|
// cpu.UpdateSZFlags(result)
|
||||||
|
// // For CPD, X and Y flags come from (A - (HL) - H_flag)
|
||||||
|
// // where H_flag is the half-carry flag AFTER the instruction
|
||||||
|
// temp := result - boolToByte(cpu.GetFlag(FLAG_H))
|
||||||
|
// cpu.SetFlagState(FLAG_3, (temp&0x08) != 0) // Bit 3
|
||||||
|
// cpu.SetFlagState(FLAG_5, (temp&0x02) != 0) // Bit 1
|
||||||
|
|
||||||
|
// // Set H flag if borrow from bit 4
|
||||||
|
// cpu.SetFlagState(FLAG_H, (cpu.A&0x0F) < (value&0x0F))
|
||||||
|
|
||||||
|
// if cpu.GetBC() != 0 {
|
||||||
|
// cpu.SetFlag(FLAG_PV, true)
|
||||||
|
// } else {
|
||||||
|
// cpu.ClearFlag(FLAG_PV)
|
||||||
|
// }
|
||||||
|
// cpu.MEMPTR--
|
||||||
|
|
||||||
|
val := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
result := int16(cpu.A) - int16(val)
|
||||||
|
cpu.SetHL(cpu.GetHL() - 1)
|
||||||
|
cpu.SetBC(cpu.GetBC() - 1)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_S, uint8(result)&0x80 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, uint8(result) == 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, (int8(cpu.A&0x0F)-int8(val&0x0F)) < 0)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.GetBC() != 0)
|
||||||
|
cpu.SetFlagState(FLAG_N, true)
|
||||||
|
|
||||||
|
// Y flag calculation - preserve S, Z, H, PV, N, C flags
|
||||||
|
n := uint8(result)
|
||||||
|
if cpu.GetFlag(FLAG_H) {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
cpu.F = (cpu.F & (FLAG_S | FLAG_Z | FLAG_H | FLAG_PV | FLAG_N | FLAG_C)) | (n & FLAG_X) | ((n & 0x02) << 4)
|
||||||
|
cpu.MEMPTR--
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ind inputs byte to (HL), decrements HL, decrements B
|
||||||
|
func (cpu *CPU) ind() {
|
||||||
|
val := cpu.IO.ReadPort(cpu.GetBC())
|
||||||
|
cpu.Memory.WriteByte(cpu.GetHL(), val)
|
||||||
|
cpu.SetHL(cpu.GetHL() - 1)
|
||||||
|
cpu.MEMPTR = cpu.GetBC() - 1
|
||||||
|
cpu.B--
|
||||||
|
|
||||||
|
// Enhanced: Accurate flag calculation for IND
|
||||||
|
// Note: Based on Z80 documentation, k = val + C (not C-1)
|
||||||
|
// HUMAN: based on fuse test , ITS + C-1
|
||||||
|
//k := int(val) + int(cpu.C)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_Z, cpu.B == 0)
|
||||||
|
cpu.SetFlagState(FLAG_S, cpu.B&0x80 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_N, (val&0x80) != 0)
|
||||||
|
// HUMAN : here was error
|
||||||
|
// cpu.SetFlagState(FLAG_H, k > 0xFF)
|
||||||
|
// cpu.SetFlagState(FLAG_C, k > 0xFF)
|
||||||
|
// // P/V flag is parity of ((k & 0x07) XOR B)
|
||||||
|
// pvVal := uint8(k&0x07) ^ cpu.B
|
||||||
|
// cpu.SetFlagState(FLAG_PV, parity(pvVal))
|
||||||
|
|
||||||
|
diff := uint16(cpu.C-1) + uint16(val)
|
||||||
|
cpu.SetFlagState(FLAG_H, diff > 0xFF)
|
||||||
|
cpu.SetFlagState(FLAG_C, diff > 0xFF)
|
||||||
|
temp := byte((diff & 0x07) ^ uint16(cpu.B))
|
||||||
|
parity := byte(0)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
parity ^= (temp >> i) & 1
|
||||||
|
}
|
||||||
|
cpu.SetFlagState(FLAG_PV, parity == 0)
|
||||||
|
|
||||||
|
// X and Y flags from B register
|
||||||
|
cpu.F = (cpu.F & 0xD7) | (cpu.B & (FLAG_X | FLAG_Y))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// outd outputs byte from (HL) to port, decrements HL, decrements B
|
||||||
|
func (cpu *CPU) outd() {
|
||||||
|
val := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
cpu.B--
|
||||||
|
cpu.IO.WritePort(uint16(cpu.C)|(uint16(cpu.B)<<8), val)
|
||||||
|
cpu.SetHL(cpu.GetHL() - 1)
|
||||||
|
|
||||||
|
k := uint16(val) + uint16(cpu.L)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_Z, cpu.B == 0)
|
||||||
|
cpu.SetFlagState(FLAG_S, cpu.B&0x80 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_N, (val&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, k > 0xFF)
|
||||||
|
cpu.SetFlagState(FLAG_C, k > 0xFF)
|
||||||
|
// P/V flag is parity of ((k & 0x07) XOR B)
|
||||||
|
pvVal := uint8(k&0x07) ^ cpu.B
|
||||||
|
cpu.SetFlagState(FLAG_PV, parity(pvVal))
|
||||||
|
// X and Y flags from B register
|
||||||
|
cpu.F = (cpu.F & 0xD7) | (cpu.B & (FLAG_X | FLAG_Y))
|
||||||
|
|
||||||
|
cpu.MEMPTR = cpu.GetBC() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ldir repeated LDI until BC=0
|
||||||
|
func (cpu *CPU) ldir() int {
|
||||||
|
cpu.ldi()
|
||||||
|
|
||||||
|
// Add T-states for this iteration (21 for continuing, 16 for final)
|
||||||
|
if cpu.GetBC() != 0 {
|
||||||
|
cpu.PC -= 2
|
||||||
|
cpu.MEMPTR = cpu.PC + 1
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpir repeated CPI until BC=0 or A=(HL)
|
||||||
|
func (cpu *CPU) cpir() int {
|
||||||
|
|
||||||
|
cpu.cpi()
|
||||||
|
|
||||||
|
if cpu.GetBC() != 0 && !cpu.GetFlag(FLAG_Z) {
|
||||||
|
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
// Return T-states for final iteration
|
||||||
|
cpu.MEMPTR = cpu.PC
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inir repeated INI until B=0
|
||||||
|
func (cpu *CPU) inir() int {
|
||||||
|
|
||||||
|
cpu.ini()
|
||||||
|
|
||||||
|
if cpu.B != 0 {
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
// Set MEMPTR to PC+1 at the end of the instruction
|
||||||
|
//cpu.MEMPTR = cpu.PC
|
||||||
|
// Return T-states for final iteration
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otir repeated OUTI until B=0
|
||||||
|
func (cpu *CPU) otir() int {
|
||||||
|
|
||||||
|
cpu.outi()
|
||||||
|
|
||||||
|
if cpu.B != 0 {
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
// Return T-states for final iteration
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lddr repeated LDD until BC=0
|
||||||
|
func (cpu *CPU) lddr() int {
|
||||||
|
|
||||||
|
// Execute one LDD operation
|
||||||
|
cpu.ldd()
|
||||||
|
|
||||||
|
// Add T-states for this iteration (21 for continuing, 16 for final)
|
||||||
|
if cpu.GetBC() != 0 {
|
||||||
|
cpu.PC -= 2
|
||||||
|
cpu.MEMPTR = cpu.PC + 1
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
return 16
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpdr repeated CPD until BC=0 or A=(HL)
|
||||||
|
func (cpu *CPU) cpdr() int {
|
||||||
|
|
||||||
|
cpu.cpd()
|
||||||
|
|
||||||
|
if cpu.GetBC() != 0 && !cpu.GetFlag(FLAG_Z) {
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
cpu.MEMPTR = cpu.PC + 1
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
cpu.MEMPTR = cpu.PC - 2
|
||||||
|
// Return T-states for final iteration
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// indr repeated IND until B=0
|
||||||
|
func (cpu *CPU) indr() int {
|
||||||
|
cpu.ind()
|
||||||
|
|
||||||
|
if cpu.B != 0 {
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
// Return T-states for final iteration
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otdr repeated OUTD until B=0
|
||||||
|
func (cpu *CPU) otdr() int {
|
||||||
|
|
||||||
|
cpu.outd()
|
||||||
|
|
||||||
|
if cpu.B != 0 {
|
||||||
|
cpu.PC -= 2 // Repeat instruction
|
||||||
|
// Return T-states for continuing iteration
|
||||||
|
return 21
|
||||||
|
} else {
|
||||||
|
return 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inC reads from port (BC)
|
||||||
|
func (cpu *CPU) inC() byte {
|
||||||
|
return cpu.IO.ReadPort(cpu.GetBC())
|
||||||
|
}
|
||||||
|
|
||||||
|
// outC writes to port (BC)
|
||||||
|
func (cpu *CPU) outC(value byte) {
|
||||||
|
cpu.IO.WritePort(cpu.GetBC(), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sbc16 subtracts 16-bit value with carry from HL
|
||||||
|
func (cpu *CPU) sbc16(val1, val2 uint16) uint16 {
|
||||||
|
carry := int32(0)
|
||||||
|
if cpu.GetFlag(FLAG_C) {
|
||||||
|
carry = 1
|
||||||
|
}
|
||||||
|
result := int32(val1) - int32(val2) - carry
|
||||||
|
halfCarry := (int16(val1&0x0FFF) - int16(val2&0x0FFF) - int16(carry)) < 0
|
||||||
|
overflow := ((val1^val2)&0x8000 != 0) && ((val1^uint16(result))&0x8000 != 0)
|
||||||
|
|
||||||
|
res16 := uint16(result)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_S, res16&0x8000 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, res16 == 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, halfCarry)
|
||||||
|
cpu.SetFlagState(FLAG_PV, overflow)
|
||||||
|
cpu.SetFlagState(FLAG_N, true)
|
||||||
|
cpu.SetFlagState(FLAG_C, result < 0)
|
||||||
|
// FIX: Set X and Y flags from high byte of result
|
||||||
|
cpu.SetFlagState(FLAG_X, uint8(res16>>8)&FLAG_X != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, uint8(res16>>8)&FLAG_Y != 0)
|
||||||
|
cpu.MEMPTR = val1 + 1
|
||||||
|
return res16
|
||||||
|
}
|
||||||
|
|
||||||
|
// sbc16WithMEMPTR subtracts 16-bit value with carry from HL and sets MEMPTR
|
||||||
|
func (cpu *CPU) sbc16WithMEMPTR(a, b uint16) uint16 {
|
||||||
|
result := cpu.sbc16(a, b)
|
||||||
|
cpu.MEMPTR = a + 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// adc16 adds 16-bit value with carry to HL
|
||||||
|
func (cpu *CPU) adc16(val1, val2 uint16) uint16 {
|
||||||
|
carry := uint32(0)
|
||||||
|
if cpu.GetFlag(FLAG_C) {
|
||||||
|
carry = 1
|
||||||
|
}
|
||||||
|
result := uint32(val1) + uint32(val2) + carry
|
||||||
|
halfCarry := (val1&0x0FFF + val2&0x0FFF + uint16(carry)) > 0x0FFF
|
||||||
|
overflow := ((val1^val2)&0x8000 == 0) && ((val1^uint16(result))&0x8000 != 0)
|
||||||
|
|
||||||
|
res16 := uint16(result)
|
||||||
|
|
||||||
|
cpu.SetFlagState(FLAG_S, res16&0x8000 != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, res16 == 0)
|
||||||
|
cpu.SetFlagState(FLAG_H, halfCarry)
|
||||||
|
cpu.SetFlagState(FLAG_PV, overflow)
|
||||||
|
cpu.SetFlagState(FLAG_N, false)
|
||||||
|
cpu.SetFlagState(FLAG_C, result > 0xFFFF)
|
||||||
|
// FIX: Set X and Y flags from high byte of result
|
||||||
|
cpu.SetFlagState(FLAG_X, uint8(res16>>8)&FLAG_X != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, uint8(res16>>8)&FLAG_Y != 0)
|
||||||
|
cpu.MEMPTR = val1 + 1
|
||||||
|
|
||||||
|
return res16
|
||||||
|
}
|
||||||
|
|
||||||
|
// adc16WithMEMPTR adds 16-bit value with carry to HL and sets MEMPTR
|
||||||
|
func (cpu *CPU) adc16WithMEMPTR(a, b uint16) uint16 {
|
||||||
|
result := cpu.adc16(a, b)
|
||||||
|
cpu.MEMPTR = a + 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// neg negates the accumulator
|
||||||
|
func (cpu *CPU) neg() {
|
||||||
|
value := cpu.A
|
||||||
|
cpu.A = 0
|
||||||
|
cpu.sub8(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// retn returns from interrupt and restores IFF1 from IFF2
|
||||||
|
func (cpu *CPU) retn() {
|
||||||
|
cpu.PC = cpu.Pop()
|
||||||
|
cpu.MEMPTR = cpu.PC
|
||||||
|
cpu.IFF1 = cpu.IFF2
|
||||||
|
}
|
||||||
|
|
||||||
|
// reti returns from interrupt (same as retn for Z80)
|
||||||
|
func (cpu *CPU) reti() {
|
||||||
|
cpu.PC = cpu.Pop()
|
||||||
|
cpu.MEMPTR = cpu.PC
|
||||||
|
cpu.IFF1 = cpu.IFF2
|
||||||
|
}
|
||||||
|
|
||||||
|
// ldAI loads I register into A and updates flags
|
||||||
|
func (cpu *CPU) ldAI() {
|
||||||
|
cpu.A = cpu.I
|
||||||
|
cpu.UpdateSZXYFlags(cpu.A)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.IFF2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ldAR loads R register into A and updates flags
|
||||||
|
func (cpu *CPU) ldAR() {
|
||||||
|
// Load the R register into A
|
||||||
|
cpu.A = cpu.R
|
||||||
|
cpu.UpdateSZXYFlags(cpu.A)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
cpu.SetFlagState(FLAG_PV, cpu.IFF2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rrd rotates digit between A and (HL) right
|
||||||
|
func (cpu *CPU) rrd() {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
ah := cpu.A & 0xF0
|
||||||
|
al := cpu.A & 0x0F
|
||||||
|
hl := value
|
||||||
|
|
||||||
|
// A bits 3-0 go to HL bits 7-4
|
||||||
|
// HL bits 7-4 go to HL bits 3-0
|
||||||
|
// HL bits 3-0 go to A bits 3-0
|
||||||
|
cpu.A = ah | (hl & 0x0F)
|
||||||
|
newHL := ((hl & 0xF0) >> 4) | (al << 4)
|
||||||
|
cpu.Memory.WriteByte(cpu.GetHL(), newHL)
|
||||||
|
|
||||||
|
cpu.UpdateSZXYPVFlags(cpu.A)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
|
||||||
|
// Set MEMPTR = HL + 1
|
||||||
|
cpu.MEMPTR = cpu.GetHL() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// rld rotates digit between A and (HL) left
|
||||||
|
func (cpu *CPU) rld() {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.GetHL())
|
||||||
|
ah := cpu.A & 0xF0
|
||||||
|
al := cpu.A & 0x0F
|
||||||
|
hl := value
|
||||||
|
|
||||||
|
// A bits 3-0 go to HL bits 3-0
|
||||||
|
// HL bits 3-0 go to HL bits 7-4
|
||||||
|
// HL bits 7-4 go to A bits 3-0
|
||||||
|
cpu.A = ah | (hl >> 4)
|
||||||
|
newHL := ((hl & 0x0F) << 4) | al
|
||||||
|
cpu.Memory.WriteByte(cpu.GetHL(), newHL)
|
||||||
|
|
||||||
|
cpu.UpdateSZXYPVFlags(cpu.A)
|
||||||
|
cpu.ClearFlag(FLAG_H)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
|
||||||
|
// Set MEMPTR = HL + 1
|
||||||
|
cpu.MEMPTR = cpu.GetHL() + 1
|
||||||
|
}
|
||||||
15
ed_undefined_nop_test.go
Normal file
15
ed_undefined_nop_test.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// ED 80..9F (mostly undefined) act as NOP with 8 cycles per the implementation.
|
||||||
|
// We probe one byte to cement the contract.
|
||||||
|
func TestED_UndefinedActsAsNOP8Cycles(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Use ED 80
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0x80)
|
||||||
|
pc := cpu.PC
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 8, "undefined ED opcode should be 8 cycles")
|
||||||
|
assertEq(t, cpu.PC, pc+2, "PC advanced over ED xx")
|
||||||
|
}
|
||||||
15
ex_swap_tests.go
Normal file
15
ex_swap_tests.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Validate EX AF,AF' behavior.
|
||||||
|
func TestEX_AF_AFPrime(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.A, cpu.F = 0x12, 0x34
|
||||||
|
cpu.A_, cpu.F_ = 0xAB, 0xCD
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x08) // EX AF,AF'
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.A != 0xAB || cpu.F != 0xCD || cpu.A_ != 0x12 || cpu.F_ != 0x34 {
|
||||||
|
t.Fatalf("EX AF,AF' swap failed: A=%02X F=%02X A'=%02X F'=%02X", cpu.A, cpu.F, cpu.A_, cpu.F_)
|
||||||
|
}
|
||||||
|
}
|
||||||
49
ex_swaps_and_sp_hl_test.go
Normal file
49
ex_swaps_and_sp_hl_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// EX/EXX/LD SP,HL and EX (SP),HL basics.
|
||||||
|
func TestEX_EXX_SP_HL_Basics(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
|
||||||
|
// EX AF,AF'
|
||||||
|
cpu.A, cpu.F = 0x12, 0x34
|
||||||
|
cpu.A_, cpu.F_ = 0xAB, 0xCD
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x08) // EX AF,AF'
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.A != 0xAB || cpu.F != 0xCD || cpu.A_ != 0x12 || cpu.F_ != 0x34 {
|
||||||
|
t.Fatalf("EX AF,AF' failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXX
|
||||||
|
cpu.SetBC(0x1111)
|
||||||
|
cpu.SetDE(0x2222)
|
||||||
|
cpu.SetHL(0x3333)
|
||||||
|
cpu.SetAF(0x0000) // ensure flags not impacted by EXX
|
||||||
|
cpu.B_, cpu.C_, cpu.D_, cpu.E_, cpu.H_, cpu.L_ = 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xD9) // EXX
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.GetBC() != 0xAABB || cpu.GetDE() != 0xCCDD || cpu.GetHL() != 0xEEFF {
|
||||||
|
t.Fatalf("EXX failed to swap alt sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LD SP,HL
|
||||||
|
cpu.SetHL(0x4321)
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xF9) // LD SP,HL
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 6, "LD SP,HL cycles")
|
||||||
|
assertEq(t, cpu.SP, uint16(0x4321), "LD SP,HL moved value")
|
||||||
|
|
||||||
|
// EX (SP),HL
|
||||||
|
cpu.SP = 0x8000
|
||||||
|
mem.WriteByte(0x8000, 0x78) // low
|
||||||
|
mem.WriteByte(0x8001, 0x56) // high -> word 0x5678
|
||||||
|
cpu.SetHL(0x9ABC)
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xE3) // EX (SP),HL
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 19, "EX (SP),HL cycles")
|
||||||
|
// HL should now be 0x5678; memory should now hold 0x9ABC
|
||||||
|
if cpu.GetHL() != 0x5678 || mem.ReadByte(0x8000) != 0xBC || mem.ReadByte(0x8001) != 0x9A {
|
||||||
|
t.Fatalf("EX (SP),HL failed: HL=%04X mem=[%02X %02X]", cpu.GetHL(), mem.ReadByte(0x8000), mem.ReadByte(0x8001))
|
||||||
|
}
|
||||||
|
}
|
||||||
376
fd_opcodes.go
Normal file
376
fd_opcodes.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// ExecuteFDOpcode executes a FD-prefixed opcode and returns the number of T-states used
|
||||||
|
func (cpu *CPU) ExecuteFDOpcode(opcode byte) int {
|
||||||
|
switch opcode {
|
||||||
|
// Load instructions
|
||||||
|
case 0x09: // ADD IY, BC
|
||||||
|
oldIY := cpu.IY
|
||||||
|
result := cpu.add16IY(cpu.IY, cpu.GetBC())
|
||||||
|
cpu.MEMPTR = oldIY + 1
|
||||||
|
cpu.IY = result
|
||||||
|
return 15
|
||||||
|
case 0x19: // ADD IY, DE
|
||||||
|
oldIY := cpu.IY
|
||||||
|
result := cpu.add16IY(cpu.IY, cpu.GetDE())
|
||||||
|
cpu.MEMPTR = oldIY + 1
|
||||||
|
cpu.IY = result
|
||||||
|
return 15
|
||||||
|
case 0x21: // LD IY, nn
|
||||||
|
cpu.IY = cpu.ReadImmediateWord()
|
||||||
|
return 14
|
||||||
|
case 0x22: // LD (nn), IY
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.Memory.WriteWord(addr, cpu.IY)
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x23: // INC IY
|
||||||
|
cpu.IY++
|
||||||
|
return 10
|
||||||
|
case 0x24: // INC IYH
|
||||||
|
cpu.SetIYH(cpu.inc8(cpu.GetIYH()))
|
||||||
|
return 8
|
||||||
|
case 0x25: // DEC IYH
|
||||||
|
cpu.SetIYH(cpu.dec8(cpu.GetIYH()))
|
||||||
|
return 8
|
||||||
|
case 0x26: // LD IYH, n
|
||||||
|
cpu.SetIYH(cpu.ReadImmediateByte())
|
||||||
|
return 11
|
||||||
|
case 0x29: // ADD IY, IY
|
||||||
|
oldIY := cpu.IY
|
||||||
|
result := cpu.add16IY(cpu.IY, cpu.IY)
|
||||||
|
cpu.MEMPTR = oldIY + 1
|
||||||
|
cpu.IY = result
|
||||||
|
return 15
|
||||||
|
case 0x2A: // LD IY, (nn)
|
||||||
|
addr := cpu.ReadImmediateWord()
|
||||||
|
cpu.IY = cpu.Memory.ReadWord(addr)
|
||||||
|
cpu.MEMPTR = addr + 1
|
||||||
|
return 20
|
||||||
|
case 0x2B: // DEC IY
|
||||||
|
cpu.IY--
|
||||||
|
return 10
|
||||||
|
case 0x2C: // INC IYL
|
||||||
|
cpu.SetIYL(cpu.inc8(cpu.GetIYL()))
|
||||||
|
return 8
|
||||||
|
case 0x2D: // DEC IYL
|
||||||
|
cpu.SetIYL(cpu.dec8(cpu.GetIYL()))
|
||||||
|
return 8
|
||||||
|
case 0x2E: // LD IYL, n
|
||||||
|
cpu.SetIYL(cpu.ReadImmediateByte())
|
||||||
|
return 11
|
||||||
|
case 0x34: // INC (IY+d)
|
||||||
|
return cpu.executeIncDecIndexedIY(true)
|
||||||
|
case 0x35: // DEC (IY+d)
|
||||||
|
return cpu.executeIncDecIndexedIY(false)
|
||||||
|
case 0x36: // LD (IY+d), n
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
value := cpu.ReadImmediateByte()
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
cpu.Memory.WriteByte(addr, value)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
case 0x39: // ADD IY, SP
|
||||||
|
oldIY := cpu.IY
|
||||||
|
result := cpu.add16IY(cpu.IY, cpu.SP)
|
||||||
|
cpu.MEMPTR = oldIY + 1
|
||||||
|
cpu.IY = result
|
||||||
|
return 15
|
||||||
|
|
||||||
|
// Load register from IY register
|
||||||
|
case 0x44: // LD B, IYH
|
||||||
|
cpu.B = cpu.GetIYH()
|
||||||
|
return 8
|
||||||
|
case 0x45: // LD B, IYL
|
||||||
|
cpu.B = cpu.GetIYL()
|
||||||
|
return 8
|
||||||
|
case 0x46: // LD B, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(0)
|
||||||
|
case 0x4C: // LD C, IYH
|
||||||
|
cpu.C = cpu.GetIYH()
|
||||||
|
return 8
|
||||||
|
case 0x4D: // LD C, IYL
|
||||||
|
cpu.C = cpu.GetIYL()
|
||||||
|
return 8
|
||||||
|
case 0x4E: // LD C, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(1)
|
||||||
|
case 0x54: // LD D, IYH
|
||||||
|
cpu.D = cpu.GetIYH()
|
||||||
|
return 8
|
||||||
|
case 0x55: // LD D, IYL
|
||||||
|
cpu.D = cpu.GetIYL()
|
||||||
|
return 8
|
||||||
|
case 0x56: // LD D, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(2)
|
||||||
|
case 0x5C: // LD E, IYH
|
||||||
|
cpu.E = cpu.GetIYH()
|
||||||
|
return 8
|
||||||
|
case 0x5D: // LD E, IYL
|
||||||
|
cpu.E = cpu.GetIYL()
|
||||||
|
return 8
|
||||||
|
case 0x5E: // LD E, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(3)
|
||||||
|
case 0x60: // LD IYH, B
|
||||||
|
cpu.SetIYH(cpu.B)
|
||||||
|
return 8
|
||||||
|
case 0x61: // LD IYH, C
|
||||||
|
cpu.SetIYH(cpu.C)
|
||||||
|
return 8
|
||||||
|
case 0x62: // LD IYH, D
|
||||||
|
cpu.SetIYH(cpu.D)
|
||||||
|
return 8
|
||||||
|
case 0x63: // LD IYH, E
|
||||||
|
cpu.SetIYH(cpu.E)
|
||||||
|
return 8
|
||||||
|
case 0x64: // LD IYH, IYH
|
||||||
|
// No operation needed
|
||||||
|
return 8
|
||||||
|
case 0x65: // LD IYH, IYL
|
||||||
|
cpu.SetIYH(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0x66: // LD H, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(4)
|
||||||
|
case 0x67: // LD IYH, A
|
||||||
|
cpu.SetIYH(cpu.A)
|
||||||
|
return 8
|
||||||
|
case 0x68: // LD IYL, B
|
||||||
|
cpu.SetIYL(cpu.B)
|
||||||
|
return 8
|
||||||
|
case 0x69: // LD IYL, C
|
||||||
|
cpu.SetIYL(cpu.C)
|
||||||
|
return 8
|
||||||
|
case 0x6A: // LD IYL, D
|
||||||
|
cpu.SetIYL(cpu.D)
|
||||||
|
return 8
|
||||||
|
case 0x6B: // LD IYL, E
|
||||||
|
cpu.SetIYL(cpu.E)
|
||||||
|
return 8
|
||||||
|
case 0x6C: // LD IYL, IYH
|
||||||
|
cpu.SetIYL(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0x6D: // LD IYL, IYL
|
||||||
|
// No operation needed
|
||||||
|
return 8
|
||||||
|
case 0x6E: // LD L, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(5)
|
||||||
|
case 0x6F: // LD IYL, A
|
||||||
|
cpu.SetIYL(cpu.A)
|
||||||
|
return 8
|
||||||
|
case 0x70: // LD (IY+d), B
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.B)
|
||||||
|
case 0x71: // LD (IY+d), C
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.C)
|
||||||
|
case 0x72: // LD (IY+d), D
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.D)
|
||||||
|
case 0x73: // LD (IY+d), E
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.E)
|
||||||
|
case 0x74: // LD (IY+d), H
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.H)
|
||||||
|
case 0x75: // LD (IY+d), L
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.L)
|
||||||
|
case 0x77: // LD (IY+d), A
|
||||||
|
return cpu.executeStoreToIndexedIY(cpu.A)
|
||||||
|
case 0x7C: // LD A, IYH
|
||||||
|
cpu.A = cpu.GetIYH()
|
||||||
|
return 8
|
||||||
|
case 0x7D: // LD A, IYL
|
||||||
|
cpu.A = cpu.GetIYL()
|
||||||
|
return 8
|
||||||
|
case 0x7E: // LD A, (IY+d)
|
||||||
|
return cpu.executeLoadFromIndexedIY(7)
|
||||||
|
|
||||||
|
// Arithmetic and logic instructions
|
||||||
|
case 0x84: // ADD A, IYH
|
||||||
|
cpu.add8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0x85: // ADD A, IYL
|
||||||
|
cpu.add8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0x86: // ADD A, (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(0)
|
||||||
|
case 0x8C: // ADC A, IYH
|
||||||
|
cpu.adc8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0x8D: // ADC A, IYL
|
||||||
|
cpu.adc8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0x8E: // ADC A, (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(1)
|
||||||
|
case 0x94: // SUB IYH
|
||||||
|
cpu.sub8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0x95: // SUB IYL
|
||||||
|
cpu.sub8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0x96: // SUB (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(2)
|
||||||
|
case 0x9C: // SBC A, IYH
|
||||||
|
cpu.sbc8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0x9D: // SBC A, IYL
|
||||||
|
cpu.sbc8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0x9E: // SBC A, (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(3)
|
||||||
|
case 0xA4: // AND IYH
|
||||||
|
cpu.and8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0xA5: // AND IYL
|
||||||
|
cpu.and8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0xA6: // AND (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(4)
|
||||||
|
case 0xAC: // XOR IYH
|
||||||
|
cpu.xor8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0xAD: // XOR IYL
|
||||||
|
cpu.xor8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0xAE: // XOR (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(5)
|
||||||
|
case 0xB4: // OR IYH
|
||||||
|
cpu.or8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0xB5: // OR IYL
|
||||||
|
cpu.or8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0xB6: // OR (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(6)
|
||||||
|
case 0xBC: // CP IYH
|
||||||
|
cpu.cp8(cpu.GetIYH())
|
||||||
|
return 8
|
||||||
|
case 0xBD: // CP IYL
|
||||||
|
cpu.cp8(cpu.GetIYL())
|
||||||
|
return 8
|
||||||
|
case 0xBE: // CP (IY+d)
|
||||||
|
return cpu.executeALUIndexedIY(7)
|
||||||
|
|
||||||
|
// POP and PUSH instructions
|
||||||
|
case 0xE1: // POP IY
|
||||||
|
cpu.IY = cpu.Pop()
|
||||||
|
return 14
|
||||||
|
case 0xE3: // EX (SP), IY
|
||||||
|
temp := cpu.Memory.ReadWord(cpu.SP)
|
||||||
|
cpu.Memory.WriteWord(cpu.SP, cpu.IY)
|
||||||
|
cpu.IY = temp
|
||||||
|
cpu.MEMPTR = cpu.IY
|
||||||
|
return 23
|
||||||
|
case 0xE5: // PUSH IY
|
||||||
|
cpu.Push(cpu.IY)
|
||||||
|
return 15
|
||||||
|
case 0xE9: // JP (IY)
|
||||||
|
cpu.PC = cpu.IY
|
||||||
|
return 8
|
||||||
|
case 0xF9: // LD SP, IY
|
||||||
|
cpu.SP = cpu.IY
|
||||||
|
return 10
|
||||||
|
|
||||||
|
// Handle FD CB prefix (IY with displacement and CB operations)
|
||||||
|
case 0xCB: // FD CB prefix
|
||||||
|
return cpu.ExecuteFDCBOpcode()
|
||||||
|
|
||||||
|
case 0x00: // Extended NOP (undocumented)
|
||||||
|
// FD 00 is an undocumented instruction that acts as an extended NOP
|
||||||
|
// It consumes the FD prefix and the 00 opcode but executes as a NOP
|
||||||
|
// Takes 8 cycles total (4 for FD prefix fetch + 4 for 00 opcode fetch)
|
||||||
|
return 8
|
||||||
|
default:
|
||||||
|
// Unimplemented opcode - treat as regular opcode
|
||||||
|
// This handles cases where FD is followed by a normal opcode
|
||||||
|
return cpu.ExecuteOpcode(opcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeIncDecIndexedIY handles INC/DEC (IY+d) instructions
|
||||||
|
func (cpu *CPU) executeIncDecIndexedIY(isInc bool) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
var result byte
|
||||||
|
if isInc {
|
||||||
|
result = cpu.inc8(value)
|
||||||
|
} else {
|
||||||
|
result = cpu.dec8(value)
|
||||||
|
}
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeLoadFromIndexedIY handles LD r, (IY+d) instructions
|
||||||
|
func (cpu *CPU) executeLoadFromIndexedIY(reg byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = value
|
||||||
|
case 1:
|
||||||
|
cpu.C = value
|
||||||
|
case 2:
|
||||||
|
cpu.D = value
|
||||||
|
case 3:
|
||||||
|
cpu.E = value
|
||||||
|
case 4:
|
||||||
|
cpu.H = value
|
||||||
|
case 5:
|
||||||
|
cpu.L = value
|
||||||
|
case 7:
|
||||||
|
cpu.A = value
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeStoreToIndexedIY handles LD (IY+d), r instructions
|
||||||
|
func (cpu *CPU) executeStoreToIndexedIY(value byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
cpu.Memory.WriteByte(addr, value)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeALUIndexedIY handles ALU operations with (IY+d) operand
|
||||||
|
func (cpu *CPU) executeALUIndexedIY(opType byte) int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
|
||||||
|
switch opType {
|
||||||
|
case 0: // ADD
|
||||||
|
cpu.add8(value)
|
||||||
|
case 1: // ADC
|
||||||
|
cpu.adc8(value)
|
||||||
|
case 2: // SUB
|
||||||
|
cpu.sub8(value)
|
||||||
|
case 3: // SBC
|
||||||
|
cpu.sbc8(value)
|
||||||
|
case 4: // AND
|
||||||
|
cpu.and8(value)
|
||||||
|
case 5: // XOR
|
||||||
|
cpu.xor8(value)
|
||||||
|
case 6: // OR
|
||||||
|
cpu.or8(value)
|
||||||
|
case 7: // CP
|
||||||
|
cpu.cp8(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
return 19
|
||||||
|
}
|
||||||
|
|
||||||
|
// add16IY adds two 16-bit values for IY register and updates flags
|
||||||
|
func (cpu *CPU) add16IY(a, b uint16) uint16 {
|
||||||
|
result := a + b
|
||||||
|
cpu.SetFlagState(FLAG_C, result < a)
|
||||||
|
cpu.SetFlagState(FLAG_H, (a&0x0FFF)+(b&0x0FFF) > 0x0FFF)
|
||||||
|
cpu.ClearFlag(FLAG_N)
|
||||||
|
// For IY operations, we update X and Y flags from high byte of result
|
||||||
|
cpu.UpdateFlags3and5FromAddress(result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
156
fdcb_opcodes.go
Normal file
156
fdcb_opcodes.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// ExecuteFDCBOpcode executes a FD CB prefixed opcode
|
||||||
|
func (cpu *CPU) ExecuteFDCBOpcode() int {
|
||||||
|
displacement := cpu.ReadDisplacement()
|
||||||
|
opcode := cpu.ReadOpcode()
|
||||||
|
cpu.R--
|
||||||
|
addr := uint16(int32(cpu.IY) + int32(displacement))
|
||||||
|
value := cpu.Memory.ReadByte(addr)
|
||||||
|
cpu.MEMPTR = addr
|
||||||
|
|
||||||
|
// Handle rotate and shift instructions (0x00-0x3F)
|
||||||
|
if opcode <= 0x3F {
|
||||||
|
return cpu.executeRotateShiftIndexedIY(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle bit test instructions (0x40-0x7F)
|
||||||
|
if opcode >= 0x40 && opcode <= 0x7F {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
cpu.bitMem(bitNum, value, byte(addr>>8))
|
||||||
|
return 20
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle reset bit instructions (0x80-0xBF)
|
||||||
|
if opcode >= 0x80 && opcode <= 0xBF {
|
||||||
|
return cpu.executeResetBitIndexedIY(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle set bit instructions (0xC0-0xFF)
|
||||||
|
if opcode >= 0xC0 {
|
||||||
|
return cpu.executeSetBitIndexedIY(opcode, addr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unimplemented opcode
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeRotateShiftIndexedIY handles rotate and shift instructions for IY indexed addressing
|
||||||
|
func (cpu *CPU) executeRotateShiftIndexedIY(opcode byte, addr uint16, value byte) int {
|
||||||
|
// Determine operation type from opcode bits 3-5
|
||||||
|
opType := (opcode >> 3) & 0x07
|
||||||
|
// Determine register from opcode bits 0-2
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
// Perform the operation
|
||||||
|
var result byte
|
||||||
|
switch opType {
|
||||||
|
case 0: // RLC
|
||||||
|
result = cpu.rlc(value)
|
||||||
|
case 1: // RRC
|
||||||
|
result = cpu.rrc(value)
|
||||||
|
case 2: // RL
|
||||||
|
result = cpu.rl(value)
|
||||||
|
case 3: // RR
|
||||||
|
result = cpu.rr(value)
|
||||||
|
case 4: // SLA
|
||||||
|
result = cpu.sla(value)
|
||||||
|
case 5: // SRA
|
||||||
|
result = cpu.sra(value)
|
||||||
|
case 6: // SLL (Undocumented)
|
||||||
|
result = cpu.sll(value)
|
||||||
|
case 7: // SRL
|
||||||
|
result = cpu.srl(value)
|
||||||
|
default:
|
||||||
|
result = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store result in memory
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeResetBitIndexedIY handles reset bit instructions for IY indexed addressing
|
||||||
|
func (cpu *CPU) executeResetBitIndexedIY(opcode byte, addr uint16, value byte) int {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
result := cpu.res(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 23
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeSetBitIndexedIY handles set bit instructions for IY indexed addressing
|
||||||
|
func (cpu *CPU) executeSetBitIndexedIY(opcode byte, addr uint16, value byte) int {
|
||||||
|
bitNum := uint((opcode >> 3) & 0x07)
|
||||||
|
reg := opcode & 0x07
|
||||||
|
|
||||||
|
result := cpu.set(bitNum, value)
|
||||||
|
cpu.Memory.WriteByte(addr, result)
|
||||||
|
|
||||||
|
// Store result in register if needed (except for (HL) case)
|
||||||
|
if reg != 6 { // reg 6 is (HL) - no register store needed
|
||||||
|
switch reg {
|
||||||
|
case 0:
|
||||||
|
cpu.B = result
|
||||||
|
case 1:
|
||||||
|
cpu.C = result
|
||||||
|
case 2:
|
||||||
|
cpu.D = result
|
||||||
|
case 3:
|
||||||
|
cpu.E = result
|
||||||
|
case 4:
|
||||||
|
cpu.H = result
|
||||||
|
case 5:
|
||||||
|
cpu.L = result
|
||||||
|
case 7:
|
||||||
|
cpu.A = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 23
|
||||||
|
}
|
||||||
67
flow_conditionals_simple_test.go
Normal file
67
flow_conditionals_simple_test.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Basic conditional flow timing: JR cc, RET cc, CALL cc
|
||||||
|
func TestJRcc_RETcc_CALLcc_Timing_Basics(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
|
||||||
|
// Make a small program space
|
||||||
|
// OR A (keeps Z=0), then JR Z,+2 (not taken), then XOR A (Z=1), JR NZ,+2 (not taken), JR Z,+2 (taken)
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0xB7, // OR A (A starts FF; Z=0)
|
||||||
|
0x28, 0x02, // JR Z, +2 -> not taken (7)
|
||||||
|
0xAF, // XOR A -> A=0 Z=1
|
||||||
|
0x20, 0x02, // JR NZ, +2 -> not taken now (7)
|
||||||
|
0x28, 0x02, // JR Z, +2 -> taken (12)
|
||||||
|
0x00, 0x00,
|
||||||
|
)
|
||||||
|
cpu.A = 0xff // HUMAN: my cpu not set A to FF
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, mustStep(t, cpu), 7, "JR Z not taken")
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, mustStep(t, cpu), 7, "JR NZ not taken")
|
||||||
|
assertEq(t, mustStep(t, cpu), 12, "JR Z taken")
|
||||||
|
|
||||||
|
// RET cc: make a simple CALL, then set flags so condition is false/true
|
||||||
|
cpu, mem, _ = testCPU()
|
||||||
|
cpu.SP = 0xFFFE
|
||||||
|
// CALL next; place RET C (D8) and RET NC (D0) in two spots and test both timings
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xCD, 0x06, 0x00) // CALL 0006
|
||||||
|
mem.WriteByte(0x0006, 0xD8) // RET C
|
||||||
|
cpu.SetFlag(FLAG_C, false)
|
||||||
|
mustStep(t, cpu) // CALL
|
||||||
|
// RET C (not taken): 5 cycles
|
||||||
|
assertEq(t, mustStep(t, cpu), 5, "RET C not taken")
|
||||||
|
|
||||||
|
// Put RET NC; set C so taken path triggers
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xCD, 0x06, 0x00)
|
||||||
|
mem.WriteByte(0x0006, 0xD0) // RET NC
|
||||||
|
cpu.SetFlag(FLAG_C, false)
|
||||||
|
pcBefore := cpu.PC
|
||||||
|
mustStep(t, cpu) // CALL
|
||||||
|
// RET NC (taken): 11 cycles
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 11, "RET NC taken")
|
||||||
|
assertEq(t, cpu.PC, pcBefore+3, "Returned to next instruction after CALL")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDJNZ_Taken(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x06, 0x02, // LD B,2
|
||||||
|
0x10, 0x02, // DJNZ +2 (B->1, taken)
|
||||||
|
)
|
||||||
|
mustStep(t, cpu) // LD B,2
|
||||||
|
assertEq(t, mustStep(t, cpu), 13, "DJNZ taken")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDJNZ_NotTaken(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x06, 0x01, // LD B,1
|
||||||
|
0x10, 0x02, // DJNZ +2 (B->0, not taken)
|
||||||
|
)
|
||||||
|
mustStep(t, cpu) // LD B,1
|
||||||
|
assertEq(t, mustStep(t, cpu), 8, "DJNZ not taken")
|
||||||
|
}
|
||||||
60
flow_timing_test.go
Normal file
60
flow_timing_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJR_taken_vs_not_taken_cycles(t *testing.T) {
|
||||||
|
// JR Z,d : when Z set -> taken 12 cycles; when not -> 7 cycles
|
||||||
|
// We'll do: OR A (so Z=0) then JR Z,+2 (not taken); then XOR A (Z=1) then JR Z,+2 (taken)
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0xB7, // OR A -> Z depends on A; initial A=FF (from New), so OR A keeps Z=0
|
||||||
|
0x28, 0x02, // JR Z,+2 (not taken)
|
||||||
|
0xAF, // XOR A -> A=0, Z=1
|
||||||
|
0x28, 0x02, // JR Z,+2 (taken)
|
||||||
|
0x00, 0x00, // padding
|
||||||
|
)
|
||||||
|
cpu.A = 0xff // HUMAN : my cpu not set A to ff
|
||||||
|
// OR A
|
||||||
|
mustStep(t, cpu)
|
||||||
|
// JR Z (not taken)
|
||||||
|
c1 := mustStep(t, cpu)
|
||||||
|
assertEq(t, c1, 7, "JR Z not taken cycles")
|
||||||
|
// XOR A
|
||||||
|
mustStep(t, cpu)
|
||||||
|
// JR Z (taken)
|
||||||
|
c2 := mustStep(t, cpu)
|
||||||
|
assertEq(t, c2, 12, "JR Z taken cycles")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHALTBehavior(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x76) // HALT
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "HALT cycles first step")
|
||||||
|
assertEq(t, cpu.HALT, true, "CPU halted")
|
||||||
|
// Subsequent step should still be 4 cycles and not change PC
|
||||||
|
pc := cpu.PC
|
||||||
|
c2 := mustStep(t, cpu)
|
||||||
|
assertEq(t, c2, 4, "HALT cycles subsequent step")
|
||||||
|
assertEq(t, cpu.PC, pc, "PC stable while halted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MEMPTR correctness for a few representative instructions.
|
||||||
|
func TestMEMPTRUpdates(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetBC(0x1234)
|
||||||
|
cpu.A = 0x9A
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x02, // LD (BC),A [MEMPTR=(A<<8)|((BC+1)&0xFF)]
|
||||||
|
0x0A, // LD A,(BC) [MEMPTR=BC+1]
|
||||||
|
)
|
||||||
|
|
||||||
|
mustStep(t, cpu)
|
||||||
|
expected := (uint16(cpu.A) << 8) | ((cpu.GetBC() + 1) & 0x00FF)
|
||||||
|
assertEq(t, cpu.MEMPTR, expected, "MEMPTR after LD (BC),A")
|
||||||
|
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.MEMPTR, cpu.GetBC()+1, "MEMPTR after LD A,(BC)")
|
||||||
|
}
|
||||||
798
fuse_test.go
Normal file
798
fuse_test.go
Normal file
@@ -0,0 +1,798 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
disasm "git.tygh.ru/kiltum/emuz80disasmgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test represents a single Z80 test case
|
||||||
|
type Test struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Input TestInput
|
||||||
|
Expected TestExpected
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInput represents the input data for a test
|
||||||
|
type TestInput struct {
|
||||||
|
Registers []string
|
||||||
|
I string
|
||||||
|
R string
|
||||||
|
IFF1 string
|
||||||
|
IFF2 string
|
||||||
|
IM string
|
||||||
|
Halted string
|
||||||
|
TStates string
|
||||||
|
MemorySetup []MemoryBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExpected represents the expected output for a test
|
||||||
|
type TestExpected struct {
|
||||||
|
Events []Event
|
||||||
|
FinalState []string
|
||||||
|
I string
|
||||||
|
R string
|
||||||
|
IFF1 string
|
||||||
|
IFF2 string
|
||||||
|
IM string
|
||||||
|
Halted string
|
||||||
|
TStates string
|
||||||
|
ChangedMemory []MemoryBlock
|
||||||
|
}
|
||||||
|
|
||||||
|
// MemoryBlock represents a block of memory
|
||||||
|
type MemoryBlock struct {
|
||||||
|
Address string
|
||||||
|
Bytes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleIO implements the IO interface for testing
|
||||||
|
type SimpleIO struct {
|
||||||
|
ports [65536]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (io *SimpleIO) ReadPort(port uint16) byte {
|
||||||
|
// For IN instructions, return the high byte of the port address
|
||||||
|
// This matches the test expectations where A register value is used as high byte
|
||||||
|
return byte(port >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (io *SimpleIO) WritePort(port uint16, value byte) {
|
||||||
|
io.ports[port] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (io *SimpleIO) CheckInterrupt() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadMemoryBlocks loads memory blocks into the memory
|
||||||
|
func (m *mockMemory) LoadMemoryBlocks(blocks []MemoryBlock, t *testing.T) error {
|
||||||
|
for _, block := range blocks {
|
||||||
|
// Parse the address
|
||||||
|
addr, err := strconv.ParseUint(block.Address, 16, 16)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid address %s: %v", block.Address, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load each byte
|
||||||
|
baseAddr := uint16(addr)
|
||||||
|
for i, byteStr := range block.Bytes {
|
||||||
|
value, err := strconv.ParseUint(byteStr, 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid byte value %s: %v", byteStr, err)
|
||||||
|
}
|
||||||
|
address := baseAddr + uint16(i)
|
||||||
|
byteValue := byte(value)
|
||||||
|
m.WriteByte(address, byteValue)
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
if t != nil {
|
||||||
|
t.Logf("Load: %04X->%02X", address, byteValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event represents a single event in the test execution
|
||||||
|
type Event struct {
|
||||||
|
Time string
|
||||||
|
Type string
|
||||||
|
Address string
|
||||||
|
Data string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagNames represents the bit names for the F register
|
||||||
|
var FlagNames = []string{"S", "Z", "5", "H", "3", "P/V", "N", "C"}
|
||||||
|
|
||||||
|
// decodeF converts F register value to bit representation string
|
||||||
|
func decodeF(fReg string) string {
|
||||||
|
// Convert hex string to integer
|
||||||
|
fValue, err := strconv.ParseUint(fReg, 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
return "Invalid F register"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode F register bits (SZ5H3PNC)
|
||||||
|
bits := make([]byte, 8)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
if fValue&(1<<(7-i)) != 0 {
|
||||||
|
bits[i] = '1'
|
||||||
|
} else {
|
||||||
|
bits[i] = '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the flag representation
|
||||||
|
flagRepr := ""
|
||||||
|
for i, name := range FlagNames {
|
||||||
|
flagRepr += fmt.Sprintf(" %s:%c", name, bits[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s (%s)", fReg, strings.Trim(flagRepr, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHex parses a hex string to uint16
|
||||||
|
func parseHex(hexStr string) (uint16, error) {
|
||||||
|
val, err := strconv.ParseUint(hexStr, 16, 16)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return uint16(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBool parses a string to bool
|
||||||
|
func parseBool(boolStr string) bool {
|
||||||
|
return boolStr == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseByte parses a hex string to byte
|
||||||
|
func parseByte(hexStr string) (byte, error) {
|
||||||
|
val, err := strconv.ParseUint(hexStr, 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return byte(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterInfo holds information about a register for loading/comparison
|
||||||
|
type RegisterInfo struct {
|
||||||
|
Name string
|
||||||
|
Index int
|
||||||
|
LoadFunc func(*CPU, uint16)
|
||||||
|
GetFunc func(*CPU) uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadRegisters loads register values from the test input into the CPU
|
||||||
|
func loadRegisters(cpu *CPU, registers []string, t *testing.T) {
|
||||||
|
if len(registers) < 13 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMap := []RegisterInfo{
|
||||||
|
{"AF", 0, func(c *CPU, v uint16) { c.SetAF(v) }, func(c *CPU) uint16 { return c.GetAF() }},
|
||||||
|
{"BC", 1, func(c *CPU, v uint16) { c.SetBC(v) }, func(c *CPU) uint16 { return c.GetBC() }},
|
||||||
|
{"DE", 2, func(c *CPU, v uint16) { c.SetDE(v) }, func(c *CPU) uint16 { return c.GetDE() }},
|
||||||
|
{"HL", 3, func(c *CPU, v uint16) { c.SetHL(v) }, func(c *CPU) uint16 { return c.GetHL() }},
|
||||||
|
{"AF'", 4, func(c *CPU, v uint16) { c.SetAF_(v) }, func(c *CPU) uint16 { return c.GetAF_() }},
|
||||||
|
{"BC'", 5, func(c *CPU, v uint16) { c.SetBC_(v) }, func(c *CPU) uint16 { return c.GetBC_() }},
|
||||||
|
{"DE'", 6, func(c *CPU, v uint16) { c.SetDE_(v) }, func(c *CPU) uint16 { return c.GetDE_() }},
|
||||||
|
{"HL'", 7, func(c *CPU, v uint16) { c.SetHL_(v) }, func(c *CPU) uint16 { return c.GetHL_() }},
|
||||||
|
{"IX", 8, func(c *CPU, v uint16) { c.IX = v }, func(c *CPU) uint16 { return c.IX }},
|
||||||
|
{"IY", 9, func(c *CPU, v uint16) { c.IY = v }, func(c *CPU) uint16 { return c.IY }},
|
||||||
|
{"SP", 10, func(c *CPU, v uint16) { c.SP = v }, func(c *CPU) uint16 { return c.SP }},
|
||||||
|
{"PC", 11, func(c *CPU, v uint16) { c.PC = v }, func(c *CPU) uint16 { return c.PC }},
|
||||||
|
{"MEMPTR", 12, func(c *CPU, v uint16) { c.MEMPTR = v }, func(c *CPU) uint16 { return c.MEMPTR }},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, regInfo := range registerMap {
|
||||||
|
if regInfo.Index < len(registers) {
|
||||||
|
if value, err := parseHex(registers[regInfo.Index]); err == nil {
|
||||||
|
regInfo.LoadFunc(cpu, value)
|
||||||
|
if t != nil {
|
||||||
|
//t.Logf("Loaded %s: 0x%04X", regInfo.Name, value)
|
||||||
|
}
|
||||||
|
} else if t != nil {
|
||||||
|
t.Logf("Failed to parse %s register value: %s", regInfo.Name, registers[regInfo.Index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareRegisters compares CPU registers with expected values and returns mismatches
|
||||||
|
func compareRegisters(cpu *CPU, expected []string, t *testing.T) []string {
|
||||||
|
if len(expected) < 13 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var mismatches []string
|
||||||
|
|
||||||
|
registerMap := []RegisterInfo{
|
||||||
|
{"AF", 0, nil, func(c *CPU) uint16 { return c.GetAF() }},
|
||||||
|
{"BC", 1, nil, func(c *CPU) uint16 { return c.GetBC() }},
|
||||||
|
{"DE", 2, nil, func(c *CPU) uint16 { return c.GetDE() }},
|
||||||
|
{"HL", 3, nil, func(c *CPU) uint16 { return c.GetHL() }},
|
||||||
|
{"AF'", 4, nil, func(c *CPU) uint16 { return c.GetAF_() }},
|
||||||
|
{"BC'", 5, nil, func(c *CPU) uint16 { return c.GetBC_() }},
|
||||||
|
{"DE'", 6, nil, func(c *CPU) uint16 { return c.GetDE_() }},
|
||||||
|
{"HL'", 7, nil, func(c *CPU) uint16 { return c.GetHL_() }},
|
||||||
|
{"IX", 8, nil, func(c *CPU) uint16 { return c.IX }},
|
||||||
|
{"IY", 9, nil, func(c *CPU) uint16 { return c.IY }},
|
||||||
|
{"SP", 10, nil, func(c *CPU) uint16 { return c.SP }},
|
||||||
|
{"PC", 11, nil, func(c *CPU) uint16 { return c.PC }},
|
||||||
|
{"MEMPTR", 12, nil, func(c *CPU) uint16 { return c.MEMPTR }},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, regInfo := range registerMap {
|
||||||
|
if regInfo.Index < len(expected) {
|
||||||
|
if expectedValue, err := parseHex(expected[regInfo.Index]); err == nil {
|
||||||
|
actualValue := regInfo.GetFunc(cpu)
|
||||||
|
if actualValue != expectedValue {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("%s: expected 0x%04X, got 0x%04X", regInfo.Name, expectedValue, actualValue))
|
||||||
|
|
||||||
|
// Add F flag bit details for AF register
|
||||||
|
if regInfo.Name == "AF" || regInfo.Name == "AF'" {
|
||||||
|
expectedF := fmt.Sprintf("%02X", expectedValue&0xFF)
|
||||||
|
actualF := fmt.Sprintf("%02X", actualValue&0xFF)
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf(" Expected F: %s", decodeF(expectedF)))
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf(" Actual F: %s", decodeF(actualF)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if t != nil {
|
||||||
|
t.Logf("Failed to parse expected %s register value: %s", regInfo.Name, expected[regInfo.Index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mismatches
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeInstructions decodes instructions starting from address 0x0000
|
||||||
|
// and logs them until a NOP instruction is encountered (after address 0) or safety limit is reached
|
||||||
|
func decodeInstructions(d *disasm.Disassembler, memory *mockMemory, t *testing.T) {
|
||||||
|
address := uint16(0x0000)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Create a buffer with the bytes at the current address
|
||||||
|
buffer := make([]byte, 4) // Read up to 4 bytes for longer instructions
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
buffer[i] = memory.ReadByte(address + uint16(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the instruction at the current address
|
||||||
|
instruction, err := d.Decode(buffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to decode instruction at 0x%04X: %v", address, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop decoding if we encounter a NOP instruction and address > 0
|
||||||
|
if instruction.Mnemonic == "NOP" && address > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the decoded instruction
|
||||||
|
//t.Logf("Decoded instruction at 0x%04X: %s", address, instruction.Mnemonic)
|
||||||
|
|
||||||
|
// Move to the next instruction
|
||||||
|
address += uint16(instruction.Length)
|
||||||
|
|
||||||
|
// Safety check to prevent infinite loops
|
||||||
|
if address > 0x1000 {
|
||||||
|
t.Logf("Stopping decode at 0x%04X: Reached safety limit", address)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeInstructions executes CPU instructions until reaching the expected T-states
|
||||||
|
func executeInstructions(cpu *CPU, memory *mockMemory, d *disasm.Disassembler, expectedTStates int, t *testing.T) {
|
||||||
|
totalTicks := 0
|
||||||
|
for totalTicks < expectedTStates {
|
||||||
|
// Capture the PC before executing the instruction for proper logging
|
||||||
|
pcBefore := cpu.PC
|
||||||
|
|
||||||
|
// Read bytes at current PC for disassembly before execution
|
||||||
|
buffer := make([]byte, 4) // Read up to 4 bytes for longer instructions
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
buffer[i] = memory.ReadByte(cpu.PC + uint16(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the instruction at the current address before execution
|
||||||
|
instruction, err := d.Decode(buffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Failed to decode instruction at 0x%04X: %v", cpu.PC, err)
|
||||||
|
// Continue execution even if we can't decode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the instruction
|
||||||
|
tickCount := cpu.ExecuteOneInstruction()
|
||||||
|
totalTicks += tickCount
|
||||||
|
|
||||||
|
// Log the executed instruction
|
||||||
|
if err == nil && instruction != nil {
|
||||||
|
t.Logf("Executed %s at 0x%04X, ticks: %d, total: %d/%d", instruction.Mnemonic, pcBefore, tickCount, totalTicks, expectedTStates)
|
||||||
|
} else {
|
||||||
|
t.Logf("Executed instruction at 0x%04X, ticks: %d, total: %d/%d", pcBefore, tickCount, totalTicks, expectedTStates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareInternalState compares CPU internal state with expected values and returns mismatches
|
||||||
|
func compareInternalState(cpu *CPU, expected TestExpected, t *testing.T) []string {
|
||||||
|
var mismatches []string
|
||||||
|
|
||||||
|
// Compare internal state
|
||||||
|
if expectedI, err := parseByte(expected.I); err == nil {
|
||||||
|
if cpu.I != expectedI {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("I: expected 0x%02X, got 0x%02X", expectedI, cpu.I))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expectedR, err := parseByte(expected.R); err == nil {
|
||||||
|
if cpu.R != expectedR {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("R: expected 0x%02X, got 0x%02X", expectedR, cpu.R))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectedIFF1 := parseBool(expected.IFF1)
|
||||||
|
if cpu.IFF1 != expectedIFF1 {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("IFF1: expected %t, got %t", expectedIFF1, cpu.IFF1))
|
||||||
|
}
|
||||||
|
expectedIFF2 := parseBool(expected.IFF2)
|
||||||
|
if cpu.IFF2 != expectedIFF2 {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("IFF2: expected %t, got %t", expectedIFF2, cpu.IFF2))
|
||||||
|
}
|
||||||
|
if expectedIM, err := parseByte(expected.IM); err == nil {
|
||||||
|
if cpu.IM != expectedIM {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("IM: expected 0x%02X, got 0x%02X", expectedIM, cpu.IM))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectedHALT := parseBool(expected.Halted)
|
||||||
|
if cpu.HALT != expectedHALT {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("HALT: expected %t, got %t", expectedHALT, cpu.HALT))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mismatches
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareMemory compares memory contents with expected values and returns mismatches
|
||||||
|
func compareMemory(memory *mockMemory, expected []MemoryBlock, t *testing.T) []string {
|
||||||
|
var mismatches []string
|
||||||
|
|
||||||
|
for _, block := range expected {
|
||||||
|
// Parse the address
|
||||||
|
addr, err := strconv.ParseUint(block.Address, 16, 16)
|
||||||
|
if err != nil {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("Invalid address %s in expected memory block", block.Address))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare each byte and group consecutive mismatches
|
||||||
|
baseAddr := uint16(addr)
|
||||||
|
var expectedBytes []string
|
||||||
|
var actualBytes []string
|
||||||
|
startAddress := baseAddr
|
||||||
|
|
||||||
|
for i, expectedByteStr := range block.Bytes {
|
||||||
|
expectedByte, err := strconv.ParseUint(expectedByteStr, 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("Invalid byte value %s in expected memory block", expectedByteStr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
address := baseAddr + uint16(i)
|
||||||
|
actualByte := memory.ReadByte(address)
|
||||||
|
|
||||||
|
if actualByte != byte(expectedByte) {
|
||||||
|
expectedBytes = append(expectedBytes, fmt.Sprintf("%02X", byte(expectedByte)))
|
||||||
|
actualBytes = append(actualBytes, fmt.Sprintf("%02X", actualByte))
|
||||||
|
} else {
|
||||||
|
// If we have accumulated mismatches and hit a match, flush the accumulated mismatches
|
||||||
|
if len(expectedBytes) > 0 {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("Memory at 0x%04X: expected %s\n got %s", startAddress, strings.Join(expectedBytes, " "), strings.Join(actualBytes, " ")))
|
||||||
|
expectedBytes = []string{}
|
||||||
|
actualBytes = []string{}
|
||||||
|
startAddress = address + 1
|
||||||
|
} else {
|
||||||
|
startAddress = address + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining mismatches
|
||||||
|
if len(expectedBytes) > 0 {
|
||||||
|
mismatches = append(mismatches, fmt.Sprintf("Exp: 0x%04X %s\nCur: %s", startAddress, strings.Join(expectedBytes, " "), strings.Join(actualBytes, " ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mismatches
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDebugInfo creates debug information for test failures
|
||||||
|
func buildDebugInfo(initialRegisters []string, cpu *CPU, expected []string) []string {
|
||||||
|
var debugInfo []string
|
||||||
|
|
||||||
|
// Add header with register names
|
||||||
|
debugInfo = append(debugInfo, " AF BC DE HL AF' BC' DE' HL' IX IY SP PC MEMPTR")
|
||||||
|
|
||||||
|
// Add initial register state in one line
|
||||||
|
initialLine := ""
|
||||||
|
for i := 0; i < 13; i++ {
|
||||||
|
if i < len(initialRegisters) {
|
||||||
|
initialLine += fmt.Sprintf("%-6s", initialRegisters[i])
|
||||||
|
} else {
|
||||||
|
initialLine += "0000 "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugInfo = append(debugInfo, "Ini: "+initialLine)
|
||||||
|
|
||||||
|
// Add current emulator register state in one line
|
||||||
|
currentLine := fmt.Sprintf(
|
||||||
|
"%04X %04X %04X %04X %04X %04X %04X %04X %04X %04X %04X %04X %04X",
|
||||||
|
cpu.GetAF(), cpu.GetBC(), cpu.GetDE(), cpu.GetHL(),
|
||||||
|
cpu.GetAF_(), cpu.GetBC_(), cpu.GetDE_(), cpu.GetHL_(),
|
||||||
|
cpu.IX, cpu.IY, cpu.SP, cpu.PC, cpu.MEMPTR)
|
||||||
|
debugInfo = append(debugInfo, "Cur: "+currentLine)
|
||||||
|
|
||||||
|
// Add expected register state in one line
|
||||||
|
expectedLine := ""
|
||||||
|
for i := 0; i < 13; i++ {
|
||||||
|
if i < len(expected) {
|
||||||
|
expectedLine += fmt.Sprintf("%-6s", strings.ToUpper(expected[i]))
|
||||||
|
} else {
|
||||||
|
expectedLine += "0000 "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugInfo = append(debugInfo, "Exp: "+expectedLine)
|
||||||
|
|
||||||
|
return debugInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeZ80Test executes a single Z80 test case
|
||||||
|
func executeZ80Test(t *testing.T, test Test) {
|
||||||
|
// Create memory and IO instances
|
||||||
|
memory := &mockMemory{}
|
||||||
|
io := &SimpleIO{}
|
||||||
|
|
||||||
|
// Load initial memory state
|
||||||
|
if err := memory.LoadMemoryBlocks(test.Input.MemorySetup, t); err != nil {
|
||||||
|
t.Errorf("Failed to load memory blocks: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create CPU instance
|
||||||
|
cpu := New(memory, io)
|
||||||
|
|
||||||
|
// Capture initial register state for debugging
|
||||||
|
initialRegisters := make([]string, 13)
|
||||||
|
copy(initialRegisters, test.Input.Registers)
|
||||||
|
|
||||||
|
// Load input registers and state
|
||||||
|
loadRegisters(cpu, test.Input.Registers, t)
|
||||||
|
|
||||||
|
// Parse internal state
|
||||||
|
if i, err := parseByte(test.Input.I); err == nil {
|
||||||
|
cpu.I = i
|
||||||
|
}
|
||||||
|
if r, err := parseByte(test.Input.R); err == nil {
|
||||||
|
cpu.R = r
|
||||||
|
}
|
||||||
|
cpu.IFF1 = parseBool(test.Input.IFF1)
|
||||||
|
cpu.IFF2 = parseBool(test.Input.IFF2)
|
||||||
|
if im, err := parseByte(test.Input.IM); err == nil {
|
||||||
|
cpu.IM = im
|
||||||
|
}
|
||||||
|
cpu.HALT = parseBool(test.Input.Halted)
|
||||||
|
|
||||||
|
// Create disassembler and decode instructions starting at address 0x0000
|
||||||
|
d := disasm.New()
|
||||||
|
decodeInstructions(d, memory, t)
|
||||||
|
|
||||||
|
// Parse T-states from input (this is the actual tick count to use)
|
||||||
|
inputTStates, err := strconv.Atoi(test.Input.TStates)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to parse input T-states: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute instructions until we reach the input T-states
|
||||||
|
executeInstructions(cpu, memory, d, inputTStates, t)
|
||||||
|
|
||||||
|
// Compare emulator registers with expected values
|
||||||
|
matches := true
|
||||||
|
var mismatchDetails []string
|
||||||
|
|
||||||
|
if len(test.Expected.FinalState) >= 13 {
|
||||||
|
// Compare registers using the helper function
|
||||||
|
registerMismatches := compareRegisters(cpu, test.Expected.FinalState, t)
|
||||||
|
if len(registerMismatches) > 0 {
|
||||||
|
matches = false
|
||||||
|
mismatchDetails = append(mismatchDetails, registerMismatches...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare internal state
|
||||||
|
internalStateMismatches := compareInternalState(cpu, test.Expected, t)
|
||||||
|
if len(internalStateMismatches) > 0 {
|
||||||
|
matches = false
|
||||||
|
mismatchDetails = append(mismatchDetails, internalStateMismatches...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare memory contents
|
||||||
|
memoryMismatches := compareMemory(memory, test.Expected.ChangedMemory, t)
|
||||||
|
if len(memoryMismatches) > 0 {
|
||||||
|
matches = false
|
||||||
|
mismatchDetails = append(mismatchDetails, memoryMismatches...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
t.Logf("Test %s PASSED: All registers and memory match expected values", test.Name)
|
||||||
|
} else {
|
||||||
|
// Add debug information when test fails
|
||||||
|
debugInfo := buildDebugInfo(initialRegisters, cpu, test.Expected.FinalState)
|
||||||
|
|
||||||
|
// Combine all information
|
||||||
|
allDetails := append(mismatchDetails, debugInfo...)
|
||||||
|
t.Errorf("Test %s FAILED:\n%s", test.Name, strings.Join(allDetails, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readTests reads all tests from tests.in file
|
||||||
|
func readTests() ([]Test, error) {
|
||||||
|
file, err := os.Open("testdata/tests.in")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open tests.in: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var tests []Test
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
var currentTest *Test
|
||||||
|
var readingMemory bool
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if line == "" || line == "-1" {
|
||||||
|
if currentTest != nil && readingMemory {
|
||||||
|
readingMemory = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a test name (starts with alphanumeric characters)
|
||||||
|
if isTestName(line) && !readingMemory {
|
||||||
|
// Save previous test if exists
|
||||||
|
if currentTest != nil {
|
||||||
|
tests = append(tests, *currentTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new test
|
||||||
|
currentTest = &Test{
|
||||||
|
Name: line,
|
||||||
|
Description: line,
|
||||||
|
Input: TestInput{},
|
||||||
|
Expected: TestExpected{},
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse registers line
|
||||||
|
if currentTest != nil && len(strings.Fields(line)) >= 13 && !readingMemory {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 13 {
|
||||||
|
currentTest.Input.Registers = fields[:13]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse flags line (6 or 7 fields, not ending with -1)
|
||||||
|
if currentTest != nil && len(strings.Fields(line)) >= 6 && !strings.HasSuffix(strings.TrimSpace(line), "-1") && !readingMemory {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 6 {
|
||||||
|
currentTest.Input.I = fields[0]
|
||||||
|
currentTest.Input.R = fields[1]
|
||||||
|
currentTest.Input.IFF1 = fields[2]
|
||||||
|
currentTest.Input.IFF2 = fields[3]
|
||||||
|
currentTest.Input.IM = fields[4]
|
||||||
|
currentTest.Input.Halted = fields[5]
|
||||||
|
if len(fields) > 6 {
|
||||||
|
currentTest.Input.TStates = fields[6]
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse memory setup (lines ending with -1)
|
||||||
|
if currentTest != nil && strings.Contains(line, " ") && strings.HasSuffix(strings.TrimSpace(line), "-1") && !strings.HasPrefix(line, " ") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
// Memory block
|
||||||
|
address := fields[0]
|
||||||
|
bytes := fields[1 : len(fields)-1]
|
||||||
|
|
||||||
|
currentTest.Input.MemorySetup = append(currentTest.Input.MemorySetup, MemoryBlock{
|
||||||
|
Address: address,
|
||||||
|
Bytes: bytes,
|
||||||
|
})
|
||||||
|
readingMemory = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add last test
|
||||||
|
if currentTest != nil {
|
||||||
|
tests = append(tests, *currentTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading tests.in: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTestName checks if a line is a test name
|
||||||
|
func isTestName(line string) bool {
|
||||||
|
if line == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Simple check: test names are usually alphanumeric with underscores
|
||||||
|
for _, r := range line {
|
||||||
|
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// readExpectedTests reads expected results from tests.expected
|
||||||
|
func readExpectedTests() (map[string]TestExpected, error) {
|
||||||
|
file, err := os.Open("testdata/tests.expected")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open tests.expected: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
expected := make(map[string]TestExpected)
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
var currentName string
|
||||||
|
var readingEvents bool
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimRight(scanner.Text(), " \t") // Keep leading spaces for event detection
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a test name (first non-indented line)
|
||||||
|
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && isTestName(strings.TrimSpace(line)) {
|
||||||
|
currentName = strings.TrimSpace(line)
|
||||||
|
expected[currentName] = TestExpected{}
|
||||||
|
readingEvents = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse events (lines starting with spaces and numbers)
|
||||||
|
if readingEvents && strings.HasPrefix(line, " ") && len(strings.Fields(strings.TrimSpace(line))) >= 3 {
|
||||||
|
fields := strings.Fields(strings.TrimSpace(line))
|
||||||
|
if len(fields) >= 3 {
|
||||||
|
event := Event{
|
||||||
|
Time: fields[0],
|
||||||
|
Type: fields[1],
|
||||||
|
Address: fields[2],
|
||||||
|
}
|
||||||
|
if len(fields) > 3 {
|
||||||
|
event.Data = fields[3]
|
||||||
|
}
|
||||||
|
// Add event to the current test
|
||||||
|
temp := expected[currentName]
|
||||||
|
temp.Events = append(temp.Events, event)
|
||||||
|
expected[currentName] = temp
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're transitioning from events to final state (registers line)
|
||||||
|
if (readingEvents || len(expected[currentName].Events) == 0) && len(strings.Fields(line)) >= 13 {
|
||||||
|
readingEvents = false
|
||||||
|
|
||||||
|
// Parse final registers
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 13 {
|
||||||
|
temp := expected[currentName]
|
||||||
|
temp.FinalState = fields[:13]
|
||||||
|
expected[currentName] = temp
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse changed memory
|
||||||
|
if strings.Contains(line, " ") && strings.HasSuffix(strings.TrimSpace(line), "-1") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
address := fields[0]
|
||||||
|
bytes := fields[1 : len(fields)-1]
|
||||||
|
|
||||||
|
temp := expected[currentName]
|
||||||
|
temp.ChangedMemory = append(temp.ChangedMemory, MemoryBlock{
|
||||||
|
Address: address,
|
||||||
|
Bytes: bytes,
|
||||||
|
})
|
||||||
|
expected[currentName] = temp
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
//fmt.Printf("Parsed memory block for test %s: address=%s, bytes=%v\n", currentName, address, bytes)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse final flags (the line after registers, with 7 fields)
|
||||||
|
// Only process this if we have final state and the line doesn't end with -1
|
||||||
|
if len(expected[currentName].FinalState) > 0 && len(strings.Fields(line)) >= 7 &&
|
||||||
|
!strings.HasPrefix(line, " ") && !strings.HasSuffix(strings.TrimSpace(line), "-1") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 7 {
|
||||||
|
temp := expected[currentName]
|
||||||
|
temp.I = fields[0]
|
||||||
|
temp.R = fields[1]
|
||||||
|
temp.IFF1 = fields[2]
|
||||||
|
temp.IFF2 = fields[3]
|
||||||
|
temp.IM = fields[4]
|
||||||
|
temp.Halted = fields[5]
|
||||||
|
temp.TStates = fields[6]
|
||||||
|
expected[currentName] = temp
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading tests.expected: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return expected, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestZ80 runs all Z80 tests
|
||||||
|
func TestFuse(t *testing.T) {
|
||||||
|
// Read input tests
|
||||||
|
inputTests, err := readTests()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read tests: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read expected results
|
||||||
|
expectedTests, err := readExpectedTests()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read expected results: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run each test
|
||||||
|
for _, test := range inputTests {
|
||||||
|
t.Run(test.Name, func(t *testing.T) {
|
||||||
|
// Find expected result for this test
|
||||||
|
expected, found := expectedTests[test.Name]
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("Expected result not found for test %s", test.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update test with expected data
|
||||||
|
test.Expected = expected
|
||||||
|
|
||||||
|
// Execute the test (will fail as this is a stub)
|
||||||
|
executeZ80Test(t, test)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.tygh.ru/kiltum/emuz80go
|
||||||
|
|
||||||
|
go 1.25.1
|
||||||
|
|
||||||
|
require git.tygh.ru/kiltum/emuz80disasmgo v1.0.1
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
git.tygh.ru/kiltum/emuz80disasmgo v1.0.1 h1:C09g6qV2WJ4PJZj+jgH0V0i3wBM2NlTVquxe6xvuN7Y=
|
||||||
|
git.tygh.ru/kiltum/emuz80disasmgo v1.0.1/go.mod h1:UAmAGvjaANolTy15wficDDt9G173wqjOYwxWhC1fEhs=
|
||||||
23
indexed_timing_more_test.go
Normal file
23
indexed_timing_more_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Extra coverage for IX/IY (HL-replacement) timings:
|
||||||
|
// - RES/SET on (IX+d)/(IY+d) should take 23 cycles via DDCB/FDCB.
|
||||||
|
func TestDDCB_SET_RES_TimingAndWriteback(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.IX = 0x3000
|
||||||
|
mem.WriteByte(0x3005, 0x00)
|
||||||
|
|
||||||
|
// DDCB 05 C6 = SET 0,(IX+5) (opcode C6 => SET 0,(HL) in CB space)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xDD, 0xCB, 0x05, 0xC6)
|
||||||
|
c1 := mustStep(t, cpu)
|
||||||
|
assertEq(t, mem.ReadByte(0x3005)&0x01, byte(1), "SET 0,(IX+5)")
|
||||||
|
assertEq(t, c1, 23, "cycles for DDCB SET on (IX+d)")
|
||||||
|
|
||||||
|
// DDCB 05 86 = RES 0,(IX+5)
|
||||||
|
loadProgram(cpu, mem, cpu.PC, 0xDD, 0xCB, 0x05, 0x86)
|
||||||
|
c2 := mustStep(t, cpu)
|
||||||
|
assertEq(t, mem.ReadByte(0x3005)&0x01, byte(0), "RES 0,(IX+5)")
|
||||||
|
assertEq(t, c2, 23, "cycles for DDCB RES on (IX+d)")
|
||||||
|
}
|
||||||
51
interrupts_tests.go
Normal file
51
interrupts_tests.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// EI delay: interrupt must not fire on the instruction immediately following EI.
|
||||||
|
func TestEI_Delay_BeforeInterruptAccept(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
// Program: EI; NOP; NOP (we'll be in IM1 so accept RST 38h)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xFB, 0x00, 0x00) // FB=EI
|
||||||
|
cpu.IM = 1
|
||||||
|
io.interrupt = true // interrupt line is asserted
|
||||||
|
|
||||||
|
// EI executes; IFF1 set; interrupts checked before enabling => no service yet
|
||||||
|
mustStep(t, cpu) // EI
|
||||||
|
pc1 := cpu.PC
|
||||||
|
mustStep(t, cpu) // NOP immediately after EI - still should not take INT
|
||||||
|
if cpu.PC != pc1+1 {
|
||||||
|
t.Fatalf("Interrupt fired too soon after EI; PC=%04X expected %04X", cpu.PC, pc1+1)
|
||||||
|
}
|
||||||
|
// Next step: now IFF1 is enabled and INT should be taken
|
||||||
|
mustStep(t, cpu)
|
||||||
|
if cpu.PC != 0x0038 {
|
||||||
|
t.Fatalf("IM1 should vector to 0038h, PC=%04X", cpu.PC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HALT exits on interrupt; cycles of the interrupt path are counted.
|
||||||
|
func TestHALT_Interrupted_ExitsAndVectors(t *testing.T) {
|
||||||
|
cpu, mem, io := testCPU()
|
||||||
|
cpu.IM = 1
|
||||||
|
cpu.IFF1, cpu.IFF2 = true, true
|
||||||
|
io.interrupt = true // interrupt line is asserted
|
||||||
|
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x76) // HALT
|
||||||
|
mustStep(t, cpu) // enter HALT
|
||||||
|
if !cpu.HALT {
|
||||||
|
t.Fatalf("CPU should be halted")
|
||||||
|
}
|
||||||
|
// Next step processes the interrupt and clears HALT
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
if cpu.HALT {
|
||||||
|
t.Fatalf("CPU should exit HALT on interrupt")
|
||||||
|
}
|
||||||
|
if cpu.PC != 0x0038 {
|
||||||
|
t.Fatalf("IM1 vector expected 0038h, got %04X", cpu.PC)
|
||||||
|
}
|
||||||
|
// Cycle count should match IM1 path (13)
|
||||||
|
if c != 13 {
|
||||||
|
t.Fatalf("Interrupt from HALT cycles got %d want 13", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
ld_r_a_test.go
Normal file
20
ld_r_a_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Clarify semantics of LD R,A (ED 4F): R should become exactly A (all 8 bits).
|
||||||
|
// This guards against accidental attempts to preserve R7 here.
|
||||||
|
func TestED_LD_R_A_CopiesAllBits(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Arrange: set A with a top bit pattern and verify R=A after ED 4F.
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x3E, 0x81, // LD A,81h
|
||||||
|
0xED, 0x4F, // LD R,A
|
||||||
|
)
|
||||||
|
cpu.R = 0x00
|
||||||
|
mustStep(t, cpu) // LD A,81
|
||||||
|
mustStep(t, cpu) // LD R,A
|
||||||
|
if cpu.R != 0x81 {
|
||||||
|
t.Errorf("LD R,A: got R=%02X want 81", cpu.R)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
ldi_ldir_tests.go
Normal file
43
ldi_ldir_tests.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// LDI/LDDR/LDIR correctness: registers, memory effects, PV from BC!=0, and timing totals.
|
||||||
|
func TestLDI_Registers_Flags_Memory(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
cpu.SetDE(0x4100)
|
||||||
|
cpu.SetBC(3)
|
||||||
|
mem.WriteByte(0x4000, 0x11)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xA0) // LDI
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 16, "LDI cycles")
|
||||||
|
assertEq(t, mem.ReadByte(0x4100), byte(0x11), "LDI moved byte")
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x4001), "HL++")
|
||||||
|
assertEq(t, cpu.GetDE(), uint16(0x4101), "DE++")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(2), "BC--")
|
||||||
|
assertFlag(t, cpu, FLAG_N, false, "N cleared")
|
||||||
|
assertFlag(t, cpu, FLAG_H, false, "H cleared")
|
||||||
|
assertFlag(t, cpu, FLAG_PV, true, "PV mirrors BC!=0")
|
||||||
|
|
||||||
|
// LDIR for 2 bytes: expect 21 + 16 = 37 cycles and final regs.
|
||||||
|
cpu, mem, _ = testCPU()
|
||||||
|
cpu.SetHL(0x5000)
|
||||||
|
cpu.SetDE(0x6000)
|
||||||
|
cpu.SetBC(2)
|
||||||
|
mem.WriteByte(0x5000, 0xAA)
|
||||||
|
mem.WriteByte(0x5001, 0xBB)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0xB0) // LDIR
|
||||||
|
total := 0
|
||||||
|
for {
|
||||||
|
total += mustStep(t, cpu)
|
||||||
|
if cpu.GetBC() == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEq(t, total, 37, "LDIR total cycles for 2 bytes")
|
||||||
|
assertEq(t, mem.ReadByte(0x6000), byte(0xAA), "LDIR first byte")
|
||||||
|
assertEq(t, mem.ReadByte(0x6001), byte(0xBB), "LDIR second byte")
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0x5002), "HL end")
|
||||||
|
assertEq(t, cpu.GetDE(), uint16(0x6002), "DE end")
|
||||||
|
}
|
||||||
99
loads_matrix_test.go
Normal file
99
loads_matrix_test.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Exhaustive LD r,r' matrix (register-register moves) + immediate/memory forms.
|
||||||
|
// Also verifies loads do NOT affect flags.
|
||||||
|
func TestLD_Register_Matrix_And_Immediates(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Prepare HL memory
|
||||||
|
cpu.SetHL(0x4000)
|
||||||
|
mem.WriteByte(0x4000, 0xA5)
|
||||||
|
|
||||||
|
// Fill registers with distinct values
|
||||||
|
cpu.A, cpu.B, cpu.C, cpu.D, cpu.E, cpu.H, cpu.L = 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77
|
||||||
|
flagsStart := cpu.F
|
||||||
|
|
||||||
|
// LD B,C ; LD D,E ; LD A,B ; LD L,H ; (skip HALT 0x76)
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x41, // LD B,C
|
||||||
|
0x53, // LD D,E
|
||||||
|
0x78, // LD A,B
|
||||||
|
0x6C, // LD L,H
|
||||||
|
0x06, 0x99, // LD B,99
|
||||||
|
0x36, 0xFE, // LD (HL),FE
|
||||||
|
0x7E, // LD A,(HL)
|
||||||
|
0x70, // LD (HL),B
|
||||||
|
)
|
||||||
|
|
||||||
|
// LD B,C
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 4, "LD r,r' cycles")
|
||||||
|
assertEq(t, cpu.B, cpu.C, "LD B,C value")
|
||||||
|
assertEq(t, cpu.F, flagsStart, "LD r,r' must not alter F")
|
||||||
|
|
||||||
|
// LD D,E
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.D, byte(0x55), "LD D,E value")
|
||||||
|
|
||||||
|
// LD A,B
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.A, cpu.B, "LD A,B value")
|
||||||
|
|
||||||
|
// LD L,H
|
||||||
|
mustStep(t, cpu)
|
||||||
|
assertEq(t, cpu.L, cpu.H, "LD L,H value")
|
||||||
|
|
||||||
|
// LD B,n
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD r,n cycles")
|
||||||
|
assertEq(t, cpu.B, byte(0x99), "LD B,n value")
|
||||||
|
|
||||||
|
// LD (HL),n
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 10, "LD (HL),n cycles")
|
||||||
|
assertEq(t, mem.ReadByte(cpu.GetHL()), byte(0xFE), "LD (HL),n stored")
|
||||||
|
|
||||||
|
// LD A,(HL)
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD r,(HL) cycles")
|
||||||
|
assertEq(t, cpu.A, byte(0xFE), "LD A,(HL) value")
|
||||||
|
|
||||||
|
// LD (HL),r
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD (HL),r cycles")
|
||||||
|
assertEq(t, mem.ReadByte(cpu.GetHL()), cpu.B, "LD (HL),B stored B")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLD_A_BC_DE_Basics(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.SetBC(0x1234)
|
||||||
|
cpu.SetDE(0x5678)
|
||||||
|
mem.WriteByte(0x1234, 0xAA)
|
||||||
|
mem.WriteByte(0x5678, 0xBB)
|
||||||
|
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x0A, // LD A,(BC)
|
||||||
|
0x1A, // LD A,(DE)
|
||||||
|
0x02, // LD (BC),A
|
||||||
|
0x12, // LD (DE),A
|
||||||
|
)
|
||||||
|
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD A,(BC) cycles")
|
||||||
|
assertEq(t, cpu.A, byte(0xAA), "LD A,(BC)")
|
||||||
|
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD A,(DE) cycles")
|
||||||
|
assertEq(t, cpu.A, byte(0xBB), "LD A,(DE)")
|
||||||
|
|
||||||
|
// LD (BC),A
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD (BC),A cycles")
|
||||||
|
assertEq(t, mem.ReadByte(0x1234), cpu.A, "LD (BC),A wrote A")
|
||||||
|
|
||||||
|
// LD (DE),A
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 7, "LD (DE),A cycles")
|
||||||
|
assertEq(t, mem.ReadByte(0x5678), cpu.A, "LD (DE),A wrote A")
|
||||||
|
}
|
||||||
1324
opcodes.go
Normal file
1324
opcodes.go
Normal file
File diff suppressed because it is too large
Load Diff
32
r_register_more_prefixes_test.go
Normal file
32
r_register_more_prefixes_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestR_Increments_On_ED_Neg(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xED, 0x44) // NEG
|
||||||
|
cpu.R = 0
|
||||||
|
r0 := cpu.R & 0x7F
|
||||||
|
mustStep(t, cpu)
|
||||||
|
r1 := cpu.R & 0x7F
|
||||||
|
// Expect 2 M1s: ED fetch after the initial opcode fetch
|
||||||
|
inc := int((r1 - r0) & 0x7F)
|
||||||
|
if inc != 2 {
|
||||||
|
t.Fatalf("R should increment by 2 for ED-prefixed single op, got %d", inc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestR_Increments_On_DD_DD_Nop(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// DD DD <anything> -- our executeDD returns 4 for a second DD as NOP;
|
||||||
|
// we only execute one Step here: first DD, then post-prefix M1 fetches second DD, then stop.
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xDD, 0xDD)
|
||||||
|
cpu.R = 0
|
||||||
|
r0 := cpu.R & 0x7F
|
||||||
|
mustStep(t, cpu)
|
||||||
|
r1 := cpu.R & 0x7F
|
||||||
|
// Should see 2 M1 increments (first DD fetch + second DD as post-prefix fetch)
|
||||||
|
if int((r1-r0)&0x7F) != 2 {
|
||||||
|
t.Fatalf("R should increment by 2 on DD DD, got %d", int((r1-r0)&0x7F))
|
||||||
|
}
|
||||||
|
}
|
||||||
19
r_register_prefix_tests.go
Normal file
19
r_register_prefix_tests.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// R must increment once per M1, including post-prefix opcode fetches.
|
||||||
|
func TestR_Increments_On_DDCB(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
cpu.IX = 0x2000
|
||||||
|
mem.WriteByte(0x2001, 0x01)
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0xDD, 0xCB, 0x01, 0x06) // RLC (IX+1)
|
||||||
|
cpu.R = 0
|
||||||
|
r0 := cpu.R & 0x7F
|
||||||
|
mustStep(t, cpu)
|
||||||
|
r1 := cpu.R & 0x7F
|
||||||
|
inc := int((r1 - r0) & 0x7F)
|
||||||
|
if inc != 3 {
|
||||||
|
t.Fatalf("R should increment by 3 M1 cycles (DD, CB, op), got %d", inc)
|
||||||
|
}
|
||||||
|
}
|
||||||
53
rp_loads_incdec_test.go
Normal file
53
rp_loads_incdec_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// 16-bit loads and increments/decrements.
|
||||||
|
func TestLD_rp_nn_AND_INC_DEC_rp(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
loadProgram(cpu, mem, 0x0000,
|
||||||
|
0x01, 0x34, 0x12, // LD BC,1234
|
||||||
|
0x11, 0x78, 0x56, // LD DE,5678
|
||||||
|
0x21, 0xCD, 0xAB, // LD HL,ABCD
|
||||||
|
0x31, 0xFE, 0xFF, // LD SP,FFFE
|
||||||
|
0x03, // INC BC
|
||||||
|
0x13, // INC DE
|
||||||
|
0x23, // INC HL
|
||||||
|
0x33, // INC SP
|
||||||
|
0x0B, // DEC BC
|
||||||
|
0x1B, // DEC DE
|
||||||
|
0x2B, // DEC HL
|
||||||
|
0x3B, // DEC SP
|
||||||
|
)
|
||||||
|
|
||||||
|
c := mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 10, "LD BC,nn cycles")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0x1234), "LD BC")
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 10, "LD DE,nn cycles")
|
||||||
|
assertEq(t, cpu.GetDE(), uint16(0x5678), "LD DE")
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 10, "LD HL,nn cycles")
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0xABCD), "LD HL")
|
||||||
|
c = mustStep(t, cpu)
|
||||||
|
assertEq(t, c, 10, "LD SP,nn cycles")
|
||||||
|
assertEq(t, cpu.SP, uint16(0xFFFE), "LD SP")
|
||||||
|
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "INC BC cycles")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0x1235), "INC BC")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "INC DE cycles")
|
||||||
|
assertEq(t, cpu.GetDE(), uint16(0x5679), "INC DE")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "INC HL cycles")
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0xABCE), "INC HL")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "INC SP cycles")
|
||||||
|
assertEq(t, cpu.SP, uint16(0xFFFF), "INC SP")
|
||||||
|
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "DEC BC cycles")
|
||||||
|
assertEq(t, cpu.GetBC(), uint16(0x1234), "DEC BC")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "DEC DE cycles")
|
||||||
|
assertEq(t, cpu.GetDE(), uint16(0x5678), "DEC DE")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "DEC HL cycles")
|
||||||
|
assertEq(t, cpu.GetHL(), uint16(0xABCD), "DEC HL")
|
||||||
|
assertEq(t, mustStep(t, cpu), 6, "DEC SP cycles")
|
||||||
|
assertEq(t, cpu.SP, uint16(0xFFFE), "DEC SP")
|
||||||
|
}
|
||||||
20
scf_ccf_cpl_xy_test.go
Normal file
20
scf_ccf_cpl_xy_test.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Ensure SCF, CCF, CPL set X/Y from A (regression locks).
|
||||||
|
func TestSCF_CCF_CPL_XY_FromA(t *testing.T) {
|
||||||
|
cpu, mem, _ := testCPU()
|
||||||
|
// Make A have both X/Y set -> e.g., 0x28
|
||||||
|
loadProgram(cpu, mem, 0x0000, 0x3E, 0x28, 0x37, 0x3F, 0x2F) // LD A,28 ; SCF ; CCF ; CPL
|
||||||
|
mustStep(t, cpu) // LD
|
||||||
|
mustStep(t, cpu) // SCF
|
||||||
|
assertFlag(t, cpu, FLAG_X, true, "SCF X from A")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, true, "SCF Y from A")
|
||||||
|
mustStep(t, cpu) // CCF
|
||||||
|
assertFlag(t, cpu, FLAG_X, true, "CCF X from A")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, true, "CCF Y from A")
|
||||||
|
mustStep(t, cpu) // CPL (A becomes ^A)
|
||||||
|
assertFlag(t, cpu, FLAG_X, (cpu.A&FLAG_X) != 0, "CPL X from new A")
|
||||||
|
assertFlag(t, cpu, FLAG_Y, (cpu.A&FLAG_Y) != 0, "CPL Y from new A")
|
||||||
|
}
|
||||||
117
test_helpers.go
Normal file
117
test_helpers.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package z80
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockMemory is a simple 64K RAM that satisfies Memory interface.
|
||||||
|
type mockMemory struct {
|
||||||
|
data [65536]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMemory) ReadByte(address uint16) byte { return m.data[address] }
|
||||||
|
func (m *mockMemory) WriteByte(address uint16, value byte) { m.data[address] = value }
|
||||||
|
func (m *mockMemory) ReadWord(address uint16) uint16 {
|
||||||
|
lo := m.data[address]
|
||||||
|
hi := m.data[address+1]
|
||||||
|
return (uint16(hi) << 8) | uint16(lo)
|
||||||
|
}
|
||||||
|
func (m *mockMemory) WriteWord(address uint16, value uint16) {
|
||||||
|
m.data[address] = byte(value)
|
||||||
|
m.data[address+1] = byte(value >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockIO is a trivial port device.
|
||||||
|
type mockIO struct {
|
||||||
|
lastOut map[uint16]byte
|
||||||
|
inVals map[uint16]byte
|
||||||
|
interrupt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockIO() *mockIO {
|
||||||
|
return &mockIO{lastOut: make(map[uint16]byte), inVals: make(map[uint16]byte), interrupt: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (io *mockIO) ReadPort(port uint16) byte { return io.inVals[port] }
|
||||||
|
func (io *mockIO) WritePort(port uint16, value byte) { io.lastOut[port] = value }
|
||||||
|
func (io *mockIO) CheckInterrupt() bool { return io.interrupt }
|
||||||
|
|
||||||
|
// testCPU creates a CPU with empty RAM/IO and PC=0, SP=0xFFFF.
|
||||||
|
func testCPU() (*CPU, *mockMemory, *mockIO) {
|
||||||
|
mem := &mockMemory{}
|
||||||
|
io := newMockIO()
|
||||||
|
cpu := New(mem, io)
|
||||||
|
cpu.SP = 0xFFFF
|
||||||
|
cpu.PC = 0x0000
|
||||||
|
return cpu, mem, io
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadProgram writes bytes at address and sets PC to that address.
|
||||||
|
func loadProgram(cpu *CPU, mem *mockMemory, addr uint16, bytes ...byte) {
|
||||||
|
for i, b := range bytes {
|
||||||
|
mem.WriteByte(addr+uint16(i), b)
|
||||||
|
}
|
||||||
|
cpu.PC = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustStep runs one instruction and logs an error if cycles <= 0.
|
||||||
|
// Returns cycles consumed.
|
||||||
|
func mustStep(t *testing.T, cpu *CPU) int {
|
||||||
|
c := cpu.ExecuteOneInstruction()
|
||||||
|
if c <= 0 {
|
||||||
|
t.Errorf("ExecuteOneInstruction returned invalid cycles: %d", c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert helper shortcuts (non-fatal).
|
||||||
|
func assertEq[T comparable](t *testing.T, got, want T, msg string) {
|
||||||
|
t.Helper()
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("%s: got %v, want %v", msg, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFlag(t *testing.T, cpu *CPU, flag byte, want bool, msg string) {
|
||||||
|
t.Helper()
|
||||||
|
if cpu.GetFlag(flag) != want {
|
||||||
|
t.Errorf("%s: flag 0x%02X got %v, want %v (F=%02X)", msg, flag, cpu.GetFlag(flag), want, cpu.F)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hexdump prints a compact hex + ascii line dump suitable for test logs.
|
||||||
|
func hexdump(p []byte, width int) string {
|
||||||
|
if width <= 0 {
|
||||||
|
width = 16
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for i := 0; i < len(p); i += width {
|
||||||
|
end := i + width
|
||||||
|
if end > len(p) {
|
||||||
|
end = len(p)
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf("%08X ", i))
|
||||||
|
for j := i; j < end; j++ {
|
||||||
|
b.WriteString(fmt.Sprintf("%02x ", p[j]))
|
||||||
|
}
|
||||||
|
for j := end; j < i+width; j++ {
|
||||||
|
b.WriteString(" ")
|
||||||
|
}
|
||||||
|
b.WriteString(" |")
|
||||||
|
for j := i; j < end; j++ {
|
||||||
|
c := p[j]
|
||||||
|
if c < 32 || c > 126 {
|
||||||
|
c = '.'
|
||||||
|
}
|
||||||
|
b.WriteByte(c)
|
||||||
|
}
|
||||||
|
b.WriteString("|\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// small helper to timestamp messages consistently
|
||||||
|
func ts() string { return time.Now().Format("15:04:05.000") }
|
||||||
50
testdata/README
vendored
Normal file
50
testdata/README
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
File formats
|
||||||
|
============
|
||||||
|
|
||||||
|
tests.in
|
||||||
|
--------
|
||||||
|
|
||||||
|
Each test has the format:
|
||||||
|
|
||||||
|
<arbitrary test description>
|
||||||
|
AF BC DE HL AF' BC' DE' HL' IX IY SP PC MEMPTR
|
||||||
|
I R IFF1 IFF2 IM <halted> <tstates>
|
||||||
|
|
||||||
|
<halted> specifies whether the Z80 is halted.
|
||||||
|
<tstates> specifies the number of tstates to run the test for, in
|
||||||
|
decimal; the number actually executed may be higher, as the final
|
||||||
|
instruction is allowed to complete.
|
||||||
|
|
||||||
|
Then followed by lines specifying the initial memory setup. Each has
|
||||||
|
the format:
|
||||||
|
|
||||||
|
<start address> <byte1> <byte2> ... -1
|
||||||
|
|
||||||
|
eg
|
||||||
|
|
||||||
|
1234 56 78 9a -1
|
||||||
|
|
||||||
|
says to put 0x56 at 0x1234, 0x78 at 0x1235 and 0x9a at 0x1236.
|
||||||
|
|
||||||
|
Finally, -1 to end the test. Blank lines may follow before the next test.
|
||||||
|
|
||||||
|
tests.expected
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Each test output starts with the test description, followed by a list
|
||||||
|
of 'events': each has the format
|
||||||
|
|
||||||
|
<time> <type> <address> <data>
|
||||||
|
|
||||||
|
<time> is simply the time at which the event occurs.
|
||||||
|
<type> is one of MR (memory read), MW (memory write), MC (memory
|
||||||
|
contend), PR (port read), PW (port write) or PC (port contend).
|
||||||
|
<address> is the address (or IO port) affected.
|
||||||
|
<data> is the byte written or read. Missing for contentions.
|
||||||
|
|
||||||
|
After that, lines specifying AF, BC etc as for .in files. <tstates>
|
||||||
|
now specifies the final time.
|
||||||
|
|
||||||
|
After that, lines specifying which bits of memory have changed since
|
||||||
|
the initial setup. Same format as for .in files.
|
||||||
|
|
||||||
18913
testdata/tests.expected
vendored
Normal file
18913
testdata/tests.expected
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9153
testdata/tests.in
vendored
Normal file
9153
testdata/tests.in
vendored
Normal file
File diff suppressed because it is too large
Load Diff
422
z80.go
Normal file
422
z80.go
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
// Package z80 implements a Z80 CPU emulator with support for all documented
|
||||||
|
// and undocumented opcodes, flags, and registers.
|
||||||
|
package z80
|
||||||
|
|
||||||
|
// Memory interface for memory operations
|
||||||
|
type Memory interface {
|
||||||
|
ReadByte(address uint16) byte
|
||||||
|
WriteByte(address uint16, value byte)
|
||||||
|
ReadWord(address uint16) uint16
|
||||||
|
WriteWord(address uint16, value uint16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO interface for input/output operations
|
||||||
|
type IO interface {
|
||||||
|
ReadPort(port uint16) byte
|
||||||
|
WritePort(port uint16, value byte)
|
||||||
|
CheckInterrupt() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FLAG_* constants represent the bit positions of the FLAGS register
|
||||||
|
const (
|
||||||
|
FLAG_C = 0x01 // Carry flag
|
||||||
|
FLAG_N = 0x02 // Add/Subtract flag
|
||||||
|
FLAG_PV = 0x04 // Parity/Overflow flag
|
||||||
|
FLAG_3 = 0x08 // Undocumented 3rd bit flag
|
||||||
|
FLAG_H = 0x10 // Half Carry flag
|
||||||
|
FLAG_5 = 0x20 // Undocumented 5th bit flag
|
||||||
|
FLAG_Z = 0x40 // Zero flag
|
||||||
|
FLAG_S = 0x80 // Sign flag
|
||||||
|
)
|
||||||
|
|
||||||
|
// FLAG_X and FLAG_Y are aliases for FLAG_3 and FLAG_5 respectively
|
||||||
|
const (
|
||||||
|
FLAG_X = FLAG_3
|
||||||
|
FLAG_Y = FLAG_5
|
||||||
|
)
|
||||||
|
|
||||||
|
// CPU represents the state of a Z80 processor
|
||||||
|
type CPU struct {
|
||||||
|
A byte // Accumulator
|
||||||
|
F byte // Flags register
|
||||||
|
B, C byte // BC register pair
|
||||||
|
D, E byte // DE register pair
|
||||||
|
H, L byte // HL register pair
|
||||||
|
A_, F_ byte // Alternate AF register pair
|
||||||
|
B_, C_ byte // Alternate BC register pair
|
||||||
|
D_, E_ byte // Alternate DE register pair
|
||||||
|
H_, L_ byte // Alternate HL register pair
|
||||||
|
IX uint16 // Index register X
|
||||||
|
IY uint16 // Index register Y
|
||||||
|
SP uint16 // Stack pointer
|
||||||
|
PC uint16 // Program counter
|
||||||
|
I byte // Interrupt vector
|
||||||
|
R byte // Memory refresh
|
||||||
|
IM byte // Interrupt mode (0, 1, or 2)
|
||||||
|
IFF1 bool // Interrupt flip-flop 1
|
||||||
|
IFF2 bool // Interrupt flip-flop 2
|
||||||
|
HALT bool // HALT state flag
|
||||||
|
MEMPTR uint16 // MEMPTR register (undocumented)
|
||||||
|
|
||||||
|
Memory Memory // Memory interface
|
||||||
|
IO IO // IO interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Z80 CPU instance
|
||||||
|
func New(memory Memory, io IO) *CPU {
|
||||||
|
return &CPU{
|
||||||
|
Memory: memory,
|
||||||
|
IO: io,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBC returns the combined value of the B and C registers
|
||||||
|
func (cpu *CPU) GetBC() uint16 {
|
||||||
|
return (uint16(cpu.B) << 8) | uint16(cpu.C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDE returns the combined value of the D and E registers
|
||||||
|
func (cpu *CPU) GetDE() uint16 {
|
||||||
|
return (uint16(cpu.D) << 8) | uint16(cpu.E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHL returns the combined value of the H and L registers
|
||||||
|
func (cpu *CPU) GetHL() uint16 {
|
||||||
|
return (uint16(cpu.H) << 8) | uint16(cpu.L)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBC sets the B and C registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetBC(value uint16) {
|
||||||
|
cpu.B = byte(value >> 8)
|
||||||
|
cpu.C = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDE sets the D and E registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetDE(value uint16) {
|
||||||
|
cpu.D = byte(value >> 8)
|
||||||
|
cpu.E = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHL sets the H and L registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetHL(value uint16) {
|
||||||
|
cpu.H = byte(value >> 8)
|
||||||
|
cpu.L = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIXH returns the high byte of the IX register
|
||||||
|
func (cpu *CPU) GetIXH() byte {
|
||||||
|
return byte(cpu.IX >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIXL returns the low byte of the IX register
|
||||||
|
func (cpu *CPU) GetIXL() byte {
|
||||||
|
return byte(cpu.IX)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIYH returns the high byte of the IY register
|
||||||
|
func (cpu *CPU) GetIYH() byte {
|
||||||
|
return byte(cpu.IY >> 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIYL returns the low byte of the IY register
|
||||||
|
func (cpu *CPU) GetIYL() byte {
|
||||||
|
return byte(cpu.IY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIXH sets the high byte of the IX register
|
||||||
|
func (cpu *CPU) SetIXH(value byte) {
|
||||||
|
cpu.IX = (cpu.IX & 0x00FF) | (uint16(value) << 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIXL sets the low byte of the IX register
|
||||||
|
func (cpu *CPU) SetIXL(value byte) {
|
||||||
|
cpu.IX = (cpu.IX & 0xFF00) | uint16(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIYH sets the high byte of the IY register
|
||||||
|
func (cpu *CPU) SetIYH(value byte) {
|
||||||
|
cpu.IY = (cpu.IY & 0x00FF) | (uint16(value) << 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIYL sets the low byte of the IY register
|
||||||
|
func (cpu *CPU) SetIYL(value byte) {
|
||||||
|
cpu.IY = (cpu.IY & 0xFF00) | uint16(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlag returns the state of a specific flag
|
||||||
|
func (cpu *CPU) GetFlag(flag byte) bool {
|
||||||
|
return (cpu.F & flag) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlag sets a flag to a specific state
|
||||||
|
func (cpu *CPU) SetFlag(flag byte, state bool) {
|
||||||
|
if state {
|
||||||
|
cpu.F |= flag
|
||||||
|
} else {
|
||||||
|
cpu.F &^= flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlagState is a helper function to set a flag based on a boolean condition
|
||||||
|
func (cpu *CPU) SetFlagState(flag byte, condition bool) {
|
||||||
|
if condition {
|
||||||
|
cpu.F |= flag
|
||||||
|
} else {
|
||||||
|
cpu.F &^= flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearFlag clears a specific flag
|
||||||
|
func (cpu *CPU) ClearFlag(flag byte) {
|
||||||
|
cpu.F &^= flag
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAllFlags clears all flags
|
||||||
|
func (cpu *CPU) ClearAllFlags() {
|
||||||
|
cpu.F = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadImmediateByte reads the next byte from memory at PC and increments PC
|
||||||
|
func (cpu *CPU) ReadImmediateByte() byte {
|
||||||
|
value := cpu.Memory.ReadByte(cpu.PC)
|
||||||
|
cpu.PC++
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadImmediateWord reads the next word from memory at PC and increments PC by 2
|
||||||
|
func (cpu *CPU) ReadImmediateWord() uint16 {
|
||||||
|
lo := cpu.Memory.ReadByte(cpu.PC)
|
||||||
|
cpu.PC++
|
||||||
|
hi := cpu.Memory.ReadByte(cpu.PC)
|
||||||
|
cpu.PC++
|
||||||
|
return (uint16(hi) << 8) | uint16(lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDisplacement reads an 8-bit signed displacement value
|
||||||
|
func (cpu *CPU) ReadDisplacement() int8 {
|
||||||
|
value := int8(cpu.Memory.ReadByte(cpu.PC))
|
||||||
|
cpu.PC++
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOpcode reads the next opcode from memory at PC and increments PC
|
||||||
|
func (cpu *CPU) ReadOpcode() byte {
|
||||||
|
opcode := cpu.Memory.ReadByte(cpu.PC)
|
||||||
|
cpu.PC++
|
||||||
|
// Increment R register (memory refresh) for each opcode fetch
|
||||||
|
// Note: R is a 7-bit register, bit 7 remains unchanged
|
||||||
|
cpu.R = (cpu.R & 0x80) | ((cpu.R + 1) & 0x7F)
|
||||||
|
return opcode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push pushes a 16-bit value onto the stack
|
||||||
|
func (cpu *CPU) Push(value uint16) {
|
||||||
|
cpu.SP -= 2
|
||||||
|
cpu.Memory.WriteWord(cpu.SP, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop pops a 16-bit value from the stack
|
||||||
|
func (cpu *CPU) Pop() uint16 {
|
||||||
|
// Read low byte first, then high byte (little-endian)
|
||||||
|
lo := cpu.Memory.ReadByte(cpu.SP)
|
||||||
|
hi := cpu.Memory.ReadByte(cpu.SP + 1)
|
||||||
|
cpu.SP += 2
|
||||||
|
return (uint16(hi) << 8) | uint16(lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSZFlags updates the S and Z flags based on an 8-bit result
|
||||||
|
func (cpu *CPU) UpdateSZFlags(result byte) {
|
||||||
|
cpu.SetFlagState(FLAG_S, (result&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, result == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cpu *CPU) UpdatePVFlags(result byte) {
|
||||||
|
// Calculate parity (even number of 1-bits = 1, odd = 0)
|
||||||
|
parity := byte(1)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
parity ^= (result >> i) & 1
|
||||||
|
}
|
||||||
|
cpu.SetFlagState(FLAG_PV, parity != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSZXYPVFlags updates the S, Z, X, Y, P/V flags based on an 8-bit result
|
||||||
|
func (cpu *CPU) UpdateSZXYPVFlags(result byte) {
|
||||||
|
cpu.SetFlagState(FLAG_S, (result&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, result == 0)
|
||||||
|
cpu.SetFlagState(FLAG_X, (result&FLAG_X) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (result&FLAG_Y) != 0)
|
||||||
|
|
||||||
|
// Calculate parity (even number of 1-bits = 1, odd = 0)
|
||||||
|
parity := byte(1)
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
parity ^= (result >> i) & 1
|
||||||
|
}
|
||||||
|
cpu.SetFlagState(FLAG_PV, parity != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFlags3and5FromValue updates the X and Y flags from an 8-bit value
|
||||||
|
func (cpu *CPU) UpdateFlags3and5FromValue(value byte) {
|
||||||
|
cpu.SetFlagState(FLAG_X, (value&FLAG_X) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (value&FLAG_Y) != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFlags3and5FromAddress updates the X and Y flags from the high byte of an address
|
||||||
|
func (cpu *CPU) UpdateFlags3and5FromAddress(address uint16) {
|
||||||
|
cpu.SetFlagState(FLAG_X, (byte(address>>8)&FLAG_X) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (byte(address>>8)&FLAG_Y) != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSZXYFlags updates the S, Z, X, Y flags based on an 8-bit result
|
||||||
|
func (cpu *CPU) UpdateSZXYFlags(result byte) {
|
||||||
|
cpu.SetFlagState(FLAG_S, (result&0x80) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Z, result == 0)
|
||||||
|
cpu.SetFlagState(FLAG_X, (result&FLAG_X) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (result&FLAG_Y) != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// boolToByte converts a boolean to a byte (1 if true, 0 if false)
|
||||||
|
func boolToByte(b bool) byte {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecuteOneInstruction executes a single instruction and returns the number of T-states used
|
||||||
|
func (cpu *CPU) ExecuteOneInstruction() int {
|
||||||
|
// Handle interrupts first if enabled
|
||||||
|
if cpu.IFF1 && cpu.IO.CheckInterrupt() {
|
||||||
|
return cpu.HandleInterrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HALT state
|
||||||
|
if cpu.HALT {
|
||||||
|
return 4 // 4 T-states for HALT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the next opcode
|
||||||
|
opcode := cpu.ReadOpcode()
|
||||||
|
|
||||||
|
// Handle prefixed opcodes
|
||||||
|
switch opcode {
|
||||||
|
case 0xCB:
|
||||||
|
cbOpcode := cpu.ReadOpcode()
|
||||||
|
return cpu.ExecuteCBOpcode(cbOpcode)
|
||||||
|
case 0xDD:
|
||||||
|
ddOpcode := cpu.ReadOpcode()
|
||||||
|
return cpu.ExecuteDDOpcode(ddOpcode)
|
||||||
|
case 0xED:
|
||||||
|
edOpcode := cpu.ReadOpcode()
|
||||||
|
return cpu.ExecuteEDOpcode(edOpcode)
|
||||||
|
case 0xFD:
|
||||||
|
fdOpcode := cpu.ReadOpcode()
|
||||||
|
return cpu.ExecuteFDOpcode(fdOpcode)
|
||||||
|
default:
|
||||||
|
return cpu.ExecuteOpcode(opcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleInterrupt handles interrupt processing
|
||||||
|
func (cpu *CPU) HandleInterrupt() int {
|
||||||
|
// Exit HALT state if in HALT
|
||||||
|
if cpu.HALT {
|
||||||
|
cpu.HALT = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset interrupt flip-flops
|
||||||
|
cpu.IFF1 = false
|
||||||
|
cpu.IFF2 = false
|
||||||
|
|
||||||
|
// Handle interrupt based on mode
|
||||||
|
switch cpu.IM {
|
||||||
|
case 0, 1:
|
||||||
|
// Mode 0/1: Restart at address 0x0038
|
||||||
|
cpu.Push(cpu.PC)
|
||||||
|
cpu.PC = 0x0038
|
||||||
|
return 13 // 13 T-states for interrupt handling
|
||||||
|
case 2:
|
||||||
|
// Mode 2: Call interrupt vector
|
||||||
|
cpu.Push(cpu.PC)
|
||||||
|
vectorAddr := (uint16(cpu.I) << 8) | 0xFF // Use 0xFF as vector for non-maskable interrupt
|
||||||
|
cpu.PC = cpu.Memory.ReadWord(vectorAddr)
|
||||||
|
return 19 // 19 T-states for interrupt handling
|
||||||
|
default:
|
||||||
|
// Should not happen, but handle gracefully
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleNMI handles non-maskable interrupt
|
||||||
|
func (cpu *CPU) HandleNMI() int {
|
||||||
|
// Save current PC on stack
|
||||||
|
cpu.Push(cpu.PC)
|
||||||
|
|
||||||
|
// Jump to NMI handler
|
||||||
|
cpu.PC = 0x0066
|
||||||
|
|
||||||
|
// Disable interrupts
|
||||||
|
cpu.IFF1 = false
|
||||||
|
|
||||||
|
return 11 // 11 T-states for NMI handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAF returns the combined value of the A and F registers
|
||||||
|
func (cpu *CPU) GetAF() uint16 {
|
||||||
|
return (uint16(cpu.A) << 8) | uint16(cpu.F)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAF sets the A and F registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetAF(value uint16) {
|
||||||
|
cpu.A = byte(value >> 8)
|
||||||
|
cpu.F = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAF_ returns the combined value of the alternate A_ and F_ registers
|
||||||
|
func (cpu *CPU) GetAF_() uint16 {
|
||||||
|
return (uint16(cpu.A_) << 8) | uint16(cpu.F_)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAF_ sets the alternate A_ and F_ registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetAF_(value uint16) {
|
||||||
|
cpu.A_ = byte(value >> 8)
|
||||||
|
cpu.F_ = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBC_ returns the combined value of the alternate B_ and C_ registers
|
||||||
|
func (cpu *CPU) GetBC_() uint16 {
|
||||||
|
return (uint16(cpu.B_) << 8) | uint16(cpu.C_)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDE_ returns the combined value of the alternate D_ and E_ registers
|
||||||
|
func (cpu *CPU) GetDE_() uint16 {
|
||||||
|
return (uint16(cpu.D_) << 8) | uint16(cpu.E_)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHL_ returns the combined value of the alternate H_ and L_ registers
|
||||||
|
func (cpu *CPU) GetHL_() uint16 {
|
||||||
|
return (uint16(cpu.H_) << 8) | uint16(cpu.L_)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBC_ sets the alternate B_ and C_ registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetBC_(value uint16) {
|
||||||
|
cpu.B_ = byte(value >> 8)
|
||||||
|
cpu.C_ = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDE_ sets the alternate D_ and E_ registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetDE_(value uint16) {
|
||||||
|
cpu.D_ = byte(value >> 8)
|
||||||
|
cpu.E_ = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHL_ sets the alternate H_ and L_ registers from a 16-bit value
|
||||||
|
func (cpu *CPU) SetHL_(value uint16) {
|
||||||
|
cpu.H_ = byte(value >> 8)
|
||||||
|
cpu.L_ = byte(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateXYFlags updates the undocumented X and Y flags based on an 8-bit result
|
||||||
|
func (cpu *CPU) UpdateXYFlags(result byte) {
|
||||||
|
cpu.SetFlagState(FLAG_X, (result&FLAG_X) != 0)
|
||||||
|
cpu.SetFlagState(FLAG_Y, (result&FLAG_Y) != 0)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user