diff options
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py')
| -rw-r--r-- | debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py | 458 |
1 files changed, 458 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py b/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py new file mode 100644 index 0000000..8063ee9 --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/tests/test_sim_devices.py @@ -0,0 +1,458 @@ +"""Device-model tests: registry, per-block semantics, profile and boot ladder. + +Every modelled register assertion here is tied to the evidence documented in +``sim/devices/*.py`` and ``docs/36-GSP-DEVICE-MODEL.md``. The real-firmware +tests are bounded (<= 20k instructions) and assert *observed* rungs only. +""" +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sim.devices import ( # noqa: E402 + CsbWindow, + DmaTranslator, + FalconBlock, + IrqRouter, + PmcBlock, + PmuStub, + PriscvBlock, + PtimerBlock, + Wpr2Block, + load_profile, + registry_from_profile, +) +from sim.devices.base import DeviceRegistry # noqa: E402 +from sim.devices import falcon as falcon_mod # noqa: E402 +from sim.machine import Machine # noqa: E402 + +TOOL_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DEFAULT_IMAGE = os.path.normpath( + os.path.join(TOOL_ROOT, "..", "..", "firmware", "57X-GSP", "570.144", + "gsp-570.144.fwimage.bin")) +DEFAULT_PROFILE = os.path.join(TOOL_ROOT, "sim", "profiles", "tu104-570.144.json") + + +class RegistryTests(unittest.TestCase): + def test_containment_and_unhandled_log(self): + reg = DeviceRegistry() + dev = PmcBlock(base=0x1000, size=0x1000) + reg.add(dev) + ok, value = reg.read(0x1A00, 4) + self.assertTrue(ok) + self.assertEqual(value, dev.boot_42) + # unmodelled offset inside the window: unhandled but window-matched + ok, _ = reg.read(0x1A04, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 1) + # outside every window + ok, _ = reg.read(0x2000, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 2) + # Only window-matched accesses are recorded; outside-window accesses + # are counted but left to the machine's unknown log. + self.assertEqual(len(reg.accesses), 2) + self.assertFalse(reg.accesses[1]["handled"]) + + def test_overlapping_windows_fall_back_on_refusal(self): + # A later window shadows an earlier one, but an offset the overlay does + # not model (here the PMU stub refuses everything) falls back to the + # wider device instead of being logged as unknown. + reg = DeviceRegistry() + fal = FalconBlock(base=0x1000, size=0x1000) + reg.add(fal) + reg.add(PmuStub(base=0x1000, size=0x1000)) + self.assertTrue(reg.write(0x1040, 4, 0xCA)) # MAILBOX0 via fallback + self.assertEqual(fal.regs[falcon_mod.MAILBOX0], 0xCA) + ok, value = reg.read(0x1040, 4) + self.assertTrue(ok) + self.assertEqual(value, 0xCA) + self.assertEqual(reg.unhandled, 0) + self.assertTrue(all(a["handled"] for a in reg.accesses)) + + def test_overlapping_windows_all_refuse_records_once(self): + reg = DeviceRegistry() + reg.add(PmuStub(base=0x1000, size=0x1000)) + reg.add(PmuStub(base=0x1000, size=0x1000)) + ok, _ = reg.read(0x1000, 4) + self.assertFalse(ok) + self.assertEqual(reg.unhandled, 1) + self.assertEqual(len(reg.accesses), 1) + self.assertFalse(reg.accesses[0]["handled"]) + self.assertFalse(reg.write(0x1000, 4, 1)) + self.assertEqual(reg.unhandled, 2) + self.assertEqual(len(reg.accesses), 2) + + def test_straddling_access_is_not_served(self): + reg = DeviceRegistry() + reg.add(PmcBlock(base=0x1000, size=0x10)) + ok, _ = reg.read(0x1008, 8) # ends at 0x1010, outside the window + self.assertFalse(ok) + + def test_state_round_trip(self): + reg = DeviceRegistry() + fal = FalconBlock(base=0x1000, size=0x1000) + reg.add(fal) + reg.write(0x1040, 4, 0xCA) + snap = reg.state() + reg2 = DeviceRegistry() + fal2 = FalconBlock(base=0x1000, size=0x1000) + reg2.add(fal2) + reg2.load_state(snap) + self.assertEqual(fal2.regs[falcon_mod.MAILBOX0], 0xCA) + self.assertEqual(fal2.state(), fal.state()) + + +class PmcTests(unittest.TestCase): + def test_tu104_value_fields(self): + dev = PmcBlock(chip_id=0x164) + value = dev.read(0xA00, 4) + self.assertEqual((value >> 20) & 0x1FF, 0x164) + self.assertEqual((value >> 24) & 0x1F, 0x16) # ARCHITECTURE TU100 + self.assertEqual((value >> 20) & 0xF, 0x4) # IMPLEMENTATION + self.assertEqual(dev.read(0xA04, 4), None) + self.assertFalse(dev.write(0xA00, 4, 0)) + + def test_profile_profile_value_override(self): + dev = PmcBlock(boot_42=0x00123456) + self.assertEqual(dev.read(0xA00, 4), 0x00123456) + + def test_chip_id_constants_are_distinct(self): + from sim.devices import pmc as pmc_mod + ids = [pmc_mod.TU102, pmc_mod.TU104, pmc_mod.TU105, pmc_mod.TU106, + pmc_mod.TU116, pmc_mod.TU117] + self.assertEqual(len(ids), len(set(ids))) + self.assertEqual(pmc_mod.TU117, 0x167) + self.assertFalse(hasattr(pmc_mod, "TU107")) + + +class FalconTests(unittest.TestCase): + def test_irq_set_clear_status(self): + dev = FalconBlock() + dev.write(falcon_mod.IRQSSET, 4, falcon_mod.IRQ_HALT) + self.assertEqual(dev.read(falcon_mod.IRQSTAT, 4), falcon_mod.IRQ_HALT) + dev.write(falcon_mod.IRQSCLR, 4, falcon_mod.IRQ_HALT) + self.assertEqual(dev.read(falcon_mod.IRQSTAT, 4), 0) + # write-only set/clear registers have no read-back + self.assertIsNone(dev.read(falcon_mod.IRQSSET, 4)) + + def test_mailbox_rw(self): + dev = FalconBlock() + dev.write(falcon_mod.MAILBOX0, 4, 0xCA) + dev.write(falcon_mod.MAILBOX1, 4, 0x11DEAD) + self.assertEqual(dev.read(falcon_mod.MAILBOX0, 4), 0xCA) + self.assertEqual(dev.read(falcon_mod.MAILBOX1, 4), 0x11DEAD) + + def test_debuginfo_storage_without_shim(self): + dev = FalconBlock(debuginfo_host_consume=False) + dev.write(falcon_mod.DEBUGINFO, 4, 0x11DEAD) + self.assertEqual(dev.read(falcon_mod.DEBUGINFO, 4), 0x11DEAD) + + def test_debuginfo_host_consume_shim(self): + dev = FalconBlock(debuginfo_host_consume=True) + dev.write(falcon_mod.DEBUGINFO, 4, 0x11DEAD) + value = dev.read(falcon_mod.DEBUGINFO, 4) + self.assertEqual(value, 0x11DEAD) # value preserved for the reader + self.assertEqual(dev.read(falcon_mod.DEBUGINFO, 4), 0x1DEAD) # stage cleared + self.assertEqual(dev.debuginfo_consumed, 1) + # debugger peek never consumes + dev.write(falcon_mod.DEBUGINFO, 4, 0x21DEAD) + self.assertEqual(dev.peek(falcon_mod.DEBUGINFO, 4), 0x21DEAD) + self.assertEqual(dev.debuginfo_consumed, 1) + + def test_dma_transfer_through_translator(self): + ram = bytearray(0x10000) + ram[0x100:0x108] = b"DEADBEEF" + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x0, "sim": 0, "size": 0x10000}]) + dev = FalconBlock(dma=dma) + # Program: destination = 0x200, source = IMEM + 0x100. The register + # encodes ((base1 << 32) | base) << 8; the profile's DMA windows decide + # which aperture that address resolves to (aperture selection itself is + # not proven yet and is documented as an unknown). + dev.write(falcon_mod.DMATRFBASE1, 4, 0) + dev.write(falcon_mod.DMATRFBASE, 4, 0) + dev.write(falcon_mod.DMATRFFBOFFS, 4, 0x200) + dev.write(falcon_mod.DMATRFMOFFS, 4, 0x100) + cmd = (falcon_mod.CMD_IMEM | falcon_mod.CMD_WRITE + | (2 << falcon_mod.CMD_SIZE_SHIFT)) # IMEM -> FB, 16 bytes + dev.write(falcon_mod.DMATRFCMD, 4, cmd) + self.assertEqual(ram[0x200:0x210], b"DEADBEEF" + bytes(8)) + self.assertTrue(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_IDLE) + self.assertEqual(dev.dma_transfers, 1) + + def test_dma_refused_when_unmapped(self): + ram = bytearray(0x1000) + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), windows=[]) + dev = FalconBlock(dma=dma) + dev.write(falcon_mod.DMATRFBASE, 4, 0) + dev.write(falcon_mod.DMATRFCMD, 4, falcon_mod.CMD_IMEM) + self.assertFalse(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_IDLE) + self.assertTrue(dev.regs[falcon_mod.DMATRFCMD] & falcon_mod.CMD_ERROR) + self.assertEqual(dev.dma_refused, 1) + + +class PriscvTests(unittest.TestCase): + def test_irq_mask_semantics(self): + dev = PriscvBlock() + dev.write(0x2AC, 4, 0x5) + dev.write(0x2B0, 4, 0x1) + self.assertEqual(dev.read(0x2B4, 4), 0x4) + self.assertFalse(dev.write(0x2B4, 4, 0xFFFF)) # read-only + + def test_trace_and_error_registers(self): + dev = PriscvBlock() + dev.write(0x344, 4, 0x1234) + self.assertEqual(dev.read(0x344, 4), 0x1234) + # observed boot panic reads (R-I4R): read-back is zero, writes refused + self.assertEqual(dev.read(0x364, 4), 0) + self.assertEqual(dev.read(0x368, 4), 0) + self.assertFalse(dev.write(0x368, 4, 0x1)) + + +class TimerTests(unittest.TestCase): + def test_modeled_time_reads(self): + clock = {"insns": 0} + dev = PtimerBlock(ns_per_insn=2.0, clock=lambda: clock["insns"]) + self.assertEqual(dev.read(0x400, 4), 0) + self.assertEqual(dev.read(0x410, 4), 0) + clock["insns"] = 16 + # 32 ns -> TIME_0 NSEC field (bits 31:5) = 1 + self.assertEqual(dev.read(0x400, 4), 1) + self.assertEqual(dev.read(0x410, 4), 32) + + def test_profile_window_reaches_time_registers(self): + # Regression: the profile window is PRIV+0x9000, so TIME_0/TIME_1 must + # be block-relative (0x400/0x410); aperture-absolute 0x9400/0x9410 fall + # outside the declared window and serve nothing. + profile = { + "schema": "greeneye-sim-profile/1", + "devices": { + "timer": { + "ns_per_insn": 1.0, + "windows": [{"base": "0x9000", "size": "0x1000"}], + } + }, + } + machine = Machine(memory_mb=1, ram_base=0) + reg = registry_from_profile(profile, machine=machine) + machine.insns = 32 + ok, value = reg.read(0x9000 + 0x400, 4, pc=0) + self.assertTrue(ok) + self.assertEqual(value, 1) # (32 ns >> 5) + ok, value = reg.read(0x9000 + 0x410, 4, pc=0) + self.assertTrue(ok) + self.assertEqual(value, 32) + + def test_timer_registration_is_opt_in(self): + machine = Machine(memory_mb=1, ram_base=0) + no_section = { + "schema": "greeneye-sim-profile/1", + "devices": {"pmc": {"windows": [{"base": "0x1000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(no_section, machine=machine) + self.assertIsNone(reg.device_by_name("timer")) + disabled = { + "schema": "greeneye-sim-profile/1", + "devices": {"timer": {"enabled": False, + "windows": [{"base": "0x9000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(disabled, machine=machine) + self.assertIsNone(reg.device_by_name("timer")) + enabled = { + "schema": "greeneye-sim-profile/1", + "devices": {"timer": {"ns_per_insn": 1.0, + "windows": [{"base": "0x9000", "size": "0x1000"}]}}, + } + reg = registry_from_profile(enabled, machine=machine) + self.assertIsNotNone(reg.device_by_name("timer")) + + +class CsbTests(unittest.TestCase): + def test_undeclared_window_serves_nothing(self): + dev = CsbWindow(base=0x1000, size=0x1000) + self.assertIsNone(dev.read(0x0, 4)) + self.assertFalse(dev.write(0x0, 4, 1)) + + def test_declared_semantics(self): + dev = CsbWindow(base=0x1000, size=0x1000, registers={ + 0x00: {"width": 4, "value": 0xF, "semantics": "w1c"}, + 0x04: {"width": 4, "value": 0, "semantics": "rw"}, + 0x08: {"width": 4, "value": 0, "semantics": "ro"}, + }) + dev.write(0x00, 4, 0x3) + self.assertEqual(dev.read(0x00, 4), 0xC) + dev.write(0x04, 4, 0x77) + self.assertEqual(dev.read(0x04, 4), 0x77) + self.assertFalse(dev.write(0x08, 4, 1)) + + +class Wpr2Tests(unittest.TestCase): + def test_range_encoding(self): + dev = Wpr2Block() + dev.set_range(0x2000000, 0x6000000) + lo = dev.read(0x1FA824, 4) + hi = dev.read(0x1FA828, 4) + self.assertEqual((lo << 12), 0x2000000) + self.assertEqual((hi << 12) + 0x20000, 0x6000000) + self.assertIsNone(dev.read(0x1FA800, 4)) + + def test_state_derives_range_after_writes(self): + # Regression: writes update regs only; state() must derive start/end + # from the registers rather than reporting a stale set_range range. + dev = Wpr2Block() + dev.write(0x1FA824, 4, 0x2000000 >> 12) + dev.write(0x1FA828, 4, (0x6000000 - 0x20000) >> 12) + state = dev.state() + self.assertEqual(state["start"], 0x2000000) + self.assertEqual(state["end"], 0x6000000) + restored = Wpr2Block() + restored.load_state(state) + self.assertEqual(restored.start, 0x2000000) + self.assertEqual(restored.end, 0x6000000) + # A stale serialised start/end never wins over the registers. + stale = Wpr2Block() + stale.load_state({"regs": {str(0x1FA824): 0x2000}, + "start": 0xDEAD, "end": 0xBEEF}) + self.assertEqual(stale.start, 0x2000000) + self.assertIsNone(stale.end) + + +class PmuTests(unittest.TestCase): + def test_stub_never_fakes(self): + dev = PmuStub(base=0x1000, size=0x1000) + self.assertIsNone(dev.read(0x0, 4)) + self.assertFalse(dev.write(0x0, 4, 1)) + self.assertEqual(dev.seen["r:0x0:4"], 1) + + +class IrqRouterTests(unittest.TestCase): + def test_aggregation(self): + fal = FalconBlock() + pri = PriscvBlock() + router = IrqRouter(falcon=fal, priscv=pri) + fal.write(falcon_mod.IRQSSET, 4, falcon_mod.IRQ_HALT) + pri.write(0x2AC, 4, 0x2) + state = router.pending() + self.assertTrue(state["falcon_halt"]) + self.assertEqual(state["riscv_irq_mask"], 0x2) + + +class DmaTranslatorTests(unittest.TestCase): + def test_translate_and_refuse(self): + ram = bytearray(0x100) + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x1000, "sim": 0, "size": 0x100}]) + self.assertEqual(dma.translate(0x1000), 0) + self.assertEqual(dma.translate(0x10FF), 0xFF) + self.assertIsNone(dma.translate(0x2000)) + self.assertTrue(dma.write(0x1000, b"AB")) + self.assertEqual(dma.read(0x1000, 2), b"AB") + self.assertIsNone(dma.read(0x2000, 2)) + self.assertEqual(dma.refused, 1) + + def test_copy_counts_only_completed_transfers_and_exports_events(self): + ram = bytearray(0x100) + ram[0:4] = b"ABCD" + dma = DmaTranslator(ram=ram, ram_base=0, ram_size=len(ram), + windows=[{"pa": 0x1000, "sim": 0, "size": 0x100}]) + # Destination outside every window: refused, not a transfer. + self.assertFalse(dma.copy(0x1000, 0x2000, 4)) + self.assertEqual(dma.transfers, 0) + self.assertEqual(dma.refused, 1) + # Both mapped: one completed transfer. + self.assertTrue(dma.copy(0x1000, 0x1004, 4)) + self.assertEqual(dma.transfers, 1) + self.assertEqual(ram[0x4:0x8], b"ABCD") + state = dma.state() + self.assertIn("events", state) + self.assertEqual(state["events"][0]["kind"], "w") + self.assertEqual(len(state["events"]), 1) + + +class MachineDeviceIntegrationTests(unittest.TestCase): + def _machine(self): + reg = DeviceRegistry() + reg.add(PmcBlock(base=0x2000000000000000, size=0x1000)) + m = Machine(memory_mb=1, ram_base=0, unknown_mmio="lenient", devices=reg) + return m, reg + + def test_modeled_device_takes_precedence_over_lenient(self): + m, reg = self._machine() + self.assertEqual(m._mmio_load(0x2000000000000A00, 4), PmcBlock().boot_42) + self.assertEqual(m.mmio_unknown, 0) + self.assertEqual(len(reg.accesses), 1) + + def test_unknown_inside_window_is_logged(self): + m, reg = self._machine() + self.assertEqual(m._mmio_load(0x2000000000000A04, 4), 0) + self.assertEqual(m.mmio_unknown, 1) + self.assertFalse(reg.accesses[0]["handled"]) + + def test_debugger_peek_and_device_state(self): + m, reg = self._machine() + self.assertEqual(m.read_mem(0x2000000000000A00, 4), + PmcBlock().boot_42.to_bytes(4, "little")) + self.assertIsInstance(m.device_state(), dict) + self.assertEqual(m.device_state()["schema"], "greeneye-sim-devices/1") + + def test_reset_regs_names(self): + m = Machine(memory_mb=1, ram_base=0, reset_regs={"a0": 0x1234, "sp": 0x8000}) + self.assertEqual(m.x[10], 0x1234) + self.assertEqual(m.x[2], 0x8000) + + +@unittest.skipUnless(os.path.isfile(DEFAULT_IMAGE), "real GSP firmware absent") +class BootLadderTests(unittest.TestCase): + """Bounded real-firmware runs: observed rungs only.""" + + def test_profile_loads_and_builds_windows(self): + profile = load_profile(DEFAULT_PROFILE) + self.assertEqual(profile["schema"], "greeneye-sim-profile/1") + m = Machine(memory_mb=16, ram_base=0, unknown_mmio="lenient") + reg = registry_from_profile(profile, machine=m) + names = {d.name for d in reg.devices} + self.assertIn("pmc", names) + self.assertIn("falcon", names) + self.assertIn("priscv", names) + self.assertIsNotNone(reg.dma) + + def test_no_shim_b1_device_model_serves_boot(self): + from sim.gsp_boot import run_boot + r = run_boot(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=False) + self.assertTrue(r.ran) + self.assertEqual(r.rung, "B1") + self.assertIn("bootargs_consumer", r.milestones) + self.assertEqual(r.mmio_unknown, 0) + self.assertEqual(r.device_unhandled, 0) + + def test_shim_reaches_relocation_b2(self): + from sim.gsp_boot import run_boot + r = run_boot(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=True) + self.assertTrue(r.ran) + self.assertEqual(r.rung, "B2") + self.assertIn("relocation_copy", r.milestones) + self.assertEqual(r.mmio_unknown, 0) + # Known unknown: the DMEM runspace content the app relocates from is + # not reconstructed; the run stops there rather than faking it. + self.assertEqual(r.stop_reason, "fault") + self.assertEqual(r.stop_pc, 0x400EA0C) + + def test_two_runs_identical(self): + from sim.gsp_boot import run_boot + kwargs = dict(image=DEFAULT_IMAGE, profile_path=DEFAULT_PROFILE, + instructions=20_000, record=False, use_shim=True) + a = run_boot(**kwargs) + b = run_boot(**kwargs) + self.assertEqual(a.instructions, b.instructions) + self.assertEqual(a.stop_pc, b.stop_pc) + self.assertEqual(a.stop_reason, b.stop_reason) + self.assertEqual(a.device_state_sha256, b.device_state_sha256) + self.assertEqual(a.registers, b.registers) + + +if __name__ == "__main__": + unittest.main() |
