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}
}