RISC-V Tetris: user guide
From the repo to Tetris on your board, then to your own program on the soft CPU. Every command runs from the repo's
root folder in the env virtualenv (the project's own commands from the project folder); the chat does them for you if you ask.
Contents
- What you need
- Build and load it
- Play
- Play and test over USB
- Write your own program
- Run it on the simulator
- Load it into the board in six seconds
- Run it on the RTL, in lock-step
- Keep it after power-off
- Change the hardware
- When something looks wrong
What you need
- A DE10-Lite with the LCD / game-pad shield (the screen and the joystick).
- Quartus Prime Lite 17.1 installed as in stage 1 of the BKM (
python scripts/doctor.pyshows no FAIL). - The repo cloned and
envcreated. The toolchain needs nothing else: it is Python inprojects/riscv_tetris/tools/. - For the RTL tests on your PC: Icarus Verilog (the OSS CAD Suite the BKM installs).
Build and load it
python scripts/de10.py build projects/riscv_tetris # about 3 minutes; the RAM is initialised with Tetris
python scripts/de10.py program projects/riscv_tetris # loads it over the USB-Blaster; runs until power-off
From the archive instead: Quartus Project → Restore Archived Project → riscv_tetris.qar, compile, program.
What you should see: the title screen, RISC-V TETRIS, the empty well with a grey frame, a green START in the middle,
000000 on the digits. LEDR9 dark (it blinks only while the CPU is held or halted).
Play
| Control | Does |
|---|---|
| Start (shield) or KEY0 | starts a game; after GAME OVER, the next one |
| Joystick left / right | moves the piece; held, it repeats after a short delay |
| Up or A | rotates (with a sideways kick if the wall is in the way) |
| Down | soft drop, one point per cell |
| B | hard drop, two points per cell |
| Start during play | pause / resume |
| 7-segment, LEDs | the score; one LED per level |
| Buzzer | a click per piece, a chirp per rotation, a note per cleared line (lower for four), a long one at game over |
Lines score 40 / 100 / 300 / 1200 × (level + 1); every ten lines a level; the fall gets faster each level.
Play and test over USB
python scripts/de10.py send s # Start; also a b e(Select) u d l r, and k = KEY0
python scripts/de10.py send l # each letter holds the key for 6 frames
python scripts/de10.py screen x.png # the screen as the board shows it (320 x 240)
python scripts/de10.py log 5 # what the program prints on its UART (puts / putnum in lib.h)
python scripts/de10.py send "?" # the debug link's status line
Write your own program
Copy sw/tetris.c or start from this, in projects/riscv_tetris/sw/hello.c:
#include "soc.h"
#include "lib.h"
int main(void)
{
int n = 0;
cls(1); /* blue background */
put_str(2, 30, "HELLO FROM RISC-V", 14, 1);
puts("hello over the UART/n");
while (1) {
wait_frame();
keys_poll();
if (keys_pressed & BTN_A)
n++;
put_num(4, 30, n, 6, 15, 1);
IO_HEX = to_bcd(n);
IO_LEDS = keys_now;
}
return 0;
}
What the compiler accepts is listed at the top of tools/cc.py and in the spec: int,
char, unsigned, pointers, arrays, globals with initialisers, functions, all control flow, all operators but the comma,
#define and #include. Not structs, typedef, float. It tells you with a line number. The helpers in sw/lib.h:
putc puts putnum puthex (UART), cls put_cell put_str put_num (screen), keys_poll (keys_now,
keys_pressed), to_bcd, wait_frame. The registers and macros are in the generated sw/soc.h:
IO_FRAME, IO_RANDOM, IO_BUZZER, TEXT_AT(row, col) = CELL(ch, fg, bg), BLOCK_FULL...
cd projects/riscv_tetris
python tools/build.py sw/hello.c # -> build/hello.hex .mif .s .sym (the .s is readable: look at it)
Assembly works too: python tools\build.py sw\my.s (no runtime; start at address 0; sw/test_isa.s is the model).
Run it on the simulator
python tools/rv32sim.py build/hello.hex --frames 120 --keys "10:a,20:a,30:a" --screen out.txt --uart
type out.txt # the 80 x 60 screen as text (block glyphs as # + : [)
It stops at EXIT, after --frames, or after --max instructions, and prints the instruction count, the
digits and the UART text. --trace x.trc writes every register write and store (and turns off the idle skip);
--seed sets the RANDOM register.
Load it into the board in six seconds
python tools/load.py build/hello.mif # 'H', write the RAM over JTAG, 'R': the new program runs
The FPGA stays as it is; only the RAM content changes. (The board must be running this project's bitstream; at the
next power-up the RAM holds Tetris again, unless you rebuild with your image in assets/: tools\build.py sw\hello.c --assets
then edit PROG_FILE in src/riscv_tetris_top.sv, or just replace assets/tetris.mif.)
Run it on the RTL, in lock-step
python tools/rv32sim.py build/hello.hex --max 300000 --trace build/hello.trc
cd ../..
python scripts/sim.py projects/riscv_tetris/sim/tb_soc.sv -D PROG="../build/hello.hex" -D TRACE="../build/hello_rtl.trc" -D MAXCYCLES=1200000
python projects/riscv_tetris/tools/trace_compare.py projects/riscv_tetris/build/hello.trc projects/riscv_tetris/build/hello_rtl.trc
The testbench also writes hello_rtl.trc.uart and hello_rtl.trc.screen. Icarus runs about 1 million clocks
in a minute, so give the RTL a short program or a --max that matches. The repo's tests do this for the ISA test
and the C suite: python -m pytest tests/test_riscv_tetris.py.
Keep it after power-off
python scripts/de10.py flash projects/riscv_tetris # writes and verifies the .pof, about 15 s; the chat asks first
Change the hardware
| You want | Where |
|---|---|
| a new I/O register | add it to tools/memmap.py (run it: it rewrites sw/soc.h), decode it in rv32_soc.sv (one case line for write, one for read), model it in rv32sim.py's io_read / io_write; pytest checks the three agree |
| a different screen size or font | text_screen.sv (cell = pixel >> 3), memmap.py (TEXT_COLS / ROWS), the text RAM depth in rv32_soc.sv |
| a faster CPU | pipeline FETCH with EXEC (the ir register is already the boundary), or add an M extension: a mul on the MAX 10's multipliers and the compiler emits mul instead of call __mul |
| more RAM | drop the debug capture (debug_link) and double soc_ram: numwords 16384, widthad 14, RAM_SIZE in memmap.py, the stack top in crt0.s |
| interrupts | a pending bit from FRAME, a vector at 0x10, a save of pc into a register on entry: about 40 lines in the CPU and mret |
After any hardware change: pytest tests/test_riscv_tetris.py (the lock-step suites), then the Quartus build.
When something looks wrong
| You see | Likely | Do |
|---|---|---|
| LEDR9 blinking, nothing moves | the CPU is halted: an illegal instruction (a jump into data), or held by 'H' | de10.py send R; in the simulator the same program reports "illegal instruction at pc": look it up in build/x.sym |
| The simulator says "fetch from 0x...6e" or an odd address | a corrupt return address: a local array overrun, a pointer gone wrong | run with --trace, find the last write of register 1 (ra) and its pc in the .sym file |
| "error: file.c:N: ..." from the compiler | a construct outside the subset | the message says what; the spec lists the alternatives (no struct: parallel arrays; no local array initialiser: assign in a loop) |
| The screen is right in the simulator, wrong on the board | a value read from FRAME / RANDOM / CYCLES differs, or the program wrote beyond the 4,800 cells | run the RTL testbench with the same image and compare the .screen dump |
load.py says the memory write did not report OK | the board is not running this bitstream, or Quartus' JTAG server is busy (another terminal) | de10.py program first; close other JTAG users |
| No sound | the program writes BUZZER 0, or the shield's buzzer is on another line | check ARDUINO_IO[12] in the top against your shield |
| The game is too fast or too slow | frames_per_drop in tetris.c | change it, build.py, load.py: ten seconds |