Foreword
In the previous post we implemented the last couple of 6502 instructions in our C64 Flutter emulator.
In this post we will be running the Klaus Dormann Test Suite on our emulator to ensure we have implemented all the instructions correctly.
Starting up the Klaus Dormann Test Suite
Let us see if we can startup the Klaus Dormann Test Suite on our emulator, although only in a single stepping fashion at the moment.
To get started, we need two files from Klaus' Github repository:
- https://github.com/Klaus2m5/6502_65C02_functional_tests/blob/master/bin_files/6502_functional_test.bin
- https://github.com/Klaus2m5/6502_65C02_functional_tests/blob/master/bin_files/6502_functional_test.lst
The second file is a listing file, containing the actual disassembled version of the binary we are running. The listing file is useful if you want to follow along to see what the program is actually doing in a certain point in time.
Firstly we dump the binary in the assets folder of our Flutter project and rename it to program.bin. This is the default binary our emulator looks for when it starts up.
Now, usually if a 6502 system starts up, it looks at the reset vector at address 0xFFFC and 0xFFFD for the starting address for which it should start executing code, something which we didn't implemented yet.
In the Klaus Test suite there is also a reset vector defined, but within the context of the Test Suite it has the function to detect if an accidental reset was triggered. So, in actual fact this Test Suite doesn't use the reset vector to everything. Rather, when using the test suite, you should just set the PC register to 0x400 and start execution. This makes our life easier, and for the moment we don't need to worry about implementing the Reset vector stuff.
So, in put cpu.dart, the following change needs to be done, change in bold:
... int _n = 0, _z = 0, _c = 0, _i = 0, _d = 0, _v = 0; int _sp = 0xff; int pc = 0x400; ...With this we can startup our emulator and single step through the code of Test Suite.
Unattended running
appBar: AppBar( title: Row( mainAxisSize: MainAxisSize.min, children: [ const Text("Emulator C64"), BlocBuilder<C64Bloc, C64State>( builder: (BuildContext context, state) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _getRunStopButton(state, context) ]); }) ]), ),We want our run button to behave like a toggle switch, toggling between a play and a pause button. To do all these fancy stuff, we need to inject some state, which we achieve by wrapping everything with a BlocBuilder. We did discuss the workings of BlocBuilder in a previous post.
Widget _getRunStopButton(C64State state, BuildContext context) { if(state is DataShowState) { return IconButton( icon: Icon(Icons.play_arrow), onPressed: () { context.read<C64Bloc>().add(RunEvent()); } ); } else if (state is RunningState) { return IconButton( icon: Icon(Icons.stop_circle), onPressed: () { context.read<C64Bloc>().add(StopEvent()); } ); } else { return const IconButton( icon: Icon(Icons.play_arrow), onPressed: null ); } }Here we test for different states. Firstly we show an enabled play button if we are in DataShowState. As you might remember from previous posts, with DataShowState, we display a dump of memory and registers, and we can single step from that point. This is the perfect scenario to provide a play button that will run the emulator at full speed.
class RunningState extends C64State {}No values or properties we need to convey to here, just conveying the mere fact when we are in the running state.
... Timer? timer; ... on<RunEvent>((event, emit) { timer = Timer.periodic(const Duration(seconds: 1), (timer) { ... }); emit(RunningState()); }); ...We define the timer variable as a global variable in our C64Bloc class, since we want to be able to cancel the timer in another event handler.
... int _cycles = 0; ... int getCycles() { return _cycles; } ... step() { ... _cycles = _cycles + CpuTables.instructionCycles[opCode]; var resolvedAddress = calculateEffectiveAddress(CpuTables.addressModes[opCode], arg0, arg1); switch (opCode) { ... } }We have defined the instructionCycles array in a previous post, which specify the number of cycles for every opcode. So, with every step we can just add the number of cycles for the opcode being executed to a _cyles variable.
... on<RunEvent>((event, emit) { timer = Timer.periodic(const Duration(seconds: 1), (timer) { int targetCycles = _cpu.getCycles() + 1000000; do { _cpu.step(); } while (_cpu.getCycles() < targetCycles); }); emit(RunningState()); }); ...So, we just add one million to our current Cpu cycle count and that will be the target at which we will stop the loop.
on<StopEvent>((event, emit) { timer?.cancel(); emit(DataShowState( dumpNo: dumpNo++, memorySnippet: ByteData.sublistView(memory.getDebugSnippet(), 0, 512), a: _cpu.getAcc(), x: _cpu.getX(), y: _cpu.getY(), n: _cpu.getN() == 1, z: _cpu.getZ() == 1, c: _cpu.getC() == 1, i: _cpu.getI() == 1, d: _cpu.getD() == 1, v: _cpu.getV() == 1, pc: _cpu.pc)); });Here we just cancel the timer emit a DataShowState, so after we have stopped the running, we want to display the current state of memory and the registers.
Running the Test Suite
So, if we let it run a bit longer, our result will look like this:
Here it is clear, if something went wrong with the test, it will do an endless loop at the address 09D7. So, obviously, our emulator failed test, but which one? Look back a couple of lines, we see the comment: The IRQ vector was never executed.
Implementing the BRK and RTI instructions
/*BRK*/ case 0x00: push(pc >> 8); push(pc & 0xff); push((_n << 7) | (_v << 6) | (3 << 4) | (_d << 3) | (_i << 2) | (_z << 1) | _c); _i = 1; pc = (memory.getMem(0xffff) << 8) | memory.getMem(0xfffe); /*RTI*/ case 0x40: int temp = pull(); _c = temp & 1; _z = (temp >> 1) & 1; _i = (temp >> 2) & 1; _d = (temp >> 3) & 1; _v = (temp >> 6) & 1; _n = (temp >> 7) & 1; pc = pull() | (pull() << 8);Now, when we run the test suite again, we get passed this failed test suite. However, we end up in another endless loop at address 0xdeb, which indicates another failed test.