A 32-bit multi-cycle RISC processor designed from scratch in Verilog. Custom ISA, structural ALU, hardwired FSM control unit, and BRAM-based memory — synthesized and tested on a Xilinx Artix-7 FPGA (Nexys A7) at 100 MHz.
Built as part of the Computer Organization and Architecture Laboratory course at IIT Kharagpur (Autumn 2025).
┌──────────────────────────────────────────────────────────────────────┐
│ CONTROL PATH (FSM) │
│ 23 states · hardwired · Moore-style │
│ FETCH → IF_WAIT → IR_LATCH → DECODE → EXEC → WB │
└───────────────┬───────────────────────────────┬──────────────────────┘
│ control signals │ opcode/funct
┌───────────────▼───────────────────────────────▼──────────────────────┐
│ DATAPATH │
│ │
│ ┌────┐ ┌──────────┐ ┌──────┐ ┌────────┐ │
│ │ PC │→→ │ Instr. │→→ │ Reg │→→ │ ALU │ │
│ │Unit│ │ Register │ │ File │ │ (16 op)│ │
│ └────┘ └──────────┘ └──────┘ └────────┘ │
│ ↕ ↕ │
│ ┌─────────┐ ┌─────────┐ │
│ │ IMEM │ │ DMEM │ │
│ │ (BRAM) │ │ (BRAM) │ │
│ └─────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────────────┘
This is a multi-cycle processor (not single-cycle or pipelined). Each instruction takes 4–7 clock cycles, depending on the instruction type. Hardware resources such as the ALU, register file, and memories are reused across cycles, while intermediate registers (IR, ALU output register, register-data buffers) preserve values between execution stages.
flowchart TB
CP["Control Path (23-state FSM)"]
DP["Datapath"]
PC["PC Unit"]
IR["Instruction Register"]
RF["Register File"]
ALU["16-op ALU"]
IM["Instruction Memory"]
DM["Data Memory"]
CP -->|control signals| DP
DP -->|opcode/funct| CP
PC --> IR --> RF --> ALU
IM --> IR
RF <--> DM
- 4 instruction formats: R-type (6-bit funct), I-type (16-bit immediate), J-type (26-bit target), and program control.
- 17 R-type ALU operations: ADD, SUB, AND, OR, XOR, NOR, NOT, SLT, SGT, SL, SRL, SRA, INC, DEC, HAM (popcount), MOVE, CMOV (conditional move).
- 15 I-type immediate operations: ADDI, SUBI, ANDI, ORI, XORI, NORI, SLTI, SLAI, SRLI, SRAI, INCI, DECI, NOTI, HAMI, LUI.
- Memory: LD and ST with base+offset addressing.
- Branches: BZ (zero), BMI (negative), BPL (positive), BR (unconditional), all PC-relative.
- Subroutines: CALL (push return address to stack, jump) and RET (pop and jump back).
- Control: NOP, HALT (stops the processor).
Full encoding tables are in docs/problem_statement.md and
docs/GRP11_ISA_RISC.pdf.
The ALU is not a behavioral case statement over a + b. Each operation is a standalone
module instantiated inside ALU_32.v:
| Unit | Modules | Design |
|---|---|---|
| Arithmetic | ADD_32, SUB_32, INC_32, DEC_32 |
Ripple-carry adder built from full_adder instances. Subtraction via complement-add. |
| Logic | AND_32, OR_32, XOR_32, NOR_32, NOT_32 |
Bitwise combinational gates. |
| Shift | SLL_32, SRL_32, SRA_32, LUI_32 |
Barrel-shift style using loop-based generation. SRA preserves sign bit. |
| Comparator | SLT_32, SGT_32 |
Built on top of subtraction result and sign/overflow analysis. |
| Hamming | HAM_32 |
Binary reduction tree — 16 full adders at level 1, widening adders at each subsequent level, producing a 6-bit popcount in O(log n) depth. |
All submodules are parameterized (#(parameter N = 32)) and reused at different widths
inside the Hamming tree.
The FSM in controlpath.v implements Moore-style control. Key design decisions:
- BRAM latency handling: Synchronous BRAM returns data one cycle after the address is
presented. The FSM adds
S_IF_WAITandS_IR_LATCHstates during fetch, andS_LD_WAITduring loads, to accommodate this. - Branch target pre-computation: During
S_DECODE, the ALU speculatively computesPC + sign_ext(imm)for branches. Non-branch instructions increment the PC in this same state. Branch instructions defer the PC update toS_BRANCH, where flags are checked. - CALL/RET as multi-step sequences: CALL takes 3 states (decrement SP → write return address to memory → jump). RET takes 3 states (read return address → increment SP → jump).
- HALT → IDLE: The
S_IDLEstate self-loops with all control signals deasserted. Thehaltedoutput signals the FPGA wrapper to stop the clock and display results.
| Type | States Traversed | Cycles |
|---|---|---|
| R-type ALU | FETCH→IF_WAIT→IR_LATCH→DECODE→EXEC_R→WB_ALU | 6 |
| I-type ALU | FETCH→IF_WAIT→IR_LATCH→DECODE→EXEC_I→WB_ALU | 6 |
| LD | FETCH→IF_WAIT→IR_LATCH→DECODE→MEM_ADDR→LD_READ→LD_WAIT→WB_LD | 8 |
| ST | FETCH→IF_WAIT→IR_LATCH→DECODE→MEM_ADDR→ST_WRITE | 6 |
| Branch | FETCH→IF_WAIT→IR_LATCH→DECODE→BRANCH | 5 |
| CALL | FETCH→IF_WAIT→IR_LATCH→DECODE→CALL_SP_DEC→CALL_WRITE→CALL_JUMP | 7 |
| RET | FETCH→IF_WAIT→IR_LATCH→DECODE→RET_READ→RET_SP_INC→RET_JUMP | 7 |
- Instruction memory: BRAM ROM, initialized from a
.coefile via Vivado IP Catalog (Block Memory Generator). Read-only, 8-bit address (256 words). - Data memory: BRAM RAM with write-enable, same addressing scheme. Pre-loaded with
test data via a separate
.coefile. - Both memories are byte-addressable at the ISA level; the hardware converts to
word-aligned BRAM addresses via
addr[9:2].
The FPGA top module (src/fpga/fpgatop_booth.v) adds:
- Button debouncer with configurable debounce time (10 ms at 100 MHz).
- Three-state wrapper FSM (IDLE → RUN → DONE) — a single button press resets and starts the processor; HALT stops it.
- Clock gating — the processor clock is AND-gated with
processor_enableto freeze execution in IDLE/DONE. - Multiplexed LED output — a slide switch selects upper or lower 16 bits of the result register for display on the board's 16 LEDs.
- Pin constraints (
constraints.xdc) for the Nexys4 DDR / Nexys A7 board.
programs/assembler.py translates assembly source into .coe files for BRAM
initialization. It supports the complete ISA including R-type, I-type, branches,
LD/ST with offset(Rn) syntax, CALL/RET, HALT, and NOP.
Multiplies two 16-bit signed integers using the radix-2 Booth algorithm.
Algorithm: Iterates 16 times. On each iteration, inspects the LSB of Q (multiplier)
and Q₋₁ (previous LSB). If {Q[0], Q₋₁} = 10, subtract M from accumulator A. If
{Q[0], Q₋₁} = 01, add M to A. Then arithmetic-right-shift the {A, Q} pair. The
final 32-bit signed product is assembled from the upper (A) and lower (Q) halves.
Register allocation:
- R1 = M (multiplicand), R2 = Q (multiplier, later result)
- R3 = A (accumulator), R4 = Q₋₁, R5 = iteration counter
Test case: M and Q are loaded from data memory (mem.coe). With M = -16 (0xFFFFFFF0)
and Q = 13 (0x0000000D), expected product = -208 (0xFFFFFF30).
Result: R2 = -208 after ~1500 cycles. Verified in simulation and on FPGA.
Computes the total popcount (Hamming weight) across 5 integers stored in data memory.
Test data (from mem.coe):
| Address | Value | Hamming Weight |
|---|---|---|
| 24 | 0x00000018 |
2 |
| 32 | 0x00000019 |
3 |
| 40 | 0x0000001A |
3 |
| 48 | 0x0000001B |
4 |
| 56 | 0x0000001C |
3 |
Register allocation:
- R3 = running total, R4 = loop counter (5 → 0)
- R5 = memory pointer, R6 = loaded value, R7 = HAM result
Result: R3 = total Hamming weight sum. Verified in simulation and on FPGA via multiplexed LED output.
miniRISC32/
├── src/
│ ├── alu/ # ALU and submodules
│ │ ├── ALU_32.v # Top-level ALU (16-op MUX)
│ │ ├── ALU_tb.v # ALU unit testbench
│ │ ├── arithmetic/ # ADD, SUB, INC, DEC, full_adder
│ │ ├── logic/ # AND, OR, XOR, NOR, NOT
│ │ ├── shift/ # SL, SRL, SRA, LUI
│ │ ├── comparator/ # SLT, SGT
│ │ ├── ham/ # HAM (popcount via reduction tree)
│ │ └── mux/ # 2:1, 4:1, 8:1, 16:1 multiplexers
│ ├── core/ # Processor core
│ │ ├── top_module.v # Top-level integration
│ │ ├── datapath.v # Registers, ALU, MUXes, buffers
│ │ ├── controlpath.v # 23-state hardwired FSM
│ │ ├── pc_unit.v # Program counter with 4-way next-PC MUX
│ │ ├── reg_bank.v # 32-entry register file (R0 = 0)
│ │ └── sign_extend.v # 16→32 sign/zero extension
│ └── fpga/ # FPGA-specific modules
│ ├── fpgatop_booth.v # Board wrapper for Booth test
│ ├── fpgatop_ham.v # Board wrapper for Hamming test
│ ├── debouncer.v # Button debouncer
│ └── constraints.xdc # Nexys A7 pin assignments
├── programs/
│ ├── asm/ # Assembly source files
│ │ ├── booth.asm
│ │ └── ham.asm
│ ├── coe/ # BRAM initialization files
│ │ ├── booth.coe
│ │ ├── ham.coe
│ │ └── mem.coe
│ └── assembler.py # Custom assembler (.asm → .coe)
├── sim/
│ ├── tb_booth.v # Booth multiplier testbench
│ └── tb_ham.v # Hamming weight testbench
├── docs/
│ ├── problem_statement.md # Full project specification
│ └── GRP11_ISA_RISC.pdf # ISA design report
└── .gitignore
- Xilinx Vivado Design Suite (tested with Vivado 2023.x / 2024.x)
- Digilent Nexys4 DDR or Nexys A7-100T board (for FPGA demo)
- Python 3.x (for the assembler)
- Create a new Vivado project targeting
xc7a100tcsg324-1. - Add all
.vfiles undersrc/as design sources. - Add
sim/tb_booth.vorsim/tb_ham.vas simulation sources. - Use Vivado IP Catalog → Block Memory Generator to create BRAM IPs:
blk_mem_gen_booth: Single-port ROM, width=32, depth=256, init fromprograms/coe/booth.coe(orham.coe).blk_mem_gen_mem: Single-port RAM, width=32, depth=256, init fromprograms/coe/mem.coe.
- Run behavioral simulation. Set simulation time to at least 10 µs.
- Check the transcript for PASS/FAIL messages.
- Set
fpgatop_booth(orfpgatop_ham) as the top module. - Add
src/fpga/constraints.xdcas a constraints file. - Run Synthesis → Implementation → Generate Bitstream.
- Program the Nexys A7 board.
- Press BTNC (reset) — the processor runs the program and halts.
- Toggle SW[0] to view upper/lower 16 bits of the result on the LEDs.
python3 programs/assembler.py programs/asm/booth.asm programs/coe/booth.coeMulti-cycle vs. single-cycle: Allowed hardware reuse (one ALU for everything) and a faster clock, at the cost of FSM complexity. A real next step would be pipelining.
Structural ALU: The assignment required building from gates up. The ripple-carry adder is O(n) delay but straightforward to implement and verify. The Hamming weight tree is a genuinely efficient O(log n) design.
BRAM vs. register arrays: BRAM is area-efficient and industry-standard for on-chip memory. It introduces one cycle of read latency, which required adding wait states to the FSM — a real-world hardware design constraint.
Hardwired vs. microprogrammed control: Hardwired is faster (combinational logic) but harder to extend. For a fixed ISA this is the right call.
Clock gating for HALT: Rather than a global enable, the FPGA wrapper AND-gates the
clock with processor_enable. Simple but effective for a demo; in production you'd use
Vivado's clock-enable primitives.
- Dasari Veera Venkata Abhinav Teja (23CS10017)
- Pulugu Lokeswara Reddy (23CS10065)
IIT Kharagpur, Department of Computer Science and Engineering