Skip to content

lince_core

The SPARC V8 integer unit. This module owns the architectural state of each core, the decoder that turns 32-bit instruction words into DecodedInsn, the per-category instruction handlers, and the top-level step() driver.

It depends only on lince_interfaces — there is no awareness of buses, peripherals, or the runtime.

Source layout

src/core/
├── include/lince/core/
│   ├── cpu_state.hpp           ← per-core architectural state
│   ├── decoder.hpp             ← Decoder + DecodedInsn
│   ├── decoded_insn.hpp        ← InsnKind enum + operand fields
│   ├── handlers.hpp            ← public execute() dispatcher
│   ├── step.hpp                ← single-cycle driver
│   ├── trap.hpp                ← tt constants + status_to_tt()
│   ├── fpu.hpp / fpu_handlers.hpp ← FPop1/FPop2 (Phase 7)
│   └── softfloat_context.hpp   ← Berkeley SoftFloat 3e wrapper
└── src/
    ├── cpu_state.cpp
    ├── decoder.cpp
    ├── handlers.cpp            ← execute() dispatcher
    ├── handlers_alu.cpp
    ├── handlers_branch.cpp
    ├── handlers_loadstore.cpp
    ├── handlers_regwin.cpp
    ├── handlers_special.cpp
    ├── handlers_internal.hpp   ← shared helpers (alu_op2, eval_cond)
    ├── step.cpp
    └── softfloat_context.cpp

CpuState

The full architectural state of one core:

  • 8 register windows (NWINDOWS = 8), 16 in/out + 8 local + 8 global = 128 physical slots + 8 globals.
  • PSR, WIM, TBR, Y registers.
  • PC and nPC.
  • The PSR write pipeline (pending_psr_, see execution model).
  • A branch-pending flag (branch_taken_, branch_target_) used by the delay-slot logic.
  • An annul flag for Bicc,a not-taken paths.
  • A power-down flag for asr19 writes (idle loop).
  • An error-mode flag for traps that fire with ET == 0.

Read accessors are noexcept; write accessors take strongly-typed arguments. Window management goes through enter_trap, leave_trap, save_window, restore_window, all of which respect the WIM check order from SPARC V8 §B.26.

Decoder

Decoder::decode(uint32_t word) → DecodedInsn covers Format 1 (CALL), Format 2 (SETHI/Bicc), and the entire Format 3 (op = 10/11) opcode space. The result is a DecodedInsn carrying:

  • InsnKind — the canonical enum (Add/Sub/And/Or/Bicc/JMPL/SAVE/…).
  • rd, rs1, rs2, imm, simm13, disp22, disp30.
  • cc, branch_cond, is_imm, is_annul, is_link.

The decoder is stateless and pure: it never reads or writes CpuState; that is the handler's job. A unit test can decode any 32-bit pattern in isolation and inspect the structured result.

Unknown opcodes decode to InsnKind::Unknown rather than throwing; the handler then maps that to IllegalInstruction.

Handlers

The dispatcher in handlers.cpp calls a per-category handler:

File Covers
handlers_alu.cpp ADD/SUB/AND/OR/XOR/SLL/SRL/SRA/MULSCC/UMUL/SMUL/UDIV/SDIV plus their cc variants and tagged-add/sub
handlers_branch.cpp Bicc, BA, JMPL, CALL, RETT, Tcc
handlers_loadstore.cpp LD/LDH/LDB (signed and unsigned) + ST/STH/STB + LDD/STD + LDSTUB + SWAP + CASA
handlers_regwin.cpp SAVE, RESTORE, RETT (the privileged subset)
handlers_special.cpp RDY, WRY, RDPSR, WRPSR, RDWIM, WRWIM, RDTBR, WRTBR, IFLUSH

Shared helpers — alu_op2, eval_cond, condition-code update, register-window arithmetic — live in handlers_internal.hpp, only included from the handler .cpp files to keep the public surface clean.

Each handler returns an ExecStatus:

enum class ExecStatus {
    Ok,                       // normal completion
    Branch,                   // branch taken (delay slot follows)
    InsnFetchError,           // tt = 0x01
    IllegalInstruction,       // tt = 0x02
    PrivilegedInstruction,    // tt = 0x03
    FpDisabled,               // tt = 0x04
    WinOverflow,              // tt = 0x05
    WinUnderflow,             // tt = 0x06
    MemNotAligned,            // tt = 0x07
    BusError,                 // tt = 0x09
    TagOverflow,              // tt = 0x0a
    DivisionByZero,           // tt = 0x2a
    SoftwareTrap,             // tt = 0x80 + sw_trap_no
    ErrorMode,                // bubbled up; trap with ET=0
};

step()

lince::core::step(CpuState&, ICpuBus&) is the single-cycle driver. Per call it does, in order:

  1. Sample interrupts (the runtime hooks here via sample_interrupts).
  2. If error_mode_ is set, return HaltReason::ErrorMode.
  3. If is_powered_down_, return — the outer loop fast-forwards time.
  4. Fetch the instruction at PC (or skip it if annul_next_).
  5. Decode + execute.
  6. Apply branch / delay-slot logic.
  7. commit_psr_pipeline().

The function is intentionally short (~80 LOC) so it is easy to keep correct.

Berkeley SoftFloat 3e (Phase 7)

fpu.hpp and softfloat_context.hpp wrap the vendored third-party/softfloat3e build. The integration is currently scaffolding (see plans/phase7-fpu.md for status); the actual FPop1/FPop2 handlers that lift FPU semantics onto SoftFloat are the first deliverable of Phase 7.

The wrapper is per-CpuState: each core has its own SoftFloat fp_state so rounding mode / exception flags do not leak between cores.

What is intentionally out of lince_core

  • No bus access — fetches go through ICpuBus, loads/stores through the same.
  • No knowledge of peripherals.
  • No I/O of any kind, not even logging — handlers return statuses, not error messages.
  • No cache, no MMU, no JIT.