Full 8080 emulation

A piece of hard-learned advice: Don't implement instructions you can't test. This is a good rule of thumb for any software development you do. If you don't try it, it is going to be broken. The further away you get from implementing it the harder it will be to find problems.

There is another option if you wanted to do a complete 8080 emulator and make sure it works. I found a piece of 8080 code called cpudiag.asm that's designed to test every instruction on an 8080 CPU.

There are a couple of reasons that I presented this process after the first one:

  1. I wanted the description of this process to be repeatable for another processor. I don't think a cpudiag.asm equivalent exists for every processor.

  2. The process is kind of fiddly as you'll see. I think a beginner to debugging assembly code would have a hard time doing this without being given the specific steps I'll present here.

This is the how I used that test with my emulator. Maybe you can use it, or figure out an even better way to integrate it.

Building the test

I tried a couple of things but ended up using this neat page. I pasted the cpudiag.asm text into the left pane, and it built without issue. It took me a minute to figure out how to download the result, but clicking on the "Make Beautiful Code" button on the bottom left downloaded a file called test.bin which is the compiled 8080 code. I was able to verify that using my disassembler.

Download cpudiag.asm mirrored on this site

Download cpudiag.bin, the 8080 compiled code of cpudiag.asm from this site

Loading the test into my emulator

Instead of loading the invaders.* files, I load up this binary.

There are a few wrinkles. First, the original assembly code has a ORG 00100H line in it, which means that the entire file is compiled assuming the first line of code is at 0x100 hex. I have never coded in 8080 assembly before so I didn't know what that line did. It only took a minute to figure out that all the branch targets were wrong and it needed to be in memory starting at 0x100.

Second, since my emulator just starts from zero, I need to make the first thing that happens a jump to the real code. Jamming the hex for JMP $0100 into memory at zero takes care of that. (I could have also just initialized my PC to 0x100.)

Third is that I found a bug in the assembled code. I think the cause might be improper handling of the last line of code STACK EQU TEMPP+256 but I'm not sure. At any rate, as compiled the stack is at $6ad, and the first few PUSHes start to overwrite the code. I assume that the variable just needs to be offset by 0x100 like the rest of the code, so I fix that up by jamming "0x7" into the line of code that initializes the stack pointer.

Lastly, since I didn't implement DAA or auxillary carry in my emulator, I am modifying the code to skip that test (just JMP over it).

    ReadFileIntoMemoryAt(state, "/Users/kpmiller/Desktop/invaders/cpudiag.bin", 0x100);    

    //Fix the first instruction to be JMP 0x100    
    state->memory[0]=0xc3;    
    state->memory[1]=0;    
    state->memory[2]=0x01;    

    //Fix the stack pointer from 0x6ad to 0x7ad    
    // this 0x06 byte 112 in the code, which is    
    // byte 112 + 0x100 = 368 in memory    
    state->memory[368] = 0x7;    

    //Skip DAA test    
    state->memory[0x59c] = 0xc3; //JMP    
    state->memory[0x59d] = 0xc2;    
    state->memory[0x59e] = 0x05;    

Test tries to print

Apparantly this test assumes some help from the CP/M OS. I deduce that CP/M has some code at address $0005 that prints messages to the console. I modified my CALL emulation to handle that. I'm not sure I got that exactly right, but it does work for the 2 messages that this program tries to print. So my CALL emulation to run this test looks like this:


        case 0xcd:                      //CALL address    
   #ifdef FOR_CPUDIAG    
            if (5 ==  ((opcode[2] << 8) | opcode[1]))    
            {    
                if (state->c == 9)    
                {    
                    uint16_t offset = (state->d<<8) | (state->e);    
                    char *str = &state->memory[offset+3];  //skip the prefix bytes    
                    while (*str != '$')    
                        printf("%c", *str++);    
                    printf("\n");    
                }    
                else if (state->c == 2)    
                {    
                    //saw this in the inspected code, never saw it called    
                    printf ("print char routine called\n");    
                }    
            }    
            else if (0 ==  ((opcode[2] << 8) | opcode[1]))    
            {    
                exit(0);    
            }    
            else    
   #endif    
            {    
            uint16_t    ret = state->pc+2;    
            state->memory[state->sp-1] = (ret >> 8) & 0xff;    
            state->memory[state->sp-2] = (ret & 0xff);    
            state->sp = state->sp - 2;    
            state->pc = (opcode[2] << 8) | opcode[1];    
            }    
                break;    

I found several problems in my emulator using this test. I'm not sure that any of them would be exposed by the game, but if they were it would be very difficult to find them.

I went ahead and implemented all the opcodes (except for DAA and friends). Fixing problems in my calls and implementing the new ones only took me 3 or 4 hours. This was definitely faster than the manual process I described earlier - I spent more than 4 hours doing the manual process before I found this test. If you can follow this explaination, I'd recommend using this method instead of a manual comparison. Knowing the manual process is great though, if you want to emulate another processor, you may have to fall back on it.

If you need help through this process, leave a comment and I'll help the best I can.

If you can't follow this or it looks too hard, you definitely will want to do something like I described earler with 2 different emulators running inside your program. Once you get a few million instructions into the program and have interrupts enabled, it will be impossible to do manual comparison of 2 emulators.

← Prev: finishing-the-cpu-emulator   Next: displays-and-refresh →


Post questions or comments on Twitter @realemulator101, or if you find issues in the code, file them on the github repository.