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}