OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_cpu_decimal.c
5.1 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// CPU conformance: decimal mode (BCD) arithmetic.
2
//
3
// Oracle: WDC W65C02S datasheet. Its 6502-vs-65C02 comparison table, p30, gives
4
// both of the differences this file rests on -- where the NMOS part has
5
// "Invalid N, V and Z flags", the CMOS part has "Valid flags" and "One
6
// additional cycle".
7
//
8
// This gets its own scenarios because it is the corner most often got wrong,
9
// and because those two differences matter to anything running on this machine:
10
//
11
//   * N and Z reflect the BCD result. On the NMOS part they were taken from
12
//     the binary intermediate value and were effectively meaningless.
13
//   * ADC and SBC take one cycle longer than in binary mode. That extra cycle
14
//     is real and observable, and the X16 ships a 65C02.
15
//
16
// Only valid BCD operands are used. The result of feeding the decimal adjust
17
// a non-decimal nibble is genuinely undefined, so asserting anything about it
18
// would be inventing a specification rather than testing one.
19
20
#include "support/cpu_fixture.h"
21
#include "support/harness.h"
22
23
#define PROG 0x0200
24
25
// SED ; CLC/SEC ; LDA #a ; ADC #b
26
static void
27
bcd_add(uint8_t a, uint8_t b, bool carry_in)
28
{
29
	const uint8_t prog[] = { 0xF8, (uint8_t)(carry_in ? 0x38 : 0x18), 0xA9, a, 0x69, b };
30
	cpu_reset_to(CPU_65C02, PROG);
31
	cpu_load(PROG, prog, sizeof prog);
32
	cpu_steps(4);
33
}
34
35
// SED ; SEC/CLC ; LDA #a ; SBC #b
36
static void
37
bcd_sub(uint8_t a, uint8_t b, bool carry_in)
38
{
39
	const uint8_t prog[] = { 0xF8, (uint8_t)(carry_in ? 0x38 : 0x18), 0xA9, a, 0xE9, b };
40
	cpu_reset_to(CPU_65C02, PROG);
41
	cpu_load(PROG, prog, sizeof prog);
42
	cpu_steps(4);
43
}
44
45
static bool c(void) { return (regs.status & FLAG_CARRY) != 0; }
46
static bool z(void) { return (regs.status & FLAG_ZERO) != 0; }
47
static bool n(void) { return (regs.status & FLAG_SIGN) != 0; }
48
49
// ---- The decimal flag itself -----------------------------------------------
50
51
static void
52
test_decimal_flag(void)
53
{
54
	const uint8_t sed[] = { 0xF8 };
55
	cpu_reset_to(CPU_65C02, PROG);
56
	cpu_load(PROG, sed, sizeof sed);
57
	cpu_steps(1);
58
	check(regs.status & FLAG_DECIMAL, "SED sets the decimal flag");
59
60
	const uint8_t cld[] = { 0xF8, 0xD8 };
61
	cpu_reset_to(CPU_65C02, PROG);
62
	cpu_load(PROG, cld, sizeof cld);
63
	cpu_steps(2);
64
	check(!(regs.status & FLAG_DECIMAL), "CLD clears it again");
65
}
66
67
// ---- Addition --------------------------------------------------------------
68
69
static void
70
test_bcd_add(void)
71
{
72
	bcd_add(0x09, 0x01, false);
73
	check_eq(regs.a, 0x10, "9 + 1 = 10 in BCD");
74
	check(!c(), "and does not carry");
75
76
	bcd_add(0x15, 0x27, false);
77
	check_eq(regs.a, 0x42, "15 + 27 = 42 in BCD");
78
79
	bcd_add(0x99, 0x01, false);
80
	check_eq(regs.a, 0x00, "99 + 1 wraps to 00 in BCD");
81
	check(c(), "and carries out of the hundreds");
82
83
	bcd_add(0x50, 0x50, false);
84
	check_eq(regs.a, 0x00, "50 + 50 = 00 with a carry");
85
	check(c(), "50 + 50 carries");
86
87
	bcd_add(0x12, 0x34, true);
88
	check_eq(regs.a, 0x47, "a carry in adds one to the BCD sum");
89
90
	bcd_add(0x79, 0x14, false);
91
	check_eq(regs.a, 0x93, "79 + 14 = 93, adjusting the low nibble only");
92
}
93
94
// ---- Subtraction -----------------------------------------------------------
95
96
static void
97
test_bcd_sub(void)
98
{
99
	bcd_sub(0x42, 0x15, true);
100
	check_eq(regs.a, 0x27, "42 - 15 = 27 in BCD");
101
	check(c(), "and does not borrow");
102
103
	bcd_sub(0x50, 0x25, true);
104
	check_eq(regs.a, 0x25, "50 - 25 = 25 in BCD");
105
106
	bcd_sub(0x00, 0x01, true);
107
	check_eq(regs.a, 0x99, "00 - 1 wraps to 99 in BCD");
108
	check(!c(), "and borrows");
109
110
	bcd_sub(0x10, 0x01, true);
111
	check_eq(regs.a, 0x09, "10 - 1 = 09, borrowing across the nibble");
112
113
	bcd_sub(0x42, 0x15, false);
114
	check_eq(regs.a, 0x26, "a clear carry subtracts one more");
115
}
116
117
// ---- 65C02: N and Z describe the decimal result ----------------------------
118
// On the NMOS part these came from the binary intermediate and were useless.
119
120
static void
121
test_flags_are_valid(void)
122
{
123
	bcd_add(0x99, 0x01, false);
124
	check(z(), "a BCD result of 00 sets Z");
125
	check(!n(), "and is not negative");
126
127
	bcd_add(0x40, 0x40, false);
128
	check_eq(regs.a, 0x80, "40 + 40 = 80 in BCD");
129
	check(n(), "a BCD result with bit 7 set reports negative");
130
	check(!z(), "and is not zero");
131
132
	bcd_sub(0x01, 0x01, true);
133
	check(z(), "a BCD subtraction reaching 00 sets Z");
134
}
135
136
// ---- 65C02: decimal costs one extra cycle ----------------------------------
137
138
static void
139
test_decimal_timing(void)
140
{
141
	// SED ; CLC ; LDA #$12 ; ADC #$34
142
	const uint8_t dec[] = { 0xF8, 0x18, 0xA9, 0x12, 0x69, 0x34 };
143
	cpu_reset_to(CPU_65C02, PROG);
144
	cpu_load(PROG, dec, sizeof dec);
145
	cpu_steps(3);
146
	check_eq(cpu_step(), 3, "ADC immediate costs 3 cycles in decimal mode");
147
148
	// The same instruction in binary mode, for contrast.
149
	const uint8_t bin[] = { 0xD8, 0x18, 0xA9, 0x12, 0x69, 0x34 };
150
	cpu_reset_to(CPU_65C02, PROG);
151
	cpu_load(PROG, bin, sizeof bin);
152
	cpu_steps(3);
153
	check_eq(cpu_step(), 2, "and 2 cycles in binary mode");
154
155
	// SED ; SEC ; LDA #$42 ; SBC #$15
156
	const uint8_t sbc[] = { 0xF8, 0x38, 0xA9, 0x42, 0xE9, 0x15 };
157
	cpu_reset_to(CPU_65C02, PROG);
158
	cpu_load(PROG, sbc, sizeof sbc);
159
	cpu_steps(3);
160
	check_eq(cpu_step(), 3, "SBC immediate also costs 3 cycles in decimal mode");
161
}
162
163
int
164
main(void)
165
{
166
	test_decimal_flag();
167
	test_bcd_add();
168
	test_bcd_sub();
169
	test_flags_are_valid();
170
	test_decimal_timing();
171
	return x16_test_summary("cpu_decimal");
172
}