Project 02 · Digital Systems / FPGA
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
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.
| Tool / Technology | Role in the project |
|---|---|
| Intel Quartus Prime Lite | Synthesis, place-and-route, bitstream generation |
| Questa / ModelSim | RTL simulation and waveform inspection |
| Verilog HDL | Hardware description for all modules |
| DE10-Lite (Intel MAX 10) | Target FPGA development board |
| USB-to-UART converter | Serial bridge between host PC and FPGA |
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].
| Resource | Signal | Use |
|---|---|---|
| 50 MHz clock | MAX10_CLK1_50 | System clock for all logic |
| Push-button 0 | KEY[0] | Active-low reset |
| Slide switch 0 | SW[0] | Manual flush / single-step into IF |
| GPIO pin 2 | GPIO[2] | UART receive (RX) from host |
| GPIO pin 1 | GPIO[1] | UART transmit (TX) to host |
| Red LEDs | LEDR[9:0] | Debug / register-state readout |
| VGA connector | VGA_R/G/B, HS, VS | Register-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.
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.
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.
r_avail_instructions interlock stops the PC fetching past the last loaded word; coordinates four cooperating FSMs.
$signed; shift amounts masked to 5 bits per the RISC-V spec.
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:
// 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:
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.
| Opcode | Format | Class |
|---|---|---|
| 0110011 | R-type | Register–register ALU |
| 0010011 | I-type | Immediate ALU / shift |
| 0000011 | I-type | Load |
| 0100011 | S-type | Store |
The ALU implements 20 operations selected by a 5-bit internal opcode:
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.
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.
// 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:
Fetch add x4, x2, x2 from program memory; raise o_data_ready.
Read x2 = 3 twice, map to ALUop = ADD; raise dec_ins_ready.
Compute 3 + 3 = 6 in the ALU; raise alu_ready.
No memory op (HOLD) — forward the result 6 unchanged; raise data_ready.
Write x4 ← 6 and raise o_flush, releasing the upstream pipeline registers.
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.
2'b01)SW[0] step