834 lines
23 KiB
Markdown
834 lines
23 KiB
Markdown
# 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!
|