first public release

This commit is contained in:
2025-09-25 15:29:49 +03:00
parent 6f90fda7a7
commit 24702e4419
41 changed files with 34202 additions and 0 deletions

52
alu16_flags_test.go Normal file
View 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")
}

View 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
View 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
View 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
View 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")
}

View 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")
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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_)
}
}

View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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=

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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))
}
}

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

9153
testdata/tests.in vendored Normal file

File diff suppressed because it is too large Load Diff

422
z80.go Normal file
View 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)
}