Skip to content

feat(cv32e40p): add CV32E40P ISS core model - #146

Open
marpac3 wants to merge 36 commits into
gvsoc:masterfrom
marpac3:mpaci/cv32e40p-clean
Open

marpac3 wants to merge 36 commits into
gvsoc:masterfrom
marpac3:mpaci/cv32e40p-clean

Conversation

@marpac3

@marpac3 marpac3 commented Apr 9, 2026

Copy link
Copy Markdown

CV32E40P reference-model line for the GVSOC core repo:

History is grouped by theme (flexfloat, RVC decoding, shared-ISS guards,
v2 exec/prefetch/LSU, debug, CV32E40P core model); 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); build smoke on pulp-open and snitch with bit-identical traces.

Once the standalone flexfloat/RVC PRs merge, this branch rebases onto master
and shrinks to the CV32E40P-specific commits.

@haugoug haugoug left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Marco,
thanks a lot for your contribution !
It seems you used co-simulation to do fine-grain comparison with RTL, can you give more details ?

@marpac3

marpac3 commented Apr 10, 2026

Copy link
Copy Markdown
Author

Hi Germain!
Yes I'm using GVSOC as an ISS to use it in the cv32e40p testbench (this is the docs of the current TB to get the idea, it uses a proprietary ISS (ImperasDV) https://docs.openhwgroup.org/projects/core-v-verif/en/latest/cv32_env.html)

The idea is to have a full model of the cv32e40p in GVSOC, so inside the testbench, at each instruction retire of bot the RTL and the GVSOC implementation, PC, GPRs and CSRs are compared.

I opened the PR since the model, GVSOC side, is stable enough. It does follow this openHW docmentaion (https://docs.openhwgroup.org/projects/cv32e40p-user-manual/en/cv32e40p_v1.8.3/intro.html).
I will add more stuff for the GVSOC "testbench"part, maybe I'll map more Virtual Peripherals to completely recreate the TB that is used in the cv32e40p project.

@haugoug

haugoug commented Apr 10, 2026

Copy link
Copy Markdown

Nice thanks !

@haugoug

haugoug commented Apr 10, 2026

Copy link
Copy Markdown

There are lots of modifications in the generic part of the ISS. Unfortunately, this ISS is still used for some old targets for which I don't have access to the tests anymore.
Could you please surround all the modifications in the generic part with ifdef ?
There are a few new files with my name, it's better if you put yours :)
Could you also put the exit device in the pulp repo ? I try to put only generic devices in this repo.

@marpac3
marpac3 force-pushed the mpaci/cv32e40p-clean branch from c8f567e to c03a77f Compare May 26, 2026 08:26
@marpac3

marpac3 commented May 26, 2026

Copy link
Copy Markdown
Author

Branch refreshed — ready for review @haugoug

The branch has been refactored and force-pushed. Head is now c03a77f5 (9 commits on top of 81298676 "Fix to build correctly on Mac OS").

Commit history (linear, cleaned)

c03a77f5 doc: drop residual internal markers from ISS comments
945fb065 fix(iss): guard target-specific code paths in shared ISS sources
09f27f9e doc(iss): clean up comments and author headers
74fb5918 fix(iss): restore CV32E40P isolation + mcountinhibit RO default
e3b10d6a fix(iss/cv32e40p): hwloop immediate target encoding — word offset
020b8051 refactor(iss/cv32e40p): use config fields for simple per-target masks
c2de42ff refactor(iss): move CV32E40P specifics out of shared ISS files
635cf8c2 refactor(devices): move cv32e40p_exit to gvsoc-pulp
11cafde1 feat(cv32e40p): add CV32E40P ISS core model

Each commit is atomic, has a focused subject, and a short generic body without internal jargon.

Cross-target safety

The 4 cross-template call sites that touch shared headers (rv32i.hpp, csr.cpp, exec_inorder.cpp) are gated with #ifdef CONFIG_GVSOC_ISS_CV32E40P — verified against origin/master (commit 81298676) that no virtual hook call is left unguarded for the other targets.

Upstream make all cross-target build: ISA generators for RV64 / Snitch / Spatz (32+64) / CVA6 (with vector) / PULP FC / PULP cluster / FlexFloat all compile clean. Only pre-existing fail is pulp/redmule/include/config.h _Float16 (unrelated to this PR — same fail in origin/master).

Related PRs

  • gvsoc/gvsoc-pulp#74 — companion: standalone platform, exit device, core config (head d52f70c).
  • gvsoc/gvsoc#252 — submodule pointer bump (head 86e2e87).

Suggested merge order: this PR first, then #74, then #252 (after submodule pointers are re-pointed to upstream).

Happy to address any feedback.

@marpac3

marpac3 commented May 26, 2026

Copy link
Copy Markdown
Author

Hi @haugoug I have reviewd and modified most of the files, at least I have cleaned up commetns, added a few ifguard and restructured the code a bit.

There are a few part of the shared code which have been modified, but every single of them is commented and explained on how the modification of the sahred code does not affects the functionality of GVSOC.

I cannot be really sure obviusly, I have build other targets with make all in GVSOC to check if nothing else was having problems, and it looked like it. But let me know if i can do other checks that you know to validate that this PR does not break anything!

@haugoug

haugoug commented May 31, 2026

Copy link
Copy Markdown

Unfortunately I don't have access anymore to part of the tests, so I just prefer to have zero modification in the generic. There are still modifications, can you guard them as well ?
Also what is the purpose of memory_config.hpp ?

@marpac3
marpac3 force-pushed the mpaci/cv32e40p-clean branch from c03a77f to 2221697 Compare June 12, 2026 15:19
@marpac3

marpac3 commented Jun 12, 2026

Copy link
Copy Markdown
Author

memory_config.hpp was an unused leftover, it's gone. All the CV32E40P changes
in the shared sources are now guarded by CONFIG_GVSOC_ISS_CV32E40P — without
the define they preprocess back to the upstream sources, byte for byte.
Force-pushed with a cleaned-up history; diff vs the previous state:
https://github.com/marpac3/gvsoc-core/compare/c03a77f5b680..22216971a572

@TheSSDGuy

TheSSDGuy commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Hello @haugoug, do you think we can merge this? @marpac3, will you open/merge a PR also for this dpi-fidelity. Actually we moved to cv32e40p core model also on magia_V3 and I might need this merged before to open a PR for magia. Thanks.

marpac3 added 17 commits August 21, 2026 10:20
Add a CV32E40P core model implemented as subclasses of the generic ISS
(Csr, Core, Irq, Exception): M-mode-only privilege handling, the
CV32E40P CSR map (mtvec/mstatus/mcause write masks, hwloop and trigger
CSRs), RTL-accurate IRQ timing, and the CoreV v2 ISA generator
description. All files compile only when CONFIG_GVSOC_ISS_CV32E40P
is set.
Restructure the CoreV (Xpulp) ISA header for the CV32E40P v2 encodings:
hwloop immediate targets use word offsets, and p.* label aliases map to
the canonical handlers per the isa_gen naming scheme. This header has
no in-tree consumer on master; its only user is the CV32E40P core
model.
Hook the CV32E40P subclasses into the shared ISS sources. Every change
in the shared files sits inside CONFIG_GVSOC_ISS_CV32E40P (declaration
of the subclass hook points, CV32E40P CSR registration, IRQ timing
check); with the config unset the sources are identical to master, so
other targets are unaffected.
…tion

Add a virtual compute_trap_entry(base, cause, is_interrupt) so the ISS
computes the trap-vector entry itself when an interrupt is taken. The base
implementation returns the mtvec base (direct mode), preserving upstream
behaviour for non-CV32E40P targets; Cv32e40pIrq overrides it to return the
vectored entry base + cause*4 when mtvec.MODE selects vectored.

This lets an external step-and-compare driver own interrupt entry via
informed injection (the DUT is the oracle for which interrupt fires; the
ISS takes exactly that cause and computes the matching vectored entry)
instead of copying the DUT trap CSRs. The call site in Irq::check() is
guarded by CONFIG_GVSOC_ISS_CV32E40P (else branch keeps the upstream
mtvec.value), so the shared ISS preprocesses byte-identical without the
define.
The RTL decoder (cv32e40p_decoder.sv) raises illegal-instruction on any
write op to UHARTID/PRIVLV/ZFINX, and rejects even reads of ZFINX
(0xCD2) when FPU=1 && ZFINX=0. The ISS declared all three in every PULP
config with silently-ignored writes, so a 'csrr 0xCD2' that traps on
the RTL executed on the ISS (matmul_32b_float probes exactly that at
runtime).

Declare ZFINX only when the RTL accepts it (FPU=0 or ZFINX=1) so the
unsupported-CSR path raises illegal-instruction like the RTL, and mark
all three write_illegal (csrrs/csrrc with rs1=x0 count as reads on both
sides, so read-only ops stay legal).
The FS-dirty promotion block in isa_lib/macros.h redefined FREG_SET/
FREG32_SET to REG_SET under the assumption that CV32E40P always builds
with ISS_SINGLE_REGFILE. That is only true for ZFINX builds: with
FPU=1 ZFINX=0 the FP bank is separate, and the redefinition sent every
FP arithmetic result to the integer regfile while FREG_GET kept reading
the FP bank. On matmul_32b_float (pulp_fpu) an fmadd.s overwrote the
x15 data pointer and the next flw faulted outside the memory map;
fmv.w.x results never reached the FP bank at all.

Keep REG_SET on single-regfile builds and use set_freg otherwise. With
this and the 0xCD2 fix, matmul_32b_float runs correctly through all FP
arithmetic.
The RTL decoder rejects the reserved rounding modes (static rm 101/110,
and dynamic rounding when frm holds 101/110/111); the ISS silently fell
through the setFFRoundingMode switch and executed the instruction with
the previous host rounding mode, diverging from the RTL at the first
reserved-rm encoding of illegal_fp_instr_test.

Raise ISS_EXCEPT_ILLEGAL from setFFRoundingMode for those encodings and
kill the side effects of the trapped instruction, as the RTL does: the
fflags update returns early when an exception is pending, and the
CV32E40P FREG_SET/FREG32_SET writeback macros evaluate the value first
(the raise happens inside it - testing has_exception before would
always see the pre-raise state) and then skip both the register write
and the FS=Dirty promotion. All the changes stay inside the
CONFIG_GVSOC_ISS_CV32E40P block; RMM (rm=4) keeps the pre-existing
unimplemented-mode exit, pending a decision on implementing it.

illegal_fp_instr_test (pulp_fpu) now passes end to end, every trap
matching the RTL one for one. Non-regression: matmul_32b_float
(pulp_fpu), pulp_hardware_loop (pulp) and hello-world (default) all
pass; the zfinx build (single-regfile branch of the same macros) is
unchanged.
REG_SET now applies the same has_exception guard as the FP write-back
macros. A reserved FP rounding mode raises illegal-instruction inside
the value expression, and fcvt-class results land in an integer rd, so
guarding only the FP macros left that path writing a trapped result.
The value is evaluated before the guard so the raise is observed.
Make corev.hpp compile on both ISS generations, following the
CONFIG_GVSOC_ISS_V2 dual-implementation pattern already used by
pulp_v2.hpp:

- Include the isa_lib macros directly (like rv32i.hpp does): iss_v2
  emits ISA-subset includes in subset order, so this header can no
  longer rely on rv32i.hpp having pulled them in first.
- Guard the v1-only hardware-loop machinery (stub handler, exec/csr
  shadow state) and add iss_v2 setters that program the Hwloop module
  directly. Same RTL-grounded semantics as the v1 path: lpstart/lpend
  bits [1:0] hardwired to 0, and the module end set to LPEND - 4
  because the v2 check() compares the just-executed pc (the core loops
  back from the last body instruction). Known bring-up limitation: the
  v2 Hwloop module also backs the CSR read path, so a raw lpend csrr
  reads back LPEND - 4 until a CV32E40P hwloop personality keeps the
  architectural value.
- cv.elw falls back to a plain load on iss_v2 (no event-load LSU on
  this core), mirroring the pulp_v2.hpp fallback.
- The CoreV2 ISA subset now declares its handler header via includes=,
  like PulpV2 does; v1 builds keep getting it from the core class.hpp
  (double include is guarded).

Validated on the cv32e40p-v2-spike target (hello + hand-encoded
cv.lw post-increment and lp.setupi probes, all passing); the v1
targets are unchanged (two identical runs).
…tatus policy

Two generic knobs for core personalities whose RTL implements a closed,
M-mode-only CSR map:

- Csr::raise_on_unsupported_csr: when set, a CSR address missing from the
  declared-register map raises illegal-instruction instead of falling back
  to the legacy switch, so the declared map is the whole CSR space. The
  exception is raised at most once per instruction: the csrrw handlers
  dispatch the read and the write back-to-back and Exception::raise is not
  idempotent (a second call would capture mpie after irq_enable was
  already cleared).
- CONFIG_GVSOC_ISS_CORE_MSTATUS_WRITE_MASK: the recipe takes over the
  whole mstatus policy; the generic vector/user-state clears and the
  forced-dirty FS/SD reset bits do not apply.

Plus CsrAbtractReg::set_write_mask and Csr::undeclare_csr accessors used
by such personalities. Default behaviour is unchanged for every existing
core.
…ent irq_riscv header

On iss_v2 the corev handlers are only pulled in by the CoreV2 subset, so
the CV32E40P word-offset encoding of lp.starti/lp.endi applies there
unconditionally (the v1 gate keys on CONFIG_GVSOC_ISS_CV32E40P, which v2
builds do not define).

irq_riscv.hpp uses InsnEntry but did not include insn.hpp; the generated
ISA header can emit it first, before any header that provides the type.
Restore the disabled exception stubs in the unsupported-CSR fallthroughs
and the original comment wording in the Core constructor: the strict-CSR
and mstatus-mask additions must not drop or reword any upstream line.
…efine

The RTL forces mstatus.FS to Dirty on any FP register-file write (FP
loads included) when the FPU registers are in the ISA. A core recipe
opts in with CONFIG_GVSOC_ISS_FP_STATE_DIRTY, which adds an
iss->csr.fp_state_dirty() call to the iss_v2 FREG_SET/FREG32_SET
macros and to the flw handlers (the LSU writes the register there, the
macros never see it). Without the define every branch compiles exactly
as before.
Inactive rows never decode nor execute (the decoder checks is_active
first), but their emitted handler symbols forced the generated table to
reference handlers from headers of disabled subsets. With NULL on
inactive rows an rv32imc core without the FPU module now builds on
iss_v2 (the compressed FP rows no longer pull in rvf.hpp).
…v2 core

Extend the three CONFIG_GVSOC_ISS_CV32E40P gates in the FP rounding
helpers to a standalone CONFIG_GVSOC_ISS_CV32E40P_FP_TRAPS define so the
iss_v2 recipe can enable reserved rounding-mode traps without the v1
core define, arming the regfile write-back suppression on the raise
(the v2 macros have no write-back guard).

On hwloop end writes, keep the architectural LPEND in the csr
personality: the loop module stores end - 4 and re-deriving the CSR
readback as get_end() + 4 would return 4 on a never-programmed loop.
The CoreV2 table accepted encodings the RTL decoder rejects, so on an
illegal-instruction probe the reference model executed the word and
trapped one instruction late (mepc off by 4). Exhaustive sweep of the
four custom opcode spaces against the cv32e40p_decoder.sv rules found
three gap classes, all fixed here:

- custom-3 SIMD: bit 25 must be zero for the non-.sci variants, cv.abs
  and cv.cplxconj require rs2=0, extract/insert and the .sci shifts and
  shuffles bound their immediates (138 rows tightened);
- cv.bitrev: bits 29:27 must be zero;
- cv.elw decodes only when the core is built for a cluster - the subset
  takes an elw parameter now, defaulting to the RTL COREV_CLUSTER=0.

The inverse direction was also swept: no RTL-legal encoding was left
without a decoder row.
marpac3 and others added 19 commits August 21, 2026 10:20
The RTL raises fflags_we on apu_valid, so compare, convert, classify and
fmv.x ops dirty FS even when no flag ends up set. FP register write-backs
already got this from the FREG*_SET macros; route the GPR-writing handlers
through the same fp_state_dirty hook, guarded by the existing
CONFIG_GVSOC_ISS_FP_STATE_DIRTY opt-in.
A zero-latency first beat retires synchronously inside data_req_aligned
and frees its request entry; arming the second-beat state on the entry
peeked afterwards contaminated the free list, so a later aligned access
fired a phantom second beat and its instruction was never terminated
(commit starvation). Arm the state before issuing the first beat, disarm
it when the queue stalls the request, and retire the held instruction
inline when the second beat completes synchronously.
PrefetchSingleLine::flush() only set buffer_start_addr=-1 and relied on
the unsigned fast-path index going out of window. The index wraps to
addr+1 for every address, so a fetch in the first bytes of the address
space (addr <= 0x0b with a 16-byte line) is still in-window and the
flushed line was read without a refill: stale bytes were decoded (bogus
next-PC on a redirect into that window). Track validity with an
explicit buffer_valid flag, force the refill path while it is clear,
and honor a pending front-end flush in the fast dispatch handler too
(only the slow handler did).
The CV32E40P manual (instruction_set_extensions.rst, cv.clipr/cv.clipur)
defines the register-form clip bound as rs2' = rs2 & 0x7FFFFFFF; the
handlers used rs2 as-is, so a negative rs2 produced an inverted range
(low > high) and lib_CLIP returned the low bound where the RTL, per
spec, clips against the masked value (verified 28/28 on a
characterization program incl. INT_MIN edge cases). Masking also
removes the implementation-defined signed narrowing of the previous
code.
External co-simulation controllers live in a separate shared object and
can only call ISS methods that inline: the model library does not export
its symbols, so a .cpp-defined method links but fails at its first call.
Hwloop state is restored through set_start/set_end/set_count; their
definitions move to the header - a pure relocation, no semantic change.
The norm-round MAC helpers (lib_MAC_*_NR_R) computed the accumulate sum
in exact precision and added the rounding constant after the fact.  The
RI5CY-family mult datapath wraps a + b*c + round to 32 bits before the
arithmetic (signed) or logical (unsigned) shift, so whenever the exact
sum crosses the 32-bit boundary the two disagree on the sign of the
result.  Move the rounding constant inside the 32-bit wrap and shift the
wrapped value.

The unsigned pair also gains an explicit uint32_t multiply: uint16_t
operands promote to int and 65535*65535 overflows it.

CV32E40P exercises the SL/SH path (its Z* variants go through local
reimplementations); the ZL/ZH helpers get the same fix for consistency.
The cv.lb/lh/lw post-increment handlers performed the load first and
wrote the incremented base last, so with rd == rs1 the increment
overwrote the loaded data.  The CV32E40P ISA extension spec gives the
loaded data priority in that case.  Capture the base (and, for the
register-register forms, the offset - rs2 may alias rd too) up front,
write the incremented base, then perform the load so the loaded data
lands last.  Covers the 20 RI/RR handlers in both the fast and the
event-accounting paths.
…g semantics)

Shared RVC decoder header (v1 and v2 exec models):

- Reserved code-points now raise an illegal-instruction exception
  instead of executing as their all-zero-field neighbours: c.addi4spn
  with nzuimm=0, c.addi16sp/c.lui with imm=0, c.lwsp with rd=0 and
  c.jr with rs1=0. Guards test the raw opcode with the same masks the
  CV32E40P compressed decoder uses; both fast and checked handler
  variants are covered.
- c_ebreak_exec now mirrors the 32-bit ebreak_exec semantics: in debug
  mode it re-enters the debug ROM at the halt address (the previous
  dcsr-bit test was dead code and the ISS walked past the instruction);
  with dcsr.ebreakm set in M-mode it enters debug; otherwise it raises
  BREAKPOINT. The semihosting probe stays on the uncompressed form
  only, where it is specified.

The reserved code-point checks are opt-in via CONFIG_GVSOC_ISS_RVC_STRICT
(defined by cv32e40p_v2.py): spec-correct, but 0x0000 turning from a
silent nop into a trap would change the traces of every other core -
they keep the historical permissive decoding by default. The c.ebreak
debug-mode alignment with the 32-bit twin stays ungated (inert for
cores that never enter debug mode).
New CONFIG_GVSOC_ISS_CV32E40P_V2 branch in the shared decoders: when
csr.ebreak_m_mode_enters_debug() (dcsr.ebreakm=1 in M-mode) the
instruction arms a debug request (cause=1) instead of raising the
breakpoint exception. The v2 personality's Cv32e40pIrq::check()
performs the actual entry at the next dispatch boundary, so the ebreak
never retires and mcause/mepc stay untouched, matching the hardware,
which redirects straight to the debug-ROM entry.

The v1 flag (CONFIG_GVSOC_ISS_CV32E40P) stays off in v2 builds: its
branch calls dbgunit.set_halt_mode, which does not exist in the v2
personality.
A store into an already-decoded page (self-modifying code executed
without fence.i) left the stale decoded instructions in place: cores
that fetch straight from memory, like CV32E40P, see the new code on
the next fetch, so the model must follow. The LSU store path now
queues a deferred front-end flush when the target hits a decoded page
(InsnCache::covers); the flush itself runs at the next dispatch
boundary (icache_flush), because flushing inline would free the page
holding the store instruction itself.

The check runs after address translation (insn-cache pages are keyed
on physical addresses - the virtual address would be the wrong key on
an MMU-enabled core), the second page lookup only fires when the
access actually crosses a page boundary (one hash per store on the
hot path) and size==0 cannot underflow. The latent SC/AMO gap is
documented at the hook: no iss_v2 core wires LsuV2::atomic today.

Seen on cv32e40p debug_hwloop_test: the debugger overwrites an ebreak
inside a hardware-loop body and jumps back into it.
The dispatch loop ran hwloop.check() unconditionally after insn_exec,
so a trapping loop-end instruction (e.g. an ebreak sitting at lpend)
still decremented the loop counter. On the RTL the exception kills
the instruction before the loop update - lpcount must not move.
Both dispatch paths now skip the check when the handler raised.
Address the core enters on an exception taken while in debug mode
(and, on CV32E40P, on mret in debug mode). Defaults to debug_handler
when not given, so existing platforms are unchanged.
The dispatch called check() only when skip_irq_check was clear, so a
personality could never evaluate synchronous debug conditions (execute-
address triggers, mcontrol timing=before) on a suppressed boundary - an
external lockstep controller holding the line high had to peek at the
model's trigger CSRs to open the gate itself, a layering violation. The
call is now unconditional and each check() implementation consumes the
flag at its head; semantics are unchanged for every existing caller
(reset, gdb resume, external stepping).
Rounding mode 4 (round to nearest, ties to max magnitude) used to abort
the simulation because fenv has no such mode. flexfloat_sanitize already
resolves rounding in software from the round and sticky bits, so RMM
only needs a flag: with FE_TONEAREST set and flexfloat_rmm active, a
tie rounds away from zero instead of to even. The flag is scoped to the
per-instruction set/restore pair in setFFRoundingMode.

Overflow now honours the direction of the rounding mode as IEEE 754
requires: toward-zero (always), downward (positive results) and upward
(negative results) clamp to the largest finite value instead of
returning infinity.
The RMM path of the flexfloat float-to-int conversions negated the input to
round on its magnitude, then dropped the sign:

- lib_flexfloat_cvt_wu_ff_round returned |x| for a negative x instead of 0.
  Unsigned conversion of a negative value saturates to 0; the negation hid
  the sign from double_to_uint, which already saturates correctly, so the
  magnitude leaked through (0xffffffff / |x| instead of 0).
- lib_flexfloat_cvt_w_ff_round saturated a negative overflow to INT32_MIN+1
  (0x80000001) instead of INT32_MIN (0x80000000): |INT32_MIN| = 2^31 is not
  representable as a positive int32, so the magnitude saturated to INT32_MAX
  and negating it was off by one.

Fix: do not hide the sign from double_to_uint/double_to_int, which saturate
with the correct signed/unsigned bounds; restore the saved rounding mode
(was passing new_round instead of old, unlike the unsigned sibling).

quick_val: data_fwd, mlt_cyc (F) and test, sanity (Zfinx) flip FAIL->PASS;
76 PASS / 10 FAIL (was 72 / 14), no regressions.
Four fixes in the shared FP backend (every core using flexfloat via
isa_lib is affected; all are spec-driven and format-generic):

- ff_fma/ff_fnma: round-to-odd binary64 intermediate (truncate toward
  zero, force the mantissa LSB on inexact). An odd intermediate keeps
  faithful round/sticky bits for any narrower destination, removing the
  fused-op double rounding structurally. Exact zeros are recomputed in
  the instruction rounding mode (on the negated fusion in ff_fnma) so
  the -0 sign under RDN survives; INVALID is re-raised, INEXACT is
  re-derived downstream.
- flexfloat_sanitize: the one-ulp rounding increment is applied after an
  integer truncation of the discarded bits, so the grid-step addition is
  exact. The former a->value += ulp rounded in the backend format and
  could overshoot the target grid by one ulp when the discarded bits
  extended to the bottom of the backend mantissa.
- underflow now follows IEEE 754 tininess after rounding with unbounded
  exponent range (matching FPnew's uf_after_round formula, including the
  subnormal-to-smallest-normal carry-out escape), gated on inexact.
- sticky bit: a shift beyond the backend width means every significant
  bit of a nonzero value lies below the sticky window; return 1 instead
  of 0 (fixes lost NX/UF and directed rounding to the smallest
  subnormal for deeply tiny results, e.g. min_normal/2 squared in RUP).

Validated bit- and flag-exact against the CV32E40P FPnew implementation
(F and Zfinx configurations) on directed boundary vectors.

The tininess detection point (the one genuinely implementation-defined
choice IEEE 754 leaves open) is opt-in: the after-rounding-unbounded
escape probe (FPnew parity) is gated by FLEXFLOAT_TININESS_AFTER_ROUNDING
(enabled by cv32e40p_v2.py); the default keeps flexfloat's historical
before-rounding detection for the other cores. The unconditional-UF fix
(underflow = tiny AND inexact) applies to everyone: exact subnormals
must not raise UF.
Five exception-flag defects in the shared FP backend, all spec-driven and
format-generic:

- flexfloat_sanitize: a backend value that is already infinite no longer
  raises OF|NX. IEEE 754 7.4 defines overflow as the result rounded with an
  unbounded exponent range exceeding the destination range; an infinity
  propagated from an operand is exact, and a division by zero raises its own
  flag. A genuine destination overflow leaves the backend finite and is
  still handled by the (exp >= inf_exp) branch.
- lib_flexfloat_min: raise NV on a signaling NaN input. IEEE 754-2019
  minimumNumber/maximumNumber, which is the semantics RISC-V gives
  fmin/fmax, signals invalid on sNaN; the fmax twin already did this.
- ff_fma/ff_fnma: inf*0 raises NV even when the addend is a quiet NaN.
  IEEE 754 7.2 leaves that sub-case implementation-defined and the host FMA
  propagates the NaN silently, but RISC-V mandates the flag (unpriv F).
- double_to_{int,uint,long,ulong}: NX is raised only on a valid conversion.
  An invalid float-to-integer, NaN or out of range, raises NV alone; the old
  test fired on NaN as well, since NaN != NaN holds.
- lib_flexfloat_cvt_ff_{w,wu,l,lu}_round: collect NX on an inexact
  integer-to-float. flexfloat_sanitize already raised FE_INEXACT in the host
  fenv, it was simply never read back into fflags.

The integer-to-float change also restores inexact reporting on the vector
vfcvt/vfncvt lanes, which go through the same conversion helpers.

Checked against riscv-arch-test 3.9.1 with the Sail C emulator 0.13.1 as
reference model through RISCOF: the F suite goes from 313/342 to 342/342 and
the whole run from 402/434 to 431/434. The three residual failures are the
mtvec reset MODE difference in the reference configuration (cebreak, ebreak,
ecall) - unrelated to floating point and out of scope here.

flexfloat and isa_lib are shared with the other cores built on this backend
(ara, cva6, snitch, spatz), so the flag behaviour changes for all of them.
Anything downstream whose golden output was calibrated on the previous flags
may move; the new behaviour is the one IEEE 754 and the RISC-V F extension
prescribe.
…lags

Three residual flag defects in the shared flexfloat backend, follow-up to
the fflags conformance pass:

- lib_flexfloat_cvt_w_ff_round, RMM: the +0.5 nudge fed the nudged value
  to the generic converter, so the flags were computed against the WRONG
  operand - a spurious NX on every exact-integer input (x+0.5 truncates)
  and a lost NX on every half-tie (x+0.5 lands on an integer). RMM is now
  trunc(|x|+0.5) with the sign re-applied, NX decided against the original
  operand, NV alone on saturation (IEEE 754 7.2, RISC-V F 11.7), and a NaN
  input keeps the generic path.
- lib_flexfloat_cvt_wu_ff_round, RMM: same flag discipline; in addition a
  negative operand that rounds half-away below zero is out of range for
  the unsigned destination and must raise NV alone with result 0 - the old
  path handed it unrounded to the RTZ converter, so RMM(-0.5) came back 0
  with a mere NX.
- lib_flexfloat_cvt_ff_ff_round: a narrowing format conversion rounds and
  can overflow/underflow; flexfloat_sanitize raises the host fenv flags
  inside ff_cast but they were never collected here, unlike every sibling
  conversion in this file. Clear + collect around the cast.

Validated with riscv-arch-test 3.9.1 via RISCOF against Sail: 431/434
overall, F suite 342/342.
…inity

In flexfloat_sanitize, the rounding step for a round-away decision at the
top of the largest binade builds its increment with flexfloat_pack at
exponent inf_exp, which packs the increment itself as infinity: adding it
turns the value into Inf during rounding, and the overflow tail then reads
the exponent as an operand-propagated infinity and raises no flags. Guard
the head of the rounding path so a finite value that crosses into the
infinite exponent through rounding still goes through the overflow tail:
the result keeps the directed clamp and now raises OF|NX as required by
IEEE 754. The window is a single exponent value per format, which is why
targeted conformance suites miss it.
@marpac3
marpac3 force-pushed the mpaci/cv32e40p-clean branch from 2221697 to 056b4bd Compare August 21, 2026 10:34
@marpac3

marpac3 commented Aug 21, 2026

Copy link
Copy Markdown
Author

Force-pushed: the branch is reworked and extended since the last push. The
history is now grouped by theme with self-contained commit messages, and it
carries the latest conformance and co-simulation fixes on top of the
previously pushed work. The flexfloat/RVC fixes are also up as standalone
PRs (#160, #161, #162, #163) and sit at the base of this branch; this PR
shrinks accordingly once they merge. Validation on this tree: lockstep RVVI
co-sim against the CV32E40P RTL (4 configurations), RISCOF 431/434
(F 342/342).

@haugoug

haugoug commented Sep 18, 2026

Copy link
Copy Markdown

I don't know how to review this huge PR to be honest. When so many files need to be modified, maybe it is time to create a new component. Could you just create a CV32 component in gvsoc_pulp repo and put all the code here ? It seems you used AI for this PR, it's something AI can do very easily.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants