Skip to content

shalan/zx16

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

115 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZX16 RISC ISA Specification

Overview

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)

Key Features

  • 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 Architecture

General-Purpose Registers

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

Special Registers

  • PC: 16-bit program counter

Reset Behavior

  • 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: PUSH does ADDI sp, -2 then SW 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, x0 is a normal general-purpose register (ABI name t0). It is not a hardwired-zero register. Writes to x0 take effect, and reads return whatever was last written. Idioms such as CLR rd (which expands to XOR rd, rd) and NOP (ADD x0, x0) rely on this.


Memory Map

Range Usage
0x0000–0x001F Interrupt vector table (16 entries × 2 bytes)
0x0020–0xEFFF RAM & ROM
0xF000–0xFFFF MMIO (I/O registers start at 0xF000)

Memory Properties

  • 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

Interrupt Vector Table

  • 16 fixed entries at 0x0000–0x001E (2 bytes each); each entry is a J instruction 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 + IE registers): a hardware IRQ is taken at an instruction boundary while IE=1; EBREAK always traps. Entry does EPC←PC, IE←0, PC←i*2; return is via RETI (PC←EPC, IE←1). STEP single-steps the debuggee one instruction. Full spec and the EBREAK/RETI/EI/DI/MFEPC/MTEPC/STEP instructions: docs/INTERRUPTS.md.

Instruction Formats

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

ZX16 Instruction Format Field Layouts

All instructions are 16 bits; bits [2:0] select the format/opcode.

R-Type (opcode = 000)

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

I-Type (opcode = 001)

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.

B-Type (opcode = 010)

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

S-Type (opcode = 011)

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

L-Type (opcode = 100)

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

J-Type (opcode = 101)

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

U-Type (opcode = 110)

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).

SYS-Type (opcode = 111)

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.


Instruction Set Reference

R-Type Instructions

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

I-Type Instructions

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)

B-Type Instructions

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]

S-Type Instructions

Mnemonic Description
SB Mem[x[rs1] + sext(imm4)] ← least-significant byte of x[rs2]
SW Mem[x[rs1] + sext(imm4)] ← x[rs2] (16 bits)

L-Type Instructions

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)]

J-Type Instructions

Mnemonic Description
J PC ← PC + offset
JAL x[rd] ← PC + 2; PC ← PC + offset

U-Type Instructions

Mnemonic Description
LUI rd ← (imm[15:7] << 7)
AUIPC rd ← PC + (imm[15:7] << 7)

SYS-Type Instructions

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 Services

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.


Instruction Identification Table

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)

Pseudo-Instructions

ZX16 supports several pseudo-instructions that expand to one or more real instructions:

LI16 rd, imm16 - Load 16-bit immediate

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 rd, label - Load address (PC-relative)

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 rd - Push register to stack

PUSH x1
# Expands to:
ADDI x2, -2        # SP -= 2 (decrement stack pointer)
SW   x1, 0(x2)     # Store register at new SP

POP rd - Pop from stack to register

POP x1
# Expands to:
LW   x1, 0(x2)     # Load from current SP
ADDI x2, 2         # SP += 2 (increment stack pointer)

CALL label - Call function

CALL func_name
# Expands to:
JAL x1, offset     # Jump and link (return address in x1/ra)

RET - Return from function

RET
# Expands to:
JR x1              # Jump to return address (x1/ra)

INC rd - Increment register

INC x1
# Expands to:
ADDI x1, 1         # rd = rd + 1

DEC rd - Decrement register

DEC x1
# Expands to:
ADDI x1, -1        # rd = rd - 1

NEG rd - Negate register (two's complement)

NEG x1
# Expands to:
XORI x1, -1        # Invert all bits (imm7 = -1 sign-extends to 0xFFFF)
ADDI x1, 1         # Add 1 to complete two's complement

NOT rd - Bitwise NOT

NOT x1
# Expands to:
XORI x1, -1        # XOR with 0xFFFF (imm7 = -1 sign-extended)

CLR rd - Clear register to zero

CLR x1
# Expands to:
XOR x1, x1         # x1 = x1 XOR x1 = 0

NOP - No operation

NOP
# Expands to:
ADD x0, x0         # x0 = x0 + x0; result discarded into x0, no architectural effect of interest

Calling Convention

Function Calls

  • 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)

Register Usage

  • Caller-saved: x0 (t0), x5 (t1), x6 (a0), x7 (a1)
  • Callee-saved: x3 (s0), x4 (s1)
  • Special: x1 (ra), x2 (sp)

Stack Management

  • 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

Implementation Notes

Immediate Ranges

  • 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 Operations

  • Shift amount: Limited to 0-15 (4 bits)
  • R-Type shifts: Use rs2 & 0xF as shift amount
  • I-Type shifts: Use imm[3:0] as shift amount, imm[6:4] selects operation

Memory Access

  • 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)

Control Flow

  • Branches: PC-relative, word-aligned targets
  • Jumps: PC-relative, word-aligned targets
  • Register jumps: JR, JALR - absolute addressing

Contribution

Please refer to the contribution guide here.

About

A 16-bit RISC-V Inspired ISA

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors