Skip to content

Latest commit

 

History

History
199 lines (171 loc) · 11.4 KB

File metadata and controls

199 lines (171 loc) · 11.4 KB

Modul-Bootloader und Firmware-Verteilung über den Bus

Status: experimentell. Der Werksflash über den Browser (Bootloader + App + BOOTEND-Fuse, Bench-Punkte 1+2) ist am Gerät verifiziert (10.09.2026). Die Firmware-Verteilung über den Bus (Punkte 3–8): erster realer Test am Gerät (19.09.2026) deckte einen Bug im Bootloader auf (derselbe DI-Pin-Fehler wie in der App vor f24b861, eigene USART0-Initialisierung — behoben in 3d65464, Firmware v1.9).

21.09.2026 — erste vollständige End-zu-Ende-Übertragung am Gerät bestätigt (Adresse 1, FW_BEGIN→FW_DATA→FW_END→GET_VERSION ohne einen einzigen Retry, neue Version per GET_VERSION bestätigt). Dabei zeigte sich aber ein zweiter, eigenständiger Bug: eine Karte, die mitten in FW_DATA/FW_END hängen bleibt (App bereits ungültig markiert, Bootloader wartet laut Design korrekt auf ein neues FW_BEGIN), beantwortet CMD_GET_STATUS nicht mehr und gilt dem Master daher als „offline". „Alle aktualisieren"/„Veraltete aktualisieren" filtern aber beide auf online — eine so hängen gebliebene Karte konnte dadurch nie wieder erreicht werden, nur noch per Browser-Werksflash. Fix (firmware v1.14): neuer Button „Offline erneut versuchen" + handle_module_update() lässt explizit angefragte Adressen immer zu, unabhängig vom Online-Status (siehe firmware/CHANGELOG.md). pio run -e attiny1616 -t upload (App @ 0x0000, ohne Bootloader) bleibt der abgesicherte Weg, wenn eine Karte dennoch per Browser-Werksflash zurückgeholt werden muss – erst ab Firmware 1.16: Der Upload schreibt keine Fuses, eine werksgeflashte Karte behält BOOTEND = 0x0C. Ältere Plain-Apps setzen dann IVSEL nicht, ihre Interrupts landen mitten im Code und die Zeitbasis steht (Issue #19); ab 1.16 setzt die Plain-App IVSEL selbst.

Stand 1.16 (Review-Fixes, Issues #29/#30/#41): Der Bootloader (BL- Version 2) startet nach einem FW_BEGIN bis zum erfolgreichen FW_END in keinem Pfad die App – auch nicht nach Busstille. Die Wiederholung des zuletzt angenommenen Chunks wird quittiert, FW_BEGIN startet jederzeit neu, der Zeitablauf wird nur von eigenen Rahmen zurückgesetzt. Er nimmt nur Unicast an die eigene Adresse an (bei ungültiger EEPROM-Adresse 250), wartet 200 µs vor jeder Antwort, das Startfenster dauert echte 2,5 s (TCB0). Als „App startbar" gelten nur die Marker 0xA5 und 0xFF. Der Master wiederholt je Schritt bis zu dreimal, beginnt danach einmal neu und prüft ein verlorenes FW_END-ACK per GET_VERSION. Bootloader-Änderungen erreichen Bestandskarten nur per Browser-Werksflash – die vier Karten im Feld laufen bis dahin mit BL 1.

Ziel

Die Master-Steuerung spielt den Daughter Cards die Firmware über den vorhandenen RS-485-Bus ein — kein PC, kein UPDI-Adapter, einzeln adressierbar, ausfallsicher.

Flash-Aufteilung (ATtiny1616, 16 KiB)

  0x0000 ┌─────────────────────┐
         │  Bootloader         │  Fuse BOOTEND = 0x0C  (0x0C * 256 = 0x0C00)
  0x0C00 ├─────────────────────┤
         │  Anwendung          │  gebaut mit env attiny1616_boot
         │  (Vektortabelle,    │  (-Wl,--section-start=.text=0x0C00, -DHAS_BOOTLOADER)
         │   IVSEL = 0)        │
  0x2400 ├─────────────────────┤
         │  frei               │
  0x4000 └─────────────────────┘
  • Der Bootloader belegt ~2,5 KiB, die Boot-Sektion 3 KiB. Die App belegt ~5,6 KiB.
  • BOOTEND sperrt den Baustein nicht. UPDI hat weiter vollen Zugriff und kann den Fuse jederzeit zurücksetzen. LOCKBIT wird nicht gesetzt; wäre es gesetzt, entsperrt ein UPDI-Chip-Erase wieder. Die einzige echte Aussperr-Falle wäre SYSCFG0.RSTPINCFG weg von UPDI — das ändert weder der Bootloader noch der Werksflash.
  • Interruptvektoren: tinyAVR-1 hat CPUINT.CTRLA.IVSEL. Nach Reset = 0 (Vektoren in der App-Sektion). Der Bootloader nutzt keine Interrupts; er setzt vor dem Sprung CPUINT.CTRLA = 0, die App setzt es zusätzlich selbst (#ifdef HAS_BOOTLOADER).

Ablauf

Reset → Bootloader. Prüft GPIOR0 (Marker 0xB7, von CMD_ENTER_BOOTLOADER) und app_valid() (erstes App-Wort ≠ 0xFFFF und EEPROM-Byte 8 ≠ 0x00).

  • gültige App, kein Marker → ~2,5 s auf CMD_FW_BEGIN lauschen, sonst App starten.
  • Marker gesetzt oder keine gültige App → im Bootloader bleiben und warten.

Update-Sitzung (Master-getrieben, bestehende Rahmenschicht lib/protocol):

Kommando Payload Antwort
CMD_ENTER_BOOTLOADER (0x55) — — (Modul: GPIOR0=0xB7, SW-Reset)
CMD_FW_BEGIN (0x56) len16 · crc16 (CRC16/MODBUS über das Image) [0x01] ok / [0x00] nak
CMD_FW_DATA (0x57) off16 · bis 30 Byte [0x01] / [0x00]
CMD_FW_END (0x58) — [0x01] ok / [Fehlercode]
CMD_GET_VERSION (0x54) — [1 · maj · min · flags · bl_ver]

Der Bootloader markiert die App bei FW_BEGIN als ungültig (EEPROM-Byte 8 = 0x00), schreibt die Seiten per NVMCTRL (Erase+Write) ab 0x0C00, prüft bei FW_END Länge und CRC, setzt dann Byte 8 = 0xA5 und startet per SW-Reset neu.

Fehler / Stromausfall: die App bleibt ungültig → beim nächsten Reset bleibt der Bootloader im Update-Modus, der Master wiederholt. Der Bootloader selbst kann von App-Code nicht überschrieben werden (Hardware-Schutz der Boot-Sektion).

Motorsicherheit

Der Bootloader konfiguriert PA7 (Triac-Treiber) nie als Ausgang → der Transistor bleibt gesperrt, der Motor kann im Bootloader nicht bestromt werden, egal welcher Fehler auftritt. Der Bootloader hat einen eigenen Watchdog (~1 s). Die App-seitige Laufzeitüberwachung (Fehlercode 0x05) und der App-Watchdog bleiben unverändert. Der Bootloader löscht beim Start nur das Ausgangslatch von PA7 (OUTCLR), für den Fall, dass er ohne Reset erreicht wird.

Bausteine

Ort Inhalt
firmware/bootloader/ der Bootloader (pio run -e bootloader)
firmware/module env attiny1616_boot App @ 0x0C00, CMD_GET_VERSION/CMD_ENTER_BOOTLOADER
firmware/module/lib/fwupdate/ empfangsseitiger Seiten-Sammler (host-getestet)
firmware/master/lib/moduleupdate/ sendeseitige Warteschlange + Zustandsautomat (host-getestet)
firmware/master/src/module_fw.h signierter .mota-Container der App, in die Master-Firmware eingebettet (Option A)
tools/build_module_firmware.py baut alle drei Images + signiert die .mota

Der Master prüft die eingebettete .mota beim Start gegen ota_pubkey.h (dieselbe ECDSA-Kette wie das Master-OTA). Der Bootloader prüft nur die CRC — Vertrauensanker ist der Master.

Master-Web-UI

Einstellungen › Modul-Firmware: Tabelle mit installierter vs. gebündelter Version je Adresse, Status (aktuell / Update verfügbar / kein Bootloader / offline), Buttons „Alle aktualisieren" und „Veraltete aktualisieren". Läuft ein Update, ruht der Statusverkehr auf dem Bus; die Module werden nacheinander abgearbeitet, das jeweils betroffene ist einige Sekunden eingefroren.

REST: GET /api/module/firmware, POST /api/module/update ({"all":true} oder {"addr":[2,3]}), GET /api/module/update/status.

Bench-Test-Checkliste (vor dem ersten Einsatz)

Verkabelung vorbereitet (11.09.2026): zwei Daughter Cards vollständig bestückt (alle Anschlüsse gelötet) und per Flachbandkabel in Reihe verbunden, Durchgangsprüfung der Kette ok. Reine Verkabelungs-/ Lötprüfung — noch kein Signal auf dem Bus. Deckt Punkte 3–7 vor, sobald die Master-Hardware aufgebaut ist.

tools/busctl.py beherrscht seit T17 auch die Bootloader-Kommandos (version/enterboot/fwbegin/fwdata/fwend, 0x54–0x58) — Punkte 3+4 und mit fwbegin/fwdata/fwend von Hand auch 5–8 lassen sich damit direkt über einen USB-Serial-Adapter (z. B. FTDI + separates RS-485-Modul mit --rts-rs485) testen, ohne dass die Master-Hardware aufgebaut sein muss. POST /api/module/update (Master-Web-UI) bleibt der bequeme Weg für den Praxisbetrieb mit mehreren Karten.

  1. ✅ Bootloader isoliert. pio run -e bootloader, per UPDI @ 0x0000 flashen, Fuse BOOTEND = 0x0C setzen (pymcuprog write -m fuses -o 8 --values 0x0C oder der Werksflasher). Ohne App: bleibt der Bootloader im Warte-Loop, WDT löst nicht aus (kein Dauerreset). Verifiziert 10.09.2026 über den Browser-Werksflasher (schreibt Bootloader + Fuse in einem Zug): Chip-Erase, Geräte-ID, Fuse- und Seitenschreiben liefen sauber, kein Dauerreset.
  2. ✅ Sprung in die App. attiny1616_boot-App @ 0x0C00 dazuflashen. Reset → Bootloader übergibt an die App (Interruptvektoren korrekt, IVSEL = 0) — verifiziert (Status-LED blinkt/leuchtet wie von der App vorgegeben, der 1-ms-Timer-Interrupt läuft also). RS-485-Kommunikation + Motorsteuerung im Zusammenspiel mit dem Master noch offen (Punkte 3+, Master-Hardware bestellt, noch nicht aufgebaut).
  3. CMD_GET_VERSION. Master (oder busctl.py version <addr>) fragt Version ab, bekommt 1 · 1 · 0 · 0x03 · x (App gültig + Bootloader vorhanden).
  4. CMD_ENTER_BOOTLOADER. Master (oder busctl.py enterboot <addr>) schickt die Karte in den Bootloader; sie bleibt dort.
  5. Update über den Bus. POST /api/module/update für eine Karte. Verlauf im Log; nach FW_END startet die Karte neu, GET_VERSION bestätigt die Version.
  6. Abbruch mitten im Transfer (Karte kurz stromlos): App bleibt ungültig, Bootloader wartet, Master-Wiederholung bringt sie zurück.
  7. „Alle aktualisieren" mit mehreren Karten: nacheinander, andere Module bleiben erreichbar.
  8. CRC-Fehlerpfad: eine bewusst verfälschte .mota → Bootloader lehnt bei FW_END ab, App bleibt ungültig.
  9. Bus-Signalintegrität + Dauer bei zehn Karten am realen Kabelbaum messen.

Werksflash über den Browser

firmware/master/prebuilt/updi.js, Tab Daughter Card: Chip-Erase → Bootloader

  • -boot-App → BOOTEND = 0x0C → BODCFG = 0xE5. Geräte-ID-Prüfung (1E 94 21). Der Fuse-Write folgt der pymcuprog-v0-Sequenz (ADDR + DATA + WFU), inklusive Rücklese-Prüfung.

BODCFG (21.09.2026): Bisher nirgends konfiguriert (Werksvorgabe 0x00 = Brown-out-Detection komplett aus) — Verdacht nach einem Fall, in dem eine Karte nach längerer Stromlosigkeit ohne erkennbaren Grund softwareseitig nicht mehr ansprechbar war. 0xE5 = LVL=BODLEVEL7 (4,2 V, höchste verfügbare Stufe; Datenblatt DS40002204A S. 2 nennt für 20 MHz 4,5–5,5 V, mehr als 4,2 V bietet der Chip nicht) + ACTIVE=ENABLED + SLEEP=ENABLED (beide kontinuierlich, Kapitel 17.5.1/17.5.2). Zusätzlich neue Diagnose-Log-Zeilen: updi.js liest EEPROM-Byte 8 (Bootloader-Marker „App gültig") direkt nach dem Chip-Erase und nochmal unmittelbar vor dem Neustart aus — damit sichtbar wird, falls ein alter Marker aus einem abgebrochenen Bus-Update den Chip-Erase überlebt.

Der Seitenpuffer wird wie in SerialUPDI (megaTinyCore) mit CTRLA.RSD = 1 (Response Signature Disable) beschrieben: ohne RSD schickt das Modul nach jedem ST-Byte ein ACK, das auf der Ein-Draht-Leitung mit den folgenden Datenbytes kollidiert. Mit RSD gehen REPEAT + ST (Wort) + 64 Datenbytes + STCS(CTRLA, RSD=0) in einem Transfer raus, danach nur das Echo lesen.