- 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
294 lines
10 KiB
Markdown
294 lines
10 KiB
Markdown
# 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:**
|
||
1. Main-Task (Core 1) ruft `esp_light_sleep_start()` auf → beide Cores schlafen
|
||
2. Timer-Wakeup nach max. 100 ms
|
||
3. FreeRTOS-Scheduler läuft → Meter-Reader-Task (Core 0, Prio 2) draint FIFO
|
||
4. 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:**
|
||
|
||
```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):**
|
||
|
||
```cpp
|
||
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).
|
||
|
||
```cpp
|
||
// 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)
|
||
|
||
1. **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`
|
||
|
||
2. **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`
|
||
|
||
3. **Auswertung:**
|
||
- Mittlerer Strom: `avg(I_baseline)` vs `avg(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
|
||
|
||
### 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_MS` auf 50 ms reduzieren,
|
||
erneut messen
|
||
- **No-Go bei Sample-Verlust:** `ENABLE_LIGHT_SLEEP_IDLE=0` als 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
|
||
|
||
1. **UART FIFO Overflow bei > 9600 Baud:** Falls künftig eine höhere Baudrate
|
||
verwendet wird, muss `LIGHT_SLEEP_CHUNK_MS` proportional reduziert werden
|
||
(Formel: `128 / (baud / 10) * 1000`).
|
||
|
||
2. **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.
|
||
|
||
3. **Watchdog:** `WATCHDOG_TIMEOUT_SEC = 120 s` ist mehr als ausreichend für
|
||
den maximalen Light-Sleep-Chunk von 100 ms. Kein Risiko.
|
||
|
||
4. **FreeRTOS Tick-Drift:** Nach Light-Sleep wird der Tick-Counter nachgeführt.
|
||
`millis()` bleibt konsistent. Kein Einfluss auf 1 Hz Timing.
|
||
|
||
5. **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 auf `METER_FAIL_BACKOFF_BASE_MS`
|
||
zurück. Kein Einfluss auf Sampling-Latenz.
|