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:

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