Total points: 30
Estimated time: 5 hours
Prerequisites: Labs 1-2 complete; full RV32I + privilege modes working; Verilator sim environment
Overview
This lab integrates the Sv32 MMU from the reference implementation (mmu.v) into your CPU, sets up a working page table, and demonstrates a demand-paging page-fault handler. By the end, user-mode code runs in a paged virtual address space and faults into the OS page-fault handler on unmapped accesses.
Part A: Integrate mmu.v (8 pts)
A1: Add mmu.v to your project (3 pts)
Copy ../virtus-academy-support/vca-csa-201/hdl/core/mmu.v into your hdl/core/ directory. Wire it into cpu.v between the fetch/data virtual address outputs and the memory bus:
- Fetch-side:
fetch_vaddr(from PC) → mmu.vfetch_vaddr→ mmu.vfetch_paddr→instr_mem.addr - Data-side:
data_vaddr(from ALU) → mmu.vdata_vaddr→ mmu.vdata_paddr→data_mem.addr
Connect satp from csrfile output to mmu.v input. Connect priv from the CPU privilege register to mmu.v input. Connect fetch_fault and data_fault from mmu.v outputs to the trap logic (they should raise mcause = 12/13/15 in the trap handler).
A2: Bare-mode regression (3 pts)
In bare mode (satp.MODE = 0), mmu.v passes addresses through unchanged (paddr = vaddr). Verify by running all prior test suites:
make rv32ui && make rv32um && make rv32mi
All tests must still pass with mmu.v integrated and satp.MODE=0.
A3: SFENCE.VMA (2 pts)
Add SFENCE.VMA to the decoder. It takes the form FENCE with funct3=1 in the privileged encoding. When decoded, assert mmu.tlb_flush for one cycle. Verify: after writing a new PTE and executing SFENCE.VMA, the TLB is flushed and the next access uses the new translation.
Unit test: (1) map VA 0x1000 → PA 0x80001000; (2) access VA 0x1000 (TLB fills); (3) remap VA 0x1000 → PA 0x80002000; (4) execute SFENCE.VMA; (5) access VA 0x1000 again -- confirm the new PA is used (write to PA 0x80002000, not 0x80001000).
Part B: Set up a page table (10 pts)
B1: Kernel page table (4 pts)
Write a minimal kernel page table in assembly that maps:
- VA 0x00000000-0x00FFFFFF → PA 0x00000000-0x00FFFFFF (identity map; kernel text + data)
- VA 0x00001000 → PA 0x80001000 (one user code page, for the test program in B2)
- VA 0x00002000 → PA 0x80002000 (one user stack page)
The page table must be a valid Sv32 structure: one root page directory and two second-level page tables (one for VPN[1]=0 which covers all low addresses; you only need to populate the VPN[0]=1 and VPN[0]=2 entries).
Set satp in the boot stub: satp = (1 << 31) | (root_page_table_ppn). Switch to S-mode using MRET with mstatus.MPP=1 (if you have S-mode); otherwise, stay in M-mode with satp set.
B2: Mapped access succeeds (3 pts)
Write a user-mode test program at VA 0x00001000 (physically at 0x80001000) that:
- Reads a word from VA 0x00002000 (stack page, mapped).
- Writes a word to VA 0x00002004 (stack page, mapped).
- Calls ECALL to signal completion.
Run under Verilator. Verify: the fetch from 0x00001000 succeeds (PA 0x80001000 reaches instr_mem); the load from 0x00002000 succeeds (PA 0x80002000); the store to 0x00002004 succeeds.
B3: Unmapped access faults correctly (3 pts)
Add a second test: the user program accesses VA 0x00003000 (not in the page table). Verify:
- For a load: mcause = 13 (load page fault)
- For an instruction fetch from 0x00003000: mcause = 12 (instruction page fault)
- For a store to 0x00003000: mcause = 15 (store/AMO page fault)
Verify all three fault codes fire on the correct access type.
Part C: Demand-paging page-fault handler (12 pts)
C1: Physical page allocator (3 pts)
Write a minimal physical page allocator in C. The allocator manages a 16-page free list (each page = 4 KiB). alloc_page() returns the physical base address of a free page; free_page(pa) returns a page to the free list. Initialize the free list to physical addresses 0x80003000 through 0x80013000 (16 pages × 4 KiB).
C2: Page-fault handler (6 pts)
In the M-mode trap handler (extending Module 2's trap_dispatch), add a case for mcause = 13 (load page fault):
- Read mtval (the faulting virtual address).
- Compute the page base:
virt_page = faulting_va & ~0xFFF. - Call
alloc_page()to get a physical page. - Walk the page table to find the L0 PTE for
virt_page:- Use
satp.PPNto find the L1 page directory. - Compute VPN[1] and VPN[0] from
virt_page. - If the L1 PTE is invalid, allocate a new L0 page table, install the L1 PTE.
- Write the L0 PTE:
(new_pa >> 12) << 10 | PTE_V | PTE_R | PTE_W | PTE_U | PTE_A | PTE_D.
- Use
- Execute SFENCE.VMA (via inline assembly or the csrw path).
- Return via MRET to the faulting instruction.
The faulting instruction will re-execute after MRET. Now that the page is mapped, it will succeed.
C3: End-to-end test (3 pts)
Write a user-mode test program that:
- Deliberately accesses 4 different unmapped VA pages (VA 0x00003000, 0x00004000, 0x00005000, 0x00006000).
- Writes a known pattern to each page after the page-fault handler maps it.
- Reads the patterns back and verifies they are correct.
- Calls ECALL SYS_WRITE to report: "demand-paged 4 pages, all data intact."
Grading
| Part | Criteria | Points |
|---|---|---|
| A1 | mmu.v integrated; fetch and data paths both connected | 3 |
| A2 | All prior test suites pass with mmu.v in bare mode | 3 |
| A3 | SFENCE.VMA decoded; TLB flush verified with the remap test | 2 |
| B1 | Valid Sv32 page table; satp configured | 4 |
| B2 | Mapped load/store/fetch all succeed; traces confirm PA | 3 |
| B3 | All three fault codes fire on correct access type | 3 |
| C1 | Physical page allocator correct; free list manages 16 pages | 3 |
| C2 | Page-fault handler maps page + SFENCE.VMA + MRET | 6 |
| C3 | 4-page demand-paging test passes; data integrity verified | 3 |
| Total | 30 |