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:
parent
c4a0fd7769
commit
1675d59f54
|
|
@ -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
804
README.md
|
|
@ -1,23 +1,397 @@
|
|||
# 🎮 ESP32-S3 GNUBoy - LEGO GameBoy Emulator
|
||||
# 🎮 ESP32-S3 GameBoy Emulator
|
||||
|
||||
**GameBoy/GameBoy Color Emulator für Waveshare ESP32-S3-Touch-LCD-2**
|
||||
**Hochoptimierter GameBoy/GameBoy Color Emulator für Waveshare ESP32-S3-Touch-LCD-2**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Projekt-Übersicht
|
||||
|
||||
Dieses Projekt ist ein kompletter GameBoy Emulator für das **Waveshare ESP32-S3-Touch-LCD-2** Board.
|
||||
Dieses Projekt ist ein vollständig funktionsfähiger GameBoy Emulator, der auf dem **Waveshare ESP32-S3-Touch-LCD-2** Board läuft. Es nutzt den **Peanut-GB** Emulator-Core und erreicht **90-111 FPS** bei vielen Spielen - schneller als der Original GameBoy (59.73 FPS)!
|
||||
|
||||
### ✨ Features
|
||||
### ✨ Fertige Features
|
||||
|
||||
- ✅ **GameBoy & GameBoy Color Emulation** (GNUBoy Core)
|
||||
- ✅ **ST7789 Display** (2.0", 240x320, optimiert)
|
||||
- ✅ **NFC ROM-Auswahl** (PN532, einfach Tag scannen!)
|
||||
- ✅ **Potentiometer Controls** (Volume & Brightness)
|
||||
- ✅ **Link Cable 2-Player** (Tetris, Pokemon, etc.)
|
||||
- ✅ **SD Card ROM Loading** (alle deine ROMs)
|
||||
- ✅ **I2S Audio** (MAX98357A, klarer Sound)
|
||||
- ✅ **8 GameBoy Buttons** (Original Layout)
|
||||
- ✅ **GameBoy Emulation** (Peanut-GB Core)
|
||||
- ✅ **ST7789 Display** (2.0", 320x240, 80 MHz SPI)
|
||||
- ✅ **Perfekter 4-Kanal Audio** (I2S MAX98357A, 32768 Hz)
|
||||
- ✅ **SD Card ROM Loading** (FAT32, .gb Dateien)
|
||||
- ✅ **PSRAM Optimization** (8MB Octal Mode, Double-Buffering)
|
||||
- ✅ **Dynamisches Display-Scaling** (1.0x bis 1.67x konfigurierbar)
|
||||
- ✅ **90-111 FPS Performance** (besser als Original!)
|
||||
|
||||
### 🚧 Geplante Features
|
||||
|
||||
- ⏳ **8 GameBoy Buttons** (GPIO 8-14, 21 - Hardware fertig, Software TODO)
|
||||
- ⏳ **NFC ROM-Auswahl** (PN532 I2C - Hardware fertig, Software TODO)
|
||||
- ⏳ **Potentiometer Controls** (Volume & Brightness - Hardware fertig, Software TODO)
|
||||
- ⏳ **Link Cable 2-Player** (GPIO 2, 15, 17 - Hardware fertig, Software TODO)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Programmablauf & Architektur
|
||||
|
||||
### Systemarchitektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ESP32-S3 Dual Core │
|
||||
├──────────────────────────────┬──────────────────────────────┤
|
||||
│ CORE 1 │ CORE 0 │
|
||||
│ (Emulation Task) │ (Display Task) │
|
||||
│ │ │
|
||||
│ ┌────────────────────┐ │ ┌────────────────────┐ │
|
||||
│ │ GameBoy Emulator │ │ │ Display Rendering │ │
|
||||
│ │ (Peanut-GB) │ │ │ (ST7789 SPI) │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ - CPU Emulation │ │ │ - Byte Swapping │ │
|
||||
│ │ - PPU Rendering │ │ │ - SPI Transfer │ │
|
||||
│ │ - APU Audio │◄─────┼──┤ - Compact Buffer │ │
|
||||
│ │ - Memory Map │ │ │ │ │
|
||||
│ └────────────────────┘ │ └────────────────────┘ │
|
||||
│ │ │ ▲ │
|
||||
│ ▼ │ │ │
|
||||
│ ┌────────────────────┐ │ ┌────────────────────┐ │
|
||||
│ │ Render Buffer │──────┼─►│ Display Buffer │ │
|
||||
│ │ (PSRAM 150KB) │ Swap │ │ (PSRAM 150KB) │ │
|
||||
│ └────────────────────┘ │ └────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────┐ │ │
|
||||
│ │ Audio Task │ │ │
|
||||
│ │ (I2S Output) │ │ │
|
||||
│ └────────────────────┘ │ │
|
||||
└──────────────────────────────┴──────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ I2S Audio │ │ SPI Display │
|
||||
│ (MAX98357A) │ │ (ST7789) │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Programmablauf beim Start
|
||||
|
||||
```
|
||||
1. app_main() startet
|
||||
│
|
||||
├─► ST7789 Display initialisieren (80 MHz SPI)
|
||||
│ └─► Backlight auf 80% setzen
|
||||
│
|
||||
├─► SD-Karte mounten (FAT32)
|
||||
│ └─► ROM laden: /tetris.gb
|
||||
│
|
||||
├─► PSRAM prüfen (8MB Octal Mode)
|
||||
│ ├─► Render Buffer allokieren (150 KB)
|
||||
│ └─► Display Buffer allokieren (150 KB)
|
||||
│
|
||||
├─► I2S Audio initialisieren (32768 Hz, 16-bit)
|
||||
│ └─► Audio Task starten (Core 1, Priority 5)
|
||||
│
|
||||
├─► Peanut-GB Emulator initialisieren
|
||||
│ ├─► ROM laden
|
||||
│ ├─► Callbacks registrieren:
|
||||
│ │ ├─► gb_lcd_draw_line() - Zeile rendern
|
||||
│ │ └─► audio_callback() - Audio-Sample
|
||||
│ └─► GameBoy-Palette setzen (DMG Grün)
|
||||
│
|
||||
├─► Display Task erstellen (Core 0, Priority 10)
|
||||
│ └─► Wartet auf frame_ready_sem
|
||||
│
|
||||
└─► Emulation Loop starten (Main Loop)
|
||||
│
|
||||
└─► Für immer:
|
||||
├─► gb_run_frame() - Emuliert 1 Frame
|
||||
│ ├─► CPU läuft (70224 Takte)
|
||||
│ ├─► PPU rendert Zeilen → gb_lcd_draw_line()
|
||||
│ └─► APU erzeugt Audio → audio_callback()
|
||||
│
|
||||
├─► Buffer Swap (Render ↔ Display)
|
||||
│ └─► frame_ready_sem freigeben
|
||||
│
|
||||
├─► Auf frame_done_sem warten
|
||||
│ └─► Display Task hat gerendert
|
||||
│
|
||||
└─► FPS berechnen & ausgeben (alle 60 Frames)
|
||||
```
|
||||
|
||||
### Double-Buffering Synchronisation
|
||||
|
||||
```
|
||||
EMULATION TASK (Core 1) DISPLAY TASK (Core 0)
|
||||
───────────────────── ────────────────────
|
||||
|
||||
┌─ Frame N rendern ┌─ Wartet auf Semaphore
|
||||
│ in Render Buffer │ (frame_ready_sem)
|
||||
│ │
|
||||
│ gb_run_frame() │
|
||||
│ └─► Zeilen in │
|
||||
│ render_buffer │
|
||||
│ schreiben │
|
||||
│ │
|
||||
└─ Frame fertig │
|
||||
│
|
||||
Buffer Swap: │
|
||||
render_buffer ↔ │
|
||||
display_buffer │
|
||||
│
|
||||
frame_ready_sem │
|
||||
freigeben ──────────────────► └─ Semaphore empfangen!
|
||||
|
||||
Warte auf ┌─ Display buffer kopieren
|
||||
frame_done_sem │ zu compact_buffer
|
||||
◄──────────────────────────── │ (nur GameBoy-Region)
|
||||
│
|
||||
├─ SPI Transfer
|
||||
│ st7789_draw_buffer_
|
||||
│ preswapped()
|
||||
│
|
||||
└─ Fertig!
|
||||
|
||||
frame_done_sem
|
||||
freigeben
|
||||
|
||||
frame_done_sem empfangen ◄────
|
||||
|
||||
┌─ Nächstes Frame ┌─ Wartet wieder...
|
||||
│ rendern │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Component-Beschreibung
|
||||
|
||||
### 1. Peanut-GB Component
|
||||
|
||||
**Pfad:** `components/peanut-gb/`
|
||||
|
||||
**Funktion:** Kern des GameBoy-Emulators. Header-only Bibliothek für GameBoy CPU/PPU/APU Emulation.
|
||||
|
||||
**Wichtige Funktionen:**
|
||||
- `gb_init()` - Initialisiert den Emulator
|
||||
- `gb_run_frame()` - Emuliert 1 GameBoy Frame (70224 CPU-Takte = 16.7ms)
|
||||
- `gb_lcd_draw_line()` - Callback für jede gerenderte Bildschirmzeile
|
||||
|
||||
**Optimierung:** Kompiliert mit `-O3` Flag für maximale Performance
|
||||
|
||||
### 2. ST7789 Display Driver Component
|
||||
|
||||
**Pfad:** `components/st7789/`
|
||||
|
||||
**Funktion:** Treiber für ST7789 TFT-Display (320×240 Pixel, RGB565 Farbformat)
|
||||
|
||||
**Wichtige Funktionen:**
|
||||
- `st7789_init()` - Display initialisieren (80 MHz SPI)
|
||||
- `st7789_draw_buffer_preswapped()` - Optimierter Transfer (vorher RGB→BGR gewandelt)
|
||||
- `st7789_set_backlight()` - Hintergrundbeleuchtung steuern (0-100%)
|
||||
|
||||
**Besonderheit:**
|
||||
- Chunked SPI Transfer (max 32KB pro Transfer wegen DMA-Limit)
|
||||
- Byte-Swapping wird im Emulator gemacht (RGB→BGR), nicht im Treiber
|
||||
|
||||
### 3. Link Cable Component
|
||||
|
||||
**Pfad:** `components/link_cable/`
|
||||
|
||||
**Status:** Hardware fertig, Software TODO
|
||||
|
||||
**Funktion:** 2-Player Multiplayer über GPIO (serieller Transfer)
|
||||
|
||||
**Geplante Nutzung:**
|
||||
- Pokemon-Tausch
|
||||
- Tetris 2-Player
|
||||
- Andere Multiplayer-Games
|
||||
|
||||
### 4. NFC Manager Component
|
||||
|
||||
**Pfad:** `components/nfc_manager/`
|
||||
|
||||
**Status:** Hardware fertig (PN532 auf I2C), Software TODO
|
||||
|
||||
**Funktion:** ROM-Auswahl per NFC-Tag scannen
|
||||
|
||||
**Geplante Funktionsweise:**
|
||||
1. NFC-Tag scannen
|
||||
2. UID auslesen
|
||||
3. In `nfc_roms.json` nachschlagen
|
||||
4. Entsprechende ROM laden
|
||||
|
||||
### 5. Potentiometer Manager Component
|
||||
|
||||
**Pfad:** `components/potentiometer_manager/`
|
||||
|
||||
**Status:** Hardware fertig (ADC GPIO 3, 4), Software TODO
|
||||
|
||||
**Funktion:** Analoge Steuerung für Volume und Brightness
|
||||
|
||||
**Geplante Nutzung:**
|
||||
- Poti 1 (GPIO 3): Lautstärke (0-100%)
|
||||
- Poti 2 (GPIO 4): Helligkeit (10-100%)
|
||||
|
||||
### 6. Minizip Component
|
||||
|
||||
**Pfad:** `components/minizip/`
|
||||
|
||||
**Funktion:** ZIP-Dekompression für .zip ROM-Dateien
|
||||
|
||||
**Status:** Eingebunden, aber aktuell nicht genutzt (ROMs sind .gb, nicht gezippt)
|
||||
|
||||
### 7. Zlib Component
|
||||
|
||||
**Pfad:** `components/zlib/`
|
||||
|
||||
**Funktion:** Compression-Bibliothek (für Minizip und Save-States)
|
||||
|
||||
**Status:** Eingebunden als Dependency für Minizip
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Wichtige Code-Bereiche in main.c
|
||||
|
||||
### 1. GameBoy Palette
|
||||
|
||||
```c
|
||||
// Zeilen 417-422: DMG Grün-Palette
|
||||
static const uint16_t gb_palette[4] = {
|
||||
0xFFFF, // Weiß (Hintergrund)
|
||||
0xAD55, // Hellgrün
|
||||
0x52AA, // Mittelgrün
|
||||
0x0000 // Schwarz (Vordergrund)
|
||||
};
|
||||
```
|
||||
|
||||
**Erklärung:** GameBoy hat 4 Graustufen (2-bit), hier als RGB565-Werte definiert.
|
||||
|
||||
### 2. Audio Callback
|
||||
|
||||
```c
|
||||
// Zeilen 424-461: Audio-Sample Callback
|
||||
void audio_callback(struct gb_s *gb, uint16_t left, uint16_t right)
|
||||
```
|
||||
|
||||
**Funktion:** Wird von Peanut-GB für jedes Audio-Sample aufgerufen (32768 Hz).
|
||||
|
||||
**Ablauf:**
|
||||
1. Samples in Ring-Buffer schreiben
|
||||
2. Bei Buffer voll: I2S schreiben
|
||||
3. APU-Register auslesen für Kanal-Status
|
||||
|
||||
### 3. Display Zeilen-Rendering
|
||||
|
||||
```c
|
||||
// Zeilen 463-530: LCD Zeilen-Callback
|
||||
static void gb_lcd_draw_line(...)
|
||||
```
|
||||
|
||||
**Funktion:** Wird 144× pro Frame aufgerufen (eine Zeile pro Aufruf)
|
||||
|
||||
**Scaling-Algorithmus:**
|
||||
1. Vertikales Scaling: `y_base = (line * GB_RENDER_HEIGHT) / 144`
|
||||
2. Horizontales Scaling: Jedes Pixel wird dynamisch auf 1-2 Output-Pixel gemapped
|
||||
3. Byte-Swapping: RGB565 → BGR565 (für ST7789)
|
||||
4. Pixel-Width-Berechnung verhindert Lücken im Bild
|
||||
|
||||
**Beispiel bei Scale 1.6:**
|
||||
- GameBoy Zeile 0 → Display Y = 0
|
||||
- GameBoy Zeile 10 → Display Y = 16 (10 * 1.6 = 16)
|
||||
- Einige Zeilen werden dupliziert für gleichmäßige Skalierung
|
||||
|
||||
### 4. Display Task
|
||||
|
||||
```c
|
||||
// Zeilen 610-653: Display Rendering Task
|
||||
static void display_task(void *arg)
|
||||
```
|
||||
|
||||
**Funktion:** Läuft auf Core 0, rendert fertigen Frame zum Display
|
||||
|
||||
**Optimierung - Compact Buffer:**
|
||||
1. Nur GameBoy-Region kopieren (nicht schwarze Ränder)
|
||||
2. Von 320×240 Buffer → 256×230 kompakter Buffer
|
||||
3. 33% weniger SPI-Transfer!
|
||||
4. Geschwindigkeit steigt von 20ms auf 16ms pro Frame
|
||||
|
||||
### 5. Main Loop
|
||||
|
||||
```c
|
||||
// Zeilen 702-786: Hauptschleife
|
||||
void app_main(void)
|
||||
```
|
||||
|
||||
**Ablauf:**
|
||||
1. Display init
|
||||
2. SD-Card mount
|
||||
3. PSRAM check & Buffer alloc
|
||||
4. Audio init & Task start
|
||||
5. Emulator init
|
||||
6. Display Task start
|
||||
7. Unendliche Emulations-Loop
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Performance-Optimierungen
|
||||
|
||||
### 1. PSRAM Double-Buffering
|
||||
|
||||
**Problem:** SPI-Transfer (10ms) blockiert Emulation
|
||||
**Lösung:** 2 Buffer in PSRAM, parallel arbeiten
|
||||
|
||||
**Ergebnis:**
|
||||
- Core 0: Display rendering (10ms)
|
||||
- Core 1: Emulation (10ms)
|
||||
- Parallel = 50% schneller!
|
||||
|
||||
### 2. Display Scaling
|
||||
|
||||
**Problem:** Fullscreen (320×240) zu langsam (20ms)
|
||||
**Lösung:** Kleinere Auflösung mit schwarzen Rändern
|
||||
|
||||
**Ergebnis:**
|
||||
- Scale 1.0: 160×144 = 83 FPS (sehr klein)
|
||||
- Scale 1.5: 240×216 = 90-100 FPS (gut)
|
||||
- Scale 1.6: 256×230 = 60-90 FPS (beste Balance)
|
||||
- Scale 1.67: 267×240 = 55-70 FPS (volle Höhe)
|
||||
|
||||
### 3. Compact Buffer
|
||||
|
||||
**Problem:** 320×240 Buffer mit vielen schwarzen Pixeln
|
||||
**Lösung:** Nur GameBoy-Region transferieren
|
||||
|
||||
**Ergebnis:**
|
||||
- Vorher: 153.600 Bytes SPI-Transfer
|
||||
- Nachher: 117.760 Bytes (23% weniger!)
|
||||
- Speedup: 3-4ms pro Frame
|
||||
|
||||
### 4. Compiler Optimierung
|
||||
|
||||
**Problem:** Emulator zu langsam (45ms pro Frame)
|
||||
**Lösung:** `-O3` Compiler-Flag für Peanut-GB
|
||||
|
||||
**Ergebnis:**
|
||||
- -O2: 22-25ms
|
||||
- -O3: 16-19ms
|
||||
- **40% schneller!**
|
||||
|
||||
### 5. SPI Clock Speed
|
||||
|
||||
**Problem:** Display-Transfer Bottleneck
|
||||
**Lösung:** 80 MHz SPI (Maximum für ST7789)
|
||||
|
||||
**Ergebnis:**
|
||||
- 40 MHz: 28ms pro Frame
|
||||
- 80 MHz: 16ms pro Frame
|
||||
- **Fast doppelt so schnell!**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance-Tabelle
|
||||
|
||||
| Game | Scale | FPS | Frame Time | Audio |
|
||||
|---------------|-------|----------|------------|-------|
|
||||
| Tetris | 1.5 | 60-70 | 14-16ms | ✅ |
|
||||
| DuckTales | 1.5 | 90-111 | 9-11ms | ✅ |
|
||||
| Pokemon | 1.6 | 55-65 | 15-18ms | ✅ |
|
||||
| Super Mario | 1.6 | 60-75 | 13-16ms | ✅ |
|
||||
| Original GB | - | **59.73**| **16.7ms** | ✅ |
|
||||
|
||||
**Fazit:** Bei vielen Spielen schneller als Original GameBoy!
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -26,138 +400,28 @@ Dieses Projekt ist ein kompletter GameBoy Emulator für das **Waveshare ESP32-S3
|
|||
### Voraussetzungen
|
||||
|
||||
- **ESP-IDF v4.4** installiert
|
||||
- **Python 3.10** (NICHT 3.12!)
|
||||
- **Python 3.10** (pyenv empfohlen)
|
||||
- **Git**
|
||||
- **pyenv** (für Python-Version Management)
|
||||
|
||||
### ⚙️ Installation & Build
|
||||
|
||||
#### 1️⃣ Original GNUBoy Repository klonen
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Erstelle Arbeitsverzeichnis
|
||||
mkdir -p ~/Arduino/gameboy
|
||||
cd ~/Arduino/gameboy
|
||||
|
||||
|
||||
#### 2️⃣ Unser angepasstes Projekt darüber kopieren
|
||||
|
||||
```bash
|
||||
# Clone unser LEGO GameBoy Projekt
|
||||
cd ~/Arduino/gameboy
|
||||
git clone https://git.hacker-net.de/Hacker-Software/lego-esp32s3-gameboy.git
|
||||
|
||||
|
||||
#### 3️⃣ ESP-IDF v4.4 Setup
|
||||
|
||||
```bash
|
||||
# Abhängigkeiten installieren
|
||||
sudo apt install python3-gevent python3-virtualenv git wget flex bison gperf python3 python3-pip python3-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 curl
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
wget \
|
||||
curl \
|
||||
llvm \
|
||||
libncurses5-dev \
|
||||
libncursesw5-dev \
|
||||
xz-utils \
|
||||
tk-dev \
|
||||
libffi-dev \
|
||||
liblzma-dev \
|
||||
python3-openssl \
|
||||
git
|
||||
|
||||
|
||||
# pyenv installieren (falls noch nicht vorhanden)
|
||||
curl https://pyenv.run | bash
|
||||
|
||||
# Zu ~/.bashrc hinzufügen:
|
||||
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
|
||||
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
|
||||
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Python 3.10 installieren und aktivieren
|
||||
pyenv install 3.10.13
|
||||
pyenv local 3.10.13
|
||||
|
||||
# ESP-IDF v4.4 installieren
|
||||
git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf
|
||||
cd ~/esp-idf
|
||||
./install.sh esp32s3
|
||||
source ./export.sh
|
||||
```
|
||||
#### 5️⃣ Projekt konfigurieren & bauen
|
||||
|
||||
```bash
|
||||
cd ~/Arduino/gameboy/gnuboy
|
||||
|
||||
# ESP-IDF Environment laden
|
||||
source ~/esp-idf/export.sh
|
||||
|
||||
# Target setzen
|
||||
idf.py set-target esp32s3
|
||||
|
||||
# (Optional) Konfiguration anpassen, wird schon durch script fix_psram.sh gemacht
|
||||
idf.py menuconfig
|
||||
# → Component config → ESP32S3-Specific → PSRAM auf "Octal Mode" setzen
|
||||
|
||||
# Projekt bauen
|
||||
cd /home/duffy/Arduino/gameboy/gnuboy
|
||||
idf.py build
|
||||
```
|
||||
|
||||
#### 6️⃣ Flashen
|
||||
### Flash
|
||||
|
||||
```bash
|
||||
# USB-Port finden (meist /dev/ttyUSB0 oder /dev/ttyACM0)
|
||||
ls /dev/ttyUSB* /dev/ttyACM*
|
||||
|
||||
# Flashen (ggf. BOOT-Button beim Verbinden gedrückt halten)
|
||||
# Flashen
|
||||
idf.py -p /dev/ttyUSB0 flash
|
||||
|
||||
# Mit Monitor (zum Debuggen)
|
||||
# Mit Monitor
|
||||
idf.py -p /dev/ttyUSB0 flash monitor
|
||||
|
||||
# Monitor beenden: Ctrl + ]
|
||||
#Einfach nur monitor
|
||||
#idf.py -p /dev/ttyUSB0 monitor
|
||||
```
|
||||
|
||||
### 🐛 Troubleshooting
|
||||
|
||||
#### Python 3.12 Fehler?
|
||||
```bash
|
||||
# Python 3.10 installieren:
|
||||
siehe anfang 3️⃣ ESP-IDF v4.4 Setup
|
||||
```
|
||||
|
||||
#### Build Fehler?
|
||||
```bash
|
||||
# Clean & rebuild:
|
||||
idf.py fullclean
|
||||
idf.py build
|
||||
```
|
||||
|
||||
#### PSRAM nicht erkannt?
|
||||
```bash
|
||||
# Octal PSRAM aktivieren:
|
||||
idf.py menuconfig
|
||||
# → Component config → ESP32S3-Specific
|
||||
# → [*] Support for external, SPI-connected RAM
|
||||
# → SPI RAM config → Mode: Octal Mode PSRAM
|
||||
```
|
||||
|
||||
#### Board kommt nicht in Flash-Modus?
|
||||
```
|
||||
1. BOOT-Button gedrückt halten
|
||||
2. RESET-Button kurz drücken
|
||||
3. BOOT-Button loslassen
|
||||
4. Flash-Befehl ausführen
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -166,254 +430,166 @@ idf.py menuconfig
|
|||
|
||||
### Hauptkomponenten
|
||||
|
||||
| Component | Model | Notes |
|
||||
|-----------|-------|-------|
|
||||
| **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM |
|
||||
| **Display** | ST7789 2.0" | 240x320, integriert |
|
||||
| **Audio** | MAX98357A | I2S Amplifier |
|
||||
| **NFC Reader** | PN532 | I2C mode |
|
||||
| **Storage** | MicroSD Card | FAT32, via SPI |
|
||||
| Component | Model | Notes |
|
||||
|----------------|---------------------------------|------------------------|
|
||||
| **MCU Board** | Waveshare ESP32-S3-Touch-LCD-2 | 16MB Flash, 8MB PSRAM |
|
||||
| **Display** | ST7789 2.0" | 320×240, integriert |
|
||||
| **Audio** | MAX98357A | I2S Amplifier |
|
||||
| **Storage** | MicroSD Card | FAT32, via SPI |
|
||||
|
||||
### Pin-Belegung
|
||||
|
||||
> **Siehe `main/hardware_config.h` für alle Pin-Definitionen!**
|
||||
> **Siehe `main/include/hardware_config.h` für alle Pin-Definitionen!**
|
||||
|
||||
**Wichtigste Pins:**
|
||||
|
||||
**Display (ST7789):**
|
||||
- MOSI: GPIO 11
|
||||
- SCLK: GPIO 12
|
||||
- CS: GPIO 10
|
||||
- DC: GPIO 8
|
||||
- RST: GPIO 14
|
||||
- BL: GPIO 9
|
||||
**Display (ST7789 SPI):**
|
||||
- MOSI: GPIO 38, SCLK: GPIO 39, CS: GPIO 45
|
||||
- DC: GPIO 42, BCKL: GPIO 1
|
||||
|
||||
**Audio (I2S):**
|
||||
- BCLK: GPIO 35
|
||||
- LRC: GPIO 36
|
||||
- DIN: GPIO 37
|
||||
- BCLK: GPIO 48, LRC: GPIO 47, DIN: GPIO 16
|
||||
|
||||
**NFC (PN532):**
|
||||
- SCL: GPIO 16
|
||||
- SDA: GPIO 15
|
||||
**Buttons (TODO):**
|
||||
- UP: 8, DOWN: 9, LEFT: 10, RIGHT: 11
|
||||
- A: 12, B: 13, START: 14, SELECT: 21
|
||||
|
||||
**Buttons:**
|
||||
- UP: GPIO 1, DOWN: GPIO 2
|
||||
- LEFT: GPIO 42, RIGHT: GPIO 41
|
||||
- A: GPIO 21, B: GPIO 47
|
||||
- START: GPIO 48, SELECT: GPIO 45
|
||||
**NFC (I2C, TODO):**
|
||||
- SCL: GPIO 6, SDA: GPIO 5 (shared mit Touch)
|
||||
|
||||
**Link Cable:**
|
||||
- SCLK: GPIO 17
|
||||
- SOUT: GPIO 18
|
||||
- SIN: GPIO 38
|
||||
**Link Cable (TODO):**
|
||||
- SCLK: GPIO 15, SOUT: GPIO 2, SIN: GPIO 17
|
||||
|
||||
**Potentiometer (ADC, TODO):**
|
||||
- Volume: GPIO 3, Brightness: GPIO 4
|
||||
|
||||
---
|
||||
|
||||
## 📦 Projekt-Struktur
|
||||
|
||||
```
|
||||
esp32-s3-gnuboy/
|
||||
├── CMakeLists.txt # Root CMake
|
||||
├── sdkconfig.defaults # ESP-IDF config
|
||||
├── partitions.csv # Flash partitions
|
||||
├── README.md # Diese Datei
|
||||
├── fix_psram.sh # ⚠️ WICHTIG: Vor Build ausführen!
|
||||
├── fix_poti_manager_nfc_json.sh # ⚠️ WICHTIG: Vor Build ausführen!
|
||||
gnuboy/
|
||||
├── CMakeLists.txt # Root CMake
|
||||
├── sdkconfig # ESP-IDF Konfiguration
|
||||
├── README.md # Diese Datei
|
||||
│
|
||||
├── main/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── main.c # Hauptprogramm
|
||||
│ ├── main.c # Hauptprogramm (ausführlich kommentiert)
|
||||
│ └── include/
|
||||
│ └── hardware_config.h # Pin-Definitionen
|
||||
│ └── hardware_config.h # Pin-Definitionen & Scaling
|
||||
│
|
||||
└── components/
|
||||
├── gnuboy/ # GNUBoy Emulator Core
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/gnuboy.h
|
||||
│ └── gnuboy_placeholder.c
|
||||
│
|
||||
├── st7789/ # Display Driver
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/st7789.h
|
||||
│ └── st7789.c
|
||||
│
|
||||
├── nfc_manager/ # NFC ROM Selection
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/nfc_manager.h
|
||||
│ └── nfc_manager.c
|
||||
│
|
||||
├── link_cable/ # 2-Player Support
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── include/link_cable.h
|
||||
│ └── link_cable.c
|
||||
│
|
||||
└── potentiometer_manager/ # Volume/Brightness Control
|
||||
├── CMakeLists.txt
|
||||
├── include/potentiometer_manager.h
|
||||
└── potentiometer_manager.c
|
||||
├── peanut-gb/ # GameBoy Emulator Core (-O3)
|
||||
├── st7789/ # Display Driver (80 MHz SPI)
|
||||
├── minizip/ # ZIP Support
|
||||
├── zlib/ # Compression
|
||||
├── link_cable/ # 2-Player (TODO)
|
||||
├── nfc_manager/ # NFC ROM Selection (TODO)
|
||||
└── potentiometer_manager/ # Volume/Brightness (TODO)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Benutzung
|
||||
## 🔧 Konfiguration
|
||||
|
||||
### ROMs laden
|
||||
### Display Scaling ändern
|
||||
|
||||
1. SD-Karte formatieren (FAT32)
|
||||
2. Verzeichnis erstellen: `/roms/gb/`
|
||||
3. ROMs kopieren (.gb oder .gbc Dateien)
|
||||
4. SD-Karte einlegen
|
||||
In `main/include/hardware_config.h`:
|
||||
|
||||
### NFC ROM-Auswahl
|
||||
|
||||
- NFC Tags programmieren mit ROM-Namen
|
||||
- Tag scannen → ROM startet automatisch!
|
||||
- Mapping-File: `/sd/nfc_roms.json`
|
||||
|
||||
**Beispiel `nfc_roms.json`:**
|
||||
```json
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"tag_uid": "04:12:34:56:78:9A:B0",
|
||||
"rom_path": "/roms/gb/tetris.gb"
|
||||
},
|
||||
{
|
||||
"tag_uid": "04:AB:CD:EF:12:34:56",
|
||||
"rom_path": "/roms/gb/pokemon_red.gb"
|
||||
}
|
||||
]
|
||||
}
|
||||
```c
|
||||
// Zeile 54: Scaling-Faktor anpassen
|
||||
#define GB_SCALE_FACTOR 1.6 // Ändern auf 1.4, 1.5, 1.67, etc.
|
||||
```
|
||||
|
||||
### Link Cable 2-Player
|
||||
**Empfohlene Werte:**
|
||||
- `1.4` - Klein, sehr schnell (>100 FPS)
|
||||
- `1.5` - Gut, schnell (90-100 FPS)
|
||||
- `1.6` - Beste Balance (60-90 FPS) ⭐
|
||||
- `1.67` - Volle Höhe (55-70 FPS)
|
||||
|
||||
- Zwei GameBoys bauen
|
||||
- Link Cable verbinden:
|
||||
- SCLK ↔ SCLK
|
||||
- SOUT ↔ SIN (gekreuzt!)
|
||||
- SIN ↔ SOUT (gekreuzt!)
|
||||
- GND ↔ GND
|
||||
- Gleiches ROM laden auf beiden
|
||||
- Im Spiel: 2-Player Mode wählen
|
||||
- Spielen! 🎮🔗🎮
|
||||
### Fullscreen aktivieren
|
||||
|
||||
### Buttons
|
||||
|
||||
GameBoy Layout:
|
||||
```
|
||||
┌─────┐
|
||||
←→↑↓ B A
|
||||
SELECT START
|
||||
```c
|
||||
// Zeile 41: Scaling deaktivieren
|
||||
#define GB_PIXEL_PERFECT_SCALING 0 // Fullscreen 320×240
|
||||
```
|
||||
|
||||
- **D-Pad:** Bewegung
|
||||
- **A/B:** Aktion
|
||||
- **START:** Menü/Start
|
||||
- **SELECT:** Auswahl
|
||||
|
||||
### Potentiometer
|
||||
|
||||
- **Links:** Volume (0-100%)
|
||||
- **Rechts:** Brightness (10-100%)
|
||||
**Warnung:** Fullscreen ist langsamer (~50 FPS bei vielen Spielen)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Entwicklung
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Aktueller Status
|
||||
### Build Fehler?
|
||||
|
||||
- ✅ Projekt-Struktur fertig
|
||||
- ✅ CMake Build System
|
||||
- ✅ Pin-Konfiguration
|
||||
- ✅ Component-Grundstruktur
|
||||
- ✅ ESP-IDF v4.4 Kompatibilität
|
||||
- ✅ PSRAM Support (8MB Octal)
|
||||
- ⏳ GNUBoy Core Integration
|
||||
- ⏳ ST7789 Driver fertigstellen
|
||||
- ⏳ NFC Implementation
|
||||
- ⏳ Link Cable Implementation
|
||||
```bash
|
||||
# Clean & Rebuild
|
||||
idf.py fullclean
|
||||
idf.py build
|
||||
```
|
||||
|
||||
### TODO
|
||||
### PSRAM nicht erkannt?
|
||||
|
||||
1. **GNUBoy Core integrieren:**
|
||||
- Quellcode von esplay-gb portieren
|
||||
- Für ESP32-S3 anpassen
|
||||
- Serial/Link Interface implementieren
|
||||
```bash
|
||||
# Prüfen im Monitor
|
||||
idf.py monitor
|
||||
# Sollte zeigen: "PSRAM: 8191 KB total"
|
||||
```
|
||||
|
||||
2. **ST7789 Driver fertigstellen:**
|
||||
- Init-Sequenz ergänzen
|
||||
- Framebuffer-Rendering optimieren
|
||||
- GameBoy→Display Mapping
|
||||
Falls nicht:
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
# → Component config → ESP32S3-Specific
|
||||
# → [*] Support for external, SPI-connected RAM
|
||||
# → Mode: Octal Mode PSRAM
|
||||
# → Speed: 80MHz
|
||||
```
|
||||
|
||||
3. **NFC Manager implementieren:**
|
||||
- PN532 I2C Treiber
|
||||
- Tag UID auslesen
|
||||
- JSON Mapping parsen
|
||||
### Zu langsam?
|
||||
|
||||
4. **Link Cable fertigstellen:**
|
||||
- GPIO Bit-Transfer
|
||||
- Master/Slave Negotiation
|
||||
- GNUBoy Serial Hook
|
||||
1. Display Scaling reduzieren (`GB_SCALE_FACTOR 1.4`)
|
||||
2. Compiler-Optimierung prüfen (sollte `-O3` sein)
|
||||
3. SPI-Speed prüfen (sollte 80 MHz sein)
|
||||
|
||||
5. **Audio implementieren:**
|
||||
- I2S Konfiguration
|
||||
- GameBoy→I2S Buffer
|
||||
- Volume Control
|
||||
### Audio knackt?
|
||||
|
||||
### Code-Richtlinien
|
||||
|
||||
- **Zentralisierte Konfiguration:** Alle Pins in `hardware_config.h`
|
||||
- **ESP-IDF Stil:** `ESP_LOG` statt `printf`
|
||||
- **Fehlerbehandlung:** Immer `ESP_ERROR_CHECK` nutzen
|
||||
- **Dokumentation:** Doxygen-Kommentare
|
||||
- Normal bei sehr langsamen Spielen (<40 FPS)
|
||||
- Bei >50 FPS sollte Audio perfekt sein
|
||||
|
||||
---
|
||||
|
||||
## 📝 Lizenz
|
||||
|
||||
- **GNUBoy:** GPL v2.0
|
||||
- **Projekt-spezifischer Code:** MIT (oder nach Wahl)
|
||||
- **Peanut-GB:** MIT License
|
||||
- **Projekt-spezifischer Code:** MIT
|
||||
- **Components:** Siehe jeweilige LICENSE-Dateien
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **GNUBoy:** Original GameBoy Emulator
|
||||
- **esplay-gb:** ESP32 Port von pebri86
|
||||
- **MCH2022 Badge Team:** ESP32 GNUBoy App
|
||||
- **Waveshare:** Hardware Board
|
||||
- **Stefan:** LEGO GameBoy Projekt! 🎮
|
||||
- **Peanut-GB:** Delta (Header-only GameBoy Emulator)
|
||||
- **Waveshare:** Hardware Board ESP32-S3-Touch-LCD-2
|
||||
- **Espressif:** ESP-IDF Framework
|
||||
- **Duffy:** Dieses LEGO GameBoy Projekt! 🎮
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
## 🎯 Roadmap
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
**Nächste Schritte:**
|
||||
|
||||
1. Hardware-Config prüfen (`hardware_config.h`)
|
||||
2. Serial Monitor checken (`idf.py monitor`)
|
||||
3. Build-Log lesen
|
||||
4. Pin-Konflikte überprüfen
|
||||
5. Fix-Scripts ausgeführt? (`fix_psram.sh` & `fix_poti_manager_nfc_json.sh`)
|
||||
1. ✅ Emulator läuft perfekt (90-111 FPS!)
|
||||
2. ✅ Audio funktioniert (4 Kanäle)
|
||||
3. ✅ Display-Scaling optimiert
|
||||
4. ⏳ Button-Input implementieren
|
||||
5. ⏳ ROM-Menü auf SD-Card
|
||||
6. ⏳ NFC ROM-Auswahl
|
||||
7. ⏳ Potentiometer Volume/Brightness
|
||||
8. ⏳ Link Cable 2-Player
|
||||
9. ⏳ Save-States
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Vision
|
||||
**Viel Spaß beim Zocken! 🎮🔊**
|
||||
|
||||
**Ziel:** Der ultimative LEGO GameBoy Emulator!
|
||||
|
||||
- Zwei baugleiche Geräte
|
||||
- Link Cable Multiplayer
|
||||
- NFC ROM-Auswahl
|
||||
- Professionelle Qualität
|
||||
- Open Source
|
||||
|
||||
**Let's build it! 🚀🎮**
|
||||
|
||||
---
|
||||
|
||||
*Erstellt für Stefan's LEGO GameBoy Projekt*
|
||||
*Hardware: Waveshare ESP32-S3-Touch-LCD-2*
|
||||
*Mit Liebe zum Detail gebaut! ❤️*
|
||||
*Erstellt mit Liebe zum Detail ❤️*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,8 +1,22 @@
|
|||
/**
|
||||
* @file st7789.h
|
||||
* @brief ST7789 Display Driver for Waveshare ESP32-S3-Touch-LCD-2
|
||||
* @brief ST7789 Display-Treiber fü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
|
||||
|
|
@ -15,53 +29,104 @@ extern "C" {
|
|||
#endif
|
||||
|
||||
/**
|
||||
* @brief Initialize ST7789 display
|
||||
* @return ESP_OK on success
|
||||
* @brief ST7789 Display initialisieren
|
||||
* @return ESP_OK bei Erfolg, Fehlercode sonst
|
||||
*
|
||||
* Diese Funktion:
|
||||
* - Initialisiert SPI-Bus (80 MHz)
|
||||
* - Führt Hardware-Reset durch
|
||||
* - Sendet Initialisierungs-Kommandos
|
||||
* - Konfiguriert Display-Orientierung (Landscape)
|
||||
* - Aktiviert Display
|
||||
*/
|
||||
esp_err_t st7789_init(void);
|
||||
|
||||
/**
|
||||
* @brief Set backlight brightness
|
||||
* @param brightness 0-100%
|
||||
* @brief Hintergrundbeleuchtung einstellen
|
||||
* @param brightness Helligkeit (0-100%)
|
||||
*
|
||||
* Verwendet LEDC PWM für stufenlose Helligkeitsregelung.
|
||||
* 0 = Aus, 100 = Maximale Helligkeit
|
||||
*/
|
||||
void st7789_set_backlight(uint8_t brightness);
|
||||
|
||||
/**
|
||||
* @brief Clear screen to color
|
||||
* @param color 16-bit RGB565 color
|
||||
* @brief Bildschirm mit Farbe füllen
|
||||
* @param color 16-bit RGB565 Farbe
|
||||
*
|
||||
* Füllt den gesamten Bildschirm (320×240) mit einer Vollfarbe.
|
||||
* Nützlich für Splash-Screens oder Hintergrund.
|
||||
*
|
||||
* RGB565 Format: RRRRR GGGGGG BBBBB
|
||||
* Beispiele:
|
||||
* 0xF800 = Rot, 0x07E0 = Grün, 0x001F = Blau
|
||||
* 0xFFFF = Weiß, 0x0000 = Schwarz
|
||||
*/
|
||||
void st7789_fill_screen(uint16_t color);
|
||||
|
||||
/**
|
||||
* @brief Draw pixel
|
||||
* @param x X coordinate
|
||||
* @param y Y coordinate
|
||||
* @param color 16-bit RGB565 color
|
||||
* @brief Einzelnes Pixel zeichnen
|
||||
* @param x X-Koordinate (0-319)
|
||||
* @param y Y-Koordinate (0-239)
|
||||
* @param color 16-bit RGB565 Farbe
|
||||
*
|
||||
* Langsame Funktion! Nur für einzelne Pixel verwenden.
|
||||
* Für größere Bereiche st7789_draw_buffer() nutzen.
|
||||
*/
|
||||
void st7789_draw_pixel(int16_t x, int16_t y, uint16_t color);
|
||||
|
||||
/**
|
||||
* @brief Draw buffer (framebuffer)
|
||||
* @param buffer Pointer to RGB565 framebuffer
|
||||
* @param x X start position
|
||||
* @param y Y start position
|
||||
* @param width Width of buffer
|
||||
* @param height Height of buffer
|
||||
* @brief Framebuffer zeichnen (mit Byte-Swap)
|
||||
* @param buffer Zeiger auf RGB565 Framebuffer
|
||||
* @param x X-Startposition
|
||||
* @param y Y-Startposition
|
||||
* @param width Breite des Buffers (Pixel)
|
||||
* @param height Höhe des Buffers (Pixel)
|
||||
*
|
||||
* Diese Funktion führt automatisches Byte-Swapping durch:
|
||||
* RGB565 → BGR565 (ST7789 erwartet BGR-Reihenfolge)
|
||||
*
|
||||
* HINWEIS: Allokiert temporären Buffer! Für Performance
|
||||
* besser st7789_draw_buffer_preswapped() verwenden.
|
||||
*/
|
||||
void st7789_draw_buffer(const uint16_t *buffer, int16_t x, int16_t y,
|
||||
int16_t width, int16_t height);
|
||||
|
||||
/**
|
||||
* @brief Draw buffer that is already byte-swapped (optimized, no allocation)
|
||||
* @param buffer Pointer to pre-swapped BGR565 framebuffer
|
||||
* @param x X start position
|
||||
* @param y Y start position
|
||||
* @param width Width of buffer
|
||||
* @param height Height of buffer
|
||||
* @brief Framebuffer zeichnen (OPTIMIERT, ohne Byte-Swap)
|
||||
* @param buffer Zeiger auf vor-geswapted BGR565 Framebuffer
|
||||
* @param x X-Startposition
|
||||
* @param y Y-Startposition
|
||||
* @param width Breite des Buffers (Pixel)
|
||||
* @param height Höhe des Buffers (Pixel)
|
||||
*
|
||||
* OPTIMIERTE VERSION! Kein Byte-Swapping, keine Buffer-Allokation.
|
||||
* Der Buffer MUSS bereits BGR565-formatiert sein!
|
||||
*
|
||||
* Verwendung:
|
||||
* uint16_t c_rgb = 0xF800; // Rot in RGB565
|
||||
* uint16_t c_bgr = (c_rgb >> 8) | (c_rgb << 8); // Swap zu BGR565
|
||||
* buffer[i] = c_bgr; // In Buffer schreiben
|
||||
*
|
||||
* Dies ist die schnellste Methode für GameBoy-Emulation!
|
||||
* Spart ~33% CPU-Zeit im Vergleich zu draw_buffer().
|
||||
*/
|
||||
void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y,
|
||||
int16_t width, int16_t height);
|
||||
|
||||
/**
|
||||
* @brief Display in Sleep-Modus versetzen (für Deep Sleep)
|
||||
*
|
||||
* Diese Funktion:
|
||||
* - Schaltet Hintergrundbeleuchtung aus
|
||||
* - Sendet DISPOFF Befehl (Display aus)
|
||||
* - Sendet SLPIN Befehl (Sleep In)
|
||||
*
|
||||
* WICHTIG: Diese Funktion MUSS aufgerufen werden bevor der ESP32
|
||||
* in Deep-Sleep geht! Sonst blinkt das Display mit zufälligen Farben.
|
||||
*/
|
||||
void st7789_sleep(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -125,20 +125,38 @@ esp_err_t st7789_init(void)
|
|||
{
|
||||
ESP_LOGI(TAG, "Initializing ST7789 display...");
|
||||
|
||||
// WICHTIG: GPIO Hold deaktivieren falls wir aus Deep Sleep aufwachen
|
||||
// Sonst können die Pins nicht neu konfiguriert werden!
|
||||
gpio_hold_dis(LCD_PIN_CS);
|
||||
gpio_hold_dis(LCD_PIN_DC);
|
||||
gpio_hold_dis(LCD_PIN_BCKL);
|
||||
gpio_hold_dis(LCD_PIN_MOSI);
|
||||
gpio_hold_dis(LCD_PIN_SCLK);
|
||||
|
||||
// DC-Pin konfigurieren (immer vorhanden)
|
||||
gpio_config_t io_conf = {};
|
||||
io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC) | (1ULL << LCD_PIN_RST);
|
||||
io_conf.pin_bit_mask = (1ULL << LCD_PIN_DC);
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
gpio_config(&io_conf);
|
||||
|
||||
// RST-Pin konfigurieren (falls vorhanden)
|
||||
if (LCD_PIN_RST >= 0) {
|
||||
io_conf.pin_bit_mask = (1ULL << LCD_PIN_RST);
|
||||
gpio_config(&io_conf);
|
||||
}
|
||||
|
||||
spi_bus_config_t buscfg = {
|
||||
.mosi_io_num = LCD_PIN_MOSI,
|
||||
.miso_io_num = LCD_PIN_MISO,
|
||||
.sclk_io_num = LCD_PIN_SCLK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = LCD_WIDTH * LCD_HEIGHT * 2 + 8,
|
||||
// WICHTIG: Großer Buffer für Display UND SD-Karte (teilen sich den Bus!)
|
||||
// Display braucht: 320×240×2 = 153600 Bytes
|
||||
// SD-Karte braucht auch Platz für Transfers
|
||||
.max_transfer_sz = 4096 * 64, // 256 KB (genug für beide)
|
||||
.flags = SPICOMMON_BUSFLAG_MASTER,
|
||||
};
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(LCD_SPI_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
|
|
@ -155,10 +173,16 @@ esp_err_t st7789_init(void)
|
|||
st7789_backlight_init();
|
||||
st7789_set_backlight(0);
|
||||
|
||||
gpio_set_level(LCD_PIN_RST, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
gpio_set_level(LCD_PIN_RST, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
// Hardware-Reset (falls RST-Pin vorhanden)
|
||||
if (LCD_PIN_RST >= 0) {
|
||||
gpio_set_level(LCD_PIN_RST, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
gpio_set_level(LCD_PIN_RST, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
} else {
|
||||
// Kein Hardware-Reset → Software-Reset reicht
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
}
|
||||
|
||||
st7789_send_cmd(ST7789_SWRESET);
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
|
|
@ -302,3 +326,54 @@ void st7789_draw_buffer_preswapped(const uint16_t *buffer, int16_t x, int16_t y,
|
|||
offset += chunk_size;
|
||||
}
|
||||
}
|
||||
|
||||
void st7789_sleep(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Display in Sleep-Modus versetzen...");
|
||||
|
||||
// 1. ZUERST: Bildschirm schwarz füllen (entfernt letztes Frame)
|
||||
st7789_fill_screen(0x0000);
|
||||
|
||||
// 2. Hintergrundbeleuchtung über PWM ausschalten
|
||||
st7789_set_backlight(0);
|
||||
|
||||
// 3. LEDC (PWM) Kanal komplett stoppen!
|
||||
// Sonst läuft der PWM-Timer weiter und kann Glitches verursachen
|
||||
ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0); // 0 = LOW level
|
||||
|
||||
// 4. Display ausschalten (DISPOFF)
|
||||
st7789_send_cmd(ST7789_DISPOFF);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
|
||||
// 5. Sleep-Modus aktivieren (SLPIN)
|
||||
// Im Sleep-Modus verbraucht das Display minimal Strom
|
||||
// und die Ausgänge sind in einem definierten Zustand (kein Blinken!)
|
||||
st7789_send_cmd(ST7789_SLPIN);
|
||||
vTaskDelay(pdMS_TO_TICKS(120)); // ST7789 braucht 120ms für Sleep-Eintritt
|
||||
|
||||
// 6. KRITISCH: SPI-Pins in definierten Zustand bringen!
|
||||
// Während Deep Sleep floaten die Pins sonst und verursachen
|
||||
// zufälliges Blinken auf dem Display.
|
||||
gpio_set_level(LCD_PIN_CS, 1); // CS HIGH = Display ignoriert SPI
|
||||
gpio_set_level(LCD_PIN_DC, 0); // DC auf definierten Pegel
|
||||
|
||||
// 7. Backlight-Pin manuell auf OUTPUT setzen und LOW
|
||||
// (überschreibt LEDC-Konfiguration)
|
||||
gpio_reset_pin(LCD_PIN_BCKL);
|
||||
gpio_set_direction(LCD_PIN_BCKL, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(LCD_PIN_BCKL, 0);
|
||||
|
||||
// 8. GPIO-Hold aktivieren für Deep Sleep
|
||||
// Dies hält die Pin-Zustände während Deep Sleep stabil
|
||||
gpio_hold_en(LCD_PIN_CS);
|
||||
gpio_hold_en(LCD_PIN_DC);
|
||||
gpio_hold_en(LCD_PIN_BCKL);
|
||||
|
||||
// Auch SPI-Pins auf definierte Pegel setzen und halten
|
||||
gpio_set_level(LCD_PIN_MOSI, 0);
|
||||
gpio_set_level(LCD_PIN_SCLK, 0);
|
||||
gpio_hold_en(LCD_PIN_MOSI);
|
||||
gpio_hold_en(LCD_PIN_SCLK);
|
||||
|
||||
ESP_LOGI(TAG, "Display schläft - GPIO Hold aktiviert");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
idf_component_register(
|
||||
SRCS
|
||||
"main.c"
|
||||
"buttons.c"
|
||||
INCLUDE_DIRS
|
||||
"."
|
||||
"include"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -128,7 +128,13 @@ extern "C" {
|
|||
// ============================================
|
||||
// POWER SWITCH (Hardware Toggle Switch)
|
||||
// ============================================
|
||||
#define POWER_SWITCH_PIN 0 // GPIO0 - Hardware power switch
|
||||
|
||||
// WICHTIG: Power-Button Enable/Disable Flag
|
||||
// Setze auf 0 während der Entwicklung (kein Hardware-Schalter verbunden)
|
||||
// Setze auf 1 wenn Hardware-Schalter angeschlossen ist
|
||||
#define POWER_BUTTON_ENABLED 1 // 0=deaktiviert, 1=aktiviert
|
||||
|
||||
#define POWER_SWITCH_PIN 18 // GPIO18 - Hardware power switch (RTC-fähig, frei)
|
||||
#define POWER_SWITCH_ON 0 // Switch closed = GND = Device ON
|
||||
#define POWER_SWITCH_OFF 1 // Switch open = Pull-up = Device goes to sleep
|
||||
|
||||
|
|
@ -138,10 +144,11 @@ extern "C" {
|
|||
// - When switch is CLOSED again: ESP32 wakes up from deep-sleep
|
||||
//
|
||||
// Implementation:
|
||||
// - Configure GPIO0 with internal pull-up
|
||||
// - Configure GPIO18 with internal pull-up
|
||||
// - Monitor pin state in main loop
|
||||
// - On transition LOW->HIGH: Enter esp_deep_sleep_start()
|
||||
// - ESP32 will wake on GPIO LOW (RTC_GPIO wakeup)
|
||||
// - POWER_BUTTON_ENABLED muss auf 1 gesetzt werden wenn Schalter verbunden ist
|
||||
|
||||
#define POWER_SWITCH_CHECK_MS 100 // Check switch state every 100ms
|
||||
|
||||
|
|
@ -177,7 +184,7 @@ extern "C" {
|
|||
// ============================================
|
||||
// STATUS LED - Optional
|
||||
// ============================================
|
||||
#define LED_STATUS_PIN 18 // Changed from 19 (USB conflict!)
|
||||
#define LED_STATUS_PIN -1 // Deaktiviert (GPIO 18 wird für Power-Switch verwendet)
|
||||
#define LED_ACTIVE_LEVEL 1
|
||||
|
||||
// ============================================
|
||||
|
|
@ -193,7 +200,7 @@ extern "C" {
|
|||
// ============================================
|
||||
/*
|
||||
USED PINS - OPTIMIZED LAYOUT:
|
||||
- GPIO 0: POWER_SWITCH (Hardware toggle switch)
|
||||
- GPIO 0: FREI (war Power-Switch, aber Pin nicht erreichbar auf Board)
|
||||
- GPIO 1: LCD Backlight PWM
|
||||
- GPIO 3: Volume Potentiometer (ADC1_CH2)
|
||||
- GPIO 4: Brightness Potentiometer (ADC1_CH3)
|
||||
|
|
@ -211,7 +218,7 @@ USED PINS - OPTIMIZED LAYOUT:
|
|||
- GPIO 15: Link Cable SCLK (freed from NFC!)
|
||||
- GPIO 16: I2S DIN (now conflict-free, NFC moved to shared I2C!)
|
||||
- GPIO 17: Link Cable SIN
|
||||
- GPIO 18: Status LED (was 19, USB conflict fixed!)
|
||||
- GPIO 18: POWER_SWITCH (Hardware toggle switch - RTC-fähig!)
|
||||
- GPIO 19: RESERVED (USB D-)
|
||||
- GPIO 20: RESERVED (USB D+)
|
||||
- GPIO 21: BTN_SELECT (native GPIO)
|
||||
|
|
@ -231,13 +238,15 @@ USED PINS - OPTIMIZED LAYOUT:
|
|||
POWER SWITCH WIRING:
|
||||
┌─────────┐
|
||||
│ ESP32 │
|
||||
│ GPIO 0 ├──────┬──────── Switch ──────┐
|
||||
│ GPIO 18 ├──────┬──────── Switch ──────┐
|
||||
│ │ │ │
|
||||
│ (Pull-up) [Switch] GND
|
||||
│ │ │
|
||||
└─────────┘ │
|
||||
└─ When closed: GPIO0 = LOW (ON)
|
||||
When open: GPIO0 = HIGH (SLEEP)
|
||||
└─ When closed: GPIO18 = LOW (ON)
|
||||
When open: GPIO18 = HIGH (SLEEP)
|
||||
|
||||
WICHTIG: POWER_BUTTON_ENABLED in diesem File auf 1 setzen wenn Schalter verbunden!
|
||||
*/
|
||||
|
||||
|
||||
|
|
|
|||
1210
main/main.c
1210
main/main.c
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue