← Back to projects

Project 02 · Digital Systems / FPGA

32-bit RISC-V Processor (UART + VGA)

A custom RV32I CPU on the DE10-Lite (Intel MAX 10) FPGA. 32-bit machine-code is streamed live over UART, processed by a five-stage pipeline coupled with explicit valid/flush handshakes, and the resulting register state is rendered in real time on a VGA display.

EECS 3216 · York University — team project: Salwan Aldhahab, Jessica Buentipo, Quardin Lyttle, Karanpreet Raja

System Overview

Instead of fixing a program in memory at synthesis time, the user streams 32-bit machine-code words from a host computer over a serial link — the processor loads each into program memory on the fly. The five-stage pipeline then fetches, decodes, executes, accesses memory, and writes back, while the architectural state (the register file) is surfaced for real-time observation. This makes an otherwise invisible datapath something an observer can watch as computation proceeds.

Host PC serial terminal
UART
ASCII bits
serial_comm UART RX/TX
byte
ascii → bits 32-bit word
load
IF · ID · EX · MEM · WB 5-stage pipeline
state
VGA / LEDR register view
50 MHz System clock
5 Pipeline stages
20 ALU operations
9600 UART baud
RV32I Base ISA
16× UART oversampling
Tool / TechnologyRole in the project
Intel Quartus Prime LiteSynthesis, place-and-route, bitstream generation
Questa / ModelSimRTL simulation and waveform inspection
Verilog HDLHardware description for all modules
DE10-Lite (Intel MAX 10)Target FPGA development board
USB-to-UART converterSerial bridge between host PC and FPGA

Hardware Platform — DE10-Lite

The design targets the Terasic DE10-Lite, built around an Intel MAX 10 (10M50DAF484C7G). All sequential logic runs on the single 50 MHz board clock; the UART derives its own oversampled bit-clock from it with a counter, so no PLL is needed for the core. Reset is taken from KEY[0].

ResourceSignalUse
50 MHz clockMAX10_CLK1_50System clock for all logic
Push-button 0KEY[0]Active-low reset
Slide switch 0SW[0]Manual flush / single-step into IF
GPIO pin 2GPIO[2]UART receive (RX) from host
GPIO pin 1GPIO[1]UART transmit (TX) to host
Red LEDsLEDR[9:0]Debug / register-state readout
VGA connectorVGA_R/G/B, HS, VSRegister-state visualisation

One integration subtlety: the front-end peripherals (UART, ASCII converter) reset on a positive edge while the core modules reset on a negative edge, so the top level forms complementary reset / negReset signals — a reset-polarity split that was a recurring source of subtle bugs.

Pipeline & Control Model

The core is a five-stage datapath with a pipeline register between each pair of stages. Rather than a textbook lock-step pipeline — which assumes every stage finishes in one cycle and instructions arrive continuously — each stage is a multi-cycle finite-state machine, and adjacent stages are coupled by an explicit handshake. This trades some throughput for robustness against a variable-latency, 9600-baud serial front end.

IF fetch
IF/ID
ID decode
ID/EX
EX ALU
EX/MEM
MEM data
MEM/WB
WB register file

Every stage announces when its output register is occupied (valid) and waits for a flush from the downstream stage before accepting new work. The flush propagates upstream as backpressure: the register file flushes MEM, MEM flushes EX, EX flushes ID, and ID flushes IF.

  1. 1
    IF — Instruction Fetch Owns the writable 8-slot program memory and the program counter. A r_avail_instructions interlock stops the PC fetching past the last loaded word; coordinates four cooperating FSMs.
  2. 2
    ID — Instruction Decode Slices opcode/rs1/rs2/rd/funct fields, reads operands from the register file (async ports), and maps the operation to a 5-bit internal ALU opcode.
  3. 3
    EX — Execute A 20-operation ALU computes the result or an effective address. Signed compares/arithmetic shifts use $signed; shift amounts masked to 5 bits per the RISC-V spec.
  4. 4
    MEM — Memory Access A five-state FSM branches on {mem_write, mem_read}: load reads, store writes, non-memory instructions hold the ALU result for write-back.
  5. 5
    WB — Write-Back Commits the result into the destination register. Asynchronous read ports make a written value visible to the decoder on the very next read, and the final flush closes the backpressure chain.

UART & Instruction Input

The serial_comm module is the processor's window onto the outside world — a standard 8-N-1 UART running at 16× oversampling so it can locate the centre of each bit cell. A counter divides the 50 MHz clock down to that oversampling tick:

serial_comm.v · oversampling divider
// 16x oversampling tick from the 50 MHz reference
CLK_DIV = SYS_CLK / (OVER_SAMPLING * BAUD_RATE)
        = 50,000,000 / (16 * 9600)  // ≈ 325

After detecting the start-bit falling edge, the receiver re-checks at mid-start (rejecting glitches), samples eight data bits LSB-first, and pulses rx_ready on the stop bit:

Instructions are entered as printable text — a string of '0' and '1' characters — so any ordinary serial terminal works with no special tooling. ascii_to_bits_converter shifts each accepted character MSB-first into a 32-bit accumulator (ignoring spaces, newlines, and stray bytes); after 32 valid characters it pulses data_ready. instructionLoad then commits the word to program memory and advances the write address:

Instruction Set Architecture

All instructions are 32 bits wide. The three implemented formats differ only in how those bits are partitioned; the opcode occupies the low seven bits in every format, which lets the decoder choose a format before interpreting the rest.

R-type — register/register (add, sub, slt)
31:25funct7
24:20rs2
19:15rs1
14:12funct3
11:7rd
6:0opcode
I-type — immediate & load (addi, slli, lw)
31:20imm[11:0]
19:15rs1
14:12funct3
11:7rd
6:0opcode
S-type — store (sw); immediate split across two fields
31:25imm[11:5]
24:20rs2
19:15rs1
14:12funct3
11:7imm[4:0]
6:0opcode
OpcodeFormatClass
0110011R-typeRegister–register ALU
0010011I-typeImmediate ALU / shift
0000011I-typeLoad
0100011S-typeStore

The ALU implements 20 operations selected by a 5-bit internal opcode:

Arithmetic
  • add
  • sub
  • addi
Logic
  • and
  • or
  • xor
  • andi
  • ori
  • xori
Shift
  • sll
  • srl
  • sra
  • slli
  • srli
  • srai
Compare
  • slt
  • sltu
  • slti
  • sltiu
Store pass-through
  • sw

VGA Visualisation

A defining goal was to make the processor's internal state visible. The VGA subsystem renders the register file onto an ordinary monitor so an observer can watch register values change as instructions execute. VGA is a raster-scan standard: two sync pulses coordinate the monitor — horizontal sync (VGA_HS) ends each scan line and vertical sync (VGA_VS) ends each frame — while between them the design drives the colour channels. The DE10-Lite gives 4 bits each of red, green and blue, a 12-bit colour space.

The display pipeline pairs a pixel-coordinate generator — driven by the same horizontal/vertical counters that produce the sync pulses — with a colour generator that maps the current beam position to a register and bit and lights each cell according to whether that bit is 0 or 1. In the captured synthesised revision, a slice of register/stage state is also surfaced on the on-board LEDs (LEDR) as a lighter-weight readout — each write-back updates the visible value, which served as the primary debug view during bring-up.

Verification & Worked Example

The design was validated in Verilog simulation (Questa/ModelSim) before deployment. One testbench exercises the instruction-input subsystem in isolation; another wires all six core modules exactly as the top level and drives R-, I-, load- and store-type instructions straight into the loader — bypassing the UART so tests run quickly and deterministically.

top_level_tb.v · applying one R-type instruction
// add x4, x2, x2  (funct7=0, rs2=2, rs1=2, funct3=0, rd=4, opcode=0110011)
r_instruction = 32'b0000000_00010_00010_000_00100_0110011;
r_data_sent   = 1'b1;
#10;
r_data_sent   = 1'b0;
#100;  // let the instruction propagate through IF..WB

With the register-file seed values (x2 = 3), the expected result of add x4, x2, x2 is x4 = 6. Each stage raises its valid signal as it completes, and the write-back triggers the backward flush chain:

  1. IF — Fetch

    Fetch add x4, x2, x2 from program memory; raise o_data_ready.

  2. ID — Decode

    Read x2 = 3 twice, map to ALUop = ADD; raise dec_ins_ready.

  3. EX — Execute

    Compute 3 + 3 = 6 in the ALU; raise alu_ready.

  4. MEM — Memory

    No memory op (HOLD) — forward the result 6 unchanged; raise data_ready.

  5. WB — Write-Back

    Write x4 ← 6 and raise o_flush, releasing the upstream pipeline registers.

Results, Challenges & Future Work

The project achieved its central goal: a user can send a machine-code instruction over UART, have it processed by the RISC-V core, and observe the resulting architectural state. The handshake-based control model proved robust against the variable-latency serial front end — its primary purpose.

Results

  • ASCII instruction text received over 9600-baud UART and reassembled into 32-bit words
  • Words loaded, fetched, decoded, executed and written back through the five-stage pipeline
  • Register state surfaced on the board outputs for real-time observation

Challenges

  • UART ↔ core clock-domain sync — solved with two-flop edge detection (pattern 2'b01)
  • Reset-polarity mismatch between front-end and core modules
  • Flush progression — multi-instruction runs can need a manual SW[0] step
  • VGA synthesis lengthened Quartus compile times

Future Work

  • Branch (B-type) and jump (J-type) support with PC redirection
  • Full 32-register file and larger instruction/data memories
  • Byte- and half-word load/store end-to-end
  • Host-side assembler; data-forwarding and hazard detection