ph00524: add emulator
This commit is contained in:
parent
7c663f42ca
commit
be255f2e6f
29
modules/ph00524/emu/README.md
Normal file
29
modules/ph00524/emu/README.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
PH00524, but emulated
|
||||
===
|
||||
|
||||
When troubleshooting the PH00524 evacuation controller board it's useful to be
|
||||
able to tell what the microcontroller _should_ be doing, rather than just
|
||||
staring at disassembly/decompilation all day long.
|
||||
|
||||
This little program executes our scope evacuation controller's code (from the
|
||||
U12 EEPROM) in an environment consisting of a Z80 CPU and all peripherals
|
||||
required to reach a pumpdown state.
|
||||
|
||||
It comes with a little text UI interface:
|
||||
|
||||
Controls: [v] vent, [p] pumpdown, [q] quit
|
||||
PC 02bc
|
||||
---- -------- VENT
|
||||
M1 M2
|
||||
|... ..
|
||||
|
||||
This shows the controls (keyboard buttons v, p and q), the last program counter
|
||||
at which an I/O operation was performed, indicator values, motor movement and
|
||||
lightbarrier status.
|
||||
|
||||
Running
|
||||
---
|
||||
|
||||
Change to this directory and run:
|
||||
|
||||
$ go run ./
|
98
modules/ph00524/emu/arbiters.go
Normal file
98
modules/ph00524/emu/arbiters.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/koron-go/z80"
|
||||
)
|
||||
|
||||
// ioPeripheral is a z80.IO device at a given address.
|
||||
type ioPeripheral struct {
|
||||
start uint8
|
||||
length uint8
|
||||
peripheral z80.IO
|
||||
}
|
||||
|
||||
// ioArbiter is a collection of I/O devices at addresses. It implements z80.IO
|
||||
// itself, dispatching to the appropriate device as needed.
|
||||
type ioArbiter struct {
|
||||
peripherals []*ioPeripheral
|
||||
}
|
||||
|
||||
func (i *ioArbiter) get(addr uint8) *ioPeripheral {
|
||||
for _, periph := range i.peripherals {
|
||||
if addr < periph.start {
|
||||
continue
|
||||
}
|
||||
if addr > periph.start+periph.length {
|
||||
continue
|
||||
}
|
||||
return periph
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ioArbiter) In(addr uint8) uint8 {
|
||||
p := i.get(addr)
|
||||
if p == nil {
|
||||
log.Fatalf("Unhandled I/O In at %02x", addr)
|
||||
return 0
|
||||
}
|
||||
return p.peripheral.In(addr - p.start)
|
||||
}
|
||||
func (i *ioArbiter) Out(addr uint8, value uint8) {
|
||||
p := i.get(addr)
|
||||
if p == nil {
|
||||
log.Fatalf("Unhandled I/O Out at %02x", addr)
|
||||
return
|
||||
}
|
||||
p.peripheral.Out(addr-p.start, value)
|
||||
}
|
||||
|
||||
// memory is a continuous memory region at a given address, optionally marked
|
||||
// read only.
|
||||
type memory struct {
|
||||
start uint16
|
||||
data []uint8
|
||||
readonly bool
|
||||
}
|
||||
|
||||
// memoryArbiter implements Z80.Memory and dispatches accesses to subordinate
|
||||
// memory instances.
|
||||
type memoryArbiter struct {
|
||||
memories []memory
|
||||
}
|
||||
|
||||
func (m *memoryArbiter) get(addr uint16) *memory {
|
||||
for _, mem := range m.memories {
|
||||
if addr < mem.start {
|
||||
continue
|
||||
}
|
||||
if addr >= mem.start+uint16(len(mem.data)) {
|
||||
continue
|
||||
}
|
||||
return &mem
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryArbiter) Get(addr uint16) uint8 {
|
||||
mem := m.get(addr)
|
||||
if mem == nil {
|
||||
log.Fatalf("Unhandled memory Get at %04x", addr)
|
||||
return 0
|
||||
}
|
||||
return mem.data[addr-mem.start]
|
||||
}
|
||||
|
||||
func (m *memoryArbiter) Set(addr uint16, value uint8) {
|
||||
mem := m.get(addr)
|
||||
if mem == nil {
|
||||
log.Fatalf("Unhandled memory Set at %04x", addr)
|
||||
return
|
||||
}
|
||||
if mem.readonly {
|
||||
log.Fatalf("Read-only memory Set at %04x", addr)
|
||||
}
|
||||
mem.data[addr-mem.start] = value
|
||||
}
|
157
modules/ph00524/emu/controller.go
Normal file
157
modules/ph00524/emu/controller.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/koron-go/z80"
|
||||
)
|
||||
|
||||
// evacuationController is the PH00524 board emulated in a synchronous step
|
||||
// manner.
|
||||
type evacuationController struct {
|
||||
cpu *z80.CPU
|
||||
|
||||
m1 *motor
|
||||
m2 *motor
|
||||
|
||||
u8 *i8255
|
||||
u9 *i8255
|
||||
|
||||
// lastIOPC is the last PC on which an I/O operation (through U8 or U9) was
|
||||
// performed. This is used for status/debugging.
|
||||
lastIOPC uint16
|
||||
}
|
||||
|
||||
func newEvacuationController(romPath string) *evacuationController {
|
||||
rom, err := os.ReadFile(romPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read ROM: %v", err)
|
||||
}
|
||||
|
||||
mem := memoryArbiter{
|
||||
memories: []memory{
|
||||
{start: 0x0000, data: rom, readonly: true},
|
||||
{start: 0x8000, data: make([]byte, 512)},
|
||||
},
|
||||
}
|
||||
|
||||
u8 := i8255{}
|
||||
u9 := i8255{}
|
||||
io := ioArbiter{
|
||||
peripherals: []*ioPeripheral{
|
||||
{start: 0x40, length: 4, peripheral: &u8},
|
||||
{start: 0x80, length: 4, peripheral: &u9},
|
||||
},
|
||||
}
|
||||
|
||||
m1 := motor{barrier: &barrier{npos: 4}}
|
||||
m2 := motor{barrier: &barrier{npos: 2}}
|
||||
|
||||
cpu := z80.CPU{
|
||||
Memory: &mem,
|
||||
IO: &io,
|
||||
}
|
||||
|
||||
res := &evacuationController{
|
||||
cpu: &cpu,
|
||||
m1: &m1,
|
||||
m2: &m2,
|
||||
u8: &u8,
|
||||
u9: &u9,
|
||||
}
|
||||
|
||||
// Make lastIOPC get updated on u8/u9 I/O.
|
||||
u8.onIn = res.onIO
|
||||
u8.onOut = res.onIO
|
||||
u9.onIn = res.onIO
|
||||
u9.onOut = res.onIO
|
||||
|
||||
// Expansion bus connector, seems to be always pulled up.
|
||||
u9.pa = 0xff
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (e *evacuationController) onIO(_ i8255Port) {
|
||||
e.lastIOPC = e.cpu.PC
|
||||
}
|
||||
|
||||
func (e *evacuationController) getIndcators() indicators {
|
||||
return indicators{
|
||||
indEvac: (e.u8.pc & 1) != 0,
|
||||
indHTReadyN: (e.u8.pc & 2) != 0,
|
||||
indPumpDown: (e.u8.pc & 4) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *evacuationController) pressPumpdown(value bool) {
|
||||
if value {
|
||||
e.u8.pb |= 1
|
||||
} else {
|
||||
e.u8.pb &= (0xff ^ 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *evacuationController) pressVent(value bool) {
|
||||
if value {
|
||||
e.u8.pb |= 2
|
||||
} else {
|
||||
e.u8.pb &= (0xff ^ 2)
|
||||
}
|
||||
}
|
||||
|
||||
// step through the simulation/emulation given a delta time.
|
||||
func (e *evacuationController) step(dt time.Duration) bool {
|
||||
e.cpu.Step()
|
||||
if e.cpu.HALT {
|
||||
return false
|
||||
}
|
||||
|
||||
// Simulate motors and barriers.
|
||||
e.m1.dir = (e.u9.pc & 16) != 0
|
||||
e.m1.en = (e.u9.pc & 4) != 0
|
||||
e.m2.dir = (e.u9.pc & 8) != 0
|
||||
e.m2.en = (e.u9.pc & 2) != 0
|
||||
e.m1.sim(dt)
|
||||
e.m2.sim(dt)
|
||||
|
||||
// Feed barrier status back into CPU.
|
||||
b1p := e.m1.barrier.pins()
|
||||
b2p := e.m2.barrier.pins()
|
||||
e.u8.pa = 0
|
||||
if b1p[0] {
|
||||
e.u8.pa |= (1 << 5)
|
||||
}
|
||||
if b1p[1] {
|
||||
e.u8.pa |= (1 << 4)
|
||||
}
|
||||
if b1p[2] {
|
||||
e.u8.pa |= (1 << 3)
|
||||
}
|
||||
if b1p[3] {
|
||||
e.u8.pa |= (1 << 2)
|
||||
}
|
||||
if b2p[0] {
|
||||
e.u8.pa |= (1 << 1)
|
||||
}
|
||||
if b2p[1] {
|
||||
e.u8.pa |= (1 << 0)
|
||||
}
|
||||
|
||||
// Simulate vacuum gauge / comparators given light barrier status.
|
||||
if b1p[3] {
|
||||
// half succ (PRE_EVAC)
|
||||
e.u8.pb |= 1 << 5
|
||||
// full succ (???)
|
||||
e.u8.pb |= 1 << 6
|
||||
}
|
||||
if !b1p[0] {
|
||||
// no more succ
|
||||
e.u8.pb &= 0xff ^ (1 << 5)
|
||||
e.u8.pb &= 0xff ^ (1 << 6)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
10
modules/ph00524/emu/go.mod
Normal file
10
modules/ph00524/emu/go.mod
Normal file
|
@ -0,0 +1,10 @@
|
|||
module git.fa-fo.de/fafo/jeol-t330a/modules/ph00524/emu
|
||||
|
||||
go 1.22.3
|
||||
|
||||
require (
|
||||
github.com/koron-go/z80 v0.10.1
|
||||
golang.org/x/term v0.23.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.23.0 // indirect
|
8
modules/ph00524/emu/go.sum
Normal file
8
modules/ph00524/emu/go.sum
Normal file
|
@ -0,0 +1,8 @@
|
|||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/koron-go/z80 v0.10.1 h1:Jfb0esP/QFL4cvcr+eFECVG0Y/mA9JBLC4EKbMU5zAY=
|
||||
github.com/koron-go/z80 v0.10.1/go.mod h1:ry+Zl9kRKelzaDG9UzEtUpUnXy0Yv/kk1YEaX958xdk=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
102
modules/ph00524/emu/i8255.go
Normal file
102
modules/ph00524/emu/i8255.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import "log"
|
||||
|
||||
// i8255 is a generic Intel 8255-compatible I/O controller chip.
|
||||
//
|
||||
// Only mode 0 is supported.
|
||||
type i8255 struct {
|
||||
pa, pb, pc uint8
|
||||
|
||||
onOut func(port i8255Port)
|
||||
onIn func(port i8255Port)
|
||||
}
|
||||
|
||||
type i8255Port string
|
||||
|
||||
const (
|
||||
PA i8255Port = "PA"
|
||||
PB i8255Port = "PB"
|
||||
PC i8255Port = "PC"
|
||||
)
|
||||
|
||||
func (i *i8255) getPort(p i8255Port) uint8 {
|
||||
switch p {
|
||||
case PA:
|
||||
return i.pa
|
||||
case PB:
|
||||
return i.pb
|
||||
case PC:
|
||||
return i.pc
|
||||
default:
|
||||
log.Fatalf("invalid port")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (i *i8255) Out(addr, value uint8) {
|
||||
switch addr {
|
||||
case 0:
|
||||
i.pa = value
|
||||
if i.onOut != nil {
|
||||
i.onOut(PA)
|
||||
}
|
||||
case 1:
|
||||
i.pb = value
|
||||
if i.onOut != nil {
|
||||
i.onOut(PB)
|
||||
}
|
||||
case 2:
|
||||
i.pc = value
|
||||
if i.onOut != nil {
|
||||
i.onOut(PC)
|
||||
}
|
||||
case 3:
|
||||
if (value >> 7) == 0 {
|
||||
// BSR mode
|
||||
bitno := (value >> 1) & 0b111
|
||||
bitval := value & 1
|
||||
if bitval == 1 {
|
||||
i.pc |= (1 << bitno)
|
||||
} else {
|
||||
i.pc &= 0xff ^ (1 << bitno)
|
||||
}
|
||||
} else {
|
||||
// Input/Output mode
|
||||
gaMode := (value >> 5) & 0b11
|
||||
gbMode := (value >> 5) & 0b11
|
||||
if gaMode != 0 {
|
||||
log.Fatalf("I8255 implementation only supports mode 0 on Port A")
|
||||
}
|
||||
if gbMode != 0 {
|
||||
log.Fatalf("I8255 implementation only supports mode 0 on Port B")
|
||||
}
|
||||
// Don't care about the rest (input/output settings per group/port).
|
||||
}
|
||||
default:
|
||||
log.Printf("I8255: invalid out")
|
||||
}
|
||||
}
|
||||
|
||||
func (i *i8255) In(addr uint8) uint8 {
|
||||
switch addr {
|
||||
case 0:
|
||||
if i.onIn != nil {
|
||||
i.onIn(PA)
|
||||
}
|
||||
return i.pa
|
||||
case 1:
|
||||
if i.onIn != nil {
|
||||
i.onIn(PB)
|
||||
}
|
||||
return i.pb
|
||||
case 2:
|
||||
if i.onIn != nil {
|
||||
i.onIn(PC)
|
||||
}
|
||||
return i.pc
|
||||
default:
|
||||
log.Printf("I8255: invalid in")
|
||||
return 0
|
||||
}
|
||||
}
|
83
modules/ph00524/emu/main.go
Normal file
83
modules/ph00524/emu/main.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctrl := newEvacuationController("../eeprom/Vac-ours FK-U12.bin")
|
||||
|
||||
// Switch stdin into 'raw' mode.
|
||||
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer term.Restore(int(os.Stdin.Fd()), oldState)
|
||||
|
||||
// Keyboard input channel, byte at a time.
|
||||
kbdC := make(chan byte)
|
||||
go func() {
|
||||
for {
|
||||
b := make([]byte, 1)
|
||||
_, err = os.Stdin.Read(b)
|
||||
if err == nil {
|
||||
kbdC <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
t := time.Now()
|
||||
|
||||
// Countdown timers for simulating a button press.
|
||||
pumpdownButton := time.Duration(0)
|
||||
ventButton := time.Duration(0)
|
||||
|
||||
lastPrint := time.Now()
|
||||
|
||||
fmt.Print("\033[H\033[2J")
|
||||
for {
|
||||
dt := time.Since(t)
|
||||
t = time.Now()
|
||||
if !ctrl.step(dt) {
|
||||
break
|
||||
}
|
||||
|
||||
select {
|
||||
case b := <-kbdC:
|
||||
switch b {
|
||||
case 'p':
|
||||
pumpdownButton = time.Millisecond * 1000
|
||||
case 'v':
|
||||
ventButton = time.Millisecond * 1000
|
||||
case 'q':
|
||||
return
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
ctrl.pressPumpdown(pumpdownButton > 0)
|
||||
if pumpdownButton > 0 {
|
||||
pumpdownButton -= dt
|
||||
}
|
||||
ctrl.pressVent(ventButton > 0)
|
||||
if ventButton > 0 {
|
||||
ventButton -= dt
|
||||
}
|
||||
|
||||
if time.Since(lastPrint) > time.Millisecond*10 {
|
||||
lastPrint = time.Now()
|
||||
fmt.Print("\033[1;1H")
|
||||
fmt.Printf("Controls: [v] vent, [p] pumpdown, [q] quit\r\n")
|
||||
fmt.Printf("PC %04x\r\n", ctrl.lastIOPC)
|
||||
fmt.Printf("%s\r\n", ctrl.getIndcators().String())
|
||||
fmt.Printf("%s %s\r\n", ctrl.m1.status("M1"), ctrl.m2.status("M2"))
|
||||
fmt.Printf("%s %s\r\n", ctrl.m1.barrier.status(), ctrl.m2.barrier.status())
|
||||
fmt.Print("\r\n")
|
||||
}
|
||||
}
|
||||
}
|
118
modules/ph00524/emu/scope.go
Normal file
118
modules/ph00524/emu/scope.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// barrier represents a multi-position light barrier attached to a motor.
|
||||
type barrier struct {
|
||||
// npos is the number of the positions of the light barrier.
|
||||
npos uint
|
||||
// cur is the current position of the motor.
|
||||
cur float64
|
||||
}
|
||||
|
||||
// pins returns an npos-length array representing the interruption state of the
|
||||
// barrier given cur. An interrupted light barrier is represented by a true
|
||||
// value, an uninterrupted one with a false value.
|
||||
//
|
||||
// Every light barrier position is half a unit away from eachother, with the
|
||||
// first light barrier at unit 1.
|
||||
func (b *barrier) pins() []bool {
|
||||
res := make([]bool, b.npos)
|
||||
for i := uint(0); i < b.npos; i++ {
|
||||
pos := float64(i) * float64(1.0)
|
||||
pos += 1.0
|
||||
if b.cur*2.0 >= pos {
|
||||
res[i] = true
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// status returns a human-readable status of the barrier like '||..' for a
|
||||
// 4-position light barrier with the first two barrier interrupted.
|
||||
func (b *barrier) status() string {
|
||||
res := ""
|
||||
for _, v := range b.pins() {
|
||||
if v {
|
||||
res += "|"
|
||||
} else {
|
||||
res += "."
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// motor represents an electrical motor which drives a linear actuator at 1 unit per second.
|
||||
type motor struct {
|
||||
// en is true if the motor is enabled.
|
||||
en bool
|
||||
// dir is used to select the direction of the motor. If true, the motor
|
||||
// actuator moves towards positive values.
|
||||
dir bool
|
||||
// pos is the current position of the motor actuator.
|
||||
pos float64
|
||||
|
||||
// barrier attached to this motor, will be automatically updated when the
|
||||
// motor simulation runs.
|
||||
barrier *barrier
|
||||
}
|
||||
|
||||
// status returns a human-readable staus of the motor like '<-ID ' indicating
|
||||
// whether the motor is moving, and if so, in which direction.
|
||||
func (s *motor) status(id string) string {
|
||||
res := ""
|
||||
if s.en {
|
||||
if s.dir {
|
||||
res += fmt.Sprintf(" %s->", id)
|
||||
} else {
|
||||
res += fmt.Sprintf("<-%s ", id)
|
||||
}
|
||||
} else {
|
||||
res += fmt.Sprintf(" %s ", id)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// sim runs a simulation step of the motor given a delta time.
|
||||
func (s *motor) sim(dt time.Duration) {
|
||||
if !s.en {
|
||||
return
|
||||
}
|
||||
d := dt.Seconds()
|
||||
if s.dir {
|
||||
s.pos += d
|
||||
} else {
|
||||
s.pos -= d
|
||||
}
|
||||
s.barrier.cur = s.pos
|
||||
}
|
||||
|
||||
type indicators struct {
|
||||
indEvac bool
|
||||
indHTReadyN bool
|
||||
indPumpDown bool
|
||||
}
|
||||
|
||||
func (i indicators) String() string {
|
||||
res := ""
|
||||
if i.indEvac {
|
||||
res += "EVAC "
|
||||
} else {
|
||||
res += "---- "
|
||||
}
|
||||
if !i.indHTReadyN {
|
||||
res += "HT READY "
|
||||
} else {
|
||||
res += "-------- "
|
||||
}
|
||||
if i.indPumpDown {
|
||||
res += "PUMP DOWN "
|
||||
} else {
|
||||
res += "VENT "
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
Loading…
Reference in a new issue