Skip to content

Toolchain — Writing & Running Programs

scripts/riscv_asm.py is a standalone RV32IM assembler: write real assembly (labels, pseudo-ops, standard mnemonics), assemble it, and run it through the actual sim — no hand-computed hex.


Why this exists

Every .asm test file in tb/ used to be hand-encoded pipe format: <asm text> | <hex> | <dest> | <check>, where a human computed the hex by hand and wrote the mnemonic next to it as a comment. That's exactly the kind of thing that hides bugs no one is looking for. Two turned up in existing files during this work, both silent because nothing exercised them:

  • tb/cpu_peak_tests.asm's INT16 NPU block has a hex value whose comment claims data=100, but the bits actually encode 25 — harmless only because that block has no register-check assertion to catch it.
  • tb/test_rv32m_control_flow_regression.asm's mul s3, s1, s2 line had funct7=0x00 instead of 0x01, so the core silently ran it as ADD (s3=9) instead of MUL (s3=20) — invisible until the file was actually run (it also had no halt sentinel and hung forever).

The assembler doesn't let either class of mistake happen: it computes the encoding from the mnemonic and operands, so there's no separate "hex" for the two to disagree on.


Quick start

python3 scripts/riscv_asm.py program.s -o tb/program.asm
TEST_TARGET=tb/program.asm ASM_TEST=program.asm source scripts/run

(scripts/run reads the ASM_TEST env var as a filename under tb/; TEST_TARGET isn't actually read by the current script but is harmless to set — just use ASM_TEST.)

Program output (the full CPU register file, since there's no console/UART peripheral yet — see todo.md) is printed unconditionally after halt under [OUTPUT] Final register file (x1-x31):, regardless of whether you wrote any # check: annotations. See tb/branch_regression.s for a worked example.

Add --list to also print an instruction-count summary to stderr, and omit -o to print the assembled pipe-format text to stdout instead of writing a file.


Source syntax

# comment
loop:                       # labels: any line "name:" — may share a line with an instruction
    addi s1, x0, 5           # check: s1==5   <- register-check annotation, passed through
    mul  s3, s1, s2
    bne  s1, x0, loop
    halt                     # pseudo-op for jal x0, 0 (the HALT_WORD sentinel)
  • Registers: x0x31 or ABI names (zero, ra, sp, gp, tp, t0-2, s0/fp, s1, a0-7, s2-11, t3-6).
  • # check: REG==VAL is the only comment form the assembler treats specially — it's carried into the pipe-format 4th field the testbench checks after halt (VAL may be decimal or 0x... hex). Any other #... is a plain, discarded comment.
  • Only CPU-destined instructions consume program-counter/imem address space when resolving labels — matches rtl/noc_router.sv, which diverts NPU (0x6B) and GPU (0x57) opcodes to their own chiplet before they ever reach the CPU's instruction memory.

Base RV32I/M instructions

All standard mnemonics: lui auipc jal jalr beq bne blt bge bltu bgeu lb lh lw lbu lhu sb sh sw addi slti sltiu xori ori andi slli srli srai add sub sll slt sltu xor srl sra or and mul mulh div divu rem remu.

mulhsu/mulhu are rejected with an explicit error (the core doesn't implement them yet — see reference.md#known-isa-gaps) rather than silently assembling into something that computes the wrong result.

Pseudo-instructions

Pseudo-op Expands to
nop addi x0, x0, 0
halt jal x0, 0 (the HALT_WORD sentinel)
li rd, imm addi if it fits in 12 signed bits, else lui+addi
mv rd, rs addi rd, rs, 0
not/neg/seqz/snez/sltz/sgtz rd, rs standard RISC-V expansions
j/jal label, jr/jalr rs, ret, call label standard RISC-V expansions
beqz/bnez/blez/bgez/bltz/bgtz rs, label branch-vs-x0 expansions

NPU / GPU pseudo-instructions

These don't advance the CPU program counter (see above) and are encoded bit-exact against rtl/npu.sv / rtl/gpu.sv's actual decode:

npu.set_prec int8            # or int16
npu.load_w   row, col, data
npu.load_a   col, data
npu.swap
npu.compute
npu.compute_acc

gpu.load_a lane, data
gpu.load_b lane, data
gpu.compute add              # add | sub | and | xor
gpu.nop

See reference.md and gpu.md for the underlying bit layouts.


Regenerating an existing hand-encoded test

If you're fixing up an old hand-encoded .asm file, write the equivalent .s source next to it and regenerate — don't hand-patch the hex:

python3 scripts/riscv_asm.py tb/my_test.s -o tb/my_test.asm

tb/branch_regression.s and tb/test_rv32m_control_flow_regression.s are worked examples of this — the latter is the regenerated, bug-fixed version of a file that used to hang.