journal 2026-05-03

P39 lands Zicntr counters

p39riscvzicntrcounters

P38 cleaned up the arch-test plumbing without touching the ISA. P39 is the first rung past that plateau where the chip actually grows: it adds read-only cycle, time, and instret counters at the Zicntr CSR addresses (0xC00/0xC01/0xC02 and their h halves).

The RTL diff is tiny:

Two new things confirmed by running it:

  1. The directed test (zicntr_probe()) reads cycles/instret before and after a 50-iteration loop and checks (cycles_advanced) > (instret_advanced). PASS - the first concrete CPI > 1 assertion on the chip. Runtime now halts at 4002 clocks (P37 was 1620); the extra is exactly the new probe’s busy loop.
  2. The arch-test sweep (make arch_test) now has a fifth batch (rv32i/Zicntr) with 2 tests at upstream rev a7c9930: Zicntr-csrrc-00 and Zicntr-csrrs-00. Both pass in 1120 clocks. The framework doesn’t include a CSRRW Zicntr test because writes to the user-mode counter aliases are illegal in M-mode anyway.

Aggregate sweep result: PASS=56 FAIL=0 NOT RUN=0 across all five batches. P38’s PASS=54 plus P39’s two new Zicntr tests, with no regression on the inherited 54.

The plumbing diff is also small - the DUT plugin is verbatim P38 content, just copied. That is exactly the inheritance pattern P38 set up: each rung copies the plugin into its own directory rather than sharing one across projects, so individual rungs can deviate later without disturbing each other.

Honest framing: P39 is small, but it is the right next step toward FreeRTOS. The port will want cycle and instret as real CSRs when it eventually computes idle-task stats. Stubbing them as zero would make the FreeRTOS port misbehave silently rather than fail loudly, and that is the worst kind of bug.