Runs on: the Sparsr VM (
SPARSR_BACKEND=vm). A C kernel is RV32I and cannot run on Sparsr FPGA hardware today — the hardware decodes the older MIPS instruction set. To target hardware, write the kernel in Sparsr Assembly instead.How to get it: everything on this page is in the Sparsr SDK download. You also need a RISC-V bare-metal GCC, which is not shipped with the SDK.
A Sparsr kernel written in C is ordinary C. There is no framework, no runtime, and no special build step.
#include "sparsr_intrinsics.h"
#define ROW_A 1
#define ROW_B 2
#define ROW_RESULT 3
void kernel_main(void) {
_sparsr_wl(SPARSR_W1, ROW_A);
_sparsr_wl(SPARSR_W2, ROW_B);
_sparsr_wxor(SPARSR_W3, SPARSR_W1, SPARSR_W2);
_sparsr_ws(SPARSR_W3, ROW_RESULT);
}
That reads two 4096-bit vectors out of co-processor memory, exclusive-ORs them, and writes the result back. Three wide instructions.
The complete example, with the host program that feeds it, is in the SDK at
examples/03_kernel_in_c/. Build and run it with make run from that directory.
1. Your entry point is a function called kernel_main. Not main. The device sets up
the stack pointer and the return address before it starts your kernel, exactly as any caller
sets them up before a function call, so kernel_main is a normal C function. Returning from
it ends the batch and reports success.
To stop early, or to report a failure the host should see:
_sparsr_exit(1); /* 0 means success, by the usual convention */
2. The wide intrinsics are macros, and their register and row numbers must be constants.
_sparsr_wl(SPARSR_W1, ROW_A) compiles to exactly one instruction. The wide register number
and the CMEM row are fields inside the instruction word, with no register behind them, so
they cannot come from a variable.
_sparsr_wl(SPARSR_W1, 3); /* fine */
int row = get_row();
_sparsr_wl(SPARSR_W1, row); /* compiler error, and it names the rule */
If you need a row chosen at run time, use the register-indirect form _sparsr_wlr /
_sparsr_wsr, which takes the base address in an ordinary register.
3. There is no floating point. The header poisons float and double, so using one is a
compiler error rather than a wrong answer later.
The kernel is compiled by stock RISC-V GCC. Sparsr ships no compiler — it ships three files that GCC needs.
riscv64-unknown-elf-gcc \
-march=rv32i -mabi=ilp32 -O2 \
-ffreestanding -nostdlib -nostartfiles -ffunction-sections -fno-pie -no-pie \
-I<sdk>/include \
-T<sdk>/ldscripts/sparsr.ld \
-Wl,--defsym=__sparsr_loads_every_segment=1 \
<sdk>/startup/sparsr_crt0.S kernel.c -o kernel.spex
On Debian or Ubuntu the toolchain is one command:
sudo apt install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf
Each unusual flag is doing a specific job:
| Flag | Why |
|---|---|
-march=rv32i -mabi=ilp32 |
The device is RV32I. Any other architecture is a compile error. |
-nostdlib -nostartfiles |
Drop the C library and GCC's startup files. sparsr_crt0.S replaces them and is far smaller. |
-ffunction-sections |
Gives kernel_main its own section, so the link script can place it first. |
-T .../sparsr.ld |
The link script. It puts the entry point at address 0 and assigns memory regions. |
-Wl,--defsym=__sparsr_loads_every_segment=1 |
Your promise that you ship the linked file whole. See below. |
The linked ELF is the kernel image. There is no packaging step and no objcopy. Its
program headers already say which bytes go to instruction memory and which to data memory,
which is exactly what the host needs in order to place both.
That is what __sparsr_loads_every_segment is about. If your build ends with
objcopy -O binary -j .text, it takes the code and silently throws your initialised data
away. The link script refuses to produce such an image unless the build declares it ships
everything — so a build that would drop your lookup table fails at link time with a message,
instead of computing the wrong answer.
A kernel has three separate memories, and none of them is the host's RAM.
| Memory | Size | Holds |
|---|---|---|
| IMEM | 1,024 words (4 KiB) | your compiled kernel |
| DMEM | 1,024 words (4 KiB) | your globals, your stack, scalar data |
| CMEM | 32 slots (7,680 bytes) | 4096-bit wide vectors, stored compressed |
Globals work the way you expect. .bss is cleared, and .data and .rodata are carried in
the image and placed for you — so a lookup table, a string literal, or an initialised global
all work.
One surprise worth knowing: a static does not survive between batches. The startup file
clears .bss at the start of every batch, so a zero-initialised global is zero again each
time. The device is deliberately not reset between batches, so anything that must outlive one
belongs in CMEM, a wide register, or a DMEM word the host owns.
If your host writes operands to the bottom of DMEM, move your globals out of the way:
-Wl,--defsym=__bss_origin=0x80000100
SPARSR_W0 to SPARSR_W31, 4096 bits each.Every one compiles to a single instruction. The full list is in sparsr_intrinsics.h; these
are the ones most kernels use.
| Intrinsic | Does |
|---|---|
_sparsr_wl(wd, row) / _sparsr_ws(ws, row) |
Load / store a wide vector at a fixed CMEM row |
_sparsr_wlr(wd, base, disp) / _sparsr_wsr(...) |
The same, with the row computed at run time |
_sparsr_wand, _sparsr_wor, _sparsr_wxor, _sparsr_wnor, _sparsr_wxnor, _sparsr_wandn, _sparsr_wnot |
Bitwise, 4096 bits at a time |
_sparsr_wpopcount(ws) |
Population count into a scalar register |
_sparsr_whamming(a, b) |
popcount(a XOR b) — one instruction |
_sparsr_woverlap(a, b) |
popcount(a AND b) — one instruction |
_sparsr_wany(ws) / _sparsr_wall(ws) |
OR-reduce / AND-reduce to a scalar |
_sparsr_wrnd(wd) |
Fill a wide register with random bits |
_sparsr_exit(status) |
End the batch early |
A C kernel is RV32I, so it needs the Sparsr VM. The default backend runs a different instruction set and would read your image as something else entirely.
SPARSR_BACKEND=vm ./my_host_app
Or run the kernel on its own, with no host program at all:
sparsr-vm run kernel.spex --trace
See Sparsr VM for what that gives you.
A CMEM row holds 128 lanes of 32 bits, stored compressed, and at most 48 of those lanes may be non-zero. That is a property of the compression format, not of your kernel. See Memory Model for the format and what happens if you exceed it.