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:
2025-12-01 22:44:18 +01:00
parent c4a0fd7769
commit 1675d59f54
10 changed files with 3259 additions and 672 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
idf_component_register(
SRCS
SRCS
"main.c"
INCLUDE_DIRS
"buttons.c"
INCLUDE_DIRS
"."
"include"
REQUIRES
+426
View File
@@ -0,0 +1,426 @@
/**
* @file buttons.c
* @brief Button-Handler Implementierung für ESP32-S3 GameBoy Emulator
*
* Implementiert vollständige Button-Verwaltung mit:
* - Software-Debouncing (50ms pro Button)
* - FreeRTOS Task für kontinuierliches Polling
* - Deep Sleep bei Power-Off mit RTC GPIO Wakeup
* - Direkte Integration mit Peanut-GB Emulator
*
* Technische Details:
* - Polling-Intervall: 10ms (100 Hz, ausreichend für Reaktionszeit)
* - Debounce-Algorithmus: Zustandsmaschine mit Zeitstempel
* - Thread-Safety: Atomic reads für Button-State
* - Power-Check: Alle 100ms auf Sleep-Bedingung prüfen
*/
#include "buttons.h"
#include "hardware_config.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "esp_sleep.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// ============================================
// LOGGING TAG
// ============================================
static const char *TAG = "BUTTONS";
// ============================================
// BUTTON PIN MAPPING
// ============================================
/**
* @brief Button-Pin-Zuordnung (8 GameBoy-Buttons)
*
* Index entspricht dem Bit-Index in button_flags_t:
* - buttons_pins[0] = BTN_A (GPIO 12)
* - buttons_pins[1] = BTN_B (GPIO 13)
* - etc.
*/
static const gpio_num_t buttons_pins[8] = {
BTN_A, // Bit 0
BTN_B, // Bit 1
BTN_SELECT, // Bit 2
BTN_START, // Bit 3
BTN_RIGHT, // Bit 4
BTN_LEFT, // Bit 5
BTN_UP, // Bit 6
BTN_DOWN, // Bit 7
};
// ============================================
// BUTTON STATE VARIABLEN
// ============================================
/**
* @brief Aktueller Button-State (8-Bit Bitmaske)
* Bit gesetzt = Button gedrückt
* Wird atomar gelesen/geschrieben
*/
static volatile uint8_t button_state = 0x00;
/**
* @brief Debouncing State für jeden Button
*
* Struktur pro Button:
* - last_state: Letzter stabiler Zustand (0=nicht gedrückt, 1=gedrückt)
* - raw_state: Aktueller GPIO-Rohwert
* - last_change_time: Zeitpunkt der letzten Änderung (ms)
*/
typedef struct {
uint8_t last_state; // 0 oder 1
uint8_t raw_state; // 0 oder 1
uint32_t last_change_time; // Millisekunden seit Boot
} button_debounce_t;
static button_debounce_t debounce_state[8] = {0}; // Ein Eintrag pro Button
// ============================================
// TASK HANDLE
// ============================================
static TaskHandle_t button_task_handle = NULL;
// ============================================
// EXTERNE FUNKTIONEN (aus main.c)
// ============================================
/**
* @brief Joypad-State an Peanut-GB Emulator übergeben
* @param state 8-Bit Bitmaske mit gedrückten Buttons
*
* Diese Funktion ist in main.c definiert und setzt den
* Joypad-State im Emulator: gb.direct.joypad = state
*/
extern void gb_set_joypad_state(uint8_t state);
// ============================================
// HILFSFUNKTIONEN
// ============================================
/**
* @brief Aktuelle Zeit in Millisekunden abrufen
* @return Zeit seit Boot in ms
*
* Verwendet FreeRTOS Tick-Counter mit configTICK_RATE_HZ.
* Standard: 1000 Ticks/Sekunde = 1 Tick = 1ms
*/
static inline uint32_t millis(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/**
* @brief Einzelnen Button mit Debouncing lesen
* @param btn_index Button-Index (0-7)
* @return 1 wenn Button gedrückt, 0 sonst
*
* Debouncing-Algorithmus:
* 1. GPIO-Rohwert lesen (invertiert, da Active-LOW)
* 2. Wenn Rohwert != letzter Rohwert → Zeitstempel aktualisieren
* 3. Wenn Rohwert stabil für DEBOUNCE_MS → State übernehmen
*
* Dies filtert mechanische Prellungen (Bouncing) heraus.
*/
static uint8_t read_button_debounced(uint8_t btn_index)
{
// GPIO lesen (0 = gedrückt wegen Active-LOW, 1 = nicht gedrückt)
// Invertieren, damit 1 = gedrückt
uint8_t raw = !gpio_get_level(buttons_pins[btn_index]);
button_debounce_t *db = &debounce_state[btn_index];
uint32_t now = millis();
// Hat sich der Rohwert geändert?
if (raw != db->raw_state) {
db->raw_state = raw;
db->last_change_time = now; // Zeitstempel aktualisieren
}
// Ist der Rohwert lange genug stabil?
if ((now - db->last_change_time) >= BTN_DEBOUNCE_MS) {
db->last_state = raw; // Stabilen Wert übernehmen
}
return db->last_state;
}
/**
* @brief Alle 8 GameBoy-Buttons lesen und State aktualisieren
*
* Diese Funktion:
* 1. Liest alle 8 Buttons mit Debouncing
* 2. Erstellt 8-Bit Bitmaske
* 3. Aktualisiert globalen button_state (atomar)
* 4. Überträgt State direkt an Peanut-GB Emulator
*/
static void update_button_state(void)
{
uint8_t new_state = 0x00;
// Alle 8 Buttons durchgehen
for (int i = 0; i < 8; i++) {
if (read_button_debounced(i)) {
new_state |= (1 << i); // Bit setzen wenn Button gedrückt
}
}
// State atomar aktualisieren
button_state = new_state;
// Direkt an Peanut-GB Emulator weiterleiten
// Die gb_set_joypad_state() Funktion in main.c setzt:
// gb.direct.joypad = state
// - Bit 0-7 für die 8 Buttons
// - 1 = gedrückt, 0 = nicht gedrückt
gb_set_joypad_state(new_state);
}
/**
* @brief Power-Button prüfen und ggf. Sleep aktivieren
* @return true wenn System weiterlaufen soll, false bei Sleep
*
* Diese Funktion liest GPIO 0 (Power-Schalter):
* - LOW (0) = Schalter ON → System läuft weiter
* - HIGH (1) = Schalter OFF → Deep Sleep aktivieren
*
* Im Deep Sleep:
* - CPU gestoppt, RAM aus, nur RTC läuft
* - Stromverbrauch: ~10 µA (vs. ~80 mA im Betrieb)
* - Wakeup nur durch GPIO 0 = LOW (Schalter wieder auf ON)
*/
static bool check_power_button(void)
{
#if POWER_BUTTON_ENABLED
// GPIO 18 lesen (0 = ON, 1 = OFF wegen Pull-Up)
int power_level = gpio_get_level(POWER_SWITCH_PIN);
if (power_level == POWER_SWITCH_OFF) {
// Schalter auf OFF → Deep Sleep
ESP_LOGI(TAG, "Power-Schalter auf OFF - aktiviere Deep Sleep...");
// 1. ZUERST: Alle Tasks sauber stoppen (Emulation, Display, Audio)
// Dies verhindert das grüne Blinken vom Display-Task!
extern void system_prepare_sleep(void);
system_prepare_sleep();
// 2. DANN: Display in Sleep-Modus versetzen
// Sendet DISPOFF + SLPIN und aktiviert GPIO-Hold
extern void st7789_sleep(void);
st7789_sleep();
// 3. Kurz warten, damit Log-Nachricht gesendet wird
vTaskDelay(pdMS_TO_TICKS(100));
// 4. Deep Sleep aktivieren
buttons_enter_sleep();
// HINWEIS: Diese Zeile wird nie erreicht, da esp_deep_sleep_start()
// das System sofort anhält. Bei Wakeup erfolgt Neustart.
return false;
}
#endif
return true; // System weiterlaufen lassen (oder Power-Button deaktiviert)
}
// ============================================
// BUTTON POLLING TASK
// ============================================
/**
* @brief FreeRTOS Task für kontinuierliches Button-Polling
* @param pvParameters Nicht verwendet (NULL)
*
* Dieser Task läuft in einer Endlosschleife und:
* 1. Liest alle 8 GameBoy-Buttons (mit Debouncing)
* 2. Aktualisiert Emulator-Joypad-State
* 3. Prüft alle 100ms den Power-Button
* 4. Schläft 10ms zwischen Iterationen (100 Hz Polling)
*
* Task-Konfiguration:
* - Core: 1 (gleicher Core wie Emulation)
* - Priorität: 5 (mittel - niedriger als Emulation/Display)
* - Stack: 2048 Bytes (ausreichend für GPIO-Operationen)
*/
static void button_task(void *pvParameters)
{
ESP_LOGI(TAG, "Button-Task gestartet (Core %d)", xPortGetCoreID());
uint32_t last_power_check = 0; // Zeitpunkt der letzten Power-Check
while (1) {
// ═══════════════════════════════════════════════════════
// 1. GameBoy-Buttons lesen und aktualisieren (jede Iteration)
// ═══════════════════════════════════════════════════════
update_button_state();
// ═══════════════════════════════════════════════════════
// 2. Power-Button prüfen (alle 100ms)
// ═══════════════════════════════════════════════════════
uint32_t now = millis();
if ((now - last_power_check) >= POWER_SWITCH_CHECK_MS) {
last_power_check = now;
if (!check_power_button()) {
// System geht in Sleep (wird nie erreicht, siehe Funktion)
break;
}
}
// ═══════════════════════════════════════════════════════
// 3. Kurz schlafen (10ms = 100 Hz Polling-Rate)
// ═══════════════════════════════════════════════════════
vTaskDelay(pdMS_TO_TICKS(10));
}
// Cleanup (wird normalerweise nie erreicht)
vTaskDelete(NULL);
}
// ============================================
// ÖFFENTLICHE API-FUNKTIONEN
// ============================================
esp_err_t buttons_init(void)
{
ESP_LOGI(TAG, "Initialisiere Button-System...");
// ═══════════════════════════════════════════════════════
// 1. GameBoy-Buttons (GPIO 8-14, 21) konfigurieren
// ═══════════════════════════════════════════════════════
gpio_config_t btn_config = {
.mode = GPIO_MODE_INPUT, // Eingang
.pull_up_en = GPIO_PULLUP_ENABLE, // Pull-Up aktivieren
.pull_down_en = GPIO_PULLDOWN_DISABLE, // Pull-Down aus
.intr_type = GPIO_INTR_DISABLE, // Keine Interrupts (Polling)
};
// Alle 8 Button-Pins konfigurieren
for (int i = 0; i < 8; i++) {
btn_config.pin_bit_mask = (1ULL << buttons_pins[i]);
esp_err_t ret = gpio_config(&btn_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Fehler beim Konfigurieren von GPIO %d: %s",
buttons_pins[i], esp_err_to_name(ret));
return ret;
}
}
ESP_LOGI(TAG, "8 GameBoy-Buttons konfiguriert (GPIO 8-14, 21)");
// ═══════════════════════════════════════════════════════
// 2. Power-Button (GPIO 18) konfigurieren - NUR WENN ENABLED!
// ═══════════════════════════════════════════════════════
#if POWER_BUTTON_ENABLED
gpio_config_t power_config = {
.pin_bit_mask = (1ULL << POWER_SWITCH_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE, // Pull-Up für Open-Drain-Schalter
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t ret = gpio_config(&power_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Fehler beim Konfigurieren von Power-GPIO %d: %s",
POWER_SWITCH_PIN, esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Power-Button konfiguriert (GPIO %d)", POWER_SWITCH_PIN);
// ═══════════════════════════════════════════════════════
// 3. Deep Sleep Wakeup konfigurieren
// ═══════════════════════════════════════════════════════
// GPIO 18 als Wakeup-Source konfigurieren
// Wakeup bei LOW (Schalter auf ON)
esp_sleep_enable_ext0_wakeup(POWER_SWITCH_PIN, 0); // 0 = LOW-Level Wakeup
ESP_LOGI(TAG, "Deep Sleep Wakeup konfiguriert (GPIO %d = LOW)", POWER_SWITCH_PIN);
#else
ESP_LOGW(TAG, "Power-Button DEAKTIVIERT (POWER_BUTTON_ENABLED=0 in hardware_config.h)");
ESP_LOGW(TAG, "Deep Sleep wird NICHT verwendet. Schalter kann später aktiviert werden.");
#endif
// ═══════════════════════════════════════════════════════
// 4. Debouncing-State initialisieren
// ═══════════════════════════════════════════════════════
for (int i = 0; i < 8; i++) {
debounce_state[i].last_state = 0;
debounce_state[i].raw_state = 0;
debounce_state[i].last_change_time = 0;
}
ESP_LOGI(TAG, "Button-System erfolgreich initialisiert");
return ESP_OK;
}
void buttons_start(void)
{
if (button_task_handle != NULL) {
ESP_LOGW(TAG, "Button-Task läuft bereits!");
return;
}
// Task erstellen auf Core 1 (gleicher Core wie Emulation)
BaseType_t ret = xTaskCreatePinnedToCore(
button_task, // Task-Funktion
"button_task", // Task-Name (für Debugging)
2048, // Stack-Größe (Bytes)
NULL, // Parameter (nicht verwendet)
5, // Priorität (5 = mittel)
&button_task_handle, // Task-Handle speichern
1 // Core 1 (Emulation-Core)
);
if (ret == pdPASS) {
ESP_LOGI(TAG, "Button-Task gestartet auf Core 1");
} else {
ESP_LOGE(TAG, "Fehler beim Starten des Button-Tasks!");
}
}
uint8_t buttons_get_state(void)
{
// Atomarer Read (8-Bit ist atomar auf ESP32)
return button_state;
}
bool buttons_is_power_on(void)
{
#if POWER_BUTTON_ENABLED
// GPIO 18 lesen: 0 = ON, 1 = OFF
// Invertieren für bool-Rückgabe: true = ON, false = OFF
return (gpio_get_level(POWER_SWITCH_PIN) == POWER_SWITCH_ON);
#else
// Power-Button deaktiviert → immer "ON" zurückgeben
return true;
#endif
}
void buttons_enter_sleep(void)
{
ESP_LOGI(TAG, "===========================================");
ESP_LOGI(TAG, " ESP32 GEHT IN DEEP SLEEP ");
ESP_LOGI(TAG, "===========================================");
ESP_LOGI(TAG, "Zum Aufwachen: Power-Schalter auf ON");
ESP_LOGI(TAG, "Stromverbrauch im Sleep: ~10 µA");
ESP_LOGI(TAG, "===========================================");
// Kurz warten, damit Log-Ausgabe gesendet wird
vTaskDelay(pdMS_TO_TICKS(200));
// Deep Sleep aktivieren
// HINWEIS: Diese Funktion kehrt NICHT zurück!
// Bei Wakeup erfolgt ein vollständiger ESP32-Neustart (Boot)
esp_deep_sleep_start();
}
+125
View File
@@ -0,0 +1,125 @@
/**
* @file buttons.h
* @brief Button-Handler für ESP32-S3 GameBoy Emulator
*
* Dieser Header definiert die Button-Verwaltung für:
* - 8× GameBoy-Buttons (D-Pad, A, B, Select, Start)
* - 1× Power-Button für Sleep/Wake-Modus
*
* Hardware-Details:
* - Alle Buttons sind Active-LOW (Pull-Up mit GND beim Drücken)
* - Debouncing: 50ms pro Button
* - Power-Button: GPIO 0 (Hardware-Schalter)
* - GameBoy-Buttons: GPIO 8-14, 21
*
* Integration:
* - Direkter Callback zu Peanut-GB Emulator
* - FreeRTOS Task für kontinuierliches Polling
* - ESP32 Deep Sleep bei Power-Off
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// ============================================
// BUTTON DEFINITIONEN
// ============================================
/**
* @brief GameBoy Button-Bits (kompatibel mit Peanut-GB Joypad)
*
* Diese Bit-Flags entsprechen dem GameBoy Joypad-Register (0xFF00)
* Bit gesetzt (1) = Button gedrückt
*/
typedef enum {
BTN_FLAG_A = (1 << 0), // A-Button (Aktions-Taste)
BTN_FLAG_B = (1 << 1), // B-Button (Aktions-Taste)
BTN_FLAG_SELECT = (1 << 2), // Select-Button (System-Taste)
BTN_FLAG_START = (1 << 3), // Start-Button (System-Taste)
BTN_FLAG_RIGHT = (1 << 4), // D-Pad Rechts
BTN_FLAG_LEFT = (1 << 5), // D-Pad Links
BTN_FLAG_UP = (1 << 6), // D-Pad Oben
BTN_FLAG_DOWN = (1 << 7), // D-Pad Unten
} button_flags_t;
// ============================================
// ÖFFENTLICHE FUNKTIONEN
// ============================================
/**
* @brief Button-System initialisieren
* @return ESP_OK bei Erfolg, Fehlercode sonst
*
* Diese Funktion:
* - Konfiguriert alle GPIO-Pins als Eingänge mit Pull-Up
* - Initialisiert Debouncing-State für alle Buttons
* - Erstellt Button-Polling-Task (Core 1, Priorität 5)
* - Konfiguriert GPIO 0 als RTC-Wakeup-Source für Deep Sleep
*
* WICHTIG: Muss vor buttons_start() aufgerufen werden!
*/
esp_err_t buttons_init(void);
/**
* @brief Button-Polling Task starten
*
* Startet den FreeRTOS Task, der kontinuierlich die Buttons abfragt.
* Der Task läuft auf Core 1 mit Priorität 5 und aktualisiert
* den Joypad-State alle 10ms.
*
* WICHTIG: buttons_init() muss vorher aufgerufen worden sein!
*/
void buttons_start(void);
/**
* @brief Aktuellen Button-State abrufen
* @return 8-Bit Bitmaske mit gedrückten Buttons (1 = gedrückt)
*
* Gibt die aktuelle Button-Kombination zurück.
* Mehrere Buttons können gleichzeitig gedrückt sein (z.B. A+B).
*
* Beispiel:
* uint8_t state = buttons_get_state();
* if (state & BTN_FLAG_A) {
* printf("A-Button ist gedrückt\n");
* }
*/
uint8_t buttons_get_state(void);
/**
* @brief Power-Button Status prüfen
* @return true wenn Power-Schalter auf ON, false wenn auf OFF
*
* Diese Funktion liest den Hardware-Schalter auf GPIO 0.
* - ON (true): Schalter geschlossen, GPIO 0 = LOW (GND)
* - OFF (false): Schalter offen, GPIO 0 = HIGH (Pull-Up)
*
* Der Button-Task überwacht diesen Status automatisch und
* aktiviert Deep Sleep wenn der Schalter auf OFF steht.
*/
bool buttons_is_power_on(void);
/**
* @brief Manuell in Deep Sleep wechseln
*
* Aktiviert ESP32 Deep Sleep Modus sofort.
* Das System wacht nur auf, wenn:
* - Power-Button (GPIO 0) auf ON geschaltet wird (LOW)
*
* Beim Aufwachen führt der ESP32 einen vollständigen Neustart durch.
* Der Emulator-State geht verloren (außer wenn vorher gespeichert).
*
* WICHTIG: Diese Funktion kehrt nicht zurück!
*/
void buttons_enter_sleep(void);
#ifdef __cplusplus
}
#endif
+20 -11
View File
@@ -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
@@ -136,12 +142,13 @@ extern "C" {
// - When switch is CLOSED (pin reads LOW): Device runs normally
// - When switch is OPENED (pin reads HIGH): Device enters deep-sleep
// - 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!
*/
+994 -312
View File
File diff suppressed because it is too large Load Diff