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