lego-esp32s3-gameboy/README.md

18 KiB
Raw Blame History

🎮 ESP32-S3 GameBoy Emulator

Hochoptimierter GameBoy/GameBoy Color Emulator für Waveshare ESP32-S3-Touch-LCD-2


📋 Projekt-Übersicht

Dieses Projekt ist ein vollständig funktionsfähiger GameBoy Emulator, der auf dem Waveshare ESP32-S3-Touch-LCD-2 Board läuft. Es nutzt den Peanut-GB Emulator-Core und erreicht 90-111 FPS bei vielen Spielen - schneller als der Original GameBoy (59.73 FPS)!

Fertige Features

  • GameBoy Emulation (Peanut-GB Core)
  • ST7789 Display (2.0", 320x240, 80 MHz SPI)
  • Perfekter 4-Kanal Audio (I2S MAX98357A, 32768 Hz)
  • SD Card ROM Loading (FAT32, .gb Dateien)
  • PSRAM Optimization (8MB Octal Mode, Double-Buffering)
  • Dynamisches Display-Scaling (1.0x bis 1.67x konfigurierbar)
  • 90-111 FPS Performance (besser als Original!)

🚧 Geplante Features

  • 8 GameBoy Buttons (GPIO 8-14, 21 - Hardware fertig, Software TODO)
  • NFC ROM-Auswahl (PN532 I2C - Hardware fertig, Software TODO)
  • Potentiometer Controls (Volume & Brightness - Hardware fertig, Software TODO)
  • Link Cable 2-Player (GPIO 2, 15, 17 - Hardware fertig, Software TODO)

🏗️ Programmablauf & Architektur

Systemarchitektur

┌─────────────────────────────────────────────────────────────┐
│                     ESP32-S3 Dual Core                      │
├──────────────────────────────┬──────────────────────────────┤
│         CORE 1               │         CORE 0               │
│    (Emulation Task)          │     (Display Task)           │
│                              │                              │
│  ┌────────────────────┐      │  ┌────────────────────┐     │
│  │  GameBoy Emulator  │      │  │  Display Rendering │     │
│  │   (Peanut-GB)      │      │  │    (ST7789 SPI)    │     │
│  │                    │      │  │                    │     │
│  │  - CPU Emulation   │      │  │  - Byte Swapping   │     │
│  │  - PPU Rendering   │      │  │  - SPI Transfer    │     │
│  │  - APU Audio       │◄─────┼──┤  - Compact Buffer  │     │
│  │  - Memory Map      │      │  │                    │     │
│  └────────────────────┘      │  └────────────────────┘     │
│           │                  │           ▲                  │
│           ▼                  │           │                  │
│  ┌────────────────────┐      │  ┌────────────────────┐     │
│  │  Render Buffer     │──────┼─►│  Display Buffer    │     │
│  │  (PSRAM 150KB)     │ Swap │  │  (PSRAM 150KB)     │     │
│  └────────────────────┘      │  └────────────────────┘     │
│                              │                              │
│  ┌────────────────────┐      │                              │
│  │  Audio Task        │      │                              │
│  │  (I2S Output)      │      │                              │
│  └────────────────────┘      │                              │
└──────────────────────────────┴──────────────────────────────┘
         │                              │
         ▼                              ▼
┌─────────────────┐            ┌─────────────────┐
│   I2S Audio     │            │   SPI Display   │
│  (MAX98357A)    │            │    (ST7789)     │
└─────────────────┘            └─────────────────┘

Programmablauf beim Start

1. app_main() startet
   │
   ├─► ST7789 Display initialisieren (80 MHz SPI)
   │   └─► Backlight auf 80% setzen
   │
   ├─► SD-Karte mounten (FAT32)
   │   └─► ROM laden: /tetris.gb
   │
   ├─► PSRAM prüfen (8MB Octal Mode)
   │   ├─► Render Buffer allokieren (150 KB)
   │   └─► Display Buffer allokieren (150 KB)
   │
   ├─► I2S Audio initialisieren (32768 Hz, 16-bit)
   │   └─► Audio Task starten (Core 1, Priority 5)
   │
   ├─► Peanut-GB Emulator initialisieren
   │   ├─► ROM laden
   │   ├─► Callbacks registrieren:
   │   │   ├─► gb_lcd_draw_line() - Zeile rendern
   │   │   └─► audio_callback() - Audio-Sample
   │   └─► GameBoy-Palette setzen (DMG Grün)
   │
   ├─► Display Task erstellen (Core 0, Priority 10)
   │   └─► Wartet auf frame_ready_sem
   │
   └─► Emulation Loop starten (Main Loop)
       │
       └─► Für immer:
           ├─► gb_run_frame() - Emuliert 1 Frame
           │   ├─► CPU läuft (70224 Takte)
           │   ├─► PPU rendert Zeilen → gb_lcd_draw_line()
           │   └─► APU erzeugt Audio → audio_callback()
           │
           ├─► Buffer Swap (Render ↔ Display)
           │   └─► frame_ready_sem freigeben
           │
           ├─► Auf frame_done_sem warten
           │   └─► Display Task hat gerendert
           │
           └─► FPS berechnen & ausgeben (alle 60 Frames)

Double-Buffering Synchronisation

EMULATION TASK (Core 1)          DISPLAY TASK (Core 0)
─────────────────────            ────────────────────

┌─ Frame N rendern              ┌─ Wartet auf Semaphore
│  in Render Buffer              │  (frame_ready_sem)
│                                │
│  gb_run_frame()                │
│  └─► Zeilen in                 │
│      render_buffer             │
│      schreiben                 │
│                                │
└─ Frame fertig                  │
                                 │
   Buffer Swap:                  │
   render_buffer ↔               │
   display_buffer                │
                                 │
   frame_ready_sem               │
   freigeben ──────────────────► └─ Semaphore empfangen!

   Warte auf                      ┌─ Display buffer kopieren
   frame_done_sem                 │  zu compact_buffer
   ◄────────────────────────────  │  (nur GameBoy-Region)
                                  │
                                  ├─ SPI Transfer
                                  │  st7789_draw_buffer_
                                  │  preswapped()
                                  │
                                  └─ Fertig!

                                     frame_done_sem
                                     freigeben

   frame_done_sem empfangen ◄────

┌─ Nächstes Frame              ┌─ Wartet wieder...
│  rendern                      │

🧩 Component-Beschreibung

1. Peanut-GB Component

Pfad: components/peanut-gb/

Funktion: Kern des GameBoy-Emulators. Header-only Bibliothek für GameBoy CPU/PPU/APU Emulation.

Wichtige Funktionen:

  • gb_init() - Initialisiert den Emulator
  • gb_run_frame() - Emuliert 1 GameBoy Frame (70224 CPU-Takte = 16.7ms)
  • gb_lcd_draw_line() - Callback für jede gerenderte Bildschirmzeile

Optimierung: Kompiliert mit -O3 Flag für maximale Performance

2. ST7789 Display Driver Component

Pfad: components/st7789/

Funktion: Treiber für ST7789 TFT-Display (320×240 Pixel, RGB565 Farbformat)

Wichtige Funktionen:

  • st7789_init() - Display initialisieren (80 MHz SPI)
  • st7789_draw_buffer_preswapped() - Optimierter Transfer (vorher RGB→BGR gewandelt)
  • st7789_set_backlight() - Hintergrundbeleuchtung steuern (0-100%)

Besonderheit:

  • Chunked SPI Transfer (max 32KB pro Transfer wegen DMA-Limit)
  • Byte-Swapping wird im Emulator gemacht (RGB→BGR), nicht im Treiber

Pfad: components/link_cable/

Status: Hardware fertig, Software TODO

Funktion: 2-Player Multiplayer über GPIO (serieller Transfer)

Geplante Nutzung:

  • Pokemon-Tausch
  • Tetris 2-Player
  • Andere Multiplayer-Games

4. NFC Manager Component

Pfad: components/nfc_manager/

Status: Hardware fertig (PN532 auf I2C), Software TODO

Funktion: ROM-Auswahl per NFC-Tag scannen

Geplante Funktionsweise:

  1. NFC-Tag scannen
  2. UID auslesen
  3. In nfc_roms.json nachschlagen
  4. Entsprechende ROM laden

5. Potentiometer Manager Component

Pfad: components/potentiometer_manager/

Status: Hardware fertig (ADC GPIO 3, 4), Software TODO

Funktion: Analoge Steuerung für Volume und Brightness

Geplante Nutzung:

  • Poti 1 (GPIO 3): Lautstärke (0-100%)
  • Poti 2 (GPIO 4): Helligkeit (10-100%)

6. Minizip Component

Pfad: components/minizip/

Funktion: ZIP-Dekompression für .zip ROM-Dateien

Status: Eingebunden, aber aktuell nicht genutzt (ROMs sind .gb, nicht gezippt)

7. Zlib Component

Pfad: components/zlib/

Funktion: Compression-Bibliothek (für Minizip und Save-States)

Status: Eingebunden als Dependency für Minizip


🔧 Wichtige Code-Bereiche in main.c

1. GameBoy Palette

// Zeilen 417-422: DMG Grün-Palette
static const uint16_t gb_palette[4] = {
    0xFFFF,  // Weiß (Hintergrund)
    0xAD55,  // Hellgrün
    0x52AA,  // Mittelgrün
    0x0000   // Schwarz (Vordergrund)
};

Erklärung: GameBoy hat 4 Graustufen (2-bit), hier als RGB565-Werte definiert.

2. Audio Callback

// Zeilen 424-461: Audio-Sample Callback
void audio_callback(struct gb_s *gb, uint16_t left, uint16_t right)

Funktion: Wird von Peanut-GB für jedes Audio-Sample aufgerufen (32768 Hz).

Ablauf:

  1. Samples in Ring-Buffer schreiben
  2. Bei Buffer voll: I2S schreiben
  3. APU-Register auslesen für Kanal-Status

3. Display Zeilen-Rendering

// Zeilen 463-530: LCD Zeilen-Callback
static void gb_lcd_draw_line(...)

Funktion: Wird 144× pro Frame aufgerufen (eine Zeile pro Aufruf)

Scaling-Algorithmus:

  1. Vertikales Scaling: y_base = (line * GB_RENDER_HEIGHT) / 144
  2. Horizontales Scaling: Jedes Pixel wird dynamisch auf 1-2 Output-Pixel gemapped
  3. Byte-Swapping: RGB565 → BGR565 (für ST7789)
  4. Pixel-Width-Berechnung verhindert Lücken im Bild

Beispiel bei Scale 1.6:

  • GameBoy Zeile 0 → Display Y = 0
  • GameBoy Zeile 10 → Display Y = 16 (10 * 1.6 = 16)
  • Einige Zeilen werden dupliziert für gleichmäßige Skalierung

4. Display Task

// Zeilen 610-653: Display Rendering Task
static void display_task(void *arg)

Funktion: Läuft auf Core 0, rendert fertigen Frame zum Display

Optimierung - Compact Buffer:

  1. Nur GameBoy-Region kopieren (nicht schwarze Ränder)
  2. Von 320×240 Buffer → 256×230 kompakter Buffer
  3. 33% weniger SPI-Transfer!
  4. Geschwindigkeit steigt von 20ms auf 16ms pro Frame

5. Main Loop

// Zeilen 702-786: Hauptschleife
void app_main(void)

Ablauf:

  1. Display init
  2. SD-Card mount
  3. PSRAM check & Buffer alloc
  4. Audio init & Task start
  5. Emulator init
  6. Display Task start
  7. Unendliche Emulations-Loop

🎯 Performance-Optimierungen

1. PSRAM Double-Buffering

Problem: SPI-Transfer (10ms) blockiert Emulation Lösung: 2 Buffer in PSRAM, parallel arbeiten

Ergebnis:

  • Core 0: Display rendering (10ms)
  • Core 1: Emulation (10ms)
  • Parallel = 50% schneller!

2. Display Scaling

Problem: Fullscreen (320×240) zu langsam (20ms) Lösung: Kleinere Auflösung mit schwarzen Rändern

Ergebnis:

  • Scale 1.0: 160×144 = 83 FPS (sehr klein)
  • Scale 1.5: 240×216 = 90-100 FPS (gut)
  • Scale 1.6: 256×230 = 60-90 FPS (beste Balance)
  • Scale 1.67: 267×240 = 55-70 FPS (volle Höhe)

3. Compact Buffer

Problem: 320×240 Buffer mit vielen schwarzen Pixeln Lösung: Nur GameBoy-Region transferieren

Ergebnis:

  • Vorher: 153.600 Bytes SPI-Transfer
  • Nachher: 117.760 Bytes (23% weniger!)
  • Speedup: 3-4ms pro Frame

4. Compiler Optimierung

Problem: Emulator zu langsam (45ms pro Frame) Lösung: -O3 Compiler-Flag für Peanut-GB

Ergebnis:

  • -O2: 22-25ms
  • -O3: 16-19ms
  • 40% schneller!

5. SPI Clock Speed

Problem: Display-Transfer Bottleneck Lösung: 80 MHz SPI (Maximum für ST7789)

Ergebnis:

  • 40 MHz: 28ms pro Frame
  • 80 MHz: 16ms pro Frame
  • Fast doppelt so schnell!

📊 Performance-Tabelle

Game Scale FPS Frame Time Audio
Tetris 1.5 60-70 14-16ms
DuckTales 1.5 90-111 9-11ms
Pokemon 1.6 55-65 15-18ms
Super Mario 1.6 60-75 13-16ms
Original GB - 59.73 16.7ms

Fazit: Bei vielen Spielen schneller als Original GameBoy!


🚀 Schnellstart - Build & Flash

Voraussetzungen

  • ESP-IDF v4.4 installiert
  • Python 3.10 (pyenv empfohlen)
  • Git

Build

# ESP-IDF Environment laden
source ~/esp-idf/export.sh

# Projekt bauen
cd /home/duffy/Arduino/gameboy/gnuboy
idf.py build

Flash

# Flashen
idf.py -p /dev/ttyUSB0 flash

# Mit Monitor
idf.py -p /dev/ttyUSB0 flash monitor

🛠️ Hardware

Hauptkomponenten

Component Model Notes
MCU Board Waveshare ESP32-S3-Touch-LCD-2 16MB Flash, 8MB PSRAM
Display ST7789 2.0" 320×240, integriert
Audio MAX98357A I2S Amplifier
Storage MicroSD Card FAT32, via SPI

Pin-Belegung

Siehe main/include/hardware_config.h für alle Pin-Definitionen!

Display (ST7789 SPI):

  • MOSI: GPIO 38, SCLK: GPIO 39, CS: GPIO 45
  • DC: GPIO 42, BCKL: GPIO 1

Audio (I2S):

  • BCLK: GPIO 48, LRC: GPIO 47, DIN: GPIO 16

Buttons (TODO):

  • UP: 8, DOWN: 9, LEFT: 10, RIGHT: 11
  • A: 12, B: 13, START: 14, SELECT: 21

NFC (I2C, TODO):

  • SCL: GPIO 6, SDA: GPIO 5 (shared mit Touch)

Link Cable (TODO):

  • SCLK: GPIO 15, SOUT: GPIO 2, SIN: GPIO 17

Potentiometer (ADC, TODO):

  • Volume: GPIO 3, Brightness: GPIO 4

📦 Projekt-Struktur

gnuboy/
├── CMakeLists.txt              # Root CMake
├── sdkconfig                   # ESP-IDF Konfiguration
├── README.md                   # Diese Datei
│
├── main/
│   ├── CMakeLists.txt
│   ├── main.c                  # Hauptprogramm (ausführlich kommentiert)
│   └── include/
│       └── hardware_config.h   # Pin-Definitionen & Scaling
│
└── components/
    ├── peanut-gb/              # GameBoy Emulator Core (-O3)
    ├── st7789/                 # Display Driver (80 MHz SPI)
    ├── minizip/                # ZIP Support
    ├── zlib/                   # Compression
    ├── link_cable/             # 2-Player (TODO)
    ├── nfc_manager/            # NFC ROM Selection (TODO)
    └── potentiometer_manager/  # Volume/Brightness (TODO)

🔧 Konfiguration

Display Scaling ändern

In main/include/hardware_config.h:

// Zeile 54: Scaling-Faktor anpassen
#define GB_SCALE_FACTOR 1.6  // Ändern auf 1.4, 1.5, 1.67, etc.

Empfohlene Werte:

  • 1.4 - Klein, sehr schnell (>100 FPS)
  • 1.5 - Gut, schnell (90-100 FPS)
  • 1.6 - Beste Balance (60-90 FPS)
  • 1.67 - Volle Höhe (55-70 FPS)

Fullscreen aktivieren

// Zeile 41: Scaling deaktivieren
#define GB_PIXEL_PERFECT_SCALING 0  // Fullscreen 320×240

Warnung: Fullscreen ist langsamer (~50 FPS bei vielen Spielen)


🐛 Troubleshooting

Build Fehler?

# Clean & Rebuild
idf.py fullclean
idf.py build

PSRAM nicht erkannt?

# Prüfen im Monitor
idf.py monitor
# Sollte zeigen: "PSRAM: 8191 KB total"

Falls nicht:

idf.py menuconfig
# → Component config → ESP32S3-Specific
# → [*] Support for external, SPI-connected RAM
# → Mode: Octal Mode PSRAM
# → Speed: 80MHz

Zu langsam?

  1. Display Scaling reduzieren (GB_SCALE_FACTOR 1.4)
  2. Compiler-Optimierung prüfen (sollte -O3 sein)
  3. SPI-Speed prüfen (sollte 80 MHz sein)

Audio knackt?

  • Normal bei sehr langsamen Spielen (<40 FPS)
  • Bei >50 FPS sollte Audio perfekt sein

📝 Lizenz

  • Peanut-GB: MIT License
  • Projekt-spezifischer Code: MIT
  • Components: Siehe jeweilige LICENSE-Dateien

🙏 Credits

  • Peanut-GB: Delta (Header-only GameBoy Emulator)
  • Waveshare: Hardware Board ESP32-S3-Touch-LCD-2
  • Espressif: ESP-IDF Framework
  • Duffy: Dieses LEGO GameBoy Projekt! 🎮

🎯 Roadmap

Nächste Schritte:

  1. Emulator läuft perfekt (90-111 FPS!)
  2. Audio funktioniert (4 Kanäle)
  3. Display-Scaling optimiert
  4. Button-Input implementieren
  5. ROM-Menü auf SD-Card
  6. NFC ROM-Auswahl
  7. Potentiometer Volume/Brightness
  8. Link Cable 2-Player
  9. Save-States

Viel Spaß beim Zocken! 🎮🔊

Erstellt mit Liebe zum Detail ❤️