eZX Spectrum — Assembler

Reference for ezxasm, the eZX-RV32 assembler. For the CPU and the custom extensions see cpu, ezx_xcrisp, ezx_xmath, ezx_xstack, ezx_xlate, ezx_xctx, and ezx_xez. For the hardware platform see spec.


Overview

ezxasm is a standard RV32 assembler with the eZX custom extensions added — Xcrisp (compiler-friendly memory and ALU fusion), Xmath (fused MAC, saturating arithmetic, min/max, distance heuristics, multi-precision carry, plus single-precision FP approximations, BAM trigonometry, 2D/3D vector bundles, animation easing, and quaternion math when F is present), Xstack (BSRAM-backed hardware stack with multi-register push/pop), Xlate (per-register load/store translators), Xctx (hardware context pool), and Xez (Z80-ergonomics with ezflags flag model). Standard F (single-precision floating-point) is also supported with full RV-F instructions and the f0f31 register file. Source files are conventional RISC-V assembly — same directives, same lexical conventions, same operand syntax. Where ezxasm differs from a stock RV32 assembler is in the custom instruction families and in the eZX-specific section layout and reset vector conventions.

Register naming follows the standard RISC-V ABI throughout — for integer: zero, ra, sp, gp, tp, t0t6, s0s11, a0a7, fp (alias for s0). For floating-point: ft0ft11, fs0fs11, fa0fa7. Numeric forms x0x31 and f0f31 are accepted but the ABI names are preferred.

The output is a RISC-V ELF object file compatible with standard linkers; the eZX toolchain ships a configured lld for final linking, but binutils ld works equally well with the appropriate linker script.

Source-level compatibility with earlier eZX drafts

Earlier eZX drafts had ADDC/SUBC/RL/RR/RLC/RRC as native Xez instructions and PUSHM/POPM for multi-register push/pop. The Ant64 EE extension set now covers those patterns (in Xmath G12, Zbb, and Xstack respectively), so the eZX has moved them upstream. ezxasm accepts the old Xez mnemonics and lowers them to the new homes:

Source mnemonic Lowers to Encoding home
ADDC / SUBC ADDC / SUBC (Xmath G12) Xmath
RL / RR ROLC / RORC (Xmath G12) Xmath
RLC / RRC rol / ror (Zbb) standard Zbb
PUSHM / POPM PUSH / POP (Xstack) Xstack
SCF / CCF csrrsi / csrrci on xcarry direct CSR op

Source written for older eZX drafts assembles correctly with no edits. The bytes emitted differ; the semantics match. See ezx_xez §9 for the full table.


Lexical structure

Comments

Two comment styles:

; line comment to end of line
// also a line comment
/* block comment */

Identifiers

[A-Za-z_.$][A-Za-z0-9_.$]* — starts with letter, underscore, period, or dollar; continues with the same characters or digits. Identifiers are case-sensitive.

Numeric literals

Form Example
Decimal 42, -17
Hexadecimal 0x1F, 0xff, $1F (eZX historical form)
Binary 0b1010, %1010 (eZX historical form)
Octal 0o755
Character 'A', '\n', '\t', '\0'

The $ prefix and % prefix are accepted alongside the standard C-style forms because they're traditional in Z80 source and many eZX users will arrive with that habit.

String literals

.asciz "Hello, world\n"
.ascii "no terminator"

Standard C escape sequences: \n, \r, \t, \\, \", \', \0, \xNN.

Whitespace

Whitespace separates tokens. Line breaks end statements. A backslash at the end of a line continues the statement onto the next line.


Source layout

Statements

One statement per line. A statement is either:

  • A label — identifier followed by colon
  • A directive — starts with a period
  • An instruction — mnemonic followed by operands
  • A combination — label followed by an instruction or directive on the same line
my_label:
    addi    a0, a0, 1
    ret

loop:   lb.pi   a1, (a0)        ; label and instruction on one line
        dbnz    a2, loop

Labels

A label is an identifier ending in :. Labels mark addresses in the current section.

Local labels are integers ending in :. They can be referenced by Nf (forward) or Nb (backward):

1:      lb.pi   a1, (a0)
        cmpi    a1, 0
        bz      2f              ; branch forward to 2:
        addi    a3, a3, 1
        j       1b              ; branch backward to 1:
2:      ret

Local labels are useful inside macros and tight loops where named labels would clutter the namespace.

Sections

.text                   ; code section
.data                   ; initialised data
.rodata                 ; read-only data
.bss                    ; uninitialised data
.section .name, "axrw"  ; explicit section with attributes

Section attributes follow the GNU as convention. Common attributes: a (allocated), x (executable), r (readable), w (writable), M (mergeable), S (strings).

Default starting section is .text.

Alignment

.align  n               ; align to 2^n bytes
.balign n               ; align to n bytes (byte alignment)
.p2align n              ; align to 2^n bytes (explicit power-of-two)

.align 4 aligns to a 16-byte boundary. The leading bytes are filled with zeros in data sections and with nop in text sections.


Registers

The standard RISC-V ABI register names are the canonical form:

Name Number Role
zero x0 Hardwired zero
ra x1 Return address
sp x2 Stack pointer
gp x3 Global pointer
tp x4 Thread pointer
t0t2 x5x7 Temporaries (caller-saved)
s0 / fp x8 Saved register / frame pointer
s1 x9 Saved register
a0a7 x10x17 Arguments / return values
s2s11 x18x27 Saved registers
t3t6 x28x31 Temporaries (caller-saved)

Numeric forms x0 through x31 are also accepted but the ABI names are strongly preferred. Mixing forms in the same file works but reads inconsistently.

ezflags is accessed via CSR operations:

csrr    a0, ezflags             ; read ezflags into a0
csrw    ezflags, a0             ; write a0 into ezflags
csrrw   a0, ezflags, a1         ; atomic swap

The assembler recognises ezflags as a CSR name (mapped to CSR address 0x809).


Directives

Data definition

Directive Effect
.byte v[, v...] Emit one or more bytes
.half v[, v...] Emit one or more 16-bit halfwords (little-endian)
.word v[, v...] Emit one or more 32-bit words
.ascii "..." Emit string bytes, no terminator
.asciz "..." Emit string bytes plus null terminator
.string "..." Synonym for .asciz
.skip n[, fill] Reserve n bytes, filled with fill (default 0)
.fill n, size, fill Emit n repetitions of fill at the given size
.org addr[, fill] Pad/move to absolute offset within section

Symbol attributes

Directive Effect
.global sym Mark sym as externally visible
.local sym Mark sym as section-local (default for non-.global)
.extern sym Declare external symbol (informational; not strictly required)
.weak sym Weak binding — overridable by strong definition elsewhere
.equ sym, expr Define sym as a constant expression
.set sym, expr Synonym for .equ, allows redefinition
.type sym, @function Set symbol type (function or object)
.size sym, expr Set symbol size for debug/profiling

Source organisation

Directive Effect
.include "file" Include the named file at this point
.file "name" Set source file name (debug info)
.line n Set source line number (debug info)
.loc f l c DWARF location — file, line, column
.macro name args... Begin macro definition
.endm End macro definition

Conditional assembly

Directive Effect
.if expr Begin conditional block (assembled if expr non-zero)
.elif expr Else-if
.else Else
.endif End conditional
.ifdef sym Conditional on symbol defined
.ifndef sym Conditional on symbol not defined

Operand syntax

Immediates

A bare expression is an immediate. Expressions support +, -, *, /, %, &, |, ^, ~, <<, >>, parentheses, and references to defined symbols and labels.

.equ    SCREEN_BASE, 0x80000000
.equ    SCREEN_WIDTH, 256

addi    t0, zero, SCREEN_WIDTH * 3

Memory operands

Standard RV32 form: imm(rs) where imm is the displacement and rs is the base register.

lw      t0, 0(sp)
sw      a0, -4(s0)
lb      t1, OFFSET_NAME(a2)

For Xcrisp auto-index loads and stores, the displacement is implicit (always the element size in v0.1) and the syntax uses an immediate offset in parentheses on the mnemonic suffix:

lb.pi   a1, (a0)        ; load byte from [a0], a0 += 1
sw.pd   a2, (sp)        ; store word at [sp], sp -= 4

Symbol references with relocations

Form Meaning
sym Address of sym (resolved by linker)
%hi(sym) High 20 bits of sym (for lui)
%lo(sym) Low 12 bits of sym (for addi, lw, etc.)
%pcrel_hi(sym) PC-relative high 20 bits
%pcrel_lo(label) PC-relative low 12 bits (paired by label)

Standard RISC-V relocation modifiers. Most code uses the pseudo-instructions la and call rather than emitting these directly.


Pseudo-instructions

Standard RISC-V pseudo-instructions are recognised:

Pseudo Expands to
nop addi zero, zero, 0
li rd, imm One or two instructions to load imm (addi, or lui + addi)
la rd, sym auipc rd, %pcrel_hi(sym) + addi rd, rd, %pcrel_lo(...)
mv rd, rs addi rd, rs, 0
not rd, rs xori rd, rs, -1
neg rd, rs sub rd, zero, rs (no-flags form — see also Xez NEG)
negw rd, rs not applicable on RV32
seqz rd, rs sltiu rd, rs, 1
snez rd, rs sltu rd, zero, rs
sltz rd, rs slt rd, rs, zero
sgtz rd, rs slt rd, zero, rs
beqz rs, label beq rs, zero, label
bnez rs, label bne rs, zero, label
bgez rs, label bge rs, zero, label
bltz rs, label blt rs, zero, label
blez rs, label bge zero, rs, label
bgtz rs, label blt zero, rs, label
bgt rs, rt, label blt rt, rs, label
ble rs, rt, label bge rt, rs, label
bgtu rs, rt, label bltu rt, rs, label
bleu rs, rt, label bgeu rt, rs, label
j label jal zero, label
jr rs jalr zero, 0(rs)
ret jalr zero, 0(ra)
call sym auipc ra, %pcrel_hi(sym) + jalr ra, %pcrel_lo(...)
tail sym auipc t1, %pcrel_hi(sym) + jalr zero, t1, %pcrel_lo(...)
csrr rd, csr csrrs rd, csr, zero
csrw csr, rs csrrw zero, csr, rs
csrs csr, rs csrrs zero, csr, rs
csrc csr, rs csrrc zero, csr, rs

eZX-specific pseudo-instructions

Pseudo Expands to
ezflags.clear csrw ezflags, zero
ezflags.read rd csrr rd, ezflags
xcarry.clear csrrci x0, xcarry, 1
xcarry.set csrrsi x0, xcarry, 1
xcarry.read rd csrr rd, xcarry (rd[0] = carry bit)
XLATE_RD rd, slot csrrc x0, xlate_rd_n, mask + csrrs x0, xlate_rd_n, value (clear & set the rd field in the appropriate xlate_rd_* CSR)
XLATE_WR rd, slot as above on the xlate_wr_* family
XLATE_OFF_RD rd XLATE_RD rd, IDENT (slot 0)
XLATE_OFF_WR rd XLATE_WR rd, IDENT
wait label Embed a wait-for-event marker (chipset-dependent; future)

The XLATE_RD / XLATE_WR pseudos select the right CSR (xlate_rd_0/1/2/3 based on which group the register falls in) and clear-then-set the 4-bit field for the register inside that CSR. Slot operands accept the mnemonic forms (IDENT, NSWAP8, BREV8, BSWAP16, BSWAP32, HSWAP32, BREV16, BREV32) or numeric 015; using a reserved slot number (5, 7, 8, 11, 12–15) emits a warning at assembly time and the runtime trap fires only if a memory op actually references the register.

Note that the Xcrisp, Xmath, Xstack, Xctx, and Xez instructions are not pseudo-instructions — they have their own opcode encodings in the custom space (see the relevant extension doc). The assembler emits real bytes for them. The pseudos listed above are CSR-manipulation conveniences.


Xez instruction syntax

Xez instructions follow the same general syntactic pattern as standard RV32 — mnemonic, comma-separated operands. A few category-specific notes:

Block operations

bmov.b  rs, rd, rc          ; rs = source, rd = destination, rc = count
bmov.h  rs, rd, rc
bmov.w  rs, rd, rc
bmovr.b rs, rd, rc          ; reverse direction
bcmp.b  rs, rd, rc          ; compare; rc decrements; exits on mismatch
bcmpr.b rs, rd, rc
bfill.b rs, rd, rc          ; fill rd[0..rc] with low byte of rs

Source register comes before destination — the same order as the RV32 ABI for memcpy-style functions, and the same order as Z80 LDIR (HL = source, DE = destination, BC = count).

Auto-index loads and stores

lb.pi   rd, (rs)            ; load byte, post-increment
lbu.pi  rd, (rs)            ; load byte unsigned, post-increment
lh.pi   rd, (rs)            ; load halfword
lhu.pi  rd, (rs)            ; load halfword unsigned
lw.pi   rd, (rs)            ; load word

lb.pd   rd, (rs)            ; load byte, post-decrement
                            ; ... and so on for .pd variants

sb.pi   rs, (rd)            ; store byte, post-increment
sh.pi   rs, (rd)            ; store halfword
sw.pi   rs, (rd)            ; store word
                            ; ... and so on for .pd variants

The displacement is implicit — always the element size — so there is no imm(rs) form. The address register is bare-(rs).

Decrement-and-branch

dbnz    rs, label           ; rs -= 1; branch if not zero
dbz     rs, label           ; rs -= 1; branch if zero

label is a PC-relative branch target with the same range as standard B-type branches (±4KB).

Push/pop multiple

Two equivalent syntaxes — register list (preferred) or explicit hex mask:

pushm   {ra, s0, s1, s2}
popm    {ra, s0, s1, s2}

; equivalent — explicit mask
pushm   0x00040E02

Register-list form accepts:

  • Single registers: ra, s0, a0, etc.
  • Ranges: s0-s3, a0-a3, t0-t6
  • Mixed: {ra, s0-s4, a0}

Order within the braces doesn't matter — the assembler computes the mask, and the hardware pushes/pops in ascending register order regardless of source-text order.

Bit operations on memory

bsetm   3, (a0)             ; set bit 3 of mem[a0]
bclrm   7, (a0)             ; clear bit 7 of mem[a0]
btglm   0, (sp)             ; toggle bit 0 of mem[sp]
btstm   5, (a1)             ; test bit 5 of mem[a1] → ezflags.Z

Bit immediate is 0–7 (byte target only in v1).

Flag-setting ALU operations

addf    rd, rs1, rs2        ; rd = rs1 + rs2; ezflags updated
addfi   rd, rs1, imm12      ; rd = rs1 + imm; ezflags updated
subf    rd, rs1, rs2        ; rd = rs1 - rs2; ezflags updated
addc    rd, rs1, rs2        ; rd = rs1 + rs2 + ezflags.C
subc    rd, rs1, rs2        ; rd = rs1 - rs2 - ezflags.C
andf    rd, rs1, rs2
andfi   rd, rs1, imm12
orf     rd, rs1, rs2
orfi    rd, rs1, imm12
xorf    rd, rs1, rs2
xorfi   rd, rs1, imm12
cmp     rs1, rs2            ; flags from rs1 - rs2, no write
cmpi    rs1, imm12          ; flags from rs1 - imm, no write
negf    rd, rs              ; flag-updating negate

Conditional branches on ezflags

bz      label               ; branch if ezflags.Z = 1
bnz     label               ; branch if ezflags.Z = 0
bc      label               ; branch if ezflags.C = 1
bnc     label
bs      label               ; branch if ezflags.S = 1
bns     label
bv      label               ; branch if ezflags.V = 1
bnv     label
bpe     label               ; branch if ezflags.P = 1
bpo     label

Rotates through carry

rlc     rd, rs              ; rotate left circular; updates Z, C, S
rrc     rd, rs              ; rotate right circular
rl      rd, rs              ; rotate left through carry
rr      rd, rs              ; rotate right through carry
slaf    rd, rs, imm5        ; shift left arithmetic with flags
sraf    rd, rs, imm5        ; shift right arithmetic with flags
srlf    rd, rs, imm5        ; shift right logical with flags

Convenience

neg     rd, rs              ; negate, no flags (use negf for flag form)
cpl     rd, rs              ; one's complement (bitwise NOT)
ex      rd1, rd2            ; swap rd1 and rd2 in one instruction
swapn   rd, rs              ; swap nibbles of low byte
mirror  rd, rs              ; reverse bit order of low byte

Macros

.macro  push_pair r1, r2
        addi    sp, sp, -8
        sw      \r1, 0(sp)
        sw      \r2, 4(sp)
.endm

.macro  pop_pair r1, r2
        lw      \r1, 0(sp)
        lw      \r2, 4(sp)
        addi    sp, sp, 8
.endm

        push_pair a0, a1            ; expands inline
        ...
        pop_pair  a0, a1

Macro arguments are referenced with backslash-prefix: \arg_name. Numeric arguments inside macros are usually local labels — use \@ to get the macro instance number for uniqueness:

.macro  retry_loop n
\@_start:
        addi    a0, a0, 1
        cmpi    a0, \n
        bnz     \@_start
.endm

Listing format

The default listing format is:

Address     Encoding       Source
00001000    fe010113       addi    sp, sp, -32
00001004    00112e23       sw      ra, 28(sp)
00001008    00812c23       sw      s0, 24(sp)
0000100c    02010413       addi    s0, sp, 32
00001010    [TBD encoding] bmov.b  a0, a1, a2

Standard RV32 instructions show their full 32-bit encoding. Xez instructions show the encoded form once the encoding spec is finalised; until then, the listing shows [TBD encoding] and the assembler emits an internal placeholder that will be patched when the encoding is locked.

Listings are produced with the -l flag and written to source.lst by default.


Worked example — copy and bit-count

A small program demonstrating Xez idioms — copy a buffer and count the set bits as we go:

;
; copy_count: copy n bytes from src to dst, returning bits-set count
;
; arguments
;   a0 = src
;   a1 = dst
;   a2 = byte count
;
; returns
;   a0 = count of bits set in the copied data
;
        .text
        .global copy_count
copy_count:
        ; save callee-saved registers we'll use
        pushm   {ra, s0, s1, s2}

        mv      s0, a0              ; src
        mv      s1, a1              ; dst
        mv      s2, a2              ; count
        li      a0, 0               ; bit count accumulator

        beqz    s2, 2f              ; empty buffer? done

1:      lb.pi   t0, (s0)            ; load byte, advance src
        sb.pi   t0, (s1)            ; store byte, advance dst

        ; count bits set in t0
        cpop    t1, t0              ; Zbb population count
        add     a0, a0, t1

        dbnz    s2, 1b              ; decrement count, loop

2:      popm    {ra, s0, s1, s2}
        ret

Notes:

  • pushm / popm collapse the prologue and epilogue to one instruction each
  • lb.pi / sb.pi avoid the explicit address-increment that a stock RV32 version would need after each load and store
  • dbnz is the entire loop control — decrement and branch in one instruction
  • cpop is the standard Zbb popcount — already at our disposal because the eZX-RV32 includes Zbb
  • The Z80 lineage is in the shape — load, store, count, decrement-and-loop — and the modern foundations are in the rest

Toolchain

ezxasm is one piece of the eZX toolchain:

  • ezxasm — assembler (this document)
  • ezxld — linker (LLD configured for eZX)
  • ezxcc — C/C++ compiler (Clang configured for eZX-RV32 with Xez intrinsics)
  • ezxdbg — debugger frontend (talks to DeMon's host debug protocol)

Standard binutils (riscv32-unknown-elf-as, riscv32-unknown-elf-ld) can also assemble and link eZX code; the binutils path lacks Xez instruction support without a patch but produces correct output for any RV32IMC + Zbb + Zbs code that doesn't use Xez. The eZX-specific tools add Xez awareness and ship with the appropriate linker scripts.


Differences from stock GAS

For developers familiar with GNU as for RISC-V, the user-facing differences are small:

  • Xez mnemonics are recognised (see Xez instruction syntax above)
  • $ and % numeric prefixes are accepted alongside 0x and 0b
  • csrr/csrw recognise ezflags as a CSR name
  • The default linker script targets the eZX memory map (text at $0001_0000 in DDR3 by default; reset vector and DeMon-shared regions reserved)
  • Listing output annotates Xez instructions appropriately

ezxasm aims for source-level compatibility with GAS for all standard RV32 constructs. A .S source file written for GNU as should assemble unchanged under ezxasm, with the caveat that any inline Xez instructions need the corresponding GAS patch on the binutils side.


Status and open questions

  • Xez encoding placeholders — until the encoding spec lands, the assembler emits internal placeholders for Xez opcodes. Object files produced now will need re-assembling once the encoding is finalised.
  • Compressed Xez forms — when added to the ISA, they will be selected automatically by the assembler the same way the C extension is handled today.
  • Inline relocations for chipset registers — symbol forms like %chipreg(name) for chipset register addresses may be added to make register access more readable; not yet defined.
  • Debug info — DWARF emission is on the roadmap; the .file / .line / .loc directives are accepted but produce limited output in v1.
  • Macro library — a standard eZX.inc library of macros (string ops, screen handling, common chipset patterns) is anticipated but not yet specified.

Important: The eZX Spectrum board is at early design/prototype stage, everything you see here is subject to change.