initial commit
This commit is contained in:
commit
60db522e87
107 changed files with 36924 additions and 0 deletions
7
tridoraemu/IOHandler.go
Normal file
7
tridoraemu/IOHandler.go
Normal 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
17
tridoraemu/LICENSE.md
Normal 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
53
tridoraemu/README.md
Normal 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
35
tridoraemu/console.go
Normal 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
|
||||
}
|
||||
75
tridoraemu/console_windows.go
Normal file
75
tridoraemu/console_windows.go
Normal 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
467
tridoraemu/cpu.go
Normal 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
132
tridoraemu/framebuffer.go
Normal 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
30
tridoraemu/go.mod
Normal 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
29
tridoraemu/irqc.go
Normal 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
117
tridoraemu/mem.go
Normal 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
272
tridoraemu/sdspi.go
Normal 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
159
tridoraemu/tridoraemu.go
Normal 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
59
tridoraemu/uart.go
Normal 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
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue