Skip to main content

Rust Crates.io Docs.rs PyPI License: Apache 2.0 CI

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.w and all amo*.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 and misa.C following it.
  • Machine-mode privileged architecture: precise synchronous exceptions (illegal instruction, misaligned jump target, instruction/load/store access faults, ecall, ebreak) delivered through mtvec with mepc/mcause/mtval/mstatus updated as hardware does; mret.
  • Interrupts & timer: machine external, software and timer interrupts with spec priority (MEI > MSI > MTI), direct and vectored mtvec, wfi that 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 through BusDevice::irq_lines.
  • Counters (Zicntr): 64-bit mcycle/minstret with mcountinhibit, read-only cycle/instret shadows; 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-rvc and the M-mode rv32mi-p-* tests); every test except rvc runs 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 BusDevice trait. 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 (opcodes 0x0B and 0x2B). Register updates are committed atomically and x0 stays 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,test0 test finisher (firmware sets the exit status), in the QEMU virt register layout so existing drivers work unchanged.
  • Command-line runner: mrve32 firmware.elf runs an ELF on a QEMU-virt-like memory map, streams the UART to stdout and exits with the firmware's status; --trace prints every instruction (GNU objdump-style disassembler), --stop-on-trap, --max-steps, double-fault detection; --htif and --signature for compliance suites (HTIF tohost, Sail-format signature dump).
  • Pluggable devices: a device is anything implementing BusDevice (Rust), a Python object with read/write methods, or a native module built separately against the device C ABI (include/mrve32_device.h, mrve32::plugin). Devices get tick() time and can raise interrupt lines. See docs/devices.md.
  • Python package (PyO3): pip install-able mrve32 module with an Emulator class (ELF loading, symbols, breakpoints, run control, instruction tracing, memory/register/CSR access, map_device), Uart, TestFinisher and disassemble. Typed (py.typed stubs).

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)

Source distribution for mrve32 0.1.0
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)

Table of built distributions (wheels) for mrve32 0.1.0
File Actions
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 log

Release 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 log

Release 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 log

Release 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 log

Release 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 log

Release 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 log

Security 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.

Select from 6 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

6 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page