lego-esp32s3-gameboy/MAIN_C_ERKLAERUNG.md

23 KiB
Raw Blame History

main.c - Ausführliche Code-Erklärung

Dieses Dokument erklärt den kompletten Aufbau von main/main.c auf Deutsch.


📋 Datei-Übersicht

Datei: main/main.c (786 Zeilen) Funktion: Hauptprogramm des GameBoy-Emulators Aufgabe: Emulation, Display-Rendering, Audio-Ausgabe koordinieren


🏗️ Code-Struktur

main.c
├── Includes & Definitionen (Zeilen 1-43)
├── APU (Audio) Variablen (Zeilen 44-100)
├── APU Register-Funktionen (Zeilen 101-336)
├── Audio-Ausgabe Task (Zeilen 337-415)
├── GameBoy Palette & Callbacks (Zeilen 416-530)
├── ROM Lade-Funktionen (Zeilen 531-609)
├── Display Task (Zeilen 610-653)
├── SD-Card Mount (Zeilen 654-701)
└── Main Loop (app_main) (Zeilen 702-786)

📦 Teil 1: Includes & Konstanten (Zeilen 1-43)

Include-Dateien

#include "freertos/FreeRTOS.h"    // FreeRTOS Kernel
#include "freertos/task.h"         // Task-Verwaltung
#include "freertos/semphr.h"       // Semaphore für Synchronisation
#include "esp_system.h"            // ESP32 System-Funktionen
#include "esp_log.h"               // Logging (ESP_LOGI, ESP_LOGE)
#include "esp_heap_caps.h"         // PSRAM Allocation
#include "esp_vfs_fat.h"           // FAT Filesystem
#include "driver/i2s.h"            // I2S Audio-Treiber

Audio-Konstanten

#define SAMPLE_RATE 32768          // GameBoy Audio-Rate (32768 Hz)
#define SAMPLES_PER_FRAME 546      // 32768 Hz / 60 FPS = 546 Samples
#define SAMPLES_PER_BUFFER 512     // I2S Buffer-Größe

#define GB_CPU_FREQ 4194304.0f     // GameBoy CPU: 4.194304 MHz
#define CYCLES_PER_SAMPLE 128      // CPU-Takte pro Audio-Sample

Erklärung:

  • GameBoy läuft mit 60 FPS (59.73 FPS genau)
  • Pro Frame werden 546 Audio-Samples erzeugt (32768 / 60)
  • GameBoy CPU läuft mit ~4.19 MHz
  • Alle 128 CPU-Takte wird 1 Audio-Sample erzeugt

📦 Teil 2: APU (Audio Processing Unit) Variablen (Zeilen 44-100)

APU Register

static uint8_t apu_regs[48] = {0};  // 48 Audio-Register (0xFF10 - 0xFF3F)
static uint8_t wave_ram[16] = {0};  // 16 Bytes Wave-Pattern (Kanal 3)

GameBoy Audio-Register:

  • 0xFF10-0xFF14: Kanal 1 (Square Wave mit Sweep)
  • 0xFF15-0xFF19: Kanal 2 (Square Wave)
  • 0xFF1A-0xFF1E: Kanal 3 (Wave Pattern)
  • 0xFF1F-0xFF23: Kanal 4 (Noise)
  • 0xFF24-0xFF26: Master Control

Kanal-Status Strukturen

// Kanal 1: Square Wave mit Frequency Sweep
static struct {
    bool active;        // Kanal läuft
    bool dac_on;        // Digital-Analog-Wandler an
    uint8_t duty;       // Tastgrad (12.5%, 25%, 50%, 75%)
    uint8_t volume;     // Lautstärke (0-15)
    uint16_t freq_raw;  // Frequenz-Wert (0-2047)
    float phase;        // Aktuelle Phase (0.0 - 1.0)
} ch1;

// Kanal 2: Square Wave (wie Kanal 1, ohne Sweep)
static struct {
    bool active;
    bool dac_on;
    uint8_t duty;
    uint8_t volume;
    uint16_t freq_raw;
    float phase;
} ch2;

// Kanal 3: Wave Pattern (Custom Wellenform)
static struct {
    bool active;
    bool dac_on;
    uint8_t volume_shift;  // Lautstärke-Shift (0, 1, 2 bits)
    uint16_t freq_raw;
    float phase;
} ch3;

// Kanal 4: Noise (Zufallsrauschen)
static struct {
    bool active;
    uint8_t volume;
    uint16_t lfsr;       // Linear Feedback Shift Register (Pseudo-Random)
    uint8_t divisor;     // Frequenz-Teiler
    uint8_t shift;       // Shift-Anzahl
    bool width_mode;     // 7-bit oder 15-bit LFSR
    float timer;         // Timer für nächstes Sample
} ch4 = {.lfsr = 0x7FFF};  // LFSR Initial-Wert

Erklärung der Duty-Werte (Tastgrad):

Duty 0 (12.5%): ─┐_______  (kurzer Puls)
Duty 1 (25%):   ─┐┐______  (1/4 hoch)
Duty 2 (50%):   ─┐┐┐┐____  (Rechteck)
Duty 3 (75%):   ─┐┐┐┐┐┐__  (3/4 hoch)

Audio-System Variablen

static bool audio_enabled = false;        // Audio-System läuft
static int16_t *audio_buffer = NULL;      // Audio Ring-Buffer
static SemaphoreHandle_t apu_mutex = NULL; // Mutex für Thread-Sicherheit

📦 Teil 3: APU Register Read/Write (Zeilen 101-336)

apu_mem_read() - Register auslesen

uint8_t apu_mem_read(struct gb_s *gb, uint16_t addr)

Funktion: Liest GameBoy Audio-Register aus (0xFF10 - 0xFF3F)

Spezialfälle:

  • 0xFF26 (NR52): Master-Enable Status
  • 0xFF30-0xFF3F: Wave RAM (16 Bytes)
  • Andere Register: Direkt aus apu_regs[] Array

apu_mem_write() - Register schreiben

void apu_mem_write(struct gb_s *gb, uint16_t addr, uint8_t val)

Funktion: Schreibt in GameBoy Audio-Register und aktualisiert Kanal-Status

Wichtige Register:

Kanal 1 (Square mit Sweep):

0xFF10 (NR10): Sweep-Einstellungen
0xFF11 (NR11): Duty + Length
    duty = (val >> 6) & 0x03;  // Bits 6-7: Tastgrad (0-3)

0xFF12 (NR12): Volume Envelope
    volume = (val >> 4) & 0x0F;     // Bits 4-7: Start-Lautstärke
    dac_on = ((val & 0xF8) != 0);   // DAC an wenn Bits 3-7 != 0

0xFF13 (NR13): Frequency Low
    freq_raw = (freq_raw & 0x700) | val;  // Untere 8 Bits

0xFF14 (NR14): Frequency High + Trigger
    freq_raw = (freq_raw & 0xFF) | ((val & 0x07) << 8);  // Obere 3 Bits
    if (val & 0x80) {  // Bit 7: Trigger
        active = true;   // Kanal starten
        phase = 0.0f;    // Phase zurücksetzen
    }

Kanal 3 (Wave):

0xFF1A (NR30): DAC Enable
    dac_on = (val & 0x80) != 0;  // Bit 7: DAC an/aus

0xFF1C (NR32): Volume
    volume_shift = (val >> 5) & 0x03;  // Bits 5-6: Shift (0-3)
    // 0 = Stumm, 1 = 100%, 2 = 50%, 3 = 25%

0xFF30-0xFF3F: Wave RAM
    wave_ram[addr - 0xFF30] = val;  // 16 Bytes Custom-Wellenform

Kanal 4 (Noise):

0xFF21 (NR42): Volume
    volume = (val >> 4) & 0x0F;  // Bits 4-7: Lautstärke

0xFF22 (NR43): Polynomial Counter
    shift = (val >> 4) & 0x0F;        // Bits 4-7: Shift
    width_mode = (val & 0x08) != 0;   // Bit 3: 7-bit (1) oder 15-bit (0)
    divisor = val & 0x07;             // Bits 0-2: Divisor

0xFF23 (NR44): Trigger
    if (val & 0x80) {
        lfsr = 0x7FFF;  // LFSR zurücksetzen
        active = true;
    }

Master Control:

0xFF24 (NR50): Master Volume
    master_vol_left = (val >> 4) & 0x07;   // Bits 4-6: Links (0-7)
    master_vol_right = val & 0x07;          // Bits 0-2: Rechts (0-7)

0xFF25 (NR51): Panning
    // Bits 0-7: Welcher Kanal auf welchem Lautsprecher
    // Bit 0: Ch1 rechts, Bit 4: Ch1 links, etc.

0xFF26 (NR52): Master Enable
    master_enable = (val & 0x80) != 0;  // Bit 7: Audio an/aus
    if (!master_enable) {
        // Alle Kanäle stoppen
        ch1.active = false;
        ch2.active = false;
        ch3.active = false;
        ch4.active = false;
    }

📦 Teil 4: Audio Output Task (Zeilen 337-415)

audio_task() - I2S Audio-Ausgabe

static void audio_task(void *arg)

Funktion: Läuft dauerhaft auf Core 1, schreibt Audio-Daten zum I2S

Ablauf:

  1. Warte auf volle Buffer (512 Samples)
  2. Schreibe zu I2S (MAX98357A Verstärker)
  3. Wiederhole

Code-Erklärung:

while (1) {
    // Warte bis Buffer voll ist (512 Samples = 1024 bytes)
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    // Schreibe zu I2S
    size_t bytes_written = 0;
    i2s_write(I2S_NUM, audio_buffer, SAMPLES_PER_BUFFER * 2,
              &bytes_written, portMAX_DELAY);
}

📦 Teil 5: GameBoy Palette & Callbacks (Zeilen 416-530)

GameBoy Farbpalette

static const uint16_t gb_palette[4] = {
    0xFFFF,  // Weiß (Hintergrund) - RGB565: 11111 111111 11111
    0xAD55,  // Hellgrün           - RGB565: 10101 101010 10101
    0x52AA,  // Mittelgrün         - RGB565: 01010 010101 01010
    0x0000   // Schwarz (Vordergrund) - RGB565: 00000 000000 00000
};

Erklärung RGB565 Format:

16-bit RGB565: RRRRR GGGGGG BBBBB
               ↑     ↑      ↑
               5 bit 6 bit  5 bit
               Rot   Grün   Blau

GameBoy Graustufen:

  • GameBoy hat nur 4 Graustufen (2-bit Farbtiefe)
  • Wert 0 = Weiß (Hintergrund)
  • Wert 1 = Hellgrün
  • Wert 2 = Mittelgrün
  • Wert 3 = Schwarz (Sprites/Text)

audio_callback() - 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 (32768x pro Sekunde)

Ablauf:

// 1. Konvertiere unsigned (0-65535) zu signed (-32768 bis +32767)
int16_t sample_l = (int16_t)(left - 32768);
int16_t sample_r = (int16_t)(right - 32768);

// 2. Schreibe in Audio-Buffer (Ring-Buffer)
static int audio_write_pos = 0;
audio_buffer[audio_write_pos++] = sample_l;  // Links
audio_buffer[audio_write_pos++] = sample_r;  // Rechts

// 3. Wenn Buffer voll → I2S schreiben
if (audio_write_pos >= SAMPLES_PER_BUFFER * 2) {
    audio_write_pos = 0;
    xTaskNotifyGive(audio_task_handle);  // Audio-Task aufwecken
}

// 4. APU-Register auslesen für Status-Anzeige
ch1.active = (apu_regs[0x16] & 0x01);  // Kanal 1 läuft
ch2.active = (apu_regs[0x16] & 0x02);  // Kanal 2 läuft
ch3.active = (apu_regs[0x16] & 0x04);  // Kanal 3 läuft
ch4.active = (apu_regs[0x16] & 0x08);  // Kanal 4 läuft

Warum -32768?

  • GameBoy Audio: 0-65535 (unsigned)
  • I2S erwartet: -32768 bis +32767 (signed)
  • Umrechnung: signed = unsigned - 32768

gb_lcd_draw_line() - Display-Zeilen Callback

static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160],
                              const uint_fast8_t line)

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

Parameter:

  • pixels[160]: 160 GameBoy-Pixel für diese Zeile (Werte 0-3)
  • line: Zeilen-Nummer (0-143)

Scaling-Algorithmus (bei GB_PIXEL_PERFECT_SCALING = 1):

// 1. Vertikales Scaling: GameBoy-Zeile → Display-Y-Position
int y_base = (line * GB_RENDER_HEIGHT) / 144;
// Beispiel bei Scale 1.6: Zeile 10 → Y = 16 (10 * 230 / 144 = 16)

// 2. Horizontales Scaling: Pixel für Pixel
int x_dst = 0;
for (int x = 0; x < 160; x++) {
    // Farbwert aus Palette holen
    uint16_t c = gb_palette[pixels[x] & 0x03];

    // RGB→BGR Byte-Swap für ST7789
    uint16_t swapped = (c >> 8) | (c << 8);

    // Berechne Pixel-Breite (verhindert Lücken!)
    int next_x_dst = ((x + 1) * GB_RENDER_WIDTH) / 160;
    int pixel_width = next_x_dst - x_dst;

    // Fülle pixel_width Output-Pixel mit dieser Farbe
    for (int w = 0; w < pixel_width; w++) {
        int dst = (y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH +
                  (x_dst + w + GB_OFFSET_X);
        render_buffer[dst] = swapped;
    }

    x_dst = next_x_dst;
}

// 3. Zeilen-Duplikation (für gleichmäßiges vertikales Scaling)
int ny = ((line + 1) * GB_RENDER_HEIGHT) / 144;
if (ny > y_base + 1) {
    // Zeile duplizieren (memcpy)
    memcpy(&render_buffer[(y_base + 1 + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],
           &render_buffer[(y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],
           GB_RENDER_WIDTH * 2);  // *2 weil uint16_t = 2 bytes
}

Warum Byte-Swap?

  • Peanut-GB liefert RGB565: RRRRR GGGGGG BBBBB
  • ST7789 erwartet BGR565: BBBBB GGGGGG RRRRR
  • Byte-Swap: (c >> 8) | (c << 8) tauscht High-/Low-Byte

Scaling-Beispiel bei 1.6×:

GameBoy:   160 Pixel Breite
Display:   256 Pixel Breite (160 * 1.6 = 256)

Pixel 0  → X 0-1   (2 Pixel breit)
Pixel 1  → X 1-3   (2 Pixel breit)
Pixel 2  → X 3-4   (1 Pixel breit)  ← Abwechselnd!
Pixel 3  → X 4-6   (2 Pixel breit)
...

Pattern: 2, 2, 1, 2, 2, 1, ... (8:5 Verhältnis)

📦 Teil 6: ROM Lade-Funktionen (Zeilen 531-609)

gb_rom_read() - ROM-Bytes lesen

uint8_t gb_rom_read(struct gb_s *gb, uint32_t addr)

Funktion: Liest 1 Byte aus ROM an Adresse addr

Code:

return rom_data[addr];  // Einfacher Array-Zugriff

gb_cart_ram_read() - Cartridge RAM lesen

uint8_t gb_cart_ram_read(struct gb_s *gb, uint32_t addr)

Funktion: Liest Save-Game RAM (für Pokemon, Zelda, etc.)

Status: Aktuell nicht implementiert (return 0xFF)

gb_cart_ram_write() - Cartridge RAM schreiben

void gb_cart_ram_write(struct gb_s *gb, uint32_t addr, uint8_t val)

Funktion: Schreibt Save-Game RAM

Status: Aktuell nicht implementiert (TODO)

load_rom_from_sd() - ROM von SD-Card laden

static esp_err_t load_rom_from_sd(const char *path)

Funktion: Lädt GameBoy ROM-Datei von SD-Card

Ablauf:

// 1. Datei öffnen
FILE *f = fopen(path, "rb");

// 2. Größe ermitteln
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);

// 3. Speicher allokieren (PSRAM bevorzugt)
rom_data = heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
if (!rom_data) {
    rom_data = malloc(size);  // Fallback: Normal-RAM
}

// 4. ROM lesen
fread(rom_data, 1, size, f);

// 5. Datei schließen
fclose(f);

// 6. ROM-Info auslesen (Bytes 0x134-0x143: Titel)
char title[17] = {0};
memcpy(title, &rom_data[0x134], 16);
ESP_LOGI(TAG, "ROM: %s (%ld bytes)", title, size);

📦 Teil 7: Display Task (Zeilen 610-653)

display_task() - Display Rendering auf Core 0

static void display_task(void *arg)

Funktion: Läuft parallel zur Emulation, rendert Frames zum Display

Optimierung - Compact Buffer:

// 1. Einmalig schwarze Ränder füllen (nur bei Scaling)
#if GB_PIXEL_PERFECT_SCALING
    st7789_fill_screen(0x0000);  // Schwarz

    // Compact Buffer allokieren (nur GameBoy-Region, ohne Ränder)
    uint16_t *compact_buffer = heap_caps_malloc(
        GB_RENDER_WIDTH * GB_RENDER_HEIGHT * 2,
        MALLOC_CAP_DMA  // DMA-fähiger Speicher für SPI
    );
#endif

// 2. Frame-Rendering Loop
while (1) {
    // Warte auf fertigen Frame
    xSemaphoreTake(frame_ready_sem, portMAX_DELAY);

    #if GB_PIXEL_PERFECT_SCALING
        // Kopiere nur GameBoy-Region (ohne schwarze Ränder)
        for (int y = 0; y < GB_RENDER_HEIGHT; y++) {
            memcpy(
                &compact_buffer[y * GB_RENDER_WIDTH],  // Ziel
                &display_buffer[(y + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X],  // Quelle
                GB_RENDER_WIDTH * 2  // Anzahl Bytes
            );
        }

        // SPI-Transfer (33% weniger Daten!)
        st7789_draw_buffer_preswapped(
            compact_buffer,
            GB_OFFSET_X, GB_OFFSET_Y,
            GB_RENDER_WIDTH, GB_RENDER_HEIGHT
        );
    #else
        // Fullscreen: Ganzen Buffer senden
        st7789_draw_buffer_preswapped(
            display_buffer,
            0, 0,
            GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT
        );
    #endif

    // Signalisiere Emulation: Fertig, nächstes Frame!
    xSemaphoreGive(frame_done_sem);
}

Warum Compact Buffer?

Ohne Compact Buffer:
┌──────────────────────────┐
│ Schwarz    (40 Pixel)    │  ← Wird mit übertragen
├──────────────────────────┤
│                          │
│   GameBoy (256×230)      │  ← Nur das wird gebraucht!
│                          │
├──────────────────────────┤
│ Schwarz    (24 Pixel)    │  ← Wird mit übertragen
└──────────────────────────┘
Total: 320×240 = 76.800 Pixel

Mit Compact Buffer:
┌──────────────────────────┐
│   GameBoy (256×230)      │  ← Nur das wird übertragen!
└──────────────────────────┘
Total: 256×230 = 58.880 Pixel (23% weniger!)

Ergebnis: 3-4ms schneller pro Frame!

📦 Teil 8: SD-Card Mount (Zeilen 654-701)

mount_sd_card() - SD-Karte mounten

static esp_err_t mount_sd_card(void)

Funktion: Initialisiert SD-Card über SPI

Code-Erklärung:

// 1. SPI-Bus Konfiguration
sdmmc_host_t host = SDSPI_HOST_DEFAULT();  // SPI-Modus
sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
slot_config.gpio_cs = SD_PIN_CS;  // GPIO 41
slot_config.host_id = SD_SPI_HOST;  // Shared mit Display

// 2. Mount-Konfiguration
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
    .format_if_mount_failed = false,  // NICHT formatieren!
    .max_files = 5,
    .allocation_unit_size = 16 * 1024
};

// 3. Mounten
sdmmc_card_t *card;
esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card);

// 4. Card-Info anzeigen
ESP_LOGI(TAG, "SD Card: %s, %llu MB",
         card->cid.name,
         ((uint64_t) card->csd.capacity) * card->csd.sector_size / (1024 * 1024));

📦 Teil 9: Main Loop (app_main) (Zeilen 702-786)

app_main() - Hauptprogramm

void app_main(void)

Kompletter Programmablauf:

// ===== 1. DISPLAY INITIALISIEREN =====
ESP_LOGI(TAG, "Init Display...");
st7789_init();  // ST7789 mit 80 MHz SPI
st7789_set_backlight(80);  // 80% Helligkeit

// ===== 2. SD-KARTE MOUNTEN =====
ESP_LOGI(TAG, "Init SD...");
mount_sd_card();  // FAT32 Filesystem

// ===== 3. ROM LADEN =====
ESP_LOGI(TAG, "Load ROM...");
load_rom_from_sd("/sdcard/tetris.gb");  // Fest verdrahtet

// ===== 4. PSRAM PRÜFEN & BUFFER ALLOKIEREN =====
size_t psram_size = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
ESP_LOGI(TAG, "PSRAM: %d KB total, %d KB free",
         psram_size / 1024,
         heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024);

// Double-Buffering: 2× 320×240 = 150 KB pro Buffer
size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2;

render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);

// Mit Schwarz füllen
memset(render_buffer, 0, buffer_size);
memset(display_buffer, 0, buffer_size);

// ===== 5. AUDIO INITIALISIEREN =====
ESP_LOGI(TAG, "Init Audio...");

// I2S Konfiguration
i2s_config_t i2s_config = {
    .mode = I2S_MODE_MASTER | I2S_MODE_TX,  // Master, nur Senden
    .sample_rate = SAMPLE_RATE,             // 32768 Hz
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_STAND_I2S,
    .dma_buf_count = I2S_DMA_BUF_COUNT,     // 8 Buffer
    .dma_buf_len = I2S_DMA_BUF_LEN,         // 1024 Samples
    .use_apll = false,
    .tx_desc_auto_clear = true
};

i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);

// Pin-Konfiguration
i2s_pin_config_t pin_config = {
    .bck_io_num = I2S_PIN_BCLK,    // GPIO 48
    .ws_io_num = I2S_PIN_LRC,       // GPIO 47
    .data_out_num = I2S_PIN_DIN,    // GPIO 16
    .data_in_num = -1               // Kein Input
};
i2s_set_pin(I2S_NUM, &pin_config);

// Audio Buffer allokieren
audio_buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 2 * sizeof(int16_t),
                                MALLOC_CAP_DMA);

// Audio Task starten (Core 1, Priority 5)
xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5,
                        &audio_task_handle, 1);
audio_enabled = true;

// ===== 6. EMULATOR INITIALISIEREN =====
struct gb_s gb;
gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write,
        &audio_callback, NULL);

gb_init_lcd(&gb, &gb_lcd_draw_line);

// Palette setzen
for (int i = 0; i < 4; i++) {
    gb.display.palette[i] = gb_palette[i];
}

// APU Callbacks registrieren
gb.apu.apu_mem_read = apu_mem_read;
gb.apu.apu_mem_write = apu_mem_write;

// ===== 7. DISPLAY TASK STARTEN =====
frame_ready_sem = xSemaphoreCreateBinary();  // Semaphore erstellen
frame_done_sem = xSemaphoreCreateBinary();
xSemaphoreGive(frame_done_sem);  // Initial freigeben

xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 10,
                        NULL, 0);  // Core 0, Priority 10

// ===== 8. EMULATION LOOP =====
ESP_LOGI(TAG, "✓ %s with FIXED AUDIO! 🎮🔊", rom_title);

uint32_t frame_count = 0;
int64_t last_time = esp_timer_get_time();

while (1) {
    // Warte auf Display-Task fertig
    xSemaphoreTake(frame_done_sem, portMAX_DELAY);

    // Emuliere 1 Frame (70224 CPU-Takte, ~16.7ms)
    gb_run_frame(&gb);

    // Buffer Swap (Render ↔ Display)
    uint16_t *temp = render_buffer;
    render_buffer = display_buffer;
    display_buffer = temp;

    // Display-Task aufwecken
    xSemaphoreGive(frame_ready_sem);

    frame_count++;

    // Alle 60 Frames: FPS ausgeben
    if (frame_count % 60 == 0) {
        int64_t now = esp_timer_get_time();
        int32_t time_ms = (now - last_time) / 1000 / 60;
        float fps = 1000.0f / time_ms;

        ESP_LOGI(TAG, "Frame %ld | time=%ldms (%.1f FPS) | sound=%s | "
                      "ch1=%d ch2=%d ch3=%d ch4=%d",
                 frame_count, time_ms, fps,
                 master_enable ? "ON" : "OFF",
                 ch1.active, ch2.active, ch3.active, ch4.active);

        last_time = now;
    }
}

Zusammenfassung:

  1. Display init (80 MHz SPI, 80% Helligkeit)
  2. SD-Card mount (FAT32)
  3. ROM laden (tetris.gb)
  4. PSRAM check & Buffer alloc (2× 150 KB)
  5. Audio init (I2S 32768 Hz, Task auf Core 1)
  6. Emulator init (Peanut-GB mit Callbacks)
  7. Display Task start (Core 0, parallel rendering)
  8. Emulation Loop (60 FPS, Double-Buffering, FPS-Logging)

🎯 Performance-Tricks im Code

1. Double-Buffering mit Semaphoren

// Emulation wartet auf Display fertig
xSemaphoreTake(frame_done_sem, portMAX_DELAY);

// ... emuliert ...

// Buffer tauschen (Pointer-Swap, keine Kopie!)
uint16_t *temp = render_buffer;
render_buffer = display_buffer;
display_buffer = temp;

// Display aufwecken
xSemaphoreGive(frame_ready_sem);

Vorteil: Emulation + Display laufen parallel = 50% schneller!

2. Byte-Swapping im Emulator

// RGB→BGR beim Rendern, nicht beim Transfer
uint16_t swapped = (c >> 8) | (c << 8);
render_buffer[dst] = swapped;

Vorteil: Kein Byte-Swap beim SPI-Transfer nötig!

3. Compact Buffer

// Nur GameBoy-Region kopieren, nicht ganze 320×240
for (int y = 0; y < GB_RENDER_HEIGHT; y++) {
    memcpy(&compact_buffer[y * GB_RENDER_WIDTH],
           &display_buffer[...], GB_RENDER_WIDTH * 2);
}

Vorteil: 23% weniger SPI-Daten = 3-4ms schneller!

4. PSRAM für große Buffer

// Große Buffer in PSRAM (nicht in limited SRAM)
render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);

Vorteil: 8MB verfügbar statt nur 512KB SRAM!


📊 Timing-Übersicht

GameBoy Frame (16.7ms @ 59.73 FPS):
├─ CPU Emulation:    ~6ms   (70224 Takte)
├─ PPU Rendering:    ~4ms   (144 Zeilen)
├─ APU Audio:        ~2ms   (546 Samples)
└─ Buffer Swap:      <1ms   (Pointer-Tausch)

Display Rendering (parallel auf Core 0):
├─ Compact Buffer:   ~2ms   (Zeilen kopieren)
├─ SPI Transfer:     ~10ms  (80 MHz, 117 KB)
└─ Semaphore:        <1ms

Total Zeit: ~16ms (beide Cores parallel!)
Ergebnis: 60-90 FPS je nach Spiel

Ende der main.c Erklärung

Dieses Dokument erklärt alle wichtigen Bereiche von main.c auf Deutsch mit Code-Beispielen und Erklärungen. Für weitere Fragen zu spezifischen Funktionen, siehe die Kommentare direkt im Code!