OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_cpu_loadstore.c
7.0 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// CPU conformance: load and store instructions, and the addressing modes they
2
// reach memory through.
3
//
4
// Oracle: WDC W65C02S datasheet. Cycle note 1, p20, covers both timing rules
5
// asserted here: "Page boundary, add 1 cycle if page boundary is crossed when
6
// forming address. Add 1 cycle for STA abs,X" -- indexed reads pay for a
7
// crossing, indexed stores pay a fixed cost either way.
8
//
9
// Cycle counts are asserted alongside the results because timing is part of the
10
// specification, and because it is what a debugger hook can disturb without
11
// changing any visible register.
12
//
13
// Programs are written as raw bytes with the mnemonic in a comment. That reads
14
// and greps better than a builder DSL would at this size.
15
16
#include "support/cpu_fixture.h"
17
#include "support/harness.h"
18
19
#define PROG 0x0200
20
21
// Assemble at PROG, run `steps` instructions from a clean machine.
22
static void
23
run(const uint8_t *prog, uint16_t len, int steps)
24
{
25
	cpu_reset_to(CPU_65C02, PROG);
26
	cpu_load(PROG, prog, len);
27
	cpu_steps(steps);
28
}
29
30
#define RUN(p, n) run((p), (uint16_t)sizeof(p), (n))
31
32
// ---- Loads set N and Z from the value loaded -------------------------------
33
34
static void
35
test_load_flags(void)
36
{
37
	const uint8_t zero[] = { 0xA9, 0x00 };       // LDA #$00
38
	RUN(zero, 1);
39
	check(regs.status & FLAG_ZERO, "LDA #$00 sets Z");
40
	check(!(regs.status & FLAG_SIGN), "LDA #$00 clears N");
41
42
	const uint8_t neg[] = { 0xA9, 0x80 };        // LDA #$80
43
	RUN(neg, 1);
44
	check(regs.status & FLAG_SIGN, "LDA #$80 sets N");
45
	check(!(regs.status & FLAG_ZERO), "LDA #$80 clears Z");
46
47
	const uint8_t pos[] = { 0xA2, 0x7F };        // LDX #$7F
48
	RUN(pos, 1);
49
	check(!(regs.status & (FLAG_SIGN | FLAG_ZERO)), "LDX #$7F clears N and Z");
50
51
	const uint8_t ldy[] = { 0xA0, 0xFF };        // LDY #$FF
52
	RUN(ldy, 1);
53
	check(regs.status & FLAG_SIGN, "LDY #$FF sets N");
54
}
55
56
// ---- Stores write, and touch no flag ---------------------------------------
57
58
static void
59
test_stores(void)
60
{
61
	// LDA #$5A ; STA $0400
62
	const uint8_t sta_abs[] = { 0xA9, 0x5A, 0x8D, 0x00, 0x04 };
63
	cpu_reset_to(CPU_65C02, PROG);
64
	cpu_load(PROG, sta_abs, sizeof sta_abs);
65
	cpu_steps(1);
66
	check_eq(cpu_step(), 4, "STA absolute costs 4 cycles");
67
	check_eq(cpu_mem[0x0400], 0x5A, "STA absolute writes the accumulator");
68
69
	// LDX #$01 ; LDA #$00 ; STA $0500,X
70
	// The load comes last so Z is set going into the store, which is what makes
71
	// "the store changed no flag" a meaningful claim. Both target bytes are
72
	// seeded non-zero first: memory resets to zero, so storing a zero into a
73
	// zeroed byte would compare equal however the address was computed, and
74
	// would assert nothing at all.
75
	const uint8_t flags[] = { 0xA2, 0x01, 0xA9, 0x00, 0x9D, 0x00, 0x05 };
76
	cpu_reset_to(CPU_65C02, PROG);
77
	cpu_load(PROG, flags, sizeof flags);
78
	cpu_mem[0x0500] = 0xFF;
79
	cpu_mem[0x0501] = 0xFF;
80
	cpu_steps(2);
81
	uint8_t before = regs.status;
82
	check_eq(cpu_step(), 5, "STA absolute,X always costs 5 cycles");
83
	check_eq(cpu_mem[0x0501], 0x00, "STA absolute,X writes at base plus index");
84
	check_eq(cpu_mem[0x0500], 0xFF, "and not at the unindexed address");
85
	check_eq(regs.status, before, "a store changes no flag");
86
87
	// LDX #$77 ; STX $30
88
	const uint8_t stx[] = { 0xA2, 0x77, 0x86, 0x30 };
89
	cpu_reset_to(CPU_65C02, PROG);
90
	cpu_load(PROG, stx, sizeof stx);
91
	cpu_steps(1);
92
	check_eq(cpu_step(), 3, "STX zero page costs 3 cycles");
93
	check_eq(cpu_mem[0x0030], 0x77, "STX zero page writes X");
94
95
	// LDY #$66 ; STY $0402
96
	const uint8_t sty[] = { 0xA0, 0x66, 0x8C, 0x02, 0x04 };
97
	RUN(sty, 2);
98
	check_eq(cpu_mem[0x0402], 0x66, "STY absolute writes Y");
99
}
100
101
// ---- Addressing modes reach the right address ------------------------------
102
103
static void
104
test_addressing_modes(void)
105
{
106
	// LDA $30
107
	const uint8_t zp[] = { 0xA5, 0x30 };
108
	cpu_reset_to(CPU_65C02, PROG);
109
	cpu_load(PROG, zp, sizeof zp);
110
	cpu_mem[0x0030] = 0x11;
111
	cpu_steps(1);
112
	check_eq(regs.a, 0x11, "LDA zero page");
113
	check_eq(cpu_last_cycles(), 3, "LDA zero page costs 3 cycles");
114
115
	// LDX #$05 ; LDA $30,X
116
	const uint8_t zpx[] = { 0xA2, 0x05, 0xB5, 0x30 };
117
	cpu_reset_to(CPU_65C02, PROG);
118
	cpu_load(PROG, zpx, sizeof zpx);
119
	cpu_mem[0x0035] = 0x22;
120
	cpu_steps(1);
121
	check_eq(cpu_step(), 4, "LDA zero page,X costs 4 cycles");
122
	check_eq(regs.a, 0x22, "LDA zero page,X");
123
124
	// LDY #$03 ; LDX $40,Y  -- the only zero page,Y load
125
	const uint8_t zpy[] = { 0xA0, 0x03, 0xB6, 0x40 };
126
	cpu_reset_to(CPU_65C02, PROG);
127
	cpu_load(PROG, zpy, sizeof zpy);
128
	cpu_mem[0x0043] = 0x33;
129
	cpu_steps(2);
130
	check_eq(regs.x, 0x33, "LDX zero page,Y");
131
132
	// LDA $0410
133
	const uint8_t abs[] = { 0xAD, 0x10, 0x04 };
134
	cpu_reset_to(CPU_65C02, PROG);
135
	cpu_load(PROG, abs, sizeof abs);
136
	cpu_mem[0x0410] = 0x44;
137
	cpu_steps(1);
138
	check_eq(regs.a, 0x44, "LDA absolute");
139
	check_eq(cpu_last_cycles(), 4, "LDA absolute costs 4 cycles");
140
}
141
142
// ---- Zero page indexing wraps inside page zero -----------------------------
143
// $FF + 2 addresses $01, not $0101. Getting this wrong reads the wrong page
144
// entirely, so it is worth pinning on its own.
145
146
static void
147
test_zero_page_wrap(void)
148
{
149
	// LDX #$02 ; LDA $FF,X
150
	const uint8_t wrap[] = { 0xA2, 0x02, 0xB5, 0xFF };
151
	cpu_reset_to(CPU_65C02, PROG);
152
	cpu_load(PROG, wrap, sizeof wrap);
153
	cpu_mem[0x0001] = 0xAB;
154
	cpu_mem[0x0101] = 0xCD; // where it would land without the wrap
155
	cpu_steps(2);
156
	check_eq(regs.a, 0xAB, "zero page,X wraps within page zero");
157
158
	// LDX #$02 ; LDA ($FE,X) -- the pointer itself wraps too
159
	const uint8_t iwrap[] = { 0xA2, 0x02, 0xA1, 0xFE };
160
	cpu_reset_to(CPU_65C02, PROG);
161
	cpu_load(PROG, iwrap, sizeof iwrap);
162
	cpu_mem[0x0000] = 0x20; // pointer low, at $FE+2 wrapped
163
	cpu_mem[0x0001] = 0x04; // pointer high
164
	cpu_mem[0x0420] = 0xEF;
165
	cpu_steps(1);
166
	check_eq(cpu_step(), 6, "LDA (zero page,X) costs 6 cycles");
167
	check_eq(regs.a, 0xEF, "(zero page,X) wraps when forming the pointer");
168
}
169
170
// ---- Indexed reads cost an extra cycle when they cross a page --------------
171
172
static void
173
test_page_cross_timing(void)
174
{
175
	// LDX #$01 ; LDA $0410,X -- stays inside page $04
176
	const uint8_t same[] = { 0xA2, 0x01, 0xBD, 0x10, 0x04 };
177
	cpu_reset_to(CPU_65C02, PROG);
178
	cpu_load(PROG, same, sizeof same);
179
	cpu_steps(1);
180
	check_eq(cpu_step(), 4, "LDA absolute,X costs 4 within a page");
181
182
	// LDX #$01 ; LDA $04FF,X -- crosses into page $05
183
	const uint8_t cross[] = { 0xA2, 0x01, 0xBD, 0xFF, 0x04 };
184
	cpu_reset_to(CPU_65C02, PROG);
185
	cpu_load(PROG, cross, sizeof cross);
186
	cpu_steps(1);
187
	check_eq(cpu_step(), 5, "LDA absolute,X costs 5 across a page");
188
189
	// LDY #$01 ; LDA $04FF,Y
190
	const uint8_t crossy[] = { 0xA0, 0x01, 0xB9, 0xFF, 0x04 };
191
	cpu_reset_to(CPU_65C02, PROG);
192
	cpu_load(PROG, crossy, sizeof crossy);
193
	cpu_steps(1);
194
	check_eq(cpu_step(), 5, "LDA absolute,Y costs 5 across a page");
195
196
	// LDY #$01 ; LDA ($20),Y with the pointer at $04FF
197
	const uint8_t indy[] = { 0xA0, 0x01, 0xB1, 0x20 };
198
	cpu_reset_to(CPU_65C02, PROG);
199
	cpu_load(PROG, indy, sizeof indy);
200
	cpu_mem[0x0020] = 0xFF;
201
	cpu_mem[0x0021] = 0x04;
202
	cpu_steps(1);
203
	check_eq(cpu_step(), 6, "LDA (zero page),Y costs 6 across a page");
204
}
205
206
int
207
main(void)
208
{
209
	test_load_flags();
210
	test_stores();
211
	test_addressing_modes();
212
	test_zero_page_wrap();
213
	test_page_cross_timing();
214
	return x16_test_summary("cpu_loadstore");
215
}