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

  1. What you need
  2. Build and load it
  3. Play
  4. Play and test over USB
  5. Write your own program
  6. Run it on the simulator
  7. Load it into the board in six seconds
  8. Run it on the RTL, in lock-step
  9. Keep it after power-off
  10. Change the hardware
  11. When something looks wrong

What you need

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

ControlDoes
Start (shield) or KEY0starts a game; after GAME OVER, the next one
Joystick left / rightmoves the piece; held, it repeats after a short delay
Up or Arotates (with a sideways kick if the wall is in the way)
Downsoft drop, one point per cell
Bhard drop, two points per cell
Start during playpause / resume
7-segment, LEDsthe score; one LED per level
Buzzera 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 wantWhere
a new I/O registeradd 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 fonttext_screen.sv (cell = pixel >> 3), memmap.py (TEXT_COLS / ROWS), the text RAM depth in rv32_soc.sv
a faster CPUpipeline 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 RAMdrop the debug capture (debug_link) and double soc_ram: numwords 16384, widthad 14, RAM_SIZE in memmap.py, the stack top in crt0.s
interruptsa 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 seeLikelyDo
LEDR9 blinking, nothing movesthe 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 addressa corrupt return address: a local array overrun, a pointer gone wrongrun with --trace, find the last write of register 1 (ra) and its pc in the .sym file
"error: file.c:N: ..." from the compilera construct outside the subsetthe 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 boarda value read from FRAME / RANDOM / CYCLES differs, or the program wrote beyond the 4,800 cellsrun the RTL testbench with the same image and compare the .screen dump
load.py says the memory write did not report OKthe board is not running this bitstream, or Quartus' JTAG server is busy (another terminal)de10.py program first; close other JTAG users
No soundthe program writes BUZZER 0, or the shield's buzzer is on another linecheck ARDUINO_IO[12] in the top against your shield
The game is too fast or too slowframes_per_drop in tetris.cchange it, build.py, load.py: ten seconds