Sweet16 CPU Emulator

by SysOps
This entry is part 3 of 8 in the series Sweet16-GP CPU: A Complete Development Cycle

Part 3

Last time we left off with a skeleton framework of housekeeping routines for our emulator. I hope you played around with it a bit. The framework was quite capable of fetching instructions from memory and incrementing the program counter (PC). Take the final code from the last post and modify the last block of code to call the run() method on the CPU.

if __name__ == '__main__':
    # This method is for simple testing
    # demonstration purposes.
    cpu = Sweet16GP()
    cpu.set_register(0, 0xffae)
    cpu.poke_byte(0, 0xbc)
    cpu.poke_byte(1, 0xd7)
    cpu.poke_word(4, 0xabcd)
    cpu.run()
    cpu.dump()

Notice I added a call to cpu.run() just before the call to cpu.dump(). If you run this you should see the following in the output:

 PC: 0x0, cur_instr: 0xbc
 PC: 0x1, cur_instr: 0xd7
 PC: 0x2, cur_instr: 0x0
 PC: 0x3, cur_instr: 0x0
 PC: 0x4, cur_instr: 0xcd
 PC: 0x5, cur_instr: 0xab
 PC: 0x6, cur_instr: 0x0
 PC: 0x7, cur_instr: 0x0
 PC: 0x8, cur_instr: 0x0
 PC: 0x9, cur_instr: 0x0
 PC: 0xa, cur_instr: 0x0
 PC: 0xb, cur_instr: 0x0

You will need to kill this program with cntrl+C or it will run forever. 

Take a good look at the output. You can clearly see our little CPU is fetching data from memory, incrementing the PC to point to the next instruction and then fetching that instruction. This loop continues forever since we have nothing to stop it. 

To solve this we need to first implement the instruction decoding. Then we’ll implement the HALT instruction. Recall that we had two basic types of instructions in our instruction set. They were register and non register instructions. Our first step in decoding our instruction will be implement code to decide which group the current instruction is in. Then, we will call the appropriate method to process that instruction. If you go back to the first post you will see that our instruction opcodes were designed so that all non register instructions lay in the range of 0 to 7. This means we use the lower 3 bits of the opcode for non register instructions if and only if the remaining upper 5 bits are zero. Now this may sound a little complicated but in fact it’s very simple. The maximum value that can be stored in the lower 3 bits is 7. I hope you know binary and can at least calculate the decimal values for binary values up to 8 bits. If you work with hardware this is a very good skill to have! 

So implementing code to test which group the current instruction is in is as simple as testing if the opcode value is greater than 7. If it is, we have some work to do. If not, we just call a subroutine to process the correct instruction.  See the following code:

# Instruction Decoding
def decode_inst(self, instr: int):
    if instr > 7:
        opcode = instr & 0b11111000
        reg_id = instr & 0b00000111
        self.decode_register_instr(opcode, reg_id)
    else:
        self.decode_non_register_instr(instr)

If our opcode value is greater than 7 then we need to mask off the lower 3 bits and store them as the register id, and use the remaining upper bits to indicate the instruction to be performed. 

Now that we can decode the instructions into the proper group, we need to implement the two group decoding methods, decode_register_instr() and decode_non_register_instr(). 

Since our next step will be to handle execution of our first instruction HALT, we will add code to the decode_non_register_instr() method to test for the opcode value 0x00 which is the HALT instruction. We’ll make a call to an exec_halt() method which we’ll implement next.

 

def decode_register_instr(self, opcode: int, reg_id: int):
    pass

def decode_non_register_instr(self, opcode: int):
    if opcode == 0x00:
        self.exec_halt()

It really is just this simple. Now let’s implement out exec_halt() method.

def exec_halt(self):
    self.halt_flag = True

Recall that in our run() method we test to see if the halt_flag was true. This is where we set it to true. Which causes the while loop in run() to exit. Add these methods to your skeleton code and run it. Now, the first time the CPU pulls a 0x00 value from ram it should stop execution and the dump() command should be called.  Your output should look something like the following:

 PC: 0x0, cur_instr: 0xbc
 PC: 0x1, cur_instr: 0xd7
 PC: 0x2, cur_instr: 0x0


 Sweet16-GP CPU
 Status Flags: 0b0000
...

We are now well on our way to having a functioning Sweet16-GP emulator, but we have a lot of instructions to implement. For now, we’ll skip implementing any further non register instructions, as they are mostly branch instructions and they don’t make sense if you have nothing to test for the branch condition. So, let’s move on to implementing a register type instruction.  For the moment if the CPU encounters an instruction with an opcode value in the range of 1 to 7, the CPU will simple decode it and return without trying to execute it. 

OK, time to move on to our first register type instruction. Our first register instruction opcode is 0x08. This is the opcode for the SET Rn instruction. Just as we did with the non register instruction, we will first implement a test for this opcode and then dispatch a call to an exec_set() method to perform the actual instruction tasks. One difference here is that we not only need the opcode, we need the register as well. This is why we broke these two apart in the decode_instr() method. We could have done this in the decode_register_instr() method but it felt best to keep it in the initial decode method. So let’s implement this:

def decode_register_instr(self, opcode: int, reg_id: int):
    if opcode == 0x08:
        self.exec_set(opcode, reg_id)
    else:
        print(f'Unknown OpCode code {opcode}')
        self.halt_flag = True

Simple, right! If the opcode is one we support, then we make a call to exec_<instruction_name>. Otherwise we simply print a message and halt.

Now we need to implement the exec_set() method. Looking at the description of the SET instruction we see: 

“The two byte constant (c-low, and c-high) is loaded into Rn (n = 0 – 7) and branch conditions are set to represent Rn’s new value.”

So this instruction takes three bytes in memory. The first byte is the opcode, the second is the lower order byte of the value to be loaded and the third is the high order byte of the value to be loaded into the register Rn. Also, don’t forget we need to set the status flags to reflect the value in Rn after the load. Here’s the code:

def exec_set(self, opcode: int, reg_id: int):
    print(f'PC: {self.get_register(self.PC)}')
    self.inc_register(self.PC)
    lo = self.peek_byte(self.get_register(self.PC))
    self.inc_register(self.PC)
    hi = self.peek_byte(self.get_register(self.PC))
    val = (hi << 8) + lo
    self.set_register(reg_id, val)
    self.set_value_flags(val)
    self.clear_flags('C')

You’ll notice that we need to increment the PC to point to the first byte of the two byte value. Then we use peek_byte() to get the value and assign it to the variable lo. Next, we increment the PC again to point to the high byte of the two byte value and again use peek_byte() to get the value and store it away in hi. 

Next, we have to do a little work to put the pieces back together in the proper order. This is simple as we only need to shift the high order byte to the left byte 8 bits. This analogous to taking the value 1010 and breaking it into two parts each of 10 and then putting the pieces back together to form the original value. Finally we take the combined value and store it in val and use val to set the register value and pass it to set_value_flags() which will set the proper status flags for us. Then we make a call to clear_flags() passing in ‘C’ to indicate we want to clear the carry flag. This is needed to ensure no other operations set the carry flag prior to us getting here. 

Update your code and let’s run a little test. First, we’ll add a new function main(). We’ll move the code from the last bloc into main and make a few simple edits. Then we’ll call main from the if __name__ == ‘__main__’ block as below:

def main():
    cpu = Sweet16GP()
    cpu.poke_byte(0, 0x08)
    cpu.poke_byte(1, 0x05)
    cpu.poke_byte(2, 0xff)
    cpu.run()
    cpu.dump()

if __name__ == '__main__':
    # This method is for simple testing
    # and demonstration purposes.
    main()

While this gives us a way to run a quick test, don’t think for a moment that this quick demo code replaces real unit tests. It just makes development easier. Now run this. You should get 

 PC: 0x0, cur_instr: 0x8
 PC: 0
 PC: 0x3, cur_instr: 0x0

 Sweet16-GP CPU
 Status Flags: 0b10000000
 N 

 Registers
 ACC (R00): 0xff05,  R01: 0x0000,  R02: 0x0000,  R03: 0x0000,  
 RETPTR (R04): 0x0000,  COMPARE (R05): 0x0000,  STATUS (R06):0x0080,  PC (R07): 0x0004, 
 
 Addr   Data                                  MEMORY DUMP
 0x0000:  0x08, 0x05, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
 ...

Let’s break this down. First, we use the poke_byte() method to place our instruction and data into memory. The 0x08 value placed at memory address 0x0 is the opcode for the SET instruction. The instruction requires a two byte value to follow in the next two addresses. So we poke the lower order byte of 0x05 into the next location followed by the high order byte 0xff in the following location. Then we call run() and run grabs the first byte at address 0x0 from memory. It then passes it to decode_instr() which then determines this is a register type instruction and calls decode_register_instr(). register_instr() then checks the opcode value and calls exec_set() passing the register id for the register to load the value into. Then the method exec_set()  increments the PC and gets the next byte in memory which stores the low order byte of the value. Again, we increment  the PC to get the high order byte and combine the two making sure to put them back together in the proper order. Finally, we set and clear the status flags and return. The PC now points to the next instruction which happens to be 0x00 a HALT instruction. The process repeats, only this time calling exec_halt() which sets the halt flag and stops CPU execution. The program then goes on to dump the CPU and memory for inspection. 

Congratulate yourself! Your emulated CPU just executed it’s first program! 

Ok, now remove the print statement from the exec_set() method and rerun the program. You’ll see you lost the second line of output. It was only there to aid us in debugging. But we need to remove it now to clean up the code.

Our next task is to implement the remaining methods for each register type instruction. In each case, we will need to hand assemble some short test code and poke it into ram for testing. You’ll want to have the list of all the instructions and their descriptions and opcode values handy for reference as we implement the remaining CPU instructions. As we walk through implementation you’ll start to see a pattern in the opcode encoding for the register type instructions. This is no accident. The original Sweet16 made use of a pattern in the instruction codes to make it easy to assemble instructions in your head. While the GP’s encoding isn’t exactly the same, it follows a very nice pattern and before you’re done with the emulator you’ll be assembling instructions on the fly without the need for an assembler. However, we will implement an assembler once we complete the emulator.

Our next instruction to implement is the LD Rn instruction. It’s description is as follows:

The ACC (R0) is loaded with the value contained in Rn. Branch conditions are changed to reflect the new value stored in ACC.

Pretty simple. First we need to update the decode_register_type_instr() to test for the proper opcode. Then we need to write the exec_ld() method. Try your hand at writing the code for this. You should end up with something like this:

def decode_register_instr(self, opcode: int, reg_id: int):
    if opcode == 0x08:
        self.exec_set(opcode, reg_id)
    elif opcode == 0x10:
        self.exec_ld(opcode, reg_id)
    else:
        print(f'Unknown OpCode code {opcode}')
        self.halt_flag = True
...

def exec_ld(self, opcode: int, reg_id: int):
    val = self.get_register(reg_id)
    self.set_register(self.ACC, val)
    self.set_value_flags(val)
    self.clear_flags('C')

Ok, try completing the rest of the register type instructions yourself. 

 

We’ve accomplished quite a bit today. You can now see how all the pieces of the emulator fit together. Given a different instruction set, I bet you could write an emulator for it following the same patterns and principles laid out here. 

Next time I’ll present my code for all the register type instructions and then we will start working on the non register instructions.  Until then, try to implement all the register type instructions yourself. Most importantly, have fun with it!

 

Series Navigation<< Sweet16 CPU EmulatorSweet16 CPU Emulator >>