Project 16: ARM-Based Retro Game Console (Capstone)

Build a complete handheld game console with color LCD display, audio output, button input, game ROM loading from SD card, and a playable game—the ultimate ARM mastery project that integrates everything you’ve learned.


Quick Reference

Attribute Value
Language C + ARM Assembly
Difficulty Master
Time 2-3 months
Prerequisites Most previous projects (3-13)
Key Topics Graphics, Audio, Input, Integration
Coolness Level 5: Pure Magic (Super Cool)
Portfolio Value Micro-SaaS / Pro Tool
Hardware STM32F4 + LCD + Buttons + Speaker

Learning Objectives

By completing this project, you will:

  1. Integrate all ARM skills: Combine bare-metal, peripherals, DMA, timers, interrupts
  2. Build a graphics system: Double-buffered framebuffer, sprite rendering, tile maps
  3. Implement real-time audio: Sound effects and music mixed during gameplay
  4. Handle real-time input: Button debouncing and responsive game controls
  5. Manage limited resources: Fit game in constrained Flash/RAM
  6. Design a complete system: Hardware and software working together seamlessly
  7. Create something tangible: A physical device you built from scratch

The Core Question You’re Answering

“How do you build a complete embedded system that handles graphics, audio, and input simultaneously in real-time while maintaining consistent frame rates—and how does this integrate everything learned about ARM architecture?”

This capstone project forces you to bring together every skill: bare-metal initialization, peripheral configuration, DMA for performance, interrupt handling for responsiveness, timing for smooth animation, and memory management for limited resources. The result is a working game console—tangible proof of ARM mastery.


Why This Is the Capstone

This project integrates concepts from nearly every previous project:

Previous Project How It’s Used Here
P3: Bare-Metal LED System initialization, GPIO for buttons/LCD
P4: UART Driver Debug console during development
P5: Memory Allocator Game asset memory management
P7: Context Switcher Game loop timing (optional RTOS)
P10: I2C OLED Display communication concepts
P11: SPI SD Card Loading game ROMs and assets
P12: PWM Motor Audio output via PWM (alternative to DAC)
P13: DMA Audio Sound effects and music playback
Integration Map:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                      Game Console                               │
│                                                                 │
│   ┌─────────────┐                                               │
│   │ Project 3   │──▶ GPIO initialization, bare-metal boot       │
│   └─────────────┘                                               │
│   ┌─────────────┐                                               │
│   │ Project 11  │──▶ SD card reading for game assets            │
│   └─────────────┘                                               │
│   ┌─────────────┐                                               │
│   │ Project 13  │──▶ DMA audio for sound effects                │
│   └─────────────┘                                               │
│   ┌─────────────┐                                               │
│   │ Project 5   │──▶ Memory management for sprites              │
│   └─────────────┘                                               │
│   ┌─────────────┐                                               │
│   │ Project 7   │──▶ Timing for consistent frame rate           │
│   └─────────────┘                                               │
│                                                                 │
│   Combined into a single, cohesive system!                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Concepts You Must Understand First

Before starting this project, ensure you understand these concepts:

Concept Why It Matters Where to Learn
SPI communication LCD and SD card both use SPI Project 11
DMA transfers Fast framebuffer-to-LCD without CPU Project 13
Timer interrupts Consistent game loop timing Project 7, 12
GPIO input Button reading with debouncing Project 3
Audio output Sound effects during gameplay Project 13
Memory management Loading/unloading game assets Project 5

Key Concepts Deep Dive

  1. Game Loop Architecture
    • What are the three phases of a game loop (input, update, render)?
    • How do you maintain consistent frame rate?
    • What is the difference between fixed and variable timestep?
    • How do you handle slow frames?
    • “Game Programming Patterns” Chapter 1
  2. Double Buffering for Graphics
    • Why is double buffering necessary?
    • How do you avoid screen tearing?
    • How do you synchronize buffer swapping with LCD refresh?
    • What is the memory cost of double buffering?
    • “Game Programming Patterns” Chapter 8
  3. Sprite Rendering
    • How do you represent sprites in memory?
    • What is a sprite sheet and why use one?
    • How do you handle transparency?
    • How do you optimize sprite blitting?
    • “Computer Graphics from Scratch”
  4. Real-Time Audio Mixing
    • How do you play multiple sounds simultaneously?
    • What is audio mixing and how does it work?
    • How do you prevent clipping when mixing?
    • How do you synchronize audio with gameplay?
    • “The Audio Programming Book”
  5. Resource Management
    • How do you fit a game in limited Flash/RAM?
    • What is asset streaming and when is it needed?
    • How do you organize game data efficiently?
    • How do you handle loading screens?
    • “Making Embedded Systems” Chapter 8

Theoretical Foundation

Game Console Architecture

A complete game console needs these components:

Game Console Block Diagram:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │                      STM32F4 MCU                         │  │
│   │  ┌───────────────────────────────────────────────────┐  │  │
│   │  │                    Game Engine                     │  │  │
│   │  │   ┌─────────┐  ┌─────────┐  ┌─────────┐           │  │  │
│   │  │   │  Input  │──│  Update │──│  Render │           │  │  │
│   │  │   └─────────┘  └─────────┘  └─────────┘           │  │  │
│   │  └───────────────────────────────────────────────────┘  │  │
│   │                         │                                │  │
│   │  ┌──────────┬───────────┼───────────┬──────────┐        │  │
│   │  │          │           │           │          │        │  │
│   │  ▼          ▼           ▼           ▼          ▼        │  │
│   │ GPIO      Timer       DMA         SPI        DAC       │  │
│   │ (buttons) (60Hz)    (transfer)  (LCD/SD)   (audio)     │  │
│   └──────┬────────┬─────────┬──────────┬────────────┬──────┘  │
│          │        │         │          │            │         │
│   ┌──────┴──────┐ │         │   ┌──────┴───────┐   │         │
│   │   Buttons   │ │         │   │    SD Card   │   │         │
│   │ ←↑↓→ A B    │ │         │   │  (game data) │   │         │
│   └─────────────┘ │         │   └──────────────┘   │         │
│                   │         │                      │         │
│            ┌──────┴─────────┴──────┐         ┌─────┴─────┐   │
│            │     LCD Display       │         │  Speaker  │   │
│            │     320x240 RGB       │         │  (audio)  │   │
│            └───────────────────────┘         └───────────┘   │
│                                                               │
└───────────────────────────────────────────────────────────────┘

The Game Loop

The fundamental structure of any real-time game:

Game Loop Structure:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   void game_loop(void) {                                        │
│       uint32_t last_time = get_ms();                            │
│                                                                 │
│       while (1) {                                               │
│           // 1. INPUT: Read button state                        │
│           uint8_t buttons = read_buttons();                     │
│                                                                 │
│           // 2. UPDATE: Game logic                              │
│           game_update(buttons);                                 │
│                                                                 │
│           // 3. RENDER: Draw to back buffer                     │
│           render_game();                                        │
│                                                                 │
│           // 4. SWAP: Send back buffer to LCD                   │
│           swap_buffers();                                       │
│                                                                 │
│           // 5. TIMING: Maintain 60 FPS                         │
│           uint32_t elapsed = get_ms() - last_time;              │
│           if (elapsed < 16) {                                   │
│               delay_ms(16 - elapsed);                           │
│           }                                                     │
│           last_time = get_ms();                                 │
│       }                                                         │
│   }                                                             │
│                                                                 │
│   Target: 60 FPS = 16.67ms per frame                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Framebuffer and Double Buffering

Prevent visual artifacts with double buffering:

Double Buffering:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Memory:                                                       │
│   ┌─────────────────────┐   ┌─────────────────────┐            │
│   │    Buffer A         │   │    Buffer B         │            │
│   │  (being displayed)  │   │   (being drawn)     │            │
│   └─────────────────────┘   └─────────────────────┘            │
│            │                          ▲                         │
│            │                          │                         │
│            ▼                     CPU draws                      │
│   ┌─────────────────────┐        sprites                       │
│   │       LCD           │        and tiles                     │
│   │  (shows Buffer A)   │                                      │
│   └─────────────────────┘                                      │
│                                                                 │
│   After frame complete:                                         │
│   ┌─────────────────────┐   ┌─────────────────────┐            │
│   │    Buffer A         │   │    Buffer B         │            │
│   │   (being drawn)     │   │  (being displayed)  │            │
│   └─────────────────────┘   └─────────────────────┘            │
│            ▲                          │                         │
│       CPU draws                       │                         │
│       sprites                         ▼                         │
│       and tiles              ┌─────────────────────┐           │
│                              │       LCD           │           │
│                              │  (shows Buffer B)   │           │
│                              └─────────────────────┘           │
│                                                                 │
│   Swap pointers, not data (fast!)                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

LCD Communication (ILI9341 Example)

The LCD uses SPI with some additional control signals:

LCD Interface:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   STM32                         ILI9341 LCD                     │
│   ─────                         ──────────                      │
│   SPI_SCK  ────────────────────▶ CLK                            │
│   SPI_MOSI ────────────────────▶ SDI (data in)                  │
│   GPIO_CS  ────────────────────▶ CS (chip select)               │
│   GPIO_DC  ────────────────────▶ D/C (data/command)             │
│   GPIO_RST ────────────────────▶ RESET                          │
│                                                                 │
│   D/C pin selects:                                              │
│   • LOW  = Command mode (sending LCD commands)                  │
│   • HIGH = Data mode (sending pixel data)                       │
│                                                                 │
│   Pixel format: RGB565 (16 bits per pixel)                      │
│   ┌─────────────────────────────────────────┐                  │
│   │ R4 R3 R2 R1 R0 │ G5 G4 G3 G2 G1 G0 │ B4 B3 B2 B1 B0 │      │
│   │    5 bits red  │     6 bits green   │    5 bits blue │      │
│   └─────────────────────────────────────────┘                  │
│                                                                 │
│   Resolution: 320 x 240 pixels                                  │
│   Buffer size: 320 * 240 * 2 = 153,600 bytes (~150 KB)          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Sprite System

Sprites are the visual building blocks of 2D games:

Sprite System:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Sprite Structure:                                             │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ x, y: position on screen                                 │  │
│   │ width, height: size in pixels                            │  │
│   │ pixels[]: RGB565 color data                              │  │
│   │ transparent_color: color to skip when drawing            │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   Sprite Sheet:                                                 │
│   ┌─────────────────────────────────────────────────────┐      │
│   │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐    │      │
│   │ │ 0 │ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │ │ 7 │    │      │
│   │ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘    │      │
│   │ Walk animation frames ─────▶                        │      │
│   │ ┌───┐ ┌───┐ ┌───┐ ┌───┐                            │      │
│   │ │ 8 │ │ 9 │ │10 │ │11 │                            │      │
│   │ └───┘ └───┘ └───┘ └───┘                            │      │
│   │ Jump animation frames ─────▶                        │      │
│   └─────────────────────────────────────────────────────┘      │
│                                                                 │
│   Blitting (drawing sprite to framebuffer):                     │
│   for each pixel in sprite:                                     │
│       if pixel != transparent:                                  │
│           framebuffer[y * 320 + x] = pixel                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Audio Mixing

Playing multiple sounds simultaneously:

Audio Mixing:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Sound Channels:                                               │
│   ┌─────────────┐ ┌─────────────┐ ┌─────────────┐              │
│   │ Channel 0   │ │ Channel 1   │ │ Channel 2   │              │
│   │ (music)     │ │ (jump SFX)  │ │ (coin SFX)  │              │
│   │ sample: 100 │ │ sample: 50  │ │ sample: 75  │              │
│   └──────┬──────┘ └──────┬──────┘ └──────┬──────┘              │
│          │               │               │                      │
│          └───────────────┼───────────────┘                      │
│                          │                                      │
│                          ▼                                      │
│                    ┌───────────┐                                │
│                    │   Mixer   │                                │
│                    │ (100+50+75)│                               │
│                    │   / 3     │                                │
│                    └─────┬─────┘                                │
│                          │                                      │
│                          ▼                                      │
│                    ┌───────────┐                                │
│                    │    DAC    │───▶ Speaker                    │
│                    │  output   │                                │
│                    └───────────┘                                │
│                                                                 │
│   Mixing formula (with clipping prevention):                    │
│   output = clamp((ch0 + ch1 + ch2) / num_active, -32768, 32767) │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Button Debouncing

Physical buttons need software debouncing:

Button Debouncing:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Physical button press (noisy signal):                         │
│   ┌──┐ ┌┐┌─────────────────────────────┐ ┌┐ ┌──┐               │
│   │  │ │││                             │ ││ │  │               │
│   ┘  └─┘└┘                             └─┘└─┘  └               │
│                                                                 │
│   After debouncing (clean signal):                              │
│   ┌────────────────────────────────────────┐                   │
│   │                                        │                   │
│   ┘                                        └                   │
│                                                                 │
│   Algorithm:                                                    │
│   1. Read button state                                          │
│   2. If changed from last reading, reset timer                  │
│   3. If stable for N milliseconds (e.g., 20ms), accept change  │
│                                                                 │
│   Code:                                                         │
│   if (current != last_state) {                                  │
│       debounce_timer = get_ms();                                │
│   }                                                             │
│   if (get_ms() - debounce_timer > DEBOUNCE_MS) {                │
│       stable_state = current;                                   │
│   }                                                             │
│   last_state = current;                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Memory Budget

Managing resources on embedded:

Memory Budget Example (STM32F446):
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Flash (512 KB):                                               │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ Code              │ ~50 KB  │ Game engine, drivers       │  │
│   │ Graphics assets   │ ~300 KB │ Sprites, tiles, fonts      │  │
│   │ Audio assets      │ ~150 KB │ Music, sound effects       │  │
│   │ Game data         │ ~10 KB  │ Levels, configurations     │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   RAM (128 KB):                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ Framebuffer A     │ ~150 KB │ 320x240 RGB565             │  │
│   │                   │         │ PROBLEM: Won't fit!        │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   Solution: Use external RAM or reduce resolution              │
│   OR: Partial update (only redraw changed regions)             │
│   OR: Line-by-line rendering (no framebuffer)                  │
│                                                                 │
│   Compact approach (128 KB RAM):                                │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │ Line buffer (x2)  │ 1.3 KB  │ Double-buffered line       │  │
│   │ Audio buffer (x2) │ 4 KB    │ Double-buffered audio      │  │
│   │ Game state        │ 10 KB   │ Player, enemies, items     │  │
│   │ Working memory    │ 50 KB   │ Decompression, temp        │  │
│   │ Stack             │ 8 KB    │ Call stack                 │  │
│   │ Free              │ ~55 KB  │ Available                  │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Project Specification

What You Will Build

A complete handheld game console with:

  • Color LCD display (240x320 or similar)
  • Button controls (D-pad + 2 action buttons)
  • Audio output (speaker or headphones)
  • Game loading from SD card
  • At least one playable game (Tetris, Snake, Breakout, etc.)
  • Menu system for game selection

Hardware Requirements

Core Components:

  • STM32F4 board (Nucleo-F446RE recommended, or Discovery)
  • ILI9341 or ST7789 LCD (SPI, 240x320)
  • Micro SD card module (SPI)
  • 6 tactile buttons (D-pad + A + B)
  • Piezo buzzer or small speaker + amplifier
  • 3.3V power supply

Optional Enhancements:

  • 3D printed enclosure
  • LiPo battery + charger
  • Headphone jack
  • Volume control potentiometer

Functional Requirements

  1. Display System:
    • Initialize LCD via SPI
    • Double-buffered rendering (or line-by-line)
    • 60 FPS target frame rate
    • RGB565 color format
    • Basic graphics primitives (rect, line, pixel)
    • Sprite blitting with transparency
    • Text rendering with bitmap font
  2. Input System:
    • Read 6 buttons (Up, Down, Left, Right, A, B)
    • Button debouncing
    • Button edge detection (pressed/released events)
    • Responsive (< 10ms latency)
  3. Audio System:
    • DAC or PWM audio output
    • DMA-driven for CPU efficiency
    • Sound effect playback
    • Background music (optional)
    • Volume control
  4. Storage System:
    • SD card via SPI
    • FAT filesystem (FatFs library)
    • Load game assets from files
    • Save/load game state
  5. Game Engine:
    • Game loop (input → update → render)
    • Fixed timestep updates
    • Collision detection
    • Game state management
    • Score tracking
  6. Menu System:
    • Game selection screen
    • Settings (volume, brightness)
    • About/credits

Non-Functional Requirements

  • Performance: Consistent 60 FPS
  • Responsiveness: Input latency < 16ms
  • Power: Reasonable battery life (4+ hours)
  • Reliability: No crashes during gameplay
  • Usability: Intuitive controls

Real World Outcome

When complete, your game console will look like this:

Physical device: A handheld game console you built from scratch!

┌─────────────────────────────────┐
│      ARM Game Console v1.0      │
│  ┌───────────────────────────┐  │
│  │                           │  │
│  │   ████████████████████    │  │
│  │   ████  TETRIS   ████    │  │
│  │   ████████████████████    │  │
│  │                           │  │
│  │     Score: 12450          │  │
│  │     Level: 5              │  │
│  │                           │  │
│  │      ░░██░░               │  │
│  │      ░░██░░               │  │
│  │    ██████████             │  │
│  │    ████░░████             │  │
│  │    ██████████             │  │
│  └───────────────────────────┘  │
│                                 │
│   [←] [→]    [↓]    [A] [B]    │
│                                 │
└─────────────────────────────────┘

Serial debug output:
FPS: 60.02 | CPU: 45% | DMA: active
Audio: 22kHz mono, 2 channels mixed
LCD: 320x240 RGB565, SPI @ 40MHz
Buttons: 0x15 (Right + A)

Console in action:

=== ARM Game Console ===
LCD: ILI9341 320x240 @ 60Hz
Audio: 22050 Hz, PWM output
SD: FAT32 mounted, 2 games found

MAIN MENU
─────────
> Tetris
  Snake
  Settings

[A] Select   [B] Back

Loading Tetris...
  Graphics: 32 sprites loaded
  Audio: 5 sound effects loaded
  High score: 15230

GAME RUNNING
────────────
Level: 1
Score: 0
Lines: 0

[Sound: piece_land.wav]
[Sound: line_clear.wav]
Score: 100
Lines: 1

GAME OVER
─────────
Final Score: 12450
High Score: 15230

[A] Play Again   [B] Menu

Questions to Guide Your Design

Work through these questions BEFORE writing code:

  1. Display Architecture: Full framebuffer or line-by-line rendering? What are the tradeoffs?

  2. SPI Sharing: LCD and SD card both use SPI. How do you share the bus?

  3. Timing: How do you maintain 60 FPS? What happens if a frame takes too long?

  4. Audio Sync: How do you play sound effects synchronized with game events?

  5. Asset Format: How do you store sprites? Raw pixels? Compressed?

  6. Memory Layout: Where does each buffer go? Stack size?

  7. Power Management: Can you sleep between frames? During menu?

  8. Extensibility: How easy is it to add a new game?


Thinking Exercise

Before writing any code, design these scenarios on paper:

Scenario 1: Frame Timing

  1. Target: 60 FPS (16.67 ms per frame)
  2. Your render takes 12 ms
  3. DMA transfer to LCD takes 8 ms
  4. How do you overlap these?

Hint: Start DMA transfer of buffer A while rendering to buffer B.

Scenario 2: Audio Event

  1. Player presses A button
  2. Jump animation starts
  3. Jump sound should play
  4. How do you trigger the sound?

Scenario 3: Asset Loading

  1. New level starts
  2. Need to load new tileset
  3. Don’t want loading screen freeze
  4. How do you stream assets?

Solution Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────────┐
│                        Game Console                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                     Application Layer                    │   │
│   │   ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │   │
│   │   │  Menu   │  │  Tetris │  │  Snake  │  │Settings │   │   │
│   │   └─────────┘  └─────────┘  └─────────┘  └─────────┘   │   │
│   └───────────────────────────────┬─────────────────────────┘   │
│                                   │                              │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                      Game Engine                         │   │
│   │   ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │   │
│   │   │Graphics │  │  Audio  │  │  Input  │  │ Assets  │   │   │
│   │   │ System  │  │ System  │  │ System  │  │ Manager │   │   │
│   │   └────┬────┘  └────┬────┘  └────┬────┘  └────┬────┘   │   │
│   └────────┼────────────┼───────────┼────────────┼──────────┘   │
│            │            │           │            │               │
│   ┌────────┼────────────┼───────────┼────────────┼──────────┐   │
│   │        │            │           │            │          │   │
│   │  ┌─────▼────┐ ┌─────▼────┐ ┌────▼────┐ ┌─────▼────┐    │   │
│   │  │   LCD    │ │   DAC    │ │  GPIO   │ │    SD    │    │   │
│   │  │  Driver  │ │  Driver  │ │ Driver  │ │  Driver  │    │   │
│   │  └─────┬────┘ └─────┬────┘ └────┬────┘ └─────┬────┘    │   │
│   │        │            │           │            │          │   │
│   │  Hardware Abstraction Layer                              │   │
│   └────────┼────────────┼───────────┼────────────┼──────────┘   │
│            ▼            ▼           ▼            ▼               │
│   ┌──────────────────────────────────────────────────────────┐  │
│   │   SPI1 (LCD)     DAC/TIM    GPIOB      SPI2 (SD)         │  │
│   │   DMA2           DMA1       EXTI       DMA1              │  │
│   └──────────────────────────────────────────────────────────┘  │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

Data Structures

// Graphics structures
typedef struct {
    uint16_t width;
    uint16_t height;
    uint16_t *pixels;        // RGB565 data
    uint16_t transparent;    // Transparent color
} Sprite;

typedef struct {
    uint16_t *buffer[2];     // Double buffer
    uint8_t  current;        // Which buffer is being displayed
    uint16_t width;
    uint16_t height;
} Framebuffer;

// Audio structures
typedef struct {
    int16_t  *samples;
    uint32_t  length;
    uint32_t  sample_rate;
    uint8_t   loop;
} AudioClip;

typedef struct {
    AudioClip *clip;
    uint32_t   position;
    uint8_t    playing;
    uint8_t    volume;
} AudioChannel;

#define MAX_AUDIO_CHANNELS 4
typedef struct {
    AudioChannel channels[MAX_AUDIO_CHANNELS];
    int16_t      mix_buffer[AUDIO_BUFFER_SIZE];
} AudioMixer;

// Input structures
typedef struct {
    uint8_t current;      // Current button state (bitmap)
    uint8_t previous;     // Previous state (for edge detection)
    uint8_t pressed;      // Just pressed this frame
    uint8_t released;     // Just released this frame
} ButtonState;

#define BTN_UP    0x01
#define BTN_DOWN  0x02
#define BTN_LEFT  0x04
#define BTN_RIGHT 0x08
#define BTN_A     0x10
#define BTN_B     0x20

// Game state
typedef struct {
    uint8_t current_screen;  // MENU, GAME, SETTINGS, etc.
    uint32_t score;
    uint32_t high_score;
    uint8_t level;
    // Game-specific state...
} GameState;

// Main console structure
typedef struct {
    Framebuffer  framebuffer;
    AudioMixer   audio;
    ButtonState  buttons;
    GameState    game;
    uint32_t     frame_count;
    uint32_t     last_frame_time;
} Console;

System Initialization

void console_init(void) {
    // 1. System clock (168 MHz for STM32F4)
    SystemClock_Config();

    // 2. GPIO for buttons
    gpio_init_buttons();

    // 3. SPI for LCD and SD
    spi_init(SPI1, 40000000);  // LCD at 40 MHz
    spi_init(SPI2, 25000000);  // SD at 25 MHz

    // 4. LCD initialization
    lcd_init();

    // 5. SD card and filesystem
    sd_init();
    f_mount(&fs, "", 1);

    // 6. Audio (DAC + Timer + DMA)
    audio_init(22050);

    // 7. Game engine
    graphics_init(&console.framebuffer);
    input_init(&console.buttons);

    // 8. Load menu assets
    menu_init();
}

Implementation Guide

Development Environment Setup

# Required tools
sudo apt-get install gcc-arm-none-eabi openocd stlink-tools

# Create project structure
mkdir -p game-console/{src,inc,assets,games,lib}
cd game-console

Project Structure

game-console/
├── src/
│   ├── main.c              # Entry point
│   ├── console.c           # Main console logic
│   ├── graphics.c          # Graphics system
│   ├── audio.c             # Audio system
│   ├── input.c             # Input handling
│   ├── assets.c            # Asset loading
│   ├── lcd_ili9341.c       # LCD driver
│   ├── sd_spi.c            # SD card driver
│   ├── dac_audio.c         # DAC audio output
│   └── system.c            # Clock, GPIO setup
├── games/
│   ├── menu.c              # Main menu
│   ├── tetris.c            # Tetris game
│   └── snake.c             # Snake game
├── inc/
│   ├── console.h
│   ├── graphics.h
│   ├── audio.h
│   └── games.h
├── assets/
│   ├── sprites/            # Sprite images
│   ├── sounds/             # Sound effects
│   └── fonts/              # Bitmap fonts
├── lib/
│   └── fatfs/              # FatFs library
├── linker.ld
├── startup.s
└── Makefile

Implementation Phases

Phase 1: Display (Week 1-2)

Goals:

  • LCD working via SPI
  • Basic graphics primitives
  • Display test pattern

Tasks:

  1. Initialize SPI for LCD
  2. Write LCD driver (init sequence, write commands/data)
  3. Implement fill_rect, draw_pixel
  4. Display color bars test pattern
  5. Implement sprite blitting
  6. Set up DMA for fast transfers

Checkpoint: LCD shows color bars and a moving sprite.

// LCD initialization (ILI9341)
void lcd_init(void) {
    // Hardware reset
    gpio_write(LCD_RST, 0); delay_ms(10);
    gpio_write(LCD_RST, 1); delay_ms(120);

    // Software reset
    lcd_cmd(0x01); delay_ms(5);

    // Exit sleep
    lcd_cmd(0x11); delay_ms(120);

    // Pixel format: 16-bit RGB565
    lcd_cmd(0x3A); lcd_data(0x55);

    // Memory access control (rotation)
    lcd_cmd(0x36); lcd_data(0x48);

    // Display on
    lcd_cmd(0x29);
}

// Draw sprite with transparency
void draw_sprite(int x, int y, Sprite *sprite) {
    uint16_t *fb = framebuffer.buffer[framebuffer.current ^ 1];

    for (int row = 0; row < sprite->height; row++) {
        for (int col = 0; col < sprite->width; col++) {
            uint16_t color = sprite->pixels[row * sprite->width + col];
            if (color != sprite->transparent) {
                int fx = x + col;
                int fy = y + row;
                if (fx >= 0 && fx < 320 && fy >= 0 && fy < 240) {
                    fb[fy * 320 + fx] = color;
                }
            }
        }
    }
}

Phase 2: Input (Week 2)

Goals:

  • Button reading working
  • Debouncing implemented
  • Edge detection for press/release

Tasks:

  1. Configure GPIO for buttons (input with pull-up)
  2. Implement button reading function
  3. Add debouncing logic
  4. Detect pressed/released events
  5. Test with sprite movement

Checkpoint: Sprite moves smoothly with D-pad, A/B trigger actions.

// Button reading with debouncing
void input_update(ButtonState *state) {
    static uint8_t debounce_count[6] = {0};
    static uint8_t raw_state = 0;

    // Read raw GPIO
    uint8_t current_raw = 0;
    if (!gpio_read(BTN_UP_PIN))    current_raw |= BTN_UP;
    if (!gpio_read(BTN_DOWN_PIN))  current_raw |= BTN_DOWN;
    if (!gpio_read(BTN_LEFT_PIN))  current_raw |= BTN_LEFT;
    if (!gpio_read(BTN_RIGHT_PIN)) current_raw |= BTN_RIGHT;
    if (!gpio_read(BTN_A_PIN))     current_raw |= BTN_A;
    if (!gpio_read(BTN_B_PIN))     current_raw |= BTN_B;

    // Debounce each button
    for (int i = 0; i < 6; i++) {
        uint8_t mask = (1 << i);
        if ((current_raw & mask) == (raw_state & mask)) {
            debounce_count[i] = 0;
        } else {
            debounce_count[i]++;
            if (debounce_count[i] >= DEBOUNCE_THRESHOLD) {
                raw_state ^= mask;
                debounce_count[i] = 0;
            }
        }
    }

    // Update state
    state->previous = state->current;
    state->current = raw_state;
    state->pressed = state->current & ~state->previous;
    state->released = ~state->current & state->previous;
}

Phase 3: Audio (Week 3)

Goals:

  • DAC outputting audio
  • DMA-driven playback
  • Sound effect triggering

Tasks:

  1. Configure DAC and timer (from Project 13)
  2. Set up DMA for audio buffer
  3. Implement audio mixer
  4. Load WAV files from SD
  5. Test sound effect playback

Checkpoint: Sound effects play when buttons pressed.

// Play sound effect
void audio_play_sfx(AudioClip *clip) {
    // Find free channel
    for (int i = 1; i < MAX_AUDIO_CHANNELS; i++) {  // 0 reserved for music
        if (!console.audio.channels[i].playing) {
            console.audio.channels[i].clip = clip;
            console.audio.channels[i].position = 0;
            console.audio.channels[i].playing = 1;
            console.audio.channels[i].volume = 100;
            return;
        }
    }
    // No free channel, steal oldest
}

// Mix all channels (called from DMA interrupt)
void audio_mix(int16_t *output, int samples) {
    memset(output, 0, samples * sizeof(int16_t));

    for (int ch = 0; ch < MAX_AUDIO_CHANNELS; ch++) {
        AudioChannel *chan = &console.audio.channels[ch];
        if (!chan->playing) continue;

        for (int i = 0; i < samples; i++) {
            if (chan->position >= chan->clip->length) {
                if (chan->clip->loop) {
                    chan->position = 0;
                } else {
                    chan->playing = 0;
                    break;
                }
            }

            int32_t sample = chan->clip->samples[chan->position++];
            sample = (sample * chan->volume) / 100;
            output[i] = clamp(output[i] + sample, -32768, 32767);
        }
    }
}

Phase 4: SD Card & Assets (Week 4)

Goals:

  • SD card reading
  • FAT filesystem working
  • Load sprites and sounds

Tasks:

  1. Port FatFs library
  2. Configure SPI for SD (from Project 11)
  3. Implement asset loader
  4. Create sprite format and loader
  5. Create audio format and loader

Checkpoint: Load and display sprites from SD card.

// Load sprite from BMP file
Sprite *load_sprite(const char *filename) {
    FIL file;
    if (f_open(&file, filename, FA_READ) != FR_OK) {
        return NULL;
    }

    // Read BMP header (simplified)
    uint8_t header[54];
    UINT read;
    f_read(&file, header, 54, &read);

    int width = *(int32_t *)&header[18];
    int height = *(int32_t *)&header[22];

    Sprite *sprite = malloc(sizeof(Sprite));
    sprite->width = width;
    sprite->height = height;
    sprite->pixels = malloc(width * height * 2);
    sprite->transparent = 0xF81F;  // Magenta

    // Read and convert pixels (BGR to RGB565)
    for (int y = height - 1; y >= 0; y--) {
        for (int x = 0; x < width; x++) {
            uint8_t bgr[3];
            f_read(&file, bgr, 3, &read);
            uint16_t rgb565 = ((bgr[2] >> 3) << 11) |
                              ((bgr[1] >> 2) << 5) |
                              (bgr[0] >> 3);
            sprite->pixels[y * width + x] = rgb565;
        }
    }

    f_close(&file);
    return sprite;
}

Phase 5: Game Engine (Week 5-6)

Goals:

  • Main game loop
  • Frame timing
  • Collision detection
  • Game state management

Tasks:

  1. Implement fixed-timestep game loop
  2. Add frame rate limiting
  3. Create collision detection helpers
  4. Implement game state machine
  5. Create menu system

Checkpoint: Menu navigable, can start game.

// Main game loop
void console_run(void) {
    uint32_t last_time = get_ms();
    uint32_t accumulator = 0;
    const uint32_t FRAME_TIME = 16;  // ~60 FPS

    while (1) {
        uint32_t current_time = get_ms();
        uint32_t delta = current_time - last_time;
        last_time = current_time;
        accumulator += delta;

        // Input (once per frame)
        input_update(&console.buttons);

        // Fixed timestep updates
        while (accumulator >= FRAME_TIME) {
            game_update(FRAME_TIME);
            accumulator -= FRAME_TIME;
        }

        // Render
        game_render();

        // Swap and send to LCD
        swap_buffers();

        // Frame counter
        console.frame_count++;
    }
}

// Game state machine
void game_update(uint32_t dt) {
    switch (console.game.current_screen) {
        case SCREEN_MENU:
            menu_update(dt);
            break;
        case SCREEN_TETRIS:
            tetris_update(dt);
            break;
        case SCREEN_SNAKE:
            snake_update(dt);
            break;
    }
}

Phase 6: First Game - Tetris (Week 7-8)

Goals:

  • Complete Tetris implementation
  • All seven tetrominoes
  • Line clearing and scoring
  • Game over detection

Tasks:

  1. Define tetromino shapes
  2. Implement piece movement and rotation
  3. Implement collision with board
  4. Implement line clearing
  5. Add scoring and levels
  6. Add sound effects

Checkpoint: Fully playable Tetris with audio.

// Tetris piece definition
const uint16_t TETROMINOES[7][4] = {
    {0x0F00, 0x2222, 0x00F0, 0x4444},  // I
    {0x8E00, 0x6440, 0x0E20, 0x44C0},  // J
    {0x2E00, 0x4460, 0x0E80, 0xC440},  // L
    {0xCC00, 0xCC00, 0xCC00, 0xCC00},  // O
    {0x6C00, 0x4620, 0x06C0, 0x8C40},  // S
    {0x4E00, 0x4640, 0x0E40, 0x4C40},  // T
    {0xC600, 0x2640, 0x0C60, 0x4C80},  // Z
};

// Game update
void tetris_update(uint32_t dt) {
    // Input handling
    if (console.buttons.pressed & BTN_LEFT) {
        try_move(-1, 0);
    }
    if (console.buttons.pressed & BTN_RIGHT) {
        try_move(1, 0);
    }
    if (console.buttons.pressed & BTN_A) {
        try_rotate(1);
        audio_play_sfx(&sfx_rotate);
    }
    if (console.buttons.current & BTN_DOWN) {
        drop_timer = 0;  // Fast drop
    }

    // Gravity
    drop_timer += dt;
    if (drop_timer >= drop_interval) {
        if (!try_move(0, 1)) {
            lock_piece();
            clear_lines();
            spawn_piece();
            if (check_game_over()) {
                console.game.current_screen = SCREEN_GAMEOVER;
            }
        }
        drop_timer = 0;
    }
}

Phase 7: Polish (Week 9-10)

Goals:

  • Second game (Snake)
  • Visual polish
  • Sound polish
  • Bug fixes

Tasks:

  1. Implement Snake game
  2. Add title screen
  3. Add high score saving
  4. Visual effects (screen shake, flash)
  5. Audio polish (music, more SFX)
  6. Testing and bug fixes

Checkpoint: Complete, polished game console.


Hints in Layers

Hint 1: LCD SPI Speed

Full framebuffer transfer at 60 FPS needs ~70 Mbit/s:

  • 320 * 240 * 2 bytes * 60 FPS = 9.2 MB/s

Use DMA and high SPI speed (40+ MHz). Or use partial updates.

Hint 2: Memory Constraints

If you can’t fit a full framebuffer:

  1. Use line-by-line rendering (1 line = 640 bytes)
  2. Render each line, DMA transfer, repeat
  3. More CPU work but fits in small RAM
Hint 3: Audio Sync

Don’t play audio directly in game code. Instead:

  1. Queue sound effect request
  2. Audio ISR picks it up
  3. Starts playback on next buffer fill
Hint 4: SPI Bus Sharing

LCD and SD share SPI? Use separate CS pins:

  1. Deassert LCD CS
  2. Assert SD CS
  3. Do SD operations
  4. Deassert SD CS
  5. Assert LCD CS
  6. Continue graphics
Hint 5: Frame Timing

Use timer interrupt for frame timing:

  1. Timer fires every 16.67 ms
  2. Sets flag
  3. Main loop checks flag, renders
  4. Precise timing without busy-wait
Hint 6: Sprite Optimization

For faster sprite rendering:

  1. Use DMA for horizontal spans
  2. Skip transparent pixels in inner loop
  3. Clip sprites to screen bounds once
  4. Consider sprite sheet packed in Flash

Testing Strategy

Test Categories

Category Purpose Examples
Component Test each system LCD alone, audio alone
Integration Systems together Graphics + input
Gameplay Game mechanics Collision, scoring
Performance Frame rate Profile slow areas
Stress Long runs Hours of play

Critical Test Cases

  1. Display: All colors display correctly
  2. Input: All buttons respond, no ghost presses
  3. Audio: Sounds play, mix correctly
  4. Timing: 60 FPS maintained
  5. Gameplay: Game rules correct
  6. Edge cases: Screen boundaries, game over
  7. Long run: 1 hour without issues

Performance Profiling

// Simple profiler
typedef struct {
    uint32_t input_time;
    uint32_t update_time;
    uint32_t render_time;
    uint32_t transfer_time;
} FrameProfile;

void profile_frame(FrameProfile *p) {
    uint32_t start = get_us();
    input_update(&console.buttons);
    p->input_time = get_us() - start;

    start = get_us();
    game_update(16);
    p->update_time = get_us() - start;

    start = get_us();
    game_render();
    p->render_time = get_us() - start;

    start = get_us();
    swap_buffers();
    p->transfer_time = get_us() - start;

    // Print every 60 frames
    if (console.frame_count % 60 == 0) {
        printf("I:%d U:%d R:%d T:%d\n",
               p->input_time, p->update_time,
               p->render_time, p->transfer_time);
    }
}

Common Pitfalls & Debugging

Frequent Mistakes

Pitfall Symptom Solution
SPI too slow Low FPS Increase SPI speed, use DMA
Wrong pixel format Garbled colors Check RGB565 byte order
No debouncing Ghost inputs Add debounce logic
Audio underrun Clicking Increase buffer size
Stack overflow Random crashes Increase stack, check recursion
DMA conflict Corruption Don’t share DMA channels

Debugging Strategies

  1. Visual Debugging: Display debug info on screen
  2. Serial Logging: Printf timing, state
  3. LED Indicators: Toggle on key events
  4. Logic Analyzer: Check SPI signals
  5. Oscilloscope: Verify audio waveform

Performance Optimization

Optimization Checklist:
[ ] DMA for all large transfers
[ ] SPI at maximum speed
[ ] Optimize inner loops (sprite blitting)
[ ] Precompute where possible
[ ] Use lookup tables for math
[ ] Profile to find bottlenecks
[ ] Consider assembly for hot paths

Extensions & Challenges

Beginner Extensions

  • More games: Breakout, Pong, Space Invaders
  • Animated sprites: Frame animation system
  • Background music: Loop music during gameplay
  • Pause menu: Pause with B button

Intermediate Extensions

  • Scrolling backgrounds: Tile-based scrolling
  • Particle effects: Explosions, sparkles
  • Save system: Save high scores to SD
  • Multiple difficulty levels: Easy/Medium/Hard

Advanced Extensions

  • Game download: Load games from SD
  • Link cable: Multiplayer via UART
  • Emulator: NES/GB emulator
  • Wireless: Bluetooth controller
  • Custom PCB: Design your own board

Self-Assessment Checklist

Understanding

  • I can explain the game loop (input, update, render)
  • I understand double buffering and why it’s needed
  • I can describe how audio mixing works
  • I know how to debounce buttons
  • I understand the memory constraints
  • I can explain how all subsystems integrate

Implementation

  • LCD displays graphics correctly at 60 FPS
  • All buttons work with no ghost presses
  • Audio plays without glitches
  • At least one game is fully playable
  • Menu system allows game selection
  • System runs for hours without issues

Quality

  • Smooth, responsive gameplay
  • Good visual presentation
  • Clear, crisp audio
  • No crashes or glitches
  • Easy to add new games

The Interview Questions They’ll Ask

After completing this project, you’ll be ready for these questions:

  1. “Tell me about a project you’re proud of”
    • This IS that project! A complete embedded system you designed and built.
    • Demonstrates: hardware/software integration, real-time systems, resource constraints
  2. “How do you optimize for performance in embedded systems?”
    • Use DMA to offload data transfer
    • Profile to find bottlenecks
    • Optimize critical loops
    • Use hardware peripherals efficiently
  3. “How do you handle real-time constraints?”
    • Fixed timestep for consistency
    • Prioritize critical paths
    • Use interrupts appropriately
    • Double buffer to decouple production/consumption
  4. “Describe a challenging bug you solved”
    • Many opportunities: audio glitches, frame drops, memory corruption
    • Describe debugging process, tools used, solution
  5. “How would you design a system with limited resources?”
    • Memory budgeting
    • Feature prioritization
    • Creative solutions (line-by-line rendering, etc.)

Books That Will Help

Topic Book Chapter
Game architecture “Game Programming Patterns” by Nystrom Chapters 1, 8, 9
Graphics “Computer Graphics from Scratch” by Gambetta Part 1
Audio “The Audio Programming Book” Chapters 1-4
Embedded systems “Making Embedded Systems” by White Chapters 8-10
ARM peripherals “Definitive Guide to ARM Cortex-M3/M4” Chapters 8-10

Resources

Hardware

Software

Example Projects

  • Previous: Project 15: Tiny OS
  • Integrates: Projects 3, 4, 5, 7, 10, 11, 12, 13
  • This is the Capstone: Congratulations on completing the ARM Deep Dive!

Congratulations!

If you’ve completed this project, you have:

  • Built a complete embedded system from scratch
  • Integrated multiple hardware peripherals
  • Implemented real-time graphics and audio
  • Created an actual playable game
  • Demonstrated mastery of ARM architecture

You are now an ARM expert. This project proves you can:

  • Design and implement complex embedded systems
  • Handle real-time constraints
  • Manage limited resources creatively
  • Debug at the hardware/software interface
  • Build something tangible and impressive

This is the kind of project that distinguishes you in interviews and opens doors to embedded systems, game development, and IoT positions.

Well done!


This guide was expanded from LEARN_ARM_DEEP_DIVE.md. For the complete learning path, see the project index.