"""GSP falcon register block (boot polling + handshake + DMA). Evidence kind: **hardware register/field header + firmware use + observed access pattern.** The block is reached through the PRIV aperture (``PRIV + 0x110000``) and, in the libos kernel's private mapping, at virtual base ``0x3_0000_0000`` (the firmware accesses both forms during boot). Modelled registers, all with header offsets relative to the block: ====================================== ====== ===== ========================= register offset access semantics ====================================== ====== ===== ========================= ``FALCON_IRQSSET`` 0x000 W ``irq_status |= value`` ``FALCON_IRQSCLR`` 0x004 W ``irq_status &= ~value`` ``FALCON_IRQSTAT`` 0x008 R ``irq_status`` ``FALCON_MAILBOX0`` 0x040 RW storage ``FALCON_MAILBOX1`` 0x044 RW storage ``FALCON_OS`` 0x080 RW storage ``FALCON_DEBUGINFO`` 0x094 RW storage (+ host shim) ``FALCON_CPUCTL`` 0x100 RW storage ``FALCON_BOOTVEC`` 0x104 RW storage ``FALCON_DMACTL`` 0x10c RW storage ``FALCON_DMATRFBASE`` 0x110 RW storage ``FALCON_DMATRFMOFFS`` 0x114 RW storage ``FALCON_DMATRFCMD`` 0x118 RW transfer on write ``FALCON_DMATRFFBOFFS`` 0x11c RW storage ``FALCON_DMATRFBASE1`` 0x128 RW storage ``FBIF_TRANSCFG(0..7)`` 0x600 RW storage ``FBIF_CTL`` 0x624 RW storage ====================================== ====== ===== ========================= Observed firmware use (570.144): the boot code writes MAILBOX0 (progress code), MAILBOX1, DEBUGINFO (boot-stage marker ``0x11DEAD``) and IRQSSET ``HALT`` (bit 4), then polls DEBUGINFO bits ``22:20`` until they clear; it reads back DMACTL/DMATRFCMD/DMATRFBASE/DMATRFBASE1/DMATRFMOFFS/ DMATRFFBOFFS and IRQSTAT. The DMA transfer semantics (size encoding, IMEM/ WRITE direction, ``((base1<<32|base)<<8)+fboffs`` addressing) follow the register header fields and are exercised only through the translation seam (:mod:`sim.devices.dma`); an unmapped transfer is refused and recorded, never zero-filled. The DEBUGINFO "host consume" behaviour is a **boot shim**, not a device claim: the firmware's own write-then-poll loop has no self-clearing path, and the documented debug channel is write-then-wait-for-zero (consumer is the host side). It is enabled per profile and recorded as an assumption. """ from __future__ import annotations from .base import Device # Register offsets (header values, relative to the falcon block base). IRQSSET = 0x000 IRQSCLR = 0x004 IRQSTAT = 0x008 MAILBOX0 = 0x040 MAILBOX1 = 0x044 OS = 0x080 DEBUGINFO = 0x094 CPUCTL = 0x100 BOOTVEC = 0x104 DMACTL = 0x10C DMATRFBASE = 0x110 DMATRFMOFFS = 0x114 DMATRFCMD = 0x118 DMATRFFBOFFS = 0x11C DMATRFBASE1 = 0x128 FBIF_TRANSCFG0 = 0x600 FBIF_TRANSCFG_STRIDE = 4 FBIF_TRANSCFG_COUNT = 8 FBIF_CTL = 0x624 # DMATRFCMD fields (header). CMD_FULL = 1 << 0 CMD_IDLE = 1 << 1 CMD_IMEM = 1 << 4 CMD_WRITE = 1 << 5 CMD_SIZE_SHIFT = 8 CMD_SIZE_MASK = 0x7 CMD_CTXDMA_SHIFT = 12 CMD_CTXDMA_MASK = 0x7 CMD_ERROR = 1 << 20 #: IRQSSET/IRQSCLR bit 4 is HALT in the header's field list; the firmware's #: boot handshake sets it after writing MAILBOX0/DEBUGINFO. IRQ_HALT = 1 << 4 # IMEM/DMEM apertures of the GSP RISC-V address map (header constants). AMAP_IMEM_START = 0x0000000000000000 AMAP_DMEM_START = 0x0000000100000000 _RW_REGISTERS = { MAILBOX0, MAILBOX1, OS, DEBUGINFO, CPUCTL, BOOTVEC, DMACTL, DMATRFBASE, DMATRFMOFFS, DMATRFCMD, DMATRFFBOFFS, DMATRFBASE1, FBIF_CTL, } class FalconBlock(Device): """Falcon/GSP register block with boot-handshake and DMA semantics.""" name = "falcon" def __init__(self, *, base: int = 0, size: int = 0x1000, dma=None, debuginfo_host_consume: bool = False, debuginfo_stage_mask: int = 0x700000, debuginfo_stage_shift: int = 20): super().__init__(base=base, size=size) self.dma = dma self.debuginfo_host_consume = bool(debuginfo_host_consume) self.debuginfo_stage_mask = debuginfo_stage_mask self.debuginfo_stage_shift = debuginfo_stage_shift self.reset() def reset(self) -> None: self.irq_status = 0 self.regs: dict[int, int] = {off: 0 for off in _RW_REGISTERS} self.regs[DMATRFCMD] = CMD_IDLE self.transcfg = [0] * FBIF_TRANSCFG_COUNT self.dma_transfers = 0 self.dma_refused = 0 self.debuginfo_consumed = 0 # -- access ------------------------------------------------------------ def read(self, offset: int, size: int) -> int | None: if size != 4: return None if offset == IRQSTAT: return self.irq_status if offset == DEBUGINFO: value = self.regs.get(DEBUGINFO, 0) if self.debuginfo_host_consume: stage = (value & self.debuginfo_stage_mask) >> self.debuginfo_stage_shift if stage: # Boot shim: the host consumes the stage marker (documented # write-then-wait-for-zero debug channel); the rest of the # register is preserved. self.regs[DEBUGINFO] = value & ~self.debuginfo_stage_mask self.debuginfo_consumed += 1 return value if offset in self.regs: return self.regs[offset] if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] return None def write(self, offset: int, size: int, value: int) -> bool: if size != 4: return False value &= 0xFFFFFFFF if offset == IRQSSET: self.irq_status = (self.irq_status | value) & 0xFFFFFFFF return True if offset == IRQSCLR: self.irq_status = (self.irq_status & ~value) & 0xFFFFFFFF return True if offset == IRQSTAT: return False # read-only if offset == DMATRFCMD: self.regs[DMATRFCMD] = value self._dma_transfer(value) return True if offset in self.regs: self.regs[offset] = value return True if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: self.transcfg[(offset - FBIF_TRANSCFG0) // 4] = value return True return False def peek(self, offset: int, size: int) -> int | None: # No shim side effects for debugger reads. if size != 4: return None if offset == IRQSTAT: return self.irq_status if offset in self.regs: return self.regs[offset] if FBIF_TRANSCFG0 <= offset < FBIF_TRANSCFG0 + 4 * FBIF_TRANSCFG_COUNT: return self.transcfg[(offset - FBIF_TRANSCFG0) // 4] return None # -- DMA --------------------------------------------------------------- def _dma_transfer(self, cmd: int) -> None: """Execute one falcon DMA transfer (header field semantics).""" if self.dma is None: self.dma_refused += 1 return size = 1 << (((cmd >> CMD_SIZE_SHIFT) & CMD_SIZE_MASK) + 2) tcm = AMAP_IMEM_START if (cmd & CMD_IMEM) else AMAP_DMEM_START base = ((self.regs.get(DMATRFBASE1, 0) & 0x1FF) << 32) | self.regs.get(DMATRFBASE, 0) fb = (base << 8) + self.regs.get(DMATRFFBOFFS, 0) tcm_off = self.regs.get(DMATRFMOFFS, 0) & 0xFFFF if cmd & CMD_WRITE: ok = self.dma.copy(tcm + tcm_off, fb, size) else: ok = self.dma.copy(fb, tcm + tcm_off, size) if ok: self.dma_transfers += 1 self.regs[DMATRFCMD] = (cmd | CMD_IDLE) & ~CMD_ERROR else: self.dma_refused += 1 self.regs[DMATRFCMD] = (cmd & ~CMD_IDLE) | CMD_ERROR # -- state ------------------------------------------------------------- def state(self) -> dict: return { "irq_status": self.irq_status, "regs": {str(k): v for k, v in sorted(self.regs.items())}, "transcfg": list(self.transcfg), "dma_transfers": self.dma_transfers, "dma_refused": self.dma_refused, "debuginfo_consumed": self.debuginfo_consumed, "debuginfo_host_consume": self.debuginfo_host_consume, } def load_state(self, data: dict) -> None: self.irq_status = int(data.get("irq_status", 0)) regs = {int(k): int(v) for k, v in data.get("regs", {}).items()} self.regs = {off: regs.get(off, 0) for off in _RW_REGISTERS} self.transcfg = [int(v) for v in data.get("transcfg", [0] * FBIF_TRANSCFG_COUNT)] self.dma_transfers = int(data.get("dma_transfers", 0)) self.dma_refused = int(data.get("dma_refused", 0)) self.debuginfo_consumed = int(data.get("debuginfo_consumed", 0)) self.debuginfo_host_consume = bool( data.get("debuginfo_host_consume", self.debuginfo_host_consume))