processes the full wire boot incl. the downloaded PAZ/sfx module ISA fixes, all derived from the toolchain's own .S<->.O pairs (AS860.ZIP: OPTFLOAT/TRISTRIP/ZBUF32, plus DNC.O) and the firmware's linked COFF header: - DATA_BASE = 0x1000 DEFINITIVE: VREND.MNG carries its original COFF header in the file tail (.data vaddr 0x1000, .bss 0x1f940, entry 0xf0400000). - Integer loads: even opcodes are register-indexed (EA = src2 + src1); op 4/5 size flag = instr bit0 (0 = ld.s 16-bit, 1 = ld.l 32-bit); ld.b/ld.s sign-extend. - Integer stores: st.s/st.l selected by offset bit0, same split-offset rule. - FP loads/stores: FP register lives in the DEST field for both fld and fst (fst does NOT use the integer split-store encoding); flag bits: bit0 = auto-increment (base <- EA), bit1 1=.l/0=.d, bit2 = .q; .d/.q span register pairs/quads. ~450 fld.d + ~300 fst.d were previously read/written 32-bit. - bla (op 0x2d, was misdecoded as shrd): branch-on-LCC-and-add with the sign-dependent LCC rule (src1<0 -> signed sum >= 0), so spent countdown loops terminate. 335 bla instructions in the firmware. - CORE ESCAPE (op 0x13): sub-op 1 = lock, 2 = calli, 7 = unlock. Previously everything decoded as calli, so every spinlock acquire jumped to address 0 -- this was the phantom "exit stub" behind most earlier derails. - f2b: IEEE overflow -> +/-inf instead of raising. emu_main.py (new): runs the firmware's OWN main() (0xf0403f10) and feeds real wire captures through a hooked dN_receive, so init/do_init/dispatch/handlers all execute authentically. Provides the transputer-monitor environment (processor id, DRAM region descriptors in the shared control block, sbrk/ shared-block seed slots) and hooks only the link primitives (bla busy-wait, dN_mynode/dN_nodes/dN_receive/dN_send, putchar path, spinlocks, page allocator + virt->phys translator pending Tier-2 VRENDMON). KEY DISCOVERY: the capture's args860/code860/data860/bss860 preamble is the host DOWNLOADING an additional i860 module (the PAZ/sfx renderer layer, banner "i860 50MHz") which installs the runtime handler tables and system objects. Feeding it through the firmware's own handlers, the module loads and makes the first IGC board-register writes. State: 834+ wire commands processed (module download + init + create); first draw_scene sits at command 1568. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
536 lines
24 KiB
Python
536 lines
24 KiB
Python
"""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
|
|
_INDEXED = os.environ.get('EMU_INDEXED', '0') == '1' # experiment: register-indexed even loads
|
|
|
|
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))
|
|
off = addr & 0xFFFF
|
|
if off <= 0xFFFC:
|
|
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, off)[0]
|
|
return (self.r8(addr) | (self.r8(addr+1) << 8) |
|
|
(self.r8(addr+2) << 16) | (self.r8(addr+3) << 24))
|
|
|
|
def w32(self, addr, val):
|
|
addr = u32(addr); val = u32(val)
|
|
m = self._mmio(addr)
|
|
if m: return m[1](addr, val)
|
|
off = addr & 0xFFFF
|
|
if off <= 0xFFFC:
|
|
struct.pack_into('<I', self._page(addr, True), off, val)
|
|
else:
|
|
for i in range(4): self.w8(addr + i, (val >> (8 * i)) & 0xFF)
|
|
|
|
def r16(self, addr):
|
|
addr = u32(addr)
|
|
if (addr & 0xFFFF) == 0xFFFF:
|
|
return self.r8(addr) | (self.r8(addr + 1) << 8)
|
|
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):
|
|
addr = u32(addr)
|
|
if (addr & 0xFFFF) == 0xFFFF:
|
|
self.w8(addr, val & 0xFF); self.w8(addr + 1, (val >> 8) & 0xFF)
|
|
else:
|
|
struct.pack_into('<H', self._page(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 link base = 0x1000, DEFINITIVE: VREND.MNG carries its original COFF
|
|
# header in the file tail (offset 12+tsize+dsize+16): .data vaddr=0x00001000,
|
|
# .bss vaddr=0x0001f940, .text vaddr=0xf0400000, entry=0xf0400000.
|
|
DATA_BASE = int(os.environ.get('EMU_DATA_BASE', '0x1000'), 0)
|
|
|
|
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):
|
|
try:
|
|
return struct.unpack('<I', struct.pack('<f', x))[0]
|
|
except OverflowError: # IEEE overflow -> +/-inf
|
|
return 0xff800000 if x < 0 else 0x7f800000
|
|
@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
|
|
|
|
# ---- integer loads: ld.b (0x00/0x01), ld.s/ld.l (0x04/0x05) ----
|
|
# dest = bits20:16. ODD op = immediate s16 offset; EVEN op = register-indexed
|
|
# EA = src2 + src1 (ground truth: DNC.O `ld.b -1(fp),r19`=0473ffff,
|
|
# VR_REMOT.S `ld.l r30(r31),r31`, DNC.S `ld.l r0(r10),r16`).
|
|
# For op 4/5 the instr bit0 selects size: 0 = .s (16-bit), 1 = .l (32-bit)
|
|
# (DNC.O `ld.l 52(fp),r23` = 14770035, imm = 52|1). ld.b/ld.s sign-extend
|
|
# (compiled code masks with `and 0xff` right after ld.b).
|
|
if op in (0x00, 0x01, 0x04, 0x05):
|
|
if op & 1:
|
|
m = 0xffff if op == 0x01 else (0xfffc if (w & 1) else 0xfffe)
|
|
ea = self.rd(src2) + s16(imm & m)
|
|
else:
|
|
ea = self.rd(src2) + self.rd(src1)
|
|
if op < 0x04:
|
|
v = self.mem.r8(ea)
|
|
if v & 0x80: v -= 0x100
|
|
elif w & 1: # ld.l
|
|
v = self.mem.r32(ea)
|
|
else: # ld.s
|
|
v = self.mem.r16(ea)
|
|
if v & 0x8000: v -= 0x10000
|
|
self.wr(dest, v); return
|
|
# ---- integer stores: st.b (0x03), st.s/st.l (0x07) ----
|
|
# source = src1 (bits15:11); the 16-bit offset is SPLIT high=bits20:16,
|
|
# low=bits10:0 (DNC.O `st.b r19,-1(fp)` = 0c7f9fff). Offset bit0 selects
|
|
# st.s (0) vs st.l (1), same flag as loads.
|
|
if op in (0x03, 0x07):
|
|
off = ((w >> 16) & 0x1f) << 11 | (w & 0x7ff)
|
|
if op == 0x03:
|
|
self.mem.w8(self.rd(src2) + s16(off), self.rd(src1) & 0xff)
|
|
elif off & 1: # st.l
|
|
self.mem.w32(self.rd(src2) + s16(off & 0xfffc), self.rd(src1))
|
|
else: # st.s
|
|
self.mem.w16(self.rd(src2) + s16(off & 0xfffe), self.rd(src1) & 0xffff)
|
|
return
|
|
# ---- FP loads/stores: fld (0x08/0x09), fst (0x0a/0x0b) ----
|
|
# FP reg = dest field for BOTH (load target / store source; fst does NOT use
|
|
# the split-store encoding -- `fst.l f20,0x3c88(r31)` = 2ff43c8a, imm=0x3c88|2).
|
|
# ODD op = flat s16 immediate offset; EVEN op = register-indexed (src2+src1).
|
|
# Flag bits (low bits of the offset field): bit0 = auto-increment (base <- EA),
|
|
# bit1: 1 = .l (32-bit), 0 = .d (64-bit); bit2 with bit1=0 = .q (128-bit).
|
|
# Derived from AS860 .S<->.O pairs (OPTFLOAT/TRISTRIP/ZBUF32, n>=28 each).
|
|
if op in (0x08, 0x09, 0x0a, 0x0b):
|
|
fl = w & 7
|
|
size = 4 if (fl & 2) else (16 if (fl & 4) else 8)
|
|
if op & 1:
|
|
off = s16(imm & (0x10000 - size))
|
|
else:
|
|
off = self.rd(src1)
|
|
ea = self.rd(src2) + off
|
|
if fl & 1: # auto-increment
|
|
self.wr(src2, ea)
|
|
b0 = dest & ~((size // 4) - 1)
|
|
if op < 0x0a: # fld
|
|
for i in range(size // 4):
|
|
self.fwr(b0 + i, self.mem.r32(ea + i * 4))
|
|
else: # fst
|
|
for i in range(size // 4):
|
|
self.mem.w32(ea + i * 4, self.frd(b0 + i))
|
|
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: # CORE ESCAPE: sub-op in low bits
|
|
sub = w & 0x1f
|
|
if sub == 0x02: # calli src1
|
|
self.wr(1, pc + 8); self.branch(self.rd(src1)); return
|
|
if sub in (0x01, 0x07): # lock / unlock (bus lock for atomic RMW)
|
|
return # single-CPU emulation: no-op
|
|
if sub == 0x04: # intovr (trap on overflow) -- ignore
|
|
return
|
|
self.stop = True
|
|
self.stopmsg = f"core-escape sub {sub:#x} @ {pc:#010x} w={w:08x}"
|
|
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
|
|
# i860: dest = SRC1 (op) SRC2, where src1 = imm (immediate form) or
|
|
# rd(src1) (register form). Subtraction is src1 - src2 (NOT reversed).
|
|
b = self.rd(src2)
|
|
a = s16(imm) if (op & 1) else self.rd(src1)
|
|
if base == 0x20: # addu
|
|
s = u32(a) + u32(b); self.wr(dest, s); self.set_cc(s > MASK32)
|
|
elif base == 0x24: # adds
|
|
s = i32(a) + i32(b); self.wr(dest, s); self.set_cc(u32(a) + u32(b) > MASK32)
|
|
elif base == 0x22: # subu: src1 - src2
|
|
self.wr(dest, u32(a) - u32(b)); self.set_cc(u32(a) < u32(b))
|
|
else: # subs: src1 - src2
|
|
self.wr(dest, i32(a) - i32(b)); self.set_cc(i32(a) < i32(b))
|
|
return
|
|
if op == 0x2d: # bla isrc1,isrc2,sbroff (loop: branch on LCC + add)
|
|
# Canonical idiom (DNC.S/compiler output): adds -1,rN,r18; adds -1,r0,r17;
|
|
# bla r17,r18,LOOP; <delay>; LOOP: body...; bla r17,r18,LOOP; <delay>
|
|
# Semantics: taken = old LCC; src2 += src1; delayed branch if taken.
|
|
# LCC rule is SIGN-dependent (i860 manual): src1 < 0 -> LCC = (signed
|
|
# sum >= 0); src1 >= 0 -> LCC = unsigned carry. The signed rule makes a
|
|
# spent countdown (src1=-1, src2=-1) yield LCC=0, so a stray follow-on
|
|
# bla falls through instead of spinning (seen at 0xf041ce68).
|
|
a = self.rd(src1); b = self.rd(src2)
|
|
taken = getattr(self, 'lcc', 0)
|
|
if i32(a) < 0:
|
|
self.lcc = 1 if i32(u32(i32(a) + i32(b))) >= 0 else 0
|
|
else:
|
|
self.lcc = 1 if (u32(a) + u32(b)) > MASK32 else 0
|
|
self.wr(src2, u32(a) + u32(b))
|
|
if taken:
|
|
off = s16((((w >> 16) & 0x1f) << 11 | (w & 0x7ff)) & 0xffff)
|
|
self.branch(pc + 4 + off * 4)
|
|
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) # i860 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 == 0x21: # fmlow.dd -- i860 FP-unit INTEGER multiply.
|
|
# The i860 has no imul: ints are ixfr'd into FP regs, fmlow.dd multiplies,
|
|
# and the low 32 bits (fdest) are fxfr/fst'd back. Operands are the low
|
|
# words (32-bit); result is the 64-bit product across the fdest pair.
|
|
a = self.frd(src1); b = self.frd(src2)
|
|
prod = a * b
|
|
b0 = dest & ~1
|
|
self.fwr(b0, prod & 0xFFFFFFFF); self.fwr(b0 | 1, (prod >> 32) & 0xFFFFFFFF)
|
|
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()
|