Anleitung: So kann man einen gebrickten Solarspeicher wiederbeleben
Heimspeicher nach Tiefentladung tot — wie ich mit einem 6-Euro-Debugger und KI-unterstütztem Reverse Engineering den Software-Lockout geknackt habe.

Fröhlich schleppte ich den knapp 30 Kilo schweren Jackery Navi 2000 Heimspeicher aus dem Make-Büro in Hannover in mein Auto. „Vielleicht kannst du damit was anfangen“, hatte Chefredakteur Daniel Bachfeld gesagt. Normalerweise ist das Gerät, mit seinen 2.048 Wattstunden LiFePO4-Kapazität und einem bidirektionalen Wechselrichter ausgestattet, dafür gedacht, Strom aus einer PV-Anlage zu speichern.
Aber dieses Exemplar wurde für tot erklärt, nachdem ein Kollege bei der c’t, der ihn als Vorproduktionseinheit zum Testen bekommen hatte, ihn in der Garage über den Winter ohne Ladung bei Minusgraden vergessen hatte. Der lässt sich sicherlich schnell und unkompliziert irgendwie beleben, dachte ich. Leider war die Rettungsaktion deutlich komplexer als ursprünglich gedacht, aber umso lehrreicher.
Die Selbstentladung hatte die Zellspannungen unter die Abschaltschwelle des Battery Management Systems (BMS) gedrückt. Das BMS ließ sich nicht mehr überreden, den Speicher zu laden oder zu entladen, egal wie ich ihn über Netzstecker und Solareingang angeschlossen habe. Aber die Elektronik selbst schien noch in Ordnung zu sein. Zwar hatte die dazugehörige App zuverlässig zwei Fehlermeldungen rausgespuckt – aber jede weitere Aktion unterbunden.

Dieses Problem ist kein Jackery-Exklusivum. Ob Bluetti, EcoFlow, Anker oder Zendure – alle großen Hersteller von Heim-und tragbaren Speichern verwenden im Kern dieselbe BMS-Architektur: ein ARM-Cortex-Mikrocontroller als Gehirn, ein Analog-Front-End von Texas Instruments als Messknecht für Zellspannungen und Temperatur, dazu ein CAN-Bus als internes Nervensystem.
Wer einen dieser Speicher nach Tiefentladung verliert, steht vor dem exakt gleichen Problem: ein Software-Latch im BMS, das die Hardware aussperrt, obwohl die Zellen physisch noch intakt sind (siehe Kasten „Was ist ein Battery Management System?"). Mein erster Gedanke: Wenn das BMS nicht laden will, lade ich eben am BMS vorbei – direkt an den Zellen.
#Operation am offenen Herzen
Das Gerät aufzuschrauben war einfach. Unter den Gummigriffen an der Oberseite des Aluminiumgehäuses fand ich sechs T15-Schrauben vor. Dann musste ich mit dem Cuttermesser eine Silikonversiegelung durchschneiden, um den mit Kühlrippen versehenen Deckel zu heben. Das war recht schwer, denn an seiner Unterseite hing der Wechselrichter fest.

Unter der Haube, hinter dem etwa 10 cm hohen Wechselrichter-Board, schaute ich direkt ins Herz des Jackery – auf den Akkupack mit seinen 32 LiFePO4-Zellen. Nachdem ich die orangefarbene Schutzmatte entfernt hatte, die die Zellen vor Kurzschluss schützt, sah ich die Busbars.

Die Zellen sind in einer 16S2P-Konfiguration angeordnet, also 16 Gruppen in Serie, je zwei Zellen parallel. Die Busbars, die die Zellgruppen verbinden, laufen serpentinenartig durch den Pack – und genau das hat ein weiteres Problem ausgelöst, das die Rettung der Einheit noch mehr erschwert hat.
Mit dem Voltmeter habe ich die äußersten beiden Busbars gefunden, das sind die beiden oben rechts auf dem oberen Bild. Dort sind auch zwei von mir mit Tape befestigte Kabel zu sehen, braun und blau. Diese sind an ein 48-V-Ladegerät angeschlossen, das eigentlich für E-Bike-Akkus vorgesehen ist. Auf diese Weise wollte ich das BMS umgehen und die Zellen direkt nachladen.

Das Problem dabei war, dass ich übersehen hatte, dass ich über die auf einer Seite des Akkupacks sichtbaren Busbars nur 14 von 16 Zellgruppen erreichen konnte. Jeder Busbar verbindet jeweils zwei Zellenpaare. Die beiden Endgruppen – Nummer 1 und Nummer 16, die jeweils am Endpunkt sitzen – wurden somit nicht geladen und blieben bei katastrophalen 1,7 Volt, der Spannung, auf der sich anfangs alle Zellen befunden hatten. Der Rest lag nach dem Laden bei gesunden 3,3 Volt.

Richtig gewesen wäre, direkt an den beiden Bügeln oben in Bild 6 zu laden, wo Plus und Minus des Akkupacks als Ganzes an das BMS geleitet werden. Da 14 von 16 Zellgruppen schon aufgeladen waren, habe ich die beiden Endgruppen mit einem TP5000-LiFePO4-Lademodul, das passenderweise auf 3,7 Volt lädt, einzeln nachgeladen. Danach waren endlich alle 16 Gruppen bei etwa 3,3 Volt angekommen – also insgesamt ca. 52,8 Volt – und endlich konnte ich das Gerät einschalten, in der Hoffnung, die Fehler im BMS hätten sich von selbst gelöscht, da die Zellen nicht mehr tiefentladen waren. Leider ohne Erfolg, denn das BMS blieb gesperrt.

Diesmal blieb das BMS aber wegen eines neuen, sehr hartnäckigen Fehlers gesperrt: Obwohl alle Zellen nun gleichmäßig aufgeladen waren, meldete die App Internal BMS fault1 . Wie ich später herausfinden sollte, steckte hinter dieser Beschreibung ein Unterspannungs-Lockout, CellUVLock . Quasi eine interne Softwaresicherung, einmal eingerastet, resistent gegen Power-Cycles, resistent gegen Laden, resistent gegen alles.
An diesem Punkt wurde mir klar: Das ist kein Hardware-Problem mehr. Das ist ein Software-Latch – ein einzelnes Bit irgendwo im System, das sagt: „Diese Batterie war mal tiefentladen, und ich weigere mich, das zu vergessen“.
Fairerweise muss ich an dieser Stelle sagen, dass der CellUVLock womöglich beim ersten Boot nach dem ungleichmäßigen Bypass-Laden geschrieben worden ist. Wenn das der Fall war, wäre die ganze folgende Arbeit überflüssig gewesen, hätte ich nur besser aufgepasst und nicht zwei Zellgruppen vergessen. Andererseits habe ich viel gelernt – Wissen, das ich gerne hier teile! Wer in dieselbe Situation kommt, sollte jedenfalls auf Nummer sicher gehen und alle Zellgruppen möglichst gleichzeitig auf gesunde Spannung bringen, bevor das BMS zum ersten Mal neu bootet.
#Claude als Sparringspartner
Als Nächstes habe ich das Problem mit dem hartnäckigen, letzten Fehler in einer Chat-Session mit Claude besprochen. Claude erklärte mir dabei die typische BMS-Architektur, half mir, die Schutzebenen zu verstehen (Hardware-Schutz im Analog-Front-End vs. Software-Fehlermanagement im Mikrocontroller) und kam schnell zum entscheidenden Vorschlag: Über den Debug-Port herausfinden, was da vor sich geht, in dem Jackery-BMS.
Dafür verwenden wir SWD – Serial Wire Debug – ein ARM-Standard-Interface, über das man direkt auf den Speicher eines Mikrocontrollers zugreifen kann. Damit lässt sich im Betrieb Lesen, Schreiben, einzelne Funktionen aufrufen, das Programm anhalten und schrittweise durchgehen. Hierfür brauchen wir allerdings einen ST-Link-V2-Adapter. Ich habe einen für nicht mal sechs Euro gekauft.
Außerdem wollte Claude auch, dass ich Ghidra installieren sollte, um damit zu versuchen, die Geheimnisse des Firmwares zu decoden. Ghidra ist ein kraftvolles Open-Source-Reverse-Engineering-Tool der amerikanischen Spionen-und Massenüberwachungsbehörde NSA. Wie sich herausstellen würde, brauchte ich Ghidra nie – Python-Skripte und ein schlanker Disassembler reichten völlig.
Für die folgende Hands-on-Arbeit – das Auslesen des Gerätes über die SWD-Verbindung, Memory-Dumps analysieren, Skripte schreiben – wechselte ich zu Claude Code, der direkt auf meinem Rechner arbeitet und Dateien lesen, schreiben und ausführen kann.
#SWD-Zugang zum Gehirn
Die BMS-Platine sitzt an der Seite des Akkupacks, und um daran heranzukommen, musste ich den Akkupack komplett aus dem Gehäuse befreien.
Es lohnt sich, das folgende Bild etwas näher anzuschauen. Die Platine trägt die Bezeichnung „HTH003-BEMS_V15“, wobei BEMS für „Battery Energy Management System“ steht. Auf der linken Seite sieht man die Leistungselektronik: zwei Reihen MOSFETs, die den Stromfluss zum Akku steuern und als Hardware-Schutz dienen, unabhängig von der Software. Die MOSFETs sind die letzte Verteidigungslinie: Sie werden direkt vom Analog-Front-End geschaltet – ohne Umweg über die Firmware – und kappen den Stromfluss sofort, wenn eine Zellspannung den sicheren Bereich verlässt.

Unterhalb davon befinden sich die Pack-Anschlüsse P+ und P– (die beste Stelle, um alle Zellen zu laden, wenn man daran herankommt, was bei mir anfangs nicht der Fall war). Geschützt wird das nackte Metall von hitzebeständigem Kapton-Klebeband (die dünne gelbe Folie).
Am oberen Rand reihen sich die Steckverbinder zu den Subsystemen: PCS (Wechselrichter), BMS_South (CAN-Bus), DC-CHG. Ganz oben rechts auf der Platine sitzt ein ESP32-C3-MINI-1U von Espressif (unter Kapton leicht versteckt) – der WLAN-und Bluetooth-Coprozessor, der die Cloud-Anbindung zur Jackery-App herstellt.
Aber das für uns Allerwichtigste befindet sich genau in der Mitte: der 5-polige, weiße SWD-Header – beschriftet mit „DEBUG" und „SWD" – über den wir die Firmware auslesen wollen. Rechts daneben thront der Hauptprozessor – das Herz – ein GD32F470ZGT6 von GigaDevice: ARM Cortex-M4F, mit 1 Megabyte Flash, 256 KB RAM und 64 KB CCM (Core Coupled Memory). Die korrekte Bezeichnung auf dem Chip zu lesen war übrigens an sich eine echte Herausforderung.
Laut Claudes Aussage ist der GD32 zwar pinkompatibel mit STM32F4-Chips von STMicroelectronics, wird aber von deren hauseigenen Tool STM32CubeProgrammer trotzdem nicht erkannt. Die Lösung heißt „pyOCD“. Der Python-basierte ARM-Debugger unterstützt nämlich auch Chips von GigaDevice. Die Installation geht einfach: pip install pyocd .
Beim Auslesen müssen das BMS und auch periphere Teile wie der Wechselrichter mit Strom versorgt werden. Das war nicht ganz einfach, da die Verbindungskabel und auch die mir zur Verfügung stehenden Jumperkabel sehr kurz waren, aber mithilfe diverser Kisten konnte ich mich endlich mit dem laufenden Jackery über die SWD-Schnittstelle verbinden.

Nachdem ich die Jumperkabel zwischen SWD-Header und dem über USB angeschlossenen ST-Link eingesteckt hatte, bekam ich die erste gute Nachricht: Die Readout Protection war ausgeschaltet. Dieser Schutzmechanismus kann verhindern, dass jemand die Firmware ausliest – aber hier war die Tür zum Glück weit offen. Ich hatte vollen Lese-und Schreibzugriff auf den gesamten Speicher.
Um pyOCD zu starten, habe ich einfach im Terminal (Eingabeaufforderung) den Befehl pyocd commander eingetippt, um eine interaktive Konsole mit eigenem Prompt zu öffnen. Darüber kann man dann den Speicher lesen ( read32 ), schreiben ( write32 ), den Prozessor anhalten ( halt ) oder weiterlaufen lassen ( go ).
Damit erwies es sich als kinderleicht, einen ersten Memory-Dump des Flash zu machen. Schnell wurden mehrere Binärdateien auf meine Festplatte heruntergesaugt: ein Megabyte Firmware, dazu 256 KB RAM und 64 KB CCM (Core Coupled Memory). Letzteres ist ein kleiner, besonders schneller RAM-Bereich, der direkt am Prozessorkern angebunden ist und von der Firmware für zeitkritische Daten wie das Alarm-Subsystem genutzt wird.

Es hat sich dabei ein bisschen so angefühlt, als würde man in eine Bank einbrechen (nicht, dass ich wüsste, wie das ist!). Eine Bank, wo die Tür offen steht. Nur handelt es sich nicht um Geld und Diamanten, sondern um einen undurchdringlichen, aus rohen Einsen und Nullen bestehenden Brei – kein Quellcode, kein Schaltplan, keine Dokumentation.
#Der Workflow
Alles, was ich letztendlich verwendet habe, war Claude Code, ein Terminal und die beiden Python-Bibliotheken pyOCD für den Speicherzugriff und Capstone als Disassembler.
Über pyOCD habe ich Speicherbereiche mithilfe von Claude geschriebener Python-Skripte ausgelesen. Die daraus gewonnenen Hexdaten habe ich gleich an Claude Code weitergeleitet. Claude hat dann darin nach Mustern, Strings, Adressen und Datenstrukturen gesucht. Mehrmals hat Claude neue Skripte geschrieben und ausgeführt, um weitere Erkenntnisse zu gewinnen. Insgesamt sind über ein Dutzend Python-Skripte entstanden, jedes davon wie ein forensisches Werkzeug für einen bestimmten Aspekt der Firmware. Ein paar Beispiele sind:
live_faults.pyzum Dekodieren aller aktiven Fehler
disasm.pyein Disassembler, der automatisch erkennt, auf welche Speicheradressen und Konstanten sich ein Befehl bezieht
ccm_decode.pyzum Parsen des Alarm-Ringpuffers
Alle verwendeten Python-Skripte sind auf die zum Artikel gehörigen GitHub-Seite frei verfügbar – siehe dazu den Link in der Kurzinfo.
Die Schlüsselfunde kamen Stück für Stück:
- Das Fehler-Wörterbuch. An Flash-Adresse
0x080A22E8fand Claude eine Tabelle mit 67 Fehlertypen. Es beinhaltet alles vom banalen Timeout bis zum kritischen Zell-Lockout. Jeder Eintrag hat einen numerischen Code, einen Namen und eine Bitposition in einem Fehlerregister.CellUVLock, unser Verdächtiger, ist Code0x0005.
- Der Alarm-Ringpuffer. Im CCM-Speicher ab Adresse
0x10006810liegt eine Art Akte aller aktiven Fehler – ein Ringpuffer mit 20 Slots à 60 Bytes. Hier wird jeder aktive Alarm mit Zeitstempel, Gerätseriennummer und Fehlercode gespeichert. Hier gab es genau einen Eintrag:CellUVLock.
- Das Analog-Front-End ist sauber. Der TI BQ76952, der die Zellspannungen misst, hatte keine Fehlerflags gesetzt – Safety Status, Permanent Failure Status, alles auf Null. Die Hardware-Schutzebene des BMS hatte also nichts auszusetzen, und damit wurde klar, dass das Problem rein in der Software-Logik lag.
#Irrwege und Sackgassen
Nicht jede Spur führte zum Ziel. Tatsächlich führten die meisten in Sackgassen – trotzdem waren einige davon lehrreich. Einer der ersten Ansätze war, das Fehler-Wort im RAM einfach auf Null zu setzen. Per SWD-Schreibzugriff ließ sich das auch leicht umsetzen – und es funktionierte: Der Fehler war weg, aber nur bis zum nächsten Power-Cycle. Sobald der Jackery frisch gestartet wurde, war der Fehler sofort wieder da, unverändert, als wäre nichts geschehen. Daraus habe ich gelernt, dass irgendetwas beim Booten den Fehler wieder abrief. Die Frage war nur: was, und woher?
Dann hat Claude in der Firmware eine Funktion namens DeviceAlarmActionBy32BitCode gefunden, die Fehler setzen und löschen kann. Das hörte sich nach dem perfekten Hebel an, aber die Funktion greift intern auf einen Mutex (Mutual Exclusion) zu – eine Art Sperre, die verhindert, dass zwei Programmteile gleichzeitig auf dieselbe Ressource zugreifen, etwa die Alarm-Daten. Wenn man diese Funktion „von außen“ aufruft – in unserem Fall über den Debugger, während das Betriebssystem normal läuft –hängt sich alles auf. Das Echtzeit-Betriebssystem (FreeRTOS) erwartet, dass solche Aufrufe aus dem richtigen Task-Kontext kommen. Die Injektion von Mutex-nehmenden Funktionen erwies sich damit als Sackgasse.
Ein wirklich nerviges Problem war auch Claudes aufgeblasenes Selbstvertrauen. Im Laufe der Analyse fiel mir öfter auf, wie die KI dazu neigte, Hypothesen zu „Fakten“ zu befördern, oft gefolgt von euphorischen Aussagen wie „Das ist ein Durchbruch!“ oder „Jetzt haben wir es“. Eine plausible Vermutung wurde in der nächsten Nachricht als gesichert behandelt. Um gegenzusteuern, habe ich Claude aufgefordert, konsequent zwischen VERIFIED (empirisch belegt) und INFERRED (Hypothese mit Konfidenzniveau) zu unterscheiden. Was hat der Debugger tatsächlich gezeigt? Was ist eine Interpretation?
Als wäre das nicht genug, wollte Claude auch mehrmals aufgeben. Mindestens drei Mal schlug Claude vor, das Projekt sein zu lassen. Die bis zu dem Zeitpunkt gewonnenen Einsichten sollten doch für einen Make-Artikel reichen, hieß es. Meine Antwort war jedes Mal: „Nimm einen Schritt zurück, suche online in Foren nach ähnlichen Problemstellungen, überprüfe sämtliche deiner Behauptungen und überleg, ob es andere Wege gibt, das Problem anzugreifen."
Wohlgemerkt bin ich kein Entwickler und habe nur ein sehr grundlegendes Verständnis von den Abläufen in einem BMS. Ich war also nicht in der Lage, Claude von Anfang an zu sagen, er solle gezielt nach einem Alarm-Ringpuffer suchen oder Ähnlichem. Trotzdem ist es möglich, auch als weniger versierter Hacker ans Ziel zu kommen, indem man die KI viel hinterfragt und darum bittet, Strukturen und Begriffe ausführlicher zu erklären.
An einem Punkt der Ermittlung war ich frustriert genug, um eine zweite KI-Meinung einzuholen. Ich bat Claude, sämtliche bis dahin gewonnenen Einsichten in einem technischen Writeup zusammenzufassen – eine Art „Übergabeprotokoll" als Markdown-Datei. Dieses habe ich dann ChatGPT (GPT-4o) gefüttert. Die Antwort war ausführlich und professionell strukturiert: Ganze sieben Experimente wurden vorgeschlagen, darunter SPI-Flash-Diffing und Hardware-Watchpoints.
#Der Durchbruch bei der Fehlersuche
Die entscheidende Frage war immer dieselbe geblieben: Wer schreibt CellUVLock beim Booten in den RAM? Wenn wir den Schreiber finden, finden wir auch die Quelle.
ARM-Cortex-M-Prozessoren haben eine eingebaute Debug-Einheit namens DWT (Data Watchpoint and Trace). Damit kann man einen Hardware-Watchpoint setzen: Die CPU hält automatisch an, auf den Taktzyklus genau, sobald eine bestimmte Speicheradresse beschrieben wird.
Dazu wurde das Skript watch_lock.py geschrieben. Es setzt einen Write-Watchpoint auf die Alarm-Ring-Adresse 0x10006814 , startet die CPU mit gehaltenem Reset und lässt sie dann erst booten. Jedes Mal, wenn an dieser Adresse geschrieben wird, stoppt die CPU, und wir lesen den Program Counter (PC) aus – also die Adresse der Instruktion, die gerade ausgeführt wurde – plus das Link Register (LR) und den Stack.
Zuerst feuerten mehrere Fehlalarme, etwa der Initialisierungscode, der den Ringpuffer vorbereitet. Dann kam aber der Treffer: 0x0005 wurde geschrieben. CellUVLock . Und die CPU steht still.
Der Callstack zeigt, dass die Funktion HistoryRecord heißt und an Flash-Adresse 0x08053990 sitzt. Und was diese Funktion tut, ist gleichzeitig das Problem und die Lösung: Beim Booten liest HistoryRecord einen 8-Byte-Record aus dem externen SPI-Flash des Mikrocontrollers – einem separaten Speicherchip auf der Platine, der im Gegensatz zum RAM seine Daten auch ohne Strom behält. Der Record beginnt an Adresse 0x440400 und enthält fünf Flag-Bytes und eine Prüfsumme. Die Funktion validiert die Checksumme, und für jedes Flag-Byte, das auf 1 steht, löst sie den zugehörigen Fehler aus. Flag Nummer 1 = CellUVLock .
Das war die Antwort. Die Tiefentladung – oder möglicherweise mein fehlerhaftes Wiederaufladen, bei dem ich zwei Zellenpaare vergessen hatte – hatte damals einen Fehlereintrag in den SPI-Flash geschrieben. Auch wenn sich die Zellen längst erholt hatten und die BQ-Schutz-ICs keine Probleme meldeten, las die Firmware bei jedem Bootvorgang brav den alten Flash-Eintrag und „weckte" den Fehler wieder auf.
#Selbstheilung der Firmware
Jetzt kam der geniale Teil. HistoryRecord hat nämlich einen eingebauten Selbstheilungsmechanismus – vermutlich gedacht für den Fall, dass die Flash-Daten korrupt sind: Wenn die Checksumme des 8-Byte-Records nicht stimmt, nimmt die Funktion einen alternativen Pfad. Sie setzt alle fünf Flag-Bytes auf Null, berechnet eine neue Checksumme, schreibt den bereinigten Record zurück in den SPI-Flash – und löst keinen einzigen Fehler aus.
Das ist der „Rebuild-Pfad" in der Firmware, an Adresse 0x080539B2 . Und der Plan war denkbar einfach: Wir zwingen die Firmware, diesen Rebuild-Pfad zu nehmen, und können uns so aufwendigere und riskantere Lösungsansätze ersparen: kein Flash-Patch, keine Firmware-Modifikation, kein Mutex-Injection-Hack. Stattdessen ließen wir die Firmware ihren eigenen Latch aufräumen.
Ein „Auf Werkseinstellungen zurücksetzen" über die App hilft hier übrigens nicht – der Reset setzt Benutzereinstellungen zurück, nicht die Fault-Flags im SPI-Flash. Um genau das zu tun, macht das Skript clear_lock_boot.py (siehe gleichnamiges Listing) Folgendes: Es setzt die CPU unter Debugger-Kontrolle zurück, platziert einen Breakpoint an der Checksummen-Verzweigung – einem beq -Befehl ( Branch if Equal ) an Adresse 0x080539B0 – und lässt die CPU booten. Wenn die CPU am Breakpoint stoppt, hat sie gerade die Checksumme geprüft und steht kurz davor, in den „Fehler aufwecken"-Pfad zu springen. Stattdessen setzen wir den Program Counter auf den Rebuild-Pfad – eine Adresse weiter, 0x080539B2 – und danach erledigt die Firmware den Rest: Die Flags werden auf Null gesetzt, die Checksumme neu berechnet und in den SPI-Flash zurückgeschrieben, diesmal ohne Fehlereintrag.
clear_lock_boot.py
# clear_lock_boot.py — CellUVLock per Firmware-Selbstheilung löschen
BP_BEQ = 0x080539B0 # Checksummen-Verzweigung (beq)
REBUILD = 0x080539B2 # Rebuild-Pfad: Flags nullen + zurückschreiben
FLAGS = 0x10000048 # CCM-Kopie des SPI-Flash-Records
RING = 0x10006810 # Alarm-Ringpuffer [head][count]
t.reset_and_halt() # CPU anhalten
t.set_breakpoint(BP_BEQ) # Breakpoint an der Verzweigung
t.resume() # Booten lassen, CPU stoppt am Breakp
# ... warten bis Breakpoint erreicht ...
pre = t.read_memory_block8(FLAGS, 8)
print(f"flags vorher: {[pre[i] for i in range(5)]}") # [0, 1, 0, 0, 0
t.write_core_register('pc', REBUILD) # PC auf Rebuild-Pfad umbiegen
t.remove_breakpoint(BP_BEQ)
t.resume() # Firmware räumt selbst auf
time.sleep(4.0)
t.halt()
post = t.read_memory_block8(FLAGS, 8)
print(f"flags nachher: {[post[i] for i in range(5)]}") # [0, 0, 0, 0,
#Der Moment der Wahrheit
Danach habe ich den Jackery-Speicher neu gebootet. Als alle vier LEDs auf dem Bedienpanel zum ersten Mal grün aufleuchteten, spürte ich eine tiefe Erleichterung.
Das Schöne an der Lösung ist, dass die Firmware dabei unverändert bleibt. Auch die Hardware-Schutzfunktionen des BQ76952 bleiben vollständig intakt. Der Fix kann also keinen aktiven Schutzmechanismus dauerhaft aushebeln – er wirkt nur, wenn die Fehlerbedingung nicht mehr besteht.
An dieser Stelle fragt man sich vielleicht, warum sich der Fehler nicht von selbst repariert. Die Firmware liest die Zellspannungen durchaus aus, und andere Fehler wie CELL_VOL_ERR haben sich von selbst gelöscht, als die Zellen wieder gesund waren. Aber CellUVLock ist ein sogenannter „Hard Latch“: Er kann durch Unterspannung gesetzt, aber nicht durch Spannungserholung gelöscht werden. Die Funktion HistoryRecord , die den Fehler beim Booten aus dem SPI-Flash nachliest, schaut nicht auf die aktuellen Zellspannungen – sie prüft nur, ob der gespeicherte Record intakt ist, und löst ihn dann blind wieder aus.
Das ist womöglich Absicht. So behält der Hersteller die Kontrolle, weil solche Fehler nur über den eigenen Cloud-Support per Fernbefehl gelöscht werden, nachdem ein Techniker den Fall geprüft hat. Gleichzeitig bedeutet das aber auch, dass ein Gerät, das aus dem Support fällt – wie dieses nicht mehr unterstützte Vorserienmodell – zum Elektroschrott wird. Nicht wegen defekter Hardware, sondern wegen eines einzelnen Software-Bits.
#Was ich gelernt habe
Dass eine Tiefentladung zu einem permanenten Software-Lockout führt, den der Benutzer nicht zurücksetzen kann, ist kein Einzelfall. In den Support-Foren von Bluetti, EcoFlow und Anker finden sich dieselben Symptome: Gerät tiefentladen, Zellen wieder gesund, aber das BMS bleibt gesperrt.
Soweit meine Recherche es feststellen konnte, ist die Architektur aus ARM-MCU, TI-AFE und CAN-Bus herstellerübergreifend oft nahezu identisch. Beispielsweise der EcoFlow Delta 3 arbeitet auch mit GigaDevice-ARM-Mikrocontroller und einem TI-Schutz-IC, also dieselbe Chip-Kombination wie im Jackery.
Das bedeutet, dass die hier dokumentierte Vorgehensweise – SWD-Zugang, Fault-Dictionary decodieren, Alarm-Mechanismus nachverfolgen, Watchpoint setzen – auf jedes ARM-basierte BMS übertragbar sein sollte. Auch wenn sich die Adressen ändern, bleibt das Prinzip dasselbe. (mch)