Sparsr Docs

Writing a Kernel in RISC-V Assembly

Runs on: the Sparsr VM (SPARSR_BACKEND=vm). A kernel in assembly targets the same device as a kernel in C: RV32I plus the wide instructions.

Most kernels are easier in C. Write assembly when you want to control every instruction yourself.

A Sparsr assembly kernel is an RV32I source file with a .S extension. It includes the SDK's sparsr.S, which gives the wide instructions their names. spasm turns the file into a .spex kernel image.

The shortest complete kernel

This is the kernel of the SDK's hello-world example, examples/01_hello_world_c/kernel/hello.S:

.include "sparsr.S"

    wl w9, 1    # Load the wide value in WMEM row 1 into wide register w9
    ws w9, 3    # Store w9 into WMEM row 3
    ret         # End the kernel with status 0

It copies one row of wide memory (WMEM) to another row.

Assemble it:

spasm hello.S      # writes hello.spex into the current directory

What sparsr.S gives you

sparsr.S is in the SDK's include/ directory. Each wide mnemonic in it is a macro. Each macro expands to exactly one .insn directive, so the stock RISC-V assembler encodes the instruction. You do not need a patched assembler.

The macros you use most:

Macro Meaning
wl wd, row Load WMEM row row into wide register wd
ws ws, row Store wide register ws into WMEM row row
wlr wd, base, disp Load WMEM row x[base] + disp into wd
wsr ws, base, disp Store ws into WMEM row x[base] + disp
wand wd, ws1, ws2 wd = ws1 & ws2, across every bit
wor wd, ws1, ws2 wd = ws1 | ws2
wxor wd, ws1, ws2 wd = ws1 ^ ws2
wnot wd, ws1 wd = ~ws1

The file also has wnor, wxnor, wandn, wrnd, wcsa, the wide shifts and rotates, the generic wop, and macros for the copy engine. The Instruction Set page lists every one, with its encoding and what each backend supports.

wl and ws take the row as a constant. wlr and wsr take it from a scalar register plus a displacement, so one kernel can serve whichever rows the host picks. The displacement counts rows, not bytes.

Registers

There are two register files.

  • Scalar registers are the ordinary RV32I ones, x0 to x31, with their usual ABI names (a0, t0, sp and so on). x0 always reads zero.
  • Wide registers are w0 to w31. They are a separate file. w3 and x3 are different registers. w0 is an ordinary, writable register.

A wide register operand is always a constant. It is a 5-bit field in the instruction, and nothing allocates it for you.

The width of a wide register comes from SPARSR_WIDE_BITS. sparsr.S defaults it to 8,192, which is also the hardware's width. The VM reached through the host library uses 4,096. To build for 4,096, pass --defsym SPARSR_WIDE_BITS=4096 to the assembler.

Macro mode and kernel entry

sparsr.S turns on .altmacro and leaves it on. That is how you can write w3 instead of 3. It has two side effects. A ! escapes the character after it. A macro argument that contains spaces or commas must be wrapped in angle brackets. You can turn it off with .noaltmacro, but the wide mnemonics stop working when you do.

The kernel is an ordinary function. The device sets the stack pointer and the return address before it starts the kernel. So ret ends the kernel with status 0. To stop early, put a status in a0, put 93 in a7, and execute ecall.

A worked example

This kernel loads two vectors and writes their AND, OR and XOR to three rows:

.include "sparsr.S"

kernel_main:
    wl   w1, 1          # WMEM row 1 -> w1
    wl   w2, 2          # WMEM row 2 -> w2
    wand w3, w1, w2     # intersection
    wor  w4, w1, w2     # union
    wxor w5, w1, w2     # difference
    ws   w3, 3          # w3 -> WMEM row 3
    ws   w4, 4
    ws   w5, 5
    ret

You can use every RV32I instruction around the wide ones: loops, branches, immediates, loads and stores. The kernel starts at its first instruction.

Assembling

spasm [--target=rv32i] <assembly-file.S>

spasm runs the stock riscv64-unknown-elf-as and ld for you. It uses two files from the SDK: include/sparsr.S and ldscripts/sparsr.ld. It writes <name>.spex into the current directory.

You need the RISC-V binutils on your PATH:

sudo apt install binutils-riscv64-unknown-elf

The Tooling and CLI Reference covers the options, the exit codes and the error messages.

Running it

Your host program loads the .spex image and runs it, exactly as it would a C kernel. The hello-world example does this. Build and run it from the SDK directory:

cd sparsr-sdk-<version>/examples/01_hello_world_c
make run

Its Makefile runs spasm on kernel/hello.S inside build/, then builds the host program. The host program writes WMEM row 1 and runs the kernel. Then it compares row 3 with row 1, and the check passes when they match. Host App Development explains each host call.

To run a kernel with no host program, use sparsr-vm run kernel.spex. Add --trace to see every instruction it retires.

The older MIPS format

spasm used to assemble a MIPS-based format in .spasm files. That format is gone, because no Sparsr device decodes it. spasm now refuses a .spasm file and --target=mips. Rewrite such a kernel as a .S file that includes sparsr.S, or in C.

Built 2026-10-05 19:25 UTC · c0237a4