- Replace delay() with light_sleep_chunked_ms() in sender idle path (100ms chunks preserve UART FIFO safety at 9600 baud) - Add ENABLE_LIGHT_SLEEP_IDLE build flag (default: on, fallback: =0) - Meter reader task: exponential backoff on consecutive poll failures (METER_FAIL_BACKOFF_BASE_MS..MAX_MS) to reduce idle Core-0 wakeups - Configurable SENDER_DIAG_LOG_INTERVAL_MS (5s debug / 30s prod) - Configurable METER_FRAME_TIMEOUT_CFG_MS, SENDER_CPU_MHZ - New PlatformIO envs: lowpower, 868-lowpower, lowpower-debug - Add docs/POWER_OPTIMIZATION.md with measurement plan and Go/No-Go
10 KiB
Energie-Optimierung: DD3 LoRa Bridge Sender
Kurzreport
Ziel
- 1 Hz Messauflösung beibehalten (
METER_SAMPLE_INTERVAL_MS = 1000) - 30 s Batch-Senden beibehalten (
METER_SEND_INTERVAL_MS = 30000) - ≥ 20 % Reduktion des durchschnittlichen Stromverbrauchs
- 0 Datenverlust, identische Batch-Semantik
Kernmaßnahmen & Priorisierung
| # | Maßnahme | Einsparung (geschätzt) | Risiko | Priorität |
|---|---|---|---|---|
| 1 | Chunked Light-Sleep zwischen 1 Hz Samples | 25–35 % avg. Strom | niedrig | P0 |
| 2 | Meter-Reader Exponential-Backoff | 2–5 % (weniger Core-0-Wakeups) | sehr niedrig | P1 |
| 3 | Log-Drosselung (konfigurierbar) | 1–3 % (weniger UART TX) | keins | P1 |
| 4 | CPU-Frequenz konfigurierbar (80→40 MHz) | 5–10 % (optional) | SPI-Timing prüfen | P2 |
| 5 | OLED Auto-Off (bereits implementiert) | ~5 mA wenn aus | keins | ✅ bereits aktiv |
| 6 | WiFi/BT deaktiviert (Sender) | ~80 mA gespart | keins | ✅ bereits aktiv |
| 7 | LoRa Sleep zwischen Batches | ~10 mA gespart | keins | ✅ bereits aktiv |
Zusammenfassung
Der größte Hebel (P0) ist der Wechsel von delay(idle_ms) zu
light_sleep_chunked_ms() in der Sender-Hauptschleife. Im Normalzustand (Zeit
synchronisiert, 1 Hz Sampling) verbringt die CPU ca. 950 ms/s im Idle. Bisher
wurde delay() verwendet (CPU aktiv bei 80 MHz ≈ 25–30 mA), jetzt wird in
100 ms-Chunks Light-Sleep eingesetzt (≈ 0,8–1,5 mA). Das allein senkt den
mittleren Strom um ~25 mA, bei einem Gesamtverbrauch von ~35–40 mA ca. 35 %.
Technischer Anhang
1. Chunked Light-Sleep (P0)
Problem: Im Sender-Loop wurde nach dem Sampling-Tick delay(idle_ms)
aufgerufen, um den Meter-Reader-Task auf Core 0 weiterlaufen zu lassen. Die CPU
blieb dabei komplett aktiv.
Lösung: light_sleep_chunked_ms(total_ms, chunk_ms) – aufgeteilt in max.
100 ms Chunks, damit die UART-Hardware-FIFO (128 Byte @ 9600 Baud ≈ 133 ms
Sicherheitspuffer) nicht überläuft.
Mechanismus:
- Main-Task (Core 1) ruft
esp_light_sleep_start()auf → beide Cores schlafen - Timer-Wakeup nach max. 100 ms
- FreeRTOS-Scheduler läuft → Meter-Reader-Task (Core 0, Prio 2) draint FIFO
- Main-Task setzt fort → nächster Chunk oder Sampling-Tick
Betroffene Dateien:
include/config.h # Neue Konstanten: LIGHT_SLEEP_IDLE, LIGHT_SLEEP_CHUNK_MS
include/power_manager.h # Neue Funktion: light_sleep_chunked_ms()
src/power_manager.cpp # Implementierung light_sleep_chunked_ms()
src/sender_state_machine.cpp # Idle-Pfad: delay() → light_sleep_chunked_ms()
Patch – power_manager.cpp:
void light_sleep_chunked_ms(uint32_t total_ms, uint32_t chunk_ms) {
if (total_ms == 0) return;
if (chunk_ms == 0) chunk_ms = total_ms;
uint32_t start = millis();
for (;;) {
uint32_t elapsed = millis() - start;
if (elapsed >= total_ms) break;
uint32_t remaining = total_ms - elapsed;
uint32_t this_chunk = remaining > chunk_ms ? chunk_ms : remaining;
if (this_chunk < 10) {
delay(this_chunk); // Light-sleep overhead nicht lohnend
break;
}
light_sleep_ms(this_chunk);
// Nach Wakeup läuft der FreeRTOS-Scheduler automatisch:
// meter_reader_task (Prio 2 > Main-Prio 1) draint UART-FIFO
}
}
Patch – sender_state_machine.cpp (Idle-Pfad):
lora_sleep();
if (LIGHT_SLEEP_IDLE) {
// Chunked light-sleep: wake every LIGHT_SLEEP_CHUNK_MS so the
// meter_reader_task (Core 0, prio 2) can drain the 128-byte UART HW FIFO
// before it overflows (~133 ms at 9600 baud). Saves ~25 mA vs delay().
light_sleep_chunked_ms(idle_ms, LIGHT_SLEEP_CHUNK_MS);
} else if (g_time_acquired) {
delay(idle_ms); // Fallback
} else {
light_sleep_ms(idle_ms);
}
Fallback-Flag: ENABLE_LIGHT_SLEEP_IDLE=0 deaktiviert Light-Sleep komplett
→ identisches Verhalten wie vorher.
2. Meter-Reader Exponential-Backoff (P1)
Problem: Der Meter-Reader-Task pollt alle 5 ms via vTaskDelay(5) – auch
wenn der Meter nicht angeschlossen ist oder dauerhaft Fehler liefert. Bei nicht
angeschlossenem Meter bedeutet das ~200 Wakeups/s auf Core 0 ohne Nutzen.
Lösung: Exponential-Backoff auf METER_FAIL_BACKOFF_BASE_MS (10 ms) bis
METER_FAIL_BACKOFF_MAX_MS (500 ms) bei konsekutiven Fehlschlägen. Bei
erfolgreichem Frame-Empfang sofortige Reset auf 5 ms (= normalem Polling).
// In meter_reader_task_entry():
uint32_t backoff_ms = METER_FAIL_BACKOFF_BASE_MS << consecutive_fails;
if (backoff_ms > METER_FAIL_BACKOFF_MAX_MS) backoff_ms = METER_FAIL_BACKOFF_MAX_MS;
vTaskDelay(pdMS_TO_TICKS(backoff_ms));
Risiko: Keines – normaler 1 Hz Betrieb mit angeschlossenem Meter liefert
dauerhaft Frames → consecutive_fails = 0 → Backoff bleibt bei 10 ms.
3. Log-Drosselung (P1)
Problem: Diagnose-Logs wurden alle 5 s gesendet, Power-Logs alle 10 s.
Jeder Serial.printf() kostet ~1 ms CPU + UART-TX-Energie.
Lösung: Konfigurierbares SENDER_DIAG_LOG_INTERVAL_MS – 5 s im Debug-Modus,
30 s im Nicht-Debug-Modus. Production-Build (SERIAL_DEBUG_MODE_FLAG=0) hat
alle Logs vollständig eliminiert (bestehendes Verhalten, jetzt explizit).
4. CPU-Frequenz (P2, optional)
SENDER_CPU_MHZ ist jetzt konfigurierbar (Default: 80 MHz). 40 MHz wäre
möglich, spart ~5 mA, erfordert aber Validierung der SPI-Timing für
LoRa-Modul (SX1276). Empfehlung: Erst mit 80 MHz validieren, dann 40 MHz
testen.
Hinweis: Kein separater Build-Flag hinzugefügt; bei Bedarf:
-DSENDER_CPU_MHZ=40 in build_flags.
5. Frame-Timeout (konfigurierbar)
METER_FRAME_TIMEOUT_CFG_MS (Default: 3000 ms) ist jetzt in config.h statt
hart kodiert in meter_driver.cpp. Erlaubt Tuning ohne Quellcode-Änderung.
Build-Varianten
| Environment | Beschreibung |
|---|---|
lilygo-t3-v1-6-1 |
Standard-Build, Debug ein, Light-Sleep ein (Default) |
lilygo-t3-v1-6-1-prod |
Production, Debug aus, Light-Sleep ein |
lilygo-t3-v1-6-1-lowpower |
Low-Power, Debug aus, Light-Sleep ein |
lilygo-t3-v1-6-1-868-lowpower |
Low-Power @ 868 MHz |
lilygo-t3-v1-6-1-lowpower-debug |
Low-Power + Debug + Meter-Diag |
Light-Sleep deaktivieren (Fallback): -DENABLE_LIGHT_SLEEP_IDLE=0
Messprotokoll/Testplan
Equipment
- USB-Multimeter (z. B. FNIRSI FNB58) oder INA219 Breakout am Batterie-Anschluss
- Sender-Board (TTGO LoRa32 v1.6.1) mit angeschlossenem Smart-Meter
- Receiver-Board für ACK
Messprozedur (30 min Run)
-
Baseline (ohne Light-Sleep):
pio run -e lilygo-t3-v1-6-1 -t upload -- -DENABLE_LIGHT_SLEEP_IDLE=0- 30 min laufen lassen, Durchschnittsstrom messen
- Serielle Ausgabe loggen:
pio device monitor -b 115200 > baseline.log
-
Light-Sleep (aktiviert):
pio run -e lilygo-t3-v1-6-1-lowpower-debug -t upload- 30 min laufen lassen, Durchschnittsstrom messen
- Serielle Ausgabe loggen:
pio device monitor -b 115200 > lowpower.log
-
Auswertung:
- Mittlerer Strom:
avg(I_baseline)vsavg(I_lowpower) - 1 Hz Jitter:
grep "diag:" lowpower.log→ Sample-Timestamps prüfen - Sample-Verluste: Batch-Logs auswerten (
valid_count,invalid_count) - Batch-Semantik: ACK-Erfolgsrate vergleichen
- Mittlerer Strom:
Akzeptanzkriterien
| Kriterium | Schwellwert |
|---|---|
| Durchschnittlicher Strom | ≥ 20 % Reduktion vs Baseline |
| Verlorene Samples | 0 in 30 min |
| 1 Hz Jitter | < 50 ms |
| Batch-Semantik | Identische ACK-Erfolgsrate (±2 %) |
| Fehlerrate | ≤ 2/h über 4 h |
| OLED-Funktion | Button weckt Display, Auto-Off funktioniert |
| Watchdog | Kein Reset in 4 h |
Go/No-Go
- Go: Alle Kriterien erfüllt → Merge in
main - No-Go bei Jitter > 100 ms:
LIGHT_SLEEP_CHUNK_MSauf 50 ms reduzieren, erneut messen - No-Go bei Sample-Verlust:
ENABLE_LIGHT_SLEEP_IDLE=0als Fallback, UART-FIFO-Puffergröße prüfen
Strombudget-Schätzung (Sender, 1 Hz Sampling + 30 s Batch)
Baseline (delay-basiert)
| Phase | Dauer/30s | Strom (mA) | Anteil |
|---|---|---|---|
| Sampling (30× ~20 ms) | 600 ms | 30 | 2 % |
| Encoding + TX (~1.5 s) | 1500 ms | 120 | 5 % |
| ACK RX Window (~3 s) | 3000 ms | 25 | 10 % |
| Idle/delay (~25 s) | 24900 ms | 28 | 83 % |
| Durchschnitt | ~32 mA |
Optimiert (Light-Sleep)
| Phase | Dauer/30s | Strom (mA) | Anteil |
|---|---|---|---|
| Sampling (30× ~20 ms) | 600 ms | 30 | 2 % |
| Encoding + TX (~1.5 s) | 1500 ms | 120 | 5 % |
| ACK RX Window (~3 s) | 3000 ms | 25 | 10 % |
| Light-Sleep (~25 s) | 24900 ms | 1.2 | 83 % |
| Durchschnitt | ~10 mA |
Geschätzte Einsparung: ~70 % (32→10 mA)
Reale Werte hängen vom Board (Quiescent-Strom des Reglers, LED), OLED-Status und LoRa-Spreading-Factor ab. Konservativ ≥ 20 % erreichbar.
PR-Plan
Branch
feat/power-light-sleep-idle
Commits
feat(power): 1Hz RTC wake + chunked light-sleep; meter backoff; log throttling
- Replace delay() with light_sleep_chunked_ms() in sender idle path
- Add ENABLE_LIGHT_SLEEP_IDLE config flag (default: on)
- Meter reader task: exponential backoff on consecutive poll failures
- Configurable SENDER_DIAG_LOG_INTERVAL_MS, METER_FRAME_TIMEOUT_CFG_MS
- Configurable SENDER_CPU_MHZ (default: 80)
- New PlatformIO environments: lowpower, 868-lowpower, lowpower-debug
Offene Risiken / Nebenwirkungen
-
UART FIFO Overflow bei > 9600 Baud: Falls künftig eine höhere Baudrate verwendet wird, muss
LIGHT_SLEEP_CHUNK_MSproportional reduziert werden (Formel:128 / (baud / 10) * 1000). -
ESP32 Light-Sleep + LoRa-Interrupt: Wenn der LoRa-Transceiver (SX1276) DIO0-Interrupts während Light-Sleep generiert, werden diese nach dem Wakeup verarbeitet. Im Sender-Modus (TX-only zwischen Batches) kein Problem, da
lora_sleep()vor dem Light-Sleep aufgerufen wird. -
Watchdog:
WATCHDOG_TIMEOUT_SEC = 120 sist mehr als ausreichend für den maximalen Light-Sleep-Chunk von 100 ms. Kein Risiko. -
FreeRTOS Tick-Drift: Nach Light-Sleep wird der Tick-Counter nachgeführt.
millis()bleibt konsistent. Kein Einfluss auf 1 Hz Timing. -
Meter-Backoff bei normalem Betrieb: Der Backoff greift nur bei
meter_poll_frame() == false(kein verfügbarer Frame). Bei normalem Betrieb mit 1 Hz Frames kehrt der Backoff sofort aufMETER_FAIL_BACKOFF_BASE_MSzurück. Kein Einfluss auf Sampling-Latenz.