Thursday, 13 October 2016

Part 10: Booting C64 system

Foreword

In the previous post we ran Klaus Test Suite on our Android C64 emulator, and fixed the bugs that surfaced.

In this post we will be booting the C64 system on our Android emulator by loading its two code ROMs, Kernal and BASIC, into memory.

However, before we start with booting the C64 system, I am just briefly going to cover something else: Compacting the code of our instruction decoding switch statement with the help of inlining.

Compacting code with Inlining

If you had been following this series of posts, you might have realised that there is lots of repeating code within the instruction decoding switch statement within cpu.c. From these repeating code includes:
  • Setting the Negative and Zero Flag when the contents of a register or memory locations changes, 
  • masking off the lower 8-bits of a result

@WhiteFlame, one of the members at 6502.org gave a very interesting suggestion on reducing the amount of repeated code involving the use of small utility functions. To avoid the potential overhead of calling these functions lot of times, one can hint the C compiler to inline these functions, that is substituting places where these functions gets called, with the body of the applicable function.

I actually ended off having great fun compacting the code of our Android emulator with inlining. All the code changes I did, I have made available on my GitHub repository.

In this section I will be giving a very brief overview on what I did, just to give a taste of the power of using small utility functions for compressing the code for our emulator.

Here is a quick list most of the utility functions I wrote:

  inline unsigned char setNZ(unsigned char value) {
      value = value & 0xff;
      zeroFlag = (value == 0) ? 1 : 0;
      negativeFlag = ((value & 0x80) != 0) ? 1 : 0;
      return value;
  }

  inline unsigned char resolveRead() {
    unsigned char addressMode = addressModes[opcode];
    if (addressMode == ADDRESS_MODE_IMMEDIATE)
      return (arg1 & 0xff);
    int effectiveAdrress = calculateEffevtiveAdd(addressMode, arg1, arg2);
    return memory_read(effectiveAdrress) & 0xff;
  }

  inline void resolveWrite(unsigned char value) {
    unsigned char addressMode = addressModes[opcode];
    int effectiveAdrress = calculateEffevtiveAdd(addressMode, arg1, arg2);
    memory_write(effectiveAdrress, value & 0xff );
  }

  inline unsigned char shiftLeft (unsigned char value, int shiftInBit) {
    int temp = (value << 1) | shiftInBit;
    carryFlag = ((temp & 0x100) == 0x100) ? 1 : 0;
    temp = temp & 0xff;
    return temp;
  }

  inline unsigned char shiftRight (unsigned char value, int shiftInBit) {
    carryFlag = value & 1;
    int temp = (value >> 1) | (shiftInBit << 7);
    temp = temp & 0xff;
    return temp;
  }


As you can see each of these functions have the word inline in the function signature, hinting the Compiler to inline the function body each place where it is called, if at all possible.

The functionality of these inline functions will become clear in a moment.

Let us work through a couple of examples where these functions are used.

The first example is the LDA instruction:

        case 0xa9:
        case 0xA5:
        case 0xB5:
        case 0xAD:
        case 0xBD:
        case 0xB9:
        case 0xA1:
        case 0xB1:
          acc = setNZ(resolveRead());
        break;

The call to resolveRead() resolves the address for the applicable instruction and returns the value in the resulting address. Please note that in order for the resolveRead() and resolveWrite() functions to work without passing parameters, I had to make the variables opcode, arg1 and arg2 global variables.

If you have a look at the implementation of resolveRead(), you will see that I have also managed to accommodate the immediate mode of the LDA instruction, which is done by just passing the value of arg1.

With the immediate mode also rolled into resolveRead(), we managed to make the opcode a9 also make use of the same case selector. Previously A9 had its own case selector.

The setNZ function takes care of setting the Negative and Zero flag with the value provided and returns the same vlaue, but trunacted to 8 bits. This value gets assigned to the accumulator.

Let us quickly have a look at the STA instruction as the next example:

        case 0x85:
        case 0x95:
        case 0x8D:
        case 0x9D:
        case 0x99:
        case 0x81:
        case 0x91:
          resolveWrite(acc);
        break;


The resolveWrite function receives a value as a parameter, figures out the resulting address and stores the value at that address.

Let us move unto a slightly more complex example, which is the INC instruction for memory:

          case 0xE6:
          case 0xF6:
          case 0xEE:
          case 0xFE:
              resolveWrite(setNZ(resolveRead() + 1));
              break;

We begin by getting the value from the applicable memory location, incrementing it and then passing it to setNZ and finally to resolveWrite().

This whole calling-train reminds me of fluent interfaces in Java where you call a train a methods and each method call returns a value object.

The final set examples we will have a look at, is the rotate instructions. Let us start with these instructions by listing all their implementations:

/*ASL  Shift Left One Bit (Memory or Accumulator)
     C <- [76543210] <- 0             N Z C I D V
                                      + + + - - -
     addressing    assembler    opc  bytes  cyles
     --------------------------------------------
     accumulator   ASL A         0A    1     2
     zeropage      ASL oper      06    2     5
     zeropage,X    ASL oper,X    16    2     6
     absolute      ASL oper      0E    3     6
     absolute,X    ASL oper,X    1E    3     7 */

        case 0x0A:
          acc = setNZ(shiftLeft(acc, 0));
              break;
        case 0x06:
        case 0x16:
        case 0x0E:
        case 0x1E:
             resolveWrite(setNZ(shiftLeft(resolveRead(), 0)));
              break;



/*LSR  Shift One Bit Right (Memory or Accumulator)
     0 -> [76543210] -> C             N Z C I D V
                                      - + + - - -
     addressing    assembler    opc  bytes  cyles
     --------------------------------------------
     accumulator   LSR A         4A    1     2
     zeropage      LSR oper      46    2     5
     zeropage,X    LSR oper,X    56    2     6
     absolute      LSR oper      4E    3     6
     absolute,X    LSR oper,X    5E    3     7 */

        case 0x4A:
          acc = setNZ(shiftRight(acc, 0));
              break;
        case 0x46:
        case 0x56:
        case 0x4E:
        case 0x5E:
             resolveWrite(setNZ(shiftRight(resolveRead(), 0)));
              break;


/*ROL  Rotate One Bit Left (Memory or Accumulator)
     C <- [76543210] <- C             N Z C I D V
                                      + + + - - -
     addressing    assembler    opc  bytes  cyles
     --------------------------------------------
     accumulator   ROL A         2A    1     2
     zeropage      ROL oper      26    2     5
     zeropage,X    ROL oper,X    36    2     6
     absolute      ROL oper      2E    3     6
     absolute,X    ROL oper,X    3E    3     7 */


        case 0x2A:
          acc = setNZ(shiftLeft(acc, carryFlag));
              break;
        case 0x26:
        case 0x36:
        case 0x2E:
        case 0x3E:
              resolveWrite(setNZ(shiftLeft(resolveRead(), carryFlag)));
              break;

/*ROR  Rotate One Bit Right (Memory or Accumulator)
     C -> [76543210] -> C             N Z C I D V
                                      + + + - - -
     addressing    assembler    opc  bytes  cyles
     --------------------------------------------
     accumulator   ROR A         6A    1     2
     zeropage      ROR oper      66    2     5
     zeropage,X    ROR oper,X    76    2     6
     absolute      ROR oper      6E    3     6
     absolute,X    ROR oper,X    7E    3     7  */

        case 0x6A:
          acc = setNZ(shiftRight(acc, carryFlag));
              break;
        case 0x66:
        case 0x76:
        case 0x6E:
        case 0x7E:
              resolveWrite(setNZ(shiftRight(resolveRead(), carryFlag)));
              break;


As you can see, there are four groups of rotate instructions. In the end, however, I found that I could get away by implementing two utility functions for rotates: shiftLeft and shiftRight :-)

The difference between the versions of a shift left instruction is really just the bit that gets shift in. The ASL instruction shifts in a 0 from the right hand side, whereas the ROL shifts in the current Carry value from the right hand side.

With the above mentioned knowledge, one can create a single shiftLeft instruction accepting a shiftInBit as a parameter.

One can apply the same reasoning to the shiftRight Instruction.

Loading ROMs into memory

Let us now start with preparing our Android emulator to boot te C64 system.

In principle the C64 boot process is simple. Just load two ROMS, Kernal and BASIC into its relevant area in memory and just kick off the emulator at a specific address in memory.

A key question at this point will be how to ship these ROMS with an Android application and how to access them when the application is running.

We will now cover the two parts of this question separately.

Shipping the ROMS

To ship the two C64 ROMS with our Android application you need to place them within an asset folder. Here is a quick screenshot on how the asset folder will look like within Android Studio:


If it is a new Android Studio project, you will not have a assets folder. In such a scenario just right click on the app node and select New/Folder/Assets Folder

Once you have created the Assets Folder, you can just copy the two ROMS with the help of a File manager to the Assets Folder.

When you build an Android Project with an Assets folder, all the files contained in it will automatically be copied over to the resulting APK file.

Accessing the ROMS

How do we access the ROMS copied to the Asset Folder within our Android Application?

We access the stored assets via the AssetsManager.

Like the other components of the Android Application Framework, the AssetManager is written mostly in Java.

This means that for our native code to access the AssetManager, we need to make use of JNI. Luckily the NDK provide us with a very nice wrapper for AssetManager that our native code can use.

Despite the wrapper, we still need to write some Java code that injects an AssetManager instance into our native code. So, let us start on the Java side.

First we need to create a native  method stub within our MainActivity class that we will use to inject an Assetmanager instance into our native code:

    private native void loadROMS (AssetManager pAssetManager);

Next let us write code within the onCreate method of MainActivity to perform the injection:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        AssetManager mgr = getResources().getAssets();
        loadROMS(mgr);

        refreshControls();
        //TextView view = (TextView) findViewById(R.id.memoryDump);
        //view.setText(mem.getMemDump());
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }


Next, we should implement the loadROMS method in memory.c:

void Java_com_johan_emulator_MainActivity_loadROMS(JNIEnv* env, jobject pObj, jobject pAssetManager) {
  AAssetManager* assetManager = AAssetManager_fromJava(env, pAssetManager);
  AAsset* assetF = AAssetManager_open(assetManager, "basic.bin", AASSET_MODE_UNKNOWN);
  uint8_t buffer[8192];
  AAsset_read(assetF, buffer, 8192);
  int i;
  for (i = 0xa000; i < 0xc000; i++) {
    mainMem[i] = buffer[i & 0x1fff];
  }
  AAsset_close(assetF);

  assetF = AAssetManager_open(assetManager, "kernal.bin", AASSET_MODE_UNKNOWN);
  AAsset_read(assetF, buffer, 8192);

  for (i = 0xe000; i < 0x10000; i++) {
    mainMem[i] = buffer[i & 0x1fff];
  }
  AAsset_close(assetF);

}


In this method we receive the AssestManager object. We then proceed open both the basic and the kernal ROM and populate the respective areas in memory with their content.

Booting and Testing

We now proceed to boot the C64 system.

On our emulator we just need to specify the correct start address for the system to start.

The start address is stored as a vector in memory locations FFFC/FFFD, with FFFD having the high byte of the start address and FFFC having the low byte of the start address.

With this info at hand, we implement a reset function within cpu.c:

void Java_com_johan_emulator_MainActivity_resetCpu(JNIEnv* pEnv, jobject pObj) 
{
  pc = memory_read (0xfffc) | (memory_read(0xfffd) << 8);
  pc = pc & 0xffff;

}


We call this function within the onCreate method in MainActivity:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        AssetManager mgr = getResources().getAssets();
        loadROMS(mgr);
        resetCpu();

        refreshControls();
        //TextView view = (TextView) findViewById(R.id.memoryDump);
        //view.setText(mem.getMemDump());
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
    }


We are now ready to fire up our emulator again.

Build and install the app on you mobile device.

When the app eventually starts up, the screen will look as follows:


At this point we know that our reset functionality worked ok, because we are at fce2, which is the value stored in the reset vector.

From one of my JavaScript emualtor episodes, here, you will know that hitting RUN at this point will cause our emulator to get stuck in this loop:

FF5E   AD 12 D0   LDA $D012
FF61   D0 FB      BNE $FF5E

To get past this point we need to implement a hack to change the memory location D012 periodically to zero. We do this within the runBatch method in cpu.c:

int runBatch(int address) {
  remainingCycles = 20000;
  int lastResult = 0;
  while ((remainingCycles > 0) && (lastResult == 0)) {
    lastResult = step();
    if (lastResult != 0)
      break;
    if ((address > 0) && (pc == address)) {
      lastResult = -1;
      break;
    }
    memory_write(0xd012, (remainingCycles < 50) ? 0 : 1);
  }

  return lastResult;
}


With this change build and redeploy the app.

When we now run the app for a couple of seconds and then stop, we should see some interesting stuff in screen memory at location 0x420:



OK, we know know for sure that our emulator at least got to the point of showing the welcome message during the booting process.


In Summary

In this post we have implemented the functionality to boot the C64 system.

We ended of booting the system and confirmed that the C64 welcome message got written to screen memory.

In the next post I will be adding a screen to our emulator to display the contents of screen memory together with a flashing cursor.

Till next time!

No comments:

Post a Comment