Tuesday, 22 December 2020

Lunchtime Coder #4-4 - Arcade Space Invaders Emulator - The 8080 Processor Part 2

I'm not going to write a full tutorial on how to write an 8080 processor emulator, as enough of these exist already but if you do want to write a Space Invaders emulator, you will need to know what all of these terms mean:

Program Counter (PC) - Memory pointer. Points to the memory location that code is executing from. Can be a variable that starts at 0.
Read the instruction at zero. Carry out the instruction. Increment PC. Repeat.

Opcode - Instruction for the processor.
Read value at memory_array(PC). Decode the value into an Opcode. Write the equivalent instruction in your programming language. Increment PC. Repeat

Operand - Byte or bytes of data that go with the Opcode. Some opcodes are a single byte instruction. Some come with a byte of data and others come with two bytes of data.
Read value at memory_array(PC). Decode the value into an Opcode. If required Read value at memory_array(PC+1) and/or memory_array(PC+2). Write the equivalent instruction in your programming language. Increment PC (By 1, 2 or 3 depending on number of operands). Repeat

OpBytes - The length of the Opcode including the operand/operands in bytes.
Read value at memory_array(PC). Decode the value into an Opcode. If required Read value at memory_array(PC+1) and/or memory_array(PC+2). Write the equivalent instruction in your programming language. Increment PC Opbytes. Repeat

Registers (A, B, C, D, E, H, L) - Registers are the processors workspace. The 8080 processor has 7 registers. Some can be used in pairs for 16 bit address values. Register A or the accu,ulator is a special register that has additional functionality over the others. These can all be variables in an emulator.

Register Pairs (BC, DE, HL, AF) - As stated above, some of the registers work in pairs for when 16 bit memory values are required.

Conditional Flags - When certain opcodes are executed, its important to know whether the result is 0 ot not, Odd or even, Plus or minus, greater than 255 or less than 0. In these cases, certain checks are done on the result and "Condition" flags are set or unset. Its important to know that if an Opcode requires conditional flags to be set, then they must be set at 1 if condition is met, or 0 if condition is not met, not just set to 1 if condition is met.
There is another conditional flag called Auxilliary Carry. This flag is used in converting hexidecimal values into binary values and is associated with Opcode DAA. Space Invaders doesn't use the Auxilliary Carry flag, but it does use DAA to convert the number of credits into a decimal number.

The Stack Pointer (SP) - The Stack Pointer is initialised by the program on the roms and is an area in the game RAM where the processor can store values that it will need back later e.g. if the program goes jumps to a subroutine, it will need to return back to the address that it has jumped from later, so it stores PC in the stack before it jumps to the sub routine address.

PSW - This is a 16 bit "Register Pair" value made up of the contents of register A, and a byte thats made up of the conditional flag bits and some fixed value bits. This exists so that if the conditional flags and value of register A need storing while a sub routine runs, PSW can be put on the stack.

Opcode Timing - Each Opcode execution takes a number of processor clock cycles to execute. As the space invaders 8080 processor is running at approx. 2 MHz or 2 million clocks per second, its important to count how many clock cycles you have used executing opcodes so that you can time screen updates and interrupts.

16 Bit Shift Register (Not part of the 8080 but required for Space Invaders emulator) - As the processor cannot shift bit around as quick as Space Invaders requires, and external 16 bit shift register is used in conjunction with the processor using Opcodes IN and OUT. This will also need emulating.

Interrupts - Interrupts stop the current program running and execute a different piece of code. In reality the interrupt pin on the processor will be clocked, and if the processor has Enabled Interrupts, then it will stop what its doing and accept an instruction from an external source. For emulation, we can count the clocks used by opcodes, and every time the clocks reach a certain value, carry out an interrupt. Space invaders has two interrupts that are Opcodes RST 1 and RST 2. 2000000 Clocks per second. 60 FPS. 33333 clocks per frame. Two interrupts per frame update (First interrupt a RST 1 and second a RST 2). One interrupt per 16666 clock cycles (If the processor has enabled Interrupts, Opcode EI). Update the screen every two interrupts.

Dip Switches - The Space Invaders board has some Dip Switches on it that set how many lives you have and at what score a bonus life is earned, amongst other things. These are read by the processor using Opcode IN.

In addition to the information above, I used the following resources:

1. List of 8080 Opcodes and what they do.
2. A Table of 8080 Opcodes and associated Opbytes and timings.
3. The Intel 8080 Programming manula. Detailed description of each Opcode.
4. An online javascript emulator that executes an opcode at a time. Very useful for debugging.
5. An explanation of shift register, and how IN and OUT opcodes work.

Top Tips

1. Do not implement any opcode until you have tested it. Write a test program and test each one to ensure that the output you get matches the 8080 manual.

2. Build in a debug log that shows PC / Opcode / SP / Registers / Flags etx. Your emulator output can then be checked against the online JS emulator in the event of a bug.

3. Build in error checks.
a. If the Opcode is one that writes to memory, ensure that its the RAM area, not the ROM area its writing to. This seems to be the most common bug encountered by people. If you don't monitor where the processor is writing, it could be in the wrong location writing over the Space Invaders code.
b. Monitor the size of the stack. If the stack depth gets more that 64 bytes, I would start to get concerned. I had a bug where the Stack Pointer decremented through the whole of the RAM and then into the ROM as the code executed.
c. Check that PC hasn't gone higher than the array allows. Theoretically I don't think PC should ever get higher than the ROM area, but I'm not certain on this.

Efficient Code

There is no commands in Blitz3D to deal with values as binaries. Instead you can convert the decimal value into a binary string, and then manipulate it using BIN$ LEFT$ RIGHT$ MID$ etc. I initially used this method a lot.

When the emulator was running at low speeds, and with some knowledge of what processors could do quickly, I started to suspect that all of the Opcodes with these commands in were slowing the program down, and I was right.

I did some speed tests and found out that a routine that used simple arithmetic and if/then, for/next etc ran around 180 times faster.

I also did some speed tests on other commands to see what was faster. It turns out that a single condition SELECT TRUE is faster that an IF/THEN, But an IF/AND/THEN is faster that a two condition SELECT TRUE. If there are two conditions then the fastest way is a single condition SELECT TRU followed by an IF/THEN. Applying this sped up the emulator 3 fold.

Keep rewriting your Opcodes differently. Try three or four different methods and then run each one in a test program 10000 times and measure how long each one takes, then select the fastest.

Bugs That I Experienced

1. I had one of the Opcodes set to 7 Opbytes instead of 7 clock cycles. This meant that when the Opcode was used and PC incremented, a couple of Opcodes were skipped which lead to all manner of different red herring indications.

2. I had an erroneous credit appear in the middle of the attract mode. Attributed to the above bug.

3. When I implemeted CALL opcode, I put the PC of the CALL opcode onto the stack, rather than incrementing PC to the next Opcode and then putting it on the stack. This meant that the PC jumped to the subroutine, but then on the RTN opcode, wnet back to the CALL opcode and got stuck in an infinite loop.

4. Think 8 bit. Opcode ADD A,B. Add the value in register B to Register A. If the result is higher than 8 bits (higher than 255 decimal) then you need to adjust the value so its a single byte, as in 254, 255, 0, 1, 2, 3 etc. A=A+B. If A>255 then A=A-256.

5. Numerous bugs where values were being written into the ROM area. Any opcode that involves a write to memory, put in a check to ensure that the memory address is above the ROM area.

6. Infinite loop that caused SP to decrement down throguh all of the RAM and into the ROM. Debug log was very useful!

Where Do I Start?

1. Probably the best way to start is by writing a disassembler. Disassemble the code, and the check it against one of the numerous disassemblies on the internet.

2. Implement the processor emulator first without Interrupts or shift registers. Let the emulator tell you what Opcode it wants.

opcode=mem(PC) select true default text 10,10,"Opcode: "+right$(hex$(opcode),2)+" has not yet been implemented." end select

After the first 50 Opcodes that the emulator expects have been implemented, you will find that when you run the emulator, after around 50,000 opcodes it goes into an infinite loop at memory locations 0ada, 0add, 0ade. If you get this far, your emulator is running correctly qnd you are ready to add the interrupts.