P39 adds the RISC-V Zicntr extension on top of the P38 platform: read-
only cycle, time, and instret counters at CSR addresses
0xC00/0xC01/0xC02 plus their h halves at 0xC80/0xC81/0xC82.
Status: RTL pass. Directed test passes (
4002clocks, all Zicntr checks green). Arch-test sweep aggregatesPASS=56 FAIL=0 NOT RUN=0acrossrv32i/I+M+Zicsr+Zifencei+ the newZicntrbatch (2 tests at upstream reva7c9930). No P39 harden run recorded yet.
What changed in the RTL
P39’s src/top.sv adds two 64-bit counters:
| counter | increments when | exposed at |
|---|---|---|
csr_cycle_counter | every clock | cycle (0xC00), cycleh (0xC80), time (0xC01), timeh (0xC81) |
csr_instret_counter | FSM enters S_WB | instret (0xC02), instreth (0xC82) |
time aliases cycle. The architecture allows that as long as
time is monotonically non-decreasing; a separate 64-bit mtime is
a future rung. The aliasing is documented, not hidden.
instret increments only on the S_WB cycle. In our multi-cycle
FSM:
- A normal instruction reaches
S_WBexactly once. Counted correctly. - A trapping instruction (misalign, access fault, illegal) leaves
S_FETCHstraight into the trap handler. Not counted - which is what the spec wants. - The halt loop spins in
S_HALT, never reachingS_WB. Not counted. - A multiply or divide spends extra cycles in
S_MUL/S_DIV/S_DIV_SIGN, then returns toS_WBexactly once. Counted once.
What changed in the directed test
runtime/main.c adds zicntr_probe() between the existing
rv32m_probe() and the first UART putc. It reads cycle/time/
instret, runs a 50-iteration mix() loop, reads them again, and
checks:
cycle advanced
time advanced
instret advanced
(cycle - cycle0) > (instret - instret0) # CPI > 1 in our FSM
cycleh / instreth read without trapping
If any of those fail the runtime returns a stable per-check error
code (40-43) and the testbench reports FAIL with that value in x5.
Arch-test sweep
P39 adds a fifth scoped batch (rv32i/Zicntr) to the unified
make arch_test target. Recorded aggregate:
rv32i/I PASS=39 FAIL=0 NOT RUN=0
rv32i/M PASS=8 FAIL=0 NOT RUN=0
rv32i/Zicsr PASS=6 FAIL=0 NOT RUN=0
rv32i/Zifencei PASS=1 FAIL=0 NOT RUN=0
rv32i/Zicntr PASS=2 FAIL=0 NOT RUN=0
total PASS=56 FAIL=0 NOT RUN=0
The Zicntr batch is small at upstream rev a7c9930 - only two tests
(Zicntr-csrrc-00 and Zicntr-csrrs-00) which exercise CSRRC and
CSRRS on the counter alias addresses. The framework does not include
a CSRRW Zicntr test because writes to the user-mode counter aliases
are illegal in M-mode anyway.
The sweep aggregator was extended to find the new
compliance/results_zicntr/ directory and report it. SWEEP.md is
written by scripts/p38_arch_test_sweep.py after all five batches
complete.
The DUT plugin is unchanged from P38 - copied verbatim into
projects/39_zicntr_counters/arch_test/. The plugin is RTL-agnostic;
each rung that wants its own results directory copies the four files
and points the runner at them.
Harden result
NOT RUN. The librelane config is a copy of P38’s, same SDC, same
fanout target. The expected delta in stdcell area is small - two
64-bit counter registers plus their increment paths is roughly 1k
extra cells.
What just happened?
P39 is the first rung past the P37/P38 plateau that adds real ISA
behaviour. It is small but it is the right next step: FreeRTOS uses
cycle and instret for run-time stats, and the eventual port will
want them as real CSRs rather than zero-stubs. P40 (the M-mode
runtime API audit per the roadmap) is the next rung;
together with P39 it sets up the FreeRTOS port at P41.