1470 lines
65 KiB
C
1470 lines
65 KiB
C
/**
|
||
* @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));
|
||
}
|
||
}
|