diff --git a/.gitignore b/.gitignore
index ec5789e..2cca2f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
uasm
uvm
*.bin
+*.hex
+*.rom
diff --git a/asm/compiler.go b/asm/compiler.go
index 210da72..7de8ecb 100644
--- a/asm/compiler.go
+++ b/asm/compiler.go
@@ -15,6 +15,18 @@ type (
nodeType uint8
)
+// Size is operand size in bytes
+func (ot OperandType) Size() int {
+ switch ot {
+ case OperandReg, OperandValue:
+ return 1
+ case OperandAddr:
+ return 2
+ default:
+ panic("unknown operand " + ot)
+ }
+}
+
const (
OperandReg OperandType = "reg"
OperandValue OperandType = "val"
@@ -136,9 +148,8 @@ func assemble(ins string, ops []string) []uint8 {
}
// Compile compiles program loaded from reader (usually strings.Reader or os.File)
-func Compile(textReader io.Reader) [1 << 16]uint8 {
-
- bin := [defines.ROMSize]uint8{}
+func Compile(textReader io.Reader) []uint8 {
+ bin := make([]byte, 1<<16)
offset := uint16(0x00)
lineNum := 0
@@ -165,7 +176,7 @@ func Compile(textReader io.Reader) [1 << 16]uint8 {
// clean-up operands
for i := 0; i < len(ops); i++ {
- ops[i] = strings.Replace(strings.TrimSpace(ops[i]), ",", "", -1)
+ ops[i] = strings.ReplaceAll(strings.TrimSpace(ops[i]), ",", "")
}
// decide on what we're looking right now - operator or macro?
diff --git a/asm/syntax.go b/asm/syntax.go
index 271a44c..3a5ea22 100644
--- a/asm/syntax.go
+++ b/asm/syntax.go
@@ -2,20 +2,25 @@ package asm
const (
OpNOP = 0x00
- OpJUMP = 0x01
OpPUSH = 0x02
OpPOP = 0x03
OpCLEAR = 0x04
OpINC = 0x05
OpHALT = 0x09
+ OpCP = 0x0A
+ OpCPI = 0x0B
+
+ OpJUMP = 0x50
+ OpJUMPIF_EQ = 0x51
+ OpJUMPIF_NE = 0x52
+
OpADDRegReg = 0x10
OpADDRegVal = 0x11
OpMOVRegReg = 0x20
OpMOVRegVal = 0x21
- OpLPM = 0x22
OpLOAD = 0x30
OpSTORE = 0x40
)
@@ -30,13 +35,21 @@ asm syntax help
// Syntax maps instruction name to opcodes with operands
var Syntax = map[string]map[uint8][]OperandType{
"NOP": {OpNOP: {}},
- "JUMP": {OpJUMP: {OperandAddr}},
"PUSH": {OpPUSH: {OperandReg}},
"POP": {OpPOP: {OperandReg}},
"CLEAR": {OpCLEAR: {OperandReg}},
"INC": {OpINC: {OperandReg}},
"HALT": {OpHALT: {}},
+ "CP": {
+ OpCP: {OperandReg, OperandReg},
+ OpCPI: {OperandReg, OperandValue},
+ },
+
+ "JUMP": {OpJUMP: {OperandAddr}},
+ "JE": {OpJUMPIF_EQ: {OperandAddr}},
+ "JNE": {OpJUMPIF_NE: {OperandAddr}},
+
"ADD": {
OpADDRegReg: {OperandReg, OperandReg}, // do reg1 + reg2 and store the result in reg1
OpADDRegVal: {OperandReg, OperandValue}, // do reg1 + value and store the result in reg1
@@ -46,8 +59,6 @@ var Syntax = map[string]map[uint8][]OperandType{
OpMOVRegVal: {OperandReg, OperandValue}, // move value to reg1 immediately
},
- "LPM": {OpLPM: {OperandReg, OperandAddr}}, // load ROM value at given addr to reg
-
// external memory
"LOAD": {OpLOAD: {OperandReg, OperandAddr}}, // load register with a value stored at addr
"STORE": {OpSTORE: {OperandAddr, OperandReg}}, // store reg's value at address
diff --git a/assets/index.html b/assets/index.html
new file mode 100644
index 0000000..80840e3
--- /dev/null
+++ b/assets/index.html
@@ -0,0 +1,49 @@
+
+
+
+ WebSocket Example
+
+
+
+
+
+
+
diff --git a/cmd/uvm/main.go b/cmd/uvm/main.go
index 4c8f01b..44d6fb1 100644
--- a/cmd/uvm/main.go
+++ b/cmd/uvm/main.go
@@ -3,11 +3,9 @@ package main
import (
"flag"
"fmt"
- "io/ioutil"
"os"
"github.com/sshaman1101/uvm/cpu"
- "github.com/sshaman1101/uvm/defines"
)
var usageFunc = func() {
@@ -27,22 +25,20 @@ func main() {
}
romFile := os.Args[1]
- image, err := ioutil.ReadFile(romFile)
+ image, err := os.ReadFile(romFile)
if err != nil {
fmt.Printf("ERR: Failed to load ROM file from %s: %v", romFile, err)
os.Exit(1)
}
- if len(image) > defines.ROMSize {
+ romSize := 1 << 16
+ if len(image) > romSize {
fmt.Printf("WARN: ROM image does not fits into memory "+
"(size = %d, but %d bytes available).\n"+
- "Image will be truncated.\n", len(image), defines.ROMSize)
+ "Image will be truncated.\n", len(image), romSize)
}
- var rom = [defines.ROMSize]uint8{}
- copy(rom[:], image)
-
uCPU := cpu.NewCPU()
- uCPU.ROM = rom
+ uCPU.LoadROM(image)
uCPU.Run()
}
diff --git a/cpu/cpu.go b/cpu/cpu.go
index b897e70..df59d02 100644
--- a/cpu/cpu.go
+++ b/cpu/cpu.go
@@ -2,7 +2,9 @@ package cpu
import (
"fmt"
+ "strings"
+ colors "github.com/nikonov1101/colors.go"
"github.com/sshaman1101/uvm/asm"
"github.com/sshaman1101/uvm/defines"
)
@@ -18,74 +20,88 @@ func (f *flags) String() string {
}
type CPU struct {
- ROM [defines.ROMSize]uint8
- RAM [defines.RAMSize]uint8
-
- registers [defines.RegisterCount]uint8
- stack *stack
- flags *flags
-
- // program counter
- pc uint16
+ flags *flags
+ // general-purpose registers are not memory-mapped (yet).
+ generalPurposeReg [defines.RegisterCount]uint8
+ // pc actually 24 bits wide
+ pc uint32
+ // stack pointer, 24 bits wide as well
+ sp uint32
+
+ // address is 24 bit wide, first 8 bits are pointed by segmentSelectorReg
+ // (inspired by 8088), next 16 bits are pointed by the address operand of
+ // an instruction, thus:
+ // MOV r1 $abcd
+ // moves value at address sreg+0xabcd into r1.
+ segmentSelectorReg uint8
+
+ // all addressable memory
+ mem [1 << defines.AddressWidth]uint8
}
+const (
+ startSegment = 0
+ startAddress = 0
+)
+
func NewCPU() *CPU {
return &CPU{
- ROM: [defines.ROMSize]uint8{},
- RAM: [defines.RAMSize]uint8{},
- registers: [defines.RegisterCount]uint8{},
- stack: newStack(defines.StackDepth),
- flags: &flags{},
- pc: 0,
+ flags: &flags{},
+ sp: defines.StackInitialAddr, // very end of the memory
+ pc: startSegment,
+ segmentSelectorReg: startSegment,
+ }
+}
+
+func (cpu *CPU) LoadROM(rom []byte) {
+ if len(rom) == 0 {
+ panic("empty ROM given")
}
+
+ romStart := 0x00FFFFFF & (uint32(startSegment)<<16 | uint32(startAddress))
+ copy(cpu.mem[romStart:], rom)
}
func (cpu *CPU) Run() {
for {
// load next value from mem,
// must be an instruction
- v := cpu.ROM[cpu.pc]
+ v := cpu.mem[cpu.pc]
- // decode instruction
+ // STAGE 1: decode instruction
// note: panics on invalid input
next := cpu.decodeInstruction(v)
+ var pcOffset uint32 = 0
+
+ // STAGE 2: fetch the operands from memory
+ for i := range next.operands {
+ // calculate next address
+ pcOffset++
- // load operands
- for i := 0; i < next.operandCount; i++ {
- // calculate next mem address
- cpu.pc++
- // fetch memory
- // todo: do it via something like MAR/MDR, as real hardware do
- // or just emulate it, at least it will looks hacky.
- given := cpu.ROM[cpu.pc]
+ // fetch next operand from the memory
+ given := cpu.mem[cpu.pc+pcOffset]
expected := next.operands[i]
// sanity check
- opName := checkOperand(given, expected.opType)
+ checkOperand(given, expected.opType)
// XXX debug
- fmt.Printf(" operand %s loaded\n", opName)
+ // fmt.Printf(" operand %s loaded\n", opName)
- // store within instruction
+ // store operand *data* within instruction
next.operands[i].value = given
}
- // XXX print instruction with operators loaded
- fmt.Printf("at PC = %d (0x%02x) -> RUN %s\n", cpu.pc, cpu.pc, next)
-
- next.execute(cpu)
-
- // dump CPU state after the each instruction
- fmt.Println("======== CPU state ========")
- fmt.Printf("PC = %d\n", cpu.pc)
- fmt.Printf("flags:\n %v\n", cpu.flags)
- fmt.Printf("registers:\n ")
- for i := 0; i < defines.RegisterCount; i++ {
- fmt.Printf("r%d = %02x", i, cpu.registers[i])
- if i+1 != defines.RegisterCount {
- fmt.Printf(" | ")
- }
- }
- fmt.Printf("\n===========================\n\n")
+ cpu.debugPre(next)
+ // update PC with a number operands fetched,
+ // do this before the actual execution, so
+ // JUMP instructions may override the PC
+ cpu.pc += pcOffset + 1
+
+ // STAGE 3: execute the instruction
+ cpu.execute(next)
+
+ // XXX debug state on the fly
+ cpu.debugPost()
if cpu.flags.halt {
return
@@ -122,6 +138,7 @@ func (cpu *CPU) decodeInstruction(opcode uint8) instruction {
// note: just a dirty crutch to add two address bytes for instruction.
// Need to find a smarter way to handle such situation.
if op == asm.OperandAddr {
+ // XXX why???
instructionOperands = append(instructionOperands, operand{opType: asm.OperandAddr}, operand{opType: asm.OperandAddr})
} else {
instructionOperands = append(instructionOperands, operand{opType: op})
@@ -129,9 +146,45 @@ func (cpu *CPU) decodeInstruction(opcode uint8) instruction {
}
return instruction{
- name: mnemonic,
- opCode: opcode,
- operandCount: len(instructionOperands),
- operands: instructionOperands,
+ name: mnemonic,
+ opCode: opcode,
+ operands: instructionOperands,
}
}
+
+func (cpu *CPU) debugPre(next instruction) {
+ pc := colors.Cyan(fmt.Sprintf("0x%06X", cpu.pc))
+ fmt.Printf("PC: %s @ %v\n", pc, next.String())
+}
+
+func (cpu *CPU) debugPost() {
+ var regs []string
+ for i, r := range cpu.generalPurposeReg {
+ rs := fmt.Sprintf("0x%02X", r)
+ if r > 0 {
+ rs = colors.Yellow(rs)
+ }
+ regs = append(regs, fmt.Sprintf("r%d: %s", i, rs))
+ }
+
+ zs := "Z: false"
+ cs := "C: false"
+ hs := "H: false"
+ if cpu.flags.zero {
+ zs = fmt.Sprintf("Z: %s", colors.Green("TRUE"))
+ }
+ if cpu.flags.carry {
+ cs = fmt.Sprintf("C: %s", colors.Green("TRUE"))
+ }
+ if cpu.flags.halt {
+ hs = colors.Red("HALT: TRUE")
+ }
+
+ flags := fmt.Sprintf("%s | %s | %s", zs, cs, hs)
+ pc := colors.Cyan(fmt.Sprintf("0x%06X", cpu.pc))
+ sp := colors.Magenta(fmt.Sprintf("0x%06X", cpu.sp))
+
+ fmt.Printf("\tPC: %s | SP: %v | flags: %s\n", pc, sp, flags)
+ fmt.Printf("\t%s\n", strings.Join(regs, " "))
+ fmt.Println("=============================================")
+}
diff --git a/cpu/instruction.go b/cpu/instruction.go
index 02cc26a..e896aab 100644
--- a/cpu/instruction.go
+++ b/cpu/instruction.go
@@ -2,7 +2,9 @@ package cpu
import (
"fmt"
+ "strings"
+ "github.com/nikonov1101/colors.go"
"github.com/sshaman1101/uvm/asm"
"github.com/sshaman1101/uvm/defines"
"github.com/sshaman1101/uvm/math"
@@ -27,71 +29,97 @@ func checkOperand(v uint8, typ asm.OperandType) string {
}
return fmt.Sprintf("r%02d", v)
case asm.OperandAddr:
- if int(v) >= defines.RAMSize {
- panic("mem address operand is out of memory")
- }
- return fmt.Sprintf("$%04X", v)
+ return fmt.Sprintf("$%02X", v)
default:
panic("operand type must be defined")
}
}
-// calculateAddress calculates 16-bit address using
-// LO and HI 8-bit address parts
-func calculateAddress(lo, hi uint8) uint16 {
- // shift HI by 8 bits, then OR with LO to fill the rest
- return uint16(hi)<<8 | uint16(lo)
-}
-
type instruction struct {
// just a name, like MOV, XOR, JUMP
name string
// what to do
opCode uint8
- // how many operands we need to fetch from memory
- operandCount int
- // on which data we need to preform operation
+ // on which data we need to preform operation,
+ // note: when the only instruction fetched, we know the
+ // operand **types** from the instruction code itself.
+ // the actual value of operands (register names, address values)
+ // will be fetched from a memory during the instruction fetch cycle.
operands []operand
}
+func (in instruction) OperandSize() int {
+ size := 0
+ for _, op := range in.operands {
+ size += op.opType.Size()
+ }
+ return size
+}
+
func (in instruction) String() string {
- ops := ""
+ var ops []string
for _, o := range in.operands {
- ops += fmt.Sprintf("%s %0X, ", o.opType, o.value)
+ switch o.opType {
+ case asm.OperandReg:
+ ops = append(ops, colors.Green(fmt.Sprintf("reg%d", o.value)))
+ case asm.OperandValue:
+ ops = append(ops, colors.Magenta(fmt.Sprintf("val 0x%02X", o.value)))
+ case asm.OperandAddr:
+ ops = append(ops, fmt.Sprintf("addr 0x%02X", o.value))
+ }
}
- return in.name + " " + ops
+ name := colors.Yellow(strings.ToUpper(in.name))
+ return name + " " + strings.Join(ops, "; ")
}
// asAddress returns 16 bit address from given operand indexes
-func (in *instruction) asAddress(loOp, hiOp int) uint16 {
- return calculateAddress(in.operands[loOp].value, in.operands[hiOp].value)
+func (in instruction) asAddress(seg uint8, loOp, hiOp int) uint32 {
+ lo := in.operands[loOp].value
+ hi := in.operands[hiOp].value
+ return uint32(seg)<<16 | uint32(hi)<<8 | uint32(lo)
}
// execute the instruction
// can touch:
-// * registers
-// * flag register
-// * program counter
-// note that in must increase PC by one
-// if it's regular instruction (not JUMP)
-// TODO: seems like the dependency must be inverted:
-// the CPU executes the instruction, not vise versa.
-func (in *instruction) execute(cpu *CPU) {
+// - registers
+// - flag register
+// - program counter (when jumps)
+func (cpu *CPU) execute(in instruction) {
switch in.opCode {
case asm.OpNOP:
// just do nothing
case asm.OpJUMP:
- // go to address, DO NOT increment PC by one
- cpu.pc = in.asAddress(0, 1)
- return
+ cpu.pc = in.asAddress(cpu.segmentSelectorReg, 0, 1)
+
+ case asm.OpJUMPIF_EQ:
+ if cpu.flags.zero {
+ cpu.pc = in.asAddress(cpu.segmentSelectorReg, 0, 1)
+ }
+
+ case asm.OpJUMPIF_NE:
+ if !cpu.flags.zero {
+ cpu.pc = in.asAddress(cpu.segmentSelectorReg, 0, 1)
+ }
+
+ case asm.OpCP:
+ r0 := cpu.generalPurposeReg[in.operands[0].value]
+ r1 := cpu.generalPurposeReg[in.operands[1].value]
+ cpu.flags.zero = r0 == r1
+ cpu.flags.carry = false
+
+ case asm.OpCPI:
+ r0 := cpu.generalPurposeReg[in.operands[0].value]
+ v1 := in.operands[1].value
+ cpu.flags.zero = r0 == v1
+ cpu.flags.carry = false
case asm.OpADDRegReg:
r0 := in.operands[0].value
r1 := in.operands[1].value
- result, carry := math.Add8(cpu.registers[r0], cpu.registers[r1])
- cpu.registers[r0] = result
+ result, carry := math.Add8(cpu.generalPurposeReg[r0], cpu.generalPurposeReg[r1])
+ cpu.generalPurposeReg[r0] = result
cpu.flags.zero = result == 0
cpu.flags.carry = carry
@@ -100,8 +128,8 @@ func (in *instruction) execute(cpu *CPU) {
reg := in.operands[0].value
value := in.operands[1].value
- result, carry := math.Add8(cpu.registers[reg], value)
- cpu.registers[reg] = result
+ result, carry := math.Add8(cpu.generalPurposeReg[reg], value)
+ cpu.generalPurposeReg[reg] = result
cpu.flags.zero = result == 0
cpu.flags.carry = carry
@@ -109,74 +137,64 @@ func (in *instruction) execute(cpu *CPU) {
case asm.OpMOVRegVal:
reg := in.operands[0].value
val := in.operands[1].value
- cpu.registers[reg] = val
- // todo: flags?
+ cpu.generalPurposeReg[reg] = val
case asm.OpMOVRegReg:
dstReg := in.operands[0].value
srcReg := in.operands[1].value
- cpu.registers[dstReg] = cpu.registers[srcReg]
- // todo: flags on dstReg value?
-
- case asm.OpLPM: // load from program memory
- // calculate 16 bit address in ROM
- addr := in.asAddress(1, 2)
- // load value in the given register
- cpu.registers[in.operands[0].value] = cpu.ROM[addr]
- // todo: flags?
-
- case asm.OpLOAD:
- addr := in.asAddress(1, 2)
+ cpu.generalPurposeReg[dstReg] = cpu.generalPurposeReg[srcReg]
+
+ case asm.OpLOAD: // LOAD reg <- $mem
+ addr := in.asAddress(cpu.segmentSelectorReg, 1, 2)
+ val := cpu.mem[addr]
+
reg := in.operands[0].value
- val := cpu.RAM[addr]
- cpu.registers[reg] = val
- cpu.flags.zero = val == 0
+ cpu.generalPurposeReg[reg] = val
- case asm.OpSTORE: // addr, reg
- addr := in.asAddress(0, 1)
+ case asm.OpSTORE: // STORE $mem <- reg
reg := in.operands[2].value
- val := cpu.registers[reg]
- cpu.RAM[addr] = val
+ val := cpu.generalPurposeReg[reg]
+
+ addr := in.asAddress(cpu.segmentSelectorReg, 0, 1)
+ cpu.mem[addr] = val
case asm.OpHALT:
cpu.flags.halt = true
- return
case asm.OpPUSH:
reg := in.operands[0].value
- val := cpu.registers[reg]
- cpu.stack.push(val)
+ val := cpu.generalPurposeReg[reg]
+ sp := 0x00FFFFFF & cpu.sp
+ cpu.mem[sp] = val
+ // TODO(nikonov): any kind of stack guards, maybe?
+ // at least .SP > .PC
+ cpu.sp--
case asm.OpPOP:
+ cpu.sp++
+ sp := 0x00FFFFFF & cpu.sp
+ val := cpu.mem[sp]
+ // TODO(nikonov): any kind of stack guards, maybe?
+
reg := in.operands[0].value
- val := cpu.stack.pop()
- cpu.registers[reg] = val
- // todo: flags for val?
+ cpu.generalPurposeReg[reg] = val
case asm.OpCLEAR:
reg := in.operands[0].value
- cpu.registers[reg] = 0
+ cpu.generalPurposeReg[reg] = 0
cpu.flags.zero = true
+ cpu.flags.carry = false
case asm.OpINC:
reg := in.operands[0].value
- val := cpu.registers[reg]
+ val := cpu.generalPurposeReg[reg]
result, carry := math.Add8(val, 1)
- cpu.registers[reg] = result
- cpu.flags.zero = reg == 0
+ cpu.generalPurposeReg[reg] = result
+ cpu.flags.zero = result == 0
cpu.flags.carry = carry
default:
panic(fmt.Sprintf("dunno how to execute instruction %2x (%s)", in.opCode, in.name))
}
-
- // todo: math
- // maybe it should be like:
- // res = cpu.add(v1, v2)
- // where res is a 8bit value, and flags are set by the method accordingly?
-
- // go to next instruction.
- // todo: something is wrong with this design.
- cpu.pc++
}
diff --git a/cpu/video.go b/cpu/video.go
new file mode 100644
index 0000000..fed7e1a
--- /dev/null
+++ b/cpu/video.go
@@ -0,0 +1,215 @@
+package cpu
+
+import (
+ "image"
+ "image/color"
+ "image/png"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/sshaman1101/uvm/defines"
+)
+
+type videoCard struct {
+ mem [defines.VideoWidth * defines.VideoHeight]uint8
+
+ display *image.NRGBA
+ redrawn chan struct{}
+}
+
+// todo:
+// it just a POC, more to fix and reconsider here:
+// * docstrings
+// * do not touch host's file system
+// * we need mem-mapped IO, so videoCard.mem have to be refactored anyway
+
+func newVideo() *videoCard {
+ if err := os.RemoveAll("/tmp/video/"); err != nil {
+ panic(err)
+ }
+ if err := os.Mkdir("/tmp/video/", 0o755); err != nil {
+ panic(err)
+ }
+
+ buf := image.NewNRGBA(image.Rect(0, 0, defines.VideoWidth, defines.VideoHeight))
+
+ black := color.NRGBA{
+ A: 255,
+ R: 0,
+ G: 0,
+ B: 0,
+ }
+
+ for y := 0; y < defines.VideoHeight; y++ {
+ for x := 0; x < defines.VideoWidth; x++ {
+ buf.Set(x, y, black)
+ }
+ }
+
+ video := &videoCard{
+ display: buf,
+ mem: [defines.VideoWidth * defines.VideoHeight]uint8{},
+ redrawn: make(chan struct{}),
+ }
+
+ go video.startWebSocket()
+ return video
+}
+
+func (v *videoCard) render() {
+ for x := 0; x < defines.VideoWidth; x++ {
+ for y := 0; y < defines.VideoHeight; y++ {
+ color8 := v.mem[x*defines.VideoWidth+y]
+ v.point(x, y, color8)
+ }
+ }
+}
+
+const lastFrameAt = "/tmp/video/last.png"
+
+var map3 = [8]uint8{
+ 0, 37, 74, 110, 146, 183, 219, 255,
+}
+
+var map2 = [4]uint8{
+ 0, 85, 170, 255,
+}
+
+func (v *videoCard) point(x, y int, color8 uint8) {
+ r := (color8 >> 5) & 0x07
+ r = map3[r]
+
+ g := (color8 >> 2) & 0x07
+ g = map3[g]
+
+ b := color8 & 0x03
+ b = map2[b]
+
+ v.display.Set(x, y, color.NRGBA{
+ A: 255, R: r, G: g, B: b,
+ })
+}
+
+func (v *videoCard) writeFrame() {
+ // it very unlikely will cause any performance issues,
+ // but consider encode to a bytes buffer once,
+ // then write such buffer to each file.
+ fd, err := os.Create(lastFrameAt)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if err := png.Encode(fd, v.display); err != nil {
+ fd.Close()
+ log.Fatal(err)
+ }
+
+ if err := fd.Close(); err != nil {
+ log.Fatal(err)
+ }
+
+ select {
+ case v.redrawn <- struct{}{}:
+ default:
+ }
+}
+
+// below is a websocket-as-a-display implementation
+
+const (
+ pongWait = 60 * time.Second
+ pingPeriod = (pongWait * 9) / 10
+ filePeriod = 20 * time.Millisecond
+ writeWait = filePeriod
+)
+
+func (v *videoCard) reader(ws *websocket.Conn) {
+ defer ws.Close()
+ ws.SetReadLimit(512)
+ ws.SetReadDeadline(time.Now().Add(pongWait))
+ ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
+ for {
+ _, _, err := ws.ReadMessage()
+ if err != nil {
+ break
+ }
+ }
+}
+
+func (v *videoCard) writer(ws *websocket.Conn) {
+ pingTicker := time.NewTicker(pingPeriod)
+ fileTicker := time.NewTicker(2 * time.Second) // alpha-like frame?
+ defer func() {
+ pingTicker.Stop()
+ fileTicker.Stop()
+ ws.Close()
+ }()
+ readAndSend := func() {
+ bs, err := os.ReadFile(lastFrameAt)
+ if err != nil {
+ log.Printf("failed to read last frame: %v", err)
+ return
+ }
+
+ ws.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := ws.WriteMessage(websocket.BinaryMessage, bs); err != nil {
+ log.Printf("failed to write: %v", err)
+ return
+ }
+ }
+ for {
+ select {
+ case <-fileTicker.C:
+ readAndSend()
+ case <-v.redrawn:
+ readAndSend()
+ case <-pingTicker.C:
+ ws.SetWriteDeadline(time.Now().Add(writeWait))
+ if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
+ return
+ }
+ }
+ }
+}
+
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+}
+
+func (v *videoCard) serveWs(w http.ResponseWriter, r *http.Request) {
+ ws, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ if _, ok := err.(websocket.HandshakeError); !ok {
+ log.Println(err)
+ }
+ return
+ }
+
+ go v.writer(ws)
+ v.reader(ws)
+}
+
+func (v *videoCard) serveIndex(w http.ResponseWriter, r *http.Request) {
+ // TODO(nikonov): use embed
+ // TODO(nikonov): implement websocket reconnet on a client
+ bs, err := os.ReadFile("assets/index.html")
+ if err != nil {
+ panic(err)
+ }
+
+ w.Write(bs)
+}
+
+func (v *videoCard) startWebSocket() {
+ http.HandleFunc("/", v.serveIndex)
+ http.HandleFunc("/ws", v.serveWs)
+
+ log.Printf("starting websocket server at :8080")
+ if err := http.ListenAndServe(":8080", nil); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/defines/defines.go b/defines/defines.go
index 62e89e9..1e37283 100644
--- a/defines/defines.go
+++ b/defines/defines.go
@@ -1,8 +1,10 @@
package defines
const (
- ROMSize = 1 << 16
- RAMSize = 1 << 10
- RegisterCount = 8
- StackDepth = 32
+ RegisterCount = 8
+ AddressWidth = 24
+ StackInitialAddr = (1 << AddressWidth) - 1
+
+ VideoWidth = 800
+ VideoHeight = 600
)
diff --git a/go.mod b/go.mod
index 14a9dd8..5dbb6f3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,17 @@
module github.com/sshaman1101/uvm
-go 1.13
+go 1.22.1
+
+toolchain go1.22.6
require (
+ github.com/gorilla/websocket v1.5.3
github.com/stretchr/testify v1.4.0
- gopkg.in/yaml.v2 v2.2.7
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.0 // indirect
+ github.com/nikonov1101/colors.go v0.0.0-20241018142902-837c37d48f63
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gopkg.in/yaml.v2 v2.2.7 // indirect
)
diff --git a/go.sum b/go.sum
index 4005ec1..9e407d9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,10 +1,15 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/nikonov1101/colors.go v0.0.0-20241018142902-837c37d48f63 h1:UMaWSI0awqe9dVkgy4Ki7T9Y3IzVwoAXcIpFHG5F2sY=
+github.com/nikonov1101/colors.go v0.0.0-20241018142902-837c37d48f63/go.mod h1:PWytY7Q83xkVQbHTlwGpe+vTlSWoLUi8sJTNXzVACg0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
diff --git a/test.asm b/test.asm
index d489c74..bd08267 100644
--- a/test.asm
+++ b/test.asm
@@ -12,7 +12,30 @@ NOP
; check pop
POP r5
; check mem load
-MOV r3, $0101
+LOAD r3 $1a2b
+
+MOV r2 #aa
+STORE $1a2c r2
+
+;test conditionals
+MOV r3 #1
+MOV r4 #a1
+CP r3 r4
+JNE $001A
+
+NOP
+NOP
+NOP
+NOP
+NOP
+
+.text $001A
+INC r3
+INC r3
+CP r3 #3
+JE $01FF
+
+
NOP
; check jump
JUMP $00FF
@@ -22,6 +45,12 @@ JUMP $00FF
.text $00FF
HALT
+.text $01FF
+ HALT
+
+.text $02FF
+ HALT
+
; place random value at $0100
; check that we can compile .byte's
.byte $0101 #42