buttons, power button pin 18, deep sleep implementation, dokumentaion, and decription. optimzed sd card and display init, remove bloink screens at start, added color screens for error notifications

This commit is contained in:
duffyduck 2025-12-01 22:44:18 +01:00
parent c4a0fd7769
commit 1675d59f54
10 changed files with 3259 additions and 672 deletions

833
MAIN_C_ERKLAERUNG.md Normal file
View File

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

804
README.md
View File

@ -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 ## 📋 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) - ✅ **GameBoy Emulation** (Peanut-GB Core)
- ✅ **ST7789 Display** (2.0", 240x320, optimiert) - ✅ **ST7789 Display** (2.0", 320x240, 80 MHz SPI)
- ✅ **NFC ROM-Auswahl** (PN532, einfach Tag scannen!) - ✅ **Perfekter 4-Kanal Audio** (I2S MAX98357A, 32768 Hz)
- ✅ **Potentiometer Controls** (Volume & Brightness) - ✅ **SD Card ROM Loading** (FAT32, .gb Dateien)
- ✅ **Link Cable 2-Player** (Tetris, Pokemon, etc.) - ✅ **PSRAM Optimization** (8MB Octal Mode, Double-Buffering)
- ✅ **SD Card ROM Loading** (alle deine ROMs) - ✅ **Dynamisches Display-Scaling** (1.0x bis 1.67x konfigurierbar)
- ✅ **I2S Audio** (MAX98357A, klarer Sound) - ✅ **90-111 FPS Performance** (besser als Original!)
- ✅ **8 GameBoy Buttons** (Original Layout)
### 🚧 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 ### Voraussetzungen
- **ESP-IDF v4.4** installiert - **ESP-IDF v4.4** installiert
- **Python 3.10** (NICHT 3.12!) - **Python 3.10** (pyenv empfohlen)
- **Git** - **Git**
- **pyenv** (für Python-Version Management)
### ⚙️ Installation & Build ### Build
#### 1⃣ Original GNUBoy Repository klonen
```bash ```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 # ESP-IDF Environment laden
source ~/esp-idf/export.sh 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 # Projekt bauen
cd /home/duffy/Arduino/gameboy/gnuboy
idf.py build idf.py build
``` ```
#### 6 Flashen ### Flash
```bash ```bash
# USB-Port finden (meist /dev/ttyUSB0 oder /dev/ttyACM0) # Flashen
ls /dev/ttyUSB* /dev/ttyACM*
# Flashen (ggf. BOOT-Button beim Verbinden gedrückt halten)
idf.py -p /dev/ttyUSB0 flash idf.py -p /dev/ttyUSB0 flash
# Mit Monitor (zum Debuggen) # Mit Monitor
idf.py -p /dev/ttyUSB0 flash 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 ### Hauptkomponenten
| Component | Model | Notes | | Component | Model | Notes |
|-----------|-------|-------| |----------------|---------------------------------|------------------------|
| **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM | | **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM |
| **Display** | ST7789 2.0" | 240x320, integriert | | **Display** | ST7789 2.0" | 320×240, integriert |
| **Audio** | MAX98357A | I2S Amplifier | | **Audio** | MAX98357A | I2S Amplifier |
| **NFC Reader** | PN532 | I2C mode | | **Storage** | MicroSD Card | FAT32, via SPI |
| **Storage** | MicroSD Card | FAT32, via SPI |
### Pin-Belegung ### 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 SPI):**
- MOSI: GPIO 38, SCLK: GPIO 39, CS: GPIO 45
**Display (ST7789):** - DC: GPIO 42, BCKL: GPIO 1
- MOSI: GPIO 11
- SCLK: GPIO 12
- CS: GPIO 10
- DC: GPIO 8
- RST: GPIO 14
- BL: GPIO 9
**Audio (I2S):** **Audio (I2S):**
- BCLK: GPIO 35 - BCLK: GPIO 48, LRC: GPIO 47, DIN: GPIO 16
- LRC: GPIO 36
- DIN: GPIO 37
**NFC (PN532):** **Buttons (TODO):**
- SCL: GPIO 16 - UP: 8, DOWN: 9, LEFT: 10, RIGHT: 11
- SDA: GPIO 15 - A: 12, B: 13, START: 14, SELECT: 21
**Buttons:** **NFC (I2C, TODO):**
- UP: GPIO 1, DOWN: GPIO 2 - SCL: GPIO 6, SDA: GPIO 5 (shared mit Touch)
- LEFT: GPIO 42, RIGHT: GPIO 41
- A: GPIO 21, B: GPIO 47
- START: GPIO 48, SELECT: GPIO 45
**Link Cable:** **Link Cable (TODO):**
- SCLK: GPIO 17 - SCLK: GPIO 15, SOUT: GPIO 2, SIN: GPIO 17
- SOUT: GPIO 18
- SIN: GPIO 38 **Potentiometer (ADC, TODO):**
- Volume: GPIO 3, Brightness: GPIO 4
--- ---
## 📦 Projekt-Struktur ## 📦 Projekt-Struktur
``` ```
esp32-s3-gnuboy/ gnuboy/
├── CMakeLists.txt # Root CMake ├── CMakeLists.txt # Root CMake
├── sdkconfig.defaults # ESP-IDF config ├── sdkconfig # ESP-IDF Konfiguration
├── partitions.csv # Flash partitions ├── README.md # Diese Datei
├── README.md # Diese Datei
├── fix_psram.sh # ⚠️ WICHTIG: Vor Build ausführen!
├── fix_poti_manager_nfc_json.sh # ⚠️ WICHTIG: Vor Build ausführen!
├── main/ ├── main/
│ ├── CMakeLists.txt │ ├── CMakeLists.txt
│ ├── main.c # Hauptprogramm │ ├── main.c # Hauptprogramm (ausführlich kommentiert)
│ └── include/ │ └── include/
│ └── hardware_config.h # Pin-Definitionen │ └── hardware_config.h # Pin-Definitionen & Scaling
└── components/ └── components/
├── gnuboy/ # GNUBoy Emulator Core ├── peanut-gb/ # GameBoy Emulator Core (-O3)
│ ├── CMakeLists.txt ├── st7789/ # Display Driver (80 MHz SPI)
│ ├── include/gnuboy.h ├── minizip/ # ZIP Support
│ └── gnuboy_placeholder.c ├── zlib/ # Compression
├── link_cable/ # 2-Player (TODO)
├── st7789/ # Display Driver ├── nfc_manager/ # NFC ROM Selection (TODO)
│ ├── CMakeLists.txt └── potentiometer_manager/ # Volume/Brightness (TODO)
│ ├── 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
``` ```
--- ---
## 🎮 Benutzung ## 🔧 Konfiguration
### ROMs laden ### Display Scaling ändern
1. SD-Karte formatieren (FAT32) In `main/include/hardware_config.h`:
2. Verzeichnis erstellen: `/roms/gb/`
3. ROMs kopieren (.gb oder .gbc Dateien)
4. SD-Karte einlegen
### NFC ROM-Auswahl ```c
// Zeile 54: Scaling-Faktor anpassen
- NFC Tags programmieren mit ROM-Namen #define GB_SCALE_FACTOR 1.6 // Ändern auf 1.4, 1.5, 1.67, etc.
- 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"
}
]
}
``` ```
### 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 ### Fullscreen aktivieren
- 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! 🎮🔗🎮
### Buttons ```c
// Zeile 41: Scaling deaktivieren
GameBoy Layout: #define GB_PIXEL_PERFECT_SCALING 0 // Fullscreen 320×240
```
┌─────┐
←→↑↓ B A
SELECT START
``` ```
- **D-Pad:** Bewegung **Warnung:** Fullscreen ist langsamer (~50 FPS bei vielen Spielen)
- **A/B:** Aktion
- **START:** Menü/Start
- **SELECT:** Auswahl
### Potentiometer
- **Links:** Volume (0-100%)
- **Rechts:** Brightness (10-100%)
--- ---
## 🔧 Entwicklung ## 🐛 Troubleshooting
### Aktueller Status ### Build Fehler?
- ✅ Projekt-Struktur fertig ```bash
- ✅ CMake Build System # Clean & Rebuild
- ✅ Pin-Konfiguration idf.py fullclean
- ✅ Component-Grundstruktur idf.py build
- ✅ ESP-IDF v4.4 Kompatibilität ```
- ✅ PSRAM Support (8MB Octal)
- ⏳ GNUBoy Core Integration
- ⏳ ST7789 Driver fertigstellen
- ⏳ NFC Implementation
- ⏳ Link Cable Implementation
### TODO ### PSRAM nicht erkannt?
1. **GNUBoy Core integrieren:** ```bash
- Quellcode von esplay-gb portieren # Prüfen im Monitor
- Für ESP32-S3 anpassen idf.py monitor
- Serial/Link Interface implementieren # Sollte zeigen: "PSRAM: 8191 KB total"
```
2. **ST7789 Driver fertigstellen:** Falls nicht:
- Init-Sequenz ergänzen ```bash
- Framebuffer-Rendering optimieren idf.py menuconfig
- GameBoy→Display Mapping # → Component config → ESP32S3-Specific
# → [*] Support for external, SPI-connected RAM
# → Mode: Octal Mode PSRAM
# → Speed: 80MHz
```
3. **NFC Manager implementieren:** ### Zu langsam?
- PN532 I2C Treiber
- Tag UID auslesen
- JSON Mapping parsen
4. **Link Cable fertigstellen:** 1. Display Scaling reduzieren (`GB_SCALE_FACTOR 1.4`)
- GPIO Bit-Transfer 2. Compiler-Optimierung prüfen (sollte `-O3` sein)
- Master/Slave Negotiation 3. SPI-Speed prüfen (sollte 80 MHz sein)
- GNUBoy Serial Hook
5. **Audio implementieren:** ### Audio knackt?
- I2S Konfiguration
- GameBoy→I2S Buffer
- Volume Control
### Code-Richtlinien - Normal bei sehr langsamen Spielen (<40 FPS)
- Bei >50 FPS sollte Audio perfekt sein
- **Zentralisierte Konfiguration:** Alle Pins in `hardware_config.h`
- **ESP-IDF Stil:** `ESP_LOG` statt `printf`
- **Fehlerbehandlung:** Immer `ESP_ERROR_CHECK` nutzen
- **Dokumentation:** Doxygen-Kommentare
--- ---
## 📝 Lizenz ## 📝 Lizenz
- **GNUBoy:** GPL v2.0 - **Peanut-GB:** MIT License
- **Projekt-spezifischer Code:** MIT (oder nach Wahl) - **Projekt-spezifischer Code:** MIT
- **Components:** Siehe jeweilige LICENSE-Dateien
--- ---
## 🙏 Credits ## 🙏 Credits
- **GNUBoy:** Original GameBoy Emulator - **Peanut-GB:** Delta (Header-only GameBoy Emulator)
- **esplay-gb:** ESP32 Port von pebri86 - **Waveshare:** Hardware Board ESP32-S3-Touch-LCD-2
- **MCH2022 Badge Team:** ESP32 GNUBoy App - **Espressif:** ESP-IDF Framework
- **Waveshare:** Hardware Board - **Duffy:** Dieses LEGO GameBoy Projekt! 🎮
- **Stefan:** LEGO GameBoy Projekt! 🎮
--- ---
## 📞 Support ## 🎯 Roadmap
Bei Fragen oder Problemen: **Nächste Schritte:**
1. Hardware-Config prüfen (`hardware_config.h`) 1. ✅ Emulator läuft perfekt (90-111 FPS!)
2. Serial Monitor checken (`idf.py monitor`) 2. ✅ Audio funktioniert (4 Kanäle)
3. Build-Log lesen 3. ✅ Display-Scaling optimiert
4. Pin-Konflikte überprüfen 4. ⏳ Button-Input implementieren
5. Fix-Scripts ausgeführt? (`fix_psram.sh` & `fix_poti_manager_nfc_json.sh`) 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! *Erstellt mit Liebe zum Detail ❤️*
- 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! ❤️*

View File

@ -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

View File

@ -1,8 +1,22 @@
/** /**
* @file st7789.h * @file st7789.h
* @brief ST7789 Display Driver for Waveshare ESP32-S3-Touch-LCD-2 * @brief ST7789 Display-Treiber r Waveshare ESP32-S3-Touch-LCD-2
* *
* 2.0" TFT Display, 240x320 resolution * 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 #pragma once
@ -15,53 +29,104 @@ extern "C" {
#endif #endif
/** /**
* @brief Initialize ST7789 display * @brief ST7789 Display initialisieren
* @return ESP_OK on success * @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); esp_err_t st7789_init(void);
/** /**
* @brief Set backlight brightness * @brief Hintergrundbeleuchtung einstellen
* @param brightness 0-100% * @param brightness Helligkeit (0-100%)
*
* Verwendet LEDC PWM für stufenlose Helligkeitsregelung.
* 0 = Aus, 100 = Maximale Helligkeit
*/ */
void st7789_set_backlight(uint8_t brightness); void st7789_set_backlight(uint8_t brightness);
/** /**
* @brief Clear screen to color * @brief Bildschirm mit Farbe füllen
* @param color 16-bit RGB565 color * @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); void st7789_fill_screen(uint16_t color);
/** /**
* @brief Draw pixel * @brief Einzelnes Pixel zeichnen
* @param x X coordinate * @param x X-Koordinate (0-319)
* @param y Y coordinate * @param y Y-Koordinate (0-239)
* @param color 16-bit RGB565 color * @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); void st7789_draw_pixel(int16_t x, int16_t y, uint16_t color);
/** /**
* @brief Draw buffer (framebuffer) * @brief Framebuffer zeichnen (mit Byte-Swap)
* @param buffer Pointer to RGB565 framebuffer * @param buffer Zeiger auf RGB565 Framebuffer
* @param x X start position * @param x X-Startposition
* @param y Y start position * @param y Y-Startposition
* @param width Width of buffer * @param width Breite des Buffers (Pixel)
* @param height Height of buffer * @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, void st7789_draw_buffer(const uint16_t *buffer, int16_t x, int16_t y,
int16_t width, int16_t height); int16_t width, int16_t height);
/** /**
* @brief Draw buffer that is already byte-swapped (optimized, no allocation) * @brief Framebuffer zeichnen (OPTIMIERT, ohne Byte-Swap)
* @param buffer Pointer to pre-swapped BGR565 framebuffer * @param buffer Zeiger auf vor-geswapted BGR565 Framebuffer
* @param x X start position * @param x X-Startposition
* @param y Y start position * @param y Y-Startposition
* @param width Width of buffer * @param width Breite des Buffers (Pixel)
* @param height Height of buffer * @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, void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y,
int16_t width, int16_t height); 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 #ifdef __cplusplus
} }
#endif #endif

View File

@ -125,20 +125,38 @@ esp_err_t st7789_init(void)
{ {
ESP_LOGI(TAG, "Initializing ST7789 display..."); 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 = {}; 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.mode = GPIO_MODE_OUTPUT;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
gpio_config(&io_conf); 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 = { spi_bus_config_t buscfg = {
.mosi_io_num = LCD_PIN_MOSI, .mosi_io_num = LCD_PIN_MOSI,
.miso_io_num = LCD_PIN_MISO, .miso_io_num = LCD_PIN_MISO,
.sclk_io_num = LCD_PIN_SCLK, .sclk_io_num = LCD_PIN_SCLK,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_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, .flags = SPICOMMON_BUSFLAG_MASTER,
}; };
ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
@ -155,10 +173,16 @@ esp_err_t st7789_init(void)
st7789_backlight_init(); st7789_backlight_init();
st7789_set_backlight(0); st7789_set_backlight(0);
gpio_set_level(LCD_PIN_RST, 0); // Hardware-Reset (falls RST-Pin vorhanden)
vTaskDelay(pdMS_TO_TICKS(100)); if (LCD_PIN_RST >= 0) {
gpio_set_level(LCD_PIN_RST, 1); gpio_set_level(LCD_PIN_RST, 0);
vTaskDelay(pdMS_TO_TICKS(100)); 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); st7789_send_cmd(ST7789_SWRESET);
vTaskDelay(pdMS_TO_TICKS(150)); 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; 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");
}

View File

@ -1,6 +1,7 @@
idf_component_register( idf_component_register(
SRCS SRCS
"main.c" "main.c"
"buttons.c"
INCLUDE_DIRS INCLUDE_DIRS
"." "."
"include" "include"

426
main/buttons.c Normal file
View File

@ -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();
}

125
main/include/buttons.h Normal file
View File

@ -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 <stdint.h>
#include <stdbool.h>
#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

View File

@ -128,7 +128,13 @@ extern "C" {
// ============================================ // ============================================
// POWER SWITCH (Hardware Toggle Switch) // 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_ON 0 // Switch closed = GND = Device ON
#define POWER_SWITCH_OFF 1 // Switch open = Pull-up = Device goes to sleep #define POWER_SWITCH_OFF 1 // Switch open = Pull-up = Device goes to sleep
@ -138,10 +144,11 @@ extern "C" {
// - When switch is CLOSED again: ESP32 wakes up from deep-sleep // - When switch is CLOSED again: ESP32 wakes up from deep-sleep
// //
// Implementation: // Implementation:
// - Configure GPIO0 with internal pull-up // - Configure GPIO18 with internal pull-up
// - Monitor pin state in main loop // - Monitor pin state in main loop
// - On transition LOW->HIGH: Enter esp_deep_sleep_start() // - On transition LOW->HIGH: Enter esp_deep_sleep_start()
// - ESP32 will wake on GPIO LOW (RTC_GPIO wakeup) // - 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 #define POWER_SWITCH_CHECK_MS 100 // Check switch state every 100ms
@ -177,7 +184,7 @@ extern "C" {
// ============================================ // ============================================
// STATUS LED - Optional // 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 #define LED_ACTIVE_LEVEL 1
// ============================================ // ============================================
@ -193,7 +200,7 @@ extern "C" {
// ============================================ // ============================================
/* /*
USED PINS - OPTIMIZED LAYOUT: 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 1: LCD Backlight PWM
- GPIO 3: Volume Potentiometer (ADC1_CH2) - GPIO 3: Volume Potentiometer (ADC1_CH2)
- GPIO 4: Brightness Potentiometer (ADC1_CH3) - GPIO 4: Brightness Potentiometer (ADC1_CH3)
@ -211,7 +218,7 @@ USED PINS - OPTIMIZED LAYOUT:
- GPIO 15: Link Cable SCLK (freed from NFC!) - GPIO 15: Link Cable SCLK (freed from NFC!)
- GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!) - GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!)
- GPIO 17: Link Cable SIN - 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 19: RESERVED (USB D-)
- GPIO 20: RESERVED (USB D+) - GPIO 20: RESERVED (USB D+)
- GPIO 21: BTN_SELECT (native GPIO) - GPIO 21: BTN_SELECT (native GPIO)
@ -231,13 +238,15 @@ USED PINS - OPTIMIZED LAYOUT:
POWER SWITCH WIRING: POWER SWITCH WIRING:
ESP32 ESP32
GPIO 0 Switch GPIO 18 Switch
(Pull-up) [Switch] GND (Pull-up) [Switch] GND
When closed: GPIO0 = LOW (ON) When closed: GPIO18 = LOW (ON)
When open: GPIO0 = HIGH (SLEEP) When open: GPIO18 = HIGH (SLEEP)
WICHTIG: POWER_BUTTON_ENABLED in diesem File auf 1 setzen wenn Schalter verbunden!
*/ */

File diff suppressed because it is too large Load Diff