Compare commits

...

6 Commits

Author SHA1 Message Date
jappel ac3b2aa90f Fixed encoder direction 2026-04-19 00:41:36 +02:00
jappel 24b349de26 Anpassung 16bit offset 2026-04-19 00:25:59 +02:00
jappel 802ab858e1 Added config, added factory reset functionality 2026-04-18 23:59:48 +02:00
jappel 433d61c29f 8 Step macro and profile switching fully working 2026-04-13 22:34:54 +02:00
jappel 098a166a9f Semi working profiles and longer macros 2026-04-13 21:42:02 +02:00
jappel 7169d3bbba Changed Anforderungskatalog 2026-03-31 22:05:59 +02:00
17 changed files with 985 additions and 868 deletions
+148 -458
View File
@@ -1,479 +1,169 @@
# VersaMCU # VersaMCU
Firmware für das VersaPad v2 Macro-Pad. Firmware fuer das VersaPad v2 Macro-Pad.
Läuft auf einem **ATSAMD21G17D** (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework. Laeuft auf einem ATSAMD21G17D mit PlatformIO und Arduino-Framework.
## Hardware ## Hardware
| Eigenschaft | Detail | | Eigenschaft | Detail |
|---|---| |---|---|
| MCU | ATSAMD21**G17D** (Cortex-M0+, 128 KB Flash, 16 KB RAM) | | MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
| Taktfrequenz | 48 MHz (DFLL, intern kein externer 32 kHz-Quarz) | | Flash / RAM | 128 KB / 16 KB |
| Framework | Arduino SAMD Core 1.8.14, PlatformIO | | USB | Composite: HID Keyboard + Consumer + CDC Serial |
| Programmer | Atmel-ICE via SWD (kein Bootloader) | | 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 ## Build und Flash
- **5×5-Matrix** (COL_04 × ROW_04) = 25 logische Keys
- **20 Cherry MX Switches** (COL_14 × ROW_04), je eine WS2812-LED
- **4 Encoder-SW-Buttons** (COL_0 × ROW_03), 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 (PA16PA23), 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
```bash ```bash
pio run
pio run --target upload 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) `main.cpp` startet genau einen `CMainController`.
Die Hauptschleife in `work()` ist:
### 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) | ✅ |
### 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 14) | ✅ |
| 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
```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.
Beim Serial-Dump der Config werden 124 Chunks zu je 6 Nutzbytes uebertragen. Implementierungen muessen den daraus berechneten Byte-Offset mindestens 16 Bit breit halten, weil Profil 2 und 3 hinter Byte 255 liegen.
## 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/ VersaMCU/
├── platformio.ini Build- und Upload-Konfiguration |-- platformio.ini
├── upload_openocd.py Benutzerdefiniertes Upload-Script (OpenOCD) |-- boards/
├── boards/ |-- variants/versapad/
│ ├── versapad.json Board-Definition mit Bootloader `-- src/
│ └── versapad_nobl.json Board-Definition ohne Bootloader (aktiv) |-- main.cpp
├── variants/versapad/ |-- CMainController.h/.cpp
│ ├── variant.h/.cpp Pin-Mapping für den SAMD21G17D |-- CButton.h/.cpp
│ └── linker_scripts/ Linkerscript (kein Bootloader, NVM-Reservierung) |-- CEventQueue.h/.cpp
└── src/ |-- SEvent.h
├── main.cpp Arduino setup()/loop() |-- config/
├── CMainController.h/.cpp Zentraler Orchestrator | |-- action.h
├── CButton.h/.cpp Button-Modell: LED-Schichten, Action, Animationen | |-- macro_config.h/.cpp
├── CEventQueue.h/.cpp Ring-Buffer FIFO (16 Slots, kein Heap) | `-- nvm_config.h/.cpp
├── SEvent.h Event-Typen (KEY_DOWN/UP, ENC_CW/CCW) `-- hal/
├── config/ |-- encoder.h/.cpp
│ ├── pins.h Pin-Nummern (Arduino-Nummern aus variant.h) |-- matrix.h/.cpp
│ ├── action.h ActionType-Enum + SAction-Struct (packed) |-- usb_hid.h/.cpp
│ └── nvm_config.h/.cpp NVM-Config: Laden, Speichern, CRC16, Defaults |-- usb_serial.h/.cpp
└── hal/ `-- ws2812.h/.cpp
├── 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
``` ```
## Architektur ## Weiterfuehrende Doku
### Schichten - [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)
│ main.cpp │ - [doc/06_nvm_config.md](doc/06_nvm_config.md)
│ CMainController (setup / work) │ - [doc/07_serial_protocol.md](doc/07_serial_protocol.md)
├────────────────────────────────────────────────┤
│ 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 (~1020× 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 024 oder Encoder 03)
Byte 2: r / Daten-Byte A
Byte 3: g / Daten-Byte B
Byte 4: b
Byte 57: 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 7222)
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 € | ~810 € |
**Warum SAME54?**
- Direkter Upgrade-Pfad: Arduino-Framework, PlatformIO, gleiche HAL-Konzepte
- Ethernet-MAC integriert → nur externer PHY nötig (z.B. **KSZ8081** oder **LAN8720A**, ~12 €)
- 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. 216 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 | 13 € |
| Ethernet PHY | LAN8720A oder KSZ8081 | 12 € |
| 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.
+81 -57
View File
@@ -1,83 +1,107 @@
# VersaMCU Architekturbersicht # VersaMCU - Architekturuebersicht
## Ziel-Hardware ## Zielplattform
| Merkmal | Wert | | Merkmal | Wert |
|---|---| |---|---|
| MCU | ATSAMD21G17D (Cortex-M0+, 48 MHz) | | MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
| Flash | 128 KB (davon 512 B am Ende für NVM-Config reserviert) | | Flash | 128 KB |
| RAM | 16 KB | | RAM | 16 KB |
| FPU | Keine alle Berechnungen in Integer-Arithmetik | | FPU | keine, deshalb Integer-Arithmetik |
| USB | Native USB, DFLL48M via USB-SOF-Kalibrierung (`-DCRYSTALLESS`) | | USB | HID Keyboard + Consumer + CDC Serial |
| Framework | Arduino + PlatformIO, kein Bootloader (Direktflash via SWD/Atmel-ICE) | | Toolchain | PlatformIO + Arduino Core |
## Loop-Ablauf ## Setup und Loop
``` ```text
setup() setup()
├── macro_config_load() Makro-Tabelle aus NVM in RAM laden macro_config_load()
├── init_buttons() CButton-Objekte aus NVM initialisieren nvm_config_load()
├── usb_hid_init() HID-Descriptor (No-Op, läuft via global ctor) init_buttons()
├── usb_serial_init() CDC Serial öffnen usb_hid_init()
├── matrix_init(cb) 5×5-Matrix + Debounce-Zustand usb_serial_init()
└── encoder_init(cb) EIC-Interrupts für 4 Encoder matrix_init(cb)
encoder_init(cb)
loop() [~20 ms Iteration] loop()
├── matrix_scan() Debounce-Zustand prüfen → Events in Queue matrix_scan()
├── poll_vendor() CDC-Pakete vom PC verarbeiten (LED-Cmds, Config, Makros) poll_vendor()
├── processEvents() Queue leeren: Aktionen ausführen, HOST_COMMAND melden processEvents()
└── updateLEDs() Dirty-CButtons → WS2812-Buffer → show() (nur wenn dirty) 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 ## Datenfluss
``` ```text
HAL-Callbacks (matrix_cb, encoder_cb) matrix_scan / encoder ISR
└─► CEventQueue (16 Slots, Ring-Buffer, kein Heap) -> EventQueue
└─► processEvents() -> processEvents()
├─► CButton.on_press() / on_release() [Hooks, aktuell leer] -> execute_action_down / execute_action_up
├─► execute_action() → USB HID / Makro-Ablauf -> usb_hid_*
└─► usb_serial_send() HOST_COMMAND-Events an PC -> usb_serial_send() fuer HOST_COMMAND
SerialUSB (CDC, PC → Board) CDC Serial
└─► poll_vendor() -> poll_vendor()
├─► CButton.set_override() / clear_override() / set_base() -> Config/Makros einlesen oder dumpen
└─► Config/Makro-Transfer (chunked, 6 B/Paket) -> 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 | | `main.cpp` | startet den Controller |
| `CMainController` | Zentraler Orchestrator, hält alle CButton-Instanzen | | `CMainController.*` | Orchestrator fuer Inputs, Actions, Serial, LEDs |
| `CButton` | LED-Layering, Animations-Engine, Action-Referenz | | `CButton.*` | LED-Zustand, Animationen, Action-Referenz |
| `CEventQueue` | ISR-sicherer Ring-Buffer, 16 Events | | `CEventQueue.*` | ISR-sicherer Ringbuffer |
| `hal/matrix` | 5×5-Matrix-Scan, 10 ms Debounce | | `config/nvm_config.*` | Config v3 laden, speichern, Defaults |
| `hal/encoder` | Quadratur-Dekodierung via EIC-ISR | | `config/macro_config.*` | Makros laden, speichern |
| `hal/ws2812` | Thin Wrapper um Adafruit NeoPixel (bit-bang) | | `hal/matrix.*` | 5x5-Matrixscan mit Debounce |
| `hal/usb_hid` | HID Keyboard + Consumer Control | | `hal/encoder.*` | Encoder-ISR und Drehrichtung |
| `hal/usb_serial` | CDC bidirektional, 8-Byte-Pakete, Ring-Buffer | | `hal/usb_hid.*` | Keyboard- und Consumer-HID |
| `config/nvm_config` | SDeviceConfig: laden, speichern, CRC16, Defaults | | `hal/usb_serial.*` | CDC-Paketpfad |
| `config/macro_config` | SMacroTable: laden, speichern (NVM Row 1) | | `hal/ws2812.*` | WS2812-Treiber |
| `config/action` | SAction-Struct + ActionType-Enum |
## Key-ID-Schema ## Key-ID-Schema
``` ```text
key_id 03 : Encoder-SW-Buttons (COL_0 × ROW_03), kein LED 0..3 = Encoder-SW
key_id 4 : nicht belegt (COL_0 × ROW_4) 4 = unbenutzt
key_id 524 : MX-Buttons (COL_14 × ROW_04), je ein WS2812-LED 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. ## Werksreset im Ablauf
- **Kein Float**: Cortex-M0+ hat keine FPU; Float würde per Software emuliert (~1020× langsamer).
- **Packed Structs**: `SAction` und `SDeviceConfig` sind `__attribute__((packed))` damit die Byte-Größen mit der C#-Serialisierung in VersaGUI übereinstimmen. Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware:
- **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. - 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
+6 -1
View File
@@ -13,12 +13,17 @@ Encoder-SW-Tasten laufen **nicht** durch diesen HAL, sondern durch den Matrix-Sc
``` ```
Zustand = (A << 1) | B → 4 Bits: 00 / 01 / 10 / 11 Zustand = (A << 1) | B → 4 Bits: 00 / 01 / 10 / 11
LUT[prev<<2 | cur] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen) LUT[prev<<2 | cur] → Roh-Vorzeichen, 0 (ungueltig/Prellen)
``` ```
Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt Halbschritte. Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt Halbschritte.
Ein Event wird erst gefeuert wenn `|accum| >= 4` (= ein vollständiger Klick). Ein Event wird erst gefeuert wenn `|accum| >= 4` (= ein vollständiger Klick).
Die VersaPad-PCB-Verdrahtung liefert das Quadratur-Vorzeichen gegenueber der sichtbaren Drehrichtung invertiert. Deshalb bleibt die LUT konventionell, aber der HAL dreht das Vorzeichen vor dem Callback mit `ENCODER_DIRECTION_SIGN = -1`. Nach aussen gilt weiterhin:
- `direction = +1` -> `ENC_CW`
- `direction = -1` -> `ENC_CCW`
## ISR-Aufbau ## ISR-Aufbau
8 ISR-Wrapper (je einer pro Pin, da `attachInterrupt` keinen Parameter unterstützt): 8 ISR-Wrapper (je einer pro Pin, da `attachInterrupt` keinen Parameter unterstützt):
+70 -49
View File
@@ -1,82 +1,103 @@
# Aktions-Engine # 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 ```cpp
struct __attribute__((packed)) SAction { struct __attribute__((packed)) SAction {
ActionType type; // 1 Byte ActionType type;
uint16_t data; // 2 Bytes (Keycode, Consumer-Code, Command-ID oder Slot-Index) uint16_t data;
}; };
// Gesamt: 3 Bytes (packed! ohne packed wären es 4 durch Alignment)
``` ```
`packed` ist zwingend damit `sizeof(SDeviceConfig) == 223` 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 | Bedeutung | data-Inhalt | | Typ | Bedeutung | `data` |
|---|---|---| |---|---|---|
| `NONE` | Keine Aktion | | | `NONE` | keine Aktion | - |
| `HID_KEY` | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier | | `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier |
| `HID_CONSUMER` | Consumer Control (Volume, Media, …) | Consumer Usage ID | | `HID_CONSUMER` | Media/Consumer-HID | usage id |
| `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | | `HOST_COMMAND` | Event an die GUI | command id |
| `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 | | `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_KEY` | `usb_hid_send_key()` |
| `HID_CONSUMER` | `usb_hid_send_consumer(usage_id)` — bleibt aktiv bis `execute_action_up()` | | `HID_CONSUMER` | `usb_hid_send_consumer()` |
| `HOST_COMMAND` | `usb_serial_send(USB_EVT_KEY_DOWN, key_id)` | | `HOST_COMMAND` | `usb_serial_send(KEY_DOWN/ENC_*)` |
| `MACRO` | Volle Sequenz ausführen (Steps[slot], keycode==0 = Ende, delay 10+20 ms) | | `MACRO` | komplette Sequenz sofort abspielen |
| `NONE` | nop | | `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_KEY` | `usb_hid_release_key()` |
| `HID_CONSUMER` | `usb_hid_release_consumer()` | | `HID_CONSUMER` | `usb_hid_release_consumer()` |
| `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) | | `HOST_COMMAND` | optionaler Up-Pfad, derzeit praktisch ohne Nutzlast |
| `MACRO`/`NONE` | nop | | `MACRO` | nichts |
| `PROFILE_SWITCH` | nichts |
| `NONE` | nichts |
## Hold-Modell (HID-Keys und Consumer Controls) ## Hold- und Tap-Modell
Normale Tasten- und Media-Aktionen folgen dem **Hold-Modell**: - MX-Buttons und Encoder-SW benutzen fuer HID und Consumer das Hold-Modell.
- Encoder `CW` und `CCW` sind immer diskrete Tap-Events:
``` ```text
KEY_DOWN-Event vom Board → execute_action_down() → HID Key-Down senden down -> delay(10 ms) -> up
[Taste bleibt physisch gedrückt...]
KEY_UP-Event vom Board → execute_action_up() → HID Key-Up senden
``` ```
Das OS erkennt die gedrückte Taste und startet sein eigenes Key-Repeat nach ~500 ms — wie auf einer normalen Tastatur. - Makros laufen komplett synchron in der Firmware.
## Tap-Modell (Encoder CW/CCW) ## Makro-Ausfuehrung
Encoder-Bewegungen sind diskret (kein Halten möglich) und verwenden das **Tap-Modell**: Bei `ActionType::MACRO` wird `action.data` als Slot interpretiert.
Die Firmware laeuft dann durch bis zu 8 Steps:
``` ```text
ENC_CW/ENC_CCW-Event → execute_action_down() + delay(10) + execute_action_up() step.keycode == 0 -> Ende
Key-Down
10 ms warten
Key-Up
20 ms warten
``` ```
(Atomare Sequenz für jeden Encoder-Schritt.) ## Profilwechsel
## Work-Loop-Reihenfolge `PROFILE_SWITCH` arbeitet direkt auf der gespeicherten Config:
```cpp 1. Config aus NVM laden
void work() { 2. `active_profile` aendern
matrix_scan(); // → Events in Queue (KEY_DOWN, KEY_UP, ENC_CW, ENC_CCW) 3. CRC neu berechnen
poll_vendor(); // Serial-Pakete verarbeiten (PC↔Board Kommandos) 4. wieder speichern
processEvents(); // → execute_action_down/up() aufrufen 5. `init_buttons()`
updateLEDs(); // Dirty-LEDs aktualisieren
}
```
**processEvents() verarbeitet:** Wichtig:
- `KEY_DOWN``execute_action_down()` `active_profile` liegt im CRC-geschuetzten Bereich. Ohne neue CRC wuerde die Config beim naechsten Laden verworfen.
- `KEY_UP``execute_action_up()`
- `ENC_CW` / `ENC_CCW``execute_action_down()` + `delay(10)` + `execute_action_up()` ## Sonderfall Werksreset
Die Reset-Kombination uebersteuert das normale Action-System fuer genau zwei Tasten:
- `key_id 9`
- `key_id 24`
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
+62 -34
View File
@@ -1,59 +1,87 @@
# Makro-System # 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 ## Datenstruktur
```cpp ```cpp
struct __attribute__((packed)) SMacroStep { struct __attribute__((packed)) SMacroStep {
uint8_t keycode; // HID Keyboard Usage (0x00 = leer → Step überspringen) uint8_t keycode;
uint8_t modifier; // HID Modifier: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08 uint8_t modifier;
}; };
#define MACRO_SLOTS 32
#define MACRO_MAX_STEPS 8
struct __attribute__((packed)) SMacroTable { struct __attribute__((packed)) SMacroTable {
SMacroStep steps[32][4]; // 32 Slots × 4 Steps × 2 Byte = 256 Byte SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
}; };
``` ```
Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine NVM-Row`. Gesamtgroesse:
## NVM-Speicherort - `32 * 8 * 2 = 512` Byte
- verteilt auf zwei NVM-Rows
- **Row 1**: Adresse `0x1FF00`, 256 Byte ## Speicherort
- Vom Linkerscript reserviert (nicht überschreibbar durch Code)
- Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt false zurück → leere Tabelle (alle Keycodes 0)
## Slot-Zuweisung (Konvention, Board speichert blind) | Row | Adresse | Inhalt |
|---|---|---|
| Macro Row 0 | `0x1FB00` | Bytes `0..255` |
| Macro Row 1 | `0x1FC00` | Bytes `256..511` |
| Slots | Verwendung | ## Slot-Konvention
Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung:
| Slots | Bedeutung |
|---|---| |---|---|
| 019 | MX-Button `mx_idx` (entspricht key_id 5) | | `0..19` | MX-Buttons |
| 2031 | Encoder-Aktionen (`enc * 3 + act_idx`, 0=SW / 1=CW / 2=CCW) | | `20..31` | Encoder-Aktionen (`enc * 3 + act_idx`) |
## Laden und Speichern ## Laden
**Laden** (`macro_config_load`): `macro_config_load()`:
- `memcpy` direkt aus Flash-Adresse in RAM-Struct
- Kein Magic/CRC (leere Tabelle bei 0xFF ist akzeptabler Zustand)
**Speichern** (`macro_config_save`): - kopiert 512 Byte aus NVM in `SMacroTable`
- SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!) - erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) - setzt dann eine leere Tabelle
- Row 1 löschen (`nvm_erase_row`)
- 4 Pages à 64 Byte schreiben (`nvm_write_page`)
> **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, ActionType::MACRO) ## Speichern
``` `macro_config_save()`:
slot = action.data (031)
für Step 03:
if step.keycode == 0: abbrechen
HID Key-Down (keycode, modifier)
delay(10 ms)
HID Key-Up
delay(20 ms)
```
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.
+104 -44
View File
@@ -1,68 +1,128 @@
# NVM-Konfiguration # NVM-Konfiguration
**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` Dateien:
- `config/nvm_config.h`
- `config/nvm_config.cpp`
## Flash-Layout ## Flash-Layout
| Row | Adresse | Größe | Inhalt | | Bereich | Adresse | Groesse | Inhalt |
|---|---|---|---| |---|---|---|---|
| Row 0 | `0x1FE00` | 256 B | SDeviceConfig (223 B genutzt, 33 B Padding) | | Macro Row 0 | `0x1FB00` | 256 B | `SMacroTable` Bytes `0..255` |
| Row 1 | `0x1FF00` | 256 B | SMacroTable (256 B, komplett genutzt) | | 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 |
Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. Makros und Config sind komplett getrennt.
## SDeviceConfig Byte-Layout (223 Byte, packed) ## `SDeviceConfig`
| Offset | Größe | Feld | Aktueller Stand:
- Magic: `0x56503203`
- Version: `3`
- Groesse: `740` Byte
- auf 3 Config-Rows verteilt
### Header
| Offset | Groesse | Feld |
|---|---|---| |---|---|---|
| 0 | 4 | `magic` = `0x56503202` ('VP2\x02') | | `0` | 4 | `magic` |
| 4 | 1 | `version` = 2 | | `4` | 1 | `version` |
| 5 | 2 | `crc` CRC16-CCITT über Bytes 7222 | | `5` | 2 | `crc` |
| 7 | 60 | `mx_actions[20]` 20 × 3 B SAction | | `7` | 1 | `active_profile` |
| 67 | 36 | `enc_actions[4][3]` 12 × 3 B SAction | | `8` | 1 | `global_brightness` |
| 103 | 20 | `led_r[20]` | | `9` | 4 | `enc_sensitivity[4]` |
| 123 | 20 | `led_g[20]` | | `13` | 19 | Reserve |
| 143 | 20 | `led_b[20]` |
| 163 | 20 | `led_anim[20]` LEDAnim-Typ als uint8_t |
| 183 | 40 | `led_period_ms[20]` uint16_t, little-endian |
| **223** | — | Ende des genutzten Bereichs |
`__attribute__((packed))` ist zwingend. Ohne packed wäre SAction 4 B statt 3 B (Alignment-Padding), was `sizeof(SDeviceConfig)` um 32 B vergrößert und die C#-Deserialisierung in VersaGUI zerstört. ### Pro Profil
## CRC16-CCITT Jedes Profil belegt 236 Byte:
- Polynom: `0x1021`, Init: `0xFFFF` | Offset im Profil | Groesse | Feld |
- Berechnet über Bytes 7248 (ab `mx_actions`, nach dem `crc`-Feld selbst) |---|---|---|
- Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel | `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]` |
## Lese-Logik Gesamtrechnung:
``` ```text
memcpy aus Flash-Adresse 0x1FE00 32 Byte Header + 3 * 236 Byte Profile = 740 Byte
if magic != 0x56503202: Defaults laden, return false
if version != 2: Defaults laden, return false
if crc != crc(cfg): Defaults laden, return false
return true
``` ```
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 ## Defaults
- Alle Aktionen: `NONE` `nvm_config_defaults()` setzt:
- LEDs: warm-weiß (R=80, G=40, B=0)
- Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms
## Schreib-Logik (NVM-Mechanik) - `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`
SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Schreiben erfordert: Praktisch sichtbares Ergebnis:
1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus, kein Auto-Write)
2. Row löschen (`NVMCTRL_CTRLA_CMD_ER`)
3. Page-Buffer löschen (`NVMCTRL_CTRLA_CMD_PBC`)
4. 64 B als `uint32_t*` in Page-Buffer schreiben
5. Page programmieren (`NVMCTRL_CTRLA_CMD_WP`)
6. Schritte 35 viermal (für alle 4 Pages)
> `NVMCTRL->ADDR.reg = addr / 2` NVMCTRL erwartet Wort-Adresse (16-Bit-Worte), nicht Byte-Adresse. - alle MX-LEDs laufen wieder im Regenbogenmodus
> **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 lokalen `uint8_t buf[256] __attribute__((aligned(4)))` + `memcpy` übergeben. ## Laden
`nvm_config_load()`:
1. 740 Byte aus NVM kopieren
2. Magic pruefen
3. Version pruefen
4. CRC pruefen
5. bei Fehlern Defaults laden und `false` zurueckgeben
Die Firmware faellt also immer auf einen gueltigen Zustand zurueck.
## Speichern
`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.
+101 -58
View File
@@ -1,86 +1,129 @@
# Serial-Protokoll (CDC USB) # 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 024 oder Encoder 03) / Chunk-Index / Chunk-Count Es gibt kein Framing und keinen Laengenheader.
Byte 2: r / Daten-Byte A
Byte 3: g / Daten-Byte B
Byte 4: b
Byte 57: reserviert (0x00)
```
## Richtungen ## Richtungen
| Richtung | ID-Bereich | Verarbeitung | | Richtung | IDs | Verarbeitung |
|---|---|---| |---|---|---|
| PC Board (Commands) | 0x010x7F | `poll_vendor()` in CMainController | | PC -> Board | `0x01..0x7F` | `poll_vendor()` |
| Board PC (Events) | 0x810xFF | `usb_serial_send()` in processEvents | | 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 | | `0x01` | `SET_LED_OVERRIDE` | temporaere LED-Override setzen |
| `0x02` | CLEAR_LED_OVERRIDE | key_id Override löschen, zurück zu base | | `0x02` | `CLEAR_LED_OVERRIDE` | Override entfernen |
| `0x03` | SET_LED_BASE | key_id, r, g, b base-Farbe dauerhaft ändern (kein NVM) | | `0x03` | `SET_LED_BASE` | Base-Farbe im RAM setzen |
| `0x05` | PING | Board antwortet sofort mit PONG (0x85) | | `0x05` | `PING` | Antwort: `PONG` |
| `0x10` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl neuen Config-Empfang starten | | `0x10` | `CONFIG_BEGIN` | Config-Transfer starten |
| `0x11` | CONFIG_DATA | Byte[1] = Chunk-Index, Byte[27] = 6 B Nutzdaten | | `0x11` | `CONFIG_DATA` | 6 Byte Config-Nutzdaten |
| `0x12` | CONFIG_COMMIT | CRC prüfen → NVM schreiben → Buttons neu laden → ACK/NACK | | `0x12` | `CONFIG_COMMIT` | Config pruefen und speichern |
| `0x13` | CONFIG_READ | Board sendet aktuelle NVM-Config zurück (BEGIN/DATA/END) | | `0x13` | `CONFIG_READ` | Config-Dump an Host senden |
| `0x20` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl neuen Makro-Empfang starten | | `0x20` | `MACRO_BEGIN` | Makro-Transfer starten |
| `0x21` | MACRO_DATA | Byte[1] = Chunk-Index, Byte[27] = 6 B Nutzdaten | | `0x21` | `MACRO_DATA` | 6 Byte Makro-Nutzdaten |
| `0x22` | MACRO_COMMIT | NVM schreiben + MACRO_ACK zurück | | `0x22` | `MACRO_COMMIT` | Makros speichern |
| `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück | | `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 | | `0x81` | `KEY_DOWN` | Host-Command-Button gedrueckt |
| `0x82` | KEY_UP | key_id (derzeit nicht gesendet) | | `0x82` | `KEY_UP` | Host-Command-Button losgelassen |
| `0x83` | ENC_CW | enc_id Encoder-Schritt CW (HOST_COMMAND) | | `0x83` | `ENC_CW` | Encoder Host-Command im Uhrzeigersinn |
| `0x84` | ENC_CCW | enc_id Encoder-Schritt CCW (HOST_COMMAND) | | `0x84` | `ENC_CCW` | Encoder Host-Command gegen Uhrzeigersinn |
| `0x85` | PONG | Antwort auf PING | | `0x85` | `PONG` | Antwort auf Ping |
| `0x90` | CONFIG_ACK | Config erfolgreich in NVM geschrieben | | `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
| `0x91` | CONFIG_NACK | Config CRC/Magic ungültig nicht geschrieben | | `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
| `0x92` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl (Config-Dump) | | `0x92` | `CONFIG_BEGIN` | Config-Dump beginnt |
| `0x93` | CONFIG_DATA | Byte[1] = Index, Byte[27] = 6 B (Config-Dump) | | `0x93` | `CONFIG_DATA` | 6 Byte Config-Dump |
| `0x94` | CONFIG_END | Config-Dump abgeschlossen | | `0x94` | `CONFIG_END` | Config-Dump fertig |
| `0x95` | MACRO_ACK | Makro-Tabelle erfolgreich gespeichert | | `0x95` | `MACRO_ACK` | Makros erfolgreich gespeichert |
| `0x96` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl (Makro-Dump) | | `0x96` | `MACRO_BEGIN` | Makro-Dump beginnt |
| `0x97` | MACRO_DATA | Byte[1] = Index, Byte[27] = 6 B (Makro-Dump) | | `0x97` | `MACRO_DATA` | 6 Byte Makro-Dump |
| `0x98` | MACRO_END | Makro-Dump abgeschlossen | | `0x98` | `MACRO_END` | Makro-Dump fertig |
| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen |
## Chunked Transfer ## Chunk-Zahlen
Config (223 B) und Makro-Tabelle (256 B) werden in 6-Byte-Chunks übertragen: Aktuelle Blob-Groessen:
``` - Config: `740` Byte
Config: ceil(223 / 6) = 38 Chunks - Makros: `512` Byte
Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes)
Bei 6 Nutzbytes pro Paket ergibt das:
```text
Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(512 / 6) = 86 Chunks
``` ```
Ablauf (PC → Board): Wichtig fuer Implementierungen: Der Byte-Offset eines Chunks muss mindestens 16 Bit breit sein. Bei der Config liegt der Offset ab Chunk 43 ueber 255 Byte; ein 8-Bit-Offset wuerde ueberlaufen und spaetere Profilbereiche falsch dumpen.
```
BEGIN (chunk_count) ## Transferablauf
DATA chunk_0 (Bytes 05)
DATA chunk_1 (Bytes 611) ### PC -> Board
```text
BEGIN(chunk_count)
DATA 0
DATA 1
... ...
COMMIT COMMIT
``` ```
COMMIT bei Config: Board prüft Magic + Version + CRC. Bei Fehler → NACK, kein NVM-Schreiben. ### Board -> PC
COMMIT bei Makro: Kein CRC, Board schreibt blind → MACRO_ACK.
```text
BEGIN(chunk_count)
DATA 0
DATA 1
...
END
```
## Validierung
`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 ## Implementierungsdetails
- **Ring-Buffer**: 256 Byte Eingangspuffer (= 32 vollständige Pakete) in `usb_serial.cpp` - RX-Ringbuffer: 256 Byte = 32 volle Pakete
- **DTR-Check**: `usb_serial_send()` sendet nur wenn `SerialUSB` aktiv ist (verhindert stilles Verwerfen wenn VersaGUI nicht verbunden) - feste 8-Byte-Pakete vereinfachen Firmware und GUI
- **SAMD21 CDC**: Nach SWD-Flash braucht Windows eine physische USB-Reinitialisierung (Kabel abziehen/stecken) damit der CDC-Port neu enumeriert - nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein
+16 -18
View File
@@ -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 | | Datei | Inhalt |
|---|---| |---|---|
| [00_architecture.md](00_architecture.md) | Loop-Ablauf, Datenfluss, Key-ID-Schema, globale Invarianten (kein Heap, kein Float, packed Structs, aligned NVM) | | [00_architecture.md](00_architecture.md) | Setup, Work-Loop, Datenfluss, Key-IDs, globale Invarianten |
| [01_matrix.md](01_matrix.md) | 5×5-Scan, Debounce, Key-ID-Berechnung | | [01_matrix.md](01_matrix.md) | 5x5-Matrixscan, Debounce, Key-ID-Berechnung |
| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, LUT, ISR-Aufbau, Halbschritt-Akkumulator | | [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, ISR-Pfad, Event-Erzeugung |
| [03_action_engine.md](03_action_engine.md) | SAction-Struct, ActionType, execute_action, Tap-Only-Modell, HOST_COMMAND-Pfad | | [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, NVM Row 1, Slot-Konvention, aligned-Buffer-Pflicht | | [04_macro_system.md](04_macro_system.md) | `SMacroTable`, 32 Slots, 8 Steps, NVM-Layout, Ausfuehrung |
| [05_led_system.md](05_led_system.md) | 2-Schicht-Modell, alle Animationen, Hue-Arithmetik (kein Float), Render-Pipeline, Bit-Bang vs. DMA | | [05_led_system.md](05_led_system.md) | LED-Schichten, Animationen, Render-Pipeline |
| [06_nvm_config.md](06_nvm_config.md) | Flash-Layout, SDeviceConfig-Byte-Map, CRC16-CCITT, Schreib-Mechanik, Defaults | | [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-Pakete, alle Command/Event-IDs, Chunked Transfer, Ring-Buffer | | [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) - aktuelle Config-Groesse: [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) - aktuelle Makro-Groesse: [04_macro_system.md](04_macro_system.md)
- **Warum Bit-Bang statt DMA?** → [05_led_system.md](05_led_system.md) - Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md)
- **Aligned-Buffer bei NVM-Write?** → [04_macro_system.md](04_macro_system.md), [06_nvm_config.md](06_nvm_config.md) - Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
- **Warum on_press/on_release leer?** → [03_action_engine.md](03_action_engine.md) - CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.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)
+200 -30
View File
@@ -33,6 +33,10 @@
#include "config/nvm_config.h" #include "config/nvm_config.h"
#include "config/macro_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 ─────────────────────────────── // ─── Static Bridge: HAL-Callbacks → EventQueue ───────────────────────────────
// //
// matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein // matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein
@@ -72,6 +76,11 @@ CMainController::CMainController()
, m_cfg_receiving(false) , m_cfg_receiving(false)
, m_macro_chunks_expected(0) , m_macro_chunks_expected(0)
, m_macro_receiving(false) , 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_cfg_buf, 0, sizeof(m_cfg_buf));
memset(m_macro_buf, 0, sizeof(m_macro_buf)); memset(m_macro_buf, 0, sizeof(m_macro_buf));
@@ -101,13 +110,16 @@ void CMainController::init_buttons()
bool valid = nvm_config_load(cfg); bool valid = nvm_config_load(cfg);
(void)valid; // false = keine gültige Config → Defaults wurden bereits geladen (void)valid; // false = keine gültige Config → Defaults wurden bereits geladen
// Aktives Profil auswählen (load() sichert bereits 02 ab)
const SDeviceProfile& prof = cfg.profiles[cfg.active_profile];
// Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1)
for (uint8_t enc = 0; enc < 4; enc++) { for (uint8_t enc = 0; enc < 4; enc++) {
m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW], RGB()); m_buttons[enc].init(enc, -1, prof.enc_actions[enc][ENC_ACTION_SW], RGB());
} }
// MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen,
// Aktion + Base-Farbe + Animation aus NVM. // Aktion + Base-Farbe + Animation aus aktivem Profil.
// mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4) // mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4)
for (uint8_t key = 5; key < MATRIX_KEYS; key++) { for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
@@ -115,11 +127,21 @@ void CMainController::init_buttons()
uint8_t row = key % MATRIX_ROWS; uint8_t row = key % MATRIX_ROWS;
int8_t led = static_cast<int8_t>(LED_INDEX(col, row)); int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
uint8_t mx_idx = key - 5; uint8_t mx_idx = key - 5;
RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]);
m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base);
LEDAnim anim = static_cast<LEDAnim>(cfg.led_anim[mx_idx]); // Effektive Farbe = base × led_brightness × global_brightness / 255²
uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.led_period_ms[mx_idx] : 4000; auto scale = [&](uint8_t val) -> uint8_t {
return (uint8_t)((uint32_t)val
* prof.led_brightness[mx_idx] / 255
* cfg.global_brightness / 255);
};
RGB base(scale(prof.led_r[mx_idx]),
scale(prof.led_g[mx_idx]),
scale(prof.led_b[mx_idx]));
m_buttons[key].init(key, led, prof.mx_actions[mx_idx], base);
LEDAnim anim = static_cast<LEDAnim>(prof.led_anim[mx_idx]);
uint16_t period = prof.led_period_ms[mx_idx] > 0 ? prof.led_period_ms[mx_idx] : 4000;
if (anim == LEDAnim::COLOR_CYCLE) { if (anim == LEDAnim::COLOR_CYCLE) {
// Phase gleichmäßig verteilen → stehender Regenbogen dreht sich // Phase gleichmäßig verteilen → stehender Regenbogen dreht sich
@@ -133,8 +155,8 @@ void CMainController::init_buttons()
// Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt // Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt
// da sie keine LED haben und kein Matrix-Key sind. // da sie keine LED haben und kein Matrix-Key sind.
for (uint8_t enc = 0; enc < 4; enc++) { for (uint8_t enc = 0; enc < 4; enc++) {
m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW]; m_enc_cw [enc] = prof.enc_actions[enc][ENC_ACTION_CW];
m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW]; m_enc_ccw[enc] = prof.enc_actions[enc][ENC_ACTION_CCW];
} }
} }
@@ -145,6 +167,7 @@ void CMainController::work()
matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
processEvents(); // 3. Queue leeren, Aktionen ausführen 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() updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
} }
@@ -198,8 +221,8 @@ void CMainController::poll_vendor()
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen // 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
uint16_t offset = (uint16_t)pkt.key_id() * 6; uint16_t offset = (uint16_t)pkt.key_id() * 6;
if (offset < sizeof(m_cfg_buf)) { if (offset < sizeof(m_cfg_buf)) {
uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset); uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset);
if (count > 6) count = 6; uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining);
memcpy(m_cfg_buf + offset, &pkt.data[2], count); memcpy(m_cfg_buf + offset, &pkt.data[2], count);
} }
} }
@@ -211,10 +234,10 @@ void CMainController::poll_vendor()
{ {
SDeviceConfig cfg; SDeviceConfig cfg;
nvm_config_load(cfg); // ungültige NVM → Defaults nvm_config_load(cfg); // ungültige NVM → Defaults
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
const uint8_t sz = sizeof(SDeviceConfig); // 223 const uint16_t sz = sizeof(SDeviceConfig); // 740
const uint8_t payload = 6; const uint8_t payload = 6;
uint8_t chunks = (sz + payload - 1) / payload; // 38 uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
@@ -222,7 +245,7 @@ void CMainController::poll_vendor()
uint8_t p[SERIAL_PKT_SIZE] = {}; uint8_t p[SERIAL_PKT_SIZE] = {};
p[0] = USB_EVT_CONFIG_DATA; p[0] = USB_EVT_CONFIG_DATA;
p[1] = i; p[1] = i;
uint8_t offset = i * payload; uint16_t offset = (uint16_t)i * payload;
for (uint8_t b = 0; b < payload; b++) { for (uint8_t b = 0; b < payload; b++) {
if (offset + b < sz) p[2 + b] = raw[offset + b]; if (offset + b < sz) p[2 + b] = raw[offset + b];
} }
@@ -242,13 +265,16 @@ void CMainController::poll_vendor()
cfg.version == NVM_CONFIG_VERSION && cfg.version == NVM_CONFIG_VERSION &&
cfg.crc == nvm_config_crc(cfg)) cfg.crc == nvm_config_crc(cfg))
{ {
nvm_config_save(cfg); if (nvm_config_save(cfg)) {
init_buttons(); init_buttons();
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
} else {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout
}
} }
else else
{ {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
} }
} }
break; break;
@@ -262,10 +288,10 @@ void CMainController::poll_vendor()
case USB_CMD_MACRO_DATA: case USB_CMD_MACRO_DATA:
if (m_macro_receiving) { if (m_macro_receiving) {
uint16_t offset = (uint16_t)pkt.key_id() * 6; uint16_t offset = (uint16_t)pkt.key_id() * 6;
if (offset < sizeof(m_macro_buf)) { if (offset < sizeof(m_macro_buf)) {
uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset); uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset);
if (count > 6) count = 6; uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining);
memcpy(m_macro_buf + offset, &pkt.data[2], count); memcpy(m_macro_buf + offset, &pkt.data[2], count);
} }
} }
@@ -275,18 +301,21 @@ void CMainController::poll_vendor()
if (m_macro_receiving) { if (m_macro_receiving) {
m_macro_receiving = false; m_macro_receiving = false;
memcpy(&m_macros, m_macro_buf, sizeof(m_macros)); memcpy(&m_macros, m_macro_buf, sizeof(m_macros));
macro_config_save(m_macros); if (macro_config_save(m_macros)) {
usb_serial_send(USB_EVT_MACRO_ACK, 0); usb_serial_send(USB_EVT_MACRO_ACK, 0);
} else {
usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout
}
} }
break; break;
// ── Makro-Dump anfordern ───────────────────────────────────────── // ── Makro-Dump anfordern ─────────────────────────────────────────
case USB_CMD_MACRO_READ: case USB_CMD_MACRO_READ:
{ {
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
const uint16_t sz = sizeof(SMacroTable); // 256 const uint16_t sz = sizeof(SMacroTable); // 512
const uint8_t payload = 6; const uint8_t payload = 6;
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 43 uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 86
usb_serial_send(USB_EVT_MACRO_BEGIN, chunks); usb_serial_send(USB_EVT_MACRO_BEGIN, chunks);
@@ -328,13 +357,22 @@ void CMainController::processEvents()
switch (ev.type) { switch (ev.type) {
case EventType::KEY_DOWN: case EventType::KEY_DOWN:
if (ev.key_id < MATRIX_KEYS) if (ev.key_id < MATRIX_KEYS) {
execute_action_down(m_buttons[ev.key_id].action(), ev.key_id); 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; break;
case EventType::KEY_UP: case EventType::KEY_UP:
if (ev.key_id < MATRIX_KEYS) if (ev.key_id < MATRIX_KEYS) {
execute_action_up(m_buttons[ev.key_id].action(), ev.key_id); 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; break;
case EventType::ENC_CW: case EventType::ENC_CW:
@@ -359,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(&macros, 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 ─────────────────────────────────────────────────────── // ─── Aktions-Ausführung ───────────────────────────────────────────────────────
// //
// execute_action_down(): Taste wird gedrückt (Hold-Start). // execute_action_down(): Taste wird gedrückt (Hold-Start).
@@ -417,6 +571,22 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id)
break; break;
} }
case ActionType::PROFILE_SWITCH:
{
SDeviceConfig cfg;
nvm_config_load(cfg);
uint8_t target = static_cast<uint8_t>(action.data);
if (target == 0xFF)
target = (cfg.active_profile + 1) % 3; // Zyklus: 0→1→2→0
if (target > 2) break;
cfg.active_profile = target;
cfg.crc = nvm_config_crc(cfg); // CRC nach Änderung aktualisieren
if (nvm_config_save(cfg))
init_buttons();
// Bei NVM-Timeout: kein Profil-Wechsel (Config unverändert in NVM)
break;
}
case ActionType::NONE: case ActionType::NONE:
default: default:
break; break;
+20 -2
View File
@@ -12,6 +12,7 @@
#include "hal/usb_hid.h" #include "hal/usb_hid.h"
#include "hal/usb_serial.h" #include "hal/usb_serial.h"
#include "config/action.h" #include "config/action.h"
#include "config/nvm_config.h"
#include "config/macro_config.h" #include "config/macro_config.h"
class CMainController class CMainController
@@ -43,15 +44,32 @@ private:
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
// ── Config-Empfangspuffer ───────────────────────────────────────────────── // ── Config-Empfangspuffer ─────────────────────────────────────────────────
uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes uint8_t m_cfg_buf[sizeof(SDeviceConfig)]; // 740 Bytes
uint8_t m_cfg_chunks_expected; uint8_t m_cfg_chunks_expected;
bool m_cfg_receiving; bool m_cfg_receiving;
// ── Makro-Empfangspuffer ────────────────────────────────────────────────── // ── Makro-Empfangspuffer ──────────────────────────────────────────────────
uint8_t m_macro_buf[256]; // sizeof(SMacroTable) = 256 Bytes uint8_t m_macro_buf[sizeof(SMacroTable)]; // 512 Bytes
uint8_t m_macro_chunks_expected; uint8_t m_macro_chunks_expected;
bool m_macro_receiving; bool m_macro_receiving;
// Geladene Makro-Tabelle (im RAM wird beim Start aus NVM geladen) // Geladene Makro-Tabelle (im RAM wird beim Start aus NVM geladen)
SMacroTable m_macros; 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();
}; };
+2 -1
View File
@@ -7,7 +7,8 @@ enum class ActionType : uint8_t
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet) HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …) HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …) HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 4 HID-Keys sequenziell MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 8 HID-Keys sequenziell
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 02); speichert in NVM
}; };
struct __attribute__((packed)) SAction struct __attribute__((packed)) SAction
+32 -17
View File
@@ -1,5 +1,5 @@
// macro_config.cpp // macro_config.cpp
// NVM-Zugriff für die Makro-Tabelle (Row 1, 0x1FF00). // NVM-Zugriff für die Makro-Tabelle (Row 0+1, 0x1FB000x1FCFF, 512 Bytes).
// Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert, // Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert,
// da static kein gemeinsamer Header für interne NVM-Helfer). // da static kein gemeinsamer Header für interne NVM-Helfer).
@@ -7,37 +7,46 @@
#include <Arduino.h> #include <Arduino.h>
#include <string.h> #include <string.h>
static const uint32_t k_macro_addr = 0x1FF00UL; // Row 1 (256B nach Row 0) static const uint32_t k_macro_addr = 0x1FB00UL; // Row 0+1 (zwei Rows à 256B)
static void nvm_wait() { while (!NVMCTRL->INTFLAG.bit.READY) {} } static bool nvm_wait()
static void nvm_exec(uint16_t cmd) {
// ~400ms Timeout bei 48MHz, konservativ 4 Zyklen pro Loop-Iteration
uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000
while (!NVMCTRL->INTFLAG.bit.READY) {
if (--timeout == 0) return false;
}
return true;
}
static bool nvm_exec(uint16_t cmd)
{ {
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd;
nvm_wait(); return nvm_wait();
} }
static void nvm_erase_row(uint32_t addr) static bool nvm_erase_row(uint32_t addr)
{ {
nvm_wait(); if (!nvm_wait()) return false;
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_ER); return nvm_exec(NVMCTRL_CTRLA_CMD_ER);
} }
static void nvm_write_page(uint32_t addr, const uint8_t* data) static bool nvm_write_page(uint32_t addr, const uint8_t* data)
{ {
nvm_exec(NVMCTRL_CTRLA_CMD_PBC); if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false;
volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr); volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(data); const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i]; for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i];
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_WP); return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
bool macro_config_load(SMacroTable& tbl) bool macro_config_load(SMacroTable& tbl)
{ {
memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl)); memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl));
// Prüfen ob Row 1 noch gelöscht ist (alle 0xFF = nie beschrieben) // Prüfen ob beide Rows noch gelöscht sind (alle 0xFF = nie beschrieben)
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&tbl); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&tbl);
bool all_ff = true; bool all_ff = true;
for (uint16_t i = 0; i < sizeof(tbl); i++) { for (uint16_t i = 0; i < sizeof(tbl); i++) {
@@ -50,17 +59,23 @@ bool macro_config_load(SMacroTable& tbl)
return true; return true;
} }
void macro_config_save(const SMacroTable& tbl) bool macro_config_save(const SMacroTable& tbl)
{ {
// Auf 4-Byte-ausgerichteten Puffer kopieren bevor nvm_write_page ihn als uint32_t* liest. // Auf 4-Byte-ausgerichteten Puffer kopieren bevor nvm_write_page ihn als uint32_t* liest.
// SMacroTable ist __attribute__((packed)) und könnte unaligned liegen → // SMacroTable ist __attribute__((packed)) und könnte unaligned liegen →
// direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen. // direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen.
uint8_t aligned_buf[256] __attribute__((aligned(4))); uint8_t aligned_buf[512] __attribute__((aligned(4)));
memcpy(aligned_buf, &tbl, sizeof(tbl)); memcpy(aligned_buf, &tbl, sizeof(tbl));
NVMCTRL->CTRLB.bit.MANW = 1; NVMCTRL->CTRLB.bit.MANW = 1;
nvm_erase_row(k_macro_addr);
for (uint8_t p = 0; p < 4; p++) { // Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00)
nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64); if (!nvm_erase_row(k_macro_addr)) return false;
if (!nvm_erase_row(k_macro_addr + 256)) return false;
// 8 Pages à 64B schreiben
for (uint8_t p = 0; p < 8; p++) {
if (!nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64)) return false;
} }
return true;
} }
+8 -7
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
// macro_config.h // macro_config.h
// Makro-Tabelle: bis zu 32 Slots, je 4 HID-Key-Steps. // Makro-Tabelle: 32 Slots, je 8 HID-Key-Steps.
// Gespeichert in NVM Row 1 (0x1FF00, 256 Bytes). // Gespeichert in NVM Row 0+1 (0x1FB000x1FCFF, 512 Bytes).
// //
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind): // Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
// Slot 019 : MX-Buttons (mx_idx) // Slot 019 : MX-Buttons (mx_idx)
@@ -13,7 +13,7 @@
#include <stdint.h> #include <stdint.h>
#define MACRO_SLOTS 32 #define MACRO_SLOTS 32
#define MACRO_MAX_STEPS 4 #define MACRO_MAX_STEPS 8
// Ein einzelner HID-Key-Step im Makro // Ein einzelner HID-Key-Step im Makro
struct __attribute__((packed)) SMacroStep struct __attribute__((packed)) SMacroStep
@@ -22,15 +22,16 @@ struct __attribute__((packed)) SMacroStep
uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08) uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08)
}; };
// Komplette Makro-Tabelle (32 × 4 × 2 = 256 Bytes = eine NVM-Row) // Komplette Makro-Tabelle (32 × 8 × 2 = 512 Bytes = zwei NVM-Rows)
struct __attribute__((packed)) SMacroTable struct __attribute__((packed)) SMacroTable
{ {
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
}; };
// Makro-Tabelle aus NVM lesen (Row 1: 0x1FF00). // Makro-Tabelle aus NVM lesen (Row 0+1: 0x1FB00).
// Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen. // Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen.
bool macro_config_load(SMacroTable& tbl); bool macro_config_load(SMacroTable& tbl);
// Makro-Tabelle in NVM schreiben (löscht Row 1, schreibt 4 Pages). // Makro-Tabelle in NVM schreiben (löscht Row 0+1, schreibt 8 Pages).
void macro_config_save(const SMacroTable& tbl); // Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird.
bool macro_config_save(const SMacroTable& tbl);
+75 -52
View File
@@ -1,56 +1,60 @@
// nvm_config.cpp
// NVM-Zugriff für SDeviceConfig (3 Rows ab 0x1FD00, 768B gesamt, 740B genutzt).
#include "nvm_config.h" #include "nvm_config.h"
#include <Arduino.h> #include <Arduino.h>
#include <string.h> #include <string.h>
// ── Flash-Adresse (aus Linkerscript) ───────────────────────────────────────── static const uint32_t k_config_addr = 0x1FD00UL; // Row 02 der Config
// Kein separates Linker-Symbol nötig Adresse ist fix und bekannt.
static const uint32_t k_config_addr = 0x1FE00UL;
// SAMD21 NVMCTRL ────────────────────────────────────────────────────────────── // ── NVMCTRL-Hilfsfunktionen ───────────────────────────────────────────────────
// Row = 256 Bytes = 4 Pages à 64 Bytes //
// Schreiben: Row löschen (ER), dann seitenweise schreiben (WP) // nvm_wait() hat einen Timeout (~400ms bei 48MHz) damit das Board nicht
// einfriert wenn der NVMCTRL aus unbekanntem Grund nicht READY meldet.
// (Beobachtet nach bestimmten Bootloader-Firmware-Flash-Zyklen auf SAMD21.)
static void nvm_wait() static bool nvm_wait()
{ {
while (!NVMCTRL->INTFLAG.bit.READY) {} // ~400ms Timeout bei 48MHz, konservativ 4 Zyklen pro Loop-Iteration
uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000
while (!NVMCTRL->INTFLAG.bit.READY) {
if (--timeout == 0) return false;
}
return true;
} }
static void nvm_exec(uint16_t cmd) static bool nvm_exec(uint16_t cmd)
{ {
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd;
nvm_wait(); return nvm_wait();
} }
static void nvm_erase_row(uint32_t addr) static bool nvm_erase_row(uint32_t addr)
{ {
nvm_wait(); if (!nvm_wait()) return false;
NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte) NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte)
nvm_exec(NVMCTRL_CTRLA_CMD_ER); return nvm_exec(NVMCTRL_CTRLA_CMD_ER);
} }
static void nvm_write_page(uint32_t addr, const uint8_t* data) static bool nvm_write_page(uint32_t addr, const uint8_t* data)
{ {
// Page-Buffer löschen if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false;
nvm_exec(NVMCTRL_CTRLA_CMD_PBC);
// 64 Bytes in den Page-Buffer schreiben (32-Bit-Zugriffe)
volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr); volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(data); const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
for (uint8_t i = 0; i < 64 / 4; i++) { for (uint8_t i = 0; i < 64 / 4; i++) {
dst[i] = src[i]; dst[i] = src[i];
} }
// Page programmieren
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_WP); return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
// ── CRC16 (CCITT, Poly 0x1021) ──────────────────────────────────────────────── // ── CRC16-CCITT (Poly 0x1021) ────────────────────────────────────────────────
uint16_t nvm_config_crc(const SDeviceConfig& cfg) uint16_t nvm_config_crc(const SDeviceConfig& cfg)
{ {
// CRC über alles nach dem crc-Feld (ab Byte 7) // CRC über alles nach dem crc-Feld (ab Byte 7: active_profile … Ende Profil 2)
const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, mx_actions); const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, active_profile);
uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions); uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, active_profile);
uint16_t crc = 0xFFFF; uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) { for (uint16_t i = 0; i < len; i++) {
crc ^= static_cast<uint16_t>(data[i]) << 8; crc ^= static_cast<uint16_t>(data[i]) << 8;
@@ -62,36 +66,48 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
} }
// ── Defaults ───────────────────────────────────────────────────────────────── // ── Defaults ─────────────────────────────────────────────────────────────────
void nvm_config_defaults(SDeviceConfig& cfg) void nvm_config_defaults(SDeviceConfig& cfg)
{ {
memset(&cfg, 0, sizeof(cfg)); memset(&cfg, 0, sizeof(cfg));
cfg.magic = NVM_CONFIG_MAGIC; cfg.magic = NVM_CONFIG_MAGIC;
cfg.version = NVM_CONFIG_VERSION; cfg.version = NVM_CONFIG_VERSION;
cfg.active_profile = 0;
cfg.global_brightness = 255;
// Alle Aktionen: NONE
for (uint8_t i = 0; i < 20; i++)
cfg.mx_actions[i] = {ActionType::NONE, 0};
for (uint8_t e = 0; e < 4; e++) for (uint8_t e = 0; e < 4; e++)
for (uint8_t a = 0; a < 3; a++) cfg.enc_sensitivity[e] = 1;
cfg.enc_actions[e][a] = {ActionType::NONE, 0};
// Base-LEDs: warm-weiß for (uint8_t p = 0; p < 3; p++) {
for (uint8_t i = 0; i < 20; i++) { SDeviceProfile& prof = cfg.profiles[p];
cfg.led_r[i] = 80;
cfg.led_g[i] = 40;
cfg.led_b[i] = 0;
}
// LED-Animationen: Regenbogen (COLOR_CYCLE=5) mit 4s Periode als Standard // Alle Aktionen: NONE
for (uint8_t i = 0; i < 20; i++) { for (uint8_t i = 0; i < 20; i++)
cfg.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE prof.mx_actions[i] = {ActionType::NONE, 0};
cfg.led_period_ms[i] = 4000; for (uint8_t e = 0; e < 4; e++)
for (uint8_t a = 0; a < 3; a++)
prof.enc_actions[e][a] = {ActionType::NONE, 0};
// Base-LEDs: warm-weiß
for (uint8_t i = 0; i < 20; i++) {
prof.led_r[i] = 80;
prof.led_g[i] = 40;
prof.led_b[i] = 0;
prof.led_brightness[i] = 255;
}
// LED-Animationen: Regenbogen mit 4s Periode
for (uint8_t i = 0; i < 20; i++) {
prof.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE
prof.led_period_ms[i] = 4000;
}
} }
cfg.crc = nvm_config_crc(cfg); cfg.crc = nvm_config_crc(cfg);
} }
// ── Laden ───────────────────────────────────────────────────────────────────── // ── Laden ─────────────────────────────────────────────────────────────────────
bool nvm_config_load(SDeviceConfig& cfg) bool nvm_config_load(SDeviceConfig& cfg)
{ {
memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg)); memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg));
@@ -100,25 +116,32 @@ bool nvm_config_load(SDeviceConfig& cfg)
if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; } if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; }
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; } if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; }
// Profil-Index absichern
if (cfg.active_profile >= 3) cfg.active_profile = 0;
return true; return true;
} }
// ── Speichern ───────────────────────────────────────────────────────────────── // ── Speichern ─────────────────────────────────────────────────────────────────
void nvm_config_save(const SDeviceConfig& cfg)
bool nvm_config_save(const SDeviceConfig& cfg)
{ {
// Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist. // Config (740B) in 768B-Puffer kopieren (3 Rows), Rest mit 0xFF füllen.
// __attribute__((aligned(4))) ist zwingend: nvm_write_page castet data zu // __attribute__((aligned(4))) ist zwingend: nvm_write_page castet zu uint32_t*.
// const uint32_t*, und unaligned 32-Bit-Zugriffe sind HardFaults auf Cortex-M0+. uint8_t row[768] __attribute__((aligned(4)));
uint8_t row[256] __attribute__((aligned(4)));
memset(row, 0xFF, sizeof(row)); memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg)); memcpy(row, &cfg, sizeof(cfg));
// Automatisches Schreiben deaktivieren (manueller Schreib-Modus)
NVMCTRL->CTRLB.bit.MANW = 1; NVMCTRL->CTRLB.bit.MANW = 1;
// Row 0 der Config löschen und seitenweise schreiben (4 × 64B) // 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00)
nvm_erase_row(k_config_addr); if (!nvm_erase_row(k_config_addr)) return false;
for (uint8_t p = 0; p < 4; p++) { if (!nvm_erase_row(k_config_addr + 256)) return false;
nvm_write_page(k_config_addr + p * 64, row + p * 64); if (!nvm_erase_row(k_config_addr + 512)) return false;
// 12 Pages à 64B schreiben
for (uint8_t p = 0; p < 12; p++) {
if (!nvm_write_page(k_config_addr + p * 64, row + p * 64)) return false;
} }
return true;
} }
+50 -37
View File
@@ -2,61 +2,74 @@
#include <stdint.h> #include <stdint.h>
#include "action.h" #include "action.h"
// ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ──────────────────────────────── // ── NVM-Config-Layout (768 Bytes, ab 0x1FD00) ────────────────────────────────
// //
// Offset Size Inhalt // Row 0 (0x1FD00, 256B): Globaler Header (32B) + Profil 0 (236B) + Padding (12B → überläuft in Row 1)
// 0 4 Magic (0x56503202 = 'VP2\x02') // Row 1 (0x1FE00, 256B): Profil 0 Rest + Profil 1 (Teil)
// 4 1 Version // Row 2 (0x1FF00, 256B): Profil 1 Rest + Profil 2 + Reserve (28B)
// 5 2 CRC16 über Bytes 7222
// 7 60 mx_actions[20] 20 × 3B (SAction packed)
// 67 36 enc_actions[4][3] 12 × 3B
// 103 20 led_r[20]
// 123 20 led_g[20]
// 143 20 led_b[20]
// 163 20 led_anim[20] LEDAnim-Typ pro Button (uint8_t)
// 183 40 led_period_ms[20] Animationsperiode in ms (uint16_t, little-endian)
// 223 33 Padding bis 256 Bytes (erste Row voll)
// 256 256 Reserviert für zukünftige Erweiterungen (zweite Row)
// //
// Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction) // Profil-Offsets (ab Byte 0 des Config-Blobs):
// Header: Bytes 0 31 (32B)
// Profil 0: Bytes 32267 (236B)
// Profil 1: Bytes 268503 (236B)
// Profil 2: Bytes 504739 (236B)
// Reserve: Bytes 740767 (28B)
//
// Alle 3 Rows werden immer gemeinsam gelöscht und neu geschrieben.
#define NVM_CONFIG_MAGIC 0x56503202UL #define NVM_CONFIG_MAGIC 0x56503203UL // 'VP2\x03' Version 3
#define NVM_CONFIG_VERSION 2 // Version 2 #define NVM_CONFIG_VERSION 3
// Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[]) // Encoder-Aktions-Indizes (in SDeviceProfile.enc_actions[])
// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW
#define ENC_ACTION_SW 0 #define ENC_ACTION_SW 0
#define ENC_ACTION_CW 1 #define ENC_ACTION_CW 1
#define ENC_ACTION_CCW 2 #define ENC_ACTION_CCW 2
// ── Pro-Profil-Daten (236 Bytes) ─────────────────────────────────────────────
struct __attribute__((packed)) SDeviceProfile
{
SAction mx_actions[20]; // 60B MX-Buttons 019
SAction enc_actions[4][3]; // 36B [Encoder 03][SW/CW/CCW]
uint8_t led_r[20]; // 20B
uint8_t led_g[20]; // 20B
uint8_t led_b[20]; // 20B
uint8_t led_brightness[20]; // 20B per-LED Helligkeit (0255, Default 255)
uint8_t led_anim[20]; // 20B LEDAnim-Typ (0=STATIC … 5=COLOR_CYCLE)
uint16_t led_period_ms[20]; // 40B Animationsperiode in ms
// Gesamt: 236B
};
// ── Globale Config (740 Bytes) ────────────────────────────────────────────────
struct __attribute__((packed)) SDeviceConfig struct __attribute__((packed)) SDeviceConfig
{ {
uint32_t magic; // Globaler Header (32B)
uint8_t version; uint32_t magic; // 4B
uint16_t crc; uint8_t version; // 1B
uint16_t crc; // 2B CRC16-CCITT über Bytes 7739
uint8_t active_profile; // 1B aktives Profil (02)
uint8_t global_brightness; // 1B globale LED-Helligkeit (0255)
uint8_t enc_sensitivity[4]; // 4B Schrittweite pro Encoder (reserviert, Default 1)
uint8_t _reserve[19]; // 19B Platz für spätere globale Felder
// Aktionen // Profile (3 × 236B = 708B)
SAction mx_actions[20]; // MX-Buttons 019 (key_id 524) SDeviceProfile profiles[3];
SAction enc_actions[4][3]; // [Encoder 03][SW/CW/CCW] // Gesamt: 32 + 708 = 740B
// Base-LED Farben
uint8_t led_r[20];
uint8_t led_g[20];
uint8_t led_b[20];
// LED-Animationen pro MX-Button
uint8_t led_anim[20]; // LEDAnim-Typ (0=STATIC, 1=BLINK, 2=PULSE, 5=COLOR_CYCLE)
uint16_t led_period_ms[20]; // Animationsperiode in ms (0 = Firmware-Default verwenden)
}; };
// Standardwerte wenn keine gültige Config im NVM // Standardwerte wenn keine gültige Config im NVM
void nvm_config_defaults(SDeviceConfig& cfg); void nvm_config_defaults(SDeviceConfig& cfg);
// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC ungültig → Defaults geladen. // Config aus NVM lesen. Gibt false zurück wenn Magic/CRC/Version ungültig → Defaults geladen.
bool nvm_config_load(SDeviceConfig& cfg); bool nvm_config_load(SDeviceConfig& cfg);
// Config in NVM schreiben (löscht 2 Rows, schreibt neu). // Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages).
void nvm_config_save(const SDeviceConfig& cfg); // Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird (Board hängt nicht).
bool nvm_config_save(const SDeviceConfig& cfg);
// CRC16 über die Nutzdaten der Config // CRC16 über die Nutzdaten der Config (Bytes 7739, nach dem crc-Feld)
uint16_t nvm_config_crc(const SDeviceConfig& cfg); uint16_t nvm_config_crc(const SDeviceConfig& cfg);
+9 -3
View File
@@ -8,7 +8,9 @@
// Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand // Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand
// bestimmt und mit dem vorherigen verglichen. // bestimmt und mit dem vorherigen verglichen.
// //
// Lookup-Tabelle [prev<<2 | curr] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen) // Lookup-Tabelle [prev<<2 | curr] → Roh-Vorzeichen, 0 (ungültig/Prellen)
// Die physische CW/CCW-Bedeutung wird nach dem Akkumulator per
// ENCODER_DIRECTION_SIGN an die VersaPad-PCB-Verdrahtung angepasst.
static const int8_t k_lut[16] = { static const int8_t k_lut[16] = {
// curr: 00 01 10 11 // curr: 00 01 10 11
0, +1, -1, 0, // prev = 00 0, +1, -1, 0, // prev = 00
@@ -28,6 +30,10 @@ static encoder_cb_t s_cb = nullptr;
static const uint8_t k_pin_a[ENCODER_COUNT] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A }; static const uint8_t k_pin_a[ENCODER_COUNT] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A };
static const uint8_t k_pin_b[ENCODER_COUNT] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B }; static const uint8_t k_pin_b[ENCODER_COUNT] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B };
// The PCB wiring makes the quadrature sign opposite to the user-facing knob
// direction. Keep the lookup table conventional and invert once at the HAL edge.
static constexpr int8_t ENCODER_DIRECTION_SIGN = -1;
// Generischer Handler — wird von den 8 ISR-Wrappern unten aufgerufen. // Generischer Handler — wird von den 8 ISR-Wrappern unten aufgerufen.
static void handle_encoder(uint8_t enc) static void handle_encoder(uint8_t enc)
{ {
@@ -45,10 +51,10 @@ static void handle_encoder(uint8_t enc)
// 4 Halb-Schritte = 1 vollständige Raste // 4 Halb-Schritte = 1 vollständige Raste
if (s_accum[enc] >= 4) { if (s_accum[enc] >= 4) {
s_accum[enc] = 0; s_accum[enc] = 0;
if (s_cb) s_cb(enc, +1); if (s_cb) s_cb(enc, +1 * ENCODER_DIRECTION_SIGN);
} else if (s_accum[enc] <= -4) { } else if (s_accum[enc] <= -4) {
s_accum[enc] = 0; s_accum[enc] = 0;
if (s_cb) s_cb(enc, -1); if (s_cb) s_cb(enc, -1 * ENCODER_DIRECTION_SIGN);
} }
} }
+1
View File
@@ -54,6 +54,7 @@
#define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B #define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen #define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen
#define USB_EVT_MACRO_ACK 0x95 // Makro-Tabelle erfolgreich gespeichert #define USB_EVT_MACRO_ACK 0x95 // Makro-Tabelle erfolgreich gespeichert
#define USB_EVT_MACRO_NACK 0x99 // Makro-Tabelle: NVM-Fehler nicht geschrieben
#define USB_EVT_MACRO_BEGIN 0x96 // Beginn Makro-Dump: Data[1] = Chunk-Anzahl #define USB_EVT_MACRO_BEGIN 0x96 // Beginn Makro-Dump: Data[1] = Chunk-Anzahl
#define USB_EVT_MACRO_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B #define USB_EVT_MACRO_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen #define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen