Add new testcases with cc65 support
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s

This commit is contained in:
2025-12-22 02:13:10 -06:00
parent c8d441297e
commit c535e4e76d
30 changed files with 3057 additions and 61 deletions

View File

@@ -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"))
);
}
}
}

View File

@@ -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!(),
}
}

View File

@@ -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
);
}),

View File

@@ -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;

Binary file not shown.

View File

@@ -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

View File

@@ -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

118
src/test_roms/common/crc.s Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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 <delay_temp_
.elseif n = 4
nop
nop
.elseif n = 5
sta <delay_temp_
nop
.elseif n = 6
nop
nop
nop
.elseif n = 7
php
plp
.elseif n = 8
nop
nop
nop
nop
.elseif n = 9
php
plp
nop
.elseif n = 10
sta <delay_temp_
php
plp
.elseif n = 11
php
plp
nop
nop
.elseif n = 13
php
plp
nop
nop
nop
.elseif n & 1
sta <delay_temp_
jsr delay_unrolled_-((n-15)/2)
.else
jsr delay_unrolled_-((n-12)/2)
.endif
.endmacro
.macro delay_nosave_ n
; 65536+17 = maximum delay using delay_256a_11_clocks_
; 255+27 = maximum delay using delay_a_25_clocks
; 27 = minimum delay using delay_a_25_clocks
.if n > 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

Binary file not shown.

View File

@@ -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: <y, x, a, p>
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

View File

@@ -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

View File

@@ -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
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

View File

@@ -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

142
src/test_roms/common/ppu.s Normal file
View File

@@ -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,<w
.endmacro
; Delays by no more than n scanlines
.macro delay_scanlines n
.if CLOCK_RATE <> 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) - <Neg_size
sty addr
stx addr+1
ldxy #Neg_size
: sta (addr),y
iny
bne :-
inc addr+1
inx
bne :-
.endmacro
; Clears 0 through ($100+S), $200 through __NVRAM_LOAD__-1, and
; __NVRAM_LOAD__+__NVRAM_SIZE__ through $7FF
clear_ram:
lda #0
bss_begin = $200
fill_ram_ bss_begin,__NVRAM_LOAD__
fill_ram_ __NVRAM_LOAD__+__NVRAM_SIZE__,$800
; Zero-page
tax
: sta 0,x
inx
bne :-
; Stack below S
tsx
inx
: dex
sta $100,x
bne :-
rts
nv_res unused_nv_var ; to avoid size=0
; Clears nvram
clear_nvram:
lda #0
fill_ram_ __NVRAM_LOAD__,__NVRAM_LOAD__+__NVRAM_SIZE__
rts
; Prints filename and newline, if available, otherwise nothing.
; Preserved: A, X, Y
print_filename:
.ifdef FILENAME_KNOWN
pha
jsr print_newline
setw addr,filename
jsr print_str_addr
jsr print_newline
pla
.endif
rts
.pushseg
.segment "RODATA"
; Filename terminated with zero byte.
filename:
.ifdef FILENAME_KNOWN
.incbin "ram:nes_temp"
.endif
.byte 0
.popseg
;**** ROM-specific ****
.ifndef BUILD_NSF
.include "ppu.s"
avoid_silent_nsf:
play_byte:
rts
; Loads ASCII font into CHR RAM
.macro load_ascii_chr
bit PPUSTATUS
setb PPUADDR,$00
setb PPUADDR,$00
setb addr,<ascii_chr
ldx #>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

View File

@@ -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

View File

@@ -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,<text_out_base
rts
; Sets final result byte in memory
set_final_result:
sta final_result
rts
; Writes character to text output
; In: A=Character to write
; Preserved: A, X, Y
write_text_out:
sty text_out_temp
; Write new terminator FIRST, then new char before it,
; in case emulator looks at string in middle of this routine.
ldy #1
pha
lda #0
sta (text_out_addr),y
dey
pla
sta (text_out_addr),y
inc text_out_addr
bne :+
inc text_out_addr+1
:
ldy text_out_temp
rts

View File

@@ -0,0 +1,17 @@
use super::rom_test;
// TODO: need to implement soft reset behavior
// rom_test!(basic_cpu, "cpu_reset_ram.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
// assert_eq!(nes.cycle, 11);
// // This is off by one from Mesen, because Mesen is left pointing at the 'invalid' opcode
// assert_eq!(nes.cpu.pc, 0x8002);
// // Off by one from Mesen, since Mesen doesn't count the clock cycle attempting to execute the 'invalid' opcode
// assert_eq!(nes.ppu.pixel, 35);
// assert_eq!(nes.cpu.sp, 0xFD);
// // assert_eq!(nes.cpu.a, 0x00);
// // assert_eq!(nes.cpu.x, 0x00);
// // assert_eq!(nes.cpu.y, 0x00);
// });

View File

@@ -0,0 +1,84 @@
; Verifies that reset doesn't alter any RAM.
CUSTOM_RESET=1
.include "shell.inc"
.include "run_at_reset.s"
nv_res bad_addr,2
reset: sei
cld
ldx #$FF
txs
ldx #7
ldy #0
; Check first byte, and assume just powered
; if not as expected
lda <0
cmp #$DB
jne std_reset
iny
; Check second byte
lda <1
cmp #$B6
bne failed
iny
; Rest of internal memory
setb <0,0
setb <1,0
lda #$6D
clc
: eor (<0),y
bne failed
lda (<0),y
rol a
iny
bne :-
inc <1
dex
bpl :-
jmp tests_passed
failed:
sty bad_addr
txa
eor #$07
sta bad_addr+1
jsr init_shell
print_str "Addr: "
lda bad_addr+1
jsr print_hex
lda bad_addr
jsr print_a
set_test 3,"Reset shouldn't modify RAM"
; stp
main:
jsr prompt_to_reset
; Fill RAM with pattern
setb <0,0
setb <1,0
ldx #8
ldy #2
lda #$6D
clc
: sta (<0),y
rol a
iny
bne :-
inc <1
dex
bne :-
setb <0,$DB
setb <1,$B6
jmp wait_reset

63
src/test_roms/crc_check.s Normal file
View File

@@ -0,0 +1,63 @@
.include "shell.inc"
.include "crc_fast.s"
run_crc:
pha
jsr reset_crc
pla
jsr update_crc_fast
ldy #0
lda checksum
sta ($0),y
iny
lda checksum+1
sta ($0),y
iny
lda checksum+2
sta ($0),y
iny
lda checksum+3
sta ($0),y
rts
.macro crc_check crc_val,val
lda #.lobyte(crc_val)
sta $0
lda #.hibyte(crc_val)
sta $1
lda #val
jsr run_crc
.endmacro
main:
crc_check crc_1,$00
crc_check crc_2,$01
crc_check crc_3,$02
crc_check crc_4,$7F
crc_check crc_5,$80
crc_check crc_6,$81
crc_check crc_7,$FF
lda #$00
jmp exit
.org $20
crc_1: .res 4
crc_2: .res 4
crc_3: .res 4
crc_4: .res 4
crc_5: .res 4
crc_6: .res 4
crc_7: .res 4
crc_8: .res 4
crc_9: .res 4
crc_10: .res 4
crc_11: .res 4
crc_12: .res 4
crc_13: .res 4
crc_14: .res 4
crc_15: .res 4
crc_16: .res 4
crc_17: .res 4
crc_18: .res 4
crc_19: .res 4
crc_20: .res 4

View File

@@ -0,0 +1,130 @@
OPERANDS = 0
.include "shell.inc"
; .include "instr_test.inc"
; .include "macros.inc"
values:
.byte 0,1,2,$40,$7F,$80,$81,$FF
values_size = * - values
zp_res in_a,2
zp_res in_x,2
zp_res in_y,2
zp_res in_p,2
zp_res in_s,2
zp_res out_a
zp_res out_x
zp_res out_y
zp_res out_p
zp_res out_s
.macro store_ptr zp_ptr, value
lda #.lobyte(value)
sta zp_ptr
lda #.hibyte(value)
sta zp_ptr+1
.endmacro
testcase_init:
store_ptr in_a, values
store_ptr in_x, values
store_ptr in_y, values
store_ptr in_p, values
store_ptr in_s, values
rts
; Calling convention: Will trash stack - anything that needs to be saved must be elsewhere
; Will call `test_code` with the requisite
zp_res return_addr
run_tc:
lda (in_s,x)
tax
txs ; As long as the following code has the same number of pushes & pulls, stack ptr will be correct
ldx #0
lda (in_p,x)
pha
lda (in_a,x)
pha
lda (in_x,x)
pha
lda (in_y,x)
pha
; Stack <y, x, a, p>
pla
tay
; Stack <x, a, p>, y correct
pla
tax
; Stack <a, p>, C: x, y
pla
; Stack <p>, 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

View File

@@ -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<u8> {
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);
});

View File

@@ -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

36
src/test_roms/nes.cfg Normal file
View File

@@ -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;
}

View File

@@ -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: