# 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 ```c #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 ```c #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 ```c 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 ```c // 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 ```c 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 ```c 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 ```c 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):** ```c 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):** ```c 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):** ```c 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:** ```c 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 ```c 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:** ```c 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 ```c 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 ```c 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:** ```c // 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 ```c 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):** ```c // 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 ```c uint8_t gb_rom_read(struct gb_s *gb, uint32_t addr) ``` **Funktion:** Liest 1 Byte aus ROM an Adresse `addr` **Code:** ```c return rom_data[addr]; // Einfacher Array-Zugriff ``` ### gb_cart_ram_read() - Cartridge RAM lesen ```c 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 ```c 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 ```c static esp_err_t load_rom_from_sd(const char *path) ``` **Funktion:** Lädt GameBoy ROM-Datei von SD-Card **Ablauf:** ```c // 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 ```c static void display_task(void *arg) ``` **Funktion:** Läuft parallel zur Emulation, rendert Frames zum Display **Optimierung - Compact Buffer:** ```c // 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 ```c static esp_err_t mount_sd_card(void) ``` **Funktion:** Initialisiert SD-Card über SPI **Code-Erklärung:** ```c // 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 ```c void app_main(void) ``` **Kompletter Programmablauf:** ```c // ===== 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 ```c // 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 ```c // 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 ```c // 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 ```c // 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!