ph00524: add emulator

This commit is contained in:
Serge Bazanski 2024-08-16 12:31:59 +02:00
parent 7c663f42ca
commit be255f2e6f
8 changed files with 605 additions and 0 deletions

View 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 ./

View 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
}

View 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
}

View 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

View 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=

View 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
}
}

View 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")
}
}
}

View 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
}