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,
x0tox31, with their usual ABI names (a0,t0,spand so on).x0always reads zero. - Wide registers are
w0tow31. They are a separate file.w3andx3are different registers.w0is 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.