Lernprojekt "Smarte Lichterkettensteuerung mit ESP32"

RTC Bugfix

Da war ein kleiner Bug in meinem Code beim setzen der Alarmzeit:

// Use DS3231_ALARM1_MATCH_SECMINHOUR for Demo purposes (matches seconds)
ds3231_set_alarm(&dev, DS3231_ALARM_1, &alarm_time, DS3231_ALARM1_MATCH_SECMINHOUR, NULL, 0);

// Use DS3231_ALARM1_MATCH_MINHOUR for production (matches only minutes)
ds3231_set_alarm(&dev, DS3231_ALARM_1, &alarm_time, DS3231_ALARM1_MATCH_MINHOUR, NULL, 0);

Zudem war noch wichtig beim DS3231 Modul das Widerstandsarray zu versetzen da der oberste Widerstand im oberen Array den SQW Pin mit 4,7kOhm auf VCC verbindet:

Das Problem hierbei ist das im VBAT Modus mit VCC auf GND (via GPIO14) dieser Widerstand wie ein Pulldown wirkt und den Drain des SQW-Pin entlädt. So kann dieser kein ordentliches LOW mehr erzeugen und die Alarmauslösung kommt nicht an.

Nach umlöten funktioniert das nun einwandfrei.

I (441) LED_STRIP_CTRL: Alarmzeit: 01.01.2000 00:00:15
I (451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:00 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (1451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:01 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (2451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:02 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (3451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:03 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (4451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:04 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (5451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:05 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (6451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:06 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (7451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:07 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (8451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:08 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (9451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:09 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (10451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:10 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (11451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:11 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (12451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:12 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (13451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:13 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (14451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:14 | GPIO14: HIGH | Alarm-Flag: Gelöscht
I (15451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:15 | GPIO14: LOW | Alarm-Flag: GESETZT
I (16451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:16 | GPIO14: LOW | Alarm-Flag: GESETZT
I (17451) LED_STRIP_CTRL: ⏰ Zeit: 00:00:17 | GPIO14: LOW | Alarm-Flag: GESETZT
1 „Gefällt mir“

“Schlaf, Kindlein, schlaf …”

Kommen wir nochmal zum Powerdown des ESP. Hierzu muss der Header eingebunden werden:

#include "esp_sleep.h"

Wie oben schon ausgeführt ist der “Light-Sleep” das Ziel. In diesem Modus bleiben RAM und CPU-Zustand erhalten. Das Programm wird an der Zeile nach dem Light-Sleep Kommando

esp_light_sleep_start();

fortgesetzt. Da es unterschiedliche Gründen dafür geben kann warum der ESP aus dem Schlaf erwacht, kann man, wenn benötigt, diesen als erstes Kommando danach abfragen:

   esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();

   switch (wakeup_reason) {
        case ESP_SLEEP_WAKEUP_GPIO:
            ESP_LOGI(TAG, "Erweckt durch LOW auf GPIO");
            break;
        case ESP_SLEEP_WAKEUP_TIMER:
            ESP_LOGI(TAG, "Erweckt durch Timout Timer");
            break;
        default:
            ESP_LOGI(TAG, "Erweckt durch unbekannte Ursache: %d", wakeup_reason);
            break;
    }

Primär möchte ich den ESP durch ein LOW auf dem GPIO14 (ausgelöst von der RTC) oder dem GPIO13 (ausgelöst vom BOOT-Button) aufwachen lassen. Das sind beides Hardware-Wakeups. Diese muss man vor dem Sleep konfiguriert haben:

// GPIO14 Konfiguration
gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << DS3231_ALARM_INT_PIN),
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,  // Pull-up aktivieren
    .pull_down_en = GPIO_PULLDOWN_DISABLE,
    .intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);

...
    
// GPIO Wake-up für DS3231 konfigurieren (aufwachen bei LOW-Signal)
gpio_wakeup_enable(DS3231_ALARM_INT_PIN, GPIO_INTR_LOW_LEVEL);

// GPIO Wake-up für BOOT-Button konfigurieren (aufwachen bei LOW-Signal)
gpio_wakeup_enable(BUTTON_GPIO, GPIO_INTR_LOW_LEVEL);
    
// GPIO Wake-up Source aktivieren
esp_sleep_enable_gpio_wakeup();

Problematisch könnte es hierbei mir dem BOOT-Button werden (GPIO13), denn dieser wird von der Espressif/Button Komponente verwendet. Damit das sauber klappt muss man vor dem sleep den Button desregistrieren:

button_handle_t btn;

btn = init_button();

...

iot_button_delete(btn);
btn = NULL;
gpio_wakeup_enable(BUTTON_GPIO, GPIO_INTR_LOW_LEVEL);
esp_light_sleep_start();

btn = init_button(); // re-init Button

So ist sichergestellt das keine Konflikte auftreten.

Tja und das war es dann fast schon. Evtl. kommt beim Save-The-Batt-Day noch der ein oder andere Tweak rein…

1 „Gefällt mir“

Timer Scheduler

Ich brauche noch eine Datenstruktur und Funktionen um die Timer im RAM und im NVS des ESP oder EEPROM des DS-Boards ablegen und verarbeiten zu können. Jeder Timer besteht dabei aus einem Schaltzeitpunkt (Uhrzeit in Stunden und Minuten) sowie einem 8-Bit Helligkeitswert für die LED.

typedef struct {
  uint8_t hour;        // 0-23
  uint8_t minute;      // 0-59
  uint8_t brightness;  // LED Helligkeitswert
  bool active = false; // Timer aktiv/inaktiv
} timer_t;

Speichere ich über die State Machine einen Schaltzeitpunkt (Start/Ende) holt sich der Code die aktuelle Uhrzeit aus dem RTC und fügt die Stunden/Minuten zusammen mit dem aktuellen Helligkeitswert der LED als neuen Timer-Eintrag an eine Liste an. Hierzu definiere ich eine globale Liste (Array of Timers):

#define MAX_TIMERS 8

static timer_t timers[MAX_TIMERS];

In dieser Liste liegen also dann mal in chronologischer Reihenfolge alle Schaltzeitpunkte mit LED Helligkeiten. Beim programmieren ist ein AUS-Schaltzeitpunkt einfach einer mit dem Helligkeitswert “0%”. z.B. “08:00 Uhr => 100%”, “10:00 Uhr => 0%”, “16:00 Uhr => 100%”, “21:00 Uhr => 70%”, “00:30 Uhr => 0%”.

Ein Scheduler arbeitet dann über eine globale Index-Variable einen Schaltzeitpunkt nach dem anderen ab, liest die LED-Helligkeit und stellt sie ein, programmiert den nächsten Schaltzeitpunkt als Alarmzeit und legt den ESP solange schlafen.

Für den Fall das die Batterie gewechselt wird und der ESP neu startet lädt er das Timer-Array aus dem NVS oder EEPROM und sucht den Schaltzeitpunkt der von der RTC-Zeit aus gesehen in der Zukunft liegt und programmiert diesen als Alarmzeit. Dann nimmt er aus dem davorliegenden Schaltzeitpunkt die LED Helligkeit und stellt diese ein und legt den ESP wieder schlafen.

Folgende Funktionen brauche ich dafür:

// Neuen Timer hinzufügen
add_timer(timer_t timer)

// Nächsten Timer, relativ zur aktuellen Uhrzeit finden und zurückliefern
get_next_timer(tm *now, timer_t *timer)

// Vorherigen (aktuellen) Timer relativ zur aktuellen Uhrzeit finden und zurückliefern
get_previous_timer(tm *now, timert *timer)

// Löscht alle gespeicherten Timer
clear_all_timers()

// Alle Timer auflisten (Debugging)
print_timers()

Die Idee ist im Timer-Programmiermodus der State Machine beim erfolgreichen Programmieren, also nicht beim Abbruch, beide Zeitwerte als Timer hinzuzufügen, einmal mit der zuletzt eingestellten Helligkeit und einmal mit Helligkeit “0%”. Der Nachteil hierbei ist natürlich das wenn sich bis zum erreichen des Ausschaltzeitpunktes der Akku entlädt, der Startzeitpunkt nicht mehr vorhanden ist, da nur im RAM. Aber damit kann ich leben.

Ich habe jetzt auch nicht vorgesehen einen einzelnen Timer aus der Liste zu löschen, wenn man der Meinung ist das der Ablauf anders sein soll, muss man alle löschen und von neu beginnen. Ich wüsste auch aktuell nicht wie man das mit einer Taste und LED-Feedback einigermaßen brauchbar umsetzen könnte weil man ja die Zeiten der Timer nicht wirklich ausgeben kann, hierzu bräuchte es eine Console.

Daher kann es mir in der gewählten Logik des Schedulers, welcher die “get_next_timer()” Funktion nutzt die wiederum aus der Liste aller Timer einfach den nächsten raussucht, egal sein ob die Timer in der Liste chronologisch angeordnet sind.

Erste Tests waren sehr vielversprechend.

1 „Gefällt mir“

Go EEPROM, Go …

Das Problem

Tja, jetzt habe ich geglaubt das die Verwendung von fertigen Libs so easy wäre und bin von meinem eigentlichen Plan, ALLES Low-Level zu Fuß zu machen etwas abgekommen und schon bekomme ich die Quittung dafür!

Ich wollte einfach eine Lib einbauen die mir das Handling mit dem On-Board EEPROM (AT24C32) des DS-Boards ermöglicht. Konkret brauche ich zwei Funktionen um die Timerliste ins EEPROM und aus diesem heraus zu bekommen.

save_timers_to_eeprom()
load_timers_from_eeprom()

Dazu habe ich nach einer Lib für AT24 EEPROMs gesucht und die hier gefunden:

Beim Versuch diese in mein Projekt zu implementieren hat sich aber gezeigt das sowohl die DS3231 Lib von Espressif, als auch diese hier von nopnop2002 versuchen den I2C Bus für sich zu reklamieren, jeweils mit eigenen I2C Treiberimplementationen. Das knallte schon beim Compile!

Espressif selbst bietet leider keine Component für den AT24C32 welche den gleichen I2C-Treiber ( espressif/i2c_bus • v1.5.0 • ESP Component Registry ) verwendet wie die DS3231 Component, sondern nur diese für AT24C02..16 (AAARGH!):

Eine Lösung muss her…

Eine Möglichkeit wäre mit I2C komplett auf nopnop2002 Components zu schwenken, der auch eine Implementation für den DS3231. Aber irgendwie mag ich das nicht tun, ich will lieber bei Espressif bleiben.

Eine andere Option wäre gewesen den Code der AT24C02 Component von Espressif zu clonen, zu modifizieren (Support für AT24C32 hinzufügen) und die zu nutzen. Aber zum einen traue ich mir das noch nicht zu, zum anderen wäre ich dann bei Updates immer im Zugzwang.

So habe ich die Dritte Möglichkeit gewählt: Eine eigene Component zu bauen. Dabei kann ich dann gleich etwas lernen :slight_smile:

Eine eigene AT24C Component bauen

Im Prinzip ist das ganz einfach. ESP-IDF sucht beim Compile automatisch Projekt-Components unterhalb des /components Verzeichnisses. Hier lege ich folgende Struktur an:

components/
└── at24c/
    ├── at24c.c
    ├── include/
    │   └── at24c.h
    └── CMakeLists.txt

Auf den Inhalt von at24c.c und at24c.h gehe ich jetzt erstmal nicht ein, weil es für den Bau der Component nicht relevant ist. Wichtig ist aber die CMakeLists.txt die so aussehen muss:

# components/at24c/CMakeLists.txt
idf_component_register(
    SRCS "at24c.c"
    INCLUDE_DIRS "include"
    REQUIRES driver
)

Damit die Component ihre eigenen Headerfiles (in dem Fall “at24c.h”) findet, muss der Unterordner in der INCLUDE_DIRS Direktive angegeben sein. Die REQUIRES Direktive besagt das der Component-Code selbst von dieser Component abhängig ist.

Nun erkennt ESP-IDF beim Compile meine Component. Um diese aber zu nutzen muss man sie in der REQUIRES Direktiven des main/CMakeLists.txt hinzufügen. Hier mal meine gesamte Datei damit es klarer wird. Die eigene Component ist am Ende hinzugefügt:

idf_component_register(
    SRCS
        "main.c"
    INCLUDE_DIRS
        ""
   REQUIRES 
        esp_driver_ledc
        nvs_flash
        ds3231
        at24c
)

Nun kann man den Include-Header im main.c setzen:

#include "at24c.h" // AT24C EEPROM (I2C EEPROM)

Das ist schon alles!

1 „Gefällt mir“

Welcome to the jungle!

Nach stundenlangem grübeln, recoden, ausprobieren und scheitern bin ich jetzt auf was gestoßen: Es gibt bei Espressif-IDF zwei verschiedene I2C APIs.

Die esp-idf-lib/ds3231 nutzt in der Version 1.1.7 (aktuelle “latest”) aber leider noch die alte API i2cdev (siehe esp-idf-lib__ds3231/idf_component.yml). Ich hatte meine eigene AT24C Component aber für die neue API geschrieben weil ich dachte… naja, falsch gedacht.

Also habe ich das alles nochmal angepasst auf die alte API. Um auf einem Bus zwei Geräte bedienen zu können erstellt man pro Gerät einen Device-Handle (mutex). Über den steuert man dann später die einzelnen Bus-Member an, in meinem Fall der DS und das EEPROM.

#define I2C_MASTER_FREQ_HZ   100000  // 100kHz needed to drive DS3231 (fast) and AT24C (slow)
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_SCL_IO GPIO_NUM_11
#define I2C_MASTER_SDA_IO GPIO_NUM_12

#define DS3231_ADDR 0x68 // DS3231 I2C Adresse
#define AT24C_ADDR 0x57 // AT24C32 I2C Adresse ("offene" A0,A1,A2 sind HIGH)

static i2c_dev_t ds3231_dev;
static i2c_dev_t at24c_dev;

// I2C bus initialisieren
i2cdev_init();

// Device-Handle für DS3231 erstellen
memset(&ds3231_dev, 0, sizeof(i2c_dev_t));
i2c_dev_create_mutex(&ds3231_dev);
ds3231_dev.port = I2C_MASTER_NUM;
ds3231_dev.addr = DS3231_ADDR;
ds3231_dev.cfg.sda_io_num = I2C_MASTER_SDA_IO;
ds3231_dev.cfg.scl_io_num = I2C_MASTER_SCL_IO;
ds3231_dev.cfg.master.clk_speed = I2C_MASTER_FREQ_HZ;

// Device-Handle für AT24C32 erstellen
memset(&at24c_dev, 0, sizeof(i2c_dev_t));
i2c_dev_create_mutex(&at24c_dev);
at24c_dev.port = port;
at24c_dev.addr = dev_addr;
at24c_dev.cfg.sda_io_num = sda_gpio;
at24c_dev.cfg.scl_io_num = scl_gpio;
at24c_dev.cfg.master.clk_speed = I2C_MASTER_FREQ_HZ;

// aktuelle Zeit vom DS3231 holen
struct tm time;
ds3231_get_time(&ds3231_dev, &time);

// 16 Bytes von Adresse 0x100 aus dem AT24C32 lesen
uint16_t addr = 0x0100;
uint8_t size = 16;
char read_buffer[64] = {0};
at24c_read(&at24c_dev, addr, (uint8_t *)read_buffer, size);
1 „Gefällt mir“

Ich habe mir ne Funktion geschrieben mit der ich die LED auf/ablenden lassen kann (fade_on und fade_off). Darin mache ich sowas:

static void led_fade_on(uint32_t duration_ms, uint8_t target_brightness)
{
    if (target_brightness > led_brightness)
    {
        int wait_ms = duration_ms / (target_brightness - led_brightness);
        for (int i = 0; i <= target_brightness; i++)
        {
            led_set_brightness(i);
            vTaskDelay(pdMS_TO_TICKS(wait_ms));
        }
    }
}

// Beispiel
uint8_t led_brightness = 150; // aktuelle Helligkeit, 0 oder 15 oder 255 oder ...
led_fade_on(2000, 255);

Die Idee dahinter war der Funktion die Gesamtlaufzeit des Effektes (duration_ms) und die Zielhelligkeit (0-255) mitgeben zu können. Dabei möchte ich nicht immer nur von 0 nach 255 faden (also dunkel auf ganz hell) sondern auch auf eine gewünschte Zielhelligkeit. Das mache ich z.B. wenn ein Timer startet, dann geht die Lichterkette ich “klatsch” an, sondern fährt schön hoch und zwar bis zum im Timer hinterlegten Wert. Um logische Fehler zu vermeiden ist die Abfrage zur aktuellen Helligkeit drin. Es wird also nur hochgefahren wenn die Zielhelligkeit größer ist als die aktuelle Helligkeit.

Theoretisch könnte man auch aktuell 0 und 1 als Ziel haben, was zu einem seltsamen Effekt führen würde bei z.B. 2 Sekunden Laufzeit. Aber auch von 0 auf 20 zu fahren würde komisch aussehen. Daher muss man ggf. beim Aufruf die Effektzeit der Differenz anpassen, aber das nur am Rande.

In der Funktion wird schrittweise (i++) hochgefahren und zwischen den Schritten anteilig der Helligkeitsentfernung mit einem Delay gewartet. Bei großen Entfernungen wie von 0 auf 255 bei 2000 ms würde das ein Delay von ca. 7-8 ms bedeuten.

Hier habe ich nach einer Weile Debugging gemerkt das das mit vTaskDelay() nicht funktioniert, die LED "blitzte” nur statt zu faden. Da war also ein Bug in meinem Code.

Die verwendete Delay-Funktion kommt aus dem zugrunde liegenden FreeRTOS. Die Umrechnung von Millisekunden in RTOS-Ticks erledigt ein Makro:

/* Converts a time in milliseconds to a time in ticks.  This macro can be
 * overridden by a macro of the same name defined in FreeRTOSConfig.h in case the
 * definition here is not suitable for your application. */
#ifndef pdMS_TO_TICKS
    #define pdMS_TO_TICKS( xTimeInMs )    ( ( TickType_t ) ( ( ( TickType_t ) ( xTimeInMs ) * ( TickType_t ) configTICK_RATE_HZ ) / ( TickType_t ) 1000U ) )
#endif

Und ist offensichtlich abhängig von der configTICK_RATE_HZ, der RTOS-Taktrate. Diese ist typischerweise 100 Hz:

#define CONFIG_FREERTOS_HZ 100

Ein “Tick” sind also 10 ms. Das Makro sorgte also dafür das meine 7 ms auf 0 ms abgerundet wurden, ergo lief der Effekt viel zu schnell, so als wäre keine Pause drin. Bei einer Mindestpausenzeit von 10 ms würde das aber bedeutet das man für einen vollen Fade (0 auf 255 oder umgekehrt) mind. 2,5 Sekunden braucht, schneller ginge er nicht. In einer anderen Funktion (led_pulse) verwende ich aufeinanderfolgende Fade-Effekte um ein “pulsieren” der LEDs zu erreichen. Diesen Pulse möchte ich aber evtl. schneller laufen lassen.

Eine andere Delay-Art wie “esp_rom_delay_us()” würde aber den RTOS-Scheduler blockieren und evtl. den Watchdog auf den Plan rufen. Man sollte das also nur dann tun wenn die zu wartende Zeit sehr kurz ist. In meinem Fall habe ich daraus einen hybriden Ansatz gebaut:

static void led_fade_on(uint32_t duration_ms, uint8_t target_brightness)
{
    int wait_ms = duration_ms / ( target_brightness - led_brightness );
    for (int i = 0; i <= target_brightness; i++) {
        led_set_brightness(i);
        if (wait_ms >= 10) {
            vTaskDelay(pdMS_TO_TICKS(wait_ms)); // vTaskDelay only works >= 10 ms
        } else {
            esp_rom_delay_us(wait_ms * 1000);
        }
    }
}

Vielleicht hat ja noch jemand eine bessere Idee?

Bezüglich des Fade ist es auch so das es besser/komplexer ginge wenn man den visuellen Eindruck fürs menschliche Auge berücksichtigt. Das sieht Lichtstärke nämlich nicht linear und so scheint es als würde sich die Helligkeit anfangs schnell und dann immer langsamer ändern. Mit entsprechenden Lookup-Tabellen kann man gegen soetwas anstehen.

1 „Gefällt mir“

wäre es nicht sinnvoller mit fixem zeitschritt zu arbeiten und den LED-Wert als float oder double zu handhaben? dann hast du volle auflösung und kannst hinterher, nach der gamma-korrektur auf int runterrechnen

gamma-korrektur kannst du für uint16_t z.B. so machen (value*value)/65536

bestenfalls baust du dir einfach einen rampengenerator der zyklisch im mainloop oder einem task aufgerufen wird und die LED-Helligkeit entsprechend fährt, config und zustand (rate hoch und runter, zielwert, aktueller wert, flag ob fertig) kannst du ja in einem struct ablegen

1 „Gefällt mir“

Ist mir etwas zu abstrakt, kannst Du das an Codebeispielen konkretisieren/erklären was der Vorteil eines 16 Bit Speichers bei einem 8 Bit PWM wäre?

Verstanden habe ich die Anregung die Schrittweite beim Fade so auszurechnen das die dazwischenliegende Wartezeit immer >= 10 ms ist. Das versuche ich jetzt mal umzusetzen da ich mit dem blockierenden Delay kein gutes Gefühl habe.

Habe es jetzt so gelöst:

static void led_fade_on(uint32_t duration_ms, uint8_t target_brightness)
{
    // der fade kann auch von einer anderen grundhelligkeit als 0 starten, es gilt die led_brightness variable
    if (target_brightness <= led_brightness) {
        ESP_LOGI(TAG, "led_fade_on: target_brightness (%d) <= current brightness (%d), nothing to do", target_brightness, led_brightness);
        return;
    }
    int brightness_range = target_brightness - led_brightness;
    int max_steps = duration_ms / 10;  // Maximal so viele Schritte, dass jeder >= 10ms
    int steps = (brightness_range < max_steps) ? brightness_range : max_steps;
    if (steps == 0) steps = 1;  // Mindestens 1 Schritt
    
    int wait_ms = duration_ms / steps;
    float brightness_step = (float)brightness_range / steps;
    
    //ESP_LOGI(TAG, "led_fade_on: duration_ms=%d, target=%d, steps=%d, stepwdth=%f, wait_ms=%d", duration_ms, target_brightness, steps, brightness_step, wait_ms);
    
    for (int i = led_brightness; i < target_brightness; i += (int)brightness_step)
    {
        if (i > target_brightness) {
            i = target_brightness;
        }
        //ESP_LOGI(TAG, "  Setting brightness to %d", i);
        led_set_brightness(i);
        vTaskDelay(pdMS_TO_TICKS(wait_ms));
    }
    led_set_brightness(target_brightness);  // Final exakt auf Zielwert
}

Der Fade kann dann auch von einer anderen Grundhelligkeit als 0 gestartet werden um z.B. ein schöneres “pulse” zu erzeugen.

1 „Gefällt mir“

Interessant wäre auch wenn ich die Timer mittels Smartphone auslesen und programmieren könnte. Der H2 kann ja grundsätzlich nur Bluetooth Low Energy (BLE) 5.3, welches für Zigbee/Thread gedacht ist, also kein klassisches Bluetooth welches ich für eine Kopplung per Handy nutzen könnte. Selbst wenn, wäre noch die Frage wie bzw. womit man die Datenausgabe/Erfassung realisieren würde? Dazu bräuchte es sicher eine App, die ich nicht vor habe zu entwickeln.

1 „Gefällt mir“

Externe Timerprogrammierung

Die Timer nur per Button zu programmieren ist nur der halbe Spaß und kann auch aufwändig werden. Vor allem wenn man mehrere Lichterketten parallel schalten lassen möchte, da muss man dann schnell zwischen denen wechseln, das kenne ich von meinen jetzigen, nicht-smarten Boxen.

Daher überlege ich wie ich am einfachsten eine externe Programmierung hin bekomme.

Option “via Smartphone”

Interessant wäre auch wenn ich die Timer mittels Smartphone auslesen und programmieren könnte. Der H2 kann ja grundsätzlich nur Bluetooth Low Energy (BLE) 5.3, welches für Zigbee/Thread gedacht ist, also kein klassisches Bluetooth welches ich für eine Kopplung per Handy nutzen könnte. Selbst wenn, wäre noch die Frage wie bzw. womit man die Datenausgabe/Erfassung realisieren würde? Dazu bräuchte es sicher eine App, die ich nicht vor habe zu entwickeln.

Ich prüfe mal ob es Apps gibt die ich für sowas nutzen könnte und die in der Lage sind mit dem Handy per BLE in Kontakt mit meinem Device zu treten. Ansonsten wäre hier ja der ESP32-C6 evtl. die bessere Wahl, weil man damit ja eine Wifi-Verbindung herstellen könnte.

In jedem Fall möchte ich diesen Programmiermodus separat wählen weil er ja vergleichsweise viel Strom benötigt. Da schwebt mir sowas vor wie die Batterie trennen, den Button drücken und gedrückt halten und wieder einschalten. Oder eine bestimmte Button-Sequenz im OFF-Modus wie 5fach Klick oder 5 Sekunden lang drücken.

Option “via USB Port”

Auch eine Option wäre den ESP, welchen ich per Hardware-Design auf einen Stecksockel meiner Platine anbringe, abzuziehen und per USB am PC zu verbinden. Dann könnte man über ein Terminalprogramm eine Art Programmiermenü anzeigen.

Normalerweise sind auf den Mikrocontroller-Boards USB/UART Chips wie ein CH340 (CP210x VCP-Treiber) oder FTDI (praktisch immer Clones) die als Bridge zwischen dem µC-UART (TX/RX) und dem PC-Terminal dienen. Der ESP32-H2 braucht soetwas nicht, er hat eine native USB-Schnittstelle. Aber er emuliert darüber keinen UART, auch wenn der auf dem PC erzeugte COM-Port dies zuggeriert, sondern verwendet USB-CDC (Communications Device Class), welches ein ganz andere Übertragungsprotokoll verwendet. Daher funktionieren klassische Terminals wie PuTTY oder HTermin nicht, da diese eine UART-Kommunikation voraussetzen. Die Baudrate die man im VS Code angibt ist nur Dekoration, wie beim VCP ist es völlig egal was man da einstellt.

Für PuTTY sollte man es so konfigurieren:

Zum testen und entwickeln nutzt man natürlich die in VS Code vorhandene “Terminal” Funktion von ESP-IDF. Das dahinter liegende idf_monitor.py -p PORT … kommt wunderbar mit USB-CDC zurecht, sogar über den Boot des Chips hinaus.

Wichtig ist auch die korrekte Einstellung im Menuconfig vom Projekt:

Dort eben NICHT irgendwas mit “UART” anwählen.

Um das nun zu nutzen muss ich zunächst eine “Interaktive Konsole” für meine ESP Firmware schreiben, die USB-CDC nutzt. Dabei muss man aufpassen nicht in die UART Falle zu tappen. Selbstverständlich kann der ESP32-H2 auch einen echten UART verwenden, dazu müsste man aber selbst eine USB/UART Bridge (Chip) mit RX/TX vom ESP verbinden. Will man den integrierten USB-Port vom Super-Mini Board verwenden muss man stattdessen die Funktionen für USB-CDC verwenden.

Hierzu benötigen wir eine weitere Component, die “console”, welche in die main/CMakeLists.txt hinzugefügt wird:

idf_component_register(
   REQUIRES 
        ...
        console
)

Nun kann man die ganz normalen Funktionen aus <stdio.h> nutzen:

#include <stdio.h>

void app_main(void)
{
    char line[128];
    while (1) {
        if (fgets(line, sizeof(line), stdin) != NULL) {
            printf("Gelesen: %s", line);
        }
    }
}

Dabei muss man nur höllisch aufpassen, da Funktionen wie z.B. “getchar()” blockierend wirken, d.h. sie kehren erst zurück wenn etwas über die Console gekommen ist. Bei RTOS-Betriebssystemen führen blockierende Funktionen (wie auch ein usleep()) dazu das der Watchdog (Default 5s) nicht mehr zurückgesetzt wird und der ESP einen RESET einleitet.

fgetchar() kehrt sofort zurück wenn keine Zeichen in der Eingabewarteschlange liegen, wenn aber doch, wartet es so lange bis ein Zeilenende oder Pufferende signalisiert wird. Das passiert in dieser Console aber erst nach drücken von ENTER. Auch kann die Console kein ECHO erzeugen, man tippt also blind. All das ginge nur mit einer echten Terminalemulation über UART.

So, nun habe ich mal meinen Nordic PPK2 Power-Profile an meinen Steckbrettaufbau gehangen um mal zu sehen wo ich aktuell so liege. Das ganze wird über einen Li-Ion Akku betrieben. Die Lichterkette ist nicht angeschlossen, ich wollte erstmal den “Leerlauf” prüfen.

Den Light-Sleep habe ich über einen Timeout-Timer realisiert, also so eine Art Watchdog der regelmäßig reseted werden muss damit das Board nicht in den Schlafmodus geht. Der Reset erfolgt über die IOT-Button Callback Funktionen.

Für den Test habe ich folgendes Szenario genommen:

  • Batteriebetrieb
  • Sleep-Timeout-Zeit auf 15 Sekunden eingestellt
  • Sleep-Erkennung über Digitaleingang vom PPK2 (Digital “0” im Schaubild), angesteuert über GPIO in der Software vor und nach dem sleep Kommando
  • Keine LED-Lichterkette am PWM angeschlossen (reine Board-Grundlast)
  • LED-Ausgang bleibt LOW, somit auch keine On-Board LEDs aktiv
  • Nach POWER-ON keine Taste gedrückt, Gerät verbleibt also im TIMER-OFF Status und fällt nach 15s in den Light-Sleep

Beim einschalten wird kräftig Energie gezogen, wow, das muss ich mal noch näher untersuchen:

Die Umschaltung in Light-Sleep kann man oben im Bald am Digitaleingang “0” gut erkennen, aber was ist das für ein Burst derweil in der Stromaufnahme?

Die kurzen Bursts sind 4.34 ms lang, was so in etwa 240 Hz entspricht, die langen Bursts 8,11 ms was ca. 120 Hz entspricht. Also nach schlafen sieht das noch nicht aus.

Update 1

Habe nun zum testen mal die Peripheriegeräte (PWM, Touch-Button, RTC) nach und nach abgesteckt, das ergab erstmal keinen erkennbaren Unterschied.

Dann habe ich die Stromversorgung vom PPK2 durchführen lassen (3,7 V Ausgangsspannung), also quasi die Batterie ersetzt, auch kein Unterschied. Anschließend auch den Spannungswandler (Buck-Boost) überbrückt, auch hier bleibt das Muster gleich.

Es muss also irgendwas im/auf dem ESP sein.

Update 2

Ich finde keine Parameter die dieses Verhalten irgendwie erkennbar beeinflussen, daher vermute ich es soll so sein. Der Light-Sleep ist ja mehr so ein Standby/Idle und da muss der Chip regelmäßig etwas tun (z.B. den PWM updaten, die internen Timer auswerten, etc.)

Schaue ich mir die Mittelwerte vom Stromverbrauch an, liegt der im aktiven TIMER-OFF Modus bei ca. 16,6 mA

Im TIMER-OFF Modus entspricht das bei einem zu 90% geladenen 3.500 mAh Akku den ich auf maximal 20% entlade einer Laufzeit von 148 Stunden (ca. 6 Tage).

Im Light-Sleep benötigt er dann nur noch ca. 5,5 mA:

was einer Laufzeit von 445 Stunden (ca. 19 Tage) entspricht. Das durchaus ordentlich, auch wenn es sicher noch besser ginge. Dazu müsste ich aber in den Deep-Sleep und das kann ich nur wenn ich extern kein PWM erzeugen muss. Es wäre jetzt also ein Tradeoff zwischen Komplexität und Hardwarekosten durch einen zusätzlichen, externen PWM-Controller und der aktuellen Methode.

Update 3: Problem gefunden und gelöst!

Nach ewigem rumcoden habe ich mir das Board nochmal vorgenommen, hier der Schaltplan davon (war nicht leicht den zu finden…!)

Dann habe ich den LDO, den Batterieladechip und die RGB-LED (die wirklich direkt an 3,3V hängt) runtergelötet und siehe da, ich habe einen Deep-Sleep Strom von 15 µA:

und einen Light-Sleep Strom von 315 µA:

und das ganz ohne Stromspitzen (Spikes)

Perfekt, das genügt mir für meine Zwecke. Bei 314 µA (im Moment noch ohne jegliche Peripherie) würde mein 3.500 mAh Akku fast 11 Monate halten (!) :slight_smile:

BLE steht dir eigentlich weniger im Weg als du denkst: Geräte können dort einfach “Werte” announcen (sry bin in der Terminologie grad nicht mehr so drin), sogar mit Namen und evtl. Einheit.

Gibt dann für debuggingwecke fertige Apps die dir sämtliche exportierten Werte eines Geräts anzeigen können, sofern als R/W markiert auch direkt setzbar, vllt. reicht dir das ja schon als Interface.

Ja, das schaue ich mir gern mal an. Kennst Du welche?

Das ist was ich damals benutzt hatte: https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp (gibts auch für iPhone)

Wie das alles so grundlegend funktioniert wird z.B. hier recht gut beschrieben: ESP32 Bluetooth Low Energy (BLE) on Arduino IDE | Random Nerd Tutorials

1 „Gefällt mir“