7
\$\begingroup\$

I am trying to emulate a basic CPU (Z80) as close as possible. It currently does not read real assembly code, but that will be implemented. If you have any views on how that could be implemented, I'd really appreciate it. Instead of reading real assembly code, I made up a simpler version with the knowledge I had at the time, so it is somewhat compressed to save space.

I am hoping to fully emulate both the RAM and the stack and as you can see I have started on that.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define INC 0b0001
#define INCREMENT 0b00010001                        // INC A            
#define DEC 0b0010
#define DECREMENT 0b00010010                        // DEC A            
#define ADD 0b0011
#define ADD_FIXED_WITH_REGISTER 0b00010011            // ADD A, 255     
#define ADD_REGISTER_WITH_REGISTER 0b00110011         // ADD A,B        
#define SUB 0b0100
#define SUB_FIXED_WITH_REGISTER 0b00010100            // SUB A, 255     
#define SUB_REGISTER_WITH_REGISTER 0b00110100         // SUB A,B        
#define LD  0b0101
#define LOAD_FIXED_TO_REGISTER 0b00010101           // LD A, 255        
#define LOAD_REGISTER_TO_REGISTER 0b00110101        // LD A, B          
#define LOAD_FIXED_MEMORY_TO_REGISTER 0b10010101    // LD A, (255)      
#define LOAD_REGISTER_TO_REGISTER_MEMORY 0b01110101 // LD (A), B        
#define LOAD_FIXED_TO_REGISTER_MEMORY 0b01010101    // LD (A), 255      
#define LOAD_REGISTER_TO_FIXED_MEMORY 0b01100101    // LD (255), A      
#define LOAD_FIXED_TO_FIXED_MEMORY 0b01000101       // LD (255), 255    
#define LOAD_REGISTER_MEMORY_TO_REGISTER 0b10110101 // LD A, (B)        
#define AND 0b0110
#define AND_A_WITH_REGISTER 0b00010110              // AND B            
#define OR 0b0111
#define OR_A_WITH_REGISTER 0b00110111               // OR B             
#define XOR 0b1000
#define XOR_A_WITH_REGISTER 0b00111000              // XOR B            
#define JUMP 0b1001
#define JUMP_TO_ADDRESS 0b00001001
#define JUMP_USING_MEMORY 0b00111001
#define PUSH 0b1011
#define PUSH_INTO_STACK 0b00011011
#define POP 0b1100
#define POP_INTO_REGISTER 0b00011100


typedef char bool;
#define true 1
#define false 0

#define byte char

unsigned byte ROM[] = { // Code to run
    LOAD_FIXED_TO_REGISTER, 0, 1,
    JUMP_TO_ADDRESS, 9,
    XOR_A_WITH_REGISTER, 0,
    JUMP_TO_ADDRESS, 17,
    INCREMENT, 0,
    POP_INTO_REGISTER, 1,
    JUMP_TO_ADDRESS, 5
};

unsigned byte RAM[255]; // 16 bytes of RAM
//Registers
unsigned int SP = sizeof(RAM)-1;  // Stack pointer
unsigned int PC = 0;    // Program counter
unsigned byte F = 0;    // Flags
unsigned byte A = 0;    // Accumulator
unsigned byte B = 0;
unsigned byte C = 0;
unsigned byte D = 0;
unsigned byte E = 0;
unsigned byte H = 0;
unsigned byte L = 0;

void run();

int main() {
    run();
    return 0;
}

void run() {
    byte *Aaddress = &A; // where is the A register in RAM?
    byte *Memstart = RAM[0]; // Where does the virtual RAM start in real RAM?
    int end = sizeof(ROM); // How much memory does the code take up? (in bytes)
    while (PC<end) {
        byte *memto = &A; // Everything goes to A register unless specified
        byte *memfrom = &A; // Will always overwrite when using
        byte usedbytes = 2; // How many bytes this command used

        unsigned byte opcode =   ROM[PC] & 0xF; // First nibble is opcode/instruction
        // Second nibble is opcode's flags
        bool firstregister =    readBit(&ROM[PC], 4);
        bool secondregister =   readBit(&ROM[PC], 5);
        bool firstpointer =     readBit(&ROM[PC], 6);
        bool secondpointer =    readBit(&ROM[PC], 7);
        unsigned byte firstoprand =   ROM[PC+1]; // First instruction parameter/oprand
        unsigned byte secondoprand =  ROM[PC+2]; // Second instruction parameter/oprand

        // Decide what two bytes to use in physical RAM
        switch(opcode) {
            case INC:
                if (firstregister) {
                    memto += firstoprand;
                }
                break;
            case DEC:
                if (firstregister) {
                    memto += firstoprand;
                }
                break;
            case ADD:
                if (firstregister) {
                    memto += firstoprand;
                }
                if (secondregister) {
                    memfrom += secondoprand;
                } else if (secondpointer) {
                    memfrom = Memstart+secondoprand;
                } else {
                    memfrom = &secondoprand;
                }
                usedbytes = 3;
                break;
            case SUB:
                if (firstregister) {
                    memto += firstoprand;
                }
                if (secondregister) {
                    memfrom += secondoprand;
                } else if (secondpointer) {
                    memfrom = Memstart+secondoprand;
                } else {
                    memfrom = &secondoprand;
                }
                usedbytes = 3;
                break;
            case AND:
                if (firstregister) {
                    memfrom = Aaddress+firstoprand;
                }
                break;
            case OR:
                if (firstregister) {
                    memfrom = Aaddress+firstoprand;
                }
                break;
            case XOR:
                if (firstregister) {
                    memfrom = Aaddress+firstoprand;
                }
                break;
            case LD:
                if (secondregister) {
                    if (secondpointer) {
                        memfrom = &RAM[*(Aaddress+secondoprand)];
                    } else {
                        memfrom = Aaddress+secondoprand;
                    }
                } else {
                    if (secondpointer) {
                        memfrom = &RAM[secondoprand];
                    } else {
                        memfrom = &secondoprand;
                    }
                }
                if (firstregister) {
                    if (firstpointer) {
                        memto = &RAM[*(Aaddress+firstoprand)];
                    } else {
                        memto = Aaddress+firstoprand;
                    }
                } else {
                    if (firstpointer) {
                        memto = &RAM[firstoprand];
                    } else {
                        memto = &firstoprand;
                    }
                }
                usedbytes = 3;
                break;
            case JUMP:
                if (firstpointer) {
                    memfrom = &RAM[firstoprand];
                } else {
                    memfrom = &firstoprand;
                }
                break;
            case PUSH:
                if (firstregister) {
                    memto = &RAM[SP];
                    memfrom = &firstoprand;
                }
                break;
            case POP:
                 if (firstregister) {
                     memfrom = &RAM[SP+1];
                     memto = Aaddress+firstoprand;
                 }
                break;
        }

        // Do stuff with memory
        switch(opcode) {
            case INC:
                *memto = *memto+1;
                clearBit(&F, 1);
                if (*memto == 0)
                    setBit(&F, 6);
                break;
            case DEC:
                *memto = *memto-1;
                clearBit(&F, 1);
                if (*memto == 0)
                    setBit(&F, 6);
                break;
            case ADD:
                *memto += *memfrom;
                clearBit(&F, 1);
                break;
            case SUB:
                *memto -= *memfrom;
                clearBit(&F, 1);
                if (*memto == 0)
                    setBit(&F, 6);
                break;
            case AND:
                A = A & *memfrom;
                clearBit(&F, 0);
                clearBit(&F, 1);
                if (A == 0)
                    setBit(&F, 6);
                break;
            case OR:
                A = A | *memfrom;
                clearBit(&F, 0);
                clearBit(&F, 1);
                if (A == 0)
                    setBit(&F, 6);
                break;
            case XOR:
                A = A ^ *memfrom;
                clearBit(&F, 0);
                clearBit(&F, 1);
                if (A == 0)
                    setBit(&F, 6);
                break;
            case LD:
                *memto = *memfrom;
                break;
            case JUMP:
                RAM[SP] = PC + usedbytes;
                SP--;
                PC = *memfrom - usedbytes;
                break;
            case PUSH:
                *memto = *memfrom;
                SP--;
                break;
            case POP:
                *memto = *memfrom;
                SP++;
                break;
        }

        PC += usedbytes;
    }
}

I would love to get this more condensed and/or use less variables during the interpretation stage. This is a purely for education and to learn how low I can get the CPU cycles, so any bitwise operators would help a lot.

FYI, I have removed all the debugging code, otherwise the paste would be huge. I am also using CodeBlocks IDE and GCC compiler.

\$\endgroup\$

2 Answers 2

4
\$\begingroup\$

There are a few things which jumped out at me:

  1. The biggest issue I have is that your code assumes that this:

    unsigned byte A = 0;    // Accumulator
    unsigned byte B = 0;
    unsigned byte C = 0;
    unsigned byte D = 0;
    unsigned byte E = 0;
    unsigned byte H = 0;
    unsigned byte L = 0;
    

    will guarantee you that &A + 1 will point to B which it doesn't. It might work or it might not. Your registers should be an array instead.

  2. Your machine state is a set of global variables. I'd actually consider encapsulating this in a struct like this:

    struct Machine
    {
        unsigned byte RAM[255];
    
        unsigned int SP;
        unsigned int PC;
        unsigned byte Flags;
    
        unsigned byte registers[7];
    }
    
    // some readability/convenience definitions for register file 
    static const int B  = 0;
    static const int C  = 1;
    static const int D  = 2;
    static const int E  = 3;
    static const int H  = 4;
    static const int L  = 5;
    static const int HL = 6;
    static const int A  = 7; // as per http://www.z80.info/zip/z80.pdf the accumulator is actually the last register.
    
  3. This statement:

    unsigned byte secondoprand =  ROM[PC+2]; // Second instruction parameter/operand
    

    has a subtle bug: For the last instruction in the ROM this will read one byte past the array if the last instruction only has one operand. Depending on memory layout and compiler this could lead to an access violation - although in most cases it will just silently work and read garbage. It probably won't matter in your case it's still not very nice.

  4. Given that there are only 256 opcodes I'd consider creating a jump table for all the opcodes (like the main table here) with a function pointer to execute it. The function pointer could take a pointer to the current machine and a pointer to the current instruction stream and return the pointer to the next instruction to allow the function to read additional bytes required for the operation. Then your run function could look like this:

    typedef char *(*OpExecFunc)(stuct machine *, char *);
    
    OpExecFunc opcodes[] = {
         ..
    };
    
    
    void run(struct machine *machine, char *instructions)
    {
         char *current_op = instructions;
         while (*current_op) // assume NULL terminated instruction stream
         {
             current_op = opcodes[*current_op](machine, current_op);
         }
    }
    

    So basically

    • Read the current opcode
    • Call the handler for that opcode
    • The handler needs to read additional bytes from the instruction stream if required and return the pointer to the next opcode following it
    • If any of the handlers run into an error they can return NULL which will terminate the run loop. They could set an error state with a message explaining what is wrong.
\$\endgroup\$
4
\$\begingroup\$

It currently does not read real assembly code, but that will be implemented.

Real assembly code might look like this:

ld a,%11010011                 ; A=%11010011, carry=?
sla a                          ; A=%10100110, carry=1
rla                            ; A=%01001101, carry=1
rlca                           ; A=%10011010, carry=0
sra a                          ; A=%11001101, carry=0
rra                            ; A=%01100110, carry=1

To do this code you'd do it in two parts:

  1. Write an "assembler", which parses the assembly and emits machine machine code
  2. Write a CPU emulator as you have done (or manufacture Z80 hardware), which runs the machine code

You have the second part (the CPU emulator) already, so the new software would be an assembler.

An assembler is like a compiler, but for assembly language. Theoretically you could instead write software to parse-and-run assembly language in one step instead of two, like an "interpreter" instead of a "compiler"; but that (parsing assembly language at run-time) should be much slower to run: therefore, assembling to machine code is the usual first step.

Pseudocode for an assembler might be something like:

  • for each line in the assembly source input file
    • trim leading and trailing whitespace and comments
    • extract the first string (which identifies the opcode)
    • switch on the opcode string (for example, case "ld":)
    • emit (to the machine code output file) the opcode
    • parse the remainder of the line (for example, a,%11010011) and emit machine code to represent these operands

A helpful feature of assembly instead of machine code is that assembly contains named labels and subroutine names, for example here:

  cp $80                         ; comparing the unsigned A to 128
  jr c,A_Is_Positive             ; if it is less, then jump to the label given
  neg                            ; multiplying A by -1
A_Is_Positive:                   ; after this label, A is between 0 and 128

A label will correspond to a location in memory (in the machine code).

You may need a "two-pass" assembler. For example, a statement might jump forward to a label which hasn't been defined yet. You need to assemble future statements (until you find the label and know where it is in memory), and then come back and fix (write the label's memory address to) to the operand of your earlier jump instruction.

You load the assembler's output file (machine code) as the input file to your emulator (i.e. the "ROM" variable in your program).


I am hoping to fully emulate both the RAM and the stack and as you can see I have started on that.

Woohoo: 16 bytes of RAM! "Surely, that will be enough for anyone."

unsigned byte RAM[255]; // 16 bytes of RAM

If you want more than that, you can allocate it using malloc or calloc.

RAM = malloc(1024*8);

I would love to get this more condensed and/or use less variables during the interpretation stage.

One idea is to define those variables as inline functions; for example, instead of ...

bool firstregister =    readBit(&ROM[PC], 4);

... try ...

inline bool firstregister() { return readBit(&ROM[PC], 4); }

In that case you code would like like:

        case INC:
            if (firstregister()) {
                memto += firstoprand();
            }
            break;

The advantage of function instead of variables is that if the opcode doesn't use one (as, for example, the INC opcode doesn't use secondoprand) then the sace statemet for that opcode don't call that secondoprand() function and therefore doesn't spend time calculating its value.

Where are your readBit and clearBit functions defined?


Another idea is to replace your switch(opcode) statement with a jump table.

// FN_PTR is a pointer to a function, which:
// - takes no input parameters (because ROM and PC are global variables)
// - doesn't return the number of bytes used; instead it alters PC before returning
typedef void (*FN_PTR)();

// see http://www.z80.info/z80oplist.txt
FN_PTR opcode_table[] = {
    Handle_NOP,
    Handle_LD_1,
    Handle_LD_2,
    Handle_INC_BC,
    ... etc ...
};

Define your case statements as separate functions:

void Handle_INC_BC()
{
    ... code to increment the BC and PC registers ...
}

Instead of switch (opcode) you can then do:

// get the function to process this opcode
FN function = opcode_table[opcode];
// invoke the function
(*function)();
\$\endgroup\$
5
  • \$\begingroup\$ During compilation it it won't be as simple as split opcode to create byte (then oprands), a lot of the time the oprands are part of the opcode. \$\endgroup\$
    – Lee Allan
    Jan 18, 2014 at 11:25
  • 1
    \$\begingroup\$ Yes, I saw that in the table of opcodes at z80.info/z80oplist.txt ... that is why I suggested (at least two) separate functions for the LD opcode. \$\endgroup\$
    – ChrisW
    Jan 18, 2014 at 11:32
  • \$\begingroup\$ I posted that comment early, apparently I cant newline. I wanted to say also: that '16 bytes of ram' is an artifact of when it did have 16 bytes, it uses 255 now, though, what is the advantage of using malloc()? Wont using firstregister() actually slow it down as it will have to run it multiple times? Using a "jump table" looks like a grate idea though - I will definitely look into that. \$\endgroup\$
    – Lee Allan
    Jan 18, 2014 at 11:37
  • \$\begingroup\$ malloc is good iff you want to allocate a lot. I don't know z80 but it's IMO hard to believe it only supports 256 bytes RAM. I mostly mentioned malloc because I wasn't sure whether you knew about dynamic memory allocation. \$\endgroup\$
    – ChrisW
    Jan 18, 2014 at 11:45
  • 1
    \$\begingroup\$ Looking through your case statements, I see that firstregister is called at most once per case statement. And it isn't called at all in case JUMP. If (rarely) there is any place where you call it multiple times, then you can call it once and cache the result in a local variable, byte firstregistervalue = firstregister(); ... use firstregistervalue instead of firstregister for the rest of this case statement ... \$\endgroup\$
    – ChrisW
    Jan 18, 2014 at 11:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.