Your first SNES ROM. Nine tiles, two colors, one message. Welcome to 1990.
Then open hello_world.sfc in your emulator (Mesen2 recommended).
No interactive controls. The text is displayed statically.
printf("HELLO WORLD") on a Super NintendoOn a modern computer, printing text is one line of code. On the SNES, there is no text system. There's no font. There's no framebuffer. The PPU (Picture Processing Unit) only understands one thing: tiles.
A tile is an 8x8 pixel image stored in VRAM. A tilemap is a grid that says "put tile #3 here, tile #7 there." That's it — that's the entire display system. If you want letters on screen, you need to draw each letter as a tile, upload them to VRAM, and then write a tilemap that spells out your message.
That's exactly what this example does.
Each letter is hand-encoded as a 16-byte tile in 2bpp format (2 bits per pixel = 4 possible colors). Every pair of bytes is one row of 8 pixels:
Take 0x66 = binary 01100110. Each 1 is a lit pixel, each 0 is background. Draw it out and you'll see two vertical bars — the sides of the letter H. The 0x7E row (01111110) is the crossbar.
Why are all the second bytes
0x00? In 2bpp format, byte 1 is bitplane 0 and byte 2 is bitplane 1. We only use color 1 (bitplane 0 = 1, bitplane 1 = 0), so the second byte is always zero. If you set both to0xFF, you'd get color 3 instead. That's how the SNES packs multiple color choices into minimal memory.
Before writing anything to VRAM, you need to tell the PPU where to find things:
Mode 0 gives you four background layers, each with 4 colors (2bpp). For a simple text display, one layer is plenty. BG1SC = 0x04 tells the PPU "the tilemap for
BG1 starts at VRAM word address `$0400`", and BG12NBA = 0x00 says "the actual tile
graphics start at `$0000`."
Why two separate addresses? Because tiles and tilemaps are different data. Tiles are the pixel art. The tilemap is the layout — which tile goes where on screen. They can live anywhere in VRAM, and you choose where by setting these registers. This separation is what makes the SNES so flexible: you can reuse the same tiles with different layouts, or swap layouts without reloading tiles.
VRAM isn't regular memory — you can't just write to it with a pointer. You talk to it through a pair of registers: set the address, then write data byte by byte:
VRAM is organized in 16-bit words. VMAIN = 0x80 means "auto-increment the address
after I write the high byte." So you write low, write high, and the address moves forward by one word. 144 bytes = 9 tiles × 16 bytes each = our complete font.
The SNES palette uses 15-bit BGR color (5 bits per channel, little-endian):
Color 0 is the background, color 1 is the text. Four bytes, two colors. That's all we need for a monochrome font.
The message is encoded as an array of tile indices:
Tile 1 = H, tile 2 = E, tile 3 = L, and so on. To place this at row 14, column 10, we calculate the tilemap address:
Each tilemap entry is 2 bytes: the low byte picks the tile, the high byte holds attributes (palette, flip, priority). We set them all to 0 — palette 0, no flipping.
What's
0x05CA? The tilemap base is$0400. Each row is 32 entries wide. Row 14 starts at$0400 + 14×32 = $0400 + $01C0 = $05C0. Column 10 adds 10 more:$05CA. That math becomes second nature after a few tilemaps.
TM (screen designation) controls which layers are visible. We enable BG1 only. setScreenOn() sets brightness to maximum. The infinite loop with WaitForVBlank() keeps the program alive and synced to 60fps — without it, the CPU would spin wildly and cause timing issues.
REG_TM has the right layer enabled. If BG1 isn't in the TM register, the PPU won't draw it, no matter what's in VRAM.0x66 byte and draw it on graph paper. Binary: 01100110. Mark the 1s. Do all 8 rows of the H tile. You'll see the letter form before your eyes.setMode(BG_MODE0, 0) and not Mode 1? Mode 0 gives 4 layers of 2bpp (4 colors each). Mode 1 gives 2 layers of 4bpp + 1 layer of 2bpp. For monochrome text, Mode 0 is the natural fit — and you get 3 extra layers free if you want them later.font_tiles[].cc65816 is the C compiler — it's actually two programs stitched together: cproc (a C11 frontend) parses your C code into intermediate representation, then QBE (with a custom 65816 backend) turns that IR into assembly. wla-65816 assembles it into an object file. Finally, wlalink links everything — your compiled code, the startup code (crt0.asm), math helpers (runtime.asm), and library modules — into a playable .sfc ROM.
CSRC can list multiple C files (e.g., CSRC := main.c engine/player.c). For this simple example, a single file is sufficient. You can also add assembly source files via ASMSRC.
| Module | Why it's here |
|---|---|
console | Provides consoleInit() and WaitForVBlank(). Sets up Mode 1, the NMI handler, and basic PPU config. |
sprite | OAM buffer management. Even with no sprites on screen, the NMI handler DMAs the OAM buffer every frame — if the sprite module is missing, the linker can't find the buffer symbols. |
dma | Provides dmaCopyVram() and dmaCopyCGram(). Without it, you'd write tile data byte-by-byte through registers (which we actually do in this example, but the library's init code uses DMA internally). |
**"But there are no sprites in Hello World!"** Correct. The dependency chain is:
console→sprite→dma. The NMI handler (in console) always DMAs the OAM buffer (in sprite) to the PPU, and that DMA call lives in the dma module. The build system does NOT auto-resolve these dependencies — if you forgetdma, you get a linker error about an undefineddmaCopyOamsymbol and no hint about why.
| Register | Address | Role in this example |
|---|---|---|
| BGMODE | $2105 | Mode 0 (4 layers, all 2bpp) |
| BG1SC | $2107 | BG1 tilemap at $0400 |
| BG12NBA | $210B | BG1 tile data at $0000 |
| VMAIN | $2115 | VRAM auto-increment mode |
| VMADDL/H | $2116-17 | VRAM write address |
| VMDATAL/H | $2118-19 | VRAM data port |
| CGADD | $2121 | Palette address |
| CGDATA | $2122 | Palette data |
| TM | $212C | Enable BG1 on main screen |
| File | What's in it |
|---|---|
main.c | Everything — tiles, palette, tilemap, main loop (~136 lines) |
Makefile | LIB_MODULES := console sprite dma background |