Implement state saving and loading for CPU and Bus, add tests for various functionalities

- Added `state.c` to handle saving and loading the state of the CPU and Bus.
- Implemented CRC32 checksum for data integrity verification during state operations.
- Enhanced `README.md` in tests/roms to document new test commands for decimal, interrupt, Apple I, and nestest.
- Created new test files: `run_apple1.c`, `run_decimal.c`, `run_interrupt.c`, `run_nestest.c`, and `run_vectors.c` to validate the new features.
- Updated `run_tests.c` to include tests for state saving/loading and other functionalities.
- Introduced `test_vectors.py` to facilitate downloading and running JSON test vectors for opcode verification.
This commit is contained in:
2026-07-20 13:55:48 +03:00
parent c6e1c5b3b5
commit 0dcd2ed687
29 changed files with 827 additions and 58 deletions
+7 -2
View File
@@ -6,8 +6,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update
- run: sudo apt-get install -y cc65 libsdl2-dev libsdl2-ttf-dev fonts-dejavu-core
- run: sudo apt-get install -y cc65 libsdl2-dev libsdl2-ttf-dev fonts-dejavu-core libc6-i386 unzip
- run: make debug
- run: make sanitize
- run: make test-functional
- run: make games/snake.bin bin/mos6502-sdl
- run: make test-decimal
- run: make test-interrupt
- run: make test-apple1
- run: make test-nestest
- run: make test-vectors
- run: make games/snake.bin bin/mos6502-sdl bin/nes
+3
View File
@@ -80,5 +80,8 @@ games/snake.bin
snake.highscore
snake.state
tests/roms/*.bin
tests/roms/*.nes
tests/roms/*.log
tests/vectors/
# End of https://www.toptal.com/developers/gitignore/api/c,visualstudiocode
+2
View File
@@ -16,6 +16,8 @@ make snake # assemble and run the SDL game (requires SDL2 + cc65)
- `Bus6502` owns RAM and Easy6502 MMIO; CPU code only uses bus accessors.
- `cpu_step` executes one instruction or interrupt and returns status/opcode/cycles.
- `src/opcode.c` has a 256-entry descriptor table for all 151 official encodings.
- `src/state.c` owns the portable, versioned state file format.
- `src/apple1.c` maps Apple I keyboard/display PIA registers through bus hooks.
- `src/frontend.c` is an optional SDL2 frontend with a dependency-free stub.
- `tests/run_tests.c` is the in-tree test harness; `tests/run_suite.c` runs external ROMs.
+83 -4
View File
@@ -8,15 +8,48 @@ CPPFLAGS += -DHAVE_SDL_UI $(SDL_CFLAGS)
LDLIBS += $(SDL_LIBS)
endif
CORE_SRC = src/addressing.c src/bus.c src/cpu.c src/loader.c src/memory.c src/opcode.c
CORE_SRC = src/addressing.c src/apple1.c src/bus.c src/cpu.c src/loader.c src/memory.c src/nes.c src/opcode.c src/state.c
APP_SRC = $(CORE_SRC) src/frontend.c src/main.c
APP_OBJ = $(APP_SRC:src/%.c=build/%.o)
TARGET = bin/mos6502
SDL_TARGET = bin/mos6502-sdl
.PHONY: all clean test test-rom test-functional fetch-roms debug sanitize snake check-deps FORCE
.PHONY: all clean test test-rom test-functional test-decimal test-interrupt test-apple1 test-nestest test-vectors fetch-roms debug sanitize snake check-deps FORCE
all: $(TARGET)
bin/apple1: $(CORE_SRC) src/apple1_main.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) src/apple1_main.c -o $@
bin/debugger: $(CORE_SRC) src/debugger_main.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) src/debugger_main.c -o $@
bin/nes: $(CORE_SRC) src/nes_main.c
@mkdir -p bin
$(CC) -Iinclude $(CFLAGS) $(SDL_CFLAGS) $(CORE_SRC) src/nes_main.c $(SDL_LIBS) -o $@
bin/run_apple1: $(CORE_SRC) tests/run_apple1.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_apple1.c -o $@
test-apple1: tests/roms/wozmon.bin bin/run_apple1
./bin/run_apple1 $<
bin/run_nestest: $(CORE_SRC) tests/run_nestest.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_nestest.c -o $@
test-nestest: tests/roms/nestest.nes tests/roms/nestest.log bin/run_nestest
./bin/run_nestest tests/roms/nestest.nes tests/roms/nestest.log
bin/run_vectors: $(CORE_SRC) tests/run_vectors.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_vectors.c -o $@
test-vectors: bin/run_vectors
python3 tools/test_vectors.py --download
$(TARGET): $(APP_OBJ)
@mkdir -p bin
$(CC) $(CFLAGS) $(APP_OBJ) $(LDLIBS) -o $@
@@ -51,13 +84,59 @@ tests/roms/6502_functional_test.bin:
curl -L --fail -o $@ https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/bin_files/6502_functional_test.bin
@echo "fa12bfc761e6f9057e4cc01a665a7b800ff01ae91f598af1e39a1201d01953fd $@" | sha256sum -c -
fetch-roms: tests/roms/6502_functional_test.bin
tests/roms/6502_decimal_test.bin:
@mkdir -p tests/roms
curl -L --fail -o $@ https://raw.githubusercontent.com/JetSetIlly/Gopher2600/master/hardware/cpu/tests/klaus2m5/decimal_mode/6502_decimal_test.bin
@echo "03798ab778456cc350044fdbe28b4078278648892712b994cdbdda09018674e7 $@" | sha256sum -c -
tests/roms/6502_interrupt_test.bin:
@mkdir -p tests/roms build/as65
curl -L --fail -o build/as65/as65.zip https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/as65_142.zip
unzip -oq build/as65/as65.zip -d build/as65
curl -L --fail -o build/as65/6502_interrupt_test.a65 https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/6502_interrupt_test.a65
chmod +x build/as65/as65
cd build/as65 && ./as65 -pmnu 6502_interrupt_test.a65
cp build/as65/6502_interrupt_test.bin $@
@echo "2657b5ce08bb59e2de69363c9174c4d0f7f475bcf2ce844eb20cccfbe4374c72 $@" | sha256sum -c -
tests/roms/wozmon.bin:
@mkdir -p tests/roms build/wozmon
curl -L --fail -o build/wozmon/wozmon.s https://raw.githubusercontent.com/jefftranter/6502/master/asm/wozmon/wozmon.s
ca65 build/wozmon/wozmon.s -o build/wozmon/wozmon.o
ld65 -C /usr/share/cc65/cfg/none.cfg -o $@ build/wozmon/wozmon.o
@echo "e5af0d1c4057bd8e0ef5cb069c208ff7cc0984a7dff53b12c5cf119de8cb5c25 $@" | sha256sum -c -
tests/roms/nestest.nes:
@mkdir -p tests/roms
curl -L --fail -o $@ http://www.qmtpro.com/~nes/misc/nestest.nes
@echo "f67d55fd6b3cf0bad1cc85f1df0d739c65b53e79cecb7fea8f77ec0eadab0004 $@" | sha256sum -c -
tests/roms/nestest.log:
@mkdir -p tests/roms
curl -L --fail -o $@ http://www.qmtpro.com/~nes/misc/nestest.log
@echo "627c8e180b1a924dfa705c5dc6958fad7ab75a62de556173caf880ccc1337540 $@" | sha256sum -c -
fetch-roms: tests/roms/6502_functional_test.bin tests/roms/6502_decimal_test.bin tests/roms/6502_interrupt_test.bin tests/roms/wozmon.bin tests/roms/nestest.nes tests/roms/nestest.log
test-functional: tests/roms/6502_functional_test.bin bin/run_suite
./bin/run_suite $< 0 0x400 0x3469 100000000
bin/run_decimal: $(CORE_SRC) tests/run_decimal.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_decimal.c -o $@
test-decimal: tests/roms/6502_decimal_test.bin bin/run_decimal
./bin/run_decimal $<
bin/run_interrupt: $(CORE_SRC) tests/run_interrupt.c
@mkdir -p bin
$(CC) $(CPPFLAGS) $(CFLAGS) $(CORE_SRC) tests/run_interrupt.c -o $@
test-interrupt: tests/roms/6502_interrupt_test.bin bin/run_interrupt
./bin/run_interrupt $<
debug: CFLAGS = -O0 -g3 -std=c11 -Wall -Wextra -Wpedantic -Werror
debug: clean all test
debug: clean all bin/debugger bin/apple1 bin/nes test
sanitize: CFLAGS = -O1 -g3 -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address,undefined
sanitize: export ASAN_OPTIONS = detect_leaks=0
+43 -4
View File
@@ -14,6 +14,9 @@ an explicit error.
make # core and command-line emulator
make test # unit/integration checks
make test-functional # official Klaus Dormann functional suite
make test-decimal # exhaustive NMOS BCD accumulator/flag suite
make test-interrupt # IRQ/NMI/BRK integration suite
make test-vectors # 2.36M independent single-instruction vectors (large download)
make debug # strict -Werror debug build and tests
make sanitize # AddressSanitizer/UBSan build and tests
```
@@ -49,6 +52,14 @@ bin/mos6502 program.bin --load 0x0600 --start 0x0600
Useful options are `--hz`, `--scale`, `--seed`, `--headless`, and `--trace`.
An interactive debugger provides stepping, continue, breakpoints, registers,
memory examination/modification, disassembly, and save/load commands:
```sh
make bin/debugger
bin/debugger program.bin 0x0600 0x0600
```
External functional ROMs can be run without modifying the application:
```sh
@@ -64,11 +75,39 @@ This is deliberately a small game-oriented computer, not an NES emulator.
Original arcade Pac-Man used a Z80; running that ROM would require a different
CPU and hardware platform.
## Apple I
The Apple I terminal target implements the keyboard/display PIA registers at
`$D010-$D013`. Supply a legally obtained 256-byte Woz Monitor image and press
Ctrl-C to exit:
```sh
make bin/apple1
bin/apple1 WOZMON.bin
```
## Nintendo Entertainment System
The NES target implements the Ricoh 2A03 CPU behavior, iNES parsing, mappers 0
(NROM), 2 (UxROM), and 3 (CNROM), mirrored CPU RAM and PPU registers, controller shift register, OAM DMA,
vblank/NMI timing, background scrolling/attributes, sprites, palette rendering,
the pulse/triangle/noise APU channels, and an SDL video/audio frontend:
```sh
make bin/nes
bin/nes game.nes
```
Controls are arrows, Z (B), X (A), Right Shift (Select), Enter (Start), and
Escape. Use legally obtained ROMs. `make test-nestest` verifies all 8,991
canonical CPU trace lines and cumulative cycle counts.
See [the design](docs/design.md) and [project status](docs/plan.md).
## Validation status
The in-tree tests exercise the CPU's major correctness boundaries. External
Klaus Dormann functional and decimal ROM validation is the next hardening gate;
the binaries are not vendored because their redistribution terms must be
respected.
The automated suite passes Klaus Dormann's functional and interrupt ROMs,
Bruce Clark's exhaustive decimal test, all 8,991 canonical `nestest` trace
lines, an authentic Woz Monitor interaction, and 2,360,000 SingleStepTests
vectors. Downloaded fixtures are checksum-verified or cached locally rather
than vendored.
+21 -11
View File
@@ -7,23 +7,33 @@
- Binary and NMOS decimal ADC/SBC, status flags, stack, BRK/RTI, IRQ and NMI.
- All official addressing modes, zero-page wrap, page penalties and indirect-JMP bug.
- Instruction-level cycle accounting and deterministic illegal-opcode errors.
- Stable undocumented NMOS opcode families and multi-byte NOP encodings.
- Safe flat-binary loader, headless runner, optional SDL2 frontend.
- Versioned, endian-defined, CRC32-protected portable save states.
- Extensible bus device hooks and an Apple I terminal/PIA target.
- NES Ricoh 2A03 mode, iNES mappers 0/2/3, PPU register/timing model, controller,
scrolling, masks, 8x8/8x16 sprites, sprite status, pulse/triangle/noise audio,
SDL frontend, and full 8,991-line nestest match.
- Original ca65 Snake game with pause, persistent high score, and a reproducible `make snake` target.
- Strict debug, sanitizer and dependency-free test targets.
## Remaining hardening
## Validation
1. Run the Klaus Dormann decimal suite and fix any discovered edge cases.
2. Expand table-driven tests to cover every official opcode/address-mode pairing.
3. Add a trace/disassembler runner for diagnosing external ROM failures.
- 2,360,000 independent SingleStepTests cases cover every implemented official
and stable undocumented opcode, final CPU/RAM state, and instruction cycles.
- The interactive debugger supports stepping, continue, breakpoints, register and
memory inspection/editing, disassembly, and portable save/load.
The official Klaus Dormann functional suite passes at `$3469` after 30,646,176
instructions. Linux CI covers strict, sanitizer, functional-ROM, Snake, and SDL
UI builds.
instructions. Linux CI covers strict, sanitizer, all external CPU/platform suites,
the complete single-step corpus, Snake, and SDL UI builds.
## Future platforms
Bruce Clark's exhaustive NMOS decimal suite passes all 131,072 operand/carry
combinations, including invalid BCD digits. Klaus Dormann's interrupt suite
passes IRQ, NMI, BRK, vector, flag, and concurrent-interrupt checks.
- Undocumented NMOS opcodes.
- Audio, save states and an interactive debugger.
- A real historical machine such as Apple I or NES. NES support is a separate major
project requiring the PPU, APU, controllers, cartridge parsing/mappers and tighter timing.
## Explicit compatibility boundary
The NES frontend is a useful first-generation NES subsystem, not a claim of full
commercial-library compatibility. Mappers other than 0, 2, and 3, DMC audio,
cycle-by-cycle PPU bus effects, and mapper IRQs remain outside its stated scope.
+14
View File
@@ -0,0 +1,14 @@
#ifndef APPLE1_H
#define APPLE1_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "bus.h"
typedef struct {
uint8_t key;bool key_ready;bool stdout_enabled;
char output[8192];size_t output_length;
} Apple1IO;
void apple1_init(Apple1IO *io,Bus6502 *bus);
void apple1_key(Apple1IO *io,uint8_t ascii);
void apple1_enable_stdout(Apple1IO *io,bool enabled);
#endif
+11 -2
View File
@@ -5,13 +5,20 @@
#include <stdint.h>
#include "memory.h"
typedef struct Bus6502 {
typedef struct Bus6502 Bus6502;
typedef bool (*bus_read_hook_t)(void *userdata,uint16_t addr,uint8_t *value);
typedef bool (*bus_write_hook_t)(void *userdata,uint16_t addr,uint8_t value);
struct Bus6502 {
Memory memory;
uint8_t keyboard;
uint32_t rng_state;
bool easy6502_io;
bool framebuffer_dirty;
} Bus6502;
bus_read_hook_t read_hook;
bus_write_hook_t write_hook;
void *hook_userdata;
};
void bus_init(Bus6502 *bus);
uint8_t bus_read(Bus6502 *bus, uint16_t addr);
@@ -20,5 +27,7 @@ uint16_t bus_read16(Bus6502 *bus, uint16_t addr);
void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value);
void bus_set_keyboard(Bus6502 *bus, uint8_t key);
void bus_seed_rng(Bus6502 *bus, uint32_t seed);
void bus_set_hooks(Bus6502 *bus,bus_read_hook_t read_hook,
bus_write_hook_t write_hook,void *userdata);
#endif
+1
View File
@@ -13,6 +13,7 @@ typedef struct CPU6502 {
uint8_t P;
bool nmi_pending;
bool irq_asserted;
bool decimal_enabled;
} CPU6502;
enum {
+28
View File
@@ -0,0 +1,28 @@
#ifndef NES_H
#define NES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "bus.h"
#include "cpu.h"
typedef struct {
uint8_t prg[512*1024],chr[256*1024],ram[2048],prg_ram[8192];
uint8_t nametable[4096],palette[32],oam[256];
size_t prg_size,chr_size;uint8_t mapper,prg_bank,chr_bank,mirroring;bool chr_ram;
uint8_t ppu_ctrl,ppu_mask,ppu_status,oam_addr,ppu_latch,read_buffer;
uint8_t scroll_x,scroll_y;
uint16_t ppu_addr;unsigned ppu_dot,ppu_scanline;bool frame_ready;
uint8_t controller,controller_shift;bool controller_strobe;
uint8_t apu_reg[0x18],apu_status;uint64_t apu_cycles;
double pulse_phase[2],triangle_phase,noise_phase;uint16_t noise_lfsr;
Bus6502 bus;CPU6502 cpu;
} NES;
int nes_load(NES *nes,const char *path);
void nes_reset(NES *nes);
cpu_step_result_t nes_step(NES *nes);
void nes_set_controller(NES *nes,uint8_t buttons);
void nes_render_frame(NES *nes,uint32_t pixels[256*240]);
float nes_audio_sample(NES *nes,double sample_rate);
#endif
+7
View File
@@ -0,0 +1,7 @@
#ifndef STATE_H
#define STATE_H
#include "bus.h"
#include "cpu.h"
int state_save(const char *path,const CPU6502 *cpu,const Bus6502 *bus);
int state_load(const char *path,CPU6502 *cpu,Bus6502 *bus);
#endif
+31
View File
@@ -0,0 +1,31 @@
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include "apple1.h"
static bool apple1_read(void *userdata,uint16_t addr,uint8_t *value)
{
Apple1IO *io=userdata;
if(addr==0xD010){*value=io->key|0x80;io->key_ready=false;return true;}
if(addr==0xD011){*value=io->key_ready?0x80:0;return true;}
if(addr==0xD012||addr==0xD013){*value=0;return true;}
return false;
}
static bool apple1_write(void *userdata,uint16_t addr,uint8_t value)
{
Apple1IO *io=userdata;
if(addr==0xD012){
uint8_t c=value&0x7F;if(c=='\r')c='\n';
if(io->output_length+1<sizeof io->output){io->output[io->output_length++]=(char)c;io->output[io->output_length]=0;}
if(io->stdout_enabled){putchar(c);fflush(stdout);}return true;
}
if(addr==0xD010||addr==0xD011||addr==0xD013)return true;
return false;
}
void apple1_init(Apple1IO *io,Bus6502 *bus)
{ memset(io,0,sizeof(*io));bus_set_hooks(bus,apple1_read,apple1_write,io); }
void apple1_key(Apple1IO *io,uint8_t ascii)
{ io->key=(uint8_t)toupper(ascii);io->key_ready=true; }
void apple1_enable_stdout(Apple1IO *io,bool enabled){io->stdout_enabled=enabled;}
+24
View File
@@ -0,0 +1,24 @@
#define _DEFAULT_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include "apple1.h"
#include "cpu.h"
#include "loader.h"
int main(int argc,char **argv)
{
Bus6502 bus;CPU6502 cpu;Apple1IO io;size_t loaded;struct termios old,raw;int tty=0;
if(argc!=2){fprintf(stderr,"usage: %s WOZMON.bin\n",argv[0]);return 2;}
bus_init(&bus);apple1_init(&io,&bus);apple1_enable_stdout(&io,true);
if(load_binary(&bus,argv[1],0xFF00,&loaded)||loaded!=256){fprintf(stderr,"Apple I monitor ROM must be exactly 256 bytes\n");return 1;}
cpu_reset(&cpu,&bus);
if(tcgetattr(STDIN_FILENO,&old)==0){tty=1;raw=old;cfmakeraw(&raw);tcsetattr(STDIN_FILENO,TCSANOW,&raw);}
fcntl(STDIN_FILENO,F_SETFL,fcntl(STDIN_FILENO,F_GETFL)|O_NONBLOCK);
for(;;){
unsigned char key;if(read(STDIN_FILENO,&key,1)==1){if(key==3)break;apple1_key(&io,key);}
for(unsigned n=0;n<1000;n++)if(cpu_step(&cpu,&bus).status!=CPU_STEP_OK)goto done;
}
done:if(tty)tcsetattr(STDIN_FILENO,TCSANOW,&old);putchar('\n');return 0;
}
+5
View File
@@ -17,6 +17,8 @@ static uint8_t random_byte(Bus6502 *bus)
uint8_t bus_read(Bus6502 *bus, uint16_t addr)
{
uint8_t value;
if(bus->read_hook&&bus->read_hook(bus->hook_userdata,addr,&value))return value;
if (bus->easy6502_io) {
if (addr == 0x00FE) return random_byte(bus);
if (addr == 0x00FF) return bus->keyboard;
@@ -26,6 +28,7 @@ uint8_t bus_read(Bus6502 *bus, uint16_t addr)
void bus_write(Bus6502 *bus, uint16_t addr, uint8_t value)
{
if(bus->write_hook&&bus->write_hook(bus->hook_userdata,addr,value))return;
mem_write(&bus->memory, addr, value);
if (addr >= 0x0200 && addr <= 0x05FF) bus->framebuffer_dirty = true;
}
@@ -45,3 +48,5 @@ void bus_write16(Bus6502 *bus, uint16_t addr, uint16_t value)
void bus_set_keyboard(Bus6502 *bus, uint8_t key) { bus->keyboard = key; }
void bus_seed_rng(Bus6502 *bus, uint32_t seed) { bus->rng_state = seed ? seed : 1; }
void bus_set_hooks(Bus6502 *bus,bus_read_hook_t read_hook,bus_write_hook_t write_hook,void *userdata)
{ bus->read_hook=read_hook;bus->write_hook=write_hook;bus->hook_userdata=userdata; }
+1
View File
@@ -8,6 +8,7 @@ void cpu_reset(CPU6502 *cpu, Bus6502 *bus)
memset(cpu, 0, sizeof(*cpu));
cpu->SP = 0xFD;
cpu->P = FLAG_U | FLAG_I;
cpu->decimal_enabled = true;
cpu->PC = bus_read16(bus, 0xFFFC);
}
+42
View File
@@ -0,0 +1,42 @@
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "bus.h"
#include "cpu.h"
#include "loader.h"
#include "opcode.h"
#include "state.h"
static unsigned long number(const char *s,int *ok)
{ char *end;unsigned long n=strtoul(s,&end,0);while(isspace((unsigned char)*end))end++;*ok=*s&&!*end&&n<=0xFFFF;return n; }
static void show(CPU6502 *c,Bus6502 *b)
{ char text[64];cpu_disassemble(b,c->PC,text,sizeof text);printf("PC:%04X A:%02X X:%02X Y:%02X P:%02X SP:%02X %s\n",c->PC,c->A,c->X,c->Y,c->P,c->SP,text); }
static int one(CPU6502 *c,Bus6502 *b)
{ uint16_t pc=c->PC;cpu_step_result_t r=cpu_step(c,b);if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X at $%04X\n",r.opcode,pc);return 0;}return 1; }
int main(int argc,char **argv)
{
Bus6502 bus;CPU6502 cpu;size_t loaded;uint8_t breaks[65536]={0};char line[256];uint16_t load=0,start=0;
if(argc<2||argc>4){fprintf(stderr,"usage: %s ROM [LOAD [START]]\n",argv[0]);return 2;}
if(argc>2)load=(uint16_t)strtoul(argv[2],NULL,0);
if(argc>3)start=(uint16_t)strtoul(argv[3],NULL,0);else start=load;
bus_init(&bus);if(load_binary(&bus,argv[1],load,&loaded)){fprintf(stderr,"could not load ROM\n");return 1;}
bus_write16(&bus,0xFFFC,start);cpu_reset(&cpu,&bus);printf("Loaded %zu bytes. Commands: s, c, b, r, m, w, save, load, q, h\n",loaded);show(&cpu,&bus);
while(fputs("dbg> ",stdout),fflush(stdout),fgets(line,sizeof line,stdin)){
char *cmd=strtok(line," \t\r\n"),*arg=strtok(NULL," \t\r\n"),*arg2=strtok(NULL," \t\r\n");int ok=0;
if(!cmd)continue;
if(!strcmp(cmd,"q")||!strcmp(cmd,"quit"))break;
else if(!strcmp(cmd,"s")||!strcmp(cmd,"step")){unsigned long n=arg?strtoul(arg,NULL,0):1;while(n--&&one(&cpu,&bus));show(&cpu,&bus);}
else if(!strcmp(cmd,"c")||!strcmp(cmd,"continue")){for(unsigned long n=0;n<100000000&&one(&cpu,&bus);n++)if(breaks[cpu.PC])break;show(&cpu,&bus);}
else if(!strcmp(cmd,"b")||!strcmp(cmd,"break")){if(!arg){puts("break requires an address");continue;}unsigned long a=number(arg,&ok);if(ok){breaks[a]^=1;printf("breakpoint $%04lX %s\n",a,breaks[a]?"set":"cleared");}}
else if(!strcmp(cmd,"r")||!strcmp(cmd,"registers"))show(&cpu,&bus);
else if(!strcmp(cmd,"m")||!strcmp(cmd,"memory")){unsigned long a=arg?number(arg,&ok):cpu.PC,n=arg2?strtoul(arg2,NULL,0):64;if(!arg)ok=1;if(ok)for(unsigned long i=0;i<n;i++){if(!(i%16))printf("\n%04lX: ",(a+i)&0xFFFF);printf("%02X ",bus_read(&bus,(uint16_t)(a+i)));}putchar('\n');}
else if(!strcmp(cmd,"w")||!strcmp(cmd,"write")){unsigned long a=arg?number(arg,&ok):0;int ok2=0;unsigned long v=arg2?number(arg2,&ok2):0;if(ok&&ok2&&v<=0xFF)bus_write(&bus,(uint16_t)a,(uint8_t)v);else puts("write ADDRESS BYTE");}
else if(!strcmp(cmd,"save")){const char *p=arg?arg:"debug.state";printf("%s\n",state_save(p,&cpu,&bus)?"save failed":"state saved");}
else if(!strcmp(cmd,"load")){const char *p=arg?arg:"debug.state";printf("%s\n",state_load(p,&cpu,&bus)?"load failed":"state loaded");show(&cpu,&bus);}
else if(!strcmp(cmd,"d")||!strcmp(cmd,"disassemble")){unsigned long a=arg?number(arg,&ok):cpu.PC,n=arg2?strtoul(arg2,NULL,0):12;if(!arg)ok=1;if(ok)while(n--){char text[64];size_t bytes=cpu_disassemble(&bus,(uint16_t)a,text,sizeof text);printf("%04lX %s\n",a,text);a=(a+bytes)&0xFFFF;}}
else puts("s [N] | c | b ADDR | r | m [ADDR [N]] | w ADDR BYTE | d [ADDR [N]] | save/load [FILE] | q");
}
return 0;
}
+3 -21
View File
@@ -6,6 +6,7 @@
#include <SDL2/SDL_ttf.h>
#include <stdlib.h>
#include <string.h>
#include "state.h"
static const uint32_t palette[16] = {
0x071018FF,0x62F294FF,0xFF5D73FF,0x8FFFAFFF,0x638CFFFF,0xD879FFFF,
@@ -27,25 +28,6 @@ static void save_high_score(uint8_t score)
if(file){fputc(score,file);fclose(file);}
}
static bool save_state(const CPU6502 *cpu, const Bus6502 *bus)
{
FILE *f=fopen("snake.state","wb");const char magic[8]="MOSST01";
if(!f)return false;
bool ok=fwrite(magic,1,8,f)==8&&fwrite(cpu,1,sizeof(*cpu),f)==sizeof(*cpu)&&
fwrite(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE;
return fclose(f)==0&&ok;
}
static bool load_state(CPU6502 *cpu, Bus6502 *bus)
{
FILE *f=fopen("snake.state","rb");char magic[8];CPU6502 saved;
if(!f)return false;
bool ok=fread(magic,1,8,f)==8&&!memcmp(magic,"MOSST01",8)&&
fread(&saved,1,sizeof(saved),f)==sizeof(saved)&&
fread(bus->memory.data,1,MEM_SIZE,f)==MEM_SIZE;
fclose(f);if(ok){*cpu=saved;bus->framebuffer_dirty=true;}return ok;
}
static void play_tone(SDL_AudioDeviceID device, int frequency, int milliseconds)
{
int16_t samples[4410];int count=44100*milliseconds/1000;
@@ -197,10 +179,10 @@ int frontend_run(CPU6502 *cpu, Bus6502 *bus, unsigned hz, unsigned scale)
paused=!paused; budget=0; bus->framebuffer_dirty=true; update_title(window,score,high,paused);
}
else if(!event.key.repeat && k==SDLK_F5) {
if(save_state(cpu,bus)) SDL_SetWindowTitle(window,"MOS 6502 Snake - State saved");
if(state_save("snake.state",cpu,bus)==0) SDL_SetWindowTitle(window,"MOS 6502 Snake - State saved");
}
else if(!event.key.repeat && k==SDLK_F9) {
if(load_state(cpu,bus)){score=bus->memory.data[0x00FD];game_state=bus->memory.data[0x00FB];update_title(window,score,high,paused);}
if(state_load("snake.state",cpu,bus)==0){score=bus->memory.data[0x00FD];game_state=bus->memory.data[0x00FB];update_title(window,score,high,paused);}
}
else if(k==SDLK_w||k==SDLK_UP) bus_set_keyboard(bus,'w');
else if(k==SDLK_a||k==SDLK_LEFT) bus_set_keyboard(bus,'a');
+116
View File
@@ -0,0 +1,116 @@
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "nes.h"
static uint16_t nt_index(const NES *n,uint16_t a)
{ unsigned table=((a-0x2000)&0x0FFF)>>10,offset=a&0x3FF;if(n->mirroring==2)return (uint16_t)(table*0x400+offset);return (uint16_t)(((n->mirroring?table&1:table>>1)<<10)|offset); }
static uint8_t palette_index(uint16_t a)
{ uint8_t p=(uint8_t)(a&0x1F);if((p&0x13)==0x10)p-=0x10;return p; }
static size_t chr_offset(const NES *n,uint16_t a)
{ return n->mapper==3?((size_t)n->chr_bank*8192+(a&0x1FFF))%n->chr_size:(a&0x1FFF)%n->chr_size; }
static uint8_t ppu_read(NES *n,uint16_t a)
{ a&=0x3FFF;if(a<0x2000)return n->chr[chr_offset(n,a)];if(a<0x3F00)return n->nametable[nt_index(n,a)];return n->palette[palette_index(a)]; }
static void ppu_write(NES *n,uint16_t a,uint8_t v)
{ a&=0x3FFF;if(a<0x2000){if(n->chr_ram)n->chr[chr_offset(n,a)]=v;}else if(a<0x3F00)n->nametable[nt_index(n,a)]=v;else n->palette[palette_index(a)]=v; }
static size_t prg_offset(const NES *n,uint16_t a)
{
if(n->mapper==2){size_t banks=n->prg_size/16384,bank=a<0xC000?n->prg_bank%(banks-1):banks-1;return bank*16384+(a&0x3FFF);}
return (a-0x8000)%n->prg_size;
}
static bool nes_read(void *ud,uint16_t a,uint8_t *v)
{
NES *n=ud;
if(a<0x2000){*v=n->ram[a&0x7FF];return true;}
if(a<0x4000){
switch(a&7){
case 2:*v=n->ppu_status;n->ppu_status&=0x7F;n->ppu_latch=0;return true;
case 4:*v=n->oam[n->oam_addr];return true;
case 7:{uint8_t value=ppu_read(n,n->ppu_addr);if((n->ppu_addr&0x3FFF)<0x3F00){*v=n->read_buffer;n->read_buffer=value;}else{*v=value;n->read_buffer=ppu_read(n,n->ppu_addr-0x1000);}n->ppu_addr+=(n->ppu_ctrl&4)?32:1;return true;}
default:*v=0;return true;}
}
if(a==0x4016){*v=n->controller_shift&1;if(!n->controller_strobe)n->controller_shift=(n->controller_shift>>1)|0x80;return true;}
if(a==0x4015){*v=n->apu_status;return true;}
if(a>=0x4000&&a<0x4020){*v=0;return true;}
if(a>=0x6000&&a<0x8000){*v=n->prg_ram[a-0x6000];return true;}
if(a>=0x8000){*v=n->prg[prg_offset(n,a)];return true;}
*v=0;return true;
}
static bool nes_write(void *ud,uint16_t a,uint8_t v)
{
NES *n=ud;
if(a<0x2000){n->ram[a&0x7FF]=v;return true;}
if(a<0x4000){switch(a&7){case 0:n->ppu_ctrl=v;break;case 1:n->ppu_mask=v;break;case 3:n->oam_addr=v;break;case 4:n->oam[n->oam_addr++]=v;break;case 5:if(!n->ppu_latch)n->scroll_x=v;else n->scroll_y=v;n->ppu_latch^=1;break;case 6:if(!n->ppu_latch)n->ppu_addr=(uint16_t)(v&0x3F)<<8;else n->ppu_addr=(n->ppu_addr&0xFF00)|v;n->ppu_latch^=1;break;case 7:ppu_write(n,n->ppu_addr,v);n->ppu_addr+=(n->ppu_ctrl&4)?32:1;break;}return true;}
if(a==0x4014){for(unsigned i=0;i<256;i++){uint8_t x;nes_read(n,(uint16_t)(v*256+i),&x);n->oam[n->oam_addr++]=x;}return true;}
if(a==0x4016){n->controller_strobe=v&1;if(n->controller_strobe)n->controller_shift=n->controller;return true;}
if(a>=0x4000&&a<0x4018){n->apu_reg[a-0x4000]=v;if(a==0x4015)n->apu_status=v&0x1F;return true;}
if(a>=0x6000&&a<0x8000){n->prg_ram[a-0x6000]=v;return true;}
if(a>=0x8000){if(n->mapper==2)n->prg_bank=v;if(n->mapper==3)n->chr_bank=v;return true;}return false;
}
int nes_load(NES *n,const char *path)
{
uint8_t h[16];FILE *f=fopen(path,"rb");size_t prg,chr;unsigned mapper;
if(!f)return errno;
memset(n,0,sizeof(*n));
if(fread(h,1,16,f)!=16||memcmp(h,"NES\x1A",4)){fclose(f);return EINVAL;}
mapper=(h[6]>>4)|(h[7]&0xF0);if((mapper!=0&&mapper!=2&&mapper!=3)||h[4]<1){fclose(f);return ENOTSUP;}
if(h[6]&4)fseek(f,512,SEEK_CUR);
prg=(size_t)h[4]*16384;chr=(size_t)h[5]*8192;if(prg>sizeof n->prg||chr>sizeof n->chr){fclose(f);return EFBIG;}
if(fread(n->prg,1,prg,f)!=prg||(chr&&fread(n->chr,1,chr,f)!=chr)){fclose(f);return EIO;}
if(fgetc(f)!=EOF){fclose(f);return EFBIG;}fclose(f);
n->prg_size=prg;n->chr_size=chr?chr:8192;n->chr_ram=chr==0;n->mapper=(uint8_t)mapper;n->mirroring=(h[6]&8)?2:(h[6]&1);
bus_init(&n->bus);bus_set_hooks(&n->bus,nes_read,nes_write,n);return 0;
}
void nes_reset(NES *n){cpu_reset(&n->cpu,&n->bus);n->cpu.decimal_enabled=false;n->ppu_scanline=n->ppu_dot=0;n->frame_ready=false;n->noise_lfsr=1;}
cpu_step_result_t nes_step(NES *n)
{
cpu_step_result_t r=cpu_step(&n->cpu,&n->bus);unsigned ticks=r.cycles*3;n->apu_cycles+=r.cycles;
while(ticks--){if(++n->ppu_dot>=341){n->ppu_dot=0;if(++n->ppu_scanline>=262){n->ppu_scanline=0;n->frame_ready=true;}}
if(n->ppu_scanline==241&&n->ppu_dot==1){n->ppu_status|=0x80;if(n->ppu_ctrl&0x80)cpu_request_nmi(&n->cpu);}
if(n->ppu_scanline==261&&n->ppu_dot==1)n->ppu_status&=0x1F;}
return r;
}
void nes_set_controller(NES *n,uint8_t buttons){n->controller=buttons;if(n->controller_strobe)n->controller_shift=buttons;}
float nes_audio_sample(NES *n,double rate)
{
static const double cpu_hz=1789773.0;static const uint16_t noise_period[16]={4,8,16,32,64,96,128,160,202,254,380,508,762,1016,2034,4068};
static const uint8_t duty_bits[4]={1,3,15,63};double mix=0.0;
for(unsigned ch=0;ch<2;ch++)if(n->apu_status&(1u<<ch)){uint8_t *r=&n->apu_reg[ch*4];unsigned timer=r[2]|((r[3]&7)<<8);double freq=cpu_hz/(16.0*(timer+1));n->pulse_phase[ch]+=freq/rate;n->pulse_phase[ch]-=(unsigned)n->pulse_phase[ch];unsigned phase=(unsigned)(n->pulse_phase[ch]*8)&7;mix+=((duty_bits[r[0]>>6]>>phase)&1)?(r[0]&15)/30.0:0.0;}
if(n->apu_status&4){uint8_t *r=&n->apu_reg[8];unsigned timer=n->apu_reg[10]|((n->apu_reg[11]&7)<<8);double freq=cpu_hz/(32.0*(timer+1));n->triangle_phase+=freq/rate;n->triangle_phase-=(unsigned)n->triangle_phase;double tri=n->triangle_phase<0.5?n->triangle_phase*4-1:3-n->triangle_phase*4;mix+=tri*(r[0]&0x7F?0.22:0.0);}
if(n->apu_status&8){uint8_t *r=&n->apu_reg[12];double freq=cpu_hz/noise_period[n->apu_reg[14]&15];n->noise_phase+=freq/rate;if(n->noise_phase>=1){unsigned tap=(n->apu_reg[14]&0x80)?6:1;n->noise_lfsr=(uint16_t)((n->noise_lfsr>>1)|(((n->noise_lfsr^(n->noise_lfsr>>tap))&1)<<14));n->noise_phase-=1;}if(!(n->noise_lfsr&1))mix+=(r[0]&15)/45.0;}
if(mix>1)mix=1;
if(mix< -1)mix=-1;
return (float)mix;
}
void nes_render_frame(NES *n,uint32_t px[256*240])
{
static const uint32_t colors[64]={
0x666666FF,0x002A88FF,0x1412A7FF,0x3B00A4FF,0x5C007EFF,0x6E0040FF,0x6C0600FF,0x561D00FF,0x333500FF,0x0B4800FF,0x005200FF,0x004F08FF,0x00404DFF,0,0,0,
0xADADADFF,0x155FD9FF,0x4240FFFF,0x7527FEFF,0xA01ACCFF,0xB71E7BFF,0xB53120FF,0x994E00FF,0x6B6D00FF,0x388700FF,0x0C9300FF,0x008F32FF,0x007C8DFF,0,0,0,
0xFFFFFFFF,0x64B0FFFF,0x9290FFFF,0xC676FFFF,0xF36AFFFF,0xFE6ECCFF,0xFE8170FF,0xEA9E22FF,0xBCBE00FF,0x88D800FF,0x5CE430FF,0x45E082FF,0x48CDDEFF,0x4F4F4FFF,0,0,
0xFFFFFFFF,0xC0DFFFFF,0xD3D2FFFF,0xE8C8FFFF,0xFBC2FFFF,0xFEC4EAFF,0xFECCC5FF,0xF7D8A5FF,0xE4E594FF,0xCFEF96FF,0xBDF4ABFF,0xB3F3CCFF,0xB5EBF2FF,0xB8B8B8FF,0,0};
uint8_t opaque[256*240]={0};uint16_t bg_pattern=(n->ppu_ctrl&0x10)?0x1000:0;n->ppu_status&=0x1F;
for(unsigned y=0;y<240;y++)for(unsigned x=0;x<256;x++){
unsigned wx=x+n->scroll_x+((n->ppu_ctrl&1)?256:0),wy=y+n->scroll_y+((n->ppu_ctrl&2)?240:0);
unsigned tx=(wx/8)%64,ty=(wy/8)%60,nt=(tx/32)+(ty/30)*2,lx=tx%32,ly=ty%30;
uint16_t base=(uint16_t)(0x2000+nt*0x400);uint8_t tile=ppu_read(n,(uint16_t)(base+ly*32+lx));
uint8_t attr=ppu_read(n,(uint16_t)(base+0x3C0+(ly/4)*8+lx/4));unsigned shift=((ly&2)<<1)|(lx&2);
uint16_t p=bg_pattern+tile*16+(wy&7);uint8_t bit=(uint8_t)(7-(wx&7));uint8_t index=(uint8_t)(((ppu_read(n,p)>>bit)&1)|(((ppu_read(n,p+8)>>bit)&1)<<1));
if(!(n->ppu_mask&8)||(x<8&&!(n->ppu_mask&2)))index=0;
unsigned pal=((attr>>shift)&3)*4+index;opaque[y*256+x]=index;px[y*256+x]=colors[n->palette[index?pal:0]&0x3F];
}
if(n->ppu_mask&0x10){unsigned height=(n->ppu_ctrl&0x20)?16:8;
for(unsigned y=0;y<240;y++){unsigned count=0;for(unsigned s=0;s<64;s++){int sy=n->oam[s*4]+1;if((int)y>=sy&&(int)y<sy+(int)height&&++count>8)n->ppu_status|=0x20;}}
for(int s=63;s>=0;s--){int sy=n->oam[s*4]+1,sx=n->oam[s*4+3];uint8_t tile=n->oam[s*4+1],attr=n->oam[s*4+2];
for(unsigned py=0;py<height;py++)for(int pxl=0;pxl<8;pxl++){int yy=sy+(int)py,xx=sx+pxl;if(xx<0||xx>=256||yy<0||yy>=240||(xx<8&&!(n->ppu_mask&4)))continue;unsigned ry=(attr&0x80)?height-1-py:py,rx=(attr&0x40)?(unsigned)pxl:7-(unsigned)pxl;uint16_t base;
if(height==16){base=(uint16_t)((tile&1)*0x1000+(tile&0xFE)*16+(ry>=8?16:0)+(ry&7));}else base=(uint16_t)(((n->ppu_ctrl&8)?0x1000:0)+tile*16+ry);
uint8_t index=(uint8_t)(((ppu_read(n,base)>>rx)&1)|(((ppu_read(n,base+8)>>rx)&1)<<1));if(index&&s==0&&opaque[yy*256+xx]&&xx<255)n->ppu_status|=0x40;if(index&&(!(attr&0x20)||!opaque[yy*256+xx]))px[yy*256+xx]=colors[n->palette[0x10+(attr&3)*4+index]&0x3F];}}
}
}
+21
View File
@@ -0,0 +1,21 @@
#include <stdio.h>
#include <SDL2/SDL.h>
#include "nes.h"
int main(int argc,char **argv)
{
NES nes;SDL_Window *w;SDL_Renderer *r;SDL_Texture *t;SDL_AudioDeviceID audio=0;uint32_t pixels[256*240];bool run=true;uint8_t pad=0;
if(argc!=2){fprintf(stderr,"usage: %s game.nes\n",argv[0]);return 2;}
if(nes_load(&nes,argv[1])){fprintf(stderr,"unsupported or invalid iNES ROM (supported mappers: 0, 2, 3)\n");return 1;}nes_reset(&nes);
if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_GAMECONTROLLER)!=0)return 1;
w=SDL_CreateWindow("MOS 6502 - NES",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,768,720,SDL_WINDOW_RESIZABLE);
r=w?SDL_CreateRenderer(w,-1,SDL_RENDERER_ACCELERATED|SDL_RENDERER_PRESENTVSYNC):NULL;if(w&&!r)r=SDL_CreateRenderer(w,-1,SDL_RENDERER_SOFTWARE);
t=r?SDL_CreateTexture(r,SDL_PIXELFORMAT_RGBA8888,SDL_TEXTUREACCESS_STREAMING,256,240):NULL;if(!t){SDL_Quit();return 1;}SDL_RenderSetLogicalSize(r,256,240);
{SDL_AudioSpec want={0};want.freq=48000;want.format=AUDIO_F32SYS;want.channels=1;want.samples=1024;audio=SDL_OpenAudioDevice(NULL,0,&want,NULL,0);if(audio)SDL_PauseAudioDevice(audio,0);}
while(run){SDL_Event e;while(SDL_PollEvent(&e)){if(e.type==SDL_QUIT)run=false;if(e.type==SDL_KEYDOWN||e.type==SDL_KEYUP){bool on=e.type==SDL_KEYDOWN;uint8_t bit=0;switch(e.key.keysym.sym){case SDLK_x:bit=1;break;case SDLK_z:bit=2;break;case SDLK_RSHIFT:bit=4;break;case SDLK_RETURN:bit=8;break;case SDLK_UP:bit=16;break;case SDLK_DOWN:bit=32;break;case SDLK_LEFT:bit=64;break;case SDLK_RIGHT:bit=128;break;case SDLK_ESCAPE:run=false;break;default:break;}if(on)pad|=bit;else pad&=(uint8_t)~bit;nes_set_controller(&nes,pad);}}
nes.frame_ready=false;while(run&&!nes.frame_ready){cpu_step_result_t step=nes_step(&nes);if(step.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X at $%04X\n",step.opcode,(uint16_t)(nes.cpu.PC-1));run=false;}}
if(run){float samples[801];for(unsigned i=0;i<801;i++)samples[i]=nes_audio_sample(&nes,48000);if(audio&&SDL_GetQueuedAudioSize(audio)<sizeof(samples)*4)SDL_QueueAudio(audio,samples,sizeof samples);nes_render_frame(&nes,pixels);SDL_UpdateTexture(t,NULL,pixels,256*4);SDL_RenderClear(r);SDL_RenderCopy(r,t,NULL,NULL);SDL_RenderPresent(r);}
}
if(audio)SDL_CloseAudioDevice(audio);
SDL_DestroyTexture(t);SDL_DestroyRenderer(r);SDL_DestroyWindow(w);SDL_Quit();return 0;
}
+44 -12
View File
@@ -10,7 +10,8 @@ typedef enum {
O_INY, O_JMP, O_JSR, O_LDA, O_LDX, O_LDY, O_LSR, O_NOP, O_ORA,
O_PHA, O_PHP, O_PLA, O_PLP, O_ROL, O_ROR, O_RTI, O_RTS, O_SBC,
O_SEC, O_SED, O_SEI, O_STA, O_STX, O_STY, O_TAX, O_TAY, O_TSX,
O_TXA, O_TXS, O_TYA
O_TXA, O_TXS, O_TYA, O_LAX, O_SAX, O_DCP, O_ISC, O_SLO, O_RLA,
O_SRE, O_RRA, O_ANC, O_ALR, O_ARR, O_AXS
} operation_t;
typedef struct { operation_t op; addr_mode_t mode; uint8_t cycles; bool page; } opcode_t;
@@ -48,12 +49,24 @@ static const opcode_t ops[256] = {
D(0xE8,INX,IMP,2,0), D(0xE9,SBC,IMM,2,0), D(0xEA,NOP,IMP,2,0), D(0xEC,CPX,ABS,4,0), D(0xED,SBC,ABS,4,0), D(0xEE,INC,ABS,6,0),
D(0xF0,BEQ,REL,2,0), D(0xF1,SBC,INDY,5,1), D(0xF5,SBC,ZPX,4,0), D(0xF6,INC,ZPX,6,0),
D(0xF8,SED,IMP,2,0), D(0xF9,SBC,ABSY,4,1), D(0xFD,SBC,ABSX,4,1), D(0xFE,INC,ABSX,7,0)
,D(0xA3,LAX,INDX,6,0),D(0xA7,LAX,ZP,3,0),D(0xAF,LAX,ABS,4,0),D(0xB3,LAX,INDY,5,1),D(0xB7,LAX,ZPY,4,0),D(0xBF,LAX,ABSY,4,1)
,D(0x83,SAX,INDX,6,0),D(0x87,SAX,ZP,3,0),D(0x8F,SAX,ABS,4,0),D(0x97,SAX,ZPY,4,0)
,D(0xC3,DCP,INDX,8,0),D(0xC7,DCP,ZP,5,0),D(0xCF,DCP,ABS,6,0),D(0xD3,DCP,INDY,8,0),D(0xD7,DCP,ZPX,6,0),D(0xDB,DCP,ABSY,7,0),D(0xDF,DCP,ABSX,7,0)
,D(0xE3,ISC,INDX,8,0),D(0xE7,ISC,ZP,5,0),D(0xEF,ISC,ABS,6,0),D(0xF3,ISC,INDY,8,0),D(0xF7,ISC,ZPX,6,0),D(0xFB,ISC,ABSY,7,0),D(0xFF,ISC,ABSX,7,0)
,D(0x03,SLO,INDX,8,0),D(0x07,SLO,ZP,5,0),D(0x0F,SLO,ABS,6,0),D(0x13,SLO,INDY,8,0),D(0x17,SLO,ZPX,6,0),D(0x1B,SLO,ABSY,7,0),D(0x1F,SLO,ABSX,7,0)
,D(0x23,RLA,INDX,8,0),D(0x27,RLA,ZP,5,0),D(0x2F,RLA,ABS,6,0),D(0x33,RLA,INDY,8,0),D(0x37,RLA,ZPX,6,0),D(0x3B,RLA,ABSY,7,0),D(0x3F,RLA,ABSX,7,0)
,D(0x43,SRE,INDX,8,0),D(0x47,SRE,ZP,5,0),D(0x4F,SRE,ABS,6,0),D(0x53,SRE,INDY,8,0),D(0x57,SRE,ZPX,6,0),D(0x5B,SRE,ABSY,7,0),D(0x5F,SRE,ABSX,7,0)
,D(0x63,RRA,INDX,8,0),D(0x67,RRA,ZP,5,0),D(0x6F,RRA,ABS,6,0),D(0x73,RRA,INDY,8,0),D(0x77,RRA,ZPX,6,0),D(0x7B,RRA,ABSY,7,0),D(0x7F,RRA,ABSX,7,0)
,D(0x0B,ANC,IMM,2,0),D(0x2B,ANC,IMM,2,0),D(0x4B,ALR,IMM,2,0),D(0x6B,ARR,IMM,2,0),D(0xCB,AXS,IMM,2,0),D(0xEB,SBC,IMM,2,0)
,D(0x1A,NOP,IMP,2,0),D(0x3A,NOP,IMP,2,0),D(0x5A,NOP,IMP,2,0),D(0x7A,NOP,IMP,2,0),D(0xDA,NOP,IMP,2,0),D(0xFA,NOP,IMP,2,0)
,D(0x80,NOP,IMM,2,0),D(0x82,NOP,IMM,2,0),D(0x89,NOP,IMM,2,0),D(0xC2,NOP,IMM,2,0),D(0xE2,NOP,IMM,2,0)
,D(0x04,NOP,ZP,3,0),D(0x44,NOP,ZP,3,0),D(0x64,NOP,ZP,3,0),D(0x14,NOP,ZPX,4,0),D(0x34,NOP,ZPX,4,0),D(0x54,NOP,ZPX,4,0),D(0x74,NOP,ZPX,4,0),D(0xD4,NOP,ZPX,4,0),D(0xF4,NOP,ZPX,4,0),D(0x0C,NOP,ABS,4,0),D(0x1C,NOP,ABSX,4,1),D(0x3C,NOP,ABSX,4,1),D(0x5C,NOP,ABSX,4,1),D(0x7C,NOP,ABSX,4,1),D(0xDC,NOP,ABSX,4,1),D(0xFC,NOP,ABSX,4,1)
};
#undef D
size_t cpu_disassemble(Bus6502 *b, uint16_t pc, char *out, size_t size)
{
static const char *names[]={"???","ADC","AND","ASL","BCC","BCS","BEQ","BIT","BMI","BNE","BPL","BRK","BVC","BVS","CLC","CLD","CLI","CLV","CMP","CPX","CPY","DEC","DEX","DEY","EOR","INC","INX","INY","JMP","JSR","LDA","LDX","LDY","LSR","NOP","ORA","PHA","PHP","PLA","PLP","ROL","ROR","RTI","RTS","SBC","SEC","SED","SEI","STA","STX","STY","TAX","TAY","TSX","TXA","TXS","TYA"};
static const char *names[]={"???","ADC","AND","ASL","BCC","BCS","BEQ","BIT","BMI","BNE","BPL","BRK","BVC","BVS","CLC","CLD","CLI","CLV","CMP","CPX","CPY","DEC","DEX","DEY","EOR","INC","INX","INY","JMP","JSR","LDA","LDX","LDY","LSR","NOP","ORA","PHA","PHP","PLA","PLP","ROL","ROR","RTI","RTS","SBC","SEC","SED","SEI","STA","STX","STY","TAX","TAY","TSX","TXA","TXS","TYA","LAX","SAX","DCP","ISC","SLO","RLA","SRE","RRA","ANC","ALR","ARR","AXS"};
opcode_t d=ops[bus_read(b,pc)];uint8_t a=bus_read(b,(uint16_t)(pc+1));uint8_t z=bus_read(b,(uint16_t)(pc+2));
const char *name=names[d.op];size_t bytes=1;
switch(d.mode){
@@ -112,15 +125,21 @@ static void compare(CPU6502 *c, uint8_t reg, uint8_t val)
static void adc(CPU6502 *c, uint8_t m)
{
uint8_t a = c->A, carry = !!(c->P & FLAG_C); uint16_t binary = (uint16_t)a + m + carry;
if ((c->P & FLAG_D) && c->decimal_enabled) {
uint16_t low=(a&0x0F)+(m&0x0F)+carry;
uint16_t intermediate;
if(low>=0x0A)low=((low+6)&0x0F)+0x10;
intermediate=(a&0xF0)+(m&0xF0)+low;
set_flag(c,FLAG_Z,(uint8_t)binary==0);
set_flag(c,FLAG_N,intermediate&0x80);
set_flag(c,FLAG_V,(~(a^m)&(a^(uint8_t)intermediate)&0x80)!=0);
set_flag(c,FLAG_C,intermediate>=0xA0);
if(intermediate>=0xA0)intermediate+=0x60;
c->A=(uint8_t)intermediate;
} else {
set_flag(c,FLAG_V,(~(a^m)&(a^(uint8_t)binary)&0x80)!=0);
if (c->P & FLAG_D) {
uint16_t decimal = binary;
if ((a & 0x0F) + (m & 0x0F) + carry > 9) decimal += 6;
set_flag(c, FLAG_C, decimal > 0x99);
if (decimal > 0x99) decimal += 0x60;
c->A = (uint8_t)decimal;
set_flag(c, FLAG_Z, (uint8_t)binary == 0); set_flag(c, FLAG_N, binary & 0x80);
} else { set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A); }
set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A);
}
}
static void sbc(CPU6502 *c, uint8_t m)
@@ -129,7 +148,7 @@ static void sbc(CPU6502 *c, uint8_t m)
uint8_t result = (uint8_t)binary;
set_flag(c, FLAG_V, ((a ^ result) & (a ^ m) & 0x80) != 0);
set_flag(c, FLAG_C, binary > 0xFF); set_nz(c, result);
if (c->P & FLAG_D) {
if ((c->P & FLAG_D) && c->decimal_enabled) {
int16_t lo = (a & 15) - (m & 15) - (carry ? 0 : 1);
int16_t hi = (a >> 4) - (m >> 4);
if (lo < 0) { lo -= 6; hi--; }
@@ -148,6 +167,7 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b)
if (c->irq_asserted && !(c->P & FLAG_I)) { interrupt(c,b,0xFFFE,false); out.cycles=7; return out; }
out.opcode = bus_read(b, c->PC++); d = ops[out.opcode];
if (d.op == O_BAD) { out.status = CPU_STEP_ILLEGAL_OPCODE; return out; }
if(d.op==O_JSR){uint8_t low=bus_read(b,c->PC++);uint16_t ret=c->PC;push(c,b,(uint8_t)(ret>>8));push(c,b,(uint8_t)ret);c->PC=(uint16_t)(low|((uint16_t)bus_read(b,c->PC)<<8));out.cycles=6;c->P|=FLAG_U;return out;}
ar = address(c,b,d.mode); out.cycles = d.cycles + (d.page && ar.page_crossed);
v = (d.mode == AM_IMP || d.mode == AM_ACC || d.mode == AM_REL) ? 0 : bus_read(b,ar.address);
if (is_branch(d.op)) {
@@ -163,7 +183,7 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b)
case O_CMP:compare(c,c->A,v);break; case O_CPX:compare(c,c->X,v);break; case O_CPY:compare(c,c->Y,v);break;
case O_DEC:v--;bus_write(b,ar.address,v);set_nz(c,v);break; case O_DEX:c->X--;set_nz(c,c->X);break; case O_DEY:c->Y--;set_nz(c,c->Y);break;
case O_EOR:c->A^=v;set_nz(c,c->A);break; case O_INC:v++;bus_write(b,ar.address,v);set_nz(c,v);break; case O_INX:c->X++;set_nz(c,c->X);break; case O_INY:c->Y++;set_nz(c,c->Y);break;
case O_JMP:c->PC=ar.address;break; case O_JSR:{uint16_t ret=c->PC-1;push(c,b,(uint8_t)(ret>>8));push(c,b,(uint8_t)ret);c->PC=ar.address;}break;
case O_JMP:c->PC=ar.address;break; case O_JSR:break;
case O_LDA:c->A=v;set_nz(c,v);break; case O_LDX:c->X=v;set_nz(c,v);break; case O_LDY:c->Y=v;set_nz(c,v);break;
case O_LSR:old=d.mode==AM_ACC?c->A:v;set_flag(c,FLAG_C,old&1);old>>=1;set_nz(c,old);if(d.mode==AM_ACC)c->A=old;else bus_write(b,ar.address,old);break;
case O_NOP:break; case O_ORA:c->A|=v;set_nz(c,c->A);break; case O_PHA:push(c,b,c->A);break; case O_PHP:push(c,b,c->P|FLAG_B|FLAG_U);break;
@@ -176,6 +196,18 @@ cpu_step_result_t cpu_step(CPU6502 *c, Bus6502 *b)
case O_STA:bus_write(b,ar.address,c->A);break; case O_STX:bus_write(b,ar.address,c->X);break; case O_STY:bus_write(b,ar.address,c->Y);break;
case O_TAX:c->X=c->A;set_nz(c,c->X);break; case O_TAY:c->Y=c->A;set_nz(c,c->Y);break; case O_TSX:c->X=c->SP;set_nz(c,c->X);break;
case O_TXA:c->A=c->X;set_nz(c,c->A);break; case O_TXS:c->SP=c->X;break; case O_TYA:c->A=c->Y;set_nz(c,c->A);break;
case O_LAX:c->A=c->X=v;set_nz(c,v);break;
case O_SAX:bus_write(b,ar.address,c->A&c->X);break;
case O_DCP:v--;bus_write(b,ar.address,v);compare(c,c->A,v);break;
case O_ISC:v++;bus_write(b,ar.address,v);sbc(c,v);break;
case O_SLO:set_flag(c,FLAG_C,v&0x80);v<<=1;bus_write(b,ar.address,v);c->A|=v;set_nz(c,c->A);break;
case O_RLA:{bool nc=v&0x80;v=(uint8_t)((v<<1)|!!(c->P&FLAG_C));set_flag(c,FLAG_C,nc);bus_write(b,ar.address,v);c->A&=v;set_nz(c,c->A);}break;
case O_SRE:set_flag(c,FLAG_C,v&1);v>>=1;bus_write(b,ar.address,v);c->A^=v;set_nz(c,c->A);break;
case O_RRA:{bool nc=v&1;v=(uint8_t)((v>>1)|((c->P&FLAG_C)?0x80:0));set_flag(c,FLAG_C,nc);bus_write(b,ar.address,v);adc(c,v);}break;
case O_ANC:c->A&=v;set_nz(c,c->A);set_flag(c,FLAG_C,c->A&0x80);break;
case O_ALR:c->A&=v;set_flag(c,FLAG_C,c->A&1);c->A>>=1;set_nz(c,c->A);break;
case O_ARR:{uint8_t anded=c->A&v,result=(uint8_t)((anded>>1)|((c->P&FLAG_C)?0x80:0));set_nz(c,result);set_flag(c,FLAG_V,((result>>6)^(result>>5))&1);if((c->P&FLAG_D)&&c->decimal_enabled){if((anded&0x0F)+(anded&1)>5)result=(uint8_t)((result&0xF0)|((result+6)&0x0F));if((anded&0xF0)+(anded&0x10)>0x50){result+=0x60;set_flag(c,FLAG_C,true);}else set_flag(c,FLAG_C,false);c->A=result;}else{c->A=result;set_flag(c,FLAG_C,result&0x40);}}break;
case O_AXS:{uint8_t anded=c->A&c->X;set_flag(c,FLAG_C,anded>=v);c->X=(uint8_t)(anded-v);set_nz(c,c->X);}break;
default:break;
}
c->P |= FLAG_U;
+45
View File
@@ -0,0 +1,45 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "state.h"
enum { HEADER_SIZE=25, STATE_SIZE=HEADER_SIZE+MEM_SIZE+4 };
static uint32_t crc32(const uint8_t *data,size_t size)
{
uint32_t crc=0xFFFFFFFFu;
for(size_t i=0;i<size;i++){crc^=data[i];for(unsigned bit=0;bit<8;bit++)crc=(crc>>1)^((uint32_t)-(int32_t)(crc&1)&0xEDB88320u);}
return ~crc;
}
static void put32(uint8_t *p,uint32_t v){p[0]=(uint8_t)v;p[1]=(uint8_t)(v>>8);p[2]=(uint8_t)(v>>16);p[3]=(uint8_t)(v>>24);}
static uint32_t get32(const uint8_t *p){return (uint32_t)p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16|(uint32_t)p[3]<<24;}
int state_save(const char *path,const CPU6502 *c,const Bus6502 *b)
{
uint8_t *data=calloc(1,STATE_SIZE);FILE *f;size_t wrote;
if(!data)return ENOMEM;
memcpy(data,"MOS6502",7);data[8]=2;data[9]=c->A;data[10]=c->X;data[11]=c->Y;
data[12]=c->SP;data[13]=c->P;data[14]=(uint8_t)c->PC;data[15]=(uint8_t)(c->PC>>8);
data[16]=c->nmi_pending;data[17]=c->irq_asserted;data[18]=c->decimal_enabled;
data[19]=b->keyboard;data[20]=b->easy6502_io;put32(data+21,b->rng_state);
memcpy(data+HEADER_SIZE,b->memory.data,MEM_SIZE);put32(data+HEADER_SIZE+MEM_SIZE,crc32(data,HEADER_SIZE+MEM_SIZE));
f=fopen(path,"wb");if(!f){int e=errno;free(data);return e;}
wrote=fwrite(data,1,STATE_SIZE,f);free(data);if(fclose(f)!=0||wrote!=STATE_SIZE)return EIO;return 0;
}
int state_load(const char *path,CPU6502 *c,Bus6502 *b)
{
uint8_t *data=malloc(STATE_SIZE);FILE *f;size_t got;int extra;
if(!data)return ENOMEM;
f=fopen(path,"rb");
if(!f){int e=errno;free(data);return e;}
got=fread(data,1,STATE_SIZE,f);extra=fgetc(f);fclose(f);
if(got!=STATE_SIZE||extra!=EOF||memcmp(data,"MOS6502",7)||data[8]!=2||
get32(data+HEADER_SIZE+MEM_SIZE)!=crc32(data,HEADER_SIZE+MEM_SIZE)){free(data);return EINVAL;}
c->A=data[9];c->X=data[10];c->Y=data[11];c->SP=data[12];c->P=data[13];
c->PC=(uint16_t)data[14]|(uint16_t)data[15]<<8;c->nmi_pending=data[16]!=0;c->irq_asserted=data[17]!=0;c->decimal_enabled=data[18]!=0;
b->keyboard=data[19];b->easy6502_io=data[20]!=0;b->rng_state=get32(data+21);
memcpy(b->memory.data,data+HEADER_SIZE,MEM_SIZE);b->framebuffer_dirty=true;free(data);return 0;
}
+15
View File
@@ -6,3 +6,18 @@ The download is pinned by SHA-256 and is not committed here.
`make test-functional` fetches and runs it. Success is the documented loop at
`$3469`.
The decimal image is Bruce Clark's public-domain test configured by Gopher2600
for NMOS accumulator and N/V/Z/C flag validation. `make test-decimal` runs all
valid and invalid BCD operand combinations and requires `ERROR == 0`.
`make test-interrupt` assembles Klaus Dormann's GPL-3.0 interrupt test with the
upstream bundled AS65 assembler and verifies IRQ, NMI, BRK, vectors, flags, and
concurrent interrupt priority through its success trap at `$06F5`.
`make test-apple1` assembles the original Woz Monitor source maintained in
Jeff Tranter's 6502 repository, boots its `$FF00` reset vector, injects a memory
examine command through the emulated PIA, and verifies the monitor's output.
`make test-nestest` downloads Kevtris's canonical NROM test and Nintendulator
golden log, then compares all 8,991 CPU states and cumulative cycle counts.
+20
View File
@@ -0,0 +1,20 @@
#include <stdio.h>
#include <string.h>
#include "apple1.h"
#include "cpu.h"
#include "loader.h"
int main(int argc,char **argv)
{
static const char command[]="FF00.FF07\r";size_t sent=0,loaded;Bus6502 bus;CPU6502 cpu;Apple1IO io;
if(argc!=2){fprintf(stderr,"usage: %s wozmon.bin\n",argv[0]);return 2;}
bus_init(&bus);apple1_init(&io,&bus);
if(load_binary(&bus,argv[1],0xFF00,&loaded)||loaded!=256)return 1;
cpu_reset(&cpu,&bus);
for(unsigned long i=0;i<2000000;i++){
if(!io.key_ready&&sent<sizeof(command)-1)apple1_key(&io,(uint8_t)command[sent++]);
if(cpu_step(&cpu,&bus).status!=CPU_STEP_OK){fprintf(stderr,"illegal instruction at $%04X\n",cpu.PC);return 1;}
if(strstr(io.output,"FF00: D8 58 A0 7F 8C 12 D0 A9")){printf("Apple I WozMon boot/PIA test passed after %lu instructions\n",i+1);return 0;}
}
fprintf(stderr,"Apple I output mismatch:\n%s\n",io.output);return 1;
}
+30
View File
@@ -0,0 +1,30 @@
#include <stdio.h>
#include "bus.h"
#include "cpu.h"
#include "loader.h"
int main(int argc,char **argv)
{
Bus6502 bus;CPU6502 cpu;size_t loaded;const unsigned long limit=200000000;
if(argc!=2){fprintf(stderr,"usage: %s 6502_decimal_test.bin\n",argv[0]);return 2;}
bus_init(&bus);
if(load_binary(&bus,argv[1],0x0200,&loaded)){fprintf(stderr,"failed to load %s\n",argv[1]);return 1;}
bus_write16(&bus,0xFFFC,0x0200);cpu_reset(&cpu,&bus);
for(unsigned long i=0;i<limit;i++){
uint16_t pc=cpu.PC;
if(bus.memory.data[pc]==0xDB){
if(bus.memory.data[0x000B]==0){printf("decimal success after %lu instructions (%zu-byte ROM)\n",i,loaded);return 0;}
fprintf(stderr,"decimal failure at stop marker, ERROR=$%02X\n",bus.memory.data[0x000B]);return 1;
}
cpu_step_result_t r=cpu_step(&cpu,&bus);
if(r.status==CPU_STEP_ILLEGAL_OPCODE){
if(r.opcode==0xDB&&bus.memory.data[0x000B]==0){printf("decimal success after %lu instructions (%zu-byte ROM)\n",i,loaded);return 0;}
fprintf(stderr,"decimal failure: opcode $%02X at $%04X, ERROR=$%02X N1=$%02X N2=$%02X "
"DA=$%02X flags=$%02X expectedA=$%02X N=$%02X V=$%02X Z=$%02X C=$%02X\n",
r.opcode,pc,bus.memory.data[0x000B],bus.memory.data[0],bus.memory.data[1],
bus.memory.data[4],bus.memory.data[5],bus.memory.data[6],bus.memory.data[7],
bus.memory.data[8],bus.memory.data[9],bus.memory.data[10]);return 1;
}
}
fprintf(stderr,"decimal test instruction limit reached at $%04X\n",cpu.PC);return 1;
}
+31
View File
@@ -0,0 +1,31 @@
#include <stdio.h>
#include "bus.h"
#include "cpu.h"
#include "loader.h"
#include "opcode.h"
int main(int argc,char **argv)
{
Bus6502 bus;CPU6502 cpu;size_t loaded;uint8_t previous=0;uint16_t history[32]={0};
if(argc!=2){fprintf(stderr,"usage: %s 6502_interrupt_test.bin\n",argv[0]);return 2;}
bus_init(&bus);
if(load_binary(&bus,argv[1],0x000A,&loaded)){fprintf(stderr,"failed to load %s\n",argv[1]);return 1;}
bus.memory.data[0xBFFC]=0;
cpu.PC=0x0400;cpu.A=cpu.X=cpu.Y=0;cpu.SP=0xFD;cpu.P=FLAG_U|FLAG_I;
cpu.nmi_pending=false;cpu.irq_asserted=false;
for(unsigned long i=0;i<10000000;i++){
uint16_t before=cpu.PC;history[i%32]=before;cpu_step_result_t r=cpu_step(&cpu,&bus);
uint8_t feedback=bus.memory.data[0xBFFC]&3;
cpu_set_irq(&cpu,(feedback&1)!=0);
if((feedback&2)&&!(previous&2))cpu_request_nmi(&cpu);
previous=feedback;
if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal $%02X at $%04X\n",r.opcode,before);return 1;}
if(cpu.PC==0x06F5){printf("interrupt success after %lu instructions (%zu-byte ROM)\n",i+1,loaded);return 0;}
if(cpu.PC==before){
fprintf(stderr,"interrupt trap at $%04X after %lu instructions\n",before,i+1);
unsigned long first=i>31?i-31:0;
for(unsigned long n=first;n<=i;n++){char text[48];uint16_t pc=history[n%32];cpu_disassemble(&bus,pc,text,sizeof text);fprintf(stderr," %04X %s\n",pc,text);}return 1;
}
}
fprintf(stderr,"interrupt test limit reached at $%04X\n",cpu.PC);return 1;
}
+26
View File
@@ -0,0 +1,26 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nes.h"
static unsigned field(const char *line,const char *name)
{ const char *p=strstr(line,name);return p?(unsigned)strtoul(p+strlen(name),NULL,16):0xFFFFFFFFu; }
int main(int argc,char **argv)
{
NES nes;FILE *log;char line[256];unsigned long lines=0,cycles=7;
if(argc!=3){fprintf(stderr,"usage: %s nestest.nes nestest.log\n",argv[0]);return 2;}
if(nes_load(&nes,argv[1])){fprintf(stderr,"failed to load nestest ROM\n");return 1;}
log=fopen(argv[2],"r");if(!log){perror(argv[2]);return 1;}
nes_reset(&nes);nes.cpu.PC=0xC000;nes.cpu.A=nes.cpu.X=nes.cpu.Y=0;nes.cpu.P=FLAG_U|FLAG_I;nes.cpu.SP=0xFD;
while(fgets(line,sizeof line,log)){
unsigned pc=(unsigned)strtoul(line,NULL,16),a=field(line,"A:"),x=field(line,"X:"),y=field(line,"Y:"),p=field(line,"P:"),sp=field(line,"SP:");
const char *cy=strstr(line,"CYC:");unsigned long expected_cycles=cy?strtoul(cy+4,NULL,10):cycles;
if(nes.cpu.PC!=pc||nes.cpu.A!=a||nes.cpu.X!=x||nes.cpu.Y!=y||nes.cpu.P!=p||nes.cpu.SP!=sp||cycles!=expected_cycles){
fprintf(stderr,"nestest mismatch line %lu\nexpected: %sactual: %04X A:%02X X:%02X Y:%02X P:%02X SP:%02X CYC:%lu\n",lines+1,line,nes.cpu.PC,nes.cpu.A,nes.cpu.X,nes.cpu.Y,nes.cpu.P,nes.cpu.SP,cycles);fclose(log);return 1;
}
cpu_step_result_t r=nes_step(&nes);if(r.status!=CPU_STEP_OK){fprintf(stderr,"illegal opcode $%02X on line %lu\n",r.opcode,lines+1);fclose(log);return 1;}
cycles+=r.cycles;lines++;
}
fclose(log);printf("nestest matched %lu instructions and cycle counts\n",lines);return lines==8991?0:1;
}
+90 -1
View File
@@ -1,7 +1,10 @@
#include <stdio.h>
#include <string.h>
#include "bus.h"
#include "apple1.h"
#include "cpu.h"
#include "state.h"
#include "nes.h"
static int tests, failures;
#define CHECK(expr) do { tests++; if (!(expr)) { failures++; fprintf(stderr,"%s:%d: check failed: %s\n",__FILE__,__LINE__,#expr); } } while(0)
@@ -68,9 +71,95 @@ static void test_branches_and_illegal(void)
b.memory.data[0x8100]=0x02;cpu_step_result_t r=step(&c,&b);CHECK(r.status==CPU_STEP_ILLEGAL_OPCODE);EQ8(r.opcode,0x02);
}
static void test_stable_undocumented(void)
{
CPU6502 c;Bus6502 b;setup(&c,&b,0x8000);
const uint8_t p[]={0xA9,0xF0,0xA2,0xCC,0x87,0x10,0xA7,0x10,0xC7,0x10,
0xE7,0x10,0x0B,0x80,0x4B,0x0F,0xCB,0x01,0x1A};
memcpy(&b.memory.data[0x8000],p,sizeof p);
step(&c,&b);step(&c,&b);step(&c,&b);EQ8(b.memory.data[0x10],0xC0);
step(&c,&b);EQ8(c.A,0xC0);EQ8(c.X,0xC0);
step(&c,&b);EQ8(b.memory.data[0x10],0xBF);CHECK(c.P&FLAG_C);
c.P|=FLAG_C;step(&c,&b);EQ8(b.memory.data[0x10],0xC0);EQ8(c.A,0x00);CHECK(c.P&FLAG_C);
step(&c,&b);EQ8(c.A,0);CHECK(!(c.P&FLAG_C));
c.A=0xFF;step(&c,&b);EQ8(c.A,7);CHECK(c.P&FLAG_C);
c.A=0xFF;c.X=0x0F;step(&c,&b);EQ8(c.X,0x0E);CHECK(c.P&FLAG_C);
EQ8(step(&c,&b).cycles,2);
}
static void test_portable_state(void)
{
const char *path="/tmp/mos6502-test.state";CPU6502 c,restored;Bus6502 b,out;
setup(&c,&b,0x8123);c.A=0x42;c.X=0x77;c.P=FLAG_U|FLAG_C;b.memory.data[0x2345]=0xAB;
b.keyboard='d';b.easy6502_io=true;bus_seed_rng(&b,12345);
CHECK(state_save(path,&c,&b)==0);bus_init(&out);memset(&restored,0,sizeof restored);
CHECK(state_load(path,&restored,&out)==0);EQ16(restored.PC,0x8123);EQ8(restored.A,0x42);
EQ8(restored.X,0x77);EQ8(restored.P,FLAG_U|FLAG_C);EQ8(out.memory.data[0x2345],0xAB);
EQ8(out.keyboard,'d');CHECK(out.easy6502_io);CHECK(out.rng_state==12345);CHECK(out.framebuffer_dirty);
CHECK(restored.decimal_enabled);
remove(path);
}
static void test_2a03_ignores_decimal(void)
{
CPU6502 c;Bus6502 b;setup(&c,&b,0x8000);c.decimal_enabled=false;
const uint8_t p[]={0xF8,0x18,0xA9,0x49,0x69,0x51};memcpy(&b.memory.data[0x8000],p,sizeof p);
step(&c,&b);step(&c,&b);step(&c,&b);step(&c,&b);EQ8(c.A,0x9A);CHECK(!(c.P&FLAG_C));
}
static void test_nes_nrom_bus(void)
{
const char *path="/tmp/mos6502-test.nes";uint8_t header[16]={ 'N','E','S',0x1A,1,1 };
uint8_t prg[16384]={0},chr[8192]={0};FILE *f=fopen(path,"wb");NES n;
prg[0]=0xF8;prg[1]=0x18;prg[2]=0xA9;prg[3]=0x49;prg[4]=0x69;prg[5]=0x51;
prg[0x3FFC]=0x00;prg[0x3FFD]=0x80;fwrite(header,1,sizeof header,f);fwrite(prg,1,sizeof prg,f);fwrite(chr,1,sizeof chr,f);fclose(f);
CHECK(nes_load(&n,path)==0);nes_reset(&n);EQ16(n.cpu.PC,0x8000);CHECK(!n.cpu.decimal_enabled);
bus_write(&n.bus,0x0001,0xAA);EQ8(bus_read(&n.bus,0x0801),0xAA);EQ8(bus_read(&n.bus,0xC000),0xF8);
nes_step(&n);nes_step(&n);nes_step(&n);nes_step(&n);EQ8(n.cpu.A,0x9A);
nes_set_controller(&n,0x05);bus_write(&n.bus,0x4016,1);bus_write(&n.bus,0x4016,0);
EQ8(bus_read(&n.bus,0x4016)&1,1);EQ8(bus_read(&n.bus,0x4016)&1,0);EQ8(bus_read(&n.bus,0x4016)&1,1);
remove(path);
}
static void write_test_rom(const char *path,uint8_t mapper,uint8_t prg_banks,uint8_t chr_banks)
{
uint8_t h[16]={'N','E','S',0x1A,prg_banks,chr_banks,(uint8_t)(mapper<<4),0};FILE *f=fopen(path,"wb");
fwrite(h,1,sizeof h,f);
for(unsigned b=0;b<prg_banks;b++)for(unsigned i=0;i<16384;i++)fputc((int)b,f);
for(unsigned b=0;b<chr_banks;b++)for(unsigned i=0;i<8192;i++)fputc((int)(0x40+b),f);
fclose(f);
}
static void test_nes_mappers_and_apu(void)
{
const char *u="/tmp/mos6502-urom.nes",*c="/tmp/mos6502-cnrom.nes";NES n;
write_test_rom(u,2,4,0);CHECK(nes_load(&n,u)==0);EQ8(n.mapper,2);EQ8(bus_read(&n.bus,0x8000),0);EQ8(bus_read(&n.bus,0xC000),3);
bus_write(&n.bus,0x8000,2);EQ8(bus_read(&n.bus,0x8000),2);EQ8(bus_read(&n.bus,0xC000),3);
write_test_rom(c,3,2,4);CHECK(nes_load(&n,c)==0);EQ8(n.mapper,3);EQ8(n.chr[0],0x40);bus_write(&n.bus,0x8000,3);EQ8(n.chr_bank,3);
bus_write(&n.bus,0x4000,0x3F);bus_write(&n.bus,0x4002,8);bus_write(&n.bus,0x4003,0);bus_write(&n.bus,0x4015,1);
CHECK(bus_read(&n.bus,0x4015)==1);float peak=0;for(unsigned i=0;i<1000;i++){float s=nes_audio_sample(&n,48000);if(s>peak)peak=s;}CHECK(peak>0.1f);
remove(u);remove(c);
}
static void test_nes_ppu_rendering(void)
{
NES n;uint32_t pixels[256*240];memset(&n,0,sizeof n);n.chr_size=8192;n.chr_ram=true;n.ppu_mask=0x18;
n.palette[0]=0;n.palette[1]=1;n.palette[0x11]=2;n.chr[0]=0x80;n.chr[16]=0x80;
memset(n.oam,0xFF,sizeof n.oam);n.oam[0]=7;n.oam[1]=1;n.oam[2]=0;n.oam[3]=8;
nes_render_frame(&n,pixels);CHECK(n.ppu_status&0x40);CHECK(!(n.ppu_status&0x20));CHECK(pixels[8*256+8]!=pixels[8*256+9]);
for(unsigned s=0;s<9;s++){n.oam[s*4]=7;n.oam[s*4+3]=(uint8_t)(s*8+8);}nes_render_frame(&n,pixels);CHECK(n.ppu_status&0x20);
}
static void test_bus_hooks_and_apple1(void)
{
Bus6502 b;Apple1IO io;bus_init(&b);apple1_init(&io,&b);apple1_key(&io,'a');
EQ8(bus_read(&b,0xD011),0x80);EQ8(bus_read(&b,0xD010),'A'|0x80);EQ8(bus_read(&b,0xD011),0);
bus_write(&b,0x1234,0x56);EQ8(bus_read(&b,0x1234),0x56);
}
int main(void)
{
test_reset_and_bus();test_load_store_and_modes();test_indirect_and_control();
test_alu_and_flags();test_stack_interrupts();test_branches_and_illegal();
test_alu_and_flags();test_stack_interrupts();test_branches_and_illegal();test_stable_undocumented();test_portable_state();test_bus_hooks_and_apple1();test_2a03_ignores_decimal();test_nes_nrom_bus();test_nes_mappers_and_apu();test_nes_ppu_rendering();
printf("%d checks, %d failures\n",tests,failures);return failures?1:0;
}
+30
View File
@@ -0,0 +1,30 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "bus.h"
#include "cpu.h"
static int byte(void){return fgetc(stdin);}
static int word(uint16_t *v){int a=byte(),b=byte();if(a==EOF||b==EOF)return 0;*v=(uint16_t)(a|(b<<8));return 1;}
static int regs(CPU6502 *c)
{
int s,a,x,y,p;if(!word(&c->PC))return 0;s=byte();a=byte();x=byte();y=byte();p=byte();if(p==EOF)return 0;
c->SP=(uint8_t)s;c->A=(uint8_t)a;c->X=(uint8_t)x;c->Y=(uint8_t)y;c->P=(uint8_t)p;c->decimal_enabled=true;return 1;
}
static int pairs(Bus6502 *b,uint16_t *addresses,uint8_t *values)
{
uint16_t count;if(!word(&count))return -1;for(uint16_t i=0;i<count;i++){uint16_t a;int v;if(!word(&a)||(v=byte())==EOF)return -1;if(b)b->memory.data[a]=(uint8_t)v;if(addresses){addresses[i]=a;values[i]=(uint8_t)v;}}return count;
}
int main(void)
{
unsigned long tests=0;int failures=0;
for(;;){CPU6502 c,want;Bus6502 b;uint16_t addresses[64];uint8_t values[64];int count,cycles;
bus_init(&b);memset(&c,0,sizeof c);memset(&want,0,sizeof want);if(!regs(&c))break;
if(pairs(&b,NULL,NULL)<0||!regs(&want)||(count=pairs(NULL,addresses,values))<0||(cycles=byte())==EOF)return 2;
cpu_step_result_t r=cpu_step(&c,&b);tests++;
int bad=r.status!=CPU_STEP_OK||r.cycles!=(uint8_t)cycles||c.PC!=want.PC||c.SP!=want.SP||c.A!=want.A||c.X!=want.X||c.Y!=want.Y||c.P!=want.P;
for(int i=0;i<count;i++)if(b.memory.data[addresses[i]]!=values[i])bad=1;
if(bad&&failures++<20)fprintf(stderr,"vector %lu failed: got PC=%04X S=%02X A=%02X X=%02X Y=%02X P=%02X C=%u\n",tests,c.PC,c.SP,c.A,c.X,c.Y,c.P,r.cycles);
}
printf("%lu single-step vectors, %d failures\n",tests,failures);return failures?1:0;
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import argparse, json, pathlib, re, struct, subprocess, sys, urllib.request
ROOT=pathlib.Path(__file__).resolve().parents[1]
URL="https://raw.githubusercontent.com/SingleStepTests/65x02/main/6502/v1/{:02x}.json"
def emit(reg, ram):
out=struct.pack("<HBBBBB",reg["pc"],reg["s"],reg["a"],reg["x"],reg["y"],reg["p"])+struct.pack("<H",len(ram))
for address,value in ram: out+=struct.pack("<HB",address,value)
return out
def main():
ap=argparse.ArgumentParser(description="Run SingleStepTests 6502 JSON vectors")
ap.add_argument("files",nargs="*",type=pathlib.Path)
ap.add_argument("--download",action="store_true",help="download vectors for every opcode implemented by this core")
args=ap.parse_args();files=args.files
if args.download:
cache=ROOT/"tests"/"vectors";cache.mkdir(parents=True,exist_ok=True)
source=(ROOT/"src"/"opcode.c").read_text();opcodes=sorted({int(x,16) for x in re.findall(r"D\(0x([0-9A-Fa-f]{2}),",source)})
for opcode in opcodes:
path=cache/f"{opcode:02x}.json"
if not path.exists():print(f"fetching {path.name}",file=sys.stderr);urllib.request.urlretrieve(URL.format(opcode),path)
files.append(path)
if not files:ap.error("provide JSON files or --download")
runner=subprocess.Popen([str(ROOT/"bin"/"run_vectors")],stdin=subprocess.PIPE);assert runner.stdin
for path in files:
for case in json.loads(path.read_text()):
initial,final=case["initial"],case["final"]
if len(final["ram"])>64:raise ValueError("vector has more than 64 final RAM entries")
runner.stdin.write(emit(initial,initial["ram"]));runner.stdin.write(emit(final,final["ram"]));runner.stdin.write(bytes([len(case["cycles"])]))
runner.stdin.close();return runner.wait()
if __name__=="__main__":raise SystemExit(main())