A production-ready RVA23U64 RISC-V SoC. Every mechanism is real, verified hardware: it boots an unmodified Linux kernel out of simulation, and its cryptographic implementations pass NIST ACVP validation.
Full RVA23U64 profile. RV64IMAFDCV with hypervisor (H), vector crypto (Zvk*), AIA interrupts, and IOMMU – every mechanism is real hardware verified end to end, not a teaching core.
Configurable at synthesis time. Every optional mechanism – the four Zvk* crypto units, the entropy source, the branch predictor, PMP, IOPMP, the FUSE controller, and the UART – is gated by its own build-time *_ENABLE parameter: set it to 0 and the hardware is removed from the design entirely, not merely tied off. Single-core or multi-core (NUM_HARTS) is likewise a synthesis-time choice.
Boots real Linux. Unmodified upstream OpenSBI plus stock, unmodified Linux v6.6 and v7.2: full extension detection, Sstc timer from S-mode, real UART console, KVM initialization.
Fully tested, fully documented. Unit tests, directed assembly tests through the real pipeline, a build-permutation matrix, RISCOF conformance, differential fuzzing harness, NIST ACVP crypto validation – with complete design documentation for every mechanism.
The following RISC-V ISA extensions are supported. The *_ENABLE column names the synthesis-time parameter that removes a mechanism from the design entirely (both simulation and real synthesis) where one exists – every such combination is verified end-to-end by verif/enable_matrix.
Extension
Description
*_ENABLE
I
Base RV64I integer ISA (all 64-bit ops incl. *W 32-bit forms)
A/D bits set by software: with ADUE=0, a leaf PTE that would otherwise succeed but has A=0 (or D=0 on write/AMO) raises a genuine page fault for the OS to fix up
A/D bits set by hardware: with ADUE=1 (the reset default), the walker atomically updates a leaf PTE’s A/D bits in place – both extensions coexist, selected at runtime per translation stage
Forward-edge CFI: LPAD landing pads; a non-exempt JALR arms an expected-landing-pad state enforced on the next fetch (software-check exception, mcause=18)
Double-trap detection: per-privilege sticky trap-in-flight bits redirect an unexpected trap one level up (mcause=16, suppressed cause in mtval2); M-level halts the hart in a permanent critical-error state
The algorithm implementations are standalone, reusable RTL modules which are
connected to the respective RISC-V ISA extensions. Thus apart from the RISC-V
ISA, the algorithms can be instantiated in separate use cases or even
extracted from the RTL code to a separate project; each core also ships with
a documented C API for driving it from Verilator or an external project.
The overarching goal with the hardware crypographic implementations are:
256 bits of security in general (ignoring the collision security strength of message digests)
availability of hardware-stored keys usable by software where software yet cannot access the key
FUSE Keys
The RTL provides a one-time-programmable (OTP) e-fuse controller holding a
synthesis-time defined number of banks – three by default – each storing
two 256 bit keys. Every key carries one additional guard/“blown” fuse bit:
a blown key is permanently unreadable, and an atomic zeroize operation blows
the selected key (or all keys) in a single step.
The fuses are structurally one-way – every write path can only ever set
bits, never clear them – and the fuse storage is the one module in this
design that does not reset on rst_n, so provisioned keys survive power
cycles and resets exactly like real e-fuse silicon. In simulation, the
array’s power-on content is preloaded from a hex file (INIT_FILE parameter
or +FUSEHEX= plusarg) instead of a programming pulse.
The keys are selectable by the caller including software for the cryptographic
operations as indicated below. In this case, the caller does not have access
to the key data itself, but can only instruct the cryptographic operation to
use the key. Inside the hardware, the cryptographic operation fetches the
key prior to performing the requested operation.
The core concept is that the caller is not able to access the key.
Creation of FUSE Keys
Two independent paths provision key material:
Manufacturing-time provisioning. A dedicated, prov_enable-gated
out-of-band port writes a key’s value exactly once after manufacturing and
before deployment; in real silicon prov_enable is tied to a physically
restricted signal (a factory-test strap or a manufacturing-only debug
credential) that is permanently de-asserted afterwards, so no code path can
ever provision new key material in the field.
Hardware-generated keys. A trigger in M-mode instructs the hardware
Random Number Generator to generate a bank of
256 bit keys and write them directly into the FUSE controller – targeting
bank 1, which is this mechanism’s exclusive consumer. The generated bits
never pass through any register or instruction result software could read;
neither software / firmware, nor other parts of the hardware can interfere
with this operation, nor can they access the generated bits in any form.
This link is one-shot: a non-volatile “provisioned” status bit latches
permanently after the single run (even across a full chip reset), so
generation can never be re-triggered and OR fresh material into an
already-programmed key.
For each generated key, the RNG performs the following steps individually:
Generate a 256 bit random bit string (each key is its own independent
generate call, not two halves of one longer output)
Write the 256 bit random bit string directly into the respective FUSE key
location
SHA-2
Compliant to FIPS 180-4, the following algorithms are available:
SHA2-256
SHA2-384
SHA2-512
Exposed to software through the Zvknhb vector
instructions (vsha2ms.vv/vsha2ch.vv/vsha2cl.vv; SEW=32 selects
SHA-256, SEW=64 selects SHA-384/512).
SHA-3 Family
Compliant to FIPS 202, the following algorithms are available:
SHA3-256
SHA3-384
SHA3-512
SHAKE128
SHAKE256
The implementation is particularly small as a central Keccak mechanism
is implemented where the SHA-3 family members only define the rate/capacity
and the padding bit. The leancrypto’s SHA-3 C implementation is conceptual identical to the RTL
implementation.
KMAC
Following SP800-185, KMAC256 and KMAC256XOF are implemented on a real,
separate Keccak core instance (cSHAKE256 as the sixth algorithm of that
shared core). The caller is allowed to either provide a key or to point to
one of the FUSE keys; an explicit key must be 128-256 bits,
enforcing SP 800-185’s own minimum-security-strength floor.
This module is not instantiated anywhere in the SoC – it exists for unit
testing and external use through its documented C API; a self-test status
bit is reserved for it on the self-test interface.
AES
The RTL implements the raw AES block operation for AES-256. It is equally
connected to the FUSE keys: bank 0 of the FUSE controller is
the AES hardware’s exclusive consumer, selected through a machine-mode CSR
that names one of the two bank-0 keys and declares whether the upcoming
instruction sequence encrypts or decrypts. While a FUSE key is active, every
round instruction’s software-supplied round key is transparently replaced by
the hardware-expanded schedule – the round keys themselves are never exposed
on any port, register, or CSR.
In addition, GCM support is provided along with GMAC: one 128-bit data block
(plus one optional AAD block) per call under a fixed 96-bit IV, built on a
constant-time GF(2^128) GHASH multiply shared with the Zvkg extension.
Random Number Generator
The random number generator is an XDRBG-256
which uses SHAKE-256 as a core. It instantiates the Keccak Core.
The reason for choosing the XDRBG-256 is:
availability of Keccak
very lean
about to be standardized in NIST SP800-90A
The XDRBG-256 is seeded by the hardware entropy sources;
in this design, its only instantiations are the entropy source’s own private
DRBG pipelines.
The XDRBG-256 implements the following aspects:
A multi-staged seeding process is supported where a caller can insert multiple
individual seed blocks before the XDRBG re-generates its own internal state.
This support is mathematically equivalent to inserting all seed blocks
concatenated as one seed block. Yet, one seed block is allowed to be at most
512 bits.
All call sizes are hardware-enforced: a finalizing instantiate/reseed accepts
at most 2176 bits of combined seed + alpha + control byte (alpha itself at
most 84 bytes), and the generate function will at most return 512 bits.
Vector Crypto Extensions
The algorithm cores above are exposed to software through the RISC-V
vector-crypto instruction set, all decoded under a dedicated vector-crypto
major opcode:
dedicated unit; strict superset of Zvknha, so no separate Zvknha exists
Zvknhk
vkeccak.vi – in-place Keccak-p[1600] permutation across four consecutive vector registers
draft proposal, not a ratified extension – deliberately not advertised in the device tree
In addition, Zvkt is a timing guarantee rather than an instruction set:
the vector engine’s add/subtract, logical, shift, compare, copy, extend,
merge, multiply/multiply-add, permute, and slide instructions (plus all of
Zvkb) execute with data-independent latency – satisfied by construction on
this core’s serial one-element-per-cycle engine and proved by a
cycle-count-equality unit test.
Self-Test Infrastructure
Every cryptographic execution unit carries a built-in known-answer self-test,
delegated to its own standalone algorithm core rather than re-implemented:
Auto-triggered on first use. The very first instruction a unit ever
executes after reset transparently runs the self-test first; software only
sees that one instruction take longer.
Software-re-runnable. Writing the mechanism’s bit of
IRISCV_CRYPTO_SELFTEST_NOT_EXECUTED (0x7C0) triggers another run –
the write is a trigger line, never a store: it changes nothing the CSR
reads back.
Sticky lock with recoverable failure. Once passed, the self-test never
re-runs (a later trigger write is a permanent no-op). While failed, the
block stays disabled for ordinary requests – every subsequent instruction
of that unit traps as an illegal instruction, a hardware-enforced error
return to software, not merely a status flag – until a software re-trigger
through NOT_EXECUTED runs it again and passes, which clears FAILED and
sets PASSED on the same cycle.
Results are reported through four custom CSRs, all readable from every
privilege mode: IRISCV_CRYPTO_SELFTEST_NOT_EXECUTED (0x7C0, resets
all-1s), IRISCV_CRYPTO_SELFTEST_PASSED (0xFC0),
IRISCV_CRYPTO_SELFTEST_FAILED (0xFC1), and
IRISCV_CRYPTO_FIPS_APPROVED (0xFC2). Only NOT_EXECUTED accepts a
software write at all – and only from M or HS mode; writing any of the other
three traps as an illegal instruction even in M-mode. Eight real mechanisms
are wired to this interface – AES/Zvkned, AES-GCM/Zvkg, SHA-256/SHA-512
(Zvknhb’s two independent per-width tests), Keccak/Zvknhk, and the entropy
source’s own DRBG/Keccak self-test bits (Entropy Source)
– each owning its own bit position; one further bit is reserved for KMAC256.
i-RISCV Hardware Entropy Sources
The i-RISCV provides a complete entropy source along with all tools necessary
to analyze it to verify the compliance with the stochastic model.
Configuration FUSES
The RTL implements a FUSE controller which, however is not backed by
OTP-FUSES, but with memory that is can be written to by software in RISC-V M
privilege mode where the software can set a lock which makes the configuration
values read-only until next reset.
The SBI code contains placeholders offering example code showing how set the
FUSES and the final lock.
The FUSE allows the following individual setting for each noise source:
health test cutoff values individually for each health test
number of bits to be collected from the noise source for one complete seeding
operation
Therefore, each noise source can be configured individually and independent
of all other noise sources.
Noise Sources
The i-RISCV implements two different types of noise sources:
4 ring oscillators based on the Elementary RO TRNG (ERO) with the Baudet et al. phase-noise model, Fischer-Lubicz embedded jitter monitor, and OpenTRNG reference RTL
2 meta-stable flip-flops
Health Tests
Each instance of the noise source, i.e. each individual ring oscillator and
meta-stable flip-flop, has its independent set of health tests which
independently monitor the quality of the data.
If any health test flags an alarm, the noise source is deactivated
independently of the others. The health status register allows software to
reset the noise source to become active again. The noise source will be
automatically reactivated upon reset.
Stochastic Model
A separate stochastic model for the ring oscillators and the meta-stable
flip-flops is used as a basis to determine the entropy rate.
Data Size
Each noise source has its own configured number of bits to be mandatory
collected for one seeding operation.
The goal with this individual configuration is to allow a post-tapeout
characterization of the noise sources along with its entropy rate. Now, the
entropy rate can be set to deliver the amount of entropy used as a baseline
specified in the seeding strategy.
Entropy Source
The integration of noise sources is intended to be compliant with SP800-90C
as well as AIS 20/31 v3.0. This section explains the integration of the noise
sources to form the entropy source used to seed the XDRBG-256.
General Seeding Strategy
Before discussing the Synchronous or asynchronous seeding strategy, the general
approach to seeding must be discussed as it applies in general. The XDRBG-256
is seeded with the following general considerations which apply always:
The XDRBG-256 after reset receives a trigger to automatically perform a
seeding operation. That seeding operation must be completed before the
XDRBG generates any data. This means that this seeding operation is
synchronous in nature.
The XDRBG has two trigger points whose thresholds can be configured with
the configuration FUSES.
Reseed trigger: When a threshold of generated bits is reached
without reseeding (default is 2^16 bits), the XDRBG triggers the reseed
with one of the possible seeding strategy. Yet, the XDRBG is allowed
to continue to generate random bits.
Drop dead trigger: When a threshold of generated bits is reached
without reseeding (default is 2^17), the XDRBG refuses to generate
any more random bits. It synchronously waits for the seeding operation
that is triggered by the reseed trigger to complete. If that
seeding operation fails (e.g. a noise source health error)
It is permissible that both trigger points are set to the same value which
implies that any reseed trigger is always synchronous and required to
be successful (i.e. at least one noise source must deliver data without
a health alarm pending).
RBG2(P) And DRG.4/PTG.2
The XDRBG offers the ES_DRNG_RAND interface which generates data compliant
to the SP800-90C RBG2(P) and AIS 20/31 v3.0 DRG.4 + PTG.2 construction.
The general construction of the seeding process can be configured at synthesis
time and implements one of the following strategies further discussed in
the subsequent sections
Synchronous seeding - albeit inferior with its performance compared to the
asynchronous operation, it only uses a separate 512 bit transfer buffer and
thus has less gate counts compared to the asynchronous approach.
Asynchronous seeding - this approach is significantly faster as entropy
collection proceeds in parallel with random bit generation by the XDRBG, it
requires a separate SHA3-512 instance that collects the bits from the
noise sources.
flowchart LR
RO["4 ring oscillators (health-tested)"] --> SBUF
FF["2 metastable flip-flops (health-tested)"] --> SBUF
RO --> AC
FF --> AC
subgraph SYN ["Synchronous seeding (caller stalls until seeded)"]
direction LR
SBUF["512-bit seed buffer"] -->|multi-staged insert| XDRBG
end
subgraph ASY ["Asynchronous seeding (parallel to generation)"]
direction LR
AC["SHA3-512 conditioning function"] -->|atomic 512-bit digest insert| XDRBG
end
XDRBG["XDRBG-256 (SHAKE-256 core)"] --> OUT["Random bits, max 512 per request"]
Synchronous Seeding Stratey
For the synchronous seeding strategy, the XDRBG will stall the caller until
the following seeding process has successfully completed.
For the seeding operation, a 512 bit wide seed buffer is used to hold
the data of the following operations.
Startup all noise sources whose health test has not failed
Obtain individual bits from each active noise source and concatenate them
in the buffer. Due to the different numbers of bits generated by the
noise sources, it is possible that at the beginning, output data from all
noise sources are concatenated, later from one less down to obtaining
data from the noise source with the longest data bit generation threshold.
When the 512 bit seed buffer is entirely saturated, insert the buffer into
the XDRBG seeding interface. That seeding interface allows a multi-staged seed
process which implies that, for example, inserting 2 512 bit buffers into
the XDRBG is the identical operation as if the 2 buffers are first
concatenated and then inserted into the XDRBG. With this consideration,
irrespective of how many instances of the seed buffer, the amount of
data is mathematically treated as one long seed buffer. Once the seed buffer
is inserted go back to step 2 to collect more data.
The steps 2 and 3 are performed until all noise sources delivered their
configured data. At that time the potentially partially filled seed buffer
is inserted again into the XDRBG seed interface, but marking the seeding
operation as complete.
The XDRBG generates the requested number of random bits and returns them
to the caller.
A seeding failure, i.e. when no noise source delivered the required amount of
data returns an error to the caller.
Asynchronous Seeding Strategy
For the asynchronous seeding strategy, the XDRBG process the seeding operation
independently of the XDRBG generate operation. That implies that stall the
caller is provided with random bits from the XDRBG while the collection of
entropy is ongoing.
For the seeding operation, a 512 bit wide seed buffer is used to hold
the data collected by the noise sources.
To ensure that the entire set of data from the noise source is inserted into the
XDRBG in an atomic operation and having a runtime-configurable amount of data
to be collected from the noise source, a SHA3-512 conditioning function is
employed to collect an arbitrary amount of data from the noise sources, compress
them with the message digest and insert the 512 bit message digest into the
XDRBG in an atomic operation. Thus, the following steps are implemented:
The XDRBG receives the (re)seeding trigger, it triggers the following steps
while servicing subsequent caller requests. Starting with the next step,
only the seeding process is outlined that now runs in parallel to the
XDRBG generate operation.
Startup all noise sources whose health test has not failed
Obtain individual bits from each active noise source and concatenate them
in the buffer. Due to the different numbers of bits generated by the
noise sources, it is possible that at the beginning, output data from all
noise sources are concatenated, later from one less down to obtaining
data from the noise source with the longest data bit generation threshold.
When the 512 bit seed buffer is entirely saturated, insert the buffer into
the conditioning function with a conceptual hash update operation. I.e.
all collected seed buffers are mathematically concatenated for generating
the message digest.
The steps 3 and 4 are performed until all noise sources delivered their
configured data. At that time the potentially partially filled seed buffer
is inserted again into the conditioning function followed by generating
the message digest.
The message digest is now inserted into the XDRBG seeding interface in an
atomic operation.
Note, the asynchronous seeding process also ensures that there is hardly any
user-visible contention for the noise sources when the XD
RBG3(RS) And PTG.3
In addition, the XDRBG also offers the ES_DRNG_SEED interface. When requesting
data from that interface, the XDRBG changes its behavior to implement
an SP800-90C RBG3(RS) and AIS 20/31 v3.0 PTG.3 construction.
flowchart LR
RO["4 ring oscillators (health-tested)"] --> BUF
FF["2 metastable flip-flops (health-tested)"] --> BUF
BUF["512-bit seed buffer"] -->|multi-staged insert when full| XDRBG["XDRBG-256 (SHAKE-256 core)"]
XDRBG --> OUT["Random bits, max 256 per request"]
OUT -.->|reseed trigger| BUF
After generating at most 256 bits of random data, a seeding operation is
performed. For the seeding operation, a 512 bit wide seed buffer is used to hold
the data of the following operations.
Startup all noise sources whose health test has not failed
Obtain individual bits from each active noise source and concatenate them
in the buffer. Due to the different numbers of bits generated by the
noise sources, it is possible that at the beginning, output data from all
noise sources are concatenated, later from one less down to obtaining
data from the noise source with the longest data bit generation threshold.
When the 512 bit seed buffer is entirely saturated, insert the buffer into
the XDRBG seeding interface. That seeding interface allows a multi-staged seed
process which implies that, for example, inserting 2 512 bit buffers into
the XDRBG is the identical operation as if the 2 buffers are first
concatenated and then inserted into the XDRBG. With this consideration,
irrespective of how many instances of the seed buffer, the amount of
data is mathematically treated as one long seed buffer. Once the seed buffer
is inserted go back to step 2 to collect more data.
The steps 2 and 3 are performed until all noise sources delivered their
configured data. At that time the potentially partially filled seed buffer
is inserted again into the XDRBG seed interface, but marking the seeding
operation as complete.
The XDRBG generates the requested number of random bits and returns them
to the caller.
Note: Access to the noise sources is serialized such that a complete seeding
operation is completed by a separate seeding operation can take place.
i-RISCV Input/Output Support
The i-RISCV SoC provides the following input/output mechanisms.
IOMMU
The SoC implements the RISC-V IOMMU (v1.0) for DMA-capable devices. Supported mechanisms:
Device Directory Table (DDT) walk in Off, Bare, 1LVL, 2LVL, and 3LVL modes
Second-stage-only DMA address translation (Sv39x4/Sv48x4/Sv57x4) with hardware A/D update on page-table entries
MSI address translation, redirecting a device’s writes to a guest’s virtual IMSIC interrupt file onto the real physical guest file (AIA/IMSIC tie-in)
IOATC: a device-context cache and an I/O TLB, invalidated via IODIR.INVAL_DDT and IOTINVAL.VMA/.GVMA commands
Command queue (IOTINVAL, IOFENCE.C, IODIR) and fault queue for translation errors
WSI (wire-signal) interrupt delivery to the PLIC/APLIC on faults or fence completion
IOPMP enforcement of the final physical address after every successful translation
AIA and IMSIC
The core implements the RISC-V Advanced Interrupt Architecture (AIA, v1.0). Supported mechanisms:
Smaia/Ssaia: M-level and S-level AIA CSRs (miselect/mireg, siselect/sireg, mtopi/stopi, mvien/mvip)
IMSIC (Incoming MSI Controller) with per-hart M-file and S-file, memory-mapped MSI delivery (seteipnum_le), and claim-and-clear via mtopei/stopei
Two VS-level guest interrupt files per hart for the H extension, selected by hstatus.VGEIN, each with its own physical MSI region to support migrating a guest between files
A minimal, MSI-mode-only, machine-level APLIC domain (aplic0, 8 sources, msi-parent = imsic_s) that forwards this platform’s wired sources into the IMSIC S-file as MSIs – under Linux, the IOMMU’s and IOPMP’s interrupts are advertised against it (sources 2/3) instead of through plic0’s own S-mode context, which keeps a position-preserving placeholder entry; the PLIC retains its M-mode context for firmware and bare-metal use
IOPMP
The SoC implements RISC-V I/O Physical Memory Protection (IOPMP), which regulates physical-address accesses issued by non-hart bus requesters (the IOMMU’s translation path). Supported mechanisms:
16 requester IDs, 8 memory domains, and 16 protection entries
TOR/NA4/NAPOT address matching with lowest-index-wins priority and per-entry r/w/x permissions
Memory-domain / source association gating (MDCFG/SRCMD_EN)
Locking of MD associations, MDCFG, and entries (W1SS/incremental-only)
A genuine ns16550a-register-compatible serial console (rtl/uncore/uart.sv), build-time optional (UART_ENABLE; at 0 it is removed from the design entirely – registers read 0, interrupt tied low, TX held idle-high). Supported mechanisms:
Standard byte-addressed 8-register map with DLAB-banked offsets 0/1 (RBR/THR/DLL, IER/DLM, IIR/FCR, LCR, MCR, LSR, MSR, SCR) – the exact layout a stock Linux 8250 driver expects, no reg-shift/reg-io-width overrides
Real baud-rate divisor: one bit period = 16 x DLL/DLM core cycles; reset to 115200 at a 50 MHz clock so bare-metal firmware gets a known rate without programming it; reprogrammed via the standard DLAB sequence
Full 16550A hardware FIFOs: 16-byte TX and RX, enabled by sticky FCR.FE (any toggle clears both), four RX trigger levels (1/4/8/14 bytes) plus a real 4-character-time RX timeout interrupt so low-throughput links never stall below the trigger level; without FCR.FE, one physical shift register feeds single holding/latch registers instead (THR writes while busy are silently ignored, THRE and TEMT collapse to one condition)
Receiver error detection – framing error, BREAK, overrun – with per-byte error tags in the RX FIFO feeding LSR’s errors-in-FIFO bit; RBR reads clear DR/pop the FIFO while LSR reads clear OE/PE/FE/BI (two distinct read-to-clear conditions, matching real 16550 semantics)
Real interrupt priority chain: line status > data available / character timeout > THR-empty (one-shot – fires exactly once per emptying, acknowledged by an IIR read or a new THR write) > modem status, with standard IIR identification codes
MCR.LOOP internal loopback feeding the transmitter back into the receiver while the external TX pin is held idle-high; in loopback mode MSR’s CTS/DSR/RI/DCD mirror MCR’s DTR/RTS/OUT1/OUT2 one-to-one with delta bits latched and cleared on read
Modem status defaults to “always ready” (CTS=DSR=DCD=1, RI=0) since no real modem lines exist
The UART’s irq is wired to both controllers’ source 1 (source 2 = the IOMMU WSI, source 3 = the IOPMP); sources 2 and 3 are advertised in the device tree against aplic0, while the UART itself carries no interrupts property – a single honest compatible = "ns16550a" – so under Linux the stock driver falls back to polling (see Linux).
Disclosed simplifications. Not silent scope cuts:
Fixed 8N1 wire framing – LCR’s word-length/parity/stop-bit fields are stored and read back faithfully but not enforced on the wire
No 16750/16950 extensions (no 64-byte FIFO, no auto-flow-control beyond the plain MCR/MSR pins)
i-RISCV CPU Internals
The i-RISCV core is a single-issue, in-order processor. The following mechanisms are supported.
Pipeline
A six-stage pipeline (IF1/IF2/ID/EX/MEM/WB) with synchronous instruction and data memory access.
flowchart LR
IF1["IF1<br/>PC gen"] --> IF2["IF2<br/>imem data phase"]
IF2 --> ID["ID<br/>decode + reg read"]
ID --> EX["EX<br/>ALU / branch resolve"]
EX --> MEM["MEM<br/>dmem addr phase,<br/>CSR r/w, traps"]
MEM --> WB["WB<br/>dmem data phase,<br/>regfile commit"]
Supported mechanisms:
Compressed (C) handling entirely within fetch: 16-bit RVC expansion to full-width instructions, self-correcting length prediction, and straddling-instruction splicing
Same-cycle result forwarding between stages for dependent instructions
Load-use / CSR-use / AMO-use hazard detection with a single-cycle consumer stall; the FP register file carries its own structurally identical check
Multi-cycle execution units: RV64M divide, RV64F/D divide/square-root, and the serial (one-element-per-cycle) vector engine
Vector load/store with per-element address translation, mid-instruction fault latching with vstart resume, and misaligned access splitting across two bus words
Vector load-use: any instruction in ID is stalled for one cycle while a vector load/store sits in EX, so neither a vector consumer of the load’s result nor a scalar consumer racing the store’s write landing can observe stale data
Branch/jump misprediction resolution in EX with a fixed squash of all younger instructions
Trap/MRET/SRET/FENCE.I/SFENCE.VMA redirects resolved at the commit point (MEM)
Synchronous exceptions take priority over pending interrupts sampled at the commit point
Branch Prediction
A trained fetch-address predictor, build-time optional (BPU_ENABLE):
256-entry pattern history table (PHT) with 2-bit saturating counters for conditional-branch direction
Return address stack (RAS, default 8 entries) giving call-site-specific predictions for shared ret instructions; push/pop only on resolved outcomes, never speculatively
Mispredictions corrected by the ordinary EX-stage redirect path, in both prediction directions
Spectre/Meltdown resistance. This core is not susceptible to Spectre-v1/v2 or Meltdown-class attacks:
Every mis-speculated instruction is squashed before it reaches MEM, the only stage that can touch memory/TLB – so a speculative load can never leave an observable cache/TLB footprint (the precondition both Spectre variants rely on)
All trained predictor state (BTB, PHT, and RAS) is unconditionally erased on every privilege-domain crossing (trap, MRET, SRET, FENCE.I), so no prediction history or call stack can survive across a privilege transition (closes the Spectre-v2 branch-target-injection setup at the source)
Meltdown requires a permission check deferred past speculative use of a faulting load; this core’s MMU translation and PMP permission checks are combinational and gate the real bus access in the same cycle, so no such path exists
MMU with PMP
The MMU implements Sv39/Sv48/Sv57 virtual memory plus H-extension two-stage (VS + G) translation. Supported mechanisms:
Variable-depth page-table walker (3/4/5 levels) for instruction and data accesses, including superpage support
H-extension two-stage translation with separate VS-stage and G-stage walkers/TLBs – see the Tagged TLB section for the shared TLB’s tagging and selective invalidation
Hardware A/D-bit management implementing both Svade and Svadu: a leaf PTE that would otherwise succeed but has A=0, or D=0 on a write/AMO, is either auto-updated in place by the walker (a real atomic read-modify-write under the cross-hart bus grant; a cached TLB entry whose A/D no longer match the current access is evicted and re-walked) when ADUE=1, or reported as a genuine page fault for software fixup when ADUE=0 – menvcfg.ADUE governs S-mode and G-stage translations, henvcfg.ADUE the VS stage (spec-forced to 0 while menvcfg.ADUE=0); both reset to 1
Physical Memory Protection (PMP), build-time optional (PMP_ENABLE; at 0 the pmpcfg/pmpaddr CSRs raise illegal-instruction and every check passes unconditionally):
TOR/NA4/NAPOT address matching with lowest-index-wins priority; addresses WARL-masked to the 54-bit address >> 2 width of this design’s 56-bit physical address space
Per-entry r/w/x permissions; M-mode bypasses unlocked entries but not locked ones
No-match default: M-mode is always granted, S/U-mode denied – so merely having PMP present (all entries still A=OFF) denies every S/U-mode access until firmware configures at least one permissive entry before entering S-mode
Entry locking (config and address read-only until reset), including the locked-TOR lower-bound rule
Enforcement on every physical bus access: instruction fetch, load, store, and page-table-walker PTE reads; a denial reports the ordinary access-fault cause of the original access
Smepmp (Enhanced PMP) is layered directly on top via three mseccfg bits:
MML (Machine Mode Lockdown): replaces the per-entry permission rule with a fixed 16-row table over {L,R,W,X} – M-mode-only, S/U-mode-only (which removes the base rule’s own M-mode bypass), and shared-region patterns; sticky-set until reset
MMWP (Machine-mode Whitelist Policy): flips the M-mode no-match default from granted to denied; sticky-set until reset
RLB (Rule Locking Bypass): while set, locked entries’ config/address become writable again; cannot be set once any entry is already locked (stuck at 0 until reset)
Tagged TLB
The MMU’s translation lookaside buffer is tagged so that cached translations remain valid across address-space switches and can be invalidated selectively instead of wholesale. Supported mechanisms:
16-entry TLB whose entries are tagged with the active ASID (full 16-bit satp.ASID width) and the PTE’s global bit
Global mappings hit under every address space; non-global entries only match their own ASID, so a satp write needs no flush – a new address space can never match stale entries installed under another ASID
Selective invalidation via the 4-case SFENCE.VMA rs1, rs2 predicate: whole-TLB, per-ASID, per-address (a superpage entry matches any address within its range), or per-address-and-ASID; global mappings are exempt only when an ASID is named
Svinval: SINVAL.VMA provides identical selectivity to SFENCE.VMA; the SFENCE.W.INVAL/SFENCE.INVAL.IR ordering fences are architecturally no-ops on this in-order core
H extension: separate VS-stage and G-stage TLBs; HFENCE.VVMA invalidates the ASID-tagged VS-stage entries with the same 4-case predicate, HFENCE.GVMA the G-stage entries – which are tagged by hgatp.VMID (14 bits) instead of an ASID, so a VMID-scoped flush touches only that guest’s cached translations and two guests under different VMIDs can switch without any intervening flush
Privilege Levels and Traps
M/S/U privilege levels with reset into M-mode; MRET restores the level from mstatus.MPP (resetting it to U), SRET from sstatus.SPP. The H extension layers an orthogonal virtualization bit on top of these – see the Hypervisor page.
Supported mechanisms:
Full machine CSR set (read-only identification CSRs mvendorid/marchid/mimpid/mhartid/mconfigptr, plus mstatus, self-describing misa, medeleg/mideleg, mie/mip, mtvec Direct + Vectored, mcounteren, mscratch, mepc/mcause/mtval, mcycle/minstret) and supervisor CSR set (sstatus as a masked view of mstatus, sie/sip, stvec, scounteren, sscratch, sepc/scause/stval, satp WARL-limited to Bare/Sv39/Sv48/Sv57)
CSRRS/CSRRC implement the spec’s own strict rs1==x0 no-write rule – a real (non-x0) register holding 0 is still a genuine write attempt
mstatus.MXR/SUM are real bits consumed by the MMU’s permission check (SUM never applies to instruction fetch); FS and VS are likewise real, reset to Off (any F/D or vector instruction traps as illegal-instruction until software enables them), and auto-transition to Dirty in hardware on the first state-modifying commit – the vector floating-point forms specifically are gated by both FS and VS at once
mstatus.MPRV/MPP/MPV are real and consulted by the data-side translation (the full six-row truth table), never affecting instruction fetch or HLV/HSV
Zicfilp forward-edge CFI: a non-exempt JALR arms an expected-landing-pad state; the next fetched instruction must be a label-matching LPAD or a software-check exception (mcause=18, mtval=2) fires – enabled per privilege via menvcfg.LPE/henvcfg.LPE/senvcfg.LPE/mseccfg.MLPE, with the state trap-preserved in xPELP
Zicfiss backward-edge CFI: a dedicated shadow stack (ssp CSR) with SSPUSH/SSPOPCHK; a mismatched pop raises the same software-check exception (mtval=3), and the shadow-stack’s own pages use a new PTE encoding that only the SS instructions may write – any other write is a fatal access fault
Ssdbltrp/Smdbltrp double-trap detection: per-level sticky trap-in-flight bits (mstatus.MDT, sstatus.SDT, vsstatus.SDT); an unexpected trap into a level already in flight redirects one level up as mcause=16 with the suppressed cause in mtval2, and an unexpected trap targeting M-mode halts the hart in a permanent critical-error state
Unprivileged counters (cycle/time/instret) and F/D’s fflags/frm/fcsr; RV32’s high-half shadow CSRs are illegal instructions on this RV64 core
Synchronous exceptions: instruction/load/store misaligned access, illegal instruction, breakpoint, ECALL from U/S/M, and instruction/load/store page faults once paging is active; the H extension adds VS-mode ECALL (cause 10), virtual-instruction (22), and guest-page-fault causes (20/21/23)
Interrupts: machine/supervisor x {software, timer, external}; MSIP/MTIP/MEIP are hardware-driven from the CLINT/CLINT/PLIC; SEIP becomes a read-only view of the IMSIC S-file’s delivery signal when AIA’s mvien.SEIE is set (the PLIC’s own dedicated S-mode context is ORed in unconditionally); STIP becomes the real mtime >= stimecmp comparator once Sstc’s menvcfg.STCE is enabled
Interrupt priority: external > software > timer, machine level before supervisor level; the H extension adds a VS tier below both and AIA adds SGEIP (bit 12) to the same scheme
menvcfg/henvcfg/senvcfg: real STCE (Sstc), PBMTE (Svpbmt), ADUE (Svade/Svadu), FIOM, CBZE/CBCFE/CBIE (Zicboz/Zicbom), and LPE/SSE/DTE (Zicfilp/Zicfiss/Ssdbltrp) fields – henvcfg mirrors them for VS-stage (ADUE/SSE/DTE spec-forced to 0 whenever their menvcfg counterpart clears), and senvcfg mirrors the U-mode subset
WFI genuinely stalls fetch/issue until an individually-enabled-and-pending interrupt is observed – independent of the global enable bits – with mstatus.TW/hstatus.VTW timeout-wait and the U-mode/VU-mode rules enforced via a bounded hardware timer
Documented simplifications. Not silent scope cuts:
XS/UBE read a constant 0 – unimplemented; nothing in this design needs them yet
Every remaining menvcfg/henvcfg/senvcfg field is WPRI and reads 0
Synchronized Timer
A single shared high-resolution timer, synchronized across all harts by construction:
Exactly one mtime register in the whole design (the CLINT), incrementing every core clock cycle – e.g. 10 ns resolution at a 100 MHz core clock; every hart’s time CSR (rdtime) observes the literal same value in the same cycle, with no per-core replica to resynchronize and no drift to bound
Per-hart state is only mtimecmp[hart] and msip[hart]; the register layout matches the SiFive/QEMU “virt” CLINT, so an unmodified OpenSBI/Linux driver works against it
Sstc: a real per-hart stimecmp CSR compared against the same shared mtime, gated by M-mode’s menvcfg.STCE – once enabled, S-mode programs its own timer interrupt directly with no CLINT MMIO access and no M-mode relay; while disabled, S-mode access to stimecmp traps illegal-instruction (the mechanism firmware uses to detect whether the hand-off is safe)
i-RISCV Hypervisor Support
The i-RISCV core implements the RISC-V H (Hypervisor) extension. The following mechanisms are supported.
Virtualization Modes and CSRs
A virtualization-mode bit V layering VS-mode and VU-mode on top of HS-mode/U-mode; SRET is the only way a hart enters a guest (hstatus.SPV/sstatus.SPP)
The full hypervisor CSR set (hstatus, hedeleg/hideleg, hie/hvip/hip, hgeip/hgeie, henvcfg, hcounteren, htimedelta, htval/htinst, hgatp) and the VS-mode shadow CSRs (vsstatus, vsie/vsip, vstvec, vsscratch, vsepc, vscause, vstval, vsatp, vstimecmp)
Transparent CSR substitution while V=1: supervisor CSRs redirect to their VS counterparts, including satp/vsatp and stimecmp/vstimecmp
henvcfg mirrors menvcfg’s own enable fields (STCE, PBMTE, ADUE, FIOM, CBZE/CBCFE/CBIE, LPE/SSE/DTE) for VS-stage and genuinely gates VS-mode independently of menvcfg – Svpbmt/Zicbom/Zicboz access in VS-mode is gated by henvcfg’s own bit alone; only STCE and ADUE stack both levels (the guest’s effective enable is the AND), with SSE/DTE spec-forced to 0 whenever their menvcfg counterpart clears
Trap Delegation and Virtual Instructions
Three-way M/HS/VS trap routing via medeleg/mideleg plus the guest-level hedeleg/hideleg; VS interrupts (VSSIP/VSTIP/VSEIP) and SGEIP are always delegated at least to HS-level, and SGEIP can never be delegated further down
Interrupt-cause renumbering when delegated into VS-mode, so a guest sees ordinary supervisor numbering
Virtual-instruction exceptions for HS-qualified instructions blocked by V=1, covering both spec groups (VTVM/VTSR-gated and unconditional, incl. WFI under VTW)
Two-Stage Address Translation
Separate VS-stage and G-stage walkers/TLBs in Sv39x4/Sv48x4/Sv57x4, mirroring the single-stage modes
“Walk of a walk”: every VS-stage page-table read – root pointer and each PTE – is itself G-stage translated before reaching memory
Guest-page-fault causes (20/21/23) distinct from ordinary page faults, always attributed to the original access kind – including when a VS-stage PTE fetch (checked as an implicit read) fails in G-stage
HS-level mstatus.MXR overrides both stages while guest vsstatus.MXR overrides only the VS stage; HS-level SUM is ignored for two-stage accesses
G-stage’s own permission quirks per spec: the U bit is always checked as though in U-mode, and a cached final result is re-checked whenever the access kind or the active VMID changes
Nested Virtualization
A single hardware VS/G-stage context supports nested (“L2+”) virtualization through software trap-and-emulate: a guest hypervisor runs at ordinary VS-mode, and its privileged operations (e.g. hgatp writes, HFENCE.GVMA) virtual-instruction-trap to the outer hypervisor for emulation
Shadow G-stage page-table merging enforces the inner hypervisor’s intended permissions on top of the outer hosting mapping; arbitrarily deep nesting by recursively re-applying the same technique, with no additional hardware
Hypervisor Instructions and Guest Interrupts
HLV/HSV/HLVX explicit guest-memory access that always acts as though V=1 at hstatus.SPVP, overriding MPRV – including execute-for-read substitution in both stages
HFENCE.VVMA / HFENCE.GVMA TLB invalidation for the VS-stage and G-stage respectively, reusing SFENCE.VMA’s 4-case selective predicate – rs2 selects an ASID for VVMA, a single VMID for GVMA (see Tagged TLB)
Guest interrupt injection via hvip, real per-guest-file MSI delivery through two IMSIC guest files (GEILEN=2) selected by hstatus.VGEIN – each with its own physical MSI region, so migrating a guest between files never moves the device’s write target – and SGEIP/SGEIE pending/enable plumbing
VS-mode virtual timer: a real vstimecmp comparator (Sstc) OR’d with hvip.VSTIP, gated by the guest’s own henvcfg.STCE; accessing vstimecmp itself requires that gate stacked on top of HS-level’s menvcfg.STCE; htimedelta offsets the guest’s view of time
Disclosed Simplifications
Spec-legal, not silent:
htval/mtval2 always read 0 – software recovers the faulting address via stval (the guest virtual address) alone
G-stage checks R rather than X permission for a guest instruction fetch’s final address (the shared walker instance also serves the always-a-read PTE-fetch case); VS-stage’s own X check is unaffected and correctly enforced
A hypervisor must still set hvip.VSEIP=1 once per guest-file assignment: the guest file’s real delivery signal is not independently OR’d into hip.VSEIP (the pre-AIA software-injection path remains required)
i-RISCV SBI Firmware
The i-RISCV SoC provides two M-mode boot-firmware paths that reach the exact same kernel image from the same device tree. The default path uses upstream OpenSBI, genuinely unmodified; the alternate path is this project’s own standalone SBI implementation. Which firmware runs is selected purely by the linux_boot Makefile target: check/image (and their 72 variants) use OpenSBI, check-sbi/image-sbi (and their 72 variants) the standalone SBI.
OpenSBI (default)
A pinned, unmodified upstream OpenSBI release built with PLATFORM=generic – recognized purely through OpenSBI’s own FDT drivers matching the SoC’s device tree nodes (serial@10000000, clint@2000000, plic0), with zero source customization
A tiny SoC-specific M-mode prestage programs the entropy-source FUSE bank and jumps directly into OpenSBI’s fw_jump entry point – still in M-mode, no mret
All standard boot setup (medeleg/mideleg, menvcfg, mcounteren, PMP) is performed by OpenSBI itself, computed from the hart’s detected extensions – including trap-safe PMP probing on builds where PMP is disabled
Standalone SBI (alternate)
A from-scratch M-mode firmware implementing the modern SBI v2.0 extension set:
Base (spec v2.0): spec/implementation ID and version, PROBE_EXTENSION, and vendor/arch/imp IDs read directly from the hart’s own CSRs
TIME, IPI, RFENCE, HSM, SRST, DBCN – each a real handler, not a stub
The full legacy (v0.1) extension set (SET_TIMER, console get/putchar, IPIs, remote fences, SHUTDOWN) for pre-v2.0 guests
Unimplemented extensions are correctly reported absent by PROBE_EXTENSION and return SBI_ERR_NOT_SUPPORTED when called, so a probing guest kernel falls back cleanly
Real Cross-Hart Behavior
IPI/RFENCE/HSM genuinely reach harts other than the one servicing the ecall – the same code path serves 2-hart and 16-hart (SMP16) builds:
IPI: decodes the real hart_mask/hart_mask_base operand pair and raises a real CLINT MSIP against every targeted hart that HSM has genuinely released
RFENCE: every FID (REMOTE_FENCE_I, REMOTE_SFENCE_VMA[_ASID], REMOTE_HFENCE_VVMA[_ASID], REMOTE_HFENCE_GVMA[_VMID]) is a synchronous cross-hart TLB shootdown – the calling hart fences itself, every other targeted hart is woken via a CLINT-MSIP request/ack handshake that blocks until its fence completes; address/ASID/VMID operands are passed straight through to real selective invalidation (see Tagged TLB)
HSM: MAX_HARTS-sized per-hart state tables backing a secondary-hart park loop, so HART_START/HART_GET_STATUS operate on real harts
Boot Setup Performed by the Firmware
Trap delegation: medeleg/mideleg set for S-mode, including the five H-extension causes (ECALL-from-VS, guest-page-faults 20/21/23, virtual-instruction) so a VS-mode guest under KVM is serviced by its own hypervisor – both paths delegate all five
menvcfg and mcounteren enabled so Sstc/Svpbmt/Zicbom/Zicboz and rdtime work from S-mode without trapping – the kernel’s early entropy initialization reads rdtime before any driver is up, so a missing mcounteren.TM breaks every boot
PMP: at least one permissive entry configured before dropping to S-mode (mandatory – a present-but-unconfigured PMP denies all S/U access); the standalone SBI writes one unlocked NAPOT entry covering the whole 56-bit physical address space, runtime-skipped on PMP_ENABLE=0 builds; OpenSBI programs its own RX/RW split from the detected PMP count with trap-safe probing
mseccfg’s Zkr sseed/useed bits enabled once at boot
Entropy-source FUSE bank programmed once at boot via shared code linked into both firmware paths (alongside the mseccfg setup)
The branch predictor needs no firmware action: transparent always-on hardware that clears its own BTB/PHT/RAS state on every privilege-domain crossing
How Linux Actually Uses It
A guest OS discovers what the firmware offers purely through PROBE_EXTENSION; for the kernels this project boots:
Timer: the device tree advertises "sstc", so S-mode writes stimecmp directly and the TIME extension’s SET_TIMER is never issued (implemented for spec completeness)
Console: bootargs select earlycon=uart8250,mmio,.../console=ttyS0, so Linux’s in-tree 8250 driver binds to the real UART node – no SBI involvement in the active console path; DBCN and legacy console stay implemented for guests configured with earlycon=sbi/hvc0
HSM/IPI/RFENCE: exercised by the kernel’s own SMP boot and TLB-shootdown code paths (see Linux)
SRST: guest reboot/poweroff issue SRST exactly as on real hardware
Standalone Firmware Validation
Before ever combining it with a kernel, the standalone firmware is validated in isolation by S-mode smoke tests linked to load where the kernel normally would (linux_boot/tools_scripts/run_sbi_smoke.sh {up,smp,dueling}): every implemented extension exercised via real ecalls on the single-hart build; a second real hart started and IPI/fenced on the 2-hart build; and a two-hart dueling-RFENCE regression test that reproduces (and proves fixed) a cross-hart fence deadlock in the non-reentrant M-mode trap context.
Disclosed Simplifications
Spec-legal, not silent:
HART_STOP parks the calling hart permanently rather than supporting a later re-HART_START of the same hart – Linux’s normal SMP boot never calls it (only CPU hotplug-remove does)
SRST reboot re-enters the firmware entry point with RAM untouched; no real board reset exists, so CSRs that only a hardware reset clears keep their values
Legacy v0.1 remote calls take hart_mask as a pointer treated as already physical – correct in practice because every Linux version booted here prefers the modern by-value IPI/RFENCE extensions and never reaches this path
i-RISCV Linux Support
The i-RISCV SoC boots an unmodified, stock Linux kernel. Both v6.6 and v7.2 are maintained as sibling boot harnesses sharing the same RTL, both firmware paths, and one device tree – a release 2.5 years newer booting with zero RTL or firmware changes is itself a stability test, and v7.2’s extension-detection table additionally recognizes six extensions v6.6’s doesn’t (Zicond, Zfa, Zawrs, Zicbop, Zihintpause, Zkt).
What Boots
Unmodified upstream Linux (v6.6 and v7.2), configured for every extension this core implements
Two interchangeable M-mode boot-firmware paths: unmodified upstream OpenSBI (default) or this project’s own standalone SBI – both hand off to the exact same kernel image at the same address from the same device tree (see SBI)
A single generated device tree (firmware/dts/gen_soc_dts.py) advertising all ratified ISA strings plus the real UART, PLIC, APLIC, IOMMU, and IMSIC nodes – v6.6 leaves the IOMMU/IMSIC/APLIC nodes unprobed (it has no in-tree drivers for them), while v7.2 genuinely resolves all three
What Works During Boot
Full extension detection by the kernel itself (base ISA plus every Z/S extension its version recognizes)
Sstc: the kernel writes stimecmp directly from S-mode for every tick – no SBI TIME calls at all
Real serial console: the stock 8250 driver binds to the ns16550a UART (earlycon bootconsole, then ttyS0)
Full boot-to-poweroff confirmed end to end: memory management, RCU, the ASID allocator, devtmpfs, IOMMU/IMSIC/APLIC driver init (v7.2), a real clocksource switch, and KVM all initialize; the non-interactive /init then dumps /proc/cpuinfo and powers off cleanly at roughly 143M-234M simulated cycles with no panic or hang
Real 2-hart SMP boot on both kernel versions (CONFIG_SMP=y against the multicore top): the second hart is parked at reset and released by SBI HSM, with IPIs via CLINT MSIP and synchronous cross-hart RFENCE TLB shootdowns (see SBI)
KVM: on v6.6 riscv_kvm_init() completes cleanly; on v7.2 the full KVM AIA SGEI path resolves against the real IMSIC S-file node and returns success within seconds of kernel time
This project’s own IOPMP platform driver binds on both kernels
Non-interactive userspace: PID 1 is a minimal POSIX-C program built against the kernel’s own vendored nolibc – it mounts devtmpfs/proc/sysfs, dumps /proc/cpuinfo to the console between fixed delimiters, and powers off; no interactive shell (an honest scope cut: one would need a full userspace cross-toolchain)
The simulator polls a testbench-only DBG_EXIT register that both firmware paths write on SRST shutdown, so a completed run stops itself immediately instead of idling to its cycle ceiling – the register is dead-code-eliminated from any real synthesis
Linux Boot Log + /proc/cpuinfo Output
The following listing is taken from a Verilator simulation run showing the unmodified OpenSBI booting an unmodified Linux 7.2.4 kernel.
At the end of the log the init process mounts /proc and /sys and dumps the content of /proc/cpuinfo.
The logs provide a glimpse on the support. The common regression testing of the i-RISCV SoC include the following testing:
SBI type
Number of Harts
Linux Kernel
OpenSBI
1
Linux kernel 7.2.4 without SMP support
OpenSBI
2
Linux kernel 7.2.4 with SMP support
OpenSBI
1
Linux kernel 6.6 without SMP support
OpenSBI
2
Linux kernel 6.6 with SMP support
standalone SBI
1
Linux kernel 7.2.4 without SMP support
standalone SBI
2
Linux kernel 7.2.4 with SMP support
standalone SBI
1
Linux kernel 6.6 without SMP support
standalone SBI
2
Linux kernel 6.6 with SMP support
All Linux kernels and OpenSBI code are used unmodified.
In addition, an 8-Hart test is implemented executing Linux 7.2.4 together with OpenSBI upon request. The goal of this test is to validate the heavy contention handling when 8 CPUs are instantiated and operated by Linux.
The i-RISCV implementation is verified with multiple, complementary test approaches – from module-level unit tests up to booting an unmodified Linux kernel, plus a fully open-source flow that takes the same RTL to a real ECP5 bitstream. The following approaches are used.
Simulation Test Layers
Module-level unit tests (tb/unit/): every RTL module gets a standalone Verilator C++ harness that instantiates only that module, drives its ports directly, and self-checks against hand-computed expected values or independent reference models (e.g. an independently re-implemented RVC decoder model swept over all 49152 encodings, the host CPU’s own IEEE-754 FPU as floating-point oracle). Dedicated builds with deliberately wrong known-answer vectors prove each crypto core’s self-test failure/lockout and recovery paths.
Directed full-core assembly tests (tb/tests/asm/): self-contained, hand-written RV64I+Zicsr+Zifencei programs, each exercising one pipeline mechanism end-to-end through the real SoC – single-core (check) and multi-core (check-mc). Every test reports its verdict as a P/F byte over the real UART peripheral, so the console path is exercised by every test’s own plumbing. Where a mechanism’s observable effect is timing rather than architectural state – e.g. WFI’s genuine fetch/issue stall versus a NOP-like retirement – the test measures real elapsed cycles (rdtime) around the instruction and asserts it falls inside the expected bound window, across ordinary wait-and-wake, same-cycle-NOP, and every TW/VTW/U-mode/VS/VU trap scenario.
Build-parameter enable/disable matrix (verif/enable_matrix/): independently elaborated builds for every combination of the optional-mechanism *_ENABLE parameters (a 32-way Zvk*/entropy-source permutation plus separate UART and async-reseed legs – 356 runs in all), each running a general regression battery plus state-specific tests – real known-answer vectors when enabled, illegal-instruction proofs when disabled. Deliberate negative-control runs confirm the harness would actually fail if the RTL gating were broken.
External NIST ACVP test-vector harness (ACVP Parser): externally supplied NIST ACVP JSON vectors reach the real RTL unmodified through a dedicated C backend – no software reimplementation of any algorithm is involved (SHA-2, SHA-3/SHAKE, AES-256, GCM/GMAC, KMAC256). The backend is compiled and linked against the real RTL today; live external-vector reconciliation is tracked follow-on work.
Conformance and System-Level Testing
RISCOF conformance suites (verif/riscof/): RISC-V International’s own official riscv-arch-test suites (I, M, C, D, Zifencei, privilege) run through a RISCOF-based harness with a bind-based monitor – test sources and expected signatures are both produced by RVI, independent of this project; the reference is riscv-arch-test’s own shipped .reference_output files rather than a live golden model. 275 of 283 tests pass; the 8 open privilege misalign-* cases stem from that shipped-reference limitation (this core splits misaligned accesses in hardware, while RVI’s static signatures assume trap-based behavior) and are independently verified by directed tests. Where no official suite exists (V, H, AIA, IOMMU, vector crypto), the project’s own directed tests stand in; all results and open issues are disclosed.
Linux boot as a system-level test: an entirely unmodified, stock Linux kernel (v6.6 and v7.2) boots against the whole SoC through both firmware paths, on one and two harts – real, independently developed software with its own timing assumptions, catching classes of bug no directed test reaches (peripheral register-ordering hazards, firmware/CSR ordering issues, asynchronous-interrupt races).
Firmware smoke tests: standalone S-mode programs (linux_boot/tools_scripts/run_sbi_smoke.sh {up,smp,dueling}) validate the boot firmware in isolation without paying for a full kernel boot – every implemented SBI extension via real ecalls on one hart; a second real hart started and IPI/fenced on two harts; and a dueling-RFENCE regression for a cross-hart fence deadlock.
Differential fuzzing harness: generates randomized-but-legal RV64I+Zicsr/Zifencei instruction streams and runs the same stream through both a golden reference model of Spike and the RTL executed with the Verilator simulator, comparing full architectural state after every retired instruction: all GPRs, PC, and the CSR list. Divergences are delta-minimized into self-checking directed reproducers kept in-tree.
Hazard-Family Regressions and Assertion Safety Nets
Interrupt-vs-commit-race regressions: real CLINT timer interrupts phase-swept across the commit windows of every multi-cycle unit (AMO, CBO.ZERO, misaligned split stores, vector stores, cold page-table walks), with poison-value consumers and negative-control reverts proving each test genuinely discriminates
Cross-hart synchronization regressions: two harts free-running unsynchronized AMO and LR/SC increment races against a shared counter (the final count must be exactly 2N), plus PLIC in-service-latch coverage – every other shared, multi-hart-reachable resource audited safe by construction
SVA assertion safety net: assert property blocks checked under Verilator’s --assert in every simulation build point guard the whole hazard family; one property cross-checks an independently maintained enumeration of privilege/interrupt/translation-relevant CSR writes against the redirect-handling list, so a future edit that drops a category fires deterministically instead of waiting for a probabilistic directed test to hit its exact race cycle
Side-Channel and Non-Functional Audits
Constant-time (cycle-count) audits: each cryptographic core and vector-crypto unit is audited that execution latency never depends on the value of keys or data – only on public control signals – measured across different contents at fixed operation shapes; the hardware entropy source receives the same treatment.
Speculative-execution resistance: a citation-backed argument (the Spectre/Meltdown disclosure papers are archived in docs/specifications/) that no mis-speculated instruction can leave an observable footprint, plus predictor-state erasure on every privilege-domain crossing – verified at unit and directed-test level.
Captured-data analysis: real noise-source data – from simulation and from real silicon via the FPGA harness – is captured over UART and analyzed against the stochastic model to determine entropy rate.
Compliance analysis: SP 800-90C (RBG2(P)/RBG3(RS)) and AIS 20/31 v3.0 (DRG.4+PTG.2 / PTG.3) compliance is documented requirement-by-requirement, with all disclosed gaps listed.
Real-Hardware (FPGA) Testing
A fully open-source flow (yosys read_slang + synth_ecp5 -> nextpnr-ecp5 -> ecppack -> openFPGALoader) targets the ULX3S (Lattice ECP5):
The directed assembly tests are reused unchanged – baked into boot RAM and reporting P/F over the real serial line
Board-only bring-up checks (heartbeat LED, reset/serial mirrors) that cannot be expressed in simulation at all
Entropy-source capture from genuine silicon jitter/metastability – data whose value is real physical noise rather than a pass/fail verdict
Current status: full-hierarchy elaboration through read_slang succeeds with zero errors (one synthesis-compatibility guard added to the RAM init path); a complete synth_ecp5 + place-and-route run is still being finalized – yosys reports logic-loop warnings localized to the FPU’s variable-shift idioms, working hypothesis a false positive pending nextpnr’s independent timing graph. No physical board has been attached yet: on-hardware execution remains a reproducible procedure for anyone with a ULX3S, not an observed result.
Waveform Inspection and CI Integration
Any test can produce FST waveforms viewable in Surfer (scripts/gen_waves.sh, per-module make wave-* targets) for signal-level debugging.
Per-feature CI runs the unit tests, single-core and multi-core directed sweeps, the enable/disable smoke matrix, the SBI firmware smoke tests, and a small Linux boot run; at the end of a development cycle the full permutation matrix, RISCOF suites, the differential fuzz campaign, and a full Linux boot are added.
About This Site
Code Signing Certificate
All source code distributed on this web site is signed. In order to verify the signature and thus the integrity and authenticity of the obtained code, use the following command:
This command only performs the verification if the associated public key was previously imported into the key ring. In case the public key needs to be imported, use the following command which imports the key from 2024: