diff --git a/MAIN_C_ERKLAERUNG.md b/MAIN_C_ERKLAERUNG.md new file mode 100644 index 0000000..90eebe9 --- /dev/null +++ b/MAIN_C_ERKLAERUNG.md @@ -0,0 +1,833 @@ +# main.c - Ausfรผhrliche Code-Erklรคrung + +Dieses Dokument erklรคrt den kompletten Aufbau von `main/main.c` auf Deutsch. + +--- + +## ๐Ÿ“‹ Datei-รœbersicht + +**Datei:** `main/main.c` (786 Zeilen) +**Funktion:** Hauptprogramm des GameBoy-Emulators +**Aufgabe:** Emulation, Display-Rendering, Audio-Ausgabe koordinieren + +--- + +## ๐Ÿ—๏ธ Code-Struktur + +``` +main.c +โ”œโ”€โ”€ Includes & Definitionen (Zeilen 1-43) +โ”œโ”€โ”€ APU (Audio) Variablen (Zeilen 44-100) +โ”œโ”€โ”€ APU Register-Funktionen (Zeilen 101-336) +โ”œโ”€โ”€ Audio-Ausgabe Task (Zeilen 337-415) +โ”œโ”€โ”€ GameBoy Palette & Callbacks (Zeilen 416-530) +โ”œโ”€โ”€ ROM Lade-Funktionen (Zeilen 531-609) +โ”œโ”€โ”€ Display Task (Zeilen 610-653) +โ”œโ”€โ”€ SD-Card Mount (Zeilen 654-701) +โ””โ”€โ”€ Main Loop (app_main) (Zeilen 702-786) +``` + +--- + +## ๐Ÿ“ฆ Teil 1: Includes & Konstanten (Zeilen 1-43) + +### Include-Dateien + +```c +#include "freertos/FreeRTOS.h" // FreeRTOS Kernel +#include "freertos/task.h" // Task-Verwaltung +#include "freertos/semphr.h" // Semaphore fรผr Synchronisation +#include "esp_system.h" // ESP32 System-Funktionen +#include "esp_log.h" // Logging (ESP_LOGI, ESP_LOGE) +#include "esp_heap_caps.h" // PSRAM Allocation +#include "esp_vfs_fat.h" // FAT Filesystem +#include "driver/i2s.h" // I2S Audio-Treiber +``` + +### Audio-Konstanten + +```c +#define SAMPLE_RATE 32768 // GameBoy Audio-Rate (32768 Hz) +#define SAMPLES_PER_FRAME 546 // 32768 Hz / 60 FPS = 546 Samples +#define SAMPLES_PER_BUFFER 512 // I2S Buffer-GrรถรŸe + +#define GB_CPU_FREQ 4194304.0f // GameBoy CPU: 4.194304 MHz +#define CYCLES_PER_SAMPLE 128 // CPU-Takte pro Audio-Sample +``` + +**Erklรคrung:** +- GameBoy lรคuft mit 60 FPS (59.73 FPS genau) +- Pro Frame werden 546 Audio-Samples erzeugt (32768 / 60) +- GameBoy CPU lรคuft mit ~4.19 MHz +- Alle 128 CPU-Takte wird 1 Audio-Sample erzeugt + +--- + +## ๐Ÿ“ฆ Teil 2: APU (Audio Processing Unit) Variablen (Zeilen 44-100) + +### APU Register + +```c +static uint8_t apu_regs[48] = {0}; // 48 Audio-Register (0xFF10 - 0xFF3F) +static uint8_t wave_ram[16] = {0}; // 16 Bytes Wave-Pattern (Kanal 3) +``` + +**GameBoy Audio-Register:** +- `0xFF10-0xFF14`: Kanal 1 (Square Wave mit Sweep) +- `0xFF15-0xFF19`: Kanal 2 (Square Wave) +- `0xFF1A-0xFF1E`: Kanal 3 (Wave Pattern) +- `0xFF1F-0xFF23`: Kanal 4 (Noise) +- `0xFF24-0xFF26`: Master Control + +### Kanal-Status Strukturen + +```c +// Kanal 1: Square Wave mit Frequency Sweep +static struct { + bool active; // Kanal lรคuft + bool dac_on; // Digital-Analog-Wandler an + uint8_t duty; // Tastgrad (12.5%, 25%, 50%, 75%) + uint8_t volume; // Lautstรคrke (0-15) + uint16_t freq_raw; // Frequenz-Wert (0-2047) + float phase; // Aktuelle Phase (0.0 - 1.0) +} ch1; + +// Kanal 2: Square Wave (wie Kanal 1, ohne Sweep) +static struct { + bool active; + bool dac_on; + uint8_t duty; + uint8_t volume; + uint16_t freq_raw; + float phase; +} ch2; + +// Kanal 3: Wave Pattern (Custom Wellenform) +static struct { + bool active; + bool dac_on; + uint8_t volume_shift; // Lautstรคrke-Shift (0, 1, 2 bits) + uint16_t freq_raw; + float phase; +} ch3; + +// Kanal 4: Noise (Zufallsrauschen) +static struct { + bool active; + uint8_t volume; + uint16_t lfsr; // Linear Feedback Shift Register (Pseudo-Random) + uint8_t divisor; // Frequenz-Teiler + uint8_t shift; // Shift-Anzahl + bool width_mode; // 7-bit oder 15-bit LFSR + float timer; // Timer fรผr nรคchstes Sample +} ch4 = {.lfsr = 0x7FFF}; // LFSR Initial-Wert +``` + +**Erklรคrung der Duty-Werte (Tastgrad):** +``` +Duty 0 (12.5%): โ”€โ”_______ (kurzer Puls) +Duty 1 (25%): โ”€โ”โ”______ (1/4 hoch) +Duty 2 (50%): โ”€โ”โ”โ”โ”____ (Rechteck) +Duty 3 (75%): โ”€โ”โ”โ”โ”โ”โ”__ (3/4 hoch) +``` + +### Audio-System Variablen + +```c +static bool audio_enabled = false; // Audio-System lรคuft +static int16_t *audio_buffer = NULL; // Audio Ring-Buffer +static SemaphoreHandle_t apu_mutex = NULL; // Mutex fรผr Thread-Sicherheit +``` + +--- + +## ๐Ÿ“ฆ Teil 3: APU Register Read/Write (Zeilen 101-336) + +### apu_mem_read() - Register auslesen + +```c +uint8_t apu_mem_read(struct gb_s *gb, uint16_t addr) +``` + +**Funktion:** Liest GameBoy Audio-Register aus (0xFF10 - 0xFF3F) + +**Spezialfรคlle:** +- `0xFF26` (NR52): Master-Enable Status +- `0xFF30-0xFF3F`: Wave RAM (16 Bytes) +- Andere Register: Direkt aus `apu_regs[]` Array + +### apu_mem_write() - Register schreiben + +```c +void apu_mem_write(struct gb_s *gb, uint16_t addr, uint8_t val) +``` + +**Funktion:** Schreibt in GameBoy Audio-Register und aktualisiert Kanal-Status + +**Wichtige Register:** + +**Kanal 1 (Square mit Sweep):** +```c +0xFF10 (NR10): Sweep-Einstellungen +0xFF11 (NR11): Duty + Length + duty = (val >> 6) & 0x03; // Bits 6-7: Tastgrad (0-3) + +0xFF12 (NR12): Volume Envelope + volume = (val >> 4) & 0x0F; // Bits 4-7: Start-Lautstรคrke + dac_on = ((val & 0xF8) != 0); // DAC an wenn Bits 3-7 != 0 + +0xFF13 (NR13): Frequency Low + freq_raw = (freq_raw & 0x700) | val; // Untere 8 Bits + +0xFF14 (NR14): Frequency High + Trigger + freq_raw = (freq_raw & 0xFF) | ((val & 0x07) << 8); // Obere 3 Bits + if (val & 0x80) { // Bit 7: Trigger + active = true; // Kanal starten + phase = 0.0f; // Phase zurรผcksetzen + } +``` + +**Kanal 3 (Wave):** +```c +0xFF1A (NR30): DAC Enable + dac_on = (val & 0x80) != 0; // Bit 7: DAC an/aus + +0xFF1C (NR32): Volume + volume_shift = (val >> 5) & 0x03; // Bits 5-6: Shift (0-3) + // 0 = Stumm, 1 = 100%, 2 = 50%, 3 = 25% + +0xFF30-0xFF3F: Wave RAM + wave_ram[addr - 0xFF30] = val; // 16 Bytes Custom-Wellenform +``` + +**Kanal 4 (Noise):** +```c +0xFF21 (NR42): Volume + volume = (val >> 4) & 0x0F; // Bits 4-7: Lautstรคrke + +0xFF22 (NR43): Polynomial Counter + shift = (val >> 4) & 0x0F; // Bits 4-7: Shift + width_mode = (val & 0x08) != 0; // Bit 3: 7-bit (1) oder 15-bit (0) + divisor = val & 0x07; // Bits 0-2: Divisor + +0xFF23 (NR44): Trigger + if (val & 0x80) { + lfsr = 0x7FFF; // LFSR zurรผcksetzen + active = true; + } +``` + +**Master Control:** +```c +0xFF24 (NR50): Master Volume + master_vol_left = (val >> 4) & 0x07; // Bits 4-6: Links (0-7) + master_vol_right = val & 0x07; // Bits 0-2: Rechts (0-7) + +0xFF25 (NR51): Panning + // Bits 0-7: Welcher Kanal auf welchem Lautsprecher + // Bit 0: Ch1 rechts, Bit 4: Ch1 links, etc. + +0xFF26 (NR52): Master Enable + master_enable = (val & 0x80) != 0; // Bit 7: Audio an/aus + if (!master_enable) { + // Alle Kanรคle stoppen + ch1.active = false; + ch2.active = false; + ch3.active = false; + ch4.active = false; + } +``` + +--- + +## ๐Ÿ“ฆ Teil 4: Audio Output Task (Zeilen 337-415) + +### audio_task() - I2S Audio-Ausgabe + +```c +static void audio_task(void *arg) +``` + +**Funktion:** Lรคuft dauerhaft auf Core 1, schreibt Audio-Daten zum I2S + +**Ablauf:** +1. Warte auf volle Buffer (512 Samples) +2. Schreibe zu I2S (MAX98357A Verstรคrker) +3. Wiederhole + +**Code-Erklรคrung:** +```c +while (1) { + // Warte bis Buffer voll ist (512 Samples = 1024 bytes) + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Schreibe zu I2S + size_t bytes_written = 0; + i2s_write(I2S_NUM, audio_buffer, SAMPLES_PER_BUFFER * 2, + &bytes_written, portMAX_DELAY); +} +``` + +--- + +## ๐Ÿ“ฆ Teil 5: GameBoy Palette & Callbacks (Zeilen 416-530) + +### GameBoy Farbpalette + +```c +static const uint16_t gb_palette[4] = { + 0xFFFF, // WeiรŸ (Hintergrund) - RGB565: 11111 111111 11111 + 0xAD55, // Hellgrรผn - RGB565: 10101 101010 10101 + 0x52AA, // Mittelgrรผn - RGB565: 01010 010101 01010 + 0x0000 // Schwarz (Vordergrund) - RGB565: 00000 000000 00000 +}; +``` + +**Erklรคrung RGB565 Format:** +``` +16-bit RGB565: RRRRR GGGGGG BBBBB + โ†‘ โ†‘ โ†‘ + 5 bit 6 bit 5 bit + Rot Grรผn Blau +``` + +**GameBoy Graustufen:** +- GameBoy hat nur 4 Graustufen (2-bit Farbtiefe) +- Wert 0 = WeiรŸ (Hintergrund) +- Wert 1 = Hellgrรผn +- Wert 2 = Mittelgrรผn +- Wert 3 = Schwarz (Sprites/Text) + +### audio_callback() - Audio-Sample Callback + +```c +void audio_callback(struct gb_s *gb, uint16_t left, uint16_t right) +``` + +**Funktion:** Wird von Peanut-GB fรผr jedes Audio-Sample aufgerufen (32768x pro Sekunde) + +**Ablauf:** +```c +// 1. Konvertiere unsigned (0-65535) zu signed (-32768 bis +32767) +int16_t sample_l = (int16_t)(left - 32768); +int16_t sample_r = (int16_t)(right - 32768); + +// 2. Schreibe in Audio-Buffer (Ring-Buffer) +static int audio_write_pos = 0; +audio_buffer[audio_write_pos++] = sample_l; // Links +audio_buffer[audio_write_pos++] = sample_r; // Rechts + +// 3. Wenn Buffer voll โ†’ I2S schreiben +if (audio_write_pos >= SAMPLES_PER_BUFFER * 2) { + audio_write_pos = 0; + xTaskNotifyGive(audio_task_handle); // Audio-Task aufwecken +} + +// 4. APU-Register auslesen fรผr Status-Anzeige +ch1.active = (apu_regs[0x16] & 0x01); // Kanal 1 lรคuft +ch2.active = (apu_regs[0x16] & 0x02); // Kanal 2 lรคuft +ch3.active = (apu_regs[0x16] & 0x04); // Kanal 3 lรคuft +ch4.active = (apu_regs[0x16] & 0x08); // Kanal 4 lรคuft +``` + +**Warum -32768?** +- GameBoy Audio: 0-65535 (unsigned) +- I2S erwartet: -32768 bis +32767 (signed) +- Umrechnung: signed = unsigned - 32768 + +### gb_lcd_draw_line() - Display-Zeilen Callback + +```c +static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], + const uint_fast8_t line) +``` + +**Funktion:** Wird 144ร— pro Frame aufgerufen (eine GameBoy-Zeile pro Aufruf) + +**Parameter:** +- `pixels[160]`: 160 GameBoy-Pixel fรผr diese Zeile (Werte 0-3) +- `line`: Zeilen-Nummer (0-143) + +**Scaling-Algorithmus (bei GB_PIXEL_PERFECT_SCALING = 1):** + +```c +// 1. Vertikales Scaling: GameBoy-Zeile โ†’ Display-Y-Position +int y_base = (line * GB_RENDER_HEIGHT) / 144; +// Beispiel bei Scale 1.6: Zeile 10 โ†’ Y = 16 (10 * 230 / 144 = 16) + +// 2. Horizontales Scaling: Pixel fรผr Pixel +int x_dst = 0; +for (int x = 0; x < 160; x++) { + // Farbwert aus Palette holen + uint16_t c = gb_palette[pixels[x] & 0x03]; + + // RGBโ†’BGR Byte-Swap fรผr ST7789 + uint16_t swapped = (c >> 8) | (c << 8); + + // Berechne Pixel-Breite (verhindert Lรผcken!) + int next_x_dst = ((x + 1) * GB_RENDER_WIDTH) / 160; + int pixel_width = next_x_dst - x_dst; + + // Fรผlle pixel_width Output-Pixel mit dieser Farbe + for (int w = 0; w < pixel_width; w++) { + int dst = (y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + + (x_dst + w + GB_OFFSET_X); + render_buffer[dst] = swapped; + } + + x_dst = next_x_dst; +} + +// 3. Zeilen-Duplikation (fรผr gleichmรครŸiges vertikales Scaling) +int ny = ((line + 1) * GB_RENDER_HEIGHT) / 144; +if (ny > y_base + 1) { + // Zeile duplizieren (memcpy) + memcpy(&render_buffer[(y_base + 1 + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], + &render_buffer[(y_base + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], + GB_RENDER_WIDTH * 2); // *2 weil uint16_t = 2 bytes +} +``` + +**Warum Byte-Swap?** +- Peanut-GB liefert RGB565: `RRRRR GGGGGG BBBBB` +- ST7789 erwartet BGR565: `BBBBB GGGGGG RRRRR` +- Byte-Swap: `(c >> 8) | (c << 8)` tauscht High-/Low-Byte + +**Scaling-Beispiel bei 1.6ร—:** +``` +GameBoy: 160 Pixel Breite +Display: 256 Pixel Breite (160 * 1.6 = 256) + +Pixel 0 โ†’ X 0-1 (2 Pixel breit) +Pixel 1 โ†’ X 1-3 (2 Pixel breit) +Pixel 2 โ†’ X 3-4 (1 Pixel breit) โ† Abwechselnd! +Pixel 3 โ†’ X 4-6 (2 Pixel breit) +... + +Pattern: 2, 2, 1, 2, 2, 1, ... (8:5 Verhรคltnis) +``` + +--- + +## ๐Ÿ“ฆ Teil 6: ROM Lade-Funktionen (Zeilen 531-609) + +### gb_rom_read() - ROM-Bytes lesen + +```c +uint8_t gb_rom_read(struct gb_s *gb, uint32_t addr) +``` + +**Funktion:** Liest 1 Byte aus ROM an Adresse `addr` + +**Code:** +```c +return rom_data[addr]; // Einfacher Array-Zugriff +``` + +### gb_cart_ram_read() - Cartridge RAM lesen + +```c +uint8_t gb_cart_ram_read(struct gb_s *gb, uint32_t addr) +``` + +**Funktion:** Liest Save-Game RAM (fรผr Pokemon, Zelda, etc.) + +**Status:** Aktuell nicht implementiert (return 0xFF) + +### gb_cart_ram_write() - Cartridge RAM schreiben + +```c +void gb_cart_ram_write(struct gb_s *gb, uint32_t addr, uint8_t val) +``` + +**Funktion:** Schreibt Save-Game RAM + +**Status:** Aktuell nicht implementiert (TODO) + +### load_rom_from_sd() - ROM von SD-Card laden + +```c +static esp_err_t load_rom_from_sd(const char *path) +``` + +**Funktion:** Lรคdt GameBoy ROM-Datei von SD-Card + +**Ablauf:** +```c +// 1. Datei รถffnen +FILE *f = fopen(path, "rb"); + +// 2. GrรถรŸe ermitteln +fseek(f, 0, SEEK_END); +long size = ftell(f); +fseek(f, 0, SEEK_SET); + +// 3. Speicher allokieren (PSRAM bevorzugt) +rom_data = heap_caps_malloc(size, MALLOC_CAP_SPIRAM); +if (!rom_data) { + rom_data = malloc(size); // Fallback: Normal-RAM +} + +// 4. ROM lesen +fread(rom_data, 1, size, f); + +// 5. Datei schlieรŸen +fclose(f); + +// 6. ROM-Info auslesen (Bytes 0x134-0x143: Titel) +char title[17] = {0}; +memcpy(title, &rom_data[0x134], 16); +ESP_LOGI(TAG, "ROM: %s (%ld bytes)", title, size); +``` + +--- + +## ๐Ÿ“ฆ Teil 7: Display Task (Zeilen 610-653) + +### display_task() - Display Rendering auf Core 0 + +```c +static void display_task(void *arg) +``` + +**Funktion:** Lรคuft parallel zur Emulation, rendert Frames zum Display + +**Optimierung - Compact Buffer:** +```c +// 1. Einmalig schwarze Rรคnder fรผllen (nur bei Scaling) +#if GB_PIXEL_PERFECT_SCALING + st7789_fill_screen(0x0000); // Schwarz + + // Compact Buffer allokieren (nur GameBoy-Region, ohne Rรคnder) + uint16_t *compact_buffer = heap_caps_malloc( + GB_RENDER_WIDTH * GB_RENDER_HEIGHT * 2, + MALLOC_CAP_DMA // DMA-fรคhiger Speicher fรผr SPI + ); +#endif + +// 2. Frame-Rendering Loop +while (1) { + // Warte auf fertigen Frame + xSemaphoreTake(frame_ready_sem, portMAX_DELAY); + + #if GB_PIXEL_PERFECT_SCALING + // Kopiere nur GameBoy-Region (ohne schwarze Rรคnder) + for (int y = 0; y < GB_RENDER_HEIGHT; y++) { + memcpy( + &compact_buffer[y * GB_RENDER_WIDTH], // Ziel + &display_buffer[(y + GB_OFFSET_Y) * GB_SCREEN_WIDTH + GB_OFFSET_X], // Quelle + GB_RENDER_WIDTH * 2 // Anzahl Bytes + ); + } + + // SPI-Transfer (33% weniger Daten!) + st7789_draw_buffer_preswapped( + compact_buffer, + GB_OFFSET_X, GB_OFFSET_Y, + GB_RENDER_WIDTH, GB_RENDER_HEIGHT + ); + #else + // Fullscreen: Ganzen Buffer senden + st7789_draw_buffer_preswapped( + display_buffer, + 0, 0, + GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT + ); + #endif + + // Signalisiere Emulation: Fertig, nรคchstes Frame! + xSemaphoreGive(frame_done_sem); +} +``` + +**Warum Compact Buffer?** +``` +Ohne Compact Buffer: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Schwarz (40 Pixel) โ”‚ โ† Wird mit รผbertragen +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ GameBoy (256ร—230) โ”‚ โ† Nur das wird gebraucht! +โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Schwarz (24 Pixel) โ”‚ โ† Wird mit รผbertragen +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +Total: 320ร—240 = 76.800 Pixel + +Mit Compact Buffer: +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ GameBoy (256ร—230) โ”‚ โ† Nur das wird รผbertragen! +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +Total: 256ร—230 = 58.880 Pixel (23% weniger!) + +Ergebnis: 3-4ms schneller pro Frame! +``` + +--- + +## ๐Ÿ“ฆ Teil 8: SD-Card Mount (Zeilen 654-701) + +### mount_sd_card() - SD-Karte mounten + +```c +static esp_err_t mount_sd_card(void) +``` + +**Funktion:** Initialisiert SD-Card รผber SPI + +**Code-Erklรคrung:** +```c +// 1. SPI-Bus Konfiguration +sdmmc_host_t host = SDSPI_HOST_DEFAULT(); // SPI-Modus +sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); +slot_config.gpio_cs = SD_PIN_CS; // GPIO 41 +slot_config.host_id = SD_SPI_HOST; // Shared mit Display + +// 2. Mount-Konfiguration +esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, // NICHT formatieren! + .max_files = 5, + .allocation_unit_size = 16 * 1024 +}; + +// 3. Mounten +sdmmc_card_t *card; +esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card); + +// 4. Card-Info anzeigen +ESP_LOGI(TAG, "SD Card: %s, %llu MB", + card->cid.name, + ((uint64_t) card->csd.capacity) * card->csd.sector_size / (1024 * 1024)); +``` + +--- + +## ๐Ÿ“ฆ Teil 9: Main Loop (app_main) (Zeilen 702-786) + +### app_main() - Hauptprogramm + +```c +void app_main(void) +``` + +**Kompletter Programmablauf:** + +```c +// ===== 1. DISPLAY INITIALISIEREN ===== +ESP_LOGI(TAG, "Init Display..."); +st7789_init(); // ST7789 mit 80 MHz SPI +st7789_set_backlight(80); // 80% Helligkeit + +// ===== 2. SD-KARTE MOUNTEN ===== +ESP_LOGI(TAG, "Init SD..."); +mount_sd_card(); // FAT32 Filesystem + +// ===== 3. ROM LADEN ===== +ESP_LOGI(TAG, "Load ROM..."); +load_rom_from_sd("/sdcard/tetris.gb"); // Fest verdrahtet + +// ===== 4. PSRAM PRรœFEN & BUFFER ALLOKIEREN ===== +size_t psram_size = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); +ESP_LOGI(TAG, "PSRAM: %d KB total, %d KB free", + psram_size / 1024, + heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024); + +// Double-Buffering: 2ร— 320ร—240 = 150 KB pro Buffer +size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2; + +render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); +display_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); + +// Mit Schwarz fรผllen +memset(render_buffer, 0, buffer_size); +memset(display_buffer, 0, buffer_size); + +// ===== 5. AUDIO INITIALISIEREN ===== +ESP_LOGI(TAG, "Init Audio..."); + +// I2S Konfiguration +i2s_config_t i2s_config = { + .mode = I2S_MODE_MASTER | I2S_MODE_TX, // Master, nur Senden + .sample_rate = SAMPLE_RATE, // 32768 Hz + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .dma_buf_count = I2S_DMA_BUF_COUNT, // 8 Buffer + .dma_buf_len = I2S_DMA_BUF_LEN, // 1024 Samples + .use_apll = false, + .tx_desc_auto_clear = true +}; + +i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL); + +// Pin-Konfiguration +i2s_pin_config_t pin_config = { + .bck_io_num = I2S_PIN_BCLK, // GPIO 48 + .ws_io_num = I2S_PIN_LRC, // GPIO 47 + .data_out_num = I2S_PIN_DIN, // GPIO 16 + .data_in_num = -1 // Kein Input +}; +i2s_set_pin(I2S_NUM, &pin_config); + +// Audio Buffer allokieren +audio_buffer = heap_caps_malloc(SAMPLES_PER_BUFFER * 2 * sizeof(int16_t), + MALLOC_CAP_DMA); + +// Audio Task starten (Core 1, Priority 5) +xTaskCreatePinnedToCore(audio_task, "audio", 4096, NULL, 5, + &audio_task_handle, 1); +audio_enabled = true; + +// ===== 6. EMULATOR INITIALISIEREN ===== +struct gb_s gb; +gb_init(&gb, &gb_rom_read, &gb_cart_ram_read, &gb_cart_ram_write, + &audio_callback, NULL); + +gb_init_lcd(&gb, &gb_lcd_draw_line); + +// Palette setzen +for (int i = 0; i < 4; i++) { + gb.display.palette[i] = gb_palette[i]; +} + +// APU Callbacks registrieren +gb.apu.apu_mem_read = apu_mem_read; +gb.apu.apu_mem_write = apu_mem_write; + +// ===== 7. DISPLAY TASK STARTEN ===== +frame_ready_sem = xSemaphoreCreateBinary(); // Semaphore erstellen +frame_done_sem = xSemaphoreCreateBinary(); +xSemaphoreGive(frame_done_sem); // Initial freigeben + +xTaskCreatePinnedToCore(display_task, "display", 4096, NULL, 10, + NULL, 0); // Core 0, Priority 10 + +// ===== 8. EMULATION LOOP ===== +ESP_LOGI(TAG, "โœ“ %s with FIXED AUDIO! ๐ŸŽฎ๐Ÿ”Š", rom_title); + +uint32_t frame_count = 0; +int64_t last_time = esp_timer_get_time(); + +while (1) { + // Warte auf Display-Task fertig + xSemaphoreTake(frame_done_sem, portMAX_DELAY); + + // Emuliere 1 Frame (70224 CPU-Takte, ~16.7ms) + gb_run_frame(&gb); + + // Buffer Swap (Render โ†” Display) + uint16_t *temp = render_buffer; + render_buffer = display_buffer; + display_buffer = temp; + + // Display-Task aufwecken + xSemaphoreGive(frame_ready_sem); + + frame_count++; + + // Alle 60 Frames: FPS ausgeben + if (frame_count % 60 == 0) { + int64_t now = esp_timer_get_time(); + int32_t time_ms = (now - last_time) / 1000 / 60; + float fps = 1000.0f / time_ms; + + ESP_LOGI(TAG, "Frame %ld | time=%ldms (%.1f FPS) | sound=%s | " + "ch1=%d ch2=%d ch3=%d ch4=%d", + frame_count, time_ms, fps, + master_enable ? "ON" : "OFF", + ch1.active, ch2.active, ch3.active, ch4.active); + + last_time = now; + } +} +``` + +**Zusammenfassung:** +1. Display init (80 MHz SPI, 80% Helligkeit) +2. SD-Card mount (FAT32) +3. ROM laden (tetris.gb) +4. PSRAM check & Buffer alloc (2ร— 150 KB) +5. Audio init (I2S 32768 Hz, Task auf Core 1) +6. Emulator init (Peanut-GB mit Callbacks) +7. Display Task start (Core 0, parallel rendering) +8. Emulation Loop (60 FPS, Double-Buffering, FPS-Logging) + +--- + +## ๐ŸŽฏ Performance-Tricks im Code + +### 1. Double-Buffering mit Semaphoren + +```c +// Emulation wartet auf Display fertig +xSemaphoreTake(frame_done_sem, portMAX_DELAY); + +// ... emuliert ... + +// Buffer tauschen (Pointer-Swap, keine Kopie!) +uint16_t *temp = render_buffer; +render_buffer = display_buffer; +display_buffer = temp; + +// Display aufwecken +xSemaphoreGive(frame_ready_sem); +``` + +**Vorteil:** Emulation + Display laufen parallel = 50% schneller! + +### 2. Byte-Swapping im Emulator + +```c +// RGBโ†’BGR beim Rendern, nicht beim Transfer +uint16_t swapped = (c >> 8) | (c << 8); +render_buffer[dst] = swapped; +``` + +**Vorteil:** Kein Byte-Swap beim SPI-Transfer nรถtig! + +### 3. Compact Buffer + +```c +// Nur GameBoy-Region kopieren, nicht ganze 320ร—240 +for (int y = 0; y < GB_RENDER_HEIGHT; y++) { + memcpy(&compact_buffer[y * GB_RENDER_WIDTH], + &display_buffer[...], GB_RENDER_WIDTH * 2); +} +``` + +**Vorteil:** 23% weniger SPI-Daten = 3-4ms schneller! + +### 4. PSRAM fรผr groรŸe Buffer + +```c +// GroรŸe Buffer in PSRAM (nicht in limited SRAM) +render_buffer = heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM); +``` + +**Vorteil:** 8MB verfรผgbar statt nur 512KB SRAM! + +--- + +## ๐Ÿ“Š Timing-รœbersicht + +``` +GameBoy Frame (16.7ms @ 59.73 FPS): +โ”œโ”€ CPU Emulation: ~6ms (70224 Takte) +โ”œโ”€ PPU Rendering: ~4ms (144 Zeilen) +โ”œโ”€ APU Audio: ~2ms (546 Samples) +โ””โ”€ Buffer Swap: <1ms (Pointer-Tausch) + +Display Rendering (parallel auf Core 0): +โ”œโ”€ Compact Buffer: ~2ms (Zeilen kopieren) +โ”œโ”€ SPI Transfer: ~10ms (80 MHz, 117 KB) +โ””โ”€ Semaphore: <1ms + +Total Zeit: ~16ms (beide Cores parallel!) +Ergebnis: 60-90 FPS je nach Spiel +``` + +--- + +**Ende der main.c Erklรคrung** + +Dieses Dokument erklรคrt alle wichtigen Bereiche von main.c auf Deutsch mit Code-Beispielen und Erklรคrungen. Fรผr weitere Fragen zu spezifischen Funktionen, siehe die Kommentare direkt im Code! diff --git a/README.md b/README.md index 2a6131e..88fbcf1 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,397 @@ -# ๐ŸŽฎ ESP32-S3 GNUBoy - LEGO GameBoy Emulator +# ๐ŸŽฎ ESP32-S3 GameBoy Emulator -**GameBoy/GameBoy Color Emulator fรผr Waveshare ESP32-S3-Touch-LCD-2** +**Hochoptimierter GameBoy/GameBoy Color Emulator fรผr Waveshare ESP32-S3-Touch-LCD-2** --- ## ๐Ÿ“‹ Projekt-รœbersicht -Dieses Projekt ist ein kompletter GameBoy Emulator fรผr das **Waveshare ESP32-S3-Touch-LCD-2** Board. +Dieses Projekt ist ein vollstรคndig funktionsfรคhiger GameBoy Emulator, der auf dem **Waveshare ESP32-S3-Touch-LCD-2** Board lรคuft. Es nutzt den **Peanut-GB** Emulator-Core und erreicht **90-111 FPS** bei vielen Spielen - schneller als der Original GameBoy (59.73 FPS)! -### โœจ Features +### โœจ Fertige Features -- โœ… **GameBoy & GameBoy Color Emulation** (GNUBoy Core) -- โœ… **ST7789 Display** (2.0", 240x320, optimiert) -- โœ… **NFC ROM-Auswahl** (PN532, einfach Tag scannen!) -- โœ… **Potentiometer Controls** (Volume & Brightness) -- โœ… **Link Cable 2-Player** (Tetris, Pokemon, etc.) -- โœ… **SD Card ROM Loading** (alle deine ROMs) -- โœ… **I2S Audio** (MAX98357A, klarer Sound) -- โœ… **8 GameBoy Buttons** (Original Layout) +- โœ… **GameBoy Emulation** (Peanut-GB Core) +- โœ… **ST7789 Display** (2.0", 320x240, 80 MHz SPI) +- โœ… **Perfekter 4-Kanal Audio** (I2S MAX98357A, 32768 Hz) +- โœ… **SD Card ROM Loading** (FAT32, .gb Dateien) +- โœ… **PSRAM Optimization** (8MB Octal Mode, Double-Buffering) +- โœ… **Dynamisches Display-Scaling** (1.0x bis 1.67x konfigurierbar) +- โœ… **90-111 FPS Performance** (besser als Original!) + +### ๐Ÿšง Geplante Features + +- โณ **8 GameBoy Buttons** (GPIO 8-14, 21 - Hardware fertig, Software TODO) +- โณ **NFC ROM-Auswahl** (PN532 I2C - Hardware fertig, Software TODO) +- โณ **Potentiometer Controls** (Volume & Brightness - Hardware fertig, Software TODO) +- โณ **Link Cable 2-Player** (GPIO 2, 15, 17 - Hardware fertig, Software TODO) + +--- + +## ๐Ÿ—๏ธ Programmablauf & Architektur + +### Systemarchitektur + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ESP32-S3 Dual Core โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ CORE 1 โ”‚ CORE 0 โ”‚ +โ”‚ (Emulation Task) โ”‚ (Display Task) โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ GameBoy Emulator โ”‚ โ”‚ โ”‚ Display Rendering โ”‚ โ”‚ +โ”‚ โ”‚ (Peanut-GB) โ”‚ โ”‚ โ”‚ (ST7789 SPI) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ - CPU Emulation โ”‚ โ”‚ โ”‚ - Byte Swapping โ”‚ โ”‚ +โ”‚ โ”‚ - PPU Rendering โ”‚ โ”‚ โ”‚ - SPI Transfer โ”‚ โ”‚ +โ”‚ โ”‚ - APU Audio โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”ค - Compact Buffer โ”‚ โ”‚ +โ”‚ โ”‚ - Memory Map โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ–ฒ โ”‚ +โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Render Buffer โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ–บโ”‚ Display Buffer โ”‚ โ”‚ +โ”‚ โ”‚ (PSRAM 150KB) โ”‚ Swap โ”‚ โ”‚ (PSRAM 150KB) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ Audio Task โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ (I2S Output) โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ + โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ I2S Audio โ”‚ โ”‚ SPI Display โ”‚ +โ”‚ (MAX98357A) โ”‚ โ”‚ (ST7789) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Programmablauf beim Start + +``` +1. app_main() startet + โ”‚ + โ”œโ”€โ–บ ST7789 Display initialisieren (80 MHz SPI) + โ”‚ โ””โ”€โ–บ Backlight auf 80% setzen + โ”‚ + โ”œโ”€โ–บ SD-Karte mounten (FAT32) + โ”‚ โ””โ”€โ–บ ROM laden: /tetris.gb + โ”‚ + โ”œโ”€โ–บ PSRAM prรผfen (8MB Octal Mode) + โ”‚ โ”œโ”€โ–บ Render Buffer allokieren (150 KB) + โ”‚ โ””โ”€โ–บ Display Buffer allokieren (150 KB) + โ”‚ + โ”œโ”€โ–บ I2S Audio initialisieren (32768 Hz, 16-bit) + โ”‚ โ””โ”€โ–บ Audio Task starten (Core 1, Priority 5) + โ”‚ + โ”œโ”€โ–บ Peanut-GB Emulator initialisieren + โ”‚ โ”œโ”€โ–บ ROM laden + โ”‚ โ”œโ”€โ–บ Callbacks registrieren: + โ”‚ โ”‚ โ”œโ”€โ–บ gb_lcd_draw_line() - Zeile rendern + โ”‚ โ”‚ โ””โ”€โ–บ audio_callback() - Audio-Sample + โ”‚ โ””โ”€โ–บ GameBoy-Palette setzen (DMG Grรผn) + โ”‚ + โ”œโ”€โ–บ Display Task erstellen (Core 0, Priority 10) + โ”‚ โ””โ”€โ–บ Wartet auf frame_ready_sem + โ”‚ + โ””โ”€โ–บ Emulation Loop starten (Main Loop) + โ”‚ + โ””โ”€โ–บ Fรผr immer: + โ”œโ”€โ–บ gb_run_frame() - Emuliert 1 Frame + โ”‚ โ”œโ”€โ–บ CPU lรคuft (70224 Takte) + โ”‚ โ”œโ”€โ–บ PPU rendert Zeilen โ†’ gb_lcd_draw_line() + โ”‚ โ””โ”€โ–บ APU erzeugt Audio โ†’ audio_callback() + โ”‚ + โ”œโ”€โ–บ Buffer Swap (Render โ†” Display) + โ”‚ โ””โ”€โ–บ frame_ready_sem freigeben + โ”‚ + โ”œโ”€โ–บ Auf frame_done_sem warten + โ”‚ โ””โ”€โ–บ Display Task hat gerendert + โ”‚ + โ””โ”€โ–บ FPS berechnen & ausgeben (alle 60 Frames) +``` + +### Double-Buffering Synchronisation + +``` +EMULATION TASK (Core 1) DISPLAY TASK (Core 0) +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +โ”Œโ”€ Frame N rendern โ”Œโ”€ Wartet auf Semaphore +โ”‚ in Render Buffer โ”‚ (frame_ready_sem) +โ”‚ โ”‚ +โ”‚ gb_run_frame() โ”‚ +โ”‚ โ””โ”€โ–บ Zeilen in โ”‚ +โ”‚ render_buffer โ”‚ +โ”‚ schreiben โ”‚ +โ”‚ โ”‚ +โ””โ”€ Frame fertig โ”‚ + โ”‚ + Buffer Swap: โ”‚ + render_buffer โ†” โ”‚ + display_buffer โ”‚ + โ”‚ + frame_ready_sem โ”‚ + freigeben โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ โ””โ”€ Semaphore empfangen! + + Warte auf โ”Œโ”€ Display buffer kopieren + frame_done_sem โ”‚ zu compact_buffer + โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ (nur GameBoy-Region) + โ”‚ + โ”œโ”€ SPI Transfer + โ”‚ st7789_draw_buffer_ + โ”‚ preswapped() + โ”‚ + โ””โ”€ Fertig! + + frame_done_sem + freigeben + + frame_done_sem empfangen โ—„โ”€โ”€โ”€โ”€ + +โ”Œโ”€ Nรคchstes Frame โ”Œโ”€ Wartet wieder... +โ”‚ rendern โ”‚ +``` + +--- + +## ๐Ÿงฉ Component-Beschreibung + +### 1. Peanut-GB Component + +**Pfad:** `components/peanut-gb/` + +**Funktion:** Kern des GameBoy-Emulators. Header-only Bibliothek fรผr GameBoy CPU/PPU/APU Emulation. + +**Wichtige Funktionen:** +- `gb_init()` - Initialisiert den Emulator +- `gb_run_frame()` - Emuliert 1 GameBoy Frame (70224 CPU-Takte = 16.7ms) +- `gb_lcd_draw_line()` - Callback fรผr jede gerenderte Bildschirmzeile + +**Optimierung:** Kompiliert mit `-O3` Flag fรผr maximale Performance + +### 2. ST7789 Display Driver Component + +**Pfad:** `components/st7789/` + +**Funktion:** Treiber fรผr ST7789 TFT-Display (320ร—240 Pixel, RGB565 Farbformat) + +**Wichtige Funktionen:** +- `st7789_init()` - Display initialisieren (80 MHz SPI) +- `st7789_draw_buffer_preswapped()` - Optimierter Transfer (vorher RGBโ†’BGR gewandelt) +- `st7789_set_backlight()` - Hintergrundbeleuchtung steuern (0-100%) + +**Besonderheit:** +- Chunked SPI Transfer (max 32KB pro Transfer wegen DMA-Limit) +- Byte-Swapping wird im Emulator gemacht (RGBโ†’BGR), nicht im Treiber + +### 3. Link Cable Component + +**Pfad:** `components/link_cable/` + +**Status:** Hardware fertig, Software TODO + +**Funktion:** 2-Player Multiplayer รผber GPIO (serieller Transfer) + +**Geplante Nutzung:** +- Pokemon-Tausch +- Tetris 2-Player +- Andere Multiplayer-Games + +### 4. NFC Manager Component + +**Pfad:** `components/nfc_manager/` + +**Status:** Hardware fertig (PN532 auf I2C), Software TODO + +**Funktion:** ROM-Auswahl per NFC-Tag scannen + +**Geplante Funktionsweise:** +1. NFC-Tag scannen +2. UID auslesen +3. In `nfc_roms.json` nachschlagen +4. Entsprechende ROM laden + +### 5. Potentiometer Manager Component + +**Pfad:** `components/potentiometer_manager/` + +**Status:** Hardware fertig (ADC GPIO 3, 4), Software TODO + +**Funktion:** Analoge Steuerung fรผr Volume und Brightness + +**Geplante Nutzung:** +- Poti 1 (GPIO 3): Lautstรคrke (0-100%) +- Poti 2 (GPIO 4): Helligkeit (10-100%) + +### 6. Minizip Component + +**Pfad:** `components/minizip/` + +**Funktion:** ZIP-Dekompression fรผr .zip ROM-Dateien + +**Status:** Eingebunden, aber aktuell nicht genutzt (ROMs sind .gb, nicht gezippt) + +### 7. Zlib Component + +**Pfad:** `components/zlib/` + +**Funktion:** Compression-Bibliothek (fรผr Minizip und Save-States) + +**Status:** Eingebunden als Dependency fรผr Minizip + +--- + +## ๐Ÿ”ง Wichtige Code-Bereiche in main.c + +### 1. GameBoy Palette + +```c +// Zeilen 417-422: DMG Grรผn-Palette +static const uint16_t gb_palette[4] = { + 0xFFFF, // WeiรŸ (Hintergrund) + 0xAD55, // Hellgrรผn + 0x52AA, // Mittelgrรผn + 0x0000 // Schwarz (Vordergrund) +}; +``` + +**Erklรคrung:** GameBoy hat 4 Graustufen (2-bit), hier als RGB565-Werte definiert. + +### 2. Audio Callback + +```c +// Zeilen 424-461: Audio-Sample Callback +void audio_callback(struct gb_s *gb, uint16_t left, uint16_t right) +``` + +**Funktion:** Wird von Peanut-GB fรผr jedes Audio-Sample aufgerufen (32768 Hz). + +**Ablauf:** +1. Samples in Ring-Buffer schreiben +2. Bei Buffer voll: I2S schreiben +3. APU-Register auslesen fรผr Kanal-Status + +### 3. Display Zeilen-Rendering + +```c +// Zeilen 463-530: LCD Zeilen-Callback +static void gb_lcd_draw_line(...) +``` + +**Funktion:** Wird 144ร— pro Frame aufgerufen (eine Zeile pro Aufruf) + +**Scaling-Algorithmus:** +1. Vertikales Scaling: `y_base = (line * GB_RENDER_HEIGHT) / 144` +2. Horizontales Scaling: Jedes Pixel wird dynamisch auf 1-2 Output-Pixel gemapped +3. Byte-Swapping: RGB565 โ†’ BGR565 (fรผr ST7789) +4. Pixel-Width-Berechnung verhindert Lรผcken im Bild + +**Beispiel bei Scale 1.6:** +- GameBoy Zeile 0 โ†’ Display Y = 0 +- GameBoy Zeile 10 โ†’ Display Y = 16 (10 * 1.6 = 16) +- Einige Zeilen werden dupliziert fรผr gleichmรครŸige Skalierung + +### 4. Display Task + +```c +// Zeilen 610-653: Display Rendering Task +static void display_task(void *arg) +``` + +**Funktion:** Lรคuft auf Core 0, rendert fertigen Frame zum Display + +**Optimierung - Compact Buffer:** +1. Nur GameBoy-Region kopieren (nicht schwarze Rรคnder) +2. Von 320ร—240 Buffer โ†’ 256ร—230 kompakter Buffer +3. 33% weniger SPI-Transfer! +4. Geschwindigkeit steigt von 20ms auf 16ms pro Frame + +### 5. Main Loop + +```c +// Zeilen 702-786: Hauptschleife +void app_main(void) +``` + +**Ablauf:** +1. Display init +2. SD-Card mount +3. PSRAM check & Buffer alloc +4. Audio init & Task start +5. Emulator init +6. Display Task start +7. Unendliche Emulations-Loop + +--- + +## ๐ŸŽฏ Performance-Optimierungen + +### 1. PSRAM Double-Buffering + +**Problem:** SPI-Transfer (10ms) blockiert Emulation +**Lรถsung:** 2 Buffer in PSRAM, parallel arbeiten + +**Ergebnis:** +- Core 0: Display rendering (10ms) +- Core 1: Emulation (10ms) +- Parallel = 50% schneller! + +### 2. Display Scaling + +**Problem:** Fullscreen (320ร—240) zu langsam (20ms) +**Lรถsung:** Kleinere Auflรถsung mit schwarzen Rรคndern + +**Ergebnis:** +- Scale 1.0: 160ร—144 = 83 FPS (sehr klein) +- Scale 1.5: 240ร—216 = 90-100 FPS (gut) +- Scale 1.6: 256ร—230 = 60-90 FPS (beste Balance) +- Scale 1.67: 267ร—240 = 55-70 FPS (volle Hรถhe) + +### 3. Compact Buffer + +**Problem:** 320ร—240 Buffer mit vielen schwarzen Pixeln +**Lรถsung:** Nur GameBoy-Region transferieren + +**Ergebnis:** +- Vorher: 153.600 Bytes SPI-Transfer +- Nachher: 117.760 Bytes (23% weniger!) +- Speedup: 3-4ms pro Frame + +### 4. Compiler Optimierung + +**Problem:** Emulator zu langsam (45ms pro Frame) +**Lรถsung:** `-O3` Compiler-Flag fรผr Peanut-GB + +**Ergebnis:** +- -O2: 22-25ms +- -O3: 16-19ms +- **40% schneller!** + +### 5. SPI Clock Speed + +**Problem:** Display-Transfer Bottleneck +**Lรถsung:** 80 MHz SPI (Maximum fรผr ST7789) + +**Ergebnis:** +- 40 MHz: 28ms pro Frame +- 80 MHz: 16ms pro Frame +- **Fast doppelt so schnell!** + +--- + +## ๐Ÿ“Š Performance-Tabelle + +| Game | Scale | FPS | Frame Time | Audio | +|---------------|-------|----------|------------|-------| +| Tetris | 1.5 | 60-70 | 14-16ms | โœ… | +| DuckTales | 1.5 | 90-111 | 9-11ms | โœ… | +| Pokemon | 1.6 | 55-65 | 15-18ms | โœ… | +| Super Mario | 1.6 | 60-75 | 13-16ms | โœ… | +| Original GB | - | **59.73**| **16.7ms** | โœ… | + +**Fazit:** Bei vielen Spielen schneller als Original GameBoy! --- @@ -26,138 +400,28 @@ Dieses Projekt ist ein kompletter GameBoy Emulator fรผr das **Waveshare ESP32-S3 ### Voraussetzungen - **ESP-IDF v4.4** installiert -- **Python 3.10** (NICHT 3.12!) +- **Python 3.10** (pyenv empfohlen) - **Git** -- **pyenv** (fรผr Python-Version Management) -### โš™๏ธ Installation & Build - -#### 1๏ธโƒฃ Original GNUBoy Repository klonen +### Build ```bash -# Erstelle Arbeitsverzeichnis -mkdir -p ~/Arduino/gameboy -cd ~/Arduino/gameboy - - -#### 2๏ธโƒฃ Unser angepasstes Projekt darรผber kopieren - -```bash -# Clone unser LEGO GameBoy Projekt -cd ~/Arduino/gameboy -git clone https://git.hacker-net.de/Hacker-Software/lego-esp32s3-gameboy.git - - -#### 3๏ธโƒฃ ESP-IDF v4.4 Setup - -```bash -# Abhรคngigkeiten installieren -sudo apt install python3-gevent python3-virtualenv git wget flex bison gperf python3 python3-pip python3-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 curl -sudo apt-get install -y \ - build-essential \ - libssl-dev \ - zlib1g-dev \ - libbz2-dev \ - libreadline-dev \ - libsqlite3-dev \ - wget \ - curl \ - llvm \ - libncurses5-dev \ - libncursesw5-dev \ - xz-utils \ - tk-dev \ - libffi-dev \ - liblzma-dev \ - python3-openssl \ - git - - -# pyenv installieren (falls noch nicht vorhanden) -curl https://pyenv.run | bash - -# Zu ~/.bashrc hinzufรผgen: -echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc -echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc -echo 'eval "$(pyenv init -)"' >> ~/.bashrc -source ~/.bashrc - -# Python 3.10 installieren und aktivieren -pyenv install 3.10.13 -pyenv local 3.10.13 - -# ESP-IDF v4.4 installieren -git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf -cd ~/esp-idf -./install.sh esp32s3 -source ./export.sh -``` -#### 5๏ธโƒฃ Projekt konfigurieren & bauen - -```bash -cd ~/Arduino/gameboy/gnuboy - # ESP-IDF Environment laden source ~/esp-idf/export.sh -# Target setzen -idf.py set-target esp32s3 - -# (Optional) Konfiguration anpassen, wird schon durch script fix_psram.sh gemacht -idf.py menuconfig -# โ†’ Component config โ†’ ESP32S3-Specific โ†’ PSRAM auf "Octal Mode" setzen - # Projekt bauen +cd /home/duffy/Arduino/gameboy/gnuboy idf.py build ``` -#### 6๏ธโƒฃ Flashen +### Flash ```bash -# USB-Port finden (meist /dev/ttyUSB0 oder /dev/ttyACM0) -ls /dev/ttyUSB* /dev/ttyACM* - -# Flashen (ggf. BOOT-Button beim Verbinden gedrรผckt halten) +# Flashen idf.py -p /dev/ttyUSB0 flash -# Mit Monitor (zum Debuggen) +# Mit Monitor idf.py -p /dev/ttyUSB0 flash monitor - -# Monitor beenden: Ctrl + ] -#Einfach nur monitor -#idf.py -p /dev/ttyUSB0 monitor -``` - -### ๐Ÿ› Troubleshooting - -#### Python 3.12 Fehler? -```bash -# Python 3.10 installieren: -siehe anfang 3๏ธโƒฃ ESP-IDF v4.4 Setup -``` - -#### Build Fehler? -```bash -# Clean & rebuild: -idf.py fullclean -idf.py build -``` - -#### PSRAM nicht erkannt? -```bash -# Octal PSRAM aktivieren: -idf.py menuconfig -# โ†’ Component config โ†’ ESP32S3-Specific -# โ†’ [*] Support for external, SPI-connected RAM -# โ†’ SPI RAM config โ†’ Mode: Octal Mode PSRAM -``` - -#### Board kommt nicht in Flash-Modus? -``` -1. BOOT-Button gedrรผckt halten -2. RESET-Button kurz drรผcken -3. BOOT-Button loslassen -4. Flash-Befehl ausfรผhren ``` --- @@ -166,254 +430,166 @@ idf.py menuconfig ### Hauptkomponenten -| Component | Model | Notes | -|-----------|-------|-------| -| **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM | -| **Display** | ST7789 2.0" | 240x320, integriert | -| **Audio** | MAX98357A | I2S Amplifier | -| **NFC Reader** | PN532 | I2C mode | -| **Storage** | MicroSD Card | FAT32, via SPI | +| Component | Model | Notes | +|----------------|---------------------------------|------------------------| +| **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM | +| **Display** | ST7789 2.0" | 320ร—240, integriert | +| **Audio** | MAX98357A | I2S Amplifier | +| **Storage** | MicroSD Card | FAT32, via SPI | ### Pin-Belegung -> **Siehe `main/hardware_config.h` fรผr alle Pin-Definitionen!** +> **Siehe `main/include/hardware_config.h` fรผr alle Pin-Definitionen!** -**Wichtigste Pins:** - -**Display (ST7789):** -- MOSI: GPIO 11 -- SCLK: GPIO 12 -- CS: GPIO 10 -- DC: GPIO 8 -- RST: GPIO 14 -- BL: GPIO 9 +**Display (ST7789 SPI):** +- MOSI: GPIO 38, SCLK: GPIO 39, CS: GPIO 45 +- DC: GPIO 42, BCKL: GPIO 1 **Audio (I2S):** -- BCLK: GPIO 35 -- LRC: GPIO 36 -- DIN: GPIO 37 +- BCLK: GPIO 48, LRC: GPIO 47, DIN: GPIO 16 -**NFC (PN532):** -- SCL: GPIO 16 -- SDA: GPIO 15 +**Buttons (TODO):** +- UP: 8, DOWN: 9, LEFT: 10, RIGHT: 11 +- A: 12, B: 13, START: 14, SELECT: 21 -**Buttons:** -- UP: GPIO 1, DOWN: GPIO 2 -- LEFT: GPIO 42, RIGHT: GPIO 41 -- A: GPIO 21, B: GPIO 47 -- START: GPIO 48, SELECT: GPIO 45 +**NFC (I2C, TODO):** +- SCL: GPIO 6, SDA: GPIO 5 (shared mit Touch) -**Link Cable:** -- SCLK: GPIO 17 -- SOUT: GPIO 18 -- SIN: GPIO 38 +**Link Cable (TODO):** +- SCLK: GPIO 15, SOUT: GPIO 2, SIN: GPIO 17 + +**Potentiometer (ADC, TODO):** +- Volume: GPIO 3, Brightness: GPIO 4 --- ## ๐Ÿ“ฆ Projekt-Struktur ``` -esp32-s3-gnuboy/ -โ”œโ”€โ”€ CMakeLists.txt # Root CMake -โ”œโ”€โ”€ sdkconfig.defaults # ESP-IDF config -โ”œโ”€โ”€ partitions.csv # Flash partitions -โ”œโ”€โ”€ README.md # Diese Datei -โ”œโ”€โ”€ fix_psram.sh # โš ๏ธ WICHTIG: Vor Build ausfรผhren! -โ”œโ”€โ”€ fix_poti_manager_nfc_json.sh # โš ๏ธ WICHTIG: Vor Build ausfรผhren! +gnuboy/ +โ”œโ”€โ”€ CMakeLists.txt # Root CMake +โ”œโ”€โ”€ sdkconfig # ESP-IDF Konfiguration +โ”œโ”€โ”€ README.md # Diese Datei โ”‚ โ”œโ”€โ”€ main/ โ”‚ โ”œโ”€โ”€ CMakeLists.txt -โ”‚ โ”œโ”€โ”€ main.c # Hauptprogramm +โ”‚ โ”œโ”€โ”€ main.c # Hauptprogramm (ausfรผhrlich kommentiert) โ”‚ โ””โ”€โ”€ include/ -โ”‚ โ””โ”€โ”€ hardware_config.h # Pin-Definitionen +โ”‚ โ””โ”€โ”€ hardware_config.h # Pin-Definitionen & Scaling โ”‚ โ””โ”€โ”€ components/ - โ”œโ”€โ”€ gnuboy/ # GNUBoy Emulator Core - โ”‚ โ”œโ”€โ”€ CMakeLists.txt - โ”‚ โ”œโ”€โ”€ include/gnuboy.h - โ”‚ โ””โ”€โ”€ gnuboy_placeholder.c - โ”‚ - โ”œโ”€โ”€ st7789/ # Display Driver - โ”‚ โ”œโ”€โ”€ CMakeLists.txt - โ”‚ โ”œโ”€โ”€ include/st7789.h - โ”‚ โ””โ”€โ”€ st7789.c - โ”‚ - โ”œโ”€โ”€ nfc_manager/ # NFC ROM Selection - โ”‚ โ”œโ”€โ”€ CMakeLists.txt - โ”‚ โ”œโ”€โ”€ include/nfc_manager.h - โ”‚ โ””โ”€โ”€ nfc_manager.c - โ”‚ - โ”œโ”€โ”€ link_cable/ # 2-Player Support - โ”‚ โ”œโ”€โ”€ CMakeLists.txt - โ”‚ โ”œโ”€โ”€ include/link_cable.h - โ”‚ โ””โ”€โ”€ link_cable.c - โ”‚ - โ””โ”€โ”€ potentiometer_manager/ # Volume/Brightness Control - โ”œโ”€โ”€ CMakeLists.txt - โ”œโ”€โ”€ include/potentiometer_manager.h - โ””โ”€โ”€ potentiometer_manager.c + โ”œโ”€โ”€ peanut-gb/ # GameBoy Emulator Core (-O3) + โ”œโ”€โ”€ st7789/ # Display Driver (80 MHz SPI) + โ”œโ”€โ”€ minizip/ # ZIP Support + โ”œโ”€โ”€ zlib/ # Compression + โ”œโ”€โ”€ link_cable/ # 2-Player (TODO) + โ”œโ”€โ”€ nfc_manager/ # NFC ROM Selection (TODO) + โ””โ”€โ”€ potentiometer_manager/ # Volume/Brightness (TODO) ``` --- -## ๐ŸŽฎ Benutzung +## ๐Ÿ”ง Konfiguration -### ROMs laden +### Display Scaling รคndern -1. SD-Karte formatieren (FAT32) -2. Verzeichnis erstellen: `/roms/gb/` -3. ROMs kopieren (.gb oder .gbc Dateien) -4. SD-Karte einlegen +In `main/include/hardware_config.h`: -### NFC ROM-Auswahl - -- NFC Tags programmieren mit ROM-Namen -- Tag scannen โ†’ ROM startet automatisch! -- Mapping-File: `/sd/nfc_roms.json` - -**Beispiel `nfc_roms.json`:** -```json -{ - "mappings": [ - { - "tag_uid": "04:12:34:56:78:9A:B0", - "rom_path": "/roms/gb/tetris.gb" - }, - { - "tag_uid": "04:AB:CD:EF:12:34:56", - "rom_path": "/roms/gb/pokemon_red.gb" - } - ] -} +```c +// Zeile 54: Scaling-Faktor anpassen +#define GB_SCALE_FACTOR 1.6 // ร„ndern auf 1.4, 1.5, 1.67, etc. ``` -### Link Cable 2-Player +**Empfohlene Werte:** +- `1.4` - Klein, sehr schnell (>100 FPS) +- `1.5` - Gut, schnell (90-100 FPS) +- `1.6` - Beste Balance (60-90 FPS) โญ +- `1.67` - Volle Hรถhe (55-70 FPS) -- Zwei GameBoys bauen -- Link Cable verbinden: - - SCLK โ†” SCLK - - SOUT โ†” SIN (gekreuzt!) - - SIN โ†” SOUT (gekreuzt!) - - GND โ†” GND -- Gleiches ROM laden auf beiden -- Im Spiel: 2-Player Mode wรคhlen -- Spielen! ๐ŸŽฎ๐Ÿ”—๐ŸŽฎ +### Fullscreen aktivieren -### Buttons - -GameBoy Layout: -``` - โ”Œโ”€โ”€โ”€โ”€โ”€โ” - โ†โ†’โ†‘โ†“ B A - SELECT START +```c +// Zeile 41: Scaling deaktivieren +#define GB_PIXEL_PERFECT_SCALING 0 // Fullscreen 320ร—240 ``` -- **D-Pad:** Bewegung -- **A/B:** Aktion -- **START:** Menรผ/Start -- **SELECT:** Auswahl - -### Potentiometer - -- **Links:** Volume (0-100%) -- **Rechts:** Brightness (10-100%) +**Warnung:** Fullscreen ist langsamer (~50 FPS bei vielen Spielen) --- -## ๐Ÿ”ง Entwicklung +## ๐Ÿ› Troubleshooting -### Aktueller Status +### Build Fehler? -- โœ… Projekt-Struktur fertig -- โœ… CMake Build System -- โœ… Pin-Konfiguration -- โœ… Component-Grundstruktur -- โœ… ESP-IDF v4.4 Kompatibilitรคt -- โœ… PSRAM Support (8MB Octal) -- โณ GNUBoy Core Integration -- โณ ST7789 Driver fertigstellen -- โณ NFC Implementation -- โณ Link Cable Implementation +```bash +# Clean & Rebuild +idf.py fullclean +idf.py build +``` -### TODO +### PSRAM nicht erkannt? -1. **GNUBoy Core integrieren:** - - Quellcode von esplay-gb portieren - - Fรผr ESP32-S3 anpassen - - Serial/Link Interface implementieren +```bash +# Prรผfen im Monitor +idf.py monitor +# Sollte zeigen: "PSRAM: 8191 KB total" +``` -2. **ST7789 Driver fertigstellen:** - - Init-Sequenz ergรคnzen - - Framebuffer-Rendering optimieren - - GameBoyโ†’Display Mapping +Falls nicht: +```bash +idf.py menuconfig +# โ†’ Component config โ†’ ESP32S3-Specific +# โ†’ [*] Support for external, SPI-connected RAM +# โ†’ Mode: Octal Mode PSRAM +# โ†’ Speed: 80MHz +``` -3. **NFC Manager implementieren:** - - PN532 I2C Treiber - - Tag UID auslesen - - JSON Mapping parsen +### Zu langsam? -4. **Link Cable fertigstellen:** - - GPIO Bit-Transfer - - Master/Slave Negotiation - - GNUBoy Serial Hook +1. Display Scaling reduzieren (`GB_SCALE_FACTOR 1.4`) +2. Compiler-Optimierung prรผfen (sollte `-O3` sein) +3. SPI-Speed prรผfen (sollte 80 MHz sein) -5. **Audio implementieren:** - - I2S Konfiguration - - GameBoyโ†’I2S Buffer - - Volume Control +### Audio knackt? -### Code-Richtlinien - -- **Zentralisierte Konfiguration:** Alle Pins in `hardware_config.h` -- **ESP-IDF Stil:** `ESP_LOG` statt `printf` -- **Fehlerbehandlung:** Immer `ESP_ERROR_CHECK` nutzen -- **Dokumentation:** Doxygen-Kommentare +- Normal bei sehr langsamen Spielen (<40 FPS) +- Bei >50 FPS sollte Audio perfekt sein --- ## ๐Ÿ“ Lizenz -- **GNUBoy:** GPL v2.0 -- **Projekt-spezifischer Code:** MIT (oder nach Wahl) +- **Peanut-GB:** MIT License +- **Projekt-spezifischer Code:** MIT +- **Components:** Siehe jeweilige LICENSE-Dateien --- ## ๐Ÿ™ Credits -- **GNUBoy:** Original GameBoy Emulator -- **esplay-gb:** ESP32 Port von pebri86 -- **MCH2022 Badge Team:** ESP32 GNUBoy App -- **Waveshare:** Hardware Board -- **Stefan:** LEGO GameBoy Projekt! ๐ŸŽฎ +- **Peanut-GB:** Delta (Header-only GameBoy Emulator) +- **Waveshare:** Hardware Board ESP32-S3-Touch-LCD-2 +- **Espressif:** ESP-IDF Framework +- **Duffy:** Dieses LEGO GameBoy Projekt! ๐ŸŽฎ --- -## ๐Ÿ“ž Support +## ๐ŸŽฏ Roadmap -Bei Fragen oder Problemen: +**Nรคchste Schritte:** -1. Hardware-Config prรผfen (`hardware_config.h`) -2. Serial Monitor checken (`idf.py monitor`) -3. Build-Log lesen -4. Pin-Konflikte รผberprรผfen -5. Fix-Scripts ausgefรผhrt? (`fix_psram.sh` & `fix_poti_manager_nfc_json.sh`) +1. โœ… Emulator lรคuft perfekt (90-111 FPS!) +2. โœ… Audio funktioniert (4 Kanรคle) +3. โœ… Display-Scaling optimiert +4. โณ Button-Input implementieren +5. โณ ROM-Menรผ auf SD-Card +6. โณ NFC ROM-Auswahl +7. โณ Potentiometer Volume/Brightness +8. โณ Link Cable 2-Player +9. โณ Save-States --- -## ๐ŸŽฏ Vision +**Viel SpaรŸ beim Zocken! ๐ŸŽฎ๐Ÿ”Š** -**Ziel:** Der ultimative LEGO GameBoy Emulator! - -- Zwei baugleiche Gerรคte -- Link Cable Multiplayer -- NFC ROM-Auswahl -- Professionelle Qualitรคt -- Open Source - -**Let's build it! ๐Ÿš€๐ŸŽฎ** - ---- - -*Erstellt fรผr Stefan's LEGO GameBoy Projekt* -*Hardware: Waveshare ESP32-S3-Touch-LCD-2* -*Mit Liebe zum Detail gebaut! โค๏ธ* +*Erstellt mit Liebe zum Detail โค๏ธ* diff --git a/components/README_COMPONENTS.md b/components/README_COMPONENTS.md new file mode 100644 index 0000000..de5e118 --- /dev/null +++ b/components/README_COMPONENTS.md @@ -0,0 +1,195 @@ +# Components รœbersicht + +Dieses Dokument beschreibt alle Components im ESP32-S3 GameBoy Projekt. + +## ๐ŸŽฎ Aktiv genutzte Components + +### 1. **peanut-gb** (GameBoy Emulator Core) +- **Pfad:** `components/peanut-gb/` +- **Typ:** Header-Only Emulator +- **Funktion:** Vollstรคndiger GameBoy (DMG) Emulator +- **Optimierung:** Kompiliert mit `-O3` Flag fรผr maximale Performance +- **Features:** + - CPU Emulation (Z80-รคhnlicher Sharp LR35902) + - PPU (Picture Processing Unit) fรผr Grafik + - APU (Audio Processing Unit) mit 4 Kanรคlen + - Memory Management & Cartridge Support + +**Verwendung in main.c:** +```c +#include "peanut_gb.h" +gb_init(&gb, &gb_rom_read, ...); // Emulator initialisieren +gb_run_frame(&gb); // Ein Frame emulieren +``` + +### 2. **st7789** (Display-Treiber) +- **Pfad:** `components/st7789/` +- **Typ:** SPI Display-Treiber +- **Hardware:** ST7789V2 Controller (320ร—240, 2.0" TFT) +- **SPI-Frequenz:** 80 MHz +- **Features:** + - Hardware-Reset รผber GPIO + - PWM-Hintergrundbeleuchtung (0-100%) + - DMA-basierte SPI-Transfers + - Optimierte `preswapped` Funktion (BGR565) + +**API-Funktionen:** +```c +st7789_init(); // Display initialisieren +st7789_set_backlight(80); // Helligkeit 80% +st7789_fill_screen(0xF800); // Rot +st7789_draw_buffer_preswapped(buffer, ...); // Framebuffer (SCHNELL!) +``` + +**Performance-Trick:** +Die `preswapped` Funktion spart ~33% CPU-Zeit, da kein Byte-Swapping nรถtig ist: +```c +// RGB565 โ†’ BGR565 Swap direkt beim Rendering +uint16_t c = gb_palette[pixel]; +uint16_t swapped = (c >> 8) | (c << 8); // Einmal swappen +buffer[i] = swapped; // โ†’ st7789_draw_buffer_preswapped() +``` + +### 3. **minizip** & **zlib** (Kompression) +- **Pfad:** `components/minizip/`, `components/zlib/` +- **Funktion:** ZIP-Datei Unterstรผtzung +- **Status:** Vorbereitet fรผr zukรผnftige ROM-Kompression +- **Verwendung:** ROMs kรถnnen als `.zip` geladen werden + +--- + +## ๐Ÿ”ฎ Fรผr zukรผnftige Features vorbereitet + +### 4. **link_cable** (2-Player Link-Kabel) +- **Pfad:** `components/link_cable/` +- **Funktion:** GameBoy Link-Kabel Emulation รผber GPIO +- **Pins:** GPIO 15 (SCLK), GPIO 2 (SOUT), GPIO 17 (SIN) +- **Status:** Component vorhanden, noch nicht integriert +- **Zukรผnftige Nutzung:** + - 2-Player Modi (z.B. Tetris vs., Pokรฉmon Tausch) + - Kommunikation via GPIO oder ESP-NOW + +**Hardware-Vorbereitung:** +```c +#define LINK_GPIO_SCLK 15 +#define LINK_GPIO_SOUT 2 +#define LINK_GPIO_SIN 17 +#define LINK_CLOCK_FREQ 8192 // GameBoy Link Cable: 8192 Hz +``` + +### 5. **nfc_manager** (NFC-Reader PN532) +- **Pfad:** `components/nfc_manager/` +- **Hardware:** PN532 NFC-Reader +- **Interface:** I2C (geteilt mit Touch-Controller) +- **I2C-Adresse:** 0x24 (Touch: 0x15) +- **Pins:** GPIO 5 (SDA), GPIO 6 (SCL) - SHARED BUS! +- **Status:** Component vorhanden, noch nicht integriert +- **Zukรผnftige Nutzung:** + - ROM-Auswahl via NFC-Tag + - Save-State auf NFC-Tag speichern + - Cheat-Codes via NFC + +**I2C-Bus-Sharing:** +```c +// Touch Controller: 0x15 @ 400 kHz +// NFC Reader: 0x24 @ 100 kHz +// Beide auf I2C_NUM_0, GPIO 5/6 +``` + +### 6. **potentiometer_manager** (ADC fรผr Potis) +- **Pfad:** `components/potentiometer_manager/` +- **Hardware:** 2ร— Potentiometer +- **Pins:** + - GPIO 3 (ADC1_CH2): Volume + - GPIO 4 (ADC1_CH3): Brightness +- **ADC:** 12-bit (0-4095) +- **Status:** Component vorhanden, noch nicht integriert +- **Zukรผnftige Nutzung:** + - Volume-Kontrolle fรผr Audio (0-100%) + - Brightness-Kontrolle fรผr Display (0-100%) + +**Geplante Integration:** +```c +// Im audio_task: +int volume = poti_read_volume(); // 0-100 +master_vol_left = volume * 7 / 100; // NR50 Master Volume + +// Im display_task: +int brightness = poti_read_brightness(); // 0-100 +st7789_set_backlight(brightness); +``` + +--- + +## ๐Ÿ—‘๏ธ Nicht genutzte Components + +Diese Components stammen vom ursprรผnglichen MCH2022-Badge Projekt und werden **NICHT** verwendet: + +- **appfs** - App-Dateisystem (MCH2022 spezifisch) +- **bus-i2c** - I2C-Bus-Manager (direkte Treiber verwenden) +- **i2c-bme680** - BME680 Umweltsensor +- **i2c-bno055** - BNO055 IMU-Sensor +- **mch2022-bsp** - MCH2022 Board Support Package +- **mch2022-rp2040** - RP2040 Co-Prozessor +- **pax-codecs** - PAX Graphics Codecs +- **pax-graphics** - PAX Graphics Library +- **pax-keyboard** - PAX Keyboard +- **sdcard** - Alte SD-Karten Component (nutzen VFS direkt) +- **spi-ice40** - ICE40 FPGA +- **spi-ili9341** - ILI9341 Display (haben ST7789) +- **ws2812** - WS2812 RGB-LEDs + +Diese Components kรถnnen gelรถscht werden, falls Speicherplatz benรถtigt wird. + +--- + +## ๐Ÿ“Š Component Abhรคngigkeiten + +``` +main + โ”œโ”€โ–บ peanut-gb (Emulator) + โ”œโ”€โ–บ st7789 (Display) + โ”œโ”€โ–บ minizip (ROM-Kompression, optional) + โ””โ”€โ–บ zlib (Kompression-Backend) + +Zukรผnftig: + โ”œโ”€โ–บ link_cable (2-Player) + โ”œโ”€โ–บ nfc_manager (NFC-Tags) + โ””โ”€โ–บ potentiometer_manager (Volume/Brightness) +``` + +--- + +## ๐Ÿ”ง Neue Component hinzufรผgen + +1. Component-Ordner in `components/` erstellen +2. `CMakeLists.txt` erstellen: + ```cmake + idf_component_register( + SRCS "meine_component.c" + INCLUDE_DIRS "include" + REQUIRES "esp_timer" + ) + ``` +3. In `main/CMakeLists.txt` als Abhรคngigkeit hinzufรผgen: + ```cmake + idf_component_register( + SRCS "main.c" + INCLUDE_DIRS "include" + REQUIRES "st7789" "peanut-gb" "meine_component" + ) + ``` +4. In `main.c` einbinden: + ```c + #include "meine_component.h" + ``` + +--- + +## ๐Ÿ“ Hinweise + +- Alle Components werden mit dem ESP-IDF Build-System kompiliert +- Components kรถnnen Optimierungs-Flags in ihrer `CMakeLists.txt` setzen +- Performance-kritische Components sollten `-O3` verwenden +- DMA-Buffer mรผssen mit `MALLOC_CAP_DMA` allokiert werden +- PSRAM-Buffer sollten `MALLOC_CAP_SPIRAM` verwenden diff --git a/components/st7789/include/st7789.h b/components/st7789/include/st7789.h index f5618f9..d74b443 100644 --- a/components/st7789/include/st7789.h +++ b/components/st7789/include/st7789.h @@ -1,8 +1,22 @@ /** * @file st7789.h - * @brief ST7789 Display Driver for Waveshare ESP32-S3-Touch-LCD-2 - * - * 2.0" TFT Display, 240x320 resolution + * @brief ST7789 Display-Treiber fรผr Waveshare ESP32-S3-Touch-LCD-2 + * + * Dieser Treiber steuert das 2.0" TFT-Display (240ร—320 Pixel) รผber SPI. + * + * Hardware-Details: + * - Controller: ST7789V2 + * - Auflรถsung: 320ร—240 Pixel (Landscape-Modus) + * - Farbtiefe: 16-bit RGB565 + * - Interface: 4-Wire SPI + * - Maximale SPI-Frequenz: 80 MHz + * - DMA-Unterstรผtzung fรผr schnelle Framebuffer-Transfers + * + * Features: + * - Hardware-Reset รผber GPIO + * - PWM-Hintergrundbeleuchtung (0-100%) + * - Optimierte Framebuffer-Funktion (preswapped) + * - DMA-basierte SPI-Transfers fรผr hohe Performance */ #pragma once @@ -15,53 +29,104 @@ extern "C" { #endif /** - * @brief Initialize ST7789 display - * @return ESP_OK on success + * @brief ST7789 Display initialisieren + * @return ESP_OK bei Erfolg, Fehlercode sonst + * + * Diese Funktion: + * - Initialisiert SPI-Bus (80 MHz) + * - Fรผhrt Hardware-Reset durch + * - Sendet Initialisierungs-Kommandos + * - Konfiguriert Display-Orientierung (Landscape) + * - Aktiviert Display */ esp_err_t st7789_init(void); /** - * @brief Set backlight brightness - * @param brightness 0-100% + * @brief Hintergrundbeleuchtung einstellen + * @param brightness Helligkeit (0-100%) + * + * Verwendet LEDC PWM fรผr stufenlose Helligkeitsregelung. + * 0 = Aus, 100 = Maximale Helligkeit */ void st7789_set_backlight(uint8_t brightness); /** - * @brief Clear screen to color - * @param color 16-bit RGB565 color + * @brief Bildschirm mit Farbe fรผllen + * @param color 16-bit RGB565 Farbe + * + * Fรผllt den gesamten Bildschirm (320ร—240) mit einer Vollfarbe. + * Nรผtzlich fรผr Splash-Screens oder Hintergrund. + * + * RGB565 Format: RRRRR GGGGGG BBBBB + * Beispiele: + * 0xF800 = Rot, 0x07E0 = Grรผn, 0x001F = Blau + * 0xFFFF = WeiรŸ, 0x0000 = Schwarz */ void st7789_fill_screen(uint16_t color); /** - * @brief Draw pixel - * @param x X coordinate - * @param y Y coordinate - * @param color 16-bit RGB565 color + * @brief Einzelnes Pixel zeichnen + * @param x X-Koordinate (0-319) + * @param y Y-Koordinate (0-239) + * @param color 16-bit RGB565 Farbe + * + * Langsame Funktion! Nur fรผr einzelne Pixel verwenden. + * Fรผr grรถรŸere Bereiche st7789_draw_buffer() nutzen. */ void st7789_draw_pixel(int16_t x, int16_t y, uint16_t color); /** - * @brief Draw buffer (framebuffer) - * @param buffer Pointer to RGB565 framebuffer - * @param x X start position - * @param y Y start position - * @param width Width of buffer - * @param height Height of buffer + * @brief Framebuffer zeichnen (mit Byte-Swap) + * @param buffer Zeiger auf RGB565 Framebuffer + * @param x X-Startposition + * @param y Y-Startposition + * @param width Breite des Buffers (Pixel) + * @param height Hรถhe des Buffers (Pixel) + * + * Diese Funktion fรผhrt automatisches Byte-Swapping durch: + * RGB565 โ†’ BGR565 (ST7789 erwartet BGR-Reihenfolge) + * + * HINWEIS: Allokiert temporรคren Buffer! Fรผr Performance + * besser st7789_draw_buffer_preswapped() verwenden. */ void st7789_draw_buffer(const uint16_t *buffer, int16_t x, int16_t y, int16_t width, int16_t height); /** - * @brief Draw buffer that is already byte-swapped (optimized, no allocation) - * @param buffer Pointer to pre-swapped BGR565 framebuffer - * @param x X start position - * @param y Y start position - * @param width Width of buffer - * @param height Height of buffer + * @brief Framebuffer zeichnen (OPTIMIERT, ohne Byte-Swap) + * @param buffer Zeiger auf vor-geswapted BGR565 Framebuffer + * @param x X-Startposition + * @param y Y-Startposition + * @param width Breite des Buffers (Pixel) + * @param height Hรถhe des Buffers (Pixel) + * + * OPTIMIERTE VERSION! Kein Byte-Swapping, keine Buffer-Allokation. + * Der Buffer MUSS bereits BGR565-formatiert sein! + * + * Verwendung: + * uint16_t c_rgb = 0xF800; // Rot in RGB565 + * uint16_t c_bgr = (c_rgb >> 8) | (c_rgb << 8); // Swap zu BGR565 + * buffer[i] = c_bgr; // In Buffer schreiben + * + * Dies ist die schnellste Methode fรผr GameBoy-Emulation! + * Spart ~33% CPU-Zeit im Vergleich zu draw_buffer(). */ void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y, int16_t width, int16_t height); +/** + * @brief Display in Sleep-Modus versetzen (fรผr Deep Sleep) + * + * Diese Funktion: + * - Schaltet Hintergrundbeleuchtung aus + * - Sendet DISPOFF Befehl (Display aus) + * - Sendet SLPIN Befehl (Sleep In) + * + * WICHTIG: Diese Funktion MUSS aufgerufen werden bevor der ESP32 + * in Deep-Sleep geht! Sonst blinkt das Display mit zufรคlligen Farben. + */ +void st7789_sleep(void); + #ifdef __cplusplus } #endif diff --git a/components/st7789/st7789.c b/components/st7789/st7789.c index e3fee7e..988c199 100644 --- a/components/st7789/st7789.c +++ b/components/st7789/st7789.c @@ -124,13 +124,28 @@ static void st7789_backlight_init(void) esp_err_t st7789_init(void) { ESP_LOGI(TAG, "Initializing ST7789 display..."); - + + // WICHTIG: GPIO Hold deaktivieren falls wir aus Deep Sleep aufwachen + // Sonst kรถnnen die Pins nicht neu konfiguriert werden! + gpio_hold_dis(LCD_PIN_CS); + gpio_hold_dis(LCD_PIN_DC); + gpio_hold_dis(LCD_PIN_BCKL); + gpio_hold_dis(LCD_PIN_MOSI); + gpio_hold_dis(LCD_PIN_SCLK); + + // DC-Pin konfigurieren (immer vorhanden) gpio_config_t io_conf = {}; - io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC) | (1ULL << LCD_PIN_RST); + io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC); io_conf.mode = GPIO_MODE_OUTPUT; io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; gpio_config(&io_conf); + + // RST-Pin konfigurieren (falls vorhanden) + if (LCD_PIN_RST >= 0) { + io_conf.pin_bit_mask = (1ULL << LCD_PIN_RST); + gpio_config(&io_conf); + } spi_bus_config_t buscfg = { .mosi_io_num = LCD_PIN_MOSI, @@ -138,7 +153,10 @@ esp_err_t st7789_init(void) .sclk_io_num = LCD_PIN_SCLK, .quadwp_io_num = -1, .quadhd_io_num = -1, - .max_transfer_sz = LCD_WIDTH * LCD_HEIGHT * 2 + 8, + // WICHTIG: GroรŸer Buffer fรผr Display UND SD-Karte (teilen sich den Bus!) + // Display braucht: 320ร—240ร—2 = 153600 Bytes + // SD-Karte braucht auch Platz fรผr Transfers + .max_transfer_sz = 4096 * 64, // 256 KB (genug fรผr beide) .flags = SPICOMMON_BUSFLAG_MASTER, }; ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); @@ -154,11 +172,17 @@ esp_err_t st7789_init(void) st7789_backlight_init(); st7789_set_backlight(0); - - gpio_set_level(LCD_PIN_RST, 0); - vTaskDelay(pdMS_TO_TICKS(100)); - gpio_set_level(LCD_PIN_RST, 1); - vTaskDelay(pdMS_TO_TICKS(100)); + + // Hardware-Reset (falls RST-Pin vorhanden) + if (LCD_PIN_RST >= 0) { + gpio_set_level(LCD_PIN_RST, 0); + vTaskDelay(pdMS_TO_TICKS(100)); + gpio_set_level(LCD_PIN_RST, 1); + vTaskDelay(pdMS_TO_TICKS(100)); + } else { + // Kein Hardware-Reset โ†’ Software-Reset reicht + vTaskDelay(pdMS_TO_TICKS(20)); + } st7789_send_cmd(ST7789_SWRESET); vTaskDelay(pdMS_TO_TICKS(150)); @@ -302,3 +326,54 @@ void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y, offset += chunk_size; } } + +void st7789_sleep(void) +{ + ESP_LOGI(TAG, "Display in Sleep-Modus versetzen..."); + + // 1. ZUERST: Bildschirm schwarz fรผllen (entfernt letztes Frame) + st7789_fill_screen(0x0000); + + // 2. Hintergrundbeleuchtung รผber PWM ausschalten + st7789_set_backlight(0); + + // 3. LEDC (PWM) Kanal komplett stoppen! + // Sonst lรคuft der PWM-Timer weiter und kann Glitches verursachen + ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); // 0 = LOW level + + // 4. Display ausschalten (DISPOFF) + st7789_send_cmd(ST7789_DISPOFF); + vTaskDelay(pdMS_TO_TICKS(20)); + + // 5. Sleep-Modus aktivieren (SLPIN) + // Im Sleep-Modus verbraucht das Display minimal Strom + // und die Ausgรคnge sind in einem definierten Zustand (kein Blinken!) + st7789_send_cmd(ST7789_SLPIN); + vTaskDelay(pdMS_TO_TICKS(120)); // ST7789 braucht 120ms fรผr Sleep-Eintritt + + // 6. KRITISCH: SPI-Pins in definierten Zustand bringen! + // Wรคhrend Deep Sleep floaten die Pins sonst und verursachen + // zufรคlliges Blinken auf dem Display. + gpio_set_level(LCD_PIN_CS, 1); // CS HIGH = Display ignoriert SPI + gpio_set_level(LCD_PIN_DC, 0); // DC auf definierten Pegel + + // 7. Backlight-Pin manuell auf OUTPUT setzen und LOW + // (รผberschreibt LEDC-Konfiguration) + gpio_reset_pin(LCD_PIN_BCKL); + gpio_set_direction(LCD_PIN_BCKL, GPIO_MODE_OUTPUT); + gpio_set_level(LCD_PIN_BCKL, 0); + + // 8. GPIO-Hold aktivieren fรผr Deep Sleep + // Dies hรคlt die Pin-Zustรคnde wรคhrend Deep Sleep stabil + gpio_hold_en(LCD_PIN_CS); + gpio_hold_en(LCD_PIN_DC); + gpio_hold_en(LCD_PIN_BCKL); + + // Auch SPI-Pins auf definierte Pegel setzen und halten + gpio_set_level(LCD_PIN_MOSI, 0); + gpio_set_level(LCD_PIN_SCLK, 0); + gpio_hold_en(LCD_PIN_MOSI); + gpio_hold_en(LCD_PIN_SCLK); + + ESP_LOGI(TAG, "Display schlรคft - GPIO Hold aktiviert"); +} diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index f9e0583..8bf4912 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,7 +1,8 @@ idf_component_register( - SRCS + SRCS "main.c" - INCLUDE_DIRS + "buttons.c" + INCLUDE_DIRS "." "include" REQUIRES diff --git a/main/buttons.c b/main/buttons.c new file mode 100644 index 0000000..bab888a --- /dev/null +++ b/main/buttons.c @@ -0,0 +1,426 @@ +/** + * @file buttons.c + * @brief Button-Handler Implementierung fรผr ESP32-S3 GameBoy Emulator + * + * Implementiert vollstรคndige Button-Verwaltung mit: + * - Software-Debouncing (50ms pro Button) + * - FreeRTOS Task fรผr kontinuierliches Polling + * - Deep Sleep bei Power-Off mit RTC GPIO Wakeup + * - Direkte Integration mit Peanut-GB Emulator + * + * Technische Details: + * - Polling-Intervall: 10ms (100 Hz, ausreichend fรผr Reaktionszeit) + * - Debounce-Algorithmus: Zustandsmaschine mit Zeitstempel + * - Thread-Safety: Atomic reads fรผr Button-State + * - Power-Check: Alle 100ms auf Sleep-Bedingung prรผfen + */ + +#include "buttons.h" +#include "hardware_config.h" + +#include "driver/gpio.h" +#include "driver/rtc_io.h" +#include "esp_sleep.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +// ============================================ +// LOGGING TAG +// ============================================ +static const char *TAG = "BUTTONS"; + +// ============================================ +// BUTTON PIN MAPPING +// ============================================ + +/** + * @brief Button-Pin-Zuordnung (8 GameBoy-Buttons) + * + * Index entspricht dem Bit-Index in button_flags_t: + * - buttons_pins[0] = BTN_A (GPIO 12) + * - buttons_pins[1] = BTN_B (GPIO 13) + * - etc. + */ +static const gpio_num_t buttons_pins[8] = { + BTN_A, // Bit 0 + BTN_B, // Bit 1 + BTN_SELECT, // Bit 2 + BTN_START, // Bit 3 + BTN_RIGHT, // Bit 4 + BTN_LEFT, // Bit 5 + BTN_UP, // Bit 6 + BTN_DOWN, // Bit 7 +}; + +// ============================================ +// BUTTON STATE VARIABLEN +// ============================================ + +/** + * @brief Aktueller Button-State (8-Bit Bitmaske) + * Bit gesetzt = Button gedrรผckt + * Wird atomar gelesen/geschrieben + */ +static volatile uint8_t button_state = 0x00; + +/** + * @brief Debouncing State fรผr jeden Button + * + * Struktur pro Button: + * - last_state: Letzter stabiler Zustand (0=nicht gedrรผckt, 1=gedrรผckt) + * - raw_state: Aktueller GPIO-Rohwert + * - last_change_time: Zeitpunkt der letzten ร„nderung (ms) + */ +typedef struct { + uint8_t last_state; // 0 oder 1 + uint8_t raw_state; // 0 oder 1 + uint32_t last_change_time; // Millisekunden seit Boot +} button_debounce_t; + +static button_debounce_t debounce_state[8] = {0}; // Ein Eintrag pro Button + +// ============================================ +// TASK HANDLE +// ============================================ +static TaskHandle_t button_task_handle = NULL; + +// ============================================ +// EXTERNE FUNKTIONEN (aus main.c) +// ============================================ + +/** + * @brief Joypad-State an Peanut-GB Emulator รผbergeben + * @param state 8-Bit Bitmaske mit gedrรผckten Buttons + * + * Diese Funktion ist in main.c definiert und setzt den + * Joypad-State im Emulator: gb.direct.joypad = state + */ +extern void gb_set_joypad_state(uint8_t state); + +// ============================================ +// HILFSFUNKTIONEN +// ============================================ + +/** + * @brief Aktuelle Zeit in Millisekunden abrufen + * @return Zeit seit Boot in ms + * + * Verwendet FreeRTOS Tick-Counter mit configTICK_RATE_HZ. + * Standard: 1000 Ticks/Sekunde = 1 Tick = 1ms + */ +static inline uint32_t millis(void) +{ + return xTaskGetTickCount() * portTICK_PERIOD_MS; +} + +/** + * @brief Einzelnen Button mit Debouncing lesen + * @param btn_index Button-Index (0-7) + * @return 1 wenn Button gedrรผckt, 0 sonst + * + * Debouncing-Algorithmus: + * 1. GPIO-Rohwert lesen (invertiert, da Active-LOW) + * 2. Wenn Rohwert != letzter Rohwert โ†’ Zeitstempel aktualisieren + * 3. Wenn Rohwert stabil fรผr DEBOUNCE_MS โ†’ State รผbernehmen + * + * Dies filtert mechanische Prellungen (Bouncing) heraus. + */ +static uint8_t read_button_debounced(uint8_t btn_index) +{ + // GPIO lesen (0 = gedrรผckt wegen Active-LOW, 1 = nicht gedrรผckt) + // Invertieren, damit 1 = gedrรผckt + uint8_t raw = !gpio_get_level(buttons_pins[btn_index]); + + button_debounce_t *db = &debounce_state[btn_index]; + uint32_t now = millis(); + + // Hat sich der Rohwert geรคndert? + if (raw != db->raw_state) { + db->raw_state = raw; + db->last_change_time = now; // Zeitstempel aktualisieren + } + + // Ist der Rohwert lange genug stabil? + if ((now - db->last_change_time) >= BTN_DEBOUNCE_MS) { + db->last_state = raw; // Stabilen Wert รผbernehmen + } + + return db->last_state; +} + +/** + * @brief Alle 8 GameBoy-Buttons lesen und State aktualisieren + * + * Diese Funktion: + * 1. Liest alle 8 Buttons mit Debouncing + * 2. Erstellt 8-Bit Bitmaske + * 3. Aktualisiert globalen button_state (atomar) + * 4. รœbertrรคgt State direkt an Peanut-GB Emulator + */ +static void update_button_state(void) +{ + uint8_t new_state = 0x00; + + // Alle 8 Buttons durchgehen + for (int i = 0; i < 8; i++) { + if (read_button_debounced(i)) { + new_state |= (1 << i); // Bit setzen wenn Button gedrรผckt + } + } + + // State atomar aktualisieren + button_state = new_state; + + // Direkt an Peanut-GB Emulator weiterleiten + // Die gb_set_joypad_state() Funktion in main.c setzt: + // gb.direct.joypad = state + // - Bit 0-7 fรผr die 8 Buttons + // - 1 = gedrรผckt, 0 = nicht gedrรผckt + gb_set_joypad_state(new_state); +} + +/** + * @brief Power-Button prรผfen und ggf. Sleep aktivieren + * @return true wenn System weiterlaufen soll, false bei Sleep + * + * Diese Funktion liest GPIO 0 (Power-Schalter): + * - LOW (0) = Schalter ON โ†’ System lรคuft weiter + * - HIGH (1) = Schalter OFF โ†’ Deep Sleep aktivieren + * + * Im Deep Sleep: + * - CPU gestoppt, RAM aus, nur RTC lรคuft + * - Stromverbrauch: ~10 ยตA (vs. ~80 mA im Betrieb) + * - Wakeup nur durch GPIO 0 = LOW (Schalter wieder auf ON) + */ +static bool check_power_button(void) +{ +#if POWER_BUTTON_ENABLED + // GPIO 18 lesen (0 = ON, 1 = OFF wegen Pull-Up) + int power_level = gpio_get_level(POWER_SWITCH_PIN); + + if (power_level == POWER_SWITCH_OFF) { + // Schalter auf OFF โ†’ Deep Sleep + ESP_LOGI(TAG, "Power-Schalter auf OFF - aktiviere Deep Sleep..."); + + // 1. ZUERST: Alle Tasks sauber stoppen (Emulation, Display, Audio) + // Dies verhindert das grรผne Blinken vom Display-Task! + extern void system_prepare_sleep(void); + system_prepare_sleep(); + + // 2. DANN: Display in Sleep-Modus versetzen + // Sendet DISPOFF + SLPIN und aktiviert GPIO-Hold + extern void st7789_sleep(void); + st7789_sleep(); + + // 3. Kurz warten, damit Log-Nachricht gesendet wird + vTaskDelay(pdMS_TO_TICKS(100)); + + // 4. Deep Sleep aktivieren + buttons_enter_sleep(); + + // HINWEIS: Diese Zeile wird nie erreicht, da esp_deep_sleep_start() + // das System sofort anhรคlt. Bei Wakeup erfolgt Neustart. + return false; + } +#endif + + return true; // System weiterlaufen lassen (oder Power-Button deaktiviert) +} + +// ============================================ +// BUTTON POLLING TASK +// ============================================ + +/** + * @brief FreeRTOS Task fรผr kontinuierliches Button-Polling + * @param pvParameters Nicht verwendet (NULL) + * + * Dieser Task lรคuft in einer Endlosschleife und: + * 1. Liest alle 8 GameBoy-Buttons (mit Debouncing) + * 2. Aktualisiert Emulator-Joypad-State + * 3. Prรผft alle 100ms den Power-Button + * 4. Schlรคft 10ms zwischen Iterationen (100 Hz Polling) + * + * Task-Konfiguration: + * - Core: 1 (gleicher Core wie Emulation) + * - Prioritรคt: 5 (mittel - niedriger als Emulation/Display) + * - Stack: 2048 Bytes (ausreichend fรผr GPIO-Operationen) + */ +static void button_task(void *pvParameters) +{ + ESP_LOGI(TAG, "Button-Task gestartet (Core %d)", xPortGetCoreID()); + + uint32_t last_power_check = 0; // Zeitpunkt der letzten Power-Check + + while (1) { + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 1. GameBoy-Buttons lesen und aktualisieren (jede Iteration) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + update_button_state(); + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 2. Power-Button prรผfen (alle 100ms) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + uint32_t now = millis(); + if ((now - last_power_check) >= POWER_SWITCH_CHECK_MS) { + last_power_check = now; + + if (!check_power_button()) { + // System geht in Sleep (wird nie erreicht, siehe Funktion) + break; + } + } + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 3. Kurz schlafen (10ms = 100 Hz Polling-Rate) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + vTaskDelay(pdMS_TO_TICKS(10)); + } + + // Cleanup (wird normalerweise nie erreicht) + vTaskDelete(NULL); +} + +// ============================================ +// ร–FFENTLICHE API-FUNKTIONEN +// ============================================ + +esp_err_t buttons_init(void) +{ + ESP_LOGI(TAG, "Initialisiere Button-System..."); + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 1. GameBoy-Buttons (GPIO 8-14, 21) konfigurieren + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + + gpio_config_t btn_config = { + .mode = GPIO_MODE_INPUT, // Eingang + .pull_up_en = GPIO_PULLUP_ENABLE, // Pull-Up aktivieren + .pull_down_en = GPIO_PULLDOWN_DISABLE, // Pull-Down aus + .intr_type = GPIO_INTR_DISABLE, // Keine Interrupts (Polling) + }; + + // Alle 8 Button-Pins konfigurieren + for (int i = 0; i < 8; i++) { + btn_config.pin_bit_mask = (1ULL << buttons_pins[i]); + esp_err_t ret = gpio_config(&btn_config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Fehler beim Konfigurieren von GPIO %d: %s", + buttons_pins[i], esp_err_to_name(ret)); + return ret; + } + } + + ESP_LOGI(TAG, "8 GameBoy-Buttons konfiguriert (GPIO 8-14, 21)"); + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 2. Power-Button (GPIO 18) konfigurieren - NUR WENN ENABLED! + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + +#if POWER_BUTTON_ENABLED + gpio_config_t power_config = { + .pin_bit_mask = (1ULL << POWER_SWITCH_PIN), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, // Pull-Up fรผr Open-Drain-Schalter + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + esp_err_t ret = gpio_config(&power_config); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Fehler beim Konfigurieren von Power-GPIO %d: %s", + POWER_SWITCH_PIN, esp_err_to_name(ret)); + return ret; + } + + ESP_LOGI(TAG, "Power-Button konfiguriert (GPIO %d)", POWER_SWITCH_PIN); + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 3. Deep Sleep Wakeup konfigurieren + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + + // GPIO 18 als Wakeup-Source konfigurieren + // Wakeup bei LOW (Schalter auf ON) + esp_sleep_enable_ext0_wakeup(POWER_SWITCH_PIN, 0); // 0 = LOW-Level Wakeup + + ESP_LOGI(TAG, "Deep Sleep Wakeup konfiguriert (GPIO %d = LOW)", POWER_SWITCH_PIN); +#else + ESP_LOGW(TAG, "Power-Button DEAKTIVIERT (POWER_BUTTON_ENABLED=0 in hardware_config.h)"); + ESP_LOGW(TAG, "Deep Sleep wird NICHT verwendet. Schalter kann spรคter aktiviert werden."); +#endif + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 4. Debouncing-State initialisieren + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + + for (int i = 0; i < 8; i++) { + debounce_state[i].last_state = 0; + debounce_state[i].raw_state = 0; + debounce_state[i].last_change_time = 0; + } + + ESP_LOGI(TAG, "Button-System erfolgreich initialisiert"); + return ESP_OK; +} + +void buttons_start(void) +{ + if (button_task_handle != NULL) { + ESP_LOGW(TAG, "Button-Task lรคuft bereits!"); + return; + } + + // Task erstellen auf Core 1 (gleicher Core wie Emulation) + BaseType_t ret = xTaskCreatePinnedToCore( + button_task, // Task-Funktion + "button_task", // Task-Name (fรผr Debugging) + 2048, // Stack-GrรถรŸe (Bytes) + NULL, // Parameter (nicht verwendet) + 5, // Prioritรคt (5 = mittel) + &button_task_handle, // Task-Handle speichern + 1 // Core 1 (Emulation-Core) + ); + + if (ret == pdPASS) { + ESP_LOGI(TAG, "Button-Task gestartet auf Core 1"); + } else { + ESP_LOGE(TAG, "Fehler beim Starten des Button-Tasks!"); + } +} + +uint8_t buttons_get_state(void) +{ + // Atomarer Read (8-Bit ist atomar auf ESP32) + return button_state; +} + +bool buttons_is_power_on(void) +{ +#if POWER_BUTTON_ENABLED + // GPIO 18 lesen: 0 = ON, 1 = OFF + // Invertieren fรผr bool-Rรผckgabe: true = ON, false = OFF + return (gpio_get_level(POWER_SWITCH_PIN) == POWER_SWITCH_ON); +#else + // Power-Button deaktiviert โ†’ immer "ON" zurรผckgeben + return true; +#endif +} + +void buttons_enter_sleep(void) +{ + ESP_LOGI(TAG, "==========================================="); + ESP_LOGI(TAG, " ESP32 GEHT IN DEEP SLEEP "); + ESP_LOGI(TAG, "==========================================="); + ESP_LOGI(TAG, "Zum Aufwachen: Power-Schalter auf ON"); + ESP_LOGI(TAG, "Stromverbrauch im Sleep: ~10 ยตA"); + ESP_LOGI(TAG, "==========================================="); + + // Kurz warten, damit Log-Ausgabe gesendet wird + vTaskDelay(pdMS_TO_TICKS(200)); + + // Deep Sleep aktivieren + // HINWEIS: Diese Funktion kehrt NICHT zurรผck! + // Bei Wakeup erfolgt ein vollstรคndiger ESP32-Neustart (Boot) + esp_deep_sleep_start(); +} diff --git a/main/include/buttons.h b/main/include/buttons.h new file mode 100644 index 0000000..44af9dc --- /dev/null +++ b/main/include/buttons.h @@ -0,0 +1,125 @@ +/** + * @file buttons.h + * @brief Button-Handler fรผr ESP32-S3 GameBoy Emulator + * + * Dieser Header definiert die Button-Verwaltung fรผr: + * - 8ร— GameBoy-Buttons (D-Pad, A, B, Select, Start) + * - 1ร— Power-Button fรผr Sleep/Wake-Modus + * + * Hardware-Details: + * - Alle Buttons sind Active-LOW (Pull-Up mit GND beim Drรผcken) + * - Debouncing: 50ms pro Button + * - Power-Button: GPIO 0 (Hardware-Schalter) + * - GameBoy-Buttons: GPIO 8-14, 21 + * + * Integration: + * - Direkter Callback zu Peanut-GB Emulator + * - FreeRTOS Task fรผr kontinuierliches Polling + * - ESP32 Deep Sleep bei Power-Off + */ + +#pragma once + +#include +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================ +// BUTTON DEFINITIONEN +// ============================================ + +/** + * @brief GameBoy Button-Bits (kompatibel mit Peanut-GB Joypad) + * + * Diese Bit-Flags entsprechen dem GameBoy Joypad-Register (0xFF00) + * Bit gesetzt (1) = Button gedrรผckt + */ +typedef enum { + BTN_FLAG_A = (1 << 0), // A-Button (Aktions-Taste) + BTN_FLAG_B = (1 << 1), // B-Button (Aktions-Taste) + BTN_FLAG_SELECT = (1 << 2), // Select-Button (System-Taste) + BTN_FLAG_START = (1 << 3), // Start-Button (System-Taste) + BTN_FLAG_RIGHT = (1 << 4), // D-Pad Rechts + BTN_FLAG_LEFT = (1 << 5), // D-Pad Links + BTN_FLAG_UP = (1 << 6), // D-Pad Oben + BTN_FLAG_DOWN = (1 << 7), // D-Pad Unten +} button_flags_t; + +// ============================================ +// ร–FFENTLICHE FUNKTIONEN +// ============================================ + +/** + * @brief Button-System initialisieren + * @return ESP_OK bei Erfolg, Fehlercode sonst + * + * Diese Funktion: + * - Konfiguriert alle GPIO-Pins als Eingรคnge mit Pull-Up + * - Initialisiert Debouncing-State fรผr alle Buttons + * - Erstellt Button-Polling-Task (Core 1, Prioritรคt 5) + * - Konfiguriert GPIO 0 als RTC-Wakeup-Source fรผr Deep Sleep + * + * WICHTIG: Muss vor buttons_start() aufgerufen werden! + */ +esp_err_t buttons_init(void); + +/** + * @brief Button-Polling Task starten + * + * Startet den FreeRTOS Task, der kontinuierlich die Buttons abfragt. + * Der Task lรคuft auf Core 1 mit Prioritรคt 5 und aktualisiert + * den Joypad-State alle 10ms. + * + * WICHTIG: buttons_init() muss vorher aufgerufen worden sein! + */ +void buttons_start(void); + +/** + * @brief Aktuellen Button-State abrufen + * @return 8-Bit Bitmaske mit gedrรผckten Buttons (1 = gedrรผckt) + * + * Gibt die aktuelle Button-Kombination zurรผck. + * Mehrere Buttons kรถnnen gleichzeitig gedrรผckt sein (z.B. A+B). + * + * Beispiel: + * uint8_t state = buttons_get_state(); + * if (state & BTN_FLAG_A) { + * printf("A-Button ist gedrรผckt\n"); + * } + */ +uint8_t buttons_get_state(void); + +/** + * @brief Power-Button Status prรผfen + * @return true wenn Power-Schalter auf ON, false wenn auf OFF + * + * Diese Funktion liest den Hardware-Schalter auf GPIO 0. + * - ON (true): Schalter geschlossen, GPIO 0 = LOW (GND) + * - OFF (false): Schalter offen, GPIO 0 = HIGH (Pull-Up) + * + * Der Button-Task รผberwacht diesen Status automatisch und + * aktiviert Deep Sleep wenn der Schalter auf OFF steht. + */ +bool buttons_is_power_on(void); + +/** + * @brief Manuell in Deep Sleep wechseln + * + * Aktiviert ESP32 Deep Sleep Modus sofort. + * Das System wacht nur auf, wenn: + * - Power-Button (GPIO 0) auf ON geschaltet wird (LOW) + * + * Beim Aufwachen fรผhrt der ESP32 einen vollstรคndigen Neustart durch. + * Der Emulator-State geht verloren (auรŸer wenn vorher gespeichert). + * + * WICHTIG: Diese Funktion kehrt nicht zurรผck! + */ +void buttons_enter_sleep(void); + +#ifdef __cplusplus +} +#endif diff --git a/main/include/hardware_config.h b/main/include/hardware_config.h index 03105dc..21d5084 100644 --- a/main/include/hardware_config.h +++ b/main/include/hardware_config.h @@ -128,7 +128,13 @@ extern "C" { // ============================================ // POWER SWITCH (Hardware Toggle Switch) // ============================================ -#define POWER_SWITCH_PIN 0 // GPIO0 - Hardware power switch + +// WICHTIG: Power-Button Enable/Disable Flag +// Setze auf 0 wรคhrend der Entwicklung (kein Hardware-Schalter verbunden) +// Setze auf 1 wenn Hardware-Schalter angeschlossen ist +#define POWER_BUTTON_ENABLED 1 // 0=deaktiviert, 1=aktiviert + +#define POWER_SWITCH_PIN 18 // GPIO18 - Hardware power switch (RTC-fรคhig, frei) #define POWER_SWITCH_ON 0 // Switch closed = GND = Device ON #define POWER_SWITCH_OFF 1 // Switch open = Pull-up = Device goes to sleep @@ -136,12 +142,13 @@ extern "C" { // - When switch is CLOSED (pin reads LOW): Device runs normally // - When switch is OPENED (pin reads HIGH): Device enters deep-sleep // - When switch is CLOSED again: ESP32 wakes up from deep-sleep -// +// // Implementation: -// - Configure GPIO0 with internal pull-up +// - Configure GPIO18 with internal pull-up // - Monitor pin state in main loop // - On transition LOW->HIGH: Enter esp_deep_sleep_start() // - ESP32 will wake on GPIO LOW (RTC_GPIO wakeup) +// - POWER_BUTTON_ENABLED muss auf 1 gesetzt werden wenn Schalter verbunden ist #define POWER_SWITCH_CHECK_MS 100 // Check switch state every 100ms @@ -177,7 +184,7 @@ extern "C" { // ============================================ // STATUS LED - Optional // ============================================ -#define LED_STATUS_PIN 18 // Changed from 19 (USB conflict!) +#define LED_STATUS_PIN -1 // Deaktiviert (GPIO 18 wird fรผr Power-Switch verwendet) #define LED_ACTIVE_LEVEL 1 // ============================================ @@ -193,7 +200,7 @@ extern "C" { // ============================================ /* USED PINS - OPTIMIZED LAYOUT: -- GPIO 0: POWER_SWITCH (Hardware toggle switch) +- GPIO 0: FREI (war Power-Switch, aber Pin nicht erreichbar auf Board) - GPIO 1: LCD Backlight PWM - GPIO 3: Volume Potentiometer (ADC1_CH2) - GPIO 4: Brightness Potentiometer (ADC1_CH3) @@ -211,7 +218,7 @@ USED PINS - OPTIMIZED LAYOUT: - GPIO 15: Link Cable SCLK (freed from NFC!) - GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!) - GPIO 17: Link Cable SIN -- GPIO 18: Status LED (was 19, USB conflict fixed!) +- GPIO 18: POWER_SWITCH (Hardware toggle switch - RTC-fรคhig!) - GPIO 19: RESERVED (USB D-) - GPIO 20: RESERVED (USB D+) - GPIO 21: BTN_SELECT (native GPIO) @@ -231,13 +238,15 @@ USED PINS - OPTIMIZED LAYOUT: POWER SWITCH WIRING: โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ESP32 โ”‚ -โ”‚ GPIO 0 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Switch โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ GPIO 18 โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Switch โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (Pull-up) [Switch] GND -โ”‚ โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ””โ”€ When closed: GPIO0 = LOW (ON) - When open: GPIO0 = HIGH (SLEEP) +โ”‚ โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ + โ””โ”€ When closed: GPIO18 = LOW (ON) + When open: GPIO18 = HIGH (SLEEP) + +WICHTIG: POWER_BUTTON_ENABLED in diesem File auf 1 setzen wenn Schalter verbunden! */ diff --git a/main/main.c b/main/main.c index 5d09dfc..bcbcb35 100644 --- a/main/main.c +++ b/main/main.c @@ -1,322 +1,545 @@ /** * @file main.c - * @brief ESP32-S3 GameBoy - FIXED Audio Frequency Calculation! + * @brief ESP32-S3 GameBoy Emulator - Hauptprogramm mit Audio, Display und Emulation + * + * Dieses Programm implementiert einen vollstรคndigen GameBoy Emulator auf dem ESP32-S3. + * Es verwendet: + * - Peanut-GB Emulator Core fรผr die GameBoy-Emulation + * - ST7789 Display (320x240, 80 MHz SPI) fรผr die Ausgabe + * - I2S Audio (MAX98357A, 32768 Hz) fรผr Sound + * - PSRAM Double-Buffering fรผr flรผssige Darstellung + * - FreeRTOS Dual-Core: Core 0 fรผr Display, Core 1 fรผr Emulation */ #include #include #include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/semphr.h" -#include "esp_system.h" -#include "esp_log.h" -#include "nvs_flash.h" -#include "esp_heap_caps.h" -#include "esp_vfs_fat.h" -#include "sdmmc_cmd.h" -#include "driver/sdmmc_host.h" -#include "driver/sdspi_host.h" -#include "driver/i2s.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" -#include "st7789.h" +#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 Constants +// APU (Audio Processing Unit) Konstanten // ============================================ -#define SAMPLE_RATE 32768 -#define SAMPLES_PER_FRAME 546 // 32768 Hz / 60 FPS = 546 samples/frame -#define SAMPLES_PER_BUFFER 512 +// Der GameBoy hat eine 4-Kanal APU fรผr Sound-Synthese -// GameBoy CPU frequency +#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 -// Samples per GB CPU cycle -#define CYCLES_PER_SAMPLE (GB_CPU_FREQ / SAMPLE_RATE) // ~128 +// 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"; +static const char *TAG = "GB"; // Log-Tag fรผr ESP_LOG Ausgaben // ============================================ -// APU Registers (directly mapped) +// APU Register-Speicher (GameBoy Audio-Register) // ============================================ -static uint8_t apu_regs[48] = {0}; -static uint8_t wave_ram[16] = {0}; +// 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 control -static bool master_enable = false; -static uint8_t master_vol_left = 7; -static uint8_t master_vol_right = 7; -static uint8_t panning = 0xFF; +// 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) -// Channel 1 state +// ============================================ +// 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; - bool dac_on; // DAC enable bit (NR12 bit 3-7 != 0) - uint8_t duty; - uint8_t volume; - uint16_t freq_raw; - float phase; + 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}; -// Channel 2 state +// ============================================ +// Kanal 2: Square Wave (einfache Rechteck-Welle) +// ============================================ +// Identisch zu Kanal 1, aber ohne Frequency Sweep static struct { - bool active; - bool dac_on; // DAC enable bit (NR22 bit 3-7 != 0) - uint8_t duty; - uint8_t volume; - uint16_t freq_raw; - float phase; + 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}; -// Channel 3 state +// ============================================ +// 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; - bool dac_on; - uint8_t volume_shift; - uint16_t freq_raw; - float phase; + 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}; -// Channel 4 state +// ============================================ +// Kanal 4: Noise (Pseudo-Zufalls-Rauschen) +// ============================================ +// Dieser Kanal erzeugt weiรŸes Rauschen mittels LFSR-Generator +// (Linear Feedback Shift Register) static struct { - bool active; - uint8_t volume; - uint16_t lfsr; - uint8_t divisor; - uint8_t shift; - bool width_mode; - float timer; -} ch4 = {.lfsr = 0x7FFF}; + 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 -static bool audio_enabled = false; -static int16_t *audio_buffer = NULL; -static SemaphoreHandle_t apu_mutex = NULL; +// ============================================ +// 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 -// Debug -static int audio_write_count = 0; +// ============================================ +// System Shutdown Flag (fรผr Deep Sleep) +// ============================================ +static volatile bool system_shutdown = false; // Wenn true: Alle Tasks beenden -// Duty waveforms (8 steps each) - BIPOLAR for proper square waves! +// 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% duty cycle - { 1, -1, -1, -1, -1, -1, -1, 1}, // 25% duty cycle - { 1, -1, -1, -1, -1, 1, 1, 1}, // 50% duty cycle - {-1, 1, 1, 1, 1, 1, 1, -1}, // 75% duty cycle (inverted) + {-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; + 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++; - - // Wave RAM + + 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; } - - // Store raw register + + // === 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 - Master Control === + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ NR52 (0xFF26) - Master Audio Control โ•‘ + // โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• case 0xFF26: - master_enable = (val & 0x80) != 0; + 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); + memset(apu_regs, 0, 0x17); // Register 0xFF10-0xFF26 lรถschen } break; - - // === Channel 1 - Square with Sweep === - case 0xFF11: // NR11 - Duty & Length - ch1.duty = (val >> 6) & 3; + + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ 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; - case 0xFF12: // NR12 - Volume Envelope - ch1.volume = (val >> 4) & 0x0F; - ch1.dac_on = (val & 0xF8) != 0; // DAC enable check - if (!ch1.dac_on) ch1.active = false; + // 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; - - case 0xFF13: // NR13 - Freq Low - ch1.freq_raw = (ch1.freq_raw & 0x700) | val; + + // 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; - - case 0xFF14: // NR14 - Freq High + Trigger - ch1.freq_raw = (ch1.freq_raw & 0xFF) | ((val & 0x07) << 8); + + // 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; // Only activate if DAC is on - ch1.phase = 0; - ch1.volume = (apu_regs[0x02] >> 4) & 0x0F; + 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; - - // === Channel 2 - Square === - case 0xFF16: // NR21 - Duty & Length - ch2.duty = (val >> 6) & 3; + + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ 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; - case 0xFF17: // NR22 - Volume Envelope - ch2.volume = (val >> 4) & 0x0F; - ch2.dac_on = (val & 0xF8) != 0; // DAC enable check + // 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; - - case 0xFF18: // NR23 - Freq Low + + // NR23 (0xFF18): Frequency Low Byte + case 0xFF18: ch2.freq_raw = (ch2.freq_raw & 0x700) | val; break; - - case 0xFF19: // NR24 - Freq High + Trigger + + // 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; // Only activate if DAC is on + ch2.active = ch2.dac_on; ch2.phase = 0; - ch2.volume = (apu_regs[0x07] >> 4) & 0x0F; + ch2.volume = (apu_regs[0x07] >> 4) & 0x0F; // NR22 nachladen } break; - - // === Channel 3 - Wave === - case 0xFF1A: // NR30 - DAC Enable + + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ 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; + if (!ch3.dac_on) ch3.active = false; // DAC aus โ†’ Kanal stoppen break; - - case 0xFF1C: // NR32 - Volume - ch3.volume_shift = (val >> 5) & 3; + + // 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; - - case 0xFF1D: // NR33 - Freq Low + + // NR33 (0xFF1D): Frequency Low Byte + case 0xFF1D: ch3.freq_raw = (ch3.freq_raw & 0x700) | val; break; - - case 0xFF1E: // NR34 - Freq High + Trigger + + // 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; - - // === Channel 4 - Noise === - case 0xFF21: // NR42 - Volume Envelope + + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ 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; - - case 0xFF22: // NR43 - Polynomial Counter - ch4.shift = (val >> 4) & 0x0F; - ch4.width_mode = (val >> 3) & 1; - ch4.divisor = val & 0x07; + + // 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; - - case 0xFF23: // NR44 - Trigger + + // NR44 (0xFF23): Trigger + // Bit 7: Trigger (1=Kanal starten) + // Bit 6: Length Enable + case 0xFF23: if (val & 0x80) { ch4.active = true; - ch4.lfsr = 0x7FFF; - ch4.timer = 0; - ch4.volume = (apu_regs[0x11] >> 4) & 0x0F; + ch4.lfsr = 0x7FFF; // LFSR auf 0111111111111111 setzen + ch4.timer = 0; // Timer zurรผcksetzen + ch4.volume = (apu_regs[0x11] >> 4) & 0x0F; // NR42 nachladen } break; - - // === Master Volume & Panning === - case 0xFF24: // NR50 + + // โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— + // โ•‘ 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; - - case 0xFF25: // NR51 + + // 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 Generation +// 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) { - // GameBoy frequency formula: f = 131072 / (2048 - freq_raw) - if (freq_raw >= 2048) return 0; + 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) { - // Wave channel: f = 65536 / (2048 - freq_raw) - if (freq_raw >= 2048) return 0; + 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; - int32_t right = 0; - + 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; - buffer[i * 2 + 1] = 0; + buffer[i * 2] = 0; // Links = 0 + buffer[i * 2 + 1] = 0; // Rechts = 0 continue; } - - // === Channel 1 - Square Wave === + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; + 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; - if (panning & 0x10) left += sample; - if (panning & 0x01) right += sample; + // 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 } - // === Channel 2 - Square Wave === + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; @@ -327,160 +550,329 @@ static void generate_samples(int16_t *buffer, int num_samples) int step = (int)(ch2.phase * 8) & 7; int sample = duty_table[ch2.duty][step] * ch2.volume; - if (panning & 0x20) left += sample; - if (panning & 0x02) right += sample; + // Panning fรผr Kanal 2 + if (panning & 0x20) left += sample; // Bit 5: Ch2 โ†’ Left + if (panning & 0x02) right += sample; // Bit 1: Ch2 โ†’ Right } - - // === Channel 3 - Wave === + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; - int byte_idx = pos / 2; + + // 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; } - // Volume shift: 0=mute, 1=100%, 2=50%, 3=25% - FIXED! + // 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; // Center around 0 + sample = (sample_raw >> shift) - 8; // Zentrieren um 0 } - - if (panning & 0x40) left += sample; - if (panning & 0x04) right += sample; + + // 6. Panning fรผr Kanal 3 + if (panning & 0x40) left += sample; // Bit 6: Ch3 โ†’ Left + if (panning & 0x04) right += sample; // Bit 2: Ch3 โ†’ Right } - - // === Channel 4 - Noise === + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // Kanal 4: Noise (LFSR Pseudo-Random Generator) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if (ch4.active && ch4.volume > 0) { - // Noise frequency calculation + // 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 step + + // 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); - ch4.lfsr |= (bit << 6); + 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; - - if (panning & 0x80) left += sample; - if (panning & 0x08) right += sample; + + // 5. Panning fรผr Kanal 4 + if (panning & 0x80) left += sample; // Bit 7: Ch4 โ†’ Left + if (panning & 0x08) right += sample; // Bit 3: Ch4 โ†’ Right } - - // Apply master volume (0-7) - FIXED scaling to prevent clipping! - // Each channel outputs -15 to +15 max (volume 0-15) - // With 4 channels: max = 60, min = -60 - // Scale by 32 for good amplitude: ยฑ60 * 32 * 8 = ยฑ15360 (fits in 16-bit) + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; - // Clamp to 16-bit range (safety) + // 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; - - buffer[i * 2] = (int16_t)left; - buffer[i * 2 + 1] = (int16_t)right; + + // 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 Setup +// Peanut-GB Emulator Setup // ============================================ +// Peanut-GB ist ein Header-Only GameBoy Emulator +// Diese Defines aktivieren Sound und LCD Support -#define ENABLE_SOUND 1 -#define ENABLE_LCD 1 +#define ENABLE_SOUND 1 // Audio-System aktivieren +#define ENABLE_LCD 1 // Display-System aktivieren #include "peanut_gb.h" -// Undefine peanut_gb's LCD definitions (they're for GameBoy, not our display) +// 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" -#define DEFAULT_ROM "/sd/tetris.gb" +#define SD_MOUNT_POINT "/sd" // SD-Karte Mount-Pfad +#define DEFAULT_ROM "/sd/tetris.gb" // Standard-ROM zum Laden -static struct gb_s gb; -static uint8_t *rom_data = NULL; -static size_t rom_size = 0; -static uint16_t *line_buffer = NULL; -static uint16_t *frame_buffer = NULL; // Full screen buffer in PSRAM -static int current_line = 0; +// ============================================ +// 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 for parallel display/emulation -static uint16_t *render_buffer = NULL; // Buffer being rendered to -static uint16_t *display_buffer = NULL; // Buffer being displayed -static SemaphoreHandle_t frame_ready_sem = NULL; -static SemaphoreHandle_t frame_done_sem = NULL; +// ============================================ +// 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, 0x6BE4, 0x3760, 0x0C20 + 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) { - // Draw into RENDER buffer (double-buffering for parallel display) + // WICHTIG: In render_buffer schreiben (Double-Buffering!) #if GB_PIXEL_PERFECT_SCALING - // Dynamic scaling based on GB_SCALE_FACTOR - // Vertical: Scale GameBoy line (0-143) to output Y coordinate + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; - // Horizontal scaling: 160 GameBoy pixels -> GB_RENDER_WIDTH output pixels - // Dynamic pixel-width algorithm ensures every pixel is filled without gaps - int x_dst = 0; + // 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]; - uint16_t swapped = (c >> 8) | (c << 8); // RGB->BGR - // Calculate how wide this pixel should be at current scaling + // 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; - // Fill pixel_width positions with this color (no gaps!) + // 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; @@ -489,234 +881,468 @@ static void gb_lcd_draw_line(struct gb_s *gb, const uint8_t pixels[160], const u x_dst = next_x_dst; } - // Vertical scaling: duplicate lines as needed based on scaling factor + // 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); + GB_RENDER_WIDTH * 2); // 2 Bytes pro Pixel } #else - // Full screen stretch: 160x144 -> 320x240 - int y = (line * 5) / 3; + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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; - // Horizontal doubling: 160 -> 320, with BYTE SWAP for display + // 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 + uint16_t swapped = (c >> 8) | (c << 8); // RGBโ†’BGR int dst = y * GB_SCREEN_WIDTH + x * 2; - render_buffer[dst] = swapped; - render_buffer[dst + 1] = swapped; + render_buffer[dst] = swapped; // Pixel 1 + render_buffer[dst + 1] = swapped; // Pixel 2 (dupliziert) } - // Vertical scaling: duplicate line if needed + // 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 pixels * 2 bytes + 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, - .max_files = 5, - .allocation_unit_size = 16 * 1024 + .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 = 400; + 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; + 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!"); + + 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; } + 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, - .sample_rate = SAMPLE_RATE, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, - .dma_buf_count = 8, - .dma_buf_len = SAMPLES_PER_BUFFER, - .use_apll = false, - .tx_desc_auto_clear = true, + .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, - .ws_io_num = I2S_PIN_LRC, - .data_out_num = I2S_PIN_DIN, - .data_in_num = I2S_PIN_NO_CHANGE + .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", + 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) { - // Generate smaller buffers (512 samples) for lower latency + // 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 - // Clear screen to black once (for borders that don't change) + // Bildschirm einmalig schwarz fรผllen (fรผr statische Borders) st7789_fill_screen(0x0000); - // Allocate temp buffer for compacted GameBoy region (240x216 = 103KB) + // 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 (1) { - // Wait for frame to be ready - xSemaphoreTake(frame_ready_sem, portMAX_DELAY); + 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 - // Copy GameBoy region to compact buffer (remove gaps from black borders) + // 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); + GB_RENDER_WIDTH * 2); // 2 Bytes pro Pixel } - // Transfer only GameBoy content (240x216 = 33% less data than 320x240!) + + // 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 - // Full screen mode - draw entire buffer + // 2. Full-Screen Modus: Gesamten Buffer รผbertragen st7789_draw_buffer_preswapped(display_buffer, 0, 0, GB_SCREEN_WIDTH, GB_SCREEN_HEIGHT); #endif - // Signal frame display is done + // 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 (1) { + while (!system_shutdown) { TickType_t frame_start = xTaskGetTickCount(); - // Run emulation - renders into render_buffer + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 1. Emulation fรผr 1 Frame durchfรผhren + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // Peanut-GB rendert in render_buffer via gb_lcd_draw_line Callback gb_run_frame(&gb); - // Swap buffers + // 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; - // Signal display task that new frame is ready + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 3. Display-Task signalisieren: Frame fertig! + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• xSemaphoreGive(frame_ready_sem); - // Wait for display to finish with previous frame - xSemaphoreTake(frame_done_sem, portMAX_DELAY); + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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) { // Every second + 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); } - // GameBoy runs at 59.7275 FPS = 16.7424ms per frame - vTaskDelayUntil(&last, pdMS_TO_TICKS(17)); // 17ms โ‰ˆ 58.8 FPS + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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, "โ•‘ ESP32-S3 GameBoy - FIXED AUDIO! โ•‘"); ESP_LOGI(TAG, "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"); - - nvs_flash_init(); - st7789_init(); - st7789_set_backlight(80); - st7789_fill_screen(0x001F); - vTaskDelay(pdMS_TO_TICKS(500)); - // Check PSRAM availability + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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); - // Allocate TWO frame buffers for double-buffering - // Size depends on scaling mode - size_t buffer_size = GB_SCREEN_WIDTH * GB_SCREEN_HEIGHT * 2; // RGB565 = 2 bytes/pixel + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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); - // Try PSRAM first, fallback to regular RAM if needed - size_t min_psram_needed = buffer_size * 2 + 50000; // 2 buffers + margin + // 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); @@ -726,61 +1352,117 @@ void app_main(void) 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); + while(1) vTaskDelay(1000); // Endlos-Schleife (Fehler-Zustand) } - // Clear buffers to black (for letterboxing in pixel-perfect mode) + // 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); - // Create semaphores for buffer synchronization + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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); // Initially, display is "done" - + xSemaphoreGive(frame_done_sem); // Initial: Display ist "fertig" + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 6. SD-Karte initialisieren + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if (init_sdcard() != ESP_OK) { - st7789_fill_screen(0xF800); - while(1) vTaskDelay(1000); + // 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 } - - st7789_fill_screen(0x07E0); - vTaskDelay(pdMS_TO_TICKS(300)); - + // Kein visuelles Feedback bei Erfolg - Display bleibt dunkel + + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 7. GameBoy ROM von SD-Karte laden + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if (!load_rom(DEFAULT_ROM)) { - st7789_fill_screen(0xF800); - ESP_LOGE(TAG, "ROM load failed!"); + // 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) { - st7789_fill_screen(0xF800); + // 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; - // Run audio on Core 1, emulator on Core 0 for better performance + // 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, "โœ“ TETRIS with FIXED AUDIO! ๐ŸŽฎ๐Ÿ”Š"); + 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); - // Start display task on Core 0 (parallel to emulation!) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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); - // Start emulation task on Core 1 (with audio for cache locality) + // 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); - // Keep app_main running (don't exit) + // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• + // 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)); }