The ZX16 RISC ISA is a 16-bit reduced-instruction-set architecture designed for simplicity, efficiency, and educational use. It features:
- 16-bit fixed-width instructions and data
- 8 general-purpose registers (x0–x7) plus a 16-bit PC
- 64 KB flat address space for code, data, MMIO, and interrupt vectors
- 8 instruction formats selected by bits [2:0]
- 50+ real & pseudo-instructions covering ALU, branches, loads/stores, jumps, upper-immediates, and syscalls
- Smart immediates: large constants are synthesized via pseudo-ops; per-instruction immediates use their field's natural width (I-type arithmetic sign-extends, LUI/AUIPC fill the upper bits)
- PC-relative control flow: all branches, J and JAL use PC-relative offsets
- Memory-mapped I/O at 0xF000–0xFFFF
- 16 interrupt vectors at 0x0000–0x001F (2 bytes each; reset at 0x0000)
- Compact 16-bit instructions
- Two-operand ALU (rd/rs1 is both destination & first source)
- Smart assembler handles large constants via pseudo-ops
- ECALL syscall interface for basic console I/O and program halt
- Little-endian byte order
- Aligned memory access required for word operations
- Byte-addressable memory
| Register | ABI | Role | Notes |
|---|---|---|---|
| x0 | t0 | Temporary | Caller-saved scratch/Assembler Temporary |
| x1 | ra | Return address | Used by JAL/JALR |
| x2 | sp | Stack pointer | Initialized to 0xF000 |
| x3 | s0 | Saved / Frame pointer | Callee-saved |
| x4 | s1 | Saved | Callee-saved |
| x5 | t1 | Temporary | Caller-saved scratch |
| x6 | a0 | Arg0 / Return value | |
| x7 | a1 | Arg1 | Further args spill to stack |
- PC: 16-bit program counter
- PC: Initialized to 0x0000 on reset
- x2 (sp): Initialized to 0xF000 on reset
- x0–x1, x3–x7: Undefined values on reset (not initialized)
On the SP reset value: SP is initialized to 0xF000, the MMIO base — one word past the top of usable RAM (0xEFFF). The stack is full-descending with pre-decrement:
PUSHdoesADDI sp, -2thenSW rd, 0(sp), so the first push writes to 0xEFFE–0xEFFF, the top word of RAM. SP is never dereferenced while it holds 0xF000, so no MMIO access occurs from the initial value.
Note for RISC-V users: In ZX16,
x0is a normal general-purpose register (ABI namet0). It is not a hardwired-zero register. Writes tox0take effect, and reads return whatever was last written. Idioms such asCLR rd(which expands toXOR rd, rd) andNOP(ADD x0, x0) rely on this.
| Range | Usage |
|---|---|
| 0x0000–0x001F | Interrupt vector table (16 entries × 2 bytes) |
| 0x0020–0xEFFF | RAM & ROM |
| 0xF000–0xFFFF | MMIO (I/O registers start at 0xF000) |
- Endianness: Little-endian
- Addressing: Byte-addressable
- Alignment: Word accesses (SW/LW) must be aligned to even addresses
- Stack: Grows downward; SP initialized to 0xF000, first push lands at 0xEFFE
- 16 fixed entries at 0x0000–0x001E (2 bytes each); each entry is a
Jinstruction to the handler (jump table). - Vector 0 = reset; vector 1 =
EBREAK(software breakpoint); vectors 2–15 = hardware IRQs (assigned by the SoC). - Trap mechanism (
EPC+IEregisters): a hardware IRQ is taken at an instruction boundary whileIE=1;EBREAKalways traps. Entry doesEPC←PC,IE←0,PC←i*2; return is viaRETI(PC←EPC,IE←1).STEPsingle-steps the debuggee one instruction. Full spec and theEBREAK/RETI/EI/DI/MFEPC/MTEPC/STEPinstructions: docs/INTERRUPTS.md.
Every instruction is 16 bits, with bits [2:0] as primary opcode:
| opcode ([2:0]) | Format |
|---|---|
| 000 | R-Type |
| 001 | I-Type |
| 010 | B-Type |
| 011 | S-Type |
| 100 | L-Type |
| 101 | J-Type |
| 110 | U-Type |
| 111 | SYS-Type |
All instructions are 16 bits; bits [2:0] select the format/opcode.
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ funct4 │ rs2 │ rd/rs1 │ func3 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:12] funct4
- [11:9] rs2
- [8:6] rd/rs1 (two‐operand: dest & first source)
- [5:3] func3
- [2:0] 000
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ imm7 (signed) │ rd/rs1 │ func3 │ 0 │ 0 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:9] imm7 (7-bit signed immediate, sign-extended)
- [8:6] rd/rs1
- [5:3] func3
- [2:0] 001
Note: For shift instructions: shift_amt = imm7[3:0]; imm7[6:4] selects the shift type.
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ imm[4:1] │ rs2 │ rs1 │ func3 │ 0 │ 1 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:12] imm[4:1] (high 4 bits of 5-bit signed offset, imm[0] = 0) -- Range: -16 to 14.
- [11:9] rs2 (ignored for BZ/BNZ)
- [8:6] rs1
- [5:3] func3
- [2:0] 010
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ imm[3:0] │ rs2 │ rs1 │ func3 │ 0 │ 1 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:12] imm[3:0] (4-bit signed store offset) -- Range: -8 to +7
- [11:9] rs2 (data register)
- [8:6] rs1 (base register)
- [5:3] func3
- [2:0] 011
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ imm[3:0] │ rs2 │ rd │ func3 │ 1 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:12] imm[3:0] (4-bit signed load offset) -- Range: -8 to +7
- [11:9] rs2 (base register)
- [8:6] rd (destination register)
- [5:3] func3
- [2:0] 100
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ L │ imm[9:4] │ rd │ imm[3:1] │ 1 │ 0 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15] link flag (0 = J, 1 = JAL)
- [14:9] imm[9:4] (with imm[3:1] and imm[0]=0 the field spans offset bits [9:1], sign bit = bit 9) -- Range: -512 to +510
- [8:6] rd (link register for JAL)
- [5:3] imm[3:1] (low 3 bits of offset)
- [2:0] 101
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ F │ imm[15:10] │ rd │ imm[9:7] │ 1 │ 1 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15] flag (0 = LUI, 1 = AUIPC)
- [14:9] imm[15:10] (high 6 bits of immediate)
- [8:6] rd
- [5:3] imm[9:7] (mid 3 bits of immediate)
- [2:0] 110
Encoding note: The assembler takes a 9-bit operand v (0–511) and the resulting
register value is v << 7. The nine bits of v are scattered into the instruction
as the immediate field shown above; equivalently, the immediate occupies result bits
[15:7]. So LUI rd, v sets rd = v << 7, and AUIPC rd, v sets rd = PC + (v << 7).
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ svc[9:0] │ 0 │ 0 │ 0 │ 1 │ 1 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
- [15:6] svc (10-bit system-call number)
- [5:3] 000 (reserved, must be zero)
- [2:0] 111
Note: only ECALL uses this format; bits [5:3] are reserved and currently always
zero. The full opcode that identifies SYS-Type is thus the 6-bit pattern 000111.
| Mnemonic | Description |
|---|---|
| ADD | rd ← rd + rs2 |
| SUB | rd ← rd – rs2 |
| SLT | rd ← (rd < rs2) ? 1 : 0 |
| SLTU | rd ← (unsigned rd < unsigned rs2) ? 1 : 0 |
| SLL | rd ← rd << (rs2 & 0xF) |
| SRL | rd ← rd >> (rs2 & 0xF) (logical) |
| SRA | rd ← rd >> (rs2 & 0xF) (arithmetic) |
| OR | rd ← rd ∣ rs2 |
| AND | rd ← rd ∧ rs2 |
| XOR | rd ← rd ⊕ rs2 |
| MV | rd ← rs2 |
| JR | PC ← rd |
| JALR | rd ← PC + 2; PC ← rs2 |
| Mnemonic | Description |
|---|---|
| ADDI | rd ← rd + sext(imm7) |
| SLTI | rd ← (rd < sext(imm7)) ? 1 : 0 |
| SLTUI | rd ← (unsigned rd < unsigned sext(imm7)) ? 1 : 0 |
| SLLI | rd ← rd << imm[3:0] |
| SRLI | rd ← rd >> imm[3:0] (logical) |
| SRAI | rd ← rd >> imm[3:0] (arithmetic) |
| ORI | rd ← rd ∣ (imm7 & 0x7F) — low 7 bits, no sign-extension |
| ANDI | rd ← rd ∧ sext(imm7) |
| XORI | rd ← rd ⊕ sext(imm7) |
| LI | rd ← sext(imm7) |
| Mnemonic | Description |
|---|---|
| BEQ | PC ← PC + offset if x[rs1] == x[rs2] |
| BNE | PC ← PC + offset if x[rs1] != x[rs2] |
| BZ | PC ← PC + offset if x[rs1] == 0 |
| BNZ | PC ← PC + offset if x[rs1] != 0 |
| BLT | PC ← PC + offset if x[rs1] < x[rs2] |
| BGE | PC ← PC + offset if x[rs1] ≥ x[rs2] |
| BLTU | PC ← PC + offset if unsigned x[rs1] < unsigned x[rs2] |
| BGEU | PC ← PC + offset if unsigned x[rs1] ≥ unsigned x[rs2] |
| Mnemonic | Description |
|---|---|
| SB | Mem[x[rs1] + sext(imm4)] ← least-significant byte of x[rs2] |
| SW | Mem[x[rs1] + sext(imm4)] ← x[rs2] (16 bits) |
| Mnemonic | Description |
|---|---|
| LB | rd ← sign-extended byte at Mem[x[rs2] + sext(imm4)] |
| LW | rd ← word at Mem[x[rs2] + sext(imm4)] (16 bits) |
| LBU | rd ← zero-extended byte at Mem[x[rs2] + sext(imm4)] |
| Mnemonic | Description |
|---|---|
| J | PC ← PC + offset |
| JAL | x[rd] ← PC + 2; PC ← PC + offset |
| Mnemonic | Description |
|---|---|
| LUI | rd ← (imm[15:7] << 7) |
| AUIPC | rd ← PC + (imm[15:7] << 7) |
SYS uses func3 (bits [5:3]) to select a sub-function (see docs/INTERRUPTS.md):
| Mnemonic | func3 | Description |
|---|---|---|
| ECALL | 000 |
Trap to service number in bits [15:6] |
| EBREAK | 001 |
Software breakpoint → trap to vector 1 |
| RETI | 010 |
Return from interrupt/trap (PC←EPC; IE←1) |
| EI | 011 |
Enable interrupts (IE←1) |
| DI | 100 |
Disable interrupts (IE←0) |
| MFEPC | 101 |
rd ← EPC (rd in [8:6]) |
| MTEPC | 110 |
EPC ← rd (rd in [8:6]) |
| STEP | 111 |
single-step: run one instruction after the next RETI, then trap |
ECALL traps to the service number encoded in bits [15:6]. Arguments are passed in
a0 (x6); results, where applicable, return in a0. The reference simulator implements:
| Service | Name | Input | Effect |
|---|---|---|---|
0x000 |
print_int | a0 = value | Print a0 as a signed decimal integer |
0x001 |
print_char | a0 = char | Print the low byte of a0 as an ASCII character |
0x3FF |
halt | — | Stop execution |
Unknown service numbers are reserved; the simulator treats them as no-ops (it does not trap). The remaining svc space is available for implementation-defined services.
This table shows, for each instruction, the key fields used to distinguish it: the primary opcode (bits [2:0]), plus any funct4, func3, link or flag bits, or immediate‐pattern conditions.
| Mnemonic | Format | opcode [2:0] | funct4 [15:12] | func3 [5:3] | link/flag (bit15) | imm‐pattern/notes |
|---|---|---|---|---|---|---|
| R-Type | ||||||
| ADD | R | 000 |
0000 |
000 |
— | two‐operand |
| SUB | R | 000 |
0001 |
000 |
— | |
| SLT | R | 000 |
0010 |
001 |
— | |
| SLTU | R | 000 |
0011 |
010 |
— | |
| SLL | R | 000 |
0100 |
011 |
— | logical left shift |
| SRL | R | 000 |
0101 |
011 |
— | logical right shift |
| SRA | R | 000 |
0110 |
011 |
— | arithmetic right shift |
| OR | R | 000 |
0111 |
100 |
— | |
| AND | R | 000 |
1000 |
101 |
— | |
| XOR | R | 000 |
1001 |
110 |
— | |
| MV | R | 000 |
1010 |
111 |
— | move |
| JR | R | 000 |
1011 |
000 |
— | PC ← rd |
| JALR | R | 000 |
1100 |
000 |
— | link in rd, then PC ← rs2 |
| I-Type | ||||||
| ADDI | I | 001 |
— | 000 |
— | imm7 signed |
| SLTI | I | 001 |
— | 001 |
— | imm7 signed |
| SLTUI | I | 001 |
— | 010 |
— | imm7 unsigned compare |
| SLLI | I | 001 |
— | 011 |
— | shift left logical, imm7[6:4]=001 |
| SRLI | I | 001 |
— | 011 |
— | shift right logical, imm7[6:4]=010 |
| SRAI | I | 001 |
— | 011 |
— | shift right arithmetic, imm7[6:4]=100 |
| ORI | I | 001 |
— | 100 |
— | |
| ANDI | I | 001 |
— | 101 |
— | |
| XORI | I | 001 |
— | 110 |
— | |
| LI | I | 001 |
— | 111 |
— | real when imm ∈ [-64,63]; otherwise assembles as LI16 (LUI+ORI) |
| B-Type | ||||||
| BEQ | B | 010 |
— | 000 |
— | offset = sext(imm[4:1]∥0) |
| BNE | B | 010 |
— | 001 |
— | |
| BZ | B | 010 |
— | 010 |
— | ignore rs2 |
| BNZ | B | 010 |
— | 011 |
— | ignore rs2 |
| BLT | B | 010 |
— | 100 |
— | |
| BGE | B | 010 |
— | 101 |
— | |
| BLTU | B | 010 |
— | 110 |
— | unsigned compare |
| BGEU | B | 010 |
— | 111 |
— | unsigned compare |
| S-Type | ||||||
| SB | S | 011 |
— | 000 |
— | store byte, offset=sext(imm[3:0]) |
| SW | S | 011 |
— | 001 |
— | store word |
| L-Type | ||||||
| LB | L | 100 |
— | 000 |
— | load byte |
| LW | L | 100 |
— | 001 |
— | load word |
| LBU | L | 100 |
— | 100 |
— | load byte unsigned |
| J-Type | ||||||
| J | J | 101 |
— | — | link=0 | offset = sext({imm[9:4],imm[3:1],0}) |
| JAL | J | 101 |
— | — | link=1 | link in rd, then PC-relative |
| U-Type | ||||||
| LUI | U | 110 |
— | — | flag=0 | imm from bits [15:7]<<7 |
| AUIPC | U | 110 |
— | — | flag=1 | PC + (imm<<7) |
| SYS-Type (func3 = bits [5:3]) | ||||||
| ECALL | SYS | 111 |
— | 000 |
— | trap to service number [15:6] |
| EBREAK | SYS | 111 |
— | 001 |
— | breakpoint → vector 1 |
| RETI | SYS | 111 |
— | 010 |
— | PC←EPC, IE←1 |
| EI / DI | SYS | 111 |
— | 011/100 |
— | IE←1 / IE←0 |
| MFEPC/MTEPC | SYS | 111 |
— | 101/110 |
— | rd←EPC / EPC←rd |
| STEP | SYS | 111 |
— | 111 |
— | single-step (trap after 1 instr) |
ZX16 supports several pseudo-instructions that expand to one or more real instructions:
LI16 x1, 0x1234
Expands to:
LUI x1, 0x24 # Load upper 9 bits into bits [15:7] (0x1234 >> 7 = 0x24)
ORI x1, 0x34 # OR in the lower 7 bits (0x1234 & 0x7F = 0x34)The lower bits are combined with ORI, not ADDI. Because LUI clears the low 7
bits of the destination, OR-ing the 7-bit remainder reconstructs the full value
exactly, with no sign-extension to correct for. Any imm16 in 0x0000–0xFFFF works
with this two-instruction sequence.
LA x1, data_label
# Expands to:
AUIPC x1, high # high = ((label - PC) - low) >> 7 (9-bit field)
ADDI x1, low # low = (label - PC) sign-reduced to [-64, 63]The offset label - PC is split so that the low part is a 7-bit signed value in
[-64, 63] and the high part carries the remainder. ADDI is correct here (unlike in
LI16) because the offset is computed as a signed quantity and the low part is
deliberately reduced into signed range before the high part is adjusted.
PUSH x1
# Expands to:
ADDI x2, -2 # SP -= 2 (decrement stack pointer)
SW x1, 0(x2) # Store register at new SPPOP x1
# Expands to:
LW x1, 0(x2) # Load from current SP
ADDI x2, 2 # SP += 2 (increment stack pointer)CALL func_name
# Expands to:
JAL x1, offset # Jump and link (return address in x1/ra)RET
# Expands to:
JR x1 # Jump to return address (x1/ra)INC x1
# Expands to:
ADDI x1, 1 # rd = rd + 1DEC x1
# Expands to:
ADDI x1, -1 # rd = rd - 1NEG x1
# Expands to:
XORI x1, -1 # Invert all bits (imm7 = -1 sign-extends to 0xFFFF)
ADDI x1, 1 # Add 1 to complete two's complementNOT x1
# Expands to:
XORI x1, -1 # XOR with 0xFFFF (imm7 = -1 sign-extended)CLR x1
# Expands to:
XOR x1, x1 # x1 = x1 XOR x1 = 0NOP
# Expands to:
ADD x0, x0 # x0 = x0 + x0; result discarded into x0, no architectural effect of interest- Arguments: x6 (a0), x7 (a1); additional arguments spill to stack
- Return value: x6 (a0)
- Return address: x1 (ra) - set by JAL/JALR, used by RET
- Stack pointer: x2 (sp) - points to the last pushed word (full-descending)
- Caller-saved: x0 (t0), x5 (t1), x6 (a0), x7 (a1)
- Callee-saved: x3 (s0), x4 (s1)
- Special: x1 (ra), x2 (sp)
- Stack grows downward (toward lower addresses)
- SP is reset to 0xF000; the first push lands at 0xEFFE–0xEFFF
- Keep SP even (word-aligned), since SW/LW require aligned addresses
- Callee must restore stack pointer before returning
- I-Type: -64 to +63 (7-bit signed)
- S-Type/L-Type: -8 to +7 (4-bit signed)
- B-Type: -16 to +14 bytes (5-bit signed, word-aligned)
- J-Type: -512 to +510 bytes (offset = sext of bits [9:1], sign bit = bit 9, word-aligned)
- U-Type: 0 to 511 (9-bit unsigned immediate 0-511, shifted left 7 bits)
- Shift amount: Limited to 0-15 (4 bits)
- R-Type shifts: Use
rs2 & 0xFas shift amount - I-Type shifts: Use
imm[3:0]as shift amount,imm[6:4]selects operation
- Byte operations: SB, LB, LBU - no alignment required
- Word operations: SW, LW - must be aligned to even addresses
- Endianness: Little-endian (LSB at lower address)
- Branches: PC-relative, word-aligned targets
- Jumps: PC-relative, word-aligned targets
- Register jumps: JR, JALR - absolute addressing
Please refer to the contribution guide here.