RISC-V Snake Game
Crafted by Bob Bu · Winter 2022
RISC-V is an open instruction set architecture. This project implements Snake on RARS using user-mode interrupts and memory-mapped I/O. All gameplay I/O goes through the simulator's devices via fixed registers, and no read or print syscalls are used.

The program opens with a level selection, then draws a compact text arena with a solid border. The snake head is @ and the body is *** with fixed length. Movement is confined to the interior cells of the grid.
Motion advances on each interrupt. The timer provides the regular tick, and a key press updates direction with lowercase w a s d and, in this implementation, also triggers a step because both paths share the same handler flow. Three difficulty levels control the initial time budget and the per-apple bonus. After the snake reaches an apple, the step cost is applied and then the bonus is added as time = time - 1 + bonus. The run ends on a border collision or when the remaining time expires.
Devices are accessed through symbolic register names for clarity: keyboard control and data, display control and data, and the timer's time and comparison registers. The code uses these names consistently so the logic reads like an interface instead of raw addresses.

Interrupt control relies on CSRs. The trap vector goes into utvec, user interrupts are enabled in ustatus, and the timer and external lines are enabled in uie. Each timer service schedules the next tick by programming the comparison register relative to the current time.
The handler saves the registers it touches using the memory pointed to by uscratch and does not branch on ucause. Each trap advances the head, shifts the body, decrements the remaining time, checks walls, rearms the next timer tick, reads one ASCII value to update direction if it matches w a s d, re-enables the keyboard device, and returns with uret after restoring registers.
Apple placement uses a small linear congruential generator stored with global variables in common.s. It returns a compact integer range that maps directly to grid coordinates after a simple offset to stay inside the playable area.
Display write using symbolic MMIO names.eqv DISP_CTRL 0xFFFF0008.eqv DISP_DATA 0xFFFF000C.eqv BELL 0x07# a0 = char, a1 = row, a2 = colprintChar:# wait until the display reports ready.wait:lw t0, DISP_CTRLandi t0, t0, 1beqz t0, .wait# move print position (text cursor in the MMIO terminal)li t1, BELLslli t2, a1, 8slli t3, a2, 20or t1, t1, t2or t1, t1, t3sw t1, DISP_DATA # positionsw a0, DISP_DATA # drawret
This subroutine polls the display's ready flag, sets the text position by writing the bell value (row and column packed into the high bits), then writes the character. Labels keep the call sites focused on intent.
Initialization selects a level, draws the border, places the initial snake and apple, sets utvec, enables interrupts, programs the first timer event, and enables the keyboard device. The main loop stays idle while the handler drives updates, so state changes are interrupt-based. The final result is shown in the video below.

The RISC-V assembly source code is in my GitHub repository. Also, please visit this link to get RARS.
University of Alberta · Edmonton, AB, Canada