lego-esp32s3-gameboy/main/buttons.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();
}