diff --git a/README.md b/README.md index 08a9277..04834dc 100644 --- a/README.md +++ b/README.md @@ -1,481 +1,167 @@ # VersaMCU -Firmware für das VersaPad v2 Macro-Pad. -Läuft auf einem **ATSAMD21G17D** (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework. +Firmware fuer das VersaPad v2 Macro-Pad. +Laeuft auf einem ATSAMD21G17D mit PlatformIO und Arduino-Framework. ## Hardware | Eigenschaft | Detail | |---|---| -| MCU | ATSAMD21**G17D** (Cortex-M0+, 128 KB Flash, 16 KB RAM) | -| Taktfrequenz | 48 MHz (DFLL, intern – kein externer 32 kHz-Quarz) | -| Framework | Arduino SAMD Core 1.8.14, PlatformIO | -| Programmer | Atmel-ICE via SWD (kein Bootloader) | +| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz | +| Flash / RAM | 128 KB / 16 KB | +| USB | Composite: HID Keyboard + Consumer + CDC Serial | +| Matrix | 5x5 logisch, davon 20 MX-Buttons + 4 Encoder-SW + 1 unbelegt | +| Encoder | 4x Rotary Encoder mit Quadratur via EIC-Interrupt | +| LEDs | 20x WS2812B an `PB22` | +| Programmer | Atmel-ICE via SWD, kein Bootloader | -### Button-Matrix - -- **5×5-Matrix** (COL_0–4 × ROW_0–4) = 25 logische Keys -- **20 Cherry MX Switches** (COL_1–4 × ROW_0–4), je eine WS2812-LED -- **4 Encoder-SW-Buttons** (COL_0 × ROW_0–3), keine LEDs -- COL_0 × ROW_4 = nicht belegt -- **Dioden**: D4148 je Taste, Anode an Switch, Kathode an ROW → Ghost-Key-freies Scannen -- **Scan-Richtung**: ROW wird OUTPUT LOW getrieben, COL-Leitungen haben 10 kΩ-Pullup nach 3V3 (werden gelesen) - -### Rotary Encoder - -- 4× Encoder (ENC0 = USB-nah, ENC3 = USB-fern) -- Alle A/B-Leitungen auf EIC (PA16–PA23), Quadratur-Dekodierung via ISR + 4-State-Lookup -- Jeder Encoder hat drei Aktionen: **CW**, **CCW**, **SW** (alle aus NVM konfigurierbar) - -### LEDs (WS2812) - -- 20× WS2812B, serpentinen-verdrahtet (Reihe 0: L→R, Reihe 1: R→L, …) -- Datenleitung: **PB22 (D18)**, bit-bang via Adafruit NeoPixel Library -- **LED_INDEX-Formel**: `row * 4 + ((row & 1) ? (4 - col) : (col - 1))` -- **Pegelproblematik**: WS2812 sind 5V-Devices (HIGH-Schwelle ~3,5 V), SAMD21 liefert 3,3 V ohne Level-Shifter. LED 0 empfängt einen marginal gültigen Pegel; ab LED 1 regeneriert jede LED intern auf 5 V → alle weiteren problemlos. Fix nächste PCB-Revision: Level-Shifter oder Diode in der VDD-Leitung. - -### Fader (ADC) - -- 3× Linearpotentiometer: PA02 (A0), PA03 (A1, auch VREFA), PB08 (A2) -- Noch nicht implementiert (siehe Roadmap) - ---- - -## Voraussetzungen - -- [PlatformIO](https://platformio.org/) (CLI oder VS Code Extension) -- **Atmel-ICE** Debugger/Programmer (SWD-Verbindung zur PCB) -- [OpenOCD](https://openocd.org/) – wird von PlatformIO automatisch installiert - -## Flashen +## Build und Flash ```bash +pio run pio run --target upload ``` -Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig – der Chip wird direkt programmiert. +Der Upload laeuft per OpenOCD ueber SWD. ---- +## Laufzeitmodell -## Funktionsumfang (Anforderungskatalog) - -### 1 Hardware-Plattform - -| # | Anforderung | Status | -|---|-------------|--------| -| 1.1 | Ziel-MCU: **ATSAMD21G17D** (Cortex-M0+, 48 MHz, 128 KB Flash, 16 KB RAM) | ✅ | -| 1.2 | Framework: **Arduino + PlatformIO**, kein Bootloader (Direktflash via SWD/Atmel-ICE) | ✅ | -| 1.3 | USB-Enumeration ohne externen Quarz (`-DCRYSTALLESS`, DFLL48M nutzt USB-SOF als Referenz) | ✅ | -| 1.4 | Benutzerdefiniertes Board-Profil (`versapad_nobl.json`) mit korrekten Flash/RAM-Limits | ✅ | - -### 2 Tasten-Matrix - -| # | Anforderung | Status | -|---|-------------|--------| -| 2.1 | **5×5-Matrix-Scan** (25 Keys, davon 20 MX-Buttons + 4 Encoder-SW + 1 NC) | ✅ | -| 2.2 | **10 ms Software-Debounce** pro Taste (Flanken-Erkennung) | ✅ | -| 2.3 | KEY_DOWN- und KEY_UP-Events werden in die Event-Queue geschrieben | ✅ | -| 2.4 | Matrix-Scan im **Loop-Kontext** (kein ISR, kein Heap) | ✅ | - -### 3 Encoder - -| # | Anforderung | Status | -|---|-------------|--------| -| 3.1 | **4 Quadratur-Encoder** (A/B-Phasen per EIC-Interrupt) | ✅ | -| 3.2 | **Richtungserkennung**: CW / CCW via 2-Bit-Greycode-Auswertung | ✅ | -| 3.3 | Encoder-Events im ISR-Kontext direkt in Event-Queue (interrupt-sicher, kein Heap) | ✅ | -| 3.4 | Encoder-SW-Tasten über Matrix-Scan (gleicher Pfad wie MX-Buttons) | ✅ | - -### 4 Aktions-Engine - -| # | Anforderung | Status | -|---|-------------|--------| -| 4.1 | **ActionType NONE**: kein HID-Event beim Drücken | ✅ | -| 4.2 | **ActionType HID_KEY**: USB-HID-Tastendruck (Keycode + Modifier-Byte) | ✅ | -| 4.3 | **ActionType HID_CONSUMER**: USB Consumer Control (Play/Pause, Lautstärke …) | ✅ | -| 4.4 | **ActionType HOST_COMMAND**: Event-ID an VersaGUI senden, App führt aus | ✅ | -| 4.5 | **ActionType MACRO**: Sequenz aus bis zu 4 HID-Key-Schritten aus NVM-Tabelle abspielen | ✅ | -| 4.6 | Jede Aktion ausführbar **ohne laufende VersaGUI** (lokal per HID/Makro) | ✅ | -| 4.7 | **Hold-Modell** für HID_KEY / HID_CONSUMER: KEY_DOWN → Key-Down senden, KEY_UP → Key-Up senden (OS-Repeat ab ~500ms) | ✅ | -| 4.8 | **Tap-Modell** für Encoder CW/CCW: atomare down+delay(10)+up Sequenz (diskrete Events, kein Hold möglich) | ✅ | - -### 5 Makro-System - -| # | Anforderung | Status | -|---|-------------|--------| -| 5.1 | **32 Makro-Slots**, je **4 Steps** (Keycode + Modifier-Byte) = 256 Byte gesamt | ✅ | -| 5.2 | Steps mit `keycode = 0` werden übersprungen (variable Makrolänge 1–4) | ✅ | -| 5.3 | Timing: 10 ms Key-Down-Dauer + 20 ms Pause zwischen Steps | ✅ | -| 5.4 | Makro-Tabelle in **separater NVM-Row** (Row 1, 0x1FF00, 256 Byte) | ✅ | -| 5.5 | Makro-Tabelle wird beim Start aus NVM geladen; gelöschter Flash (0xFF) → leere Tabelle | ✅ | - -### 6 LED-System (WS2812) - -| # | Anforderung | Status | -|---|-------------|--------| -| 6.1 | **20 WS2812-LEDs**, serpentiner Verdrahtung; Adafruit-NeoPixel bit-bang Treiber | ✅ | -| 6.2 | **2-Schicht-Modell** pro Button: `base` (Idle) + `override` (temporär von GUI) | ✅ | -| 6.3 | **STATIC**: feste Farbe aus NVM | ✅ | -| 6.4 | **BLINK**: binäres An/Aus mit konfigurierbarer Halbperiode | ✅ | -| 6.5 | **PULSE**: lineares Helligkeitsdreieck (0→255→0), kein Float | ✅ | -| 6.6 | **COLOR_CYCLE** (Regenbogen): Hue-Sweep über 6 Segmente, ignoriert base/override | ✅ | -| 6.7 | **COLOR_FADE**: einmaliger RGB-Crossfade zu Zielfarbe | ✅ | -| 6.8 | Phasenversatz beim Start: Regenbogen-LEDs sind gleichmäßig über die Periode verteilt | ✅ | -| 6.9 | `ws2812_show()` nur bei dirty-Flag aufgerufen (~600 µs Blockzeit vermieden) | ✅ | -| 6.10 | Alle Animationen in **Integer-Arithmetik** (kein FPU auf M0+) | ✅ | - -### 7 Konfigurations-Speicherung (NVM) - -| # | Anforderung | Status | -|---|-------------|--------| -| 7.1 | Config-Layout **Version 2**, 223 Byte packed, mit Magic `0x56503202` + CRC16-CCITT | ✅ | -| 7.2 | Config gespeichert in **NVM Row 0** (0x1FE00, 256 Byte, via Linkerscript reserviert) | ✅ | -| 7.3 | Makro-Tabelle in **NVM Row 1** (0x1FF00, 256 Byte) | ✅ | -| 7.4 | NVM-Schreiben: Row löschen + 4 Pages à 64 Byte manuell schreiben (MANW=1) | ✅ | -| 7.5 | Bei ungültigem Magic / falscher Version / CRC-Fehler → **Defaults** laden (kein Crash) | ✅ | -| 7.6 | Defaults: alle Aktionen NONE, LEDs warm-weiß, Animation Regenbogen 4 s | ✅ | - -### 8 Serial-Kommunikation mit VersaGUI - -| # | Anforderung | Status | -|---|-------------|--------| -| 8.1 | **CDC Serial** (USB), kein Treiber nötig; 8-Byte-Festlängen-Pakete | ✅ | -| 8.2 | **Ring-Buffer** (256 Byte = 32 Pakete) für eingehende Bytes; kein Datenverlust bei Burst | ✅ | -| 8.3 | **Ping / Pong** (0x05 / 0x85) zur Verbindungsdiagnose | ✅ | -| 8.4 | **Config-Transfer PC→Board**: BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12) → ACK/NACK | ✅ | -| 8.5 | **Config-Dump Board→PC**: auf READ(0x13) → BEGIN(0x92) → 38×DATA(0x93) → END(0x94) | ✅ | -| 8.6 | **Makro-Transfer PC→Board**: BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22) → ACK(0x95) | ✅ | -| 8.7 | **Makro-Dump Board→PC**: auf READ(0x23) → BEGIN(0x96) → 43×DATA(0x97) → END(0x98) | ✅ | -| 8.8 | Config-COMMIT validiert Magic + Version + CRC; bei Fehler **NACK** ohne NVM-Schreiben | ✅ | -| 8.9 | Alle Sende-Pakete nur wenn `SerialUSB` aktiv (DTR-Check verhindert stilles Verwerfen) | ✅ | - -### 9 Nicht implementiert / Roadmap - -| # | Anforderung | Status | -|---|-------------|--------| -| 9.1 | **Fader/Potentiometer**: 3× ADC-Kanäle auf Board vorhanden, HAL nicht implementiert | 🔲 TODO | -| 9.2 | **HOST_COMMAND-Payload**: Board sendet Command-ID, App-Seite führt aus (halbfertig) | 🔲 TODO | -| 9.3 | **FADE_IN / FADE_OUT** per GUI konfigurierbar (Firmware vorhanden, kein GUI-Eintrag) | 🔲 TODO | - ---- - -## Projekt-Struktur +`main.cpp` startet genau einen `CMainController`. +Die Hauptschleife in `work()` ist: +```text +matrix_scan() +poll_vendor() +processEvents() +check_factory_reset() +updateLEDs() ``` + +Dabei gilt: + +- Matrix und Encoder erzeugen `SEvent`s. +- `processEvents()` fuehrt daraus HID, Makros, Host-Commands oder Profilwechsel aus. +- `poll_vendor()` verarbeitet das 8-Byte-CDC-Protokoll mit Config- und Makro-Transfers. +- `updateLEDs()` rendert nur dann zu den WS2812, wenn sich etwas geaendert hat. + +## Action-System + +Unterstuetzte `ActionType`s: + +| Typ | Verhalten | +|---|---| +| `NONE` | keine Aktion | +| `HID_KEY` | Keyboard-Hold ueber USB HID | +| `HID_CONSUMER` | Media/Consumer-Hold ueber USB HID | +| `HOST_COMMAND` | Event an die GUI per CDC Serial | +| `MACRO` | Firmware spielt Makro-Slot komplett ab | +| `PROFILE_SWITCH` | aktives Profil in NVM wechseln | + +Wichtige Semantik: + +- normale Keys und Consumer folgen dem Hold-Modell +- Encoder `CW` / `CCW` sind immer Tap-Events +- Makros laufen komplett in der Firmware, ohne laufende App + +## LED-System + +Jeder MX-Button hat: + +- eine Base-Farbe +- optional eine temporaere Override-Farbe +- eine Animation + +Aktuelle Animationsmodi: + +- `STATIC` +- `BLINK` +- `PULSE` +- `FADE_IN` +- `FADE_OUT` +- `COLOR_CYCLE` +- `COLOR_FADE` + +Die GUI nutzt derzeit vor allem `STATIC`, `BLINK`, `PULSE` und `COLOR_CYCLE`. + +## Aktuelles NVM-Layout + +### DeviceConfig + +- Version: `3` +- Magic: `0x56503203` +- Groesse: `740` Byte +- CRC16-CCITT ueber Bytes `7..739` +- 3 Profile +- globale Helligkeit +- per-LED-Helligkeit + +### MacroTable + +- 32 Slots +- 8 Steps pro Slot +- 512 Byte gesamt + +### Flash-Bereich + +| Bereich | Adresse | Groesse | +|---|---|---| +| Makros | `0x1FB00-0x1FCFF` | 512 B | +| Config | `0x1FD00-0x1FFFF` | 768 B, davon 740 B genutzt | + +Config und Makros liegen in getrennten reservierten NVM-Bereichen. + +## Werksreset + +Die Firmware hat einen eingebauten Recovery-Pfad: + +- unteren linken und unteren rechten MX-Button gleichzeitig 5 Sekunden halten +- waehrend des Holds leuchten diese beiden Tasten rot +- ihre normalen HID-Aktionen werden waehrenddessen unterdrueckt +- bei Erfolg blinken alle LEDs kurz rot +- danach werden Config und Makros auf Werkseinstellungen zurueckgesetzt und neu geladen + +Reset-Inhalt: + +- alle Aktionen `NONE` +- alle Makro-Slots leer +- Base-LEDs auf Defaultwerte +- sichtbarer Idle-Zustand wieder Regenbogen + +Wichtig: + +- ein SWD-Reflash loescht diese NVM-Daten nicht automatisch +- der Werksreset ist der vorgesehene Weg, um eine kaputte Konfiguration zu bereinigen + +## Projektstruktur + +```text VersaMCU/ -├── platformio.ini – Build- und Upload-Konfiguration -├── upload_openocd.py – Benutzerdefiniertes Upload-Script (OpenOCD) -├── boards/ -│ ├── versapad.json – Board-Definition mit Bootloader -│ └── versapad_nobl.json – Board-Definition ohne Bootloader (aktiv) -├── variants/versapad/ -│ ├── variant.h/.cpp – Pin-Mapping für den SAMD21G17D -│ └── linker_scripts/ – Linkerscript (kein Bootloader, NVM-Reservierung) -└── src/ - ├── main.cpp – Arduino setup()/loop() - ├── CMainController.h/.cpp – Zentraler Orchestrator - ├── CButton.h/.cpp – Button-Modell: LED-Schichten, Action, Animationen - ├── CEventQueue.h/.cpp – Ring-Buffer FIFO (16 Slots, kein Heap) - ├── SEvent.h – Event-Typen (KEY_DOWN/UP, ENC_CW/CCW) - ├── config/ - │ ├── pins.h – Pin-Nummern (Arduino-Nummern aus variant.h) - │ ├── action.h – ActionType-Enum + SAction-Struct (packed) - │ └── nvm_config.h/.cpp – NVM-Config: Laden, Speichern, CRC16, Defaults - └── hal/ - ├── matrix.h/.cpp – 5×5-Matrix-Scan, 10 ms Debounce - ├── encoder.h/.cpp – Quadratur-Dekodierung via EIC-Interrupts - ├── ws2812.h/.cpp – WS2812-LED-Treiber (Adafruit NeoPixel, bit-bang) - ├── usb_hid.h/.cpp – HID Keyboard + Consumer Control - └── usb_serial.h/.cpp – CDC Serial bidirektional, 8-Byte-Pakete +|-- platformio.ini +|-- boards/ +|-- variants/versapad/ +`-- src/ + |-- main.cpp + |-- CMainController.h/.cpp + |-- CButton.h/.cpp + |-- CEventQueue.h/.cpp + |-- SEvent.h + |-- config/ + | |-- action.h + | |-- macro_config.h/.cpp + | `-- nvm_config.h/.cpp + `-- hal/ + |-- encoder.h/.cpp + |-- matrix.h/.cpp + |-- usb_hid.h/.cpp + |-- usb_serial.h/.cpp + `-- ws2812.h/.cpp ``` -## Architektur +## Weiterfuehrende Doku -### Schichten - -``` -┌────────────────────────────────────────────────┐ -│ main.cpp │ -│ CMainController (setup / work) │ -├────────────────────────────────────────────────┤ -│ Modell │ -│ CButton (LED 2-Layer, Action, dirty-Flag) │ -│ CEventQueue (Ring-Buffer FIFO, 16 Slots) │ -├────────────────────────────────────────────────┤ -│ HAL │ -│ hal/matrix – 5×5-Scan, 10 ms Debounce │ -│ hal/encoder – Quadratur via EIC-ISR │ -│ hal/ws2812 – Adafruit NeoPixel bit-bang │ -│ hal/usb_hid – HID Keyboard + Consumer │ -│ hal/usb_serial– CDC Serial bidirektional │ -├────────────────────────────────────────────────┤ -│ Config │ -│ config/pins.h – Pin-Mapping │ -│ config/action.h – ActionType + SAction │ -│ config/nvm_config – Flash R/W, CRC16 │ -└────────────────────────────────────────────────┘ -``` - -### Datenfluss - -``` -HAL (matrix_scan, Encoder-ISR) - └─► matrix_cb / encoder_cb - └─► CEventQueue.push() - └─► CMainController.processEvents() - ├─► CButton.on_press() / on_release() - ├─► execute_action() → usb_hid_send_key / send_consumer - └─► usb_serial_send() (nur bei HOST_COMMAND) - -SerialUSB (PC → Board, 8-Byte-Pakete) - └─► CMainController.poll_vendor() - └─► CButton.set_override() / set_base() / clear_override() - -CMainController.updateLEDs() - └─► CButton.render_led() → ws2812_set() - └─► ws2812_show() (nur wenn dirty) -``` - -### Loop-Ablauf - -``` -loop() - ├── matrix_scan() → matrix_cb() → CEventQueue.push() - │ (Encoder-ISRs laufen asynchron) → CEventQueue.push() - ├── poll_vendor() → Serial-Pakete von VersaGUI verarbeiten - ├── processEvents() → Queue leeren, Aktionen ausführen - └── updateLEDs() → Dirty-CButtons → WS2812-Buffer → show() -``` - -### CButton – LED-Schichten - -Jeder MX-Button hat zwei LED-Schichten: -- **base**: Konfigurierte Idle-Farbe (aus NVM) -- **override**: Temporär von VersaGUI gesetzt (Benachrichtigungen etc.) - -Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sofort zu `base` zurück. - -### LED-Animationen - -Animationen modulieren die Helligkeit oder Farbe der aktiven Schicht (base oder override). Alle Berechnungen in **Integer-Arithmetik** (Cortex-M0+ hat keine FPU). - -| Animation | Typ | Verhalten | `period_ms`-Semantik | -|---|---|---|---| -| `STATIC` | – | Feste Helligkeit (Standardzustand) | – | -| `BLINK` | Helligkeit | Binäres An/Aus, endlos | Halbperiode (An = Aus) | -| `PULSE` | Helligkeit | Lineares Dreieck 0→255→0, endlos | Vollperiode | -| `FADE_IN` | Helligkeit | Einmalig: schwarz → voll, dann STATIC | Dauer | -| `FADE_OUT` | Helligkeit | Einmalig: voll → schwarz, dann STATIC + base=schwarz | Dauer | -| `COLOR_CYCLE` | Farbe | Hue-Sweep Regenbogen, endlos, ignoriert base/override | Eine volle Runde | -| `COLOR_FADE` | Farbe | Einmalig: Crossfade akt. Farbe → Zielfarbe, dann STATIC + base=Ziel | Dauer | - -**API:** - -```cpp -set_anim(LEDAnim, period_ms, phase_offset_ms = 0) -// Für alle Typen außer COLOR_FADE. -// phase_offset_ms verschiebt den Startpunkt in die Vergangenheit → -// mehrere LEDs versetzt starten (z. B. Regenbogen-Welle). - -set_color_fade(RGB to, period_ms) -// Startet COLOR_FADE von aktueller Farbe zu `to`. - -clear_anim() -// Sofort zurück zu STATIC (volle Helligkeit). - -render_led() -// Gibt true zurück solange dirty oder Animation läuft → ws2812_show() nötig. -``` - -**Idle-Zustand:** Alle 20 MX-LEDs laufen mit `COLOR_CYCLE` (4 s/Runde, 40 % Helligkeit). Der Phasenversatz ist gleichmäßig über alle 20 LEDs verteilt, sodass immer ein vollständiger Regenbogen auf dem Pad liegt. - -**Warum Bit-Bang statt DMA?** - -WS2812-DMA auf dem SAMD21 würde einen SERCOM im SPI-Modus bei exakt 2,4 MHz benötigen, wobei jedes WS2812-Bit als 3 SPI-Bits kodiert wird (`110` = 1, `100` = 0). Das erfordert einen zusätzlichen Puffer von 20 LEDs × 24 Bit × 3 = 1440 Byte — mehr als 8 % des gesamten RAM — plus DMAC-Konfiguration und Transfer-Ende-Erkennung. - -`ws2812_show()` blockiert ~600 µs mit gesperrten Interrupts, wird aber nur bei gesetztem dirty-Flag aufgerufen. Bei 20 ms Loop-Rate entspricht das 3 % der Loop-Zeit. Encoder-Impulse, die in dieses Fenster fallen, werden maximal um eine Loop-Iteration verzögert; bei typischen Drehgeschwindigkeiten (< 20 Rastschritte/s, Impulsabstand > 50 ms) ist das Risiko eines verlorenen Impulses praktisch null. - -Ergebnis: Bit-Bang via Adafruit NeoPixel reicht für 20 LEDs vollständig aus, belegt keinen SERCOM und keinen zusätzlichen RAM. - -**Warum keine Adafruit-Animationsfunktionen?** - -Die Adafruit-NeoPixel-Library stellt ausschließlich den LED-Treiber bereit (`setPixelColor`, `show`, `fill`, `clear`). Animations-Logik (Blinken, Pulsieren, Farbverläufe) ist nicht enthalten und muss in jedem Fall selbst implementiert werden. Darüber hinaus: - -- `Adafruit_NeoPixel::ColorHSV()` verwendet intern float-Operationen für die HSV→RGB-Konvertierung. Der Cortex-M0+ hat keine FPU; float wird per Software emuliert (~10–20× langsamer). `hue_to_rgb()` in `CButton.cpp` erreicht dasselbe Ergebnis mit reiner Integer-Arithmetik (6 lineare Segmente à 43 Hue-Einheiten). -- Das 2-Schicht-Modell (base + override) und die dirty-Flag-gesteuerte Render-Pipeline sind projektspezifische Logik ohne Entsprechung in der Library. - -**Idle-Zustand:** Alle 20 MX-LEDs zeigen einen rotierenden Regenbogen (`COLOR_CYCLE`, 4 s/Runde, 40 % Helligkeit, gleichmäßig phasenverschoben). - -### Serial-Protokoll (8 Bytes, fixed) - -``` -Byte 0: Command/Event-ID -Byte 1: key_id (Button 0–24 oder Encoder 0–3) -Byte 2: r / Daten-Byte A -Byte 3: g / Daten-Byte B -Byte 4: b -Byte 5–7: reserviert (0x00) -``` - -| ID | Richtung | Bedeutung | -|---|---|---| -| 0x01 | PC→Board | LED-Override setzen | -| 0x02 | PC→Board | LED-Override löschen | -| 0x03 | PC→Board | LED-Base setzen | -| 0x05 | PC→Board | Ping | -| 0x10 | PC→Board | Config-Begin (Chunks-Anzahl) | -| 0x11 | PC→Board | Config-Data (Chunk-Index + 6B Nutzdaten) | -| 0x12 | PC→Board | Config-Commit (CRC prüfen + NVM schreiben) | -| 0x13 | PC→Board | Config-Read (Board sendet NVM-Config zurück) | -| 0x81 | Board→PC | KEY_DOWN | -| 0x82 | Board→PC | KEY_UP | -| 0x83 | Board→PC | ENC_CW | -| 0x84 | Board→PC | ENC_CCW | -| 0x85 | Board→PC | Pong | -| 0x90 | Board→PC | Config-ACK | -| 0x91 | Board→PC | Config-NACK | -| 0x20 | PC→Board | Makro-Begin (Chunk-Anzahl = 43) | -| 0x21 | PC→Board | Makro-Data (Chunk-Index + 6B Nutzdaten) | -| 0x22 | PC→Board | Makro-Commit (in NVM schreiben) | -| 0x23 | PC→Board | Makro-Read (Board sendet Tabelle zurück) | -| 0x90 | Board→PC | Config-ACK | -| 0x91 | Board→PC | Config-NACK (CRC/Magic/Version ungültig) | -| 0x92 | Board→PC | Config-Begin (Dump-Start, Chunks-Anzahl) | -| 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) | -| 0x94 | Board→PC | Config-End | -| 0x95 | Board→PC | Makro-ACK | -| 0x96 | Board→PC | Makro-Begin (Dump-Start) | -| 0x97 | Board→PC | Makro-Data (Chunk-Index + 6B) | -| 0x98 | Board→PC | Makro-End | - -### NVM-Config-Layout (Version 2, 223 Bytes, packed) - -``` -Offset 0 4B Magic 0x56503202 -Offset 4 1B Version 2 -Offset 5 2B CRC16-CCITT (über Bytes 7–222) -Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B) -Offset 67 36B enc_actions[4][3] – je 3B -Offset 103 20B led_r[20] -Offset 123 20B led_g[20] -Offset 143 20B led_b[20] -Offset 163 20B led_anim[20] – LEDAnim-Typ (uint8_t) -Offset 183 40B led_period_ms[20] – Animationsperiode in ms (uint16_t, LE) -``` - -**NVM Row 0** (0x1FE00, 256 Byte): Config (223B genutzt, 33B Padding) -**NVM Row 1** (0x1FF00, 256 Byte): Makro-Tabelle (32 Slots × 4 Steps × 2B) -Via Linkerscript reserviert. Bei ungültigem Magic/Version/CRC werden Defaults geladen. - -## Bekannte Fallstricke - -| Problem | Lösung | -|---|---| -| Kaltstart hängt (XOSC32KRDY) | `-DCRYSTALLESS` in build_flags pflicht | -| Adafruit NeoPixel ZeroDMA inkompatibel | Standard bit-bang Library verwenden | -| WS2812 Pegel 3.3V statt 5V | LED 0 marginal OK, ab LED 1 selbst-regenerierend. Fix nächste PCB-Rev: Level-Shifter | -| `ws2812_show()` blockiert ~600 µs | Dirty-Flag-Pattern: nur aufrufen wenn nötig, nie aus ISR | -| SAction muss `__attribute__((packed))` haben | Ohne packed: 4B statt 3B → CRC-Mismatch beim Config-Laden | -| Windows HID-Descriptor-Cache | Bei PID-Änderung Board neu einstecken | -| `SERCOM5 CTRLB.RXEN` Sync-Bug | `while(SYNCBUSY.bit.CTRLB)` hängt vor ENABLE → nur relevant bei manueller SERCOM5-Konfiguration; nicht im Normalbetrieb | -| `PluggableUSBModule` nicht nutzbar | `USB_SendControl` / `USB_RecvControl` nicht verlinkt in dieser Core-Version → CDC Serial statt Vendor HID für PC-Kommunikation verwenden | -| HID-Descriptor vor USB-Enumeration registrieren | Registrierung via globalem Konstruktor (läuft vor `main()`), nicht in `setup()` | - ---- - -## Next-Generation Hardware – MCU-Empfehlung - -### Warum der SAMD21G17D an seine Grenzen stößt - -| Einschränkung | Auswirkung auf geplante Features | -|---|---| -| **128 KB Flash** | Mehrere Profile + lange Makros + OLED-Fonts füllen den Speicher vollständig | -| **16 KB RAM** | OLED-Framebuffer (128×64 px = 1 KB) + Profil-Puffer + Makro-Tabellen + Stack = kaum Luft | -| **Kein FPU** | LED-Animationen erfordern Integer-Arithmetik-Workarounds; aufwändigere Effekte unwirtschaftlich | -| **Kein Ethernet-MAC** | Ethernet nur via langsamen SPI-Chip möglich | -| **Kein USB High-Speed** | CDC bleibt auf 12 Mbit/s (Full Speed); für schnelle Konfigurationsübertragungen ausreichend, aber kein Spielraum | -| **WS2812 bit-bang blockiert 600 µs** | Mit mehr LEDs oder höherer Auflösung kritisch; kein DMA ohne SERCOM-Umbau | - ---- - -### Empfehlung: Microchip SAME54P20A - -**Primäre Empfehlung** – selber Hersteller, gleicher Toolchain-Stack, deutlich mehr Reserven. - -| Merkmal | SAMD21G17D (aktuell) | SAME54P20A (empfohlen) | -|---|---|---| -| Kern | Cortex-M0+, 48 MHz | Cortex-M4F, 120 MHz | -| **FPU** | ✗ | ✓ (single-precision) | -| **Flash** | 128 KB | **1 MB** | -| **RAM** | 16 KB | **256 KB** | -| **USB** | Full Speed (12 Mbit/s) | Full Speed + optionaler HS-PHY | -| **Ethernet MAC** | ✗ | ✓ (IEEE 802.3, braucht ext. PHY) | -| **DMA** | 12 Kanäle | 32 Kanäle | -| SERCOM | 6 | 8 | -| NVM (intern) | 128 KB | 1 MB (kein ext. Flash nötig) | -| Preis (LCSC ca.) | ~2 € | ~8–10 € | - -**Warum SAME54?** -- Direkter Upgrade-Pfad: Arduino-Framework, PlatformIO, gleiche HAL-Konzepte -- Ethernet-MAC integriert → nur externer PHY nötig (z.B. **KSZ8081** oder **LAN8720A**, ~1–2 €) -- 1 MB Flash reicht für viele Profile, lange Makros und OLED-Font-Tabellen ohne externen Flash -- 256 KB RAM: OLED-Framebuffer, Profil-Puffer und komplexe Makro-Engines kein Problem -- FPU: sauberere LED-Animationen, kein Integer-Workaround mehr nötig -- DMA: WS2812 via SERCOM-SPI + DMA möglich → kein Bit-Bang, keine Interrupt-Sperre - ---- - -### Alternative: Raspberry Pi RP2350 - -Falls Ethernet nicht zwingend auf dem MCU selbst integriert sein muss (z.B. W5500 via SPI): - -| Merkmal | RP2350 | -|---|---| -| Kern | Dual Cortex-M33 oder RISC-V, 150 MHz | -| FPU | ✓ | -| RAM | **520 KB** SRAM | -| Flash | Kein interner; ext. QSPI (typ. 2–16 MB) | -| USB | Full Speed (Device + Host) | -| Ethernet MAC | ✗ (W5500 via SPI, ~3 €) | -| PIO | 3 × 4 PIO-Blöcke → WS2812 hardwareseitig ohne CPU | -| Preis | ~1,50 € | - -**Vorteil:** PIO-Blöcke übernehmen WS2812-Timing hardwareseitig (kein bit-bang, kein DMA-Setup). Sehr viel RAM für komplexe Logik. Günstiger als SAME54. - -**Nachteil:** Kein integrierter Ethernet-MAC. W5500 übernimmt TCP/IP-Stack per SPI (ausreichend für einfaches HTTP/Telnet-Protokoll), ist aber kein vollwertiger Network-Stack. - ---- - -### System-Architektur für PoE-Betrieb - -PoE erfordert unabhängig vom MCU zusätzliche Hardware: - -``` -RJ45-Buchse (mit integrierten Magnetics) - └─► PoE-PD-Controller (z.B. TPS2372-4, AG9800) - ├─► DC/DC-Wandler → 3.3V / 5V Versorgung des Boards - └─► Ethernet-Signal → MCU-Ethernet-MAC → ext. PHY (LAN8720A / KSZ8081) -``` - -Der PoE-PD-Controller ist zwingend: Er verhandelt mit dem PoE-Switch (IEEE 802.3af/at), isoliert galvanisch und liefert geregelte Spannung. Typische PoE-Leistungsklasse 0 (15,4 W) reicht für MCU + LEDs + Display mehrfach aus. - -**Empfohlene ICs:** - -| Funktion | IC | Preis ca. | -|---|---|---| -| PoE PD Controller | AG9800 oder TPS2372-4 | 1–3 € | -| Ethernet PHY | LAN8720A oder KSZ8081 | 1–2 € | -| QSPI-Flash (falls RP2350) | W25Q128 (16 MB) | ~0,80 € | -| OLED Controller | SSD1306 (128×64, I2C/SPI) | im Modul enthalten | - ---- - -### Empfehlung nach Szenario - -| Szenario | Empfehlung | -|---|---| -| Voller Feature-Umfang (Ethernet-MAC, PoE, Profile, OLED) | **SAME54P20A** + LAN8720A + PoE-PD | -| Maximale RAM/Flash-Reserve, WS2812 ohne CPU-Last | **RP2350** + W5500 + PoE-PD | -| Minimaler Footprint, kein Ethernet | **RP2040** (günstiger als RP2350, 264 KB RAM reicht für OLED + Profile) | - -Für VersaPad v3 mit allen genannten Features ist der **SAME54P20A** die solideste Wahl: gleicher Hersteller, etablierter Toolchain, integrierter Ethernet-MAC, und der Firmware-Code von v2 (HAL-Struktur, Event-Queue, CButton-Modell) ist weitgehend übertragbar. +- [doc/INDEX.md](doc/INDEX.md) +- [doc/00_architecture.md](doc/00_architecture.md) +- [doc/03_action_engine.md](doc/03_action_engine.md) +- [doc/04_macro_system.md](doc/04_macro_system.md) +- [doc/06_nvm_config.md](doc/06_nvm_config.md) +- [doc/07_serial_protocol.md](doc/07_serial_protocol.md) diff --git a/doc/00_architecture.md b/doc/00_architecture.md index a41ba84..c743af5 100644 --- a/doc/00_architecture.md +++ b/doc/00_architecture.md @@ -1,83 +1,107 @@ -# VersaMCU – Architektur-Übersicht +# VersaMCU - Architekturuebersicht -## Ziel-Hardware +## Zielplattform | Merkmal | Wert | |---|---| -| MCU | ATSAMD21G17D (Cortex-M0+, 48 MHz) | -| Flash | 128 KB (davon 512 B am Ende für NVM-Config reserviert) | +| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz | +| Flash | 128 KB | | RAM | 16 KB | -| FPU | Keine – alle Berechnungen in Integer-Arithmetik | -| USB | Native USB, DFLL48M via USB-SOF-Kalibrierung (`-DCRYSTALLESS`) | -| Framework | Arduino + PlatformIO, kein Bootloader (Direktflash via SWD/Atmel-ICE) | +| FPU | keine, deshalb Integer-Arithmetik | +| USB | HID Keyboard + Consumer + CDC Serial | +| Toolchain | PlatformIO + Arduino Core | -## Loop-Ablauf +## Setup und Loop -``` +```text setup() - ├── macro_config_load() – Makro-Tabelle aus NVM in RAM laden - ├── init_buttons() – CButton-Objekte aus NVM initialisieren - ├── usb_hid_init() – HID-Descriptor (No-Op, läuft via global ctor) - ├── usb_serial_init() – CDC Serial öffnen - ├── matrix_init(cb) – 5×5-Matrix + Debounce-Zustand - └── encoder_init(cb) – EIC-Interrupts für 4 Encoder + macro_config_load() + nvm_config_load() + init_buttons() + usb_hid_init() + usb_serial_init() + matrix_init(cb) + encoder_init(cb) -loop() [~20 ms Iteration] - ├── matrix_scan() – Debounce-Zustand prüfen → Events in Queue - ├── poll_vendor() – CDC-Pakete vom PC verarbeiten (LED-Cmds, Config, Makros) - ├── processEvents() – Queue leeren: Aktionen ausführen, HOST_COMMAND melden - └── updateLEDs() – Dirty-CButtons → WS2812-Buffer → show() (nur wenn dirty) +loop() + matrix_scan() + poll_vendor() + processEvents() + check_factory_reset() + updateLEDs() ``` -Encoder-ISRs laufen asynchron (CHANGE-Interrupt auf A und B) und schreiben direkt in die Event-Queue. Die Queue ist interrupt-sicher (keine Locks nötig auf Single-Core-M0+). +Die Reihenfolge ist absichtlich simpel: + +- Eingaben einsammeln +- CDC-Kommandos vom Host verarbeiten +- Event-Queue leeren +- Sonderlogik fuer den Werksreset pruefen +- LED-Frame nur bei Bedarf rendern ## Datenfluss -``` -HAL-Callbacks (matrix_cb, encoder_cb) - └─► CEventQueue (16 Slots, Ring-Buffer, kein Heap) - └─► processEvents() - ├─► CButton.on_press() / on_release() [Hooks, aktuell leer] - ├─► execute_action() → USB HID / Makro-Ablauf - └─► usb_serial_send() → HOST_COMMAND-Events an PC +```text +matrix_scan / encoder ISR + -> EventQueue + -> processEvents() + -> execute_action_down / execute_action_up + -> usb_hid_* + -> usb_serial_send() fuer HOST_COMMAND -SerialUSB (CDC, PC → Board) - └─► poll_vendor() - ├─► CButton.set_override() / clear_override() / set_base() - └─► Config/Makro-Transfer (chunked, 6 B/Paket) +CDC Serial + -> poll_vendor() + -> Config/Makros einlesen oder dumpen + -> LED-Overrides setzen/loeschen + +LED-Render + -> CButton.render_led() + -> ws2812_set() + -> ws2812_show() nur wenn dirty ``` -## Komponenten-Übersicht +## Zentrale Komponenten -| Datei | Verantwortung | +| Datei | Aufgabe | |---|---| -| `main.cpp` | `setup()` / `loop()` – ruft nur CMainController auf | -| `CMainController` | Zentraler Orchestrator, hält alle CButton-Instanzen | -| `CButton` | LED-Layering, Animations-Engine, Action-Referenz | -| `CEventQueue` | ISR-sicherer Ring-Buffer, 16 Events | -| `hal/matrix` | 5×5-Matrix-Scan, 10 ms Debounce | -| `hal/encoder` | Quadratur-Dekodierung via EIC-ISR | -| `hal/ws2812` | Thin Wrapper um Adafruit NeoPixel (bit-bang) | -| `hal/usb_hid` | HID Keyboard + Consumer Control | -| `hal/usb_serial` | CDC bidirektional, 8-Byte-Pakete, Ring-Buffer | -| `config/nvm_config` | SDeviceConfig: laden, speichern, CRC16, Defaults | -| `config/macro_config` | SMacroTable: laden, speichern (NVM Row 1) | -| `config/action` | SAction-Struct + ActionType-Enum | +| `main.cpp` | startet den Controller | +| `CMainController.*` | Orchestrator fuer Inputs, Actions, Serial, LEDs | +| `CButton.*` | LED-Zustand, Animationen, Action-Referenz | +| `CEventQueue.*` | ISR-sicherer Ringbuffer | +| `config/nvm_config.*` | Config v3 laden, speichern, Defaults | +| `config/macro_config.*` | Makros laden, speichern | +| `hal/matrix.*` | 5x5-Matrixscan mit Debounce | +| `hal/encoder.*` | Encoder-ISR und Drehrichtung | +| `hal/usb_hid.*` | Keyboard- und Consumer-HID | +| `hal/usb_serial.*` | CDC-Paketpfad | +| `hal/ws2812.*` | WS2812-Treiber | ## Key-ID-Schema -``` -key_id 0–3 : Encoder-SW-Buttons (COL_0 × ROW_0–3), kein LED -key_id 4 : nicht belegt (COL_0 × ROW_4) -key_id 5–24 : MX-Buttons (COL_1–4 × ROW_0–4), je ein WS2812-LED +```text +0..3 = Encoder-SW +4 = unbenutzt +5..24 = MX-Buttons ``` -LED-Index folgt serpentiner Verdrahtung: `LED_INDEX(col, row)`. +Die beiden Werksreset-Tasten sind: -## Invarianten / Constraints +- `key_id 9` = unten links +- `key_id 24` = unten rechts -- **Kein Heap**: kein `new`/`malloc` – alle Objekte statisch oder als Felder in CMainController. -- **Kein Float**: Cortex-M0+ hat keine FPU; Float würde per Software emuliert (~10–20× langsamer). -- **Packed Structs**: `SAction` und `SDeviceConfig` sind `__attribute__((packed))` damit die Byte-Größen mit der C#-Serialisierung in VersaGUI übereinstimmen. -- **Aligned NVM-Writes**: `nvm_write_page` castet Pointer zu `uint32_t*`; Puffer müssen vor dem Aufruf in `__attribute__((aligned(4)))`-Variablen kopiert werden (sonst HardFault auf M0+). -- **DTR-Check**: `usb_serial_send()` prüft ob SerialUSB aktiv ist, bevor Bytes gesendet werden. +## Werksreset im Ablauf + +Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware: + +- sobald beide Reset-Tasten gleichzeitig gehalten werden, werden ihre normalen Actions unterdrueckt +- falls bereits ein HID-Hold aktiv war, wird er sofort freigegeben +- nach 5 Sekunden gemeinsamer Haltezeit wird Default-Config + leere Makro-Tabelle in NVM geschrieben +- danach folgt ein kurzes rotes Feedback-Blinken + +## Invarianten + +- kein Heap +- keine Floats +- `packed` fuer serielle und NVM-relevante Structs +- NVM-Schreibpuffer muessen 4-Byte-aligned sein +- `usb_serial_send()` sendet nur bei aktiver CDC-Verbindung diff --git a/doc/03_action_engine.md b/doc/03_action_engine.md index 1786262..66d75c5 100644 --- a/doc/03_action_engine.md +++ b/doc/03_action_engine.md @@ -1,100 +1,103 @@ # Aktions-Engine -**Dateien:** `config/action.h`, `CButton.h/.cpp`, `CMainController.cpp` (`processEvents`, `execute_action_down`, `execute_action_up`) +Dateien: -## SAction-Struct +- `config/action.h` +- `CMainController.h/.cpp` +- `CButton.h/.cpp` + +## `SAction` ```cpp struct __attribute__((packed)) SAction { - ActionType type; // 1 Byte - uint16_t data; // 2 Bytes (Keycode, Consumer-Code, Command-ID oder Slot-Index) + ActionType type; + uint16_t data; }; -// Gesamt: 3 Bytes (packed! ohne packed wären es 4 durch Alignment) ``` -`packed` ist zwingend damit `sizeof(SDeviceConfig) == 740` mit der C#-Serialisierung in VersaGUI übereinstimmt. +Groesse: 3 Byte. +Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI uebereinstimmen muss. -## ActionType +## `ActionType` -| Typ | Wert | Bedeutung | data-Inhalt | -|---|---|---|---| -| `NONE` | 0 | Keine Aktion | — | -| `HID_KEY` | 1 | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier | -| `HID_CONSUMER` | 2 | Consumer Control (Volume, Media, …) | Consumer Usage ID | -| `HOST_COMMAND` | 3 | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | -| `MACRO` | 4 | Makro-Sequenz aus NVM-Tabelle | Slot-Index 0–31 | -| `PROFILE_SWITCH` | 5 | Aktives Profil wechseln | 0–2 = Ziel-Profil, 0xFF = nächstes Profil (Zyklus 0→1→2→0) | +| Typ | Bedeutung | `data` | +|---|---|---| +| `NONE` | keine Aktion | - | +| `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier | +| `HID_CONSUMER` | Media/Consumer-HID | usage id | +| `HOST_COMMAND` | Event an die GUI | command id | +| `MACRO` | Makro aus `SMacroTable` | slot 0..31 | +| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil | -## execute_action_down() — Taste gedrückt (Hold-Start) +## Verhalten bei `KEY_DOWN` -| ActionType | Verhalten | +| Typ | Effekt | |---|---| -| `HID_KEY` | `usb_hid_send_key(keycode, modifier)` — Taste bleibt gedrückt bis `execute_action_up()` | -| `HID_CONSUMER` | `usb_hid_send_consumer(usage_id)` — bleibt aktiv bis `execute_action_up()` | -| `HOST_COMMAND` | `usb_serial_send(USB_EVT_KEY_DOWN, key_id)` | -| `MACRO` | Volle Sequenz ausführen (Steps[slot], keycode==0 = Ende, delay 10+20 ms) | -| `PROFILE_SWITCH` | NVM laden → `active_profile` setzen → CRC neu berechnen → NVM speichern → `init_buttons()` | -| `NONE` | nop | +| `HID_KEY` | `usb_hid_send_key()` | +| `HID_CONSUMER` | `usb_hid_send_consumer()` | +| `HOST_COMMAND` | `usb_serial_send(KEY_DOWN/ENC_*)` | +| `MACRO` | komplette Sequenz sofort abspielen | +| `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren | +| `NONE` | nichts | -## execute_action_up() — Taste losgelassen (Hold-Ende) +## Verhalten bei `KEY_UP` -| ActionType | Verhalten | +| Typ | Effekt | |---|---| | `HID_KEY` | `usb_hid_release_key()` | | `HID_CONSUMER` | `usb_hid_release_consumer()` | -| `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) | -| `MACRO`/`PROFILE_SWITCH`/`NONE` | nop | +| `HOST_COMMAND` | optionaler Up-Pfad, derzeit praktisch ohne Nutzlast | +| `MACRO` | nichts | +| `PROFILE_SWITCH` | nichts | +| `NONE` | nichts | -## PROFILE_SWITCH — Ablauf +## Hold- und Tap-Modell -```cpp -SDeviceConfig cfg; -nvm_config_load(cfg); // komplette Config aus NVM -uint8_t target = (uint8_t)action.data; -if (target == 0xFF) - target = (cfg.active_profile + 1) % 3; // Zyklus -cfg.active_profile = target; -cfg.crc = nvm_config_crc(cfg); // CRC MUSS nach Änderung neu berechnet werden! -if (nvm_config_save(cfg)) // bool: false = NVM-Timeout - init_buttons(); +- MX-Buttons und Encoder-SW benutzen fuer HID und Consumer das Hold-Modell. +- Encoder `CW` und `CCW` sind immer diskrete Tap-Events: + +```text +down -> delay(10 ms) -> up ``` -> **Wichtig:** `active_profile` liegt im CRC-geschützten Bereich (ab Byte 7). Wird die CRC nicht aktualisiert, findet das nächste `nvm_config_load()` einen CRC-Fehler und lädt die Defaults (alle Aktionen NONE, alle LEDs Regenbogen). +- Makros laufen komplett synchron in der Firmware. -## Hold-Modell (HID-Keys und Consumer Controls) +## Makro-Ausfuehrung -Normale Tasten- und Media-Aktionen folgen dem **Hold-Modell**: +Bei `ActionType::MACRO` wird `action.data` als Slot interpretiert. +Die Firmware laeuft dann durch bis zu 8 Steps: -``` -KEY_DOWN-Event vom Board → execute_action_down() → HID Key-Down senden -[Taste bleibt physisch gedrückt...] -KEY_UP-Event vom Board → execute_action_up() → HID Key-Up senden +```text +step.keycode == 0 -> Ende +Key-Down +10 ms warten +Key-Up +20 ms warten ``` -Das OS erkennt die gedrückte Taste und startet sein eigenes Key-Repeat nach ~500 ms — wie auf einer normalen Tastatur. +## Profilwechsel -## Tap-Modell (Encoder CW/CCW) +`PROFILE_SWITCH` arbeitet direkt auf der gespeicherten Config: -Encoder-Bewegungen sind diskret (kein Halten möglich) und verwenden das **Tap-Modell**: +1. Config aus NVM laden +2. `active_profile` aendern +3. CRC neu berechnen +4. wieder speichern +5. `init_buttons()` -``` -ENC_CW/ENC_CCW-Event → execute_action_down() + delay(10) + execute_action_up() -``` +Wichtig: +`active_profile` liegt im CRC-geschuetzten Bereich. Ohne neue CRC wuerde die Config beim naechsten Laden verworfen. -(Atomare Sequenz für jeden Encoder-Schritt.) +## Sonderfall Werksreset -## Work-Loop-Reihenfolge +Die Reset-Kombination uebersteuert das normale Action-System fuer genau zwei Tasten: -```cpp -void work() { - matrix_scan(); // → Events in Queue (KEY_DOWN, KEY_UP, ENC_CW, ENC_CCW) - poll_vendor(); // Serial-Pakete verarbeiten (PC↔Board Kommandos) - processEvents(); // → execute_action_down/up() aufrufen - updateLEDs(); // Dirty-LEDs aktualisieren -} -``` +- `key_id 9` +- `key_id 24` -**processEvents() verarbeitet:** -- `KEY_DOWN` → `execute_action_down()` -- `KEY_UP` → `execute_action_up()` -- `ENC_CW` / `ENC_CCW` → `execute_action_down()` + `delay(10)` + `execute_action_up()` +Sobald beide gleichzeitig gehalten werden: + +- ihre normalen Actions werden nicht weiter ausgefuehrt +- eventuell bereits gestartete HID-Holds werden sofort freigegeben +- die LEDs der beiden Tasten leuchten rot +- nach 5 Sekunden wird `perform_factory_reset()` ausgefuehrt diff --git a/doc/04_macro_system.md b/doc/04_macro_system.md index d439a82..d1f702e 100644 --- a/doc/04_macro_system.md +++ b/doc/04_macro_system.md @@ -1,66 +1,87 @@ # Makro-System -**Dateien:** `config/macro_config.h`, `config/macro_config.cpp`, `CMainController.cpp` +Dateien: + +- `config/macro_config.h` +- `config/macro_config.cpp` +- `CMainController.cpp` ## Datenstruktur ```cpp struct __attribute__((packed)) SMacroStep { - uint8_t keycode; // HID Keyboard Usage (0x00 = leer → Step überspringen) - uint8_t modifier; // HID Modifier: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08 + uint8_t keycode; + uint8_t modifier; }; #define MACRO_SLOTS 32 #define MACRO_MAX_STEPS 8 struct __attribute__((packed)) SMacroTable { - SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; // 32 × 8 × 2 = 512 Byte + SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; }; ``` -Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 512 == zwei NVM-Rows`. +Gesamtgroesse: -## NVM-Speicherort +- `32 * 8 * 2 = 512` Byte +- verteilt auf zwei NVM-Rows + +## Speicherort | Row | Adresse | Inhalt | |---|---|---| -| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0–255 | -| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256–511 | +| Macro Row 0 | `0x1FB00` | Bytes `0..255` | +| Macro Row 1 | `0x1FC00` | Bytes `256..511` | -Beide Rows sind im Linkerscript reserviert. Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt `false` zurück → leere Tabelle (alle Keycodes 0). +## Slot-Konvention -## Slot-Zuweisung (Konvention, Board speichert blind) +Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung: -| Slots | Verwendung | +| Slots | Bedeutung | |---|---| -| 0–19 | MX-Button `mx_idx` (entspricht key_id − 5) | -| 20–31 | Encoder-Aktionen (`enc * 3 + act_idx`, 0=SW / 1=CW / 2=CCW) | +| `0..19` | MX-Buttons | +| `20..31` | Encoder-Aktionen (`enc * 3 + act_idx`) | -## Laden und Speichern +## Laden -**Laden** (`macro_config_load`): -- `memcpy` direkt aus Flash-Adresse in RAM-Struct -- Kein Magic/CRC — leere Tabelle (alle 0xFF) ist ein akzeptabler Zustand +`macro_config_load()`: -**Speichern** (`macro_config_save`) — gibt `bool` zurück: -- SMacroTable in `uint8_t aligned_buf[512] __attribute__((aligned(4)))` kopieren (Pflicht!) -- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) -- Beide Rows löschen (`nvm_erase_row`) — bei NVM-Timeout: `return false` -- 8 Pages à 64 Byte schreiben (`nvm_write_page`) — bei NVM-Timeout: `return false` -- `return true` +- kopiert 512 Byte aus NVM in `SMacroTable` +- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben" +- setzt dann eine leere Tabelle -> **Warum aligned_buf?** `nvm_write_page` castet den Pointer zu `volatile uint32_t*`. Wenn `&tbl` nicht 4-Byte-aligned ist (möglich bei packed struct), entsteht ein HardFault auf Cortex-M0+ (kein unaligned 32-Bit-Zugriff auf Peripherie-Adressen). +Eine leere Tabelle ist also ein gueltiger Default-Zustand. -## Ausführung (in execute_action_down, ActionType::MACRO) +## Speichern -``` -slot = action.data (0–31) -für Step 0–7: - if step.keycode == 0: abbrechen - HID Key-Down (keycode, modifier) - delay(10 ms) - HID Key-Up - delay(20 ms) -``` +`macro_config_save()`: -Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung. +1. Tabelle in einen 4-Byte-aligned Puffer kopieren +2. beide Rows loeschen +3. 8 Pages zu je 64 Byte schreiben + +Rueckgabewert: + +- `true` bei Erfolg +- `false` bei NVM-Timeout + +## Ausfuehrung + +Beim Triggern eines Makros: + +- Slot aus `action.data` +- bis zu 8 Steps abarbeiten +- `keycode == 0` beendet das Makro vorzeitig +- pro Step: + - HID key down + - 10 ms warten + - HID key up + - 20 ms warten + +Die Ausfuehrung laeuft aus `m_macros` im RAM, nicht direkt aus NVM. + +## Zusammenhang mit Werksreset + +Beim Werksreset wird die komplette `SMacroTable` auf 0 gesetzt und in beide Makro-Rows zurueckgeschrieben. +Danach sind alle 32 Slots leer. diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md index 7eb832e..b081b63 100644 --- a/doc/06_nvm_config.md +++ b/doc/06_nvm_config.md @@ -1,107 +1,128 @@ # NVM-Konfiguration -**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` +Dateien: -## Flash-Layout (5 Rows, 0x1FB00–0x1FFFF) +- `config/nvm_config.h` +- `config/nvm_config.cpp` -| Row | Adresse | Größe | Inhalt | +## Flash-Layout + +| Bereich | Adresse | Groesse | Inhalt | |---|---|---|---| -| Macro Row 0 | `0x1FB00` | 256 B | SMacroTable Bytes 0–255 | -| Macro Row 1 | `0x1FC00` | 256 B | SMacroTable Bytes 256–511 | -| Config Row 0 | `0x1FD00` | 256 B | Globaler Header + Profil 0 (teilweise) | -| Config Row 1 | `0x1FE00` | 256 B | Profil 0 (Rest) + Profil 1 (teilweise) | -| Config Row 2 | `0x1FF00` | 256 B | Profil 1 (Rest) + Profil 2 + 28 B Reserve | +| Macro Row 0 | `0x1FB00` | 256 B | `SMacroTable` Bytes `0..255` | +| Macro Row 1 | `0x1FC00` | 256 B | `SMacroTable` Bytes `256..511` | +| Config Row 0 | `0x1FD00` | 256 B | Header + Profil 0 Anfang | +| Config Row 1 | `0x1FE00` | 256 B | Profil 0 Rest + Profil 1 Anfang | +| Config Row 2 | `0x1FF00` | 256 B | Profil 1 Rest + Profil 2 + Reserve | -Alle Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. Config und Makros liegen in vollständig getrennten, zusammenhängenden Row-Blöcken. +Makros und Config sind komplett getrennt. -## SDeviceConfig – Byte-Layout (740 Byte, packed) +## `SDeviceConfig` -### Globaler Header (32 B, Offset 0) +Aktueller Stand: -| Offset | Größe | Feld | +- Magic: `0x56503203` +- Version: `3` +- Groesse: `740` Byte +- auf 3 Config-Rows verteilt + +### Header + +| Offset | Groesse | Feld | |---|---|---| -| 0 | 4 | `magic` = `0x56503203` ('VP2\x03') | -| 4 | 1 | `version` = 3 | -| 5 | 2 | `crc` – CRC16-CCITT über Bytes 7–739 | -| 7 | 1 | `active_profile` (0–2) | -| 8 | 1 | `global_brightness` (0–255) | -| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder, Default 1) | -| 13 | 19 | Reserve | +| `0` | 4 | `magic` | +| `4` | 1 | `version` | +| `5` | 2 | `crc` | +| `7` | 1 | `active_profile` | +| `8` | 1 | `global_brightness` | +| `9` | 4 | `enc_sensitivity[4]` | +| `13` | 19 | Reserve | -### Pro Profil (236 B, Offset `32 + idx × 236`) +### Pro Profil -| Offset | Größe | Feld | +Jedes Profil belegt 236 Byte: + +| Offset im Profil | Groesse | Feld | |---|---|---| -| 0 | 60 | `mx_actions[20]` – 20 × 3 B SAction | -| 60 | 36 | `enc_actions[4][3]` – 12 × 3 B SAction | -| 96 | 20 | `led_r[20]` | -| 116 | 20 | `led_g[20]` | -| 136 | 20 | `led_b[20]` | -| 156 | 20 | `led_brightness[20]` – per-LED Helligkeit (0–255) | -| 176 | 20 | `led_anim[20]` – LEDAnim-Typ als uint8_t | -| 196 | 40 | `led_period_ms[20]` – uint16_t little-endian | +| `0` | 60 | `mx_actions[20]` | +| `60` | 36 | `enc_actions[4][3]` | +| `96` | 20 | `led_r[20]` | +| `116` | 20 | `led_g[20]` | +| `136` | 20 | `led_b[20]` | +| `156` | 20 | `led_brightness[20]` | +| `176` | 20 | `led_anim[20]` | +| `196` | 40 | `led_period_ms[20]` | -Gesamt: 32 B Header + 3 × 236 B Profile = **740 B**. +Gesamtrechnung: -`__attribute__((packed))` ist zwingend. Ohne packed wäre SAction 4 B statt 3 B, was `sizeof(SDeviceConfig)` um 32 B vergrößert und die C#-Deserialisierung zerstört. - -## CRC16-CCITT - -- Polynom: `0x1021`, Init: `0xFFFF` -- Berechnet über Bytes 7–739 (ab `active_profile`, nach dem `crc`-Feld selbst) -- Sichert alle Nutzdaten einschließlich `active_profile` - -> **Wichtig bei PROFILE_SWITCH:** `active_profile` liegt im CRC-Bereich. Nach jeder Änderung muss `cfg.crc = nvm_config_crc(cfg)` aufgerufen werden bevor gespeichert wird — sonst lädt `nvm_config_load()` die Defaults. - -## Lese-Logik - -``` -memcpy aus Flash-Adresse 0x1FD00 (740 B) -if magic != 0x56503203: Defaults laden, return false -if version != 3: Defaults laden, return false -if crc != crc(cfg): Defaults laden, return false -if active_profile >= 3: active_profile = 0 -return true +```text +32 Byte Header + 3 * 236 Byte Profile = 740 Byte ``` -Kein Absturz bei ungültiger Config – Defaults greifen immer. +## CRC + +CRC16-CCITT: + +- Polynom `0x1021` +- Init `0xFFFF` +- Bereich: Bytes `7..739` + +Damit sind auch `active_profile` und globale Helligkeit abgesichert. ## Defaults -- Alle Aktionen: `NONE` -- LEDs: warm-weiß (R=80, G=40, B=0), `led_brightness=255` -- Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms -- `active_profile = 0`, `global_brightness = 255`, `enc_sensitivity = 1` +`nvm_config_defaults()` setzt: -## Schreib-Logik (nvm_config_save) +- `active_profile = 0` +- `global_brightness = 255` +- `enc_sensitivity[*] = 1` +- alle Actions auf `NONE` +- alle `led_brightness[*] = 255` +- Base-Farbe `R=80, G=40, B=0` +- `led_anim = COLOR_CYCLE` +- `led_period_ms = 4000` -`nvm_config_save()` gibt `bool` zurück. `false` bedeutet NVM-Timeout — der NVM-Controller hat nicht rechtzeitig READY gemeldet (beobachtet nach bestimmten Bootloader/Flash-Zyklen auf SAMD21). +Praktisch sichtbares Ergebnis: -SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Ablauf: +- alle MX-LEDs laufen wieder im Regenbogenmodus -1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) -2. 3 Rows löschen (`NVMCTRL_CTRLA_CMD_ER`) — bei Fehler: `return false` -3. Für jede der 12 Pages à 64 B: - - Page-Buffer löschen (`NVMCTRL_CTRLA_CMD_PBC`) - - 64 B als `uint32_t*` in Page-Buffer schreiben - - Page programmieren (`NVMCTRL_CTRLA_CMD_WP`) — bei Fehler: `return false` -4. `return true` +## Laden -### nvm_wait() – Timeout +`nvm_config_load()`: -```cpp -static bool nvm_wait() -{ - uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000 Iterationen ≈ 400 ms - while (!NVMCTRL->INTFLAG.bit.READY) { - if (--timeout == 0) return false; - } - return true; -} -``` +1. 740 Byte aus NVM kopieren +2. Magic pruefen +3. Version pruefen +4. CRC pruefen +5. bei Fehlern Defaults laden und `false` zurueckgeben -Der Timeout verhindert ein dauerhaftes Einfrieren des Boards wenn NVMCTRL aus unbekanntem Grund nicht READY meldet. Bei Timeout sendet das Board `CONFIG_NACK` statt zu hängen. +Die Firmware faellt also immer auf einen gueltigen Zustand zurueck. -> `NVMCTRL->ADDR.reg = addr / 2` – NVMCTRL erwartet Wort-Adresse (16-Bit-Worte), nicht Byte-Adresse. +## Speichern -> **Aligned-Buffer-Pflicht**: `nvm_write_page` castet `data` zu `const uint32_t*`. Der Puffer muss `__attribute__((aligned(4)))` sein. Packed Structs sind nicht garantiert aligned → immer via lokalem `uint8_t buf[] __attribute__((aligned(4)))` + `memcpy` übergeben. +`nvm_config_save()`: + +1. 740-Byte-Config in einen 768-Byte-Row-Puffer kopieren +2. Rest mit `0xFF` fuellen +3. `MANW = 1` +4. 3 Rows loeschen +5. 12 Pages zu je 64 Byte schreiben + +Rueckgabewert: + +- `true` bei Erfolg +- `false` bei NVM-Timeout + +Wichtig: + +- der Schreibpuffer muss 4-Byte-aligned sein +- `packed` allein reicht dafuer nicht + +## Zusammenhang mit Werksreset + +Der Werksreset nutzt denselben Pfad: + +- `nvm_config_defaults(cfg)` +- `nvm_config_save(cfg)` + +Dadurch werden auch kaputte, aber formal noch vorhandene Alt-Daten im NVM wirklich ueberschrieben. diff --git a/doc/07_serial_protocol.md b/doc/07_serial_protocol.md index 4ef385a..1522b43 100644 --- a/doc/07_serial_protocol.md +++ b/doc/07_serial_protocol.md @@ -1,92 +1,127 @@ # Serial-Protokoll (CDC USB) -**Dateien:** `hal/usb_serial.h`, `hal/usb_serial.cpp` +Dateien: -## Grundprinzip +- `hal/usb_serial.h` +- `hal/usb_serial.cpp` +- `CMainController.cpp` -Board erscheint unter Windows als CDC Serial-Port (kein Treiber nötig). Alle Pakete haben feste Größe von **8 Byte** – kein Längen-Header, kein Framing, kein Escape. +## Paketformat +Alle Pakete sind exakt 8 Byte lang: + +```text +Byte 0: command / event id +Byte 1: key_id oder chunk-index oder chunk-count +Byte 2: daten a +Byte 3: daten b +Byte 4: daten c +Byte 5..7: reserviert ``` -Byte 0: Command / Event-ID -Byte 1: key_id (Button 0–24 oder Encoder 0–3) / Chunk-Index / Chunk-Count -Byte 2: r / Daten-Byte A -Byte 3: g / Daten-Byte B -Byte 4: b -Byte 5–7: reserviert (0x00) -``` + +Es gibt kein Framing und keinen Laengenheader. ## Richtungen -| Richtung | ID-Bereich | Verarbeitung | +| Richtung | IDs | Verarbeitung | |---|---|---| -| PC → Board (Commands) | 0x01–0x7F | `poll_vendor()` in CMainController | -| Board → PC (Events) | 0x81–0xFF | `usb_serial_send()` in processEvents | +| PC -> Board | `0x01..0x7F` | `poll_vendor()` | +| Board -> PC | `0x81..0xFF` | `usb_serial_send()` | -## Command-Referenz (PC → Board) +## Commands -| ID | Name | Bedeutung | +| ID | Name | Zweck | |---|---|---| -| `0x01` | SET_LED_OVERRIDE | key_id, r, g, b – temporäre Override-Farbe setzen | -| `0x02` | CLEAR_LED_OVERRIDE | key_id – Override löschen, zurück zu base | -| `0x03` | SET_LED_BASE | key_id, r, g, b – base-Farbe dauerhaft ändern (kein NVM) | -| `0x05` | PING | Board antwortet sofort mit PONG (0x85) | -| `0x10` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl – neuen Config-Empfang starten | -| `0x11` | CONFIG_DATA | Byte[1] = Chunk-Index, Byte[2–7] = 6 B Nutzdaten | -| `0x12` | CONFIG_COMMIT | CRC prüfen → NVM schreiben → Buttons neu laden → ACK/NACK | -| `0x13` | CONFIG_READ | Board sendet aktuelle NVM-Config zurück (BEGIN/DATA/END) | -| `0x20` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl – neuen Makro-Empfang starten | -| `0x21` | MACRO_DATA | Byte[1] = Chunk-Index, Byte[2–7] = 6 B Nutzdaten | -| `0x22` | MACRO_COMMIT | NVM schreiben → MACRO_ACK oder MACRO_NACK | -| `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück | +| `0x01` | `SET_LED_OVERRIDE` | temporaere LED-Override setzen | +| `0x02` | `CLEAR_LED_OVERRIDE` | Override entfernen | +| `0x03` | `SET_LED_BASE` | Base-Farbe im RAM setzen | +| `0x05` | `PING` | Antwort: `PONG` | +| `0x10` | `CONFIG_BEGIN` | Config-Transfer starten | +| `0x11` | `CONFIG_DATA` | 6 Byte Config-Nutzdaten | +| `0x12` | `CONFIG_COMMIT` | Config pruefen und speichern | +| `0x13` | `CONFIG_READ` | Config-Dump an Host senden | +| `0x20` | `MACRO_BEGIN` | Makro-Transfer starten | +| `0x21` | `MACRO_DATA` | 6 Byte Makro-Nutzdaten | +| `0x22` | `MACRO_COMMIT` | Makros speichern | +| `0x23` | `MACRO_READ` | Makro-Dump an Host senden | -## Event-Referenz (Board → PC) +## Events -| ID | Name | Bedeutung | +| ID | Name | Zweck | |---|---|---| -| `0x81` | KEY_DOWN | key_id – HOST_COMMAND-Button gedrückt | -| `0x82` | KEY_UP | key_id – (derzeit nicht gesendet) | -| `0x83` | ENC_CW | enc_id – Encoder-Schritt CW (HOST_COMMAND) | -| `0x84` | ENC_CCW | enc_id – Encoder-Schritt CCW (HOST_COMMAND) | -| `0x85` | PONG | Antwort auf PING | -| `0x90` | CONFIG_ACK | Config erfolgreich in NVM geschrieben | -| `0x91` | CONFIG_NACK | Config CRC/Magic ungültig oder NVM-Timeout – nicht geschrieben | -| `0x92` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl (Config-Dump) | -| `0x93` | CONFIG_DATA | Byte[1] = Index, Byte[2–7] = 6 B (Config-Dump) | -| `0x94` | CONFIG_END | Config-Dump abgeschlossen | -| `0x95` | MACRO_ACK | Makro-Tabelle erfolgreich gespeichert | -| `0x96` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl (Makro-Dump) | -| `0x97` | MACRO_DATA | Byte[1] = Index, Byte[2–7] = 6 B (Makro-Dump) | -| `0x98` | MACRO_END | Makro-Dump abgeschlossen | -| `0x99` | MACRO_NACK | Makro-Tabelle: NVM-Timeout – nicht geschrieben | +| `0x81` | `KEY_DOWN` | Host-Command-Button gedrueckt | +| `0x82` | `KEY_UP` | Host-Command-Button losgelassen | +| `0x83` | `ENC_CW` | Encoder Host-Command im Uhrzeigersinn | +| `0x84` | `ENC_CCW` | Encoder Host-Command gegen Uhrzeigersinn | +| `0x85` | `PONG` | Antwort auf Ping | +| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert | +| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout | +| `0x92` | `CONFIG_BEGIN` | Config-Dump beginnt | +| `0x93` | `CONFIG_DATA` | 6 Byte Config-Dump | +| `0x94` | `CONFIG_END` | Config-Dump fertig | +| `0x95` | `MACRO_ACK` | Makros erfolgreich gespeichert | +| `0x96` | `MACRO_BEGIN` | Makro-Dump beginnt | +| `0x97` | `MACRO_DATA` | 6 Byte Makro-Dump | +| `0x98` | `MACRO_END` | Makro-Dump fertig | +| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen | -## Chunked Transfer +## Chunk-Zahlen -Config (740 B) und Makro-Tabelle (512 B) werden in 6-Byte-Chunks übertragen: +Aktuelle Blob-Groessen: -``` -Config: ceil(740 / 6) = 124 Chunks -Makros: ceil(512 / 6) = 86 Chunks (letzter Chunk hat 2 Nutzbytes) +- Config: `740` Byte +- Makros: `512` Byte + +Bei 6 Nutzbytes pro Paket ergibt das: + +```text +Config: ceil(740 / 6) = 124 Chunks +Makros: ceil(512 / 6) = 86 Chunks ``` -Ablauf (PC → Board): -``` -BEGIN (chunk_count) -DATA chunk_0 (Bytes 0–5) -DATA chunk_1 (Bytes 6–11) +## Transferablauf + +### PC -> Board + +```text +BEGIN(chunk_count) +DATA 0 +DATA 1 ... COMMIT ``` -**CONFIG_COMMIT**: Board prüft Magic + Version + CRC. Bei Fehler → `CONFIG_NACK`. Bei NVM-Timeout während Erase/Write → `CONFIG_NACK`. Bei Erfolg → `CONFIG_ACK`. +### Board -> PC -**MACRO_COMMIT**: Kein CRC, Board schreibt direkt. Bei Erfolg → `MACRO_ACK`. Bei NVM-Timeout → `MACRO_NACK`. +```text +BEGIN(chunk_count) +DATA 0 +DATA 1 +... +END +``` -### ACK-Synchronisation (GUI-Seite) +## Validierung -VersaGUI wartet nach COMMIT auf das ACK/NACK via `SemaphoreSlim` (Timeout 3 s). Erst nach Freigabe des Gates startet der nächste Transfer. Dies verhindert, dass Makro-Chunks gesendet werden während das Board noch den Config-NVM schreibt (~750 ms für 3 Rows). +`CONFIG_COMMIT` prueft: + +- Magic +- Version +- CRC + +Nur bei erfolgreicher Pruefung wird in NVM geschrieben. + +`MACRO_COMMIT` schreibt ohne CRC direkt nach NVM und signalisiert nur Erfolg oder Fehler. + +## Praktische Hinweise fuer die GUI + +- nach `CONFIG_COMMIT` auf `CONFIG_ACK` oder `CONFIG_NACK` warten +- danach erst `MACRO_*` senden +- Dumps besser sequenziell lesen: zuerst Config, danach Makros +- `DtrEnable` muss aktiv sein, sonst verwirft das Board CDC-Ausgaben ## Implementierungsdetails -- **Ring-Buffer**: 256 Byte Eingangspuffer (= 32 vollständige Pakete) in `usb_serial.cpp` -- **DTR-Check**: `usb_serial_send()` sendet nur wenn `SerialUSB` aktiv ist (verhindert stilles Verwerfen wenn VersaGUI nicht verbunden) -- **SAMD21 CDC**: Nach SWD-Flash braucht Windows eine physische USB-Reinitialisierung (Kabel abziehen/stecken) damit der CDC-Port neu enumeriert +- RX-Ringbuffer: 256 Byte = 32 volle Pakete +- feste 8-Byte-Pakete vereinfachen Firmware und GUI +- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein diff --git a/doc/INDEX.md b/doc/INDEX.md index 74e40be..b70b034 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -1,24 +1,22 @@ -# VersaMCU – Dokumentations-Index +# VersaMCU - Dokumentationsindex -Jede Datei deckt eine Firmware-Komponente ab. Für Claude: die relevante(n) Dateien zu Beginn einer Aufgabe lesen statt die gesamten Quelldateien zu scannen. +Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profilen und 32x8 Makros. | Datei | Inhalt | |---|---| -| [00_architecture.md](00_architecture.md) | Loop-Ablauf, Datenfluss, Key-ID-Schema, globale Invarianten (kein Heap, kein Float, packed Structs, aligned NVM) | -| [01_matrix.md](01_matrix.md) | 5×5-Scan, Debounce, Key-ID-Berechnung | -| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, LUT, ISR-Aufbau, Halbschritt-Akkumulator | -| [03_action_engine.md](03_action_engine.md) | SAction-Struct, ActionType, execute_action, Tap-Only-Modell, HOST_COMMAND-Pfad | -| [04_macro_system.md](04_macro_system.md) | SMacroTable, NVM Row 1, Slot-Konvention, aligned-Buffer-Pflicht | -| [05_led_system.md](05_led_system.md) | 2-Schicht-Modell, alle Animationen, Hue-Arithmetik (kein Float), Render-Pipeline, Bit-Bang vs. DMA | -| [06_nvm_config.md](06_nvm_config.md) | Flash-Layout, SDeviceConfig-Byte-Map, CRC16-CCITT, Schreib-Mechanik, Defaults | -| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Pakete, alle Command/Event-IDs, Chunked Transfer, Ring-Buffer | +| [00_architecture.md](00_architecture.md) | Setup, Work-Loop, Datenfluss, Key-IDs, globale Invarianten | +| [01_matrix.md](01_matrix.md) | 5x5-Matrixscan, Debounce, Key-ID-Berechnung | +| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, ISR-Pfad, Event-Erzeugung | +| [03_action_engine.md](03_action_engine.md) | `SAction`, `ActionType`, Hold/Tap-Verhalten, Profilwechsel, Reset-Sonderfall | +| [04_macro_system.md](04_macro_system.md) | `SMacroTable`, 32 Slots, 8 Steps, NVM-Layout, Ausfuehrung | +| [05_led_system.md](05_led_system.md) | LED-Schichten, Animationen, Render-Pipeline | +| [06_nvm_config.md](06_nvm_config.md) | Config v3, 3 Profile, CRC16, Defaults, Werksreset-Bezug | +| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK | -## Schnell-Referenz: Was steht wo? +## Schnellreferenz -- **Warum packed?** → [03_action_engine.md](03_action_engine.md), [06_nvm_config.md](06_nvm_config.md) -- **Warum kein Float?** → [05_led_system.md](05_led_system.md), [00_architecture.md](00_architecture.md) -- **Warum Bit-Bang statt DMA?** → [05_led_system.md](05_led_system.md) -- **Aligned-Buffer bei NVM-Write?** → [04_macro_system.md](04_macro_system.md), [06_nvm_config.md](06_nvm_config.md) -- **Warum on_press/on_release leer?** → [03_action_engine.md](03_action_engine.md) -- **Kein Hold-Support?** → [03_action_engine.md](03_action_engine.md) -- **USB nach SWD-Flash nicht erkannt?** → [07_serial_protocol.md](07_serial_protocol.md) +- aktuelle Config-Groesse: [06_nvm_config.md](06_nvm_config.md) +- aktuelle Makro-Groesse: [04_macro_system.md](04_macro_system.md) +- Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md) +- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md) +- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md) diff --git a/src/CMainController.cpp b/src/CMainController.cpp index 63896f6..f091e2b 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -33,6 +33,10 @@ #include "config/nvm_config.h" #include "config/macro_config.h" +static constexpr uint8_t FACTORY_RESET_LEFT_KEY = 9; +static constexpr uint8_t FACTORY_RESET_RIGHT_KEY = 24; +static constexpr uint32_t FACTORY_RESET_HOLD_MS = 5000; + // ─── Static Bridge: HAL-Callbacks → EventQueue ─────────────────────────────── // // matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein @@ -72,6 +76,11 @@ CMainController::CMainController() , m_cfg_receiving(false) , m_macro_chunks_expected(0) , m_macro_receiving(false) + , m_factory_left_held(false) + , m_factory_right_held(false) + , m_factory_reset_armed(false) + , m_factory_reset_done(false) + , m_factory_hold_started_ms(0) { memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); memset(m_macro_buf, 0, sizeof(m_macro_buf)); @@ -158,6 +167,7 @@ void CMainController::work() matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten processEvents(); // 3. Queue leeren, Aktionen ausführen + check_factory_reset();// 4. Long-Press-Kombination für Werksreset prüfen updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() } @@ -347,13 +357,22 @@ void CMainController::processEvents() switch (ev.type) { case EventType::KEY_DOWN: - if (ev.key_id < MATRIX_KEYS) - execute_action_down(m_buttons[ev.key_id].action(), ev.key_id); + if (ev.key_id < MATRIX_KEYS) { + update_factory_reset_hold(ev.key_id, true); + if (!(is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id))) { + execute_action_down(m_buttons[ev.key_id].action(), ev.key_id); + } + } break; case EventType::KEY_UP: - if (ev.key_id < MATRIX_KEYS) - execute_action_up(m_buttons[ev.key_id].action(), ev.key_id); + if (ev.key_id < MATRIX_KEYS) { + bool suppress = is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id); + update_factory_reset_hold(ev.key_id, false); + if (!suppress) { + execute_action_up(m_buttons[ev.key_id].action(), ev.key_id); + } + } break; case EventType::ENC_CW: @@ -378,6 +397,122 @@ void CMainController::processEvents() } } +bool CMainController::is_factory_reset_key(uint8_t key_id) const +{ + return key_id == FACTORY_RESET_LEFT_KEY || key_id == FACTORY_RESET_RIGHT_KEY; +} + +bool CMainController::is_factory_reset_combo_active() const +{ + return m_factory_left_held && m_factory_right_held; +} + +void CMainController::update_factory_reset_led_feedback() +{ + // Einzelne Reset-Taste gehalten: diese Taste rot hervorheben. + // Beide gehalten: beide Tasten rot hervorheben. + if (m_factory_left_held) { + m_buttons[FACTORY_RESET_LEFT_KEY].set_override(RGB(96, 0, 0)); + } else { + m_buttons[FACTORY_RESET_LEFT_KEY].clear_override(); + } + + if (m_factory_right_held) { + m_buttons[FACTORY_RESET_RIGHT_KEY].set_override(RGB(96, 0, 0)); + } else { + m_buttons[FACTORY_RESET_RIGHT_KEY].clear_override(); + } +} + +void CMainController::update_factory_reset_hold(uint8_t key_id, bool pressed) +{ + if (key_id == FACTORY_RESET_LEFT_KEY) { + m_factory_left_held = pressed; + } else if (key_id == FACTORY_RESET_RIGHT_KEY) { + m_factory_right_held = pressed; + } else { + return; + } + + update_factory_reset_led_feedback(); + + if (m_factory_left_held && m_factory_right_held) { + if (!m_factory_reset_armed) { + // Sobald beide Reset-Tasten gleichzeitig gehalten werden, sollen sie + // keine normale Aktion mehr auf dem Host auslösen. Falls die zuerst + // gedrückte Taste bereits ein HID-/Consumer-Hold gestartet hat, + // geben wir sie hier sofort wieder frei. + execute_action_up(m_buttons[FACTORY_RESET_LEFT_KEY].action(), FACTORY_RESET_LEFT_KEY); + execute_action_up(m_buttons[FACTORY_RESET_RIGHT_KEY].action(), FACTORY_RESET_RIGHT_KEY); + m_factory_reset_armed = true; + m_factory_reset_done = false; + m_factory_hold_started_ms = millis(); + } + } else { + m_factory_reset_armed = false; + m_factory_reset_done = false; + m_factory_hold_started_ms = 0; + } +} + +void CMainController::check_factory_reset() +{ + if (!m_factory_reset_armed || m_factory_reset_done) return; + if (!(m_factory_left_held && m_factory_right_held)) return; + + if ((millis() - m_factory_hold_started_ms) >= FACTORY_RESET_HOLD_MS) { + m_factory_reset_done = true; + perform_factory_reset(); + } +} + +void CMainController::perform_factory_reset() +{ + SDeviceConfig cfg; + SMacroTable macros; + bool cfg_ok; + bool macro_ok; + + nvm_config_defaults(cfg); + memset(¯os, 0, sizeof(macros)); + + cfg_ok = nvm_config_save(cfg); + macro_ok = macro_config_save(macros); + + // Laufzeit-Zustand immer an die Defaults angleichen – selbst wenn NVM gerade + // nicht geschrieben werden konnte, sieht das Gerät sofort wieder "frisch" aus. + m_macros = macros; + usb_hid_release_key(); + usb_hid_release_consumer(); + init_buttons(); + show_factory_reset_feedback(); + + // Während die beiden Tasten weiter gehalten werden, keine erneuten Resets. + // Neue Arming-Phase erst nach vollständigem Loslassen beider Tasten. + if (!(cfg_ok && macro_ok)) { + // Keine Host-Meldung vorgesehen – das Gerät bleibt aber betriebsfähig + // und kann über die GUI erneut konfiguriert werden. + } +} + +void CMainController::show_factory_reset_feedback() +{ + // Kurze rote Bestätigung ähnlich der Startsequenz, aber kompakter. + ws2812_fill(100, 0, 0); + ws2812_show(); + delay(180); + ws2812_clear(); + delay(90); + ws2812_fill(100, 0, 0); + ws2812_show(); + delay(180); + ws2812_clear(); + delay(60); + + // Danach sofort die frisch geladenen Default-Animationen wieder anzeigen. + updateLEDs(); +} + // ─── Aktions-Ausführung ─────────────────────────────────────────────────────── // // execute_action_down(): Taste wird gedrückt (Hold-Start). diff --git a/src/CMainController.h b/src/CMainController.h index 61b629e..6c84236 100644 --- a/src/CMainController.h +++ b/src/CMainController.h @@ -55,4 +55,21 @@ private: // Geladene Makro-Tabelle (im RAM – wird beim Start aus NVM geladen) SMacroTable m_macros; + + // Werksreset per Long-Press-Kombination: + // key_id 9 = unterster linker MX-Button (COL_1 / ROW_4) + // key_id 24 = unterster rechter MX-Button (COL_4 / ROW_4) + bool m_factory_left_held; + bool m_factory_right_held; + bool m_factory_reset_armed; + bool m_factory_reset_done; + uint32_t m_factory_hold_started_ms; + + bool is_factory_reset_key(uint8_t key_id) const; + bool is_factory_reset_combo_active() const; + void update_factory_reset_led_feedback(); + void update_factory_reset_hold(uint8_t key_id, bool pressed); + void check_factory_reset(); + void perform_factory_reset(); + void show_factory_reset_feedback(); };