The soft RISC-V core at the heart of the tinyCLUNX33 SoM and our MIPI2UVC IP manages sensor initialization, peripheral configuration, and data flow — and increasingly, runtime-defined logic via MicroPython. We wanted to know how much performance was sitting on the table, and what it would cost in FPGA area to get it. This is the engineering writeup.
The tinyCLUNX33 System on Module, from Lattice development partner tinyVision.ai, is built around the LIFCL-33U platform and uses VexRiscv, an FPGA-friendly RISC-V CPU implementation, as its computing core — the same base architecture as Lattice's own RX Core IP.
Getting the best performance out of that core matters for real client use cases, especially where dynamic MicroPython scripting is involved for runtime-defined applications. But raw performance is secondary to leaving enough FPGA area for the application itself. So rather than chase peak performance, we went looking for the best ratio of performance to area — efficient performance, not just fast performance.
Structure of VexRiscv and CPUs
VexRiscv is a pipelined, in-order, 2-to-5-stage design that follows an operation order common to most CPU implementations.

Defining the adjustable variables
VexRiscv ships multiple example configurations in its git repository, each demonstrating a different performance tier and pointing at what actually moves the needle. Taking an example configuration like GenSmallAndProductiveICache, a VexRiscv build is really just a list of plugins:
object GenSmallAndProductiveICache extends App{
def cpu() = new VexRiscv(
config = VexRiscvConfig(
plugins = List(
new PcManagerSimplePlugin(
resetVector = 0x80000000l,
relaxedPcCalculation = false
),
new IBusCachedPlugin(
config = InstructionCacheConfig(
cacheSize = 4096,
bytePerLine = 32,
wayCount = 1,
addressWidth = 32,
cpuDataWidth = 32,
memDataWidth = 32,
catchIllegalAccess = false,
catchAccessFault = false,
asyncTagMemory = false,
twoCycleRam = false,
twoCycleCache = true
)
),
new DBusSimplePlugin(
catchAddressMisaligned = false,
catchAccessFault = false
),
new CsrPlugin(CsrPluginConfig.smallest),
new DecoderSimplePlugin(
catchIllegalInstruction = false
),
new RegFilePlugin(
regFileReadyKind = plugin.SYNC,
zeroBoot = false
),
new IntAluPlugin,
new SrcPlugin(
separatedAddSub = false,
executeInsertion = true
),
new LightShifterPlugin,
new HazardSimplePlugin(
bypassExecute = true,
bypassMemory = true,
bypassWriteBack = true,
bypassWriteBackBuffer = true,
pessimisticUseSrc = false,
pessimisticWriteRegFile = false,
pessimisticAddressMatch = false
),
new BranchPlugin(
earlyBranch = false,
catchAddressMisaligned = false
),
new YamlPlugin("cpu0.yaml")
)
)
)
SpinalVerilog(cpu())
}
Each plugin is one piece of the CPU:
- PcManagerSimplePlugin — program counter management
- IBusCachedPlugin — the instruction bus, this version implements a cache to reduce the limitations of slow program memory / XIP
- DBusSimplePlugin — the data bus, uncached, direct memory access
- CsrPlugin — standard RISC-V Control and Status Registers
- RegFilePlugin — non-CSR memory-mapped registers
- IntAluPlugin — basic integer math (ADD, SUB...)
- SrcPlugin — feeds other instruction pipeline plugins
- LightShifterPlugin — low-area bit shifting
- HazardSimplePlugin — organizes and gates instruction execution, handles decode/execution hazards
- BranchPlugin — branching instructions
- YamlPlugin — emits a YAML description of the generated CPU configuration
Every plugin has configuration knobs that can affect performance, and several have faster or smaller alternate implementations entirely.
Method
The approach was straightforward: toggle settings, try variants, compare results.
Performance was measured with CoreMark running on Zephyr. We gathered CoreMark scores running from both XIP and RAM, and computed CoreMark/MHz to make configurations comparable regardless of clock. Area usage came from the Device utilization summary section of the Radiant generation logs. Everything else — frequency, included peripherals, compilation settings — was held constant, so any performance change is a direct efficiency change.
Reference system: 65MHz core clock, SPI flash XIP for program memory, LRAM blocks for RAM, an I2C peripheral, JTAG debug, and a basic peripheral set (UART, interrupts, timers) — sized for its role as a USB3 camera-handling system, reserving most of its area for USB and camera-specific logic.
What we adjusted, and what it cost
Instruction Bus
VexRiscv offers IBusSimplePlugin (cacheless — not used in the tinyVision design) and IBusCachedPlugin, which we run at 2KB, one-way.
Bumping this to 4KB / two-way (which also requires setting regFileReadyKind to ASYNC in RegFilePlugin) narrows the XIP-vs-RAM benchmark gap and adds roughly 220 LUTs and 220 registers, plus more BRAM usage (without needing a new block). On its own it doesn't move peak performance — but stacked on top of the other changes below, it's worth up to a 10% gain.
Branch prediction
Branch prediction is controlled jointly by BranchPlugin and the IBus plugin. The tinyVision design uses STATIC prediction, speculating in the decode stage to cut branch cost to 1 cycle instead of 2–4.
Switching to DYNAMIC_TARGET — 2-bit history speculation in the fetch stage, VexRiscv's fastest mode — doubles EBR block usage (4 → 8), adds ~300 LUTs and ~120 registers, for a modest 4.5% performance gain. Expensive for what it returns.
Integer multiplication
Two options: MulDivIterativePlugin (iterative, also the only option for division, unrollable for speed at the cost of area) and MulPlugin (four pipelined 17x17 multiplications, no execution stalls).
The tinyVision design defaults to MulDivIterativePlugin tuned for minimal area. Raising its unroll factor from 1 to 4 buys 24% more CoreMark performance for ~190 LUTs and ~115 registers. Switching to MulPlugin entirely is the biggest single lever in this whole exercise: +50% performance, at the cost of DSP resources (8 MULT9, 4 MULT18, 8 REG18, 8 PREADD9) plus 240 LUTs and 140 registers.
Shift instructions
LightShifterPlugin (default, smallest area, one cycle per bit shifted) versus FullBarrelShifterPlugin (full shift in a single cycle). Switching to the barrel shifter is +33% performance for a modest 130 LUTs and 30 registers — one of the best efficiency trades available.
Program Counter relaxation
The tinyVision design had PC calculation "relaxed" (more tolerant of FPGA timing variation, to allow higher frequencies) — but at the frequency actually in use, this turned out to be unnecessary. Disabling it (relaxedPcCalculation = false) is a free win: +7% performance while also cutting ~20 LUTs and ~115 registers.
Other things we tried
Raising core clock frequency further was limited by LRAM block access timings (already using the 2-cycle OUTREG relaxation). Caching the data bus via DBusCachedPlugin was attempted but not successfully enabled. Tweaking SrcPlugin's separatedAddSub, executeInsertion, and decodeAddSub affected timing but yielded no performance improvement.
Results
All scores below run from RAM (except the nrf52840, which refused to) with CONFIG_SPEED_OPTIMIZATIONS=y.
| Configuration | Score | Score/MHz |
|---|---|---|
| Initial tinyVision VexRiscv configuration | 55 | 0.84 |
| Bigger cache | 55 | 0.84 |
| Branch prediction | 57 | 0.876 |
| tinyVision LiteX VexRiscv @ 80MHz | 73 | 0.912 |
| MulPlugin | 68 | 1.04 |
| FullBarrelShifterPlugin | 73 | 1.12 |
| FullBarrelShifterPlugin & unrelaxed PC | 78 | 1.2 |
| FullBarrelShifterPlugin & unrelaxed PC & MulPlugin | 117 | 1.8 |
| RP2040 @ 125MHz (reference) | 230 | 1.84 |
| FullBarrelShifterPlugin & unrelaxed PC & MulPlugin & bigger cache @ 72MHz | 142 | 1.97 |
| NRF52840 @ 64MHz (reference) | 153 | 2.39 |
Conclusion
Tweaking these settings got us within reach of VexRiscv's advertised ceiling: our best configuration hit 1.97 CoreMark/MHz — better than the RP2040, more than double our starting point, and just shy of VexRiscv's documented maximum of 2.57 CoreMark/MHz. Compared to Lattice's RX core, this configuration uses very little Embedded Memory, leaning on abundant Large RAM instead, and gets similar performance out of a simpler SPI flash XIP setup through caching.
The best area-to-performance trade in the whole set is disabling relaxed PC calculation and switching to FullBarrelShifterPlugin — a combined 43% performance improvement for zero extra FPGA area, raising CoreMark/MHz from 0.84 to 1.2. That's the one we ship by default.
The rest — bigger cache, dynamic branch prediction, MulPlugin — are exposed as configuration options, so customers building on the tinyCLUNX33 SoM can make an informed call about how much FPGA area to trade for CPU performance, and try the change against their own design in minutes.
All artifacts and raw data are public: github.com/tinyvision-ai-inc/vexriscv_optimization.
This kind of tuning work runs on the same open-source stack we upstream into mainline Zephyr — including the USB video-class device driver and the Synopsys DWC3 USB3 controller driver that TI and RockChip now reuse in their own Zephyr ports. If you're evaluating an FPGA-based MIPI-to-USB bridge for a product and want to talk through the tradeoffs, get in touch.