mksim

A PDP-8 emulator written in Go.

package main

import (
	"bufio"
	"fmt"
	"os"
	"path"
	"strconv"
	"time"
)

// Instructions
const (
	AND = 0o0
	TAD = 0o1
	ISZ = 0o2
	DCA = 0o3
	JMS = 0o4
	JMP = 0o5
	IOT = 0o6
	OPR = 0o7
)

// OPR Instruction Groups
const (
	OPR_GROUP_1 = 0
	OPR_GROUP_2 = 1
	OPR_GROUP_3 = 2
)

// Special memory addresses
const (
	INT_vect   = 0o0
	RESET_vect = 0o200

	AUTO_begin = 0o10
	AUTO_end   = 0o17
)

// The Front Panel relays information back to the user about the runtime status
type FrontPanel interface {
	PowerOn(mk MK12)           // Called on start with a mostly-default mk-12
	PowerOff()                 // Called on shutdown
	Update(mk MK12)            // Update the register bulbs/display
	ReadSwitches() (sr uint16) // Read the front panel switches
}

// This structure contains the various components of a theoretical MK-12
// All registers are stored as int16 but have a valid range of -/+4096 (12-bit signed int)
type MK12 struct {
	// Program Counter
	PC uint16

	// Instruction register
	IR uint16

	// Decoded instruction register
	IRd string

	// Accumulator Register
	AC uint16

	// Link Flag [1-bit]
	L bool

	// Memory Address Register
	MA uint16

	// Memory Buffer Register
	MB uint16

	// Memory [4K x 12 (int16)]
	// Addresses 0o0 to 0o7777
	MEM [4096]uint16

	// Switch Register
	// (unused)
	SR uint16

	// The state structure holds the current state of the CPU
	STATE struct {
		// If halt is set, the computer is halted during the fetch phase
		HALT bool

		// If SSTEP is set, the computer halts after every instruction
		SSTEP bool

		// If EXIT is set, the computer exits upon a HLT instruction
		EXIT bool
	}

	// IOT is an array of IOT devices.
	IOT []Device

	// Front panel attached to this computer
	fp FrontPanel

	// The HW struct contains information about the simulated hardware
	HW struct {
		// F_CPU is the theoretical clock speed in Hz
		F_CPU int64
	}
}

// This function handles the HALT state, listening for inputs
func (mk *MK12) halt() {
	// If EXIT flag is set, we exit upon a halt
	if mk.STATE.EXIT {
		// mk.fp.PowerOff()
		return
		// os.Exit(int(mk.AC))
	}

	// Listen for keyboard inputs
	// if !mk.STATE.SSTEP {
	// debugPrint(mk.g, "** SYSTEM HALTED **  [ENTER] CONTINUE  |  [CTRL] + [C] EXIT  |  [SPACE] SINGLE STEP")
	// updateStatus(mk.g, "HALT", gocui.AttrBold|gocui.ColorRed)
	// } else {
	// updateStatus(mk.g, "STEP", gocui.AttrBold|gocui.ColorBlue)
	// }

	for mk.STATE.HALT {
		c := getLastKey()
		switch c {
		case '\n':
			// debugPrint(mk.g, "Detected [ENTER]")
			mk.STATE.SSTEP = false
			mk.STATE.HALT = false

		case ' ':
			// debugPrint(mk.g, "Detected [SPACE]")
			mk.STATE.SSTEP = true
			mk.STATE.HALT = false

		case 17: // Device Control 1 Loads the program counter with the switch register
			mk.PC = mk.SR

		case 0: // No keypress
			fallthrough
		default:
			// Sleep for a bit - this solves the problem of high cpu usage
			time.Sleep(time.Millisecond * 1)
		}
	}
}

// This function implements the fetch process:
//  1. Load PC into MA, MB
//  2. Increment PC
//  3. Load instruction into IR using MA
//  4. Determines the Effective Address (EA) for memory reference instructions and loads it into MA
//     4a) If page bit is set, use the current page. If not set, use page 0
//     4b) If indirect bit is set, the EA contains the actual address to use
//  5. Fetches the Content of the Effective Address (CA) for instructions that require an operand
func (mk *MK12) fetch() {

	// Check if step button pressed
	if c := getLastKey(); c == ' ' {
		mk.STATE.SSTEP = true
	} // else if c == 17 { // or if home was pressed
	// 	mk.PC = mk.SR
	// }

	// Set HALT if single stepping
	if mk.STATE.SSTEP {
		mk.STATE.HALT = true
	}

	// Catch halt
	if mk.STATE.HALT {
		mk.halt()
		if mk.STATE.HALT && mk.STATE.EXIT {
			return
		}
	}

	// Update to RUN status after returning from HALT or STEP
	// updateStatus(mk.g, "RUN", gocui.AttrBold|gocui.ColorGreen)

	// Load PC into MA to get next instruction,
	// Save PC into MB for later use (indirect addressing)
	mk.MA = mk.PC
	mk.MB = mk.PC

	// Increment PC to point to the next instruction to execute
	mk.PC = (mk.PC + 1) % 4096

	// Load instruction register
	mk.IR = mk.MEM[mk.MA]
	mk.IRd = ""

	// Shorthand variable for the current instruction operator
	inOpr := mk.IR >> 9
	// Load correct address and/or operand for memory reference instructions
	if inOpr <= JMP {
		var addr uint16
		if (mk.IR & 0b0000000010000000) > 0 {
			// If page bit is set, we use the current page
			addr = mk.MB & 0b0000111110000000
		} else {
			// If bit is not set, we use the first page
			addr = 0
		}
		// Fill in word address in page
		addr = addr | (mk.IR & 0b0000000001111111)

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

			// Auto increment addresses 0o10 0o17
			if (addr >= AUTO_begin) && (addr <= AUTO_end) {
				mk.MEM[addr], _ = MKadd(mk.MEM[addr], 1)
			}

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

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

	// Load data from address for data reference instructions
	if inOpr == AND || inOpr == TAD || inOpr == ISZ {
		mk.MB = mk.MEM[mk.MA]
	}
}

// Executes the fetched instruction
func (mk *MK12) execute() {

	switch mk.IR >> 9 {

	case AND:
		// AND data with AC and store it back in AC
		tAC := mk.AC & mk.MB
		mk.IRd = fmt.Sprintf("AND %o & %o = %o --> AC", mk.AC, mk.MB, tAC)
		mk.AC = tAC

	case TAD:
		tAC, c := MKadd(mk.AC, mk.MB)
		mk.IRd = fmt.Sprintf("TAD %o + %o = %o --> AC", mk.AC, mk.MB, tAC)
		mk.L = c
		mk.AC = tAC

	case ISZ:
		// Increment MB and store it in MEM
		mk.MB, _ = MKadd(mk.MB, 1)
		mk.MEM[mk.MA] = mk.MB
		// If MB is zero, skip next instruction
		if mk.MB == 0 {
			mk.IRd = fmt.Sprintf("ISZ %o + 1 = %o --> %o; SKP %o", mk.MB-1, mk.MB, mk.MA, mk.PC)
			mk.PC = mk.PC + 1
		} else {
			mk.IRd = fmt.Sprintf("ISZ %o + 1 = %o --> %o", mk.MB-1, mk.MB, mk.MA)
		}

	case DCA:
		mk.MB = mk.AC
		mk.MEM[mk.MA] = mk.MB
		mk.AC = 0
		mk.IRd = fmt.Sprintf("DCA %o --> %o ; 0 --> AC", mk.MB, mk.MA)

	case JMS:
		mk.MEM[mk.MA] = mk.PC
		mk.IRd = fmt.Sprintf("JMS %o ; RET %o", mk.MA, mk.PC)
		mk.PC = mk.MA + 1

	case JMP:
		// Jump to the address stored in MA by storing it in the PC
		mk.PC = mk.MA
		mk.IRd = fmt.Sprintf("JMP %o", mk.MA)

	case IOT:
		devAddr := (mk.IR >> 3) & 0o77
		op1 := mk.IR & 0b001
		op2 := (mk.IR & 0b010) >> 1
		op4 := (mk.IR & 0b100) >> 2
		mk.IRd = fmt.Sprintf("IOT %.3o %.3b", devAddr, op1|op2|op4)

		for _, dev := range mk.IOT {
			if dev.Select(devAddr, mk) {
				// IOP1
				if op1 == 1 {
					skip, clr, or := dev.Iop1()
					if skip {
						mk.PC += 1 // Skip next instruction
					}
					if clr {
						mk.AC = 0 // Clear AC
					}
					if or {
						mk.AC |= dev.Get() // OR AC with device input
					}
				}
				// IOP2
				if op2 == 1 {
					skip, clr, or := dev.Iop2()
					if skip {
						mk.PC += 1 // Skip next instruction
					}
					if clr {
						mk.AC = 0 // Clear AC
					}
					if or {
						mk.AC |= dev.Get() // OR AC with device input
					}
				}
				// IOP4
				if op4 == 1 {
					skip, clr, or := dev.Iop4()
					if skip {
						mk.PC += 1 // Skip next instruction
					}
					if clr {
						mk.AC = 0 // Clear AC
					}
					if or {
						mk.AC |= dev.Get() // OR AC with device input
					}
				}
				break
			}
		}

	case OPR:
		// We wait a millisecond for a NOP instruction.
		if mk.IR == 0o7000 {
			time.Sleep(time.Millisecond)
		}

		group := (mk.IR >> 8) & 1
		if group > 0 {
			group += mk.IR & 1
		}

		switch group {
		case OPR_GROUP_1:
			var debugInst string = "OPR "

			if ((mk.IR >> 7) & 1) == 1 { // CLA - Clear Accumulator
				mk.AC = 0
				debugInst += "CLA "
			}
			if ((mk.IR >> 6) & 1) == 1 { // CLL - Clear Link
				mk.L = false
				debugInst += "CLL "
			}

			if ((mk.IR >> 5) & 1) == 1 { // CMA - Complement Accumulator
				mk.AC = MKcomplement(mk.AC)
				debugInst += "CMA "
			}
			if ((mk.IR >> 4) & 1) == 1 { // CML - Complement Link
				if mk.L {
					mk.L = false
				} else {
					mk.L = true
				}
				debugInst += "CML "
			}

			if ((mk.IR) & 1) == 1 { // IAC - Increment Accumulator
				mk.AC, mk.L = MKadd(mk.AC, 1)
				debugInst += "IAC "
			}

			if ((mk.IR >> 1) & 1) == 1 { // Rotate twice
				if ((mk.IR >> 3) & 1) == 1 { // RTR
					mk.AC, mk.L = MKrotateRight(mk.AC, mk.L)
					mk.AC, mk.L = MKrotateRight(mk.AC, mk.L)
					debugInst += "RTR"
				}
				if ((mk.IR >> 2) & 1) == 1 { // RTL
					mk.AC, mk.L = MKrotateLeft(mk.AC, mk.L)
					mk.AC, mk.L = MKrotateLeft(mk.AC, mk.L)
					debugInst += "RTL"
				}

			} else { // Single rotate
				if ((mk.IR >> 3) & 1) == 1 { // RAR
					mk.AC, mk.L = MKrotateRight(mk.AC, mk.L)
					debugInst += "RAR"
				}
				if ((mk.IR >> 2) & 1) == 1 { // RAL
					mk.AC, mk.L = MKrotateLeft(mk.AC, mk.L)
					debugInst += "RAL"
				}
			}

			mk.IRd = debugInst

		case OPR_GROUP_2:
			var debugInst string = "OPR "
			if ((mk.IR >> 7) & 1) == 1 { // CLA - Clear AC
				mk.AC = 0
				debugInst += "CLA "
			}

			// Determine state of skip conditions
			debugInst += "("
			skip := false
			if ((mk.IR >> 6) & 1) == 1 { // SMA - Skip on AC < 0
				if mk.AC < 0 || (mk.AC&0o4000) > 0 {
					skip = true
				}
				debugInst += "SMA "
			}
			if ((mk.IR >> 5) & 1) == 1 { // SZA - Skip on AC == 0
				if mk.AC == 0 {
					skip = true
				}
				debugInst += "SZA "
			}
			if ((mk.IR >> 4) & 1) == 1 { // SNL - Skip on L == 1
				if mk.L {
					skip = true
				}
				debugInst += "SNL "
			}
			debugInst += ")"
			// Do the actual skip
			if ((mk.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 {
					mk.PC = mk.PC + 1
					debugInst += "SKIP[NOR]"
				}
			} else {
				// If bit is not set, skip occurs if any condition is satisfied
				if skip {
					debugInst += "SKIP[OR]"
					mk.PC = mk.PC + 1
				}
			}
			debugInst += ")"

			if ((mk.IR >> 2) & 1) == 1 { // OSR - OR switch register with AC
				mk.AC |= mk.SR
				debugInst += "OSR "
			}
			if ((mk.IR >> 1) & 1) == 1 { // HLT - Halt the system
				debugInst += "HALT"
				mk.STATE.HALT = true
			}

			mk.IRd = debugInst

		case OPR_GROUP_3:
			fmt.Fprintf(os.Stderr, "ERROR: group 3 operate instructions not implemented!\ninstruction: %04o\n", mk.IR)
			os.Exit(1)
		}

	default:
		fmt.Fprintf(os.Stderr, "ERROR: Unknown instruction:"+strconv.Itoa(int(mk.IR)))
		os.Exit(1)
	}
}

func (mk *MK12) run() {
	// Init the registers with some default value because we get stuck in the first fetch halt loop
	mk.fp.Update(*mk)
	// Loop forever
	for {
		mk.fetch()
		// Break from loop if we entered an EXIT state during fetch
		if mk.STATE.HALT && mk.STATE.EXIT {
			break
		}
		// Update SR after we fetch because we might be returning from a HALT, so
		// the switches might have changed. Update it before execute for same reason
		mk.SR = mk.fp.ReadSwitches()
		mk.fp.Update(*mk)

		mk.execute()

		mk.fp.Update(*mk)

		if !mk.STATE.SSTEP {
			time.Sleep(((time.Duration(mk.HW.F_CPU / 1000000)) * time.Millisecond))
		}
	}
}

func main() {
	// Parse Arguments
	args := parseArgs()

	// Create a new MK-12 computer and configure needed flags for startup
	myMK12 := MK12{}
	myMK12.HW.F_CPU = args.F_CPU
	myMK12.STATE.HALT = args.HALT
	myMK12.STATE.EXIT = args.EXIT

	// Create our front panel
	if args.NoGui {
		myMK12.fp = new(CLIFrontPanel)
		myMK12.fp.PowerOn(myMK12)
		// Setup IOT Teleprinter to stdin/stdout
		teleType := TeleTypeDevice{
			Keyboard: NewStdinKeyboard(),
			Printer:  bufio.NewWriter(os.Stdout),
		}
		myMK12.IOT = append(myMK12.IOT, &teleType)
	} else {
		cfp := new(CUIFrontPanel)
		cfp.MemoryViewerPage = args.Page
		myMK12.fp = cfp
		// Power up the front panel
		myMK12.fp.PowerOn(myMK12)
		ctele := CursedTeleprinter{g: cfp.g}
		teleType := TeleTypeDevice{
			Keyboard: NewStdinKeyboard(),
			Printer:  &ctele,
		}
		myMK12.IOT = append(myMK12.IOT, &teleType)
	}

	// Create our papertape reader/punch
	// infile, er := os.OpenFile(args.iTapeFile, os.O_CREATE|os.O_RDONLY, 0644)
	// if er != nil {
	// 	panic(er)
	// }
	// Append to out file
	// outfile, er := os.OpenFile(args.oTapeFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	// if er != nil {
	// 	panic(er)
	// }
	// Defer closing papertape files
	// defer infile.Close()
	// defer outfile.Close()
	// paperTape := PaperTapeDevice{
	// 	inTape:  infile,
	// 	outTape: outfile,
	// }
	// myMK12.IOT = append(myMK12.IOT, &paperTape)

	// Load our compiled object file, basing the format off the extension
	var m [4096]uint16
	var err error
	switch path.Ext(args.InFile) {
	case ".rim":
		m, err = LoadRIMFile(args.InFile)
	case ".po":
		fallthrough
	default:
		m, err = LoadPObjFile(args.InFile)
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "ERROR: Unknown input file")
		myMK12.AC = 1
	} else {
		myMK12.MEM = m

		// Set PC to RESET vector and start computer
		myMK12.PC = 0o200
		myMK12.run()
		myMK12.fp.PowerOff()
	}

	if args.Return {
		fmt.Println(strconv.FormatInt(int64(myMK12.AC), 10))
	}

	// re enable stdin character echoing
	// _ = exec.Command("stty", "-F", "/dev/tty", "echo").Run()
	// if err != nil {
	// 	panic(err)
	// }

	os.Exit(int(myMK12.AC))
}