427 lines
15 KiB
C
427 lines
15 KiB
C
/**
|
|
* @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();
|
|
}
|