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, optional paged external memory, MMIO/VRAM, 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 and VRAM reserved at 0xC000–0xFFFF (see docs/MEMORY_MAP.md)
- 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 | Top of general RAM (see memory map) |
| 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 the top of implemented general memory on reset (see docs/MEMORY_MAP.md; legacy docs used 0xF000)
- x0–x1, x3–x7: Undefined values on reset (not initialized)
On the SP reset value: SP is initialized one word past the top of implemented general memory (base map: typically
0x8000so the first push lands at0x7FFE–0x7FFF). Full-descending, pre-decrement (PUSH=ADDI sp, -2thenSW). SP must not sit in the optional page window (0x8000–0xBFFF) or in MMIO/VRAM (0xC000–0xFFFF). Details: docs/MEMORY_MAP.md.
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.
Base map (ISA/ABI): portable software uses general memory only through
0x7FFF.
0x8000–0xBFFFis an optional page window (not assumed by the base ABI).
0xC000–0xFFFFis reserved for MMIO and VRAM.
Full rules: docs/MEMORY_MAP.md.
| Range | Usage |
|---|---|
| 0x0000–0x001F | Interrupt vector table (16 entries × 2 bytes) |
| 0x0020–0x7FFF | General memory (code, data, heap, stack) |
| 0x8000–0xBFFF | Optional external page window — not part of base ABI |
| 0xC000–0xFFFF | Reserved: MMIO + VRAM (machine-defined submap) |
- Endianness: Little-endian
- Addressing: Byte-addressable
- Alignment: Word accesses (SW/LW) must be aligned to even addresses
- Stack: Grows downward within general memory; base ABI does not place SP in the page window or MMIO. (Classic teaching reset SP=
0xF000is superseded for the v2 map by SP at the top of implemented general RAM — see docs/MEMORY_MAP.md.)
- 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 the top of implemented general memory (see docs/MEMORY_MAP.md); keep the stack out of
0x8000–0xFFFF - 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.
Educational and extended SoC machine profiles live under machines/.
Each machine defines reset state, memory map, MMIO, ecalls, an ISS, and a zx16cc port.
| Machine | Description |
|---|---|
su26 |
CSCE 2303 Summer 2026 platform (handout): SP=0xEFFE, tile VRAM, course ecalls |
su26-ext |
su26 + UART (0xC000) + timer (0xD000) + enhanced 2D GFX (32 tiles, ATTR, 4× RGB565 banks, 8 sprites, vblank/scroll) |
# Compile + run (from repo root)
python3 -m machines.zx16cc --machine su26 machines/su26/cc/examples/hello.c -o /tmp/hello
python3 -m machines.zx16sim --machine su26 /tmp/hello.bin
python3 -m machines.zx16cc --machine su26-ext machines/su26_ext/cc/examples/uart_hello.c -o /tmp/uh
python3 -m machines.zx16sim --machine su26-ext /tmp/uh.bin
# Browser debugger (framebuffer + tile/tilemap/palette viewers)
python3 -m machines.zx16sim --machine su26-ext /tmp/uh.bin --gui
# → http://127.0.0.1:8765/See machines/README.md and machines/docs/SU26_EXT_GFX.md.
The existing compiler/ ISS remains the golden toolchain simulator; machines/ is additive.