Zesty - a pin-accurate, cycle-accurate NES emulator written in Zig

initial commit

pluie.me a2d803e2

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