Conversation
3e7da68 to
d52f70c
Compare
Branch refreshed — ready for review @haugougThe branch has been refactored and force-pushed. Head is now Commit history (linear, cleaned)Notes on
|
d52f70c to
9d9835a
Compare
|
Refreshed the branch. |
Add the CV32E40P standalone GVSOC target for UVM co-simulation via the RVVI bridge, plus the CV32E40P core model in pulp_cores.py. The platform exposes the CV32E40P RTL generics (corev_pulp, fpu, zfinx, corev_cluster, core_version, num_mhpmcounters) as gvrun parameters and wires the core to a router with the CV32E40P memory map (RAM, virtual stdout/timer sinks, debug ROM, virtual exit). The core model computes misa and mimpid from the configured generics to match cv32e40p_cs_registers.sv, and writes the vendor CSR reset values and write masks consumed by Cv32e40pCsr::build() in csr_cv32e40p.cpp.
Add the CV32E40P virtual exit device that terminates the GVSOC simulation when the test program writes exit_valid at 0x2000_0004, mirroring the cv32e40p virtual peripheral status flags described in test_programs.rst. Includes the C++ device model, its Python wrapper, and the CMake wiring under pulp/cv32e40p_exit/.
Map a new byte-granular sparse-memory component as the interconnect default route (size=0 mapping, absolute addresses). The UVM testbench serves the whole address space from a zero-default sparse memory, so out-of-map accesses must behave the same on the reference platform: reads return 0 until written, writes persist. Without this, stray stores were silently dropped (later readback diverged) and stray fetches faulted with mcause=1 where the RTL fetches 0 and traps illegal instruction (mcause=2). Also: - exit device: retain writes in a 256B backing store so write-then-read (e.g. of the signature addresses) returns the stored value, as it does through the testbench memory. - debug ROM: shrink 16KB -> 4KB to match the linker script dbg region; accesses past it now fall through to the background memory.
First step of the CV32E40P migration to the modular iss_v2 core: - pulp/cpu/iss/cv32e40p_v2.py: core recipe on RiscvCommon with the generic v2 slots (IrqExternal, Event, Csr, ExecInOrder with scoreboard and in-order commit, LsuV2, scoreboarded Regfile, Hwloop) plus the CoreV ISA subset. The CV32E40P-specific CSR map, counters and trap behaviour will come in as slot overrides on top of this base, same layering as Ri5ky. - cv32e40p-v2-spike.py: minimal bring-up platform. LsuV2 drives the fetch/data ports with the io_v2 protocol, so the platform lives on the io_v2 plane (router_v2, memory_v3, loader_v2) following the Ri5ky testbench layout, and reuses its MMIO peripheral (putchar, exit) at 0x10000000. Integer bring-up ISA is rv32im + CoreV: with C enabled the generated decode table still references the rvf compressed handlers even when they are inactive (no F), and nothing declares them without an FPU module — to be addressed separately. Validated with hand-encoded probes on the spike target: cv.lw post-increment (data + pointer) and a 5-iteration lp.setupi hardware loop, both passing.
Cv32e40pCsr models the RTL CSR map on the v2 modular core (same layering as Ri5ky): M-mode-only strict CSR space (undeclared addresses raise illegal-instruction), RTL write masks and reset values, read-only machine information and PULP custom CSRs (uhartid/privlv/zfinx), hardware-loop CSRs readable but not writable through CSR instructions (the architectural LPEND is re-derived from the hwloop module), trigger CSRs with the RTL tie-offs, mcycle with inhibit-aware offset semantics, and fflags/frm/fcsr legality following the RTL fs_off signal across the FPU/ZFINX/no-FPU configurations. The recipe switches the irq slot to the RISC-V privileged scheme (the RTL implements mie/mip/mtvec with standard mcause codes, not the PULP event unit) and keys the ISA cache on the PULP extension set.
…argets The recipe forwards zfinx to the v2 core (register-file routing), tags the ISA cache and the generated ISA name with the pulp/zfinx variant, and emits CONFIG_GVSOC_ISS_FP_STATE_DIRTY on FPU configurations so FP write-backs dirty mstatus.FS as in the RTL. The bring-up platform moves to pulp/cv32e40p_v2_spike.py, shared by three thin targets (cv32e40p-v2-spike, -fpu, -zfinx): one target name per core configuration, so each gets its own serialized platform tree (a non-default --parameter run would fall back to the JSON config path, which the io_v2 components do not support).
The spike platform now builds rv32imc (base) and rv32imfc (fpu/zfinx) decode tables. On the ZFINX variant the recipe disables the compressed FP loads/stores: the RTL compressed decoder only accepts them with FPU == 1 && ZFINX == 0 (cv32e40p_compressed_decoder.sv).
Counter model matching the RTL mhpm scheme (cv32e40p_cs_registers.sv): minstret counts retires gated on mcountinhibit.IR, mhpmcounterN advances at most once per retire when its mhpmeventN mask intersects the event lines the instruction fired, mcycle/mcycleh become one 64-bit count that freezes at the current value while mcountinhibit.CY is set. Events are accumulated per instruction by Cv32e40pEvents and committed at retire (trapping instructions drop them); Cv32e40pExec keeps the core on the full handlers while any counter is enabled, since only those fire the event lines (same scheme as the Ri5ky PCMR pairing). Only the architectural lines are modelled: instr, load, store, jump, branch, branch taken, compressed. The timing lines (stalls, imiss, APU) never fire and their counters read zero.
Port of the cv32e40p-standalone co-simulation platform to the io_v2 plane the iss_v2 LSU requires: same memory map (RAM, stdout/timer sinks, debug ROM, virtual exit device, background sparse memory catch-all), rebuilt on router_v2/memory_v3/loader_v2, one target per core configuration (cv32e40p-v2-standalone, -fpu, -zfinx). The exit device and the sparse background memory get io_v2 siblings with the same register map and semantics as the io-plane originals; atomics are rejected explicitly (no A extension on this platform). Memories pass init=False so never-written bytes read 0 like the testbench memory (also fixed on the spike platform, which inherited the poison default).
…he v2 core An external stepper (RVVI bridge) samples the register file at each retire, but the retire hook fires at issue while an async load writes its rd only with the LSU response: expose a commit stream instead - a PC ring pushed once the writeback is architecturally visible, at commit-FIFO drain for held instructions - and pin the core to the full dispatch path while the stream is observed (the fast path skips the FIFO bookkeeping the stream is built on). Restore the RTL WARL result of mtvec over the generic IrqRiscv callback, which drops the mode bit the RTL keeps (cv32e40p_cs_registers.sv:667). Boot the co-simulation platform at the fixed BOOT_ADDR like the RTL: the ELF-entry sync would also rewrite mtvec after a CSR injection.
…code fixes on the v2 core
- Cv32e40pIrq personality: RTL priority ladder (31..16, MEI, MSI, MTI),
vectored entry from mtvec{base,mode}, single-source IRQ_MASK and a
mie write fixup (the generic mie_access bypasses the write mask).
- cv32e40p_irq_injector component: 19 wire masters driven through
external_bind; the standalone platform binds them to core.i_IRQ(n).
- Cv32e40pCore / Cv32e40pException personalities: mcause stays sticky
across mret and exception entry masks the mtvec mode bits.
- Cv32e40pRegfile: one-shot write-back suppression armed by the FP
trap sites (CONFIG_GVSOC_ISS_CV32E40P_FP_TRAPS, set by the recipe on
fpu/zfinx variants).
- Csr personality: user counter aliases (cycle/instret/hpm and their
high views), mip front-end view (reads mirror the wire-driven store,
writes dropped as on the RTL), architectural hwloop LPEND shadow,
mip cleared in reset, FS not dirtied by trapped FP instructions.
- Events: trap sequence stamps on redirected commits so the bridge can
gate the deferred state compare across a drain window.
- priv.hpp: CV32E40P csr dispatch where csrrc/csrrs with rs1=x0 and
csrrsi/csrrci with uimm=0 are reads, not writes.
- Recipe: fence/fence.i decode relaxed to funct3-only as in the RTL
decoder (reserved fields ignored), pulp parameter and the
cv32e40p-v2-standalone-nopulp target (rv32imc, no CoreV, no X bit).
…sor on the v2 core - The recipe now emits RISCV into the generated ISA header: the shared isa_lib int.h selects the RISC-V (not legacy RISCY) source order for lib_VEC_SHUFFLE2_*, and no v2 build was defining it (the v1 build gets it from the iss CMakeLists). Fixes cv.shuffle2.h half-word swap. - Debug entry writes dcsr.cause atomically with the entry, as the RTL does, and bumps trap_seq so a commit-FIFO entry spanning a haltreq redirect is flagged stale like any other trap redirect. - inflight_pending() exposes the parked-instruction state the bridge needs to drain the pipeline before a forced redirect.
Debug entry with a caller-supplied cause (req_debug_cause, written into dcsr atomically with the entry), debug-CSR views over the base raw fields (dcsr with the RTL WARL mask, dpc 16-bit aligned, dscratch0/1) so the debug-ROM csrrw accesses stop raising illegal-instruction, no interrupt take while in debug mode (the RTL controller ignores irq_req there), and the debug_handler address in the v2 recipe (was 0).
The 0x7B0-0x7B3 views were declared with the base CsrAbtractReg type, whose default check_access grants every access. The RTL decoder (cv32e40p_decoder.sv, CSR_DCSR..CSR_DSCRATCH1) raises illegal-instruction whenever they are touched outside debug mode, and generic_exception_test checks exactly that. Give the views a Cv32e40pDebugCsr type that raises illegal-instruction while exec.debug_mode is clear; debug-ROM code still passes the check.
The Debug spec only defines dret in debug mode, and the RTL decoder treats it as illegal elsewhere. The model executed it unconditionally and jumped through a never-written dpc. Guard dret_exec on debug_mode, matching the sfence.vma handling.
dcsr.prv is WARL and CV32E40P implements machine mode only, so the hardware reads back 3 regardless of what debug software writes. The view kept the written value. Mask the write down to the implemented fields (ebreakm, ebreaku, stepie, step) and force prv to M.
…atch) The core implements one mcontrol trigger with an execute-address match: - tdata1 exposes bit 2 (execute match enable) as the only writable bit, tdata2 is fully writable; both writes are accepted only in debug mode and silently dropped otherwise, matching the RTL's tmatch_*_we & debug_mode_i gating (an M-mode write is NOT an illegal instruction). - Cv32e40pIrq::check() raises the debug request with cause=2 when the trigger is armed and the about-to-dispatch PC equals tdata2: the match is evaluated BEFORE execution, as in the RTL (trigger_match_o on pc_id), so the matched instruction is not retired and dpc points at it.
With latency=1 on the soc memories every load parks its sync follower in the inflight ring for one extra boundary: at any asynchronous injection point (debug entry, interrupt take) the ISS has already executed an instruction the RTL killed in ID, and no later repair can undo the writeback. latency=0 on all ports (mem, stdout, timer, debug_rom) makes every commit architecturally final at its own boundary - the contract an external lockstep driver relies on when it steps the model one commit at a time. Standalone-platform behaviour is unaffected: instruction traces carry the same retire order; only stall cycles differ.
…formed IRQ collision) The v2 personality now models the four CV32E40P debug-entry causes natively, with the arming order in Cv32e40pIrq::check() encoding the Debug-spec priority (trigger 2 > haltreq 3 > step 4): - ebreak with dcsr.ebreakm=1 (cause 1): the new CONFIG_GVSOC_ISS_CV32E40P_V2 define (cv32e40p_v2.py) gates the hook in the shared isa/rv32i.hpp / rv32c.hpp decoders; the instruction arms the request via ebreak_enter_debug() and never retires (mcause/mepc untouched, entry row = first debug-ROM instruction, like the RTL). - single-step (cause 4): dret with dcsr.step=1 opens the window (dret_step_check, depc still live); check() re-enters once current_insn moved off step_pc - an exception redirect lands the entry on the handler address as the spec requires. Interrupts are masked inside the window unless dcsr.stepie=1. Every entry closes the window, so a haltreq during the window (or a debugger clearing dcsr.step before dret) cannot fire a stale cause=4 later. - haltreq (cause 3): first-class injector wire (RTL debug_req_i), handled model-side by haltreq_sync - it arms req_debug AND wakes a WFI-parked hart with the full three-step release, which only the model can run. The RTL sleep unit exits on debug_req_i regardless of mie/mip, while the generic release is gated on mie & mip alone: a halt request arriving with mie=0 would otherwise never wake the model. The level is tracked so a held-high line re-halts right after dret, as the level-sensitive RTL input does. - wfi_wake wire (co-simulation only, no architectural effect): pulsed externally when the DUT's retire stream proves a wake the interrupt wires cannot carry; runs the same three-step release. wfi with a pending debug request degrades to a nop (the RTL never sleeps with debug_req_i asserted). Informed interrupt+debug collision: when the DUT's entry row carries an interrupt take's CSR writes, the external lockstep driver posts the taken cause id in collide_irq_id and check() takes exactly that line ahead of the entry (irq_take, extracted from the ladder) - dpc lands on the vectored handler entry, mstatus/mepc/mcause carry the take. The model never guesses the arbitration: the outcome depends on cycle timing only the DUT observes. Injector grows from 19 to 21 lines (haltreq, wfi_wake), bound in cv32e40p_v2_standalone.py to the ports the Cv32e40pIrq constructor registers. Hardening: - check() masks the informed collision line against IRQ_MASK before irq_take (defense in depth on top of the driver-side validation) - cv32e40p_irq_pick's priority fallback would otherwise turn an unwired id into a silent phantom MTI take. - Cv32e40pIrq::reset() override clears step_state, collide_irq_id and req_debug_cause: a live single-step window or an unconsumed collision id must not survive a reset. - the wfi debugger-responsiveness guard (debug mode, dcsr.step, pending req_debug) is owned by the personality's wfi_exec - the shared IrqRiscv::wfi_handle keeps its upstream behaviour. - release_wfi() helper deduplicates the three-step release shared by haltreq_sync and wfi_wake_sync. - cv32e40p_v2.py enables the opt-in conformance the core commits gate for the other cores: CONFIG_GVSOC_ISS_RVC_STRICT and the FLEXFLOAT_TININESS_AFTER_ROUNDING c-flag.
…dary An IRQ take adjacent to a debug entry lands its mcause on the row BEFORE the entry row; an external lockstep driver can only nominate a candidate (wired id + the take's expected mepc), since only the model knows the dispatch boundary. The model certifies the candidate in Cv32e40pIrq::check(), where current_insn IS the boundary: the take fires only when the boundary matches the expected mepc, stale candidates are discarded with a warning. New fields collide_expected_mepc / collide_certify, armed alongside collide_irq_id and cleared on every check()/reset().
The external stepper needs to know, on rvfi_trap rows, whether the committed instruction itself raised an architectural exception: an ecall/ebreak commit is the faulting step and must be consumed there, while a pipeline kill-and-replay row commits a NORMAL instruction the DUT re-executes on the next row - consuming it there shifts the compare stream by one retire. The trap_seq stamp cannot separate the two (the faulting insn is stamped after its own raise), so retire_account records exec.has_exception into commit_trapped[], carried through the inflight ring at drain like the trap_seq stamp.
…addr CV32E40P UM (debug.rst): an exception taken in debug mode jumps to dm_exception_addr without updating mepc/mcause/mstatus or the privilege mode, and mret/uret in debug mode jump back to this address "without affecting status registers". The generic paths went to mtvec (clobbering the CSRs) and to mepc respectively. Cv32e40pException::raise gains the debug-mode branch and reads the new debug_exception_handler config key (falls back to the debug handler entry when absent); Cv32e40pCore::mret_handle redirects before the generic side effects. The platform passes the testbench default 0x1A111600 (uvme_cv32e40p_cfg). Exercised by debug_test tests 11/12/13 (illegal CSR access, ecall and mret inside the debugger).
… like the RTL Two entry paths, two rules (cv32e40p_controller.sv): on a shared DBG_TAKEN_ID boundary the trigger outranks a haltreq that latched asynchronously before it (priority table, trigger highest), while a closing single-step window enters through DBG_TAKEN_IF, whose cause mux never looks at trigger_match - step wins and the trigger fires on the following session. The trigger arm now overrides an armed cause-3 request but yields when the step window closes on the boundary; a window still sitting on its own step_pc (dret straight onto the matched address) keeps the trigger-first behaviour. The trigger match - synchronous debug, not an asynchronous event - is evaluated ahead of the internal async gate (skip_irq_check, consumed inside check() core-side) on EVERY dispatch boundary and falls through to the entry when it arms; the haltreq re-arm, step window and interrupt ladder stay behind the gate. Both directions were wrong before: dcsr.cause read 3 where the DUT said 2 (debug_test_trigger) and 2 where the DUT said 4 (debug_test, single-step region).
CV32E40P has no PMP: the UM's CSR chapter lists no pmpcfg/pmpaddr bank and the RTL raises illegal instruction on any access. The generic model declares the whole bank (pmpcfg0..15, pmpaddr0..63) even when the PMP module is the empty variant - CONFIG_GVSOC_ISS_PMP is a type name and is always defined - so a read returned zero instead of trapping. Undeclare the bank alongside the other nonexistent CSRs; the raise-on-unsupported path then matches the hardware. Found by the all_csr_por CSR sweep: it diverged at pmpcfg0 after 1.16M clean retires (the DUT entered the illegal handler, the ISS carried on into pmpcfg1). With the fix the full sweep passes cleanly; cv32e40p_csr_access_test, modeled_csr_por and readonly_csr_access pass unchanged.
… count retires per spec Two extensions to the model's external-stepper contract, both inert in standalone runs. Async-event hold (Cv32e40pIrq::dpi_async_hold). exec.skip_irq_check is a one-shot consumed at the first check() of a step quantum, but a 20 ns engine quantum runs several dispatches: every dispatch after the first ran unguarded, and the model took pending wire IRQs (or re-armed haltreq) at a boundary of its own stepping cadence, racing the DUT's entry row. The hold is a LEVEL the driver owns: asynchronous events are delivered as state (mip from the wires, req_debug latched) but never taken at a model-chosen boundary; the take_irq / take_debug injection windows lower it so the entry lands on the DUT-proven boundary. Synchronous conditions - execute-address trigger, single-step window close, ebreak-to-debug, driver-armed causes - keep their architectural timing and ignore the hold. Retire accounting (Cv32e40pEvents / Cv32e40pCsr). The commit ring now carries the raw encoding next to the PC (truncated to 16 bits on RVC rows: the fetched word holds the next parcel in its upper half), through the inflight FIFO as well, so the stepper can serve a live instruction-binary compare. minstret follows the RTL event exactly (cv32e40p_id_stage.sv:1639 and :1659): EBREAK never counts - the trapping forms were already dropped with the event lines, count_instr covers the debug-entry ebreak that retires without a trap - and a CSR write to either counter half suppresses that row's increment (cs_registers write-wins gate), armed by the new write callbacks and consumed at the same retire in hpm_commit. Validated in step-and-compare with minstret/minstreth/instreth and the instruction-binary compare enabled.
…legal Undeclaring a CSR removes it 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 synchronous traps to S-mode: mstatus.spp set, mcause/mepc left stale, entry through a garbage stvec. Zero the backing fields once at construction; nothing can write them afterwards, since an undeclared CSR raises illegal on any ISA access, like the RTL. sret itself now raises illegal instruction instead of running the generic handler: CV32E40P has no S-mode and the RTL decoder rejects the opcode. The generic path would jump through sepc - never architecturally written on this core - and demote the privilege mode via mstatus.spp. Same guard shape as dret_exec outside debug mode.
x0 is hardwired to zero (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: cv.lbu x30,(x0),x14 computed the load at address 0 correctly and then executed x0 += x14 in the model, while the RTL register file discards the write. The stray write is invisible at the writing instruction (the architecturally written register is x30, and it is correct); everything downstream that reads x0 then diverges. Writes to x0 are architectural no-ops: drop them up front in set_reg, so they cannot consume the wb_suppress one-shot either. The v1 ISS has the same defect (regfile_implem.hpp:55, and pulp_v2.hpp writes in_regs back at 32 sites without even the rd==rs1 guard): it affects ri5cy standalone runs and is a candidate upstream report.
… the writing instruction The RTL evaluates the counter increment gates on mcountinhibit_q in the cycle the CSR write commits (cv32e40p_cs_registers.sv:1428), so the instruction that writes mcountinhibit still counts under the OLD gates and the write takes effect from the next instruction on. Latch the pre-write value when the write executes and let hpm_commit consume it at that same retire; both flags clear unconditionally, they belong to that retire only.
9d9835a to
f5764c7
Compare
|
Force-pushed: the branch is reworked and extended since the last push. The |
CV32E40P personality for the ISS v2, under
cpu/iss_v2/{include,src}/cores/cv32e40p/:mstatus write mask, the CV32E40P-only CSRs, illegal-instruction behaviour
for the undeclared banks (PMP, S-mode);
dm_exception_addr;(trigger > ebreak > haltreq, single-step, async-take hold for a lockstep
driver);
(exhaustive sweep in both directions);
CV32E40P-specific behaviour is opt-in (defines and the new target). The few
changes to shared XPULP sources are plain bugfixes — for example the
post-increment forms corrupted x0.
History is grouped by theme; each commit message states what changed, why,
and how it maps to the CV32E40P RTL or the RISC-V spec.
Validation on this exact tree: lockstep RVVI co-simulation against the
CV32E40P RTL, 4 configurations, corev-dv random plus directed tests;
riscv-arch-test 3.9.1 via RISCOF with Sail 0.13.1 (431/434 overall, F suite
342/342).