code wiki / _hdl_build / rv64im_min_mmu.nx

rv64im_min_mmu.nx source

↩ module page · 176 lines · 8859 B

1// rv64im_min_mmu.nx -- the Sv39 PAGE-TABLE-WALK device (virtual-memory-paging-mmu). 2// 3// The genuine hard core of paging: a SOVEREIGN Sv39 multi-level address-translation datapath. On 4// a doorbell the device reads satp (root page-table PPN + MODE) and a virtual address, WALKS the 5// 3-level Sv39 page table in guest RAM (level 2->1->0, checking each PTE's Valid bit and detecting 6// a leaf via the R/W/X bits, assembling the physical address with the right page size for the leaf 7// level -- gigapage/megapage/4K), and latches the translated PADDR + a FAULT flag (set on an 8// invalid PTE or a walk that reaches no leaf). This is the MMU's translation algorithm, exercised 9// end-to-end by a driver (emitted by nx_drv_proto_emit) -- the same MMIO+doorbell pattern as the 10// virtio/nndev devices. Wiring translation transparently into the CPU's load/store datapath + 11// privilege modes + page-fault traps is the documented follow-on rung (this proves the WALK). 12// 13// Register window @ NX_MMU_BASE (256 bytes), independent instance (other devices untouched): 14// 0x00 SATP_LO (WO) low 32 bits of satp (root PPN) 15// 0x04 SATP_HI (WO) high 32 bits of satp (MODE in bits 31:28 -> satp bits 63:60; 8 = Sv39) 16// 0x08 VADDR (WO) the (low 32 of the) virtual address to translate 17// 0x0C DOORBELL (WO) write 1 -> walk the page table in guest RAM, latch PADDR + FAULT 18// 0x10 PADDR (RO) translated physical address (low 32) from the last walk 19// 0x14 FAULT (RO) 0 = translated OK; 1 = page fault (invalid PTE / no leaf) 20// 21// Status: SEED 2026-06-13 (virtual-memory-paging-mmu, Sv39 walk). license_tier: ORIGINAL 22import "nx_syscalls.nx" 23import "nishi_hdl_primitives.nx" 24 25const NX_MMU_BASE: i64 = 0x10006000 26const NX_MMU_END: i64 = 0x10006100 27 28const NX_MMU_OFF_SATPLO: i64 = 0x00 29const NX_MMU_OFF_SATPHI: i64 = 0x04 30const NX_MMU_OFF_VADDR: i64 = 0x08 31const NX_MMU_OFF_DOORBELL: i64 = 0x0C 32const NX_MMU_OFF_PADDR: i64 = 0x10 33const NX_MMU_OFF_FAULT: i64 = 0x14 34 35const NX_MMU_SLOT_SATPLO: i64 = 0 36const NX_MMU_SLOT_SATPHI: i64 = 1 37const NX_MMU_SLOT_VADDR: i64 = 2 38const NX_MMU_SLOT_PADDR: i64 = 3 39const NX_MMU_SLOT_FAULT: i64 = 4 40const NX_MMU_SLOT_N: i64 = 5 41 42const NX_MMU_OK: i64 = 0 43const NX_MMU_ADDR_OUT_OF_RANGE: i64 = 1 44 45// Sv39: 3 levels, 9-bit VPN each, 12-bit page offset. satp.MODE field (bits 63:60) == 8 = Sv39. 46const NX_SV39_MODE: i64 = 8 47const NX_PTE_V: i64 = 1 // Valid 48const NX_PTE_RWX: i64 = 0xE // R|W|X (bits 3:1); any set => leaf 49const NX_PPN_MASK: i64 = 0xFFFFFFFFFFF // 44-bit PPN 50 51struct NxMmu { 52 storage: *i64 // NX_MMU_SLOT_N i64s 53 valid: i64 54 base: i64 55} 56 57func nx_mmu_reset(storage: *i64) -> i64 { 58 storage[NX_MMU_SLOT_SATPLO] = 0 59 storage[NX_MMU_SLOT_SATPHI] = 0 60 storage[NX_MMU_SLOT_VADDR] = 0 61 storage[NX_MMU_SLOT_PADDR] = 0 62 storage[NX_MMU_SLOT_FAULT] = 0 63 return NX_MMU_OK 64} 65 66func nx_mmu_init(d: *NxMmu, storage: *i64) -> i64 { 67 if (d as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 68 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 69 d.storage = storage 70 d.valid = 1 71 d.base = NX_MMU_BASE 72 nx_mmu_reset(storage) 73 return NX_MMU_OK 74} 75 76func nx_mmu_in_range(d: *NxMmu, addr: i64) -> i64 { 77 if addr < d.base { return 0 } 78 if addr >= d.base + 0x100 { return 0 } 79 return 1 80} 81 82func nx_mmu_read32(d: *NxMmu, addr: i64, value_out: *i64) -> i64 { 83 if d.valid != 1 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 84 if (value_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND } 85 if nx_mmu_in_range(d, addr) != 1 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 86 let off: i64 = addr - d.base 87 if off == NX_MMU_OFF_PADDR { value_out[0] = d.storage[NX_MMU_SLOT_PADDR]; return NX_MMU_OK } 88 if off == NX_MMU_OFF_FAULT { value_out[0] = d.storage[NX_MMU_SLOT_FAULT]; return NX_MMU_OK } 89 value_out[0] = 0 90 return NX_MMU_OK 91} 92 93func nx_mmu_write32(d: *NxMmu, addr: i64, value: i64) -> i64 { 94 if d.valid != 1 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 95 if nx_mmu_in_range(d, addr) != 1 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 96 let off: i64 = addr - d.base 97 if off == NX_MMU_OFF_SATPLO { d.storage[NX_MMU_SLOT_SATPLO] = value & 0xffffffff; return NX_MMU_OK } 98 if off == NX_MMU_OFF_SATPHI { d.storage[NX_MMU_SLOT_SATPHI] = value & 0xffffffff; return NX_MMU_OK } 99 if off == NX_MMU_OFF_VADDR { d.storage[NX_MMU_SLOT_VADDR] = value & 0xffffffff; return NX_MMU_OK } 100 if off == NX_MMU_OFF_DOORBELL { return NX_MMU_OK } // walk run by the sim's store32 dispatch 101 return NX_MMU_OK 102} 103 104// little-endian 64-bit read of a PTE from guest RAM (bounds-checked by caller). 105func nx_mmu_rd64(mem_buf: *u8, off: i64) -> i64 { 106 var v: i64 = 0 107 var i: i64 = 0 108 while i < 8 { v = v | ((mem_buf[off + i] as i64) << (i * 8)); i = i + 1 } 109 return v 110} 111// little-endian 64-bit write into guest RAM -- used to write back a PTE's Accessed/Dirty bits. 112func nx_mmu_wr64(mem_buf: *u8, off: i64, v: i64) -> i64 { 113 var i: i64 = 0 114 while i < 8 { mem_buf[off + i] = ((v >> (i * 8)) & 0xff) as u8; i = i + 1 } 115 return 0 116} 117func nx_mmu_inrange(off: i64, w: i64, mem_size: i64) -> i64 { if off < 0 { return 0 } if off > mem_size - w { return 0 } return 1 } 118 119// ===== DOORBELL: perform the Sv39 walk ================================================= 120// satp = (SATP_HI<<32)|SATP_LO. If MODE != Sv39, fault. Else walk level 2->1->0: 121// pte_addr = (table_ppn<<12) + VPN[level]*8 ; pte = rd64(pte_addr) 122// if !V -> fault ; if leaf (R/W/X) -> assemble PA with page size for this level ; else descend. 123// PA (leaf at level L): mask=(1<<(12+9*L))-1 ; PA = ((pte.ppn<<12) & ~mask) | (vaddr & mask). 124// SHARED Sv39 walk: translate vaddr via the page table in guest RAM under satp. Returns the 125// physical address; sets fault_out[0]=1 on any fault (wrong MODE / invalid PTE / no leaf / out of 126// range), else 0. Used BOTH by the MMU walk device (doorbell) AND by the CPU's load/store datapath 127// (nx_rv64im_xlate in the sim) -- one walk implementation, DRY. 128func nx_sv39_walk(satp: i64, mem_buf: *u8, mem_base: i64, mem_size: i64, vaddr: i64, fault_out: *i64, perm_out: *i64, pte_addr_out: *i64) -> i64 { 129 fault_out[0] = 0 130 perm_out[0] = 0 // leaf PTE perm bits (V|R|W|X); the CPU enforces X/R/W per access type 131 pte_addr_out[0] = 0 // physical address of the leaf PTE (so the CPU can write back A/D bits) 132 let mode: i64 = (satp >> 60) & 0xf 133 if mode != NX_SV39_MODE { fault_out[0] = 1; return 0 } 134 var table_ppn: i64 = satp & NX_PPN_MASK 135 var level: i64 = 2 136 var done: i64 = 0 137 var pa: i64 = 0 138 while done == 0 { 139 let vpn: i64 = (vaddr >> (12 + 9 * level)) & 0x1ff 140 let pte_phys: i64 = (table_ppn << 12) + (vpn * 8) 141 let pte_off: i64 = pte_phys - mem_base 142 if nx_mmu_inrange(pte_off, 8, mem_size) != 1 { fault_out[0] = 1; done = 1 } else { 143 let pte: i64 = nx_mmu_rd64(mem_buf, pte_off) 144 if (pte & NX_PTE_V) == 0 { fault_out[0] = 1; done = 1 } else { 145 if (pte & NX_PTE_RWX) != 0 { 146 let ppn: i64 = (pte >> 10) & NX_PPN_MASK 147 let pbits: i64 = 12 + 9 * level 148 let mask: i64 = (1 << pbits) - 1 149 pa = ((ppn << 12) & (0 - 1 - mask)) | (vaddr & mask) 150 perm_out[0] = pte & 0xff // expose V|R|W|X|U|G|A|D for per-access + user/supervisor enforcement 151 pte_addr_out[0] = pte_phys // the leaf PTE's physical address (A/D write-back target) 152 done = 1 153 } else { 154 table_ppn = (pte >> 10) & NX_PPN_MASK 155 level = level - 1 156 if level < 0 { fault_out[0] = 1; done = 1 } 157 } 158 } 159 } 160 } 161 return pa 162} 163 164func nx_mmu_doorbell_walk(d: *NxMmu, mem_buf: *u8, mem_base: i64, mem_size: i64) -> i64 { 165 if d.valid != 1 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 166 if (mem_buf as i64) == 0 { return 0 - NX_MMU_ADDR_OUT_OF_RANGE } 167 let satp: i64 = (d.storage[NX_MMU_SLOT_SATPHI] << 32) | (d.storage[NX_MMU_SLOT_SATPLO] & 0xffffffff) 168 let vaddr: i64 = d.storage[NX_MMU_SLOT_VADDR] & 0xffffffff 169 let fb: *i64 = sys_mmap(16) as *i64 170 let pmb: *i64 = sys_mmap(16) as *i64 // perm bits ignored by the device walk (the CPU enforces, not the MMU probe) 171 let pab: *i64 = sys_mmap(16) as *i64 // PTE-addr ignored by the device walk (no A/D write-back on the probe) 172 let pa: i64 = nx_sv39_walk(satp, mem_buf, mem_base, mem_size, vaddr, fb, pmb, pab) 173 if fb[0] == 1 { d.storage[NX_MMU_SLOT_FAULT] = 1; d.storage[NX_MMU_SLOT_PADDR] = 0 } 174 else { d.storage[NX_MMU_SLOT_FAULT] = 0; d.storage[NX_MMU_SLOT_PADDR] = pa & 0xffffffff } 175 return NX_MMU_OK 176}