mkweb

PDP-8 Emulator written in Javascipt with HTML5 Canvas based frontend.

const OPCODES = {
    AND: 0,
    TAD: 1,
    ISZ: 2,
    DCA: 3,
    JMS: 4,
    JMP: 5,
    IOT: 6,
    OPR: 7,
}


class MK12 {
    _PC = 0;
    _IR = 0;
    _AC = 0;
    _MA = 0;
    _MB = 0;
    _MQ = 0;
    _SR = 0;
    _L = false;
    _MEM;
    _STATE = {
        POWER: false,   // Start powered off
        HALT: true,     // Start HALT'ed
        FETCH: true,
        STEP: false,
    };
    _rstVect = 0o200;
    _F_CPU = 4;
    get PC() { return this._PC; }
    set PC(value) { this._PC = value; }
    get IR() { return this._IR }
    set IR(value) { this._IR = value; }
    get AC() { return this._AC }
    set AC(value) { this._AC = value; }
    get MA() { return this._MA }
    set MA(value) { this._MA = value; }
    get MB() { return this._MB }
    set MB(value) { this._MB = value; }
    get MQ() { return this._MQ }
    set MQ(value) { this._MQ = value; }
    get SR() { return this._SR }
    set SR(value) { this._SR = value; }
    get L() { return this._L; }
    set L(value) { this._L = value; }
    get STATE() { return this._STATE }
    get F_CPU() { return this._F_CPU; }
    set F_CPU(value) {this._F_CPU = value; }
    get rstVect() { return this._rstVect; }
    set rstVect(value) { this._rstVect = value; }
    get MEM() { return this._MEM; }
    
    read() {
        this._MB = this._MEM[this._MA];
    }
    write() {
        this._MEM[this.MA] = this.MB;
    }

    constructor() {
        this._MEM = new Array(4096);
        for (let i = 0; i < this._MEM.length; i++) {
            // this._MEM[i] = Math.round(Math.random()*10000)%4096; // Fill with garbage
            this._MEM[i] = 0; // Fill with zeros
        }
        console.log(this._MEM);

        const urlParams = new URLSearchParams(window.location.search);
        if (urlParams.has("core")) {    // Extract memory from url if present
            let memory = urlParams.get("core").split(",")
            let address = 0
            for (let loc of memory) {
                if (loc.startsWith("*")) {
                    address = parseOctInt(loc.slice(1));
                } else {
                    this._MEM[address] = parseOctInt(loc);
                    address++;
                }
            }
        }
        if (urlParams.has("reset")) { // Extract reset vector if present
            let rstVect = urlParams.get("reset")
            this.rstVect = parseOctInt(rstVect);
        }
        if (urlParams.has("f_cpu")) {
            this.F_CPU = parseOctInt(urlParams.get("f_cpu"))
        }
    }

    fetch() {
        // console.debug("fetching instruction");
        this.MA = this.PC;              // Load current instruction memory location
        this.MB = this.PC;
        this.PC = (this.PC + 1) % 4096; // Increment program counter
        // console.log(this.MA);

        this.IR = this._MEM[this.MA];    // Load instruction from memory
        // console.log(this._MEM[this.MA]);
        // console.log(this.IR);
        let instOp = this.IR >> 9;      // Operation code

        if (instOp <= OPCODES.JMP) {    // Memory reference instructions
            var addr
            if (this.IR & 0b0000000010000000) {
                // If page bit is set, we use the current page
                addr = this.MB & 0b0000111110000000
            } else {
                // If bit is not set, we use the first page
                addr = 0
            }
            // Fill in word address in page
            addr = addr | (this.IR & 0b0000000001111111)

            // Check if indirect bit is set
            if (this.IR & 0b0000000100000000) {

                // Auto increment addresses 0o10 0o17
                if ((addr >= 0o10) && (addr <= 0o17)) {
                    var ans = mkAdd(this.MEM[addr], 1);
                    this._MEM[addr] = ans.x;
                }

                // Get address stored at addr
                addr = this.MEM[addr]
            }

            // Store address in MA
            this.MA = addr
        }

        // Load data from address for data reference instructions
        if (instOp == OPCODES.AND || instOp == OPCODES.TAD || instOp == OPCODES.ISZ) {
            this.MB = this.MEM[this.MA]
        }

        this._STATE.FETCH = false; // Leaving fetch phase
    }
    execute() {
        switch (this.IR >> 9) {
            case OPCODES.AND:
                this.AC = this.AC & this.MB;
                break;
            case OPCODES.TAD:
                var ans = mkAdd(this.AC, this.MB);
                this.AC = ans.x;
                if (ans.c) { // Complement link on overflow
                    this.L = !this.L;
                }
                break;
            case OPCODES.ISZ:
                var ans = mkAdd(this.MB, 1);
                this.MB = ans.x;
                this.write();
                if (this.MB == 0) {
                    this.PC = this.PC + 1;
                }
                break;
            case OPCODES.DCA:
                this.MB = this.AC;
                this.write();
                this.AC = 0;
                break;
            case OPCODES.JMP:
                this.PC = this.MA;
                break;
            case OPCODES.JMS:
                this.MB = this.PC;
                this.write();
                this.PC = this.MA + 1;
                break;
            case OPCODES.IOT:
                let iop1 = (this.IR >> 2) & 1;
                let iop2 = (this.IR >> 1) & 1;
                let iop4 = this.IR & 1;
                let device = (this.IR >> 3) & 0o77;
                if (device == 0o03) {
                    if (iop4) { // KSF - 06031
                        // Skip if keyboard flag set (keys in buffer)
                        if (KEYBOARD_BUFFER.length > 0) {
                            this.PC += 1;
                        }
                    } else { // KRB - 6036
                        if (iop2) { // KCC - 6032
                            // Clear AC and keyboard flag
                            this.AC = 0;
                        }
                        if (iop1) { // KRS - 6034
                            // Read keyboard buffer
                            if (KEYBOARD_BUFFER.length > 0) {
                                this.AC |= KEYBOARD_BUFFER.shift();
                            }
                        }
                    }
                } else if (device == 0o04) {
                    if (iop4) { // TSF - 6041
                        // Skip if printer flag set
                        if (PRINTER_BUFFER.length <= PRINTER_BUFSIZE) {
                            this.PC += 1
                        }
                    } else { // TLS - 6046
                        if (iop2) { // TCF - 6042
                            // Clear printer flag
                        }
                        if (iop1) { // TPC - 6044
                            // Print AC
                            if (PRINTER_BUFFER.length <= PRINTER_BUFSIZE) {
                                PRINTER_BUFFER.push(String.fromCharCode(this.AC));
                            }
                        }
                    }
                } else {
                    console.warn("IOT device not implemented:", device);
                }
                break;
            case OPCODES.OPR:
                let group = ((this.IR >> 8) & 1) + ((this.IR&1) & ((this.IR >> 8) & 1));
                if (!group) {
                    // Group 1 operate microinstructions
                    if (((this.IR >> 7) & 1) == 1) { // CLA - Clear Accumulator
                        this.AC = 0;
                    }
                    if (((this.IR >> 6) & 1) == 1) { // CLL - Clear Link
                        this.L = false;
                    }
        
                    if (((this.IR >> 5) & 1) == 1) { // CMA - Complement Accumulator
                        this.AC = (~this.AC) & 0o7777;
                    }

                    if (((this.IR >> 4) & 1) == 1) { // CML - Complement Link
                        this.L = !this.L;
                    }
        
                    if (((this.IR) & 1) == 1) { // IAC - Increment Accumulator
                        var ans = mkAdd(this.AC, 1);
                        this.AC = ans.x;
                        if (ans.c) { // Complement link on overflow
                            this.L = !this.L;
                        }
                    }
        
                    if (((this.IR >> 1) & 1) == 1) { // Rotate twice
                        if (((this.IR >> 3) & 1) == 1) { // RTR
                            var ans = rotateRight(this.AC, this.L);
                            ans = rotateRight(ans.x, ans.c);
                            this.AC = ans.x;
                            this.L = ans.c;
                        }
                        if (((this.IR >> 2) & 1) == 1) { // RTL
                            var ans = rotateLeft(this.AC, this.L);
                            ans = rotateLeft(ans.x, ans.c);
                            this.AC = ans.x;
                            this.L = ans.c;
                        }
        
                    } else { // Single rotate
                        if (((this.IR >> 3) & 1) == 1) { // RAR
                            var ans = rotateRight(this.AC, this.L);
                            this.AC = ans.x;
                            this.L = ans.c;
                        }
                        if (((this.IR >> 2) & 1) == 1) { // RAL
                            var ans = rotateLeft(this.AC, this.L);
                            this.AC = ans.x;
                            this.L = ans.c;
                        }
                    }

                } else if (group == 1) {
                    // Group 2 operate microinstructions
                    if (((this.IR >> 7) & 1) == 1) { // CLA - Clear AC
                        this.AC = 0;
                    }
        
                    // Determine state of skip conditions
                    let skip = false;
                    if (((this.IR >> 6) & 1) == 1) { // SMA - Skip on AC < 0
                        if (this.AC < 0 || (this.AC & 0o4000) > 0) {
                            skip = true;
                        }
                    }
                    if (((this.IR >> 5) & 1) == 1) { // SZA - Skip on AC == 0
                        if (this.AC == 0) {
                            skip = true;
                        }
                    }
                    if (((this.IR >> 4) & 1) == 1) { // SNL - Skip on L == 1
                        if (this.L) {
                            skip = true;
                        }
                    }

                    // Do the actual skip
                    if (((this.IR >> 3) & 1) == 1) { // Sense of skip (any or none)
                        // If bit is set, no skip occurs if any condition has been satisfied (skip=true)
                        if (!skip) {
                            this.PC = this.PC + 1;
                        }
                    } else {
                        // If bit is not set, skip occurs if any condition is satisfied
                        if (skip) {
                            this.PC = this.PC + 1;
                        }
                    }
        
                    if (((this.IR >> 2) & 1) == 1) { // OSR - OR switch register with AC
                        this.AC |= this.SR;
                    }
                    if (((this.IR >> 1) & 1) == 1) { // HLT - Halt the system
                        this.STATE.FETCH = true; // Signal we've already executed this instruction
                        this.halt(); // Halt the computer
                        return; // Return immediatly
                    }          
                } else {
                    // Group 3 operate microinstructions from the PDP-8/A
                    var bitCLA = (this.IR >> 7) & 1;
                    var bitMQL = (this.IR >> 4) & 1;
                    var bitMQA = (this.IR >> 6) & 1;
                    var swap = bitMQL & bitMQA;
                    
                    if (bitCLA) { // CLA - clear AC
                        this.AC = 0;
                    }

                    if (swap) {// SWP - Swap AC and MQ
                        var tempAC = this.AC;
                        this.AC = this.MQ;
                        this.MQ = tempAC;
                    } else if (bitMQL) { // MQL - Load MQ with AC
                        this.MQ = this.AC;
                    } else if (bitMQA) { // MQA - OR MQ with AC and store the result in AC
                        this.AC |= this.MQ;
                    }
                }

                break;
            default:
                console.error("unknown instruction", this.IR);
                break;
        }
        this.STATE.FETCH = true;
    }

    on () {
        // Turn on computer that is powered off
        if (!this.STATE.POWER) {
            this.PC = this.rstVect;
            this.STATE.POWER = true;
        }
    }

    off () {
        // Turn off a computer that is powered on
        if (this.STATE.POWER) {
            // Halt running computer
            if (!this.STATE.HALT) {
                this.halt();
            }

            this.STATE.POWER = false;   // Cut power
            this.AC = 0;                // Regisers lose their state
            this.PC = 0;
            this.IR = 0;
            this.MA = 0;
            this.MB = 0;
            this.MQ = 0;
        }

    }

    halt () {
        if (!this.STATE.HALT) { // Only halt a running computer
            window.clearInterval(this.CLK); // Halt execution
            this.STATE.HALT = true; // Update state
            if (!this.STATE.FETCH) {
                // Finish cycle if already fetched
                this.execute();
            }
        }
    }

    start () {
        if (this.STATE.HALT) { // Only start a halted computer
            this.STATE.HALT = false;
            if (this.F_CPU > 0) {
                // Execute run function every F_CPU ms
                this.CLK = window.setInterval(()=>{this.run()}, this.F_CPU);
            } else {
                // Call run function in a loop as fast as possible
                while (!this.STATE.HALT) {
                    this.run();
                }
            }
        }
    }

    run () {
        if (!this.STATE.HALT) { // Only run when not halted
            if (this.STATE.FETCH) { // Check if we've already fetched (by single stepping)
                this.fetch();
            }
            this.execute();
        }
    }

    cycle () {
        if (this.STATE.HALT) { // Only cycle when halted
            if (this.STATE.FETCH) {
                this.fetch();
            }
            this.execute();
        }
    }

    step () {
        if (this.STATE.HALT) { // Only step when halted
            if (this.STATE.FETCH) { // Fetch instruction
                this.fetch();
            } else { // If instruction has been fetched, execute it
                this.execute();
            }
        }
    }
}

// utils

function parseOctInt(string) {
    if (string.startsWith("0")) { // Parse numbers starting with 0 as octal
        return parseInt(string, 8)
    } else { // Parse numbers as decimal/hexadecimal otherwise
        return parseInt(string)
    }
}

// computer functions

function mkAdd(a, b) {
    
    let x = a + b;
    if (x > 4095) {
        return {x: x%4096, c: true};
    } else {
        return {x: x, c: false};
    }
}

function rotateRight(a, b) {
    var c = false;
    var x = 0;
    if (a & 1) { // Set L to ac[11]
        c = true;
    }
    x = a >> 1; // Shift right
    if (b) { // Set ac[0] to L
        x |= 0o4000;
    }
    return {x, c}
}

function rotateLeft(a, b) {
    var c = false;
    var x = 0;
    if (a & 0o4000) { // Set L to ac[0]
        c = true;
    }
    x = (a << 1) & 0o7777; // Shift left and mask
    if (b) { // Set ac11] to L
        x |= 1;
    }
    return {x, c}
}