initial commit

This commit is contained in:
slederer 2024-09-19 14:12:22 +02:00
commit 60db522e87
107 changed files with 36924 additions and 0 deletions

7
tridoraemu/IOHandler.go Normal file
View file

@ -0,0 +1,7 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
type IOHandler interface {
read(byteaddr word) (word, error)
write(value word, byteaddr word) (error)
}

17
tridoraemu/LICENSE.md Normal file
View file

@ -0,0 +1,17 @@
# Copyright and Licensing
All files, except where explicitly stated otherwise, are licensed according to the BSD-3-Clause license as follows:
------------------------------------------------------------------------------
Copyright 2024 Sebastian Lederer
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

53
tridoraemu/README.md Normal file
View file

@ -0,0 +1,53 @@
# Tridora Emulator
- an emulator for the Tridora CPU / System
- emulates the CPU, UART, SD-Card controller, VGA controller
- supports reading the tick counter from the interrupt controller, but does not support any interrupts
- written in Golang
## Getting started
From the command line, run the *tridoraemu* or *tridoraemu.exe* program inside the *tridoraemu* directory (see below for details).
A precompiled binary for Windows is provided.
To build the program yourself, you need to have the Go language installed on your system. Building has been tested on Windows and Linux.
## Building
Run the following commands inside the *tridoraemu* directory:
go get
go build
On the first run, this may take a while as the go build system fetches some external libraries and compiles them.
## Running the emulator
Start the *tridoraemu* binary in the same directory as the SD-Card image file (*sdcard.img*) and the ROM file (*rommon.prog*). It needs to be started on the command line as it uses the terminal window for the serial console. On startup, the emulator opens the VGA framebuffer window which is only used for graphics output.
The Tridora software (esp. the editor) requires a decent vt100-compatible (plus colors) terminal emulator. It has been successfully tested with (new) Windows Terminal, Alacritty, WezTerm and xterm.
The color scheme in the editor is meant for a dark terminal background.
The runtime system expects the Backspace key to send the DEL character (ASCII 127).
## Stopping the emulator
To stop the emulator, close the VGA framebuffer window.
The emulator will also stop if it encounters an infinite loop (a BRANCH @+0 instruction).
## Things to try out
On the ROM monitor prompt, press *B* to boot from the SD-card image. This should boot into the shell, which will first require you to enter the current date and time.
In the shell, try the *L* command to list directories and the *V* command to change volumes. The *Examples* volume contains some example programs in source form.
The programs *lines*, *conway* and *mandelbrot*, among others, show some (hopefully) interesting VGA graphics. The *viewpict* program can show image files (*.pict files) which contain 640x400x4 bitmaps. A few sample image files are provided.
To compile a program, set the file name (e.g. *lines.pas*) with the *W* command in the shell. Then, use *B* and *R* to build and run the program.
To edit the source file, have the name set with *W* and then use the *E* shell command. Inside the editor, press F1 for the help screen (and RETURN to leave the help screen). Control-X exits the editor, abandoning any changes.
The volume *Testvolume 1* (note the space) contains a precompiled game called *chase*. This is a game that was written for UCSD Pascal around 1980, and compiles with a few lines of changes with the Tridora Pascal compiler. The source code is also provided on that volume.
You can run the program with the *O* command in the shell (just press Return for the program arguments), or you can set the workfile name with *W* and then use the *R* command.
The *K* command in the shell is used to reclaim the space occupied by deleted or overwritten files.
A running program can be terminated by pressing Control-C, but only if the program is expecting keyboard input at that time.

35
tridoraemu/console.go Normal file
View file

@ -0,0 +1,35 @@
// +build !windows
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"os"
"golang.org/x/term"
)
type ConsoleState struct {
state term.State
}
func SetRawConsole() (*ConsoleState, error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
return &ConsoleState{*oldState}, err
}
func RestoreConsole(st *ConsoleState) error {
return term.Restore(int(os.Stdin.Fd()), &st.state)
}
func ConsoleRead(buf []byte) (count int, err error) {
n, e := os.Stdin.Read(buf)
return n, e
}
func ConsoleWrite(char byte) (err error) {
buf := make([] byte, 1)
buf[0] = char
_ , e := os.Stdout.Write(buf)
return e
}

View file

@ -0,0 +1,75 @@
// +build windows
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"io"
"os"
"golang.org/x/sys/windows"
)
type ConsoleState struct {
modeStdin uint32
modeStdout uint32
}
func SetRawConsole() (*ConsoleState, error) {
var stIn uint32
var stOut uint32
stdinFd := os.Stdin.Fd()
if err := windows.GetConsoleMode(windows.Handle(stdinFd), &stIn); err != nil {
return nil, err
}
raw := stIn &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
if err := windows.SetConsoleMode(windows.Handle(stdinFd), raw); err != nil {
return nil, err
}
/*
stdoutFd := os.Stdout.Fd()
if err := windows.GetConsoleMode(windows.Handle(stdoutFd), &stOut); err != nil {
return nil, err
}
raw = stOut | windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_OUTPUT
if err := windows.SetConsoleMode(windows.Handle(stdoutFd), raw); err != nil {
return nil, err
}
*/
return &ConsoleState{stIn,stOut}, nil
}
func RestoreConsole(st *ConsoleState) error {
stdinFd := os.Stdin.Fd()
stdoutFd := os.Stdout.Fd()
err := windows.SetConsoleMode(windows.Handle(stdinFd), st.modeStdin)
if err != nil { return err }
err = windows.SetConsoleMode(windows.Handle(stdoutFd), st.modeStdin)
return err
}
func ConsoleRead(buf []byte) (count int, err error) {
n, e := os.Stdin.Read(buf)
if e == io.EOF { // ugly hack to handle ^Z on windows
// this can probably be done in a better way
// but tbh I am glad it works and I don't
// have to dig deeper into that windows
// console i/o crap
n = 1; buf[0] = 26
return n, nil
}
return n, e
}
func ConsoleWrite(char byte) (err error) {
buf := make([] byte, 1)
buf[0] = char
_ , err = os.Stdout.Write(buf)
return err
}

467
tridoraemu/cpu.go Normal file
View file

@ -0,0 +1,467 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"fmt"
"strings"
)
type word uint32
const wordbits = 32
const wordbytes = 4
const wordmask = 0xFFFFFFFF
const hsbmask = 0x80000000
const estackDepth = 64
type CPU struct {
ESP word;
X word;
PC, FP,BP,RP word;
IR,IV word;
estack [estackDepth] word;
mem *Mem;
stopped bool
trace bool
singlestep bool
}
func sign_extend(bits word, wordbits int) int {
signmask := word(1 << (wordbits - 1))
signbit := (bits & signmask) != 0
// fmt.Printf("sign_extend %b %v signmask %08X signbit %v\n", bits, wordbits, signmask, signbit)
if signbit {
return int(bits & ^signmask) - int(signmask)
} else {
return int(bits)
}
}
func (c *CPU) initialize() {
c.ESP = 0
c.PC = 0
c.X = 0
c.FP = 65020 // these are the values set by the ROM monitor
c.RP = 65024 // for FP and RP
c.singlestep = false
}
func (c *CPU) printEstack() {
fmt.Printf("[")
for i := word(1); i <= c.ESP; i++ {
fmt.Printf(" %08X", c.estack[i])
}
fmt.Printf(" %08X ] (%v)\n", c.X, c.ESP)
}
func (c *CPU) showStep(desc string, operand int, opWidth int) {
if !c.trace { return }
var opStr string
if opWidth == 0 {
opStr = ""
} else {
opStr = fmt.Sprintf(" %04X", operand)
}
fmt.Printf("%08x %-10s%5s ", c.PC, desc, opStr)
c.printEstack()
}
func (c *CPU) getOperand(insWord word, bits int) int {
return int(insWord & word((1 << bits) - 1))
}
func (c *CPU) getSignedOperand(insWord word, bits int) int {
bitValue := (1 << (bits - 1))
signBit := insWord & word(bitValue)
bitMask := (bitValue << 1) - 1
//fmt.Printf("getSignedOperand bitValue: %v, bitMask: %v\n", bitValue, bitMask)
o := int(insWord & word(bitMask))
if signBit != 0 {
o = int(o) - (bitValue << 1)
}
//fmt.Printf("getSignedOperand: %v %v -> %d\n",insWord, bits, o)
return o
}
func (c *CPU) getBit(insWord word, bitnum int) int {
return int(insWord >> bitnum) & 1
}
func (c *CPU) getBits(insWord word, mask word, shiftCount int) word {
return (insWord & mask) >> shiftCount
}
func (c *CPU) getSignedBits(insWord word, mask word, shiftCount int) int {
result := (insWord & mask) >> shiftCount
signMask := ((mask >> shiftCount) + 1) >> 1
//fmt.Printf("getSignedBits %016b signMask %v signBit %v\n", insWord, signMask, insWord & signMask)
if result & signMask != 0 {
return - int(result & ^signMask)
} else {
return int(result)
}
}
func (c * CPU) addToWord(v word, offset int) word {
if offset < 0 {
v -= word(-offset)
} else {
v += word(offset)
}
return v
}
func (c *CPU) step() error {
nPC := c.PC
nFP := c.FP
nBP := c.BP
nRP := c.RP
nX := c.X
nESP := c.ESP
Y := c.estack[c.ESP]
insWord, err := c.mem.read(c.PC)
if err != nil { return err }
if c.PC % 4 == 0 {
insWord = insWord >> 16
} else {
insWord = insWord & 0xFFFF
}
baseIns := insWord >> 13
nPC += 2
x2y := false
deltaESP := 0
oplen := 0
switch baseIns {
// BRANCH
case 0b000:
operand := c.getSignedOperand(insWord, 13)
c.showStep("BRANCH", operand, 13)
nPC = word(int(c.PC) + operand)
if operand == 0 {
fmt.Printf("BRANCH 0 encountered - stopped at PC %08X\n", nPC)
c.stopped = true
}
// ALU
case 0b001:
aluop := c.getBits(insWord, 0b0001111000000000, 9)
deltaESP = c.getSignedBits(insWord, 0b0000000000110000, 4)
ext := c.getBit(insWord, 7) != 0
x2y = c.getBit(insWord, 6) != 0
operand := c.getOperand(insWord, 4)
// fmt.Printf("aluop %v %v %v %v %v\n", aluop, s, ext, x2y, operand)
name := "ALU"
switch aluop {
case 0:
name = "ADD"
nX = c.X + Y
case 1:
name = "SUB"
nX = Y - c.X
case 2:
name = "NOT"
nX = ^ c.X
case 3:
name = "AND"
nX = c.X & Y
case 4:
name = "OR"
nX = c.X | Y
case 5:
name = "XOR"
nX = c.X ^ Y
case 6:
name = "CMP"
oplen = 2
cmp_i := c.getBit(insWord,2) != 0
cmp_eq := c.getBit(insWord,1) != 0
cmp_lt := c.getBit(insWord,0) != 0
s_x := sign_extend(c.X, wordbits)
s_y := sign_extend(Y, wordbits)
result := (cmp_eq && (s_x == s_y)) || (cmp_lt && (s_y < s_x))
if cmp_i { result = !result }
if result { nX = 1 } else { nX = 0 }
case 7:
name = "Y"
if !x2y && (deltaESP == -1) { name = "DROP" }
if x2y && (deltaESP == 0) { name = "SWAP" }
nX = Y
case 8:
name = "SHR"
nX = c.X >> 1
if ext {
nX = nX | (c.X & hsbmask)
}
case 9:
name = "SHL"
nX = c.X << 1
if operand & 2 != 0 {
nX = nX << 1
}
oplen = 2
case 10:
name = "INC"
oplen = 4
if (operand == 0) && (deltaESP == 1) && x2y { name = "DUP" }
nX = c.X + word(operand)
case 11:
name = "DEC"
oplen = 4
nX = c.X - word(operand)
case 12:
name = "CMPU"
oplen = 2
cmp_i := c.getBit(insWord,2) != 0
cmp_eq := c.getBit(insWord,1) != 0
cmp_lt := c.getBit(insWord,0) != 0
result := (cmp_eq && (c.X == Y)) || (cmp_lt && (Y < c.X))
if cmp_i { result = !result }
if result { nX = 1 } else { nX = 0 }
case 13:
name = "BPLC"
nX = (c.X & 0xFF) << ((3 - (Y & 3)) * 8)
case 14:
name = "BROT"
nX = ((c.X & 0x00FFFFFF) << 8 ) | ((c.X & 0xFF000000) >> 24)
case 15:
name = "BSEL"
shift := (3 - (Y & 3)) * 8
nX = (c.X >> shift) & 0xFF
}
c.showStep(name, operand, oplen)
// STORE
case 0b010:
operand := c.getOperand(insWord, 13)
var ea word
var name string
if (insWord & 1) == 1 {
name = "STORE.B"
ea = c.BP + word(operand)
} else {
name = "STORE"
ea = c.FP + word(operand)
}
c.showStep(name, operand, oplen)
err = c.mem.write(c.X, ea)
if err != nil { return err }
deltaESP = -1
nX = Y
// XFER
case 0b011:
var name string
deltaRP := c.getSignedBits(insWord, 0b0000001100000000, 8)
deltaESP = c.getSignedBits(insWord, 0b0000000000110000, 4)
r2p := c.getBit(insWord, 7) != 0
p2r := c.getBit(insWord, 6) != 0
x2p := c.getBit(insWord, 0) != 0
if deltaRP >= 0 {
nRP = c.RP + word(deltaRP * wordbytes)
} else {
nRP = c.RP - word(-deltaRP * wordbytes)
}
if (deltaRP == 1) && (deltaESP == -1) && p2r && x2p {
name = "CALL"
} else
if (deltaRP == -1) && (deltaESP == 0) && r2p {
name = "RET"
} else
if (deltaRP == 0) && (deltaESP == -1) && x2p && !p2r {
name = "JUMP"
} else {
var b strings.Builder
b.WriteString("XFER")
if deltaRP == -1 { b.WriteString(".RSM1") }
if deltaRP == 1 { b.WriteString(".RS1") }
if deltaESP == -1 { b.WriteString(".SM1") }
if deltaESP == 1 { b.WriteString(".S1") }
if r2p { b.WriteString(".R2P") }
if p2r { b.WriteString(".P2R") }
if x2p { b.WriteString(".X2P") }
name = b.String()
}
c.showStep(name, 0, 0)
if r2p {
nPC, err = c.mem.read(c.RP)
if err != nil { return err }
}
if p2r {
err = c.mem.write(nPC, nRP)
if err != nil { return err }
}
if x2p {
nPC = c.X
nX = Y
}
// LOAD
case 0b100:
operand := c.getOperand(insWord, 13)
var ea word
var name string
if (insWord & 1) == 1 {
name = "LOAD.B"
operand &= ^1
ea = c.BP + word(operand)
} else {
name = "LOAD"
ea = c.FP + word(operand)
}
c.showStep(name, operand, oplen)
deltaESP = 1
x2y = true
nX, err = c.mem.read(ea)
if err != nil { return err }
// CBRANCH
case 0b101:
operand := c.getSignedOperand(insWord, 13)
var name string
invert := (operand & 1) == 0
operand = operand & -2 // clear bit 0
if invert { name = "CBRANCH.Z" } else { name = "CBRANCH" }
c.showStep(name, operand, 13)
deltaESP = -1
nX = Y
if (c.X != 0 && !invert) || (c.X == 0 && invert) {
nPC = word(int(c.PC) + operand)
}
// LOADC
case 0b110:
operand := c.getSignedOperand(insWord, 13)
oplen = 13
c.showStep("LOADC", operand, oplen)
deltaESP = 1
x2y = true
nX = word(operand)
// EXT
case 0b111:
extop := c.getBits(insWord, 0b0001111000000000, 10)
deltaESP = c.getSignedBits(insWord, 0b0000000000110000, 4)
writeFlag := c.getBit(insWord, 9) != 0
// signExtend := c.getBit(insWord,7) != 0
x2y = c.getBit(insWord, 6) != 0
operand := c.getOperand(insWord, 4)
var name string
switch extop {
// LOADREG/STOREREG
case 0:
oplen = 4
if writeFlag {
name = "STOREREG"
switch operand {
case 0: nFP = c.X
case 1: nBP = c.X
case 2: nRP = c.X
case 3: c.IV = c.X // should be nIV
case 4: c.IR = c.X // should be nIR
default: fmt.Errorf("Invalid STOREREG operand %v at %08X", operand, c.PC)
}
c.showStep(name, operand, oplen)
deltaESP = -1
x2y = false
nX = Y
} else {
name = "LOADREG"
switch operand {
case 0: nX = c.FP
case 1: nX = c.BP
case 2: nX = c.RP
case 3: nX = c.IV
case 4: nX = c.IR
case 5: nX = c.ESP
default: fmt.Errorf("Invalid LOADREG operand %v at %08X", operand, c.PC)
}
c.showStep(name, operand, oplen)
deltaESP = 1
x2y = true
}
// LOADI/STOREI
case 1:
if writeFlag { name = "STOREI" } else { name = "LOADI" }
c.showStep(name, operand, oplen)
if writeFlag {
oplen = 4
err = c.mem.write(c.X, Y)
if err != nil { return err }
nX = Y + word(operand)
} else {
nX, err = c.mem.read(c.X)
if err != nil { return err }
}
// FPADJ
case 3:
operand := c.getSignedOperand(insWord, 10)
oplen = 10
nFP = c.FP + word(operand)
deltaESP = 0
x2y = false
c.showStep("FPADJ", operand, oplen)
// LOADREL
case 5:
offset := c.getOperand(insWord, 10)
c.showStep("LOADREL", offset, 10)
nX, err = c.mem.read(c.PC + word(offset))
if err != nil { return err }
x2y = true
deltaESP = 1
default:
return fmt.Errorf("Invalid EXT instruction %v at %08X", extop, c.PC)
}
default:
return fmt.Errorf("Invalid instruction %04X at %08X", insWord, c.PC)
}
nESP = c.addToWord(nESP, deltaESP)
if nESP < 0 || nESP >= estackDepth {
return fmt.Errorf("estack overflow %v at %08X", nESP, c.PC)
}
if x2y {
c.estack[nESP] = c.X
}
c.PC = nPC
c.FP = nFP
c.BP = nBP
c.X = nX
c.RP = nRP
c.ESP = nESP
return nil
}

132
tridoraemu/framebuffer.go Normal file
View file

@ -0,0 +1,132 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
// "fmt"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
)
const VmemWords = 32768
const PaletteSlots = 16
const FB_RA = 0
const FB_WA = 1
const FB_IO = 2
const FB_PS = 3
const FB_PD = 4
const FB_CTL= 5
const PixelMask = 0b11110000000000000000000000000000
const PixelPerWord = 8
const VmemWidth = 32
const BitsPerPixel = 4
const ScreenWidth = 640
const ScreenHeight = 400
const WordsPerLine = ScreenWidth / PixelPerWord
type Framebuffer struct {
framebuffer *ebiten.Image
palette [PaletteSlots] color.Color
readAddr word
writeAddr word
paletteSlot word
vmem [VmemWords]word
readCount int
}
func (f *Framebuffer) initialize() {
f.framebuffer = ebiten.NewImage(ScreenWidth, ScreenHeight)
for i := 0; i <PaletteSlots; i++ {
f.palette[i] = color.RGBA{0,0,0,0}
}
}
func (f *Framebuffer) read(byteaddr word) (word, error) {
result := word(0)
addr := byteaddr & 0x7F
switch addr {
case FB_RA: result = f.readAddr
case FB_WA: result = f.writeAddr
case FB_IO: result = f.readVmem()
case FB_PS: result = f.paletteSlot
case FB_PD: result = f.readPalette()
case FB_CTL: result = f.readCtl()
default:
}
return result, nil
}
func (f *Framebuffer) write(value word, byteaddr word) (error) {
addr := byteaddr & 0x7F
switch addr {
case FB_RA: f.readAddr = value
case FB_WA: f.writeAddr = value
case FB_IO: f.writeVmem(value)
case FB_PS: f.paletteSlot = value
case FB_PD: f.writePalette(value)
case FB_CTL: f.writeCtl(value)
default:
}
idle(false)
return nil
}
func (f *Framebuffer) readVmem() word {
result := f.vmem[f.readAddr & (VmemWords - 1)]
f.readAddr += 1
return result
}
func (f *Framebuffer) writeVmem(value word) {
vaddr := f.writeAddr & (VmemWords - 1)
f.vmem[vaddr] = value
y := vaddr / WordsPerLine
x := vaddr % WordsPerLine * PixelPerWord
for i := 0; i < PixelPerWord; i++ {
pixel := (value & PixelMask) >> (VmemWidth - BitsPerPixel)
value = value << BitsPerPixel
col := f.palette[pixel]
//fmt.Printf("set pixel %v, %v\n", x,y)
f.framebuffer.Set(int(x), int(y), col)
x = x + 1
}
f.writeAddr += 1
}
func (f *Framebuffer) readPalette() word {
return word(0)
}
func (f *Framebuffer) writePalette(value word) {
// 4 bits per color channel
r := uint8((value & 0b111100000000) >> 8)
g := uint8((value & 0b000011110000) >> 4)
b := uint8((value & 0b000000001111) >> 0)
// scale to 0-255
r = r << 4
g = g << 4
b = b << 4
f.palette[f.paletteSlot] = color.RGBA{r,g,b,0}
}
func (f *Framebuffer) readCtl() word {
if f.readCount == 0 {
f.readCount = 1000
return word(0)
} else {
f.readCount -= 1
return word(1)
}
}
func (f *Framebuffer) writeCtl(value word) {
}

30
tridoraemu/go.mod Normal file
View file

@ -0,0 +1,30 @@
module tridoraemu
go 1.22.5
require (
atomicgo.dev/keyboard v0.2.9
github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203
github.com/gopxl/pixel v1.0.0
github.com/hajimehoshi/ebiten/v2 v2.7.8
github.com/nsf/termbox-go v1.1.1
golang.org/x/image v0.18.0
golang.org/x/sys v0.22.0
golang.org/x/term v0.22.0
)
require (
github.com/containerd/console v1.0.3 // indirect
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect
github.com/faiface/glhf v0.0.0-20211013000516-57b20770c369 // indirect
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 // indirect
github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
github.com/go-gl/mathgl v1.1.0 // indirect
github.com/jezek/xgb v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/sync v0.7.0 // indirect
)

29
tridoraemu/irqc.go Normal file
View file

@ -0,0 +1,29 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"time"
// "fmt"
)
const MSecsPerTick = 50
type IRQC struct {
start time.Time
}
func (i *IRQC) initialize() {
i.start = time.Now()
}
func (i *IRQC) read(byteaddr word) (word, error) {
elapsedms := time.Since(i.start).Milliseconds()
elapsedTicks := elapsedms / MSecsPerTick
result := word((elapsedTicks & 0x0FFFFFFF) << 8)
//fmt.Printf("** IRQC read: %08X (%v)\n", result, elapsedms)
return result, nil
}
func (i *IRQC) write(value word, byteaddr word) (error) {
return nil
}

117
tridoraemu/mem.go Normal file
View file

@ -0,0 +1,117 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"fmt"
"log"
"os"
"io"
"bufio"
"encoding/binary"
)
const IOStartAddr = 2048
const RAMStartAddr = 4096
const IOSlotSize = 128
const IOSlotCount = 16
type Mem struct {
ram [] word
iohandler [IOSlotCount] IOHandler
}
func (m *Mem) wordAddr(byteaddr word) (int, error) {
wordaddr := int(byteaddr / 4)
if wordaddr >= len(m.ram) {
return 0, fmt.Errorf("Invalid address %08X", byteaddr)
}
return wordaddr, nil
}
func (m *Mem) initialize(sizewords int) {
m.ram = make([] word, sizewords)
for i := 0; i < len(m.ram); i++ {
m.ram[i] = 0
}
}
func (m *Mem) loadFromFile(path string, startAddr int) {
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
buf := make([]byte,4)
reader := bufio.NewReader(f)
count := 0
wordaddr := startAddr / 4
for {
n, e := reader.Read(buf)
if e != nil && e != io.EOF {
panic(e)
}
if n < 4 {
if n == 2 {
m.ram[wordaddr] = word(binary.BigEndian.Uint32(buf) & 0xFFFF0000)
count += 2
}
fmt.Printf("%v bytes read at %08X from %v\n", count, startAddr, path)
break
} else {
m.ram[wordaddr] = word(binary.BigEndian.Uint32(buf))
// fmt.Printf("%08X %08X\n", addr, m.ram[addr])
count += 4
wordaddr += 1
}
}
}
func (m *Mem) attachIO(h IOHandler, slot int) {
if m.iohandler[slot] != nil {
log.Panicf("I/O handler %d already attached", slot)
}
m.iohandler[slot] = h
}
func (m *Mem) read(byteaddr word) (word, error) {
if byteaddr >= IOStartAddr && byteaddr < RAMStartAddr {
ioslot := (byteaddr - IOStartAddr) / IOSlotSize
if m.iohandler[ioslot] != nil {
return m.iohandler[ioslot].read(byteaddr)
}
return 42, nil
}
wordaddr, err := m.wordAddr(byteaddr)
if err == nil {
return m.ram[wordaddr], err
} else {
return 0, err
}
}
func (m *Mem) write(value word, byteaddr word) error {
if byteaddr < IOStartAddr {
return fmt.Errorf("Write to ROM area at %08X value %08X", byteaddr, value)
}
if byteaddr >= IOStartAddr && byteaddr < RAMStartAddr {
ioslot := (byteaddr - IOStartAddr) / IOSlotSize
if m.iohandler[ioslot] != nil {
return m.iohandler[ioslot].write(value, byteaddr)
}
return nil
}
wordaddr, err := m.wordAddr(byteaddr)
if err == nil {
m.ram[wordaddr] = value
}
return err
}

272
tridoraemu/sdspi.go Normal file
View file

@ -0,0 +1,272 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"os"
"io"
"encoding/binary"
"fmt"
)
type SDState uint
const (
IDLE SDState = iota
WRCMD
WRDATA
RDDATA
)
const (
CTRL_WRITE = 0b100000000000000
RX_FILTER_EN = 0b010000000000000
TXRX_EN = 0b001000000000000
CLK_F_EN = 0b000100000000000
CLK_DIV_WR = 0b000010000000000
RX_RD = 0b000001000000000
TX_WR = 0b000000100000000
C_D = 0b100000000000000
C_CHG = 0b010000000000000
C_BUSY = 0b001000000000000
TX_RDY = 0b000100000000000
TX_EMPTY = 0b000010000000000
RX_AVAIL = 0b000001000000000
RX_OVR = 0b000000100000000
)
type SDSPI struct {
state SDState
ksectors uint
lastSector uint
imgfile *os.File
cmd uint
cmdcount uint
arg uint
receiving bool
blockaddr uint
readbuf []byte
readpos int
writebuf []byte
writepos int
debug bool
dbgwaiten bool
}
func (s *SDSPI) openImage(filename string) error {
var err error
s.imgfile, err = os.OpenFile(filename, os.O_RDWR, 0644)
if err != nil { return err }
buf := make([]byte,4)
_, err = s.imgfile.ReadAt(buf, 48)
if err != nil { return err }
blocks := binary.BigEndian.Uint32(buf)
s.ksectors = uint(blocks / 1024)
s.lastSector = uint(blocks - 1)
fmt.Printf("opened SD card image %v, PHYS blocks: %v\n", filename, blocks)
return nil
}
func (s *SDSPI) closeImage() {
s.imgfile.Close()
}
func (s *SDSPI) read(byteaddr word) (word, error) {
result := word(0)
// always detect a card, transmitter always ready
result = C_D | TX_RDY
if s.debug {
fmt.Printf("** SDSPI read readbuf len: %v receiving: %v readpos: %v\n",
len(s.readbuf), s.receiving, s.readpos)
}
if s.receiving && len(s.readbuf) > 0 {
if s.debug { fmt.Printf(" byte: %02X\n", s.readbuf[s.readpos]) }
result |= RX_AVAIL // there is data to be read
result |= word(s.readbuf[s.readpos])
// the read position is advanced only by writing RX_RD to the
// SDSPI register
} else {
result |= 0xFF
}
// always signal TX_EMPTY since we immediately process
// all written data
result |= TX_EMPTY
return result, nil
}
func (s *SDSPI) sendIdleResponse() {
s.readbuf = []byte{0x01}
}
func (s *SDSPI) sendOkResponse() {
s.readbuf = []byte{0x00}
}
func (s *SDSPI) sendDataResponse() {
s.readbuf = []byte{0b00101}
}
func (s *SDSPI) sendBusy() {
s.readbuf = append(s.readbuf, 0xFF, 0x00, 0xFF)
}
func (s *SDSPI) sendDataPkt(dataBytes []byte) {
s.readbuf = append(s.readbuf, 0xFE) // data token
s.readbuf = append(s.readbuf, dataBytes...) // data block
s.readbuf = append(s.readbuf, 0, 0) // crc/unused in SPI mode
}
func (s *SDSPI) sendCSD() {
size := s.ksectors - 1
sizehi := byte((size & 0b1111110000000000000000) >> 16)
sizemid := byte((size & 0b0000001111111100000000) >> 8)
sizelow := byte((size & 0b0000000000000011111111))
s.sendDataPkt( []byte{0b01000000,
0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6,
sizehi, sizemid, sizelow,
0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF})
}
func (s *SDSPI) readSendBlock() {
buf := make([]byte, 512)
if s.arg <= s.lastSector {
s.imgfile.Seek(int64(s.arg) * 512, 0)
_, err := s.imgfile.Read(buf)
if err != nil && err != io.EOF { panic(err) }
}
s.sendDataPkt(buf)
}
func (s *SDSPI) writeBlock() {
if s.arg <= s.lastSector {
s.imgfile.Seek(int64(s.arg) * 512, 0)
_, err := s.imgfile.Write(s.writebuf)
if err != nil { panic(err) }
}
s.writebuf = make([]byte, 0)
s.writepos = 0
}
func (s *SDSPI) write(value word, byteaddr word) (error) {
if (value & CTRL_WRITE) != 0 {
s.receiving = (value & TXRX_EN) != 0
}
if s.debug { fmt.Printf("** SDSPI write %032b\n", value) }
if (value & CLK_DIV_WR) != 0 {
if s.debug {
fmt.Printf("** SDSPI clock divider set to %v\n", value & 0xFF)
}
}
if (value & RX_RD) != 0 {
// advance read position when RX_RD i set
s.readpos += 1
if s.readpos >= len(s.readbuf) {
s.readbuf = make([]byte, 0)
s.readpos = 0
// if in WRDATA state, do not go IDLE when all data has been read.
// In that case, we just read the R1 response for the write command
// and after that the data packet will be written.
if s.state != WRDATA { s.state = IDLE }
}
}
if (value & TX_WR) != 0 {
// we ignore the TXRX_EN flag for the transmitter and
// always process data written with TX_WR
value8 := value & 0xFF
switch s.state {
case IDLE:
if value8 != 0xFF {
s.state = WRCMD
s.cmd = uint(value & 0x3F)
s.arg = 0
s.cmdcount = 5
if s.debug {
fmt.Printf(" cmd: %02d\n", s.cmd)
}
}
case WRCMD:
if s.cmdcount > 0 { // any more argument bytes to be received?
s.cmdcount -= 1
if s.cmdcount == 0 {
s.state = RDDATA
switch s.cmd {
case 0: s.sendIdleResponse() // GO_IDLE_STATE
case 8: s.readbuf = []byte{0x01, 0xA1, 0xA2, 0xA3, 0xA4} // SEND_IF_COND
case 9: s.sendOkResponse() // SEND_CSD
s.sendCSD()
case 16: s.sendOkResponse() // SET_BLOCKLEN, ignored
case 17: s.sendOkResponse() // READ_SINGLE_BLOCK
s.readSendBlock()
case 24: s.sendOkResponse() // WRITE_SINGLE_BLOCK
s.sendOkResponse()
s.state = WRDATA
case 58: s.readbuf = []byte{0x01, 0xB1, 0xB2, 0xB3, 0xB4} // READ_OCR
case 55: s.sendIdleResponse() // APP_CMD, we just ignore it and treat CMD41 as ACMD41
case 41: s.sendOkResponse() // APP_SEND_OP_COND
default:
if s.debug {
fmt.Printf("** SDSPI invalid CMD %v\n", s.cmd)
}
}
} else {
// process an argument byte
s.arg = uint((s.arg << 8)) | uint(value8)
}
} else {
if s.debug {
fmt.Printf("** SDSPI extra bytes in command %v\n", value8)
}
}
case WRDATA:
if len(s.writebuf) == 0 {
// wait for data token
if value8 == 0xFE { // data token found
s.writebuf = make([]byte, 512)
s.writepos = 0
}
} else { // collecting data bytes to write
if s.writepos < 512 {
s.writebuf[s.writepos] = byte(value8)
}
s.writepos += 1
// after getting and ignoring two crc bytes, write block
// and return to idle state
if s.writepos >= 514 {
s.state = IDLE
s.writeBlock()
s.sendDataResponse()
s.sendBusy()
}
}
default:
if value8 != 0xFF {
if s.debug {
fmt.Printf("** SDSPI invalid state %v on TX_WR byte %v\n", s.state, value8)
}
}
}
}
return nil
}

159
tridoraemu/tridoraemu.go Normal file
View file

@ -0,0 +1,159 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
"fmt"
"log"
"errors"
"flag"
"time"
"github.com/hajimehoshi/ebiten/v2"
// "github.com/hajimehoshi/ebiten/v2/ebitenutil"
// "image/color"
)
const IdleTicks = 1000
var consoleChan chan byte
var cpu CPU
var mem Mem
var uart UART
var framebuffer Framebuffer
var sdspi SDSPI
var irqc IRQC
var Terminated = errors.New("terminated")
var idleCounter int = IdleTicks
func idle(canGoIdle bool) {
if canGoIdle {
if idleCounter > 0 { idleCounter -= 1 }
} else {
if idleCounter != IdleTicks { idleCounter = IdleTicks }
}
}
type Game struct{
x,y int
stepsPerFrame int
lastFrameDuration time.Duration
}
func (g *Game) Update() error {
startTime := time.Now()
for i := 0; i < g.stepsPerFrame; i++ {
err := cpu.step()
if err != nil {
log.Printf("Stopped by error at PC %08X",cpu.PC)
log.Print(err)
return Terminated
}
if cpu.stopped { return Terminated }
if idleCounter == 0 { break }
}
g.lastFrameDuration = time.Since(startTime)
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
screen.DrawImage(framebuffer.framebuffer, nil)
/*
buf := fmt.Sprintf("PC: %08X FP: %08X RP: %08X ESP: %2X\n%v", cpu.PC, cpu.FP, cpu.RP, cpu.ESP, g.lastFrameDuration)
ebitenutil.DebugPrint(screen, buf)
screen.Set(g.x, g.y, color.RGBA{255,0,0,0})
screen.Set(g.x, g.y+1, color.RGBA{0,255,0,0})
screen.Set(g.x, g.y+2, color.RGBA{0,255,255,0})
screen.Set(g.x, g.y+3, color.RGBA{255,255,255,0})
g.x += 1
if g.x > 319 { g.x = 0 }
*/
// if idleCounter == 0 { ebitenutil.DebugPrint(screen, "idle") }
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return 640, 400
}
func main() {
var codefile string = ""
addrPtr := flag.Int("a",0,"starting address")
tracePtr := flag.Bool("t",false,"trace")
cardImgPtr := flag.String("i", "sdcard.img", "SD card image file")
flag.Parse()
if len(flag.Args()) > 0 {
codefile = flag.Args()[0]
} else {
codefile = "rommon.prog"
}
log.SetFlags(0)
oldState, err := SetRawConsole()
if err != nil {
panic(err)
}
defer RestoreConsole(oldState)
cpu.initialize()
mem.initialize(4096 * 1024 / 4)
uart.cpu = &cpu
mem.attachIO(&uart, 0)
err = sdspi.openImage(*cardImgPtr)
if err != nil {
panic(err)
}
defer sdspi.closeImage()
//sdspi.debug = true
mem.attachIO(&sdspi, 1)
framebuffer.initialize()
mem.attachIO(&framebuffer, 2)
irqc.initialize()
mem.attachIO(&irqc, 3)
cpu.mem = &mem
cpu.PC = word(*addrPtr)
if codefile != "" {
mem.loadFromFile(codefile, *addrPtr)
}
consoleChan = make(chan byte)
uart.consoleChan = make(chan byte)
ebiten.SetWindowSize(800, 600)
ebiten.SetWindowTitle("Tridora Framebuffer")
g := Game{}
cpu.trace = *tracePtr
g.stepsPerFrame = 166666
// g.stepsPerFrame = 1
go func(ch chan byte) {
for {
buf := make([] byte,1);
n, err := ConsoleRead(buf)
if err != nil {
fmt.Println("read error on stdin, closing channel")
close(ch)
return
}
if n > 0 {ch <- buf[0] }
}
}(uart.consoleChan)
if err := ebiten.RunGame(&g); err != Terminated && err != nil {
log.Panic(err)
}
}

59
tridoraemu/uart.go Normal file
View file

@ -0,0 +1,59 @@
// Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details
package main
import (
// "fmt"
"os"
"errors"
)
type UART struct {
available bool
buf [] byte
consoleChan chan byte
cpu *CPU
}
func (u *UART) read(byteaddr word) (word, error) {
var result word = 0
if len(u.buf) > 0 {
result = word(u.buf[0])
result |= 512
} else {
select {
case inbyte, ok := <-u.consoleChan:
if ! ok {
return 0, errors.New("console channel error")
} else {
u.buf = make([]byte, 1)
u.buf[0] = inbyte
// fmt.Println("Read input:", inbyte)
idle(false)
}
default:
idle(true)
}
}
return result, nil
}
func (u *UART) write(value word, byteaddr word) (error) {
var err error = nil
idle(false)
if value & 512 != 0 {
u.buf = u.buf[1:]
// fmt.Println("rx_clear: len ", len(u.buf))
}
if value & 1024 != 0 {
buf := make([] byte, 1)
buf[0] = byte(value & 255)
_ , err = os.Stdout.Write(buf)
}
return err
}