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