Advertisements
Chip8 Emulator running Space Invaders

Write a Chip8 retro gaming emulator in one day. Loading the first ROM.

After the introduction to the Chip8 platform it’s time to start coding the emulator. This post focuses on the choice of the language and some issues faced with it, and the first two building blocks: the RAM and the Interpreter that loads the ROM.

Before we start I want to increase the hype a little bit so here’s a picture of the end result. This took just one day to have up and running. It took a some more time to fix all the bugs, but my patience and ability to stay calm and relaxed definitely improved after that.

Chip8 Emulator running Space Invaders
See the complete code on GitHub

Which language?

Any object oriented language will do. I choose Scala because I’m learning this language and I like it a lot. So I figured writing an emulator was a good exercise. One thing I did not consider when I choose Scala is that it does not support unsigned values. This gave me some headaches converting between Bytes, Shorts and Ints because of the automatic sign extension that Scala performs.
This is an example


val theByte:Byte = 0xF0.toByte
val theShort:Short = theByte.toShort
println("The byte value " + theByte + " was converted to short value: " + theShort)
println("The byte value " + f"$theByte%X" + " was converted to short value: " + f"$theShort%X")

This code will produce the following output

The byte value -16 was converted to short value: -16
The byte value F0 was converted to short value: FFF0

This makes perfect sense in a signed world, the value after the conversion is still the same. However if this happens while we are fetching the bytes of an opcode from memory it can be catasthropic. Sign extension also applies to the left shift binary operator <<, luckily Scala has a second version of this operator which does not perform sign extension <<< so the issue is easily fixed.


val highByte:Byte = 0x01          // assume this was read from memory
val lowByte:Byte = 0xF0.toByte    // assume this was read from memory
val opcode:Short = ((highByte <<< 8) | lowByte).toShort

In this case we would expect opcode to be 0x01F0 but instead we get 0xFFF0, lowByte and highByte both get converted to Int inside the last expression, and therefore they both get sign extended. This is called binary numeric promotion and it is not what I wanted. Scala comes to the rescue by providing implicit conversions. I defined a bunch of implicit conversion functions between frequently used datatypes, these functions take care of doing what I want instead of what Scala wants.


import scala.language.implicitConversions

object Implicits {
  implicit def intToShort(i: Int): Short = {
    val highByte = i & 0xFF00
    val lowByte = i & 0xFF
    (highByte | lowByte).toShort
  }

  implicit def intToByte(i: Int): Byte = {
    (i & 0xFF).toByte
  }

  implicit def shortToByte(i: Short): Byte = {
    (i & 0xFF).toByte
  }

  implicit def byteToShort(i: Byte): Short = {
    (i.toShort & 0xFF).toShort
  }
}

The compiler looks for these methods to perform automatic conversions before doing the default automatic conversions. For me it is as easy as writing


import Implicits._

At the beginning of all my source files.
This solves the first issue. It is very language specific and you might not have to face it if your language supports unsigned types.

To load a ROM you need a RAM

A Chip8 rom is a binary file, the extension is usually ch8. Unlike roms for more advanced system this file does not contain any header or data structure in general. It is just a plain byte array that we have to copy starting at address 0x200 inside the emulated ram. The class Memory in my emulator contains the emulated ram, which is just an array of Bytes. The object Memory is known in Scala as a companion object. It is the equivalent of a Java static class and it is useful for defining constants that are not bound to a specific instance of the Memory class.


object Memory {
  // Size of the RAM in bytes
  val RAM_SIZE = 4096
  // A constant to specify the start of program execution
  val PROGRAM_START = 0x200
}

class Memory {
  private val ram = Array.ofDim[Byte](Memory.RAM_SIZE)
  private var storingPointer:Short = Memory.RAM_SIZE
  
  // Start bulk transfer of bytes
  def ramStartStoring(from:Short) : Unit = {
    storingPointer = from
  }

  // Finalize bulk transfer of bytes
  def ramFinishStoring() : Unit = {
    storingPointer = Memory.RAM_SIZE
  }

  // Store one byte
  def ramStoreByte(byte:Byte) : Unit = {
    ram(storingPointer) = byte
    storingPointer += 1
  }

  // Read one byte
  def ramReadByte(address:Short) : Byte = {
    ram(address)
  }

  // Read two bytes
  def ramReadTwoBytes(address:Short) : Short = {
    val highByte = ram(address)
    val lowByte = ram(address + 1)
    (((highByte << 8) & 0xFF00) | (lowByte & 0xFF)).toShort
  }
}

It is very simple code. I decided to implement some logic to support bulk transfers inside the memory. For this I use a storingPointer that is automatically incremented during bulk transfer. The only reason for this is to simplify the usage of the Memory class when storing a lot of bytes, without the bulk transfer support I would have to keep a pointer and specify the destination address every time. With the bulk transfer support everything is easier since I just call ramStoreByte(value) several times and I don’t have to worry about pointers. However I have to remember to call ramStartStoring(address) at the beginning of the transfer.

The Interpreter

With the RAM in place I can now move on to loading the ROM inside it. For this I created another class called Interpreter. This loosely represents the real Chip8 interpreter and it is responsible for loading the system default fonts and for reading the rom file and copying it at the correct address.
The system default font is a set of 16 sprites, 0 to 9 and A to F. They are 8 pixels wide and 5 pixels tall. Each sprite is a sequence of bytes in a Byte array inside the Interpreter class.


private val fonts = Array[Byte](
    // 0
      Integer.parseInt("01100000", 2).toByte,
      Integer.parseInt("10010000", 2).toByte,
      Integer.parseInt("10010000", 2).toByte,
      Integer.parseInt("10010000", 2).toByte,
      Integer.parseInt("01100000", 2).toByte,
    // 1
      Integer.parseInt("00100000", 2).toByte,
      Integer.parseInt("01100000", 2).toByte,
      Integer.parseInt("00100000", 2).toByte,
      Integer.parseInt("00100000", 2).toByte,
      Integer.parseInt("01110000", 2).toByte,
      ...
)

The first task of Interpreter is to load the font in memory. We are free to load these sprites at any location which is not occupied by the program. This is a perfect use case for those first 512 unused bytes. I load the font at address 0.


def loadSelf() : Unit = {
    memory.ramStartStoring(0)
    for (v <- fonts) memory.ramStoreByte(v)
    memory.ramFinishStoring()
}

The second task of Interpreter is to load the ROM. This is easily done by reading the ROM file and copying every single byte in RAM starting from address 0x200. So I created another method inside Interpreter that does just this. I won’t paste the code here because it is a long method and nothing really interesting is happening there. You can have a look at it on GitHub. It is the loadProgram(path) method inside the Interpreter class.

Loading the ROM

I can now create the basic code that initializes the memory and loads the interpreter plus the ROM. This is the code inside my Main object. Main object is again a Scala construct, it is simply the entry point of the emulator.


// Just some constants
object EmulatorParameters {
  val NAME = "Chip8 Emulator"
}

println("+++ " + EmulatorParameters.NAME + " started +++")
// Create the Memory
val memory = new Memory

// Create the Interpreter and load the fonts
println("Loading interpreter")
val interpreter = new Interpreter(memory)
interpreter.loadSelf()

val programName = "rom.ch8"

println("Loading '" + programName + "'")
val programSize = interpreter
    .loadProgram(programName)
    .getOrElse {
      System.exit(1)
      0
}
println("Loaded " + programName + " : " + programSize + " bytes" )

Conclusion

So far I created the Memory and the Interpreter, the first two basic blocks of my emulator. We can now start the emulator and load a rom file at the right place. In the next post I will create the Controller and the Display. These will be the last two building blocks before moving on to the core of the system, the CPU. After that the emulator will be finally ready.

Unique opportunity! Help a fellow grow his blog!

Hi there! If you’ve read this far maybe you think this was useful, or fun, or I don’t know what but for some reason You Got Here! Great! Please consider sharing this post with your network, I am trying to get The Code Butchery to grow so I can provide more content like this, will you help me in my journey? Thank you!

Series Navigation<< Write a Chip8 retro gaming emulator in one day. Introduction.Write a Chip8 retro gaming emulator in one day. Controller and Display. >>
Advertisements
Share this

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.