lego-esp32s3-gameboy/main/main.c

1470 lines
65 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#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));
}
}