Inhero MR2 Energieverwaltung - Implementierungs-Dokumentation (Rev 1.1)¶
Inhaltsverzeichnis¶
- Überblick
- Hardware-Architektur
- 1. Low-Voltage-Erkennung (INA228 ALERT ISR)
- 2. Coulomb Counter & SOC (Ladezustand)
- 3. Tägliche Energiebilanz
- 4. Solar-Energieverwaltung
- BQ25798 ADC bei niedrigen Akkuspannungen
- JEITA-WARM-Zone & VBAT_OVP-Vermeidung
- 5. Batt-TTL-Prognose
- 6. RTC-Wakeup-Management
- 7. Energieverwaltungsablauf
- 8. INA228 ALERT-Pin (Rev 1.1)
- 9. SX1262 Power Control & PE4259 RF-Switch
- 10. BQ25798 CE-Pin Safety (Rev 1.1 — FET-invertiert)
- 11. Statistik-Persistenz
- 12. CLI-Befehle
- Siehe auch
Diese Dokumentation beschreibt die Energieverwaltungs-Implementierung für das Inhero MR2 Board. Hardware Rev 1.1: INA228 ALERT auf P1.02, TPS62840 EN via 3.3V_off-Schalter, CE-Pin via DMN2004TK-7 FET (invertiert).
Überblick¶
Das System kombiniert INA228 ALERT-basierte Low-Voltage-Erkennung + System Sleep mit GPIO-Latch + Coulomb Counter + tägliche Energiebilanz + CE-Pin FET-Safety für maximale Energie-Effizienz:
- INA228 ALERT ISR (P1.02) - Low-Voltage-Erkennung via Hardware-Interrupt
- System Sleep mit GPIO-Latch (< 500µA) mit RTC-Wake - Minimaler Stromverbrauch bei Low-Voltage
- CE-Pin FET-Safety (DMN2004TK-7) - Invertierte Logik, Solar-Laden in System Sleep möglich
- Coulomb Counter (INA228) - Echtzeit-SOC-Tracking
- Tägliche Energiebilanz (7-Tage rolling) - Solar vs. Akku
- RTC-Wakeup-Management (RV-3028-C7) - Periodische Recovery-Checks
Feature-Matrix¶
| Funktion | Status | Hinweis |
|---|---|---|
| INA228 ALERT → Low-Voltage System Sleep | Aktiv | ISR auf P1.02 → volatile Flag → tickPeriodic() → System Sleep mit GPIO-Latch + RTC-Wake |
| RTC-Wakeup (Low-Voltage-Recovery) | Aktiv | 60 min (periodisch) |
| BQ CE-Pin Safety (FET-invertiert) | Aktiv | GPIO HIGH → FET ON → CE LOW → Laden an (BQ25798 CE active-low), Dual-Layer: GPIO + I2C |
| System Sleep mit gelatchtem CE | Aktiv | < 500µA, GPIO4-Latch HIGH erhalten → FET ON → CE LOW → Solar-Laden möglich |
| SOC via INA228 + manuelle Akkukapazität | Aktiv | set board.batcap verfügbar |
| SOC→Li-ion mV Mapping (Workaround) | Aktiv | Wird entfernt wenn MeshCore SOC% nativ übermittelt |
| MPPT-Recovery + Stuck-PGOOD-Handling | Aktiv | Cooldown-Logik aktiv |
Hardware-Architektur¶
Komponenten¶
| Komponente | Funktion | I2C | Pin | Details |
|---|---|---|---|---|
| RAK4630 | Core Module | — | — | nRF52840 SoC + SX1262 LoRa Transceiver |
| INA228 | Power Monitor | 0x40 | ALERT→P1.02 (ISR) | 100mΩ Shunt, 1.6A max, Coulomb Counter, BUVL Alert |
| BME280 | Temperatur-/Feuchte-/Drucksensor | 0x76 | — | NTC-Kalibrier-Referenz (set board.tccal), Selftest |
| RV-3028-C7 | RTC | 0x52 | INT→GPIO17 | Zeitbasis, Countdown-Timer, Wake-up. Siehe FAQ #23 zur Bedeutung der Uhrzeit. |
| BQ25798 | Battery Charger | 0x6B | INT→GPIO21 | MPPT, JEITA, 15-bit ADC (IBUS ~±30mA error at low currents; ADC hat VBAT-abhängige Schwellen, siehe Abschnitt 4) |
| BQ CE-Pin | Charge Enable | — | GPIO4 (P0.04) | Via DMN2004TK-7 FET: GPIO HIGH → FET ON → CE LOW → Laden an (BQ25798 CE active-low) |
| TPS62840 | Buck Converter | - | EN via 3.3V_off-Schalter | 750mA, 3.3V Rail |
| DMN2004TK-7 | CE-FET | — | Gate←GPIO4 (ext. Pull-Down) | N-FET, Drain→CE, Source→GND. GPIO HIGH → FET ON → CE LOW → Laden an. Pull-Down zieht Gate LOW wenn floating. |
| Schottky-Diode | USB→VBUS-Diode | — | — | VBUS-USB → VBUS-BQ (Solareingang). USB-C CC1/CC2 über 4,7kΩ auf GND (USB-Sink). ⚠ Solar-Kurzschluss shortet auch VBUS-USB. |
1. Low-Voltage-Erkennung (INA228 ALERT ISR)¶
Implementierung (Rev 1.1 — Flag/Tick-Architektur)¶
- Trigger: INA228 BUVL (Bus Under-Voltage Limit) ALERT auf P1.02
- ISR:
BoardConfigContainer::lowVoltageAlertISR()→ setztlowVoltageAlertFired = true(nur Flag, kein FreeRTOS-Aufruf) - Verarbeitung:
tickPeriodic()prüft Flag im Main-Loop-Kontext →board.initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE) - Arming:
armLowVoltageAlert()wird bei Akku-Konfiguration aufgerufen (setzt BUVL-Schwelle + aktiviert ISR)
Low-Voltage-Flow¶
INA228 BUVL Alert (P1.02, FALLING edge)
│
▼
lowVoltageAlertISR() [ISR-Kontext]
│ Sets lowVoltageAlertFired = true (volatile Flag)
▼
tickPeriodic() [Main-Loop-Kontext, nächster tick()]
│ Prüft lowVoltageAlertFired == true
▼
board.initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE)
│ CE gelatcht HIGH (GPIO-Latch erhalten → FET ON → CE LOW → Laden bleibt AN)
│ RTC-Wake konfiguriert (LOW_VOLTAGE_SLEEP_MINUTES = 60)
│ GPREGRET2 → LOW_VOLTAGE_SLEEP-Flag
▼
sd_power_system_off() → System Sleep mit GPIO-Latch (< 500µA)
Chemie-spezifische Schwellen (1-Level System, einheitliche 200mV Hysterese)¶
| Chemie | lowv_sleep_mv (ALERT) | lowv_wake_mv (0% SOC) | Hysterese |
|---|---|---|---|
| Li-ion 1S | 3100 | 3300 | 200mV |
| LiFePO4 1S | 2700 | 2900 | 200mV |
| LTO 2S | 3900 | 4100 | 200mV |
| Na-ion 1S | 2500 | 2700 | 200mV |
Implementierung: BoardConfigContainer — battery_properties[] Lookup-Tabelle
- lowv_sleep_mv → INA228 BUVL Alert-Schwelle, löst System Sleep aus
- lowv_wake_mv → RTC-Wake-Schwelle (Early Boot prüft VBAT, entscheidet ob Boot oder erneut Sleep)
- Statische Methoden: getLowVoltageSleepThreshold(type), getLowVoltageWakeThreshold(type)
2. Coulomb Counter & SOC (Ladezustand)¶
INA228 Integration¶
- Driver:
lib/Ina228Driver.cpp - Init:
BoardConfigContainer::begin() - 100mΩ Shunt-Kalibrierung
- CURRENT_LSB = 1.6384A / 524288 ≈ 3.125µA
- ADC-Bereich ±163.84mV (ADCRANGE=0, optimal für 1A @ 100mΩ)
- ADC-Mittelung: 256 Samples (filtert TX-Spannungsspitzen)
- BUVL Alert konfiguriert auf
lowv_sleep_mv(chemie-spezifisch)
SOC-Berechnung¶
Methode: updateBatterySOC() in BoardConfigContainer.cpp
- Primär: Coulomb Counting (INA228 CHARGE-Register)
- Update-Intervall: alle 60s via tickPeriodic(); im Low-Voltage-Sleep gibt es keine SOC-Updates (der RTC-Wake prüft nur VBAT und schläft weiter oder bootet)
Formel:
SOC_delta = charge_delta_mah / capacity_mah × 100%
SOC_new = SOC_old + SOC_delta
Auto-Sync: Bei BQ25798 "Charge Done" wird SOC auf 100% gesetzt.
Kapazitäts-Management¶
Konfiguration erforderlich¶
Die Akkukapazität muss manuell gesetzt werden, da sie in der Praxis stark variiert:
- Typischer Bereich: 4000-24000mAh (4-24Ah)
- CLI-Befehl: set board.batcap <mAh>
- Erlaubter Bereich: 100-100000mAh
Wichtig: Ohne korrekte Kapazität sind SOC% und Batt-TTL-Berechnungen ungenau!
Persistenz-Mechanik¶
Speicherpfad: /inheromr2/batCap.txt (LittleFS via SimplePreferences)
Save-Methode: setBatteryCapacity() in BoardConfigContainer.cpp (persistiert via SimplePreferences)
Load-Methode: loadBatteryCapacity()
Speichern bei:
1. Manuelles Setzen: CLI-Befehl set board.batcap <mAh>
- Schreibt sofort in LittleFS
- Aktualisiert batteryStats.capacity_mah
Laden bei:
- Boot-Zeit: BoardConfigContainer::begin() ruft loadBatteryCapacity() auf
- Fallback: Wenn keine gespeicherte Kapazität vorhanden
- Validierung: Range-Check 100-100000mAh
Persistenz-Eigenschaften:
- ✅ Überlebt Software-Shutdowns (System Sleep)
- ✅ Überlebt Power-Cycle und Low-Voltage-Recovery
- ✅ Überlebt Firmware-Update (LittleFS bleibt erhalten)
- ⚠️ Verloren bei: Flash-Erase, rm -rf /inheromr2/, Filesystem-Korruption
3. Tägliche Energiebilanz¶
Tracking (168-Stunden-Ringpuffer)¶
Methoden: updateHourlyStats() + calculateRollingStats() in BoardConfigContainer.cpp
- Aufgerufen von: tickPeriodic() (alle 60 min)
- Abtastung: An jeder Stundengrenze (RTC-Zeit auf volle Stunden abgeschnitten) wird die abgeschlossene Stunde in den Ringpuffer geschrieben
Datenstruktur: BatterySOCStats.hours[168] (7 Tage × 24 Stunden)
typedef struct {
uint32_t timestamp; // Stundenbeginn, Unix-Sekunden
float charged_mah; // in dieser Stunde geladen
float discharged_mah; // in dieser Stunde entladen
float solar_mah; // Solar-Anteil dieser Stunde
} HourlyBatteryStats;
Die Stundenwerte werden in updateBatterySOC() (alle 60s) aus den Deltas des INA228-CHARGE-Registers aufsummiert: ein positives Delta zählt als geladen (und solar), ein negatives Delta als entladen.
Berechnungen¶
Rollierende Summen (calculateRollingStats(), nach jeder abgeschlossenen Stunde):
last_24h_net_mah = Σ(solar − entladen) über die letzten 24 Stunden
avg_3day_daily_net_mah = Σ(solar − entladen) über 72h / 3 (braucht ≥ 24h Daten)
avg_7day_daily_net_mah = Σ(solar − entladen) über 168h / 7 (braucht ≥ 24h Daten; Basis für Batt-TTL)
Living Status:
- living_on_battery = true wenn last_24h_net_mah < 0 (Netto-Defizit über die letzten 24h)
- Solar-Überschuss (SOL in board.stats) ist schlicht living_on_battery == false
4. Solar-Energieverwaltung¶
Designprinzip¶
Der BQ25798 entscheidet selbst über PowerGood (PG), ob ein Eingang nutzbar ist. Der Charger läuft im Always-Active-Modus (HIZ deaktiviert). Die Firmware überwacht Solar-Status und reaktiviert MPPT bei Bedarf.
Kein INT-Pin-Interrupt — alles läuft über Polling in runMpptCycle() (60s Intervall).
Solar-Checks¶
runMpptCycle() führt bei jedem Zyklus zwei Prüfungen durch:
1. checkAndFixSolarLogic() — PG-Stuck Recovery + MPPT-Reaktivierung
2. updateMpptStats() — Aktualisiert MPPT-Statistiken für 7-Tage-Durchschnitt
PFM Forward Mode¶
- PFM-Forward-Modus ist ab Werk im BQ25798 aktiv (PFM_FWD_DIS=0, REG0x12); die Firmware ändert ihn nicht
- PFM verbessert die Effizienz bei niedrigen Solarströmen
MPPT Recovery + PG-Stuck¶
checkAndFixSolarLogic() behandelt zwei Szenarien:
PG=1: MPPT-Reaktivierung — BQ25798 deaktiviert MPPT automatisch bei Faults. Readback-Check: nur schreiben wenn tatsächliche Änderung nötig.
PG=0 + VBUS ≥ 4.5V: PG-Stuck Recovery — Panel liefert Spannung, aber BQ hat die
Inputquelle nicht qualifiziert (typisch bei langsamem Sonnenaufgang). HIZ-Toggle erzwingt
neue Input-Qualifikation. 5-Minuten-Cooldown verhindert übermäßiges Toggling.
Konstante: PG_STUCK_VBUS_THRESHOLD_MV = 4500 in BoardConfigContainer.h
BQ25798 Interrupt Handling¶
BQ INT-Pin (GPIO 21): Nicht als Interrupt genutzt — INPUT_PULLUP gegen Floating.
BQ-Status wird via Polling in runMpptCycle() alle 60s geprüft.
Flag-Clearing beim Boot: BqDriver::clearInterruptFlags() (aufgerufen aus BoardConfigContainer::begin())
- Liest die CHARGER_FLAG/FAULT_FLAG-Register 0x22–0x27 → INT-Leitung wird freigegeben
- Vermeidet stehengebliebene Faults vom vorherigen Power-Cycle
Flag/Tick-Architektur¶
Alle I2C-Operationen laufen im Main-Loop-Kontext über tickPeriodic() (aufgerufen von InheroMr2Board::tick()). Es gibt keine FreeRTOS-Tasks für I2C-Zugriffe — dadurch entfallen Mutex und Race Conditions.
I2C Bus Recovery (in InheroMr2Board::begin()): Nach OTA/Warm-Reset kann ein I2C-Slave SDA festhalten. Vor Wire.begin() werden bis zu 9 SCL-Pulse + STOP-Condition generiert um den Bus freizugeben.
tickPeriodic() dispatcht periodische Arbeit via millis()-Timer:
tickPeriodic() [aufgerufen von tick(), Main-Loop]
├─ Low-Voltage Alert Flag prüfen → initiateShutdown()
├─ Alle 60s: runMpptCycle()
│ ├─ checkAndFixSolarLogic() — PG-Stuck Recovery (HIZ-Toggle) + MPPT-Recovery
│ └─ updateMpptStats() — MPPT-Statistiken aktualisieren
├─ Alle 60s: updateBatterySOC()
└─ Alle 60min: updateHourlyStats()
Verbleibende FreeRTOS-Tasks (nur GPIO, kein I2C):
- heartbeatTask — blaue LED Blink-Muster
- ErrorLED Lambda — rote LED bei fehlenden Komponenten
Timing Summary: - MPPT Cycle: 60 Sekunden (via tickPeriodic) - SOC Update: 60 Sekunden (via tickPeriodic) - Hourly Stats: 60 Minuten (via tickPeriodic)
BQ25798 ADC bei niedrigen Akkuspannungen¶
Referenz: BQ25798 Datasheet (TI SLUSE22), Section 9.3.16 — ADC
Problem¶
Der 15-bit ADC im BQ25798 hat spannungsabhängige Betriebsschwellen, die im reinen Akkubetrieb (ohne Solar) relevant werden. Bei niedrigen Akkuspannungen kann der ADC seine Konvertierung nicht abschließen — ADC_EN bleibt gesetzt und die Firmware läuft in einen Timeout.
Datasheet-Zitat (Section 9.3.16)¶
"The ADC is allowed to operate if either VBUS > 3.4V or VBAT > 2.9V is valid. At battery only condition, if the TS_ADC channel is enabled, the ADC only works when battery voltage is higher than 3.2V, otherwise, the ADC works when the battery voltage is higher than 2.9V."
Betriebsszenarien¶
| Bedingung | VBUS | VBAT | TS-Kanal | ADC | Temperatur |
|---|---|---|---|---|---|
| Solar angeschlossen | > 3.4V | beliebig | aktiviert | ✅ läuft | ✅ verfügbar |
| Akkubetrieb, normal | — | ≥ 3.2V | aktiviert | ✅ läuft | ✅ verfügbar |
| Akkubetrieb, niedrig | — | 2.9–3.2V | deaktiviert | ✅ läuft | ❌ nicht verfügbar |
| Akkubetrieb, kritisch | — | < 2.9V | deaktiviert | ❌ Timeout | ❌ nicht verfügbar |
Firmware-Lösung: VBAT-abhängige TS-Kanal-Steuerung¶
Die Firmware liest die aktuelle Akkuspannung vom INA228 und übergibt sie an BqDriver::getTelemetryData(vbat_mv):
- VBAT ≥ 3.2V (oder unbekannt): TS-Kanal aktiviert → ADC-Schwelle 3.2V, Temperatur verfügbar
- VBAT < 3.2V: TS-Kanal deaktiviert → ADC-Schwelle sinkt auf 2.9V, Temperatur als "N/A" angezeigt
Dadurch funktioniert der ADC im Bereich 2.9–3.2V weiterhin für Solar-Messungen (VBUS, IBUS), auch wenn die Akkutemperatur nicht gelesen werden kann.
ADC-Kanal-Konfiguration (nur benötigte Kanäle)¶
Auf dem MR2 sind D+, D−, VAC1, VAC2 nicht verbunden. Die Firmware aktiviert nur die tatsächlich genutzten Kanäle:
| Register | Wert (TS ein) | Wert (TS aus) | Aktive Kanäle |
|---|---|---|---|
| 0x2F (ADC_FUNCTION_DISABLE_0) | 0x5A |
0x5E |
IBUS, VBUS, (TS) |
| 0x30 (ADC_FUNCTION_DISABLE_1) | 0xF0 |
0xF0 |
keine (D+/D−/VAC disabled) |
Wichtig: Im One-Shot-Modus wird ADC_EN erst gelöscht, wenn alle aktivierten Kanäle fertig konvertiert haben. Nicht-verbundene Kanäle können dies blockieren → deshalb werden nur die benötigten Kanäle aktiviert.
Temperatur-Sentinel-Werte¶
Die Firmware verwendet spezielle Rückgabewerte für ungültige Temperaturen:
| Wert | Bedeutung | Anzeige |
|---|---|---|
| −999.0 | I2C-Kommunikationsfehler | N/A |
| −888.0 | ADC nicht bereit / TS deaktiviert (niedriges VBAT) | N/A |
| −99.0 | NTC offen/nicht angeschlossen | N/A |
| +99.0 | NTC Kurzschluss | N/A |
| −50…+90°C | Gültiger Messwert | XX°C |
Anzeigeregel: Werte ≤ −100°C werden in der CLI als "N/A" angezeigt und in CayenneLPP-Paketen weggelassen.
Code-Referenzen¶
BqDriver::getTelemetryData(vbat_mv)— Hauptfunktion mit VBAT-abhängiger TS-SteuerungBqDriver::startADCOneShot(ts_enabled)— Konfiguriert ADC-Kanäle und startet KonvertierungBoardConfigContainer::getTelemetryData()— Übergibt INA228-VBAT an BqDriver
JEITA-WARM-Zone & VBAT_OVP-Vermeidung¶
Problem: Standard-JEITA-Konfiguration + Inhero-Teiler¶
Das Inhero MR2 verwendet einen nicht-standardmäßigen NTC-Spannungsteiler (RT1=5,6 kΩ Pullup an REGN, RT2=27 kΩ parallel zu GND) anstelle des TI-Referenzdesigns (5,24 kΩ / 30,31 kΩ). Dies verschiebt TS-Schwellen um einen temperaturabhängigen Betrag nach unten: ~5–6 °C im Kaltbereich (wo der NTC-Widerstand groß relativ zu RT2 ist und den Teiler-Unterschied verstärkt) und ~2–3 °C im Warm-/Hot-Bereich.
Mit den BQ25798 POR-Defaults (TS_WARM = 45°C, JEITA_VSET = VREG−400mV, EN_AUTO_IBATDIS = 1) verursachte dies eine kritische Fehlerkette bei moderaten Temperaturen (~42 °C):
42°C Umgebung → TS = 44,65% REGN (unter VT3_FALL = 44,8%)
→ BQ tritt in WARM-Zone ein
→ JEITA_VSET reduziert VREG: 3,5V − 400mV = 3,1V (LiFePO4)
→ Akku bei 3,47V > 104% × 3,1V = 3,224V → VBAT_OVP ausgelöst
→ Wandler stoppt, EN_AUTO_IBATDIS zieht IBAT_LOAD = 30mA aus dem Akku
→ Gesamtverbrauch: −11mA (System) + −30mA (IBAT_LOAD) = −41mA
→ Recovery erfordert VBAT < 102% × 3,1V = 3,162V → stundenlanger Akkuverbrauch
Fix: Drei Register-Einstellungen in configureBaseBQ()¶
| Einstellung | Register | Wert | Wirkung |
|---|---|---|---|
setTsWarm(BQ25798_TS_WARM_55C) |
NTC Control 1 (0x18), Bits 5:4 | 55 °C (37,7% REGN) | WARM-Zone beginnt bei ~52 °C (Inhero), nicht ~42 °C |
setJeitaVSet(BQ25798_JEITA_VSET_UNCHANGED) |
NTC Control 0 (0x17), Bits 7:5 | UNCHANGED | Keine VREG-Reduktion in WARM — verhindert VBAT_OVP |
JEITA_ISETH (POR-Default beibehalten) |
NTC Control 0, Bits 4:3 | 11b = ICHG unchanged | Keine Ladestrom-Reduktion in WARM |
setAutoIBATDIS(false) |
Charger Control 0, Bit 7 | 0 | Deaktiviert 30-mA-Akkuentladung bei OVP |
Ergebnis: Mit JEITA_VSET=UNCHANGED und JEITA_ISETH=ICHG unchanged ist die WARM-Zone (T3–T5) effektiv neutralisiert. Die Ladung läuft mit voller Spannung und vollem Strom bis T-Hot (~58 °C), wo die Ladung komplett gesperrt wird.
TS-Schwellenvergleich¶
| Zonengrenze | BQ-Register | % REGN | TI-Referenz (°C) | Inhero MR2 (°C) | Shift |
|---|---|---|---|---|---|
| VT1 (Cold) | — | 72,0% | +3,7 | −2,0 | −5,7 °C |
| VT2 (Cool) | — | 69,8% | +7,9 | +2,8 | −5,1 °C |
| VT3 (Warm) | TS_WARM=55°C | 37,7% | +54,5 | +52,2 | −2,3 °C |
| VT5 (Hot) | — | 34,2% | +59,9 | +57,7 | −2,2 °C |
NTC-Modelle: 103AT (B25/50=3435) für TI-Referenz, NCP15XH103F03RC (B25/85=3380) für Inhero. Typische %REGN-Werte aus BQ25798-Datenblatt.
Code-Referenzen¶
BoardConfigContainer::configureBaseBQ()— Wendet alle drei Einstellungen beim Start anBqDriver::setTsWarm()/setJeitaVSet()— Bestehende Driver-APIBqDriver::setAutoIBATDIS()— Zum Driver hinzugefügt (Charger Control 0, Bit 7)
5. Batt-TTL-Prognose¶
Batt-TTL steht für Battery Time-To-Live — die geschätzte Restlaufzeit im Akkubetrieb. Gemeint ist nicht das Hop-Limit, das „TTL“ im Mesh-Netz bezeichnet.
Datenbasis und Zeitbasis¶
Die Batt-TTL-Berechnung basiert auf dem 7-Tage gleitenden Durchschnitt des täglichen Netto-Energieverbrauchs, der aus einem 168-Stunden-Ringpuffer (7 Tage) stündlicher INA228-Coulomb-Counter-Messungen berechnet wird.
Datenfluss¶
INA228 Hardware Coulomb Counter (20-bit ADC, ±0.1% Genauigkeit)
│
▼
updateHourlyStats() — jede Stunde
│ Speichert pro Stunde: charged_mah, discharged_mah, solar_mah
│ in hours[168] Ringpuffer (BatterySOCStats.hours[])
▼
calculateRollingStats() — nach jedem Stunden-Update
│ Summiert letzte 168 Stunden → teilt durch 7
│ → avg_7day_daily_net_mah (= solar − discharged pro Tag)
│ Mindestvoraussetzung: ≥ 24 Stunden gültige Daten
▼
calculateTTL() — nach calculateRollingStats()
│ extractable_mah / |deficit_per_day| × 24 = Batt-TTL Stunden
│ (extractable = gespeichert − eingeschlossene Ladung, siehe Formel unten)
▼
socStats.ttl_hours → getTTL_Hours() → board.stats / Telemetrie
Berechnung¶
Methode: calculateTTL() in BoardConfigContainer.cpp
- Aufgerufen: Nach calculateRollingStats() (stündlich)
- Zeitbasis: 7-Tage gleitender Durchschnitt (avg_7day_daily_net_mah) aus stündlichen Samples
Voraussetzungen für Batt-TTL > 0:
1. living_on_battery == true (24h-Netto ist negativ, d.h. Energiedefizit)
2. avg_7day_daily_net_mah < 0 (7-Tage-Durchschnitt zeigt Netto-Entladung)
3. capacity_mah > 0 (Akkukapazität bekannt, via set board.batcap)
4. Mindestens 24 Stunden gültige Daten im Ringpuffer
Formel (Trapped-Charge-Modell):
remaining_capacity_mah = (SOC% / 100) × capacity_mah
trapped_mah = capacity_mah × (1 − f(T))
extractable_mah = max(0, remaining_capacity_mah − trapped_mah)
daily_deficit_mah = -avg_7day_daily_net_mah (positiver Wert)
TTL_hours = extractable_mah / daily_deficit_mah × 24
temp_derating_factor); f(T) = 1 bei ≥ 25 °C — bei moderaten Temperaturen ist also nichts eingeschlossen und die Formel reduziert sich auf remaining/deficit.
Batt-TTL = 0 bedeutet:
- Gerät wird solar versorgt (Netto-Überschuss) → living_on_battery == false
- Noch keine 24h Daten gesammelt (Kaltstart)
- Akkukapazität unbekannt
Infinite Batt-TTL (Telemetrie):
- Wenn living_on_battery == false und SOC valide → wird als 990 Tage (Max-Wert) übertragen
Beispiel: - SOC: 60% = 1200mAh gespeichert (bei 2000mAh Kapazität) - Temperatur ≥ 25 °C → f(T) = 1, nichts eingeschlossen → entnehmbar = 1200mAh - 7-Tage-Durchschnitt: -100 mAh/Tag (aus 168h Stunden-Samples) - Batt-TTL: 1200 / 100 × 24 = 288 Stunden = 12 Tage
CLI-Ausgabe: board.stats
+150/+120/+90mAh C:200 D:50 3C:180 3D:60 7C:160 7D:70 SOL M:85% BT:N/A ← Solar-Überschuss
-80/-100/-110mAh C:10 D:90 3C:15 3D:115 7C:20 7D:130 BAT M:45% BT:12d0h ← 12 Tage bis leer
6. RTC-Wakeup-Management¶
RV-3028-C7 Integration¶
Pin: GPIO17 (WB_IO1) → RTC INT
Init: InheroMr2Board::begin()
- attachInterrupt(RTC_INT_PIN, rtcInterruptHandler, FALLING)
- Prüft GPREGRET2 für den Wake-up-Grund
Countdown-Timer Konfiguration¶
Methode: configureRTCWake() in InheroMr2Board.cpp
- Tick-Rate: 1/60 Hz (1 Minute pro Tick), konfiguriert via TD=11 in CTRL1
- Max. Countdown: 4095 Minuten ≈ 2,8 Tage (12-bit-Timer-Register)
- Low-Voltage-Sleep-Intervall: LOW_VOLTAGE_SLEEP_MINUTES = 60 min (1h)
- Begründung: Jeder Wake ist ein System-ON-Reset mit Early-Boot-Fast-Path (minimales I2C: RTC-TF clearen, VBAT lesen, wieder schlafen) und kostet nur ~0.03 mAh
Register:
RV3028_CTRL1 (0x0F): TE=1, TD=11 (1/60 Hz), TRPT=0 (Single shot)
RV3028_CTRL2 (0x10): TIE=1 (Timer Interrupt Enable, bit 4)
RV3028_STATUS (0x0E): TF (Timer Flag, bit 3) — nach Wake clearen!
RV3028_TIMER_VALUE_0 (0x0A): Countdown value LSB
RV3028_TIMER_VALUE_1 (0x0B): Countdown value MSB (upper 4 bits)
Interrupt Handler¶
Methode: rtcInterruptHandler() — setzt nur rtc_irq_pending = true.
Der eigentliche TF-Clear passiert im Main-Loop-Kontext in tick() per I2C (Read-Modify-Write, nur das TF-Bit wird gelöscht):
// In InheroMr2Board::tick() — Main-Loop-Kontext:
if (rtc_irq_pending) {
rtc_irq_pending = false;
// RV3028_REG_STATUS lesen ...
uint8_t status = Wire.read();
status &= ~(1 << 3); // Nur TF-Bit löschen → INT-Pin geht via Pull-Up wieder HIGH
Wire.beginTransmission(RTC_I2C_ADDR);
Wire.write(RV3028_REG_STATUS);
Wire.write(status); // zurückschreiben — die übrigen Status-Flags bleiben erhalten
Wire.endTransmission();
}
Warum nicht in der ISR? I2C (Wire) darf nicht aus einem ISR-Kontext aufgerufen werden.
Der ISR setzt nur das Flag; tick() prüft es im Main-Loop.
7. Energieverwaltungsablauf¶
Shutdown-Sequenz (Rev 1.1 — System Sleep mit GPIO-Latch)¶
Methode: initiateShutdown() in InheroMr2Board.cpp
Bei Low-Voltage → System Sleep mit GPIO-Latch (< 500µA, CE-FET hält Zustand):
Ablauf: INA228 ALERT ISR → Flag → tickPeriodic() → board.initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE):
- Background-Tasks stoppen:
BoardConfigContainer::stopBackgroundTasks() - Stoppt Heartbeat-Task (einziger verbleibender FreeRTOS-Task mit GPIO)
-
Disarmt INA228 Low-Voltage Alert (ISR detachen, BUVL deaktivieren)
-
INA228 auf Minimalstrom: ALERT-Pin freigeben (
enableAlert(false, ...),setUnderVoltageAlert(0)— ein LOW gelatchter ALERT würde ~330µA über den Pull-Up verheizen), danachshutdown()(ADC aus, ~3.5µA) -
SX1262 Sleep + PE4259 aus:
inhero::prepareRadioForSystemOff()— zuerstradio.sleep(false)(Cold Sleep via SPI, ~0.16µA), danndigitalWrite(SX126X_POWER_EN, LOW)(PE4259 VDD abschalten) -
LEDs aus: PIN_LED1, PIN_LED2 LOW
-
CE-Pin HIGH latchen (GPIO-Output-Latch für P0.04 erhalten):
digitalWrite(BQ_CE_PIN, HIGH)→ DMN2004TK-7 ON → CE LOW → Laden aktiv- P0.04 wird von
disconnectLeakyPullups()ausgeschlossen → GPIO-Latch bleibt HIGH im System Sleep -
Ohne Latch: ext. Pull-Down am Gate → FET OFF → Pull-Up am CE → CE HIGH → Laden AUS
-
INA228 + BQ25798 auf Minimalstrom:
inhero::prepareIcsForSystemOff()(Raw-I2C-Sicherheitsnetz, wiederholt den INA228-Shutdown mit Readback) -
BME280 schlafen legen: Sleep-Modus per I2C erzwingen (spart ~1–7µA; harmloser NACK, wenn nicht bestückt)
-
RTC-Wake konfigurieren:
configureRTCWake(LOW_VOLTAGE_SLEEP_MINUTES)(60 min) -
P0-LATCH für den RTC-INT-Pin löschen (ein stehengebliebener Latch würde DETECT sofort auslösen → sofortiger Wake → Boot-Schleife)
-
I2C freigeben:
Wire.end(), danachinhero::disconnectLeakyPullups()(jeder LOW gehaltene Pull-Up verheizt ~250µA) -
Shutdown-Grund speichern:
NRF_POWER->GPREGRET2 = GPREGRET2_LOW_VOLTAGE_SLEEP | reason -
System Sleep mit GPIO-Latch:
sd_power_system_off()→ nRF52840 System-Off (< 500µA gesamt)- GPIO4-Latch erhalten (von disconnectLeakyPullups ausgeschlossen) → FET bleibt ON → CE LOW → Laden aktiv
- RAM-Inhalt geht verloren (168h-Statistiken, SOC, etc.)
- RTC-Interrupt auf GPIO17 weckt System nach Timer-Ablauf
Der SOC wird beim Shutdown nicht geschrieben — beim nächsten erfolgreichen Recovery-Boot ruft begin() setSOCManually(0.0) auf (Low-Voltage-Recovery), der SOC startet also bei 0%.
Warum System Sleep mit GPIO-Latch? - DMN2004TK-7 FET für CE-Pin → GPIO4-Latch HIGH erhalten → FET ON → CE LOW → Laden aktiv - Gesamtverbrauch: < 500µA (nRF52840 System-Off + RTC + quiescent currents aller Komponenten)
168h-Statistiken gehen bei System Sleep verloren — es existiert kein Persistenzmechanismus für die Ring-Buffer-Daten. Nach Recovery starten die Statistiken bei Null.
Wake-up-Check (Anti-Motorboating)¶
Methode: InheroMr2Board::begin()
Der Code prüft GPREGRET2 für den Shutdown-Grund und die Akkuspannung für Wake-up-Entscheidungen.
2 Fälle:
Fall 1: Wake aus dem Low-Voltage-Sleep ((GPREGRET2 & 0x03) == SHUTDOWN_REASON_LOW_VOLTAGE)
// InheroMr2Board::begin() — Early-Boot-Fast-Path (vereinfacht)
uint8_t shutdown_reason = NRF_POWER->GPREGRET2;
if ((shutdown_reason & 0x03) == SHUTDOWN_REASON_LOW_VOLTAGE) {
Wire.begin();
inhero::clearTimerFlag(); // Wake war ein Reset — der ISR hat das RTC-Event nie gesehen
uint16_t vbat_mv = Ina228Driver::readVBATDirect(&Wire, INA228_I2C_ADDR);
uint16_t wake_threshold = getLowVoltageWakeThreshold();
if (vbat_mv == 0 || vbat_mv < wake_threshold) {
// Spannung noch zu niedrig → zurück in den System Sleep.
// Der Wake-Reset hat alle PIN_CNF gelöscht — der GPIO-Latch aus dem Sleep
// überlebt ihn NICHT. CE muss neu als OUTPUT HIGH getrieben werden,
// sonst stoppt das Solar-Laden.
pinMode(BQ_CE_PIN, OUTPUT);
digitalWrite(BQ_CE_PIN, HIGH);
inhero::prepareIcsForSystemOff(); // INA228 + BQ25798 auf Minimalstrom
inhero::prepareRadioForSystemOff(false); // SX1262 zurück in Cold Sleep
configureRTCWake(LOW_VOLTAGE_SLEEP_MINUTES);
inhero::disconnectLeakyPullups();
NRF_POWER->GPREGRET2 = GPREGRET2_LOW_VOLTAGE_SLEEP | SHUTDOWN_REASON_LOW_VOLTAGE;
sd_power_system_off(); // Bleibt im Low-Voltage-Sleep-Zyklus
}
// Spannung OK → normaler Boot; Low-Voltage-Recovery-Markierung + SOC=0%
// folgen erst nach boardConfig.begin()
NRF_POWER->GPREGRET2 = SHUTDOWN_REASON_NONE;
}
Fall 2: Normaler Kaltstart (Power-On, Reset-Taste, Spannung OK)
else {
// Normaler Boot wird fortgesetzt
// INA228 und alle anderen Komponenten werden initialisiert
}
Direkter ADC-Read (boardConfig noch nicht bereit):
// Muss direkt aus den INA228-ADC-Registern lesen (20-Bit-ADC, linksbündig im 24-Bit-Register, ±0,1 % Genauigkeit)
uint16_t vbat_mv = Ina228Driver::readVBATDirect(&Wire, INA228_I2C_ADDR);
Spannungsschwellen (chemie-spezifisch, 1-Level-System): | Chemie | lowv_sleep_mv (ALERT) | lowv_wake_mv (Recovery) | Hysterese | |--------|----------------------|------------------------|-----------| | Li-ion 1S | 3100 | 3300 | 200mV | | LiFePO4 1S | 2700 | 2900 | 200mV | | LTO 2S | 3900 | 4100 | 200mV | | Na-ion 1S | 2500 | 2700 | 200mV |
Anti-Motorboating: Der Early-Boot-Check in begin() verhindert, dass das System bei knapper Spannung immer wieder bootet und sofort abstürzt. Erst wenn VBAT über lowv_wake_mv liegt, wird normal gebootet.
Stromverbrauch im System Sleep mit GPIO-Latch (Low-Voltage Sleep): - Gesamt: < 500µA (nRF52840 System-Off + RTC + quiescent currents aller Komponenten) - CE-FET: GPIO4-Latch HIGH erhalten → FET ON → CE LOW → Solar-Laden aktiv
8. INA228 ALERT-Pin (Rev 1.1)¶
Verdrahtung¶
Pin: INA228 ALERT → P1.02 (nRF52840 GPIO, mit ext. Pull-Up) TPS62840 EN: Via 3.3V_off-Schalter geschaltet
Funktionsweise¶
Der ALERT-Pin wird als Software-Interrupt genutzt:
armLowVoltageAlert()konfiguriert INA228 BUVL (Bus Under-Voltage Limit) auflowv_sleep_mv- ALERT feuert als FALLING-Edge-Interrupt auf P1.02
- ISR (
lowVoltageAlertISR()) setztlowVoltageAlertFired = true(nur Flag, kein FreeRTOS-Aufruf) tickPeriodic()prüft Flag im nächsten Main-Loop-Tick und ruftinitiateShutdown()→ System Sleep
Kein Latch-Problem: Da der ALERT nicht an TPS62840 EN geht, gibt es kein latched-off-Verhalten.
Das System kann nach RTC-Wake normal booten und die Spannung in begin() prüfen.
9. SX1262 Power Control & PE4259 RF-Switch¶
Hardware-Architektur¶
- SX1262: LoRa Transceiver (SPI-Bus), Sleep-Mode via
SetSleepSPI-Befehl - PE4259: SPDT RF-Antennenweiche im Single-Pin-Modus:
- Pin 6 (VDD): GPIO 37 (P1.05,
SX126X_POWER_EN) — Stromversorgung (muss HIGH sein für Betrieb) - Pin 4 (CTRL): SX1262 DIO2 — TX/RX Umschaltung (automatisch via
setDio2AsRfSwitch(true))
Shutdown-Sequenz (in initiateShutdown())¶
Die SX1262 wird in zwei Schritten abgeschaltet — Reihenfolge ist kritisch:
// Schritt 1: SX1262 in Cold Sleep via SPI (MUSS zuerst!)
radio_driver.powerOff(); // → radio.sleep(false) → SPI SetSleep command
delay(10);
// Schritt 2: PE4259 RF-Switch Stromversorgung abschalten
digitalWrite(SX126X_POWER_EN, LOW); // VDD weg → PE4259 aus
Warum diese Reihenfolge?
- radio.sleep(false) sendet einen SPI-Befehl an den SX1262 → sauberer Radio-Shutdown
- PE4259 VDD (GPIO 37) versorgt den RF-Switch, NICHT den SX1262 direkt
- SPI wird über den nRF52840 3.3V Rail versorgt, nicht über PE4259
- Sicherheitshalber: Erst SX1262 schlafen legen, dann PE4259 abschalten
Boot-Sequenz (in begin())¶
// PE4259 VDD einschalten → RF-Switch betriebsbereit
pinMode(SX126X_POWER_EN, OUTPUT);
digitalWrite(SX126X_POWER_EN, HIGH);
delay(10); // PE4259 Einschaltzeit
// Später in radio_init() → target.cpp:
radio.std_init(&SPI); // → setDio2AsRfSwitch(true) → DIO2 steuert TX/RX
Wichtige Details:
- SX126X_POWER_EN (GPIO 37 / P1.05) steuert die PE4259 VDD, NICHT die SX1262 Power
- DIO2 wird intern vom SX1262 gesteuert (setDio2AsRfSwitch(true)) — kein GPIO nötig
- Sleep-Strom SX1262: ~0.16µA (Cold Sleep) — Datenblatt-Wert
- Ohne radio_driver.powerOff(): SX1262 bleibt im RX-Modus → ~5mA Stromverbrauch!
10. BQ25798 CE-Pin Safety (Rev 1.1 — FET-invertiert)¶
Problem¶
Der BQ25798 startet mit Default-Konfiguration (1S Li-ion, 4.2V Ladespannung). Wenn ein LiFePO4-Akku (3.5V max) verbunden ist und der RAK noch nicht gebootet hat, würde der BQ25798 den Akku überladen → Brandgefahr.
Hardware-Design (Rev 1.1 — FET-invertiert)¶
- Pin:
BQ_CE_PIN= GPIO 4 (P0.04 / WB_IO4) - DMN2004TK-7 N-FET: Gate ← GPIO4 (ext. Pull-Down), Drain → CE, Source → GND
- Externer Pull-Down am Gate: Zieht Gate LOW wenn GPIO floated → FET OFF
- Externer Pull-Up am CE: 10kΩ zu VSYS → CE HIGH wenn FET OFF → Laden AUS (BQ25798 CE active-low)
- GPIO HIGH → FET ON → CE an GND (LOW) → Laden AN
- GPIO LOW → Pull-Down am Gate → FET OFF → Pull-Up am CE → CE HIGH → Laden AUS
- GPIO High-Z (stromlos/Reset) → Pull-Down am Gate → FET OFF → Pull-Up am CE → CE HIGH → Laden AUS
Kernpunkt Rev 1.1: Laden ist nur aktiv, wenn GPIO4 HIGH getrieben wird (durch Firmware oder GPIO-Output-Latch im System Sleep). Wenn der RAK stromlos oder ungeflasht ist, sorgt der externe Pull-Down für FET OFF → CE HIGH → Laden deaktiviert — ein bewusstes Safety-Feature.
3-Schicht-Sicherung (Rev 1.1)¶
| Schicht | Ort | Mechanismus | Wann |
|---|---|---|---|
| 1. Hardware (passiv) | Pull-Down + Pull-Up | RAK stromlos → Pull-Down am Gate → FET OFF → Pull-Up am CE → CE HIGH → Laden AUS | Immer (Safety-Default) |
| 2. Early Boot | InheroMr2Board::begin() |
GPIO4 noch nicht getrieben → FET OFF → CE HIGH → Laden AUS bis Firmware konfiguriert | Vor I2C-Init |
| 3. Chemie-Konfiguration | configureChemistry() |
GPIO HIGH → FET ON → CE LOW → Laden AN + I2C Register bei bekannter Chemie | Nach BQ25798-Konfiguration |
Dual-Layer Safety (Hardware + Software)¶
// In configureChemistry() — nach BQ25798 Register-Konfiguration:
bq.setChargeEnable(props->charge_enable); // Software-Schicht (I2C Register)
#ifdef BQ_CE_PIN
pinMode(BQ_CE_PIN, OUTPUT);
// Rev 1.1 FET-invertiert: HIGH → FET ON → CE LOW → Laden aktiv (BQ25798: CE active-low)
// FET OFF → Pull-Up am CE → CE HIGH → Laden deaktiviert (Safety-Default)
digitalWrite(BQ_CE_PIN, props->charge_enable ? HIGH : LOW); // HIGH=FET ON=CE LOW=Laden an
#endif
charge_enableist Teil derBatteryProperties-TabelleBAT_UNKNOWN→charge_enable = false→ GPIO LOW → FET OFF → CE HIGH → Laden deaktiviert + Register disabled- Bekannte Chemie →
charge_enable = true→ GPIO HIGH → FET ON → CE LOW → Laden aktiviert + Register enabled
Verhalten im System Sleep mit GPIO-Latch (Rev 1.1)¶
In Rev 1.1 wird System Sleep mit GPIO-Latch verwendet (via initiateShutdown()):
- digitalWrite(BQ_CE_PIN, HIGH) wird vor System Sleep aufgerufen
- P0.04 wird von disconnectLeakyPullups() ausgeschlossen → GPIO-Output-Latch bleibt HIGH
- GPIO4 gelatcht HIGH → DMN2004TK-7 FET ON → CE LOW → Laden aktiv
- BQ25798 MPPT/CC/CV läuft autonom in Hardware → Solar-Laden möglich
- Stromverbrauch: < 500µA (nRF52840 System-Off + RTC + quiescent currents aller Komponenten)
| Zustand | CE-Pin | Laden | Solar-Recovery |
|---|---|---|---|
| RAK stromlos (kein Akku) | HIGH (Pull-Up, FET OFF) | Deaktiviert (Safety-Default) | N/A |
| Early Boot | HIGH (Pull-Up, GPIO nicht getrieben) | Deaktiviert (noch nicht konfiguriert) | Nein |
| BAT_UNKNOWN | HIGH (GPIO LOW → FET OFF) | Deaktiviert (CE + I2C Register) | Nein |
| Chemie konfiguriert | LOW (GPIO HIGH → FET ON) | Aktiv | Ja |
| System Sleep (Low-Voltage) | LOW (GPIO-Latch HIGH → FET ON) | Aktiv | Ja |
11. Statistik-Persistenz¶
Aktueller Stand¶
Die 168h-Ringpuffer-Statistiken (Coulomb Counter, MPPT-Daten, SOC-Zustand) sind nur im RAM gespeichert und gehen bei jedem Reboot verloren — egal ob System Sleep oder Cold Boot. Es existiert kein Persistenzmechanismus (weder .noinit-Section noch LittleFS-Snapshot).
Persistente Daten (überleben Reboots via LittleFS):
- Akkutyp (batType)
- Akkukapazität (batCap)
- NTC-Kalibrierung (tcCal)
- MPPT-Einstellung (mpptEn)
- Frostverhalten (frost)
- Max. Ladestrom (maxChrg)
- LED-Einstellung (leds_en)
Nicht-persistente Daten (gehen bei Reboot verloren): - 168h Energie-Ringpuffer (stündliche Charge/Discharge/Solar mAh) - MPPT-Statistiken (168h MPPT-Aktivitätsbuffer) - SOC-Prozentwert (wird nach Recovery auf 0% gesetzt, bei "Charging Done" auf 100% synchronisiert) - Batt-TTL-Berechnung (benötigt mind. 24h Daten nach jedem Neustart) - Tägliche Energiebilanz (7-Tage-Fenster baut sich nach Neustart neu auf)
Die INA228-Kalibrierung (SHUNT_CAL aus CURRENT_LSB und dem 100mΩ-Shunt) wird bei jedem Boot in Ina228Driver::begin() aus festen Konstanten berechnet und braucht deshalb keine Persistenz. Einen Laufzeit-Korrekturfaktor gibt es nicht mehr — bei Rev 1.1 ist er durch das PCB-Layout und die Shunt-Toleranz entbehrlich.
12. CLI-Befehle¶
Getter¶
board.bat # Akkutyp abfragen
# Ausgabe: liion1s | lifepo1s | lto2s | naion1s | none
board.fmax # Frost-Ladeverhalten abfragen
# Ausgabe: 0% | 20% | 40% | 100% (LTO/Na-ion: N/A)
board.imax # Maximaler Ladestrom abfragen
# Ausgabe: <strom>mA (z.B. 500mA)
board.mppt # MPPT-Status abfragen
# Ausgabe: MPPT=1 | MPPT=0
board.telem # Echtzeit-Telemetrie mit SOC
# Ausgabe: B:<V>V/<I>mA/<T>C SOC:<Prozent>% S:<V>V/<SolarStrom>
# Beispiel: B:3.85V/125.4mA/22C SOC:68.5% S:5.12V/385mA
# Beispiel: B:3.85V/-8.2mA/N/A SOC:N/A S:0.00V/0mA
board.stats # Energie-Statistiken (Bilanz + MPPT + Batt-TTL)
# Ausgabe: <24h>/<3d>/<7d>mAh C:<24h> D:<24h> 3C:<3d> 3D:<3d> 7C:<7d> 7D:<7d> <SOL|BAT> M:<mppt>% BT:<ttl>
# Beispiel: +125/+45/+38mAh C:200 D:75 3C:150 3D:105 7C:140 7D:102 SOL M:85% BT:N/A
# Beispiel: -30/-45/-40mAh C:10 D:40 3C:5 3D:50 7C:8 7D:48 BAT M:45% BT:12d0h
# SOL = Solar-Überschuss, BAT = Energiedefizit
# BT: Batt-TTL (N/A bei Solar-Überschuss oder <24h Daten)
board.cinfo # Ladegerät-Info + letzter PG-Stuck HIZ-Toggle
# Ausgabe: "PG / CC HIZ:never" oder "!PG / !CHG HIZ:3m ago"
board.selftest # I²C-Hardware-Probe (alle Onboard-Komponenten)
# Ausgabe: "INA:OK BQ:OK RTC:OK BME:OK"
# States je Gerät: OK | NACK | WR_FAIL (nur RTC)
board.conf # Alle Konfigurationswerte
# Ausgabe: B:<bat> F:<fmax> M:<mppt> I:<imax> Vco:<V> V0:<V>
# Beispiel: B:liion1s F:0% M:1 I:500mA Vco:4.10 V0:3.30
board.tccal # NTC-Temperatur-Kalibrieroffset
# Ausgabe: TC offset: +0.00 C (0.00=default)
board.leds # LED-Aktivstatus (Heartbeat + BQ Stat)
# Ausgabe: "LEDs: ON (Heartbeat + BQ Stat)"
board.batcap # Akkukapazität
# Ausgabe: 10000 mAh (gesetzt) oder 2000 mAh (Default; LiFePO4-Default 1500 mAh)
Setter¶
set board.bat <type> # Akkuchemie setzen
# Optionen: liion1s | lifepo1s | lto2s | naion1s | none
set board.fmax <wert> # Frost-Ladestromabsenkung setzen
# Optionen: 0% | 20% | 40% | 100%
# Begrenzt Ladestrom im T-Cool-Bereich (ca. -2 °C bis +3 °C, siehe JEITA-Tabelle im README)
# Bei LTO / Na-ion ohne Wirkung (JEITA deaktiviert)
set board.imax <mA> # Maximalen Ladestrom setzen
# Bereich: 50-1500 mA
set board.mppt <0|1> # MPPT ein-/ausschalten
set board.batcap <mAh> # Akkukapazität setzen
# Bereich: 100-100000 mAh
set board.tccal # NTC-Temperatur kalibrieren (auto via BME280)
set board.tccal reset # Offset auf 0.00 zurücksetzen
set board.leds <on|off> # LEDs ein-/ausschalten (on/1, off/0)
set board.soc <percent> # SOC manuell setzen (0-100, INA228 muss bereit sein)
Dateien-Übersicht¶
Hauptimplementierung¶
| Datei | Beschreibung |
|---|---|
| InheroMr2Board.h/cpp | Board-Klasse, Init, Shutdown, RTC, CLI-Commands |
| BoardConfigContainer.h/cpp | Battery Management, BQ25798, INA228, MPPT, SOC, Daily Balance |
| lib/Ina228Driver.h/cpp | INA228 I2C Communication, Calibration, Coulomb Counter |
| lib/BqDriver.h/cpp | BQ25798 I2C Communication, MPPT, Charging |
Schlüssel-Methoden¶
| Methode | Datei | Funktion |
|---|---|---|
begin() |
InheroMr2Board.cpp | Board-Initialisierung, Wake-up-Check, Early-Boot-Low-Voltage-Check |
initiateShutdown() |
InheroMr2Board.cpp | System-Sleep-Shutdown (aufgerufen von tickPeriodic nach ALERT) |
configureRTCWake() |
InheroMr2Board.cpp | RTC-Countdown-Timer |
rtcInterruptHandler() |
InheroMr2Board.cpp | RTC-INT-ISR (setzt Flag) |
queryBoardTelemetry() |
InheroMr2Board.cpp | CayenneLPP-Telemetrie-Erfassung |
getLowVoltageSleepThreshold() |
InheroMr2Board.cpp | Chemie-spezifische Sleep-Spannung (INA228 ALERT) |
getLowVoltageWakeThreshold() |
InheroMr2Board.cpp | Chemie-spezifische Wake-Spannung (0% SOC) |
armLowVoltageAlert() |
BoardConfigContainer.cpp | INA228-BUVL-Alert armen + ISR registrieren |
disarmLowVoltageAlert() |
BoardConfigContainer.cpp | INA228-Alert disarmen + ISR detachen |
lowVoltageAlertISR() |
BoardConfigContainer.cpp | ISR: setzt lowVoltageAlertFired-Flag (geprüft in tickPeriodic) |
tickPeriodic() |
BoardConfigContainer.cpp | Main-Loop-Dispatch: MPPT (60s), SOC (60s), stündlich (60min), Low-V-Check |
runMpptCycle() |
BoardConfigContainer.cpp | Einzelner MPPT-Zyklus (Solar-Checks, MPPT-Recovery) |
updateBatterySOC() |
BoardConfigContainer.cpp | Coulomb-Counter-SOC-Berechnung |
updateHourlyStats() |
BoardConfigContainer.cpp | Stündliche Abtastung in den 168h-Ringpuffer |
calculateRollingStats() |
BoardConfigContainer.cpp | Rollierende 24h/3d/7d-Summen + living_on_battery |
calculateTTL() |
BoardConfigContainer.cpp | Batt-TTL-Prognose |
Ina228Driver::begin() |
lib/Ina228Driver.cpp | 100mΩ-Kalibrierung, ADC-Konfiguration |
Ina228Driver::readVBATDirect() |
lib/Ina228Driver.cpp | Statischer Early-Boot-VBAT-Read |
Code-Fragmente (Key Sections)¶
INA228 Shutdown Mode¶
// Ina228Driver.cpp — liefert bool: false, wenn der INA228 im Continuous-Modus bleibt
bool Ina228Driver::shutdown() {
// Betriebsmodus auf Shutdown setzen (MODE = 0x0)
// Deaktiviert alle Konvertierungen und den Coulomb Counter.
// Bis zu 3 Versuche mit Readback — I2C-Writes können still fehlschlagen.
uint16_t adc_config = 0x0000; // MODE = 0x0 (Shutdown)
// ... Write + Readback-Retry-Schleife, prüft MODE-Bits [15:12] ...
}
INA228 Wake-up¶
// Ina228Driver.cpp
void Ina228Driver::wakeup() {
// Continuous-Messmodus mit voller ADC-Konfiguration reaktivieren
// Konvertierungszeiten aus begin() müssen wiederhergestellt werden —
// die Defaults sind deutlich kürzer (50µs)
uint16_t adc_config = (INA228_ADC_MODE_CONT_ALL << 12) | // MODE: Continuous all
(INA228_ADC_CT_2074us << 9) | // VBUSCT: 2074µs
(INA228_ADC_CT_4120us << 6) | // VSHCT: 4120µs
(INA228_ADC_CT_540us << 3) | // VTCT: 540µs
(INA228_ADC_AVG_256 << 0); // AVG: 256 Samples (TX-Peak-Filterung)
writeRegister16(INA228_REG_ADC_CONFIG, adc_config);
}
RTC Interrupt Handler¶
// InheroMr2Board.cpp — ISR setzt nur Flag, kein I2C!
void InheroMr2Board::rtcInterruptHandler() {
rtc_irq_pending = true;
}
// TF-Clear passiert im Main-Loop-Kontext (tick())
INA228 Driver Zugriff¶
// Direkter Zugriff auf INA228 Driver
if (boardConfig.getIna228Driver() != nullptr) {
// INA228 specific code
}
Szenarien¶
Szenario A: Normale Entladung (Low-Voltage System Sleep) - Li-ion¶
t=0: VBAT = 3.7V → Normal (60s checks, Coulomb Counter läuft)
Daily balance: Today +150mAh SOLAR
t=+1h: VBAT = 3.5V → Normal (INA228 ALERT nicht getriggert)
SOC: 45%
t=+2h: VBAT = 3.08V → INA228 ALERT feuert (< 3100mV lowv_sleep_mv)
- lowVoltageAlertISR() → setzt lowVoltageAlertFired Flag
- tickPeriodic() erkennt Flag im nächsten tick()
- board.initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE)
- CE gelatcht (GPIO4-Latch HIGH → FET ON → CE LOW → Laden aktiv)
- RTC: Wake in 1h (LOW_VOLTAGE_SLEEP_MINUTES = 60)
- sd_power_system_off() → System Sleep mit GPIO-Latch (< 500µA)
t=+3h: RTC weckt → System bootet → Early Boot Check
- Ina228Driver::readVBATDirect() → VBAT = 3.15V
- VBAT < lowv_wake_mv (3300mV) → sofort wieder schlafen
- configureRTCWake(60) + sd_power_system_off()
t=+4h: RTC weckt → System bootet → Early Boot Check
- VBAT = 3.20V → noch unter 3300mV → wieder schlafen
t=+5h: RTC weckt → System bootet → Early Boot Check
- VBAT = 3.45V (Solar-Recovery!)
- VBAT > lowv_wake_mv (3300mV) → normaler Boot
- Low-Voltage-Recovery markiert, SOC bei 0%
- Coulomb Counter startet neu
- Daily balance baut sich neu auf
Szenario B: Kritische Entladung (Rev 1.1 — kein Hardware-UVLO)¶
In Rev 1.1 gibt es kein Hardware-UVLO (TPS62840 EN via 3.3V_off-Schalter).
Der INA228 ALERT auf P1.02 dient als Software-Interrupt für System Sleep.
t=0: VBAT = 3.08V → INA228 ALERT feuert
- tickPeriodic() → initiateShutdown()
- System Sleep mit GPIO-Latch (< 500µA), CE gelatcht LOW (Laden aktiv), RTC-Wake 1h
t=+1h: RTC-Wake → Early Boot → VBAT = 3.05V (noch unter 3300mV)
- Sofort wieder schlafen (CE bleibt gelatcht LOW → Solar-Laden möglich)
t=+2h: RTC-Wake → VBAT = 2.95V (weiter gesunken, kein Solar)
- Sofort wieder schlafen
- Board pendelt weiter mit < 500µA + stündlichem Boot (~0.03mAh)
t=+∞: Bei < 500µA kann der Akku monatelang überleben
- Sobald Solar verfügbar → VBAT steigt → normaler Boot bei >3300mV
- KEIN Latching: System kann IMMER von selbst recovern
Szenario C: Energiebilanz-Tracking - LiFePO4¶
Tag 0: VBAT = 3.2V, SOC = 85%
24 Stunden-Einträge landen in hours[]: Σ geladen +800mAh (Solar), Σ entladen -450mAh
last_24h_net = +350mAh → SOLAR
Tag 1: VBAT = 3.15V, SOC = 72%
Geladen: +650mAh, Entladen: -520mAh
last_24h_net = +130mAh → SOLAR
Tag 2: VBAT = 3.05V, SOC = 58%
Geladen: +200mAh (dichte Bewölkung), Entladen: -480mAh
last_24h_net = -280mAh → BAT (living_on_battery = true)
3-Tage-Durchschnitt: (350+130-280)/3 = +66.7 mAh/Tag
7-Tage-Durchschnitt: (350+130-280)/7 = +28.6 mAh/Tag
(168h-Fenster noch teilgefüllt — die Summe wird immer durch 7 geteilt)
→ 7-Tage-Durchschnitt positiv → Batt-TTL bleibt 0 (Anzeige N/A)
Tag 3: VBAT = 2.95V, SOC = 42%
Geladen: +150mAh (dichte Bewölkung), Entladen: -500mAh
last_24h_net = -350mAh → BAT
3-Tage-Durchschnitt: (130-280-350)/3 = -166.7 mAh/Tag
7-Tage-Durchschnitt: (350+130-280-350)/7 = -21.4 mAh/Tag → negativ → Batt-TTL wird berechnet
living_on_battery = true
Batt-TTL-Berechnung (Basis 7-Tage-Durchschnitt, ≥25 °C → f(T)=1, nichts eingeschlossen):
gespeichert = 42% × 1500mAh = 630mAh
Defizit = |-21.4| = 21.4 mAh/Tag
Batt-TTL = (630 / 21.4) × 24 ≈ 706 Stunden ≈ 29.4 Tage
CLI-Ausgabe: "-350/-167/-21mAh C:150 D:500 3C:.. 3D:.. 7C:.. 7D:.. BAT M:45% BT:29d10h"
Siehe auch¶
- README.md — Benutzer-Dokumentation und CLI-Referenz
- DATASHEET.md — Hardware-Spezifikationen und Pinout
- TELEMETRY.md — Telemetrie-Kanäle erklärt (was die App anzeigt)
- QUICK_START.md — Inbetriebnahme und Konfiguration
- BATTERY_GUIDE.md — Akkuchemie-Vergleich und Einsatzempfehlungen
- FAQ.md — Häufig gestellte Fragen
- CLI_CHEAT_SHEET.md — Alle CLI-Befehle auf einen Blick
Datasheets¶
- INA228: https://www.ti.com/product/INA228
- RV-3028-C7: https://www.microcrystal.com/en/products/real-time-clock-rtc-modules/rv-3028-c7/
- BQ25798: https://www.ti.com/product/BQ25798
- TPS62840: https://www.ti.com/product/TPS62840
- nRF52840: https://www.nordicsemi.com/products/nrf52840