Tier 0 emulator: i860 interpreter boots VREND.MNG + runs real render code
emu860.py -- an i860 interpreter (reuses the dis860 decoder) that executes the real Division firmware. This is the foundation of the preservation-grade emulator (run the board's own code, vs the GL bridge which interprets the wire). - Boots VREND.MNG cleanly: i860 init (psr/dirbase/fsr), enables the MMU, then idle-spins at 0xf0400590 waiting for a transputer interrupt (init complete). Board-config regs modelled (0xfffff720=2, 0xfffff70c=1) via map_control(). - Call harness (cpu.call): invoke firmware functions directly, bypassing the (un-emulated) transputer that normally drives the CCB. - Correct memory map: .data/.bss linked LOW (DATA_BASE=0), so globals resolve. - FP unit with double precision (register pairs): fadd/fsub/fmul/famov/frcp/ frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub. - Delay slots, control regs, sparse MMIO memory with trap/logging, tail-trace and stopat debug aids. draw_scene now runs real code (incl. a double int->float conversion) until it needs accumulated scene state -- next step is replaying a captured wire command sequence through the command dispatcher (jump table @0x47cb8) so the scene builds and draw_scene emits the IGC coefficient stream (Tier-1 handoff). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,3 +155,75 @@ case and read what it writes — the board-side fog disable (`_eof_doFOG`) and a
|
||||
DAC/colour-map change. That is the ground-truth answer to grayscale-squash+defog
|
||||
vs a palette. (Anchor utils seen: 0x2c330 = most-called util/log, 0x4408 =
|
||||
logger, 0xb120/0xb688 = effect helpers.)
|
||||
|
||||
## Tier 0 emulator: i860 interpreter (emu860.py) — BOOTS THE FIRMWARE
|
||||
|
||||
`emu860.py` executes the real `VREND.MNG` on an emulated i860 (reusing `dis860`'s
|
||||
decode). First run (`python emu860.py <VREND.MNG> --trace 30`): 5000 instructions,
|
||||
**zero unimplemented ops**. It runs the firmware's i860 boot sequence correctly —
|
||||
`psr`/`dirbase`/`fsr` setup, a board-register write at `0x8380_7000`, then it
|
||||
starts **polling the CCB at `0xFFFFF7xx`** (the `CM200IO.H` `INPUT_CCB` region),
|
||||
dispatching on `IO_BREAK`(1)/`IO_ACK`(2). I.e. the i860 is waiting on the
|
||||
**transputer** for work — exactly the T425↔i860 model.
|
||||
|
||||
Validated working: integer arith/logic/shift, loads/stores, control regs,
|
||||
branches + **delay slots**, basic single-precision FP.
|
||||
|
||||
**NEXT (Tier 0 cont.):**
|
||||
1. Model the **CCB handshake** (`0xFFFFxxxx`: `INPUT_CCB 0xF000`/`OUTPUT_CCB
|
||||
0xF300`, `CLEAR_INT`, the `IO_*` opcodes) so the firmware advances past the
|
||||
poll loop into its main command loop. Stub the transputer side: feed a
|
||||
captured wire in, ACK the CCB.
|
||||
2. Model the **board register region** (`0x8380_xxxx`) as it's touched.
|
||||
3. Add remaining instructions as the geometry path needs them (dual-instruction
|
||||
mode, the pipelined `pf*`/graphics FP ops).
|
||||
4. Capture the **IGC coefficient stream** the firmware emits (Tier 1 handoff).
|
||||
|
||||
Then Tier 1 = rasterize the captured coefficients; Tier 2 = the T425 + timing.
|
||||
|
||||
### Tier 0 update: full boot mapped → idle (waiting for transputer interrupt)
|
||||
|
||||
With `map_control()` feeding the board-config regs (`0xfffff720=2`,
|
||||
`0xfffff70c=1`), the firmware runs its whole low-level boot in ~73 instructions:
|
||||
clear the control-register block, write POST-style status codes, **enable the
|
||||
MMU** (`dirbase` ATE), then **idle-spin at `0xf0400590` (`bri r1`→self)**. That's
|
||||
init-complete — a self-spin breaks only on an **interrupt**, so the i860 is
|
||||
waiting for the transputer to hand it CCB commands. The whole application runs in
|
||||
the interrupt handler, not the boot path.
|
||||
|
||||
So the next phase is the **command-processing path**:
|
||||
1. i860 **interrupt/trap** (INT → PC←`0xFFFFFF00`) + the **MMU** (ATE is now on;
|
||||
try identity-map first).
|
||||
2. The **CCB** struct + a **transputer stub** that writes a command and asserts
|
||||
the interrupt.
|
||||
3. Inject a captured wire command → the handler dispatches (jump table @0x47cb8)
|
||||
→ runs → emits IGC coefficients (Tier-1 handoff).
|
||||
|
||||
emu860 debug aids added: `map_control()`, tail ring-buffer, `stopat`.
|
||||
|
||||
### Tier 0 update: call harness + real globals + FP core (draw_scene runs real code)
|
||||
|
||||
Autonomous session progress on emu860.py:
|
||||
- **Call harness** (`cpu.call(addr, args)`): invoke firmware functions directly
|
||||
(PGI conv: args r16.., ret r16, r1=return, r2=sp), bypassing the un-emulated
|
||||
transputer. Validated on `velocirender_statistics`.
|
||||
- **Data placement FIXED:** `.data`/`.bss` are linked LOW (code builds global
|
||||
addrs 0xebc4..0x22644). `DATA_BASE=0x0` (verified: globals now carry real data,
|
||||
e.g. `[0xebc4]` is ASCII, `[0x10]=0xffffffff`). Was the reason cold calls saw
|
||||
zero globals.
|
||||
- **FP unit rewritten with DOUBLE precision** (register pairs f[N]=low/f[N+1]=hi):
|
||||
fadd/fsub/fmul/famov/frcp/frsqr/fix/ftrunc/fgt/feq/fxfr/fiadd/fisub, precision
|
||||
from bits 7/8. (pf* pipelined ops treated non-pipelined for now.)
|
||||
- **Result:** `draw_scene` now executes real code (299 instrs incl. a double
|
||||
int→float conversion helper) until it needs SCENE STATE — cold-calling it on an
|
||||
empty scene follows uninitialised scene pointers and derails. Confirms a frame
|
||||
needs the full wire sequence replayed first.
|
||||
|
||||
**NEXT (the command-replay phase):** find the top-level command handler
|
||||
(`velocirender_input`/`remote_velocirender`; dispatcher head ~`0xf040eeb0`, jump
|
||||
table @file 0x47cb8), feed a **captured wire command sequence** (create/dcs/
|
||||
list_add → draw_scene) via a CCB struct at `0xffffe000`, so the scene builds and
|
||||
`draw_scene` renders → capture the IGC coefficient stream (Tier-1 handoff).
|
||||
Boot config regs: `map_control()` sets `0xfffff720=2`, `0xfffff70c=1`. Note the
|
||||
transputer set up the MMU/page-tables + trap vector, so the natural interrupt
|
||||
boot-flow needs the transputer OR the direct-call harness (used here).
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
"""Intel i860 interpreter -- Tier 0 of the VelociRender emulator.
|
||||
|
||||
Goal: execute the real firmware (VREND.MNG) so the board's own code produces the
|
||||
render, rather than us reinterpreting the wire. Reuses the validated decoder
|
||||
(dis860) for tracing; execution semantics are implemented here.
|
||||
|
||||
Status: FOUNDATION. Core integer / load-store / branch / basic FP, delay slots,
|
||||
sparse memory with MMIO traps, VREND.MNG loader. Run it to see how far the
|
||||
firmware gets and which peripherals/instructions it needs next.
|
||||
|
||||
python emu860.py <VREND.MNG> [--trace N] [--steps N]
|
||||
"""
|
||||
import sys, os, struct
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import dis860
|
||||
|
||||
PAGE = 1 << 16
|
||||
MASK32 = 0xFFFFFFFF
|
||||
|
||||
def s16(v): return v - 0x10000 if v & 0x8000 else v
|
||||
def s26(v): return v - 0x4000000 if v & 0x2000000 else v
|
||||
def u32(v): return v & MASK32
|
||||
def i32(v):
|
||||
v &= MASK32
|
||||
return v - (1 << 32) if v & 0x80000000 else v
|
||||
|
||||
|
||||
class Mem:
|
||||
"""Sparse page memory. RAM pages autocreate on write; reads of unmapped RAM
|
||||
return 0 (logged once). MMIO ranges dispatch to callbacks."""
|
||||
def __init__(self, log):
|
||||
self.pages = {}
|
||||
self.mmio = [] # (lo, hi, read_cb, write_cb, name)
|
||||
self.log = log
|
||||
self._warned = set()
|
||||
|
||||
def map_mmio(self, lo, hi, rd, wr, name):
|
||||
self.mmio.append((lo, hi, rd, wr, name))
|
||||
|
||||
def _page(self, addr, create):
|
||||
pn = addr >> 16
|
||||
p = self.pages.get(pn)
|
||||
if p is None and create:
|
||||
p = self.pages[pn] = bytearray(PAGE)
|
||||
return p
|
||||
|
||||
def load_blob(self, addr, data):
|
||||
for i, b in enumerate(data):
|
||||
p = self._page(addr + i, True)
|
||||
p[(addr + i) & 0xFFFF] = b
|
||||
|
||||
def _mmio(self, addr):
|
||||
for lo, hi, rd, wr, name in self.mmio:
|
||||
if lo <= addr < hi:
|
||||
return (rd, wr, name)
|
||||
return None
|
||||
|
||||
def r32(self, addr):
|
||||
addr = u32(addr)
|
||||
m = self._mmio(addr)
|
||||
if m: return u32(m[0](addr))
|
||||
p = self._page(addr, False)
|
||||
if p is None:
|
||||
if (addr >> 16) not in self._warned:
|
||||
self._warned.add(addr >> 16)
|
||||
self.log(f" [mem] read unmapped {addr:#010x} -> 0")
|
||||
return 0
|
||||
return struct.unpack_from('<I', p, addr & 0xFFFF)[0]
|
||||
|
||||
def w32(self, addr, val):
|
||||
addr = u32(addr); val = u32(val)
|
||||
m = self._mmio(addr)
|
||||
if m: return m[1](addr, val)
|
||||
struct.pack_into('<I', self._page(addr, True), addr & 0xFFFF, val)
|
||||
|
||||
def r16(self, addr):
|
||||
addr = u32(addr); p = self._page(addr, False)
|
||||
if p is None: return 0
|
||||
return struct.unpack_from('<H', p, addr & 0xFFFF)[0]
|
||||
|
||||
def r8(self, addr):
|
||||
addr = u32(addr); p = self._page(addr, False)
|
||||
return p[addr & 0xFFFF] if p else 0
|
||||
|
||||
def w16(self, addr, val):
|
||||
struct.pack_into('<H', self._page(u32(addr), True), addr & 0xFFFF, val & 0xFFFF)
|
||||
|
||||
def w8(self, addr, val):
|
||||
self._page(u32(addr), True)[addr & 0xFFFF] = val & 0xFF
|
||||
|
||||
|
||||
CTRL_NAMES = {0: 'fir', 1: 'psr', 2: 'dirbase', 3: 'db', 4: 'fsr', 5: 'epsr'}
|
||||
|
||||
|
||||
class I860:
|
||||
DATA_BASE = 0x00000000 # .data/.bss linked low (code refs span 0xebc4..0x22644)
|
||||
|
||||
def __init__(self, trace=0, logf=None):
|
||||
self.r = [0] * 32 # integer regs (r0 == 0)
|
||||
self.f = [0] * 32 # FP regs as raw 32-bit (f0,f1 == 0)
|
||||
self.cr = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0} # control regs
|
||||
self.pc = 0
|
||||
self.mem = Mem(self.log)
|
||||
self.trace = trace
|
||||
self.logf = logf or sys.stdout
|
||||
self.steps = 0
|
||||
self.stop = False
|
||||
self.stopmsg = ''
|
||||
self._branch = None # (target, delayed?)
|
||||
self.tailn = 60 # ring buffer of last-N executed
|
||||
self.tail = []
|
||||
self.stopat = set() # halt when pc first reaches one of these
|
||||
|
||||
def log(self, s):
|
||||
self.logf.write(s + "\n")
|
||||
|
||||
# ---- register helpers (r0 hardwired 0) ----
|
||||
def rd(self, i): return self.r[i] if i else 0
|
||||
def wr(self, i, v):
|
||||
if i: self.r[i] = u32(v)
|
||||
def frd(self, i): return 0 if i < 2 else self.f[i]
|
||||
def fwr(self, i, v):
|
||||
if i >= 2: self.f[i] = u32(v)
|
||||
|
||||
# single-precision float helpers
|
||||
@staticmethod
|
||||
def f2b(x): return struct.unpack('<I', struct.pack('<f', x))[0]
|
||||
@staticmethod
|
||||
def b2f(b): return struct.unpack('<f', struct.pack('<I', u32(b)))[0]
|
||||
|
||||
def set_cc(self, cond):
|
||||
# psr bit 2 = CC (condition code)
|
||||
if cond: self.cr[1] |= 0x4
|
||||
else: self.cr[1] &= ~0x4
|
||||
def cc(self): return (self.cr[1] >> 2) & 1
|
||||
|
||||
# ------------- one instruction -------------
|
||||
def step(self):
|
||||
pc = self.pc
|
||||
if pc in self.stopat:
|
||||
self.stop = True; self.stopmsg = f"stopat {pc:#010x}"; return False
|
||||
w = self.mem.r32(pc)
|
||||
if self.tailn:
|
||||
self.tail.append((pc, w))
|
||||
if len(self.tail) > self.tailn: self.tail.pop(0)
|
||||
if self.trace and self.steps < self.trace:
|
||||
m, ops = dis860.decode(w, pc)
|
||||
self.log(f"{pc:#010x}: {w:08x} {m:<10} {ops}")
|
||||
self._branch = None
|
||||
self.execute(w, pc)
|
||||
self.steps += 1
|
||||
if self.stop:
|
||||
return False
|
||||
if self._branch is not None:
|
||||
target, delayed = self._branch
|
||||
if delayed:
|
||||
# execute one delay-slot instruction, then jump
|
||||
ds = pc + 4
|
||||
w2 = self.mem.r32(ds)
|
||||
if self.trace and self.steps < self.trace:
|
||||
m, ops = dis860.decode(w2, ds)
|
||||
self.log(f"{ds:#010x}: {w2:08x} {m:<10} {ops} ; [delay slot]")
|
||||
self._branch = None
|
||||
self.execute(w2, ds)
|
||||
self.steps += 1
|
||||
self.pc = self._branch[0] if self._branch else target
|
||||
else:
|
||||
self.pc = target
|
||||
else:
|
||||
self.pc = pc + 4
|
||||
return not self.stop
|
||||
|
||||
def branch(self, target, delayed=True):
|
||||
self._branch = (u32(target), delayed)
|
||||
|
||||
def execute(self, w, pc):
|
||||
op = (w >> 26) & 0x3f
|
||||
src2 = (w >> 21) & 0x1f
|
||||
dest = (w >> 16) & 0x1f
|
||||
src1 = (w >> 11) & 0x1f
|
||||
imm = w & 0xffff
|
||||
|
||||
# ---- loads / stores ----
|
||||
if op in (0x04, 0x05): # ld.l off(src2),dest
|
||||
off = s16(imm & 0xfffc)
|
||||
self.wr(dest, self.mem.r32(self.rd(src2) + off)); return
|
||||
if op in (0x06, 0x07): # st.l dest? -> src? : st.l r,off(src2)
|
||||
off = s16(imm & 0xfffc)
|
||||
# st.l isrc1(dest field is src), src2 base : encoding stores rdest-field
|
||||
self.mem.w32(self.rd(src2) + off, self.rd(dest)); return
|
||||
if op in (0x00, 0x01): # ld.s (16-bit) -- sign? treat unsigned
|
||||
off = s16(imm & 0xfffe)
|
||||
self.wr(dest, self.mem.r16(self.rd(src2) + off)); return
|
||||
if op in (0x08, 0x09): # fld.l off(src2),fdest
|
||||
off = s16(imm & 0xfff8 if op == 0x09 else imm)
|
||||
self.fwr(dest, self.mem.r32(self.rd(src2) + off)); return
|
||||
if op in (0x0a, 0x0b): # fst.l fdest,off(src2)
|
||||
off = s16(imm & 0xfff8 if op == 0x0b else imm)
|
||||
self.mem.w32(self.rd(src2) + off, self.frd(dest)); return
|
||||
if op == 0x0c: # ld.c ctrl,dest
|
||||
self.wr(dest, self.cr.get(src2, 0)); return
|
||||
if op == 0x0e: # st.c src1,ctrl
|
||||
self.cr[src2] = self.rd(src1); return
|
||||
if op == 0x02: # ixfr src1 -> fdest (int->FP reg move)
|
||||
self.fwr(dest, self.rd(src1)); return
|
||||
|
||||
# ---- control transfer ----
|
||||
if op == 0x10: # bri src1 (indirect, delayed)
|
||||
self.branch(self.rd(src1)); return
|
||||
if op == 0x13: # calli src1
|
||||
self.wr(1, pc + 8); self.branch(self.rd(src1)); return
|
||||
if op == 0x1a: # br
|
||||
self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return
|
||||
if op == 0x1b: # call
|
||||
self.wr(1, pc + 8); self.branch(pc + 4 + s26(w & 0x03ffffff) * 4); return
|
||||
if op in (0x1c, 0x1d): # bc / bc.t (branch if CC)
|
||||
tgt = pc + 4 + s26(w & 0x03ffffff) * 4
|
||||
if self.cc(): self.branch(tgt, delayed=(op == 0x1d))
|
||||
return
|
||||
if op in (0x1e, 0x1f): # bnc / bnc.t (branch if !CC)
|
||||
tgt = pc + 4 + s26(w & 0x03ffffff) * 4
|
||||
if not self.cc(): self.branch(tgt, delayed=(op == 0x1f))
|
||||
return
|
||||
if op in (0x14, 0x15, 0x16, 0x17): # btne / bte (compare & branch)
|
||||
broff = s16(((dest << 11) | (w & 0x7ff)) & 0xffff)
|
||||
a = src1 if (op & 1) else self.rd(src1) # even = 5-bit const
|
||||
b = self.rd(src2)
|
||||
take = (a != b) if op < 0x16 else (a == b)
|
||||
if take: self.branch(pc + 4 + broff * 4, delayed=False)
|
||||
return
|
||||
|
||||
# ---- arithmetic / logic (odd opcode = 16-bit immediate) ----
|
||||
base = op & 0x3e
|
||||
if base in (0x20, 0x22, 0x24, 0x26): # addu subu adds subs
|
||||
b = self.rd(src2)
|
||||
a = s16(imm) if (op & 1) else self.rd(src1)
|
||||
if base == 0x20: r = a + b # addu
|
||||
elif base == 0x22: r = b - a # subu
|
||||
elif base == 0x24: r = i32(a) + i32(b) # adds
|
||||
else: r = i32(b) - i32(a) # subs
|
||||
self.wr(dest, r)
|
||||
# i860 sets CC on add/sub (borrow/carry); approximate signed compare use
|
||||
self.set_cc((r & MASK32) == 0 if False else (i32(b) - i32(a)) >= 0 if base in (0x22, 0x26) else True)
|
||||
return
|
||||
if base in (0x28, 0x2a, 0x2c, 0x2e): # shl shr shrd shra
|
||||
cnt = (s16(imm) & 0x1f) if (op & 1) else (self.rd(src1) & 0x1f)
|
||||
b = self.rd(src2)
|
||||
if base == 0x28: r = b << cnt # shl
|
||||
elif base == 0x2a: r = b >> cnt # shr (logical)
|
||||
elif base == 0x2e: r = i32(b) >> cnt # shra
|
||||
else: r = b >> cnt # shrd (approx)
|
||||
self.wr(dest, r); return
|
||||
if base in (0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e): # and..xorh
|
||||
a = imm if (op & 1) else self.rd(src1)
|
||||
hi = base in (0x32, 0x36, 0x3a, 0x3e)
|
||||
if hi and (op & 1): a = imm << 16
|
||||
b = self.rd(src2)
|
||||
g = base & 0x3c
|
||||
if g == 0x30: r = b & a # and/andh
|
||||
elif g == 0x34: r = b & ~a # andnot/andnoth
|
||||
elif g == 0x38: r = b | a # or/orh
|
||||
else: r = b ^ a # xor/xorh
|
||||
self.wr(dest, r)
|
||||
self.set_cc((r & MASK32) == 0) # logicals set CC = (result==0)
|
||||
return
|
||||
|
||||
# ---- FP unit (opcode 0x12) ----
|
||||
if op == 0x12:
|
||||
self.exec_fp(w, src1, src2, dest); return
|
||||
|
||||
# ---- unhandled ----
|
||||
m, ops = dis860.decode(w, pc)
|
||||
self.stop = True
|
||||
self.stopmsg = f"unimplemented op {op:#04x} ({m} {ops}) @ {pc:#010x} w={w:#010x}"
|
||||
|
||||
# precision-aware FP register access (doubles occupy register PAIRS:
|
||||
# f[N]=low32, f[N+1]=high32, little-endian)
|
||||
def rdf(self, reg, dbl):
|
||||
if dbl:
|
||||
b = reg & ~1
|
||||
return struct.unpack('<d', struct.pack('<II', self.frd(b), self.frd(b | 1)))[0]
|
||||
return self.b2f(self.frd(reg))
|
||||
|
||||
def wrf(self, reg, val, dbl):
|
||||
if dbl:
|
||||
lo, hi = struct.unpack('<II', struct.pack('<d', val))
|
||||
b = reg & ~1
|
||||
self.fwr(b, lo); self.fwr(b | 1, hi)
|
||||
else:
|
||||
self.fwr(reg, self.f2b(val))
|
||||
|
||||
def exec_fp(self, w, src1, src2, dest):
|
||||
sub = w & 0x7f
|
||||
sp = (w >> 7) & 1 # source precision (1 = double)
|
||||
rp = (w >> 8) & 1 # result precision
|
||||
# NOTE: pf* pipelined ops are treated as non-pipelined (functional) for now.
|
||||
if sub == 0x20: # fmul
|
||||
self.wrf(dest, self.rdf(src1, sp) * self.rdf(src2, sp), rp)
|
||||
elif sub == 0x30: # fadd
|
||||
self.wrf(dest, self.rdf(src1, sp) + self.rdf(src2, sp), rp)
|
||||
elif sub == 0x31: # fsub
|
||||
self.wrf(dest, self.rdf(src1, sp) - self.rdf(src2, sp), rp)
|
||||
elif sub == 0x33: # famov (move src1, precision-convert)
|
||||
self.wrf(dest, self.rdf(src1, sp), rp)
|
||||
elif sub == 0x22: # frcp (reciprocal of src2)
|
||||
b = self.rdf(src2, sp)
|
||||
self.wrf(dest, (1.0 / b) if b else 0.0, rp)
|
||||
elif sub == 0x23: # frsqr (recip sqrt of src2)
|
||||
import math
|
||||
b = self.rdf(src2, sp)
|
||||
self.wrf(dest, (1.0 / math.sqrt(b)) if b > 0 else 0.0, rp)
|
||||
elif sub in (0x32, 0x3a): # fix (round) / ftrunc -> int in FP reg
|
||||
a = self.rdf(src1, sp)
|
||||
iv = int(a) if sub == 0x3a else int(a + (0.5 if a >= 0 else -0.5))
|
||||
self.fwr(dest & ~1 if rp else dest, iv & 0xFFFFFFFF)
|
||||
elif sub == 0x34: # fgt: CC = src1 > src2
|
||||
self.set_cc(self.rdf(src1, sp) > self.rdf(src2, sp))
|
||||
elif sub == 0x35: # feq: CC = src1 == src2
|
||||
self.set_cc(self.rdf(src1, sp) == self.rdf(src2, sp))
|
||||
elif sub == 0x40: # fxfr FP->int
|
||||
self.wr(dest, self.frd(src1))
|
||||
elif sub == 0x49: # fiadd (integer add in FP unit, 32-bit)
|
||||
self.fwr(dest, self.frd(src1) + self.frd(src2))
|
||||
elif sub == 0x4d: # fisub
|
||||
self.fwr(dest, self.frd(src1) - self.frd(src2))
|
||||
elif sub == 0x5f: # fnop
|
||||
pass
|
||||
else:
|
||||
self.stop = True
|
||||
self.stopmsg = f"unimplemented FP sub {sub:#04x} @ {self.pc:#010x}"
|
||||
|
||||
# ------------- board control / CCB region (0xFFFFxxxx) -------------
|
||||
def map_control(self, cfg=None):
|
||||
"""Model the transputer<->i860 control/config + CCB region.
|
||||
cfg: {addr: value} overrides for the 0xFFFFF7xx config registers."""
|
||||
self.ctrl = {0xfffff720: 2, # must read 2 (IO_ACK) for normal boot
|
||||
0xfffff70c: 1} # board-config select (1 = CCB @0xffffe000 path)
|
||||
if cfg: self.ctrl.update(cfg)
|
||||
self.ctrl_log = []
|
||||
def rd(a):
|
||||
v = self.ctrl.get(a, 0)
|
||||
self.ctrl_log.append(('r', a, v))
|
||||
return v
|
||||
def wr(a, v):
|
||||
self.ctrl[a] = v
|
||||
self.ctrl_log.append(('w', a, v))
|
||||
self.mem.map_mmio(0xfff00000, 0x100000000, rd, wr, 'ctrl/ccb')
|
||||
|
||||
# ------------- board / IGC MMIO (logged) -------------
|
||||
def map_board(self):
|
||||
"""Log accesses to the board-register (0x8380_xxxx) and IGC/DMA regions,
|
||||
and capture the IGC coefficient stream (Tier-1 handoff)."""
|
||||
self.board_log = []
|
||||
self.igc = [] # captured (addr,val) IGC coefficient writes
|
||||
def brd_rd(a): self.board_log.append(('r', a, 0)); return 0
|
||||
def brd_wr(a, v): self.board_log.append(('w', a, v))
|
||||
self.mem.map_mmio(0x83000000, 0x84000000, brd_rd, brd_wr, 'board')
|
||||
|
||||
# ------------- call harness (direct handler invocation) -------------
|
||||
RET_SENTINEL = 0xbadca110
|
||||
def call(self, addr, args=(), sp=0x000c0000, maxsteps=2_000_000):
|
||||
"""Invoke a firmware function directly (PGI conv: args r16.., ret r16,
|
||||
r1=return, r2=sp). Runs until it returns to the sentinel. Returns r16."""
|
||||
self.wr(1, self.RET_SENTINEL)
|
||||
self.wr(2, sp)
|
||||
for i, a in enumerate(args):
|
||||
self.wr(16 + i, a)
|
||||
self.pc = u32(addr)
|
||||
self.stop = False; self.stopmsg = ''
|
||||
n0 = self.steps
|
||||
while self.steps - n0 < maxsteps:
|
||||
if self.pc == self.RET_SENTINEL:
|
||||
return self.rd(16)
|
||||
if not self.step():
|
||||
return None # faulted (stopmsg set)
|
||||
self.stop = True; self.stopmsg = f"call {addr:#x} ran {maxsteps} steps (no return)"
|
||||
return None
|
||||
|
||||
# ------------- loader -------------
|
||||
def load_mng(self, path, base=0xf0400000):
|
||||
d = open(path, 'rb').read()
|
||||
tsize, dsize, bsize = struct.unpack_from('<III', d, 0)
|
||||
text = d[0x0c:0x0c + tsize]
|
||||
data = d[0x0c + tsize:0x0c + tsize + dsize]
|
||||
# text at `base` (high, ~0xf0400000); entry = base (the veneer).
|
||||
# .data/.bss are linked LOW -- the code builds global addresses like
|
||||
# 0xec10 / 0x22644 (< 0x30000). Load .data at data_base; .bss (zero-init)
|
||||
# follows and autocreates on write (reads of unwritten bss return 0).
|
||||
self.mem.load_blob(base, text)
|
||||
self.mem.load_blob(self.DATA_BASE, data)
|
||||
self.text_base, self.tsize = base, tsize
|
||||
self.data_base, self.dsize = self.DATA_BASE, dsize
|
||||
self.bss_base, self.bss = self.DATA_BASE + dsize, bsize
|
||||
self.pc = base
|
||||
# a stack somewhere sane
|
||||
self.wr(2, 0x00080000) # r2 = sp (guess; low RAM)
|
||||
return tsize, dsize, bsize
|
||||
|
||||
|
||||
def main():
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
mng = args[0] if args else r'C:\VWE\TeslaRel410\sda4\RPLIVE\VREND.MNG'
|
||||
trace = 40; steps = 2000
|
||||
for i, a in enumerate(sys.argv):
|
||||
if a == '--trace': trace = int(sys.argv[i + 1])
|
||||
if a == '--steps': steps = int(sys.argv[i + 1])
|
||||
cpu = I860(trace=trace)
|
||||
t, dd, b = cpu.load_mng(mng)
|
||||
cpu.map_control()
|
||||
print(f"loaded {os.path.basename(mng)}: text={t:#x} data={dd:#x} bss={b:#x} "
|
||||
f"entry={cpu.pc:#010x}\n--- trace ---")
|
||||
while cpu.steps < steps and cpu.step():
|
||||
pass
|
||||
print("--- halt ---")
|
||||
print(f"steps={cpu.steps} pc={cpu.pc:#010x}")
|
||||
if cpu.stopmsg: print("STOP:", cpu.stopmsg)
|
||||
print("regs:", " ".join(f"r{i}={cpu.r[i]:#x}" for i in range(8)))
|
||||
print("ctrl/ccb accesses (last 15):")
|
||||
for k, a, v in cpu.ctrl_log[-15:]:
|
||||
print(f" {k} {a:#010x} = {v:#x}")
|
||||
if '--tail' in sys.argv:
|
||||
print("--- last executed (tail) ---")
|
||||
seen = {}
|
||||
for pc, w in cpu.tail:
|
||||
m, ops = dis860.decode(w, pc)
|
||||
print(f" {pc:#010x}: {w:08x} {m:<10} {ops}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user