Saturday, 1 March 2025

A Commodore 64 Emulator in Flutter: Part 10

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:

The first link is the actually binary which will execute in our emulator. This is a 64KB binary which will fill the whole address space accessible by the 6502.

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

To single step through the Klaus Dormann Test Suite in our emulator will be such a daunting tasks. You will probably need to click the step button thousands of times.

It would make our lives easier if we could just let the Test Suite run unattended, with us just pausing the execution once in while, to see how far we have progressed through the tests.

We do this by adding a button right next to the title. As part of the process we need to wrap both the title and the button in a row in order for everything to align properly. All this is happening in the main.dart file:

        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.

Now, the method _getRunStopButton() returns for us three possible buttons, depending on the state, which could be a play button, a stop button, and a disabled play button if everything hasn't initialised yet:

  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.

Pressing the play button emits a RunEvent, which we still need to implement a listener for. We will do that in a bit.

Secondly if our emulator is in the RunningState, we display the stop button. We should still implement the RunningState State, which in actual fact is a very simple implementation:

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.

Finally, for any other state we just want to show a play button that is disabled. This will only happen when our Application is loading up and loading the memory image, which is our case is the Test Suite.

Now, we have defined a number of events that we need to listen for in c64_bloc.dart.

Firstly, let us define the listener for RunEvent. This will be the core of our unattended running. Here we want schedule a timer that runs every second and then we also execute a second worth of CPU instructions (aka 1 000 000 CPU cycles). We need to emit a RunningState state so our front end can update accordingly.

Let us start with an outline:

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

Now, to determine when our CPU has executed 1 million cycles worth of instructions, our CPU needs to keep record of the cycles for each of the instructions it executes. This is obviously in the step method:

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

With this implemented, we can add some meat to our timer callback function:

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

Finally, we need to implement the stop event:

    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.

When the emulator runs unattended, we also want to hide the state display to avoid confusion and just show "running". To keep the discussion focused, I will not be going into this detail.

Running the Test Suite

Finally we are at a point where we can run Klaus Dormann's Test Suite. On startup, the screen look like this:

As dicussed, the play button to start the emulator in unattended mode is next to the title.

When clicking play, the screen changes like this:

One weird thing you might notice, is if you click run and quickly stop again, you will see the Program counter is still at 0x400, the starting address of the test suite. As if nothing executed. The reason for this is very subtle. Our timer callback will only execute if the timer lapsed. So, in our case we need to wait at least 1 second to expect some results before clicking the stop button.

So, if we let it run a bit longer, our result will look like this:


So, when stopped our Program counter was at 0x9D7. Funny thing is, you can let it run for long as you want to, but the Program counter remains stuck at 0x9D7.

What is going on here?

To find the answer we need to look at the source listing of the Test Suit and search for that address:


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

Aha! We never implemented IRQ's (Interrupt Requests) in our emulator. Having said that, it briefly caught me in a mystical moment, almost like as a kid and playing on a Commodore 64, I wondered for the first time what was going on underneath the hood.

In this case I wondered where the IRQ came from. This Test Suite doesn't implement any magical peripherals? After a moment I realise that this was probably caused by me not implementing the BRK instruction, and looking further back in the listing did confirm this.

This was actually a very interesting experience for me. It was the first time I encountered a problem, and my first instinct is moment of nostalgia 😂

In the following section we will implement the BRK instruction and then run the emulator again.

Implementing the BRK and RTI instructions

So, let us quickly implement the BRK and RTI instructions. There is one caveat with the BRK instruction. It is a one byte instruction, but in actual fact it behaves like a 2 byte instruction. The BRK triggers an IRQ and when it returns it doesnt return to the address directly after the BRK instruction, but one address further on.

To account for this quirk of the BRK instruction, we can adjust the instruction length in the instructionLen table for the BRK instruction to 2.

With the table adjusted, we implement the BRK and RTI instruction as follows:

      /*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.

We will investigate this failed case, as well as other potential failed cases in the next post.

In Summary

In this post we ran the Klaus Dormann Test Suite on our Emulator in unattended mode. The first failed test case we encountered was the BRK/RTI instruction that wasn't implemented.

With the BRK/RTI instruction implemented we encountered another failed test case which we will investigate in the next post, as well as other potential failed test cases which will pop up.

You can find all the source code for this project as well as the binary image containing the Klaus Dormann test suite, here.

Until next time!

No comments:

Post a Comment