ISA Extensions, Custom Instructions & the On-Chip Bus

Rodolfo Azevedo

Institute of Computing, University of Campinas (UNICAMP), Brazil

rodolfo.azevedo@unicamp.br

http://www.ic.unicamp.br/~rodolfo/mo801

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Goal of this class

Module 3, Class 2: extending the core with standard and custom instructions, and the SystemVerilog implementation of the bus.

RISC-V was designed to be extended. Today we add standard instructions (Zmmul), define a custom one (CMAC), and build the bus that connects the CPU to the rest of the system.

At the end of this class, you should be able to:

  • Explain how RISC-V extensions are composed (Zmmul, Zicsr, custom opcodes) and why this matters for embedded systems.
  • Decode a custom instruction opcode and add its handling to the processor control FSM.
  • Implement a simple on-chip bus with an address decoder that routes transactions to the correct peripheral.
  • Connect multiple peripherals to the bus and verify correct address decoding in simulation.
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

RISC-V is modular by design

RV32I has ~47 instructions — intentionally minimal.

Capability is added through extensions, not accumulated into a monolithic ISA:

  • x86: ~1,500 instructions accumulated over 45 years. Every core implements (almost) everything.
  • ARM: fixed profiles (Cortex-M vs Cortex-A). You pick a profile, not individual features.
  • RISC-V: you pick exactly what you need. A soft core on an FPGA and a Linux server share the same base ISA; only the extensions differ.

The name says it all: RV32I_Zicsr_Zicntr_Zmmul — every capability is explicit.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

The extension landscape

Category Examples Purpose
Standard letters I (base), M (mul/div), A (atomic), F (float32), D (float64), C (compressed), V (vector) Core building blocks
G (shorthand) G = IMAFD + Zicsr + Zifencei "General purpose" — typical for Linux-capable cores
Z (fine-grained) Zmmul, Zicsr, Zicntr, Zifencei Standard subsets and specific mechanisms
Zb* (bit manipulation) Zba, Zbb, Zbc, Zbs Address generation, bit ops, carry-less multiply
Zk* (cryptography) Zkne, Zknd, Zknh AES encrypt/decrypt, SHA-2
Zve* (vector subsets) Zve32x, Zve32f, Zve64d Vector for embedded (without full V)
S (supervisor) Sstc, Svnapot Timer comparison, page tables
H (hypervisor) H Hardware virtualization
X (vendor/custom) Xcvmac (OpenHW), XTheadVector (T-Head) Proprietary extensions
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Real-world configurations

Core / Chip ISA Config Segment
This course (Tang Nano 9K) RV32I + Zicsr + Zicntr + Zmmul + CMAC Didactic FPGA soft core
ESP32-C3 RV32IMC IoT / Wi-Fi microcontroller
SiFive E31 RV32IMAC Embedded real-time
SiFive U74 (Milk-V Mars) RV64GC Linux-capable application processor
Kendryte K230 RV64GCV AI / vision with vector extension
XuanTie C910 (T-Head/Alibaba) RV64GCV + XTheadVector Server / AI with vendor extensions

The same base ISA scales from a blinking LED on your FPGA to a Linux server — the difference is the extensions.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Our extensions in Project 2

We add three standard extensions and one custom instruction:

Extension Instructions Purpose
Zicsr CSRRW, CSRRS, CSRRC + imm variants (6 total) Read/write Control and Status Registers
Zicntr — (defines CSRs: cycle, instret, time) Hardware performance counters
Zmmul MUL, MULH, MULHU, MULHSU (4 total) Integer multiplication (no division)
CMAC (custom) CMAC rs1, rs2 (opcode custom-0) Multiply-accumulate with CSR accumulator

Final core: RV32I + Zicsr + Zicntr + Zmmul + CMAC

GCC flag: -march=rv32i_zicsr_zmmul (CMAC via inline assembly)

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Zicsr: CSR instructions

Six instructions that atomically read-modify-write a CSR:

Instruction Operation
CSRRW rd, csr, rs1 rd = csr; csr = rs1 (read-write)
CSRRS rd, csr, rs1 `rd = csr; csr
CSRRC rd, csr, rs1 rd = csr; csr &= ~rs1 (read-clear bits)
CSRRWI rd, csr, imm Same as above, with 5-bit immediate
CSRRSI rd, csr, imm
CSRRCI rd, csr, imm

Common pseudoinstructions: csrr rd, csr (read), csrw csr, rs1 (write), rdcycle rd, rdinstret rd.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Zicntr: hardware counters

Three 64-bit counters accessible as read-only CSRs:

CSR Address Purpose
cycle / cycleh 0xC00 / 0xC80 Clock cycles since reset
instret / instreth 0xC02 / 0xC82 Instructions retired since reset
time / timeh 0xC01 / 0xC81 Wall-clock time

For profiling (Project 3):

uint32_t t0, t1;
asm volatile("rdcycle %0" : "=r"(t0));
kws_infer(input);
asm volatile("rdcycle %0" : "=r"(t1));
uint32_t cycles = t1 - t0;

Comparing cycle vs instret gives the CPI (cycles per instruction) of your multicycle core — a key architectural metric.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Zmmul: multiply without divide

The full M extension has 8 instructions (MUL + DIV + REM variants). Zmmul is the multiply-only subset — ratified in 2022, designed for microcontrollers and FPGA soft cores.

Instruction funct3 Result
MUL 000 Low 32 bits of rs1 × rs2
MULH 001 High 32 bits (signed × signed)
MULHSU 010 High 32 bits (signed × unsigned)
MULHU 011 High 32 bits (unsigned × unsigned)

All share the same encoding: R-type, opcode = 0110011, funct7 = 0000001. The multiplier hardware is the same — the difference is which 32 bits of the 64-bit product to return, and sign handling.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Zmmul: datapath changes

logic [63:0] mul_result_full;

always_comb begin
    case (funct3)
        3'b000: mul_result_full = $signed(rs1_data) * $signed(rs2_data);       // MUL
        3'b001: mul_result_full = $signed(rs1_data) * $signed(rs2_data);       // MULH
        3'b010: mul_result_full = $signed(rs1_data) * $unsigned(rs2_data);     // MULHSU
        3'b011: mul_result_full = $unsigned(rs1_data) * $unsigned(rs2_data);   // MULHU
        default: mul_result_full = '0;
    endcase
end

assign mul_result = (funct3 == 3'b000) ? mul_result_full[31:0]    // MUL: low
                                       : mul_result_full[63:32];  // MULH*: high

The Gowin GW1NR-9C has DSP blocks — the synthesis tool infers them automatically for * operations.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Custom instructions: the custom-0 to custom-3 opcodes

RISC-V reserves four opcodes for implementation-defined instructions:

Opcode Encoding (bits [6:0])
custom-0 0001011
custom-1 0101011
custom-2 1011011
custom-3 1111011

These are your design space — any instruction you define here is valid RISC-V. The encoding of the remaining bits (funct3, funct7, rs1, rs2, rd) is up to you.

This is distinct from standard extensions (Zmmul, etc.) where the encoding is fixed by the spec.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

CMAC: our custom multiply-accumulate

Problem: the KWS inner loop is acc += a[i] * b[i]. With standard MUL, each iteration takes: lw + lw + mul + add + sw (5 instructions, ~15 cycles).

Solution: a custom CMAC instruction with an implicit accumulator in a CSR.

Operation: macc_csr += rs1 * rs2 (accumulate into a dedicated 32-bit CSR)

The inner loop becomes: lw + lw + cmac (3 instructions, ~9 cycles). Read the result with csrr rd, macc; clear with csrw macc, zero.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

CMAC: using it from C

Since CMAC is custom, GCC does not know about it. Two options:

Inline assembly (simplest)

#define CSR_MACC 0x800   // custom CSR address (machine read/write range)

#define cmac(a, b)  asm volatile(".insn r 0x0B, 0, 0, x0, %0, %1" :: "r"(a), "r"(b))
#define macc_read(dst) asm volatile("csrr %0, 0x800" : "=r"(dst))
#define macc_clear()   asm volatile("csrw 0x800, zero")

int32_t dot_product(const int8_t *a, const int8_t *b, int n) {
    macc_clear();
    for (int i = 0; i < n; i++)
        cmac((int32_t)a[i], (int32_t)b[i]);
    int32_t result;
    macc_read(result);
    return result;
}

GCC intrinsic / builtin (production approach)

Requires modifying the compiler backend — not in scope for this course, but worth knowing it exists: vendor extensions like Xcvmac (OpenHW CORE-V) do exactly this.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Trade-off: ISA extension vs. custom instruction vs. memory-mapped

Zmmul (standard ext.) CMAC (custom instr.) Memory-mapped accel.
Software mul rd, rs1, rs2 .insn + inline asm sw / lw to registers
Overhead Zero (1–3 cycles) Zero (1–3 cycles) 2–4 bus transactions
Toolchain -march=rv32i_zmmul Inline asm or intrinsic Standard C (volatile ptrs)
ISA change Decode + datapath Decode + datapath + CSR None
Scalability 2 regs in, 1 out 2 regs in, implicit acc Large buffers, DMA
Best for General multiply Tight accumulation loops Bulk operations

MUL/CMAC are ideal for the scalar inner loop. The Project 3 accelerator is memory-mapped because it processes entire tiles — far more data than registers can hold.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

A note on FENCE

FENCE is part of the RV32I base. On our multicycle, single-hart, no-cache core, it is a legitimate no-op: memory accesses are already naturally ordered. The correct implementation is to decode it and advance to the next instruction.

Not every base instruction requires hardware — sometimes the microarchitecture already guarantees the semantics.

(Similarly, FENCE.I was moved out of the base ISA into Zifencei precisely because it is expensive on cores with separate instruction/data caches — ours doesn't have that problem.)

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

The on-chip bus in SystemVerilog

Using the struct packed pattern from M01A04:

typedef struct packed {
    logic [31:0] addr;
    logic [31:0] wdata;
    logic [3:0]  sel;      // byte enables
    logic        we;
    logic        re;
    logic        valid;
} bus_req_t;

typedef struct packed {
    logic [31:0] rdata;
    logic        ready;
    logic        error;
} bus_resp_t;

The CPU drives one bus_req_t; each peripheral produces one bus_resp_t. The bus decoder selects which response feeds back to the CPU.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Bus top-level wiring

module bus_top (
    input  logic    clk, rst,
    input  bus_req_t  cpu_req,
    output bus_resp_t cpu_resp
);
    bus_resp_t uart_resp, timer_resp, gpio_resp, dmem_resp;

    // Address decoder
    logic sel_uart, sel_timer, sel_gpio, sel_dmem;
    bus_decoder dec (.addr(cpu_req.addr),
                     .sel_uart(sel_uart), .sel_timer(sel_timer),
                     .sel_gpio(sel_gpio), .sel_dmem(sel_dmem));

    // Peripherals
    uart_periph  uart  (.clk(clk), .rst(rst), .req(cpu_req),
                        .en(sel_uart),  .resp(uart_resp));
    timer_periph timer (.clk(clk), .rst(rst), .req(cpu_req),
                        .en(sel_timer), .resp(timer_resp));
    gpio_periph  gpio  (.clk(clk), .rst(rst), .req(cpu_req),
                        .en(sel_gpio),  .resp(gpio_resp));
    dmem_periph  dmem  (.clk(clk), .rst(rst), .req(cpu_req),
                        .en(sel_dmem),  .resp(dmem_resp));

    // Response mux: select the enabled peripheral's response
    always_comb begin
        cpu_resp = dmem_resp;   // default
        if (sel_uart)  cpu_resp = uart_resp;
        if (sel_timer) cpu_resp = timer_resp;
        if (sel_gpio)  cpu_resp = gpio_resp;
    end
endmodule
MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Memory map — the Project 2 definition

Base address Size Peripheral
0x0000_0000 4 KB Instruction memory (BRAM)
0x0001_0000 4 KB Data memory (BRAM)
0x0002_0000 256 B UART (TX data, RX data, status, control)
0x0002_0100 256 B Timer (count low, count high, compare, control)
0x0002_0200 256 B GPIO (output, input, direction)
0x0002_0300 256 B (reserved for Project 3 accelerator)

The accelerator slot is reserved now so Project 2's bus and Project 3's accelerator connect without redesigning the memory map.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0

Next class

Peripheral Design: UART transmitter and receiver, timer/cycle counter, GPIO — and the C driver patterns (volatile pointers) that software uses to talk to them.

MO801/MC972 · Topics in Computer Architecture and Hardware · Rodolfo Azevedo · CC BY-SA 4.0