P32W ISA v0¶
Status: ACCEPTED (2026-10-11, D8). Implements decisions D5–D10 in decisions.md. Scope: what the instruction-level reference emulator v1 (D7) needs. Privileged instructions, the MMU, interrupts and exceptions-as-traps come in a later revision.
1. Machine state (v1)¶
| State | Notes |
|---|---|
| r0 | Always 0; writes discarded (D4) |
| r1–r14 | General purpose, 32 bits |
| r15 | Stack pointer (D6). Banked: supervisor SP / user SP. v1 runs in supervisor mode only, so only the supervisor SP is used |
| PC | 32 bits, always a multiple of 4 |
No flags register (D5). Memory is byte-addressed and little-endian (D4).
2. Instruction formats¶
Every instruction is 32 bits, word-aligned (D3). The fields never move:
31 24 23 20 19 16 15 12 11 0
+----------+------+------+------+--------------------+
| OP | A | B | C | unused (0) | R register-register
+----------+------+------+------+--------------------+
| OP | A | B | IMM16 | I immediate / load / store / branch
+----------+------+------+---------------------------+
| OP | OFF24 | J jump / call
+----------+------------------------------------------+
| OP | A | B | C | 0 (10 bits) | S | X indexed load / store (D59)
+----------+------+------+------+--------------------+
| OP | A | B | OFF8 (15:8) | INC8 (7:0)| P post-increment load / store (D59)
+----------+------+------+---------------------------+
| OP | 0 | rd | 0 (16 bits) | L32 LI32, followed by the value's word (D59)
+----------+------+------+---------------------------+
- OP — 8-bit opcode. The microcode will be addressed by this field (D3).
- A — destination register (rd); for stores, the data register; for branches, the first compared register.
- B — first source register (rs1); base register for loads/stores; second compared register for branches; target register for JR/CALLR.
- C — second source register (rs2), R format only.
- IMM16 — 16-bit immediate, sign- or zero-extended per instruction.
- OFF24 — 24-bit signed word offset.
- Unused bits must be 0. The emulator reports a nonzero unused field as an illegal instruction, keeping those bits free for later revisions.
Because fields sit on 4-bit boundaries, a hex dump reads directly:
ADD r1,r2,r3 = 0x10123000, ADDI r1,r0,42 = 0x2010002A.
Branch and jump targets are relative to the next instruction:
target = PC + 4 + offset × 4.
- Conditional branches (IMM16): ±128 KiB. (D5 estimated ±512 KiB for a
6-bit opcode; an 8-bit opcode trades that for simpler decode.)
- JMP/CALL (OFF24): ±32 MiB.
- JR/CALLR reach anywhere.
3. Opcode map¶
sext = sign-extend, zext = zero-extend, mem32[x] = word at byte address
x. All arithmetic is modulo 2³². rd = A, rs1 = B, rs2 = C.
0x00–0x0F — Special¶
| OP | Mnemonic | Fmt | Operation |
|---|---|---|---|
| 0x00 | — | — | Illegal (so executing zeroed memory stops at once) |
| 0x01 | HALT |
— | Stop. Emulator exit status = r1 & 0xFF. (Privileged in later revisions) |
| 0xFF | — | — | Illegal (so executing erased EEPROM stops at once) |
0x10–0x1F — Register-register ALU (R)¶
| OP | Mnemonic | Operation |
|---|---|---|
| 0x10 | ADD rd,rs1,rs2 |
rd = rs1 + rs2 |
| 0x11 | SUB rd,rs1,rs2 |
rd = rs1 − rs2 |
| 0x12 | AND rd,rs1,rs2 |
rd = rs1 & rs2 |
| 0x13 | OR rd,rs1,rs2 |
rd = rs1 | rs2 |
| 0x14 | XOR rd,rs1,rs2 |
rd = rs1 ^ rs2 |
| 0x15 | SLL rd,rs1,rs2 |
rd = rs1 << (rs2 & 31) |
| 0x16 | SRL rd,rs1,rs2 |
rd = rs1 >> (rs2 & 31), zero fill |
| 0x17 | SRA rd,rs1,rs2 |
rd = rs1 >> (rs2 & 31), sign fill |
| 0x18 | SLT rd,rs1,rs2 |
rd = (signed rs1 < signed rs2) ? 1 : 0 |
| 0x19 | SLTU rd,rs1,rs2 |
rd = (unsigned rs1 < unsigned rs2) ? 1 : 0 |
| 0x1A | NOR rd,rs1,rs2 |
rd = ~(rs1 | rs2) |
0x20–0x2F — Immediate ALU (I)¶
The low nibble matches the register form where one exists.
| OP | Mnemonic | Operation |
|---|---|---|
| 0x20 | ADDI rd,rs1,imm |
rd = rs1 + sext(imm) |
| 0x22 | ANDI rd,rs1,imm |
rd = rs1 & zext(imm) |
| 0x23 | ORI rd,rs1,imm |
rd = rs1 | zext(imm) |
| 0x24 | XORI rd,rs1,imm |
rd = rs1 ^ zext(imm) |
| 0x25 | SLLI rd,rs1,sh |
rd = rs1 << sh (imm bits 4:0; bits 15:5 must be 0) |
| 0x26 | SRLI rd,rs1,sh |
logical right shift |
| 0x27 | SRAI rd,rs1,sh |
arithmetic right shift |
| 0x28 | SLTI rd,rs1,imm |
rd = (signed rs1 < signed sext(imm)) ? 1 : 0 |
| 0x29 | SLTIU rd,rs1,imm |
rd = (unsigned rs1 < unsigned sext(imm)) ? 1 : 0 |
| 0x2F | LUI rd,imm |
rd = imm << 16 (B must be 0) |
There is no SUBI (use ADDI with a negative immediate). A full 32-bit constant
is LUI rd,hi + ORI rd,rd,lo; the assembler provides LI rd,value, which
emits one instruction when the value fits and two otherwise.
0x30–0x3F — Multiply / divide (R)¶
Implemented by the FPGA ALU in hardware (D21); the emulator models them directly.
| OP | Mnemonic | Operation |
|---|---|---|
| 0x30 | MUL rd,rs1,rs2 |
rd = low 32 bits of rs1 × rs2 |
| 0x31 | MULH rd,rs1,rs2 |
rd = high 32 bits, signed × signed |
| 0x32 | MULHU rd,rs1,rs2 |
rd = high 32 bits, unsigned × unsigned |
| 0x34 | DIV rd,rs1,rs2 |
signed quotient, rounded toward zero |
| 0x35 | DIVU rd,rs1,rs2 |
unsigned quotient |
| 0x36 | REM rd,rs1,rs2 |
signed remainder (sign of rs1) |
| 0x37 | REMU rd,rs1,rs2 |
unsigned remainder |
Divide never traps: - Division by zero: quotient = 0xFFFFFFFF, remainder = rs1. - Signed overflow (−2³¹ ÷ −1): quotient = −2³¹, remainder = 0.
These are the RISC-V results, chosen so that software checks the cases it cares about and the hardware needs no divide trap.
0x40–0x4F — Loads and stores (I)¶
Address = rs1 + sext(imm). Misaligned halfword/word access → exception (D4).
| OP | Mnemonic | Operation |
|---|---|---|
| 0x40 | LB rd,imm(rs1) |
rd = sext(mem8) |
| 0x41 | LH rd,imm(rs1) |
rd = sext(mem16) |
| 0x42 | LW rd,imm(rs1) |
rd = mem32 |
| 0x44 | LBU rd,imm(rs1) |
rd = zext(mem8) |
| 0x45 | LHU rd,imm(rs1) |
rd = zext(mem16) |
| 0x48 | SB ra,imm(rs1) |
mem8 = ra & 0xFF |
| 0x49 | SH ra,imm(rs1) |
mem16 = ra & 0xFFFF |
| 0x4A | SW ra,imm(rs1) |
mem32 = ra |
| 0x4C | SWAP rd,imm(rs1) |
t = mem32; mem32 = rd; rd = t — atomic (D51) |
(ra = the A field = the data register.)
SWAP (D51) exchanges a register with an aligned word in memory
as one indivisible operation: the read and the write are one bus
tenure under -LOCK (bus.md), so no DMA master (or,
later, another processor) can come between them. It is the primitive for
locks: SWAP a 1 into the lock word; a 0 back means the lock was free and is
now yours. Faults are taken as for a load and then a store of the same word
(MISALIGNED, TLBMISS_R, then PROTECT if the page is read-only): the memory
and rd change only when both accesses succeed.
0x80–0x9F — Indexed and post-increment loads and stores (D59)¶
Added for the three-bus datapath with its address adder (D59): a load or store's address goes straight into MAR, so these take no longer than a plain load. The low opcode bits mean what they do in 0x40–0x4F (size, sign, store), in every group.
Indexed (X): address = rs1 + rs2 × scale, scale 1, 2, 4 or 8 (S =
log2 scale). Written LWX rd, rs1, rs2, 4 (, 1 may be left out).
| OP | Mnemonic | Operation |
|---|---|---|
| 0x80 | LBX rd,rs1,rs2,s |
rd = sext(mem8) |
| 0x81 | LHX rd,rs1,rs2,s |
rd = sext(mem16) |
| 0x82 | LWX rd,rs1,rs2,s |
rd = mem32 |
| 0x84 | LBUX rd,rs1,rs2,s |
rd = zext(mem8) |
| 0x85 | LHUX rd,rs1,rs2,s |
rd = zext(mem16) |
| 0x88 | SBX ra,rs1,rs2,s |
mem8 = ra & 0xFF |
| 0x89 | SHX ra,rs1,rs2,s |
mem16 = ra & 0xFFFF |
| 0x8A | SWX ra,rs1,rs2,s |
mem32 = ra |
Post-increment (P): the access at rs1 + sext(OFF8), then rs1 += sext(INC8)
(both −128…127). Written LWP rd, off(rs1), inc ((rs1) for offset 0). rs1
is written after the access has succeeded, and last: a fault leaves rd
and rs1 unchanged (the instruction can be restarted), and with rd = rs1 a
load's value is overwritten by the increment.
| OP | Mnemonic | Operation |
|---|---|---|
| 0x90 | LBP rd,off(rs1),inc |
rd = sext(mem8); rs1 += inc |
| 0x91 | LHP rd,off(rs1),inc |
rd = sext(mem16); rs1 += inc |
| 0x92 | LWP rd,off(rs1),inc |
rd = mem32; rs1 += inc |
| 0x94 | LBUP rd,off(rs1),inc |
rd = zext(mem8); rs1 += inc |
| 0x95 | LHUP rd,off(rs1),inc |
rd = zext(mem16); rs1 += inc |
| 0x98 | SBP ra,off(rs1),inc |
mem8 = ra & 0xFF; rs1 += inc |
| 0x99 | SHP ra,off(rs1),inc |
mem16 = ra & 0xFFFF; rs1 += inc |
| 0x9A | SWP ra,off(rs1),inc |
mem32 = ra; rs1 += inc |
LI32 rd, value (0x2E, two words): rd = the word that follows the
instruction, read as instructions are (execute permission); the next
instruction is the one after it. rd is in field B (A and the low 16 bits
are 0), which keeps the CPU card's decode table at eight must-be-zero
classes. The assembler's LA, and LI of a value that needs 32 bits, are
LI32 (the same 8 bytes as LUI + ORI, in 3 cycles instead of 5).
0x50–0x5F — Conditional branches (I)¶
target = PC + 4 + sext(imm) × 4. Compares A with B (D5).
| OP | Mnemonic | Taken if |
|---|---|---|
| 0x50 | BEQ ra,rb,label |
ra == rb |
| 0x51 | BNE ra,rb,label |
ra != rb |
| 0x54 | BLT ra,rb,label |
signed ra < rb |
| 0x55 | BGE ra,rb,label |
signed ra ≥ rb |
| 0x56 | BLTU ra,rb,label |
unsigned ra < rb |
| 0x57 | BGEU ra,rb,label |
unsigned ra ≥ rb |
Assembler aliases: BGT a,b = BLT b,a; BLE a,b = BGE b,a (and the
unsigned forms); BEQZ a = BEQ a,r0; BNEZ a = BNE a,r0.
0x60–0x6F — Jumps, calls, return¶
The stack is full-descending, SP = r15 (D6). Calls push the return address (D6). SP and PC are committed only after the memory access succeeds, so a faulted CALL or RET can be restarted.
| OP | Mnemonic | Fmt | Operation |
|---|---|---|---|
| 0x60 | JMP label |
J | PC = PC + 4 + sext(off) × 4 |
| 0x61 | CALL label |
J | mem32[SP−4] = PC + 4; SP −= 4; PC = target |
| 0x62 | JR rb |
I (B only) | PC = rb (must be a multiple of 4) |
| 0x63 | CALLR rb |
I (B only) | mem32[SP−4] = PC + 4; SP −= 4; PC = rb |
| 0x64 | RET |
— | PC = mem32[SP]; SP += 4 |
Assembler aliases:
| Alias | Expands to |
|---|---|
NOP |
ADD r0,r0,r0 |
MOV rd,rs |
ADD rd,rs,r0 |
NEG rd,rs |
SUB rd,r0,rs |
NOT rd,rs |
NOR rd,rs,r0 |
PUSH rs |
ADDI sp,sp,-4 + SW rs,0(sp) |
POP rd |
LW rd,0(sp) + ADDI sp,sp,4 |
0x70–0x7F — System (reserved in v0)¶
Reserved for SYSCALL, return-from-exception, status/MMU register access and
user-SP access (D1, D6). In v1 they decode as illegal.
All other opcodes are illegal.
4. Reset state (v1)¶
- PC = 0x0000_0000.
- Supervisor mode.
- r1–r15 = 0 in the emulator (undefined in hardware). Startup code must set SP.
5. Memory map (emulator v1, physical, no MMU)¶
| Range | Size | Use |
|---|---|---|
| 0x0000_0000 – 0x000F_FFFF | 1 MiB | ROM (program image loaded here; writes are an error) |
| 0x0010_0000 – 0x03FF_FFFF | up to 63 MiB | RAM: one to four 16 MiB memory cards in windows 0–3 (D52); the ROM hides the first MiB of window 0 |
| 0xFE00_0000 – 0xFE07_FFFF | 64 KiB a card | video memory (D52, D53) |
| 0xFFFF_8000 – 0xFFFF_FFFF | 32 KiB | I/O page: eight 4 KiB device windows (D52) |
| everything else | — | unmapped → exception |
The I/O page is the top 32 KiB, so LB rd,-256(r0) reaches 0xFFFF_FF00
with no extra instructions: a sign-extended 16-bit offset from r0 covers
0xFFFF_8000–0xFFFF_FFFF, echoing the P8X $FF00 I/O page.
I/O page (D50, D52): each device card answers in a 4 KiB window chosen
by its jumpers, its registers in the window's top 256 bytes; the defaults
put the I/O card in window 7 and the disk card in window 6. Byte
registers answer byte accesses only; the timer, word accesses only; anything
else gets no -ACK → BUSERROR. Full register descriptions: the programmer's
guide (programmers-guide.md).
| Address | Device |
|---|---|
| 0xFFFF_FF00–07 | UART 0, the console (TL16C550 registers 0–7): data at FF00, line status at FF05 (bit 0 = receive data ready, bit 5 = transmit ready) |
| 0xFFFF_FF08–0F | UART 1 (TL16C550) |
| 0xFFFF_FF10/14/18 | timer TCOUNT / TRELOAD / TCTRL (isa-system.md B4) |
| 0xFFFF_FF20 | input port (8 bits) |
| 0xFFFF_FF21 | output port (8 bits, reads back) |
| 0xFFFF_FF30–3F | UARTs 2 and 3 (TL16C550) |
| 0xFFFF_FF40/41 | real-time clock (D23) |
| 0xFFFF_FF50/51, FF54/55 | USB ports 0 and 1 (CH376S, D43) |
| 0xFFFF_FF52/53 | network coprocessor (D45) |
| 0xFFFF_EF00–07 | the disk card's port 0, CompactFlash, True-IDE task file (16-bit data register, D26); ports 1–3 follow, 16 bytes each |
6. Exceptions (v1)¶
The emulator stops and prints the PC, the offending instruction and all registers on: - illegal instruction - misaligned access, or a JR/CALLR target that is not a multiple of 4 - unmapped address - write to ROM
7. Review outcome (2026-10-11)¶
All four review questions were accepted as proposed:
1. 8-bit opcode; conditional branches reach ±128 KiB.
2. Branch and jump offsets are relative to PC + 4 (the next instruction).
3. Memory map as in §5.
4. NOR added at 0x1A; NOT rd,rs = NOR rd,rs,r0.