P8 CPU Emulator - Java

emulator java assembly

A Java implementation of the P8 CPU.

Github Repo here

Based on the documentation found Here

P8.java

package lib.cpu.p8;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

/**
 * Simple 8-bit CPU modeled after: http://www.rexfisher.com/P8/P8_TOC.htm
 * http://www.rexfisher.com/P8/P8_Instructions.htm
 *
 * @author Kenny
 *
 */
public class P8 {

    private static final String[] OPCODE_NAMES = new String[] {
         "",         // ---
         "IN",       // 00001
         "OUT",      // 00010
         "",         // ---
         "JMP",      // 00100
         "JNZ",      // 00101
         "JZ",       // 00110
         "CMP",      // 00111
         "LDA",      // 01000
         "LDR",      // 01001
         "STA",      // 01010
         "STR",      // 01011
         "ADD",      // 01100
         "SUB",      // 01101
         "DEC",      // 01110
         "",         // ---
         "OR",       // 10000
         "INV",      // 10001
         "SHL"       // 10010
    };

    private static final String[] ADDRESS_MODE_NAMES = new String[] {
         "MEM(address)",    // Direct Memory Address in Byte 2
         "---",             // Not Used
         "A",                 // A Register
         "R",                 // R Register
         "MEM(R)",             // Memory Address in R Register
         "---",             // Not Used
         "data",             // Byte 2 of Instruction
         "---"                // Not Used
    };

    public Registers r;

    public MMU mmu;

    public Port port;

    public boolean halt;

    public boolean printStack = false;

    public boolean printOp = true;

    public P8() {
        this(0x100, 0x100);
    }

    public P8(int stackSize) {
        this(stackSize, 0x100);
    }

    public P8(int stackSize, int portSize) {
        r = new Registers();
        mmu = new MMU(stackSize); // 0x00 - 0xFF
        port = new Port(portSize);
        halt = false;
    }

    public void exec() {
        if (!halt) {
            try {
                int instr = mmu.readByte(r.IP++);
                int opCode = (instr & 0xF8) >> 3; // high 5 bits
                int operand = getOperand(instr);
                callOp(opCode, operand);
                if(printOp) {
                    System.out.println(getInstructionName(instr));
                }
            } catch (Exception e) {
                halt = true;
                System.out.println("["  + e.getMessage() + "] Halting...");
            }
        }

    }

    public void dispatch() {
        while (!halt) {
            exec();
            if (printStack) {
                System.out.println(this);
            }
        }
    }

    public void reset() {
        r.reset();
        mmu.reset();
        port.reset();
        halt = false;
    }

    private int getOperand(int instr) {
        int addrMode = instr & 0x07; // low 3 bits
        switch (addrMode) {
            case 0x00: // Direct Memory Address in Byte 2
                return mmu.readByte(mmu.readByte(r.IP++));
            case 0x01: // Not Used ---
                halt = true;
                return 0x00;
            case 0x02: // Register A Register
                return r.A;
            case 0x03: // Register R Regsiter
                return r.R;
            case 0x04: // Indirect Memory Address in R Register
                r.IP++;
                return mmu.readByte(r.R);
            case 0x05: // Not Used ---
                halt = true;
                return 0x00;
            case 0x06: // Immediate Byte 2 of Instruction
                return mmu.readByte(r.IP++);
            case 0x07: // Not Used ---
                halt = true;
                return 0x00;
            default:
                halt = true;
                return 0x00;
        }
    }

    private void callOp(int opCode, int operand) {
        switch (opCode) {
            case 0x01: // IN 00001
                IN(operand);
                break;
            case 0x02: // OUT 00010
                OUT(operand);
                break;
            case 0x04: // JMP 00100
                JMP(operand);
                break;
            case 0x05: // JNZ 00101
                JNZ(operand);
                break;
            case 0x06: // JZ 00110
                JZ(operand);
                break;
            case 0x07: // CMP 00111
                CMP(operand);
                break;
            case 0x08: // LDA 01000
                LDA(operand);
                break;
            case 0x09: // LDR 01001
                LDR(operand);
                break;
            case 0x0A: // STA 01010
                STA(operand);
                break;
            case 0X0B: // STR 01011
                STR(operand);
                break;
            case 0x0C: // ADD 01100
                ADD(operand);
                break;
            case 0x0D: // SUB 01101
                SUB(operand);
                break;
            case 0x0E: // DEC 01110
                DEC(operand);
                break;
            case 0x10: // OR 10000
                OR(operand);
                break;
            case 0x11: // INV 10001
                INV(operand);
                break;
            case 0x12: // SHL 10010
                SHL(operand);
                break;
            default:
                XX();
                break;
        }
    }

    private String getInstructionName(int instr) {
        int opCode = (instr & 0xF8) >> 3; // high 5 bits
        int addrMode = instr & 0x07; // low 3 bits
        String instrName = " ["
                + String.format("%5s", Integer.toBinaryString(opCode)).replace(
                        ' ', '0')
                + " "
                + String.format("%3s", Integer.toBinaryString(addrMode))
                        .replace(' ', '0') + "] ";

        if(opCode < OPCODE_NAMES.length) {
            instrName += P8.OPCODE_NAMES[opCode] + " " + P8.ADDRESS_MODE_NAMES[addrMode];
        }
        return instrName;
    }

    /*
     * Instructions
     */
    private void XX() {
        System.out.println("Unknown Instruction. Halting...");
        halt = true;
    }

    // 1. Input / Output. These instructions transfer data between the
    // accumulator and external I/O devices.
    /**
     * IN = Read Input Port
     *
     * @param operand
     */
    private void IN(int operand) {
        r.A = port.readByte(operand);
    }

    /**
     * OUT = Write Output Port
     *
     * @param operand
     */
    private void OUT(int operand) {
        port.writeByte(operand, r.A);
    }

    // 2. Program Control. These instructions change the sequence of program
    // execution. They are often called branch instructions.

    /**
     * JMP = Unconditional Jump
     */
    private void JMP(int operand) {
        r.IP = operand;
    }

    /**
     * JNZ = Jump If Not Zero (Conditional Jump)
     */
    private void JNZ(int operand) {
        if (r.Z == 0) {
            JMP(operand);
        }
    }

    /**
     * JZ = Jump If Zero (Conditional Jump)
     */
    private void JZ(int operand) {
        if (r.Z == 1) {
            JMP(operand);
        }
    }

    /**
     * CMP = Compare (Sets / Resets Zero Bit For Conditional Jumps)
     */
    private void CMP(int operand) {
        if (r.A - operand == 0) {
            r.Z = 1;
        }
    }

    // 3. Data Transfer. These instructions cause data in one location (either
    // the internal registers or external memory) to be copied to another
    // location.

    /**
     * LDA = Load A Register
     */
    private void LDA(int operand) {
        r.A = operand;
    }

    /**
     * LDR = Load R Register
     */
    private void LDR(int operand) {
        r.R = operand;
    }

    /**
     * STA = Store A Register
     *
     * @param operand
     */
    private void STA(int operand) {
        mmu.writeByte(operand, r.A);
    }

    /**
     * STR = Store R Register
     */
    private void STR(int operand) {
        mmu.writeByte(operand, r.R);
    }

    // 4. Arithmetic. These instructions perform numerical operations on data.
    // Floating point operations are not supported.

    /**
     * ADD = Add To A Register
     */
    private void ADD(int operand) {
        r.A += operand;
    }

    /**
     * SUB = Subtract From A Register
     */
    private void SUB(int operand) {
        r.A -= operand;
    }

    /**
     * DEC = Decrement
     */
    private void DEC(int operand) {
        r.A = operand--;
    }

    // 5. Logical. These instructions perform Boolean operations on data,
    // including bit shifting.

    /**
     * OR = Or With A Register
     *
     * @param operand
     */
    private void OR(int operand) {
        r.A |= operand;
    }

    /**
     * INV = Invert & Move To A Register
     */
    private void INV(int operand) {
        r.A = ~operand;
    }

    /**
     * SHL = Shift Left & Move To A Register
     */
    private void SHL(int operand) {
        r.A = operand << 1;
    }

    /**
     *
     * @param instructions
     */
    public void loadInstructions(int[] instructions) {
        for (int i = 0; i < instructions.length; i++) {
            mmu.writeByte(i, instructions[i]);
        }
    }

    /**
     * Binary representation
     *
     * @param instructions
     */
    public void loadInstructions(String[] instructions) {
        for (int i = 0; i < instructions.length; i++) {
            mmu.writeByte(i, Integer.parseInt(instructions[i], 2));
        }
    }

    /**
     *
     */
    public void loadInstructionsFromFile(String fileName) {
        try {
            BufferedReader br = new BufferedReader(new FileReader(fileName));
            String line = "";
            ArrayList<String> instructions = new ArrayList<String>();
            while ((line = br.readLine()) != null) {
                line = line.trim();
                instructions.add(line.substring(0, 8));
            }
            br.close();
            loadInstructions(instructions.toArray(new String[instructions.size()]));
        } catch(FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("Halting...");
            halt = true;
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Halting...");
            halt = true;
        }
    }

    public String toString() {
        return r.toString() + "\n" + mmu.toString();
    }

}

Registers.java

package lib.cpu.p8;

public class Registers {

    public int IP;    // Instruction Pointer

    public  int A;    // Accumulator

    public int R;    // Data/Address

    public int Z;    // zero flag

    public void reset() {
        IP = 0;
        A = 0;
        R = 0;
        Z = 0;
    }

    public String toString() {
        return "[\n\tIP: " + IP + "\n" +
                "\tA: " + A + "\n" +
                "\tR: " + R + "\n" +
                "\tZ: " + Z + "\n]";
    }

}

MMU.java

package lib.cpu.p8;

public class MMU extends AbstractObservableBuffer {

    public MMU(int size) {
        super(size);
    }

}

Port.java

package lib.cpu.p8;

public class Port extends AbstractObservableBuffer {

    public Port(int size) {
        super(size);
    }

}

AbstractObservableBuffer.java

package lib.cpu.p8;

import java.util.Observable;

public class AbstractObservableBuffer extends Observable {

    protected int[] buffer;

    public AbstractObservableBuffer(int size) {
        buffer = new int[size];
    }

    public void reset() {
        for(int i = 0; i < buffer.length; i++) {
            buffer[i] = 0x00;
        }
    }

    public void writeByte(int address, int value) throws ArrayIndexOutOfBoundsException {
        if(address < 0 || address >= buffer.length) {
            throw new ArrayIndexOutOfBoundsException(address);
        }
        buffer[address] = value & 0xFF;
        setChanged();
        notifyObservers(address);
    }

    public void writeWord(int address, int value)  throws ArrayIndexOutOfBoundsException {
        writeByte(address, value & 0xFF);
        writeByte(address + 1, (value >> 8) & 0xFF);
    }

    public int readByte(int address) throws ArrayIndexOutOfBoundsException {
        if(address < 0 || address >= buffer.length) {
            throw new ArrayIndexOutOfBoundsException(address);
        }
        // System.out.println("Reading: " + Integer.toHexString(address).toUpperCase() + " => " +  Integer.toBinaryString(buffer[address]).toUpperCase());
        return buffer[address];
    }

    public int readWord(int address) throws ArrayIndexOutOfBoundsException {
        return readByte(address) + (readByte(address + 1) << 8);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < buffer.length; i++) {
            sb.append("[" + String.format("%8s", Integer.toBinaryString(i)).replace(' ', '0')  + "] 0x" + Integer.toHexString(i).toUpperCase());
            sb.append("\t=>\t");
            sb.append("[" + String.format("%8s", Integer.toBinaryString(buffer[i])).replace(' ', '0')  + "] 0x" + Integer.toHexString(buffer[i]).toUpperCase());
            sb.append("\n");
        }
        return sb.toString();
    }

}

IPortListener.java

package lib.cpu.p8;

import java.util.Observer;

interface IPortListener extends Observer {

}

SinglePortListener.java

package lib.cpu.p8;

import java.util.LinkedList;
import java.util.Observable;
import java.util.Queue;

public class SinglePortListener implements IPortListener {

    private final int port;

    private Queue<Integer> data;

    public SinglePortListener(int port) {
        this.port = port;
        data = new LinkedList<Integer>();
    }

    @Override
    public void update(Observable o, Object obj) {
        if(o instanceof Port && obj instanceof Integer) {
            Port p = (Port) o;
            Integer portRead = (Integer) obj;
            if(portRead == port) {
                // System.out.println("Reading Port: 0x" + Integer.toHexString(port).toUpperCase() + " => " + p.readByte(port));
                data.add( p.readByte(port));
            }
        }
    }

    public int getPort() {
        return port;
    }

    public int size() {
        return data.size();
    }

    public int pop() {
        if(data.size() > 0) {
            return data.poll();
        }
        return -1;
    }

}

test.asm

01100110    ; ADD A, data(0x4)
00000100
01101110    ; SUB A, data(0x1)
00000001
00111110    ; CMP A, data(0x00)
00000000
00110110    ; JZ data(0x00)
11111111    ; End program
00101110    ; JNZ data(0x02)
00000010

MMUTest.java

package lib.cpu.p8;

import static org.junit.Assert.*;
import lib.cpu.p8.MMU;

import org.junit.Test;

public class MMUTest {

    @Test
    public void testReadWrite() {
        MMU mmu = new MMU(0x10);

        // first
        mmu.writeByte(0x00, 0xFF);
        assertEquals(0xFF, mmu.readByte(0x00));

        // last
        mmu.writeByte(0xF, 0xFF);
        assertEquals(0xFF, mmu.readByte(0x0F));

        // out of bounds
        try {
            mmu.readByte(0x10);
            assertTrue(false);
        } catch(ArrayIndexOutOfBoundsException e) {
            assertTrue(true);
        }

        try {
            mmu.readByte(-1);
            assertTrue(false);
        } catch(ArrayIndexOutOfBoundsException e) {
            assertTrue(true);
        }

        // read/write word
        mmu.reset();
        mmu.writeWord(0x00, 0xABCD);
        assertEquals(0xABCD, mmu.readWord(0x00));

        mmu.writeWord(0xE, 0xABCD);
        assertEquals(0xABCD, mmu.readWord(0x0E));

        System.out.println(mmu.toString());
    }

    @Test
    public void testDefault() {
        MMU mmu = new MMU(0x10);
        for(int i = 0; i < 0x10; i++) {
            assertEquals(0, mmu.readByte(i));
        }
    }

    @Test
    public void miscTests() {
        MMU mmu = new MMU(0x10);
        int ip = 0x00;
        mmu.writeByte(ip++, 0xFF);
        mmu.writeByte(ip++, 0xCC);
        System.out.println(mmu.toString());

        //System.out.println(Integer.toBinaryString(0xF8));
    }

}

P8Test.java

package lib.cpu.p8;

import static org.junit.Assert.assertEquals;
import lib.utils.FileUtils;

import org.junit.Test;

public class P8Test {

    @Test
    public void test() {
        P8 p8 = new P8();

        int[] instructions = new int[] {
                0x66,    // 0b01100110  ADD A, data(0x4)
                0x04,

                0x6E,    // 0b01101110  SUB A, data(0x1)
                0x01,

                0x3E,    // 0b00111010  CMP A, data(0x00)
                0x00,
                0x36,    // 0b00110110  JZ data(0x00)
                0xFF,   // Will end program
                0x2E,    // 0b00110110  JNZ data(0x02)
                0x02,        
        };
        p8.loadInstructions(instructions);
        // System.out.println(p8);
        //p8.printStack = true;
        p8.dispatch();

    }

    @Test
    public void testLoadBinary() {
        P8 p8 = new P8();

        String[] instructions = new String[] {
                "01100110", // ADD A, data(0x4)
                "00000100",

                "01101110",    // SUB A, data(0x1)
                "00000001",

                "00111110",    // CMP A, data(0x00)
                "00000000",
                "00110110",    // JZ data(0x00)
                "11111111", // Will end program
                "00101110",    // JNZ data(0x02)
                "00000010",        
        };
        p8.loadInstructions(instructions);
        // p8.printStack = true;
        // System.out.println(p8);
        p8.dispatch();

    }

    @Test
    public void testOUT() {
        // test 1
        SinglePortListener port0 = new SinglePortListener(0);
        SinglePortListener port1 = new SinglePortListener(1);

        P8 p8 = new P8();
        p8.port.addObserver(port0);
        p8.port.addObserver(port1);

        String[] instructions = new String[] {
                "01100110", // ADD A, data(0x4)
                "00000100",

                "00010110", // OUT data(0x0) ; write A to port 0x00
                "00000000",

        };
        p8.loadInstructions(instructions);
        p8.exec();
        p8.exec();
        assertEquals(1, port0.size());
        assertEquals(0x4, port0.pop());
        assertEquals(0, port1.size());

        // test 2
        p8.reset();
        instructions = new String[] {
                "01100110", // ADD A, data(0x7)
                "00000111",

                "00010110", // OUT data(0x01) ; write A to port 0x01
                "00000001",
        };
        p8.loadInstructions(instructions);
        p8.exec();
        p8.exec();
        assertEquals(0, port0.size());
        assertEquals(1, port1.size());
        assertEquals(0x7, port1.pop());

    }

    @Test
    public void testPortIN() {

        P8 p8 = new P8();
        p8.port.writeByte(0x00, 0xFE); // write to port

        String[] instructions = new String[] {

                "00001010", // IN A ; read port(A)
                "01010110", // STA data

        };
        p8.loadInstructions(instructions);
        assertEquals(p8.r.A, 0x00);
        p8.exec();
        assertEquals(p8.r.A, 0xFE);
        p8.exec();
        assertEquals(0xFE, p8.mmu.readByte(0x00));

    }

    @Test
    public void testLoadFile() {
        P8 p8 = new P8();
        p8.loadInstructionsFromFile(FileUtils.home() + "krunch/src/lib/cpu/p8/test.asm");
        assertEquals(p8.mmu.readByte(0x00), 0x66);
        assertEquals(p8.mmu.readByte(0x01), 0x04);
        assertEquals(p8.mmu.readByte(0x02), 0x6E);
        assertEquals(p8.mmu.readByte(0x03), 0x01);
        assertEquals(p8.mmu.readByte(0x04), 0x3E);
        assertEquals(p8.mmu.readByte(0x05), 0x00);
        assertEquals(p8.mmu.readByte(0x06), 0x36);
        assertEquals(p8.mmu.readByte(0x07), 0xFF);
        assertEquals(p8.mmu.readByte(0x08), 0x2E);
        assertEquals(p8.mmu.readByte(0x09), 0x02);

    }

}