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
- NTC-Plausibilitätsprüfung (BME280)
- JEITA-WARM-Zone & VBAT_OVP-Vermeidung
- JEITA-Override (set board.jeitaignore)
- 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 N-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 - 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(H) | Core Module | — | — | nRF52840 SoC + SX1262 LoRa Transceiver, Hochband-Variante (siehe DATASHEET.md — LoRa-Frequenzbänder) |
| 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 N-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 |
| CE-FET | N-FET | — | Gate←GPIO4 (ext. Pull-Down) | 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 ISR-Flag und ALERT-Pegel im Main-Loop →board.initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE) - Absicherung: Einmal pro Sekunde wird die gemittelte INA228-Akkuspannung mit der aktiven Sleep-Schwelle verglichen, unabhängig von SOC und CLI-Abfragen. Fehlgeschlagene Messungen (0 mV) werden ignoriert. Zur Reaktionszeit kommen die ADC-Mittelung und die Abschaltsequenz hinzu.
- Arming:
armLowVoltageAlert(type)erhält bei einer Konfigurationsänderung direkt den neuen Akkutyp, setzt BUVL und aktiviert die ISR. Ein bereits anliegender LOW-Pegel wird nach der Interruptregistrierung erfasst.nonedeaktiviert Interrupt und Spannungsprüfung und löscht einen ausstehenden Alarm.
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
Minimale Systemspannung und MPPT¶
Die Firmware setzt VSYSMIN auf 2,50 V (REG00[5:0] = 0), auch nach jedem
CELL-Schreibzugriff. Das Schreiben der Zellzahl setzt VSYSMIN, VREG und ICHG auf
die zellzahlabhängigen Standardwerte zurück. Alle drei wiederherzustellen
verhindert den früheren LTO-2S-Hitzefehler durch linearen BATFET-Betrieb.
Die thermische Regelgrenze des Chips bleibt bei 60 °C.
Unterhalb von VSYSMIN arbeitet der BQ25798 in der Minimal-Systemspannungsregelung und lässt Hardware-MPPT nicht zu. Die bisherigen 2,75 V blockierten deshalb MPPT bei einer Na-Ion-Zelle mit 2,62–2,65 V. Die gespeicherte MPPT-Einstellung wird nun nach dem Wiederherstellen der Chemieparameter angewendet. Die Sleep-/Wake-Schwellen des Akkus ändern sich dadurch nicht.
get board.mppt zeigt die gespeicherte Einstellung. get board.mpptdiag
vergleicht sie mit dem tatsächlichen Hardware-Bit und liest die Spannungs- und
Stromeinstellungen zurück. MPPT:cfg=1/hw=0 bedeutet gewünscht an, Hardware aus;
VSYS_MIN:1 zeigt die Minimal-Systemspannungsregelung an. VSYSMIN, VINDPM und VREG
stehen in mV, ICHG in mA. Die Diagnose liest nur Register und meldet fehlgeschlagene
Lesezugriffe als Fehler.
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: PG-Recovery — ein HIZ-Toggle stößt die Input-Qualifikation erneut an. Eine VBUS-Messung ist dafür keine Voraussetzung: Bei niedriger VBAT und HIZ=1 kann gerade der ADC ausfallen, den die frühere Prüfung benötigt hat. Der BQ prüft selbst, ob die Quelle ausreichend ist; auch nachts ist ein Versuch zulässig. Voraussetzungen sind ein antwortender BQ sowie aktiviertes MPPT und Ladefreigabe laut Batterie-Konfiguration. Zwischen Versuchen liegen mindestens 5 Minuten, auch wenn ein Registerzugriff während des Versuchs fehlschlägt.
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-Flag/Pegel und 1s-Spannungsprüfung → 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 SLUSDV2B), Abschnitt 9.3.10 — ADC; in SLUSDV2C Abschnitt 7.3.10.
Problem¶
Der 15-bit ADC im BQ25798 hat spannungsabhängige Betriebsschwellen. Bei unzureichender Versorgung kann die Konvertierung ausbleiben. Im beobachteten Zustand mit niedriger VBAT und HIZ=1 blieb ADC_EN trotz Startanforderung gelöscht; deshalb genügt dieses Bit allein nicht als Abschlussnachweis.
Datasheet-Zitat (Abschnitt 9.3.10, Rev. B)¶
"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 |
|---|---|---|---|---|---|
| Solarquelle qualifiziert (PG=1) | > 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: TS-Kanal-Steuerung¶
Die Firmware liest die aktuelle Akkuspannung vom INA228, übergibt sie an
BqDriver::getTelemetryData(vbat_mv) und prüft zusätzlich das Power-Good-Bit des Ladereglers:
- VBAT ≥ 3.2V (oder unbekannt): TS-Kanal aktiviert → ADC-Schwelle 3.2V, Temperatur verfügbar
- VBAT < 3.2V mit qualifizierter Eingangsquelle (PG = 1): TS-Kanal bleibt aktiv — der ADC läuft über VBUS, die Temperatur bleibt verfügbar
- VBAT < 3.2V im reinen Akkubetrieb: 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.
Die 3,2-V-Bedingung gilt für den reinen Akkubetrieb (SLUSDV2B 9.3.10). Bei qualifizierter Eingangsquelle bleibt TS auch mit niedriger VBAT aktiviert, damit die Akkutemperatur während des Ladens verfügbar ist.
Der Akkutyp geht nie in die Entscheidung ein. getTelemetryData() startet für jede Chemie mit ts_enabled = true; der Treiber kennt die konfigurierte Chemie nicht und hat kein „NTC bestückt"-Flag. Ob eine Akkutemperatur verfügbar ist, hängt an VBAT, an der Eingangsquelle und an der Plausibilitätsprüfung weiter unten.
Praktische Folge je Chemie, weil die 3,2-V-Marke an verschiedenen Stellen der Entladekurve liegt — all das gilt für den reinen Akkubetrieb, beim Laden über das Modul ist die Temperatur durchgehend verfügbar:
| Chemie | Nominal | TS-Kanal über die Entladekurve (reiner Akkubetrieb) |
|---|---|---|
| LTO 2S | 4,6V | liegt immer über 3,2V → Temperatur durchgehend verfügbar |
| Li-ion 1S | 3,7V | über weite Teile der Kurve über 3,2V (Sleep-Schwelle 3100mV) |
| LiFePO4 1S | 3,2V | großer Teil des flachen Plateaus liegt auf oder unter 3,2V → häufig N/A |
| Na-ion 1S | 3,1V | großer Teil der Kurve unter 3,2V → häufig N/A |
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) | 0x58 |
0x5C |
IBUS, VBUS, TDIE, (TS) |
| 0x30 (ADC_FUNCTION_DISABLE_1) | 0xF0 |
0xF0 |
keine (D+/D−/VAC disabled) |
TDIE (Bit 1) ist in beiden Werten gelöscht, der Chip konvertiert seine eigene Silizium-Die-Temperatur also in jedem One-Shot. Daher stammt das TDIE:-Feld von board.cinfo — es ist die Sperrschichttemperatur des BQ25798, nicht die des Akkus, nicht die der Platine und nicht die des MCU.
Im One-Shot-Modus bestätigt ein frisches ADC_DONE_FLAG bei gelöschtem ADC_EN den Abschluss der aktivierten Kanäle. ADC_EN=0 allein genügt nicht. Nicht benötigte Kanäle werden deaktiviert, um Messzeit zu sparen.
Frische Solarmessungen¶
Vor jedem One-Shot stoppt die Firmware eine mögliche vorherige Konvertierung,
löscht alte Abschlussflags und prüft die Kanalmaske. Unmittelbar vor ADC_EN=1
wird HIZ auf 0 gesetzt. Es gibt keine zusätzliche Anlaufpause und keine erneute
HIZ-Freigabe innerhalb derselben Messung, falls der BQ HIZ selbst wieder setzt.
CE, EN_CHG und die gespeicherte Konfiguration werden dabei nicht geändert.
Eine Messung gilt nur dann als frisch, wenn innerhalb von 250 ms ein neues
ADC_DONE_FLAG und gelöschtes ADC_EN erkannt werden und beide U/I-Lesezugriffe
erfolgreich sind. Andernfalls zeigt get board.telem S:N/A; ungültige Solar-U/I
werden nicht per LPP übertragen. HIZ wird nach der Messung nicht wieder von der
Firmware eingeschaltet.
Im Na-Ion-Test bei etwa 2,59 V genügte eine hohe Panel-Leerlaufspannung bei !PG nicht für eine erfolgreiche ADC-Messung. Nach HIZ-Freigabe und erfolgreicher Quellenqualifizierung (PG=1) funktionierten Laden und ADC. Bei zu schwacher Quelle stellte der BQ HIZ wieder her und die Messung blieb ungültig. Die PG-Pflege benötigt deshalb keine ADC-/VBUS-Messung als Voraussetzung.
Die vorübergehenden ADC-, TS- und A/B-Diagnosebefehle sind aus der Release-Firmware
entfernt. Zur regulären Board-Diagnose dienen weiterhin cinfo, bqdiag und
selftest.
Temperatur-Sentinel-Werte¶
Die Firmware verwendet spezielle Rückgabewerte für ungültige Temperaturen:
| Wert | Bedeutung | Anzeige |
|---|---|---|
| −999.0 | I2C-Kommunikationsfehler oder ein von der BME280-Plausibilitätsprüfung verworfener Messwert | N/A |
| −888.0 | ADC nicht bereit, One-Shot nicht abgeschlossen, oder TS-Kanal aus (VBAT zwischen 0 und 3200mV) | N/A |
| −99.0 | NTC offen/nicht angeschlossen (k > 0,99, oder der RT2-Pol-Schutz) | N/A |
| +99.0 | NTC Kurzschluss (k < 0,01) | N/A |
| −50…+90°C | Gültiger Messwert | XX°C |
−999.0 hat jetzt zwei Erzeuger: BqDriver::calculateBatteryTemp() bei einem I2C-Lesefehler und BoardConfigContainer::getTelemetryData() bei einem unplausiblen Messwert (siehe nächster Abschnitt). Die beiden Fälle sind an der CLI und in der Telemetrie nicht unterscheidbar — beide zeigen schlicht "N/A".
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, wendet Kalibrierung und Plausibilitätsprüfung an
NTC-Plausibilitätsprüfung (BME280)¶
Warum die Bereichsprüfung nicht reicht¶
Ein fehlender oder offener NTC erzeugt keinen Wert außerhalb des Bereichs. Mit dem Inhero-Teiler (RT1 = 5,6 kΩ Pullup an REGN, RT2 = 27 kΩ auf GND) liegt der Teiler ohne NTC genau auf dem RT2-Pol von calculateBatteryTemp(). Die Extraktion 1 / (g_total − g_rt2) löst dann entweder den g_total <= g_rt2-Schutz aus (−99.0) oder liefert einen riesigen NTC-Widerstand, der zu einem Scheinwert nahe −46 °C decodiert — innerhalb des Fensters −50…+90 °C, die Bereichsprüfung lässt ihn also durch. Ein TS-ADC-LSB entspricht 0,09765625 % von REGN und verschiebt die decodierte Temperatur an diesem Arbeitspunkt um zig Grad, der Scheinwert ist also nicht einmal stabil.
Die Prüfung¶
Nachdem das Fenster −50…+90 °C bestanden und der tccal-Offset addiert ist, holt BoardConfigContainer::getTelemetryData() einen frischen BME280-Messwert und vergleicht:
ntcTemp = bqTemp + tcCalOffset // kalibriert, nicht roh
bmeTemp = readBmeTemperature()
if (bmeTemp > -100.0 && bmeTemp < 100.0 &&
fabsf(ntcTemp - bmeTemp) > NTC_BME_MAX_DIFF_C) // 15.0 °C, echt größer
→ Akkutemperatur = -999.0f // Anzeige N/A
- Die Schwelle ist
NTC_BME_MAX_DIFF_C = 15.0 °C. Eine Differenz von exakt 15,0 °C besteht die Prüfung. - Verglichen wird der kalibrierte Wert (roh +
tcCalOffset). - Ist der BME280 nicht lesbar — keine Antwort auf 0x76, erzwungene Messung fehlgeschlagen oder
ENV_INCLUDE_BME280 = 0— liefertreadBmeTemperature()−999.0; das scheitert am Schutz (−100, +100), die Prüfung tritt zurück und der NTC-Wert geht unverändert durch. Auf dem MR2 ist der Sensor einkompiliert (ENV_INCLUDE_BME280=1), das ist also der Fehlerpfad.
Folgen¶
| Bereich | Wirkung |
|---|---|
board.telem / CayenneLPP |
Ein verworfener Messwert erscheint als "N/A" bzw. das LPP-Temperaturfeld entfällt. Eine eigene Kennzeichnung „verworfen" gibt es nicht. |
| SOC-Derating | Ein verworfener Messwert aktualisiert lastValidBatteryTemp / lastTempUpdateMs nicht. Nach 300000 ms (5 min) ohne akzeptierten NTC-Wert greift refreshTempDerating() auf den BME280 zurück; scheitert auch der, behält lastValidBatteryTemp seinen bisherigen Wert (Default 25,0 °C = kein Derating). |
set board.tccal |
Unberührt. performTcCalibration() liest BqDriver::getTelemetryData(0) direkt, mit vorübergehend genulltem tcCalOffset, und mittelt Rohwerte; verworfen werden nur die Fehlercodes des Treibers (roh ≤ −800.0 oder ≥ 98.0), nötig sind 3 von 5 gültigen Samples. Die Kalibrierung funktioniert also auch auf einem Board, dessen Messwert der Filter verwerfen würde. |
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)
JEITA-Override (set board.jeitaignore)¶
Unterhalb der T-Cold-Grenze — auf dem Inhero-Teiler −2,0 °C, siehe Schwellentabelle oben — sperrt der BQ25798 das Laden für Li-ion und LiFePO4. set board.jeitaignore 1 setzt das TS_IGNORE-Bit des Ladereglers (NTC Control 1, Register 0x18, Bit 0); der Regler behandelt den TS-Pin dann als immer in Ordnung, das Laden läuft im Frost weiter. Der Override ist ab Werk aus, er ist durch ein Laderaten-Gate begrenzt, und er wird auf eigenes Risiko des Betreibers eingesetzt — was er kostet, steht unter Was der Override kostet. Die Feld-Erfahrungen und die zwei Positionen zum Kaltladen stehen im BATTERY_GUIDE.md.
Gespeicherter Wunsch vs. abgeleiteter Zustand¶
Der Befehl speichert kein Ein/Aus-Flag für den Override. Er speichert einen Benutzerwunsch unter dem SimplePreferences-Key jeitaIgn (Namespace inheromr2) und leitet daraus sofort den wirksamen Zustand ab und programmiert den BQ25798:
ignore = !props->needs_jeita || (getJeitaIgnoreWish() && jeitaIgnoreGateOk())
Das Ergebnis landet in der statischen jeitaIgnoreActive, die nie persistiert wird. applyJeitaIgnore() wird aus setJeitaIgnoreWish() gerufen, aus den CLI-Schreibern von imax und batcap, und am Ende von configureChemistry().
needs_jeita ist ein Feld der BatteryProperties-Tabelle und beantwortet genau eine Frage: braucht diese Chemie überhaupt eine JEITA-Temperaturüberwachung. Über die Bestückung eines NTC sagt das Feld nichts.
| Chemie | needs_jeita |
Override | Grund |
|---|---|---|---|
| Li-ion 1S | true | Wunsch UND Gate | Kaltladen scheidet Lithium auf der Anode ab |
| LiFePO4 1S | true | Wunsch UND Gate | derselbe Mechanismus, milder, aber vorhanden |
| LTO 2S | false | fest an | verträgt Laden im Frost, es gibt keine Kältegrenze durchzusetzen |
| Na-ion 1S | false | fest an | das Board setzt keine Kältegrenze durch; die zulässige Ladetemperatur steht im Zell-Datenblatt (je nach Zelle 0 °C bis −20 °C) |
| BAT_UNKNOWN | false | fest an | charge_enable = false, GPIO4 LOW → FET aus → CE über den Pull-up auf HIGH → Laden aus; es gibt kein Laden zu schützen |
Für eine Chemie mit needs_jeita = false wird der Befehl abgewiesen, bevor irgendetwas gespeichert wird: Err: This chemistry runs without JEITA (always 1). Ist noch keine Chemie gesetzt, lautet die Antwort Err: Set board.bat first.
Fail-Safe: Ist bqInitialized false oder der Properties-Zeiger null, setzt applyJeitaIgnore() jeitaIgnoreActive = false und kehrt zurück, ohne den BQ anzufassen. Bei totem BQ25798 zeigt get board.fmax deshalb auch bei LTO/Na-ion das gespeicherte Frostverhalten; die Antwort folgt dem abgeleiteten Laufzeitzustand.
Das Gate: imax ≤ 0,05C¶
jeitaIgnoreGateOk() besteht aus einer Vorbedingung und einer Formel:
- Vorbedingung —
batcapmuss vom Benutzer geschrieben worden sein (ein nicht-leererbatCap-Preference-String). Ohne das scheitert das Gate sofort: eine unbekannte Kapazität gibt der Ratenbegrenzung keinen Bezug. - Formel —
getMaxChargeCurrent_mA() <= jeitaIgnoreLimit_mA(capacity_mah), wobeijeitaIgnoreLimit_mA()0.05f * capacity_mahliefert. Verglichen wird mit<=, ein imax exakt auf 0,05C besteht das Gate also.
Das Gate liest die persistierte Kapazität über loadBatteryCapacity(). getBatteryCapacity() liefert den socStats-RAM-Cache, den begin() erst nach configureChemistry() füllt; die Boot-Ableitung würde damit gegen 0 mAh prüfen und immer scheitern.
Kapazität (set board.batcap) |
0,05C-Grenze | Beispiel-imax, der besteht |
|---|---|---|
| 1000 mAh | 50 mA | 50 mA |
| 3000 mAh | 150 mA | 150 mA, 100 mA |
| 8000 mAh | 400 mA | 400 mA, 250 mA |
| 20000 mAh | 1000 mA | 1000 mA, 500 mA |
1000 mAh ist aus einem Grund das untere Ende der Tabelle: set board.imax hat eine Untergrenze von 50 mA, unterhalb dieser Kapazität liegt die 0,05C-Grenze also unter jedem imax, den die CLI annimmt, und das Gate kann nie bestehen.
Ein gescheitertes Gate verwirft nichts. Der Wunsch bleibt im NVS; nur jeitaIgnoreActive geht auf false und TS_IGNORE wird gelöscht. Sinkt imax wieder unter 0,05C oder steigt batcap, schärft sich der Override von selbst wieder — die CLI meldet das mit ; jeitaignore 1, ein erneutes set board.jeitaignore 1 ist nicht nötig.
Boot-Verhalten¶
| Schritt | Was passiert |
|---|---|
| POR | jeitaIgnoreActive ist eine statische Variable mit Startwert false; der BQ25798 kommt mit gelöschtem TS_IGNORE hoch |
configureBaseBQ() |
ruft explizit bq.setTsIgnore(false) — ab hier führt der Hardware-Temperaturschutz |
configureChemistry() |
stellt Zellzahl, VREG, VSYSMIN und ICHG wieder her, leitet dann den JEITA-Override ab und wendet die gespeicherte MPPT-Einstellung an |
begin() |
ruft danach setFrostChargeBehaviour(frost), was JEITA_ISETC aus dem gespeicherten fmax-Mapping neu schreibt |
Die Ableitung steht mit Absicht am Ende von configureChemistry(). Das Schreiben der CELL-Bits setzt ICHG auf den POR-Default 1 A zurück; eine frühere Ableitung würde ein Fenster öffnen, in dem der Temperaturschutz schon aus ist, während ICHG noch auf 1 A steht — und ein I2C-Fehler in diesem Fenster würde das Board genau so einfrieren. Auf dem nicht ladenden Early-Return-Pfad für BAT_UNKNOWN läuft die Ableitung trotzdem, allein damit der gemeldete Zustand ehrlich bleibt.
Netto nach einem Spannungsausfall: der Schutz führt, bis configureChemistry() durch ist. Ein Override, der das Gate weiterhin besteht, ist danach wieder aktiv; einer, der es nicht mehr besteht, bleibt aus — der Wunsch bleibt erhalten.
set board.bat durchläuft dieselbe Ableitung — der Befehl ruft configureBaseBQ() und configureChemistry(), der Override wird also für die neue Chemie neu abgeleitet. Der gespeicherte Wunsch überlebt einen Chemiewechsel, und die Antwort Bat set to <type> trägt keinen Zusatz über einen Zustandswechsel; der neue Zustand zeigt sich in get board.jeitaignore. Beim Wechsel auf Li-ion oder LiFePO4 wird zusätzlich das gespeicherte fmax auf 0% zurückgesetzt.
Ein Register-Detail: weil begin() ISETC nach der Ableitung neu schreibt, hält ISETC bei einem Boot mit aktivem Override das gespeicherte fmax-Mapping; applyJeitaIgnore() allein schreibt dort UNCHANGED. Verhaltensmäßig ist der Unterschied ohne Belang — mit TS_IGNORE = 1 ignoriert der BQ den TS-Pin vollständig.
Ausschalten¶
set board.jeitaignore 0 speichert den Wunsch false und leitet neu ab. Beim Übergang von aktiv nach inaktiv ruft applyJeitaIgnore() setFrostChargeBehaviour(getFrostChargeBehaviour()) und programmiert JEITA_ISETC aus dem gespeicherten fmax-Mapping:
board.fmax |
JEITA_ISETC |
|---|---|
| 0% | ISETC_SUSPEND |
| 20% | 20_PERCENT |
| 40% | 40_PERCENT |
| 100% | UNCHANGED |
JEITA_ISETH wird nicht wiederhergestellt — sein POR-Default ist bereits „unchanged".
Solange der Override aktiv ist, wird set board.fmax mit Err: Fmax N/A while jeitaignore is on abgewiesen und speichert nichts; der zuvor gespeicherte Wert ist der, der nach dem Ausschalten wieder programmiert wird. Eine Chemie, die ohne JEITA läuft, wird zuerst abgewiesen, mit Err: Fmax setting N/A for this chemistry (JEITA disabled).
Was der Override kostet¶
TS_IGNORE wirkt auf beide Enden des JEITA-Fensters. Laut BQ25798-Datenblatt betrachtet der Regler mit TS_IGNORE = 1 den TS-Pin als immer geeignet für Laden und OTG, und TS_COLD_STAT / TS_COOL_STAT / TS_WARM_STAT / TS_HOT_STAT melden alle 000. Solange der Override an ist, gilt also:
- das Laden läuft unter −2 °C weiter, mit bis zu 0,05C;
- die Ladesperre auf der heißen Seite bei T-Hot (≈ +57,7 °C auf dem Inhero-Teiler) ist ebenfalls weg.
Die Firmware bietet für beides keinen Ersatz. Das Board schläft im SYSTEMOFF mit aktivem Laderegler, dort läuft keine Regelschleife; eine Regelschleife würde in genau den Stunden schlafen, in denen sie zählt, deshalb kommt der Entwurf ohne sie aus. Die 0,05C-Grenze ist das gesamte Sicherheitsargument. Auf der heißen Seite bleibt das Risiko klein, weil 0,05C thermisch uninteressant ist.
Auf der kalten Seite begrenzt das Gate die Rate; der Mechanismus bleibt. Wer eine gefrorene Graphitanode lädt, scheidet metallisches Lithium auf ihrer Oberfläche ab. Das ist kumulativ und dauerhaft, und es zeigt sich als still verschwundene Kapazität.
Das Gate ist eine Zahl für jede Temperatur, während die vertretbare Laderate mit sinkender Zelltemperatur fällt. Was das für die Standortwahl bedeutet — und was die Felderfahrung trägt und was nicht — steht in BATTERY_GUIDE.md.
Zustand zurücklesen¶
| Befehl | Bei aktivem Override |
|---|---|
get board.jeitaignore |
jeitaignore 1 — bzw. jeitaignore 1 (chemistry) bei LTO/Na-ion |
get board.fmax |
N/A (geprüft wird isJeitaIgnoreActive(), das gilt also für jede Chemie) |
get board.conf |
F: zeigt N/A; J:1 wird nur bei einer needs_jeita-Chemie angehängt |
get board.bqdiag |
endet mit N:%02X = rohes NTC_CONTROL_1 (0x18). Bit 0 ist TS_IGNORE, ein ungerader N:-Wert bedeutet also, dass der Override in Hardware programmiert ist. Das TS:-Feld derselben Antwort decodiert STATUS_4 und zeigt bei aktivem Override "OK", weil TS_IGNORE alle vier TS-Statusbits auf 000 zwingt. |
Code-Referenzen¶
BoardConfigContainer::setJeitaIgnoreWish()— speichert den Wunsch, leitet neu ab, programmiert den BQBoardConfigContainer::jeitaIgnoreGateOk()/jeitaIgnoreLimit_mA()— das 0,05C-GateBoardConfigContainer::applyJeitaIgnore(props)— Ableitung + Programmierung von TS_IGNORE/ISETC/ISETHBqDriver::setTsIgnore()— NTC Control 1 (0x18), Bit 0
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)→ CE-FET 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? - N-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) - CE-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: 100 kΩ auf REGN → 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()):
- CE wird vor System Sleep anhand der gespeicherten Batterie-Konfiguration gesetzt (unbekannte Chemie: GPIO LOW).
- P0.04 wird von disconnectLeakyPullups() ausgeschlossen → der konfigurierte GPIO-Output-Latch bleibt erhalten
- Bei freigegebenem Laden: GPIO4 gelatcht HIGH → CE-FET ON → CE LOW → Laden aktiv
- BQ25798 MPPT/CC/CV läuft autonom in Hardware → Solar-Laden möglich
Beim stündlichen UV-Wake wird CE nach dem GPIO-Reset anhand der gespeicherten Chemie wieder als Ausgang gesetzt. Ist Laden freigegeben, prüft die Firmware bei konfiguriertem MPPT zuerst, ob der BQ25798 antwortet. Die Pflege benötigt keine ADC- oder VBUS-Messung. Bei I²C-Fehlern endet sie. PG=0 wird durch einen einmaligen HIZ-Toggle behandelt; anschließend wird maximal 1 s auf PG gewartet und MPPT bei PG=1 noch im selben Wake aktiviert. Vor dem erneuten Sleep werden ADC und Interrupts wieder in den stromsparenden Zustand versetzt. Im Sleep selbst bleibt CE vom RAK aktiv getrieben; die Software-Pflege läuft nur beim Wake. - 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)
- Aufstellhöhe für die QNH-Korrektur (altitude)
- Frostverhalten (frost)
- Max. Ladestrom (maxChrg)
- LED-Einstellung (leds_en)
- JEITA-Override-Wunsch (jeitaIgn) — der wirksame Override wird daraus bei jeder Chemie-Anwendung abgeleitet und nie persistiert
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%
# Ausgabe: N/A, sobald der JEITA-Override aktiv ist
# (chemiebedingt bei LTO/Na-ion/none, oder vom Benutzer geschärft)
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
# F: zeigt N/A, solange der JEITA-Override aktiv ist;
# " J:1" wird nur bei einer needs_jeita-Chemie mit aktivem
# Benutzer-Override angehängt (nie bei lto2s/naion1s, nie bei none)
# Beispiel: B:liion1s F:N/A M:1 I:400mA Vco:4.10 V0:3.30 J:1
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)
board.jeitaignore # Zustand des JEITA-Overrides
# Ausgabe: N/A ← keine Chemie gesetzt (none)
# Ausgabe: jeitaignore 1 (chemistry) ← lto2s | naion1s
# Ausgabe: jeitaignore 1 ← Benutzer-Override wirksam aktiv
# Ausgabe: jeitaignore 1, N/A, C>0.05 ← Wunsch gespeichert, imax über 0,05C
# Ausgabe: jeitaignore 1, N/A, batcap not set
# Ausgabe: jeitaignore 0 ← kein Wunsch gespeichert
Unbekannter Getter → Err: bat|fmax|imax|mppt|telem|stats|cinfo|conf|tccal|leds|batcap|jeitaignore. Die Liste lässt die Getter bqdiag, selftest und socdebug aus, die es aber gibt.
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)
# Err: Set board.bat first ← none
# Err: Fmax setting N/A for this chemistry (JEITA disabled) ← lto2s | naion1s
# Err: Fmax N/A while jeitaignore is on ← Benutzer-Override aktiv
# Keine der drei Abweisungen speichert etwas.
set board.imax <mA> # Maximalen Ladestrom setzen
# Bereich: 50-1500 mA (außerhalb: "Err: Try 50-1500", nichts wird geschrieben)
# imax ist eine Gate-Größe des JEITA-Overrides — die Antwort
# benennt einen Zustandswechsel:
# Max charge current set to 400mA
# Max charge current set to 400mA; jeitaignore 1
# Max charge current set to 800mA; jeitaignore N/A, C>0.05
set board.mppt <0|1> # MPPT ein-/ausschalten
set board.batcap <mAh> # Akkukapazität setzen
# Bereich: 100-100000 mAh
# (außerhalb: "Err: Invalid capacity (100-100000 mAh)", keine Neuableitung)
# Ebenfalls Gate-Größe — gleiches Antwortmuster wie imax:
# Battery capacity set to 8000 mAh
# Battery capacity set to 8000 mAh; jeitaignore 1
# Battery capacity set to 4000 mAh; jeitaignore N/A, C>0.05
# Nebeneffekt: socStats.soc_valid = false, SOC zeigt also N/A bis zur
# nächsten "Charging Done"-Synchronisation oder einem manuellen set board.soc
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)
set board.jeitaignore <1|0> # JEITA-Override — Laden im Frost, auf eigenes Risiko des Betreibers
# Nimmt 1|0|true|false; alles andere: "Err: Use 1|0"
# Ohne gesetzte Chemie: "Err: Set board.bat first"
# Nur für needs_jeita-Chemien (liion1s, lifepo1s); sonst:
# Err: This chemistry runs without JEITA (always 1)
# Speichert einen Wunsch und leitet daraus den Override ab:
# jeitaignore set to 1
# jeitaignore set to 1, N/A, C>0.05
# jeitaignore set to 1, N/A, batcap not set
# jeitaignore set to 0
# Gate: batcap muss gesetzt sein UND imax <= 0,05C. Der Wunsch
# überlebt ein gescheitertes Gate und schärft sich von selbst
# wieder. Siehe „JEITA-Override" in Abschnitt 4 und BATTERY_GUIDE.md.
Unbekannter Setter → Err: bat|imax|fmax|mppt|batcap|tccal|leds|soc|jeitaignore.
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 |
applyJeitaIgnore() |
BoardConfigContainer.cpp | Leitet den wirksamen JEITA-Override ab, programmiert TS_IGNORE/ISETC/ISETH |
jeitaIgnoreGateOk() |
BoardConfigContainer.cpp | Das 0,05C-Gate (batcap vom Benutzer gesetzt UND imax ≤ 0,05C) |
refreshTempDerating() |
BoardConfigContainer.cpp | Derating-Temperatur, BME280-Fallback nach 5 min ohne akzeptierten NTC-Wert |
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