Skip to content

Commit 95fe8f4

Browse files
committed
Added tests for opcode length and timings
1 parent 697e3fc commit 95fe8f4

File tree

6 files changed

+304
-6
lines changed

6 files changed

+304
-6
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package lr35902
2+
3+
import "github.com/colecrouter/gameboy-go/private/processor/cpu/flags"
4+
5+
// conditionType represents the type of condition that an opcode requires.
6+
// The condition is based on the CPU flags, namely the Zero and Carry flags.
7+
type conditionType int
8+
9+
const (
10+
CondNone conditionType = iota
11+
CondNZ
12+
CondZ
13+
CondNC
14+
CondC
15+
)
16+
17+
// conditionMap maps opcodes to their corresponding condition type.
18+
// Only include opcodes with conditional behavior.
19+
var conditionMap = [0x100]conditionType{
20+
0xC0: CondNZ, // RET NZ
21+
0xC2: CondNZ, // JP NZ, a16
22+
0xC4: CondNZ, // CALL NZ, a16
23+
0xC8: CondZ, // RET Z
24+
0xCA: CondZ, // JP Z, a16
25+
0xCC: CondZ, // CALL Z, a16
26+
0xD0: CondNC, // RET NC
27+
0xD2: CondNC, // JP NC, a16
28+
0xD4: CondNC, // CALL NC, a16
29+
0xD8: CondC, // RET C
30+
0xDA: CondC, // JP C, a16
31+
0xDC: CondC, // CALL C, a16
32+
}
33+
34+
// setupConditionalByOpcode configures the CPU flags based on the opcode's condition.
35+
// trigger == true means the condition is met.
36+
func setupConditionalByOpcode(cpu *LR35902, opcode byte, trigger bool) {
37+
condType := conditionMap[opcode]
38+
39+
switch condType {
40+
case CondNZ:
41+
if trigger {
42+
// For NZ: condition met → Zero flag must be false.
43+
cpu.flags.Set(flags.Reset, flags.Leave, flags.Leave, flags.Leave)
44+
} else {
45+
// Condition not met: Zero flag is true.
46+
cpu.flags.Set(flags.Set, flags.Leave, flags.Leave, flags.Leave)
47+
}
48+
case CondZ:
49+
if trigger {
50+
// For Z: condition met → Zero flag is true.
51+
cpu.flags.Set(flags.Set, flags.Leave, flags.Leave, flags.Leave)
52+
} else {
53+
// Condition not met: Zero flag is false.
54+
cpu.flags.Set(flags.Reset, flags.Leave, flags.Leave, flags.Leave)
55+
}
56+
case CondNC:
57+
if trigger {
58+
// For NC: condition met → Carry flag must be false.
59+
cpu.flags.Set(flags.Leave, flags.Leave, flags.Leave, flags.Reset)
60+
} else {
61+
// Condition not met: Carry flag is true.
62+
cpu.flags.Set(flags.Leave, flags.Leave, flags.Leave, flags.Set)
63+
}
64+
case CondC:
65+
if trigger {
66+
// For C: condition met → Carry flag is true.
67+
cpu.flags.Set(flags.Leave, flags.Leave, flags.Leave, flags.Set)
68+
} else {
69+
// Condition not met: Carry flag is false.
70+
cpu.flags.Set(flags.Leave, flags.Leave, flags.Leave, flags.Reset)
71+
}
72+
}
73+
}

private/processor/cpu/lr35902/helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func (c *LR35902) Halt() {
6969

7070
// Stop halts the CPU until a button is pressed
7171
func (c *LR35902) Stop() {
72-
c.Stop()
72+
panic("not implemented")
7373
}
7474

7575
// EI enables interrupts

private/processor/cpu/lr35902/instructions/cb_instructions.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,9 @@ func generateCbInstructions() [0x100]Instruction {
120120
OP: func(c cpu.CPU) {
121121
addr := cpu.ToRegisterPair(c.Registers().H, c.Registers().L)
122122
val := c.Read(addr)
123-
c.Clock()
123+
// c.Clock()
124124
gen(&val)(c)
125125
c.Write(addr, val)
126-
c.Clock()
127126
},
128127
}
129128
} else {

private/processor/cpu/lr35902/lr35902.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ func (c *LR35902) MClock() {
7171
}
7272

7373
// Fetch the next instruction
74-
opcode = c.Read(c.registers.PC)
74+
// We don't clock here, because the fetch stage overlaps with the previous instruction's execute stage
75+
opcode = c.bus.Read(c.registers.PC)
7576

7677
if c.cb {
7778
instruction = instructions.CBInstructions[opcode]
@@ -91,8 +92,6 @@ func (c *LR35902) MClock() {
9192
// Execute instruction
9293
op(c)
9394

94-
// We don't clock here, because the fetch stage overlaps with the previous instruction's execute stage
95-
9695
// Update DI and EI delay
9796
if c.eiDelay > 0 {
9897
c.eiDelay--
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,98 @@
11
package lr35902
2+
3+
import (
4+
"testing"
5+
6+
"github.com/colecrouter/gameboy-go/private/memory"
7+
"github.com/colecrouter/gameboy-go/private/memory/io"
8+
"github.com/colecrouter/gameboy-go/private/system"
9+
)
10+
11+
func TestByteLengths(t *testing.T) {
12+
// Create a new LR35902 CPU
13+
bus := &memory.Bus{}
14+
ir := &io.Interrupt{}
15+
ie := &io.Interrupt{}
16+
ioreg := io.NewRegisters(nil, bus, ir)
17+
mem := &memory.Memory{Buffer: make([]uint8, 0xFFFF)}
18+
bus.AddDevice(0, 0xFFFF, mem)
19+
broadcaster := system.NewBroadcaster()
20+
cpu := NewLR35902(broadcaster, bus, ioreg, ie)
21+
22+
go system.ClockGenerator(broadcaster, 4)
23+
24+
for i := 0; i < 0x100; i++ {
25+
// Lookup mnemonic
26+
mnemonic := mnemonics[i]
27+
28+
t.Run(mnemonic, func(t *testing.T) {
29+
// Reset PC
30+
cpu.registers.PC = 0
31+
32+
// Load instruction
33+
mem.Write(0, uint8(i))
34+
35+
// Execute instruction
36+
cpu.MClock()
37+
38+
// Check PC
39+
// +1 because the PC is incremented after the instruction is fetched
40+
if int(cpu.registers.PC) != instrLengths[i] {
41+
t.Errorf("PC: got %d, want %d", cpu.registers.PC, instrLengths[i])
42+
}
43+
})
44+
}
45+
}
46+
47+
// TestCyclesUnconditional uses the new helper to deduplicate boilerplate.
48+
func TestCyclesUnconditional(t *testing.T) {
49+
for i := range uint8(0xFF) {
50+
// Only run for unconditional instructions.
51+
if instrCycles[i] != instrCyclesCond[i] {
52+
continue
53+
}
54+
mnemonic := mnemonics[i]
55+
t.Run(mnemonic, func(t *testing.T) {
56+
runCyclesTest(t, uint8(i), instrCycles[i], instrLengths[i], false, nil)
57+
})
58+
}
59+
}
60+
61+
// TestCyclesConditional tests opcodes with conditional cycle differences.
62+
func TestCyclesConditional(t *testing.T) {
63+
// For each opcode where the unconditional cycles differ from the conditional ones,
64+
// and one exists in the conditionMap, run subtests.
65+
for i := range uint8(0xFF) {
66+
// Both of the below checks *should* effectively be the same
67+
// Might as well keep them in in case of mistakes
68+
69+
// Skip if timings are identical.
70+
if instrCycles[i] == instrCyclesCond[i] {
71+
continue
72+
}
73+
// Skip if this opcode doesn't have a condition type.
74+
if cond := conditionMap[uint8(i)]; cond == CondNone {
75+
continue
76+
}
77+
78+
mnemonic := mnemonics[i]
79+
t.Run(mnemonic, func(t *testing.T) {
80+
runCyclesTest(t, uint8(i), instrCyclesCond[i], instrLengths[i], true, nil)
81+
})
82+
}
83+
}
84+
85+
// TestCyclesCB tests the cycle timings for CB-prefixed instructions.
86+
func TestCyclesCB(t *testing.T) {
87+
for i := range uint8(0xFF) {
88+
mnemonic := getCBMnemonic(uint8(i))
89+
t.Run(mnemonic, func(t *testing.T) {
90+
adjust := func(cpu *LR35902) {
91+
cpu.cb = true
92+
}
93+
// For CB opcodes, preload ticks = instrCyclesCB[i] - 1 and expect PC to advance by that amount.
94+
ticks := instrCyclesCB[i] - 1
95+
runCyclesTest(t, uint8(i), ticks, ticks, false, adjust)
96+
})
97+
}
98+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package lr35902
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/colecrouter/gameboy-go/private/memory"
8+
"github.com/colecrouter/gameboy-go/private/memory/io"
9+
)
10+
11+
var instrLengths = [0x100]int{
12+
1, 3, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1, 1, 2, 1,
13+
0, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
14+
2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
15+
2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
16+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
17+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
18+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
19+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
20+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
21+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
22+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
23+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
24+
1, 1, 3, 3, 3, 1, 2, 1, 1, 1, 3, 0, 3, 3, 2, 1,
25+
1, 1, 3, 0, 3, 1, 2, 1, 1, 1, 3, 0, 3, 0, 2, 1,
26+
2, 1, 1, 0, 0, 1, 2, 1, 2, 1, 3, 0, 0, 0, 2, 1,
27+
2, 1, 1, 1, 0, 1, 2, 1, 2, 1, 3, 1, 0, 0, 2, 1,
28+
}
29+
30+
var instrCycles = [0x100]int{
31+
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
32+
0, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
33+
2, 3, 2, 2, 1, 1, 2, 1, 2, 2, 2, 2, 1, 1, 2, 1,
34+
2, 3, 2, 2, 3, 3, 3, 1, 2, 2, 2, 2, 1, 1, 2, 1,
35+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
36+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
37+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
38+
2, 2, 2, 2, 2, 2, 0, 2, 1, 1, 1, 1, 1, 1, 2, 1,
39+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
40+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
41+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
42+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
43+
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4,
44+
2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4,
45+
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
46+
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4,
47+
}
48+
49+
var instrCyclesCond = [0x100]int{
50+
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
51+
0, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
52+
3, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
53+
3, 3, 2, 2, 3, 3, 3, 1, 3, 2, 2, 2, 1, 1, 2, 1,
54+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
55+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
56+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
57+
2, 2, 2, 2, 2, 2, 0, 2, 1, 1, 1, 1, 1, 1, 2, 1,
58+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
59+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
60+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
61+
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
62+
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 0, 6, 6, 2, 4,
63+
5, 3, 4, 0, 6, 4, 2, 4, 5, 4, 4, 0, 6, 0, 2, 4,
64+
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
65+
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4,
66+
}
67+
68+
var instrCyclesCB = [0x100]int{
69+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
70+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
71+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
72+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
73+
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
74+
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
75+
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
76+
2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
77+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
78+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
79+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
80+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
81+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
82+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
83+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
84+
2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2,
85+
}
86+
87+
// newTestCPU creates a fresh CPU instance and its required bus, memory and registers.
88+
func newTestCPU() (*LR35902, *memory.Memory, *memory.Bus) {
89+
bus := &memory.Bus{}
90+
ir := &io.Interrupt{}
91+
ie := &io.Interrupt{}
92+
ioreg := io.NewRegisters(nil, bus, ir)
93+
mem := &memory.Memory{Buffer: make([]uint8, 0x10000)}
94+
bus.AddDevice(0, 0xFFFF, mem)
95+
cpu := NewLR35902(nil, bus, ioreg, ie)
96+
return cpu, mem, bus
97+
}
98+
99+
// runCyclesTest is a helper to run a test iteration for a given opcode.
100+
// adjust can be used to modify the CPU state (for CB tests or conditional flags).
101+
func runCyclesTest(t *testing.T, opcode uint8, ticks int, expectedPC int, condition bool, adjust func(cpu *LR35902)) {
102+
cpu, mem, _ := newTestCPU()
103+
cpu.registers.PC = 0
104+
mem.Write(0, opcode)
105+
106+
// Set up conditional flags if needed.
107+
setupConditionalByOpcode(cpu, opcode, condition)
108+
109+
if adjust != nil {
110+
adjust(cpu)
111+
}
112+
manualClock := make(chan struct{}, ticks)
113+
cpu.clock = manualClock
114+
for j := 0; j < ticks; j++ {
115+
manualClock <- struct{}{}
116+
}
117+
done := make(chan struct{})
118+
go func() {
119+
cpu.MClock()
120+
close(done)
121+
}()
122+
select {
123+
case <-done:
124+
if int(cpu.registers.PC) != expectedPC {
125+
t.Errorf("opcode 0x%X: PC got %d, want %d", opcode, cpu.registers.PC, expectedPC)
126+
}
127+
case <-time.After(100 * time.Millisecond):
128+
t.Errorf("opcode 0x%X did not complete within the timeout", opcode)
129+
}
130+
}

0 commit comments

Comments
 (0)