RISC-V Tetris

A computer inside the FPGA: a small RISC-V (RV32I) CPU written in Verilog, 32 KB of RAM, an 80 × 60 colour text screen and ten I/O registers. Then a toolchain for it, three Python files: an assembler, a small C compiler and an instruction-set simulator. Then Tetris, written in C, compiled by that compiler, running on that CPU on the board. The hardware was proven by running the same programs on the simulator and on the RTL and comparing every register write: identical over hundreds of thousands of instructions. New programs load over JTAG in six seconds, no rebuild.

Runs on the board Quartus Prime Lite 17.1 Shield required (screen, joystick) 10,658 logic elements (21 %) RV32I, 3.3 clocks per instruction Skill: build-soft-cpu
Download riscv_tetris.qar (full Quartus project) Build and load it

No bitstream in the repo (a .sof is 3 MB per project): restore the .qar or use the sources here, one build makes it in about three minutes. The RAM is initialised with the Tetris program (assets/tetris.mif), so the board plays right after loading. The user guide has every command, including how to write and run your own program.

Contents

  1. What you will see
  2. Block diagram
  3. The toolchain
  4. How an instruction flows
  5. The game
  6. Numbers
  7. How it was tested
  8. Review log
  9. Status and what is next
  10. Files

What you will see

The screen read back over USB: the Tetris well with an L and a T piece at the bottom, a cyan I piece falling, score 30, the next piece shown on the right, the controls listed on the left.
The screen read back over USB mid-game: the well, the falling I, the NEXT preview, SCORE / LEVEL / LINES, the controls. Every character is a cell the C program wrote into the text RAM.
Webcam photo of the DE10-Lite with the shield: the game on the LCD and 000030 on the 7-segment display.
The same moment on the board: the LCD, and the score on the 7-segment digits (the program writes it in BCD to a register).

Power up: the title screen, PUSH START. Press Start (or KEY0): pieces fall into the 10 × 20 well. The joystick moves (held: auto-repeat), Up or A rotates, Down drops softly, B slams the piece down, Start pauses. Lines clear, the level rises every ten lines and the pieces fall faster; the buzzer clicks and chirps. All of it is C code running on the soft CPU at about 7 million instructions per second, updating once per video frame.

The title screen read back over USB: RISC-V TETRIS, the empty well with a solid grey frame, START in the middle, the SoC facts on the right.
The title screen. The frame is drawn with the screen's own block glyph; the small print on the right is the CPU describing itself.

Block diagram

25 MHz: the SoC and the screen rv32_cpuRV32I, multi-cycleFETCH → EXEC → MEM32 registers in flip-flopstrace port for the testbench bus decodereq / ack0x0: RAM0x1: TEXT0x2: I/O soc_ram 32 KBaltsyncram, editable over JTAG text RAM 4800 × 16ram_dp: CPU writes, screen reads I/O registersBUTTONS FRAME RANDOMSWITCHES HEX LEDS BUZZERUART CYCLES EXIT text_screenfont8x8 + block glyphs, 16 colours Screens_dispaly (lib)pxl_x / pxl_y; LCD + VGA out rgb HEX5..0, LEDR, buzzer 50 MHz shield padjoystick, A B Start ADC IP+ gamepad debounce2 FF sync buttons USB letters (top)a b e s u d l r k → keys for 6 framesH hold CPU, R reset and run debug_link + tx_mux + jtag_uart'?' status, 'F' screen capturethe program's UART text USB-Blaster → your PC (tools/load.py, de10.py) rx keys UART tx JTAG: In-System Memory Content Editor writes the RAM (PROG) hold / reset

The toolchain

tetris.c+ lib.h, soc.h tools/cc.pyC subset → RV32I asm sw/crt0.s_start, __mul, __div tools/asm.pylabels, pseudo-ops build/x.hex+ x.mif, x.sym tools/rv32sim.pythe simulator: 10 s a game sim/tb_soc.svthe RTL in Icarus trace_compareidentical? tools/load.pyJTAG, 6 s, no rebuild assets/tetris.mif → Quartus build the boardde10.py send / screen

Everything a user needs is in the project folder and runs in the repo's env: no cross-compiler to download, nothing to install. The compiler accepts a C subset that is enough for games and drivers (its docstring lists exactly what, and it refuses the rest with a line number); the generated assembly is plain enough to read next to the C, which is the point of the course this BKM belongs to.

How an instruction flows

  1. FETCH. The CPU puts pc on the bus with req. The RAM answers one clock later with ack and the word; it becomes ir.
  2. EXEC. Everything is decoded from ir at once: the immediate, the two source registers, the ALU result, the branch condition, the next pc. ALU, branch and jump instructions finish here: the register is written (and a trace pulse tells the testbench), pc moves on, back to FETCH. Three clocks.
  3. MEM for loads and stores: the data address goes on the bus (with byte enables for sb / sh); at the ack a load's byte or half is picked and sign- or zero-extended into the register. Five clocks.
  4. The slaves. RAM, text RAM and I/O all answer in the same way (ack one clock after the request), so the decode is one case on addr[31:28]. The text RAM's second port is read by the screen 25 million times a second, independently.
  5. The program waits for the frame counter to change, polls the buttons, updates the game, writes the cells that changed. Tetris uses a few per cent of the CPU; the rest is spent spinning on IO_FRAME.

The game

sw/tetris.c is 420 lines of the C subset: the seven pieces as 4 × 4 bit masks with four rotations, a 7-bag randomiser fed by the RANDOM register, collision tests, delayed auto-shift for held moves, soft and hard drop, line clearing with the classic 40 / 100 / 300 / 1200 points, a level every ten lines, speed from the level, the NEXT preview, pause, game over. Each well cell is 2 × 2 characters (16 × 16 pixels) drawn with the screen's block glyph in the piece's colour, with a darker quarter for a bevel; the well is redrawn only when something moved. The score goes to the 7-segment digits in BCD, the level to the LEDs, sounds to the buzzer as half-periods.

Numbers

DeviceMAX 10 10M50DAF484C7G, Quartus Prime Lite 17.1
Logic10,658 logic elements (21 %), 6,435 registers, no multipliers (RV32I has none; the CPU is about 2,900 LEs of it, 1,024 of them the register file)
Memory1,277,696 bits (76 %): the 32 KB RAM (262,144), the text RAM (76,800), the debug screen capture (921,600; not needed to play), the LCD command ROM and the ADC store
CPURV32I, multi-cycle: 3.10 clocks per instruction on the ISA test, 3.31 on the C suite, 3.37 on Tetris; about 7.5 MIPS at 25 MHz
ProgramTetris: 20,352 bytes (62 % of the RAM) including the runtime; the C suite 12.7 KB; the compiler emits about 12 instructions per C line
Toolchaincc.py 1,320 lines, asm.py 440, rv32sim.py 340; all Python, standard library only
Clocks25 MHz SoC and VGA, 50 MHz ADC, 100 MHz LCD; timing met with the generated .sdc
Buildabout 3 minutes; 83 warnings, all from the library IP and unused pins
Program load6 s over JTAG (tools/load.py), versus a 3-minute rebuild

How it was tested

The toolchain, on the simulator

The hardware, in lock-step with the simulator

sim/tb_soc.sv runs the same images on the RTL CPU in Icarus and writes the same trace (every register write, every store). tools/trace_compare.py: ISA test identical, C suite identical, Tetris 294,803 lines identical. This is how the one real CPU bug was found in minutes: the arithmetic shift coming out logical. The testbench also dumps the UART text and the text screen, so a program's output can be read without a board.

On the board

Programmed, the title appeared (read back over USB). tools/load.py replaced the program in 6 s. Keys sent as USB letters started a game, moved and dropped pieces; the capture shows the well, the score (30) and the NEXT preview; the webcam shows the LCD and 000030 on the 7-segment digits.

Only a person can check

The joystick feel and the auto-repeat speed, the sounds, the pace of the levels, and the power cycle after a flash.

Review log

FoundCauseFix
A function returned to address 0x6E (the letter 'o')the compiler put locals at s0-4, where the prologue saves ralocals start below the saved ra / s0; seen in the simulator's trace
i > 0 loops never ran; comparisons with constants invertedthe constant-operand shortcut loaded the constant into the register meant for the left valueoperand order fixed; the C suite now covers every comparison form
10 + 20 * 2 did not parsethe shortcut accepted a constant followed by a tighter operatoronly operators of the same or looser precedence end a constant operand
4000000000u % 7 folded as signedconstant folding ignored the u suffixunsigned folding for / % >> and comparisons
srai gave a logical shift on the RTL (simulator right)?: with an unsigned branch made >>> logicalthe arithmetic shift on its own signed wire (gotcha 20)
cls took 300k instructions80 * 60 recomputed with a 32-step software multiply every iterationconstant folding; multiplies by small constants as shifts and adds: 10 software multiplies left in the game
Testbench syntax errorscell is a Verilog keyword; a declaration inside an unnamed blockrenamed; declared at module scope (gotcha 21)
First store of s0 traced as xxxxxxxxthe register file had no resetregisters reset to 0, like the simulator
The well's frame invisible in capturesthe checkerboard glyph vanishes at half resolutionframes drawn with the full block
A scripted game took 4.5 minutes to simulate95 % of the instructions were the wait-for-frame spinthe simulator skips a short backward loop that read FRAME (off while tracing): 10 s

Status and what is next

ItemStatus
CPU: every RV32I instruction, loads / stores of all sizes, traps on illegalverified: lock-step with the simulator, on the board
Assembler, C compiler, simulator, build and load scriptsverified: test suites, CI
Text screen, I/O registers, UART, JTAG program loadverified on the board
Tetris: pieces, rotation, drops, lines, levels, score, pause, game oververified: simulation (scripted) and board (USB keys)
Joystick feel, sounds, the paceto do by a person
Interrupts, a timer, CSRs, the M extension, structs and typedef in the compilernot built, by choice

Files

FileWhat it is
index.html, spec.html, guide.htmlthis overview, the architecture spec, the user guide
riscv_tetris.qarthe whole Quartus project (sources, settings, the program image)
src/rv32_cpu.svthe CPU
src/rv32_soc.sv, src/soc_ram.svthe bus decode, the I/O registers, the text RAM port, the UART; the editable 32 KB RAM
src/text_screen.sv80 × 60 cells to pixels
src/riscv_tetris_top.svthe board: clocks, display chain, pad, USB letters, hold / reset, 7-segment, buzzer
tools/memmap.pythe memory map; writes sw/soc.h
tools/asm.py, tools/cc.py, tools/rv32sim.pyassembler, C compiler, instruction-set simulator
tools/build.py, tools/load.py, tools/trace_compare.py, tools/play_test.pybuild an image; load it over JTAG; compare traces; play a scripted game
sw/tetris.c, sw/lib.h, sw/soc.h, sw/crt0.sthe game, the little library, the generated header, the runtime
sw/test_isa.s, sw/test_c.cthe ISA test, the compiler's test suite
sim/tb_soc.svruns any image on the RTL: trace, UART text, screen dump
assets/tetris.mif, assets/tetris.hexthe program image the FPGA build and the simulator use
media/the pictures on these pages