Tutorial 4 - A Custom Instruction via VexRiscv's CFU (optional)
This tutorial is entirely optional and safe to skip. If you're already comfortable with Tutorials 1-3, it's a self-contained extra: a second way to add a custom instruction to a RISC-V core, alongside the CMAC extension you build by hand in Project 2. If you're short on time, skip straight to Tutorial 2/3 instead - nothing later in the course depends on this one.
Goal
By the end of this tutorial, you should be able to: add a custom RISC-V instruction to VexRiscv (the CPU LiteX gives you) using its built-in CFU (Custom Function Unit) port, without modifying the CPU's own source code - and compare that experience against building CMAC by hand in Project 2.
Background: a second way to add a custom instruction
Project 2's CMAC extends a CPU you wrote yourself - you add a new opcode, a new CSR, and the decode logic all inside riscv_core.sv. VexRiscv (the CPU LiteX assembles into your SoC) is a CPU you don't write, but its authors anticipated exactly this need: a CFU port - a fixed-shape hardware socket for exactly one custom instruction, wired in via a command-line flag instead of editing VexRiscv's own source.
Available on the full+cfu (and full+cfu+debug) CPU variants, via --cpu-variant=full+cfu --cpu-cfu=<yourfile>.sv on the command line.
Important: this is a different opcode from the course's own CMAC. The course's CMAC uses the RISC-V-reserved custom-0 opcode (0001011). VexRiscv's CFU port, once wired in, decodes to custom-1 (0101011, 0x2B) instead - confirmed directly from VexRiscv's generated Verilog, not from its (Scala) source. Same idea (a reserved custom opcode carrying a CPU-specific extension), different bit pattern - don't expect the two to be interchangeable or to compare .insn encodings directly against your Project 2 CMAC.
1. Write the CFU module
The module must be named exactly Cfu (capital C) - LiteX's VexRiscv.add_cfu() hardcodes Instance("Cfu", ...) regardless of the filename you pass to --cpu-cfu. This example mirrors CMAC's own semantics as closely as CFU's request/response shape allows: an internal accumulator, one function_id to accumulate (macc += inputs_0 * inputs_1), another to read-and-clear it.
cmac_cfu.sv:
One always_ff block, not two - the exact same reason Tutorial 3 calls out: two separate always_ff blocks both driving macc would pass Verilator simulation but fail real Yosys synthesis with a "multiply driven" error. This module is written correctly from the start.
2. Build and boot in simulation
Confirm the boot banner reports CPU: VexRiscv_FullCfu and you still reach a litex> prompt, same as Tutorials 1 and 3.
If you're iterating and rebuilding with a different --cpu-variant than a previous run, do a full rebuild (don't pass --no-compile-software). A cached BIOS binary built for a different CPU variant produces a SoC that boots (consumes CPU) but never prints anything - confusing, since nothing looks broken. Rebuilding both gateware and software together avoids this.
3. Call it from C
GCC has no built-in knowledge of this instruction - same situation as CMAC - so it's invoked via inline .insn, using the confirmed encoding (custom-1 = 0x2B; funct7 unused; funct3 selects the operation):
Write a small program calling cmac_accumulate a few times and printing cmac_read_and_clear()'s result - e.g. cmac_accumulate(3,4); cmac_accumulate(5,6); cmac_accumulate(2,7); should make cmac_read_and_clear() return 56 (3×4 + 5×6 + 2×7). Load it the same way as Tutorial 1 (litex_term --kernel firmware.bin socket://localhost:1234) and confirm the printed result.
4. Confirm on real hardware
Repeat Tutorial 2's build-and-load flow, adding the same two flags:
A real trade-off to expect, not a bug: full+cfu is a noticeably bigger VexRiscv configuration than the standard variant Tutorials 1-3 use (more pipeline stages, likely more branch-prediction/cache logic) - independent of this tiny CFU module itself. Expect roughly 67% LUT4 utilization here vs. ~57% for the standard-variant baseline. If you're combining this with Tutorial 3's peripheral in the same build, budget for it: headroom drops noticeably, though it still fits on the Tang Nano 9K.
A note on the multiply inside cmac_cfu.sv
full+cfu already includes VexRiscv's own hardware multiplier (needed for the variant regardless of this exercise). The * inside cmac_cfu.sv above is separate, dedicated logic - it doesn't reuse or conflict with the CPU's multiplier - but it does mean this exercise isn't teaching you to build a multiplier (Project 2's Zmmul extension already covers that). What it teaches is the custom-instruction interface: encoding, decode-side wiring, and the request/response protocol - the same conceptual step as CMAC, via a different, pre-built hardware door.
Checklist
-
cmac_cfu.svwritten as a singlealways_ffmodule named exactlyCfu. - SoC boots in simulation with
--cpu-variant=full+cfu --cpu-cfu=cmac_cfu.sv. - A C program using
.insn r 0x2B, ...compiles and, run in simulation, prints the expected accumulated value. - Same behavior confirmed on the Tang Nano 9K.
- You can explain, in your own words, why this opcode (
custom-1) is not the same as the course's own CMAC opcode (custom-0), despite the similar idea.