Skip to content

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.

1
2
3
4
5
6
cmd.valid, cmd.ready                (handshake in)
cmd.payload.function_id  [10 bits]  (which operation - selects sub-behavior)
cmd.payload.inputs_0     [32 bits]  (operand 1, i.e. rs1)
cmd.payload.inputs_1     [32 bits]  (operand 2, i.e. rs2)
rsp.valid, rsp.ready                (handshake out)
rsp.payload.outputs_0    [32 bits]  (result)

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:

// Module name MUST be exactly "Cfu" - core.py's do_finalize() hardcodes
// Instance("Cfu", ...) regardless of the source filename.
module Cfu (
    input  logic        clk,
    input  logic        reset,

    input  logic        cmd_valid,
    output logic        cmd_ready,
    input  logic [9:0]  cmd_payload_function_id,
    input  logic [31:0] cmd_payload_inputs_0,
    input  logic [31:0] cmd_payload_inputs_1,

    output logic        rsp_valid,
    input  logic        rsp_ready,
    output logic [31:0] rsp_payload_outputs_0
);
    // function_id[2:0] (i.e. funct3) selects the operation; funct7 unused (0).
    //   0: accumulate     (macc += inputs_0 * inputs_1, result discarded)
    //   1: read-and-clear (outputs_0 = macc; macc <= 0, mirrors csrr+csrw 0)
    logic [31:0] macc;

    assign cmd_ready = 1'b1; // single-cycle, always ready

    always_ff @(posedge clk) begin
        if (reset) begin
            macc                  <= 32'd0;
            rsp_valid             <= 1'b0;
            rsp_payload_outputs_0 <= 32'd0;
        end else begin
            rsp_valid <= 1'b0;
            if (cmd_valid && cmd_ready) begin
                unique case (cmd_payload_function_id[2:0])
                    3'd0: begin
                        macc                  <= macc + (cmd_payload_inputs_0 * cmd_payload_inputs_1);
                        rsp_valid             <= 1'b1;
                        rsp_payload_outputs_0 <= 32'd0; // no meaningful result on accumulate
                    end
                    3'd1: begin
                        rsp_payload_outputs_0 <= macc;
                        macc                  <= 32'd0;
                        rsp_valid             <= 1'b1;
                    end
                    default: rsp_valid <= 1'b1; // reserved, ack anyway
                endcase
            end
        end
    end
endmodule

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

python3 litex_sim_tcp_serial.py --cpu-type=vexriscv --cpu-variant=full+cfu \
    --cpu-cfu=cmac_cfu.sv --serial-tcp-port=1234 --non-interactive

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):

static inline void cmac_accumulate(uint32_t a, uint32_t b) {
    register uint32_t ra asm("a0") = a;
    register uint32_t rb asm("a1") = b;
    asm volatile (".insn r 0x2B, 0, 0, x0, %0, %1" :: "r"(ra), "r"(rb));
}

static inline uint32_t cmac_read_and_clear(void) {
    uint32_t result;
    asm volatile (".insn r 0x2B, 1, 0, %0, x0, x0" : "=r"(result));
    return result;
}

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:

1
2
3
python3 tang_nano_9k_cfu.py --cpu-type=vexriscv --cpu-variant=full+cfu \
    --cpu-cfu=cmac_cfu.sv --toolchain=apicula \
    --integrated-main-ram-size=0x2000 --build --load

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.sv written as a single always_ff module named exactly Cfu.
  • 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.