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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: sudo apt-get update - 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 debug
- run: make sanitize - run: make sanitize
- run: make test-functional - 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.highscore
snake.state snake.state
tests/roms/*.bin tests/roms/*.bin
tests/roms/*.nes
tests/roms/*.log
tests/vectors/
# End of https://www.toptal.com/developers/gitignore/api/c,visualstudiocode # 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. - `Bus6502` owns RAM and Easy6502 MMIO; CPU code only uses bus accessors.
- `cpu_step` executes one instruction or interrupt and returns status/opcode/cycles. - `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/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. - `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. - `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) LDLIBS += $(SDL_LIBS)
endif 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_SRC = $(CORE_SRC) src/frontend.c src/main.c
APP_OBJ = $(APP_SRC:src/%.c=build/%.o) APP_OBJ = $(APP_SRC:src/%.c=build/%.o)
TARGET = bin/mos6502 TARGET = bin/mos6502
SDL_TARGET = bin/mos6502-sdl 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) 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) $(TARGET): $(APP_OBJ)
@mkdir -p bin @mkdir -p bin
$(CC) $(CFLAGS) $(APP_OBJ) $(LDLIBS) -o $@ $(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 curl -L --fail -o $@ https://raw.githubusercontent.com/Klaus2m5/6502_65C02_functional_tests/master/bin_files/6502_functional_test.bin
@echo "fa12bfc761e6f9057e4cc01a665a7b800ff01ae91f598af1e39a1201d01953fd $@" | sha256sum -c - @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 test-functional: tests/roms/6502_functional_test.bin bin/run_suite
./bin/run_suite $< 0 0x400 0x3469 100000000 ./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: 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: CFLAGS = -O1 -g3 -std=c11 -Wall -Wextra -Wpedantic -fsanitize=address,undefined
sanitize: export ASAN_OPTIONS = detect_leaks=0 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 # core and command-line emulator
make test # unit/integration checks make test # unit/integration checks
make test-functional # official Klaus Dormann functional suite 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 debug # strict -Werror debug build and tests
make sanitize # AddressSanitizer/UBSan 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`. 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: External functional ROMs can be run without modifying the application:
```sh ```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 Original arcade Pac-Man used a Z80; running that ROM would require a different
CPU and hardware platform. 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). See [the design](docs/design.md) and [project status](docs/plan.md).
## Validation status ## Validation status
The in-tree tests exercise the CPU's major correctness boundaries. External The automated suite passes Klaus Dormann's functional and interrupt ROMs,
Klaus Dormann functional and decimal ROM validation is the next hardening gate; Bruce Clark's exhaustive decimal test, all 8,991 canonical `nestest` trace
the binaries are not vendored because their redistribution terms must be lines, an authentic Woz Monitor interaction, and 2,360,000 SingleStepTests
respected. 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. - 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. - All official addressing modes, zero-page wrap, page penalties and indirect-JMP bug.
- Instruction-level cycle accounting and deterministic illegal-opcode errors. - 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. - 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. - Original ca65 Snake game with pause, persistent high score, and a reproducible `make snake` target.
- Strict debug, sanitizer and dependency-free test targets. - 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,360,000 independent SingleStepTests cases cover every implemented official
2. Expand table-driven tests to cover every official opcode/address-mode pairing. and stable undocumented opcode, final CPU/RAM state, and instruction cycles.
3. Add a trace/disassembler runner for diagnosing external ROM failures. - 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 The official Klaus Dormann functional suite passes at `$3469` after 30,646,176
instructions. Linux CI covers strict, sanitizer, functional-ROM, Snake, and SDL instructions. Linux CI covers strict, sanitizer, all external CPU/platform suites,
UI builds. 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. ## Explicit compatibility boundary
- Audio, save states and an interactive debugger.
- A real historical machine such as Apple I or NES. NES support is a separate major The NES frontend is a useful first-generation NES subsystem, not a claim of full
project requiring the PPU, APU, controllers, cartridge parsing/mappers and tighter timing. 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 <stdint.h>
#include "memory.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; Memory memory;
uint8_t keyboard; uint8_t keyboard;
uint32_t rng_state; uint32_t rng_state;
bool easy6502_io; bool easy6502_io;
bool framebuffer_dirty; bool framebuffer_dirty;
} Bus6502; bus_read_hook_t read_hook;
bus_write_hook_t write_hook;
void *hook_userdata;
};
void bus_init(Bus6502 *bus); void bus_init(Bus6502 *bus);
uint8_t bus_read(Bus6502 *bus, uint16_t addr); 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_write16(Bus6502 *bus, uint16_t addr, uint16_t value);
void bus_set_keyboard(Bus6502 *bus, uint8_t key); void bus_set_keyboard(Bus6502 *bus, uint8_t key);
void bus_seed_rng(Bus6502 *bus, uint32_t seed); 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 #endif
+1
View File
@@ -13,6 +13,7 @@ typedef struct CPU6502 {
uint8_t P; uint8_t P;
bool nmi_pending; bool nmi_pending;
bool irq_asserted; bool irq_asserted;
bool decimal_enabled;
} CPU6502; } CPU6502;
enum { 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 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 (bus->easy6502_io) {
if (addr == 0x00FE) return random_byte(bus); if (addr == 0x00FE) return random_byte(bus);
if (addr == 0x00FF) return bus->keyboard; 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) 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); mem_write(&bus->memory, addr, value);
if (addr >= 0x0200 && addr <= 0x05FF) bus->framebuffer_dirty = true; 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_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_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)); memset(cpu, 0, sizeof(*cpu));
cpu->SP = 0xFD; cpu->SP = 0xFD;
cpu->P = FLAG_U | FLAG_I; cpu->P = FLAG_U | FLAG_I;
cpu->decimal_enabled = true;
cpu->PC = bus_read16(bus, 0xFFFC); 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 <SDL2/SDL_ttf.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "state.h"
static const uint32_t palette[16] = { static const uint32_t palette[16] = {
0x071018FF,0x62F294FF,0xFF5D73FF,0x8FFFAFFF,0x638CFFFF,0xD879FFFF, 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);} 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) static void play_tone(SDL_AudioDeviceID device, int frequency, int milliseconds)
{ {
int16_t samples[4410];int count=44100*milliseconds/1000; 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); paused=!paused; budget=0; bus->framebuffer_dirty=true; update_title(window,score,high,paused);
} }
else if(!event.key.repeat && k==SDLK_F5) { 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) { 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_w||k==SDLK_UP) bus_set_keyboard(bus,'w');
else if(k==SDLK_a||k==SDLK_LEFT) bus_set_keyboard(bus,'a'); 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;
}
+45 -13
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_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_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_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; } operation_t;
typedef struct { operation_t op; addr_mode_t mode; uint8_t cycles; bool page; } opcode_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(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(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(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 #undef D
size_t cpu_disassemble(Bus6502 *b, uint16_t pc, char *out, size_t size) 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)); 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; const char *name=names[d.op];size_t bytes=1;
switch(d.mode){ 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) 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; uint8_t a = c->A, carry = !!(c->P & FLAG_C); uint16_t binary = (uint16_t)a + m + carry;
set_flag(c, FLAG_V, (~(a ^ m) & (a ^ (uint8_t)binary) & 0x80) != 0); if ((c->P & FLAG_D) && c->decimal_enabled) {
if (c->P & FLAG_D) { uint16_t low=(a&0x0F)+(m&0x0F)+carry;
uint16_t decimal = binary; uint16_t intermediate;
if ((a & 0x0F) + (m & 0x0F) + carry > 9) decimal += 6; if(low>=0x0A)low=((low+6)&0x0F)+0x10;
set_flag(c, FLAG_C, decimal > 0x99); intermediate=(a&0xF0)+(m&0xF0)+low;
if (decimal > 0x99) decimal += 0x60; set_flag(c,FLAG_Z,(uint8_t)binary==0);
c->A = (uint8_t)decimal; set_flag(c,FLAG_N,intermediate&0x80);
set_flag(c, FLAG_Z, (uint8_t)binary == 0); set_flag(c, FLAG_N, binary & 0x80); set_flag(c,FLAG_V,(~(a^m)&(a^(uint8_t)intermediate)&0x80)!=0);
} else { set_flag(c, FLAG_C, binary > 0xFF); c->A = (uint8_t)binary; set_nz(c, c->A); } 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);
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) 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; uint8_t result = (uint8_t)binary;
set_flag(c, FLAG_V, ((a ^ result) & (a ^ m) & 0x80) != 0); set_flag(c, FLAG_V, ((a ^ result) & (a ^ m) & 0x80) != 0);
set_flag(c, FLAG_C, binary > 0xFF); set_nz(c, result); 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 lo = (a & 15) - (m & 15) - (carry ? 0 : 1);
int16_t hi = (a >> 4) - (m >> 4); int16_t hi = (a >> 4) - (m >> 4);
if (lo < 0) { lo -= 6; hi--; } 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; } 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]; 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_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); 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); v = (d.mode == AM_IMP || d.mode == AM_ACC || d.mode == AM_REL) ? 0 : bus_read(b,ar.address);
if (is_branch(d.op)) { 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_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_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_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_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_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; 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_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_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_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; default:break;
} }
c->P |= FLAG_U; 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 `make test-functional` fetches and runs it. Success is the documented loop at
`$3469`. `$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 <stdio.h>
#include <string.h> #include <string.h>
#include "bus.h" #include "bus.h"
#include "apple1.h"
#include "cpu.h" #include "cpu.h"
#include "state.h"
#include "nes.h"
static int tests, failures; 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) #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); 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) int main(void)
{ {
test_reset_and_bus();test_load_store_and_modes();test_indirect_and_control(); 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; 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())