diff --git a/UE1/Software/Go-UE1/README.md b/UE1/Software/Go-UE1/README.md new file mode 100644 index 0000000..4bcc546 --- /dev/null +++ b/UE1/Software/Go-UE1/README.md @@ -0,0 +1,54 @@ +# UE1 Assembler and Emulator in Go + +For those who would rather not fire up DOSBox and QuickBASIC to write and run UE1 software, this is for you! + +## Building + +`go build` in this directory should be sufficient on any operating system where Go is installable. + +## Usage + +### ue1 build + +`ue1 build ` is the most basic use. Extension can be anything, but the default output is `.bin`. Only the final extension is changed, so `foo.bar.asm` would become `foo.bar.bin`. + +If desired, you can pass the `-d` or `--dump` flags to dump the binary to stdout rather than a bin file. This will print the bytes in a human-readable form (ala hexdump), as below: + +``` +000  40 A8 B8 58 80 81 82 83  84 85 86 87 88 89 8A 8B  |@..X............| +010  8C 8D 8E 8F 90 98 40 58  10 24 80 11 25 81 12 26  |......@X.$..%..&| +020  82 13 27 83 10 88 11 89  12 8A 13 8B 40 58 10 24  |..'.........@X.$| +030  84 11 25 85 12 26 86 13  27 87 14 88 15 89 16 8A  |..%..&..'.......| +040  17 8B 40 58 10 24 80 11  25 81 12 26 82 13 27 83  |..@X.$..%..&..'.| +050  10 88 11 89 12 8A 13 8B  40 58 10 24 84 11 25 85  |........@X.$..%.| +060  12 26 86 13 27 87 14 88  15 89 16 8A 17 8B 40 58  |.&..'.........@X| +070  10 24 80 11 25 81 12 26  82 13 27 83 10 88 11 89  |.$..%..&..'.....| +080  12 8A 13 8B 40 58 10 24  84 11 25 85 12 26 86 13  |....@X.$..%..&..| +090  27 87 14 88 15 89 16 8A  17 8B 40 58 10 24 80 11  |'.........@X.$..| +0A0  25 81 12 26 82 13 27 83  10 88 11 89 12 8A 13 8B  |%..&..'.........| +0B0  40 58 10 24 84 11 25 85  12 26 86 13 27 87 28 8C  |@X.$..%..&..'.(.| +0C0  14 88 15 89 16 8A 17 8B  C0 F0 |..........| +0CA +``` + +### ue1 emu + +Options are: + +``` +-a +--asm + Use provided unassembled assembly file for input. + +-n +--non-interactive + Do not display execution, only show output register (and maybe ring terminal bell). + +-s +--speed + Speed in Hertz. Must be an integer. Default is 60. +``` + +## Demo + +![Fibonacci at 10 Hz](demo/demo.gif) diff --git a/UE1/Software/Go-UE1/asm.go b/UE1/Software/Go-UE1/asm.go new file mode 100644 index 0000000..b76de6e --- /dev/null +++ b/UE1/Software/Go-UE1/asm.go @@ -0,0 +1,112 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "time" +) + +func getBitPosition(addr string) (int, error) { + pos, err := strconv.Atoi(string(addr[2])) + if err != nil { + return -1, fmt.Errorf("error converting address bit position: %v", err) + } + return pos, nil +} + +func (c *CPU) parseAddressString(addr string) (err error) { + var bitPos int + dcAddress := strings.ToLower(addr) + + switch { + case strings.HasPrefix(dcAddress, "sr"): + if bitPos, err = getBitPosition(dcAddress); err == nil { + c.whichBit = 1 << bitPos + c.whichOutput = Scratch + c.tmpbit = (c.register.scratch & c.whichBit) >> bitPos + } + case strings.HasPrefix(dcAddress, "or"): + if bitPos, err = getBitPosition(dcAddress); err == nil { + c.whichBit = 1 << bitPos + c.whichOutput = Output + c.tmpbit = (c.register.output & c.whichBit) >> bitPos + } + case strings.HasPrefix(dcAddress, "ir"): + if bitPos, err = getBitPosition(dcAddress); err == nil { + c.tmpbit = (c.register.input & c.whichBit) >> bitPos + } + case dcAddress == "rr": + c.tmpbit = c.register.rr + default: + return fmt.Errorf("unable to parse address pnemonic `%s`: %v", + addr, err) + } + return +} + +func (c *CPU) parseOpcodeString(opcode string) (err error) { + // I ain't writin' no long switch statement twice! + dcOpcode := strings.ToLower(opcode) + opcodeBin := opcodes[dcOpcode] + + return c.parseOpcodeBin(opcodeBin) +} + +func (c *CPU) processAsm(args []string, speed int, nonint bool) { + ticker := time.NewTicker(time.Second / time.Duration(speed)) + defer ticker.Stop() + + source, err := os.Open(args[len(args)-1]) + if err != nil { + fatalln(err) + } + defer source.Close() + + scanner := bufio.NewScanner(source) + for scanner.Scan() { + line := scanner.Text() + tokens := strings.Fields(line) + + if tokens[0] == ";" { + continue + } + + if err = c.parseAddressString(tokens[1]); err != nil { + fatalln(err) + } + + if err = c.parseOpcodeString(tokens[0]); err != nil { + fatalln(err) + } + + if c.flag.wrt == 1 { + if c.whichOutput == Output { + err = c.writeMemRegister(Output) + } else { + err = c.writeMemRegister(Scratch) + } + } + if err != nil { + fatalln(err) + } + + if nonint { + c.flag.wrt = 0 + continue + } + + <-ticker.C + fmt.Println("INSTRUCTION : ", tokens[0]) + fmt.Println("MEMORY ADDRESS: ", tokens[1]) + + c.printCpuStat() + c.flag.wrt = 0 + } + if nonint { + fmt.Printf("OUTPUT = %b (%d)\n", + c.register.output, c.register.output) + } +} diff --git a/UE1/Software/Go-UE1/bin.go b/UE1/Software/Go-UE1/bin.go new file mode 100644 index 0000000..21fc641 --- /dev/null +++ b/UE1/Software/Go-UE1/bin.go @@ -0,0 +1,185 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "time" +) + +var opcodeStrings = [16]string{ + "NOP0", "LD", "ADD", "SUB", + "ONE", "NAND", "OR", "XOR", + "STO", "STOC", "IEN", "OEN", + "IOC", "RTN", "SKZ", "NOPF", +} + +func (c *CPU) parseOpcodeBin(b byte) (err error) { + switch b { + case nopz: + c.flag.zero = 1 + case ld: + if c.register.ien == 1 { + c.register.rr = c.tmpbit + } + case add: + if c.register.ien == 1 { + c.tmprr = c.register.rr + c.register.carry + c.tmpbit + + c.register.rr = c.tmprr & 1 + c.register.carry = (c.tmprr & 0b10) >> 1 + } + case sub: + if c.register.ien == 1 { + c.tmpdb = ^c.tmpbit & 1 + c.tmprr = c.register.rr + c.register.carry + c.tmpdb + + c.register.rr = c.tmprr & 1 + c.register.carry = (c.tmprr & 0b10) >> 1 + } + case one: + c.register.rr = 1 + c.register.carry = 0 + case nand: + if c.register.ien == 1 { + c.tmprr = c.register.rr & c.tmpbit + + if c.tmprr == 1 { + c.register.rr = 0 + } else if c.register.rr == 0 { + c.register.rr = 1 + } + } + case or: + if c.register.ien == 1 { + c.register.rr = c.register.rr | c.tmpbit + } + case xor: + if c.register.ien == 1 { + c.register.rr = c.register.rr ^ c.tmpbit + } + case sto: + if c.register.oen == 1 { + c.flag.wrt = 1 + } + case stoc: + if c.register.oen == 1 { + c.flag.wrt = 1 + c.tmprr = ^c.register.rr & 1 + } + case ien: + c.register.ien = c.tmpbit + case oen: + c.register.oen = c.tmpbit + case ioc: + c.flag.ioc = 1 + fmt.Print("\a") // BEEP, depending on terminal settings + case rtn: + c.flag.rtn = 1 + case skz: + c.flag.skz = 1 + case nopf: + c.flag.f = 1 + default: + return fmt.Errorf("unrecognized opcode. byte value: %X", b) + } + + if c.flag.wrt == 1 { + if b != stoc { + c.tmprr = c.register.rr + } + } + return +} + +func (c *CPU) parseAddressBin(b byte) (err error) { + var bitPos int + + op := b & 0xF0 + addr := b & 0x0F + switch { + case addr < 0x08: + // SR0 - SR7 + c.whichBit = 1 << int(addr) + c.whichOutput = Scratch + c.tmpbit = (c.register.scratch & c.whichBit) >> int(addr) + case addr == 0x08 && !(op == sto || op == stoc): + // RR + c.tmpbit = c.register.rr + case addr < 0x10: + bitPos = int(addr - 0x08) + c.whichBit = 1 << bitPos + + // OR0 - OR7 + if op == sto || op == stoc { + c.whichBit = 1 << bitPos + c.whichOutput = Output + c.tmpbit = (c.register.output & c.whichBit) >> bitPos + } else { + // IR1 - IR7 + c.tmpbit = (c.register.input & c.whichBit) >> bitPos + } + default: + return fmt.Errorf("unable to parse address in byte: %v", err) + } + return +} + +func (c *CPU) processBin(args []string, speed int, nonint bool) { + ticker := time.NewTicker(time.Second / time.Duration(speed)) + defer ticker.Stop() + + source, err := os.Open(args[len(args)-1]) + if err != nil { + fatalln(err) + } + defer source.Close() + + reader := bufio.NewReader(source) + for { + b, err := reader.ReadByte() + if err != nil { + if err.Error() == "EOF" { + break + } + fatalln(err) + } + + if err = c.parseAddressBin(b); err != nil { + fatalln(err) + } + + if err = c.parseOpcodeBin(b & 0xF0); err != nil { + fatalln(err) + } + + if c.flag.wrt == 1 { + if c.whichOutput == Output { + err = c.writeMemRegister(Output) + } else { + err = c.writeMemRegister(Scratch) + } + } + if err != nil { + fatalln(err) + } + + if nonint { + c.flag.wrt = 0 + continue + } + + <-ticker.C + opString := opcodeStrings[b&0xF0>>4] + addrString := fmt.Sprintf("%02X", int(b&0x0F)) + fmt.Println("INSTRUCTION : ", opString) + fmt.Println("MEMORY ADDRESS: ", addrString) + + c.printCpuStat() + c.flag.wrt = 0 + } + if nonint { + fmt.Printf("OUTPUT = %b (%d)\n", + c.register.output, c.register.output) + } +} diff --git a/UE1/Software/Go-UE1/build.go b/UE1/Software/Go-UE1/build.go new file mode 100644 index 0000000..81cdf7e --- /dev/null +++ b/UE1/Software/Go-UE1/build.go @@ -0,0 +1,185 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "os" + "strings" +) + +func isAscii(b byte) bool { return b >= 32 && b <= 126 } + +func writeOpcode(op string) (byte, error) { + dcOp := strings.ToLower(op) + if b, ok := opcodes[dcOp]; ok { + return b, nil + } + return 0, fmt.Errorf("Opcode invalid: %s\n", op) +} + +func writeAddress(b byte, addr string) (byte, error) { + var err error + var bitPos int + dcAddress := strings.ToLower(addr) + + switch { + case strings.HasPrefix(dcAddress, "sr"): + if bitPos, err = getBitPosition(dcAddress); err == nil { + b += byte(bitPos) + } + case strings.HasPrefix(dcAddress, "or"), + strings.HasPrefix(dcAddress, "ir"): + if bitPos, err = getBitPosition(dcAddress); err == nil { + b += byte(bitPos + 8) + } + case dcAddress == "rr": + b += 8 + default: + return 0, fmt.Errorf("Memory address invalid: %v\n", err) + } + return b, nil +} + +func makeBinFilename(filename string) string { + split := strings.Split(filename, ".") + prefix := strings.Join(split[:len(split)-1], ".") + + return prefix + ".bin" +} + +func handleDump(bytes []byte) { + for i := 0; i < len(bytes); i++ { + // byte count to start the row, much like hexdump + if i%16 == 0 { + fmt.Printf("%03X\u00A0 ", i) + } + + // bytes are a space apart, except after 8 bytes, + // just like hexd-... well, you get the idea. + fmt.Printf("%02X ", bytes[i]) + if (i+1)%8 == 0 { + fmt.Printf("\u00A0") + } + + // there will likely never be deliberate characters + // in the output, but we're on a roll! + if (i+1)%16 == 0 { + fmt.Printf("|") + for j := i - 15; j <= i; j++ { + if isAscii(bytes[j]) { + fmt.Printf("%c", bytes[j]) + } else { + fmt.Printf(".") + } + } + fmt.Printf("|\n") + } + } + + // handle the last partial line, if there is one + if len(bytes)%16 != 0 { + remaining := len(bytes) % 16 + for k := 0; k < (16-remaining)*3; k++ { // padding + fmt.Printf(" ") + } + if (16 - remaining) > 8 { + fmt.Printf(" ") // add extra space for the 8-byte boundary + } + fmt.Printf(" |") + + start := len(bytes) - remaining + for l := start; l < len(bytes); l++ { + if isAscii(bytes[l]) { + fmt.Printf("%c", bytes[l]) + } else { + fmt.Printf(".") + } + } + fmt.Printf("|\n") + } + fmt.Printf("%03X\n", len(bytes)) +} + +func assemble(f *os.File) (bytes []byte, err error) { + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + tokens := strings.Fields(line) + + if tokens[0] == ";" { + continue + } + + op, err := writeOpcode(tokens[0]) + if err != nil { + break + } + + fullbyte, err := writeAddress(op, tokens[1]) + if err != nil { + break + } + + bytes = append(bytes, fullbyte) + } + return +} + +func buildAsm(args []string) { + flagSet := flag.NewFlagSet("ue1 build", flag.ExitOnError) + flagSet.Usage = usage("Usage: ue1 build (options) \n", flagSet) + + dump := flagSet.Bool("d", false, "") + flagSet.BoolVar(dump, "dump", false, "dump assembled output to stdout in hex") + + output := flagSet.String("o", "", "filename for output (default is .bin)") + + err := flagSet.Parse(args) + if err != nil { + fatalln(err) + } + args = flagSet.Args() + + if len(args) != 1 { + fatalln(fmt.Errorf("ASM file not supplied")) + } + + asmFile := args[len(args)-1] + binFile := makeBinFilename(asmFile) + if *output != "" { + binFile = *output + } + + src, err := os.Open(asmFile) + if err != nil { + fatalln(err) + } + + assembled, err := assemble(src) + if err != nil { + fatalln(err) + } + + // dump to stdout rather than a binary, in a hexdump-y sort of way + if *dump { + handleDump(assembled) + return + } + + dest, err := os.Create(binFile) + if err != nil { + fatalln(err) + } + defer dest.Close() + + length, err := dest.Write(assembled) + if err != nil { + fatalln(err) + } + dest.Sync() + + fmt.Printf("Wrote %d bytes to '%s'\n", length, binFile) +} diff --git a/UE1/Software/Go-UE1/demo/demo.gif b/UE1/Software/Go-UE1/demo/demo.gif new file mode 100644 index 0000000..28054df Binary files /dev/null and b/UE1/Software/Go-UE1/demo/demo.gif differ diff --git a/UE1/Software/Go-UE1/emu.go b/UE1/Software/Go-UE1/emu.go new file mode 100644 index 0000000..e5e92d2 --- /dev/null +++ b/UE1/Software/Go-UE1/emu.go @@ -0,0 +1,118 @@ +package main + +import ( + "flag" + "fmt" +) + +type Flags struct { + f byte + ioc byte + skz byte + rtn byte + wrt byte + zero byte +} + +type Registers struct { + carry byte + ien byte + input byte + oen byte + output byte + rr byte + scratch byte +} + +type CPU struct { + tmpbit byte + tmpdb byte + tmprr byte + + whichBit byte + whichOutput int + + flag *Flags + register *Registers +} + +func (c *CPU) writeMemRegister(outRegister int) (err error) { + registerValue := c.register.scratch + if outRegister == Output { + registerValue = c.register.output + } + + val := registerValue + if c.tmpbit == 1 && c.tmprr == 0 { + val = registerValue - c.whichBit + } else if c.tmpbit == 0 && c.tmprr == 1 { + val = registerValue + c.whichBit + } + + switch outRegister { + case Output: + c.register.output = val + case Scratch: + c.register.scratch = val + default: + return fmt.Errorf("unrecognized output number. Got: `%d`", + outRegister) + } + return +} + +func (c *CPU) printCpuStat() { + fmt.Println("--------------------") + fmt.Println("REGISTERS") + fmt.Printf("CARRY = %b\n", c.register.carry) + fmt.Printf("RESULTS = %b\n", c.register.rr) + fmt.Printf("INPUT EN = %b\n", c.register.ien) + fmt.Printf("OUTPUT EN = %b\n", c.register.oen) + fmt.Printf("SCRATCH = %b (%d)\n", + c.register.scratch, c.register.scratch) + fmt.Printf("OUTPUT = %b (%d)\n", + c.register.output, c.register.output) + fmt.Printf("INPUT SW. = %b\n\n", c.register.input) + + fmt.Println("FLAGS") + fmt.Printf("FLAG 0 = %b\n", c.flag.zero) + fmt.Printf("WRITE = %b\n", c.flag.wrt) + fmt.Printf("I/O CON = %b\n", c.flag.ioc) + fmt.Printf("RETURN = %b\n", c.flag.rtn) + fmt.Printf("SKIP Z = %b\n\n", c.flag.skz) +} + +func (c *CPU) startEmulation(args []string) { + flagSet := flag.NewFlagSet("ue1 emu", flag.ExitOnError) + + flagSet.Usage = usage("Usage: ue1 emu (options) \n", flagSet) + + asmRun := flagSet.Bool("a", false, "") + flagSet.BoolVar(asmRun, "asm", false, "use a .asm file for input rather than a binary") + + nonInt := flagSet.Bool("n", false, "") + flagSet.BoolVar(nonInt, "non-interactive", false, "noninteractive mode - only output register displayed") + + speed := flagSet.Int("s", 60, "") + flagSet.IntVar(speed, "speed", 60, "simulated clock speed in Hertz") + + err := flagSet.Parse(args) + if err != nil { + fatalln(err) + } + args = flagSet.Args() + + if len(args) != 1 { + fatalln(fmt.Errorf("input file not supplied")) + } + + if *speed <= 0 { + fatalf("user-supplied speed cannot be used. got: %d\n", *speed) + } + + if *asmRun { + c.processAsm(args, *speed, *nonInt) + return + } + c.processBin(args, *speed, *nonInt) +} diff --git a/UE1/Software/Go-UE1/go.mod b/UE1/Software/Go-UE1/go.mod new file mode 100644 index 0000000..735dd2b --- /dev/null +++ b/UE1/Software/Go-UE1/go.mod @@ -0,0 +1,3 @@ +module ue1 + +go 1.23.5 diff --git a/UE1/Software/Go-UE1/main.go b/UE1/Software/Go-UE1/main.go new file mode 100644 index 0000000..3b64a60 --- /dev/null +++ b/UE1/Software/Go-UE1/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +// enumerate both output and opcodes here in main - +// assembly and emulation both need these constants +const ( + Output = iota + Scratch +) + +const ( + nopz byte = iota << 4 + ld + add + sub + one + nand + or + xor + sto + stoc + ien + oen + ioc + rtn + skz + nopf +) + +// a little concordance we'll use later +var opcodes = map[string]byte{ + "nop0": nopz, "ld": ld, "add": add, "sub": sub, + "one": one, "nand": nand, "or": or, "xor": xor, + "sto": sto, "stoc": stoc, "ien": ien, "oen": oen, + "ioc": ioc, "rtn": rtn, "skz": skz, "nopf": nopf, +} + +func fatalf(f string, v ...any) { + fmt.Printf(f, v) + os.Exit(1) +} + +func fatalln(e error) { + fmt.Println(e) + os.Exit(1) +} + +func usage(msg string, f *flag.FlagSet) func() { + return func() { + fmt.Fprintf(os.Stderr, msg) + + if f != nil { + f.PrintDefaults() + } + os.Exit(1) + } +} + +func main() { + defaultUsage := usage("Usage: ue1 [build, emu] (options) \n", nil) + flag.Usage = defaultUsage + flag.Parse() + + args := flag.Args() + if len(args) == 0 { + defaultUsage() + } + cmd, args := args[0], args[1:] + + switch cmd { + case "build": + buildAsm(args) + case "emu": + ueOne := &CPU{ + flag: new(Flags), + register: new(Registers), + } + + ueOne.startEmulation(args) + default: + fmt.Printf("Unrecognized subcommand %q.\n", cmd) + defaultUsage() + } +}