lego-esp32s3-gameboy/MAIN_C_ERKLAERUNG.md

834 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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!