/** * @file main.c * @brief ESP32-S3 GameBoy Emulator - Hauptprogramm mit Audio, Display und Emulation * * Dieses Programm implementiert einen vollständigen GameBoy Emulator auf dem ESP32-S3. * Es verwendet: * - Peanut-GB Emulator Core für die GameBoy-Emulation * - ST7789 Display (320x240, 80 MHz SPI) für die Ausgabe * - I2S Audio (MAX98357A, 32768 Hz) für Sound * - PSRAM Double-Buffering für flüssige Darstellung * - FreeRTOS Dual-Core: Core 0 für Display, Core 1 für Emulation */ #include #include #include #include "freertos/FreeRTOS.h" // FreeRTOS Betriebssystem #include "freertos/task.h" // Task-Management (Multi-Threading) #include "freertos/semphr.h" // Semaphoren für Buffer-Synchronisation #include "esp_system.h" // ESP32 System-Funktionen #include "esp_log.h" // Logging-System #include "nvs_flash.h" // Non-Volatile Storage (Einstellungen) #include "esp_heap_caps.h" // Heap-Verwaltung (PSRAM, DMA) #include "esp_vfs_fat.h" // FAT-Dateisystem für SD-Karte #include "sdmmc_cmd.h" // SD-Karten Kommandos #include "driver/sdmmc_host.h" // SD-Karten Host-Treiber #include "driver/sdspi_host.h" // SD-Karten SPI-Modus #include "driver/i2s.h" // I2S Audio-Treiber #include "driver/gpio.h" // GPIO für Power-Button Check #include "esp_sleep.h" // Deep Sleep Funktionen #include "hardware_config.h" // Hardware-Pin-Definitionen #include "st7789.h" // ST7789 Display-Treiber #include "buttons.h" // Button-Handler für GameBoy-Eingabe und Power // Display-Größe zwischenspeichern (wird von peanut_gb.h überschrieben) #define DISPLAY_WIDTH LCD_WIDTH #define DISPLAY_HEIGHT LCD_HEIGHT #undef LCD_WIDTH #undef LCD_HEIGHT // ============================================ // APU (Audio Processing Unit) Konstanten // ============================================ // Der GameBoy hat eine 4-Kanal APU für Sound-Synthese #define SAMPLE_RATE 32768 // Audio-Sample-Rate: 32768 Hz (GameBoy-nativ) #define SAMPLES_PER_FRAME 546 // Pro Frame bei 60 FPS: 32768 / 60 = 546 Samples #define SAMPLES_PER_BUFFER 512 // Buffer-Größe für I2S DMA Transfer // GameBoy CPU-Frequenz: 4.194304 MHz (exakte DMG GameBoy Frequenz) #define GB_CPU_FREQ 4194304.0f // CPU-Zyklen pro Audio-Sample: ~128 Zyklen // Dies wird verwendet, um Audio-Events zu synchronisieren #define CYCLES_PER_SAMPLE (GB_CPU_FREQ / SAMPLE_RATE) static const char *TAG = "GB"; // Log-Tag für ESP_LOG Ausgaben // ============================================ // APU Register-Speicher (GameBoy Audio-Register) // ============================================ // Der GameBoy hat Audio-Register von 0xFF10 bis 0xFF3F // Diese 48 Bytes speichern alle Sound-Einstellungen static uint8_t apu_regs[48] = {0}; // Register-Werte 0xFF10-0xFF3F static uint8_t wave_ram[16] = {0}; // Wave-Pattern für Kanal 3 (16 Bytes, 32 Samples) // Master-Audio-Kontrolle static bool master_enable = false; // Master Audio AN/AUS (NR52, Bit 7) static uint8_t master_vol_left = 7; // Master-Lautstärke links (0-7) static uint8_t master_vol_right = 7; // Master-Lautstärke rechts (0-7) static uint8_t panning = 0xFF; // Kanal-Routing: Links/Rechts Mixer (NR51) // ============================================ // Kanal 1: Square Wave mit Frequency Sweep // ============================================ // Dieser Kanal erzeugt Rechteck-Wellen mit einstellbarem Tastgrad // und kann die Frequenz automatisch ändern (Sweep-Effekt) static struct { bool active; // Kanal spielt gerade bool dac_on; // Digital-Analog-Wandler ist aktiv (NR12 Bit 3-7) uint8_t duty; // Tastgrad-Index (0-3): 12.5%, 25%, 50%, 75% uint8_t volume; // Lautstärke (0-15) uint16_t freq_raw; // Frequenz-Rohwert (0-2047) aus NR13/NR14 float phase; // Aktuelle Wellenform-Phase (0.0-1.0) } ch1 = {0}; // ============================================ // Kanal 2: Square Wave (einfache Rechteck-Welle) // ============================================ // Identisch zu Kanal 1, aber ohne Frequency Sweep static struct { bool active; // Kanal spielt gerade bool dac_on; // Digital-Analog-Wandler ist aktiv (NR22 Bit 3-7) uint8_t duty; // Tastgrad-Index (0-3): 12.5%, 25%, 50%, 75% uint8_t volume; // Lautstärke (0-15) uint16_t freq_raw; // Frequenz-Rohwert (0-2047) aus NR23/NR24 float phase; // Aktuelle Wellenform-Phase (0.0-1.0) } ch2 = {0}; // ============================================ // Kanal 3: Wave Pattern (benutzerdefinierte Wellenform) // ============================================ // Dieser Kanal spielt eine benutzerdefinierte Wellenform ab, // die im Wave RAM (32 x 4-bit Samples) gespeichert ist static struct { bool active; // Kanal spielt gerade bool dac_on; // Digital-Analog-Wandler ist aktiv (NR30 Bit 7) uint8_t volume_shift; // Lautstärke-Shift (0=stumm, 1=100%, 2=50%, 3=25%) uint16_t freq_raw; // Frequenz-Rohwert (0-2047) aus NR33/NR34 float phase; // Aktuelle Wellenform-Phase (0.0-1.0) } ch3 = {0}; // ============================================ // Kanal 4: Noise (Pseudo-Zufalls-Rauschen) // ============================================ // Dieser Kanal erzeugt weißes Rauschen mittels LFSR-Generator // (Linear Feedback Shift Register) static struct { bool active; // Kanal spielt gerade uint8_t volume; // Lautstärke (0-15) uint16_t lfsr; // 15-bit LFSR für Pseudo-Zufall (initialisiert mit 0x7FFF) uint8_t divisor; // Frequenz-Divisor (0-7) uint8_t shift; // Frequenz-Shift (0-15) bool width_mode; // 7-bit oder 15-bit LFSR Modus float timer; // Interner Timer für LFSR-Schritte } ch4 = {.lfsr = 0x7FFF}; // LFSR mit allen Bits auf 1 initialisieren // ============================================ // Audio-System Variablen // ============================================ static bool audio_enabled = false; // Audio-System aktiv static int16_t *audio_buffer = NULL; // DMA-Buffer für I2S Audio static SemaphoreHandle_t apu_mutex = NULL;// Mutex für Thread-sichere Register-Zugriffe // ============================================ // System Shutdown Flag (für Deep Sleep) // ============================================ static volatile bool system_shutdown = false; // Wenn true: Alle Tasks beenden // Debug-Zähler static int audio_write_count = 0; // Zählt Audio-Register-Schreibvorgänge // ============================================ // Duty Cycle Wellenformen (Tastgrad-Tabellen) // ============================================ // Jede Wellenform hat 8 Schritte und ist BIPOLAR (-1, +1) // Dies erzeugt korrekte Rechteck-Wellen mit AC-Kopplung // // Visualisierung: // Duty 0 (12.5%): ─┐_______ (1 Step HIGH, 7 Steps LOW) // Duty 1 (25%): ─┐┐______ (2 Steps HIGH, 6 Steps LOW) // Duty 2 (50%): ─┐┐┐┐____ (4 Steps HIGH, 4 Steps LOW) // Duty 3 (75%): ─┐┐┐┐┐┐__ (6 Steps HIGH, 2 Steps LOW, invertiert) // static const int8_t duty_table[4][8] = { {-1, -1, -1, -1, -1, -1, -1, 1}, // 12.5% Tastgrad { 1, -1, -1, -1, -1, -1, -1, 1}, // 25% Tastgrad { 1, -1, -1, -1, -1, 1, 1, 1}, // 50% Tastgrad (Rechteck) {-1, 1, 1, 1, 1, 1, 1, -1}, // 75% Tastgrad (invertiert) }; // ============================================ // Peanut-GB Audio Callbacks // ============================================ // Diese Funktionen werden vom Peanut-GB Emulator aufgerufen, // wenn das GameBoy-ROM Audio-Register liest oder schreibt uint8_t audio_read(const uint16_t addr); void audio_write(const uint16_t addr, const uint8_t val); /** * @brief Audio-Register lesen * @param addr Register-Adresse (0xFF10-0xFF3F) * @return Aktueller Register-Wert * * Diese Funktion wird vom Emulator aufgerufen, wenn das GameBoy-ROM * ein Audio-Register ausliest (z.B. NR52 um Kanal-Status zu prüfen) */ uint8_t audio_read(const uint16_t addr) { // Wave RAM lesen (0xFF30-0xFF3F): 16 Bytes für Kanal 3 Wellenform if (addr >= 0xFF30 && addr <= 0xFF3F) { return wave_ram[addr - 0xFF30]; } // NR52 (0xFF26): Master Audio Status Register // Bit 7: Master Enable (1=AN, 0=AUS) // Bit 3: Kanal 4 Status (1=aktiv, 0=inaktiv) // Bit 2: Kanal 3 Status // Bit 1: Kanal 2 Status // Bit 0: Kanal 1 Status // Bit 6-4: Immer 1 (unused bits) if (addr == 0xFF26) { uint8_t status = master_enable ? 0x80 : 0x00; if (ch1.active) status |= 0x01; if (ch2.active) status |= 0x02; if (ch3.active) status |= 0x04; if (ch4.active) status |= 0x08; return status | 0x70; // Bits 6-4 immer gesetzt } // Normale APU Register lesen (0xFF10-0xFF3F) if (addr >= 0xFF10 && addr <= 0xFF3F) { return apu_regs[addr - 0xFF10]; } // Unbekannte Adresse: 0xFF zurückgeben (GameBoy Hardware Verhalten) return 0xFF; } /** * @brief Audio-Register schreiben * @param addr Register-Adresse (0xFF10-0xFF3F) * @param val Zu schreibender Wert * * Diese Funktion wird vom Emulator aufgerufen, wenn das GameBoy-ROM * ein Audio-Register beschreibt (z.B. NR10-NR52 für Sound-Kontrolle) * * Register-Map: * - 0xFF10-0xFF14: Kanal 1 (Square mit Sweep) * - 0xFF16-0xFF19: Kanal 2 (Square) * - 0xFF1A-0xFF1E: Kanal 3 (Wave) * - 0xFF20-0xFF23: Kanal 4 (Noise) * - 0xFF24-0xFF25: Master Volume & Panning * - 0xFF26: Master Enable * - 0xFF30-0xFF3F: Wave RAM (32 x 4-bit Samples) */ void audio_write(const uint16_t addr, const uint8_t val) { // Thread-Sicherheit: Mutex sperren für atomare Register-Änderungen if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY); audio_write_count++; // Debug-Zähler erhöhen // === Wave RAM schreiben (0xFF30-0xFF3F) === // 16 Bytes = 32 Samples à 4 Bit für Kanal 3 Wellenform if (addr >= 0xFF30 && addr <= 0xFF3F) { wave_ram[addr - 0xFF30] = val; if (apu_mutex) xSemaphoreGive(apu_mutex); return; } // === Alle APU Register im Array speichern === // Dies erlaubt späteres Auslesen der Register-Werte if (addr >= 0xFF10 && addr <= 0xFF3F) { apu_regs[addr - 0xFF10] = val; } // === Register-Verarbeitung === switch (addr) { // ╔═══════════════════════════════════════════════════════╗ // ║ NR52 (0xFF26) - Master Audio Control ║ // ╚═══════════════════════════════════════════════════════╝ case 0xFF26: master_enable = (val & 0x80) != 0; // Bit 7: Master AN/AUS // Wenn Master AUS: Alle Kanäle stoppen und Register löschen if (!master_enable) { ch1.active = ch2.active = ch3.active = ch4.active = false; memset(apu_regs, 0, 0x17); // Register 0xFF10-0xFF26 löschen } break; // ╔═══════════════════════════════════════════════════════╗ // ║ Kanal 1: Square Wave mit Frequency Sweep ║ // ╚═══════════════════════════════════════════════════════╝ // NR11 (0xFF11): Duty Cycle & Length // Bit 7-6: Duty (00=12.5%, 01=25%, 10=50%, 11=75%) // Bit 5-0: Sound Length (wird vom Length Counter benutzt) case 0xFF11: ch1.duty = (val >> 6) & 3; // Duty-Index extrahieren break; // NR12 (0xFF12): Volume Envelope // Bit 7-4: Initial Volume (0-15) // Bit 3: Envelope Direction (0=decrease, 1=increase) // Bit 2-0: Envelope Sweep (0=off, 1-7=Schritte) // WICHTIG: DAC ist nur aktiv, wenn Bit 3-7 != 0 case 0xFF12: ch1.volume = (val >> 4) & 0x0F; // Start-Lautstärke ch1.dac_on = (val & 0xF8) != 0; // DAC Check (Bit 3-7) if (!ch1.dac_on) ch1.active = false;// DAC aus → Kanal stoppen break; // NR13 (0xFF13): Frequency Low Byte // Bit 7-0: Untere 8 Bits der Frequenz (0-255) case 0xFF13: ch1.freq_raw = (ch1.freq_raw & 0x700) | val; // Low-Byte setzen break; // NR14 (0xFF14): Frequency High + Trigger // Bit 7: Trigger (1=Kanal starten) // Bit 6: Length Enable (1=Length Counter aktiv) // Bit 2-0: Obere 3 Bits der Frequenz (0-7) case 0xFF14: ch1.freq_raw = (ch1.freq_raw & 0xFF) | ((val & 0x07) << 8); // High-Bits // Trigger-Bit gesetzt: Kanal neu starten if (val & 0x80) { ch1.active = ch1.dac_on; // Nur starten wenn DAC an ch1.phase = 0; // Phase zurücksetzen ch1.volume = (apu_regs[0x02] >> 4) & 0x0F; // Volume neu laden } break; // ╔═══════════════════════════════════════════════════════╗ // ║ Kanal 2: Square Wave (ohne Sweep) ║ // ╚═══════════════════════════════════════════════════════╝ // NR21 (0xFF16): Duty Cycle & Length // Identisch zu Kanal 1, aber ohne Sweep-Register case 0xFF16: ch2.duty = (val >> 6) & 3; // Duty-Index extrahieren break; // NR22 (0xFF17): Volume Envelope case 0xFF17: ch2.volume = (val >> 4) & 0x0F; // Start-Lautstärke ch2.dac_on = (val & 0xF8) != 0; // DAC Check if (!ch2.dac_on) ch2.active = false; break; // NR23 (0xFF18): Frequency Low Byte case 0xFF18: ch2.freq_raw = (ch2.freq_raw & 0x700) | val; break; // NR24 (0xFF19): Frequency High + Trigger case 0xFF19: ch2.freq_raw = (ch2.freq_raw & 0xFF) | ((val & 0x07) << 8); if (val & 0x80) { ch2.active = ch2.dac_on; ch2.phase = 0; ch2.volume = (apu_regs[0x07] >> 4) & 0x0F; // NR22 nachladen } break; // ╔═══════════════════════════════════════════════════════╗ // ║ Kanal 3: Wave Pattern (Custom Waveform) ║ // ╚═══════════════════════════════════════════════════════╝ // NR30 (0xFF1A): DAC Enable // Bit 7: DAC Power (1=AN, 0=AUS) // Kanal 3 benötigt explizites DAC-Enable case 0xFF1A: ch3.dac_on = (val & 0x80) != 0; if (!ch3.dac_on) ch3.active = false; // DAC aus → Kanal stoppen break; // NR32 (0xFF1C): Output Level // Bit 6-5: Volume Code // 00 = Stumm (0%) // 01 = 100% (kein Shift) // 10 = 50% (>> 1) // 11 = 25% (>> 2) case 0xFF1C: ch3.volume_shift = (val >> 5) & 3; // Volume-Code extrahieren break; // NR33 (0xFF1D): Frequency Low Byte case 0xFF1D: ch3.freq_raw = (ch3.freq_raw & 0x700) | val; break; // NR34 (0xFF1E): Frequency High + Trigger case 0xFF1E: ch3.freq_raw = (ch3.freq_raw & 0xFF) | ((val & 0x07) << 8); if (val & 0x80) { ch3.active = ch3.dac_on; ch3.phase = 0; } break; // ╔═══════════════════════════════════════════════════════╗ // ║ Kanal 4: Noise (LFSR Generator) ║ // ╚═══════════════════════════════════════════════════════╝ // NR42 (0xFF21): Volume Envelope // Identisch zu NR12/NR22 case 0xFF21: ch4.volume = (val >> 4) & 0x0F; // Kanal 4 DAC Check: Bits 3-7 müssen != 0 sein if ((val & 0xF8) == 0) ch4.active = false; break; // NR43 (0xFF22): Polynomial Counter // Bit 7-4: Clock Shift (0-15) // Bit 3: LFSR Width (0=15-bit, 1=7-bit) // Bit 2-0: Clock Divisor (0-7) // Frequenz = 524288 Hz / divisor / 2^(shift+1) case 0xFF22: ch4.shift = (val >> 4) & 0x0F; // Frequenz-Shift ch4.width_mode = (val >> 3) & 1; // 7-bit oder 15-bit LFSR ch4.divisor = val & 0x07; // Divisor-Code break; // NR44 (0xFF23): Trigger // Bit 7: Trigger (1=Kanal starten) // Bit 6: Length Enable case 0xFF23: if (val & 0x80) { ch4.active = true; ch4.lfsr = 0x7FFF; // LFSR auf 0111111111111111 setzen ch4.timer = 0; // Timer zurücksetzen ch4.volume = (apu_regs[0x11] >> 4) & 0x0F; // NR42 nachladen } break; // ╔═══════════════════════════════════════════════════════╗ // ║ NR50/NR51 - Master Volume & Panning ║ // ╚═══════════════════════════════════════════════════════╝ // NR50 (0xFF24): Master Volume // Bit 7: Vin→Left Enable (nicht implementiert) // Bit 6-4: Left Master Volume (0-7) // Bit 3: Vin→Right Enable (nicht implementiert) // Bit 2-0: Right Master Volume (0-7) case 0xFF24: master_vol_left = (val >> 4) & 7; master_vol_right = val & 7; break; // NR51 (0xFF25): Sound Panning // Bit 7: Kanal 4 → Left // Bit 6: Kanal 3 → Left // Bit 5: Kanal 2 → Left // Bit 4: Kanal 1 → Left // Bit 3: Kanal 4 → Right // Bit 2: Kanal 3 → Right // Bit 1: Kanal 2 → Right // Bit 0: Kanal 1 → Right case 0xFF25: panning = val; break; } // Thread-Sicherheit: Mutex freigeben if (apu_mutex) xSemaphoreGive(apu_mutex); } // ============================================ // Audio Sample-Generierung // ============================================ // Diese Funktionen berechnen die tatsächlichen Audio-Samples // für alle 4 Kanäle und mischen sie zusammen /** * @brief Frequenz-Berechnung für Kanal 1 & 2 (Square Wave) * @param freq_raw Rohwert aus NRx3/NRx4 Register (0-2047) * @return Frequenz in Hz * * GameBoy Frequenz-Formel für Square Channels: * f = 131072 / (2048 - freq_raw) * * Beispiele: * freq_raw = 1024 → f = 131072 / 1024 = 128 Hz * freq_raw = 1800 → f = 131072 / 248 = 528.6 Hz (C5) * freq_raw = 2000 → f = 131072 / 48 = 2730.7 Hz (hoher Ton) */ static inline float get_frequency(uint16_t freq_raw) { if (freq_raw >= 2048) return 0; // Ungültiger Wert return 131072.0f / (2048.0f - freq_raw); } /** * @brief Frequenz-Berechnung für Kanal 3 (Wave Channel) * @param freq_raw Rohwert aus NR33/NR34 Register (0-2047) * @return Frequenz in Hz * * GameBoy Frequenz-Formel für Wave Channel: * f = 65536 / (2048 - freq_raw) * * Der Wave Channel läuft mit halber Frequenz, da die Wellenform * 32 Samples lang ist (statt 8 bei Square Channels) */ static inline float get_wave_frequency(uint16_t freq_raw) { if (freq_raw >= 2048) return 0; // Ungültiger Wert return 65536.0f / (2048.0f - freq_raw); } /** * @brief Audio-Samples generieren und mischen * @param buffer Ausgabe-Buffer für Stereo-Samples (L, R, L, R, ...) * @param num_samples Anzahl der zu generierenden Samples (pro Kanal) * * Diese Funktion wird vom audio_task aufgerufen und generiert * die Audio-Samples für alle 4 Kanäle. Die Samples werden gemischt * und als 16-bit Stereo (interleaved) ausgegeben. * * Format: buffer[0]=L, buffer[1]=R, buffer[2]=L, buffer[3]=R, ... */ static void generate_samples(int16_t *buffer, int num_samples) { // Thread-Sicherheit: Register-Zugriff sperren if (apu_mutex) xSemaphoreTake(apu_mutex, portMAX_DELAY); // Für jedes Sample in diesem Buffer... for (int i = 0; i < num_samples; i++) { int32_t left = 0; // Linker Kanal Akkumulator int32_t right = 0; // Rechter Kanal Akkumulator // Master Audio AUS: Stille ausgeben if (!master_enable) { buffer[i * 2] = 0; // Links = 0 buffer[i * 2 + 1] = 0; // Rechts = 0 continue; } // ═══════════════════════════════════════════════════════ // Kanal 1: Square Wave mit Duty Cycle // ═══════════════════════════════════════════════════════ if (ch1.active && ch1.dac_on && ch1.volume > 0 && ch1.freq_raw > 0) { // 1. Frequenz aus Register berechnen float freq = get_frequency(ch1.freq_raw); // 2. Phase-Inkrement: Wie viel der Wellenform pro Sample? // Bei 440 Hz und 32768 Hz Sample-Rate: // phase_inc = 440 / 32768 = 0.0134 pro Sample float phase_inc = freq / SAMPLE_RATE; // 3. Phase vorwärts bewegen (0.0 bis 1.0) ch1.phase += phase_inc; if (ch1.phase >= 1.0f) ch1.phase -= 1.0f; // Wrap-around // 4. Wellenform-Index bestimmen (0-7 Schritte) int step = (int)(ch1.phase * 8) & 7; // 5. Sample aus Duty-Tabelle holen und mit Volume skalieren // duty_table[duty][step] gibt -1 oder +1 // Multipliziert mit volume (0-15) → -15 bis +15 int sample = duty_table[ch1.duty][step] * ch1.volume; // 6. Panning: Kanal zu Links/Rechts routen (NR51) if (panning & 0x10) left += sample; // Bit 4: Ch1 → Left if (panning & 0x01) right += sample; // Bit 0: Ch1 → Right } // ═══════════════════════════════════════════════════════ // Kanal 2: Square Wave (identisch zu Kanal 1) // ═══════════════════════════════════════════════════════ if (ch2.active && ch2.dac_on && ch2.volume > 0 && ch2.freq_raw > 0) { float freq = get_frequency(ch2.freq_raw); float phase_inc = freq / SAMPLE_RATE; ch2.phase += phase_inc; if (ch2.phase >= 1.0f) ch2.phase -= 1.0f; int step = (int)(ch2.phase * 8) & 7; int sample = duty_table[ch2.duty][step] * ch2.volume; // Panning für Kanal 2 if (panning & 0x20) left += sample; // Bit 5: Ch2 → Left if (panning & 0x02) right += sample; // Bit 1: Ch2 → Right } // ═══════════════════════════════════════════════════════ // Kanal 3: Wave Pattern (Custom Waveform) // ═══════════════════════════════════════════════════════ if (ch3.active && ch3.dac_on && ch3.freq_raw > 0) { // 1. Frequenz berechnen (Wave Channel Formel) float freq = get_wave_frequency(ch3.freq_raw); float phase_inc = freq / SAMPLE_RATE; // 2. Phase vorwärts bewegen ch3.phase += phase_inc; if (ch3.phase >= 1.0f) ch3.phase -= 1.0f; // 3. Wave RAM Position bestimmen (0-31) // Wave RAM hat 32 Samples à 4 Bit int pos = (int)(ch3.phase * 32) & 31; // 4. Sample aus Wave RAM lesen // Wave RAM: 16 Bytes, jedes Byte = 2 Samples (4-bit packed) // Format: [High Nibble | Low Nibble] pro Byte int byte_idx = pos / 2; // Welches Byte? int sample_raw; if (pos & 1) { // Ungerade Position: Low Nibble (Bits 0-3) sample_raw = wave_ram[byte_idx] & 0x0F; } else { // Gerade Position: High Nibble (Bits 4-7) sample_raw = wave_ram[byte_idx] >> 4; } // 5. Volume-Shift anwenden (NR32) // volume_shift: 0=Stumm, 1=100%, 2=50%, 3=25% int sample = 0; if (ch3.volume_shift > 0) { int shift = ch3.volume_shift - 1; // 1→0, 2→1, 3→2 sample = (sample_raw >> shift) - 8; // Zentrieren um 0 } // 6. Panning für Kanal 3 if (panning & 0x40) left += sample; // Bit 6: Ch3 → Left if (panning & 0x04) right += sample; // Bit 2: Ch3 → Right } // ═══════════════════════════════════════════════════════ // Kanal 4: Noise (LFSR Pseudo-Random Generator) // ═══════════════════════════════════════════════════════ if (ch4.active && ch4.volume > 0) { // 1. Noise-Frequenz berechnen (NR43) // Formel: f = 524288 Hz / divisor / 2^(shift+1) // divisor: 0→8, 1→16, 2→32, 3→48, 4→64, 5→80, 6→96, 7→112 int divisor = (ch4.divisor == 0) ? 8 : (ch4.divisor * 16); float noise_freq = 524288.0f / divisor / (1 << (ch4.shift + 1)); float timer_inc = noise_freq / SAMPLE_RATE; // 2. Timer vorwärts bewegen ch4.timer += timer_inc; // 3. LFSR-Schritte durchführen (falls Timer >= 1.0) // Ein LFSR-Schritt = ein neues Zufalls-Bit while (ch4.timer >= 1.0f) { ch4.timer -= 1.0f; // LFSR-Algorithmus: XOR von Bit 0 und Bit 1 int bit = (ch4.lfsr ^ (ch4.lfsr >> 1)) & 1; // LFSR nach rechts schieben, neues Bit an Position 14 ch4.lfsr = (ch4.lfsr >> 1) | (bit << 14); // Width Mode: 7-bit LFSR (Bit auch an Position 6 setzen) if (ch4.width_mode) { ch4.lfsr &= ~(1 << 6); // Bit 6 löschen ch4.lfsr |= (bit << 6); // Neues Bit setzen } } // 4. Sample generieren: Bit 0 des LFSR // LFSR Bit 0 = 1 → Sample = 0 (Stille) // LFSR Bit 0 = 0 → Sample = volume (Rauschen) int sample = (ch4.lfsr & 1) ? 0 : ch4.volume; // 5. Panning für Kanal 4 if (panning & 0x80) left += sample; // Bit 7: Ch4 → Left if (panning & 0x08) right += sample; // Bit 3: Ch4 → Right } // ═══════════════════════════════════════════════════════ // Master Volume anwenden und Ausgabe-Skalierung // ═══════════════════════════════════════════════════════ // // Jeder Kanal gibt Werte von -15 bis +15 aus (volume = 0-15) // Mit 4 Kanälen: max = 4 × 15 = 60, min = -60 // // Master Volume (NR50): 0-7 → Verstärkung (0-7) + 1 = 1-8 // Skalierung: × 32 für gute Amplitude // // Beispiel: // 4 Kanäle @ max volume (15), master = 7: // ±60 × 8 × 32 = ±15360 (passt in 16-bit: -32768 bis +32767) // left = left * (master_vol_left + 1) * 32; right = right * (master_vol_right + 1) * 32; // Clipping-Schutz: Auf 16-bit Bereich begrenzen // (sollte nie passieren, aber Sicherheit geht vor) if (left > 32767) left = 32767; if (left < -32768) left = -32768; if (right > 32767) right = 32767; if (right < -32768) right = -32768; // Ausgabe: Interleaved Stereo (L, R, L, R, ...) buffer[i * 2] = (int16_t)left; // Linker Kanal buffer[i * 2 + 1] = (int16_t)right; // Rechter Kanal } // Thread-Sicherheit: Mutex freigeben if (apu_mutex) xSemaphoreGive(apu_mutex); } // ============================================ // Peanut-GB Emulator Setup // ============================================ // Peanut-GB ist ein Header-Only GameBoy Emulator // Diese Defines aktivieren Sound und LCD Support #define ENABLE_SOUND 1 // Audio-System aktivieren #define ENABLE_LCD 1 // Display-System aktivieren #include "peanut_gb.h" // Peanut-GB überschreibt LCD_WIDTH/LCD_HEIGHT mit GameBoy-Werten (160x144) // Wir setzen sie zurück auf unsere Display-Größe (320x240) #undef LCD_WIDTH #undef LCD_HEIGHT #define LCD_WIDTH DISPLAY_WIDTH #define LCD_HEIGHT DISPLAY_HEIGHT #define SD_MOUNT_POINT "/sd" // SD-Karte Mount-Pfad #define DEFAULT_ROM "/sd/tetris.gb" // Standard-ROM zum Laden // ============================================ // GameBoy Emulator Variablen // ============================================ static struct gb_s gb; // Peanut-GB Emulator-Instanz static uint8_t *rom_data = NULL; // ROM-Daten im RAM (malloc) static size_t rom_size = 0; // ROM-Größe in Bytes static uint16_t *line_buffer = NULL; // Zeilen-Buffer (nicht genutzt) static uint16_t *frame_buffer = NULL; // Frame-Buffer in PSRAM (nicht genutzt) static int current_line = 0; // Aktuelle Zeile (nicht genutzt) // ============================================ // Double-Buffering für parallele Display/Emulation // ============================================ // Zwei vollständige Framebuffer in PSRAM (je 320×240×2 = 153.6 KB) // - render_buffer: Wird vom Emulator beschrieben (Core 1) // - display_buffer: Wird zum Display gesendet (Core 0) static uint16_t *render_buffer = NULL; // Buffer für Rendering (Emulator) static uint16_t *display_buffer = NULL; // Buffer für Display (Display-Task) // Semaphoren für Buffer-Synchronisation static SemaphoreHandle_t frame_ready_sem = NULL; // Frame fertig gerendert static SemaphoreHandle_t frame_done_sem = NULL; // Display fertig // ============================================ // GameBoy Farbpalette (RGB565) // ============================================ // GameBoy hat 4 Graustufen: Weiß, Hellgrün, Dunkelgrün, Schwarz // RGB565 Format: RRRRR GGGGGG BBBBB (16-bit) static const uint16_t gb_palette[4] = { 0x9FE7, // Farbe 0: Weiß/Hellgrün 0x6BE4, // Farbe 1: Hellgrün 0x3760, // Farbe 2: Dunkelgrün 0x0C20 // Farbe 3: Schwarz/Sehr dunkel }; // ============================================ // Button-Handler Hilfsfunktion // ============================================ /** * @brief Joypad-State an Emulator übergeben (von buttons.c aufgerufen) * @param state 8-Bit Bitmaske mit gedrückten Buttons * * Diese Funktion wird vom Button-Handler in buttons.c aufgerufen, * um den aktuellen Button-State an den Peanut-GB Emulator zu übergeben. */ void gb_set_joypad_state(uint8_t state) { gb.direct.joypad = state; } /** * @brief System für Deep Sleep vorbereiten (von buttons.c aufgerufen) * * Diese Funktion stoppt alle laufenden Tasks sauber: * 1. Setzt system_shutdown Flag * 2. Wartet kurz damit Tasks ihre Schleifen beenden * 3. Stoppt Audio * * MUSS aufgerufen werden BEVOR st7789_sleep() aufgerufen wird! */ void system_prepare_sleep(void) { ESP_LOGI(TAG, "System Shutdown eingeleitet..."); // 1. Shutdown-Flag setzen - alle Tasks prüfen diese Flag system_shutdown = true; // 2. Audio sofort stoppen audio_enabled = false; // 3. Semaphoren freigeben falls Tasks darauf warten // Damit sie ihre while-Schleifen verlassen können if (frame_ready_sem) xSemaphoreGive(frame_ready_sem); if (frame_done_sem) xSemaphoreGive(frame_done_sem); // 4. Warten bis Tasks ihre Schleifen beenden (max 500ms) // Die Tasks prüfen system_shutdown und beenden sich selbst vTaskDelay(pdMS_TO_TICKS(300)); // 5. I2S stoppen für sauberen Audio-Shutdown i2s_stop(I2S_NUM); ESP_LOGI(TAG, "Alle Tasks gestoppt"); } // ============================================ // Peanut-GB Callback-Funktionen // ============================================ // Diese Funktionen werden vom Emulator aufgerufen, um // Hardware-Zugriffe zu simulieren (ROM lesen, RAM lesen/schreiben, Fehler) /** * @brief ROM-Daten lesen * @param gb Emulator-Instanz * @param addr ROM-Adresse (0x0000-0x7FFF für 32KB ROMs) * @return Byte an der angegebenen Adresse */ static uint8_t gb_rom_read(struct gb_s *gb, const uint_fast32_t addr) { // ROM-Daten aus malloc-Buffer zurückgeben return (addr < rom_size) ? rom_data[addr] : 0xFF; } /** * @brief Cartridge RAM lesen (nicht implementiert) * @param gb Emulator-Instanz * @param addr RAM-Adresse * @return 0xFF (kein RAM vorhanden) */ static uint8_t gb_cart_ram_read(struct gb_s *gb, const uint_fast32_t addr) { // Kein Save-RAM implementiert → 0xFF zurückgeben return 0xFF; } /** * @brief Cartridge RAM schreiben (nicht implementiert) * @param gb Emulator-Instanz * @param addr RAM-Adresse * @param val Zu schreibender Wert */ static void gb_cart_ram_write(struct gb_s *gb, const uint_fast32_t addr, const uint8_t val) { // Kein Save-RAM implementiert → Schreibvorgang ignorieren } /** * @brief Fehlerbehandlung * @param gb Emulator-Instanz * @param err Fehler-Code * @param addr Adresse, an der der Fehler auftrat */ static void gb_error(struct gb_s *gb, const enum gb_error_e err, const uint16_t addr) { ESP_LOGE(TAG, "GB Error %d at 0x%04X", err, addr); } /** * @brief GameBoy LCD Zeilen-Callback - Wird für jede gerenderte Zeile aufgerufen * @param gb Emulator-Instanz * @param pixels Pixel-Array (160 Pixel, Werte 0-3) * @param line Zeilen-Nummer (0-143) * * Diese Funktion wird vom Peanut-GB Emulator 144-mal pro Frame aufgerufen. * Sie konvertiert die GameBoy-Pixel (160×144) in unser Display-Format * und skaliert sie auf die gewünschte Größe (z.B. 240×216 bei Scale 1.5). */ static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], const uint_fast8_t line) { // WICHTIG: In render_buffer schreiben (Double-Buffering!) #if GB_PIXEL_PERFECT_SCALING // ═══════════════════════════════════════════════════════ // Pixel-Perfect Scaling mit schwarzen Borders // ═══════════════════════════════════════════════════════ // 1. Vertikale Skalierung: GameBoy-Zeile → Display-Y // Beispiel bei Scale 1.5: 144 Zeilen → 216 Zeilen // line 0 → y=0, line 72 → y=108, line 143 → y=215 int y_base = (line * GB_RENDER_HEIGHT) / 144; if (y_base >= GB_RENDER_HEIGHT) return; // 2. Horizontale Skalierung: 160 GameBoy-Pixel → GB_RENDER_WIDTH Pixel // Dynamic Pixel-Width Algorithmus: Jedes Pixel bekommt exakte Breite int x_dst = 0; // Aktuelle Output-Position for (int x = 0; x < 160; x++) { // a) Farbe aus Palette holen (0-3 → RGB565) uint16_t c = gb_palette[pixels[x] & 0x03]; // b) RGB565 → BGR565 Byte-Swap für ST7789 Display // ST7789 erwartet BGR-Reihenfolge! uint16_t swapped = (c >> 8) | (c << 8); // c) Pixel-Breite berechnen: Wie viele Output-Pixel für dieses Input-Pixel? // Bei Scale 1.5: 160 → 240, also ~1.5 Pixel pro Input int next_x_dst = ((x + 1) * GB_RENDER_WIDTH) / 160; int pixel_width = next_x_dst - x_dst; // d) Pixel-Breite mal kopieren (keine Lücken!) for (int w = 0; w < pixel_width && x_dst + w < GB_RENDER_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. Vertikale Duplikation: Zeile ggf. verdoppeln // Bei Scale 1.5: Manche Zeilen werden dupliziert int ny = ((line + 1) * GB_RENDER_HEIGHT) / 144; if (ny > y_base + 1 && ny < GB_RENDER_HEIGHT) { 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 Bytes pro Pixel } #else // ═══════════════════════════════════════════════════════ // Full-Screen Stretch Modus (160×144 → 320×240) // ═══════════════════════════════════════════════════════ // Vertikale Skalierung: 144 → 240 (× 1.67) int y = (line * 5) / 3; // Schnelle Multiplikation if (y >= GB_SCREEN_HEIGHT) return; // Horizontale Verdopplung: 160 → 320 (× 2) for (int x = 0; x < 160; x++) { uint16_t c = gb_palette[pixels[x] & 0x03]; uint16_t swapped = (c >> 8) | (c << 8); // RGB→BGR int dst = y * GB_SCREEN_WIDTH + x * 2; render_buffer[dst] = swapped; // Pixel 1 render_buffer[dst + 1] = swapped; // Pixel 2 (dupliziert) } // Zeilen-Duplikation falls nötig int ny = ((line + 1) * 5) / 3; if (ny > y + 1 && ny < GB_SCREEN_HEIGHT) { memcpy(&render_buffer[(y + 1) * GB_SCREEN_WIDTH], &render_buffer[y * GB_SCREEN_WIDTH], GB_SCREEN_WIDTH * 2); // 320 Pixel × 2 Bytes } #endif } /** * @brief SD-Karte initialisieren * @return ESP_OK bei Erfolg, Fehlercode sonst * * Mountet die SD-Karte im SPI-Modus und stellt sie als FAT-Dateisystem * unter /sd zur Verfügung. */ static esp_err_t init_sdcard(void) { ESP_LOGI(TAG, "Init SD..."); // FAT-Dateisystem Mount-Konfiguration esp_vfs_fat_sdmmc_mount_config_t cfg = { .format_if_mount_failed = false, // NICHT formatieren bei Fehler! .max_files = 5, // Max. 5 gleichzeitig offene Dateien .allocation_unit_size = 16 * 1024 // 16 KB Cluster-Größe }; sdmmc_card_t *card; // WICHTIG: SPI-Bus wird mit Display geteilt! // Der Bus wurde bereits von st7789_init() initialisiert. // Wir dürfen ihn NICHT nochmal initialisieren! // SD-Karte im SPI-Modus (Shared Bus mit Display) sdmmc_host_t host = SDSPI_HOST_DEFAULT(); host.max_freq_khz = 20000; // 20 MHz für SD-Karte (Display: 80 MHz) host.slot = SD_SPI_HOST; // SPI Slot-Konfiguration (Shared Bus Flag!) sdspi_device_config_t slot = SDSPI_DEVICE_CONFIG_DEFAULT(); slot.gpio_cs = SD_PIN_CS; // CS-Pin aus hardware_config.h (GPIO 41) slot.host_id = host.slot; // KRITISCH: Bus wurde bereits initialisiert, nicht nochmal! // Das SPICOMMON_BUSFLAG_MASTER Flag in st7789.c hat den Bus bereits erstellt. // Wir nutzen nur esp_vfs_fat_sdspi_mount mit NO_BUS_INIT Flag (wenn verfügbar) // Workaround: Manuelles Mount ohne Bus-Reinitialisierung // Verwende sdmmc_card_init statt esp_vfs_fat_sdspi_mount // SD-Karte mounten (teilt sich SPI-Bus mit Display) esp_err_t ret = esp_vfs_fat_sdspi_mount(SD_MOUNT_POINT, &host, &slot, &cfg, &card); if (ret == ESP_OK) { ESP_LOGI(TAG, "✓ SD OK!"); } else { ESP_LOGE(TAG, "SD Init failed (0x%x): %s", ret, esp_err_to_name(ret)); ESP_LOGE(TAG, "Mögliche Ursachen:"); ESP_LOGE(TAG, " - SD-Karte nicht eingelegt"); ESP_LOGE(TAG, " - SD-Karte defekt"); ESP_LOGE(TAG, " - SPI-Bus Konflikt (teilt sich mit Display)"); } return ret; } /** * @brief GameBoy ROM von SD-Karte laden * @param path Dateipfad auf der SD-Karte (z.B. "/sd/tetris.gb") * @return true bei Erfolg, false bei Fehler * * Liest die gesamte ROM-Datei in den RAM (malloc). * Die ROM-Größe wird in rom_size gespeichert. */ static bool load_rom(const char *path) { // Datei öffnen (binary read mode) FILE *f = fopen(path, "rb"); if (!f) return false; // Dateigröße ermitteln fseek(f, 0, SEEK_END); rom_size = ftell(f); fseek(f, 0, SEEK_SET); // Speicher allokieren rom_data = malloc(rom_size); if (!rom_data) { fclose(f); return false; } // ROM-Daten einlesen fread(rom_data, 1, rom_size, f); fclose(f); ESP_LOGI(TAG, "✓ ROM: %d bytes", rom_size); return true; } /** * @brief I2S Audio-System initialisieren * @return ESP_OK bei Erfolg, Fehlercode sonst * * Konfiguriert den I2S-Bus für Audio-Ausgabe: * - 32768 Hz Sample-Rate (GameBoy-nativ) * - 16-bit Stereo * - DMA-Buffer für flüssige Wiedergabe */ static esp_err_t init_audio(void) { ESP_LOGI(TAG, "Init Audio..."); // Mutex für Thread-sichere APU-Register-Zugriffe apu_mutex = xSemaphoreCreateMutex(); // I2S Konfiguration i2s_config_t cfg = { .mode = I2S_MODE_MASTER | I2S_MODE_TX, // Master, nur TX (kein RX) .sample_rate = SAMPLE_RATE, // 32768 Hz .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // 16-bit Samples .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // Stereo (L+R) .communication_format = I2S_COMM_FORMAT_STAND_I2S, // Standard I2S .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt-Priorität .dma_buf_count = 8, // 8 DMA-Buffer .dma_buf_len = SAMPLES_PER_BUFFER, // 512 Samples pro Buffer .use_apll = false, // Kein APLL (genauer PLL) .tx_desc_auto_clear = true, // Buffer auto-clear }; // I2S Pin-Konfiguration (aus hardware_config.h) i2s_pin_config_t pins = { .bck_io_num = I2S_PIN_BCLK, // Bit Clock .ws_io_num = I2S_PIN_LRC, // Word Select (L/R Clock) .data_out_num = I2S_PIN_DIN, // Data Out .data_in_num = I2S_PIN_NO_CHANGE // Kein Data In }; // I2S Treiber installieren esp_err_t ret = i2s_driver_install(I2S_NUM, &cfg, 0, NULL); if (ret != ESP_OK) return ret; // Pins konfigurieren ret = i2s_set_pin(I2S_NUM, &pins); if (ret != ESP_OK) return ret; // DMA-Buffer mit Stille füllen i2s_zero_dma_buffer(I2S_NUM); // Audio-Buffer allokieren (DMA-fähiger Speicher!) // 512 Samples × 2 Kanäle (Stereo) × 2 Bytes (16-bit) = 2048 Bytes audio_buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA); if (!audio_buffer) return ESP_ERR_NO_MEM; audio_enabled = true; ESP_LOGI(TAG, "✓ Audio OK! BCLK=%d LRC=%d DIN=%d", I2S_PIN_BCLK, I2S_PIN_LRC, I2S_PIN_DIN); return ESP_OK; } /** * @brief Audio-Task (FreeRTOS Task) * @param arg Nicht verwendet * * Dieser Task läuft auf Core 1 und generiert kontinuierlich Audio-Samples * für alle 4 GameBoy-Kanäle. Die Samples werden via I2S-DMA zum MAX98357A * Verstärker gesendet. * * Buffer-Größe: 512 Samples → 512/32768 = 15.6 ms Latenz */ static void audio_task(void *arg) { ESP_LOGI(TAG, "🎵 Audio task started"); // DMA-Buffer allokieren (muss DMA-fähig sein!) int16_t *buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 4, MALLOC_CAP_DMA); while (audio_enabled) { // 1. Audio-Samples generieren (alle 4 Kanäle mischen) generate_samples(buffer, SAMPLES_PER_BUFFER); // 2. Samples via I2S-DMA senden // Buffer-Größe: 512 Samples × 2 Kanäle × 2 Bytes = 2048 Bytes size_t written; i2s_write(I2S_NUM, buffer, SAMPLES_PER_BUFFER * 4, &written, portMAX_DELAY); } // Cleanup free(buffer); vTaskDelete(NULL); } /** * @brief Display-Task (FreeRTOS Task auf Core 0) * @param arg Nicht verwendet * * Dieser Task ist für die Display-Ausgabe zuständig: * - Wartet auf fertiges Frame (frame_ready_sem) * - Sendet Frame via SPI zum ST7789 Display * - Signalisiert Fertigstellung (frame_done_sem) * * Läuft parallel zur Emulation auf Core 0! * Optimierung: Compact Buffer spart 23% SPI-Bandbreite bei Pixel-Perfect Mode */ static void display_task(void *arg) { #if GB_PIXEL_PERFECT_SCALING // Bildschirm einmalig schwarz füllen (für statische Borders) st7789_fill_screen(0x0000); // Compact Buffer: Nur GameBoy-Region ohne schwarze Ränder // Bei Scale 1.5: 240×216 = 103.7 KB (statt 153.6 KB für 320×240) uint16_t *compact_buffer = heap_caps_malloc(GB_RENDER_WIDTH * GB_RENDER_HEIGHT * 2, MALLOC_CAP_DMA); #endif while (!system_shutdown) { // 1. Auf fertiges Frame warten (von Emulation-Task signalisiert) // Mit Timeout damit wir system_shutdown prüfen können if (xSemaphoreTake(frame_ready_sem, pdMS_TO_TICKS(100)) != pdTRUE) { continue; // Timeout - prüfe shutdown Flag } // Nochmal prüfen nach dem Warten if (system_shutdown) break; #if GB_PIXEL_PERFECT_SCALING // 2a. GameBoy-Region in Compact Buffer kopieren (Borders entfernen) // Nur die 240×216 Pixel GameBoy-Content, ohne schwarze Ränder for (int y = 0; y < GB_RENDER_HEIGHT; y++) { memcpy(&compact_buffer[y * GB_RENDER_WIDTH], &display_buffer[(y + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], GB_RENDER_WIDTH * 2); // 2 Bytes pro Pixel } // 2b. Nur GameBoy-Region übertragen (33% weniger Daten!) // 240×216 statt 320×240 → weniger SPI-Traffic = höhere FPS st7789_draw_buffer_preswapped(compact_buffer, GB_OFFSET_X, GB_OFFSET_Y, GB_RENDER_WIDTH, GB_RENDER_HEIGHT); #else // 2. Full-Screen Modus: Gesamten Buffer übertragen st7789_draw_buffer_preswapped(display_buffer, 0, 0, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT); #endif // 3. Display fertig → Emulation darf weiter machen xSemaphoreGive(frame_done_sem); } ESP_LOGI(TAG, "Display-Task beendet"); vTaskDelete(NULL); } /** * @brief Emulation-Task (FreeRTOS Task auf Core 1) * @param arg Nicht verwendet * * Hauptschleife des GameBoy Emulators: * - Führt Emulation für 1 Frame aus (gb_run_frame) * - Swap der Framebuffer (render ↔ display) * - Synchronisation mit Display-Task * - Frame-Timing (17ms pro Frame ≈ 59 FPS) * * Läuft parallel zum Display auf Core 1! */ static void emulation_task(void *arg) { int frame = 0; TickType_t last = xTaskGetTickCount(); int16_t *frame_audio = heap_caps_malloc(SAMPLES_PER_FRAME * 4, MALLOC_CAP_DMA); while (!system_shutdown) { TickType_t frame_start = xTaskGetTickCount(); // ═══════════════════════════════════════════════════════ // 1. Emulation für 1 Frame durchführen // ═══════════════════════════════════════════════════════ // Peanut-GB rendert in render_buffer via gb_lcd_draw_line Callback gb_run_frame(&gb); // Shutdown-Check nach Emulation if (system_shutdown) break; // ═══════════════════════════════════════════════════════ // 2. Buffer-Swap (Double-Buffering) // ═══════════════════════════════════════════════════════ // render_buffer → display_buffer (neues Frame zur Anzeige) // display_buffer → render_buffer (für nächstes Frame) uint16_t *temp = render_buffer; render_buffer = display_buffer; display_buffer = temp; // ═══════════════════════════════════════════════════════ // 3. Display-Task signalisieren: Frame fertig! // ═══════════════════════════════════════════════════════ xSemaphoreGive(frame_ready_sem); // ═══════════════════════════════════════════════════════ // 4. Warten bis Display fertig (Doppel-Buffering!) // ═══════════════════════════════════════════════════════ // Mit Timeout damit wir shutdown prüfen können xSemaphoreTake(frame_done_sem, pdMS_TO_TICKS(100)); // Shutdown-Check if (system_shutdown) break; // ═══════════════════════════════════════════════════════ // 5. Performance-Logging (jede Sekunde) // ═══════════════════════════════════════════════════════ frame++; TickType_t frame_end = xTaskGetTickCount(); int frame_time_ms = (frame_end - frame_start) * portTICK_PERIOD_MS; if (frame % 60 == 0) { // Alle 60 Frames = ~1 Sekunde ESP_LOGI(TAG, "Frame %d | time=%dms (%.1f FPS) | writes=%d | sound=%s | ch1=%d ch2=%d ch3=%d ch4=%d", frame, frame_time_ms, 1000.0f / frame_time_ms, audio_write_count, master_enable ? "ON" : "OFF", ch1.active, ch2.active, ch3.active, ch4.active); } // ═══════════════════════════════════════════════════════ // 6. Frame-Timing (GameBoy läuft mit ~60 FPS) // ═══════════════════════════════════════════════════════ // GameBoy Original: 59.7275 FPS = 16.7424 ms pro Frame // Wir verwenden 17ms ≈ 58.8 FPS (nahe genug) vTaskDelayUntil(&last, pdMS_TO_TICKS(17)); } ESP_LOGI(TAG, "Emulation-Task beendet"); vTaskDelete(NULL); } /** * @brief Haupt-Einstiegspunkt des Programms * * Diese Funktion wird beim ESP32-Start automatisch aufgerufen. * Sie initialisiert alle Hardware-Komponenten und startet die Tasks: * * Initialisierung: * 1. NVS (Non-Volatile Storage) * 2. ST7789 Display (80 MHz SPI) * 3. SD-Karte (FAT32 Dateisystem) * 4. PSRAM Check (8 MB für Framebuffer) * 5. Framebuffer allokieren (2× 153.6 KB in PSRAM) * 6. ROM von SD-Karte laden * 7. Peanut-GB Emulator initialisieren * 8. I2S Audio initialisieren (32768 Hz) * * Tasks: * - display_task auf Core 0 (Display-Ausgabe) * - emulation_task auf Core 1 (GameBoy Emulation) * - audio_task auf Core 1 (Audio-Synthese) */ void app_main(void) { // ═══════════════════════════════════════════════════════ // 0. SOFORT prüfen: Sind wir aus Deep Sleep aufgewacht // und ist der Power-Schalter noch OFF? // ═══════════════════════════════════════════════════════ #if POWER_BUTTON_ENABLED // Power-Switch Pin konfigurieren (minimal, nur für Check) gpio_config_t pwr_cfg = { .pin_bit_mask = (1ULL << POWER_SWITCH_PIN), .mode = GPIO_MODE_INPUT, .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&pwr_cfg); // Prüfen ob Power-Schalter noch auf OFF steht if (gpio_get_level(POWER_SWITCH_PIN) == POWER_SWITCH_OFF) { // Schalter ist OFF → SOFORT wieder schlafen! // Kein Display-Init, kein nichts - direkt zurück in Deep Sleep ESP_LOGI(TAG, "Power-Schalter noch OFF - bleibe im Deep Sleep"); // Wakeup erneut konfigurieren (für nächstes Aufwachen) esp_sleep_enable_ext0_wakeup(POWER_SWITCH_PIN, 0); // Wake on LOW // Sofort wieder schlafen esp_deep_sleep_start(); // Diese Zeile wird nie erreicht } ESP_LOGI(TAG, "Power-Schalter ist ON - starte normal"); #endif // ═══════════════════════════════════════════════════════ // Startup-Banner // ═══════════════════════════════════════════════════════ ESP_LOGI(TAG, ""); ESP_LOGI(TAG, "╔═══════════════════════════════════════╗"); ESP_LOGI(TAG, "║ ESP32-S3 GameBoy - FIXED AUDIO! ║"); ESP_LOGI(TAG, "╚═══════════════════════════════════════╝"); // ═══════════════════════════════════════════════════════ // 1. NVS Flash initialisieren // ═══════════════════════════════════════════════════════ // NVS = Non-Volatile Storage für WiFi/BT Kalibrierung, etc. nvs_flash_init(); // ═══════════════════════════════════════════════════════ // 2. ST7789 Display initialisieren // ═══════════════════════════════════════════════════════ st7789_init(); // SPI initialisieren, Display-Reset // Backlight bleibt erstmal AUS (st7789_init setzt es bereits auf 0) // Display wird erst eingeschaltet wenn Emulation bereit ist // ═══════════════════════════════════════════════════════ // 3. PSRAM Check und Info // ═══════════════════════════════════════════════════════ // PSRAM = SPI RAM, 8 MB Octal Mode @ 80 MHz // Wird für große Framebuffer benötigt (2× 153.6 KB) size_t psram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "PSRAM: %d KB total, %d KB free", psram_total / 1024, psram_free / 1024); // ═══════════════════════════════════════════════════════ // 4. Framebuffer allokieren (Double-Buffering!) // ═══════════════════════════════════════════════════════ // Wir brauchen ZWEI komplette Framebuffer: // - render_buffer: Emulator schreibt hier rein // - display_buffer: Display liest hier aus // // Größe: 320 × 240 × 2 Bytes (RGB565) = 153.6 KB pro Buffer size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2; ESP_LOGI(TAG, "Buffer size: %d KB (%dx%d)", buffer_size / 1024, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT); // Versuche PSRAM-Allokation (bevorzugt, da PSRAM viel Platz hat) size_t min_psram_needed = buffer_size * 2 + 50000; // 2 Buffer + Reserve if (psram_free > min_psram_needed) { render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); ESP_LOGI(TAG, "Double buffers allocated in PSRAM"); } // Fallback: Internes RAM (nur wenn PSRAM fehlschlägt) if (!render_buffer || !display_buffer) { ESP_LOGW(TAG, "PSRAM alloc failed, trying regular RAM..."); if (render_buffer) free(render_buffer); if (display_buffer) free(display_buffer); render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT); display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_8BIT); ESP_LOGI(TAG, "Double buffers allocated in internal RAM"); } // Kein Speicher? → Abbruch! if (!render_buffer || !display_buffer) { ESP_LOGE(TAG, "No memory for double framebuffers!"); while(1) vTaskDelay(1000); // Endlos-Schleife (Fehler-Zustand) } // Buffer mit Schwarz füllen (für Letterbox-Borders bei Pixel-Perfect Mode) memset(render_buffer, 0, buffer_size); memset(display_buffer, 0, buffer_size); // ═══════════════════════════════════════════════════════ // 5. Semaphoren für Buffer-Synchronisation erstellen // ═══════════════════════════════════════════════════════ // frame_ready_sem: Emulation signalisiert "Frame fertig!" // frame_done_sem: Display signalisiert "Display fertig!" frame_ready_sem = xSemaphoreCreateBinary(); frame_done_sem = xSemaphoreCreateBinary(); xSemaphoreGive(frame_done_sem); // Initial: Display ist "fertig" // ═══════════════════════════════════════════════════════ // 6. SD-Karte initialisieren // ═══════════════════════════════════════════════════════ if (init_sdcard() != ESP_OK) { // FEHLER: SD-Karte nicht lesbar → Rot anzeigen st7789_fill_screen(0xF800); // Rot = SD-Karten Fehler st7789_set_backlight(80); // Backlight an für Fehler-Anzeige ESP_LOGE(TAG, "SD-Karte nicht lesbar! Bitte SD-Karte prüfen."); while(1) vTaskDelay(1000); // Endlos-Schleife } // Kein visuelles Feedback bei Erfolg - Display bleibt dunkel // ═══════════════════════════════════════════════════════ // 7. GameBoy ROM von SD-Karte laden // ═══════════════════════════════════════════════════════ if (!load_rom(DEFAULT_ROM)) { // FEHLER: ROM nicht gefunden → Orange anzeigen (unterscheidbar von SD-Fehler) st7789_fill_screen(0xFD20); // Orange = ROM-Ladefehler st7789_set_backlight(80); // Backlight an für Fehler-Anzeige ESP_LOGE(TAG, "ROM '%s' nicht gefunden!", DEFAULT_ROM); while(1) vTaskDelay(1000); } // ═══════════════════════════════════════════════════════ // 8. Peanut-GB Emulator initialisieren // ═══════════════════════════════════════════════════════ // Callback-Funktionen registrieren: // - gb_rom_read: ROM-Bytes lesen // - gb_cart_ram_read/write: Cartridge RAM (nicht implementiert) // - gb_error: Fehlerbehandlung if (gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, &gb_error, NULL) != GB_INIT_NO_ERROR) { // FEHLER: Emulator-Init fehlgeschlagen → Magenta anzeigen st7789_fill_screen(0xF81F); // Magenta = Emulator-Init-Fehler st7789_set_backlight(80); // Backlight an für Fehler-Anzeige ESP_LOGE(TAG, "Emulator konnte nicht initialisiert werden!"); while(1) vTaskDelay(1000); } // LCD-Callback registrieren (wird 144-mal pro Frame aufgerufen) gb_init_lcd(&gb, &gb_lcd_draw_line); // ═══════════════════════════════════════════════════════ // 9. Audio-System initialisieren und Audio-Task starten // ═══════════════════════════════════════════════════════ if (init_audio() == ESP_OK) { audio_enabled = true; // Audio-Task auf Core 1 starten (zusammen mit Emulation für Cache-Lokalität) // Stack: 4096 Bytes, Priorität: 5, Core: 1 xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, NULL, 1); } // ═══════════════════════════════════════════════════════ // 10. Button-System initialisieren und starten // ═══════════════════════════════════════════════════════ // 8 GameBoy-Buttons (D-Pad, A, B, Select, Start) + 1 Power-Button if (buttons_init() == ESP_OK) { buttons_start(); // Button-Polling-Task auf Core 1 starten ESP_LOGI(TAG, "Button-System aktiv (9 Buttons: 8 GameBoy + 1 Power)"); } else { ESP_LOGW(TAG, "Button-System konnte nicht initialisiert werden!"); } // ═══════════════════════════════════════════════════════ // 11. Erfolgs-Banner und Display einschalten // ═══════════════════════════════════════════════════════ ESP_LOGI(TAG, ""); ESP_LOGI(TAG, "═══════════════════════════════════════"); ESP_LOGI(TAG, "Emulation bereit - starte..."); ESP_LOGI(TAG, "═══════════════════════════════════════"); // Display mit schwarzem Hintergrund vorbereiten st7789_fill_screen(0x0000); // JETZT erst Backlight einschalten - kein Blinken mehr! st7789_set_backlight(80); // ═══════════════════════════════════════════════════════ // 12. FreeRTOS Tasks starten (Dual-Core!) // ═══════════════════════════════════════════════════════ // Display-Task auf Core 0 (parallel zur Emulation!) // Stack: 4096 Bytes, Priorität: 5, Core: 0 xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 5, NULL, 0); // Emulation-Task auf Core 1 (mit Audio für bessere Cache-Nutzung) // Stack: 8192 Bytes (größer, da Emulation komplexer), Priorität: 5, Core: 1 xTaskCreatePinnedToCore(emulation_task, "emulation", 8192, NULL, 5, NULL, 1); // ═══════════════════════════════════════════════════════ // 13. app_main am Leben halten (FreeRTOS Requirement) // ═══════════════════════════════════════════════════════ // app_main() darf nicht beenden, sonst crashed das System! // Endlos-Schleife mit 1 Sekunde Delay while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }