CPU / Interpreter

RISC-V interpreter loop, TLB, CSR, and exception handling

Data Flow

graph TD A[virt_machine_interp] --> B[riscv_machine_interp] B --> C[riscv_cpu_interp] C --> D[Instruction Fetch via TLB] D --> E[Decode opcode / rd / rs1 / rs2] E --> F{Instruction type} F -->|ALU| G[Register ops] F -->|Load/Store| H[TLB read/write] F -->|Branch| I[PC update] F -->|System| J[CSR / ECALL / EBREAK] H --> K[target_read_slow / target_write_slow] K --> L[MMU walk if TLB miss] J --> M[raise_interrupt / raise_exception] M --> N[PLIC / CLINT mip/mie]

CPU State

RISCVCPUState (defined in riscv_cpu_priv.h) holds all CPU context: integer registers reg[32], floating-point registers fp_reg[32], program counter pc, privilege level priv, CSRs, and three TLBs (read, write, code).

struct RISCVCPUState {
    RISCVCPUCommonState common;
    target_ulong pc;
    target_ulong reg[32];
#if FLEN > 0
    fp_uint fp_reg[32];
    uint32_t fflags;
    uint8_t frm;
#endif
    uint8_t cur_xlen;
    uint8_t priv;
    uint8_t fs;
    uint8_t mxl;
    /* CSRs */
    target_ulong mstatus, mtvec, mepc, mcause, mtval, mhartid;
    uint32_t misa, mie, mip, medeleg, mideleg, mcounteren;
    target_ulong stvec, sscratch, sepc, scause, stval;
    target_ulong satp;
    /* TLBs */
    TLBEntry tlb_read[TLB_SIZE];
    TLBEntry tlb_write[TLB_SIZE];
    TLBEntry tlb_code[TLB_SIZE];
    PhysMemoryMap *mem_map;
};

Interpreter Loop

The interpreter is generated from riscv_cpu_template.h by compiling riscv_cpu.c three times with MAX_XLEN=32/64/128. The core function is riscv_cpu_interp_x<XLEN>.

static void no_inline glue(riscv_cpu_interp_x, XLEN)(RISCVCPUState *s,
                                                   int n_cycles1)
{
    uint32_t opcode, insn, rd, rs1, rs2, funct3;
    int32_t imm, cond, err;
    target_ulong addr, val, val2;
    uint64_t insn_counter_addend;

    if (n_cycles1 == 0) return;
    insn_counter_addend = s->insn_counter + n_cycles1;
    s->n_cycles = n_cycles1;

    /* check pending interrupts */
    if (unlikely((s->mip & s->mie) != 0)) {
        if (raise_interrupt(s)) {
            s->n_cycles--;
            goto done_interp;
        }
    }

    s->pending_exception = -1;
    code_ptr = NULL;
    code_end = NULL;
    code_to_pc_addend = s->pc;

    for(;;) {
        if (unlikely(code_ptr >= code_end)) {
            /* TLB lookup for code fetch */
            addr = s->pc;
            tlb_idx = (addr >> PG_SHIFT) & (TLB_SIZE - 1);
            if (likely(s->tlb_code[tlb_idx].vaddr == (addr & ~PG_MASK))) {
                ptr = (uint8_t *)(s->tlb_code[tlb_idx].mem_addend +
                                  (uintptr_t)addr);
            } else {
                if (unlikely(target_read_insn_slow(s, &ptr, addr)))
                    goto mmu_exception;
            }
            code_ptr = ptr;
            code_end = ptr + (PG_MASK - 1 - (addr & PG_MASK));
            code_to_pc_addend = addr - (uintptr_t)code_ptr;
            insn = get_insn32(code_ptr);
        } else {
            insn = get_insn32(code_ptr);
        }
        s->n_cycles--;
        opcode = insn & 0x7f;
        rd = (insn >> 7) & 0x1f;
        rs1 = (insn >> 15) & 0x1f;
        rs2 = (insn >> 20) & 0x1f;
        switch(opcode) {
            /* ... opcode handlers ... */
        }
    }
}

TLB Fast Path

Memory accesses use inlined TLB checks. If the virtual page matches, the access is a direct pointer dereference. On miss, target_read_slow or target_write_slow performs the page-table walk.

static inline __exception int target_read_u32(RISCVCPUState *s,
                                              uint32_t *pval,
                                              target_ulong addr)
{
    uint32_t tlb_idx = (addr >> PG_SHIFT) & (TLB_SIZE - 1);
    if (likely(s->tlb_read[tlb_idx].vaddr ==
               (addr & ~(PG_MASK & ~3)))) {
        *pval = *(uint32_t *)(s->tlb_read[tlb_idx].mem_addend +
                              (uintptr_t)addr);
    } else {
        mem_uint_t val;
        int ret = target_read_slow(s, &val, addr, 2);
        if (ret) return ret;
        *pval = val;
    }
    return 0;
}

Interrupts & Exceptions

The CPU checks mip & mie between instruction blocks. If an interrupt is pending and enabled, raise_interrupt() updates mcause, saves pc to mepc, and jumps to mtvec. Exceptions (page faults, illegal instructions) follow a similar path.

Floating Point (SoftFP)

TinyEMU uses its own SoftFP library (softfp.c/h) for bit-exact 32/64/128-bit floating-point emulation. The interpreter calls functions like add_sf64, mul_sf128, etc., updating fflags for rounding/exception status.