MRVE32 (Modular RISC-V Emulator 32-bit)
A modular, deterministic RV32IMAC bare-metal microcontroller emulator in Rust, with pluggable devices.
MRVE32 is a lightweight RV32IMAC emulator designed for bare-metal firmware execution, custom SoC peripheral development, and hardware coprocessor prototyping. The core is a CPU, a memory-mapped I/O (MMIO) bus, RAM, a CLINT and an ELF loader; every other peripheral is a device module that anyone can write — in Rust, in Python, or as a separately built native package through a small, versioned C ABI. Python bindings make it a testbench engine.
Key Architecture & Features
- RV32IMAC + Zicsr + Zifencei: base integer ISA, multiplication/division, atomics (
lr.w/sc.wand allamo*.w), compressed 16-bit instructions (C/Zca), CSR instructions,fence/fence.i. C can be switched off (Cpu::set_compressed(false),--isa rv32ima,Emulator(compressed=False)) to model a core without it, with IALIGN = 32 andmisa.Cfollowing it. - Machine-mode privileged architecture: precise synchronous exceptions (illegal instruction, misaligned jump target, instruction/load/store access faults,
ecall,ebreak) delivered throughmtvecwithmepc/mcause/mtval/mstatusupdated as hardware does;mret. - Interrupts & timer: machine external, software and timer interrupts with spec priority (MEI > MSI > MTI), direct and vectored
mtvec,wfithat stalls the hart until an interrupt is pending. A CLINT device (msip,mtime,mtimecmp, SiFive memory map) is included; any device can raise interrupt lines throughBusDevice::irq_lines. - Counters (Zicntr): 64-bit
mcycle/minstretwithmcountinhibit, read-onlycycle/instretshadows;mhpmcounter*/mhpmevent*are hardwired to zero. - Strict CSR file: accesses to unimplemented CSRs and writes to read-only CSRs raise illegal-instruction exceptions; WARL fields (
mstatus,mtvec,mepc,mie,mip) are masked. Implemented:mstatus,mstatush,misa,mie,mip,mtvec,mscratch,mepc,mcause,mtval,mcountinhibit,mcycle[h],minstret[h],cycle[h],instret[h],mvendorid,marchid,mimpid,mhartid,mconfigptr. - Tested against riscv-tests: passes all 75 applicable tests from
riscv-tests(rv32ui-p-*,rv32um-p-*,rv32ua-p-*,rv32uc-p-rvcand the M-moderv32mi-p-*tests); every test exceptrvcruns both with and without C. The RISC-V Architectural Certification Tests (riscv-arch-test, ACT4) have a device configuration for MRVE32: the 104 unprivileged tests for RV32IMAC agree with QEMU at every signature point, and CI runs the full Sail-based flow. - Bare-Metal & Deterministic: Machine mode only, no MMU, virtual memory, or OS overhead.
- Modular Bus Architecture: Devices, RAM, and memory-mapped peripherals implement the
BusDevicetrait. The bus performs offset translation and address routing. - Hardware Coprocessor Slot: extension point via
trait Coprocessor(or a Python object,Emulator.set_coprocessor) for custom instructions (opcodes0x0Band0x2B). Register updates are committed atomically andx0stays zero. - Unidirectional Ownership: Clean Rust ownership model (
cpu.step(&mut bus)). The CPU core maintains zero references to the system bus. - No dependencies: the core library depends only on Rust
std. - Standard peripherals: RAM, CLINT, an NS16550A-compatible UART and a
sifive,test0test finisher (firmware sets the exit status), in the QEMUvirtregister layout so existing drivers work unchanged. - Command-line runner:
mrve32 firmware.elfruns an ELF on a QEMU-virt-like memory map, streams the UART to stdout and exits with the firmware's status;--traceprints every instruction (GNU objdump-style disassembler),--stop-on-trap,--max-steps, double-fault detection;--htifand--signaturefor compliance suites (HTIFtohost, Sail-format signature dump). - Pluggable devices: a device is anything implementing
BusDevice(Rust), a Python object withread/writemethods, or a native module built separately against the device C ABI (include/mrve32_device.h,mrve32::plugin). Devices gettick()time and can raise interrupt lines. Seedocs/devices.md. - Python package (PyO3):
pip install-ablemrve32module with anEmulatorclass (ELF loading, symbols, breakpoints, run control, instruction tracing, memory/register/CSR access,map_device),Uart,TestFinisheranddisassemble. Typed (py.typedstubs).
Device modules
| Module | Description |
|---|---|
mrve32-snn |
SNN layer accelerator model (draft v0): LIF/IF neurons, i16 potentials, i8 weights, a bit-exact golden model behind a swappable SnnBackend, an analytical cycle model, a completion interrupt, and a NumPy reference. Spec. |
Modules live in modules/ for now but depend on the core only through its public API and could be moved to their own repositories unchanged.
Not implemented (yet)
The time/timeh CSRs (read mtime from the CLINT instead), an interrupt controller (PLIC; the UART drives MEIP directly), User/Supervisor modes, PMP, debug triggers, HTIF/semihosting, the F/D extensions, and Zcb/Zcmp. Atomics assume a single hart: aq/rl have no effect and a trap clears the LR reservation.
Build firmware with -march=rv32imac_zicsr_zifencei -mabi=ilp32 (or any subset, e.g. rv32imc_zicsr). Misaligned loads and stores are performed in hardware rather than trapped.
Command-line runner
cargo install --path . # installs the `mrve32` binary
mrve32 firmware.elf # UART -> stdout, exit status from the finisher
mrve32 --trace --max-steps 20 firmware.elf
mrve32 --htif --signature out.sig test.elf # Sail/Spike-style tests: HTIF tohost, signature dump
mrve32 --help
Default memory map (QEMU virt-compatible, every address configurable):
| Device | Base | Notes |
|---|---|---|
| RAM | 0x8000_0000 |
--ram-size, default 128 MiB |
| UART (NS16550A) | 0x1000_0000 |
output to stdout; --input FILE|- feeds its receiver |
Test finisher (sifive,test0) |
0x0010_0000 |
write 0x5555 to pass, (code << 16) | 0x3333 to fail |
| CLINT | 0x0200_0000 |
mtime, mtimecmp, msip |
Exit status is the firmware's (0–255), or 123 (idle in wfi forever),
124 (--max-steps), 125 (usage/load error), 126 (trap with
--stop-on-trap, or a double fault such as running without mtvec).
tests/firmware is a minimal freestanding C example
(startup, linker script, UART, finisher) built by
scripts/build-test-firmware.sh.
Architecture Overview
+------------------------------------+
| CPU Core (RV32IMAC) |
| - 32x Regs (x0 hardwired to zero) |<--- [ Coprocessor Slot ]
| - Program Counter (PC) | (Custom opcodes 0x0B, 0x2B)
+-----------------+------------------+
| cpu.step(&mut bus)
+-----------------v------------------+
| System Bus |
+--------+------------------+--------+
| |
+----------v---------+ +-----v--------------+
| RAM | | CLINT, Custom |
| (Little-Endian MM) | | Peripherals, IRQs |
+--------------------+ +--------------------+
Rust Usage
Add MRVE32 to your Cargo.toml, then use the core:
use mrve32::{Bus, BusDevice, Cpu, Ram};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut bus = Bus::new();
let mut ram = Ram::new(1024);
// Write machine instructions into RAM:
// addi x1, x0, 42 (0x02A0_0093)
ram.write_u32(0, 0x02A0_0093)?;
bus.map_device(0x1000, 1024, Box::new(ram))?;
let mut cpu = Cpu::new(0x1000);
// `step` returns `Some(trap)` if the instruction raised an exception.
// The trap has already been taken: `pc` points to the `mtvec` handler.
if let Some(trap) = cpu.step(&mut bus) {
return Err(trap.into());
}
assert_eq!(cpu.read_reg(1), 42);
assert_eq!(cpu.pc, 0x1004);
Ok(())
}
Time and interrupts
Each step is one clock cycle: the CPU samples mip from the bus
interrupt lines, takes an enabled pending interrupt (or executes one
instruction), then calls BusDevice::tick(1) on every mapped device.
Map a Clint to get mtime/mtimecmp and software interrupts:
use mrve32::devices::clint::CLINT_SIZE;
use mrve32::{Bus, Clint};
let mut bus = Bus::new();
// mtime advances once every 10 CPU cycles
bus.map_device(0x0200_0000, CLINT_SIZE, Box::new(Clint::with_divider(10)))
.unwrap();
Custom peripherals raise interrupts by overriding BusDevice::irq_lines
(return a mask of MIP_MEIP/MIP_MSIP/MIP_MTIP) and advance their own
state in BusDevice::tick; see docs/devices.md.
Python Testbenches (PyO3)
Install the mrve32 package (builds the extension with maturin;
needs a Rust toolchain and Python >= 3.10):
pip install . # or, for development: pip install -e ".[test]"
Run firmware and inspect the machine:
import mrve32
# RAM at 0x8000_0000 and a CLINT at 0x0200_0000 by default
# (clint_base=None omits it); add a UART and a test finisher as in QEMU virt.
emu = mrve32.Emulator(ram_size=1 << 20)
uart = mrve32.Uart()
emu.map_device(0x1000_0000, uart)
emu.map_device(0x0010_0000, mrve32.TestFinisher())
entry = emu.load_elf("firmware.elf") # path or bytes; sets pc to the entry
res = emu.run(1_000_000, stop_on_trap=True)
print(res.reason, res.steps) # "exit" | "max_steps" | "breakpoint" | "trap" | "idle"
print(res.exit_code, uart.output()) # finisher status, bytes sent on the UART
if res.trap:
print(hex(res.trap.cause), res.trap.description)
print(emu.regs, hex(emu.pc), emu.cycles, emu.instret)
emu.add_breakpoint(emu.symbol("main"))
emu.set_tracer(lambda pc, word, trap: print(hex(pc), mrve32.disassemble(word, pc) if word else trap))
data = emu.read_bytes(emu.symbol("buffer"), 64) # raises mrve32.BusError if unmapped
Attach devices: native modules or plain Python objects (docs/devices.md):
class Gpio:
size = 4
def __init__(self): self.pins = 0
def read(self, offset, width): return self.pins
def write(self, offset, width, value): self.pins = value
emu.map_device(0x1000_0000, Gpio()) # Python device
emu.map_device(0x2000_0000, mrve32.Ram(4096)) # native device
print(emu.devices) # [(base, size, name), ...]
Prototype custom instructions (custom-0 0x0B / custom-1 0x2B) with a Python
coprocessor; register writes are committed only if execute succeeds:
class Mac:
def execute(self, call): # call: mrve32.CoprocessorCall
if call.funct3 != 0:
return False # illegal instruction trap
acc = call.read_reg(call.rd) + call.read_reg(call.rs1) * call.read_reg(call.rs2)
call.write_reg(call.rd, acc & 0xFFFF_FFFF)
# call.read_u32(addr) / call.write_u32(addr, v): bus access
# (an uncaught BusError becomes a load/store access fault);
# call.next_pc = target: branch
emu.set_coprocessor(Mac()) # None detaches it
For example, with the SNN module (pip install ./modules/snn):
import numpy as np
from mrve32 import Emulator
from mrve32_snn import SnnAccelerator, SnnDriver, reference_step
emu = Emulator()
emu.map_device(0x1000_0000, SnnAccelerator())
dev = SnnDriver(emu, 0x1000_0000)
weights = np.random.randint(-128, 128, size=(64, 32), dtype=np.int8)
dev.load_layer(weights, v_th=100, leak_shift=4)
vmem = np.zeros(32, np.int16)
for t in range(10):
spikes = np.random.rand(64) < 0.2
out = dev.step(spikes) # write inputs, START, wait, read outputs
expected, vmem = reference_step(spikes, weights, vmem, v_th=100, leak_shift=4)
assert (out == expected).all() and (dev.read_vmem() == vmem).all()
print(t, out.sum(), "spikes,", dev.cycles, "cycles")
Run the Python tests:
pytest tests/python modules/snn/tests/python
Verification & Quality Assurance
Run the full test suite (unit tests, README examples, an interrupt-driven firmware test and the riscv-tests suite):
cargo test --workspace
Run only the riscv-tests suite (per-test results are printed):
cargo test --test compliance -- --nocapture
The test ELF files in tests/isa/ are built reproducibly from source with a
pinned riscv-tests revision (requires a RISC-V GCC cross toolchain):
scripts/build-riscv-tests.sh
RISC-V Architectural Certification Tests (needs a RISC-V GCC; --qemu also
runs every self-checking ELF on QEMU; the Sail-based flow is described in
tests/arch-test/README.md):
scripts/arch-test.sh local --qemu
Robustness: tests/decoder_fuzz.rs runs every 16-bit encoding, every
opcode/funct3/funct7 combination and a million random words through the
decoders and the disassembler, and random programs through the CPU.
Coverage-guided targets for cargo-fuzz are in fuzz/
(CI runs them weekly). The disassembler was cross-checked against GNU objdump
2.46: the only differences are encodings the spec reserves on RV32
(slli/srli/srai with shamt[5] = 1, c.addi16sp with immediate 0),
which objdump decodes and MRVE32 rejects, and the names of C HINT encodings.
Interpreter throughput (MIPS) for a few instruction mixes:
cargo bench --bench mips
Run static analysis and linter checks:
cargo clippy --workspace --all-targets --all-features -- -D warnings
The minimum supported Rust version (MSRV) is 1.88 (checked in CI).
Changes are listed in CHANGELOG.md.
License
Licensed under Apache License, Version 2.0.
Metadata
Release files for mrve32 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mrve32-0.1.0.tar.gz | 167.3 kB | With provenance | Details for mrve32-0.1.0.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| mrve32-0.1.0-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details for mrve32-0.1.0-cp310-abi3-win_amd64.whl |
| mrve32-0.1.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details for mrve32-0.1.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
| mrve32-0.1.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details for mrve32-0.1.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
| mrve32-0.1.0-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details for mrve32-0.1.0-cp310-abi3-macosx_11_0_arm64.whl |
| mrve32-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details for mrve32-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl |
Total release size: 1.9 MB
Release files / mrve32-0.1.0.tar.gz
| Download URL | mrve32-0.1.0.tar.gz |
|---|---|
| Size | 167.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f65860fdc763d3e6b8fe042a8f2508d1fcc9abfbe55c14b9b82a3f577cca8d65
|
|
BLAKE2b-256 checksum How to use checksums |
a7bdd3dcccda5d97dcc8ca0ed34f95ecd49d6437b93ea54b6afc1e5c8665f55f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / mrve32-0.1.0-cp310-abi3-win_amd64.whl
| Download URL | mrve32-0.1.0-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 250.7 kB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
69e7c1950cd2814f4034e95db077d02045b67d656ff1d67514741ad6ed21f2c2
|
|
BLAKE2b-256 checksum How to use checksums |
78491b6c4c034e3d57f581ead7b410216ad700ee3ea220f00d91f0741bcb8e36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / mrve32-0.1.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | mrve32-0.1.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 398.2 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b5cac2b943ef3bdef27e7d51a728da84b5e3d31812c4f54004c61ec9ccb6214e
|
|
BLAKE2b-256 checksum How to use checksums |
5f9a4aacf4d50d3134c14dbd7427d6f417c52921cade56f08373686949e2599a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / mrve32-0.1.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
| Download URL | mrve32-0.1.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl |
|---|---|
| Size | 394.2 kB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
ff048ddd00c26377544b396792d835d76525402bfef7eee717e4d29ba2e3cc15
|
|
BLAKE2b-256 checksum How to use checksums |
824ba92370cf2680987193f0bad3661278b520db0e9102b24b9701247547811f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / mrve32-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | mrve32-0.1.0-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 353.6 kB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
97bd72d7043bce1a53b68eeb33fe4c9b934a00b2e783d7f665baa3635e6f7765
|
|
BLAKE2b-256 checksum How to use checksums |
302f0432ed8fe8a378c341bfea26efd4b70c41133ff664405d3fff46c2a60044
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logRelease files / mrve32-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | mrve32-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 353.4 kB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
effc3f5b23acf85832f24598223f87b1c9c58e9d1cc8dbb5b3e41d5927eb5770
|
|
BLAKE2b-256 checksum How to use checksums |
75f3018d90de2ea01e91c65bd38d2d24afcafdf995122b62119ca37fc1471e3c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logSecurity for mrve32 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 6 files in this release have been uploaded with provenance metadata.
Attestation for mrve32-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 3, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.