a GBA snake clone in ARM assembly
2026-05-16
running on my game boy micro
of the many good reasons to be upset about the LLM craze being pushed by the tech industry, the thing that irritates me on a personal level is the insistence that not only should it not be necessary to understand how computers work, but that such knowledge is now as outmoded as weaving by hand
partly in reaction to this (you say i shouldn't bother understanding the computer? well, i'm going to go Understand The Computer Even More!!) i recently started teaching myself assembly language - specifically, ARM assembly for the game boy advance, since it's a system i already have some experience with and enjoy programming for
for my first handwritten assembly program, i've written a snake clone (yes, again) for the GBA. you can download it here and play it on the emulator of your choice! :)
i'm not going to write a tutorial for ARM assembly on the GBA - more knowledgeable people have already written better ones than i could - but i am including including the full source for my snake clone at the bottom of this post. hopefully someone will find it helpful or interesting.
i based my project on GBA bootstrap, so if you want to build the game from source, you'll need to start by downloading that and installing the gcc-arm-none-eabi toolchain.
then:
- make a copy of the
template_cdirectory and name itagbsnake - within
agbsnake, deletesource/main.candsource/sys/syscalls.c- those are only necessary if you're writing a C program - open the
Makefilein a text editor and change the ROM config propertiesNAMEtosnakeandGAME_TITLEto"SNAKE" - create a new text file in
sourcenamedagbsnake.s - open
agbsnake.sin a text editor, copy the source code (below) into it, and save - run
maketo build!
source:
@ agbsnake.s
@ by adam le doux
@ (CC0-1.0: https://creativecommons.org/publicdomain/zero/1.0/)
@ graphics macros:
#define DISPLAY_CONTROL_REGISTER 0x04000000
#define VIDEO_MODE3 0x0003
#define VIDEO_BACKGROUND2 0x0400
#define VCOUNT_REGISTER 0x04000006
#define VCOUNT_MASK #0x000000FF
#define VBLANK #160
#define COLOR_WHITE 0x7FFF
#define COLOR_BLACK #0x0000
@ input macros:
#define INPUT_REGISTER 0x4000130
#define DPAD_NONE #0x0000
#define DPAD_UP #0x0040
#define DPAD_DOWN #0x0080
#define DPAD_LEFT #0x0020
#define DPAD_RIGHT #0x0010
#define DPAD_VERT #0x00C0
#define DPAD_HORZ #0x0030
@ sound macros:
#define SOUND_STATUS_REGISTER 0x4000084
#define SOUND_STATUS_ON 0x0080
#define SOUND_SAMPLE_REGISTER 0x4000082
#define SOUND_SAMPLE_CHIP025 0x0000
#define SOUND_SAMPLE_CHIP050 0x0001
#define SOUND_SAMPLE_CHIP100 0x0002
#define SOUND_CHIP_REGISTER 0x4000080
#define SOUND_CHIP_MODE_SQUARE2 0x02
#define SOUND_CHIP_VOLUME_MAX 7
#define SOUND_CHIP_MODE(mode) (((mode) << 12) | ((mode) << 8))
#define SOUND_CHIP_VOLUME(vol) (((vol) << 4) | (vol))
#define SQUARE2_CONTROL_REGISTER 0x4000068
#define SQUARE_DUTY_1_8 0x0
#define SQUARE_DUTY_1_4 0x1
#define SQUARE_DUTY_1_2 0x2
#define SQUARE_DUTY_3_4 0x3
#define SQUARE_VOLUME_MAX 15
#define SQUARE_DURATION_MAX 250
#define SQUARE_VOLUME(vol) ((vol) << 12)
#define SQUARE_DUTY(duty) (((duty) & 3) << 6)
#define SQUARE_DURATION(ms) ((64 - (((ms) + 6) >> 2)) & 63)
#define SQUARE2_FREQUENCY_REGISTER 0x400006C
#define FREQUENCY_OFF 0x0000
#define FREQUENCY_RESET 0x8000
#define FREQUENCY_TIMER_ON 0x4000
#define FREQUENCY(hz) (2048 - ((1 << 17) / (hz)))
@ game macros:
#define STAGE_WIDTH 30
#define STAGE_HEIGHT 20
#define GAME_UPDATE_WAIT_FRAMES #10
#define GAME_OVER_WAIT_FRAMES #30
#define SNAKE_ARRAY_SIZE (STAGE_WIDTH * STAGE_HEIGHT * 4)
@ text section: contains executable code
.text
@ vsync: syncs program execution with the vblank period
.global vsync
vsync:
@ load the address of the vcount register
ldr r0, =VCOUNT_REGISTER
@ if still in vblank, wait for the current vblank to end
.Lvblankend:
ldr r1, [r0]
and r1, r1, VCOUNT_MASK
cmp r1, VBLANK
bpl .Lvblankend
@ then wait for the next vblank to start
.Lvblankstart:
ldr r1, [r0]
and r1, r1, VCOUNT_MASK
cmp r1, VBLANK
bmi .Lvblankstart
@ return
bx lr
@ drawsqr: draw a 8x8 pixel square
@ arguments:
@ - r0: (x, y) position of the square on a 30x20 grid
@ - r1: (r, g, b) color of the square
.global drawsqr
drawsqr:
@ unpack the (x, y) coordinates into separate registers
ldr r3, =0x0000FFFF
and r2, r0, r3
mov r3, r0, lsr #16
@ each word of the screen buffer represents 2 pixels, so double the rgb color
add r1, r1, r1, lsl #16
@ construct the pixel index offset from the (x, y) coordinates
@ offset = (y * 240 pixels/row * 2 bytes/pixel * 8 rows) + (x * 2 bytes/pixel * 8 cols)
mov r0, #3840
mul r3, r0, r3
add r3, r3, r2, lsl #4
@ load the address of the screen buffer and add the offset
@ to get the start index of the first row of the square
ldr r0, =0x06000000
add r0, r0, r3
@ draw the 8 rows that make up the square
@ for each row, increment the index by 480
@ row increment = 240 pixels/row * 2 bytes/pixel
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
add r0, r0, #480
str r1, [r0]
str r1, [r0, #4]
str r1, [r0, #8]
str r1, [r0, #12]
@ return
bx lr
@ main: program execution starts here
.global main
main:
@ initialize graphics:
@ load the address of the display control register
ldr r0, =DISPLAY_CONTROL_REGISTER
@ combine the flags required to enable video mode 3 with background 2
ldr r1, =(VIDEO_MODE3 | VIDEO_BACKGROUND2)
@ enable video
str r1, [r0]
@ initialize sound:
@ enable sound
ldr r0, =SOUND_STATUS_REGISTER
ldr r1, =SOUND_STATUS_ON
str r1, [r0]
@ set mix to 100% soundchip-generated sound (disables sample-based sound)
ldr r0, =SOUND_SAMPLE_REGISTER
ldr r1, =SOUND_SAMPLE_CHIP100
str r1, [r0]
@ set soundchip channel 2 (square wave) to max volume for both speakers
ldr r0, =SOUND_CHIP_REGISTER
ldr r1, =(SOUND_CHIP_MODE(SOUND_CHIP_MODE_SQUARE2) | SOUND_CHIP_VOLUME(SOUND_CHIP_VOLUME_MAX))
str r1, [r0]
@ set the square wave settings for channel 2
ldr r0, =SQUARE2_CONTROL_REGISTER
ldr r1, =(SQUARE_VOLUME(SQUARE_VOLUME_MAX - 3) | SQUARE_DUTY(SQUARE_DUTY_1_2) | SQUARE_DURATION(150))
str r1, [r0]
@ clear out the channel 2 frequency so no sound is playing
ldr r0, =SQUARE2_FREQUENCY_REGISTER
ldr r1, =FREQUENCY_OFF
str r1, [r0]
@ initialize game state:
@ current snake direction (low 16 bits) + last d-pad input (high 16 bits)
mov r4, #0
@ frame counter to control speed of the snake
mov r5, GAME_UPDATE_WAIT_FRAMES
@ snake x and y position
mov r0, #15
mov r1, #10
add r6, r0, r1, lsl #16
@ head and tail indices for the snake's array of previous locations
mov r7, #0
mov r8, #0
@ current max length of the snake
mov r9, #3
@ food x and y position
mov r10, r6
@ random seed
mov r11, #1
@ draw starting position of snake
mov r0, r6
ldr r1, =COLOR_WHITE
push {lr}
bl drawsqr
pop {lr}
@ start main loop
.Lloop:
@ sync video frames
push {lr}
bl vsync
pop {lr}
@ get input from the d-pad:
@ first unpack the last dpad input and current snake direction into separate registers
ldr r0, =0x0000FFFF
and r1, r4, r0
mov r2, r4, lsr #16
@ then read the currently held buttons from the input register
ldr r0, =INPUT_REGISTER
ldr r3, [r0]
@ if the snake is moving and the current direction matches one of the held buttons, then skip updating input
cmp r1, #0
beq .Ldpadvert
and r0, r3, r1
cmp r0, #0
beq .Ldpadend
.Ldpadvert:
@ if already moving vertically, skip checking up/down
and r0, r1, DPAD_VERT
cmp r0, #0
bne .Ldpadhorz
@ check d-pad up
and r0, r3, DPAD_UP
cmp r0, #0
moveq r2, DPAD_UP
@ check d-pad down
and r0, r3, DPAD_DOWN
cmp r0, #0
moveq r2, DPAD_DOWN
.Ldpadhorz:
@ if already moving horizontally, skip checking left/right
and r0, r1, DPAD_HORZ
cmp r0, #0
bne .Ldpadend
@ check d-pad left
and r0, r3, DPAD_LEFT
cmp r0, #0
moveq r2, DPAD_LEFT
@ check d-pad right
and r0, r3, DPAD_RIGHT
cmp r0, #0
moveq r2, DPAD_RIGHT
.Ldpadend:
@ pack the dpad input and snake direction back into the same register
add r4, r1, r2, lsl #16
@ decrement frame counter
sub r5, r5, #1
cmp r5, #0
bne .Lloop
@ reset frame counter
mov r5, GAME_UPDATE_WAIT_FRAMES
@ if snake collides with food, pick new food position:
cmp r6, r10
bne .Lfoodend
@ generate next psuedorandom number
eor r11, r11, r11, lsl #7
eor r11, r11, r11, lsr #9
eor r11, r11, r11, lsl #8
@ split generated number in two to get x and y coordinates
ldr r0, =0x0000FFFF
and r1, r11, r0
mov r2, r11, lsr #16
@ ensure x-coordinate is on-screen
.Lfoodx:
cmp r1, #STAGE_WIDTH
bmi .Lfoody
sub r1, r1, #STAGE_WIDTH
b .Lfoodx
@ ensure y-coordinate is on-screen
.Lfoody:
cmp r2, #STAGE_HEIGHT
bmi .Lfoodxy
sub r2, r2, #STAGE_HEIGHT
b .Lfoody
@ pack x and y to make the combined food position
.Lfoodxy:
add r10, r1, r2, lsl #16
.Lfoodend:
@ wait for player input before starting game
cmp r4, #0x00000000
moveq r10, r6
beq .Lloop
@ update the snake's current direction
ldr r0, =0xFFFF0000
and r1, r4, r0
cmp r1, #0
movne r4, r4, lsr #16
@ store the snake's previous position
ldr r0, =snake
str r6, [r0, r7]
@ increment the head index of the snake
add r7, r7, #4
cmp r7, #SNAKE_ARRAY_SIZE
moveq r7, #0
@ move snake and detect wall collisions:
ldr r0, =0x0000FFFF
and r1, r6, r0
mov r2, r6, lsr #16
@ up
cmp r4, DPAD_UP
subeq r6, r6, #0x00010000
cmpeq r2, #0
beq .Lend
@ down
cmp r4, DPAD_DOWN
addeq r6, r6, #0x00010000
cmpeq r2, #(STAGE_HEIGHT - 1)
beq .Lend
@ left
cmp r4, DPAD_LEFT
subeq r6, r6, #0x00000001
cmpeq r1, #0
beq .Lend
@ right
cmp r4, DPAD_RIGHT
addeq r6, r6, #0x00000001
cmpeq r1, #(STAGE_WIDTH - 1)
beq .Lend
@ detect collision with self:
ldr r0, =snake
mov r1, r8
.Lcollision:
@ ensure index remains in bounds while looping
cmp r1, #SNAKE_ARRAY_SIZE
moveq r1, #0
@ load segment position and check for collision
ldr r2, [r0, r1]
cmp r2, r6
beq .Lend
@ increment segment index until reaching the head
cmp r1, r7
addne r1, r1, #4
bne .Lcollision
@ if snake collides with food:
cmp r6, r10
@ increase snake length,
addeq r9, r9, #1
@ and play a beep
ldreq r0, =SQUARE2_FREQUENCY_REGISTER
ldreq r1, =(FREQUENCY_RESET | FREQUENCY_TIMER_ON | FREQUENCY(440))
streq r1, [r0]
@ update snake tail:
@ compare the snake's current length with its max length
sub r1, r7, r8
cmp r1, #0
addmi r1, r1, #SNAKE_ARRAY_SIZE
cmp r1, r9, lsl #2
@ if it's not max length, skip erasing the end of the tail
bne .Ltailend
@ otherwise, erase its last segment and increment its tail index
ldr r2, =snake
ldr r0, [r2, r8]
mov r1, COLOR_BLACK
push {lr}
bl drawsqr
pop {lr}
add r8, r8, #4
cmp r8, #SNAKE_ARRAY_SIZE
moveq r8, #0
.Ltailend:
@ draw head of the snake
mov r0, r6
ldr r1, =COLOR_WHITE
push {lr}
bl drawsqr
pop {lr}
@ draw food
mov r0, r10
ldr r1, =COLOR_WHITE
push {lr}
bl drawsqr
pop {lr}
@ continue main loop
b .Lloop
@ exit main loop
.Lend:
@ play low beep on game over
ldr r0, =SQUARE2_FREQUENCY_REGISTER
ldr r1, =(FREQUENCY_RESET | FREQUENCY_TIMER_ON | FREQUENCY(220))
str r1, [r0]
@ brief pause before clearing screen
mov r5, GAME_OVER_WAIT_FRAMES
.Lwait:
push {lr}
bl vsync
pop {lr}
sub r5, r5, #1
cmp r5, #0
bne .Lwait
@ erase food
mov r0, r10
mov r1, COLOR_BLACK
push {lr}
bl drawsqr
pop {lr}
@ erase body of snake
.Lerase:
@ ensure tail index remains in bounds while looping
cmp r8, #SNAKE_ARRAY_SIZE
moveq r8, #0
@ draw a black square at the tail position
ldr r2, =snake
ldr r0, [r2, r8]
mov r1, COLOR_BLACK
push {lr}
bl drawsqr
pop {lr}
@ keep incrementing tail index until reaching the head
cmp r8, r7
addne r8, r8, #4
bne .Lerase
@ return from main
mov r0, #0
bx lr
@ bss (block starting symbol) section: contains zero-initialized data
.bss
@ make space for an array to store the positions of the snake trail
@ array size = (600 entries) * 4 bytes/word
snake:
.space SNAKE_ARRAY_SIZE