RISC-V reserves thirty-two unprivileged, read-only, 64-bit counters at
CSR addresses 0xC00–0xC1F — on RV32, their upper halves appear as
separate CSRs at 0xC80–0xC9F. The first three (Zicntr) have fixed
meanings; the other twenty-nine (Zihpm) are platform-defined performance
counters. Both extensions build on Zicsr, and the “RD…” mnemonics are just
pseudoinstructions for csrrs rd, counter, x0.
| CSR (RV32 high half) | Counts | Sharp edges | |
|---|---|---|---|
| cycle | 0xC00 (0xC80) | Core clock cycles since an arbitrary start | Per-CORE, not per-hart |
| time | 0xC01 (0xC81) | Wall-clock ticks since an arbitrary start | Cross-hart sync within 1 tick |
| instret | 0xC02 (0xC82) | Instructions retired by this hart | Faulting instructions don’t count |
| hpmcounter3–31 | 0xC03–0xC1F (+0xC83–) | Platform-defined events (Zihpm) | Unimplemented → trap or constant |
Why 64-bit even on RV32 — and how to read one safely
Anything narrower makes overflow undetectable. The cost is that an RV32 hart reads a 64-bit counter in two halves, racing the counter itself. The canonical fix (Listing 1):
again:
rdcycleh x3 # high
rdcycle x2 # low
rdcycleh x4 # high, again
bne x3, x4, again
If the two high-half reads match, the low half cannot have wrapped between
them — the pair x3:x2 is a coherent 64-bit sample. No hardware snapshot
latches required; the spec considered and rejected them (they’d bloat the
user-visible context on richly countered machines).
Zihpm: the programmable 29
hpmcounter3–hpmcounter31 exist for microarchitectural event counting —
cache misses, branch mispredicts, whatever the platform defines. The
unprivileged spec deliberately says almost nothing: implemented count,
widths, and events are platform-specific; reading an unimplemented one
may raise illegal-instruction or return a constant; a misconfigured
event may read constant. The machinery that picks events and grants user
access lives in the privileged architecture (Vol II ch. 3: mhpmevent*,
mcounteren).
Hardware Designer Notes
For a Linux-bootable v1: three real counters, and time almost certainly
sourced from the memory-mapped mtime of your platform timer (Vol II
ch. 3.2) — a synchronized, single source of truth trivially satisfies the
one-tick rule. The instret gate belongs in your commit stage next to the
exception logic; getting ECALL counted is a classic off-by-one against
reference models. hpmcounters can wait: expose none, let mcounteren hide
them, and Linux’s perf falls back gracefully.
Minimal Linux-boot hart MUST
- Make cycle/time/instret 64-bit counters; on RV32 expose high halves at 0xC80+ as separate CSRs
- Gate the instret increment on retirement — trap-raising instructions must not count
- Freeze cycle when the core is fully clock-gated/powered down in deep sleep
- Keep RDTIME cross-hart skew unobservable beyond one tick and strictly monotonic
MAY simplify / trap-and-emulate
- Implement time by distributing the platform mtime value (the usual design) rather than a per-core clock
- Return the cycle count for RDTIME on a simple single-clock platform
- Trap OR return a constant for unimplemented hpmcounters — document which
- Implement zero hpm events at first: Linux boots fine with Zicntr alone
Check yourself — counters & timers
1.An ECALL executes. Does instret increment?
2.Why does the RV32 64-bit counter read loop (rdcycleh; rdcycle; rdcycleh; bne → retry) work?
3.Your platform updates the time CSR value only at 100 MHz, but the tick period is defined as 1 ns (1 GHz). Compliant?
4.User code reads hpmcounter7 on your core, which implements only hpmcounter3-6. What may happen?