Compare commits
9 Commits
0b3c5f3217
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ac3b2aa90f | |||
| 24b349de26 | |||
| 802ab858e1 | |||
| 433d61c29f | |||
| 098a166a9f | |||
| 7169d3bbba | |||
| 3e83758f05 | |||
| d6ed7cb81f | |||
| 9079fefad8 |
@@ -1,273 +1,169 @@
|
||||
# VersaMCU
|
||||
|
||||
Firmware für das VersaPad v2 Macro-Pad.
|
||||
Läuft auf einem **ATSAMD21G17D** (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework.
|
||||
Firmware fuer das VersaPad v2 Macro-Pad.
|
||||
Laeuft auf einem ATSAMD21G17D mit PlatformIO und Arduino-Framework.
|
||||
|
||||
## Voraussetzungen
|
||||
## Hardware
|
||||
|
||||
- [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
|
||||
| Eigenschaft | Detail |
|
||||
|---|---|
|
||||
| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
|
||||
| Flash / RAM | 128 KB / 16 KB |
|
||||
| USB | Composite: HID Keyboard + Consumer + CDC Serial |
|
||||
| Matrix | 5x5 logisch, davon 20 MX-Buttons + 4 Encoder-SW + 1 unbelegt |
|
||||
| Encoder | 4x Rotary Encoder mit Quadratur via EIC-Interrupt |
|
||||
| LEDs | 20x WS2812B an `PB22` |
|
||||
| Programmer | Atmel-ICE via SWD, kein Bootloader |
|
||||
|
||||
## Flashen
|
||||
## Build und Flash
|
||||
|
||||
```bash
|
||||
pio run
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig – der Chip wird direkt programmiert.
|
||||
Der Upload laeuft per OpenOCD ueber SWD.
|
||||
|
||||
---
|
||||
## Laufzeitmodell
|
||||
|
||||
## Funktionsumfang (Anforderungskatalog)
|
||||
`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 1–4) | ✅ |
|
||||
| 5.3 | Timing: 10 ms Key-Down-Dauer + 20 ms Pause zwischen Steps | ✅ |
|
||||
| 5.4 | Makro-Tabelle in **separater NVM-Row** (Row 1, 0x1FF00, 256 Byte) | ✅ |
|
||||
| 5.5 | Makro-Tabelle wird beim Start aus NVM geladen; gelöschter Flash (0xFF) → leere Tabelle | ✅ |
|
||||
|
||||
### 6 LED-System (WS2812)
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 6.1 | **20 WS2812-LEDs**, serpentiner Verdrahtung; Adafruit-NeoPixel bit-bang Treiber | ✅ |
|
||||
| 6.2 | **2-Schicht-Modell** pro Button: `base` (Idle) + `override` (temporär von GUI) | ✅ |
|
||||
| 6.3 | **STATIC**: feste Farbe aus NVM | ✅ |
|
||||
| 6.4 | **BLINK**: binäres An/Aus mit konfigurierbarer Halbperiode | ✅ |
|
||||
| 6.5 | **PULSE**: lineares Helligkeitsdreieck (0→255→0), kein Float | ✅ |
|
||||
| 6.6 | **COLOR_CYCLE** (Regenbogen): Hue-Sweep über 6 Segmente, ignoriert base/override | ✅ |
|
||||
| 6.7 | **COLOR_FADE**: einmaliger RGB-Crossfade zu Zielfarbe | ✅ |
|
||||
| 6.8 | Phasenversatz beim Start: Regenbogen-LEDs sind gleichmäßig über die Periode verteilt | ✅ |
|
||||
| 6.9 | `ws2812_show()` nur bei dirty-Flag aufgerufen (~600 µs Blockzeit vermieden) | ✅ |
|
||||
| 6.10 | Alle Animationen in **Integer-Arithmetik** (kein FPU auf M0+) | ✅ |
|
||||
|
||||
### 7 Konfigurations-Speicherung (NVM)
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 7.1 | Config-Layout **Version 2**, 223 Byte packed, mit Magic `0x56503202` + CRC16-CCITT | ✅ |
|
||||
| 7.2 | Config gespeichert in **NVM Row 0** (0x1FE00, 256 Byte, via Linkerscript reserviert) | ✅ |
|
||||
| 7.3 | Makro-Tabelle in **NVM Row 1** (0x1FF00, 256 Byte) | ✅ |
|
||||
| 7.4 | NVM-Schreiben: Row löschen + 4 Pages à 64 Byte manuell schreiben (MANW=1) | ✅ |
|
||||
| 7.5 | Bei ungültigem Magic / falscher Version / CRC-Fehler → **Defaults** laden (kein Crash) | ✅ |
|
||||
| 7.6 | Defaults: alle Aktionen NONE, LEDs warm-weiß, Animation Regenbogen 4 s | ✅ |
|
||||
|
||||
### 8 Serial-Kommunikation mit VersaGUI
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 8.1 | **CDC Serial** (USB), kein Treiber nötig; 8-Byte-Festlängen-Pakete | ✅ |
|
||||
| 8.2 | **Ring-Buffer** (256 Byte = 32 Pakete) für eingehende Bytes; kein Datenverlust bei Burst | ✅ |
|
||||
| 8.3 | **Ping / Pong** (0x05 / 0x85) zur Verbindungsdiagnose | ✅ |
|
||||
| 8.4 | **Config-Transfer PC→Board**: BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12) → ACK/NACK | ✅ |
|
||||
| 8.5 | **Config-Dump Board→PC**: auf READ(0x13) → BEGIN(0x92) → 38×DATA(0x93) → END(0x94) | ✅ |
|
||||
| 8.6 | **Makro-Transfer PC→Board**: BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22) → ACK(0x95) | ✅ |
|
||||
| 8.7 | **Makro-Dump Board→PC**: auf READ(0x23) → BEGIN(0x96) → 43×DATA(0x97) → END(0x98) | ✅ |
|
||||
| 8.8 | Config-COMMIT validiert Magic + Version + CRC; bei Fehler **NACK** ohne NVM-Schreiben | ✅ |
|
||||
| 8.9 | Alle Sende-Pakete nur wenn `SerialUSB` aktiv (DTR-Check verhindert stilles Verwerfen) | ✅ |
|
||||
|
||||
### 9 Nicht implementiert / Roadmap
|
||||
|
||||
| # | Anforderung | Status |
|
||||
|---|-------------|--------|
|
||||
| 9.1 | **Fader/Potentiometer**: 3× ADC-Kanäle auf Board vorhanden, HAL nicht implementiert | 🔲 TODO |
|
||||
| 9.2 | **HOST_COMMAND-Payload**: Board sendet Command-ID, App-Seite führt aus (halbfertig) | 🔲 TODO |
|
||||
| 9.3 | **FADE_IN / FADE_OUT** per GUI konfigurierbar (Firmware vorhanden, kein GUI-Eintrag) | 🔲 TODO |
|
||||
|
||||
---
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
VersaMCU/
|
||||
├── platformio.ini – Build- und Upload-Konfiguration
|
||||
├── upload_openocd.py – Benutzerdefiniertes Upload-Script (OpenOCD)
|
||||
├── boards/
|
||||
│ ├── versapad.json – Board-Definition mit Bootloader
|
||||
│ └── versapad_nobl.json – Board-Definition ohne Bootloader (aktiv)
|
||||
├── variants/versapad/
|
||||
│ ├── variant.h/.cpp – Pin-Mapping für den SAMD21G17D
|
||||
│ └── linker_scripts/ – Linkerscript (kein Bootloader, NVM-Reservierung)
|
||||
└── src/
|
||||
├── main.cpp – Arduino setup()/loop()
|
||||
├── CMainController.h/.cpp – Zentraler Orchestrator
|
||||
├── CButton.h/.cpp – Button-Modell: LED-Schichten, Action, Animationen
|
||||
├── CEventQueue.h/.cpp – Ring-Buffer FIFO (16 Slots, kein Heap)
|
||||
├── SEvent.h – Event-Typen (KEY_DOWN/UP, ENC_CW/CCW)
|
||||
├── config/
|
||||
│ ├── pins.h – Pin-Nummern (Arduino-Nummern aus variant.h)
|
||||
│ ├── action.h – ActionType-Enum + SAction-Struct (packed)
|
||||
│ └── nvm_config.h/.cpp – NVM-Config: Laden, Speichern, CRC16, Defaults
|
||||
└── hal/
|
||||
├── matrix.h/.cpp – 5×5-Matrix-Scan, 10 ms Debounce
|
||||
├── encoder.h/.cpp – Quadratur-Dekodierung via EIC-Interrupts
|
||||
├── ws2812.h/.cpp – WS2812-LED-Treiber (Adafruit NeoPixel, bit-bang)
|
||||
├── usb_hid.h/.cpp – HID Keyboard + Consumer Control
|
||||
└── usb_serial.h/.cpp – CDC Serial bidirektional, 8-Byte-Pakete
|
||||
```text
|
||||
matrix_scan()
|
||||
poll_vendor()
|
||||
processEvents()
|
||||
check_factory_reset()
|
||||
updateLEDs()
|
||||
```
|
||||
|
||||
## Architektur
|
||||
Dabei gilt:
|
||||
|
||||
### Loop-Ablauf
|
||||
- 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.
|
||||
|
||||
```
|
||||
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()
|
||||
```
|
||||
## Action-System
|
||||
|
||||
### CButton – LED-Schichten
|
||||
Unterstuetzte `ActionType`s:
|
||||
|
||||
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
|
||||
|
||||
| Animation | Verhalten |
|
||||
| Typ | Verhalten |
|
||||
|---|---|
|
||||
| `STATIC` | Feste Farbe |
|
||||
| `BLINK` | Binäres An/Aus |
|
||||
| `PULSE` | Lineares Dreieck 0→255→0 |
|
||||
| `FADE_IN / FADE_OUT` | Einmaliges Ein-/Ausblenden |
|
||||
| `COLOR_CYCLE` | Hue-Sweep (Regenbogen), ignoriert base/override |
|
||||
| `COLOR_FADE` | Crossfade zu Zielfarbe |
|
||||
| `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 |
|
||||
|
||||
Alle Berechnungen in Integer-Arithmetik (kein FPU auf Cortex-M0+).
|
||||
Wichtige Semantik:
|
||||
|
||||
**Warum Bit-Bang statt DMA?**
|
||||
- normale Keys und Consumer folgen dem Hold-Modell
|
||||
- Encoder `CW` / `CCW` sind immer Tap-Events
|
||||
- Makros laufen komplett in der Firmware, ohne laufende App
|
||||
|
||||
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.
|
||||
## LED-System
|
||||
|
||||
`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.
|
||||
Jeder MX-Button hat:
|
||||
|
||||
Ergebnis: Bit-Bang via Adafruit NeoPixel reicht für 20 LEDs vollständig aus, belegt keinen SERCOM und keinen zusätzlichen RAM.
|
||||
- eine Base-Farbe
|
||||
- optional eine temporaere Override-Farbe
|
||||
- eine Animation
|
||||
|
||||
**Warum keine Adafruit-Animationsfunktionen?**
|
||||
Aktuelle Animationsmodi:
|
||||
|
||||
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:
|
||||
- `STATIC`
|
||||
- `BLINK`
|
||||
- `PULSE`
|
||||
- `FADE_IN`
|
||||
- `FADE_OUT`
|
||||
- `COLOR_CYCLE`
|
||||
- `COLOR_FADE`
|
||||
|
||||
- `Adafruit_NeoPixel::ColorHSV()` verwendet intern float-Operationen für die HSV→RGB-Konvertierung. Der Cortex-M0+ hat keine FPU; float wird per Software emuliert (~10–20× langsamer). `hue_to_rgb()` in `CButton.cpp` erreicht dasselbe Ergebnis mit reiner Integer-Arithmetik (6 lineare Segmente à 43 Hue-Einheiten).
|
||||
- Das 2-Schicht-Modell (base + override) und die dirty-Flag-gesteuerte Render-Pipeline sind projektspezifische Logik ohne Entsprechung in der Library.
|
||||
Die GUI nutzt derzeit vor allem `STATIC`, `BLINK`, `PULSE` und `COLOR_CYCLE`.
|
||||
|
||||
**Idle-Zustand:** Alle 20 MX-LEDs zeigen einen rotierenden Regenbogen (`COLOR_CYCLE`, 4 s/Runde, 40 % Helligkeit, gleichmäßig phasenverschoben).
|
||||
## Aktuelles NVM-Layout
|
||||
|
||||
### Serial-Protokoll (8 Bytes, fixed)
|
||||
### DeviceConfig
|
||||
|
||||
```
|
||||
Byte 0: Command/Event-ID
|
||||
Byte 1: key_id (Button 0–24 oder Encoder 0–3)
|
||||
Byte 2: r / Daten-Byte A
|
||||
Byte 3: g / Daten-Byte B
|
||||
Byte 4: b
|
||||
Byte 5–7: reserviert (0x00)
|
||||
```
|
||||
- Version: `3`
|
||||
- Magic: `0x56503203`
|
||||
- Groesse: `740` Byte
|
||||
- CRC16-CCITT ueber Bytes `7..739`
|
||||
- 3 Profile
|
||||
- globale Helligkeit
|
||||
- per-LED-Helligkeit
|
||||
|
||||
| ID | Richtung | Bedeutung |
|
||||
### MacroTable
|
||||
|
||||
- 32 Slots
|
||||
- 8 Steps pro Slot
|
||||
- 512 Byte gesamt
|
||||
|
||||
### Flash-Bereich
|
||||
|
||||
| Bereich | Adresse | Groesse |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| Makros | `0x1FB00-0x1FCFF` | 512 B |
|
||||
| Config | `0x1FD00-0x1FFFF` | 768 B, davon 740 B genutzt |
|
||||
|
||||
### NVM-Config-Layout (Version 2, 223 Bytes, packed)
|
||||
Config und Makros liegen in getrennten reservierten NVM-Bereichen.
|
||||
|
||||
```
|
||||
Offset 0 4B Magic 0x56503202
|
||||
Offset 4 1B Version 2
|
||||
Offset 5 2B CRC16-CCITT (über Bytes 7–222)
|
||||
Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B)
|
||||
Offset 67 36B enc_actions[4][3] – je 3B
|
||||
Offset 103 20B led_r[20]
|
||||
Offset 123 20B led_g[20]
|
||||
Offset 143 20B led_b[20]
|
||||
Offset 163 20B led_anim[20] – LEDAnim-Typ (uint8_t)
|
||||
Offset 183 40B led_period_ms[20] – Animationsperiode in ms (uint16_t, LE)
|
||||
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/
|
||||
|-- platformio.ini
|
||||
|-- boards/
|
||||
|-- variants/versapad/
|
||||
`-- src/
|
||||
|-- main.cpp
|
||||
|-- CMainController.h/.cpp
|
||||
|-- CButton.h/.cpp
|
||||
|-- CEventQueue.h/.cpp
|
||||
|-- SEvent.h
|
||||
|-- config/
|
||||
| |-- action.h
|
||||
| |-- macro_config.h/.cpp
|
||||
| `-- nvm_config.h/.cpp
|
||||
`-- hal/
|
||||
|-- encoder.h/.cpp
|
||||
|-- matrix.h/.cpp
|
||||
|-- usb_hid.h/.cpp
|
||||
|-- usb_serial.h/.cpp
|
||||
`-- ws2812.h/.cpp
|
||||
```
|
||||
|
||||
**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.
|
||||
## Weiterfuehrende Doku
|
||||
|
||||
## 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 |
|
||||
- [doc/INDEX.md](doc/INDEX.md)
|
||||
- [doc/00_architecture.md](doc/00_architecture.md)
|
||||
- [doc/03_action_engine.md](doc/03_action_engine.md)
|
||||
- [doc/04_macro_system.md](doc/04_macro_system.md)
|
||||
- [doc/06_nvm_config.md](doc/06_nvm_config.md)
|
||||
- [doc/07_serial_protocol.md](doc/07_serial_protocol.md)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# VersaMCU - Architekturuebersicht
|
||||
|
||||
## Zielplattform
|
||||
|
||||
| Merkmal | Wert |
|
||||
|---|---|
|
||||
| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
|
||||
| Flash | 128 KB |
|
||||
| RAM | 16 KB |
|
||||
| FPU | keine, deshalb Integer-Arithmetik |
|
||||
| USB | HID Keyboard + Consumer + CDC Serial |
|
||||
| Toolchain | PlatformIO + Arduino Core |
|
||||
|
||||
## Setup und Loop
|
||||
|
||||
```text
|
||||
setup()
|
||||
macro_config_load()
|
||||
nvm_config_load()
|
||||
init_buttons()
|
||||
usb_hid_init()
|
||||
usb_serial_init()
|
||||
matrix_init(cb)
|
||||
encoder_init(cb)
|
||||
|
||||
loop()
|
||||
matrix_scan()
|
||||
poll_vendor()
|
||||
processEvents()
|
||||
check_factory_reset()
|
||||
updateLEDs()
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```text
|
||||
matrix_scan / encoder ISR
|
||||
-> EventQueue
|
||||
-> processEvents()
|
||||
-> execute_action_down / execute_action_up
|
||||
-> usb_hid_*
|
||||
-> usb_serial_send() fuer HOST_COMMAND
|
||||
|
||||
CDC Serial
|
||||
-> poll_vendor()
|
||||
-> Config/Makros einlesen oder dumpen
|
||||
-> LED-Overrides setzen/loeschen
|
||||
|
||||
LED-Render
|
||||
-> CButton.render_led()
|
||||
-> ws2812_set()
|
||||
-> ws2812_show() nur wenn dirty
|
||||
```
|
||||
|
||||
## Zentrale Komponenten
|
||||
|
||||
| Datei | Aufgabe |
|
||||
|---|---|
|
||||
| `main.cpp` | startet den Controller |
|
||||
| `CMainController.*` | Orchestrator fuer Inputs, Actions, Serial, LEDs |
|
||||
| `CButton.*` | LED-Zustand, Animationen, Action-Referenz |
|
||||
| `CEventQueue.*` | ISR-sicherer Ringbuffer |
|
||||
| `config/nvm_config.*` | Config v3 laden, speichern, Defaults |
|
||||
| `config/macro_config.*` | Makros laden, speichern |
|
||||
| `hal/matrix.*` | 5x5-Matrixscan mit Debounce |
|
||||
| `hal/encoder.*` | Encoder-ISR und Drehrichtung |
|
||||
| `hal/usb_hid.*` | Keyboard- und Consumer-HID |
|
||||
| `hal/usb_serial.*` | CDC-Paketpfad |
|
||||
| `hal/ws2812.*` | WS2812-Treiber |
|
||||
|
||||
## Key-ID-Schema
|
||||
|
||||
```text
|
||||
0..3 = Encoder-SW
|
||||
4 = unbenutzt
|
||||
5..24 = MX-Buttons
|
||||
```
|
||||
|
||||
Die beiden Werksreset-Tasten sind:
|
||||
|
||||
- `key_id 9` = unten links
|
||||
- `key_id 24` = unten rechts
|
||||
|
||||
## Werksreset im Ablauf
|
||||
|
||||
Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware:
|
||||
|
||||
- sobald beide Reset-Tasten gleichzeitig gehalten werden, werden ihre normalen Actions unterdrueckt
|
||||
- falls bereits ein HID-Hold aktiv war, wird er sofort freigegeben
|
||||
- nach 5 Sekunden gemeinsamer Haltezeit wird Default-Config + leere Makro-Tabelle in NVM geschrieben
|
||||
- danach folgt ein kurzes rotes Feedback-Blinken
|
||||
|
||||
## Invarianten
|
||||
|
||||
- kein Heap
|
||||
- keine Floats
|
||||
- `packed` fuer serielle und NVM-relevante Structs
|
||||
- NVM-Schreibpuffer muessen 4-Byte-aligned sein
|
||||
- `usb_serial_send()` sendet nur bei aktiver CDC-Verbindung
|
||||
@@ -0,0 +1,45 @@
|
||||
# Tasten-Matrix
|
||||
|
||||
**Dateien:** `hal/matrix.h`, `hal/matrix.cpp`, `config/pins.h`
|
||||
|
||||
## Hardware
|
||||
|
||||
5×5-Matrix mit externer 10-kΩ-Pullup-Beschaltung auf den COL-Leitungen (immer HIGH im Ruhezustand). Dioden zwischen Schalter-DO und ROW-Leitung (Anode = Schalter, Kathode = ROW) verhindern Geistertasten bei Mehrfachdrücken.
|
||||
|
||||
Scan-Prinzip: ROW LOW treiben → gedrückter Schalter zieht zugehörige COL durch Diode auf LOW.
|
||||
|
||||
## Scan-Logik
|
||||
|
||||
```
|
||||
für jede ROW r:
|
||||
ROW r → OUTPUT LOW
|
||||
warte 10 µs (Einschwingen)
|
||||
für jede COL c:
|
||||
raw = (digitalRead(COL[c]) == LOW)
|
||||
Debounce prüfen
|
||||
ROW r → INPUT (hochohmig, Pullup-Freigabe)
|
||||
```
|
||||
|
||||
ROW-Pins wechseln zwischen OUTPUT-LOW (während Scan) und INPUT (hochohmig) – kein dauerhaftes LOW.
|
||||
|
||||
## Debounce
|
||||
|
||||
- **10 ms**, Software-seitig pro Taste
|
||||
- Flanken-Erkennung: Zustandsänderung (raw) wird mit Timestamp notiert
|
||||
- Erst nach 10 ms stabiler neuer Zustand wird `s_debounced` aktualisiert und der Callback aufgerufen
|
||||
- Callback: `matrix_cb(key_id, pressed)` → `CEventQueue::push(KEY_DOWN / KEY_UP)`
|
||||
|
||||
## Key-ID-Berechnung
|
||||
|
||||
```cpp
|
||||
key_id = col * MATRIX_ROWS + row
|
||||
// col 0–4, row 0–4
|
||||
// key_id 0–4: Encoder-SW (COL_0)
|
||||
// key_id 5–24: MX-Buttons (COL_1–4)
|
||||
```
|
||||
|
||||
## Kontext
|
||||
|
||||
- Läuft im Loop-Kontext (kein ISR)
|
||||
- Encoder-SW-Tasten gehen durch denselben Matrix-Pfad (COL_0)
|
||||
- `matrix_scan()` wird einmal pro `loop()` aufgerufen
|
||||
@@ -0,0 +1,49 @@
|
||||
# Quadratur-Encoder
|
||||
|
||||
**Dateien:** `hal/encoder.h`, `hal/encoder.cpp`, `config/pins.h`
|
||||
|
||||
## Hardware
|
||||
|
||||
4 mechanische Quadratur-Encoder mit je 2 Phasen-Pins (A, B). Pins sind INPUT_PULLUP.
|
||||
Encoder-SW-Tasten laufen **nicht** durch diesen HAL, sondern durch den Matrix-Scan (COL_0).
|
||||
|
||||
## Dekodierung
|
||||
|
||||
4-State-Lookup-Table über `(prev_state << 2) | cur_state`:
|
||||
|
||||
```
|
||||
Zustand = (A << 1) | B → 4 Bits: 00 / 01 / 10 / 11
|
||||
LUT[prev<<2 | cur] → Roh-Vorzeichen, 0 (ungueltig/Prellen)
|
||||
```
|
||||
|
||||
Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt Halbschritte.
|
||||
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
|
||||
|
||||
8 ISR-Wrapper (je einer pro Pin, da `attachInterrupt` keinen Parameter unterstützt):
|
||||
|
||||
```cpp
|
||||
static void isr_enc0_a() { handle_encoder(0); }
|
||||
static void isr_enc0_b() { handle_encoder(0); }
|
||||
// ... analog für Encoder 1–3
|
||||
```
|
||||
|
||||
`attachInterrupt(..., CHANGE)` auf beiden Pins jedes Encoders.
|
||||
|
||||
`handle_encoder()` → `encoder_cb(enc_id, direction)` → `CEventQueue::push(ENC_CW / ENC_CCW)`
|
||||
|
||||
## ISR-Sicherheit
|
||||
|
||||
- `s_state[]` und `s_accum[]` sind `volatile`
|
||||
- `CEventQueue::push()` ist ISR-sicher (atomare Index-Inkremente auf Single-Core-M0+, kein Heap)
|
||||
- Der Callback-Pointer `s_cb` wird einmalig in `setup()` gesetzt, bevor Interrupts aktiviert werden
|
||||
|
||||
## Initialisierung
|
||||
|
||||
Initialer Zustand von A/B wird beim `encoder_init()` gelesen damit der erste Interrupt korrekt ausgewertet wird (kein "Phantom-Step" beim Einschalten).
|
||||
@@ -0,0 +1,103 @@
|
||||
# Aktions-Engine
|
||||
|
||||
Dateien:
|
||||
|
||||
- `config/action.h`
|
||||
- `CMainController.h/.cpp`
|
||||
- `CButton.h/.cpp`
|
||||
|
||||
## `SAction`
|
||||
|
||||
```cpp
|
||||
struct __attribute__((packed)) SAction {
|
||||
ActionType type;
|
||||
uint16_t data;
|
||||
};
|
||||
```
|
||||
|
||||
Groesse: 3 Byte.
|
||||
Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI uebereinstimmen muss.
|
||||
|
||||
## `ActionType`
|
||||
|
||||
| Typ | Bedeutung | `data` |
|
||||
|---|---|---|
|
||||
| `NONE` | keine Aktion | - |
|
||||
| `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier |
|
||||
| `HID_CONSUMER` | Media/Consumer-HID | usage id |
|
||||
| `HOST_COMMAND` | Event an die GUI | command id |
|
||||
| `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
|
||||
| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil |
|
||||
|
||||
## Verhalten bei `KEY_DOWN`
|
||||
|
||||
| Typ | Effekt |
|
||||
|---|---|
|
||||
| `HID_KEY` | `usb_hid_send_key()` |
|
||||
| `HID_CONSUMER` | `usb_hid_send_consumer()` |
|
||||
| `HOST_COMMAND` | `usb_serial_send(KEY_DOWN/ENC_*)` |
|
||||
| `MACRO` | komplette Sequenz sofort abspielen |
|
||||
| `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren |
|
||||
| `NONE` | nichts |
|
||||
|
||||
## Verhalten bei `KEY_UP`
|
||||
|
||||
| Typ | Effekt |
|
||||
|---|---|
|
||||
| `HID_KEY` | `usb_hid_release_key()` |
|
||||
| `HID_CONSUMER` | `usb_hid_release_consumer()` |
|
||||
| `HOST_COMMAND` | optionaler Up-Pfad, derzeit praktisch ohne Nutzlast |
|
||||
| `MACRO` | nichts |
|
||||
| `PROFILE_SWITCH` | nichts |
|
||||
| `NONE` | nichts |
|
||||
|
||||
## Hold- und Tap-Modell
|
||||
|
||||
- MX-Buttons und Encoder-SW benutzen fuer HID und Consumer das Hold-Modell.
|
||||
- Encoder `CW` und `CCW` sind immer diskrete Tap-Events:
|
||||
|
||||
```text
|
||||
down -> delay(10 ms) -> up
|
||||
```
|
||||
|
||||
- Makros laufen komplett synchron in der Firmware.
|
||||
|
||||
## Makro-Ausfuehrung
|
||||
|
||||
Bei `ActionType::MACRO` wird `action.data` als Slot interpretiert.
|
||||
Die Firmware laeuft dann durch bis zu 8 Steps:
|
||||
|
||||
```text
|
||||
step.keycode == 0 -> Ende
|
||||
Key-Down
|
||||
10 ms warten
|
||||
Key-Up
|
||||
20 ms warten
|
||||
```
|
||||
|
||||
## Profilwechsel
|
||||
|
||||
`PROFILE_SWITCH` arbeitet direkt auf der gespeicherten Config:
|
||||
|
||||
1. Config aus NVM laden
|
||||
2. `active_profile` aendern
|
||||
3. CRC neu berechnen
|
||||
4. wieder speichern
|
||||
5. `init_buttons()`
|
||||
|
||||
Wichtig:
|
||||
`active_profile` liegt im CRC-geschuetzten Bereich. Ohne neue CRC wuerde die Config beim naechsten Laden verworfen.
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,87 @@
|
||||
# Makro-System
|
||||
|
||||
Dateien:
|
||||
|
||||
- `config/macro_config.h`
|
||||
- `config/macro_config.cpp`
|
||||
- `CMainController.cpp`
|
||||
|
||||
## Datenstruktur
|
||||
|
||||
```cpp
|
||||
struct __attribute__((packed)) SMacroStep {
|
||||
uint8_t keycode;
|
||||
uint8_t modifier;
|
||||
};
|
||||
|
||||
#define MACRO_SLOTS 32
|
||||
#define MACRO_MAX_STEPS 8
|
||||
|
||||
struct __attribute__((packed)) SMacroTable {
|
||||
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
|
||||
};
|
||||
```
|
||||
|
||||
Gesamtgroesse:
|
||||
|
||||
- `32 * 8 * 2 = 512` Byte
|
||||
- verteilt auf zwei NVM-Rows
|
||||
|
||||
## Speicherort
|
||||
|
||||
| Row | Adresse | Inhalt |
|
||||
|---|---|---|
|
||||
| Macro Row 0 | `0x1FB00` | Bytes `0..255` |
|
||||
| Macro Row 1 | `0x1FC00` | Bytes `256..511` |
|
||||
|
||||
## Slot-Konvention
|
||||
|
||||
Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung:
|
||||
|
||||
| Slots | Bedeutung |
|
||||
|---|---|
|
||||
| `0..19` | MX-Buttons |
|
||||
| `20..31` | Encoder-Aktionen (`enc * 3 + act_idx`) |
|
||||
|
||||
## Laden
|
||||
|
||||
`macro_config_load()`:
|
||||
|
||||
- kopiert 512 Byte aus NVM in `SMacroTable`
|
||||
- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
|
||||
- setzt dann eine leere Tabelle
|
||||
|
||||
Eine leere Tabelle ist also ein gueltiger Default-Zustand.
|
||||
|
||||
## Speichern
|
||||
|
||||
`macro_config_save()`:
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,88 @@
|
||||
# LED-System (WS2812)
|
||||
|
||||
**Dateien:** `hal/ws2812.h`, `hal/ws2812.cpp`, `CButton.h`, `CButton.cpp`
|
||||
|
||||
## Hardware-Treiber (ws2812 HAL)
|
||||
|
||||
Dünner Wrapper um **Adafruit NeoPixel** (bit-bang, kein DMA, kein SERCOM).
|
||||
|
||||
| Funktion | Bedeutung |
|
||||
|---|---|
|
||||
| `ws2812_init()` | `begin()` + `clear()` + `show()` |
|
||||
| `ws2812_set(idx, r, g, b)` | `setPixelColor()` – schreibt in RAM-Puffer |
|
||||
| `ws2812_fill(r, g, b)` | Alle LEDs gleiche Farbe |
|
||||
| `ws2812_show()` | Bit-Bang-Übertragung (~600 µs, Interrupts gesperrt) |
|
||||
| `ws2812_clear()` | `clear()` + `show()` |
|
||||
|
||||
`ws2812_show()` wird in `CMainController::updateLEDs()` **nur** aufgerufen wenn mindestens ein Button dirty war – 600 µs Blockzeit werden so vermieden wenn keine Änderung nötig ist.
|
||||
|
||||
**Warum kein DMA?** DMA + SERCOM-SPI würde ~1,5 KB extra RAM (1440 Byte Kodier-Puffer) und erhebliche Implementierungskomplexität erfordern. Bei 20 LEDs und ~20 ms Loop-Rate sind 600 µs gesperrte Interrupts (= 3 % der Loop-Zeit) unkritisch.
|
||||
|
||||
## 2-Schicht-Modell (CButton)
|
||||
|
||||
Jeder MX-Button hat zwei LED-Schichten:
|
||||
|
||||
```
|
||||
override (aktiv wenn set_override() aufgerufen) ← temporär (GUI-Benachrichtigungen)
|
||||
base (Idle-Farbe aus NVM) ← dauerhaft
|
||||
```
|
||||
|
||||
Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sofort zu `base` zurück, ohne base zu verändern.
|
||||
|
||||
## Animationen
|
||||
|
||||
| Animation | Typ | Verhalten | Endbedingung |
|
||||
|---|---|---|---|
|
||||
| `STATIC` | — | Feste Farbe | — |
|
||||
| `BLINK` | Helligkeit | An/Aus, `period_ms` = Halbperiode | endlos |
|
||||
| `PULSE` | Helligkeit | Lineares Dreieck 0→255→0 | endlos |
|
||||
| `FADE_IN` | Helligkeit | Einmalig schwarz → voll | → STATIC (voll) |
|
||||
| `FADE_OUT` | Helligkeit | Einmalig voll → schwarz | → STATIC (base=schwarz) |
|
||||
| `COLOR_CYCLE` | Farbe | Hue-Sweep, ignoriert base/override | endlos |
|
||||
| `COLOR_FADE` | Farbe | Crossfade from→to | → STATIC (base=to) |
|
||||
|
||||
**Helligkeits-Animationen** (`compute_scale`): Multiplizieren die aktive Farbe mit einem Skalierungsfaktor 0–255. Formel: `(channel * scale) / 255`.
|
||||
|
||||
**Farb-Animationen** (`compute_rgb`): Berechnen RGB direkt; base/override werden nicht verändert (außer bei Abschluss).
|
||||
|
||||
### COLOR_CYCLE – Hue-Arithmetik (kein Float)
|
||||
|
||||
Hue 0–255 aufgeteilt in 6 Segmente à 43 Einheiten. Innerhalb jedes Segments steigt/fällt ein Kanal linear:
|
||||
|
||||
```
|
||||
Seg 0: R=255, G steigt (Rot → Gelb)
|
||||
Seg 1: R fällt, G=255 (Gelb → Grün)
|
||||
Seg 2: G=255, B steigt (Grün → Cyan)
|
||||
Seg 3: G fällt, B=255 (Cyan → Blau)
|
||||
Seg 4: B=255, R steigt (Blau → Magenta)
|
||||
Seg 5: R=255, B fällt (Magenta → Rot)
|
||||
```
|
||||
|
||||
Ausgabe wird auf 40 % Helligkeit skaliert (Faktor 102/255) damit die LEDs nicht blenden.
|
||||
|
||||
`Adafruit_NeoPixel::ColorHSV()` ist nicht nutzbar: verwendet intern float (kein FPU auf M0+).
|
||||
|
||||
### Phasenversatz (Regenbogen-Wellen)
|
||||
|
||||
`set_anim(COLOR_CYCLE, period, phase_offset_ms)`: `m_anim_start_ms = millis() - phase_offset_ms`. Die 20 MX-Buttons werden in `init_buttons()` gleichmäßig phasenverschoben initialisiert:
|
||||
|
||||
```cpp
|
||||
uint16_t phase = (mx_idx * period) / 20;
|
||||
```
|
||||
|
||||
## Render-Pipeline
|
||||
|
||||
```
|
||||
CMainController::updateLEDs()
|
||||
für jeden CButton:
|
||||
CButton::render_led()
|
||||
if !dirty && !is_animating(): return false
|
||||
rgb = compute_rgb()
|
||||
ws2812_set(led_index, rgb.r, rgb.g, rgb.b)
|
||||
dirty = false
|
||||
return true
|
||||
if any returned true:
|
||||
ws2812_show()
|
||||
```
|
||||
|
||||
`dirty` wird gesetzt bei: `init()`, `set_base()`, `set_override()`, `clear_override()`, `set_anim()`, `clear_anim()`, Animations-Abschluss.
|
||||
@@ -0,0 +1,128 @@
|
||||
# NVM-Konfiguration
|
||||
|
||||
Dateien:
|
||||
|
||||
- `config/nvm_config.h`
|
||||
- `config/nvm_config.cpp`
|
||||
|
||||
## Flash-Layout
|
||||
|
||||
| Bereich | Adresse | Groesse | Inhalt |
|
||||
|---|---|---|---|
|
||||
| Macro Row 0 | `0x1FB00` | 256 B | `SMacroTable` Bytes `0..255` |
|
||||
| Macro Row 1 | `0x1FC00` | 256 B | `SMacroTable` Bytes `256..511` |
|
||||
| Config Row 0 | `0x1FD00` | 256 B | 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 |
|
||||
|
||||
Makros und Config sind komplett getrennt.
|
||||
|
||||
## `SDeviceConfig`
|
||||
|
||||
Aktueller Stand:
|
||||
|
||||
- Magic: `0x56503203`
|
||||
- Version: `3`
|
||||
- Groesse: `740` Byte
|
||||
- auf 3 Config-Rows verteilt
|
||||
|
||||
### Header
|
||||
|
||||
| Offset | Groesse | Feld |
|
||||
|---|---|---|
|
||||
| `0` | 4 | `magic` |
|
||||
| `4` | 1 | `version` |
|
||||
| `5` | 2 | `crc` |
|
||||
| `7` | 1 | `active_profile` |
|
||||
| `8` | 1 | `global_brightness` |
|
||||
| `9` | 4 | `enc_sensitivity[4]` |
|
||||
| `13` | 19 | Reserve |
|
||||
|
||||
### Pro Profil
|
||||
|
||||
Jedes Profil belegt 236 Byte:
|
||||
|
||||
| Offset im Profil | Groesse | Feld |
|
||||
|---|---|---|
|
||||
| `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]` |
|
||||
|
||||
Gesamtrechnung:
|
||||
|
||||
```text
|
||||
32 Byte Header + 3 * 236 Byte Profile = 740 Byte
|
||||
```
|
||||
|
||||
## CRC
|
||||
|
||||
CRC16-CCITT:
|
||||
|
||||
- Polynom `0x1021`
|
||||
- Init `0xFFFF`
|
||||
- Bereich: Bytes `7..739`
|
||||
|
||||
Damit sind auch `active_profile` und globale Helligkeit abgesichert.
|
||||
|
||||
## Defaults
|
||||
|
||||
`nvm_config_defaults()` setzt:
|
||||
|
||||
- `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`
|
||||
|
||||
Praktisch sichtbares Ergebnis:
|
||||
|
||||
- alle MX-LEDs laufen wieder im Regenbogenmodus
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Serial-Protokoll (CDC USB)
|
||||
|
||||
Dateien:
|
||||
|
||||
- `hal/usb_serial.h`
|
||||
- `hal/usb_serial.cpp`
|
||||
- `CMainController.cpp`
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
Es gibt kein Framing und keinen Laengenheader.
|
||||
|
||||
## Richtungen
|
||||
|
||||
| Richtung | IDs | Verarbeitung |
|
||||
|---|---|---|
|
||||
| PC -> Board | `0x01..0x7F` | `poll_vendor()` |
|
||||
| Board -> PC | `0x81..0xFF` | `usb_serial_send()` |
|
||||
|
||||
## Commands
|
||||
|
||||
| ID | Name | Zweck |
|
||||
|---|---|---|
|
||||
| `0x01` | `SET_LED_OVERRIDE` | temporaere LED-Override setzen |
|
||||
| `0x02` | `CLEAR_LED_OVERRIDE` | Override entfernen |
|
||||
| `0x03` | `SET_LED_BASE` | Base-Farbe im RAM setzen |
|
||||
| `0x05` | `PING` | Antwort: `PONG` |
|
||||
| `0x10` | `CONFIG_BEGIN` | Config-Transfer starten |
|
||||
| `0x11` | `CONFIG_DATA` | 6 Byte Config-Nutzdaten |
|
||||
| `0x12` | `CONFIG_COMMIT` | Config pruefen und speichern |
|
||||
| `0x13` | `CONFIG_READ` | Config-Dump an Host senden |
|
||||
| `0x20` | `MACRO_BEGIN` | Makro-Transfer starten |
|
||||
| `0x21` | `MACRO_DATA` | 6 Byte Makro-Nutzdaten |
|
||||
| `0x22` | `MACRO_COMMIT` | Makros speichern |
|
||||
| `0x23` | `MACRO_READ` | Makro-Dump an Host senden |
|
||||
|
||||
## Events
|
||||
|
||||
| ID | Name | Zweck |
|
||||
|---|---|---|
|
||||
| `0x81` | `KEY_DOWN` | Host-Command-Button gedrueckt |
|
||||
| `0x82` | `KEY_UP` | Host-Command-Button losgelassen |
|
||||
| `0x83` | `ENC_CW` | Encoder Host-Command im Uhrzeigersinn |
|
||||
| `0x84` | `ENC_CCW` | Encoder Host-Command gegen Uhrzeigersinn |
|
||||
| `0x85` | `PONG` | Antwort auf Ping |
|
||||
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
|
||||
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
|
||||
| `0x92` | `CONFIG_BEGIN` | Config-Dump beginnt |
|
||||
| `0x93` | `CONFIG_DATA` | 6 Byte Config-Dump |
|
||||
| `0x94` | `CONFIG_END` | Config-Dump fertig |
|
||||
| `0x95` | `MACRO_ACK` | Makros erfolgreich gespeichert |
|
||||
| `0x96` | `MACRO_BEGIN` | Makro-Dump beginnt |
|
||||
| `0x97` | `MACRO_DATA` | 6 Byte Makro-Dump |
|
||||
| `0x98` | `MACRO_END` | Makro-Dump fertig |
|
||||
| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen |
|
||||
|
||||
## Chunk-Zahlen
|
||||
|
||||
Aktuelle Blob-Groessen:
|
||||
|
||||
- Config: `740` Byte
|
||||
- Makros: `512` Byte
|
||||
|
||||
Bei 6 Nutzbytes pro Paket ergibt das:
|
||||
|
||||
```text
|
||||
Config: ceil(740 / 6) = 124 Chunks
|
||||
Makros: ceil(512 / 6) = 86 Chunks
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Transferablauf
|
||||
|
||||
### PC -> Board
|
||||
|
||||
```text
|
||||
BEGIN(chunk_count)
|
||||
DATA 0
|
||||
DATA 1
|
||||
...
|
||||
COMMIT
|
||||
```
|
||||
|
||||
### Board -> PC
|
||||
|
||||
```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
|
||||
|
||||
- RX-Ringbuffer: 256 Byte = 32 volle Pakete
|
||||
- feste 8-Byte-Pakete vereinfachen Firmware und GUI
|
||||
- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein
|
||||
@@ -0,0 +1,22 @@
|
||||
# VersaMCU - Dokumentationsindex
|
||||
|
||||
Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profilen und 32x8 Makros.
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| [00_architecture.md](00_architecture.md) | Setup, Work-Loop, Datenfluss, Key-IDs, globale Invarianten |
|
||||
| [01_matrix.md](01_matrix.md) | 5x5-Matrixscan, Debounce, Key-ID-Berechnung |
|
||||
| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, ISR-Pfad, Event-Erzeugung |
|
||||
| [03_action_engine.md](03_action_engine.md) | `SAction`, `ActionType`, Hold/Tap-Verhalten, Profilwechsel, Reset-Sonderfall |
|
||||
| [04_macro_system.md](04_macro_system.md) | `SMacroTable`, 32 Slots, 8 Steps, NVM-Layout, Ausfuehrung |
|
||||
| [05_led_system.md](05_led_system.md) | LED-Schichten, Animationen, Render-Pipeline |
|
||||
| [06_nvm_config.md](06_nvm_config.md) | Config v3, 3 Profile, CRC16, Defaults, Werksreset-Bezug |
|
||||
| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK |
|
||||
|
||||
## Schnellreferenz
|
||||
|
||||
- aktuelle Config-Groesse: [06_nvm_config.md](06_nvm_config.md)
|
||||
- aktuelle Makro-Groesse: [04_macro_system.md](04_macro_system.md)
|
||||
- Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md)
|
||||
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
|
||||
- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)
|
||||
+7
-19
@@ -64,25 +64,13 @@ CButton::CButton()
|
||||
|
||||
void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base)
|
||||
{
|
||||
m_key_id = key_id;
|
||||
m_led_index = led_index;
|
||||
m_action = action;
|
||||
m_base = base;
|
||||
m_override_active = false;
|
||||
m_anim = LEDAnim::STATIC;
|
||||
m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben
|
||||
}
|
||||
|
||||
// ── Tastendruck-Hooks ─────────────────────────────────────────────────────────
|
||||
|
||||
void CButton::on_press()
|
||||
{
|
||||
// Reserviert für zukünftige Button-Logik (Hold, Toggle, Doppelklick …)
|
||||
}
|
||||
|
||||
void CButton::on_release()
|
||||
{
|
||||
// Reserviert für zukünftige Button-Logik
|
||||
m_key_id = key_id;
|
||||
m_led_index = led_index;
|
||||
m_action = action;
|
||||
m_base = base;
|
||||
m_override_active = false;
|
||||
m_anim = LEDAnim::STATIC;
|
||||
m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben
|
||||
}
|
||||
|
||||
// ── LED Layer 1: base ─────────────────────────────────────────────────────────
|
||||
|
||||
+2
-7
@@ -61,13 +61,8 @@ public:
|
||||
CButton();
|
||||
|
||||
// Initialisierung (ersetzt Konstruktor-Parameter).
|
||||
// led_index = -1 → kein LED (Encoder-SW-Buttons).
|
||||
void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB());
|
||||
|
||||
// Hooks für Tastendruck/-loslassen.
|
||||
// Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …).
|
||||
void on_press();
|
||||
void on_release();
|
||||
void init(uint8_t key_id, int8_t led_index, SAction action,
|
||||
RGB base = RGB());
|
||||
|
||||
// ── LED Layer 1: base ─────────────────────────────────────────────────────
|
||||
// Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt.
|
||||
|
||||
+266
-58
@@ -33,6 +33,10 @@
|
||||
#include "config/nvm_config.h"
|
||||
#include "config/macro_config.h"
|
||||
|
||||
static constexpr uint8_t FACTORY_RESET_LEFT_KEY = 9;
|
||||
static constexpr uint8_t FACTORY_RESET_RIGHT_KEY = 24;
|
||||
static constexpr uint32_t FACTORY_RESET_HOLD_MS = 5000;
|
||||
|
||||
// ─── Static Bridge: HAL-Callbacks → EventQueue ───────────────────────────────
|
||||
//
|
||||
// matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein
|
||||
@@ -72,6 +76,11 @@ CMainController::CMainController()
|
||||
, m_cfg_receiving(false)
|
||||
, m_macro_chunks_expected(0)
|
||||
, m_macro_receiving(false)
|
||||
, m_factory_left_held(false)
|
||||
, m_factory_right_held(false)
|
||||
, m_factory_reset_armed(false)
|
||||
, m_factory_reset_done(false)
|
||||
, m_factory_hold_started_ms(0)
|
||||
{
|
||||
memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
|
||||
memset(m_macro_buf, 0, sizeof(m_macro_buf));
|
||||
@@ -101,13 +110,16 @@ void CMainController::init_buttons()
|
||||
bool valid = nvm_config_load(cfg);
|
||||
(void)valid; // false = keine gültige Config → Defaults wurden bereits geladen
|
||||
|
||||
// Aktives Profil auswählen (load() sichert bereits 0–2 ab)
|
||||
const SDeviceProfile& prof = cfg.profiles[cfg.active_profile];
|
||||
|
||||
// Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1)
|
||||
for (uint8_t enc = 0; enc < 4; enc++) {
|
||||
m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW]);
|
||||
m_buttons[enc].init(enc, -1, prof.enc_actions[enc][ENC_ACTION_SW], RGB());
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
|
||||
@@ -115,11 +127,21 @@ void CMainController::init_buttons()
|
||||
uint8_t row = key % MATRIX_ROWS;
|
||||
int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
|
||||
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]);
|
||||
uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.led_period_ms[mx_idx] : 4000;
|
||||
// Effektive Farbe = base × led_brightness × global_brightness / 255²
|
||||
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) {
|
||||
// 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
|
||||
// da sie keine LED haben und kein Matrix-Key sind.
|
||||
for (uint8_t enc = 0; enc < 4; enc++) {
|
||||
m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW];
|
||||
m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW];
|
||||
m_enc_cw [enc] = prof.enc_actions[enc][ENC_ACTION_CW];
|
||||
m_enc_ccw[enc] = prof.enc_actions[enc][ENC_ACTION_CCW];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,10 +164,11 @@ void CMainController::init_buttons()
|
||||
|
||||
void CMainController::work()
|
||||
{
|
||||
matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue
|
||||
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
|
||||
processEvents(); // 3. Queue leeren: Aktionen ausführen, Buttons benachrichtigen
|
||||
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
|
||||
matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue
|
||||
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
|
||||
processEvents(); // 3. Queue leeren, Aktionen ausführen
|
||||
check_factory_reset();// 4. Long-Press-Kombination für Werksreset prüfen
|
||||
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
|
||||
}
|
||||
|
||||
// ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
|
||||
@@ -198,8 +221,8 @@ void CMainController::poll_vendor()
|
||||
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
|
||||
uint16_t offset = (uint16_t)pkt.key_id() * 6;
|
||||
if (offset < sizeof(m_cfg_buf)) {
|
||||
uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset);
|
||||
if (count > 6) count = 6;
|
||||
uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset);
|
||||
uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining);
|
||||
memcpy(m_cfg_buf + offset, &pkt.data[2], count);
|
||||
}
|
||||
}
|
||||
@@ -211,10 +234,10 @@ void CMainController::poll_vendor()
|
||||
{
|
||||
SDeviceConfig cfg;
|
||||
nvm_config_load(cfg); // ungültige NVM → Defaults
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
|
||||
const uint8_t sz = sizeof(SDeviceConfig); // 163
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
|
||||
const uint16_t sz = sizeof(SDeviceConfig); // 740
|
||||
const uint8_t payload = 6;
|
||||
uint8_t chunks = (sz + payload - 1) / payload; // 28
|
||||
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124
|
||||
|
||||
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
|
||||
|
||||
@@ -222,7 +245,7 @@ void CMainController::poll_vendor()
|
||||
uint8_t p[SERIAL_PKT_SIZE] = {};
|
||||
p[0] = USB_EVT_CONFIG_DATA;
|
||||
p[1] = i;
|
||||
uint8_t offset = i * payload;
|
||||
uint16_t offset = (uint16_t)i * payload;
|
||||
for (uint8_t b = 0; b < payload; 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.crc == nvm_config_crc(cfg))
|
||||
{
|
||||
nvm_config_save(cfg);
|
||||
init_buttons();
|
||||
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
|
||||
if (nvm_config_save(cfg)) {
|
||||
init_buttons();
|
||||
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
|
||||
} else {
|
||||
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden
|
||||
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -262,10 +288,10 @@ void CMainController::poll_vendor()
|
||||
|
||||
case USB_CMD_MACRO_DATA:
|
||||
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)) {
|
||||
uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset);
|
||||
if (count > 6) count = 6;
|
||||
uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset);
|
||||
uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining);
|
||||
memcpy(m_macro_buf + offset, &pkt.data[2], count);
|
||||
}
|
||||
}
|
||||
@@ -275,18 +301,21 @@ void CMainController::poll_vendor()
|
||||
if (m_macro_receiving) {
|
||||
m_macro_receiving = false;
|
||||
memcpy(&m_macros, m_macro_buf, sizeof(m_macros));
|
||||
macro_config_save(m_macros);
|
||||
usb_serial_send(USB_EVT_MACRO_ACK, 0);
|
||||
if (macro_config_save(m_macros)) {
|
||||
usb_serial_send(USB_EVT_MACRO_ACK, 0);
|
||||
} else {
|
||||
usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// ── Makro-Dump anfordern ─────────────────────────────────────────
|
||||
case USB_CMD_MACRO_READ:
|
||||
{
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
|
||||
const uint16_t sz = sizeof(SMacroTable); // 256
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
|
||||
const uint16_t sz = sizeof(SMacroTable); // 512
|
||||
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);
|
||||
|
||||
@@ -316,43 +345,49 @@ void CMainController::poll_vendor()
|
||||
// Verarbeitet alle Events in der Queue bis sie leer ist.
|
||||
// Reihenfolge: ältestes Event zuerst (FIFO).
|
||||
//
|
||||
// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App
|
||||
// gemeldet – die App entscheidet dann was passiert (URL öffnen, Programm starten…).
|
||||
// KEY_DOWN: execute_action_down() – HID-Taste wird gedrückt, bleibt aktiv bis KEY_UP.
|
||||
// KEY_UP: execute_action_up() – HID-Taste wird losgelassen.
|
||||
// Encoder CW/CCW: execute_action_down() + execute_action_up() für atomare TAP-Sequenz.
|
||||
|
||||
void CMainController::processEvents()
|
||||
{
|
||||
SEvent ev;
|
||||
|
||||
while (m_queue.pop(ev)) {
|
||||
switch (ev.type) {
|
||||
|
||||
case EventType::KEY_DOWN:
|
||||
if (ev.key_id < MATRIX_KEYS) {
|
||||
m_buttons[ev.key_id].on_press();
|
||||
execute_action(m_buttons[ev.key_id].action());
|
||||
// Bei HOST_COMMAND: Event-ID an Windows-App senden
|
||||
if (m_buttons[ev.key_id].action().type == ActionType::HOST_COMMAND)
|
||||
usb_serial_send(USB_EVT_KEY_DOWN, 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;
|
||||
|
||||
case EventType::KEY_UP:
|
||||
if (ev.key_id < MATRIX_KEYS)
|
||||
m_buttons[ev.key_id].on_release();
|
||||
if (ev.key_id < MATRIX_KEYS) {
|
||||
bool suppress = is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id);
|
||||
update_factory_reset_hold(ev.key_id, false);
|
||||
if (!suppress) {
|
||||
execute_action_up(m_buttons[ev.key_id].action(), ev.key_id);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType::ENC_CW:
|
||||
if (ev.key_id < 4) {
|
||||
execute_action(m_enc_cw[ev.key_id]);
|
||||
if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND)
|
||||
usb_serial_send(USB_EVT_ENC_CW, ev.key_id);
|
||||
execute_action_down(m_enc_cw[ev.key_id], ev.key_id);
|
||||
delay(10);
|
||||
execute_action_up(m_enc_cw[ev.key_id], ev.key_id);
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType::ENC_CCW:
|
||||
if (ev.key_id < 4) {
|
||||
execute_action(m_enc_ccw[ev.key_id]);
|
||||
if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND)
|
||||
usb_serial_send(USB_EVT_ENC_CCW, ev.key_id);
|
||||
execute_action_down(m_enc_ccw[ev.key_id], ev.key_id);
|
||||
delay(10);
|
||||
execute_action_up(m_enc_ccw[ev.key_id], ev.key_id);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -362,35 +397,167 @@ void CMainController::processEvents()
|
||||
}
|
||||
}
|
||||
|
||||
// Führt eine einzelne Aktion aus.
|
||||
// HID_KEY / HID_CONSUMER: direkt über USB HID gesendet (funktioniert ohne Windows-App).
|
||||
// HOST_COMMAND: kein direkter Aufruf hier – das Event wird in processEvents() via
|
||||
// usb_serial_send() an die Windows-App weitergeleitet.
|
||||
void CMainController::execute_action(SAction action)
|
||||
bool CMainController::is_factory_reset_key(uint8_t key_id) const
|
||||
{
|
||||
return key_id == FACTORY_RESET_LEFT_KEY || key_id == FACTORY_RESET_RIGHT_KEY;
|
||||
}
|
||||
|
||||
bool CMainController::is_factory_reset_combo_active() const
|
||||
{
|
||||
return m_factory_left_held && m_factory_right_held;
|
||||
}
|
||||
|
||||
void CMainController::update_factory_reset_led_feedback()
|
||||
{
|
||||
// Einzelne Reset-Taste gehalten: diese Taste rot hervorheben.
|
||||
// Beide gehalten: beide Tasten rot hervorheben.
|
||||
if (m_factory_left_held) {
|
||||
m_buttons[FACTORY_RESET_LEFT_KEY].set_override(RGB(96, 0, 0));
|
||||
} else {
|
||||
m_buttons[FACTORY_RESET_LEFT_KEY].clear_override();
|
||||
}
|
||||
|
||||
if (m_factory_right_held) {
|
||||
m_buttons[FACTORY_RESET_RIGHT_KEY].set_override(RGB(96, 0, 0));
|
||||
} else {
|
||||
m_buttons[FACTORY_RESET_RIGHT_KEY].clear_override();
|
||||
}
|
||||
}
|
||||
|
||||
void CMainController::update_factory_reset_hold(uint8_t key_id, bool pressed)
|
||||
{
|
||||
if (key_id == FACTORY_RESET_LEFT_KEY) {
|
||||
m_factory_left_held = pressed;
|
||||
} else if (key_id == FACTORY_RESET_RIGHT_KEY) {
|
||||
m_factory_right_held = pressed;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
update_factory_reset_led_feedback();
|
||||
|
||||
if (m_factory_left_held && m_factory_right_held) {
|
||||
if (!m_factory_reset_armed) {
|
||||
// Sobald beide Reset-Tasten gleichzeitig gehalten werden, sollen sie
|
||||
// keine normale Aktion mehr auf dem Host auslösen. Falls die zuerst
|
||||
// gedrückte Taste bereits ein HID-/Consumer-Hold gestartet hat,
|
||||
// geben wir sie hier sofort wieder frei.
|
||||
execute_action_up(m_buttons[FACTORY_RESET_LEFT_KEY].action(), FACTORY_RESET_LEFT_KEY);
|
||||
execute_action_up(m_buttons[FACTORY_RESET_RIGHT_KEY].action(), FACTORY_RESET_RIGHT_KEY);
|
||||
m_factory_reset_armed = true;
|
||||
m_factory_reset_done = false;
|
||||
m_factory_hold_started_ms = millis();
|
||||
}
|
||||
} else {
|
||||
m_factory_reset_armed = false;
|
||||
m_factory_reset_done = false;
|
||||
m_factory_hold_started_ms = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CMainController::check_factory_reset()
|
||||
{
|
||||
if (!m_factory_reset_armed || m_factory_reset_done) return;
|
||||
if (!(m_factory_left_held && m_factory_right_held)) return;
|
||||
|
||||
if ((millis() - m_factory_hold_started_ms) >= FACTORY_RESET_HOLD_MS) {
|
||||
m_factory_reset_done = true;
|
||||
perform_factory_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void CMainController::perform_factory_reset()
|
||||
{
|
||||
SDeviceConfig cfg;
|
||||
SMacroTable macros;
|
||||
bool cfg_ok;
|
||||
bool macro_ok;
|
||||
|
||||
nvm_config_defaults(cfg);
|
||||
memset(¯os, 0, sizeof(macros));
|
||||
|
||||
cfg_ok = nvm_config_save(cfg);
|
||||
macro_ok = macro_config_save(macros);
|
||||
|
||||
// Laufzeit-Zustand immer an die Defaults angleichen – selbst wenn NVM gerade
|
||||
// nicht geschrieben werden konnte, sieht das Gerät sofort wieder "frisch" aus.
|
||||
m_macros = macros;
|
||||
usb_hid_release_key();
|
||||
usb_hid_release_consumer();
|
||||
init_buttons();
|
||||
show_factory_reset_feedback();
|
||||
|
||||
// Während die beiden Tasten weiter gehalten werden, keine erneuten Resets.
|
||||
// Neue Arming-Phase erst nach vollständigem Loslassen beider Tasten.
|
||||
if (!(cfg_ok && macro_ok)) {
|
||||
// Keine Host-Meldung vorgesehen – das Gerät bleibt aber betriebsfähig
|
||||
// und kann über die GUI erneut konfiguriert werden.
|
||||
}
|
||||
}
|
||||
|
||||
void CMainController::show_factory_reset_feedback()
|
||||
{
|
||||
// Kurze rote Bestätigung ähnlich der Startsequenz, aber kompakter.
|
||||
ws2812_fill(100, 0, 0);
|
||||
ws2812_show();
|
||||
delay(180);
|
||||
ws2812_clear();
|
||||
delay(90);
|
||||
ws2812_fill(100, 0, 0);
|
||||
ws2812_show();
|
||||
delay(180);
|
||||
ws2812_clear();
|
||||
delay(60);
|
||||
|
||||
// Danach sofort die frisch geladenen Default-Animationen wieder anzeigen.
|
||||
updateLEDs();
|
||||
}
|
||||
|
||||
// ─── Aktions-Ausführung ───────────────────────────────────────────────────────
|
||||
//
|
||||
// execute_action_down(): Taste wird gedrückt (Hold-Start).
|
||||
// HID_KEY: sendet Key-Down, bleibt aktiv.
|
||||
// HID_CONSUMER: sendet Consumer-Down, bleibt aktiv.
|
||||
// HOST_COMMAND: sendet KEY_DOWN-Event an Windows-App.
|
||||
// MACRO: führt volle Sequenz aus (Key-Down/Up jeweils mit Pause).
|
||||
// NONE: keine Aktion.
|
||||
//
|
||||
// execute_action_up(): Taste wird losgelassen (Hold-Ende).
|
||||
// HID_KEY: sendet Key-Up.
|
||||
// HID_CONSUMER: sendet Consumer-Up.
|
||||
// HOST_COMMAND: kann USB_EVT_KEY_UP senden.
|
||||
// MACRO/NONE: keine Aktion.
|
||||
|
||||
void CMainController::execute_action_down(SAction action, uint8_t key_id)
|
||||
{
|
||||
switch (action.type) {
|
||||
|
||||
case ActionType::HID_KEY:
|
||||
{
|
||||
// data-Encoding: Low-Byte = Keycode, High-Byte = Modifier
|
||||
usb_hid_send_key(static_cast<uint8_t>(action.data & 0xFF),
|
||||
static_cast<uint8_t>(action.data >> 8));
|
||||
delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up
|
||||
usb_hid_release_key();
|
||||
uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
|
||||
uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
|
||||
usb_hid_send_key(keycode, modifier);
|
||||
// Taste bleibt gedrückt bis execute_action_up() aufgerufen wird
|
||||
break;
|
||||
}
|
||||
|
||||
case ActionType::HID_CONSUMER:
|
||||
{
|
||||
usb_hid_send_consumer(action.data);
|
||||
usb_hid_release_consumer();
|
||||
// Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird
|
||||
break;
|
||||
}
|
||||
|
||||
case ActionType::HOST_COMMAND:
|
||||
// Wird in processEvents() über Serial gesendet
|
||||
// Windows-App übernimmt Ausführung; KEY_DOWN-Event senden
|
||||
usb_serial_send(USB_EVT_KEY_DOWN, key_id);
|
||||
break;
|
||||
|
||||
case ActionType::MACRO:
|
||||
{
|
||||
// Makro-Slot aus dem RAM ausführen (bei setup() aus NVM geladen).
|
||||
// Steps mit keycode=0 werden übersprungen; erstes leeres Step stoppt.
|
||||
// Makros sind Sequenzen – Steps mit keycode=0 werden übersprungen;
|
||||
// erstes leeres Step stoppt.
|
||||
uint8_t slot = static_cast<uint8_t>(action.data);
|
||||
if (slot >= MACRO_SLOTS) break;
|
||||
for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) {
|
||||
@@ -404,12 +571,53 @@ void CMainController::execute_action(SAction action)
|
||||
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:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CMainController::execute_action_up(SAction action, uint8_t key_id)
|
||||
{
|
||||
switch (action.type) {
|
||||
|
||||
case ActionType::HID_KEY:
|
||||
usb_hid_release_key();
|
||||
break;
|
||||
|
||||
case ActionType::HID_CONSUMER:
|
||||
usb_hid_release_consumer();
|
||||
break;
|
||||
|
||||
case ActionType::HOST_COMMAND:
|
||||
// Optional: USB_EVT_KEY_UP senden (aktuell nicht implementiert)
|
||||
break;
|
||||
|
||||
case ActionType::MACRO:
|
||||
case ActionType::NONE:
|
||||
default:
|
||||
// MACRO: Sequenz ist in execute_action_down() komplett abgelaufen, nop hier
|
||||
// NONE: keine Aktion
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── LED-Rendering ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt
|
||||
|
||||
+26
-7
@@ -12,6 +12,7 @@
|
||||
#include "hal/usb_hid.h"
|
||||
#include "hal/usb_serial.h"
|
||||
#include "config/action.h"
|
||||
#include "config/nvm_config.h"
|
||||
#include "config/macro_config.h"
|
||||
|
||||
class CMainController
|
||||
@@ -35,22 +36,40 @@ private:
|
||||
SAction m_enc_cw[4];
|
||||
SAction m_enc_ccw[4];
|
||||
|
||||
void init_buttons(); // Buttons aus NVM-Config initialisieren
|
||||
void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten
|
||||
void processEvents(); // Queue leeren, Aktionen ausführen
|
||||
void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial)
|
||||
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
|
||||
void init_buttons(); // Buttons aus NVM-Config initialisieren
|
||||
void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten
|
||||
void processEvents(); // Queue leeren, Aktionen ausführen
|
||||
void execute_action_down(SAction action, uint8_t key_id); // Taste drücken (Hold-Start)
|
||||
void execute_action_up(SAction action, uint8_t key_id); // Taste losgelassen (Hold-Ende)
|
||||
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
|
||||
|
||||
// ── 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;
|
||||
bool m_cfg_receiving;
|
||||
|
||||
// ── 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;
|
||||
bool m_macro_receiving;
|
||||
|
||||
// Geladene Makro-Tabelle (im RAM – wird beim Start aus NVM geladen)
|
||||
SMacroTable m_macros;
|
||||
|
||||
// Werksreset per Long-Press-Kombination:
|
||||
// key_id 9 = unterster linker MX-Button (COL_1 / ROW_4)
|
||||
// key_id 24 = unterster rechter MX-Button (COL_4 / ROW_4)
|
||||
bool m_factory_left_held;
|
||||
bool m_factory_right_held;
|
||||
bool m_factory_reset_armed;
|
||||
bool m_factory_reset_done;
|
||||
uint32_t m_factory_hold_started_ms;
|
||||
|
||||
bool is_factory_reset_key(uint8_t key_id) const;
|
||||
bool is_factory_reset_combo_active() const;
|
||||
void update_factory_reset_led_feedback();
|
||||
void update_factory_reset_hold(uint8_t key_id, bool pressed);
|
||||
void check_factory_reset();
|
||||
void perform_factory_reset();
|
||||
void show_factory_reset_feedback();
|
||||
};
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ enum class ActionType : uint8_t
|
||||
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
|
||||
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
|
||||
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
|
||||
MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 4 HID-Keys sequenziell
|
||||
MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 8 HID-Keys sequenziell
|
||||
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 0–2); speichert in NVM
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) SAction
|
||||
|
||||
+32
-17
@@ -1,5 +1,5 @@
|
||||
// macro_config.cpp
|
||||
// NVM-Zugriff für die Makro-Tabelle (Row 1, 0x1FF00).
|
||||
// NVM-Zugriff für die Makro-Tabelle (Row 0+1, 0x1FB00–0x1FCFF, 512 Bytes).
|
||||
// Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert,
|
||||
// da static – kein gemeinsamer Header für interne NVM-Helfer).
|
||||
|
||||
@@ -7,37 +7,46 @@
|
||||
#include <Arduino.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 void nvm_exec(uint16_t cmd)
|
||||
static bool nvm_wait()
|
||||
{
|
||||
// ~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;
|
||||
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;
|
||||
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);
|
||||
const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
|
||||
for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i];
|
||||
NVMCTRL->ADDR.reg = addr / 2;
|
||||
nvm_exec(NVMCTRL_CTRLA_CMD_WP);
|
||||
return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
|
||||
}
|
||||
|
||||
bool macro_config_load(SMacroTable& 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);
|
||||
bool all_ff = true;
|
||||
for (uint16_t i = 0; i < sizeof(tbl); i++) {
|
||||
@@ -50,17 +59,23 @@ bool macro_config_load(SMacroTable& tbl)
|
||||
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.
|
||||
// SMacroTable ist __attribute__((packed)) und könnte unaligned liegen →
|
||||
// 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));
|
||||
|
||||
NVMCTRL->CTRLB.bit.MANW = 1;
|
||||
nvm_erase_row(k_macro_addr);
|
||||
for (uint8_t p = 0; p < 4; p++) {
|
||||
nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64);
|
||||
|
||||
// Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// macro_config.h
|
||||
// Makro-Tabelle: bis zu 32 Slots, je 4 HID-Key-Steps.
|
||||
// Gespeichert in NVM Row 1 (0x1FF00, 256 Bytes).
|
||||
// Makro-Tabelle: 32 Slots, je 8 HID-Key-Steps.
|
||||
// Gespeichert in NVM Row 0+1 (0x1FB00–0x1FCFF, 512 Bytes).
|
||||
//
|
||||
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
|
||||
// Slot 0–19 : MX-Buttons (mx_idx)
|
||||
@@ -13,7 +13,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#define MACRO_SLOTS 32
|
||||
#define MACRO_MAX_STEPS 4
|
||||
#define MACRO_MAX_STEPS 8
|
||||
|
||||
// Ein einzelner HID-Key-Step im Makro
|
||||
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)
|
||||
};
|
||||
|
||||
// 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
|
||||
{
|
||||
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.
|
||||
bool macro_config_load(SMacroTable& tbl);
|
||||
|
||||
// Makro-Tabelle in NVM schreiben (löscht Row 1, schreibt 4 Pages).
|
||||
void macro_config_save(const SMacroTable& tbl);
|
||||
// Makro-Tabelle in NVM schreiben (löscht Row 0+1, schreibt 8 Pages).
|
||||
// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird.
|
||||
bool macro_config_save(const SMacroTable& tbl);
|
||||
|
||||
+75
-50
@@ -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 <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
// ── Flash-Adresse (aus Linkerscript) ─────────────────────────────────────────
|
||||
// Kein separates Linker-Symbol nötig – Adresse ist fix und bekannt.
|
||||
static const uint32_t k_config_addr = 0x1FE00UL;
|
||||
static const uint32_t k_config_addr = 0x1FD00UL; // Row 0–2 der Config
|
||||
|
||||
// SAMD21 NVMCTRL ──────────────────────────────────────────────────────────────
|
||||
// Row = 256 Bytes = 4 Pages à 64 Bytes
|
||||
// Schreiben: Row löschen (ER), dann seitenweise schreiben (WP)
|
||||
// ── NVMCTRL-Hilfsfunktionen ───────────────────────────────────────────────────
|
||||
//
|
||||
// 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;
|
||||
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)
|
||||
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
|
||||
nvm_exec(NVMCTRL_CTRLA_CMD_PBC);
|
||||
|
||||
// 64 Bytes in den Page-Buffer schreiben (32-Bit-Zugriffe)
|
||||
if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false;
|
||||
volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
|
||||
const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
|
||||
for (uint8_t i = 0; i < 64 / 4; i++) {
|
||||
dst[i] = src[i];
|
||||
}
|
||||
|
||||
// Page programmieren
|
||||
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)
|
||||
{
|
||||
// CRC über alles nach dem crc-Feld (ab Byte 7)
|
||||
const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, mx_actions);
|
||||
uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions);
|
||||
// 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, active_profile);
|
||||
uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, active_profile);
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
crc ^= static_cast<uint16_t>(data[i]) << 8;
|
||||
@@ -62,36 +66,48 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
|
||||
}
|
||||
|
||||
// ── Defaults ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void nvm_config_defaults(SDeviceConfig& cfg)
|
||||
{
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.magic = NVM_CONFIG_MAGIC;
|
||||
cfg.version = NVM_CONFIG_VERSION;
|
||||
cfg.magic = NVM_CONFIG_MAGIC;
|
||||
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 a = 0; a < 3; a++)
|
||||
cfg.enc_actions[e][a] = {ActionType::NONE, 0};
|
||||
cfg.enc_sensitivity[e] = 1;
|
||||
|
||||
// Base-LEDs: warm-weiß
|
||||
for (uint8_t i = 0; i < 20; i++) {
|
||||
cfg.led_r[i] = 80;
|
||||
cfg.led_g[i] = 40;
|
||||
cfg.led_b[i] = 0;
|
||||
}
|
||||
for (uint8_t p = 0; p < 3; p++) {
|
||||
SDeviceProfile& prof = cfg.profiles[p];
|
||||
|
||||
// LED-Animationen: Regenbogen (COLOR_CYCLE=5) mit 4s Periode als Standard
|
||||
for (uint8_t i = 0; i < 20; i++) {
|
||||
cfg.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE
|
||||
cfg.led_period_ms[i] = 4000;
|
||||
// Alle Aktionen: NONE
|
||||
for (uint8_t i = 0; i < 20; i++)
|
||||
prof.mx_actions[i] = {ActionType::NONE, 0};
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Laden ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool nvm_config_load(SDeviceConfig& cfg)
|
||||
{
|
||||
memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg));
|
||||
@@ -100,23 +116,32 @@ bool nvm_config_load(SDeviceConfig& cfg)
|
||||
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; }
|
||||
|
||||
// Profil-Index absichern
|
||||
if (cfg.active_profile >= 3) cfg.active_profile = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 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
|
||||
uint8_t row[256];
|
||||
// Config (740B) in 768B-Puffer kopieren (3 Rows), Rest mit 0xFF füllen.
|
||||
// __attribute__((aligned(4))) ist zwingend: nvm_write_page castet zu uint32_t*.
|
||||
uint8_t row[768] __attribute__((aligned(4)));
|
||||
memset(row, 0xFF, sizeof(row));
|
||||
memcpy(row, &cfg, sizeof(cfg));
|
||||
|
||||
// Automatisches Schreiben deaktivieren (manueller Schreib-Modus)
|
||||
NVMCTRL->CTRLB.bit.MANW = 1;
|
||||
|
||||
// Row 0 der Config löschen und seitenweise schreiben (4 × 64B)
|
||||
nvm_erase_row(k_config_addr);
|
||||
for (uint8_t p = 0; p < 4; p++) {
|
||||
nvm_write_page(k_config_addr + p * 64, row + p * 64);
|
||||
// 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00)
|
||||
if (!nvm_erase_row(k_config_addr)) return false;
|
||||
if (!nvm_erase_row(k_config_addr + 256)) return false;
|
||||
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
@@ -2,61 +2,74 @@
|
||||
#include <stdint.h>
|
||||
#include "action.h"
|
||||
|
||||
// ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ────────────────────────────────
|
||||
// ── NVM-Config-Layout (768 Bytes, ab 0x1FD00) ────────────────────────────────
|
||||
//
|
||||
// Offset Size Inhalt
|
||||
// 0 4 Magic (0x56503202 = 'VP2\x02')
|
||||
// 4 1 Version
|
||||
// 5 2 CRC16 über Bytes 7–222
|
||||
// 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)
|
||||
// Row 0 (0x1FD00, 256B): Globaler Header (32B) + Profil 0 (236B) + Padding (−12B → überläuft in Row 1)
|
||||
// Row 1 (0x1FE00, 256B): Profil 0 Rest + Profil 1 (Teil)
|
||||
// Row 2 (0x1FF00, 256B): Profil 1 Rest + Profil 2 + Reserve (28B)
|
||||
//
|
||||
// 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 32–267 (236B)
|
||||
// Profil 1: Bytes 268–503 (236B)
|
||||
// Profil 2: Bytes 504–739 (236B)
|
||||
// Reserve: Bytes 740–767 (28B)
|
||||
//
|
||||
// Alle 3 Rows werden immer gemeinsam gelöscht und neu geschrieben.
|
||||
|
||||
#define NVM_CONFIG_MAGIC 0x56503202UL
|
||||
#define NVM_CONFIG_VERSION 2 // Version 2: led_anim + led_period_ms hinzugefügt
|
||||
#define NVM_CONFIG_MAGIC 0x56503203UL // 'VP2\x03' – Version 3
|
||||
#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
|
||||
#define ENC_ACTION_SW 0
|
||||
#define ENC_ACTION_CW 1
|
||||
#define ENC_ACTION_CCW 2
|
||||
|
||||
// ── Pro-Profil-Daten (236 Bytes) ─────────────────────────────────────────────
|
||||
|
||||
struct __attribute__((packed)) SDeviceProfile
|
||||
{
|
||||
SAction mx_actions[20]; // 60B – MX-Buttons 0–19
|
||||
SAction enc_actions[4][3]; // 36B – [Encoder 0–3][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 (0–255, 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
|
||||
{
|
||||
uint32_t magic;
|
||||
uint8_t version;
|
||||
uint16_t crc;
|
||||
// Globaler Header (32B)
|
||||
uint32_t magic; // 4B
|
||||
uint8_t version; // 1B
|
||||
uint16_t crc; // 2B – CRC16-CCITT über Bytes 7–739
|
||||
uint8_t active_profile; // 1B – aktives Profil (0–2)
|
||||
uint8_t global_brightness; // 1B – globale LED-Helligkeit (0–255)
|
||||
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
|
||||
SAction mx_actions[20]; // MX-Buttons 0–19 (key_id 5–24)
|
||||
SAction enc_actions[4][3]; // [Encoder 0–3][SW/CW/CCW]
|
||||
|
||||
// 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)
|
||||
// Profile (3 × 236B = 708B)
|
||||
SDeviceProfile profiles[3];
|
||||
// Gesamt: 32 + 708 = 740B
|
||||
};
|
||||
|
||||
// Standardwerte wenn keine gültige Config im NVM
|
||||
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);
|
||||
|
||||
// Config in NVM schreiben (löscht 2 Rows, schreibt neu).
|
||||
void nvm_config_save(const SDeviceConfig& cfg);
|
||||
// Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages).
|
||||
// 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 7–739, nach dem crc-Feld)
|
||||
uint16_t nvm_config_crc(const SDeviceConfig& cfg);
|
||||
|
||||
+9
-3
@@ -8,7 +8,9 @@
|
||||
// Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand
|
||||
// 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] = {
|
||||
// curr: 00 01 10 11
|
||||
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_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.
|
||||
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
|
||||
if (s_accum[enc] >= 4) {
|
||||
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) {
|
||||
s_accum[enc] = 0;
|
||||
if (s_cb) s_cb(enc, -1);
|
||||
if (s_cb) s_cb(enc, -1 * ENCODER_DIRECTION_SIGN);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#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_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_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B
|
||||
#define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen
|
||||
|
||||
Reference in New Issue
Block a user