From be255f2e6fc2758b964f7126f382af372ca5bf04 Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Fri, 16 Aug 2024 12:31:59 +0200 Subject: [PATCH] ph00524: add emulator --- modules/ph00524/emu/README.md | 29 ++++++ modules/ph00524/emu/arbiters.go | 98 +++++++++++++++++++ modules/ph00524/emu/controller.go | 157 ++++++++++++++++++++++++++++++ modules/ph00524/emu/go.mod | 10 ++ modules/ph00524/emu/go.sum | 8 ++ modules/ph00524/emu/i8255.go | 102 +++++++++++++++++++ modules/ph00524/emu/main.go | 83 ++++++++++++++++ modules/ph00524/emu/scope.go | 118 ++++++++++++++++++++++ 8 files changed, 605 insertions(+) create mode 100644 modules/ph00524/emu/README.md create mode 100644 modules/ph00524/emu/arbiters.go create mode 100644 modules/ph00524/emu/controller.go create mode 100644 modules/ph00524/emu/go.mod create mode 100644 modules/ph00524/emu/go.sum create mode 100644 modules/ph00524/emu/i8255.go create mode 100644 modules/ph00524/emu/main.go create mode 100644 modules/ph00524/emu/scope.go diff --git a/modules/ph00524/emu/README.md b/modules/ph00524/emu/README.md new file mode 100644 index 0000000..a42f155 --- /dev/null +++ b/modules/ph00524/emu/README.md @@ -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 ./ diff --git a/modules/ph00524/emu/arbiters.go b/modules/ph00524/emu/arbiters.go new file mode 100644 index 0000000..bb6cc4c --- /dev/null +++ b/modules/ph00524/emu/arbiters.go @@ -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 +} diff --git a/modules/ph00524/emu/controller.go b/modules/ph00524/emu/controller.go new file mode 100644 index 0000000..c18cc0b --- /dev/null +++ b/modules/ph00524/emu/controller.go @@ -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 +} diff --git a/modules/ph00524/emu/go.mod b/modules/ph00524/emu/go.mod new file mode 100644 index 0000000..260d332 --- /dev/null +++ b/modules/ph00524/emu/go.mod @@ -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 diff --git a/modules/ph00524/emu/go.sum b/modules/ph00524/emu/go.sum new file mode 100644 index 0000000..fe6c7d7 --- /dev/null +++ b/modules/ph00524/emu/go.sum @@ -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= diff --git a/modules/ph00524/emu/i8255.go b/modules/ph00524/emu/i8255.go new file mode 100644 index 0000000..6c0d858 --- /dev/null +++ b/modules/ph00524/emu/i8255.go @@ -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 + } +} diff --git a/modules/ph00524/emu/main.go b/modules/ph00524/emu/main.go new file mode 100644 index 0000000..d1aa73b --- /dev/null +++ b/modules/ph00524/emu/main.go @@ -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") + } + } +} diff --git a/modules/ph00524/emu/scope.go b/modules/ph00524/emu/scope.go new file mode 100644 index 0000000..d22277c --- /dev/null +++ b/modules/ph00524/emu/scope.go @@ -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 +}