diff --git a/cpu/iss_v2/include/cores/cv32e40p/core.hpp b/cpu/iss_v2/include/cores/cv32e40p/core.hpp new file mode 100644 index 00000000..0c5b26d8 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/core.hpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +class Cv32e40pCore : public Core +{ +public: + Cv32e40pCore(Iss &iss) : Core(iss), iss(iss) {} + + /* MRET with the mcause hold-over of the RTL; shadows the generic + * handler (static dispatch via CONFIG_GVSOC_ISS_CORE). */ + iss_reg_t mret_handle(); + +private: + Iss &iss; +}; diff --git a/cpu/iss_v2/include/cores/cv32e40p/csr.hpp b/cpu/iss_v2/include/cores/cv32e40p/csr.hpp new file mode 100644 index 00000000..828cd6d2 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/csr.hpp @@ -0,0 +1,296 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +/* Static personality configuration, set by the Python recipe + * (pulp/cpu/iss/cv32e40p_v2.py): + * CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA F extension present (0 for ZFINX) + * CONFIG_GVSOC_ISS_CV32E40P_ZFINX ZFINX variant + * CONFIG_GVSOC_ISS_CV32E40P_PULP COREV_PULP (XPULP) configuration + * CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS implemented HPM counters + */ +#ifndef CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA +#define CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA 0 +#endif +#ifndef CONFIG_GVSOC_ISS_CV32E40P_ZFINX +#define CONFIG_GVSOC_ISS_CV32E40P_ZFINX 0 +#endif +#ifndef CONFIG_GVSOC_ISS_CV32E40P_PULP +#define CONFIG_GVSOC_ISS_CV32E40P_PULP 1 +#endif +#ifndef CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS +#define CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS 1 +#endif +static_assert(CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS <= 29, + "at most 29 HPM counters (mhpmcounter3..31)"); + +/* CSR that is read-only from CSR instructions: any write attempt raises an + * illegal-instruction exception before the access happens, so the destination + * register is not written (matches the RTL decoder behaviour). */ +class Cv32e40pRoCsr : public CsrReg +{ +public: + bool check_access(Iss *iss, bool write, bool read) override; +}; + +/* fflags / frm / fcsr front-end. Access legality follows the RTL fs_off + * signal: rejected with an illegal-instruction exception while mstatus.FS + * is Off (FPU in the ISA), always rejected without an FPU, always granted + * for ZFINX — see fp_access_illegal(). The register content lives in the + * base class fcsr field; the value mapping is done by the registered + * callback. */ +class Cv32e40pFpCsr : public CsrAbtractReg +{ +public: + bool check_access(Iss *iss, bool write, bool read) override; +}; + +/* Hardware-loop CSR front-end (lpstart/lpend/lpcount). Readable via CSR + * instructions, but the RTL decoder raises illegal-instruction on any CSR + * write to them (they are only programmed through the cv.* instructions). */ +class Cv32e40pHwloopCsr : public CsrAbtractReg +{ +public: + bool check_access(Iss *iss, bool write, bool read) override; +}; + +/* User-mode counter alias (cycle/instret/hpmcounterN and the H views): + * reads mirror the machine counter through a registered callback, writes + * raise illegal-instruction (0xCxx is the architecturally read-only CSR + * range, and the RTL has no write path for it). */ +class Cv32e40pCounterAlias : public CsrAbtractReg +{ +public: + bool check_access(Iss *iss, bool write, bool read) override; +}; + +/* Debug-mode CSR front-end (dcsr/dpc/dscratch0-1): accessible only while in + * debug mode. The RTL decoder raises illegal-instruction on any M-mode + * access (cv32e40p_decoder.sv, CSR_DCSR..CSR_DSCRATCH1 with !debug_mode_i), + * so generic_exception_test relies on these accesses trapping. Debug-ROM + * code runs with debug_mode set and passes the check. */ +class Cv32e40pDebugCsr : public CsrAbtractReg +{ +public: + bool check_access(Iss *iss, bool write, bool read) override; +}; + +class Cv32e40pCsr : public Csr +{ +public: + /* mcountinhibit implemented bits: CY, IR and one per HPM counter. */ + static constexpr iss_reg_t MCOUNTINHIBIT_MASK = + 0x5 | (((1u << CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS) - 1) << 3); + + Cv32e40pCsr(Iss &iss); + + void start(); + void reset(bool active); + + /* FP CSR access legality: illegal while mstatus.FS == Off (00). */ + inline bool fp_access_illegal(); + + /* Promote mstatus.FS to Dirty (11) on FP state change. The RTL forces it + * on FP regfile writes, fflags updates and FP-CSR writes when the FPU is + * in the ISA (FPU=1, ZFINX=0). SD (bit 31) is derived at read time. + * Out-of-line: the trapped-instruction guard needs the full Iss type. */ + void fp_state_dirty(); + + /* Advance the counters for one retired instruction: events is the OR of + * the RTL hpm_events lines it fired (see cores/cv32e40p/events.hpp); + * count_instr is the RTL minstret event line (false for EBREAK, which + * never counts - cv32e40p_id_stage.sv:1639). Called once per retire by + * Cv32e40pEvents::event_retire_account. */ + inline void hpm_commit(uint32_t events, bool count_instr); + + /* True while any implemented counter is enabled: keeps the core on the + * full handlers, where the event lines fire (Cv32e40pExec). */ + inline bool hpm_counting(); + + /* EBREAK in M-mode enters debug when dcsr.ebreakm=1 (RISC-V Debug + * Spec, dcsr bit 15). Consumed by ebreak_exec/c_ebreak_exec + * (isa/rv32i.hpp, isa/rv32c.hpp), which check debug_mode first, so + * this is only reached outside debug mode. */ + bool ebreak_m_mode_enters_debug() { return ((this->dcsr >> 15) & 1) != 0; } + + /* CV32E40P-only CSRs, absent from the generic register file. */ + Cv32e40pRoCsr mvendorid_ro; /* 0xF11 (replaces the base read/write reg) */ + Cv32e40pRoCsr marchid_ro; /* 0xF12 (replaces the base read/write reg) */ + Cv32e40pRoCsr mimpid; /* 0xF13 */ + Cv32e40pRoCsr mhartid_csr; /* 0xF14 */ + CsrReg tinfo; /* 0x7A4, read-only through a zero mask */ + CsrReg mcontext; /* 0x7A8, writable only from debug mode */ + CsrReg scontext; /* 0x7AA, writable only from debug mode */ + CsrReg minstret; /* 0xB02 */ +#if ISS_REG_WIDTH == 32 + CsrReg mcycleh; /* 0xB80 */ + CsrReg minstreth; /* 0xB82 */ +#endif + CsrReg mhpmevent[29]; /* 0x323..0x33F */ + + /* PULP custom CSRs (COREV_PULP configurations). */ + Cv32e40pRoCsr uhartid; /* 0xCD0 */ + Cv32e40pRoCsr privlv; /* 0xCD1 */ + Cv32e40pRoCsr zfinx_csr; /* 0xCD2, undeclared when FPU=1 && ZFINX=0 */ + + /* Hardware-loop CSRs: 0xCC0..0xCC2 / 0xCC4..0xCC6 (gap at 0xCC3). */ + Cv32e40pHwloopCsr hwloop_csr[6]; + + /* Architectural LPEND per loop, written by the corev.hpp setters. The + * Hwloop module stores the loop-back point (LPEND - 4), so it cannot + * serve the CSR read: a never-programmed loop must read back 0. */ + iss_reg_t hwloop_lpend[2] = {0, 0}; + + /* mip front-end (0x344): reads mirror the wire-driven base register, + * CSR writes are silently dropped — the RTL has no mip write path + * (cv32e40p_cs_registers.sv reads it from the interrupt lines only). + * Replaces the base mip in the CSR map, so the generic IrqRiscv write + * callback (wdata & 0xAAA, which also clears the fast-line bits) can + * never corrupt the pending state. */ + CsrAbtractReg mip_view; + + /* Debug-mode CSRs (0x7B0-0x7B3): views over the base raw fields + * (dcsr/depc/scratch0/scratch1), which the debug-entry and dret paths + * write directly. The base register file leaves these addresses + * undeclared, so without the views every debug-ROM csrrw raises + * illegal-instruction. Access is legal from debug mode only: the RTL + * decoder (not cv32e40p_cs_registers.sv, which decodes them at any + * time) rejects M-mode accesses with illegal-instruction. */ + Cv32e40pDebugCsr dcsr_view; /* 0x7B0 */ + Cv32e40pDebugCsr dpc_view; /* 0x7B1 */ + Cv32e40pDebugCsr dscratch0_view; /* 0x7B2 */ + Cv32e40pDebugCsr dscratch1_view; /* 0x7B3 */ + + /* User counter aliases: 0xC00/0xC02/0xC03..0xC1F and the H views at + * 0xC80/0xC82/0xC83..0xC9F. time (0xC01) is absent. */ + Cv32e40pCounterAlias cycle_alias; + Cv32e40pCounterAlias instret_alias; + Cv32e40pCounterAlias hpmcounter_alias[29]; +#if ISS_REG_WIDTH == 32 + Cv32e40pCounterAlias cycleh_alias; + Cv32e40pCounterAlias instreth_alias; + Cv32e40pCounterAlias hpmcounterh_alias[29]; +#endif + + /* fflags / frm / fcsr (0x001..0x003). */ + Cv32e40pFpCsr fflags_csr; + Cv32e40pFpCsr frm_csr; + Cv32e40pFpCsr fcsr_csr; + +private: + bool fflags_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool frm_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool fcsr_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool hwloop_csr_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index); + bool tselect_read_zero(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool tdata_debug_gate(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool mip_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool dcsr_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool dpc_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool dscratch0_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool dscratch1_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool mcycle_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool mcycleh_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool minstret_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool minstreth_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool cycle_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool cycleh_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool instret_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool instreth_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool hpm_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index); + bool hpmh_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index); + bool mcountinhibit_access(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool mstatus_read_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value); + bool mtvec_write_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value); + + /* Current 64-bit mcycle count: the frozen register pair while + * mcountinhibit.CY is set, the offset clock otherwise. */ + uint64_t mcycle_count(); + void mcycle_set(uint64_t count); + + int64_t mcycle_offset = 0; + + /* Set by a CSR write to minstret/minstreth, consumed (and cleared) by + * hpm_commit at that same instruction's retire: the RTL suppresses the + * minstret increment on the cycle the counter is written + * (cv32e40p_cs_registers.sv, !write_lower && !write_upper gate), so + * the csrw itself must not count on top of the written value. */ + bool minstret_written = false; + + /* Armed by a CSR write to mcountinhibit, consumed (and cleared) by + * hpm_commit at that same instruction's retire: the RTL evaluates the + * increment gates on mcountinhibit_q in the cycle the write commits + * (cv32e40p_cs_registers.sv:1428), so the writing instruction is still + * gated by the OLD value and the write takes effect from the next + * instruction on. */ + bool mcountinhibit_stale = false; + iss_reg_t mcountinhibit_old = 0; +}; + +inline bool Cv32e40pCsr::fp_access_illegal() +{ + /* RTL (cv32e40p_cs_registers.sv:1110 + decoder): illegal when there is + * no FPU; gated on mstatus.FS only with the FPU registers in the ISA; + * always legal for ZFINX (no FS state, flags/rm still implemented). */ +#if CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA + return this->mstatus.fs == 0; +#elif CONFIG_GVSOC_ISS_CV32E40P_ZFINX + return false; +#else + return true; +#endif +} + +inline bool Cv32e40pCsr::hpm_counting() +{ + /* CY excluded: mcycle is clock-derived and needs no full-handler + * support, only minstret and the event counters do. */ + constexpr iss_reg_t event_bits = MCOUNTINHIBIT_MASK & ~(iss_reg_t)0x1; + return (this->mcountinhibit.value & event_bits) != event_bits; +} + +inline void Cv32e40pCsr::hpm_commit(uint32_t events, bool count_instr) +{ + /* An instruction writing mcountinhibit is gated by the pre-write + * value; both flags clear unconditionally: they belong to this + * retire only. */ + iss_reg_t inhibit = this->mcountinhibit_stale ? this->mcountinhibit_old + : this->mcountinhibit.value; + this->mcountinhibit_stale = false; + /* minstret: retired instructions, gated on mcountinhibit.IR (bit 2), + * on the RTL event line (count_instr, false for EBREAK) and on the + * same-row write suppression. */ + bool wrote_counter = this->minstret_written; + this->minstret_written = false; + if (count_instr && !wrote_counter && !(inhibit & 0x4)) + { + if (++this->minstret.value == 0) + { +#if ISS_REG_WIDTH == 32 + this->minstreth.value++; +#endif + } + } + /* mhpmcounterN advances at most +1 per retire when the mhpmeventN mask + * intersects the fired lines and its mcountinhibit bit is clear. */ + for (int i = 0; i < CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS; i++) + { + if ((this->mhpmevent[i].value & events) + && !(inhibit & (1u << (3 + i)))) + { + if (++this->mhpmcounter[i].value == 0) + { +#if ISS_REG_WIDTH == 32 + this->mhpmcounterh[i].value++; +#endif + } + } + } +} diff --git a/cpu/iss_v2/include/cores/cv32e40p/events.hpp b/cpu/iss_v2/include/cores/cv32e40p/events.hpp new file mode 100644 index 00000000..c9ffacfd --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/events.hpp @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +/* RTL hpm_events bit positions (cv32e40p_cs_registers.sv:1327). Only the + * architectural, instruction-derived lines are accounted by the model; the + * timing lines (cycle, stalls, imiss, APU) never fire. */ +#define CV32E40P_HPM_INSTR (1u << 1) +#define CV32E40P_HPM_LD (1u << 5) +#define CV32E40P_HPM_ST (1u << 6) +#define CV32E40P_HPM_JUMP (1u << 7) +#define CV32E40P_HPM_BRANCH (1u << 8) +#define CV32E40P_HPM_BRANCH_TAKEN (1u << 9) +#define CV32E40P_HPM_COMP_INSTR (1u << 10) + +class Cv32e40pEvents : public Events +{ +public: + Cv32e40pEvents(Iss &iss) : Events(iss) {} + + void reset(bool active); + + inline void event_load_account(int incr); + inline void event_store_account(int incr); + inline void event_branch_account(); + inline void event_taken_branch_account(); + inline void event_jump_account(); + inline void event_retire_account(iss_insn_t *insn); + inline void insn_stall_account(); + + /* Architectural commit stream for an external stepper (RVVI bridge). + * A PC is pushed when the instruction's result is architecturally + * visible: at retire for sync instructions, at commit-FIFO drain for + * held ones (async load, WFI) - insn_stall_account only fires there - + * where the writeback has already happened. The stepper pops. Sampling + * the regfile on the raw retire hook instead would race the load + * writeback (the LSU response lands one cycle later). */ + static constexpr int COMMIT_RING = 64; + uint64_t commit_push = 0; + uint64_t commit_pop = 0; + iss_reg_t commit_pc[COMMIT_RING]; + + /* Raw encoding of the committed instruction (insn->opcode at the push + * site). The external stepper forwards it for the RVVI INSBIN compare + * against the DUT's rvfi_insn. Same push/pop discipline as commit_pc. */ + iss_reg_t commit_insn[COMMIT_RING]; + + /* Trap-redirect sequence, bumped by Cv32e40pException::raise and the + * Cv32e40pIrq take. Each commit entry is stamped with the value seen + * when the instruction executed; a stamp older than the current + * trap_seq tells the stepper the sampled CSR/GPR state already + * includes a later redirect (a trap taken while this entry was still + * held in the commit FIFO), so state compares on it must be skipped. */ + uint64_t trap_seq = 0; + uint64_t commit_trap_seq[COMMIT_RING]; + + /* Whether the committed instruction itself TRAPPED (exec.has_exception + * at the retire hook). The external stepper needs this on trap rows: + * an ecall/ebreak commit is the faulting step and must be consumed + * there, while a pipeline kill-and-replay row (rvfi_trap with no + * architectural trap) commits a NORMAL instruction that the DUT + * re-executes on the next row - consuming it there would shift the + * compare stream by one retire. The trap_seq stamp cannot separate + * the two: the faulting insn is stamped after its own raise(). */ + bool commit_trapped[COMMIT_RING]; + + /* Drop commits not consumed yet (external resync forced a new PC). */ + inline void commit_stream_flush(); + + /* Instructions parked in the exec commit FIFO, not yet drained. Their + * load-use scoreboard bits are already set, so redirecting while this + * is true needs a drain first (see gvsoc_engine_set_pc). */ + bool inflight_pending() const { return this->inflight_pop != this->inflight_push; } + +private: + /* Program-order PCs of the instructions parked in the exec commit + * FIFO (held or sync follower); drain pops them in the same order. + * The trap_seq stamp is taken here, at execution, and carried to the + * commit entry at drain. */ + uint64_t inflight_push = 0; + uint64_t inflight_pop = 0; + iss_reg_t inflight_pc[COMMIT_RING]; + iss_reg_t inflight_insn[COMMIT_RING]; + uint64_t inflight_trap_seq[COMMIT_RING]; + bool inflight_trapped[COMMIT_RING]; + + /* Event lines fired by the executing instruction, committed as one OR + * mask at retire: each counter advances at most +1 per instruction, as + * the RTL advances at most +1 per cycle (cv32e40p_cs_registers.sv:1437). + * Relies on the LSU firing its hooks only for accepted requests (no + * hook before a stall is detected), so nothing leaks across retries. */ + uint32_t pending_events = 0; +}; diff --git a/cpu/iss_v2/include/cores/cv32e40p/events_implem.hpp b/cpu/iss_v2/include/cores/cv32e40p/events_implem.hpp new file mode 100644 index 00000000..edf2537d --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/events_implem.hpp @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include "cpu/iss_v2/include/cores/cv32e40p/csr.hpp" +#include +#include +#include + +inline void Cv32e40pEvents::event_load_account(int incr) +{ + Events::event_load_account(incr); + this->pending_events |= CV32E40P_HPM_LD; +} + +inline void Cv32e40pEvents::event_store_account(int incr) +{ + Events::event_store_account(incr); + this->pending_events |= CV32E40P_HPM_ST; +} + +inline void Cv32e40pEvents::event_branch_account() +{ + Events::event_branch_account(); + this->pending_events |= CV32E40P_HPM_BRANCH; +} + +inline void Cv32e40pEvents::event_taken_branch_account() +{ + /* A taken branch fires both RTL event lines (base cascades to + * event_branch_account, the explicit OR keeps that non-load-bearing). */ + Events::event_taken_branch_account(); + this->pending_events |= CV32E40P_HPM_BRANCH | CV32E40P_HPM_BRANCH_TAKEN; +} + +inline void Cv32e40pEvents::event_jump_account() +{ + Events::event_jump_account(); + this->pending_events |= CV32E40P_HPM_JUMP; +} + +inline void Cv32e40pEvents::event_retire_account(iss_insn_t *insn) +{ + Events::event_retire_account(insn); + /* Encoding for the RVVI INS compare. The fetched word carries the NEXT + * parcel in its upper half on RVC rows, so truncate to the insn size + * (the bridge masks the DUT side the same way). */ + iss_reg_t enc = (insn->size == 2) ? (insn->opcode & 0xFFFF) : insn->opcode; +#ifdef CONFIG_GVSOC_ISS_EXEC_INORDER_COMMIT + if (this->iss.exec.queue_head != NULL) + { + /* Parked in the commit FIFO (held, or sync follower behind a held + * head): visible at drain time, through insn_stall_account, in + * this same program order. */ + this->inflight_pc[this->inflight_push % COMMIT_RING] = insn->addr; + this->inflight_insn[this->inflight_push % COMMIT_RING] = enc; + this->inflight_trap_seq[this->inflight_push % COMMIT_RING] = this->trap_seq; + this->inflight_trapped[this->inflight_push % COMMIT_RING] = + this->iss.exec.has_exception; + this->inflight_push++; + } + else +#endif + { + this->commit_pc[this->commit_push % COMMIT_RING] = insn->addr; + this->commit_insn[this->commit_push % COMMIT_RING] = enc; + this->commit_trap_seq[this->commit_push % COMMIT_RING] = this->trap_seq; + this->commit_trapped[this->commit_push % COMMIT_RING] = + this->iss.exec.has_exception; + this->commit_push++; + } + /* A trapping instruction does not retire: drop its event lines. */ + if (this->iss.exec.has_exception) + { + this->pending_events = 0; + return; + } + /* RTL minstret event (cv32e40p_id_stage.sv:1639) excludes EBREAK + * unconditionally; the compressed-retired event shares the gate + * (:1664, minstret && is_compressed). The trapping ebreak forms were + * dropped above with has_exception - this covers the debug-entry + * ebreak (dcsr.ebreakm=1), which retires without an architectural + * trap yet must not count. */ + bool count_instr = !(enc == 0x00100073u + || (insn->size == 2 && enc == 0x9002u)); + uint32_t events = this->pending_events + | (count_instr ? (CV32E40P_HPM_INSTR + | (insn->size == 2 ? CV32E40P_HPM_COMP_INSTR : 0)) : 0); + this->pending_events = 0; + this->iss.csr.hpm_commit(events, count_instr); +} + +inline void Cv32e40pEvents::insn_stall_account() +{ + /* Fires only from ExecInOrder::drain_entry: one commit-FIFO entry + * retired, writeback done. The guard covers entries flushed by + * commit_stream_flush while their drain was still pending. */ + if (this->inflight_pop < this->inflight_push) + { + this->commit_pc[this->commit_push % COMMIT_RING] = + this->inflight_pc[this->inflight_pop % COMMIT_RING]; + this->commit_insn[this->commit_push % COMMIT_RING] = + this->inflight_insn[this->inflight_pop % COMMIT_RING]; + this->commit_trap_seq[this->commit_push % COMMIT_RING] = + this->inflight_trap_seq[this->inflight_pop % COMMIT_RING]; + this->commit_trapped[this->commit_push % COMMIT_RING] = + this->inflight_trapped[this->inflight_pop % COMMIT_RING]; + this->inflight_pop++; + this->commit_push++; + } +} + +inline void Cv32e40pEvents::commit_stream_flush() +{ + this->commit_pop = this->commit_push; + this->inflight_pop = this->inflight_push; +} diff --git a/cpu/iss_v2/include/cores/cv32e40p/exception.hpp b/cpu/iss_v2/include/cores/cv32e40p/exception.hpp new file mode 100644 index 00000000..150f54f5 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/exception.hpp @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +class Cv32e40pException : public Exception +{ +public: + /* Defined in the .cpp: reads the debug_exception_handler config key, + * which needs the complete Iss type. */ + Cv32e40pException(Iss &iss); + + /* Exception entry at the mtvec base; shadows the generic raise + * (static dispatch via CONFIG_GVSOC_ISS_EXCEPTION). */ + void raise(iss_reg_t pc, int id); + + iss_addr_t debug_exception_handler_addr; + +private: + Iss &iss; +}; diff --git a/cpu/iss_v2/include/cores/cv32e40p/exec.hpp b/cpu/iss_v2/include/cores/cv32e40p/exec.hpp new file mode 100644 index 00000000..2e94bbaf --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/exec.hpp @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include + +class Iss; + +class Cv32e40pExec : public ExecInOrder +{ +public: + Cv32e40pExec(Iss &iss) : ExecInOrder(iss) {} + + inline bool can_switch_to_fast_mode(); + + /* Set by an external observer (RVVI bridge) consuming the commit + * stream: the fast dispatch path skips the commit-FIFO bookkeeping + * the stream is built on, so stay on the full handlers. */ + bool commit_stream_observed = false; +}; diff --git a/cpu/iss_v2/include/cores/cv32e40p/exec_implem.hpp b/cpu/iss_v2/include/cores/cv32e40p/exec_implem.hpp new file mode 100644 index 00000000..a6bcfa95 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/exec_implem.hpp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include "cpu/iss_v2/include/cores/cv32e40p/exec.hpp" +#include "cpu/iss_v2/include/exec/exec_inorder.hpp" + +inline bool Cv32e40pExec::can_switch_to_fast_mode() +{ + if (!ExecInOrder::can_switch_to_fast_mode()) return false; + + if (this->commit_stream_observed) return false; + + /* The event lines only fire from the full handlers: stay there while + * any implemented counter is enabled (see Cv32e40pCsr::hpm_counting). */ + return !this->iss.csr.hpm_counting(); +} diff --git a/cpu/iss_v2/include/cores/cv32e40p/irq.hpp b/cpu/iss_v2/include/cores/cv32e40p/irq.hpp new file mode 100644 index 00000000..10d9d123 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/irq.hpp @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +class Cv32e40pIrq : public IrqRiscv +{ +public: + /* Interrupt lines wired in the RTL: MSI(3), MTI(7), MEI(11) and the + * sixteen fast lines irq[31:16] (cv32e40p_int_controller.sv IRQ_MASK). */ + static constexpr iss_reg_t IRQ_MASK = 0xFFFF0888; + + /* Defined in irq.cpp (Iss is incomplete here): registers the haltreq + * slave port - the debug halt request line (RTL debug_req_i), a + * first-class wire like the interrupt lines, handled by haltreq_sync. */ + Cv32e40pIrq(Iss &iss); + + void start(); + + /* Shadows IrqRiscv::reset (static dispatch via CONFIG_GVSOC_ISS_IRQ, + * iss.cpp): the base reset does not know the personality's state. A + * live single-step window (step_state/step_pc) or an unconsumed + * collision id surviving a reset would fire a phantom cause=4 entry + * or a stale interrupt take on the first post-reset boundary. */ + void reset(bool active); + + /* Interrupt take with the RTL priority order and vectored entry; + * shadows the generic RISC-V ladder (static dispatch via + * CONFIG_GVSOC_ISS_IRQ). */ + int check(); + + /* haltreq wire: arms req_debug and wakes a WFI-parked hart. */ + static void haltreq_sync(vp::Block *__this, bool value); + + /* wfi_wake wire: releases a WFI-parked hart (full three-step release, + * only callable inside the model) with NO architectural side effect. + * Driven externally when the DUT's retire stream proves the wake + * happened (the RTL retires wfi at execute and sleeps after; + * wake sources like debug_req are not all visible as interrupt + * wires). */ + static void wfi_wake_sync(vp::Block *__this, bool value); + + /* ebreak with dcsr.ebreakm=1 outside debug mode (isa/rv32i.hpp, + * isa/rv32c.hpp): arms the debug request; check() performs the entry + * at the next dispatch boundary, so the ebreak never retires and + * mcause/mepc stay untouched - like the RTL, where the entry row is + * the first debug-ROM instruction. Inline: touches members only + * (Iss is incomplete here). */ + void ebreak_enter_debug() + { + this->req_debug = true; + this->req_debug_cause = 1; + } + + /* dret with dcsr.step=1 (priv.hpp dret_exec): opens the single-step + * window. check() re-enters debug with cause=4 once the stepped + * instruction is done. Defined in irq.cpp (reads dcsr/depc). */ + void dret_step_check(); + + /* Single-step window state: 0 = idle, 1 = stepping (step_pc holds the + * address of the one instruction to execute). The exit condition in + * check() is current_insn != step_pc: a completed instruction moved + * the PC, and an exception redirect (has_exception, consumed before + * check() runs) lands the entry on the handler address, as the Debug + * spec requires. A stepped jump-to-self never trips it; that corner + * is handled by an externally armed entry. */ + int step_state = 0; + iss_reg_t step_pc = 0; + + /* Level of the haltreq wire (RTL debug_req_i is level-sensitive: while + * high the hart re-halts right after dret). haltreq_sync records it; + * check() re-arms req_debug from it outside debug mode, since the wire + * only syncs on level CHANGES and a still-high level after an entry + * consumed req_debug would otherwise be lost. */ + bool haltreq_level = false; + + /* dcsr.cause for the next req_debug take (debug spec: 1=ebreak, + * 3=haltreq, 4=single-step). The haltreq wire path leaves the + * default; an external debug-entry request sets it from the DUT's + * dcsr before arming req_debug. Reset to 3 by the entry itself. */ + int req_debug_cause = 3; + + /* Informed interrupt+debug collision (co-sim): when the DUT's debug + * entry row carries the CSR writes of an interrupt take, the + * external driver stores the taken cause id here before arming the + * entry; check() then takes exactly that line ahead of the entry, so + * dpc lands on the (vectored) handler entry and mstatus/mepc/mcause + * carry the take, as in the RTL. -1 = no collision. The model never + * guesses this on its own: the arbitration outcome depends on cycle + * timing only the DUT can observe. */ + int collide_irq_id = -1; + + /* Certification of an adjacent-row collision candidate (the take's + * mcause rode the row BEFORE the entry row): the take fires only if + * the entry boundary (current_insn, the future depc source) equals + * the take's mepc - a stale-mcause candidate fails this by + * construction. Checked here, at the entry itself: only the model + * knows the boundary at dispatch time. collide_certify=false keeps + * the unconditional same-row behaviour. */ + iss_reg_t collide_expected_mepc = 0; + bool collide_certify = false; + + /* Co-sim asynchronous-event hold (level, external-driver owned). + * + * While set, check() delivers asynchronous external events as STATE + * (mip via the wires, req_debug latched from the haltreq level) but + * never TAKES them at a dispatch boundary: no interrupt-ladder take, + * no haltreq re-arm, no cause-3 (haltreq) debug entry. The take + * boundary of an asynchronous event is decided by pipeline timing the + * model cannot see; in lockstep the DUT proves the boundary and the + * driver injects the take there (take_irq/take_debug windows lower + * this hold together with skip_irq_check). + * + * Synchronous conditions keep their architectural timing and ignore + * the hold: the execute-address trigger (evaluated on the matched + * boundary itself), the single-step window close (exactly one + * instruction after dret), ebreak-to-debug and driver-armed entries + * (cause 1/2/4). + * + * This is a LEVEL, unlike exec.skip_irq_check (a one-shot consumed at + * the first check() of a step quantum): a 20 ns engine quantum runs + * several dispatches, and every dispatch after the first ran with the + * one-shot already consumed - the window through which the model used + * to take wire IRQs on its own, racing the DUT's entry boundary. + * Standalone (non co-sim) runs never set it: behaviour unchanged. */ + bool dpi_async_hold = false; + + vp::WireSlave haltreq_itf; + vp::WireSlave wfi_wake_itf; + +private: + bool mie_write_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value); + + /* Interrupt take (RTL priority + vectored entry); shared by the + * check() ladder and the simultaneous-interrupt debug entry. */ + void irq_take(iss_reg_t pending); + + /* Full WFI release (flag clear, retain_dec, terminate of the held + * entry - the terminated entry drains into the commit stream). Only + * callable inside the model; shared by haltreq_sync and + * wfi_wake_sync. No-op when the hart is not parked. */ + void release_wfi(); +}; diff --git a/cpu/iss_v2/include/cores/cv32e40p/priv.hpp b/cpu/iss_v2/include/cores/cv32e40p/priv.hpp new file mode 100644 index 00000000..1e736186 --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/priv.hpp @@ -0,0 +1,256 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +/* CV32E40P privileged-instruction handlers, replacing + * for the whole priv subset (the recipe + * swaps the subset include, so this file provides the full surface: + * csrr*, wfi, xret, sfence.vma). + * + * Difference from the generic handlers: CSRRC with rs1=x0 and + * CSRRSI/CSRRCI with uimm=0 must not write the CSR (privileged spec + * §2.2), so a read of a read-only CSR through them is legal. The generic + * csrrc/csrrsi/csrrci treat every access as a write. Same fix as the v1 + * core header (cpu/iss/include/cores/cv32e40p/priv.hpp). + */ + +#pragma once + +static inline void csr_decode(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + // In case traces are active, convert the CSR number into a name +#ifdef VP_TRACE_ACTIVE + insn->args[2].flags = (iss_decoder_arg_flag_e)(insn->args[2].flags | ISS_DECODER_ARG_FLAG_DUMP_NAME); + insn->args[2].name = iss_csr_name(iss, UIM_GET(0)); +#endif +} + +static inline iss_reg_t csrrw_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr) + { + return csr->handle(iss, insn, pc, REG_GET(0)); + } + + iss_reg_t value; + iss_reg_t reg_value = REG_GET(0); + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + iss_csr_write(iss, insn, UIM_GET(0), reg_value); + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t csrrc_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + iss_reg_t value; + iss_reg_t reg_value = REG_GET(0); + + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr && !csr->check_access(iss, REG_IN(0) != 0, true)) + { + return pc; + } + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + if (REG_IN(0) != 0) + { + iss_csr_write(iss, insn, UIM_GET(0), value & ~reg_value); + } + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t csrrs_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + iss_reg_t value; + iss_reg_t reg_value = REG_GET(0); + + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr && !csr->check_access(iss, REG_IN(0) != 0, true)) + { + return pc; + } + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + if (REG_IN(0) != 0) + { + iss_csr_write(iss, insn, UIM_GET(0), value | reg_value); + } + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t csrrwi_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr) + { + return csr->handle(iss, insn, pc, UIM_GET(1)); + } + + iss_reg_t value; + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + iss_csr_write(iss, insn, UIM_GET(0), UIM_GET(1)); + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t csrrci_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + iss_reg_t value; + + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr && !csr->check_access(iss, UIM_GET(1) != 0, true)) + { + return pc; + } + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + if (UIM_GET(1) != 0) + { + iss_csr_write(iss, insn, UIM_GET(0), value & ~UIM_GET(1)); + } + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t csrrsi_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + iss_reg_t value; + + CsrAbtractReg *csr = iss->csr.get_csr(UIM_GET(0)); + if (csr && !csr->check_access(iss, UIM_GET(1) != 0, true)) + { + return pc; + } + + if (iss_csr_read(iss, insn, UIM_GET(0), &value) == 0) + { + if (insn->out_regs[0] != 0) + { + iss->regfile.memcheck_set_valid(REG_OUT(0), true); + REG_SET(0, value); + } + } + + if (UIM_GET(1) != 0) + { + iss_csr_write(iss, insn, UIM_GET(0), value | UIM_GET(1)); + } + + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t wfi_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + /* wfi degrades to a nop whenever the hart must stay responsive to the + * debugger (RISC-V Debug Spec; RTL cv32e40p_sleep_unit.sv / + * controller): in debug mode, in the single-step window (dcsr.step) + * and with a pending debug request - the RTL never sleeps with + * debug_req_i asserted, and a level-high haltreq produces no fresh + * wire edge to wake a parked hart. The guard lives HERE, in the + * personality, so the shared IrqRiscv::wfi_handle keeps its + * historical behaviour for the other iss_v2 cores. */ + if (!iss->irq.req_debug && !iss->exec.debug_mode && + !((iss->csr.dcsr >> 2) & 1)) + { + iss->irq.wfi_handle(insn); + } + return iss_insn_next(iss, insn, pc); +} + +static inline iss_reg_t mret_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + iss->exec.irq_exit.set(1); + iss->timing.stall_insn_dependency_account(5); + return iss->core.mret_handle(); +} + +static inline iss_reg_t dret_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + /* dret is legal only in debug mode; outside it the RTL raises an illegal + * instruction (RISC-V Debug spec, cv32e40p debug.rst). dret_handle() + * itself is unconditional (clears debug_mode, restores irq_enable, jumps + * to depc), so the guard must live here. */ + if (!iss->exec.debug_mode) + { + iss->exception.raise(pc, ISS_EXCEPT_ILLEGAL); + return pc; + } + /* dcsr.step=1: open the single-step window (depc still live here); + * check() re-enters debug with cause=4 after one instruction. */ + iss->irq.dret_step_check(); + return iss->core.dret_handle(); +} + +static inline iss_reg_t sret_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + /* No S-mode on CV32E40P: the RTL decodes sret as illegal. The generic + * handler would jump through sepc - never architecturally written on + * this core - and demote the privilege mode via mstatus.spp. Same + * guard shape as dret_exec above. */ + iss->exception.raise(pc, ISS_EXCEPT_ILLEGAL); + return pc; +} + +static inline iss_reg_t sfence_vma_exec(Iss *iss, iss_insn_t *insn, iss_reg_t pc) +{ + if (iss->core.mode_get() == PRIV_S && iss->csr.mstatus.tvm) + { + iss->exception.raise(pc, ISS_EXCEPT_ILLEGAL); + return pc; + } + else + { +#ifdef CONFIG_GVSOC_ISS_MMU + iss->mmu.flush(REG_GET(0), REG_GET(1)); +#endif + iss->insn_cache.mode_flush(); + return iss_insn_next(iss, insn, pc); + } +} diff --git a/cpu/iss_v2/include/cores/cv32e40p/regfile.hpp b/cpu/iss_v2/include/cores/cv32e40p/regfile.hpp new file mode 100644 index 00000000..09c86d6f --- /dev/null +++ b/cpu/iss_v2/include/cores/cv32e40p/regfile.hpp @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#pragma once + +#include +#include + +/* CV32E40P register-file personality. + * + * A trapped instruction writes no destination register on the RTL. The + * reserved-rounding-mode raise (isa_lib int.h) fires inside the value + * expression of the write-back macro, so the write that follows must be + * dropped: the raise site arms wb_suppress and the next set_reg/set_freg + * consumes it (one-shot — the instruction's own write always follows the + * raise within the same handler, so unrelated writes are never dropped). */ +class Cv32e40pRegfile : public Regfile +{ +public: + Cv32e40pRegfile(Iss &iss) : Regfile(iss) {} + + void reset(bool active) + { + this->wb_suppress = false; + this->Regfile::reset(active); + } + + inline void wb_suppress_arm() { this->wb_suppress = true; } + + /* Shadows the base setters (static dispatch via CONFIG_GVSOC_ISS_REGFILE). */ + inline void set_reg(int reg, uint64_t value) + { + /* x0 is hardwired to zero (RTL: cv32e40p_register_file_ff.sv "R0 is + * nil"; unpriv spec). The decoder redirects rd==x0 writes to + * ISS_DUMMY_REG, but the XPULP post-increment addressing modes write + * the base register back through in_regs (IN_REG_SET, corev.hpp), + * which is never remapped: with rs1==x0 the increment lands in the + * real x0 slot and corrupts it (class C11, fv_ms1_20260816). Writes + * to x0 are architectural no-ops: drop them up front so they can + * never consume the wb_suppress one-shot either. */ + if (reg == 0) + { + return; + } + if (this->wb_suppress) + { + this->wb_suppress = false; + return; + } + this->Regfile::set_reg(reg, value); + } + + inline void set_freg(int reg, uint64_t value) + { + if (this->wb_suppress) + { + this->wb_suppress = false; + return; + } + this->Regfile::set_freg(reg, value); + } + +private: + bool wb_suppress = false; +}; diff --git a/cpu/iss_v2/src/cores/cv32e40p/core.cpp b/cpu/iss_v2/src/cores/cv32e40p/core.cpp new file mode 100644 index 00000000..f36fd863 --- /dev/null +++ b/cpu/iss_v2/src/cores/cv32e40p/core.cpp @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#include + +iss_reg_t Cv32e40pCore::mret_handle() +{ + /* MRET executed while in debug mode (CV32E40P UM, debug.rst): the PC + * jumps to dm_exception_addr "without affecting status registers" - + * no mstatus/mcause/privilege side effects, the hart stays in debug + * mode. The generic handler below would return to mepc and restore + * mstatus.mie. */ + if (this->iss.exec.debug_mode) + { + this->iss.exec.switch_to_full_mode(); + return this->iss.exception.debug_exception_handler_addr & ~(iss_reg_t)0x3; + } + + /* mcause holds its value across MRET on CV32E40P (cleared only by the + * next trap or an explicit CSR write); the generic handler zeroes it. */ + iss_reg_t mcause = this->iss.csr.mcause.value; + iss_reg_t pc = this->Core::mret_handle(); + this->iss.csr.mcause.value = mcause; + return pc; +} diff --git a/cpu/iss_v2/src/cores/cv32e40p/csr.cpp b/cpu/iss_v2/src/cores/cv32e40p/csr.cpp new file mode 100644 index 00000000..60b85893 --- /dev/null +++ b/cpu/iss_v2/src/cores/cv32e40p/csr.cpp @@ -0,0 +1,706 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +/* CV32E40P CSR personality for iss_v2. + * + * Same CSR map and write-legality rules as the v1 model + * (cpu/iss/src/cv32e40p/csr_cv32e40p.cpp), expressed as a Csr subclass: + * base registers are tightened through their write masks, core-only + * registers are declared here, and registers the core does not implement + * are undeclared so access falls through to the unsupported-CSR path, + * which this core configures to raise illegal-instruction. */ + +#include +#include + +bool Cv32e40pRoCsr::check_access(Iss *iss, bool write, bool read) +{ + if (write) + { + iss->exception.raise(iss->exec.current_insn, ISS_EXCEPT_ILLEGAL); + return false; + } + return true; +} + +bool Cv32e40pFpCsr::check_access(Iss *iss, bool write, bool read) +{ + if (iss->csr.fp_access_illegal()) + { + iss->exception.raise(iss->exec.current_insn, ISS_EXCEPT_ILLEGAL); + return false; + } + return true; +} + +bool Cv32e40pHwloopCsr::check_access(Iss *iss, bool write, bool read) +{ + if (write) + { + iss->exception.raise(iss->exec.current_insn, ISS_EXCEPT_ILLEGAL); + return false; + } + return true; +} + +bool Cv32e40pCounterAlias::check_access(Iss *iss, bool write, bool read) +{ + if (write) + { + iss->exception.raise(iss->exec.current_insn, ISS_EXCEPT_ILLEGAL); + return false; + } + return true; +} + +bool Cv32e40pDebugCsr::check_access(Iss *iss, bool write, bool read) +{ + if (!iss->exec.debug_mode) + { + iss->exception.raise(iss->exec.current_insn, ISS_EXCEPT_ILLEGAL); + return false; + } + return true; +} + +Cv32e40pCsr::Cv32e40pCsr(Iss &iss) +: Csr(iss) +{ + /* M-mode-only core: drop the registers the RTL does not implement so + * access raises illegal-instruction through the unsupported-CSR path. + * Compared with the v1 model this also drops satp and the user + * counters (cycle/time/instret): no U-mode, no mcounteren. */ + const iss_reg_t nonexistent[] = { + 0x100, 0x104, 0x105, 0x106, /* sstatus, sie, stvec, scounteren */ + 0x140, 0x141, 0x142, 0x143, 0x144, /* sscratch, sepc, scause, stval, sip */ + 0x180, /* satp */ + 0x302, 0x303, /* medeleg, mideleg */ + 0x306, /* mcounteren */ + 0x740, 0x741, 0x742, 0x744, /* mnscratch, mnepc, mncause, mnstatus */ + 0x008, 0x009, 0x00A, 0x00F, /* vstart, vxstat, vxrm, vcsr */ + 0xC20, 0xC21, 0xC22, /* vl, vtype, vlenb */ + 0xC00, 0xC01, 0xC02, /* cycle, time, instret (0xC00/0xC02 + re-declared below as RO aliases) */ + }; + for (iss_reg_t addr : nonexistent) + { + this->undeclare_csr(addr); + } + + /* No PMP: the UM's CSR chapter has no pmpcfg/pmpaddr bank and the RTL + * raises illegal on any access. The generic model declares the whole + * bank even when the PMP module is the empty variant + * (CONFIG_GVSOC_ISS_PMP is a type name, always defined). */ + for (iss_reg_t addr = 0x3A0; addr < 0x3B0; addr++) /* pmpcfg0..15 */ + { + this->undeclare_csr(addr); + } + for (iss_reg_t addr = 0x3B0; addr < 0x3F0; addr++) /* pmpaddr0..63 */ + { + this->undeclare_csr(addr); + } + + /* Undeclaring removes a register from Csr::reset() coverage (reset walks + * the declared map only) while its raw .value - never initialized by the + * CsrReg constructor - is still read unguarded by shared trap code: + * Exception::raise consults medeleg for delegation and redirects through + * stvec; Core::sret_handle returns sepc. Left as heap garbage, a stray + * medeleg bit silently delegated sync traps to S-mode: mstatus.spp set + * (the bit-8 delta of the C3 trap-snapshot lanes), mcause/mepc stale, + * entry at garbage stvec (the 0x20202020 runaways). Zero them once here; + * nothing can write them afterwards - undeclared means any ISA access + * raises illegal, like the RTL. satp is left alone: guarded by + * CONFIG_GVSOC_ISS_MMU on every read path and unreachable on this core. */ + this->medeleg.value = 0; + this->mideleg.value = 0; + this->sstatus.value = 0; + this->sie.value = 0; + this->stvec.value = 0; + this->scounteren.value = 0; + this->sscratch.value = 0; + this->sepc.value = 0; + this->scause.value = 0; + this->stval.value = 0; + this->sip.value = 0; + this->mcounteren.value = 0; + + this->raise_on_unsupported_csr = true; + + /* Machine information registers: read-only, writes raise illegal. + * mvendorid/marchid are re-declared with the read-only register type + * (the base class versions accept writes). */ + this->undeclare_csr(0xF11); + this->undeclare_csr(0xF12); + this->declare_csr(&this->mvendorid_ro, "mvendorid", 0xF11, 0x00000602); + this->declare_csr(&this->marchid_ro, "marchid", 0xF12, 0x00000004); +#if CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA || CONFIG_GVSOC_ISS_CV32E40P_ZFINX || CONFIG_GVSOC_ISS_CV32E40P_PULP + this->declare_csr(&this->mimpid, "mimpid", 0xF13, 1); +#else + this->declare_csr(&this->mimpid, "mimpid", 0xF13, 0); +#endif + this->declare_csr(&this->mhartid_csr, "mhartid", 0xF14, this->mhartid); + + /* Counter CSRs. */ + this->declare_csr(&this->minstret, "minstret", 0xB02); +#if ISS_REG_WIDTH == 32 + this->declare_csr(&this->mcycleh, "mcycleh", 0xB80); + this->declare_csr(&this->minstreth, "minstreth", 0xB82); +#endif + + /* minstret/minstreth writes go through the default masked store; the + * callbacks (return true) only arm the same-row increment suppression + * consumed by hpm_commit (RTL: the write wins over the increment in + * the writing instruction's own retire cycle). */ + this->minstret.register_callback(std::bind(&Cv32e40pCsr::minstret_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#if ISS_REG_WIDTH == 32 + this->minstreth.register_callback(std::bind(&Cv32e40pCsr::minstreth_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#endif + + /* mcycle/mcycleh: one 64-bit count derived from the clock with a write + * offset, frozen into the register pair while mcountinhibit.CY is set. + * Registered after the base callback so these have the last word. */ + this->mcycle.register_callback(std::bind(&Cv32e40pCsr::mcycle_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#if ISS_REG_WIDTH == 32 + this->mcycleh.register_callback(std::bind(&Cv32e40pCsr::mcycleh_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#endif + + /* mcountinhibit: reset with all implemented bits set (RTL behaviour), + * i.e. counters disabled out of reset. The callback freezes/unfreezes + * the mcycle count on CY toggles. */ + const int num_hpm = CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS; + this->mcountinhibit.set_write_mask(MCOUNTINHIBIT_MASK); + this->mcountinhibit.reset_val = MCOUNTINHIBIT_MASK; + this->mcountinhibit.register_callback(std::bind(&Cv32e40pCsr::mcountinhibit_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + + /* HPM counters and event selectors: only the first num_mhpmcounters + * are implemented, the rest are WARL zero (writes ignored). Only + * mhpmevent bits [15:0] exist (16 HPM event lines). */ + for (int i = 0; i < 29; i++) + { + iss_reg_t counter_mask = (i < num_hpm) ? (iss_reg_t)-1 : 0; + this->mhpmcounter[i].set_write_mask(counter_mask); +#if ISS_REG_WIDTH == 32 + this->mhpmcounterh[i].set_write_mask(counter_mask); +#endif + this->declare_csr(&this->mhpmevent[i], "mhpmevent" + std::to_string(i + 3), + 0x323 + i, 0, (i < num_hpm) ? 0xFFFF : 0); + } + + /* User counter aliases: the RTL read mux maps 0xC00..0xC1F and + * 0xC80..0xC9F straight onto the machine counter bank + * (cv32e40p_cs_registers.sv); writes trap through check_access. */ + this->declare_csr(&this->cycle_alias, "cycle", 0xC00); + this->cycle_alias.register_callback(std::bind(&Cv32e40pCsr::cycle_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->instret_alias, "instret", 0xC02); + this->instret_alias.register_callback(std::bind(&Cv32e40pCsr::instret_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#if ISS_REG_WIDTH == 32 + this->declare_csr(&this->cycleh_alias, "cycleh", 0xC80); + this->cycleh_alias.register_callback(std::bind(&Cv32e40pCsr::cycleh_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->instreth_alias, "instreth", 0xC82); + this->instreth_alias.register_callback(std::bind(&Cv32e40pCsr::instreth_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +#endif + for (int i = 0; i < 29; i++) + { + this->declare_csr(&this->hpmcounter_alias[i], "hpmcounter" + std::to_string(i + 3), + 0xC03 + i); + this->hpmcounter_alias[i].register_callback(std::bind(&Cv32e40pCsr::hpm_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, i)); +#if ISS_REG_WIDTH == 32 + this->declare_csr(&this->hpmcounterh_alias[i], "hpmcounter" + std::to_string(i + 3) + "h", + 0xC83 + i); + this->hpmcounterh_alias[i].register_callback(std::bind(&Cv32e40pCsr::hpmh_alias_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, i)); +#endif + } + + /* Interrupt and trap CSRs: masks from the RTL (cv32e40p_cs_registers.sv). + * mstatus: only MIE/MPIE (and FS with an FPU) are writable; the mask is + * applied by Core::mstatus_update via CONFIG_GVSOC_ISS_CORE_MSTATUS_WRITE_MASK + * set by the recipe. Reset is MPP=M, FS=Off for every configuration. */ + this->mstatus.reset_val = 0x00001800; + /* mie: declarative only — IrqRiscv::mie_access bypasses the register + * write mask; the effective masking is Cv32e40pIrq::mie_write_fixup. */ + this->mie.set_write_mask(Cv32e40pIrq::IRQ_MASK); + /* mip: read-only front-end replaces the base register in the CSR map + * (see csr.hpp). The base object stays as the wire-driven store, its + * IrqRiscv wire callbacks keep writing it directly. */ + this->undeclare_csr(0x344); + this->declare_csr(&this->mip_view, "mip", 0x344); + this->mip_view.register_callback(std::bind(&Cv32e40pCsr::mip_view_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->mtvec.set_write_mask(0xFFFFFF01); + this->mtvec.reset_val = 0x1; + this->mtval.set_write_mask(0); + this->mcause.set_write_mask(0x8000001F); + + /* Trigger module: one trigger, tselect hardwired to 0, tinfo reports + * type 2. tdata1 (only bit 2, execute match enable) and tdata2 (match + * address) latch only from debug mode: tmatch_control_we/tmatch_value_we + * are gated on debug_mode_i, so an M-mode write is silently dropped, + * not an illegal instruction. The execute match itself is evaluated at + * the dispatch boundary, before the matched instruction runs (see + * Cv32e40pIrq::check()). */ + this->tselect.set_write_mask(0); + this->tselect.register_callback(std::bind(&Cv32e40pCsr::tselect_read_zero, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->tdata1.reset_val = 0x28001040; + this->tdata1.set_write_mask(0x4); + this->tdata1.register_callback(std::bind(&Cv32e40pCsr::tdata_debug_gate, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->tdata2.set_write_mask(0xFFFFFFFF); + this->tdata2.register_callback(std::bind(&Cv32e40pCsr::tdata_debug_gate, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->tdata3.set_write_mask(0); + this->declare_csr(&this->tinfo, "tinfo", 0x7A4, 0x4, 0); + this->declare_csr(&this->mcontext, "mcontext", 0x7A8, 0, 0); + this->declare_csr(&this->scontext, "scontext", 0x7AA, 0, 0); + + /* Debug-mode CSRs: views over the base raw fields, kept coherent with + * the debug-entry (Cv32e40pIrq::check) and dret paths which write the + * raw fields directly. Undeclared in the base, they would otherwise + * raise illegal-instruction on the first debug-ROM access. */ + this->declare_csr(&this->dcsr_view, "dcsr", 0x7B0); + this->dcsr_view.register_callback(std::bind(&Cv32e40pCsr::dcsr_view_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->dpc_view, "dpc", 0x7B1); + this->dpc_view.register_callback(std::bind(&Cv32e40pCsr::dpc_view_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->dscratch0_view, "dscratch0", 0x7B2); + this->dscratch0_view.register_callback(std::bind(&Cv32e40pCsr::dscratch0_view_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->dscratch1_view, "dscratch1", 0x7B3); + this->dscratch1_view.register_callback(std::bind(&Cv32e40pCsr::dscratch1_view_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + +#if CONFIG_GVSOC_ISS_CV32E40P_PULP + /* PULP custom CSRs. The RTL decoder raises illegal-instruction on any + * write to them (read-only register type). */ + this->declare_csr(&this->uhartid, "uhartid", 0xCD0, this->mhartid); + this->declare_csr(&this->privlv, "privlv", 0xCD1, 3); +#if !CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA + /* zfinx indicator: reads 1 on ZFINX, 0 without an FPU. With FPU=1 && + * ZFINX=0 the RTL rejects even reads, so it stays undeclared and the + * unsupported-CSR path raises illegal-instruction. */ + this->declare_csr(&this->zfinx_csr, "zfinx", 0xCD2, + CONFIG_GVSOC_ISS_CV32E40P_ZFINX ? 1 : 0); +#endif + + /* Hardware-loop CSRs, readable via CSR instructions only (writes go + * through the cv.* instructions and csrrw raises illegal). The values + * live in the Hwloop module. */ + for (int loop = 0; loop < 2; loop++) + { + static const char *names[] = { "lpstart", "lpend", "lpcount" }; + for (int kind = 0; kind < 3; kind++) + { + int index = loop * 3 + kind; + this->declare_csr(&this->hwloop_csr[index], + names[kind] + std::to_string(loop), 0xCC0 + loop * 4 + kind); + this->hwloop_csr[index].register_callback( + std::bind(&Cv32e40pCsr::hwloop_csr_access, this, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, index)); + } + } +#endif + + /* fflags / frm / fcsr front-ends: legality gated on mstatus.FS by the + * register type, value mapping onto the shared fcsr field here. */ + this->declare_csr(&this->fflags_csr, "fflags", 0x001); + this->fflags_csr.register_callback(std::bind(&Cv32e40pCsr::fflags_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->frm_csr, "frm", 0x002); + this->frm_csr.register_callback(std::bind(&Cv32e40pCsr::frm_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + this->declare_csr(&this->fcsr_csr, "fcsr", 0x003); + this->fcsr_csr.register_callback(std::bind(&Cv32e40pCsr::fcsr_access, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +} + +void Cv32e40pCsr::start() +{ + /* Registered here so it runs after Core::mstatus_update, which is + * registered by the Core constructor (after this class is built) and + * overwrites the read value with the stored one. */ + this->mstatus.register_callback(std::bind(&Cv32e40pCsr::mstatus_read_fixup, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); + + /* Registered here so it runs after IrqRiscv::mtvec_access, which is + * registered by the IrqRiscv constructor and stores the value with the + * mode bit cleared. */ + this->mtvec.register_callback(std::bind(&Cv32e40pCsr::mtvec_write_fixup, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +} + +void Cv32e40pCsr::reset(bool active) +{ + Csr::reset(active); + + if (active) + { + /* dcsr: xdebugver=4 in [31:28], prv=M in [1:0] (base reset leaves + * prv=0). */ + this->dcsr = (4 << 28) | 0x3; + + this->mcycle_offset = 0; + this->minstret_written = false; + this->mcountinhibit_stale = false; + + /* Excluded from the base reset sweep, which walks the CSR map: + * mip left it for the mip_view front-end, hwloop_lpend is a plain + * shadow. */ + this->mip.value = 0; + this->hwloop_lpend[0] = 0; + this->hwloop_lpend[1] = 0; + } +} + +bool Cv32e40pCsr::mstatus_read_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ +#if CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA + /* SD (bit 31) is derived on read: set when FS or XS is Dirty. */ + if (!is_write) + { + if (((value >> 13) & 3) == 3 || ((value >> 15) & 3) == 3) + { + value |= 1ULL << 31; + } + } +#endif + return false; +} + +bool Cv32e40pCsr::mtvec_write_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (!is_write) + { + /* Keep the default read (value = stored register). */ + return true; + } + + /* RTL WARL result (cv32e40p_cs_registers.sv): base = wdata[31:8], + * bits [7:1] read 0, mode = wdata[0]; the reset / boot-address path + * (insn == NULL) forces mode = 1 (MTVEC_MODE). IrqRiscv::mtvec_access + * ran before this and stored the value with the mode bit cleared. */ + iss_reg_t mode = (insn == NULL) ? 1 : (value & 1); + this->mtvec.value = (value & 0xFFFFFF00) | mode; + return false; +} + +bool Cv32e40pCsr::tselect_read_zero(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* One trigger: tselect always reads 0 (the base callback reads -1). */ + if (!is_write) + { + value = 0; + } + return false; +} + +bool Cv32e40pCsr::tdata_debug_gate(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* tdata1/tdata2 writes latch only in debug mode; outside it the RTL + * drops them silently (no illegal-instruction), reads are unrestricted. */ + return !is_write || this->iss.exec.debug_mode; +} + +bool Cv32e40pCsr::mip_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* Reads mirror the wire-driven register; writes are dropped. */ + if (!is_write) + { + value = this->mip.value; + } + return false; +} + +/* RTL WARL (cv32e40p_cs_registers.sv, CSR_DCSR): writable bits are + * ebreakm(15), ebreaku(12), stepie(11) and step(2). prv[1:0] is WARL-3: + * the core is M-mode only, so any written value reads back as M. + * xdebugver, cause and the hardwired-zero fields keep the stored value, + * which the debug entry writes directly. */ +bool Cv32e40pCsr::dcsr_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + constexpr iss_reg_t WRITABLE = (1u << 15) | (1u << 12) | (1u << 11) | (1u << 2); + this->dcsr = (this->dcsr & ~WRITABLE) | (value & WRITABLE) | 0x3; + } + else + { + value = this->dcsr; + } + return false; +} + +/* RTL forces 16-bit alignment on dpc writes (depc_n = wdata & ~1). */ +bool Cv32e40pCsr::dpc_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->depc = value & ~(iss_reg_t)1; + } + else + { + value = this->depc; + } + return false; +} + +bool Cv32e40pCsr::dscratch0_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->scratch0 = value; + } + else + { + value = this->scratch0; + } + return false; +} + +bool Cv32e40pCsr::dscratch1_view_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->scratch1 = value; + } + else + { + value = this->scratch1; + } + return false; +} + +uint64_t Cv32e40pCsr::mcycle_count() +{ +#if ISS_REG_WIDTH == 32 + uint64_t frozen = ((uint64_t)this->mcycleh.value << 32) | this->mcycle.value; +#else + uint64_t frozen = this->mcycle.value; +#endif + if (this->mcountinhibit.value & 0x1) + { + return frozen; + } + return (uint64_t)((int64_t)this->iss.clock.get_cycles() + this->mcycle_offset); +} + +void Cv32e40pCsr::mcycle_set(uint64_t count) +{ + this->mcycle.value = (iss_reg_t)count; +#if ISS_REG_WIDTH == 32 + this->mcycleh.value = (iss_reg_t)(count >> 32); +#endif + /* While frozen (CY set) this offset is dead: mcountinhibit_access + * recomputes it from the register pair at unfreeze time. */ + this->mcycle_offset = (int64_t)count - (int64_t)this->iss.clock.get_cycles(); +} + +bool Cv32e40pCsr::mcycle_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->mcycle_set((this->mcycle_count() & ~(uint64_t)0xFFFFFFFF) | value); + } + else + { + value = (iss_reg_t)this->mcycle_count(); + } + return false; +} + +bool Cv32e40pCsr::minstret_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* Arm the same-row increment suppression; the store itself is the + * default masked one (return true). */ + if (is_write) + { + this->minstret_written = true; + } + return true; +} + +bool Cv32e40pCsr::minstreth_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* RTL suppresses the increment on a write to EITHER half. */ + if (is_write) + { + this->minstret_written = true; + } + return true; +} + +bool Cv32e40pCsr::mcycleh_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->mcycle_set(((uint64_t)value << 32) | (uint32_t)this->mcycle_count()); + } + else + { + value = (iss_reg_t)(this->mcycle_count() >> 32); + } + return false; +} + +void Cv32e40pCsr::fp_state_dirty() +{ +#if CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA + /* A trapped FP instruction has no architectural side effects; the raise + * (reserved rounding mode, isa_lib int.h) runs inside the write-back + * macro before this call. */ + if (this->iss.exec.has_exception) + return; + this->mstatus.fs = 3; +#endif +} + +/* User counter aliases: read-only mirrors of the machine counters (writes + * never reach these callbacks, Cv32e40pCounterAlias::check_access traps + * them first). */ +bool Cv32e40pCsr::cycle_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + value = (iss_reg_t)this->mcycle_count(); + return false; +} + +bool Cv32e40pCsr::cycleh_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + value = (iss_reg_t)(this->mcycle_count() >> 32); + return false; +} + +bool Cv32e40pCsr::instret_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + value = this->minstret.value; + return false; +} + +bool Cv32e40pCsr::instreth_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ +#if ISS_REG_WIDTH == 32 + value = this->minstreth.value; +#endif + return false; +} + +bool Cv32e40pCsr::hpm_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index) +{ + value = this->mhpmcounter[index].value; + return false; +} + +bool Cv32e40pCsr::hpmh_alias_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index) +{ +#if ISS_REG_WIDTH == 32 + value = this->mhpmcounterh[index].value; +#endif + return false; +} + +bool Cv32e40pCsr::mcountinhibit_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + /* Freeze the count into the register pair when CY sets, re-anchor the + * clock offset to the frozen value when CY clears. Runs before the + * masked store, so this->mcountinhibit.value still holds the old CY. */ + if (is_write) + { + /* Event lines fire only from the full handlers (same scheme as the + * Ri5ky PCMR write): switch when software touches the inhibit CSR. */ + this->iss.exec.switch_to_full_mode(); + /* The writing instruction itself still counts under the OLD gates + * (consumed by hpm_commit at this same retire). */ + this->mcountinhibit_old = this->mcountinhibit.value; + this->mcountinhibit_stale = true; + bool old_cy = this->mcountinhibit.value & 0x1; + bool new_cy = value & 0x1; + if (old_cy != new_cy) + { + uint64_t count = this->mcycle_count(); + if (new_cy) + { + this->mcycle.value = (iss_reg_t)count; +#if ISS_REG_WIDTH == 32 + this->mcycleh.value = (iss_reg_t)(count >> 32); +#endif + } + else + { + this->mcycle_offset = (int64_t)count - (int64_t)this->iss.clock.get_cycles(); + } + } + } + return true; +} + +bool Cv32e40pCsr::hwloop_csr_access(iss_insn_t *insn, bool is_write, iss_reg_t &value, int index) +{ + int loop = index / 3; + + switch (index % 3) + { + case 0: value = this->iss.hwloop.get_start(loop); break; + /* The Hwloop module stores the loop-back point, LPEND - 4; the + * architectural value lives in the shadow the setters keep. */ + case 1: value = this->hwloop_lpend[loop]; break; + case 2: value = this->iss.hwloop.get_count(loop); break; + } + return false; +} + +bool Cv32e40pCsr::fflags_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->fcsr.fflags = value; + /* RTL: an fflags write (fflags_we_i) forces mstatus.FS=Dirty. */ + this->fp_state_dirty(); + } + else + { + value = this->fcsr.fflags; + } + return false; +} + +bool Cv32e40pCsr::frm_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->fcsr.frm = value; + this->fp_state_dirty(); + } + else + { + value = this->fcsr.frm; + } + return false; +} + +bool Cv32e40pCsr::fcsr_access(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (is_write) + { + this->fcsr.raw = value & 0xff; + this->fp_state_dirty(); + } + else + { + value = this->fcsr.raw; + } + return false; +} diff --git a/cpu/iss_v2/src/cores/cv32e40p/events.cpp b/cpu/iss_v2/src/cores/cv32e40p/events.cpp new file mode 100644 index 00000000..1b418aaa --- /dev/null +++ b/cpu/iss_v2/src/cores/cv32e40p/events.cpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#include + +void Cv32e40pEvents::reset(bool active) +{ + Events::reset(active); + if (active) + { + this->pending_events = 0; + this->commit_push = 0; + this->commit_pop = 0; + this->inflight_push = 0; + this->inflight_pop = 0; + } +} diff --git a/cpu/iss_v2/src/cores/cv32e40p/exception.cpp b/cpu/iss_v2/src/cores/cv32e40p/exception.cpp new file mode 100644 index 00000000..f296bf6b --- /dev/null +++ b/cpu/iss_v2/src/cores/cv32e40p/exception.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +#include + +Cv32e40pException::Cv32e40pException(Iss &iss) +: Exception(iss), iss(iss) +{ + /* dm_exception_addr_i of the RTL: exceptions taken while in debug + * mode enter here (RISC-V debug spec 0.13.2). Falls back to + * the debug handler entry when the platform config lacks the key. */ + js::Config *conf = iss.get_js_config()->get("debug_exception_handler"); + this->debug_exception_handler_addr = + conf != NULL ? (iss_addr_t)conf->get_int() : this->debug_handler_addr; +} + +void Cv32e40pException::raise(iss_reg_t pc, int id) +{ + /* Commit-stream consumers gate state compares on this (events.hpp). */ + this->iss.timing.trap_seq++; + + /* Exception taken while in debug mode (RISC-V debug spec 0.13.2, + * CV32E40P manual): the hart jumps to dm_exception_addr and stays in + * debug mode; mepc/mcause/mstatus and the privilege mode are NOT + * updated. The generic raise would route to mtvec and clobber them. */ + if (id != ISS_EXCEPT_DEBUG && this->iss.exec.debug_mode) + { + this->iss.exec.switch_to_full_mode(); + this->iss.exec.has_exception = true; + this->iss.exec.exception_pc = + this->debug_exception_handler_addr & ~(iss_reg_t)0x3; + return; + } + + this->Exception::raise(pc, id); + + /* Exceptions enter at the mtvec base. mtvec.value keeps the RTL mode + * bits (mtvec[1:0] = 01) and the generic raise copies it verbatim + * into the entry PC. Assumes the mtvec-based entry path, i.e. + * CONFIG_GVSOC_ISS_RISCV_EXCEPTIONS (always set by Cv32e40pIrq). */ + if (id != ISS_EXCEPT_DEBUG) + { + this->iss.exec.exception_pc &= ~(iss_reg_t)0x3; + } +} diff --git a/cpu/iss_v2/src/cores/cv32e40p/irq.cpp b/cpu/iss_v2/src/cores/cv32e40p/irq.cpp new file mode 100644 index 00000000..2c3b3d7e --- /dev/null +++ b/cpu/iss_v2/src/cores/cv32e40p/irq.cpp @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: 2026 Fondazione Chips-it +// +// SPDX-License-Identifier: Apache-2.0 +// +// Authors: Marco Paci (marco.paci@chips.it) + +/* CV32E40P interrupt personality for iss_v2. + * + * The generic IrqRiscv take (irq_riscv.cpp check()) uses the standard + * RISC-V priority ladder and jumps to the mtvec base for every interrupt. + * The RTL differs on both counts (cv32e40p_int_controller.sv, + * cv32e40p_controller.sv): the fast lines irq[31:16] outrank MEI/MSI/MTI, + * and vectored mode sends each interrupt to base + 4*id. This override + * implements the RTL behaviour; delivery (mip update, WFI wake-up) stays + * on the inherited wire-sync path. */ + +#include +#include + +/* Debug halt request line (RTL debug_req_i), a first-class wire like the + * interrupt lines. Handling it inside the model lets the model run the + * full RTL semantics - in particular waking a WFI-parked hart: the RTL + * sleep unit exits on debug_req_i regardless of mie/mip, while the generic + * check_interrupts() release is gated on (mie & mip) alone. */ +Cv32e40pIrq::Cv32e40pIrq(Iss &iss) : IrqRiscv(iss) +{ + this->haltreq_itf.set_sync_meth(&Cv32e40pIrq::haltreq_sync); + this->iss.new_slave_port("haltreq", &this->haltreq_itf, (vp::Block *)this); + this->wfi_wake_itf.set_sync_meth(&Cv32e40pIrq::wfi_wake_sync); + this->iss.new_slave_port("wfi_wake", &this->wfi_wake_itf, (vp::Block *)this); +} + +void Cv32e40pIrq::start() +{ + /* Registered here so it runs after IrqRiscv::mie_access (bound in the + * base constructor), which stores the written value unmasked and + * suppresses the register's own write mask. */ + this->iss.csr.mie.register_callback(std::bind(&Cv32e40pIrq::mie_write_fixup, + this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +} + +void Cv32e40pIrq::reset(bool active) +{ + IrqRiscv::reset(active); + + if (active) + { + /* A live single-step window or an unconsumed collision id must not + * survive the reset: stale step_pc would fire a phantom cause=4 + * entry at the boot address, a stale collision id would make the + * first post-reset debug entry take an interrupt that never + * happened. haltreq_level is deliberately kept: it mirrors the + * actual wire level, which the reset does not change. */ + this->step_state = 0; + this->collide_irq_id = -1; + this->collide_certify = false; + this->req_debug_cause = 3; + } +} + +/* Full WFI release: the same three-step sequence as check_interrupts() - + * the held WFI entry must drain into the commit stream, a bare wfi-flag + * clear would leave it parked forever. */ +void Cv32e40pIrq::release_wfi() +{ + if (this->iss.exec.wfi.get()) + { + this->iss.exec.wfi.set(false); + this->iss.exec.retain_dec(); + this->iss.exec.insn_terminate(this->wfi_entry); + } +} + +/* Debug halt request wire (RTL debug_req_i). + * + * Arms req_debug - consumed by check() at the next dispatch, exactly like + * an externally armed request - and wakes a WFI-parked hart: the + * RTL sleep unit exits sleep on debug_req_i regardless of pending + * interrupts (cv32e40p_sleep_unit.sv / controller wake-up), while the + * generic check_interrupts() release is gated on (mie & mip) alone, so a + * halt request arriving with mie=0 would otherwise never wake the model. */ +void Cv32e40pIrq::haltreq_sync(vp::Block *__this, bool value) +{ + Cv32e40pIrq *_this = (Cv32e40pIrq *)__this; + + _this->haltreq_level = value; /* check() re-arms from a held-high level */ + + if (!value) + { + return; /* level deassert: an armed req_debug stays latched */ + } + + _this->req_debug = true; /* req_debug_cause keeps its default (3 = haltreq) */ + + _this->release_wfi(); +} + +/* wfi_wake wire: releases a WFI-parked hart with no architectural side + * effect (release_wfi is only callable inside the model); driven + * externally when the DUT's retire stream proves the wake happened + * - the RTL retires wfi at execute and sleeps after, and wake sources + * like a debug_req level are not all visible as interrupt wires. The + * terminated entry drains into the commit stream, serving the DUT's own + * wfi retire. */ +void Cv32e40pIrq::wfi_wake_sync(vp::Block *__this, bool value) +{ + Cv32e40pIrq *_this = (Cv32e40pIrq *)__this; + + if (!value) + { + return; /* deassert edge of the pulse */ + } + + _this->release_wfi(); +} + +/* dret with dcsr.step=1 (priv.hpp dret_exec, called before dret_handle + * while depc is still live): opens the single-step window. The RTL + * controller re-enters debug after one instruction (cv32e40p_controller.sv + * debug_single_step_i); the matching entry is armed by check() once the + * stepped instruction is done. */ +void Cv32e40pIrq::dret_step_check() +{ + if ((this->iss.csr.dcsr >> 2) & 1) + { + this->step_state = 1; + this->step_pc = this->iss.csr.depc; + } +} + +/* RTL WARL result: only the wired interrupt lines are writable in mie + * (cv32e40p_cs_registers.sv, csr_mie_wdata & IRQ_MASK). */ +bool Cv32e40pIrq::mie_write_fixup(iss_insn_t *insn, bool is_write, iss_reg_t &value) +{ + if (!is_write) + { + return true; + } + this->iss.csr.mie.value &= IRQ_MASK; + return false; +} + +/* RTL priority: irq[31] highest, down to irq[16], then MEI(11), MSI(3), + * MTI(7). The caller guarantees at least one bit of IRQ_MASK is set. */ +static int cv32e40p_irq_pick(iss_reg_t pending) +{ + for (int id = 31; id >= 16; id--) + { + if ((pending >> id) & 1) + { + return id; + } + } + if ((pending >> 11) & 1) return 11; + if ((pending >> 3) & 1) return 3; + return 7; +} + +int Cv32e40pIrq::check() +{ + /* Cause arbitration on a shared entry boundary follows the RTL, which + * has TWO distinct entry paths: + * - DBG_TAKEN_ID (kill of the ID insn): TRIGGER (highest) > EBREAK > + * HALTREQ. The trigger block therefore OVERRIDES an armed haltreq - + * e.g. the async wire edge that latched before the matched boundary; + * the level re-arm serves it after dret, like the RTL re-halt. + * - DBG_TAKEN_IF (single-step window close): its cause mux never + * looks at trigger_match, so an armed STEP request (cause 4) is + * NOT overridden even when the next insn sits at tdata2 - the + * trigger fires on the following session instead. An armed cause + * 1/2 (injected ebreak/trigger, DUT-observed) is left alone too. */ + + /* Execute-address trigger (trigger module, mcontrol): the match fires + * BEFORE the instruction at tdata2 executes (RTL trigger_match_o on + * pc_id), entering debug with dcsr.cause=2 and dpc = the matched PC. + * Evaluated at the dispatch boundary so the matched instruction is + * never retired - a batched co-sim step cannot run past the entry. + * Only the slow dispatch handler runs check(): the co-sim personality + * pins it; a standalone fast-mode run does not evaluate triggers. */ + /* The step-window guard mirrors DBG_TAKEN_IF: when the window closes + * at this boundary (armed and the stepped insn is done) the step entry + * wins even if the NEXT insn sits at tdata2 - the trigger block runs + * first in program order, so it must yield explicitly. A window still + * on its own step_pc (dret straight onto the matched insn) does not + * close here and the trigger fires as on the RTL. */ + bool trigger_match = + (this->iss.csr.tdata1.value & (1u << 2)) && + !this->iss.exec.debug_mode && + (!this->req_debug || this->req_debug_cause == 3) && + !(this->step_state && this->iss.exec.current_insn != this->step_pc) && + this->iss.exec.current_insn == this->iss.csr.tdata2.value; + if (trigger_match) + { + this->req_debug = true; + this->req_debug_cause = 2; + } + + /* One-shot async gate, consumed AFTER the trigger match: the execute + * trigger is synchronous debug (mcontrol timing=before), not an + * asynchronous event, so it fires even on a suppressed dispatch and + * falls through to the entry below. Everything past this point - + * haltreq wire re-arm, step window, interrupt ladder - is asynchronous + * and stays out of a suppressed boundary (DPI lockstep stepping: the + * engine holds the line high and injects takes explicitly). */ + if (this->iss.exec.skip_irq_check) + { + this->iss.exec.skip_irq_check = false; + if (!trigger_match) + { + return 0; + } + } + + /* Held-high haltreq re-arms (RTL debug_req_i is level-sensitive: the + * hart re-halts right after dret while the line stays asserted; the + * wire itself only syncs on level changes). Under the co-sim hold the + * re-arm waits for the driver's injection window: the level is state, + * the halt boundary is the DUT's to prove (dpi_async_hold contract). */ + if (this->haltreq_level && !this->dpi_async_hold && + !this->req_debug && !this->iss.exec.debug_mode) + { + this->req_debug = true; /* req_debug_cause keeps its default (3) */ + } + + /* Single-step window (dcsr.step, armed by dret_step_check): re-enter + * debug with cause=4 once the stepped instruction is done. "Done" is + * current_insn != step_pc: a completed instruction moved the PC, and + * an exception during the step lands here after the has_exception + * redirect (consumed at the top of the dispatch, before check()), so + * depc points at the handler entry, as the Debug spec requires. While + * current_insn == step_pc the instruction has not executed yet (fetch + * or scoreboard stalls re-run check() on the same boundary). The + * window is closed by the entry itself, whatever the winning cause. */ + if (this->step_state && !this->iss.exec.debug_mode && !this->req_debug) + { + if (this->iss.exec.current_insn != this->step_pc) + { + this->req_debug = true; + this->req_debug_cause = 4; + } + } + + /* Debug entry: generic implementation plus dcsr.cause, written + * atomically with the entry as the RTL does. The cause comes from + * req_debug_cause: 3 (haltreq) on the wire path, 2 (trigger) from the + * local execute-trigger match above, 1 (ebreak) or 4 (single-step) + * when armed by an external debug-entry request. */ + if (this->req_debug && !this->iss.exec.debug_mode) + { + /* Wire-armed haltreq entry (cause 3) under the co-sim hold: stay + * latched. The haltreq_sync edge arms req_debug at net-delivery + * time, which is row-granular - taking at the next dispatch would + * race the DUT's own halt boundary (the same race as the interrupt + * ladder below). The request is not lost: the driver's take_debug + * window lowers the hold and the entry lands on the DUT-proven + * boundary. Driver-armed causes (1 ebreak / 4 step) only ever run + * inside such a window; the synchronous trigger match (cause 2, + * set above) keeps its architectural boundary and enters here. */ + if (this->dpi_async_hold && this->req_debug_cause == 3 && !trigger_match) + { + return 0; + } + /* Informed interrupt+debug collision: the RTL takes the interrupt + * first and enters debug on the first handler instruction - dpc is + * the (vectored) entry and mstatus/mepc/mcause carry the take; the + * handler instruction itself never executes. Whether the collision + * happened is decided by the DUT (its entry row carries the take's + * CSR writes), never guessed here: the arbitration outcome depends + * on cycle timing the model cannot see. Both trap_seq bumps land + * before debug_mode flips, so an external observer sees one + * atomic entry. */ + if (this->collide_irq_id >= 0) + { + /* Defense in depth: irq_take's + * priority pick falls back to MTI when no IRQ_MASK bit is set, + * so an unwired id would silently become a phantom cause-7 + * take. The id is DUT-provided (mcause & 0x1f), never trusted + * blindly. */ + iss_reg_t line = (this->collide_irq_id < 32) ? + (((iss_reg_t)1 << this->collide_irq_id) & IRQ_MASK) : 0; + /* Adjacent-row candidates carry the take's mepc and are + * certified HERE, where current_insn is the entry boundary + * (the future depc source): a stale-mcause candidate - a take + * this hart already followed rows ago - parks the boundary + * elsewhere and is discarded without a take. Same-row + * candidates (collide_certify=false) keep the unconditional + * behaviour. */ + if (this->collide_certify && + this->iss.exec.current_insn != this->collide_expected_mepc) + { + this->trace.msg(vp::Trace::LEVEL_WARNING, + "Informed IRQ+debug collision id %d discarded: entry " + "boundary 0x%x != take mepc 0x%x\n", + this->collide_irq_id, + (unsigned)this->iss.exec.current_insn, + (unsigned)this->collide_expected_mepc); + } + else if (line) + { + this->irq_take(line); + } + else + { + this->trace.msg(vp::Trace::LEVEL_WARNING, + "Informed IRQ+debug collision with unwired cause id %d - " + "take skipped\n", this->collide_irq_id); + } + this->collide_irq_id = -1; + this->collide_certify = false; + } + + /* Any entry closes a live single-step window: without this, a + * haltreq during the window (or a dret whose debugger cleared + * dcsr.step) leaves step_state armed on a stale step_pc and the + * next boundary would fire a spurious cause=4 entry. */ + this->step_state = 0; + this->iss.exec.debug_mode = true; + this->iss.csr.depc = this->iss.exec.current_insn; + this->iss.csr.dcsr = (this->iss.csr.dcsr & ~(0x7u << 6)) | + ((iss_reg_t)(this->req_debug_cause & 0x7) << 6); + this->req_debug_cause = 3; + /* Commit-stream consumers gate state compares on this (events.hpp). */ + this->iss.timing.trap_seq++; + this->debug_saved_irq_enable = this->irq_enable.get(); + this->irq_enable.set(0); + this->req_debug = false; + this->iss.exec.current_insn = this->debug_handler; + return 1; + } + + /* No interrupt is taken in debug mode (the RTL controller ignores + * irq_req entirely there). Explicit guard: inside the take_debug + * injection window the defense is down until the first debug-ROM + * commit lands, so check() can run again right after the entry and + * the ladder below would hijack it with a pending line. */ + if (this->iss.exec.debug_mode) + { + return 0; + } + + /* Inside the single-step window interrupts are masked unless + * dcsr.stepie=1 (bit 11): the RTL controller holds irq_req off while + * single-stepping (cv32e40p_controller.sv debug_single_step_i). */ + if (this->step_state && !((this->iss.csr.dcsr >> 11) & 1)) + { + return 0; + } + + /* Co-sim hold: pending wired interrupts stay pending. The ladder take + * below picks a boundary out of the model's own stepping cadence, which + * races the DUT's controller timing; the lockstep driver injects the + * take at the DUT-proven entry boundary instead (take_irq window). */ + if (this->dpi_async_hold) + { + return 0; + } + + /* M-mode only core: the take needs a wired pending line and the global + * enable (mstatus.MIE). */ + iss_reg_t pending = this->iss.csr.mie.value & this->iss.csr.mip.value & IRQ_MASK; + if (!pending || !this->iss.csr.mstatus.mie) + { + return 0; + } + + this->irq_take(pending); + + return 1; +} + +/* Interrupt take with the RTL priority order and vectored entry. The + * caller guarantees at least one bit of pending (mie & mip & IRQ_MASK) + * and mstatus.MIE=1. Shared by the ladder in check() and the + * simultaneous-interrupt path of the debug entry. */ +void Cv32e40pIrq::irq_take(iss_reg_t pending) +{ + int irq = cv32e40p_irq_pick(pending); + + /* mtvec holds {base[31:8], 0, mode} (Cv32e40pCsr::mtvec_write_fixup); + * vectored mode enters at base + 4*id, direct mode at base. */ + iss_reg_t base = this->iss.csr.mtvec.value & 0xFFFFFF00; + iss_reg_t entry = (this->iss.csr.mtvec.value & 1) ? base + (irq << 2) : base; + + this->trace.msg(vp::Trace::LEVEL_TRACE, "Handling IRQ (irq: %d, entry: 0x%lx)\n", + irq, entry); + + /* Commit-stream consumers gate state compares on this (events.hpp). */ + this->iss.timing.trap_seq++; + + this->iss.exec.interrupt_taken(); + this->iss.csr.mepc.value = this->iss.exec.current_insn; + this->iss.csr.mstatus.mpie = this->iss.csr.mstatus.mie; + this->iss.csr.mstatus.mie = 0; + this->iss.csr.mstatus.mpp = this->iss.core.mode_get(); + this->iss.csr.mcause.value = (1ULL << (ISS_REG_WIDTH - 1)) | (unsigned int)irq; + this->iss.exec.current_insn = entry; + this->iss.core.mode_set(PRIV_M); + this->irq_enable.set(0); + + this->iss.timing.stall_insn_dependency_account(4); +} diff --git a/cv32e40p-standalone.py b/cv32e40p-standalone.py new file mode 100644 index 00000000..1af7f939 --- /dev/null +++ b/cv32e40p-standalone.py @@ -0,0 +1,145 @@ +# +# Copyright (C) 2020 GreenWaves Technologies, SAS, ETH Zurich and +# University of Bologna +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P standalone GVSOC target for UVM co-simulation with RVVI bridge. +# +# Follows the pattern of tutorial 17 (how_to_control_gvsoc_from_an_external_simulator) +# adapted for CV32E40P (ri5cy/PULP FC core) with correct memory map. +# +# Memory map (from cv32e40p/bsp/): +# 0x00000000 4MB Main RAM (entry point 0x00000080) +# 0x10000000 256B Virtual STDOUT (write-only sink) +# 0x15000000 256B Virtual TIMER (write-only sink) +# 0x1A110800 4KB Debug ROM (linker script `dbg` region) +# 0x20000000 256B Virtual EXIT (terminates the simulation) +# everywhere else background sparse memory (default route: reads 0 until +# written, writes persist - same as the UVM testbench) + +import memory.memory +import vp.clock_domain +import interco.router +import utils.loader.loader +import gvsoc.systree +import gvsoc.runner +from gvrun.parameter import TargetParameter +from pulp.cpu.iss.pulp_cores import cv32e40p +from pulp.cv32e40p_exit.cv32e40p_exit_device import Cv32e40pExitDevice +from pulp.cv32e40p_sparse_mem.cv32e40p_sparse_mem import Cv32e40pSparseMem + + +class Cv32e40pSoc(gvsoc.systree.Component): + + def __init__(self, parent, name, binary, corev_pulp, fpu, zfinx, corev_cluster, core_version, + num_mhpmcounters=1): + super().__init__(parent, name) + + # Main interconnect + ico = interco.router.Router(self, 'ico') + + # 4MB main RAM @ 0x00000000 (entry point 0x00000080) + mem = memory.memory.Memory(self, 'mem', size=0x00400000, init=False) + ico.o_MAP(mem.i_INPUT(), 'mem', base=0x00000000, size=0x00400000, rm_base=True) + + # Virtual STDOUT sink @ 0x10000000 (256B) + stdout_mem = memory.memory.Memory(self, 'stdout', size=0x100, init=False) + ico.o_MAP(stdout_mem.i_INPUT(), 'stdout', base=0x10000000, size=0x100, rm_base=True) + + # Virtual TIMER sink @ 0x15000000 (256B) + timer_mem = memory.memory.Memory(self, 'timer', size=0x100, init=False) + ico.o_MAP(timer_mem.i_INPUT(), 'timer', base=0x15000000, size=0x100, rm_base=True) + + # Debug ROM @ 0x1A110800 (4KB, matches the linker script `dbg` region) + debug_mem = memory.memory.Memory(self, 'debug_rom', size=0x1000, init=False) + ico.o_MAP(debug_mem.i_INPUT(), 'debug_rom', base=0x1A110800, size=0x1000, rm_base=True) + + # Virtual EXIT device @ 0x20000000 (256B) + # Terminates GVSOC when the program writes exit_valid to offset +0x04 + exit_dev = Cv32e40pExitDevice(self, 'exit') + ico.o_MAP(exit_dev.i_INPUT(), 'exit', base=0x20000000, size=0x100, rm_base=True) + + # Background sparse memory: default route for everything not mapped + # above (size=0 mapping). The UVM testbench serves the whole address + # space from a zero-default sparse memory; without this, out-of-map + # stores are dropped (readback diverges) and out-of-map fetches fault + # with mcause=1 where the RTL executes 0 and traps illegal (mcause=2). + # Absolute addresses are forwarded (rm_base=False) so the store is + # indexed like the testbench's. + bg_mem = Cv32e40pSparseMem(self, 'background_mem') + ico.o_MAP(bg_mem.i_INPUT(), 'background', base=0x00000000, size=0, rm_base=False) + + # CV32E40P core: uses the cv32e40p model which sets CONFIG_ISS_CORE=cv32e40p + # and computes misa/mimpid from fpu/zfinx/pulpv2 to match the RTL configuration. + core = cv32e40p(self, 'core', fetch_enable=False, boot_addr=0x00000080, + cluster_id=0, pulpv2=corev_pulp, fpu=fpu, zfinx=zfinx, + corev_cluster=corev_cluster, core_version=core_version, + num_mhpmcounters=num_mhpmcounters) + core.o_FETCH(ico.i_INPUT()) + core.o_DATA(ico.i_INPUT()) + + + # ELF loader: loads binary into RAM, signals core entry point and fetch enable + loader = utils.loader.loader.ElfLoader(self, 'loader', binary=binary) + loader.o_OUT(ico.i_INPUT()) + loader.o_START(core.i_FETCHEN()) + loader.o_ENTRY(core.i_ENTRY()) + + +# Wrapping component that attaches a clock generator, following tutorial 17 pattern. +class Cv32e40p(gvsoc.systree.Component): + + def __init__(self, parent, name=None): + super().__init__(parent, name) + + binary = TargetParameter( + self, name='binary', value=None, description='ELF binary to simulate' + ).get_value() + + # Core configuration parameters — match CV32E40P RTL generics. + # Pass via --parameter on the gvrun command line (default = base config). + corev_pulp = TargetParameter(self, name='corev_pulp', value=False, + description='Enable PULP extensions (COREV_PULP RTL param)').get_value() + fpu = TargetParameter(self, name='fpu', value=False, + description='Enable FPU (FPU RTL param)').get_value() + zfinx = TargetParameter(self, name='zfinx', value=False, + description='FPU uses integer regfile (ZFINX RTL param)').get_value() + corev_cluster = TargetParameter(self, name='corev_cluster', value=False, + description='Cluster variant (COREV_CLUSTER RTL param)').get_value() + core_version = TargetParameter(self, name='core_version', value=2, + description='ISA version: 1 for legacy PULP, 2 for CORE-V v2').get_value() + num_mhpmcounters = TargetParameter(self, name='num_mhpmcounters', value=1, + description='Number of HPM counters (NUM_MHPMCOUNTERS RTL param)').get_value() + + clock = vp.clock_domain.Clock_domain(self, 'clock', frequency=50000000) + soc = Cv32e40pSoc(self, 'soc', binary, + corev_pulp=corev_pulp, fpu=fpu, + zfinx=zfinx, corev_cluster=corev_cluster, + core_version=core_version, + num_mhpmcounters=num_mhpmcounters) + clock.o_CLOCK(soc.i_CLOCK()) + + +# Top target that gvrun will instantiate (mirrors tutorial 17 Target class structure) +class Target(gvsoc.runner.Target): + + description = "CV32E40P standalone for UVM co-simulation" + model = Cv32e40p + name = "cv32e40p-standalone" diff --git a/cv32e40p-v2-spike-fpu.py b/cv32e40p-v2-spike-fpu.py new file mode 100644 index 00000000..9e6124ea --- /dev/null +++ b/cv32e40p-v2-spike-fpu.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 bring-up target, FPU configuration (rv32imf + CoreV). + +import gvsoc.runner +from pulp.cv32e40p_v2_spike import Cv32e40pSpikeTop + + +class Cv32e40p(Cv32e40pSpikeTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name, fpu=1) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 bring-up (FPU)" + model = Cv32e40p + name = "cv32e40p-v2-spike-fpu" diff --git a/cv32e40p-v2-spike-zfinx.py b/cv32e40p-v2-spike-zfinx.py new file mode 100644 index 00000000..91068747 --- /dev/null +++ b/cv32e40p-v2-spike-zfinx.py @@ -0,0 +1,38 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 bring-up target, ZFINX configuration (FP on the integer +# register file). + +import gvsoc.runner +from pulp.cv32e40p_v2_spike import Cv32e40pSpikeTop + + +class Cv32e40p(Cv32e40pSpikeTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name, zfinx=1) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 bring-up (ZFINX)" + model = Cv32e40p + name = "cv32e40p-v2-spike-zfinx" diff --git a/cv32e40p-v2-spike.py b/cv32e40p-v2-spike.py new file mode 100644 index 00000000..8aadbd22 --- /dev/null +++ b/cv32e40p-v2-spike.py @@ -0,0 +1,39 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 bring-up target, integer configuration (rv32im + CoreV). +# Platform in pulp/cv32e40p_v2_spike.py, shared by the cv32e40p-v2-spike* +# variants. + +import gvsoc.runner +from pulp.cv32e40p_v2_spike import Cv32e40pSpikeTop + + +class Cv32e40p(Cv32e40pSpikeTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 bring-up" + model = Cv32e40p + name = "cv32e40p-v2-spike" diff --git a/cv32e40p-v2-standalone-fpu.py b/cv32e40p-v2-standalone-fpu.py new file mode 100644 index 00000000..a2384984 --- /dev/null +++ b/cv32e40p-v2-standalone-fpu.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 co-simulation target, FPU configuration (rv32imfc + CoreV). + +import gvsoc.runner +from pulp.cv32e40p_v2_standalone import Cv32e40pStandaloneTop + + +class Cv32e40p(Cv32e40pStandaloneTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name, fpu=1) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 standalone for UVM co-simulation (FPU)" + model = Cv32e40p + name = "cv32e40p-v2-standalone-fpu" diff --git a/cv32e40p-v2-standalone-nopulp.py b/cv32e40p-v2-standalone-nopulp.py new file mode 100644 index 00000000..e63d0537 --- /dev/null +++ b/cv32e40p-v2-standalone-nopulp.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 co-simulation target, no-PULP configuration (rv32imc, no CoreV extensions). + +import gvsoc.runner +from pulp.cv32e40p_v2_standalone import Cv32e40pStandaloneTop + + +class Cv32e40p(Cv32e40pStandaloneTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name, pulp=0) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 standalone for UVM co-simulation (no PULP)" + model = Cv32e40p + name = "cv32e40p-v2-standalone-nopulp" diff --git a/cv32e40p-v2-standalone-zfinx.py b/cv32e40p-v2-standalone-zfinx.py new file mode 100644 index 00000000..8fb586c7 --- /dev/null +++ b/cv32e40p-v2-standalone-zfinx.py @@ -0,0 +1,38 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 co-simulation target, ZFINX configuration (rv32imfc + CoreV, +# FP operations on the integer register file). + +import gvsoc.runner +from pulp.cv32e40p_v2_standalone import Cv32e40pStandaloneTop + + +class Cv32e40p(Cv32e40pStandaloneTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name, zfinx=1) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 standalone for UVM co-simulation (ZFINX)" + model = Cv32e40p + name = "cv32e40p-v2-standalone-zfinx" diff --git a/cv32e40p-v2-standalone.py b/cv32e40p-v2-standalone.py new file mode 100644 index 00000000..c7cde8db --- /dev/null +++ b/cv32e40p-v2-standalone.py @@ -0,0 +1,37 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 co-simulation target, base configuration (rv32imc + CoreV). + +import gvsoc.runner +from pulp.cv32e40p_v2_standalone import Cv32e40pStandaloneTop + + +class Cv32e40p(Cv32e40pStandaloneTop): + + def __init__(self, parent, name=None): + super().__init__(parent, name) + + +class Target(gvsoc.runner.Target): + + description = "CV32E40P iss_v2 standalone for UVM co-simulation" + model = Cv32e40p + name = "cv32e40p-v2-standalone" diff --git a/pulp/CMakeLists.txt b/pulp/CMakeLists.txt index 80bb94de..54ba73ed 100644 --- a/pulp/CMakeLists.txt +++ b/pulp/CMakeLists.txt @@ -19,3 +19,5 @@ add_subdirectory(datamover) add_subdirectory(snitch) add_subdirectory(redmule) add_subdirectory(pcie_vfio_bridge) +add_subdirectory(cv32e40p_exit) +add_subdirectory(cv32e40p_sparse_mem) diff --git a/pulp/cpu/iss/cv32e40p_v2.py b/pulp/cpu/iss/cv32e40p_v2.py new file mode 100644 index 00000000..94d9a3c9 --- /dev/null +++ b/pulp/cpu/iss/cv32e40p_v2.py @@ -0,0 +1,312 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +from __future__ import annotations + +from typing import Iterable +from typing_extensions import override +from gvsoc.systree import Component +from cpu.iss_v2.riscv import (RiscvCommon, IssModule, ExecInOrder, + Regfile, LsuV2, Hwloop) +from cpu.iss.isa_gen.isa_gen import Isa, IsaSubset +from cpu.iss.isa_gen.isa_riscv_gen import RiscvIsa +from cpu.iss.isa_gen.isa_cv32e40pv2 import CoreV2 +from cpu.iss_v2.riscv_config import RiscvConfig + +isa_instances: dict[tuple[str, str], Isa] = {} + +# misa: MXL=1 | I | M | C, plus X for the PULP extensions and F when the +# FPU registers are in the ISA (not for ZFINX). Same values as the v1 +# model (pulp/cpu/iss/pulp_cores.py). +_MISA_BASE = 0x40001104 + + +def _apply_rtl_decode_fixes(isa: Isa) -> None: + """Decode-level differences between the generated RISC-V tables and the + CV32E40P RTL, applied once per ISA instance: + + - FENCE/FENCE.I: the RTL decoder checks funct3 only and ignores the + reserved rd/rs1/fm fields (cv32e40p_decoder.sv, OPCODE_FENCE), as the + unprivileged spec requires for forward compatibility; the generated + encodings pin those fields to zero, turning e.g. a fence with rd=x31 + into an illegal instruction. + - CSRRC with rs1=x0 and CSRRSI/CSRRCI with uimm=0 must not write the CSR + (privileged spec §2.2): the priv subset is routed to the core's + handlers (cores/cv32e40p/priv.hpp), same fix as the v1 model. + """ + relaxed = { + 'fence': '------- ----- ----- 000 ----- 0001111', + 'fence.i': '------- ----- ----- 001 ----- 0001111', + } + for insn in isa.get_isa('rv32i').instrs: + encoding = relaxed.get(insn.label) + if encoding is not None: + # Same transform as Instr.__init__ (reversed, spaces stripped). + insn.encoding = encoding[::-1].replace(' ', '') + + isa.get_isa('priv').includes = [ + '', + ] + + +class Cv32e40pConfig(RiscvConfig): + pass + + +class Cv32e40pExec(ExecInOrder): + """CV32E40P execution loop: stays on the full handlers while any + implemented counter is enabled, so the event lines fire (same scheme + as Ri5kyExec).""" + + def __init__(self): + super().__init__(scoreboard=True, class_name='Cv32e40pExec', + inorder_commit=True) + + @override + def gen(self, iss: RiscvCommon): + super().gen(iss) + iss.isa.add_include('') + iss.isa.add_implem_include('') + + +class Cv32e40pIrq(IssModule): + """CV32E40P interrupt personality. + + Selects the Cv32e40pIrq C++ class for the irq slot: RISC-V privileged + interrupt scheme (mie/mip/mtvec, standard mcause codes) with the RTL + priority order (fast lines irq[31:16] above MEI/MSI/MTI) and vectored + entry (base + 4*id). + """ + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_IRQ', 'Cv32e40pIrq') + iss.isa.add_define('CONFIG_GVSOC_ISS_RISCV_EXCEPTIONS', 1) + # Marks the CV32E40P iss_v2 personality build for the shared ISA + # headers: gates the debug-entry hooks (ebreak with dcsr.ebreakm, + # dret single-step window) in isa/rv32i.hpp and isa/rv32c.hpp. + # The v1 flag CONFIG_GVSOC_ISS_CV32E40P must stay off here (it + # also gates v1-only core.hpp/csr.hpp/dbgunit paths). + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_V2', 1) + # Strict RVC decoding (isa/rv32c.hpp): reserved code-points + # (c.addi4spn nzuimm=0, c.addi16sp/c.lui imm=0, c.lwsp rd=0, + # c.jr rs1=0) raise illegal-instruction, as the CV32E40P RTL does. + # Opt-in so the other cores keep the historical permissive + # decoding (and their golden traces) by default. + iss.isa.add_define('CONFIG_GVSOC_ISS_RVC_STRICT', 1) + # IEEE754 leaves the tininess detection point to the + # implementation: FPnew (the CV32E40P FPU) detects it after + # rounding with unbounded exponent (fpnew_fma.sv:616). Opt-in for + # parity with the RTL; other cores keep flexfloat's default + # (before-rounding) so their FP flags are unchanged. + iss.add_c_flags(['-DFLEXFLOAT_TININESS_AFTER_ROUNDING=1']) + iss.isa.add_include('') + iss.add_sources([ + 'cpu/iss_v2/src/irq/irq_riscv.cpp', + 'cpu/iss_v2/src/cores/cv32e40p/irq.cpp', + ]) + + +class Cv32e40pEvent(IssModule): + """CV32E40P event accounting. + + Selects the Cv32e40pEvents C++ class for the event slot: routes the + architectural event lines (instr, load, store, jump, branch, taken + branch, compressed) into the mhpm counters via Cv32e40pCsr::hpm_commit. + """ + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_EVENT', 'Cv32e40pEvents') + iss.isa.add_include('') + iss.isa.add_implem_include('') + iss.add_sources([ + 'cpu/iss_v2/src/event/event.cpp', + 'cpu/iss_v2/src/cores/cv32e40p/events.cpp', + ]) + + +class Cv32e40pCsr(IssModule): + """CV32E40P CSR personality. + + Selects the Cv32e40pCsr C++ class for the csr slot: M-mode-only CSR + map, RTL write masks, PULP custom CSRs, hardware-loop CSRs and + illegal-instruction on unsupported CSR accesses. + """ + + def __init__(self, fpu: bool=False, zfinx: bool=False, pulp: bool=True, + num_mhpmcounters: int=1): + self.fpu = fpu + self.zfinx = zfinx + self.pulp = pulp + self.num_mhpmcounters = num_mhpmcounters + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_CSR', 'Cv32e40pCsr') + iss.isa.add_include('') + # Select the RISC-V (not legacy RISCY) SIMD operand order in the + # shared isa_lib int.h (lib_VEC_SHUFFLE2_*); the v1 build gets this + # from the iss CMakeLists. + iss.isa.add_define('RISCV', 1) + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_FPU_IN_ISA', 1 if self.fpu else 0) + if self.fpu: + # FP write-backs must dirty mstatus.FS (see iss_v2 isa_lib/macros.h). + iss.isa.add_define('CONFIG_GVSOC_ISS_FP_STATE_DIRTY', 1) + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_ZFINX', 1 if self.zfinx else 0) + if self.fpu or self.zfinx: + # Reserved FP rounding modes raise illegal-instruction with no + # architectural side effects (isa_lib int.h + Cv32e40pRegfile). + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_FP_TRAPS', 1) + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_PULP', 1 if self.pulp else 0) + iss.isa.add_define('CONFIG_GVSOC_ISS_CV32E40P_NUM_MHPMCOUNTERS', self.num_mhpmcounters) + # mstatus write policy, applied by Core::mstatus_update: only + # MIE/MPIE are writable, plus FS with FPU registers in the ISA. + iss.isa.add_define('CONFIG_GVSOC_ISS_CORE_MSTATUS_WRITE_MASK', + '0x6088' if self.fpu else '0x88') + iss.add_sources([ + 'cpu/iss_v2/src/cores/cv32e40p/csr.cpp', + 'cpu/iss_v2/src/csr.cpp', + ]) + + +class Cv32e40pRegfileModule(IssModule): + """CV32E40P register-file personality. + + Selects the Cv32e40pRegfile C++ class for the regfile slot: a trapped + instruction writes no destination register (the reserved-rounding-mode + raise in isa_lib int.h arms the one-shot write-back suppression). + """ + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_REGFILE', 'Cv32e40pRegfile') + iss.isa.add_define('CONFIG_GVSOC_ISS_REGFILE_SCOREBOARD', '1') + iss.isa.add_include('') + iss.add_sources(['cpu/iss_v2/src/regfile.cpp']) + + +class Cv32e40pCoreModule(IssModule): + """CV32E40P core personality. + + Selects the Cv32e40pCore C++ class for the core slot: MRET keeps + mcause (the RTL holds it until the next trap or an explicit CSR + write, the generic handler clears it). + """ + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_CORE', 'Cv32e40pCore') + iss.isa.add_include('') + iss.add_sources([ + 'cpu/iss_v2/src/core.cpp', + 'cpu/iss_v2/src/cores/cv32e40p/core.cpp', + ]) + + +class Cv32e40pExceptionModule(IssModule): + """CV32E40P exception personality. + + Selects the Cv32e40pException C++ class for the exception slot: + exceptions enter at the mtvec base, with the mode bits kept out of + the entry PC. + """ + + @override + def gen(self, iss: RiscvCommon): + iss.isa.add_define('CONFIG_GVSOC_ISS_EXCEPTION', 'Cv32e40pException') + iss.isa.add_include('') + iss.add_sources([ + 'cpu/iss_v2/src/exception.cpp', + 'cpu/iss_v2/src/cores/cv32e40p/exception.cpp', + ]) + + +class Cv32e40p(RiscvCommon): + """CV32E40P on the iss_v2 modular core. + + Bring-up recipe: generic v2 slots plus the CoreV ISA subset. The + CV32E40P-specific CSR map, event counters and trap behaviour come in + as dedicated slot overrides on top of this base (same layering as + Ri5ky). + """ + + # Tag used in ISA-cache and generated ISA-class names (see Ri5ky). + isa_name: str = 'cv32e40p_v2' + + def __init__(self, parent: Component, name: str, config: Cv32e40pConfig, + fpu: bool=False, zfinx: bool=False, pulp: bool=True, + num_mhpmcounters: int=1, + extra_extensions: Iterable[IsaSubset] = ()): + + # pulp and zfinx change what gets compiled behind one ISA string, + # so both are part of the cache key and of the generated ISA name. + isa_tag = f"{config.isa}_pulp" if pulp else config.isa + if zfinx: + isa_tag += '_zfinx' + cache_key = (type(self).isa_name, isa_tag) + isa_instance: Isa | None = isa_instances.get(cache_key) + + if isa_instance is None: + extensions: list[IsaSubset] = [ + *extra_extensions, + ] + if pulp: + extensions.append(CoreV2()) + + isa_instance = RiscvIsa(f"{type(self).isa_name}_{isa_tag}", + config.isa, extensions=extensions) + + if zfinx: + # RTL decodes the compressed FP loads/stores only with + # FPU == 1 && ZFINX == 0 (cv32e40p_compressed_decoder.sv). + isa_instance.disable_from_isa_tag('cf') + + _apply_rtl_decode_fixes(isa_instance) + + isa_instances[cache_key] = isa_instance + + misa = _MISA_BASE + if fpu and not zfinx: + misa |= 1 << 5 # F + if pulp: + misa |= 1 << 23 # X + + modules: dict[str, IssModule] = { + 'irq': Cv32e40pIrq(), + 'core': Cv32e40pCoreModule(), + 'exception': Cv32e40pExceptionModule(), + 'event': Cv32e40pEvent(), + 'csr': Cv32e40pCsr(fpu=fpu, zfinx=zfinx, pulp=pulp, + num_mhpmcounters=num_mhpmcounters), + 'exec': Cv32e40pExec(), + 'lsu': LsuV2(), + 'regfile': Cv32e40pRegfileModule(), + 'hwloop': Hwloop(), + } + + # dm_halt_addr / dm_exception_addr of the RTL testbench: the debug + # entry redirects to the first, exceptions taken while in debug mode + # to the second (linker script `dbg` region, loaded from the test + # ELF; uvme_cv32e40p_cfg defaults). + super().__init__(parent, name, config=config, isa=isa_instance, + misa=misa, zfinx=zfinx, modules=modules, + debug_handler=0x1A110800, + debug_exception_handler=0x1A111600) diff --git a/pulp/cpu/iss/pulp_cores.py b/pulp/cpu/iss/pulp_cores.py index e83549b8..b3447d05 100644 --- a/pulp/cpu/iss/pulp_cores.py +++ b/pulp/cpu/iss/pulp_cores.py @@ -16,10 +16,15 @@ # limitations under the License. # +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + import cpu.iss.riscv from cpu.iss.isa_gen.isa_riscv_gen import * from cpu.iss.isa_gen.isa_smallfloats import * from cpu.iss.isa_gen.isa_pulpv2 import * +from cpu.iss.isa_gen.isa_cv32e40pv2 import * from cpu.iss.isa_gen.isa_pulpnn import PulpNn @@ -109,3 +114,121 @@ def __init__(self, parent, name, fetch_enable: bool=False, boot_addr: int=0, clu super().__init__(parent, name, isa=_fc_isa, cluster_id=cluster_id, core_id=0, fetch_enable=fetch_enable, boot_addr=boot_addr) + + +def _build_cv32e40p_isa(name, pulpv2=True, fpu=True, zfinx=False, core_version=2): + """Build the CV32E40P ISA. + + fpu=True → rv32imfc (F instructions in decoder, regardless of zfinx). + fpu=False → rv32imc (no F instructions). + + ZFINX uses the same F-extension opcodes but routes them to GPR via + ISS_SINGLE_REGFILE (set in RiscvCommon.__init__). The MISA F-bit and + mstatus FS mask are controlled separately by fpu_in_isa in add_properties. + """ + + # ZFINX needs F instructions in the decoder (routed to GPR by ISS_SINGLE_REGFILE). + base_isa = 'rv32imfc' if fpu else 'rv32imc' + + extensions = [] + if pulpv2: + if core_version == 2: + extensions.append(CoreV2()) + else: + extensions.append(PulpV2()) + + isa = cpu.iss.isa_gen.isa_riscv_gen.RiscvIsa(name, base_isa, extensions=extensions) + + return isa + + +class cv32e40p(cpu.iss.riscv.RiscvCommon): + + def __init__(self, parent, name, fetch_enable: bool=False, boot_addr: int=0, cluster_id: int=31, core_id: int=0, + pulpv2: bool=True, fpu: bool=True, zfinx: bool=False, corev_cluster: bool=False, core_version: int=2, + num_mhpmcounters: int=1): + """ + CV32E40P core model. + + Parameters + ---------- + pulpv2 : bool + Enable PULP extensions (COREV_PULP). Default True. + fpu : bool + Enable FPU (rv32imfc ISA, misa bit F set). Default True. + zfinx : bool + FPU uses integer registers (Zfinx). Clears misa bit F. Default False. + corev_cluster : bool + Cluster variant (COREV_CLUSTER). Contributes to mimpid. Default False. + core_version : int + 1 for legacy PULP v1 encodings, 2 for CORE-V v2 (custom-0/1/2). Default 2. + num_mhpmcounters : int + Number of HPM counters (RTL NUM_MHPMCOUNTERS param). Default 1. + """ + # Unique ISA name per instance/configuration to avoid generator collisions + isa_name = f'cv32e40p_{name}_v{core_version}_{"f" if fpu else "nof"}_{"z" if zfinx else "noz"}' + isa = _build_cv32e40p_isa(isa_name, pulpv2=pulpv2, fpu=fpu, zfinx=zfinx, core_version=core_version) + + # misa: MXL=1(RV32) | I(bit8) | M(bit12) | C(bit2) [| F(bit5) if fpu and not zfinx] [| X(bit23) if pulpv2] + _MISA_BASE = 0x40001104 # RV32 | I | M | C + fpu_in_isa = fpu and not zfinx + misa = _MISA_BASE | (0x20 if fpu_in_isa else 0) | (0x00800000 if pulpv2 else 0) + + # mimpid = (FPU || COREV_PULP || COREV_CLUSTER) ? 1 : 0 (RTL cv32e40p_cs_registers.sv) + mimpid = 0x1 if (fpu or pulpv2 or corev_cluster) else 0x0 + + super().__init__(parent, name, isa=isa, + riscv_dbg_unit=True, fetch_enable=fetch_enable, boot_addr=boot_addr, + first_external_pcer=12, debug_handler=0x1a110800, misa=misa, core="riscv", + cluster_id=cluster_id, core_id=core_id, wrapper="pulp/cpu/iss/default_iss_wrapper.cpp", + scoreboard=True, timed=True, handle_misaligned=True, zfinx=zfinx, + riscv_exceptions=True) + + # CV32E40P / PULP vendor CSR values — written to JSON config. + # These are read by Cv32e40pCsr::build() in csr_cv32e40p.cpp. + # Values derived from cv32e40p_cs_registers.sv and cv32e40p_pkg.sv. + # mstatus effective write mask — matches RTL always_ff forcing (PULP_SECURE=0). + # RTL cv32e40p_cs_registers.sv:1222-1230 forces MPP=M, MPRV=0, UIE=0, UPIE=0. + # Only MIE(3) + MPIE(7) are writable. With FPU: add FS(14:13). + mstatus_mask = 0x6088 if fpu_in_isa else 0x0088 + # mcountinhibit: CY(0) + IR(2) + HPM3..HPM(2+N) — bit 1 always reserved + # With N=1: mask=0x0D (bits 0,2,3). With N=29: mask=0xFFFFFFFD (all except bit 1). + mcountinhibit_mask = 0x5 | (((1 << num_mhpmcounters) - 1) << 3) + self.add_properties({ + 'mvendorid_value': 0x602, + 'marchid_value': 0x4, + 'mimpid': mimpid, + 'fpu_in_isa': fpu_in_isa, + 'mtvec_reset': 0x1, + 'mtvec_write_mask': 0xFFFFFF01, # bits[7:1] hardwired 0 + 'mcause_mask': 0x8000001F, # bit[31] + bits[4:0] + 'mcountinhibit_mask': mcountinhibit_mask, + 'mstatus_write_mask': mstatus_mask, # MPP/MPIE/MIE [+FS if FPU] + 'mie_write_mask': 0xFFFF0888, # IRQ_MASK + 'mtval_write_mask': 0x00000000, # CV32E40P mtval is hardwired to 0 + 'tdata1_reset': 0x28001040, # type=2,dmode=1,action=1,m=1,u=0 (no U-mode) + 'tdata1_write_mask': 0x00000000, # writable ONLY from Debug Mode (RTL: tmatch_control_we = csr_we_int & debug_mode_i) + 'tdata2_write_mask': 0x00000000, # writable ONLY from Debug Mode (RTL: tmatch_value_we = csr_we_int & debug_mode_i) + 'tinfo_reset': 0x4, # bit[2] = mcontrol supported + 'num_mhpmcounters': num_mhpmcounters, + 'num_hpm_events': 16, + 'pulpv2': pulpv2, # COREV_PULP — gates UHARTID/PRIVLV + 'zfinx': zfinx, # ZFINX mode — gates CSR_ZFINX (0xCD2) + }) + + self.add_c_flags([ + "-DPIPELINE_STALL_THRESHOLD=1", + "-DCONFIG_ISS_CORE=cv32e40p", + f"-DCONFIG_GVSOC_CORE_VERSION={core_version}", + "-DCONFIG_GVSOC_ISS_HWLOOP=1", + "-DCONFIG_GVSOC_ISS_CV32E40P=1", + ]) + + # CV32E40P-specific CSR subclass (Cv32e40pCsr) — must be compiled + # into the ISS model .so. The base riscv.py add_sources() list + # doesn't include it because it's CV32E40P-specific. + self.add_sources([ + "cpu/iss/src/cv32e40p/csr_cv32e40p.cpp", + "cpu/iss/src/cv32e40p/irq_cv32e40p.cpp", + "cpu/iss/src/cv32e40p/core_cv32e40p.cpp", + ]) diff --git a/pulp/cv32e40p_exit/CMakeLists.txt b/pulp/cv32e40p_exit/CMakeLists.txt new file mode 100644 index 00000000..3de331b8 --- /dev/null +++ b/pulp/cv32e40p_exit/CMakeLists.txt @@ -0,0 +1,3 @@ +vp_model(NAME pulp.cv32e40p_exit.cv32e40p_exit_device + SOURCES "cv32e40p_exit_device.cpp" + ) diff --git a/pulp/cv32e40p_exit/cv32e40p_exit_device.cpp b/pulp/cv32e40p_exit/cv32e40p_exit_device.cpp new file mode 100644 index 00000000..3a9330f5 --- /dev/null +++ b/pulp/cv32e40p_exit/cv32e40p_exit_device.cpp @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2020 GreenWaves Technologies, SAS, ETH Zurich and + * University of Bologna + * Copyright (C) 2026 Fondazione Chips-it + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) + */ + +/* + * CV32E40P virtual exit device for GVSOC. + * + * Implements the cv32e40p virtual peripheral status flags (test_programs.rst): + * + * Offset 0x00 (0x2000_0000): test_passed / test_failed flags + * Offset 0x04 (0x2000_0004): assert exit_valid → terminates GVSOC simulation + * exit_value = wdata + * Offset 0x08 (0x2000_0008): signature_start_address + * Offset 0x0C (0x2000_000C): signature_end_address + * Offset 0x10 (0x2000_0010): signature write trigger + * also asserts exit_valid with exit_value = 0 + * + * Every write is also retained and readable back: in the UVM testbench this + * region is ordinary sparse memory that the virtual peripheral snoops, so a + * write-then-read (e.g. of the signature addresses) returns the stored value + * there and must do the same here. + */ + +#include + +#include +#include + +#define VP_STATUS_FLAGS_OFFSET 0x00 +#define VP_EXIT_VALID_OFFSET 0x04 +#define VP_SIG_START_OFFSET 0x08 +#define VP_SIG_END_OFFSET 0x0C +#define VP_SIG_WRITE_OFFSET 0x10 + +class Cv32e40pExitDevice : public vp::Component +{ +public: + Cv32e40pExitDevice(vp::ComponentConf &config); + +private: + static vp::IoReqStatus handle_req(vp::Block *__this, vp::IoReq *req); + + vp::IoSlave input_itf; + vp::Trace trace; + + /* Backing store (256B region): writes persist and read back, like the + * testbench memory under the virtual peripheral. */ + uint8_t mem[0x100] = {}; +}; + +Cv32e40pExitDevice::Cv32e40pExitDevice(vp::ComponentConf &config) + : vp::Component(config) +{ + this->input_itf.set_req_meth(&Cv32e40pExitDevice::handle_req); + this->new_slave_port("input", &this->input_itf); + this->traces.new_trace("trace", &this->trace, vp::DEBUG); +} + +vp::IoReqStatus Cv32e40pExitDevice::handle_req(vp::Block *__this, vp::IoReq *req) +{ + Cv32e40pExitDevice *_this = (Cv32e40pExitDevice *)__this; + + uint32_t offset = (uint32_t)req->get_addr(); + + if (!req->get_is_write()) + { + if (offset + req->get_size() <= sizeof(_this->mem)) + memcpy(req->get_data(), &_this->mem[offset], req->get_size()); + else + memset(req->get_data(), 0, req->get_size()); + return vp::IO_REQ_OK; + } + + if (offset + req->get_size() <= sizeof(_this->mem)) + memcpy(&_this->mem[offset], req->get_data(), req->get_size()); + + uint32_t wdata = (req->get_size() == 4) ? *(uint32_t *)req->get_data() : 0; + + switch (offset) + { + case VP_STATUS_FLAGS_OFFSET: + if (wdata == 123456789U) /* 0x075BCD15 — TEST PASSED (same magic as UVM VP) */ + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x — TEST PASSED, stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] tests_passed=1 exit_value=0x00000000\n"); + fflush(stdout); + _this->time.get_engine()->quit(0); + } + else if (wdata == 1U) /* TEST FAILED */ + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x — TEST FAILED, stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] tests_failed=1 exit_value=0x00000001\n"); + fflush(stdout); + _this->time.get_engine()->quit(1); + } + else + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x (unrecognized status flag — ignored)\n", wdata); + } + break; + + case VP_EXIT_VALID_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "exit_valid asserted: exit_value=0x%08x — stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] exit_valid=1 exit_value=0x%08x\n", wdata); + fflush(stdout); + _this->time.get_engine()->quit((int32_t)wdata); + break; + + case VP_SIG_START_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature_start_address=0x%08x (not implemented)\n", wdata); + break; + + case VP_SIG_END_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature_end_address=0x%08x (not implemented)\n", wdata); + break; + + case VP_SIG_WRITE_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature write triggered — stopping simulation (exit_value=0)\n"); + fprintf(stdout, "[cv32e40p_exit] signature write → exit_valid=1 exit_value=0x00000000\n"); + fflush(stdout); + _this->time.get_engine()->quit(0); + break; + + default: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "unknown offset 0x%02x wdata=0x%08x — ignored\n", offset, wdata); + break; + } + + return vp::IO_REQ_OK; +} + +extern "C" vp::Component *gv_new(vp::ComponentConf &config) +{ + return new Cv32e40pExitDevice(config); +} diff --git a/pulp/cv32e40p_exit/cv32e40p_exit_device.py b/pulp/cv32e40p_exit/cv32e40p_exit_device.py new file mode 100644 index 00000000..cc98fdfa --- /dev/null +++ b/pulp/cv32e40p_exit/cv32e40p_exit_device.py @@ -0,0 +1,46 @@ +# +# Copyright (C) 2020 GreenWaves Technologies, SAS, ETH Zurich and +# University of Bologna +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# Python wrapper for the CV32E40P virtual exit device. +# Terminates GVSOC simulation when the test program writes to 0x2000_0004. + +import gvsoc.systree as st + + +class Cv32e40pExitDevice(st.Component): + """ + CV32E40P virtual peripheral status flags device. + + Memory map (base-relative): + +0x00 VP status flags (test_passed / test_failed — sink) + +0x04 exit_valid write → quits GVSOC simulation with exit_value = wdata + +0x08 sig_start_addr (sink) + +0x0C sig_end_addr (sink) + +0x10 sig_write trigger → quits GVSOC simulation with exit_value = 0 + """ + + def __init__(self, parent, name): + super().__init__(parent, name) + self.set_component('pulp.cv32e40p_exit.cv32e40p_exit_device') + + def i_INPUT(self) -> st.SlaveItf: + return st.SlaveItf(self, 'input', signature='io') diff --git a/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.cpp b/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.cpp new file mode 100644 index 00000000..934ae8af --- /dev/null +++ b/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.cpp @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2026 Fondazione Chips-it + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) + */ + +/* + * CV32E40P virtual exit device, io_v2 plane (iss_v2 platforms). + * + * Same register map and semantics as the io-plane sibling + * (cv32e40p_exit_device.cpp): mirrors the UVM virtual peripheral at + * 0x20000000 — status flags (+0x00, magic 123456789 = PASSED, 1 = FAILED), + * exit_valid (+0x04), signature registers (+0x08..+0x10). Writes persist in + * a 256B backing store and read back like testbench memory. + */ + +#include + +#include +#include + +#define VP_STATUS_FLAGS_OFFSET 0x00 +#define VP_EXIT_VALID_OFFSET 0x04 +#define VP_SIG_START_OFFSET 0x08 +#define VP_SIG_END_OFFSET 0x0C +#define VP_SIG_WRITE_OFFSET 0x10 + +class Cv32e40pExitDeviceV2 : public vp::Component +{ +public: + Cv32e40pExitDeviceV2(vp::ComponentConf &config); + +private: + static vp::IoReqStatus req(vp::Block *__this, vp::IoReq *req); + + vp::Trace trace; + vp::IoSlave in{&Cv32e40pExitDeviceV2::req}; + + /* Backing store (256B region): writes persist and read back, like the + * testbench memory under the virtual peripheral. */ + uint8_t mem[0x100] = {}; +}; + +Cv32e40pExitDeviceV2::Cv32e40pExitDeviceV2(vp::ComponentConf &config) + : vp::Component(config) +{ + this->traces.new_trace("trace", &this->trace, vp::DEBUG); + this->new_slave_port("input", &this->in); +} + +vp::IoReqStatus Cv32e40pExitDeviceV2::req(vp::Block *__this, vp::IoReq *req) +{ + Cv32e40pExitDeviceV2 *_this = (Cv32e40pExitDeviceV2 *)__this; + + uint32_t offset = (uint32_t)req->get_addr(); + + /* Atomics are not supported (no A extension on this platform). */ + if (req->get_opcode() != vp::IoReqOpcode::READ && + req->get_opcode() != vp::IoReqOpcode::WRITE) + { + req->set_resp_status(vp::IO_RESP_INVALID); + return vp::IO_REQ_DONE; + } + + if (req->get_opcode() == vp::IoReqOpcode::READ) + { + if (offset + req->get_size() <= sizeof(_this->mem)) + memcpy(req->get_data(), &_this->mem[offset], req->get_size()); + else + memset(req->get_data(), 0, req->get_size()); + return vp::IO_REQ_DONE; + } + + if (offset + req->get_size() <= sizeof(_this->mem)) + memcpy(&_this->mem[offset], req->get_data(), req->get_size()); + + uint32_t wdata = (req->get_size() == 4) ? *(uint32_t *)req->get_data() : 0; + + switch (offset) + { + case VP_STATUS_FLAGS_OFFSET: + if (wdata == 123456789U) /* 0x075BCD15 — TEST PASSED (same magic as UVM VP) */ + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x — TEST PASSED, stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] tests_passed=1 exit_value=0x00000000\n"); + fflush(stdout); + _this->time.get_engine()->quit(0); + } + else if (wdata == 1U) /* TEST FAILED */ + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x — TEST FAILED, stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] tests_failed=1 exit_value=0x00000001\n"); + fflush(stdout); + _this->time.get_engine()->quit(1); + } + else + { + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "vp_status write: wdata=0x%08x (unrecognized status flag — ignored)\n", wdata); + } + break; + + case VP_EXIT_VALID_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "exit_valid asserted: exit_value=0x%08x — stopping simulation\n", wdata); + fprintf(stdout, "[cv32e40p_exit] exit_valid=1 exit_value=0x%08x\n", wdata); + fflush(stdout); + _this->time.get_engine()->quit((int32_t)wdata); + break; + + case VP_SIG_START_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature_start_address=0x%08x (not implemented)\n", wdata); + break; + + case VP_SIG_END_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature_end_address=0x%08x (not implemented)\n", wdata); + break; + + case VP_SIG_WRITE_OFFSET: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "signature write triggered — stopping simulation (exit_value=0)\n"); + fprintf(stdout, "[cv32e40p_exit] signature write → exit_valid=1 exit_value=0x00000000\n"); + fflush(stdout); + _this->time.get_engine()->quit(0); + break; + + default: + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "unknown offset 0x%02x wdata=0x%08x — ignored\n", offset, wdata); + break; + } + + return vp::IO_REQ_DONE; +} + +extern "C" vp::Component *gv_new(vp::ComponentConf &config) +{ + return new Cv32e40pExitDeviceV2(config); +} diff --git a/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.py b/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.py new file mode 100644 index 00000000..3e23a090 --- /dev/null +++ b/pulp/cv32e40p_exit/cv32e40p_exit_device_v2.py @@ -0,0 +1,35 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P virtual exit device on the io_v2 plane: same register map as the +# io-plane sibling (UVM virtual peripheral at 0x20000000). + +import gvsoc.systree +import gvsoc.signature + + +class Cv32e40pExitDeviceV2(gvsoc.systree.Component): + + def __init__(self, parent, name): + super().__init__(parent, name) + self.add_sources(['pulp/cv32e40p_exit/cv32e40p_exit_device_v2.cpp']) + + def i_INPUT(self) -> gvsoc.systree.SlaveItf: + return gvsoc.systree.SlaveItf(self, 'input', signature=gvsoc.signature.IoV2Sync()) diff --git a/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.cpp b/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.cpp new file mode 100644 index 00000000..f138a52c --- /dev/null +++ b/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2026 Fondazione Chips-it + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) + */ + +/* + * CV32E40P interrupt-line injector. + * + * Exposes the core interrupt wires to an external gv:: client, which + * binds each line with gv::wire_bind and drives it as the RTL irq_i + * inputs change. Every line forwards to the matching + * IrqRiscv slave port, so mip and the wake-up logic follow the same path + * as a platform interrupt source. + */ + +#include + +#include +#include +#include + +class Cv32e40pIrqInjector : public vp::Component +{ +public: + Cv32e40pIrqInjector(vp::ComponentConf &config); + + void *external_bind(std::string comp_name, std::string itf_name, + void *handle) override; + +private: + /* One interrupt line: gv::Wire_binding facade over the master port. */ + class Line : public gv::Wire_binding + { + public: + void update(int value) override + { + if (this->itf.is_bound()) + { + this->itf.sync(value != 0); + } + } + + std::string name; + vp::WireMaster itf; + }; + + /* msi, mti, mei, the sixteen fast lines irq[31:16], plus the debug + * halt request (RTL debug_req_i - wakes a WFI-parked hart, so it must + * travel the wire path like the interrupt lines, not a struct write) + * and the wfi_wake release (externally driven, no architectural + * effect: fires when the DUT's stream proves a wake the wires cannot + * carry). */ + static constexpr int NB_IRQ_LINES = 19; + static constexpr int NB_LINES = 21; + Line lines[NB_LINES]; +}; + +Cv32e40pIrqInjector::Cv32e40pIrqInjector(vp::ComponentConf &config) + : vp::Component(config) +{ + this->lines[0].name = "msi"; + this->lines[1].name = "mti"; + this->lines[2].name = "mei"; + for (int i = 3; i < NB_IRQ_LINES; i++) + { + this->lines[i].name = "external_irq_" + std::to_string(16 + i - 3); + } + this->lines[NB_IRQ_LINES].name = "haltreq"; + this->lines[NB_IRQ_LINES + 1].name = "wfi_wake"; + + for (auto &line : this->lines) + { + this->new_master_port(line.name, &line.itf); + } +} + +void *Cv32e40pIrqInjector::external_bind(std::string comp_name, + std::string itf_name, void *handle) +{ + (void)handle; + + if (comp_name != this->get_name()) + { + return NULL; + } + + for (auto &line : this->lines) + { + if (line.name == itf_name) + { + return static_cast(&line); + } + } + + return NULL; +} + +extern "C" vp::Component *gv_new(vp::ComponentConf &config) +{ + return new Cv32e40pIrqInjector(config); +} diff --git a/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.py b/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.py new file mode 100644 index 00000000..45768a90 --- /dev/null +++ b/pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.py @@ -0,0 +1,42 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P interrupt-line injector: lets the external co-simulation bridge +# drive the core interrupt wires (msi/mti/mei + fast irq16..31) through +# gv::wire_bind. One master wire port per line, named after the core slave +# port it drives. + +import gvsoc.systree + +# Line names and their interrupt numbers, index-aligned, in RVVI net order +# (MSWInterrupt, MTimerInterrupt, MExternalInterrupt, LocalInterrupt0..15). +IRQ_LINES: tuple = ('msi', 'mti', 'mei', + *(f'external_irq_{i}' for i in range(16, 32))) +IRQ_NUMBERS: tuple = (3, 7, 11, *range(16, 32)) + + +class Cv32e40pIrqInjector(gvsoc.systree.Component): + + def __init__(self, parent, name): + super().__init__(parent, name) + self.add_sources(['pulp/cv32e40p_irq_injector/cv32e40p_irq_injector.cpp']) + + def o_LINE(self, name: str, itf: gvsoc.systree.SlaveItf): + self.itf_bind(name, itf, signature='wire') diff --git a/pulp/cv32e40p_sparse_mem/CMakeLists.txt b/pulp/cv32e40p_sparse_mem/CMakeLists.txt new file mode 100644 index 00000000..d5c8e2ce --- /dev/null +++ b/pulp/cv32e40p_sparse_mem/CMakeLists.txt @@ -0,0 +1,3 @@ +vp_model(NAME pulp.cv32e40p_sparse_mem.cv32e40p_sparse_mem + SOURCES "cv32e40p_sparse_mem.cpp" + ) diff --git a/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.cpp b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.cpp new file mode 100644 index 00000000..88f9dac9 --- /dev/null +++ b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2026 Fondazione Chips-it + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) + */ + +/* + * Background sparse memory for the CV32E40P standalone platform. + * + * Mapped as the interconnect's default route (size=0 mapping), it serves + * every access that no explicit device claims. The UVM testbench answers the + * whole address space from a sparse memory model - a never-written location + * reads 0, a write persists - so the reference platform must do the same or + * stray software accesses diverge from the RTL: + * + * - an out-of-map store followed by a load must return the stored value, + * not 0 (the store used to be dropped); + * - a fetch from an unmapped address must return 0x00000000, which decodes + * as an illegal instruction (mcause=2) exactly like the RTL executing + * the testbench's zero response - not an instruction access fault + * (mcause=1) raised before execution. + * + * Contract with the testbench: this component reads 0 for never-written + * bytes, which matches the cv32e40p environment only because its memory + * model defaults to zero-fill. A testbench configured to random-fill would + * diverge from any zero-defaulting reference by construction. + */ + +#include +#include + +#include +#include + +class Cv32e40pSparseMem : public vp::Component +{ +public: + Cv32e40pSparseMem(vp::ComponentConf &config); + +private: + static vp::IoReqStatus handle_req(vp::Block *__this, vp::IoReq *req); + + vp::IoSlave input_itf; + vp::Trace trace; + + /* Byte-granular backing store: only written bytes are kept. */ + std::unordered_map store; +}; + +Cv32e40pSparseMem::Cv32e40pSparseMem(vp::ComponentConf &config) + : vp::Component(config) +{ + this->input_itf.set_req_meth(&Cv32e40pSparseMem::handle_req); + this->new_slave_port("input", &this->input_itf); + this->traces.new_trace("trace", &this->trace, vp::DEBUG); +} + +vp::IoReqStatus Cv32e40pSparseMem::handle_req(vp::Block *__this, vp::IoReq *req) +{ + Cv32e40pSparseMem *_this = (Cv32e40pSparseMem *)__this; + + uint64_t addr = req->get_addr(); + uint64_t size = req->get_size(); + uint8_t *data = req->get_data(); + + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "background access (addr: 0x%llx, size: 0x%llx, is_write: %d)\n", + addr, size, req->get_is_write()); + + if (req->get_is_write()) + { + for (uint64_t i = 0; i < size; i++) + _this->store[addr + i] = data[i]; + } + else + { + for (uint64_t i = 0; i < size; i++) + { + auto it = _this->store.find(addr + i); + data[i] = (it != _this->store.end()) ? it->second : 0; + } + } + + return vp::IO_REQ_OK; +} + +extern "C" vp::Component *gv_new(vp::ComponentConf &config) +{ + return new Cv32e40pSparseMem(config); +} diff --git a/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.py b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.py new file mode 100644 index 00000000..5eed6ede --- /dev/null +++ b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem.py @@ -0,0 +1,36 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# Python wrapper for the CV32E40P background sparse memory. +# Mapped as the interconnect's default route: serves every access no explicit +# device claims (never-written bytes read 0, writes persist), matching the UVM +# testbench sparse memory model. + +import gvsoc.systree as st + + +class Cv32e40pSparseMem(st.Component): + + def __init__(self, parent, name): + super().__init__(parent, name) + self.set_component('pulp.cv32e40p_sparse_mem.cv32e40p_sparse_mem') + + def i_INPUT(self) -> st.SlaveItf: + return st.SlaveItf(self, 'input', signature='io') diff --git a/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.cpp b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.cpp new file mode 100644 index 00000000..21f48129 --- /dev/null +++ b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2026 Fondazione Chips-it + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) + */ + +/* + * Background sparse memory, io_v2 plane (iss_v2 platforms). + * + * Same contract as the io-plane sibling (cv32e40p_sparse_mem.cpp): mapped as + * the interconnect's catch-all route, it makes never-written bytes read 0 and + * writes persist, matching the UVM testbench sparse memory model. + */ + +#include +#include + +#include +#include + +class Cv32e40pSparseMemV2 : public vp::Component +{ +public: + Cv32e40pSparseMemV2(vp::ComponentConf &config); + +private: + static vp::IoReqStatus req(vp::Block *__this, vp::IoReq *req); + + vp::Trace trace; + vp::IoSlave in{&Cv32e40pSparseMemV2::req}; + + /* Byte-granular backing store: only written bytes are kept. */ + std::unordered_map store; +}; + +Cv32e40pSparseMemV2::Cv32e40pSparseMemV2(vp::ComponentConf &config) + : vp::Component(config) +{ + this->traces.new_trace("trace", &this->trace, vp::DEBUG); + this->new_slave_port("input", &this->in); +} + +vp::IoReqStatus Cv32e40pSparseMemV2::req(vp::Block *__this, vp::IoReq *req) +{ + Cv32e40pSparseMemV2 *_this = (Cv32e40pSparseMemV2 *)__this; + + uint64_t addr = req->get_addr(); + uint64_t size = req->get_size(); + uint8_t *data = req->get_data(); + + _this->trace.msg(vp::Trace::LEVEL_DEBUG, + "background access (addr: 0x%llx, size: 0x%llx, is_write: %d)\n", + addr, size, req->get_is_write()); + + if (req->get_opcode() == vp::IoReqOpcode::WRITE) + { + for (uint64_t i = 0; i < size; i++) + _this->store[addr + i] = data[i]; + } + else if (req->get_opcode() == vp::IoReqOpcode::READ) + { + for (uint64_t i = 0; i < size; i++) + { + auto it = _this->store.find(addr + i); + data[i] = (it != _this->store.end()) ? it->second : 0; + } + } + else + { + /* Atomics are not supported (no A extension on this platform). */ + req->set_resp_status(vp::IO_RESP_INVALID); + } + + return vp::IO_REQ_DONE; +} + +extern "C" vp::Component *gv_new(vp::ComponentConf &config) +{ + return new Cv32e40pSparseMemV2(config); +} diff --git a/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.py b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.py new file mode 100644 index 00000000..1e15e239 --- /dev/null +++ b/pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.py @@ -0,0 +1,36 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# Background sparse memory on the io_v2 plane: catch-all route for iss_v2 +# platforms, same contract as the io-plane sibling (never-written bytes read +# 0, writes persist, like the UVM testbench sparse memory model). + +import gvsoc.systree +import gvsoc.signature + + +class Cv32e40pSparseMemV2(gvsoc.systree.Component): + + def __init__(self, parent, name): + super().__init__(parent, name) + self.add_sources(['pulp/cv32e40p_sparse_mem/cv32e40p_sparse_mem_v2.cpp']) + + def i_INPUT(self) -> gvsoc.systree.SlaveItf: + return gvsoc.systree.SlaveItf(self, 'input', signature=gvsoc.signature.IoV2Sync()) diff --git a/pulp/cv32e40p_v2_spike.py b/pulp/cv32e40p_v2_spike.py new file mode 100644 index 00000000..b8c79e8c --- /dev/null +++ b/pulp/cv32e40p_v2_spike.py @@ -0,0 +1,152 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 bring-up platform, shared by the cv32e40p-v2-spike* +# targets (one target name per core configuration, so each gets its own +# serialized platform tree). +# +# The iss_v2 LSU (LsuV2) drives the fetch/data ports with the io_v2 +# protocol, so the whole platform lives on the io_v2 plane: router_v2, +# memory_v3 and loader_v2, following the Ri5ky testbench layout +# (pulp/ri5ky/ri5ky_testbench.py). The MMIO peripheral is reused as-is +# from that testbench: putchar @ +0x0, exit @ +0x4. + +import vp.clock_domain +import gvsoc.systree +from gvrun.parameter import TargetParameter +from config_tree import Config, cfg_field +from memory.memory_v3 import Memory, MemoryV3Config +from interco.router_v2 import Router, RouterConfig, RouterMapping +from utils.loader.loader_v2 import ElfLoader +from pulp.ri5ky.ri5ky_mmio import Ri5kyMmio +from pulp.cpu.iss.cv32e40p_v2 import Cv32e40p as Cv32e40pV2Core, Cv32e40pConfig + + +class Cv32e40pSpikeConfig(Config): + """Configuration for the CV32E40P iss_v2 bring-up SoC. + + Minimal layout: + - mem at 0x0000_0000 (4 MB), entry point 0x80 as in the RTL testbench + - MMIO at 0x1000_0000 (4 KB): putchar @ +0, exit @ +4 + """ + + mem_base: int = cfg_field(default=0x0000_0000, fmt="hex", dump=True, desc=( + "Base address of the main memory" + )) + + mem_size: int = cfg_field(default=0x40_0000, fmt="hex", dump=True, desc=( + "Size of the main memory" + )) + + mmio_base: int = cfg_field(default=0x1000_0000, fmt="hex", dump=True, desc=( + "Base address of the MMIO peripheral" + )) + + mmio_size: int = cfg_field(default=0x1000, fmt="hex", dump=True, desc=( + "Size of the MMIO peripheral window" + )) + + boot_addr: int = cfg_field(default=0x80, fmt="hex", dump=True, desc=( + "Boot address (matches RTL BOOT_ADDR)" + )) + + fpu: int = cfg_field(default=0, dump=True, desc=( + "FPU configuration (F extension, FP register file)" + )) + + zfinx: int = cfg_field(default=0, dump=True, desc=( + "ZFINX configuration (FP operations on the integer register file)" + )) + + core: Cv32e40pConfig = cfg_field(init=False, desc=( + "CV32E40P core configuration" + )) + + mem: MemoryV3Config = cfg_field(init=False, desc=( + "Backing memory configuration" + )) + + router: RouterConfig = cfg_field(init=False, desc=( + "Router configuration" + )) + + mem_mapping: RouterMapping = cfg_field(init=False, desc=( + "Address range of the main memory" + )) + + mmio_mapping: RouterMapping = cfg_field(init=False, desc=( + "Address range of the MMIO peripheral" + )) + + def __post_init__(self): + super().__post_init__() + # ZFINX needs the F opcodes in the decoder (routed to the integer + # register file); the compressed FP rows are disabled by the core + # recipe to match the RTL compressed decoder. + isa = 'rv32imfc' if (self.fpu or self.zfinx) else 'rv32imc' + self.core = Cv32e40pConfig(isa=isa, boot_addr=self.boot_addr) + # init=False: unwritten memory reads 0, not the 0x57 poison default. + self.mem = MemoryV3Config('mem', size=self.mem_size, atomics=False, latency=1, + init=False) + self.router = RouterConfig(kind='bandwidth') + self.mem_mapping = RouterMapping(name='mem_mapping', + base=self.mem_base, size=self.mem_size) + self.mmio_mapping = RouterMapping(name='mmio_mapping', + base=self.mmio_base, size=self.mmio_size) + + +class Cv32e40pSpikeSoc(gvsoc.systree.Component): + + def __init__(self, parent, name, config: Cv32e40pSpikeConfig, binary): + super().__init__(parent, name, config=config) + + mem = Memory ( self, 'mem' , config=config.mem ) + mmio = Ri5kyMmio ( self, 'mmio' ) + ico = Router ( self, 'ico' , config=config.router ) + core = Cv32e40pV2Core( self, 'core' , config=config.core , + fpu=bool(config.fpu), zfinx=bool(config.zfinx) ) + loader = ElfLoader ( self, 'loader', binary=binary ) + + ico.o_MAP ( mem.i_INPUT() , mapping=config.mem_mapping ) + ico.o_MAP ( mmio.i_INPUT(), mapping=config.mmio_mapping ) + + # Three independent masters, one router input port each. + loader.o_OUT ( ico.i_INPUT(0) ) + loader.o_START ( core.i_FETCHEN() ) + loader.o_ENTRY ( core.i_ENTRY() ) + + core.o_FETCH ( ico.i_INPUT(1) ) + core.o_DATA ( ico.i_INPUT(2) ) + + +class Cv32e40pSpikeTop(gvsoc.systree.Component): + + def __init__(self, parent, name=None, fpu: int=0, zfinx: int=0): + super().__init__(parent, name) + + binary = TargetParameter( + self, name='binary', value=None, description='ELF binary to simulate' + ).get_value() + + config = Cv32e40pSpikeConfig('soc', fpu=fpu, zfinx=zfinx) + + clock = vp.clock_domain.Clock_domain(self, 'clock', frequency=50000000) + soc = Cv32e40pSpikeSoc(self, 'soc', config, binary) + clock.o_CLOCK(soc.i_CLOCK()) diff --git a/pulp/cv32e40p_v2_standalone.py b/pulp/cv32e40p_v2_standalone.py new file mode 100644 index 00000000..ae7fae23 --- /dev/null +++ b/pulp/cv32e40p_v2_standalone.py @@ -0,0 +1,219 @@ +# +# Copyright (C) 2026 Fondazione Chips-it +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Authors: Marco Paci, Fondazione Chips-it (marco.paci@chips.it) +# + +# CV32E40P iss_v2 standalone platform for UVM co-simulation, shared by the +# cv32e40p-v2-standalone* targets (one target name per core configuration). +# +# Same memory map as the io-plane co-sim platform (cv32e40p-standalone.py), +# rebuilt on the io_v2 plane the iss_v2 LSU requires: +# 0x00000000 4MB Main RAM (entry point 0x00000080) +# 0x10000000 256B Virtual STDOUT (write-only sink) +# 0x15000000 256B Virtual TIMER (write-only sink) +# 0x1A110800 4KB Debug ROM (linker script `dbg` region) +# 0x20000000 256B Virtual EXIT (terminates the simulation) +# everywhere else background sparse memory (catch-all route: reads 0 +# until written, writes persist - same as the testbench) + +import vp.clock_domain +import gvsoc.systree +from gvrun.parameter import TargetParameter +from config_tree import Config, cfg_field +from memory.memory_v3 import Memory, MemoryV3Config +from interco.router_v2 import Router, RouterConfig, RouterMapping +from utils.loader.loader_v2 import ElfLoader +from pulp.cv32e40p_exit.cv32e40p_exit_device_v2 import Cv32e40pExitDeviceV2 +from pulp.cv32e40p_sparse_mem.cv32e40p_sparse_mem_v2 import Cv32e40pSparseMemV2 +from pulp.cv32e40p_irq_injector.cv32e40p_irq_injector import (Cv32e40pIrqInjector, + IRQ_LINES, IRQ_NUMBERS) +from pulp.cpu.iss.cv32e40p_v2 import Cv32e40p as Cv32e40pV2Core, Cv32e40pConfig + + +class Cv32e40pStandaloneConfig(Config): + """Configuration for the CV32E40P iss_v2 co-simulation SoC.""" + + boot_addr: int = cfg_field(default=0x80, fmt="hex", dump=True, desc=( + "Boot address (matches RTL BOOT_ADDR)" + )) + + fpu: int = cfg_field(default=0, dump=True, desc=( + "FPU configuration (F extension, FP register file)" + )) + + zfinx: int = cfg_field(default=0, dump=True, desc=( + "ZFINX configuration (FP operations on the integer register file)" + )) + + pulp: int = cfg_field(default=1, dump=True, desc=( + "PULP configuration (CoreV extensions, misa X bit)" + )) + + core: Cv32e40pConfig = cfg_field(init=False, desc=( + "CV32E40P core configuration" + )) + + mem: MemoryV3Config = cfg_field(init=False, desc=( + "Main RAM configuration" + )) + + stdout: MemoryV3Config = cfg_field(init=False, desc=( + "Virtual STDOUT sink configuration" + )) + + timer: MemoryV3Config = cfg_field(init=False, desc=( + "Virtual TIMER sink configuration" + )) + + debug_rom: MemoryV3Config = cfg_field(init=False, desc=( + "Debug ROM configuration" + )) + + router: RouterConfig = cfg_field(init=False, desc=( + "Router configuration" + )) + + mem_mapping: RouterMapping = cfg_field(init=False, desc=( + "Main RAM address range" + )) + + stdout_mapping: RouterMapping = cfg_field(init=False, desc=( + "Virtual STDOUT address range" + )) + + timer_mapping: RouterMapping = cfg_field(init=False, desc=( + "Virtual TIMER address range" + )) + + debug_rom_mapping: RouterMapping = cfg_field(init=False, desc=( + "Debug ROM address range" + )) + + exit_mapping: RouterMapping = cfg_field(init=False, desc=( + "Virtual EXIT device address range" + )) + + background_mapping: RouterMapping = cfg_field(init=False, desc=( + "Background sparse memory catch-all route" + )) + + def __post_init__(self): + super().__post_init__() + # ZFINX needs the F opcodes in the decoder (routed to the integer + # register file); compressed FP rows are disabled by the core recipe. + isa = 'rv32imfc' if (self.fpu or self.zfinx) else 'rv32imc' + self.core = Cv32e40pConfig(isa=isa, boot_addr=self.boot_addr) + # init=False: never-written bytes must read 0 (testbench memory + # contract), not the 0x57 poison pattern of the default init=True. + self.mem = MemoryV3Config('mem', size=0x0040_0000, atomics=False, latency=0, + init=False) + self.stdout = MemoryV3Config('stdout', size=0x100, atomics=False, latency=0, + init=False) + self.timer = MemoryV3Config('timer', size=0x100, atomics=False, latency=0, + init=False) + self.debug_rom = MemoryV3Config('debug_rom', size=0x1000, atomics=False, latency=0, + init=False) + self.router = RouterConfig(kind='bandwidth') + self.mem_mapping = RouterMapping(name='mem_mapping', + base=0x0000_0000, size=0x0040_0000) + self.stdout_mapping = RouterMapping(name='stdout_mapping', + base=0x1000_0000, size=0x100) + self.timer_mapping = RouterMapping(name='timer_mapping', + base=0x1500_0000, size=0x100) + self.debug_rom_mapping = RouterMapping(name='debug_rom_mapping', + base=0x1A11_0800, size=0x1000) + self.exit_mapping = RouterMapping(name='exit_mapping', + base=0x2000_0000, size=0x100) + # Catch-all (size=0): absolute addresses forwarded so the sparse + # store is indexed like the testbench's. + self.background_mapping = RouterMapping(name='background_mapping', + base=0x0000_0000, size=0, + remove_base=False) + + +class Cv32e40pStandaloneSoc(gvsoc.systree.Component): + + def __init__(self, parent, name, config: Cv32e40pStandaloneConfig, binary): + super().__init__(parent, name, config=config) + + mem = Memory ( self, 'mem' , config=config.mem ) + stdout = Memory ( self, 'stdout' , config=config.stdout ) + timer = Memory ( self, 'timer' , config=config.timer ) + dbgrom = Memory ( self, 'debug_rom', config=config.debug_rom ) + exit_d = Cv32e40pExitDeviceV2( self, 'exit' ) + bg_mem = Cv32e40pSparseMemV2 ( self, 'background_mem' ) + ico = Router ( self, 'ico' , config=config.router ) + core = Cv32e40pV2Core ( self, 'core' , config=config.core , + fpu=bool(config.fpu), zfinx=bool(config.zfinx), + pulp=bool(config.pulp) ) + loader = ElfLoader ( self, 'loader' , binary=binary ) + + ico.o_MAP ( mem.i_INPUT() , mapping=config.mem_mapping ) + ico.o_MAP ( stdout.i_INPUT(), mapping=config.stdout_mapping ) + ico.o_MAP ( timer.i_INPUT() , mapping=config.timer_mapping ) + ico.o_MAP ( dbgrom.i_INPUT(), mapping=config.debug_rom_mapping ) + ico.o_MAP ( exit_d.i_INPUT(), mapping=config.exit_mapping ) + ico.o_MAP ( bg_mem.i_INPUT(), mapping=config.background_mapping ) + + # Three independent masters, one router input port each. + # o_ENTRY is deliberately NOT bound: like the RTL, the core boots at + # the fixed BOOT_ADDR (config boot_addr), not at the ELF entry; the + # entry sync would also rewrite mtvec after a co-sim CSR injection. + loader.o_OUT ( ico.i_INPUT(0) ) + loader.o_START ( core.i_FETCHEN() ) + + core.o_FETCH ( ico.i_INPUT(1) ) + core.o_DATA ( ico.i_INPUT(2) ) + + # Interrupt lines: an external client drives the injector through + # gv::wire_bind; each line lands on the core's native slave port, + # so mip and the WFI wake-up follow the hardware path. + irq_inj = Cv32e40pIrqInjector(self, 'irq_injector') + for name, irq in zip(IRQ_LINES, IRQ_NUMBERS): + irq_inj.o_LINE(name, core.i_IRQ(irq)) + # Debug halt request (RTL debug_req_i): same wire path as the + # interrupt lines. Handled by Cv32e40pIrq::haltreq_sync, which arms + # req_debug and wakes a WFI-parked hart (the RTL sleep unit exits on + # debug_req_i regardless of mie/mip). The port is CV32E40P-specific + # (registered by the Cv32e40pIrq constructor), hence the inline + # SlaveItf instead of a riscv.py getter. + irq_inj.o_LINE('haltreq', gvsoc.systree.SlaveItf( + core, itf_name='haltreq', signature='wire')) + # WFI release (co-simulation only): pulsed externally when the + # DUT's retire stream proves the hart woke (the RTL retires wfi at + # execute and sleeps after; wakes like a debug_req level are not + # all visible as interrupt wires). Cv32e40pIrq::wfi_wake_sync runs + # the three-step release with no architectural side effect. + irq_inj.o_LINE('wfi_wake', gvsoc.systree.SlaveItf( + core, itf_name='wfi_wake', signature='wire')) + + +class Cv32e40pStandaloneTop(gvsoc.systree.Component): + + def __init__(self, parent, name=None, fpu: int=0, zfinx: int=0, pulp: int=1): + super().__init__(parent, name) + + binary = TargetParameter( + self, name='binary', value=None, description='ELF binary to simulate' + ).get_value() + + config = Cv32e40pStandaloneConfig('soc', fpu=fpu, zfinx=zfinx, pulp=pulp) + + clock = vp.clock_domain.Clock_domain(self, 'clock', frequency=50000000) + soc = Cv32e40pStandaloneSoc(self, 'soc', config, binary) + clock.o_CLOCK(soc.i_CLOCK())