summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/tests/test_mpu.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
commit1f2073d4fbb1d412d9bbf373312e3c51b83db92a (patch)
treea661537d7ec9510324e0a40a5a65e4b8c853f01b /debug/risc-v-gsp-rm-vm/tests/test_mpu.py
parentc1adf2727983f5cace8ca0f5d1119ac2256c348e (diff)
downloadopen-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.tar.gz
open-iov-1f2073d4fbb1d412d9bbf373312e3c51b83db92a.zip
greeneye: GMMU/FBIF observability, PTE decoder and oracle, MPU/libos simulator wave
Simulator development wave (docs 42-44): GMMU page-table entry decode with its oracle/workflow harness, FBIF observability, and MPU/LIBOS device surfaces in the device model, plus refreshed boot/machine/profile data (tu104-570.144), the qemu-line results, and the new unit tests. Docs 00/06/35-41 updated to match; harness helpers and .gitignore included.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/tests/test_mpu.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_mpu.py177
1 files changed, 177 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_mpu.py b/debug/risc-v-gsp-rm-vm/tests/test_mpu.py
new file mode 100644
index 0000000..73ac81f
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/tests/test_mpu.py
@@ -0,0 +1,177 @@
+"""MMU (NVMPU) model tests: CSR semantics, translation, machine integration.
+
+The model is exercised directly and through the machine's guest paths; the
+real-firmware relocation-copy success is covered by
+``tests/test_sim_devices.py`` (boot ladder). All fixtures are synthetic; no
+hardware, module, guest or network is touched.
+"""
+from __future__ import annotations
+
+import unittest
+
+from sim.machine import Machine, R_EBREAK
+from sim.mpu import (
+ MSTATUS_VM_NVMPU,
+ MMPUATTR,
+ MMPUIDX,
+ MMPUPA,
+ MMPURNG,
+ MMPUVA,
+ RiscvMpu,
+)
+
+KB = 1024
+
+
+def enable(mpu: RiscvMpu) -> None:
+ mpu.on_csr_write(0x300, MSTATUS_VM_NVMPU << 24)
+
+
+def entry(mpu: RiscvMpu, index: int, va: int, pa: int, rng: int, attr: int = 0x600) -> None:
+ mpu.on_csr_write(MMPUIDX, index)
+ mpu.on_csr_write(MMPUPA, pa)
+ mpu.on_csr_write(MMPURNG, rng)
+ mpu.on_csr_write(MMPUATTR, attr)
+ mpu.on_csr_write(MMPUVA, va | 1) # VLD
+
+
+class RiscvMpuTests(unittest.TestCase):
+ def test_disabled_does_not_translate(self):
+ mpu = RiscvMpu()
+ entry(mpu, 0, 0x80000000, 0x1000, 0x1000)
+ self.assertFalse(mpu.enabled)
+ self.assertIsNone(mpu.translate(0x80000000))
+
+ def test_enable_is_the_nvmpu_vm_mode(self):
+ mpu = RiscvMpu()
+ entry(mpu, 0, 0x80000000, 0x1000, 0x1000)
+ mpu.on_csr_write(0x300, 0x1E000000)
+ self.assertTrue(mpu.enabled)
+ # Bare mode disables translation again.
+ mpu.on_csr_write(0x300, 0)
+ self.assertFalse(mpu.enabled)
+ self.assertIsNone(mpu.translate(0x80000000))
+
+ def test_translate_range_and_offset(self):
+ mpu = RiscvMpu()
+ entry(mpu, 2, 0x400E000, 0x1000, 0x3000)
+ enable(mpu)
+ self.assertEqual(mpu.translate(0x400E000), 0x1000)
+ self.assertEqual(mpu.translate(0x400E800), 0x1800)
+ self.assertEqual(mpu.translate(0x4010FFF), 0x3FFF)
+ self.assertIsNone(mpu.translate(0x400DFFF))
+ self.assertIsNone(mpu.translate(0x4011000))
+
+ def test_lowest_index_precedence(self):
+ mpu = RiscvMpu()
+ entry(mpu, 3, 0x1000, 0x9000, 0x1000)
+ entry(mpu, 0, 0x1000, 0x5000, 0x1000)
+ entry(mpu, 1, 0x1000, 0x7000, 0x1000)
+ enable(mpu)
+ self.assertEqual(mpu.translate(0x1000), 0x5000)
+
+ def test_vld_clear_disables_entry(self):
+ mpu = RiscvMpu()
+ entry(mpu, 0, 0x80000000, 0x1000, 0x1000)
+ enable(mpu)
+ self.assertIsNotNone(mpu.translate(0x80000000))
+ mpu.on_csr_write(MMPUIDX, 0)
+ mpu.on_csr_write(MMPUVA, 0) # VLD clear
+ self.assertIsNone(mpu.translate(0x80000000))
+
+ def test_index_field_is_six_bits(self):
+ mpu = RiscvMpu()
+ mpu.on_csr_write(MMPUIDX, 0x140) # 0x40 | 0x00 -> index 0
+ entry(mpu, 0, 0x1000, 0x2000, 0x1000)
+ enable(mpu)
+ self.assertEqual(mpu.translate(0x1000), 0x2000)
+
+ def test_state_and_reset(self):
+ mpu = RiscvMpu()
+ entry(mpu, 0, 0x80000000, 0x1000, 0x1000)
+ enable(mpu)
+ state = mpu.state()
+ self.assertTrue(state["enabled"])
+ self.assertEqual(state["entries"][0]["index"], 0)
+ self.assertEqual(state["entries"][0]["pa"], 0x1000)
+ mpu.reset()
+ self.assertFalse(mpu.enabled)
+ self.assertIsNone(mpu.translate(0x80000000))
+
+
+class MachineMpuTests(unittest.TestCase):
+ def test_guest_load_store_through_mapping(self):
+ m = Machine(memory_mb=1, ram_base=0, unknown_mmio="fault")
+ m.write_mem(0x1000, b"\x11\x22\x33\x44\x55\x66\x77\x88")
+ entry(m.mpu, 0, 0x80000000, 0x1000, 0x1000)
+ enable(m.mpu)
+ self.assertEqual(m._guest_load(0x80000000, 8),
+ int.from_bytes(b"\x11\x22\x33\x44\x55\x66\x77\x88", "little"))
+ m._guest_store(0x80000004, 8, 0xAABBCCDD)
+ self.assertEqual(m.read_mem(0x1004, 4), b"\xdd\xcc\xbb\xaa")
+ # The debugger surface stays raw/strict: the mapped VA has no backing.
+ with self.assertRaises(Exception):
+ m.read_mem(0x80000000, 4)
+
+ def test_instruction_fetch_is_translated(self):
+ m = Machine(memory_mb=1, ram_base=0, unknown_mmio="fault")
+ program = bytes.fromhex("13057000" "73001000") # addi a0,zero,7; ebreak
+ m.write_mem(0x2000, program)
+ entry(m.mpu, 0, 0x80002000, 0x2000, 0x1000)
+ enable(m.mpu)
+ m.pc = 0x80002000 # virtual entry; the backing lives at PA 0x2000
+ reason, pc, _, _ = m.run(10)
+ self.assertEqual(reason, R_EBREAK)
+ self.assertEqual(m.x[10], 7)
+ self.assertEqual(pc, 0x80002004)
+
+ def test_unmapped_fetch_still_faults(self):
+ m = Machine(memory_mb=1, ram_base=0, unknown_mmio="fault")
+ entry(m.mpu, 0, 0x80002000, 0x2000, 0x1000)
+ enable(m.mpu)
+ m.pc = 0x90000000 # no entry covers it, no backing RAM
+ reason, _, _, _ = m.run(1)
+ self.assertNotEqual(reason, R_EBREAK)
+
+
+class Wpr2RelativeOffsetTests(unittest.TestCase):
+ def test_window_relative_offsets_read_the_same_pair(self):
+ from sim.devices.fwsec import Wpr2Block
+
+ dev = Wpr2Block()
+ dev.set_range(0x2000000, 0x6000000)
+ self.assertEqual(dev.read(0x824, 4), dev.read(0x1FA824, 4))
+ self.assertEqual(dev.read(0x828, 4), dev.read(0x1FA828, 4))
+ self.assertIsNone(dev.read(0x820, 4))
+
+
+class BootShimArgsTests(unittest.TestCase):
+ def test_bootstrap_args_written_and_image_mirror_absent(self):
+ from sim.gsp_boot import _apply_boot_shim, DEFAULT_IMAGE, DEFAULT_PROFILE
+ from greeneye.elf import load_elf
+ from sim.devices import load_profile
+
+ profile = load_profile(DEFAULT_PROFILE)
+ m = Machine(memory_mb=16, ram_base=0, unknown_mmio="lenient")
+ shim = dict(profile.get("boot_shim", {}))
+ shim["_image_path"] = DEFAULT_IMAGE
+ meta = {"enabled": True, "assumptions": []}
+ _apply_boot_shim(m, shim, load_elf(DEFAULT_IMAGE), meta)
+ args = profile["boot_shim"]["bootstrap_args"]
+ pa = int(args["pa"], 0)
+ import struct
+
+ fields = struct.unpack("<6Q", m.read_mem(pa, 48))
+ self.assertEqual(fields[0], int(args["imem_base_pa"], 0))
+ self.assertEqual(fields[1], int(args["imem_size"], 0))
+ self.assertEqual(fields[2], int(args["imem_bs_size"], 0))
+ self.assertEqual(fields[3], int(args["dmem_base_pa"], 0))
+ self.assertEqual(fields[4], int(args["dmem_size"], 0))
+ self.assertEqual(fields[5], int(args["dmem_bs_size"], 0))
+ # The DMEM image mirror hack is gone from this profile.
+ self.assertNotIn("image_at", shim)
+ self.assertNotIn("image_at", meta)
+
+
+if __name__ == "__main__":
+ unittest.main()