Font Sprites

CHIP-8 defines 16 built-in sprites for the hexadecimal digits 0–F. Every interpreter must provide them — programs rely on these sprites to draw numbers on screen without embedding their own.

Sprite format

Each digit is 5 bytes tall and 4 pixels wide. The pixels occupy the upper 4 bits of each byte (the lower 4 are always zero):

digit 0:          digit A:
0xF0  ████░░░░    0xF0  ████░░░░
0x90  █░░█░░░░    0x90  █░░█░░░░
0x90  █░░█░░░░    0xF0  ████░░░░
0x90  █░░█░░░░    0x90  █░░█░░░░
0xF0  ████░░░░    0x90  █░░█░░░░

Memory layout

Fonts are loaded into 0x000 at startup — before any ROM. The address of digit N is simply N × 5:

0x000  digit 0  (5 bytes)
0x005  digit 1
0x00A  digit 2
...
0x04B  digit F

Loading in Memory::new()

#![allow(unused)]
fn main() {
const FONTS: [u8; 80] = [
    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
    0x20, 0x60, 0x20, 0x20, 0x70, // 1
    0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
    // ... 13 more digits
];

mem.ram[FONT_START..FONT_START + FONTS.len()].copy_from_slice(&FONTS);
}

Every Memory instance has fonts loaded from the very first instruction — they are part of initialization, not an optional setup step.

Opcode FX29 — LD F, Vx

Sets I to the address of the font sprite for digit Vx:

#![allow(unused)]
fn main() {
0x29 => {
    self.i = (self.v[x] as usize * FONT_SPRITE_SIZE) as u16;
}
}

After this, DRW with N=5 draws that digit on screen.

Typical usage in a program

LD  V0, 0xA    ; digit to draw = A (10)
LD  F,  V0     ; I = address of 'A' sprite = 0x032
LD  V1, 10     ; x position
LD  V2, 5      ; y position
DRW V1, V2, 5  ; draw 5-row sprite at (10, 5)

The test program in main.rs uses exactly this pattern to draw all 16 digits across the screen.


Rust concepts used here: copy_from_slice with a range is covered in Slices and Ranges. The constant array FONTS uses the array syntax from Primitive Types and Arrays. See DRW — Drawing Sprites for how sprites are read from memory and drawn using I.