From c535e4e76d6ad94004e70bea85f7fcd75baae485 Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Mon, 22 Dec 2025 02:13:10 -0600 Subject: [PATCH] Add new testcases with cc65 support --- build.rs | 78 ++++-- src/debugger.rs | 36 ++- src/lib.rs | 32 +-- src/main.rs | 3 +- src/test_roms/common/ascii.chr | Bin 0 -> 1536 bytes src/test_roms/common/build_rom.s | 118 ++++++++ src/test_roms/common/console.s | 331 ++++++++++++++++++++++ src/test_roms/common/crc.s | 118 ++++++++ src/test_roms/common/crc_fast.s | 124 +++++++++ src/test_roms/common/delay.s | 190 +++++++++++++ src/test_roms/common/devcart.bin | Bin 0 -> 256 bytes src/test_roms/common/instr_test.inc | 245 ++++++++++++++++ src/test_roms/common/instr_test_end.s | 199 +++++++++++++ src/test_roms/common/macros.inc | 169 ++++++++++++ src/test_roms/common/neshw.inc | 43 +++ src/test_roms/common/ppu.s | 142 ++++++++++ src/test_roms/common/print.s | 235 ++++++++++++++++ src/test_roms/common/run_at_reset.s | 64 +++++ src/test_roms/common/shell.inc | 26 ++ src/test_roms/common/shell.s | 384 ++++++++++++++++++++++++++ src/test_roms/common/testing.s | 106 +++++++ src/test_roms/common/text_out.s | 61 ++++ src/test_roms/cpu_reset_ram.rs | 17 ++ src/test_roms/cpu_reset_ram.s.old | 84 ++++++ src/test_roms/crc_check.s | 63 +++++ src/test_roms/implied_instrs.s | 130 +++++++++ src/test_roms/instr_test_v3.rs | 63 +++++ src/test_roms/mod.rs | 13 +- src/test_roms/nes.cfg | 36 +++ test-rom/basic-cpu.asm | 8 +- 30 files changed, 3057 insertions(+), 61 deletions(-) create mode 100644 src/test_roms/common/ascii.chr create mode 100644 src/test_roms/common/build_rom.s create mode 100644 src/test_roms/common/console.s create mode 100644 src/test_roms/common/crc.s create mode 100644 src/test_roms/common/crc_fast.s create mode 100644 src/test_roms/common/delay.s create mode 100644 src/test_roms/common/devcart.bin create mode 100644 src/test_roms/common/instr_test.inc create mode 100644 src/test_roms/common/instr_test_end.s create mode 100644 src/test_roms/common/macros.inc create mode 100644 src/test_roms/common/neshw.inc create mode 100644 src/test_roms/common/ppu.s create mode 100644 src/test_roms/common/print.s create mode 100644 src/test_roms/common/run_at_reset.s create mode 100644 src/test_roms/common/shell.inc create mode 100644 src/test_roms/common/shell.s create mode 100644 src/test_roms/common/testing.s create mode 100644 src/test_roms/common/text_out.s create mode 100644 src/test_roms/cpu_reset_ram.rs create mode 100644 src/test_roms/cpu_reset_ram.s.old create mode 100644 src/test_roms/crc_check.s create mode 100644 src/test_roms/implied_instrs.s create mode 100644 src/test_roms/instr_test_v3.rs create mode 100644 src/test_roms/nes.cfg diff --git a/build.rs b/build.rs index 3d23073..f8b1752 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,31 @@ -use std::{io::Read, process::Stdio}; +use std::{ + fmt::Arguments, + io::Read, + process::{Command, Stdio}, +}; + +fn run(name: Arguments, cmd: &mut Command) { + let mut proc = cmd + .current_dir("src/test_roms") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to start process"); + let rc = proc.wait(); + if rc.is_err() || rc.is_ok_and(|s| !s.success()) { + let mut stdout = String::new(); + proc.stdout + .unwrap() + .read_to_string(&mut stdout) + .expect("Failed to read stdout"); + let mut stderr = String::new(); + proc.stderr + .unwrap() + .read_to_string(&mut stderr) + .expect("Failed to read stderr"); + panic!("Failed to run {name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}"); + } +} fn main() { println!("cargo::rerun-if-changed=src/test_roms/"); @@ -16,30 +43,31 @@ fn main() { let file_name = file.file_name(); let file_name = file_name.to_str().unwrap(); if let Some(file_name) = file_name.strip_suffix(".asm") { - let mut proc = std::process::Command::new("./asm6f") - .arg(file.file_name()) - .arg(format!("{name}/{file_name}.nes")) - .current_dir("src/test_roms") - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("Failed to start process"); - let rc = proc.wait(); - if rc.is_err() || rc.is_ok_and(|s| !s.success()) { - let mut stdout = String::new(); - proc.stdout - .unwrap() - .read_to_string(&mut stdout) - .expect("Failed to read stdout"); - let mut stderr = String::new(); - proc.stderr - .unwrap() - .read_to_string(&mut stderr) - .expect("Failed to read stderr"); - panic!( - "Failed to compile {file_name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}" - ); - } + run( + format_args!("asm6f {file_name}"), + Command::new("./asm6f") + .arg(file.file_name()) + .arg(format!("{name}/{file_name}.nes")), + ); + } else if let Some(file_name) = file_name.strip_suffix(".s") { + run( + format_args!("ca65 {file_name}"), + Command::new("/home/matthew/cc65/bin/ca65") + .arg("-I") + .arg("common") + .arg("-o") + .arg(format!("{name}/{file_name}.o")) + .arg(file.file_name()), + ); + run( + format_args!("ld65 {file_name}"), + Command::new("/home/matthew/cc65/bin/ld65") + .arg("-C") + .arg("nes.cfg") + .arg("-o") + .arg(format!("{name}/{file_name}.nes")) + .arg(format!("{name}/{file_name}.o")) + ); } } } diff --git a/src/debugger.rs b/src/debugger.rs index 9bb8b33..faf6144 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -158,13 +158,16 @@ impl DebuggerState { ], ] .spacing(5.), - scrollable(column( - nes.debug_log() - .history() - .into_iter() - .rev() - .map(|s| text(s).line_height(0.9).into()) - ).spacing(0)) + scrollable( + column( + nes.debug_log() + .history() + .into_iter() + .rev() + .map(|s| text(s).line_height(0.9).into()) + ) + .spacing(0) + ) .width(Fill), ],], ] @@ -176,11 +179,20 @@ impl DebuggerState { fn run_n_clock_cycles(nes: &mut NES, n: usize) { for _ in 0..n { nes.run_one_clock_cycle(); + if nes.halted { + break; + } } } - fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool) { + fn run_until(nes: &mut NES, mut f: impl FnMut(CycleResult, &NES) -> bool, mut count: usize) { loop { if f(nes.run_one_clock_cycle(), nes) { + count -= 1; + if count <= 0 { + break; + } + } + if nes.halted { break; } } @@ -196,13 +208,15 @@ impl DebuggerState { DebuggerMessage::SetFrames(n) => self.frames = n, DebuggerMessage::RunPPUCycles => Self::run_n_clock_cycles(nes, self.ppu_cycles), DebuggerMessage::RunCPUCycles => Self::run_n_clock_cycles(nes, self.cpu_cycles * 3), - DebuggerMessage::RunInstructions => Self::run_until(nes, |c, _| c.cpu_exec), + DebuggerMessage::RunInstructions => { + Self::run_until(nes, |c, _| c.cpu_exec, self.instructions) + } DebuggerMessage::RunScanLines => Self::run_n_clock_cycles(nes, self.scan_lines * 341), DebuggerMessage::RunToScanLine => { - Self::run_until(nes, |_, n| n.ppu.scanline == self.to_scan_line) + Self::run_until(nes, |_, n| n.ppu.scanline == self.to_scan_line, 1) } DebuggerMessage::RunFrames => Self::run_n_clock_cycles(nes, self.frames * 341 * 262), - DebuggerMessage::Run => todo!(), + DebuggerMessage::Run => Self::run_until(nes, |_, nes| nes.halted, 1), DebuggerMessage::Pause => todo!(), } } diff --git a/src/lib.rs b/src/lib.rs index 2770480..713d498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1777,7 +1777,7 @@ impl NES { self.cpu.status.set_carry(self.cpu.a & 0b1 == 0b1); self.cpu.a = self.cpu.a >> 1 | old_carry; self.cpu.status.set_zero(self.cpu.a == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(self.cpu.a & 0x80 == 0x80); log!("{addr:04X}: ROR A | {:02X}", self.cpu.a); }), 0x66 => inst!("ROR zp", 3, |off| { @@ -1786,7 +1786,7 @@ impl NES { self.cpu.status.set_carry(val & 0b1 == 0b1); let val = val >> 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs(off, 0, val); log!("{addr:04X}: ROR ${:02X} | {:02X}", off, self.cpu.a); }), @@ -1796,9 +1796,9 @@ impl NES { self.cpu.status.set_carry(val & 0b1 == 0b1); let val = val >> 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_zp_x(off, val); - log!("{addr:04X}: ROR ${:02X},x | {:02X}", off, self.cpu.a); + log!("{addr:04X}: ROR ${:02X},x | {:02X}", off, val); }), 0x6E => inst!("ROR abs", 3, |low, high| { let old_carry = if self.cpu.status.carry() { 0x80 } else { 0x00 }; @@ -1806,13 +1806,13 @@ impl NES { self.cpu.status.set_carry(val & 0b1 == 0b1); let val = val >> 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs(low, high, val); log!( "{addr:04X}: ROR ${:02X}{:02X} | {:02X}", high, low, - self.cpu.a + val ); }), 0x7E => inst!("ROR abs,x", 4, |low, high| { @@ -1821,13 +1821,13 @@ impl NES { self.cpu.status.set_carry(val & 0b1 == 0b1); let val = val >> 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs_x(low, high, val); log!( "{addr:04X}: ROR ${:02X}{:02X},x | {:02X}", high, low, - self.cpu.a + val ); }), 0x2A => inst!("ROL A", 1, || { @@ -1835,7 +1835,7 @@ impl NES { self.cpu.status.set_carry(self.cpu.a & 0x80 == 0x80); self.cpu.a = self.cpu.a << 1 | old_carry; self.cpu.status.set_zero(self.cpu.a == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(self.cpu.a & 0x80 == 0x80); log!("{addr:04X}: ROL A | {:02X}", self.cpu.a); }), 0x26 => inst!("ROL zp", 3, |off| { @@ -1844,9 +1844,9 @@ impl NES { self.cpu.status.set_carry(val & 0x80 == 0x80); let val = val << 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs(off, 0, val); - log!("{addr:04X}: ROL ${:02X} | {:02X}", off, self.cpu.a); + log!("{addr:04X}: ROL ${:02X} | {:02X}", off, val); }), 0x36 => inst!("ROL zp,x", 4, |off| { let old_carry = if self.cpu.status.carry() { 0x01 } else { 0x00 }; @@ -1854,9 +1854,9 @@ impl NES { self.cpu.status.set_carry(val & 0x80 == 0x80); let val = val << 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_zp_x(off, val); - log!("{addr:04X}: ROL ${:02X},x | {:02X}", off, self.cpu.a); + log!("{addr:04X}: ROL ${:02X},x | {:02X}", off, val); }), 0x2E => inst!("ROL abs", 3, |low, high| { let old_carry = if self.cpu.status.carry() { 0x01 } else { 0x00 }; @@ -1864,7 +1864,7 @@ impl NES { self.cpu.status.set_carry(val & 0x80 == 0x80); let val = val << 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs(low, high, val); log!( "{addr:04X}: ROL ${:02X}{:02X} | {:02X}", @@ -1879,13 +1879,13 @@ impl NES { self.cpu.status.set_carry(val & 0x80 == 0x80); let val = val << 1 | old_carry; self.cpu.status.set_zero(val == 0); - self.cpu.status.set_negative(false); + self.cpu.status.set_negative(val & 0x80 == 0x80); self.write_abs_x(low, high, val); log!( "{addr:04X}: ROL ${:02X}{:02X},x | {:02X}", high, low, - self.cpu.a + val ); }), diff --git a/src/main.rs b/src/main.rs index c90016b..5195ca4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,8 @@ use tokio::runtime::Runtime; use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes"); -const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; +const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "crc_check.nes"); +// const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; extern crate nes_emu; diff --git a/src/test_roms/common/ascii.chr b/src/test_roms/common/ascii.chr new file mode 100644 index 0000000000000000000000000000000000000000..2c5b26bdb43aac9cd7899428c45f832832c056c7 GIT binary patch literal 1536 zcmZA1F>Bl~6bJA}22bM1sKMN4&{PZ+N2X5w5TXtj#5g8 zk|~7Jq1<59{-30Gxm3CEvn*NCd$L9JvS==ZzveVerGrRQp70}4@0{&C`ZwQ=N}KTw ziT$g7_&B~MQ7Ki%6FY^8R7avSs2fKjjrOk{@rbDPf&~&5Smn_S9(Z;s*F++btIg~c zCNl2k?C|zJKD-}D)B^{;km%#a&Ob7Vi_*4>p2XfxDI+Fv4Qt2kqIF@7sEavbeq%_S zzxvkj# zQ;a!A60vyxk!Z&d!f4UIdP*_IbRsc_u!fN7rJ>aW?OeEzGlyPoUKaU3~x67!}X_IUrqZOO})#2f6s z>aIw%_^^~h{+id=Nd6u~z9z~Q7@R5BBGIfo;uk|WrG)(zaX*DLb9+&tcS@c_zQ2ar z&i2O4oF9=0{XJgrW*s-R@e{sfe9h>8wjM?Mb|mdXC>xLdRql*Z9%6|e^Wku~@c4)Y zqApraE$SlTJ7MGT23O1aa7c;AM}9B(`RW{Ibwa&qJf`ij-X>wTZxh-toOxZS7qL1% zKS{*hnb&u=qL>E843YoQ_&=NGz`d-WPF?(^Ts{iSGAj#DXKR$=8?VAHdBS Ag#Z8m literal 0 HcmV?d00001 diff --git a/src/test_roms/common/build_rom.s b/src/test_roms/common/build_rom.s new file mode 100644 index 0000000..80a24cd --- /dev/null +++ b/src/test_roms/common/build_rom.s @@ -0,0 +1,118 @@ +; Builds program as iNES ROM + +; Default is 32K PRG and 8K CHR ROM, NROM (0) + +.if 0 ; Options to set before .include "shell.inc": +CHR_RAM=1 ; Use CHR-RAM instead of CHR-ROM +CART_WRAM=1 ; Use mapper that supports 8K WRAM in cart +CUSTOM_MAPPER=n ; Specify mapper number +.endif + +.ifndef CUSTOM_MAPPER + .ifdef CART_WRAM + CUSTOM_MAPPER = 2 ; UNROM + .else + CUSTOM_MAPPER = 0 ; NROM + .endif +.endif + +;;;; iNES header +.ifndef CUSTOM_HEADER + .segment "HEADER" + .byte $4E,$45,$53,26 ; "NES" EOF + + .ifdef CHR_RAM + .byte 2,0 ; 32K PRG, CHR RAM + .else + .byte 2,1 ; 32K PRG, 8K CHR + .endif + + .byte CUSTOM_MAPPER*$10+$01 ; vertical mirroring +.endif + +.ifndef CUSTOM_VECTORS + .segment "VECTORS" + .word $FFFF,$FFFF,$FFFF, nmi, reset, irq +.endif + +;;;; CHR-RAM/ROM +.ifdef CHR_RAM + .define CHARS "CHARS_PRG" + .segment CHARS + ascii_chr: + + .segment "CHARS_PRG_ASCII" + .align $200 + .incbin "ascii.chr" + ascii_chr_end: +.else + .define CHARS "CHARS" + .segment "CHARS_ASCII" + .align $200 + .incbin "ascii.chr" + .res $1800 +.endif + + .segment "CODE" +.ifndef CUSTOM_RESET +reset: +.endif +std_reset: + jmp main + + zp_byte nmi_count + zp_byte flags_from_nmi + zp_byte pclo_from_nmi + zp_byte nmi_temp +.ifndef CUSTOM_NMI +nmi: +.endif +std_nmi: + sta nmi_temp + pla + sta flags_from_nmi + pla + sta pclo_from_nmi + pha + lda flags_from_nmi + pha + lda nmi_temp + inc nmi_count + rti + + zp_byte flags_from_irq + zp_byte pclo_from_irq + zp_byte irq_count + +.ifndef CUSTOM_IRQ +irq: +.endif +std_irq: ; Record flags and PC low byte from stack + pla + sta flags_from_irq + pla + sta pclo_from_irq + pha + lda flags_from_irq + pha + inc irq_count + bit SNDCHN ; clear frame IRQ flag + rti + +fail: + lda #$01 +.ifndef CUSTOM_EXIT +exit: +.endif +std_exit: + .byte $02 + +.segment CHARS + .res $10,0 + +; Move code to $C000 +.segment "CODE" + .res $4000 + +.code +.align 256 diff --git a/src/test_roms/common/console.s b/src/test_roms/common/console.s new file mode 100644 index 0000000..d4f5ddd --- /dev/null +++ b/src/test_roms/common/console.s @@ -0,0 +1,331 @@ +; Scrolling text console with line wrapping, 30x29 characters. +; Buffers lines for speed. Will work even if PPU doesn't +; support scrolling (until text reaches bottom). Keeps border +; along bottom in case TV cuts it off. +; +; Defers most initialization until first newline, at which +; point it clears nametable and makes palette non-black. +; +; ** ASCII font must already be in CHR, and mirroring +; must be vertical or single-screen. + +.ifndef CONSOLE_COLOR + CONSOLE_COLOR = $30 ; white +.endif + +console_screen_width = 32 ; if lower than 32, left-justifies + +; Number of characters of margin on left and right, to avoid +; text getting cut off by common TVs. OK if either/both are 0. +console_left_margin = 1 +console_right_margin = 1 + +console_width = console_screen_width - console_left_margin - console_right_margin + +zp_byte console_pos ; 0 to console_width +zp_byte console_scroll +zp_byte console_temp +bss_res console_buf,console_width + + +; Initializes console +console_init: + ; Flag that console hasn't been initialized + setb console_scroll,$FF + + setb console_pos,0 + rts + + +; Hides console by disabling PPU rendering and blacking out +; first four entries of palette. +; Preserved: A, X, Y +console_hide: + pha + jsr console_wait_vbl_ + setb PPUMASK,0 + lda #$0F + jsr console_load_palette_ + pla + rts + + +; Shows console display +; Preserved: X, Y +console_show: + pha + lda #CONSOLE_COLOR + jsr console_show_custom_color_ + pla + rts + + +; Prints char A to console. Will not appear until +; a newline or flush occurs. +; Preserved: A, X, Y +console_print: + cmp #10 + beq console_newline + + sty console_temp + + ldy console_pos + cpy #console_width + beq console_full_ + sta console_buf,y + iny + sty console_pos + + ldy console_temp + rts + + +; Displays current line and starts new one +; Preserved: A, X, Y +console_newline: + pha + jsr console_wait_vbl_ + jsr console_flush_ + jsr console_scroll_up_ + setb console_pos,0 + pla + rts + + +; Displays current line's contents without scrolling. +; Preserved: A, X, Y +console_flush: + pha + jsr console_wait_vbl_ + jsr console_flush_ + jsr console_apply_scroll_ + pla + rts + + +;**** Internal routines **** + +console_full_: + ldy console_temp + + ; Line is full + + ; If space, treat as newline + cmp #' ' + beq console_newline + + ; Wrap current line at appropriate point + pha + tya + pha + jsr console_wrap_ + pla + tay + pla + + jmp console_print + + +; Inserts newline into buffer at appropriate position, leaving +; next line ready in buffer +; Preserved: X, console_temp +console_wrap_: + ; Find beginning of last word + ldy #console_width + lda #' ' +: dey + bmi console_newline + cmp console_buf,y + bne :- + + ; y = 0 to console_width-1 + + ; Flush through current word and put remaining + ; in buffer for next line + jsr console_wait_vbl_ + + ; Time to last PPU write: 207 + 32*(26 + 10) + + lda console_scroll + jsr console_set_ppuaddr_ + + stx console_pos ; save X + + ldx #0 + + ; Print everything before last word +: lda console_buf,x + sta PPUDATA + inx + dey + bpl :- + + ; x = 1 to console_width + + ; Move last word to beginning of buffer, and + ; print spaces for rest of line + ldy #0 + beq :++ +: lda #' ' + sta PPUDATA + lda console_buf,x + inx + sta console_buf,y + iny +: cpx #console_width + bne :-- + + ldx console_pos ; restore X + + ; Append new text after that + sty console_pos + + ; FALL THROUGH + + +; Scrolls up 8 pixels and clears one line BELOW new line +; Preserved: X, console_temp +console_scroll_up_: + ; Scroll up 8 pixels + lda console_scroll + jsr console_add_8_to_scroll_ + sta console_scroll + + ; Clear line AFTER that on screen + jsr console_add_8_to_scroll_ + jsr console_set_ppuaddr_ + + ldy #console_width + lda #' ' +: sta PPUDATA + dey + bne :- + ; FALL THROUGH + + +; Applies current scrolling position to PPU +; Preserved: X, Y, console_temp +console_apply_scroll_: + lda #0 + sta PPUADDR + sta PPUADDR + + sta PPUSCROLL + lda console_scroll + jsr console_add_8_to_scroll_ + jsr console_add_8_to_scroll_ + sta PPUSCROLL + rts + + +; Sets PPU address for row +; In: A = scroll position +; Preserved: X, Y +console_set_ppuaddr_: + sta console_temp + lda #$08 + asl console_temp + rol a + asl console_temp + rol a + sta PPUADDR + lda console_temp + ora #console_left_margin + sta PPUADDR + rts + + +; A = (A + 8) % 240 +; Preserved: X, Y +console_add_8_to_scroll_: + cmp #240-8 + bcc :+ + adc #16-1;+1 for set carry +: adc #8 + rts + + +console_show_custom_color_: + pha + jsr console_wait_vbl_ + setb PPUMASK,PPUMASK_BG0 + pla + jsr console_load_palette_ + jmp console_apply_scroll_ + + +console_load_palette_: + pha + setb PPUADDR,$3F + setb PPUADDR,$00 + setb PPUDATA,$0F ; black + pla + sta PPUDATA + sta PPUDATA + sta PPUDATA + rts + + +; Initializes PPU if necessary, then waits for VBL +; Preserved: A, X, Y, console_temp +console_wait_vbl_: + lda console_scroll + cmp #$FF + bne @already_initialized + + ; Deferred initialization of PPU until first use of console + + ; In case PPU doesn't support scrolling, start a + ; couple of lines down + setb console_scroll,16 + + jsr console_hide + tya + pha + + ; Fill nametable with spaces + setb PPUADDR,$20 + setb PPUADDR,$00 + ldy #240 + lda #' ' +: sta PPUDATA + sta PPUDATA + sta PPUDATA + sta PPUDATA + dey + bne :- + + ; Clear attributes + lda #0 + ldy #$40 +: sta PPUDATA + dey + bne :- + + pla + tay + + jsr console_show +@already_initialized: + jmp wait_vbl_optional + + +; Flushes current line +; Preserved: X, Y +console_flush_: + lda console_scroll + jsr console_set_ppuaddr_ + + sty console_temp + + ; Copy line + ldy #0 + beq :++ +: lda console_buf,y + sta PPUDATA + iny +: cpy console_pos + bne :-- + + ldy console_temp + rts diff --git a/src/test_roms/common/crc.s b/src/test_roms/common/crc.s new file mode 100644 index 0000000..de96c2a --- /dev/null +++ b/src/test_roms/common/crc.s @@ -0,0 +1,118 @@ +; CRC-32 checksum calculation + +zp_res checksum,4 +zp_byte checksum_temp +zp_byte checksum_off_ + +; Turns CRC updating on/off. Allows nesting. +; Preserved: A, X, Y +crc_off: + dec checksum_off_ + rts + +crc_on: inc checksum_off_ + beq :+ + jpl internal_error ; catch unbalanced crc calls +: rts + + +; Initializes checksum module. Might initialize tables +; in the future. +init_crc: + jmp reset_crc + + +; Clears checksum and turns it on +; Preserved: X, Y +reset_crc: + lda #0 + sta checksum_off_ + lda #$FF + sta checksum + sta checksum + 1 + sta checksum + 2 + sta checksum + 3 + rts + + +; Updates checksum with byte in A (unless disabled via crc_off) +; Preserved: A, X, Y +; Time: 357 clocks average +update_crc: + bit checksum_off_ + bmi update_crc_off +update_crc_: + pha + stx checksum_temp + eor checksum + ldx #8 +@bit: lsr checksum+3 + ror checksum+2 + ror checksum+1 + ror a + bcc :+ + sta checksum + lda checksum+3 + eor #$ED + sta checksum+3 + lda checksum+2 + eor #$B8 + sta checksum+2 + lda checksum+1 + eor #$83 + sta checksum+1 + lda checksum + eor #$20 +: dex + bne @bit + sta checksum + ldx checksum_temp + pla +update_crc_off: + rts + + +; Prints checksum as 8-character hex value +print_crc: + jsr crc_off + + ; Print complement + ldx #3 +: lda checksum,x + eor #$FF + jsr print_hex + dex + bpl :- + + jmp crc_on + + +; EQ if checksum matches CRC +; Out: A=0 and EQ if match, A>0 and NE if different +; Preserved: X, Y +.macro is_crc crc + jsr_with_addr is_crc_,{.dword crc} +.endmacro + +is_crc_: + tya + pha + + ; Compare with complemented checksum + ldy #3 +: lda (ptr),y + sec + adc checksum,y + bne @wrong + dey + bpl :- + pla + tay + lda #0 + rts + +@wrong: + pla + tay + lda #1 + rts diff --git a/src/test_roms/common/crc_fast.s b/src/test_roms/common/crc_fast.s new file mode 100644 index 0000000..d2b6078 --- /dev/null +++ b/src/test_roms/common/crc_fast.s @@ -0,0 +1,124 @@ +; Fast table-based CRC-32 +zp_res checksum,4 +zp_byte checksum_temp +zp_byte checksum_off_ +; Clears checksum and turns it on +; Preserved: X, Y +reset_crc: + lda #0 + sta checksum_off_ + lda #$FF + sta checksum + sta checksum + 1 + sta checksum + 2 + sta checksum + 3 + rts + +; Initializes fast CRC tables and resets checksum. +; Preserved: Y +init_crc_fast = reset_crc + +; Updates checksum with byte from A +; Preserved: X, Y +; Time: 54 clocks +update_crc_fast: + stx checksum_temp + +; Updates checksum with byte from A +; Preserved: Y +; Time: 42 clocks +.macro update_crc_fast + eor checksum + tax + lda checksum+1 + eor checksum_t0,x + sta checksum + lda checksum+2 + eor checksum_t1,x + sta checksum+1 + lda checksum+3 + eor checksum_t2,x + sta checksum+2 + lda checksum_t3,x + sta checksum+3 +.endmacro + + update_crc_fast + ldx checksum_temp + rts + +.pushseg +.segment "RODATA" +.align 256 +checksum_t0: + .byte $8D,$1B,$A1,$37,$94,$02,$B8,$2E,$BF,$29,$93,$05,$A6,$30,$8A,$1C + .byte $E9,$7F,$C5,$53,$F0,$66,$DC,$4A,$DB,$4D,$F7,$61,$C2,$54,$EE,$78 + .byte $45,$D3,$69,$FF,$5C,$CA,$70,$E6,$77,$E1,$5B,$CD,$6E,$F8,$42,$D4 + .byte $21,$B7,$0D,$9B,$38,$AE,$14,$82,$13,$85,$3F,$A9,$0A,$9C,$26,$B0 + .byte $1D,$8B,$31,$A7,$04,$92,$28,$BE,$2F,$B9,$03,$95,$36,$A0,$1A,$8C + .byte $79,$EF,$55,$C3,$60,$F6,$4C,$DA,$4B,$DD,$67,$F1,$52,$C4,$7E,$E8 + .byte $D5,$43,$F9,$6F,$CC,$5A,$E0,$76,$E7,$71,$CB,$5D,$FE,$68,$D2,$44 + .byte $B1,$27,$9D,$0B,$A8,$3E,$84,$12,$83,$15,$AF,$39,$9A,$0C,$B6,$20 + .byte $AD,$3B,$81,$17,$B4,$22,$98,$0E,$9F,$09,$B3,$25,$86,$10,$AA,$3C + .byte $C9,$5F,$E5,$73,$D0,$46,$FC,$6A,$FB,$6D,$D7,$41,$E2,$74,$CE,$58 + .byte $65,$F3,$49,$DF,$7C,$EA,$50,$C6,$57,$C1,$7B,$ED,$4E,$D8,$62,$F4 + .byte $01,$97,$2D,$BB,$18,$8E,$34,$A2,$33,$A5,$1F,$89,$2A,$BC,$06,$90 + .byte $3D,$AB,$11,$87,$24,$B2,$08,$9E,$0F,$99,$23,$B5,$16,$80,$3A,$AC + .byte $59,$CF,$75,$E3,$40,$D6,$6C,$FA,$6B,$FD,$47,$D1,$72,$E4,$5E,$C8 + .byte $F5,$63,$D9,$4F,$EC,$7A,$C0,$56,$C7,$51,$EB,$7D,$DE,$48,$F2,$64 + .byte $91,$07,$BD,$2B,$88,$1E,$A4,$32,$A3,$35,$8F,$19,$BA,$2C,$96,$00 + +checksum_t1: + .byte $EF,$DF,$8E,$BE,$2B,$1B,$4A,$7A,$67,$57,$06,$36,$A3,$93,$C2,$F2 + .byte $FF,$CF,$9E,$AE,$3B,$0B,$5A,$6A,$77,$47,$16,$26,$B3,$83,$D2,$E2 + .byte $CF,$FF,$AE,$9E,$0B,$3B,$6A,$5A,$47,$77,$26,$16,$83,$B3,$E2,$D2 + .byte $DF,$EF,$BE,$8E,$1B,$2B,$7A,$4A,$57,$67,$36,$06,$93,$A3,$F2,$C2 + .byte $AE,$9E,$CF,$FF,$6A,$5A,$0B,$3B,$26,$16,$47,$77,$E2,$D2,$83,$B3 + .byte $BE,$8E,$DF,$EF,$7A,$4A,$1B,$2B,$36,$06,$57,$67,$F2,$C2,$93,$A3 + .byte $8E,$BE,$EF,$DF,$4A,$7A,$2B,$1B,$06,$36,$67,$57,$C2,$F2,$A3,$93 + .byte $9E,$AE,$FF,$CF,$5A,$6A,$3B,$0B,$16,$26,$77,$47,$D2,$E2,$B3,$83 + .byte $6C,$5C,$0D,$3D,$A8,$98,$C9,$F9,$E4,$D4,$85,$B5,$20,$10,$41,$71 + .byte $7C,$4C,$1D,$2D,$B8,$88,$D9,$E9,$F4,$C4,$95,$A5,$30,$00,$51,$61 + .byte $4C,$7C,$2D,$1D,$88,$B8,$E9,$D9,$C4,$F4,$A5,$95,$00,$30,$61,$51 + .byte $5C,$6C,$3D,$0D,$98,$A8,$F9,$C9,$D4,$E4,$B5,$85,$10,$20,$71,$41 + .byte $2D,$1D,$4C,$7C,$E9,$D9,$88,$B8,$A5,$95,$C4,$F4,$61,$51,$00,$30 + .byte $3D,$0D,$5C,$6C,$F9,$C9,$98,$A8,$B5,$85,$D4,$E4,$71,$41,$10,$20 + .byte $0D,$3D,$6C,$5C,$C9,$F9,$A8,$98,$85,$B5,$E4,$D4,$41,$71,$20,$10 + .byte $1D,$2D,$7C,$4C,$D9,$E9,$B8,$88,$95,$A5,$F4,$C4,$51,$61,$30,$00 + +checksum_t2: + .byte $02,$05,$0C,$0B,$6F,$68,$61,$66,$D9,$DE,$D7,$D0,$B4,$B3,$BA,$BD + .byte $B5,$B2,$BB,$BC,$D8,$DF,$D6,$D1,$6E,$69,$60,$67,$03,$04,$0D,$0A + .byte $6C,$6B,$62,$65,$01,$06,$0F,$08,$B7,$B0,$B9,$BE,$DA,$DD,$D4,$D3 + .byte $DB,$DC,$D5,$D2,$B6,$B1,$B8,$BF,$00,$07,$0E,$09,$6D,$6A,$63,$64 + .byte $DE,$D9,$D0,$D7,$B3,$B4,$BD,$BA,$05,$02,$0B,$0C,$68,$6F,$66,$61 + .byte $69,$6E,$67,$60,$04,$03,$0A,$0D,$B2,$B5,$BC,$BB,$DF,$D8,$D1,$D6 + .byte $B0,$B7,$BE,$B9,$DD,$DA,$D3,$D4,$6B,$6C,$65,$62,$06,$01,$08,$0F + .byte $07,$00,$09,$0E,$6A,$6D,$64,$63,$DC,$DB,$D2,$D5,$B1,$B6,$BF,$B8 + .byte $BA,$BD,$B4,$B3,$D7,$D0,$D9,$DE,$61,$66,$6F,$68,$0C,$0B,$02,$05 + .byte $0D,$0A,$03,$04,$60,$67,$6E,$69,$D6,$D1,$D8,$DF,$BB,$BC,$B5,$B2 + .byte $D4,$D3,$DA,$DD,$B9,$BE,$B7,$B0,$0F,$08,$01,$06,$62,$65,$6C,$6B + .byte $63,$64,$6D,$6A,$0E,$09,$00,$07,$B8,$BF,$B6,$B1,$D5,$D2,$DB,$DC + .byte $66,$61,$68,$6F,$0B,$0C,$05,$02,$BD,$BA,$B3,$B4,$D0,$D7,$DE,$D9 + .byte $D1,$D6,$DF,$D8,$BC,$BB,$B2,$B5,$0A,$0D,$04,$03,$67,$60,$69,$6E + .byte $08,$0F,$06,$01,$65,$62,$6B,$6C,$D3,$D4,$DD,$DA,$BE,$B9,$B0,$B7 + .byte $BF,$B8,$B1,$B6,$D2,$D5,$DC,$DB,$64,$63,$6A,$6D,$09,$0E,$07,$00 + +checksum_t3: + .byte $D2,$A5,$3C,$4B,$D5,$A2,$3B,$4C,$DC,$AB,$32,$45,$DB,$AC,$35,$42 + .byte $CF,$B8,$21,$56,$C8,$BF,$26,$51,$C1,$B6,$2F,$58,$C6,$B1,$28,$5F + .byte $E9,$9E,$07,$70,$EE,$99,$00,$77,$E7,$90,$09,$7E,$E0,$97,$0E,$79 + .byte $F4,$83,$1A,$6D,$F3,$84,$1D,$6A,$FA,$8D,$14,$63,$FD,$8A,$13,$64 + .byte $A4,$D3,$4A,$3D,$A3,$D4,$4D,$3A,$AA,$DD,$44,$33,$AD,$DA,$43,$34 + .byte $B9,$CE,$57,$20,$BE,$C9,$50,$27,$B7,$C0,$59,$2E,$B0,$C7,$5E,$29 + .byte $9F,$E8,$71,$06,$98,$EF,$76,$01,$91,$E6,$7F,$08,$96,$E1,$78,$0F + .byte $82,$F5,$6C,$1B,$85,$F2,$6B,$1C,$8C,$FB,$62,$15,$8B,$FC,$65,$12 + .byte $3F,$48,$D1,$A6,$38,$4F,$D6,$A1,$31,$46,$DF,$A8,$36,$41,$D8,$AF + .byte $22,$55,$CC,$BB,$25,$52,$CB,$BC,$2C,$5B,$C2,$B5,$2B,$5C,$C5,$B2 + .byte $04,$73,$EA,$9D,$03,$74,$ED,$9A,$0A,$7D,$E4,$93,$0D,$7A,$E3,$94 + .byte $19,$6E,$F7,$80,$1E,$69,$F0,$87,$17,$60,$F9,$8E,$10,$67,$FE,$89 + .byte $49,$3E,$A7,$D0,$4E,$39,$A0,$D7,$47,$30,$A9,$DE,$40,$37,$AE,$D9 + .byte $54,$23,$BA,$CD,$53,$24,$BD,$CA,$5A,$2D,$B4,$C3,$5D,$2A,$B3,$C4 + .byte $72,$05,$9C,$EB,$75,$02,$9B,$EC,$7C,$0B,$92,$E5,$7B,$0C,$95,$E2 + .byte $6F,$18,$81,$F6,$68,$1F,$86,$F1,$61,$16,$8F,$F8,$66,$11,$88,$FF +.popseg diff --git a/src/test_roms/common/delay.s b/src/test_roms/common/delay.s new file mode 100644 index 0000000..21d5f47 --- /dev/null +++ b/src/test_roms/common/delay.s @@ -0,0 +1,190 @@ +; Delays in CPU clocks, milliseconds, etc. All routines are re-entrant +; (no global data). No routines touch X or Y during execution. +; Code generated by macros is relocatable; it contains no JMPs to itself. + +zp_byte delay_temp_ ; only written to + +; Delays n clocks, from 2 to 16777215 +; Preserved: A, X, Y, flags +.macro delay n + .if (n) < 0 .or (n) = 1 .or (n) > 16777215 + .error "Delay out of range" + .endif + delay_ (n) +.endmacro + + +; Delays n milliseconds (1/1000 second) +; n can range from 0 to 1100. +; Preserved: A, X, Y, flags +.macro delay_msec n + .if (n) < 0 .or (n) > 1100 + .error "time out of range" + .endif + delay ((n)*CLOCK_RATE+500)/1000 +.endmacro + + +; Delays n microseconds (1/1000000 second). +; n can range from 0 to 100000. +; Preserved: A, X, Y, flags +.macro delay_usec n + .if (n) < 0 .or (n) > 100000 + .error "time out of range" + .endif + delay ((n)*((CLOCK_RATE+50)/100)+5000)/10000 +.endmacro + +.align 64 + +; Delays A clocks + overhead +; Preserved: X, Y +; Time: A+25 clocks (including JSR) +: sbc #7 ; carry set by CMP +delay_a_25_clocks: + cmp #7 + bcs :- ; do multiples of 7 + lsr a ; bit 0 + bcs :+ +: ; A=clocks/2, either 0,1,2,3 + beq @zero ; 0: 5 + lsr a + beq :+ ; 1: 7 + bcc :+ ; 2: 9 +@zero: bne :+ ; 3: 11 +: rts ; (thanks to dclxvi for the algorithm) + + +; Delays A*256 clocks + overhead +; Preserved: X, Y +; Time: A*256+16 clocks (including JSR) +delay_256a_16_clocks: + cmp #0 + bne :+ + rts +delay_256a_11_clocks_: +: pha + lda #256-19-22 + jsr delay_a_25_clocks + pla + clc + adc #$FF + bne :- + rts + + +; Delays A*65536 clocks + overhead +; Preserved: X, Y +; Time: A*65536+16 clocks (including JSR) +delay_65536a_16_clocks: + cmp #0 + bne :+ + rts +delay_65536a_11_clocks_: +: pha + lda #256-19-22-13 + jsr delay_a_25_clocks + lda #255 + jsr delay_256a_11_clocks_ + pla + clc + adc #$FF + bne :- + rts + +max_short_delay = 41 + + ; delay_short_ macro jumps into these + .res (max_short_delay-12)/2,$EA ; NOP +delay_unrolled_: + rts + +.macro delay_short_ n + .if n < 0 .or n = 1 .or n > max_short_delay + .error "Internal delay error" + .endif + .if n = 0 + ; nothing + .elseif n = 2 + nop + .elseif n = 3 + sta 65536+17 + lda #^(n - 15) + jsr delay_65536a_11_clocks_ + ; +2 ensures remaining clocks is never 1 + delay_nosave_ (((n - 15) & $FFFF) + 2) + .elseif n > 255+27 + lda #>(n - 15) + jsr delay_256a_11_clocks_ + ; +2 ensures remaining clocks is never 1 + delay_nosave_ (<(n - 15) + 2) + .elseif n >= 27 + lda #<(n - 27) + jsr delay_a_25_clocks + .else + delay_short_ n + .endif +.endmacro + +.macro delay_ n + .if n > max_short_delay + php + pha + delay_nosave_ (n - 14) + pla + plp + .else + delay_short_ n + .endif +.endmacro + diff --git a/src/test_roms/common/devcart.bin b/src/test_roms/common/devcart.bin new file mode 100644 index 0000000000000000000000000000000000000000..684fbb99be38d6e3c146eadd8f8dd005ffc5268f GIT binary patch literal 256 zcmWH^NL5J7&&kOz&&*3#NX}15Rmdz>C@oG+QAny(NJ%YAPAn?XQ}8SS3FoDjr4}iq zR-`7EmOvDgq!yPb1o`{wF|6e1Wo+nWSim*mmrujL|HOg?>t;w)Uiv%JiQ&YB?>-ES zViwD1s88?d&`L@>csa>_f|eTR_2XX!HG8V%xUMJ9nVkFdh4L14siw*QS43#N*4V(k eG_}z~_wClCXDKH%UZp1I&L`I9|NlLK&IACCCY;*< literal 0 HcmV?d00001 diff --git a/src/test_roms/common/instr_test.inc b/src/test_roms/common/instr_test.inc new file mode 100644 index 0000000..afc0ae4 --- /dev/null +++ b/src/test_roms/common/instr_test.inc @@ -0,0 +1,245 @@ +REGION_FREE = 1 +.include "shell.inc" +.include "crc_fast.s" + +zp_byte in_p +zp_byte in_a +zp_byte in_x +zp_byte in_y +zp_byte in_s + +.macro entry opcode,name,crc_0,crc_1,crc_2,crc_3 + jsr reset_crc + lda #opcode + sta opcode_byte - testcase_entry + testcase_instance + jsr run_testcase + ; testcase_0 OPERANDS + 4,{testcase opcode} + lda checksum + cmp #crc_0 + jne fail + lda checksum+1 + cmp #crc_1 + jne fail + lda checksum+2 + cmp #crc_2 + jne fail + lda checksum+3 + cmp #crc_3 + jne fail +.endmacro + +testcase_entry: + lda in_p + pha + lda in_a + ldx in_x + ldy in_y + plp +opcode_byte: .byte 0 + .if OPERANDS > 0 + ; .out .sprintf(" 1: %s", .string(op_1)) + .byte 0 + .endif + .if OPERANDS > 1 + ; .out .sprintf(" 2: %s", .string(op_2)) + .byte 0 + .endif + php + pha + txa + pha + tya + pha + ; Stack: + pla + jsr update_crc_fast + pla + jsr update_crc_fast + pla + jsr update_crc_fast + pla + jsr update_crc_fast + rts +testcase_entry_size = * - testcase_entry + +; .macro testcase opcode, reg_p, reg_a, reg_x, reg_y, op_1, op_2 + ; jsr crc_calc + ; .out .sprintf("OP: %s", .string(opcode)) + ; .out .sprintf(" S: %s", .string(reg_p)) + ; .out .sprintf(" A: %s", .string(reg_a)) + ; .out .sprintf(" X: %s", .string(reg_x)) + ; .out .sprintf(" Y: %s", .string(reg_y)) +; .endmacro +; .macro testcase_6 opcode, ops +; testcase opcode, ops +; ; .mid(0, 1, ops), .mid(1, 1, ops), .mid(2, 1, ops), .mid(3, 1, ops), .mid(4, 1, ops), .mid(5, 1, ops) +; .endmacro +; .macro testcase_0 count,ins +; .if count > 0 +; testcase_0 count - 1, {ins,0} +; testcase_0 count - 1, {ins,1} +; testcase_0 count - 1, {ins,2} +; testcase_0 count - 1, {ins,$40} +; testcase_0 count - 1, {ins,$7F} +; testcase_0 count - 1, {ins,$80} +; testcase_0 count - 1, {ins,$81} +; testcase_0 count - 1, {ins,$FF} +; ; .byte 0,1,2,$40,$7F,$80,$81,$FF +; .else +; ins +; .endif +; .endmacro +.pushseg +.segment "BSS" +testcase_instance: .res testcase_entry_size +.popseg + +testcase_init: + ldx #0 +@loop: lda testcase_entry,x + sta testcase_instance,x + inx + cpx #testcase_entry_size + bmi @loop + rts + +; values: +; .byte 0,1,2,$40,$7F,$80,$81,$FF +; values_size = * - values + ; .byte 0,1,2,$40,$7F,$80,$81,$FF +.macro increment pos, next, done + lda pos + cmp #0 + bne @a_1 + lda #1 + sta pos + jmp done +@a_1: cmp #1 + bne @a_2 + lda #2 + sta pos + jmp done +@a_2: cmp #2 + bne @a_40 + lda #40 + sta pos + jmp done +@a_40: cmp #$40 + bne @a_7F + lda #$7F + sta pos + jmp done +@a_7F: cmp #$7F + bne @a_80 + lda #$80 + sta pos + jmp done +@a_80: cmp #$80 + bne @a_81 + lda #$81 + sta pos + jmp done +@a_81: cmp #$81 + bne @a_FF + lda #$FF + sta pos + jmp done +@a_FF: cmp #$FF + lda #0 + sta pos + jmp next +.endmacro +run_testcase: + increment in_a, increment_x, done +increment_x: + increment in_x, increment_y, done +increment_y: + increment in_y, increment_p, done +increment_p: + increment in_p, ret, done +done: + jsr testcase_instance + jmp run_testcase +ret: + rts + +; Defines instruction to test + +; Values set_paxyso sets registers to. Set +; before calling test_values, which can then +; overwrite them if desired. + +; Temporary space for check_paxyso +zp_byte out_a +zp_byte out_x +zp_byte out_s + +; Values to cycle through for registers + +; ; Sets bytes on stack around in_s +; .macro set_stack +; ldx in_s +; inx +; inx +; ldy #6 +; : txa +; asl +; eor #$A5 +; sta $100,x +; dex +; dey +; bne :- +; .endmacro + +; ; Checksums bytes on stack around in_s +; .macro check_stack +; ldx in_s +; inx +; inx +; ldy #6 +; : lda $100,x +; dex +; jsr update_crc +; dey +; bne :- +; .endmacro + +; ; Sets P, A, X, Y, S, and operand +; .macro set_paxyso +; ldx in_s +; txs +; lda values,y +; sta operand +; lda in_p +; pha +; lda in_a +; ldx in_x +; ldy in_y +; plp +; .endmacro + +; ; Checksums P, A, X, Y, S, and operand +; .macro check_paxyso +; php +; sta out_a +; pla + +; stx out_x +; tsx +; stx out_s +; ldx saved_s +; txs + +; cld +; jsr update_crc_fast +; lda out_a +; jsr update_crc_fast +; lda out_x +; jsr update_crc_fast +; tya +; jsr update_crc_fast +; lda out_s +; jsr update_crc_fast +; lda operand +; jsr update_crc_fast +; .endmacro diff --git a/src/test_roms/common/instr_test_end.s b/src/test_roms/common/instr_test_end.s new file mode 100644 index 0000000..07fca18 --- /dev/null +++ b/src/test_roms/common/instr_test_end.s @@ -0,0 +1,199 @@ +; Offset of current instruction +zp_byte instrs_idx + +zp_byte failed_count + +main: + ; Stack slightly lower than top + ldx #$A2 + txs + + jsr init_crc_fast + + ; Test each instruction + lda #0 +@loop: sta instrs_idx + tay + + jsr reset_crc + lda instrs,y + jsr test_instr + jsr check_result + + lda instrs_idx + clc + adc #4 + cmp #instrs_size + bne @loop + +.ifdef BUILD_DEVCART + lda #0 + jmp exit +.endif + + lda failed_count + jne test_failed + jmp tests_passed + +; Check result of test +check_result: +.ifdef BUILD_DEVCART + ; Print correct CRC + jsr crc_off + print_str ".dword $" + ldx #0 +: lda checksum,x + jsr print_hex + inx + cpx #4 + bne :- + jsr print_newline + jsr crc_on +.else + ; Verify CRC + ldx #3 + ldy instrs_idx +: lda checksum,x + cmp correct_checksums,y + bne @wrong + iny + dex + bpl :- +.endif + rts + +; Print failed opcode and name +@wrong: + ldy instrs_idx + lda instrs,y + jsr print_a + jsr play_byte + lda instrs+2,y + sta addr + lda instrs+3,y + sta addr+1 + jsr print_str_addr + jsr print_newline + inc failed_count + rts + +; Place where instruction is executed +instr = $3A0 + +; Tests instr A +test_instr: + sta instr + jsr avoid_silent_nsf + + ; Copy rest of template + ldx #instr_template_size - 1 +: lda instr_template,x + sta instr,x + dex + bne :- + + ; Disable and be sure APU IRQs are clear, since + ; I flag gets cleared during testing. + setb SNDMODE,$C0 + setb $4010,0 + nop + lda SNDCHN + + ; Default stack + lda #$90 + sta in_s + + ; Test with different flags + lda #$00 + jsr test_flags + lda #$FF + jsr test_flags + + rts + +; Position in operand table +zp_byte operand_idx + +test_flags: + sta in_p + + ldy #values_size-1 +: sty operand_idx + + lda values,y + sta in_a + + lda values+1,y + sta in_x + + lda values+2,y + sta in_y + + jsr test_values + + ldy operand_idx + dey + bpl :- + + rts + +.ifndef values2 + values2 = values + values2_size = values_size +.endif + +.macro test_normal +zp_byte a_idx +zp_byte saved_s + + tsx + stx saved_s + + set_stack + + ldy #values2_size-1 +inner: sty a_idx + + lda values2,y + sta operand + + set_in + +; For debugging +.if 0 + ; P A X Y S O (z,x) (z),y + jsr print_p + jsr print_a + jsr print_x + jsr print_y + jsr print_s + lda operand + jsr print_a +.ifdef address + lda (address,x) + jsr print_a + lda (address),y + jsr print_a +.else + lda operand,x + jsr print_a + lda operand,y + jsr print_a +.endif + jsr print_newline +.endif + + jmp instr +instr_done: + + check_out + + ldy a_idx + dey + bpl inner + + check_stack + + ldx saved_s + txs +.endmacro diff --git a/src/test_roms/common/macros.inc b/src/test_roms/common/macros.inc new file mode 100644 index 0000000..d4cb72e --- /dev/null +++ b/src/test_roms/common/macros.inc @@ -0,0 +1,169 @@ +; jxx equivalents to bxx +.macpack longbranch + +; blt, bge equivalents to bcc, bcs +.define blt bcc +.define bge bcs +.define jge jcs +.define jlt jcc + +; Puts data in another segment +.macro seg_data seg,data + .pushseg + .segment seg + data + .popseg +.endmacro + +; Reserves size bytes in zeropage/bss for name. +; If size is omitted, reserves one byte. +.macro zp_res name,size + .ifblank size + zp_res name,1 + .else + seg_data "ZEROPAGE",{name: .res size} + .endif +.endmacro + +.macro bss_res name,size + .ifblank size + bss_res name,1 + .else + seg_data "BSS",{name: .res size} + .endif +.endmacro + +.macro nv_res name,size + .ifblank size + nv_res name,1 + .else + seg_data "NVRAM",{name: .res size} + .endif +.endmacro + +; Reserves one byte in zeropage for name (very common) +.macro zp_byte name + seg_data "ZEROPAGE",{name: .res 1} +.endmacro + +; Passes constant data to routine in addr +; Preserved: A, X, Y +.macro jsr_with_addr routine,data + .local Addr + pha + lda #Addr + sta addr+1 + pla + jsr routine + seg_data "RODATA",{Addr: data} +.endmacro + +; Calls routine multiple times, with A having the +; value 'start' the first time, 'start+step' the +; second time, up to 'end' for the last time. +.macro for_loop routine,start,end,step + lda #start +: pha + jsr routine + pla + clc + adc #step + cmp #<((end)+(step)) + bne :- +.endmacro + +; Calls routine n times. The value of A in the routine +; counts from 0 to n-1. +.macro loop_n_times routine,n + for_loop routine,0,n-1,+1 +.endmacro + +; Same as for_loop, except uses 16-bit value in YX. +; -256 <= step <= 255 +.macro for_loop16 routine,start,end,step +.if (step) < -256 || (step) > 255 + .error "Step must be within -256 to 255" +.endif + ldy #>(start) + lda #<(start) +: tax + pha + tya + pha + jsr routine + pla + tay + pla + clc + adc #step +.if (step) > 0 + bcc :+ + iny +.else + bcs :+ + dey +.endif +: cmp #<((end)+(step)) + bne :-- + cpy #>((end)+(step)) + bne :-- +.endmacro + +; Copies byte from in to out +; Preserved: X, Y +.macro mov out, in + lda in + sta out +.endmacro + +; Stores byte at addr +; Preserved: X, Y +.macro setb addr, byte + lda #byte + sta addr +.endmacro + +; Stores word at addr +; Preserved: X, Y +.macro setw addr, word + lda #<(word) + sta addr + lda #>(word) + sta addr+1 +.endmacro + +; Loads XY with 16-bit immediate or value at address +.macro ldxy Arg + .if .match( .left( 1, {Arg} ), # ) + ldy #<(.right( .tcount( {Arg} )-1, {Arg} )) + ldx #>(.right( .tcount( {Arg} )-1, {Arg} )) + .else + ldy (Arg) + ldx (Arg)+1 + .endif +.endmacro + +; Increments word at Addr and sets Z flag appropriately +; Preserved: A, X, Y +.macro incw Addr + .local @incw_skip ; doesn't work, so HOW THE HELL DO YOU MAKE A LOCAL LABEL IN A MACRO THAT DOESN"T DISTURB INVOKING CODE< HUH?????? POS + inc Addr + bne @incw_skip + inc Addr+1 +@incw_skip: +.endmacro + +; Increments XY as 16-bit register, in CONSTANT time. +; Z flag set based on entire result. +; Preserved: A +; Time: 7 clocks +.macro inxy + iny ; 2 + beq *+4 ; 3 + ; -1 + bne *+3 ; 3 + ; -1 + inx ; 2 +.endmacro diff --git a/src/test_roms/common/neshw.inc b/src/test_roms/common/neshw.inc new file mode 100644 index 0000000..814d772 --- /dev/null +++ b/src/test_roms/common/neshw.inc @@ -0,0 +1,43 @@ +; NES I/O locations and masks + +; Clocks per second +.ifndef CLOCK_RATE + CLOCK_RATE = 1789773 ; NTSC +; CLOCK_RATE = 1662607 ; PAL +.endif + +.ifndef BUILD_NSF + +; PPU +PPUCTRL = $2000 +PPUMASK = $2001 +PPUSTATUS = $2002 +SPRADDR = $2003 +SPRDATA = $2004 +PPUSCROLL = $2005 +PPUADDR = $2006 +PPUDATA = $2007 +SPRDMA = $4014 + +PPUCTRL_NMI = $80 +PPUMASK_BG0 = $0A +PPUCTRL_8X8 = $00 +PPUCTRL_8X16 = $20 +PPUMASK_SPR = $14 +PPUMASK_BG0CLIP = $08 + +.endif + +; APU +SNDCHN = $4015 +JOY1 = $4016 +JOY2 = $4017 +SNDMODE = $4017 + +SNDMODE_NOIRQ = $40 + +.if CLOCK_RATE = 1789773 + PPU_FRAMELEN = 29781 +.elseif CLOCK_RATE = 1662607 + PPU_FRAMELEN = 33248 +.endif diff --git a/src/test_roms/common/ppu.s b/src/test_roms/common/ppu.s new file mode 100644 index 0000000..21cad12 --- /dev/null +++ b/src/test_roms/common/ppu.s @@ -0,0 +1,142 @@ +; PPU utilities + +bss_res ppu_not_present + +; Sets PPUADDR to w +; Preserved: X, Y +.macro set_ppuaddr w + bit PPUSTATUS + setb PPUADDR,>w + setb PPUADDR, 1789773 + .error "Currently only supports NTSC" + .endif + delay ((n)*341)/3 +.endmacro + + +; Waits for VBL then disables PPU rendering. +; Preserved: A, X, Y +disable_rendering: + pha + jsr wait_vbl_optional + setb PPUMASK,0 + pla + rts + + +; Fills first nametable with $00 +; Preserved: Y +clear_nametable: + ldx #$20 + bne clear_nametable_ + +clear_nametable2: + ldx #$24 +clear_nametable_: + lda #0 + jsr fill_screen_ + + ; Clear pattern table + ldx #64 +: sta PPUDATA + dex + bne :- + rts + + +; Fills screen with tile A +; Preserved: A, Y +fill_screen: + ldx #$20 + bne fill_screen_ + +; Same as fill_screen, but fills other nametable +fill_screen2: + ldx #$24 +fill_screen_: + stx PPUADDR + ldx #$00 + stx PPUADDR + ldx #240 +: sta PPUDATA + sta PPUDATA + sta PPUDATA + sta PPUDATA + dex + bne :- + rts + + +; Fills palette with $0F +; Preserved: Y +clear_palette: + set_ppuaddr $3F00 + ldx #$20 + lda #$0F +: sta PPUDATA + dex + bne :- + + +; Fills OAM with $FF +; Preserved: Y +clear_oam: + lda #$FF + +; Fills OAM with A +; Preserved: A, Y +fill_oam: + ldx #0 + stx SPRADDR +: sta SPRDATA + dex + bne :- + rts + + +; Initializes wait_vbl_optional. Must be called before +; using it. +.align 32 +init_wait_vbl: + ; Wait for VBL flag to be set, or ~60000 + ; clocks (2 frames) to pass + ldy #24 + ldx #1 + bit PPUSTATUS +: bit PPUSTATUS + bmi @set + dex + bne :- + dey + bpl :- +@set: + ; Be sure flag didn't stay set (in case + ; PPUSTATUS always has high bit set) + tya + ora PPUSTATUS + sta ppu_not_present + rts + + +; Same as wait_vbl, but returns immediately if PPU +; isn't working or doesn't support VBL flag +; Preserved: A, X, Y +.align 16 +wait_vbl_optional: + bit ppu_not_present + bmi :++ + ; FALL THROUGH + +; Clears VBL flag then waits for it to be set. +; Preserved: A, X, Y +wait_vbl: + bit PPUSTATUS +: bit PPUSTATUS + bpl :- +: rts diff --git a/src/test_roms/common/print.s b/src/test_roms/common/print.s new file mode 100644 index 0000000..29fbc23 --- /dev/null +++ b/src/test_roms/common/print.s @@ -0,0 +1,235 @@ +; Prints values in various ways to output, +; including numbers and strings. + +newline = 10 + +zp_byte print_temp_ + +; Prints indicated register to console as two hex +; chars and space +; Preserved: A, X, Y, flags +print_a: + php + pha +print_reg_: + jsr print_hex + lda #' ' + jsr print_char_ + pla + plp + rts + +print_x: + php + pha + txa + jmp print_reg_ + +print_y: + php + pha + tya + jmp print_reg_ + +print_p: + php + pha + php + pla + jmp print_reg_ + +print_s: + php + pha + txa + tsx + inx + inx + inx + inx + jsr print_x + tax + pla + plp + rts + + +; Prints A as two hex characters, NO space after +; Preserved: A, X, Y +print_hex: + jsr update_crc + + pha + lsr a + lsr a + lsr a + lsr a + jsr @nibble + pla + + pha + and #$0F + jsr @nibble + pla + rts + +@nibble: + cmp #10 + blt @digit + adc #6;+1 since carry is set +@digit: adc #'0' + jmp print_char_ + + +; Prints character and updates checksum UNLESS +; it's a newline. +; Preserved: A, X, Y +print_char: + cmp #newline + beq :+ + jsr update_crc +: pha + jsr print_char_ + pla + rts + + +; Prints space. Does NOT update checksum. +; Preserved: A, X, Y +print_space: + pha + lda #' ' + jsr print_char_ + pla + rts + + +; Advances to next line. Does NOT update checksum. +; Preserved: A, X, Y +print_newline: + pha + lda #newline + jsr print_char_ + pla + rts + + +; Prints string +; Preserved: A, X, Y +.macro print_str str,str2 + jsr print_str_ + .byte str + .ifnblank str2 + .byte str2 + .endif + .byte 0 +.endmacro + + +print_str_: + sta print_temp_ + + pla + sta addr + pla + sta addr+1 + + jsr inc_addr + jsr print_str_addr + + lda print_temp_ + jmp (addr) + + +; Prints string at addr and leaves addr pointing to +; byte AFTER zero terminator. +; Preserved: A, X, Y +print_str_addr: + pha + tya + pha + + ldy #0 + beq :+ ; always taken +@loop: jsr print_char + jsr inc_addr +: lda (addr),y + bne @loop + + pla + tay + pla + ; FALL THROUGH + +; Increments 16-bit value in addr. +; Preserved: A, X, Y +inc_addr: + inc addr + beq :+ + rts +: inc addr+1 + rts + + +; Prints A as 1-3 digit decimal value, NO space after. +; Preserved: A, X, Y +print_dec: + pha + sta print_temp_ + txa + pha + lda print_temp_ + + ; Hundreds + cmp #10 + blt @ones + cmp #100 + blt @tens + ldx #'0'-1 +: inx + sbc #100 + bge :- + adc #100 + jsr @digit + + ; Tens +@tens: sec + ldx #'0'-1 +: inx + sbc #10 + bge :- + adc #10 + jsr @digit + + ; Ones +@ones: ora #'0' + jsr print_char + pla + tax + pla + rts + + ; Print a single digit +@digit: pha + txa + jsr print_char + pla + rts + + +; Prints one of two characters based on condition. +; SEC; print_cc bcs,'C','-' prints 'C'. +; Preserved: A, X, Y, flags +.macro print_cc cond,yes,no + ; Avoids labels since they're not local + ; to macros in ca65. + php + pha + cond *+6 + lda #no + bne *+4 + lda #yes + jsr print_char + pla + plp +.endmacro diff --git a/src/test_roms/common/run_at_reset.s b/src/test_roms/common/run_at_reset.s new file mode 100644 index 0000000..aeb09f1 --- /dev/null +++ b/src/test_roms/common/run_at_reset.s @@ -0,0 +1,64 @@ +; Keeps track of number of times reset, and prompts user. + +power_flag_value = $42 + +nv_res power_flag_ +nv_res num_resets_ + +; Out: A = number of times NES has been reset since turned on +; Preserved: X, Y +num_resets: + lda power_flag_ + cmp #power_flag_value + bne :+ + lda num_resets_ + rts +: lda #0 + rts + + +; Prompts user to press reset after message disappears, +; then hides message, increments reset count, and asks +; emulator to reset NES. +; Preserved: X, Y +prompt_to_reset: + print_str {newline,newline,"Press reset AFTER this message",newline,"disappears"} + + ; Add "again" if this isn't first requested reset + jsr num_resets + beq :+ + print_str ", again" +: + ; Show for a few seconds + print_str {newline,newline,newline} + jsr console_show + delay_msec 1000 + delay_msec 1000 + + jsr inc_reset_count + + ; Tell emulator that NES should be reset now + lda #$81 + jsr set_final_result + + jsr console_hide + rts + + +; Increments reset count and marks it as valid +; Preserved: X, Y +inc_reset_count: + jsr num_resets + clc + adc #1 + bcc :+ + lda #$FF ; don't wrap around +: sta num_resets_ + setb power_flag_,power_flag_value + rts + + +; Waits in infinite loop for reset +; Preserved: A, X, Y, flags +wait_reset: + jmp wait_reset diff --git a/src/test_roms/common/shell.inc b/src/test_roms/common/shell.inc new file mode 100644 index 0000000..0a750c1 --- /dev/null +++ b/src/test_roms/common/shell.inc @@ -0,0 +1,26 @@ +; Included at beginning of program + +.macro stp + .byte #$02 +.endmacro +.include "macros.inc" +.include "neshw.inc" + +.ifdef CUSTOM_PREFIX + .include "custom_prefix.s" +.endif + +; ; Devcart +; .ifdef BUILD_DEVCART +; .include "build_devcart.s" +; .endif + +; NES internal RAM +.ifdef BUILD_NOCART + .include "build_nocart.s" +.endif + +; NES ROM (default) +.ifndef SHELL_INCLUDED + .include "build_rom.s" +.endif diff --git a/src/test_roms/common/shell.s b/src/test_roms/common/shell.s new file mode 100644 index 0000000..38bc415 --- /dev/null +++ b/src/test_roms/common/shell.s @@ -0,0 +1,384 @@ +; Common routines and runtime + +; Detect inclusion loops (otherwise ca65 goes crazy) +.ifdef SHELL_INCLUDED + .error "shell.s included twice" + .end +.endif +SHELL_INCLUDED = 1 + +; Temporary variables that ANY routine might modify, so +; only use them between routine calls. +temp = <$A +temp2 = <$B +temp3 = <$C +addr = <$E +ptr = addr + +; Move code to $E200 ($200 bytes for text output in devcarts +; where WRAM is mirrored to $E000) +.segment "CODE" + .res $2200 + +; Put shell code after user code, so user code is in more +; consistent environment +.segment "CODE2" + + ; Any user code which runs off end might end up here, + ; so catch that mistake. + nop ; in case there was three-byte opcode before this + nop + jmp internal_error + +;**** Common routines **** + +.include "macros.inc" +.include "neshw.inc" +.include "delay.s" +.include "print.s" +.include "crc.s" +.include "testing.s" + +.ifdef NEED_CONSOLE + .include "console.s" +.else + ; Stubs so code doesn't have to care whether + ; console exists + console_init: + console_show: + console_hide: + console_print: + console_flush: + rts +.endif + +.ifndef CUSTOM_PRINT + .include "text_out.s" + + print_char_: + jsr write_text_out + jmp console_print + + stop_capture: + rts +.endif + +;**** Shell core **** + +.ifndef CUSTOM_RESET + reset: + sei + jmp std_reset +.endif + + +; Sets up hardware then runs main +run_shell: + sei + cld ; unnecessary on NES, but might help on clone + ldx #$FF + txs + jsr init_shell + set_test $FF + jmp run_main + + +; Initializes shell +init_shell: + jsr clear_ram + jsr init_wait_vbl ; waits for VBL once here, + jsr wait_vbl_optional ; so only need to wait once more + ; jsr init_text_out + jsr init_testing + .byte $02 + jsr init_runtime + jsr console_init + rts + + +; Runs main in consistent PPU/APU environment, then exits +; with code 0 +run_main: + jsr pre_main + jsr main + lda #0 + jmp exit + + +; Sets up environment for main to run in +pre_main: + +.ifndef BUILD_NSF + jsr disable_rendering + setb PPUCTRL,0 + jsr clear_palette + jsr clear_nametable + jsr clear_nametable2 + jsr clear_oam +.endif + + lda #$34 + pha + lda #0 + tax + tay + jsr wait_vbl_optional + plp + sta SNDMODE + rts + + +.ifndef CUSTOM_EXIT + exit: +.endif + +; Reports result and ends program +std_exit: + .byte $02 + sei + cld + ldx #$FF + txs + + ldx #0 + stx SNDCHN + .ifndef BUILD_NSF + stx PPUCTRL + .endif + + jsr report_result + jmp post_exit + + +; Reports final result code in A +report_result: + jsr :+ + jmp play_byte + +: jsr print_newline + jsr console_show + + ; 0: "" + cmp #1 + bge :+ + rts +: + ; 1: "Failed" + bne :+ + print_str {"Failed",newline} + rts + + ; n: "Failed #n" +: print_str "Failed #" + jsr print_dec + jsr print_newline + rts + +;**** Other routines **** + +; Reports internal error and exits program +internal_error: + print_str newline,"Internal error" + lda #255 + jmp exit + + +.import __NVRAM_LOAD__, __NVRAM_SIZE__ + +.macro fill_ram_ Begin, End + .local Neg_size + Neg_size = (Begin) - (End) + ldxy #(Begin) - ascii_chr + ldy #0 +@page: + stx addr+1 +: lda (addr),y + sta PPUDATA + iny + bne :- + inx + cpx #>ascii_chr_end + bne @page +.endmacro + +; Disables interrupts and loops forever +.ifndef CUSTOM_FOREVER +forever: + sei + lda #0 + sta PPUCTRL +: beq :- + .res $10,$EA ; room for code to run loader +.endif + + +; Default NMI +.ifndef CUSTOM_NMI + zp_byte nmi_count + + nmi: + inc nmi_count + rti + + ; Waits for NMI. Must be using NMI handler that increments + ; nmi_count, with NMI enabled. + ; Preserved: X, Y + wait_nmi: + lda nmi_count + : cmp nmi_count + beq :- + rts +.endif + + +; Default IRQ +.ifndef CUSTOM_IRQ + irq: + bit SNDCHN ; clear APU IRQ flag + rti +.endif + +.endif + + +; Reports A in binary as high and low tones, with +; leading low tone for reference. Omits leading +; zeroes. Doesn't hang if no APU is present. +; Preserved: A, X, Y +play_hex: + pha + + ; Make low reference beep + clc + jsr @beep + + ; Remove high zero bits + sec +: rol a + bcc :- + + ; Play remaining bits + beq @zero +: jsr @beep + asl a + bne :- +@zero: + + delay_msec 300 + pla + rts + +; Plays low/high beep based on carry +; Preserved: A, X, Y +@beep: + pha + + ; Set up square + lda #1 + sta SNDCHN + sta $4001 + sta $4003 + adc #$FE ; period=$100 if carry, $1FF if none + sta $4002 + + ; Fade volume + lda #$0F +: ora #$30 + sta $4000 + delay_msec 8 + sec + sbc #$31 + bpl :- + + ; Silence + sta SNDCHN + delay_msec 160 + + pla + rts diff --git a/src/test_roms/common/testing.s b/src/test_roms/common/testing.s new file mode 100644 index 0000000..ba41f03 --- /dev/null +++ b/src/test_roms/common/testing.s @@ -0,0 +1,106 @@ +; Utilities for writing test ROMs + +; In NVRAM so these can be used before initializing runtime, +; then runtime initialized without clearing them +nv_res test_code ; code of current test +nv_res test_name,2 ; address of name of current test, or 0 of none + + +; Sets current test code and optional name. Also resets +; checksum. +; Preserved: A, X, Y +.macro set_test code,name + pha + lda #code + jsr set_test_ + .ifblank name + setb test_name+1,0 + .else + .local Addr + setw test_name,Addr + seg_data "RODATA",{Addr: .byte name,0} + .endif + pla +.endmacro + +set_test_: + sta test_code + jmp reset_crc + + +; Initializes testing module +init_testing: + jmp init_crc + + +; Reports that all tests passed +tests_passed: + jsr print_filename + print_str newline,"Passed" + lda #0 + jmp exit + + +; Reports "Done" if set_test has never been used, +; "Passed" if set_test 0 was last used, or +; failure if set_test n was last used. +tests_done: + ldx test_code + jeq tests_passed + inx + bne test_failed + jsr print_filename + print_str newline,"Done" + lda #0 + jmp exit + + +; Reports that the current test failed. Prints code and +; name last set with set_test, or just "Failed" if none +; have been set yet. +test_failed: + ldx test_code + + ; Treat $FF as 1, in case it wasn't ever set + inx + bne :+ + inx + stx test_code +: + ; If code >= 2, print name + cpx #2-1 ; -1 due to inx above + blt :+ + lda test_name+1 + beq :+ + jsr print_newline + sta addr+1 + lda test_name + sta addr + jsr print_str_addr + jsr print_newline +: + jsr print_filename + + ; End program + lda test_code + jmp exit + + +; If checksum doesn't match expected, reports failed test. +; Clears checksum afterwards. +; Preserved: A, X, Y +.macro check_crc expected + jsr_with_addr check_crc_,{.dword expected} +.endmacro + +check_crc_: + pha + jsr is_crc_ + bne :+ + jsr reset_crc + pla + rts + +: jsr print_newline + jsr print_crc + jmp test_failed diff --git a/src/test_roms/common/text_out.s b/src/test_roms/common/text_out.s new file mode 100644 index 0000000..3a4137f --- /dev/null +++ b/src/test_roms/common/text_out.s @@ -0,0 +1,61 @@ +; Text output as expanding zero-terminated string at text_out_base + +; The final exit result byte is written here +final_result = $6000 + +; Text output is written here as an expanding +; zero-terminated string +text_out_base = $6004 + +bss_res text_out_temp +zp_res text_out_addr,2 + +init_text_out: + ldx #0 + + ; Put valid data first + setb text_out_base,0 + + lda #$80 + jsr set_final_result + + ; Now fill in signature that tells emulator there's + ; useful data there + setb text_out_base-3,$DE + setb text_out_base-2,$B0 + setb text_out_base-1,$61 + + ldx #>text_out_base + stx text_out_addr+1 + setb text_out_addr, + pla + tay + ; Stack , y correct + pla + tax + ; Stack , C: x, y + pla + ; Stack

, C: x, y + plp + ; Stack <>, C: x, y, p, (also S) + jsr test_code + php + sta out_a + pla + sta out_p + stx out_x + sty out_y + tsx + stx out_s + + jmp (return_addr) + +test_code: + rts + +main: + jsr testcase_init + ; $A2,$67,$77,$F8 + ; entry $2A,"ROL A",$CB,$4B,$9A,$4C ; A = op A + ; entry $0A,"ASL A",$00,$00,$00,$00 + ; entry $0A,"ROR A",$00,$00,$00,$00 + ; entry $4A,"LSR A",$00,$00,$00,$00 + + ; entry $8A,"TXA",$00,$00,$00,$00 ; AXY = AXY + ; entry $98,"TYA",$00,$00,$00,$00 + ; entry $AA,"TAX",$00,$00,$00,$00 + ; entry $A8,"TAY",$00,$00,$00,$00 + + ; entry $E8,"INX",$00,$00,$00,$00 ; XY = op XY + ; entry $C8,"INY",$00,$00,$00,$00 + ; entry $CA,"DEX",$00,$00,$00,$00 + ; entry $88,"DEY",$00,$00,$00,$00 + + ; entry $38,"SEC",$00,$00,$00,$00 ; flags = op flags + ; entry $18,"CLC",$00,$00,$00,$00 + ; entry $F8,"SED",$00,$00,$00,$00 + ; entry $D8,"CLD",$00,$00,$00,$00 + ; entry $78,"SEI",$00,$00,$00,$00 + ; entry $58,"CLI",$00,$00,$00,$00 + ; entry $B8,"CLV",$00,$00,$00,$00 + + ; entry $EA,"NOP",$00,$00,$00,$00 + + lda #$00 + jmp exit + + +; .ifndef OFFICIAL_ONLY +; entry $1A,"NOP" +; entry $3A,"NOP" +; entry $5A,"NOP" +; entry $7A,"NOP" +; entry $DA,"NOP" +; entry $FA,"NOP" +; .endif +; instrs_size = * - instrs + +; instr_template: +; nop +; jmp instr_done +; instr_template_size = * - instr_template + +; .include "instr_test_end.s" + +; test_values: +; test_normal +; rts diff --git a/src/test_roms/instr_test_v3.rs b/src/test_roms/instr_test_v3.rs new file mode 100644 index 0000000..c1ab77e --- /dev/null +++ b/src/test_roms/instr_test_v3.rs @@ -0,0 +1,63 @@ +use std::fmt; + +use crate::{NES, hex_view::Memory}; + +use super::rom_test; + +enum HexDump<'a> { + NES(&'a NES, u16, usize), + Lit(&'a [u8]), +} + +impl HexDump<'_> { + fn get(&self, off: usize) -> Option { + match self { + Self::NES(_, _, len) if off >= *len => None, + Self::NES(nes, addr, _) => nes.cpu_mem().peek(addr + off as u16), + Self::Lit(items) => items.get(off).copied(), + } + } +} + +impl fmt::Display for HexDump<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut i = 0; + while let Some(val) = self.get(i) { + if i % 16 == 0 { + write!(f, "\n{i:04X}:")?; + } + write!(f, " {val:02X}")?; + i += 1; + } + Ok(()) + } +} + +fn mem_cmp(nes: &NES, addr: u16, vals: &[u8]) { + for (i, v) in vals.iter().enumerate() { + if nes.cpu_mem().peek(addr + i as u16) != Some(*v) { + panic!( + "memcmp assertion failed:\nNES:{}\nTest:{}", + HexDump::NES(nes, addr, vals.len()), + HexDump::Lit(vals) + ); + } + } +} + +rom_test!(crc_check, "crc_check.nes", |nes| { + assert_eq!(nes.last_instruction, "0x8026 HLT :2 []"); + assert_eq!(nes.cpu.a, 0x00); + mem_cmp(&nes, 0x20, &[0xFF, 0xFF, 0xFF, 0xFF]); + mem_cmp(&nes, 0x24, &[0x69, 0xCF, 0xF8, 0x88]); + mem_cmp(&nes, 0x28, &[0xD3, 0x9E, 0xF1, 0x11]); + mem_cmp(&nes, 0x2C, &[0x52, 0x93, 0x45, 0x3F]); + mem_cmp(&nes, 0x30, &[0xDF, 0x7C, 0x47, 0x12]); + mem_cmp(&nes, 0x34, &[0x49, 0x4C, 0x40, 0x65]); + mem_cmp(&nes, 0x38, &[0x72, 0x10, 0xFD, 0xD2]); +}); + +rom_test!(implied_instructions, "implied_instrs.nes", |nes| { + assert_eq!(nes.last_instruction, "0x8026 HLT :2 []"); + assert_eq!(nes.cpu.a, 0x00); +}); diff --git a/src/test_roms/mod.rs b/src/test_roms/mod.rs index 905d739..23cfe6e 100644 --- a/src/test_roms/mod.rs +++ b/src/test_roms/mod.rs @@ -1,15 +1,18 @@ -use crate::{NES, hex_view::Memory}; +mod cpu_reset_ram; +mod instr_test_v3; + +use crate::hex_view::Memory; macro_rules! rom_test { ($name:ident, $rom:literal, |$nes:ident| $eval:expr) => { - rom_test!($name, $rom, timeout = 10000000, |$nes| $eval); + rom_test!($name, $rom, timeout = 10000000, |$nes| $eval); }; ($name:ident, $rom:literal, timeout = $timeout:expr, |$nes:ident| $eval:expr) => { #[test] fn $name() { let rom_file = concat!(env!("ROM_DIR"), "/", $rom); println!("{}: {}", stringify!($name), rom_file); - let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object"); + let mut $nes = crate::NES::load_nes_file(rom_file).expect("Failed to create nes object"); $nes.reset_and_run_with_timeout($timeout); println!("Final: {:?}", $nes); $eval @@ -23,7 +26,7 @@ macro_rules! rom_test { fn $name() { let rom_file = $rom; println!("{}: {}", stringify!($name), rom_file); - let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object"); + let mut $nes = crate::NES::load_nes_file(rom_file).expect("Failed to create nes object"); $nes.reset_and_run_with_timeout($timeout); println!("Final: {:?}", $nes); $eval @@ -31,6 +34,8 @@ macro_rules! rom_test { }; } +pub(crate) use rom_test; + rom_test!(basic_cpu, "basic-cpu.nes", |nes| { assert_eq!(nes.last_instruction, "0x8001 HLT :2 []"); // Off by one from Mesen, since Mesen doesn't count the clock cycle attempting to execute the 'invalid' opcode diff --git a/src/test_roms/nes.cfg b/src/test_roms/nes.cfg new file mode 100644 index 0000000..f75935c --- /dev/null +++ b/src/test_roms/nes.cfg @@ -0,0 +1,36 @@ +# 32K iNES ROM with optional 8K CHR + +MEMORY +{ + ZP: start = $10, size = $E0; # leave $10 free at each end + RAM: start = $200, size = $500; + + HEADER: start = 0, size = $10, fill=yes; + + ROM: start = $8000, size = $7E00, fill=yes, fillval=$FF; + LOADER: start = $FE00, size = $100, fill=yes, fillval=$FF; + FF00: start = $FF00, size = $F4, fill=yes, fillval=$FF; + VECTORS:start = $FFF4, size = $C, fill=yes; + + CHARS: start = 0, size = $3000, fillval=$FF; +} + +SEGMENTS +{ + ZEROPAGE: load = ZP, type = zp; + BSS: load = RAM, type = bss,align=$100; + NVRAM: load = RAM, type = bss,define=yes, optional=yes; + + HEADER: load = HEADER, type = ro; + CODE: load = ROM, type = ro, align=$100; + CODE2: load = ROM, type = ro, align=$100, optional=yes; + RODATA: load = ROM, type = ro, align=$100; + CHARS_PRG: load = ROM, type = ro, align=$200, optional=yes; + CHARS_PRG_ASCII:load = ROM, type = ro, align=$200, optional=yes; + LOADER: load = LOADER, type = ro, optional=yes; + FF00: load = FF00, type = ro, align=$100, optional=yes; + VECTORS: load = VECTORS, type = ro; + + CHARS: load = CHARS, type = ro, align=$100, optional=yes; + CHARS_ASCII:load = CHARS, type = ro, align=$200, optional=yes; +} diff --git a/test-rom/basic-cpu.asm b/test-rom/basic-cpu.asm index 80655d0..c72eea5 100644 --- a/test-rom/basic-cpu.asm +++ b/test-rom/basic-cpu.asm @@ -1,9 +1,9 @@ ; FINAL = "" -.inesprg 2 ; 2 banks -.ineschr 1 ; -.inesmap 0 ; mapper 0 = NROM -.inesmir 0 ; background mirroring, horizontal +; .inesprg 2 ; 2 banks +; .ineschr 1 ; +; .inesmap 0 ; mapper 0 = NROM +; .inesmir 0 ; background mirroring, horizontal .org $8000 RESET: