TinyEMU Data FlowCPU and memory

CPU accesses split into RAM and MMIO

The CPU interpreter fetches and executes guest instructions. Every slow path translates the guest virtual address, finds the physical memory range, and either touches host RAM directly or calls a device callback.

Physical memory map is a range tableRAM and devices share one lookup mechanism.

`PhysMemoryMap` stores `PhysMemoryRange` entries. RAM ranges carry `phys_mem` and optional dirty bits; device ranges carry read/write function pointers and an opaque state pointer.

get_phys_mem_range(map, paddr)
  for each range:
    if paddr in [addr, addr + size):
      return range

range.is_ram ? use range.phys_mem : call range.read_func/write_func
Virtual addresses are translated before lookup`get_phys_addr()` handles privilege, XLEN truncation, SATP modes, PTE permissions, and accessed/dirty updates.
target virtual address
  -> select effective privilege
  -> machine mode: direct/truncated physical address
  -> supervisor/user: walk page tables from satp
  -> validate V/U/X/W/R/A/D bits
  -> set A/D bits if needed
  -> produce physical address or pending page fault
Data reads and writes populate TLBs on RAM hitsSlow paths turn future aligned RAM accesses into direct pointer math.

For RAM, the slow read/write path stores a page-aligned virtual tag and a `mem_addend` so later helper calls can compute the host address quickly. Writes mark dirty bits for framebuffer and other RAM observers.

target_read_slow()
  translate virtual -> physical
  range = get_phys_mem_range()
  if RAM:
    tlb_read[index] = { vaddr page, host_ptr - vaddr }
    read scalar from host memory
  else:
    read_func(opaque, offset, size_log2)

target_write_slow()
  same translation
  if RAM: dirty bit + tlb_write + store scalar
  else: write_func(opaque, offset, value, size_log2)
Instruction fetch is RAM-onlyCode fetches translate with execute permission and fault if the result is not mapped RAM.

`target_read_insn_slow()` rejects missing ranges and device ranges because TinyEMU fetches executable code only from RAM. It fills `tlb_code` separately from data TLBs.

fetch PC
  -> get_phys_addr(..., ACCESS_CODE)
  -> range lookup
  -> must be RAM
  -> tlb_code[index] = host pointer addend
  -> interpreter decodes 16-bit or 32-bit instruction
MMIO calls device-specific callbacksThe CPU does not know CLINT, PLIC, HTIF, VirtIO, framebuffer, or PCI details.

Device registration binds an address range to functions. That makes the CPU memory path generic: once a physical address lands in a device range, the registered callback receives only offset, value, size, and opaque state.

cpu_register_device(map, base, size, opaque, read, write, flags)

guest store to base + offset
  -> target_write_slow()
  -> range is device
  -> write(opaque, offset, value, size_log2)