Reference
Quick-lookup tables for encodings, constants, and signal names.
RV32I/M Opcode Map
Opcode [6:0] |
Hex |
Instruction class |
Engine |
0110111 |
0x37 |
LUI |
CPU |
0010111 |
0x17 |
AUIPC |
CPU |
1101111 |
0x6F |
JAL |
CPU |
1100111 |
0x67 |
JALR |
CPU |
1100011 |
0x63 |
BRANCH (BEQ/BNE/BLT/BGE/BLTU/BGEU) |
CPU |
0000011 |
0x03 |
LOAD (LB/LH/LW/LBU/LHU) |
CPU |
0100011 |
0x23 |
STORE (SB/SH/SW) |
CPU |
0010011 |
0x13 |
ALU-IMM (ADDI/SLTI/ANDI/ORI/XORI/SLLI/SRLI/SRAI) |
CPU |
0110011 |
0x33 |
ALU-REG (ADD/SUB/AND/OR/XOR/SLT/SLL/SRL/SRA) + M-ext |
CPU |
1010111 |
0x57 |
custom-0 (GPU vector ALU, see gpu.md) |
GPU |
1101011 |
0x6B |
custom-1 (NPU v2) |
NPU |
ALU Control Codes
Defined in both rtl/alu.sv and rtl/riscv_core.sv as localparam:
alu_control[3:0] |
Name |
Operation |
4'd0 |
ALU_ADD |
a + b |
4'd1 |
ALU_SUB |
a - b |
4'd2 |
ALU_AND |
a & b |
4'd3 |
ALU_OR |
a \| b |
4'd4 |
ALU_XOR |
a ^ b |
4'd5 |
ALU_SLT |
($signed(a) < $signed(b)) ? 1 : 0 |
4'd6 |
ALU_SLTU |
(a < b) ? 1 : 0 (unsigned) |
4'd7 |
ALU_SLL |
a << b[4:0] |
4'd8 |
ALU_SRL |
a >> b[4:0] (logical) |
4'd9 |
ALU_SRA |
$signed(a) >>> b[4:0] (arithmetic) |
4'd10 |
ALU_MUL |
a × b lower 32 bits (3-cycle) |
4'd11 |
ALU_MULH |
a × b upper 32 bits (3-cycle, signed) |
4'd12 |
ALU_DIV |
$signed(a) / $signed(b) (34-cycle; div-by-0 → -1, INT_MIN/-1 → INT_MIN) |
4'd13 |
ALU_DIVU |
a / b unsigned (34-cycle; div-by-0 → 0xFFFFFFFF) |
4'd14 |
ALU_REM |
$signed(a) % $signed(b) (34-cycle; div-by-0 → a, INT_MIN/-1 → 0) |
4'd15 |
ALU_REMU |
a % b unsigned (34-cycle; div-by-0 → a) |
All 16 values of the 4-bit alu_control bus are in use — see "Known ISA Gaps" below for why MULHSU/MULHU aren't in this table.
CPU Writeback Selector (wb_sel[1:0])
| Value |
Source |
Used by |
2'b00 |
alu_result |
ALU-reg, ALU-imm, AUIPC, JALR address |
2'b01 |
mem_rdata (load-extended) |
LW/LH/LB/LHU/LBU |
2'b10 |
pc + 4 |
JAL, JALR (link address) |
2'b11 |
imm_ext |
LUI (no ALU needed) |
NPU Instruction Encoding
Opcode: 7'h6B (1101011)
31 26 25 18 17 14 13 10 9 7 6 0
┌───────┬────────┬───────┬───────┬─────┬──────────┐
│unused │ data │ col │ row │ cmd │ 0x6B │
│[31:26]│[25:18] │[17:14]│[13:10]│[9:7]│ [6:0] │
└───────┴────────┴───────┴───────┴─────┴──────────┘
| Field |
Bits |
Width |
Type |
| opcode |
[6:0] |
7 |
constant 0x6B |
| cmd |
[9:7] |
3 |
NPU command |
| row |
[13:10] |
4 |
weight row index (0–15) |
| col |
[17:14] |
4 |
weight/activation col index (0–15) |
| data |
[25:18] |
8 |
INT8 signed payload |
| unused |
[31:26] |
6 |
— |
NPU Command Codes
cmd [9:7] |
Mnemonic |
Payload used |
3'b000 |
LOAD_W |
row, col, data |
3'b001 |
LOAD_A |
col, data |
3'b010 |
COMPUTE |
— |
3'b011 |
COMPUTE_ACC |
— |
3'b100 |
SWAP |
— (flips the ping-pong weight bank) |
3'b101 |
SET_PREC |
row[0] (instr[10]): 0=INT8, 1=INT16 |
| other |
NOP |
— |
INT16 mode repurposes the data field as 14 raw bits at instr[31:18] (sign-extended to 16 — see rtl/npu.sv's data_int16), not the 8-bit [25:18] field shown in the diagram above; the assembler (scripts/riscv_asm.py) always writes the value at instr[31:18] and lets the NPU's current precision mode decide how many of those bits matter.
Python Instruction Builder
def npu_instr(cmd, row=0, col=0, data=0):
return (0x6B
| (cmd & 0x7) << 7
| (row & 0xF) << 10
| (col & 0xF) << 14
| (data & 0xFF) << 18)
NPU FSM States
| State |
Encoding |
Description |
IDLE |
3'd0 |
Ready to accept instruction |
LOAD_W_ST |
3'd1 |
Writing one weight element |
LOAD_A_ST |
3'd2 |
Writing one activation element |
COMPUTE_ST |
3'd3 |
Computing acc = W·A (1 cycle) |
COMP_ACC_ST |
3'd4 |
Computing acc += W·A (1 cycle) |
OUTPUT_ST |
3'd5 |
Streaming 16 INT32 results |
RV32I funct3 — Branch
funct3 |
Instruction |
ALU op selected |
3'h0 |
BEQ |
ALU_SUB (checks zero) |
3'h1 |
BNE |
ALU_SUB (checks zero) |
3'h4 |
BLT |
ALU_SLT |
3'h5 |
BGE |
ALU_SLT |
3'h6 |
BLTU |
ALU_SLTU |
3'h7 |
BGEU |
ALU_SLTU |
See cpu.md (Stage 3 — Execute) for the full branch-resolution writeup — BLT/BGE/BLTU/BGEU need an actual comparison, not just alu_result[0] off a subtraction.
RV32I funct3 — Load
funct3 |
Instruction |
Sign |
3'h0 |
LB |
signed byte |
3'h1 |
LH |
signed halfword |
3'h2 |
LW |
32-bit word |
3'h4 |
LBU |
unsigned byte |
3'h5 |
LHU |
unsigned halfword |
RV32I funct3 — Store / Byte Enable
funct3 |
Instruction |
mem_byte_en |
3'h0 |
SB |
0001/0010/0100/1000 by addr[1:0] |
3'h1 |
SH |
0011 (addr[1]=0) / 1100 (addr[1]=1) |
3'h2 |
SW |
1111 |
RV32M funct7 + funct3
All M-extension instructions: opcode=0x33, funct7=0x01
funct3 |
Instruction |
3'h0 |
MUL (lower 32 bits) |
3'h1 |
MULH (upper 32 bits, signed×signed) |
3'h2 |
MULHSU (signed×unsigned) |
3'h3 |
MULHU (unsigned×unsigned) |
3'h4 |
DIV |
3'h5 |
DIVU |
3'h6 |
REM |
3'h7 |
REMU |
MUL/MULH/DIV/DIVU/REM/REMU are all implemented in alu.sv (3-cycle multiplier, 34-cycle divider). MULHSU/MULHU are the only gap — see "Known ISA Gaps" below.
Known ISA Gaps
| Instruction |
Status |
Why |
MULHSU (funct3=3'h2) |
Not implemented |
riscv_core.sv's OP_REG/M-ext decode has no case for it — falls through to default: ;, leaving alu_ctrl at its ALU_ADD default. The instruction silently computes rs1 + rs2 instead of a high-multiply. |
MULHU (funct3=3'h3) |
Not implemented |
Same gap as MULHSU. |
Fixing this needs a real ALU control code for each (there is no free encoding — all 16 values of the 4-bit alu_control bus are already assigned, see the ALU Control Codes table above), which means widening alu_control past 4 bits across the riscv_core.sv ↔ alu.sv boundary. Tracked as a future item rather than done opportunistically, to keep that interface change deliberate rather than a side effect of an unrelated fix.
scripts/riscv_asm.py (the assembler — see the toolchain doc) refuses to assemble mulhsu/mulhu with an explicit error rather than silently emitting an instruction that computes the wrong thing.
Key Signal Names
| Signal |
Location |
Description |
net_valid/ready |
system_top boundary |
Network source handshake |
cpu_accept |
server_dispatch_unit → system_top |
1-cycle SRAM write pulse |
imem_wr_ptr |
system_top |
SRAM write pointer (word address) |
core_stall |
riscv_core → system_top |
MUL busy signal |
mul_busy |
alu → riscv_core |
ALU multiply counter running |
mul_done |
riscv_core internal |
1-cycle pulse: MUL result valid |
branch_taken |
riscv_core EX stage |
Redirect fetch PC |
flush_if_id |
riscv_core EX stage |
Clear IF/ID register (branch penalty) |
load_use_stall |
riscv_core ID stage |
1-cycle stall for load-use hazard |
npu_ready |
npu |
Only high in IDLE state |
result_valid |
npu |
High during OUTPUT_ST |
halt |
system_top |
jal x0,0 sentinel detected |
Simulation Constants
| Constant |
Value |
Description |
HALT_WORD |
32'h0000_006F |
jal x0, 0 — simulation terminator |
| SRAM size |
256 words (1 KiB) |
Instruction memory |
| MUL latency |
3 cycles |
ALU counter: mul_cnt runs 2→1→0 |
| NPU output lanes |
16 |
One INT32 result per cycle in OUTPUT_ST |
| NPU max INT32 |
258,064 |
16 × 127 × 127, fits in INT32 |
| cocotb version |
2.0.1 |
From Docker container |
| iverilog version |
11.0 (stable) |
From Docker container |