tangled
alpha
login
or
join now
pluie.me
/
zesty
2
fork
atom
Zesty - a pin-accurate, cycle-accurate NES emulator written in Zig
2
fork
atom
overview
issues
pulls
pipelines
initial commit
pluie.me
6 months ago
a2d803e2
+2210
17 changed files
expand all
collapse all
unified
split
.envrc
.gitignore
build.zig
build.zig.zon
flake.lock
flake.nix
src
Cpu.zig
Ppu.zig
cartridge.zig
main.zig
tests
cpu
addressing.zig
alu.zig
assembler.zig
control_flow.zig
cpu.zig
tests.zig
zesty.zig
+1
.envrc
···
1
1
+
has nix && use flake
+3
.gitignore
···
1
1
+
.direnv/
2
2
+
.zig-cache/
3
3
+
zig-out/
+46
build.zig
···
1
1
+
const std = @import("std");
2
2
+
3
3
+
pub fn build(b: *std.Build) void {
4
4
+
const target = b.standardTargetOptions(.{});
5
5
+
const optimize = b.standardOptimizeOption(.{});
6
6
+
7
7
+
// Top-level steps
8
8
+
const run_step = b.step("run", "Run Zesty");
9
9
+
const test_step = b.step("test", "Test Zesty");
10
10
+
11
11
+
_ = b.addModule("zesty", .{
12
12
+
.root_source_file = b.path("src/zesty.zig"),
13
13
+
.target = target,
14
14
+
.optimize = optimize,
15
15
+
});
16
16
+
17
17
+
// Exe
18
18
+
{
19
19
+
const exe_module = b.createModule(.{
20
20
+
.root_source_file = b.path("src/main.zig"),
21
21
+
.target = target,
22
22
+
.optimize = optimize,
23
23
+
});
24
24
+
const compile_exe = b.addExecutable(.{
25
25
+
.name = "zesty",
26
26
+
.root_module = exe_module,
27
27
+
});
28
28
+
const run_exe = b.addRunArtifact(compile_exe);
29
29
+
run_step.dependOn(&run_exe.step);
30
30
+
}
31
31
+
32
32
+
// Test
33
33
+
{
34
34
+
const test_module = b.addModule("zesty", .{
35
35
+
.root_source_file = b.path("src/tests.zig"),
36
36
+
.target = target,
37
37
+
.optimize = optimize,
38
38
+
});
39
39
+
const compile_test = b.addTest(.{
40
40
+
.name = "zesty-test",
41
41
+
.root_module = test_module,
42
42
+
});
43
43
+
const run_test = b.addRunArtifact(compile_test);
44
44
+
test_step.dependOn(&run_test.step);
45
45
+
}
46
46
+
}
+9
build.zig.zon
···
1
1
+
.{
2
2
+
.name = .zes,
3
3
+
.version = "0.0.1",
4
4
+
.minimum_zig_version = "0.15.1",
5
5
+
.paths = .{
6
6
+
"src",
7
7
+
},
8
8
+
.fingerprint = 0x77a18bf055c88ec2,
9
9
+
}
+24
flake.lock
···
1
1
+
{
2
2
+
"nodes": {
3
3
+
"nixpkgs": {
4
4
+
"locked": {
5
5
+
"lastModified": 1757419078,
6
6
+
"narHash": "sha256-cUwlZ3aSduI9dw5AbF3PzktsCztcvt+0GBClTeGaksI=",
7
7
+
"rev": "b599843bad24621dcaa5ab60dac98f9b0eb1cabe",
8
8
+
"type": "tarball",
9
9
+
"url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre858212.b599843bad24/nixexprs.tar.xz"
10
10
+
},
11
11
+
"original": {
12
12
+
"type": "tarball",
13
13
+
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
14
14
+
}
15
15
+
},
16
16
+
"root": {
17
17
+
"inputs": {
18
18
+
"nixpkgs": "nixpkgs"
19
19
+
}
20
20
+
}
21
21
+
},
22
22
+
"root": "root",
23
23
+
"version": 7
24
24
+
}
+13
flake.nix
···
1
1
+
{
2
2
+
inputs.nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
3
3
+
4
4
+
outputs = { nixpkgs, ... }: let
5
5
+
forAllSystems = f: with nixpkgs; lib.genAttrs lib.systems.flakeExposed (s: f legacyPackages.${s});
6
6
+
in {
7
7
+
devShells = forAllSystems (pkgs: {
8
8
+
default = pkgs.mkShell {
9
9
+
packages = with pkgs; [ zig_0_15 zls_0_15 ];
10
10
+
};
11
11
+
});
12
12
+
};
13
13
+
}
+712
src/Cpu.zig
···
1
1
+
//! The NES's 6502-based CPU core.
2
2
+
const Cpu = @This();
3
3
+
4
4
+
const std = @import("std");
5
5
+
const zesty = @import("zesty.zig");
6
6
+
7
7
+
const log = std.log.scoped(.cpu);
8
8
+
9
9
+
//------------------------------------------
10
10
+
// Registers
11
11
+
12
12
+
a: u8 = 0,
13
13
+
x: u8 = 0,
14
14
+
y: u8 = 0,
15
15
+
sp: u8 = 0,
16
16
+
pc: u16 = 0x00ff,
17
17
+
status: Status = .{},
18
18
+
19
19
+
/// Temporary storage of the target address.
20
20
+
/// Used by complex addressing modes.
21
21
+
hilo: Addr = .{},
22
22
+
23
23
+
/// The currently executing opcode.
24
24
+
opcode: u8 = 0,
25
25
+
26
26
+
/// The execution cycle of the opcode.
27
27
+
/// At most 7.
28
28
+
cycle: u3 = 0,
29
29
+
30
30
+
// Not exposed on the 2A03 chip
31
31
+
/// Sync pin (new instruction)
32
32
+
sync: bool = false,
33
33
+
34
34
+
in_reset: bool = false,
35
35
+
36
36
+
pub fn tick(self: *Cpu, pins: *zesty.Pins, last_pins: zesty.Pins) void {
37
37
+
_ = last_pins;
38
38
+
// TODO: emulate M2 duty cycle
39
39
+
pins.cpu_m2 = pins.cpu_clk;
40
40
+
if (!pins.cpu_clk) return;
41
41
+
42
42
+
defer self.cycle +|= 1;
43
43
+
44
44
+
// *Most* cycles are reads.
45
45
+
pins.cpu_rw = .read;
46
46
+
47
47
+
if (pins.cpu_rst) {
48
48
+
pins.cpu_rst = false;
49
49
+
self.in_reset = true;
50
50
+
self.cycle = 0;
51
51
+
}
52
52
+
53
53
+
if (self.in_reset) {
54
54
+
// Exact cycle-by-cycle breakdown: https://www.pagetable.com/?p=410
55
55
+
switch (self.cycle) {
56
56
+
// First three cycles do essentially nothing.
57
57
+
0, 1 => {
58
58
+
self.sp = 0;
59
59
+
self.opcode = 0;
60
60
+
},
61
61
+
// Fake stack push; note that SP does not change.
62
62
+
2 => pins.cpu_addr = 0x0100,
63
63
+
// Fake stack push
64
64
+
3 => pins.cpu_addr = 0x01ff,
65
65
+
4 => pins.cpu_addr = 0x01fe,
66
66
+
5 => {
67
67
+
self.sp = 0xfd;
68
68
+
pins.cpu_addr = 0xfffc;
69
69
+
},
70
70
+
6 => {
71
71
+
self.hilo.lo = pins.cpu_data;
72
72
+
pins.cpu_addr = 0xfffd;
73
73
+
},
74
74
+
else => {
75
75
+
self.hilo.hi = pins.cpu_data;
76
76
+
self.pc = self.hilo.addr();
77
77
+
pins.cpu_addr = self.pc;
78
78
+
79
79
+
// IRQ is always disabled after every reset
80
80
+
self.status.irq_disabled = true;
81
81
+
82
82
+
self.in_reset = false;
83
83
+
self.sync = true;
84
84
+
},
85
85
+
}
86
86
+
return;
87
87
+
}
88
88
+
89
89
+
if (self.sync) {
90
90
+
self.opcode = pins.cpu_data;
91
91
+
self.cycle = 0;
92
92
+
self.sync = false;
93
93
+
}
94
94
+
95
95
+
// if (self.sync or pins.cpu_irq or pins.cpu_nmi or pins.cpu_rst) {
96
96
+
// // NMI is triggered by a edge detector
97
97
+
// if (!last_pins.cpu_nmi and pins.cpu_nmi) {
98
98
+
// // TODO: handle NMI
99
99
+
// }
100
100
+
101
101
+
// // IRQ is detected by a level detector
102
102
+
// if (pins.cpu_irq and !self.status.irq_disabled) {
103
103
+
// // TODO: handle IRQ
104
104
+
// }
105
105
+
// }
106
106
+
107
107
+
// log.err("BOBER={x}", .{self.opcode});
108
108
+
109
109
+
const v = pins.cpu_data;
110
110
+
switch (self.opcode) {
111
111
+
0x00 => self.brk(pins), // BRK
112
112
+
113
113
+
0x01 => if (self.zpXInd(pins)) self.ora(pins, v), // ORA (zp,X)
114
114
+
0x05 => if (self.zp(pins)) self.ora(pins, v), // ORA zp
115
115
+
0x06 => if (self.zp(pins)) self.asl(pins, .mem), // ASL zp
116
116
+
0x09 => if (self.imm(pins)) self.ora(pins, v), // ORA #
117
117
+
0x0a => if (self.imm(pins)) self.asl(pins, .acc), // ASL A
118
118
+
0x0d => if (self.abs(pins)) self.ora(pins, v), // ORA abs
119
119
+
0x0e => if (self.abs(pins)) self.asl(pins, .mem), // ASL abs
120
120
+
121
121
+
0x10 => if (self.imm(pins)) self.branch(pins, !self.status.negative), // BPL
122
122
+
0x11 => if (self.zpIndY(pins)) self.ora(pins, v), // ORA (zp),Y
123
123
+
0x15 => if (self.zpOff(pins, self.x)) self.ora(pins, v), // ORA zp,X
124
124
+
0x16 => if (self.zpOff(pins, self.x)) self.asl(pins, .mem), // ASL zp,X
125
125
+
0x18 => self.set(pins, .carry, false), // CLC
126
126
+
0x19 => if (self.absOff(pins, self.y)) self.ora(pins, v), // ORA abs,Y
127
127
+
0x1d => if (self.absOff(pins, self.x)) self.ora(pins, v), // ORA abs,X
128
128
+
0x1e => if (self.absOff(pins, self.x)) self.asl(pins, .mem), // ASL abs,X
129
129
+
130
130
+
0x21 => if (self.zpXInd(pins)) self._and(pins, v), // AND (zp,X)
131
131
+
0x25 => if (self.zp(pins)) self._and(pins, v), // AND zp
132
132
+
0x26 => if (self.zp(pins)) self.rol(pins, .mem), // ROL zp
133
133
+
0x29 => if (self.imm(pins)) self._and(pins, v), // AND #
134
134
+
0x2a => if (self.imm(pins)) self.rol(pins, .acc), // ROL A
135
135
+
0x2d => if (self.abs(pins)) self._and(pins, v), // AND abs
136
136
+
0x2e => if (self.abs(pins)) self.rol(pins, .mem), // ROL abs
137
137
+
138
138
+
0x30 => if (self.imm(pins)) self.branch(pins, self.status.negative), // BMI
139
139
+
0x31 => if (self.zpIndY(pins)) self._and(pins, v), // AND (zp),Y
140
140
+
0x35 => if (self.zpOff(pins, self.x)) self._and(pins, v), // AND zp,X
141
141
+
0x36 => if (self.zpOff(pins, self.x)) self.rol(pins, .mem), // ROL zp,X
142
142
+
0x38 => self.set(pins, .carry, true), // SEC
143
143
+
0x39 => if (self.absOff(pins, self.y)) self._and(pins, v), // AND abs,Y
144
144
+
0x3d => if (self.absOff(pins, self.x)) self._and(pins, v), // AND abs,X
145
145
+
0x3e => if (self.absOff(pins, self.x)) self.rol(pins, .mem), // ROL abs,X
146
146
+
147
147
+
0x40 => self.rti(pins), // RTI
148
148
+
0x41 => if (self.zpXInd(pins)) self.eor(pins, v), // EOR (zp,X)
149
149
+
0x45 => if (self.zp(pins)) self.eor(pins, v), // EOR zp
150
150
+
0x46 => if (self.zp(pins)) self.lsr(pins, .mem), // LSR zp
151
151
+
0x49 => if (self.imm(pins)) self.eor(pins, v), // EOR #
152
152
+
0x4a => if (self.imm(pins)) self.lsr(pins, .acc), // LSR A
153
153
+
0x4d => if (self.abs(pins)) self.eor(pins, v), // EOR abs
154
154
+
0x4e => if (self.abs(pins)) self.lsr(pins, .mem), // LSR abs
155
155
+
156
156
+
0x50 => if (self.imm(pins)) self.branch(pins, !self.status.overflow), // BVC
157
157
+
0x51 => if (self.zpIndY(pins)) self.eor(pins, v), // EOR (zp),Y
158
158
+
0x55 => if (self.zpOff(pins, self.x)) self.eor(pins, v), // EOR zp,X
159
159
+
0x56 => if (self.zpOff(pins, self.x)) self.lsr(pins, .mem), // LSR zp,X
160
160
+
0x58 => self.set(pins, .irq_disabled, false), // CLI
161
161
+
0x59 => if (self.absOff(pins, self.y)) self.eor(pins, v), // EOR abs,Y
162
162
+
0x5d => if (self.absOff(pins, self.x)) self.eor(pins, v), // EOR abs,X
163
163
+
0x5e => if (self.absOff(pins, self.x)) self.lsr(pins, .mem), // LSR abs,X
164
164
+
165
165
+
// 0x61 => if (self.zpXInd(pins)) self.adc(pins, v), // ADC (zp,X)
166
166
+
// 0x65 => if (self.zp(pins)) self.adc(pins, v), // ADC zp
167
167
+
0x66 => if (self.zp(pins)) self.ror(pins, .mem), // ROR zp
168
168
+
// 0x69 => if (self.imm(pins)) self.adc(pins, v), // ADC #
169
169
+
0x6a => if (self.imm(pins)) self.ror(pins, .acc), // ROR A
170
170
+
// 0x6d => if (self.abs(pins)) self.adc(pins, v), // ADC abs
171
171
+
0x6e => if (self.abs(pins)) self.ror(pins, .mem), // ROR abs
172
172
+
173
173
+
0x70 => if (self.imm(pins)) self.branch(pins, self.status.overflow), // BVS
174
174
+
// 0x71 => if (self.zpIndY(pins)) self.adc(pins, v), // ADC (zp),Y
175
175
+
// 0x75 => if (self.zpOff(pins, self.x)) self.adc(pins, v), // ADC zp,X
176
176
+
0x76 => if (self.zpOff(pins, self.x)) self.ror(pins, .mem), // ROR zp,X
177
177
+
0x78 => self.set(pins, .irq_disabled, true), // SEI
178
178
+
// 0x79 => if (self.absOff(pins, self.y)) self.adc(pins, v), // ADC abs,Y
179
179
+
// 0x7d => if (self.absOff(pins, self.x)) self.adc(pins, v), // ADC abs,X
180
180
+
0x7e => if (self.absOff(pins, self.x)) self.ror(pins, .mem), // ROR abs,X
181
181
+
182
182
+
0x81 => if (self.zpXInd(pins)) self.st(pins, self.y), // STA (zp,X)
183
183
+
0x84 => if (self.zp(pins)) self.st(pins, self.y), // STY zp
184
184
+
0x85 => if (self.zp(pins)) self.st(pins, self.a), // STA zp
185
185
+
0x86 => if (self.zp(pins)) self.st(pins, self.x), // STX zp
186
186
+
0x88 => if (self.imm(pins)) self.dec(pins, &self.y), // DEY
187
187
+
0x8a => if (self.imm(pins)) self.ld(pins, &self.a, self.x), // TXA
188
188
+
0x8c => if (self.abs(pins)) self.st(pins, self.y), // STY abs
189
189
+
0x8d => if (self.abs(pins)) self.st(pins, self.a), // STA abs
190
190
+
0x8e => if (self.abs(pins)) self.st(pins, self.x), // STX abs
191
191
+
192
192
+
0x90 => if (self.imm(pins)) self.branch(pins, !self.status.carry), // BCC
193
193
+
0x91 => if (self.zpIndY(pins)) self.ld(pins, &self.a, v), // STA (zp),Y
194
194
+
0x94 => if (self.zpOff(pins, self.x)) self.st(pins, self.y), // STY zp,X
195
195
+
0x95 => if (self.zpOff(pins, self.x)) self.st(pins, self.a), // STA zp,X
196
196
+
0x96 => if (self.zpOff(pins, self.y)) self.st(pins, self.x), // STX zp,Y
197
197
+
0x99 => if (self.absOff(pins, self.y)) self.st(pins, self.a), // STA abs,Y
198
198
+
0x9a => if (self.imm(pins)) self.ld(pins, &self.sp, self.x), // TXS
199
199
+
0x9c => if (self.absOff(pins, self.x)) self.st(pins, self.y), // STY abs,X
200
200
+
0x9d => if (self.absOff(pins, self.x)) self.st(pins, self.a), // STA abs,X
201
201
+
0x9e => if (self.absOff(pins, self.y)) self.st(pins, self.x), // STX abs,Y
202
202
+
203
203
+
0xa0 => if (self.imm(pins)) self.ld(pins, &self.y, v), // LDY #
204
204
+
0xa1 => if (self.zpXInd(pins)) self.ld(pins, &self.y, v), // LDA (zp,X)
205
205
+
0xa2 => if (self.imm(pins)) self.ld(pins, &self.x, v), // LDX #
206
206
+
0xa4 => if (self.zp(pins)) self.ld(pins, &self.y, v), // LDY zp
207
207
+
0xa5 => if (self.zp(pins)) self.ld(pins, &self.a, v), // LDA zp
208
208
+
0xa6 => if (self.zp(pins)) self.ld(pins, &self.x, v), // LDX zp
209
209
+
0xa8 => if (self.imm(pins)) self.ld(pins, &self.y, self.a), // TAY
210
210
+
0xa9 => if (self.imm(pins)) self.ld(pins, &self.a, v), // LDA #
211
211
+
0xaa => if (self.imm(pins)) self.ld(pins, &self.x, self.a), // TAX
212
212
+
0xac => if (self.abs(pins)) self.ld(pins, &self.y, v), // LDY abs
213
213
+
0xad => if (self.abs(pins)) self.ld(pins, &self.a, v), // LDA abs
214
214
+
0xae => if (self.abs(pins)) self.ld(pins, &self.x, v), // LDX abs
215
215
+
216
216
+
0xb0 => if (self.imm(pins)) self.branch(pins, self.status.carry), // BCS
217
217
+
0xb1 => if (self.zpIndY(pins)) self.ld(pins, &self.a, v), // LDA (zp),Y
218
218
+
0xb4 => if (self.zpOff(pins, self.x)) self.ld(pins, &self.y, v), // LDY zp,X
219
219
+
0xb5 => if (self.zpOff(pins, self.x)) self.ld(pins, &self.a, v), // LDA zp,X
220
220
+
0xb6 => if (self.zpOff(pins, self.y)) self.ld(pins, &self.x, v), // LDX zp,Y
221
221
+
0xb8 => self.set(pins, .overflow, true), // SEV
222
222
+
0xb9 => if (self.absOff(pins, self.y)) self.ld(pins, &self.a, v), // LDA abs,Y
223
223
+
0xba => if (self.imm(pins)) self.ld(pins, &self.x, self.sp), // TSX
224
224
+
0xbc => if (self.absOff(pins, self.x)) self.ld(pins, &self.y, v), // LDY abs,X
225
225
+
0xbd => if (self.absOff(pins, self.x)) self.ld(pins, &self.a, v), // LDA abs,X
226
226
+
0xbe => if (self.absOff(pins, self.y)) self.ld(pins, &self.x, v), // LDX abs,Y
227
227
+
228
228
+
0xd0 => if (self.imm(pins)) self.branch(pins, !self.status.zero), // BNE
229
229
+
0xd8 => self.set(pins, .decimal, false), // CLD
230
230
+
231
231
+
0xea => if (self.imm(pins)) self.fetch(pins), // NOP
232
232
+
233
233
+
0xf0 => if (self.imm(pins)) self.branch(pins, self.status.zero), // BEQ
234
234
+
0xf8 => self.set(pins, .decimal, true), // SED
235
235
+
236
236
+
else => log.err("UNHANDLED OP={X}", .{self.opcode}),
237
237
+
}
238
238
+
}
239
239
+
240
240
+
inline fn imm(self: *Cpu, pins: *zesty.Pins) bool {
241
241
+
switch (self.cycle) {
242
242
+
0 => {
243
243
+
self.pc +%= 1;
244
244
+
pins.cpu_addr = self.pc;
245
245
+
},
246
246
+
else => return true,
247
247
+
}
248
248
+
return false;
249
249
+
}
250
250
+
inline fn zp(self: *Cpu, pins: *zesty.Pins) bool {
251
251
+
switch (self.cycle) {
252
252
+
0 => {
253
253
+
self.pc +%= 1;
254
254
+
pins.cpu_addr = self.pc;
255
255
+
},
256
256
+
1 => {
257
257
+
self.hilo = .{ .lo = pins.cpu_data };
258
258
+
pins.cpu_addr = self.hilo.addr();
259
259
+
},
260
260
+
else => return true,
261
261
+
}
262
262
+
return false;
263
263
+
}
264
264
+
inline fn zpOff(self: *Cpu, pins: *zesty.Pins, v: u8) bool {
265
265
+
switch (self.cycle) {
266
266
+
0 => {
267
267
+
self.pc +%= 1;
268
268
+
pins.cpu_addr = self.pc;
269
269
+
},
270
270
+
1 => {
271
271
+
self.hilo = .{ .lo = pins.cpu_data };
272
272
+
pins.cpu_addr = self.hilo.addr();
273
273
+
},
274
274
+
2 => {
275
275
+
self.hilo.lo +%= v;
276
276
+
pins.cpu_addr = self.hilo.addr();
277
277
+
},
278
278
+
else => return true,
279
279
+
}
280
280
+
return false;
281
281
+
}
282
282
+
inline fn abs(self: *Cpu, pins: *zesty.Pins) bool {
283
283
+
switch (self.cycle) {
284
284
+
0 => {
285
285
+
self.pc +%= 1;
286
286
+
pins.cpu_addr = self.pc;
287
287
+
},
288
288
+
1 => {
289
289
+
self.pc +%= 1;
290
290
+
pins.cpu_addr = self.pc;
291
291
+
self.hilo = .{ .lo = pins.cpu_data };
292
292
+
},
293
293
+
2 => {
294
294
+
self.hilo.hi = pins.cpu_data;
295
295
+
pins.cpu_addr = self.hilo.addr();
296
296
+
},
297
297
+
else => return true,
298
298
+
}
299
299
+
return false;
300
300
+
}
301
301
+
inline fn absOff(self: *Cpu, pins: *zesty.Pins, v: u8) bool {
302
302
+
switch (self.cycle) {
303
303
+
0 => {
304
304
+
self.pc +%= 1;
305
305
+
pins.cpu_addr = self.pc;
306
306
+
},
307
307
+
1 => {
308
308
+
self.pc +%= 1;
309
309
+
self.hilo = .{ .lo = pins.cpu_data };
310
310
+
pins.cpu_addr = self.pc;
311
311
+
},
312
312
+
2 => {
313
313
+
self.hilo.hi = pins.cpu_data;
314
314
+
pins.cpu_addr, const page_crossed = self.hilo.offsetWithPageFaultBehavior(v, false);
315
315
+
if (!page_crossed) self.cycle += 1;
316
316
+
},
317
317
+
3 => {
318
318
+
pins.cpu_addr = self.hilo.addr();
319
319
+
},
320
320
+
else => return true,
321
321
+
}
322
322
+
return false;
323
323
+
}
324
324
+
inline fn zpXInd(self: *Cpu, pins: *zesty.Pins) bool {
325
325
+
switch (self.cycle) {
326
326
+
0 => {
327
327
+
self.pc +%= 1;
328
328
+
pins.cpu_addr = self.pc;
329
329
+
},
330
330
+
1 => {
331
331
+
self.hilo = .{ .lo = pins.cpu_data };
332
332
+
pins.cpu_addr +%= 1;
333
333
+
},
334
334
+
2 => {
335
335
+
self.hilo.lo +%= self.x;
336
336
+
pins.cpu_addr = self.hilo.addr();
337
337
+
},
338
338
+
3 => {
339
339
+
self.hilo.lo +%= 1;
340
340
+
pins.cpu_addr = self.hilo.addr();
341
341
+
self.hilo = .{ .lo = pins.cpu_data };
342
342
+
},
343
343
+
4 => {
344
344
+
self.hilo.hi = pins.cpu_data;
345
345
+
pins.cpu_addr = self.hilo.addr();
346
346
+
},
347
347
+
else => return true,
348
348
+
}
349
349
+
return false;
350
350
+
}
351
351
+
inline fn zpIndY(self: *Cpu, pins: *zesty.Pins) bool {
352
352
+
switch (self.cycle) {
353
353
+
0 => {
354
354
+
self.pc +%= 1;
355
355
+
pins.cpu_addr = self.pc;
356
356
+
},
357
357
+
1 => {
358
358
+
pins.cpu_addr = pins.cpu_data;
359
359
+
},
360
360
+
2 => {
361
361
+
self.hilo = .{ .lo = pins.cpu_data };
362
362
+
pins.cpu_addr +%= 1;
363
363
+
},
364
364
+
3 => {
365
365
+
self.hilo.hi = pins.cpu_data;
366
366
+
},
367
367
+
4 => {
368
368
+
pins.cpu_addr = self.hilo.addr();
369
369
+
},
370
370
+
else => return true,
371
371
+
}
372
372
+
return false;
373
373
+
}
374
374
+
375
375
+
inline fn fetch(self: *Cpu, pins: *zesty.Pins) void {
376
376
+
self.fetchAt(pins, self.pc +% 1);
377
377
+
}
378
378
+
379
379
+
inline fn fetchAt(self: *Cpu, pins: *zesty.Pins, pc: u16) void {
380
380
+
self.pc = pc;
381
381
+
pins.cpu_addr = pc;
382
382
+
self.sync = true;
383
383
+
}
384
384
+
385
385
+
//------------------------------------------------------
386
386
+
// Opcodes: Load/Store & Arithmetic
387
387
+
const Dst = enum { acc, mem };
388
388
+
389
389
+
inline fn ld(self: *Cpu, pins: *zesty.Pins, to: *u8, from: u8) void {
390
390
+
to.* = from;
391
391
+
self.setNZ(to.*);
392
392
+
self.fetch(pins);
393
393
+
}
394
394
+
inline fn st(self: *Cpu, pins: *zesty.Pins, from: u8) void {
395
395
+
pins.cpu_data = from;
396
396
+
pins.cpu_rw = .write;
397
397
+
self.fetch(pins);
398
398
+
}
399
399
+
inline fn ora(self: *Cpu, pins: *zesty.Pins, v: u8) void {
400
400
+
self.a |= v;
401
401
+
self.setNZ(self.a);
402
402
+
self.fetch(pins);
403
403
+
}
404
404
+
inline fn _and(self: *Cpu, pins: *zesty.Pins, v: u8) void {
405
405
+
self.a &= v;
406
406
+
self.setNZ(self.a);
407
407
+
self.fetch(pins);
408
408
+
}
409
409
+
inline fn eor(self: *Cpu, pins: *zesty.Pins, v: u8) void {
410
410
+
self.a ^= v;
411
411
+
self.setNZ(self.a);
412
412
+
self.fetch(pins);
413
413
+
}
414
414
+
inline fn asl(self: *Cpu, pins: *zesty.Pins, comptime dst: Dst) void {
415
415
+
const v, self.status.carry = switch (dst) {
416
416
+
.acc => v: {
417
417
+
self.a, const carry = @shlWithOverflow(self.a, 1);
418
418
+
break :v .{ self.a, carry > 0 };
419
419
+
},
420
420
+
.mem => v: {
421
421
+
pins.cpu_data, const carry = @shlWithOverflow(pins.cpu_data, 1);
422
422
+
pins.cpu_rw = .write;
423
423
+
break :v .{ pins.cpu_data, carry > 0 };
424
424
+
},
425
425
+
};
426
426
+
self.setNZ(v);
427
427
+
self.fetch(pins);
428
428
+
}
429
429
+
inline fn lsr(self: *Cpu, pins: *zesty.Pins, comptime dst: Dst) void {
430
430
+
// There's no @shrWithOverflow :(
431
431
+
const v, self.status.carry = switch (dst) {
432
432
+
.acc => v: {
433
433
+
const carry = self.a & 0b1;
434
434
+
self.a >>= 1;
435
435
+
break :v .{ self.a, carry > 0 };
436
436
+
},
437
437
+
.mem => v: {
438
438
+
const carry = pins.cpu_data & 0b1;
439
439
+
pins.cpu_data >>= 1;
440
440
+
pins.cpu_rw = .write;
441
441
+
break :v .{ pins.cpu_data, carry > 0 };
442
442
+
},
443
443
+
};
444
444
+
445
445
+
self.setNZ(v);
446
446
+
self.fetch(pins);
447
447
+
}
448
448
+
inline fn rol(self: *Cpu, pins: *zesty.Pins, comptime dst: Dst) void {
449
449
+
const v, self.status.carry = switch (dst) {
450
450
+
.acc => v: {
451
451
+
self.a, const carry = @shlWithOverflow(self.a, 1);
452
452
+
self.a |= @intFromBool(self.status.carry);
453
453
+
break :v .{ self.a, carry > 0 };
454
454
+
},
455
455
+
.mem => v: {
456
456
+
pins.cpu_data, const carry = @shlWithOverflow(pins.cpu_data, 1);
457
457
+
pins.cpu_data |= @intFromBool(self.status.carry);
458
458
+
pins.cpu_rw = .write;
459
459
+
break :v .{ pins.cpu_data, carry > 0 };
460
460
+
},
461
461
+
};
462
462
+
463
463
+
self.setNZ(v);
464
464
+
self.fetch(pins);
465
465
+
}
466
466
+
inline fn ror(self: *Cpu, pins: *zesty.Pins, comptime dst: Dst) void {
467
467
+
const v, self.status.carry = switch (dst) {
468
468
+
.acc => v: {
469
469
+
const carry = self.a & 1;
470
470
+
self.a >>= 1;
471
471
+
if (self.status.carry) self.a |= 0b1000_0000;
472
472
+
break :v .{ self.a, carry > 0 };
473
473
+
},
474
474
+
.mem => v: {
475
475
+
const carry = pins.cpu_data & 1;
476
476
+
pins.cpu_data >>= 1;
477
477
+
if (self.status.carry) pins.cpu_data |= 0b1000_0000;
478
478
+
pins.cpu_rw = .write;
479
479
+
break :v .{ pins.cpu_data, carry > 0 };
480
480
+
},
481
481
+
};
482
482
+
483
483
+
self.setNZ(v);
484
484
+
self.fetch(pins);
485
485
+
}
486
486
+
inline fn dec(self: *Cpu, pins: *zesty.Pins, v: *u8) void {
487
487
+
v.* -%= 1;
488
488
+
self.setNZ(v.*);
489
489
+
self.fetch(pins);
490
490
+
}
491
491
+
492
492
+
//------------------------------------------------------
493
493
+
// Opcodes: Control flow
494
494
+
495
495
+
/// Branch instructions (BPL, BMI, BVC, BVS, BCC, BCS, BNE, BEQ)
496
496
+
inline fn branch(self: *Cpu, pins: *zesty.Pins, cond: bool) void {
497
497
+
switch (self.cycle) {
498
498
+
0 => {
499
499
+
// TODO: Poll interrupts
500
500
+
self.pc +%= 1;
501
501
+
pins.cpu_addr = self.pc;
502
502
+
},
503
503
+
1 => {
504
504
+
pins.cpu_addr = self.pc;
505
505
+
// We're done
506
506
+
if (!cond) self.sync = true;
507
507
+
},
508
508
+
2 => {
509
509
+
self.hilo = .from(self.pc);
510
510
+
const pc, const page_crossed = self.hilo.offsetWithPageFaultBehavior(
511
511
+
pins.cpu_data,
512
512
+
true,
513
513
+
);
514
514
+
pins.cpu_addr = self.pc;
515
515
+
if (!page_crossed) self.fetchAt(pins, pc);
516
516
+
},
517
517
+
else => self.fetchAt(pins, self.hilo.addr()),
518
518
+
}
519
519
+
}
520
520
+
521
521
+
/// Set status flags (CLC, SEC, CLI, SEI, CLV, CLD, SED)
522
522
+
inline fn set(
523
523
+
self: *Cpu,
524
524
+
pins: *zesty.Pins,
525
525
+
flag: enum { carry, irq_disabled, overflow, decimal },
526
526
+
v: bool,
527
527
+
) void {
528
528
+
switch (flag) {
529
529
+
.carry => self.status.carry = v,
530
530
+
.irq_disabled => self.status.irq_disabled = v,
531
531
+
.overflow => self.status.overflow = v,
532
532
+
.decimal => self.status.decimal = v,
533
533
+
}
534
534
+
self.fetch(pins);
535
535
+
}
536
536
+
537
537
+
/// BRK (break)
538
538
+
inline fn brk(self: *Cpu, pins: *zesty.Pins) void {
539
539
+
const pc: Addr = .from(self.pc);
540
540
+
switch (self.cycle) {
541
541
+
0 => {
542
542
+
// Set B flag.
543
543
+
//
544
544
+
// The B flag isn't really a *real* flag in the status register
545
545
+
// on a silicon level, but we're setting it here for easier debugging
546
546
+
// visualization, and to also stop most tests from running any further.
547
547
+
self.status.brk = true;
548
548
+
549
549
+
// Store PC hi
550
550
+
self.hilo = .stack(self.sp);
551
551
+
pins.cpu_addr = self.hilo.addr();
552
552
+
553
553
+
pins.cpu_data = pc.hi;
554
554
+
pins.cpu_rw = .write;
555
555
+
self.sp -%= 1;
556
556
+
},
557
557
+
1 => {
558
558
+
// Store PC lo
559
559
+
self.hilo = .stack(self.sp);
560
560
+
pins.cpu_addr = self.hilo.addr();
561
561
+
562
562
+
pins.cpu_data = pc.lo;
563
563
+
pins.cpu_rw = .write;
564
564
+
self.sp -%= 1;
565
565
+
},
566
566
+
2 => {
567
567
+
// Store processor status
568
568
+
self.hilo = .stack(self.sp);
569
569
+
pins.cpu_addr = self.hilo.addr();
570
570
+
571
571
+
pins.cpu_data = self.status.toByte();
572
572
+
pins.cpu_rw = .write;
573
573
+
self.sp -%= 1;
574
574
+
},
575
575
+
3 => {
576
576
+
// Fetch 0xFFFE into PC lo
577
577
+
pins.cpu_addr = 0xfffe;
578
578
+
},
579
579
+
4 => {
580
580
+
// Fetch 0xFFFF into PC hi
581
581
+
self.hilo = .{ .lo = pins.cpu_data };
582
582
+
pins.cpu_addr = 0xffff;
583
583
+
},
584
584
+
else => {
585
585
+
// Break sequence is done. Unset B flag
586
586
+
self.status.brk = false;
587
587
+
self.hilo.hi = pins.cpu_data;
588
588
+
self.fetchAt(pins, self.hilo.addr());
589
589
+
},
590
590
+
}
591
591
+
}
592
592
+
593
593
+
/// RTI (return from interrupt)
594
594
+
inline fn rti(self: *Cpu, pins: *zesty.Pins) void {
595
595
+
switch (self.cycle) {
596
596
+
0 => {
597
597
+
pins.cpu_addr = self.pc;
598
598
+
},
599
599
+
1 => {
600
600
+
const stack: Addr = .stack(self.sp);
601
601
+
pins.cpu_addr = stack.addr();
602
602
+
self.sp +%= 1;
603
603
+
},
604
604
+
2 => {
605
605
+
// Pop status register
606
606
+
const stack: Addr = .stack(self.sp);
607
607
+
pins.cpu_addr = stack.addr();
608
608
+
self.sp +%= 1;
609
609
+
},
610
610
+
3 => {
611
611
+
self.status = .from(pins.cpu_data);
612
612
+
613
613
+
// Pop PC lo
614
614
+
const stack: Addr = .stack(self.sp);
615
615
+
pins.cpu_addr = stack.addr();
616
616
+
self.sp +%= 1;
617
617
+
},
618
618
+
4 => {
619
619
+
self.hilo = .{ .lo = pins.cpu_data };
620
620
+
621
621
+
// Pop PC hi
622
622
+
const stack: Addr = .stack(self.sp);
623
623
+
pins.cpu_addr = stack.addr();
624
624
+
self.sp +%= 1;
625
625
+
},
626
626
+
else => {
627
627
+
self.hilo.hi = pins.cpu_data;
628
628
+
self.fetchAt(pins, self.hilo.addr());
629
629
+
},
630
630
+
}
631
631
+
}
632
632
+
633
633
+
//------------------------------------------------------
634
634
+
// Helpers
635
635
+
636
636
+
inline fn setNZ(self: *Cpu, v: u8) void {
637
637
+
self.status.zero = v == 0;
638
638
+
self.status.negative = v >= 0x80;
639
639
+
}
640
640
+
641
641
+
pub const Status = packed struct(u8) {
642
642
+
carry: bool = false,
643
643
+
zero: bool = false,
644
644
+
irq_disabled: bool = true,
645
645
+
decimal: bool = false,
646
646
+
brk: bool = false,
647
647
+
_unused: bool = true,
648
648
+
overflow: bool = false,
649
649
+
negative: bool = false,
650
650
+
651
651
+
fn from(v: u8) Status {
652
652
+
var new: Status = @bitCast(v);
653
653
+
new._unused = true;
654
654
+
// BRK is never restored from a read value.
655
655
+
new.brk = false;
656
656
+
return new;
657
657
+
}
658
658
+
fn toByte(v: Status) u8 {
659
659
+
return @bitCast(v);
660
660
+
}
661
661
+
662
662
+
pub fn format(
663
663
+
self: Status,
664
664
+
writer: *std.Io.Writer,
665
665
+
) std.Io.Writer.Error!void {
666
666
+
try writer.writeByte(if (self.negative) 'N' else 'n');
667
667
+
try writer.writeByte(if (self.overflow) 'V' else 'v');
668
668
+
try writer.writeByte('-');
669
669
+
try writer.writeByte(if (self.brk) 'B' else 'b');
670
670
+
try writer.writeByte(if (self.decimal) 'D' else 'd');
671
671
+
try writer.writeByte(if (self.irq_disabled) 'I' else 'i');
672
672
+
try writer.writeByte(if (self.zero) 'Z' else 'z');
673
673
+
try writer.writeByte(if (self.carry) 'C' else 'c');
674
674
+
}
675
675
+
};
676
676
+
677
677
+
const Addr = packed struct(u16) {
678
678
+
lo: u8 = 0,
679
679
+
hi: u8 = 0,
680
680
+
681
681
+
fn stack(sp: u8) Addr {
682
682
+
return .{ .hi = 0x01, .lo = sp };
683
683
+
}
684
684
+
fn from(v: u16) Addr {
685
685
+
return @bitCast(v);
686
686
+
}
687
687
+
fn addr(v: Addr) u16 {
688
688
+
return @bitCast(v);
689
689
+
}
690
690
+
691
691
+
/// The 6502 CPU has a very interesting behavior where if an internal
692
692
+
/// address has an offset applied to it, and said offset would make the
693
693
+
/// address cross pages, then the CPU would require one extra cycle to
694
694
+
/// properly calculate the final address.
695
695
+
///
696
696
+
/// This calculation can also be signed, which is used in branch offsets.
697
697
+
inline fn offsetWithPageFaultBehavior(
698
698
+
self: *Addr,
699
699
+
v: u8,
700
700
+
signed: bool,
701
701
+
) struct { u16, bool } {
702
702
+
self.lo, const page_crossed = if (!signed and v < 0x80)
703
703
+
@addWithOverflow(self.lo, v)
704
704
+
else
705
705
+
// v should NEVER be lower than 0x80, but saturate down to 0 to be safe
706
706
+
@subWithOverflow(self.lo, v -| 0x80);
707
707
+
708
708
+
// If we haven't crossed the page boundary, skip over the next cycle.
709
709
+
defer self.hi +%= page_crossed;
710
710
+
return .{ self.addr(), page_crossed > 0 };
711
711
+
}
712
712
+
};
+176
src/Ppu.zig
···
1
1
+
//! The NES's 2C02 Picture Processing Unit (PPU).
2
2
+
const Ppu = @This();
3
3
+
4
4
+
const std = @import("std");
5
5
+
const zesty = @import("zesty.zig");
6
6
+
7
7
+
oam: [0x100]u8 = @splat(0xaa),
8
8
+
palette: [0x20]u8 = @splat(0xaa),
9
9
+
10
10
+
//------------------------------------------
11
11
+
// Registers
12
12
+
13
13
+
/// If false, then this is the first write.
14
14
+
/// Cleared by reading `status`
15
15
+
first_write: bool = false,
16
16
+
17
17
+
ctrl: Ctrl = .init,
18
18
+
mask: Mask = .init,
19
19
+
status: Status = .init,
20
20
+
oam_addr: u8 = 0,
21
21
+
oam_data: u8 = 0,
22
22
+
23
23
+
scroll_x: u8 = 0,
24
24
+
scroll_y: u8 = 0,
25
25
+
26
26
+
/// PPUDATA read buffer.
27
27
+
/// See https://www.nesdev.org/wiki/PPU_registers#The_PPUDATA_read_buffer
28
28
+
ppu_data_buf: u8 = 0,
29
29
+
30
30
+
pub const Ctrl = packed struct(u8) {
31
31
+
scroll: u2,
32
32
+
increment: enum(u1) { horizontal, vertical },
33
33
+
spr_tile_select: bool,
34
34
+
bgr_tile_select: bool,
35
35
+
sprite_height: bool,
36
36
+
master_slave: bool,
37
37
+
nmi_enable: bool,
38
38
+
39
39
+
pub const init: Ctrl = .{
40
40
+
.scroll = 0,
41
41
+
.increment = .horizontal,
42
42
+
.spr_tile_select = false,
43
43
+
.bgr_tile_select = false,
44
44
+
.sprite_height = false,
45
45
+
.master_slave = false,
46
46
+
.nmi_enable = false,
47
47
+
};
48
48
+
};
49
49
+
pub const Mask = packed struct(u8) {
50
50
+
grayscale: bool,
51
51
+
bgr_left_col_enable: bool,
52
52
+
spr_left_col_enable: bool,
53
53
+
bgr_enable: bool,
54
54
+
spr_enable: bool,
55
55
+
emphasize_red: bool,
56
56
+
emphasize_green: bool,
57
57
+
emphasize_blue: bool,
58
58
+
59
59
+
pub const init: Mask = .{
60
60
+
.grayscale = false,
61
61
+
.bgr_left_col_enable = false,
62
62
+
.spr_left_col_enable = false,
63
63
+
.bgr_enable = false,
64
64
+
.spr_enable = false,
65
65
+
.emphasize_red = false,
66
66
+
.emphasize_green = false,
67
67
+
.emphasize_blue = false,
68
68
+
};
69
69
+
};
70
70
+
pub const Status = packed struct(u8) {
71
71
+
_open_bus: u5,
72
72
+
spr_overflow: bool,
73
73
+
spr_0_hit: bool,
74
74
+
vblank: bool,
75
75
+
76
76
+
pub const init: Status = .{
77
77
+
._open_bus = 0,
78
78
+
.spr_overflow = true,
79
79
+
.spr_0_hit = false,
80
80
+
.vblank = true,
81
81
+
};
82
82
+
};
83
83
+
84
84
+
pub const Oam = packed struct {
85
85
+
y: u8,
86
86
+
tile: u8,
87
87
+
attr: u8,
88
88
+
x: u8,
89
89
+
};
90
90
+
91
91
+
pub fn tick(self: *Ppu, pins: *zesty.Pins) void {
92
92
+
if (pins.ppuChipSelect()) self.updateRegisters(pins);
93
93
+
}
94
94
+
95
95
+
/// Refresh the CPU-side address and data pins
96
96
+
/// from contents of registers.
97
97
+
fn updateRegisters(self: *Ppu, pins: *zesty.Pins) void {
98
98
+
switch (pins.cpu_addr & 0x7) {
99
99
+
// PPUCTRL (write only)
100
100
+
0 => switch (pins.cpu_rw) {
101
101
+
.read => {},
102
102
+
.write => self.ctrl = @bitCast(pins.cpu_data),
103
103
+
},
104
104
+
// PPUMASK (write only)
105
105
+
1 => switch (pins.cpu_rw) {
106
106
+
.read => {},
107
107
+
.write => self.mask = @bitCast(pins.cpu_data),
108
108
+
},
109
109
+
// PPUSTATUS (read only)
110
110
+
2 => switch (pins.cpu_rw) {
111
111
+
.read => {
112
112
+
// TODO: Add 2C05 identifier support
113
113
+
self.status._open_bus = @truncate(pins.cpu_data);
114
114
+
// Side-effect: clear first write flag
115
115
+
self.first_write = false;
116
116
+
pins.cpu_data = @bitCast(self.status);
117
117
+
},
118
118
+
.write => {},
119
119
+
},
120
120
+
// OAMADDR (write only)
121
121
+
3 => switch (pins.cpu_rw) {
122
122
+
.read => {},
123
123
+
.write => self.oam_addr = pins.cpu_data,
124
124
+
},
125
125
+
// TODO: OAMDATA (read/write)
126
126
+
4 => {},
127
127
+
// PPUSCROLL (write only)
128
128
+
5 => switch (pins.cpu_rw) {
129
129
+
.read => {},
130
130
+
.write => {
131
131
+
if (self.first_write)
132
132
+
self.scroll_x = pins.cpu_data
133
133
+
else
134
134
+
self.scroll_y = pins.cpu_data;
135
135
+
self.first_write = !self.first_write;
136
136
+
},
137
137
+
},
138
138
+
// PPUADDR (write only)
139
139
+
6 => switch (pins.cpu_rw) {
140
140
+
.read => {},
141
141
+
.write => {
142
142
+
if (self.first_write)
143
143
+
pins.ppu_addr_hi = @truncate(pins.cpu_data)
144
144
+
else
145
145
+
pins.ppu_addr_data = pins.cpu_data;
146
146
+
self.first_write = !self.first_write;
147
147
+
},
148
148
+
},
149
149
+
// PPUDATA (read/write)
150
150
+
7 => {
151
151
+
switch (pins.cpu_rw) {
152
152
+
.read => {
153
153
+
// Use the cached PPUDATA
154
154
+
pins.cpu_data = self.ppu_data_buf;
155
155
+
pins.ppu_rd = true;
156
156
+
pins.ppu_wr = true;
157
157
+
},
158
158
+
.write => {
159
159
+
pins.ppu_addr_data = pins.cpu_data;
160
160
+
pins.ppu_wr = true;
161
161
+
pins.ppu_rd = false;
162
162
+
},
163
163
+
}
164
164
+
// TODO: increment
165
165
+
// self.pins.addr_data +%= switch (self.ppu.ctrl.increment) {
166
166
+
// .horizontal => 1,
167
167
+
// .vertical => 32,
168
168
+
// };
169
169
+
},
170
170
+
else => unreachable,
171
171
+
}
172
172
+
}
173
173
+
174
174
+
pub fn oamSlice(self: *Ppu) [64]Oam {
175
175
+
return std.mem.bytesAsSlice(self.oam);
176
176
+
}
+22
src/cartridge.zig
···
1
1
+
//! NES and Famicom cartridges.
2
2
+
const std = @import("std");
3
3
+
const zesty = @import("zesty.zig");
4
4
+
5
5
+
pub const Cartridge = union(enum) {
6
6
+
nrom128: Nrom128,
7
7
+
8
8
+
pub fn update(self: *Cartridge, pins: *zesty.Pins) void {
9
9
+
if (pins.romSel()) switch (self.*) {
10
10
+
inline else => |*v| v.update(pins),
11
11
+
};
12
12
+
}
13
13
+
};
14
14
+
15
15
+
pub const Nrom128 = struct {
16
16
+
rom: [0x4000]u8 = @splat(0x00),
17
17
+
18
18
+
pub fn update(self: *Nrom128, pins: *zesty.Pins) void {
19
19
+
if (pins.cpu_addr < 0x8000 or pins.cpu_rw != .read) return;
20
20
+
pins.cpu_data = self.rom[pins.cpu_addr & 0x3fff];
21
21
+
}
22
22
+
};
+5
src/main.zig
···
1
1
+
const std = @import("std");
2
2
+
3
3
+
pub fn main() !void {
4
4
+
std.debug.print("Hello from Zesty :3");
5
5
+
}
+3
src/tests.zig
···
1
1
+
test {
2
2
+
_ = @import("tests/cpu.zig");
3
3
+
}
+100
src/tests/cpu.zig
···
1
1
+
const std = @import("std");
2
2
+
const zesty = @import("../zesty.zig");
3
3
+
const assembler = @import("cpu/assembler.zig");
4
4
+
5
5
+
pub const assemble = assembler.assembleComptime;
6
6
+
7
7
+
test {
8
8
+
_ = assembler;
9
9
+
_ = @import("cpu/addressing.zig");
10
10
+
_ = @import("cpu/alu.zig");
11
11
+
_ = @import("cpu/control_flow.zig");
12
12
+
}
13
13
+
14
14
+
//------------------------------------------------------
15
15
+
// Test harnesses
16
16
+
17
17
+
pub const TestInit = struct {
18
18
+
/// NMI vector
19
19
+
nmi: u16 = 0x8000,
20
20
+
/// RESET vector
21
21
+
reset: u16 = 0x8000,
22
22
+
/// IRQ/BRK vector
23
23
+
irq: u16 = 0x8000,
24
24
+
25
25
+
/// Pre-run the reset sequence
26
26
+
init_cpu: bool = true,
27
27
+
28
28
+
ram: []const struct { u16, []const u8 } = &.{},
29
29
+
rom: []const struct { u16, []const u8 } = &.{},
30
30
+
};
31
31
+
32
32
+
pub fn testZes(opts: TestInit) zesty.Zesty {
33
33
+
var cart: zesty.cartridge.Nrom128 = .{};
34
34
+
35
35
+
// NMI vector
36
36
+
@memmove(
37
37
+
cart.rom[0xfffa & 0x3fff ..][0..2],
38
38
+
&std.mem.toBytes(std.mem.nativeToLittle(u16, opts.nmi)),
39
39
+
);
40
40
+
// RESET vector
41
41
+
@memmove(
42
42
+
cart.rom[0xfffc & 0x3fff ..][0..2],
43
43
+
&std.mem.toBytes(std.mem.nativeToLittle(u16, opts.reset)),
44
44
+
);
45
45
+
// IRQ vector
46
46
+
@memmove(
47
47
+
cart.rom[0xfffe & 0x3fff ..][0..2],
48
48
+
&std.mem.toBytes(std.mem.nativeToLittle(u16, opts.irq)),
49
49
+
);
50
50
+
51
51
+
for (opts.rom) |v| {
52
52
+
const offset, const bytes = v;
53
53
+
@memmove(cart.rom[offset & 0x3fff ..][0..bytes.len], bytes);
54
54
+
}
55
55
+
var z: zesty.Zesty = .init(.top_loader_ntsc);
56
56
+
z.cart = .{ .nrom128 = cart };
57
57
+
58
58
+
for (opts.ram) |v| {
59
59
+
const offset, const bytes = v;
60
60
+
@memmove(z.ram[offset & 0x7ff ..][0..bytes.len], bytes);
61
61
+
}
62
62
+
63
63
+
if (opts.init_cpu)
64
64
+
while (!z.cpu.sync) z.stepCpu();
65
65
+
66
66
+
return z;
67
67
+
}
68
68
+
69
69
+
pub fn run(z: *zesty.Zesty) void {
70
70
+
z.stepCpu();
71
71
+
while (!z.cpu.sync) z.stepCpu();
72
72
+
}
73
73
+
74
74
+
pub fn runDebug(z: *zesty.Zesty) void {
75
75
+
z.stepCpu();
76
76
+
debug(z);
77
77
+
while (!z.cpu.sync) {
78
78
+
z.stepCpu();
79
79
+
debug(z);
80
80
+
}
81
81
+
}
82
82
+
83
83
+
pub fn debug(z: *zesty.Zesty) void {
84
84
+
std.debug.print(
85
85
+
"#{} addr:{X:04} data:{X:02} {c} pc:{X:04} a:{X:02} x:{X:02} y:{X:02} sp:{X:02} ir:{X:02} {f}\n",
86
86
+
.{
87
87
+
z.cpu.cycle -| 1,
88
88
+
z.pins.cpu_addr,
89
89
+
z.pins.cpu_data,
90
90
+
@as(u8, if (z.pins.cpu_rw == .read) 'r' else 'w'),
91
91
+
z.cpu.pc,
92
92
+
z.cpu.a,
93
93
+
z.cpu.x,
94
94
+
z.cpu.y,
95
95
+
z.cpu.sp,
96
96
+
z.cpu.opcode,
97
97
+
z.cpu.status,
98
98
+
},
99
99
+
);
100
100
+
}
+291
src/tests/cpu/addressing.zig
···
1
1
+
//! Various addressing mode tests.
2
2
+
const std = @import("std");
3
3
+
const cpu = @import("../cpu.zig");
4
4
+
5
5
+
// Implicitly-addressed instructions and
6
6
+
// branch instructions are separately tested.
7
7
+
8
8
+
test "immediate" {
9
9
+
var z = cpu.testZes(.{
10
10
+
.rom = &.{
11
11
+
.{
12
12
+
0x8000, cpu.assemble(
13
13
+
\\LDA #$42
14
14
+
),
15
15
+
},
16
16
+
},
17
17
+
});
18
18
+
19
19
+
// Cycle 0. Fetch operand
20
20
+
z.stepCpu();
21
21
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
22
22
+
try std.testing.expectEqual(0x8001, z.pins.cpu_addr);
23
23
+
try std.testing.expectEqual(0x42, z.pins.cpu_data);
24
24
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
25
25
+
26
26
+
// Cycle 1. Load operand into register
27
27
+
z.stepCpu();
28
28
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
29
29
+
try std.testing.expectEqual(0x8002, z.pins.cpu_addr);
30
30
+
try std.testing.expectEqual(0x42, z.cpu.a);
31
31
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
32
32
+
try std.testing.expect(!z.cpu.status.negative);
33
33
+
try std.testing.expect(!z.cpu.status.zero);
34
34
+
}
35
35
+
36
36
+
test "zeropage" {
37
37
+
var z = cpu.testZes(.{
38
38
+
.ram = &.{
39
39
+
.{ 0x42, &.{0xcc} },
40
40
+
},
41
41
+
.rom = &.{
42
42
+
.{
43
43
+
0x8000, cpu.assemble(
44
44
+
\\LDX <$42
45
45
+
),
46
46
+
},
47
47
+
},
48
48
+
});
49
49
+
50
50
+
// Cycle 0. Fetch address
51
51
+
z.stepCpu();
52
52
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
53
53
+
try std.testing.expectEqual(0x8001, z.pins.cpu_addr);
54
54
+
try std.testing.expectEqual(0x42, z.pins.cpu_data);
55
55
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
56
56
+
57
57
+
// Cycle 1. Fetch operand at address
58
58
+
z.stepCpu();
59
59
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
60
60
+
try std.testing.expectEqual(0x0042, z.pins.cpu_addr);
61
61
+
try std.testing.expectEqual(0xcc, z.pins.cpu_data);
62
62
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
63
63
+
64
64
+
// Cycle 2. Load operand into register
65
65
+
z.stepCpu();
66
66
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
67
67
+
try std.testing.expectEqual(0x8002, z.pins.cpu_addr);
68
68
+
try std.testing.expectEqual(0xcc, z.cpu.x);
69
69
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
70
70
+
try std.testing.expect(z.cpu.status.negative);
71
71
+
try std.testing.expect(!z.cpu.status.zero);
72
72
+
}
73
73
+
74
74
+
test "zeropage indexed" {
75
75
+
var z = cpu.testZes(.{
76
76
+
.ram = &.{
77
77
+
.{ 0x40, &.{ 0xcc, 0xdd, 0xee } },
78
78
+
.{ 0xdd, &.{ 0xff, 0x77 } },
79
79
+
},
80
80
+
.rom = &.{
81
81
+
.{
82
82
+
0x8000, cpu.assemble(
83
83
+
\\LDX <$40,Y
84
84
+
\\LDA <$01,X
85
85
+
),
86
86
+
},
87
87
+
},
88
88
+
});
89
89
+
// Assume Y=1
90
90
+
z.cpu.y = 1;
91
91
+
92
92
+
//----------------------
93
93
+
// zp,Y
94
94
+
95
95
+
// Cycle 0. Fetch address
96
96
+
z.stepCpu();
97
97
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
98
98
+
try std.testing.expectEqual(0x8001, z.pins.cpu_addr);
99
99
+
try std.testing.expectEqual(0x40, z.pins.cpu_data);
100
100
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
101
101
+
102
102
+
// Cycle 1. Dummy read at address
103
103
+
z.stepCpu();
104
104
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
105
105
+
try std.testing.expectEqual(0x0040, z.pins.cpu_addr);
106
106
+
try std.testing.expectEqual(0xcc, z.pins.cpu_data);
107
107
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
108
108
+
109
109
+
// Cycle 2. Fetch operand at (address + Y)
110
110
+
z.stepCpu();
111
111
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
112
112
+
try std.testing.expectEqual(0x0041, z.pins.cpu_addr);
113
113
+
try std.testing.expectEqual(0xdd, z.pins.cpu_data);
114
114
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
115
115
+
116
116
+
// Cycle 3. Load operand into register
117
117
+
z.stepCpu();
118
118
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
119
119
+
try std.testing.expectEqual(0x8002, z.pins.cpu_addr);
120
120
+
try std.testing.expectEqual(0xdd, z.cpu.x);
121
121
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
122
122
+
try std.testing.expect(z.cpu.status.negative);
123
123
+
try std.testing.expect(!z.cpu.status.zero);
124
124
+
125
125
+
//----------------------
126
126
+
// zp,X
127
127
+
128
128
+
// Cycle 0. Fetch address
129
129
+
z.stepCpu();
130
130
+
try std.testing.expectEqual(0x8003, z.cpu.pc);
131
131
+
try std.testing.expectEqual(0x8003, z.pins.cpu_addr);
132
132
+
try std.testing.expectEqual(0x01, z.pins.cpu_data);
133
133
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
134
134
+
135
135
+
// Cycle 1. Dummy read at address
136
136
+
z.stepCpu();
137
137
+
try std.testing.expectEqual(0x8003, z.cpu.pc);
138
138
+
try std.testing.expectEqual(0x0001, z.pins.cpu_addr);
139
139
+
// Uninit
140
140
+
try std.testing.expectEqual(0xaa, z.pins.cpu_data);
141
141
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
142
142
+
143
143
+
// Cycle 2. Fetch operand at (address + X)
144
144
+
z.stepCpu();
145
145
+
try std.testing.expectEqual(0x8003, z.cpu.pc);
146
146
+
try std.testing.expectEqual(0x00de, z.pins.cpu_addr);
147
147
+
try std.testing.expectEqual(0x77, z.pins.cpu_data);
148
148
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
149
149
+
150
150
+
// Cycle 3. Load operand into register
151
151
+
z.stepCpu();
152
152
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
153
153
+
try std.testing.expectEqual(0x8004, z.pins.cpu_addr);
154
154
+
try std.testing.expectEqual(0x77, z.cpu.a);
155
155
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
156
156
+
try std.testing.expect(!z.cpu.status.negative);
157
157
+
try std.testing.expect(!z.cpu.status.zero);
158
158
+
}
159
159
+
160
160
+
test "absolute" {
161
161
+
var z = cpu.testZes(.{
162
162
+
.rom = &.{
163
163
+
.{
164
164
+
0x8000, cpu.assemble(
165
165
+
\\LDY $8964
166
166
+
),
167
167
+
},
168
168
+
.{ 0x8964, &.{0x00} },
169
169
+
},
170
170
+
});
171
171
+
172
172
+
// Cycle 0. Fetch address lo byte
173
173
+
z.stepCpu();
174
174
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
175
175
+
try std.testing.expectEqual(0x8001, z.pins.cpu_addr);
176
176
+
try std.testing.expectEqual(0x64, z.pins.cpu_data);
177
177
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
178
178
+
179
179
+
// Cycle 1. Fetch address hi byte
180
180
+
z.stepCpu();
181
181
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
182
182
+
try std.testing.expectEqual(0x8002, z.pins.cpu_addr);
183
183
+
try std.testing.expectEqual(0x89, z.pins.cpu_data);
184
184
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
185
185
+
186
186
+
// Cycle 2. Fetch operand at address
187
187
+
z.stepCpu();
188
188
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
189
189
+
try std.testing.expectEqual(0x8964, z.pins.cpu_addr);
190
190
+
try std.testing.expectEqual(0x00, z.pins.cpu_data);
191
191
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
192
192
+
193
193
+
// Cycle 3. Load operand into register
194
194
+
z.stepCpu();
195
195
+
try std.testing.expectEqual(0x8003, z.cpu.pc);
196
196
+
try std.testing.expectEqual(0x8003, z.pins.cpu_addr);
197
197
+
try std.testing.expectEqual(0x00, z.cpu.y);
198
198
+
try std.testing.expect(!z.cpu.status.negative);
199
199
+
try std.testing.expect(z.cpu.status.zero);
200
200
+
}
201
201
+
202
202
+
test "absolute indexed" {
203
203
+
var z = cpu.testZes(.{
204
204
+
.rom = &.{
205
205
+
.{
206
206
+
0x8000, cpu.assemble(
207
207
+
\\LDX $cc40, Y
208
208
+
\\LDA $dd01, X
209
209
+
),
210
210
+
},
211
211
+
.{ 0xcc40, &.{ 0x22, 0x33, 0x44 } },
212
212
+
.{ 0xdd33, &.{ 0x1f, 0x00 } },
213
213
+
},
214
214
+
});
215
215
+
// Assume Y=1
216
216
+
z.cpu.y = 1;
217
217
+
218
218
+
//----------------------
219
219
+
// abs,Y
220
220
+
221
221
+
// Cycle 0. Fetch address lo
222
222
+
z.stepCpu();
223
223
+
try std.testing.expectEqual(0x8001, z.cpu.pc);
224
224
+
try std.testing.expectEqual(0x8001, z.pins.cpu_addr);
225
225
+
try std.testing.expectEqual(0x40, z.pins.cpu_data);
226
226
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
227
227
+
228
228
+
// Cycle 1. Fetch address hi
229
229
+
z.stepCpu();
230
230
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
231
231
+
try std.testing.expectEqual(0x8002, z.pins.cpu_addr);
232
232
+
try std.testing.expectEqual(0xcc, z.pins.cpu_data);
233
233
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
234
234
+
235
235
+
// Cycle 2. Fetch operand at (address + Y)
236
236
+
z.stepCpu();
237
237
+
try std.testing.expectEqual(0x8002, z.cpu.pc);
238
238
+
try std.testing.expectEqual(0xcc41, z.pins.cpu_addr);
239
239
+
try std.testing.expectEqual(0x33, z.pins.cpu_data);
240
240
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
241
241
+
242
242
+
// Cycle 3. Load operand into register
243
243
+
z.stepCpu();
244
244
+
try std.testing.expectEqual(0x8003, z.cpu.pc);
245
245
+
try std.testing.expectEqual(0x8003, z.pins.cpu_addr);
246
246
+
try std.testing.expectEqual(0x33, z.cpu.x);
247
247
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
248
248
+
try std.testing.expect(!z.cpu.status.negative);
249
249
+
try std.testing.expect(!z.cpu.status.zero);
250
250
+
251
251
+
//----------------------
252
252
+
// abs,X
253
253
+
254
254
+
// Cycle 0. Fetch address lo
255
255
+
z.stepCpu();
256
256
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
257
257
+
try std.testing.expectEqual(0x8004, z.pins.cpu_addr);
258
258
+
try std.testing.expectEqual(0x01, z.pins.cpu_data);
259
259
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
260
260
+
261
261
+
// Cycle 1. Fetch address hi
262
262
+
z.stepCpu();
263
263
+
try std.testing.expectEqual(0x8005, z.cpu.pc);
264
264
+
try std.testing.expectEqual(0x8005, z.pins.cpu_addr);
265
265
+
try std.testing.expectEqual(0xdd, z.pins.cpu_data);
266
266
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
267
267
+
268
268
+
// Cycle 2. Fetch operand at (address + X)
269
269
+
z.stepCpu();
270
270
+
try std.testing.expectEqual(0x8005, z.cpu.pc);
271
271
+
try std.testing.expectEqual(0xdd34, z.pins.cpu_addr);
272
272
+
try std.testing.expectEqual(0x00, z.pins.cpu_data);
273
273
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
274
274
+
275
275
+
// Cycle 3. Load operand into register
276
276
+
z.stepCpu();
277
277
+
try std.testing.expectEqual(0x8006, z.cpu.pc);
278
278
+
try std.testing.expectEqual(0x8006, z.pins.cpu_addr);
279
279
+
try std.testing.expectEqual(0x00, z.cpu.a);
280
280
+
try std.testing.expectEqual(.read, z.pins.cpu_rw);
281
281
+
try std.testing.expect(!z.cpu.status.negative);
282
282
+
try std.testing.expect(z.cpu.status.zero);
283
283
+
}
284
284
+
285
285
+
test "relative" {}
286
286
+
287
287
+
test "indirect" {}
288
288
+
289
289
+
test "indexed indirect" {}
290
290
+
291
291
+
test "indirect indexed" {}
+133
src/tests/cpu/alu.zig
···
1
1
+
//! ALU-related tests.
2
2
+
const std = @import("std");
3
3
+
const cpu = @import("../cpu.zig");
4
4
+
5
5
+
test "ORA" {
6
6
+
var z = cpu.testZes(.{ .rom = &.{
7
7
+
.{
8
8
+
0x8000, cpu.assemble(
9
9
+
\\LDA #%01000010
10
10
+
\\ORA #%11000000
11
11
+
),
12
12
+
},
13
13
+
} });
14
14
+
cpu.run(&z); // Setup A register
15
15
+
cpu.run(&z);
16
16
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
17
17
+
try std.testing.expectEqual(0b11000010, z.cpu.a);
18
18
+
try std.testing.expect(z.cpu.status.negative);
19
19
+
try std.testing.expect(!z.cpu.status.zero);
20
20
+
}
21
21
+
22
22
+
test "AND" {
23
23
+
var z = cpu.testZes(.{ .rom = &.{
24
24
+
.{
25
25
+
0x8000, cpu.assemble(
26
26
+
\\LDA #%01000010
27
27
+
\\AND #%11000000
28
28
+
),
29
29
+
},
30
30
+
} });
31
31
+
cpu.run(&z); // Setup A register
32
32
+
cpu.run(&z);
33
33
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
34
34
+
try std.testing.expectEqual(0b01000000, z.cpu.a);
35
35
+
try std.testing.expect(!z.cpu.status.negative);
36
36
+
try std.testing.expect(!z.cpu.status.zero);
37
37
+
}
38
38
+
39
39
+
test "EOR" {
40
40
+
var z = cpu.testZes(.{ .rom = &.{
41
41
+
.{
42
42
+
0x8000, cpu.assemble(
43
43
+
\\LDA #%01000010
44
44
+
\\EOR #%11000000
45
45
+
),
46
46
+
},
47
47
+
} });
48
48
+
cpu.run(&z); // Setup A register
49
49
+
cpu.run(&z);
50
50
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
51
51
+
try std.testing.expectEqual(0b10000010, z.cpu.a);
52
52
+
try std.testing.expect(z.cpu.status.negative);
53
53
+
try std.testing.expect(!z.cpu.status.zero);
54
54
+
}
55
55
+
56
56
+
test "ASL" {
57
57
+
var z = cpu.testZes(.{ .rom = &.{
58
58
+
.{
59
59
+
0x8000, cpu.assemble(
60
60
+
\\LDA #%10000101
61
61
+
\\ASL
62
62
+
),
63
63
+
},
64
64
+
} });
65
65
+
cpu.run(&z); // Setup A register
66
66
+
cpu.run(&z);
67
67
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
68
68
+
try std.testing.expectEqual(0b00001010, z.cpu.a);
69
69
+
try std.testing.expect(!z.cpu.status.negative);
70
70
+
try std.testing.expect(!z.cpu.status.zero);
71
71
+
try std.testing.expect(z.cpu.status.carry);
72
72
+
}
73
73
+
test "LSR" {
74
74
+
var z = cpu.testZes(.{ .rom = &.{
75
75
+
.{
76
76
+
0x8000, cpu.assemble(
77
77
+
\\LDA #%10000101
78
78
+
\\LSR
79
79
+
),
80
80
+
},
81
81
+
} });
82
82
+
cpu.run(&z); // Setup A register
83
83
+
cpu.run(&z);
84
84
+
try std.testing.expectEqual(0x8004, z.cpu.pc);
85
85
+
try std.testing.expectEqual(0b01000010, z.cpu.a);
86
86
+
try std.testing.expect(!z.cpu.status.negative);
87
87
+
try std.testing.expect(!z.cpu.status.zero);
88
88
+
try std.testing.expect(z.cpu.status.carry);
89
89
+
}
90
90
+
test "ROL" {
91
91
+
var z = cpu.testZes(.{
92
92
+
.rom = &.{
93
93
+
.{
94
94
+
0x8000, cpu.assemble(
95
95
+
\\LDA #%10000101
96
96
+
\\SEC
97
97
+
\\ROL
98
98
+
),
99
99
+
},
100
100
+
},
101
101
+
});
102
102
+
cpu.run(&z); // Setup A register
103
103
+
cpu.run(&z); // Set carry
104
104
+
cpu.run(&z);
105
105
+
106
106
+
try std.testing.expectEqual(0x8005, z.cpu.pc);
107
107
+
try std.testing.expectEqual(0b00001011, z.cpu.a);
108
108
+
try std.testing.expect(!z.cpu.status.negative);
109
109
+
try std.testing.expect(!z.cpu.status.zero);
110
110
+
try std.testing.expect(z.cpu.status.carry);
111
111
+
}
112
112
+
test "ROR" {
113
113
+
var z = cpu.testZes(.{
114
114
+
.rom = &.{
115
115
+
.{
116
116
+
0x8000, cpu.assemble(
117
117
+
\\LDA #%10000101
118
118
+
\\SEC
119
119
+
\\ROR
120
120
+
),
121
121
+
},
122
122
+
},
123
123
+
});
124
124
+
cpu.run(&z); // Setup A register
125
125
+
cpu.run(&z); // Set carry
126
126
+
cpu.run(&z);
127
127
+
128
128
+
try std.testing.expectEqual(0x8005, z.cpu.pc);
129
129
+
try std.testing.expectEqual(0b11000010, z.cpu.a);
130
130
+
try std.testing.expect(z.cpu.status.negative);
131
131
+
try std.testing.expect(!z.cpu.status.zero);
132
132
+
try std.testing.expect(z.cpu.status.carry);
133
133
+
}
+405
src/tests/cpu/assembler.zig
···
1
1
+
//! A comptime assembler.
2
2
+
//!
3
3
+
//! Not at all ready for real-world use; this is only designed
4
4
+
//! to make the test cases easier to write.
5
5
+
const std = @import("std");
6
6
+
7
7
+
pub fn assemble(
8
8
+
in: []const u8,
9
9
+
writer: *std.Io.Writer,
10
10
+
) !void {
11
11
+
var it = std.mem.tokenizeScalar(u8, in, '\n');
12
12
+
13
13
+
while (it.next()) |l| {
14
14
+
var line = l;
15
15
+
if (std.mem.indexOfScalar(u8, line, ';')) |semicolon|
16
16
+
line = line[0..semicolon];
17
17
+
line = std.mem.trim(u8, line, whitespace);
18
18
+
19
19
+
var tokens = std.mem.tokenizeScalar(u8, line, ' ');
20
20
+
21
21
+
const opcode_s = tokens.next() orelse continue;
22
22
+
const opcode: Opcode = for (std.meta.tags(Opcode)) |op| {
23
23
+
if (std.ascii.eqlIgnoreCase(@tagName(op), opcode_s)) break op;
24
24
+
} else return error.InvalidOpcode;
25
25
+
26
26
+
const rest = tokens.rest();
27
27
+
const operand: Operand = if (rest.len > 0) try .parse(rest) else .implied;
28
28
+
const op_byte = opcode.encode(operand) orelse return error.InvalidPairing;
29
29
+
30
30
+
try writer.writeByte(op_byte);
31
31
+
switch (operand) {
32
32
+
.implied => {},
33
33
+
34
34
+
.immediate,
35
35
+
.zeropage,
36
36
+
.zeropage_x,
37
37
+
.zeropage_y,
38
38
+
.indexed_indirect,
39
39
+
.indirect_indexed,
40
40
+
=> |v| try writer.writeByte(v),
41
41
+
42
42
+
.absolute,
43
43
+
.absolute_x,
44
44
+
.absolute_y,
45
45
+
.indexed,
46
46
+
=> |v| try writer.writeInt(u16, v, .little),
47
47
+
}
48
48
+
}
49
49
+
}
50
50
+
51
51
+
pub fn assembleComptime(comptime in: []const u8) []const u8 {
52
52
+
// Heuristically defined
53
53
+
@setEvalBranchQuota(in.len * 100);
54
54
+
55
55
+
return comptime v: {
56
56
+
var count_w: std.Io.Writer.Discarding = .init(&.{});
57
57
+
assemble(in, &count_w.writer) catch |err| std.debug.panic("error: {}", .{err});
58
58
+
59
59
+
var buf: [count_w.fullCount()]u8 = undefined;
60
60
+
var w: std.Io.Writer = .fixed(&buf);
61
61
+
assemble(in, &w) catch unreachable;
62
62
+
63
63
+
const result = buf;
64
64
+
break :v &result;
65
65
+
};
66
66
+
}
67
67
+
68
68
+
pub const Opcode = enum {
69
69
+
brk,
70
70
+
lda,
71
71
+
ldx,
72
72
+
ldy,
73
73
+
sta,
74
74
+
stx,
75
75
+
sty,
76
76
+
77
77
+
ora,
78
78
+
@"and",
79
79
+
eor,
80
80
+
asl,
81
81
+
lsr,
82
82
+
rol,
83
83
+
ror,
84
84
+
85
85
+
clc,
86
86
+
sec,
87
87
+
cli,
88
88
+
sei,
89
89
+
clv,
90
90
+
cld,
91
91
+
sed,
92
92
+
93
93
+
fn encode(op: Opcode, opr: Operand) ?u8 {
94
94
+
return switch (op) {
95
95
+
.brk => switch (opr) {
96
96
+
// A common extension is to allow BRK to take an operand,
97
97
+
// though it is usually completely ignored.
98
98
+
.implied, .immediate => 0x00,
99
99
+
else => null,
100
100
+
},
101
101
+
.lda => switch (opr) {
102
102
+
.zeropage => 0xa5,
103
103
+
.zeropage_x => 0xb5,
104
104
+
.absolute => 0xad,
105
105
+
.absolute_x => 0xbd,
106
106
+
.absolute_y => 0xb9,
107
107
+
.immediate => 0xa9,
108
108
+
.indexed_indirect => 0xa1,
109
109
+
.indirect_indexed => 0xb1,
110
110
+
else => null,
111
111
+
},
112
112
+
.ldx => switch (opr) {
113
113
+
.zeropage => 0xa6,
114
114
+
.zeropage_y => 0xb6,
115
115
+
.absolute => 0xae,
116
116
+
.absolute_y => 0xbe,
117
117
+
.immediate => 0xa2,
118
118
+
else => null,
119
119
+
},
120
120
+
.ldy => switch (opr) {
121
121
+
.zeropage => 0xa4,
122
122
+
.zeropage_x => 0xb4,
123
123
+
.absolute => 0xac,
124
124
+
.absolute_x => 0xbc,
125
125
+
.immediate => 0xa0,
126
126
+
else => null,
127
127
+
},
128
128
+
.sta => switch (opr) {
129
129
+
.zeropage => 0x85,
130
130
+
.zeropage_x => 0x95,
131
131
+
.absolute => 0x8d,
132
132
+
.absolute_x => 0x9d,
133
133
+
.absolute_y => 0x99,
134
134
+
.indexed_indirect => 0x81,
135
135
+
.indirect_indexed => 0x91,
136
136
+
else => null,
137
137
+
},
138
138
+
.stx => switch (opr) {
139
139
+
.zeropage => 0x86,
140
140
+
.zeropage_y => 0x96,
141
141
+
.absolute => 0x8e,
142
142
+
else => null,
143
143
+
},
144
144
+
.sty => switch (opr) {
145
145
+
.zeropage => 0x84,
146
146
+
.zeropage_y => 0x94,
147
147
+
.absolute => 0x8c,
148
148
+
else => null,
149
149
+
},
150
150
+
.ora => switch (opr) {
151
151
+
.zeropage => 0x05,
152
152
+
.zeropage_x => 0x15,
153
153
+
.absolute => 0x0d,
154
154
+
.absolute_x => 0x1d,
155
155
+
.absolute_y => 0x19,
156
156
+
.immediate => 0x09,
157
157
+
.indexed_indirect => 0x01,
158
158
+
.indirect_indexed => 0x11,
159
159
+
else => null,
160
160
+
},
161
161
+
.@"and" => switch (opr) {
162
162
+
.zeropage => 0x25,
163
163
+
.zeropage_x => 0x35,
164
164
+
.absolute => 0x2d,
165
165
+
.absolute_x => 0x3d,
166
166
+
.absolute_y => 0x39,
167
167
+
.immediate => 0x29,
168
168
+
.indexed_indirect => 0x21,
169
169
+
.indirect_indexed => 0x31,
170
170
+
else => null,
171
171
+
},
172
172
+
.eor => switch (opr) {
173
173
+
.zeropage => 0x45,
174
174
+
.zeropage_x => 0x55,
175
175
+
.absolute => 0x4d,
176
176
+
.absolute_x => 0x5d,
177
177
+
.absolute_y => 0x59,
178
178
+
.immediate => 0x49,
179
179
+
.indexed_indirect => 0x41,
180
180
+
.indirect_indexed => 0x51,
181
181
+
else => null,
182
182
+
},
183
183
+
.asl => switch (opr) {
184
184
+
.zeropage => 0x06,
185
185
+
.zeropage_x => 0x16,
186
186
+
.absolute => 0x0e,
187
187
+
.absolute_x => 0x1e,
188
188
+
.implied => 0x0a,
189
189
+
else => null,
190
190
+
},
191
191
+
.lsr => switch (opr) {
192
192
+
.zeropage => 0x46,
193
193
+
.zeropage_x => 0x56,
194
194
+
.absolute => 0x4e,
195
195
+
.absolute_x => 0x5e,
196
196
+
.implied => 0x4a,
197
197
+
else => null,
198
198
+
},
199
199
+
.rol => switch (opr) {
200
200
+
.zeropage => 0x26,
201
201
+
.zeropage_x => 0x36,
202
202
+
.absolute => 0x2e,
203
203
+
.absolute_x => 0x3e,
204
204
+
.implied => 0x2a,
205
205
+
else => null,
206
206
+
},
207
207
+
.ror => switch (opr) {
208
208
+
.zeropage => 0x66,
209
209
+
.zeropage_x => 0x76,
210
210
+
.absolute => 0x6e,
211
211
+
.absolute_x => 0x7e,
212
212
+
.implied => 0x6a,
213
213
+
else => null,
214
214
+
},
215
215
+
.clc => switch (opr) {
216
216
+
.implied => 0x18,
217
217
+
else => null,
218
218
+
},
219
219
+
.sec => switch (opr) {
220
220
+
.implied => 0x38,
221
221
+
else => null,
222
222
+
},
223
223
+
.cli => switch (opr) {
224
224
+
.implied => 0x58,
225
225
+
else => null,
226
226
+
},
227
227
+
.sei => switch (opr) {
228
228
+
.implied => 0x78,
229
229
+
else => null,
230
230
+
},
231
231
+
.clv => switch (opr) {
232
232
+
.implied => 0xb8,
233
233
+
else => null,
234
234
+
},
235
235
+
.cld => switch (opr) {
236
236
+
.implied => 0xd8,
237
237
+
else => null,
238
238
+
},
239
239
+
.sed => switch (opr) {
240
240
+
.implied => 0xf8,
241
241
+
else => null,
242
242
+
},
243
243
+
};
244
244
+
}
245
245
+
};
246
246
+
247
247
+
const whitespace = " \t";
248
248
+
249
249
+
pub const Operand = union(enum) {
250
250
+
implied,
251
251
+
zeropage: u8,
252
252
+
zeropage_x: u8,
253
253
+
zeropage_y: u8,
254
254
+
absolute: u16,
255
255
+
absolute_x: u16,
256
256
+
absolute_y: u16,
257
257
+
immediate: u8,
258
258
+
indexed: u16,
259
259
+
indexed_indirect: u8,
260
260
+
indirect_indexed: u8,
261
261
+
262
262
+
fn parse(s: []const u8) !Operand {
263
263
+
const maybe_comma = std.mem.indexOfScalar(u8, s, ',');
264
264
+
265
265
+
switch (s[0]) {
266
266
+
'#' => return .{ .immediate = try parseInt(u8, s[1..]) },
267
267
+
'<' => {
268
268
+
if (maybe_comma) |comma| {
269
269
+
if (std.mem.indexOfAny(
270
270
+
u8,
271
271
+
s[comma + 1 ..],
272
272
+
"Xx",
273
273
+
)) |_| return .{
274
274
+
.zeropage_x = try parseInt(u8, std.mem.trim(
275
275
+
u8,
276
276
+
s[1..comma],
277
277
+
whitespace,
278
278
+
)),
279
279
+
};
280
280
+
if (std.mem.indexOfAny(
281
281
+
u8,
282
282
+
s[comma + 1 ..],
283
283
+
"Yy",
284
284
+
)) |_| return .{
285
285
+
.zeropage_y = try parseInt(u8, std.mem.trim(
286
286
+
u8,
287
287
+
s[1..comma],
288
288
+
whitespace,
289
289
+
)),
290
290
+
};
291
291
+
}
292
292
+
293
293
+
return .{ .zeropage = try parseInt(u8, s[1..]) };
294
294
+
},
295
295
+
'(' => {
296
296
+
const r_paren = std.mem.lastIndexOfScalar(
297
297
+
u8,
298
298
+
s,
299
299
+
')',
300
300
+
) orelse return error.InvalidSyntax;
301
301
+
302
302
+
if (maybe_comma) |comma| {
303
303
+
// (zp,X)
304
304
+
if (comma < r_paren) {
305
305
+
if (std.mem.indexOfAny(
306
306
+
u8,
307
307
+
s[comma + 1 .. r_paren],
308
308
+
"Xx",
309
309
+
) == null) return error.InvalidSyntax;
310
310
+
311
311
+
return .{
312
312
+
.indexed_indirect = try parseInt(u8, std.mem.trim(
313
313
+
u8,
314
314
+
s[1..comma],
315
315
+
whitespace,
316
316
+
)),
317
317
+
};
318
318
+
}
319
319
+
320
320
+
// (zp),Y
321
321
+
if (std.mem.indexOfAny(
322
322
+
u8,
323
323
+
s[comma + 1 ..],
324
324
+
"Yy",
325
325
+
) == null) return error.InvalidSyntax;
326
326
+
327
327
+
return .{
328
328
+
.indirect_indexed = try parseInt(u8, std.mem.trim(
329
329
+
u8,
330
330
+
s[1..comma],
331
331
+
whitespace,
332
332
+
)),
333
333
+
};
334
334
+
}
335
335
+
336
336
+
if (r_paren != s.len) return error.InvalidSyntax;
337
337
+
338
338
+
// (ind)
339
339
+
return .{
340
340
+
.indexed = try parseInt(u16, std.mem.trim(
341
341
+
u8,
342
342
+
s[1..r_paren],
343
343
+
whitespace,
344
344
+
)),
345
345
+
};
346
346
+
},
347
347
+
else => {
348
348
+
if (maybe_comma) |comma| {
349
349
+
if (std.mem.indexOfAny(
350
350
+
u8,
351
351
+
s[comma + 1 ..],
352
352
+
"Xx",
353
353
+
)) |_| return .{
354
354
+
.absolute_x = try parseInt(u16, std.mem.trim(
355
355
+
u8,
356
356
+
s[0..comma],
357
357
+
whitespace,
358
358
+
)),
359
359
+
};
360
360
+
if (std.mem.indexOfAny(
361
361
+
u8,
362
362
+
s[comma + 1 ..],
363
363
+
"Yy",
364
364
+
)) |_| return .{
365
365
+
.absolute_y = try parseInt(u16, std.mem.trim(
366
366
+
u8,
367
367
+
s[0..comma],
368
368
+
whitespace,
369
369
+
)),
370
370
+
};
371
371
+
return error.InvalidSyntax;
372
372
+
}
373
373
+
374
374
+
return .{ .absolute = try parseInt(u16, s) };
375
375
+
},
376
376
+
}
377
377
+
}
378
378
+
};
379
379
+
380
380
+
fn parseInt(comptime T: type, s: []const u8) !T {
381
381
+
const base: u8, const slice = switch (s[0]) {
382
382
+
'$' => .{ 16, s[1..] },
383
383
+
'%' => .{ 2, s[1..] },
384
384
+
else => .{ 10, s },
385
385
+
};
386
386
+
387
387
+
var v: T = 0;
388
388
+
for (slice) |d| {
389
389
+
const digit = try std.fmt.charToDigit(d, base);
390
390
+
v = try std.math.mul(T, v, base);
391
391
+
v = try std.math.add(T, v, digit);
392
392
+
}
393
393
+
return v;
394
394
+
}
395
395
+
396
396
+
test "basic ops" {
397
397
+
try std.testing.expectEqualSlices(
398
398
+
u8,
399
399
+
&.{ 0xa9, 0x42, 0xae, 0x00, 0x69 },
400
400
+
assembleComptime(
401
401
+
\\LDA #$42
402
402
+
\\LDX $6900
403
403
+
),
404
404
+
);
405
405
+
}
+69
src/tests/cpu/control_flow.zig
···
1
1
+
//! Interrupt-related (IRQ/NMI/BRK/RESET) tests.
2
2
+
const std = @import("std");
3
3
+
const cpu = @import("../cpu.zig");
4
4
+
5
5
+
test "reset" {
6
6
+
var z = cpu.testZes(.{
7
7
+
.init_cpu = false,
8
8
+
.rom = &.{
9
9
+
.{ 0x8000, &.{0x69} },
10
10
+
},
11
11
+
});
12
12
+
while (!z.cpu.sync) z.stepCpu();
13
13
+
14
14
+
// Correctly read the first byte of ROM
15
15
+
try std.testing.expectEqual(0x69, z.pins.cpu_data);
16
16
+
}
17
17
+
18
18
+
test "BRK and RTI" {
19
19
+
var z = cpu.testZes(.{
20
20
+
// Set a recognizable IRQ offset
21
21
+
.irq = 0x8964,
22
22
+
.rom = &.{
23
23
+
.{ 0x8000, cpu.assemble("brk") }, // BRK
24
24
+
},
25
25
+
});
26
26
+
27
27
+
// Setup some initial setup state.
28
28
+
z.cpu.status.carry = true;
29
29
+
z.cpu.status.irq_disabled = true;
30
30
+
z.cpu.status.negative = true;
31
31
+
32
32
+
var status_copy = z.cpu.status;
33
33
+
// Since this is a BRK, the B flag will be set
34
34
+
status_copy.brk = true;
35
35
+
36
36
+
while (!z.cpu.status.brk) z.stepCpu();
37
37
+
38
38
+
// Store PC hi
39
39
+
try std.testing.expectEqual(0x01fd, z.pins.cpu_addr);
40
40
+
41
41
+
// Store PC lo
42
42
+
z.stepCpu();
43
43
+
try std.testing.expectEqual(0x01fc, z.pins.cpu_addr);
44
44
+
45
45
+
// Store processor status
46
46
+
z.stepCpu();
47
47
+
const status: u8 = @bitCast(status_copy);
48
48
+
try std.testing.expectEqual(0x01fb, z.pins.cpu_addr);
49
49
+
50
50
+
// Fetch PC lo
51
51
+
z.stepCpu();
52
52
+
try std.testing.expectEqual(0xfffe, z.pins.cpu_addr);
53
53
+
try std.testing.expectEqual(0x64, z.pins.cpu_data);
54
54
+
55
55
+
// Fetch PC hi
56
56
+
z.stepCpu();
57
57
+
try std.testing.expectEqual(0xffff, z.pins.cpu_addr);
58
58
+
try std.testing.expectEqual(0x89, z.pins.cpu_data);
59
59
+
60
60
+
// PC should be reset correctly
61
61
+
z.stepCpu();
62
62
+
try std.testing.expectEqual(0x8964, z.cpu.pc);
63
63
+
try std.testing.expectEqual(0x8964, z.pins.cpu_addr);
64
64
+
65
65
+
// Old PC should remain in stack
66
66
+
try std.testing.expectEqual(0x80, z.ram[0x1fd]);
67
67
+
try std.testing.expectEqual(0x00, z.ram[0x1fc]);
68
68
+
try std.testing.expectEqual(status, z.ram[0x1fb]);
69
69
+
}
+198
src/zesty.zig
···
1
1
+
//! Zesty - a pin-accurate, cycle-accurate NES/Famicom emulator in Zig
2
2
+
3
3
+
pub const Cpu = @import("Cpu.zig");
4
4
+
pub const Ppu = @import("Ppu.zig");
5
5
+
pub const cartridge = @import("cartridge.zig");
6
6
+
pub const Cartridge = cartridge.Cartridge;
7
7
+
8
8
+
pub const Options = struct {
9
9
+
/// Number of main clock cycles per PPU clock cycle.
10
10
+
ppu_clock_divider: u8,
11
11
+
12
12
+
/// Number of main clock cycles per CPU clock cycle.
13
13
+
cpu_clock_divider: u8,
14
14
+
15
15
+
pub const famicom: Options = .{
16
16
+
.ppu_clock_divider = 4,
17
17
+
.cpu_clock_divider = 12,
18
18
+
};
19
19
+
pub const top_loader_ntsc: Options = .{
20
20
+
.ppu_clock_divider = 4,
21
21
+
.cpu_clock_divider = 12,
22
22
+
};
23
23
+
pub const top_loader_pal: Options = .{
24
24
+
.ppu_clock_divider = 5,
25
25
+
.cpu_clock_divider = 16,
26
26
+
};
27
27
+
};
28
28
+
29
29
+
pub const Zesty = struct {
30
30
+
cpu: Cpu,
31
31
+
ppu: Ppu,
32
32
+
cart: ?Cartridge,
33
33
+
ram: [0x800]u8,
34
34
+
35
35
+
pins: Pins,
36
36
+
last_pins: Pins,
37
37
+
38
38
+
/// The main clock of the emulator.
39
39
+
/// Resets every 24 cycles.
40
40
+
main_clock: u8,
41
41
+
42
42
+
opts: Options,
43
43
+
44
44
+
pub fn init(opts: Options) Zesty {
45
45
+
return .{
46
46
+
.cpu = .{},
47
47
+
.ppu = .{},
48
48
+
.cart = null,
49
49
+
.ram = @splat(0xaa),
50
50
+
.pins = .{},
51
51
+
.last_pins = .{},
52
52
+
.main_clock = 0,
53
53
+
.opts = opts,
54
54
+
};
55
55
+
}
56
56
+
57
57
+
pub fn tick(self: *Zesty) void {
58
58
+
defer {
59
59
+
self.main_clock +%= 1;
60
60
+
self.last_pins = self.pins;
61
61
+
}
62
62
+
63
63
+
self.pins.cpu_clk = self.main_clock % self.opts.cpu_clock_divider == 0;
64
64
+
self.pins.ppu_clk = self.main_clock % self.opts.ppu_clock_divider == 0;
65
65
+
66
66
+
if (self.pins.cpuRamChipSelect()) {
67
67
+
const addr = self.pins.cpu_addr & 0x7ff;
68
68
+
switch (self.pins.cpu_rw) {
69
69
+
.read => self.pins.cpu_data = self.ram[addr],
70
70
+
.write => self.ram[addr] = self.pins.cpu_data,
71
71
+
}
72
72
+
}
73
73
+
if (self.cart) |*cart| cart.update(&self.pins);
74
74
+
75
75
+
self.cpu.tick(&self.pins, self.last_pins);
76
76
+
self.ppu.tick(&self.pins);
77
77
+
}
78
78
+
79
79
+
pub fn reset(self: *Zesty) void {
80
80
+
self.pins.cpu_rst = true;
81
81
+
}
82
82
+
83
83
+
pub fn stepCpu(self: *Zesty) void {
84
84
+
for (0..self.opts.cpu_clock_divider) |_| self.tick();
85
85
+
}
86
86
+
};
87
87
+
88
88
+
/// The collection of all pins and electrical
89
89
+
/// connections inside the system.
90
90
+
pub const Pins = packed struct {
91
91
+
//----------------------------------------------
92
92
+
// CPU
93
93
+
94
94
+
/// APU audio out
95
95
+
/// (2A03 p1-2)
96
96
+
audio: u2 = 0,
97
97
+
98
98
+
/// CPU clock
99
99
+
cpu_clk: bool = false,
100
100
+
101
101
+
/// CPU reset
102
102
+
/// (2A03 p3 / 2C02 22)
103
103
+
cpu_rst: bool = true,
104
104
+
105
105
+
/// CPU address bus
106
106
+
/// (2A03 p4-19 / 2C02 p10-12 / Cart p2-13,68-70)
107
107
+
cpu_addr: u16 = 0x00ff,
108
108
+
109
109
+
/// CPU data bus
110
110
+
/// (2A03 p21-28 / 2C02 p2-9 / Cart p60-67)
111
111
+
cpu_data: u8 = 0,
112
112
+
113
113
+
/// CPU M2
114
114
+
/// (2A03 p31 / Cart p71)
115
115
+
cpu_m2: bool = false,
116
116
+
117
117
+
/// CPU IRQ
118
118
+
/// (2A03 p32 / Cart p15)
119
119
+
cpu_irq: bool = false,
120
120
+
121
121
+
/// CPU NMI / PPU INT
122
122
+
/// (2A03 p33 / 2C02 p19)
123
123
+
cpu_nmi: bool = false,
124
124
+
125
125
+
/// CPU R/W
126
126
+
/// (2A03 p34 / 2C02 p1 / Cart p14)
127
127
+
cpu_rw: Rw = .read,
128
128
+
129
129
+
cpu_out0: bool = false,
130
130
+
cpu_out1: bool = false,
131
131
+
cpu_out2: bool = false,
132
132
+
cpu_oe1: bool = false,
133
133
+
cpu_oe2: bool = false,
134
134
+
135
135
+
//----------------------------------------------
136
136
+
// PPU
137
137
+
ppu_clk: bool = false,
138
138
+
139
139
+
/// EXT pins (pins 14-17, inout)
140
140
+
/// Usually just grounded.
141
141
+
ppu_ext: u4 = 0,
142
142
+
143
143
+
/// Shifted analog video output (pin 21, out)
144
144
+
ppu_vout: bool = false,
145
145
+
146
146
+
/// VRAM write pin (pin 23, out)
147
147
+
ppu_wr: bool = false,
148
148
+
149
149
+
/// VRAM read pin (pin 24, out)
150
150
+
ppu_rd: bool = false,
151
151
+
152
152
+
/// VRAM high address (pins 25-30, out)
153
153
+
ppu_addr_hi: u6 = 0,
154
154
+
155
155
+
/// VRAM address/data (pins 31-38, inout)
156
156
+
ppu_addr_data: u8 = 0,
157
157
+
158
158
+
/// Address latch enable (pin 39, out)
159
159
+
ppu_ale: bool = false,
160
160
+
161
161
+
//----------------------------------------------
162
162
+
// Cartridge
163
163
+
164
164
+
/// EXP(?) (pins 16-20 & 54-58, inout)
165
165
+
exp: u10 = 0,
166
166
+
167
167
+
/// CIRAM address pin 10 (pin 22, out)
168
168
+
ciram_a10: bool = false,
169
169
+
/// CIRAM chip enable pin (pin 52, out)
170
170
+
ciram_ce: bool = false,
171
171
+
172
172
+
/// CIC out pin (pin 34, in)
173
173
+
cic_out: bool = false,
174
174
+
/// CIC in pin (pin 35, out)
175
175
+
cic_in: bool = false,
176
176
+
/// CIC clock pin (pin 38, out)
177
177
+
cic_clk: bool = false,
178
178
+
/// CIC reset pin (pin 39, out)
179
179
+
cic_rst: bool = false,
180
180
+
181
181
+
//----------------------------------------------
182
182
+
// Logic chip emulation
183
183
+
184
184
+
/// Cartridge ROM select
185
185
+
pub fn romSel(pins: Pins) bool {
186
186
+
return !(pins.cpu_m2 and pins.cpu_addr >= 0x8000);
187
187
+
}
188
188
+
/// CPU RAM chip select
189
189
+
pub fn cpuRamChipSelect(pins: Pins) bool {
190
190
+
return pins.cpu_addr >> 13 == 0;
191
191
+
}
192
192
+
/// PPU chip select
193
193
+
pub fn ppuChipSelect(pins: Pins) bool {
194
194
+
return pins.cpu_addr >> 13 == 1;
195
195
+
}
196
196
+
};
197
197
+
198
198
+
pub const Rw = enum(u1) { write, read };