Compare commits

...

9 Commits

35 changed files with 1880 additions and 1004 deletions
+103
View File
@@ -0,0 +1,103 @@
# Arbeitsanweisungen für Coding-Agents
Diese Datei gilt für das gesamte Repository. Sie ist zugleich der kompakte
Einstiegskontext für LLM-basierte Entwicklungswerkzeuge.
## Ziel und Plattform
VersaMCU ist die Firmware des VersaPad-v2-Makropads. Das aktive und unterstützte
PlatformIO-Ziel ist `env:versapad`:
- ATSAMD21G17D, Cortex-M0+, 48 MHz
- 128 KiB Flash, 16 KiB RAM
- Arduino-SAMD-Core über PlatformIO
- Upload per Atmel-ICE/CMSIS-DAP und OpenOCD, ohne Bootloader
- USB Composite Device: Keyboard-HID, Consumer-HID und CDC Serial
Das auskommentierte Bootloader-Ziel in `platformio.ini` ist kein verifiziertes
Produktionsziel.
## Vor dem Ändern lesen
In dieser Reihenfolge:
1. `README.md` für Scope, Build und Einstieg
2. `doc/INDEX.md` für die thematische Navigation
3. `doc/00_architecture.md` für Datenfluss und Laufzeitmodell
4. `doc/09_known_limitations.md` für bewusst noch nicht gelöste Risiken
5. die zum Task gehörende Fachdokumentation und anschließend den Quellcode
Bei Widersprüchen ist der kompilierte Code die Quelle für das aktuelle
Verhalten. Hardwarekonstanten stehen in `src/config/pins.h` und
`variants/versapad/`; binäre Formate stehen in den Structs unter `src/config/`
und in `src/hal/usb_serial.h`. Widersprüche zwischen Code und Dokumentation
müssen im selben Change behoben oder ausdrücklich als bekannte Einschränkung
festgehalten werden.
## Unverzichtbare Verträge
- `SAction`, `SDeviceProfile`, `SDeviceConfig`, `SMacroStep` und
`SMacroTable` sind persistente beziehungsweise hostseitige Binärverträge.
- Änderungen an Feldreihenfolge, Enum-Werten, Packing, Größen, Magic, Version,
CRC-Bereich oder Chunking benötigen gleichzeitig:
Firmware-Migration/Versionswechsel, Anpassung der externen GUI und
Aktualisierung von `doc/06_nvm_config.md` sowie
`doc/07_serial_protocol.md`.
- Die Windows-GUI liegt im benachbarten, eigenständigen Repository
`../VersaGUI`. Bei gemeinsamen Verträgen beide Repositories ändern, prüfen
und jeweils bedarfsgerecht committen. `DelphiGUI` wird nicht gepflegt.
- Die NVM-Adressen liegen am oberen Ende des 128-KiB-Flash. Vor Änderungen an
Linker-Skripten, Boardgrößen oder NVM-Layouts immer
`doc/09_known_limitations.md` lesen.
- Der Code läuft auf 16 KiB RAM. Keine unnötige dynamische Allokation, keine
großen Stackpuffer und keine Float-Arithmetik in Loop-/ISR-Pfaden einführen.
- ISR-Code muss kurz und nicht blockierend bleiben. Niemals USB, NVM,
WS2812-Ausgabe oder `delay()` aus einer ISR aufrufen.
- Das feste CDC-Protokoll besteht aus 8-Byte-Paketen. Blob-Transfers prüfen
Chunkzahl, eindeutige Indizes und Vollständigkeit; paketweises Framing und
eine Makro-CRC gibt es weiterhin nicht.
## Änderungsleitfaden
- Kleine, lokale Änderungen bevorzugen; HAL, Controller und persistente Config
nicht ohne Grund vermischen.
- Neue Hardwarezugriffe gehören unter `src/hal/`.
- Neue konfigurierbare Werte benötigen definierte Defaults und eine
Validierungsstrategie für Daten vom Host.
- Neue Action- oder Eventtypen müssen in Firmware, Protokolldoku und externer
GUI gemeinsam geplant werden.
- Bei Matrix- oder Encoderänderungen die ISR-/Queue-Interaktion prüfen.
- Bei LED-Animationen nur ganzzahlige Arithmetik verwenden und Randwerte wie
`period_ms <= 1` behandeln.
- Bestehende deutsch- und englischsprachige Kommentare dürfen vereinheitlicht
werden; Dateien als UTF-8 speichern.
- Keine generierten Inhalte aus `.pio/` committen.
## Verifikation
Mindestens:
```bash
pio run -e versapad
git diff --check
```
Für Hardware-, USB-, NVM- oder Timingänderungen zusätzlich einen passenden
Gerätetest beschreiben. Für die Firmware gibt es derzeit keine automatisierten
Unit- oder Integrationstests. Ein erfolgreicher Build beweist daher weder
elektrische Funktion noch GUI-Kompatibilität.
Bei Änderungen an Config-, Makro- oder CDC-Verträgen zusätzlich:
```bash
dotnet build ../VersaGUI/src/VersaGUI.csproj --no-restore
dotnet run --project ../VersaGUI/tests/VersaGUI.ContractTests/VersaGUI.ContractTests.csproj
```
Vor Abschluss prüfen:
- Stimmen README und betroffene `doc/*.md` noch?
- Wurden Binärgrößen und Offsets neu berechnet statt übernommen?
- Bleiben Firmware und NVM-Bereiche kollisionsfrei?
- Ist klar getrennt, was verifiziert, nur aus Code abgeleitet oder noch offen
ist?
+133 -453
View File
@@ -1,479 +1,159 @@
# VersaMCU # VersaMCU
Firmware für das VersaPad v2 Macro-Pad. Firmware für das VersaPad-v2-Makropad. Das Projekt läuft auf einem
Läuft auf einem **ATSAMD21G17D** (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework. ATSAMD21G17D mit PlatformIO und dem Arduino-SAMD-Framework.
## Aktueller Funktionsumfang
| Bereich | Stand |
|---|---|
| Eingaben | 20 MX-Tasten, 4 Encoder-Taster und 4 Quadratur-Encoder |
| USB | Keyboard-HID, Consumer-HID und CDC Serial |
| Aktionen | HID-Key, Consumer-Key, Host-Event, Makro, Profilwechsel |
| Makros | 32 Slots mit je bis zu 8 HID-Schritten |
| Profile | 3 Profile in Config v3 |
| LEDs | 20 WS2812B mit Base-/Override-Farbe und 7 Animationsmodi |
| Persistenz | Config und Makros im internen Flash |
| Recovery | Werksreset über zwei Tasten |
Die drei Fader-Pins sind im Board-Variant definiert, werden von der aktuellen
Firmware aber noch nicht eingelesen.
## 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 KiB / 16 KiB |
| Framework | Arduino SAMD Core 1.8.14, PlatformIO | | Matrix | logisch 5×5: 20 MX, 4 Encoder-SW, 1 unbelegt |
| Programmer | Atmel-ICE via SWD (kein Bootloader) | | Encoder | 4× Quadratur über EIC-Interrupts |
| LEDs | 20× WS2812B an `PB22` |
| USB | Native USB als HID + CDC Composite Device |
| Programmer | Atmel-ICE/CMSIS-DAP über SWD, standardmäßig ohne Bootloader |
### Button-Matrix ## Schnellstart
- **5×5-Matrix** (COL_04 × ROW_04) = 25 logische Keys Voraussetzungen sind PlatformIO Core oder die PlatformIO IDE sowie für den
- **20 Cherry MX Switches** (COL_14 × ROW_04), je eine WS2812-LED Upload ein angeschlossener Atmel-ICE beziehungsweise kompatibler
- **4 Encoder-SW-Buttons** (COL_0 × ROW_03), keine LEDs CMSIS-DAP-Adapter.
- 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 --target upload pio run -e versapad
pio run -e versapad --target upload
``` ```
Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig der Chip wird direkt programmiert. Das Standard-Environment `versapad` baut für `boards/versapad_nobl.json`. Der
Upload wird durch `upload_openocd.py` über das von PlatformIO installierte
OpenOCD ausgeführt.
--- Das in `platformio.ini` auskommentierte Bootloader-Environment ist derzeit
nicht als Produktionsziel unterstützt. Details stehen unter
[bekannte Einschränkungen](doc/09_known_limitations.md).
## Funktionsumfang (Anforderungskatalog) ## Laufzeitmodell
### 1 Hardware-Plattform `main.cpp` besitzt genau einen `CMainController`. Nach einem roten Startsignal
initialisiert er NVM, USB, Matrix und Encoder. Die Hauptschleife ist:
| # | 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()
``` ```
- Matrix und Encoder legen `SEvent`s in eine feste Queue.
- Der Controller setzt Events in HID-Aktionen, Makros, Host-Events oder
Profilwechsel um.
- `poll_vendor()` verarbeitet feste 8-Byte-Pakete über CDC.
- LEDs werden nur neu übertragen, wenn ein Zustand dirty ist oder eine
Animation läuft.
- Makros, Encoder-Taps, NVM-Zugriffe und visuelles Reset-Feedback blockieren
den Loop kurzzeitig; es gibt keinen Scheduler.
## Wichtige Datenverträge
### Config v3
- Magic `0x56503203`
- `SDeviceConfig`: 740 Byte
- CRC16-CCITT über Bytes `7..739`
- 3 Profile
- globale und LED-spezifische Helligkeit
- 124 Chunks mit je 6 Nutzbytes beim CDC-Transfer
### Makros
- `SMacroTable`: 512 Byte
- 32 Slots × 8 Schritte × 2 Byte
- 86 Chunks mit je 6 Nutzbytes beim CDC-Transfer
### Flashzugriffe
| Bereich | Adresse | Größe |
|---|---|---|
| Makros | `0x1FB00..0x1FCFF` | 512 B |
| Config | `0x1FD00..0x1FFFF` | 768 B, davon 740 B genutzt |
Das aktive Linker-Skript und `boards/versapad_nobl.json` begrenzen das
Firmware-Image auf `0x00000..0x1FAFF`. Damit sind alle fünf NVM-Rows gegen
Firmwarewachstum geschützt.
## Werksreset
Unteren linken und unteren rechten MX-Button gleichzeitig fünf Sekunden
halten:
- die Tasten werden während des Haltens rot markiert,
- ihre normalen Aktionen werden unterdrückt,
- bei Erfolg blinken alle LEDs kurz rot,
- Config und Makrotabelle werden auf Defaults zurückgesetzt.
Ein normaler SWD-Reflash löscht diese NVM-Daten nicht automatisch.
## Projektstruktur
```text
VersaMCU/ VersaMCU/
├── platformio.ini Build- und Upload-Konfiguration |-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs
├── upload_openocd.py Benutzerdefiniertes Upload-Script (OpenOCD) |-- README.md
├── boards/ |-- platformio.ini
│ ├── versapad.json Board-Definition mit Bootloader |-- boards/ # PlatformIO-Boarddefinitionen
│ └── versapad_nobl.json Board-Definition ohne Bootloader (aktiv) |-- variants/versapad/ # Pinmapping und Linker-Skripte
├── variants/versapad/ |-- doc/ # Architektur- und Protokolldokumentation
│ ├── variant.h/.cpp Pin-Mapping für den SAMD21G17D `-- src/
│ └── linker_scripts/ Linkerscript (kein Bootloader, NVM-Reservierung) |-- main.cpp
└── src/ |-- CMainController.* # Orchestrierung
├── main.cpp Arduino setup()/loop() |-- CButton.* # Actions und LED-Zustand
├── CMainController.h/.cpp Zentraler Orchestrator |-- CEventQueue.* # feste Event-Queue
├── CButton.h/.cpp Button-Modell: LED-Schichten, Action, Animationen |-- config/ # Binärformate, NVM und Pins
├── CEventQueue.h/.cpp Ring-Buffer FIFO (16 Slots, kein Heap) `-- hal/ # Matrix, Encoder, HID, CDC, WS2812
├── 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
``` ```
## Architektur ## Einstieg für Entwickler und LLMs
### Schichten Für einen neuen Kollegen:
``` 1. [Entwicklung und Einstieg](doc/08_development.md)
┌────────────────────────────────────────────────┐ 2. [Architektur](doc/00_architecture.md)
│ main.cpp │ 3. die zum Task passende Fachdokumentation im [Dokumentationsindex](doc/INDEX.md)
│ CMainController (setup / work) │ 4. [bekannte Einschränkungen](doc/09_known_limitations.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 Für einen Coding-Agent zusätzlich [`AGENTS.md`](AGENTS.md) als
Repository-Anweisung mitgeben. Die Datei enthält Quellenhierarchie,
Binärverträge, Änderungsregeln und die minimale Verifikation.
``` ## Dokumentation
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) - [Dokumentationsindex](doc/INDEX.md)
└─► CMainController.poll_vendor() - [Architektur](doc/00_architecture.md)
└─► CButton.set_override() / set_base() / clear_override() - [Matrix](doc/01_matrix.md)
- [Encoder](doc/02_encoder.md)
CMainController.updateLEDs() - [Action-Engine](doc/03_action_engine.md)
└─► CButton.render_led() → ws2812_set() - [Makros](doc/04_macro_system.md)
└─► ws2812_show() (nur wenn dirty) - [LED-System](doc/05_led_system.md)
``` - [NVM-Config](doc/06_nvm_config.md)
- [CDC-Protokoll](doc/07_serial_protocol.md)
### Loop-Ablauf - [Entwicklung und Einstieg](doc/08_development.md)
- [Bekannte Einschränkungen](doc/09_known_limitations.md)
```
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.
+1 -1
View File
@@ -20,7 +20,7 @@
"name": "VersaPad v2 (Atmel-ICE, no bootloader)", "name": "VersaPad v2 (Atmel-ICE, no bootloader)",
"upload": { "upload": {
"maximum_ram_size": 16384, "maximum_ram_size": 16384,
"maximum_size": 131072, "maximum_size": 129792,
"protocol": "atmel-ice", "protocol": "atmel-ice",
"require_upload_port": false, "require_upload_port": false,
"use_1200bps_touch": false "use_1200bps_touch": false
+100 -58
View File
@@ -1,83 +1,125 @@
# VersaMCU Architekturbersicht # VersaMCU Architekturübersicht
## 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; LED-/Timingpfade verwenden 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() Arduino setup()
├── macro_config_load() Makro-Tabelle aus NVM in RAM laden delay(500)
├── init_buttons() CButton-Objekte aus NVM initialisieren ws2812_init()
├── usb_hid_init() HID-Descriptor (No-Op, läuft via global ctor) rotes Startsignal für 1 s
├── usb_serial_init() CDC Serial öffnen CMainController::setup()
├── matrix_init(cb) 5×5-Matrix + Debounce-Zustand macro_config_load()
└── encoder_init(cb) EIC-Interrupts für 4 Encoder init_buttons() -> nvm_config_load()
Queue-Bridge setzen
usb_hid_init()
usb_serial_init()
matrix_init(cb)
encoder_init(cb)
loop() [~20 ms Iteration] Arduino 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
Makros, Encoder-Taps, NVM-Schreiben sowie Start- und Reset-Feedback verwenden
blockierende Delays. Währenddessen werden Matrix und CDC nicht bearbeitet.
## 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.*` | fester Ringbuffer mit 16 nutzbaren Slots |
| `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 |
Die drei Fader sind nur im Variant und in `config/pins.h` definiert. Es gibt
aktuell keinen Fader-HAL und keine Verarbeitung im Controller.
## 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
- Projektcode vermeidet dynamische Allokation
- Integer-Arithmetik in zeitkritischen LED-/ISR-Pfaden
- `packed` fuer serielle und NVM-relevante Structs
- NVM-Schreibpuffer muessen 4-Byte-aligned sein
- `usb_serial_send()` sendet nur bei aktiver CDC-Verbindung
## Nebenläufigkeit
Encoder-Callbacks laufen im EIC-Interrupt, Matrixcallbacks im Loop. Beide
schreiben in dieselbe `CEventQueue`; `processEvents()` liest im Loop. Der
Matrixcallback maskiert Interrupts während seines Queue-Pushs, sodass Loop und
ISR den Tail-Index nicht gleichzeitig ändern. Bei Überlauf werden Events
weiterhin verworfen.
+4 -1
View File
@@ -41,5 +41,8 @@ key_id = col * MATRIX_ROWS + row
## Kontext ## Kontext
- Läuft im Loop-Kontext (kein ISR) - Läuft im Loop-Kontext (kein ISR)
- Encoder-SW-Tasten gehen durch denselben Matrix-Pfad (COL_0) - Encoder-SW-Tasten gehen durch denselben Matrix-Pfad (`COL_0`)
- `matrix_scan()` wird einmal pro `loop()` aufgerufen - `matrix_scan()` wird einmal pro `loop()` aufgerufen
- Der Callback schreibt in dieselbe Queue wie die Encoder-ISRs. Ein
kurzer `noInterrupts()`/`interrupts()`-Abschnitt schützt den Matrix-Push
davor, von einem Encoderinterrupt unterbrochen zu werden.
+12 -2
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):
@@ -36,8 +41,13 @@ static void isr_enc0_b() { handle_encoder(0); }
## ISR-Sicherheit ## ISR-Sicherheit
- `s_state[]` und `s_accum[]` sind `volatile` - `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 - Der Callback-Pointer `s_cb` wird einmalig in `setup()` gesetzt, bevor Interrupts aktiviert werden
- ISR-Wrapper führen nur Dekodierung und Queue-Push aus; sie rufen kein USB,
NVM, LED-Rendering oder `delay()` auf
Die Queue wird zusätzlich vom Matrixcallback im Loop beschrieben. Dieser
Loop-Push läuft in einer kurzen Critical Section; gleichpriorisierte
Encoder-ISRs unterbrechen sich auf dem Cortex-M0+ nicht gegenseitig.
## Initialisierung ## Initialisierung
+103 -46
View File
@@ -1,70 +1,127 @@
# Aktions-Engine # Aktions-Engine
**Dateien:** `config/action.h`, `CMainController.cpp` (`processEvents`, `execute_action`) 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 | 16-Bit-Command-ID |
| `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 | | `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
| `PROFILE_SWITCH` | Profilwechsel | 0..2, `0x00FF` oder `0xFFFF` für nächstes Profil |
## Ausführung (execute_action) ## Verhalten bei `KEY_DOWN`
**HID_KEY:** | Typ | Effekt |
``` |---|---|
usb_hid_send_key(keycode, modifier) | `HID_KEY` | `usb_hid_send_key()` |
delay(10 ms) | `HID_CONSUMER` | `usb_hid_send_consumer()` |
usb_hid_release_key() | `HOST_COMMAND` | `USB_EVT_KEY_DOWN (0x81)` mit `key_id` und Command-ID senden |
``` | `MACRO` | komplette Sequenz sofort abspielen |
→ Tap-Only-Modell. Kein Hold-Support. KEY_UP löst kein HID-Release aus. | `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren |
| `NONE` | nichts |
**HID_CONSUMER:** ## Verhalten bei `KEY_UP`
```
usb_hid_send_consumer(usage_id)
usb_hid_release_consumer()
```
→ Kein Delay nötig (Consumer-Keys sind Edge-getriggert).
**HOST_COMMAND:** | Typ | Effekt |
`execute_action()` macht nichts. `processEvents()` sendet zusätzlich `USB_EVT_KEY_DOWN` via CDC Serial an VersaGUI. Die App entscheidet was passiert (URL öffnen, Programm starten, …). |---|---|
| `HID_KEY` | `usb_hid_release_key()` |
| `HID_CONSUMER` | `usb_hid_release_consumer()` |
| `HOST_COMMAND` | `USB_EVT_KEY_UP (0x82)` mit `key_id` und Command-ID senden |
| `MACRO` | nichts |
| `PROFILE_SWITCH` | nichts |
| `NONE` | nichts |
**MACRO:** ## Hold- und Tap-Modell
```
für jeden Step [03] im Slot: - MX-Buttons und Encoder-SW benutzen fuer HID und Consumer das Hold-Modell.
if keycode == 0: abbrechen - Encoder `CW` und `CCW` sind immer diskrete Tap-Events:
usb_hid_send_key(keycode, modifier)
delay(10 ms) ```text
usb_hid_release_key() down -> delay(10 ms) -> up
delay(20 ms) // Pause damit der Host den Step verarbeiten kann
``` ```
## Event-Verarbeitung - Makros laufen komplett synchron in der Firmware.
Events werden in `processEvents()` aus `CEventQueue` konsumiert (FIFO): Keyboard-Keys und Modifier werden im HID-HAL referenzgezählt. Bis zu sechs
unterschiedliche Keyboard-Usages können der Report gleichzeitig abbilden;
beim Loslassen einer Action bleiben die übrigen Holds aktiv.
- `KEY_DOWN``CButton.on_press()` (aktuell leer, Erweiterungspunkt) + `execute_action()` Der Consumer-Descriptor kann jeweils nur ein Usage darstellen. Der HAL
- `KEY_UP``CButton.on_release()` (aktuell leer) verwaltet mehrere Holds und zeigt das zuletzt gedrückte aktive Usage; nach
- `ENC_CW/CCW``execute_action(m_enc_cw/ccw[enc_id])` dessen Release wird das zuvor aktive Usage wiederhergestellt.
Encoder CW/CCW-Aktionen sind in `CMainController` direkt als `SAction`-Arrays gehalten (kein CButton-Objekt, da Encoder keine LED haben). ## Host-Commands
## Bekannte Einschränkungen Der aktuelle Code sendet für jede `HOST_COMMAND`-Action nur:
- **Kein Hold**: `execute_action` bei KEY_DOWN sendet sofort Key-Down + Key-Up. Halten der Taste löst keine Wiederholung aus. ```text
- **HOST_COMMAND KEY_UP**: Board sendet derzeit kein `USB_EVT_KEY_UP` für KEY_UP-Events (nur KEY_DOWN wird gemeldet). Byte 0 = USB_EVT_KEY_DOWN (0x81) oder USB_EVT_KEY_UP (0x82)
Byte 1 = Matrix-Key-ID oder Encoder-ID
Byte 2 = Command-ID Low-Byte
Byte 3 = Command-ID High-Byte
```
Encoder-Host-Actions senden genau ein Richtungsereignis:
`ENC_CW (0x83)` beziehungsweise `ENC_CCW (0x84)`, ebenfalls mit Encoder-ID und
Command-ID. Die GUI erhält damit die konfigurierte Action direkt aus dem
Event und muss den aktiven Profilstand nicht rekonstruieren.
## 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
+69 -34
View File
@@ -1,59 +1,94 @@
# 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 (`20 + enc * 3 + act_idx`) |
## Laden und Speichern Diese Zuordnung ist eine Konvention der externen GUI. Die Firmware erzwingt
sie nicht: `SAction.data` wird direkt als Slotindex `0..31` verwendet.
**Laden** (`macro_config_load`): ## Laden
- `memcpy` direkt aus Flash-Adresse in RAM-Struct
- Kein Magic/CRC (leere Tabelle bei 0xFF ist akzeptabler Zustand)
**Speichern** (`macro_config_save`): `macro_config_load()`:
- SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!)
- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus)
- 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). - kopiert 512 Byte aus NVM in `SMacroTable`
- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
- prüft alle belegten Steps gegen den HID-Descriptor (`keycode <= 0x65`)
- setzt bei gelöschtem oder ungültigem Inhalt eine leere Tabelle
## Ausführung (in execute_action, ActionType::MACRO) Eine leere Tabelle ist also ein gueltiger Default-Zustand.
``` ## Speichern
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. `macro_config_save()`:
1. HID-Keycodes validieren
2. Tabelle in einen 4-Byte-aligned Puffer kopieren
3. beide Rows loeschen
4. 8 Pages zu je 64 Byte schreiben
Rueckgabewert:
- `true` bei Erfolg
- `false` bei NVM-Timeout
`static_assert` schützt die erwarteten Größen 2 und 512 Byte beim Build.
## 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.
+22 -4
View File
@@ -14,9 +14,12 @@ Dünner Wrapper um **Adafruit NeoPixel** (bit-bang, kein DMA, kein SERCOM).
| `ws2812_show()` | Bit-Bang-Übertragung (~600 µs, Interrupts gesperrt) | | `ws2812_show()` | Bit-Bang-Übertragung (~600 µs, Interrupts gesperrt) |
| `ws2812_clear()` | `clear()` + `show()` | | `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. `ws2812_show()` wird in `CMainController::updateLEDs()` nur aufgerufen, wenn
mindestens ein Button dirty war oder eine Animation läuft. Bei einer endlos
laufenden Animation wird daher in jedem Loop ein Frame übertragen.
**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. Der Treiber nutzt `Adafruit NeoPixel` per Bit-Banging; es gibt keinen
DMA-/SERCOM-Ausgabepfad.
## 2-Schicht-Modell (CButton) ## 2-Schicht-Modell (CButton)
@@ -34,7 +37,7 @@ Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sof
| Animation | Typ | Verhalten | Endbedingung | | Animation | Typ | Verhalten | Endbedingung |
|---|---|---|---| |---|---|---|---|
| `STATIC` | — | Feste Farbe | — | | `STATIC` | — | Feste Farbe | — |
| `BLINK` | Helligkeit | An/Aus, `period_ms` = Halbperiode | endlos | | `BLINK` | Helligkeit | erste Hälfte an, zweite Hälfte aus; `period_ms` = Vollperiode | endlos |
| `PULSE` | Helligkeit | Lineares Dreieck 0→255→0 | endlos | | `PULSE` | Helligkeit | Lineares Dreieck 0→255→0 | endlos |
| `FADE_IN` | Helligkeit | Einmalig schwarz → voll | → STATIC (voll) | | `FADE_IN` | Helligkeit | Einmalig schwarz → voll | → STATIC (voll) |
| `FADE_OUT` | Helligkeit | Einmalig voll → schwarz | → STATIC (base=schwarz) | | `FADE_OUT` | Helligkeit | Einmalig voll → schwarz | → STATIC (base=schwarz) |
@@ -45,6 +48,12 @@ Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sof
**Farb-Animationen** (`compute_rgb`): Berechnen RGB direkt; base/override werden nicht verändert (außer bei Abschluss). **Farb-Animationen** (`compute_rgb`): Berechnen RGB direkt; base/override werden nicht verändert (außer bei Abschluss).
Die globale und LED-spezifische Helligkeit werden beim Initialisieren in die
Base-Farbe eingerechnet. `COLOR_CYCLE` ignoriert diese Base-Farbe und rendert
mit einem festen Faktor von 40 %. Auch CDC-Overrides werden von
`COLOR_CYCLE`/`COLOR_FADE` visuell überdeckt, solange die Farbanimation aktiv
ist.
### COLOR_CYCLE Hue-Arithmetik (kein Float) ### COLOR_CYCLE Hue-Arithmetik (kein Float)
Hue 0255 aufgeteilt in 6 Segmente à 43 Einheiten. Innerhalb jedes Segments steigt/fällt ein Kanal linear: Hue 0255 aufgeteilt in 6 Segmente à 43 Einheiten. Innerhalb jedes Segments steigt/fällt ein Kanal linear:
@@ -60,7 +69,16 @@ Seg 5: R=255, B fällt (Magenta → Rot)
Ausgabe wird auf 40 % Helligkeit skaliert (Faktor 102/255) damit die LEDs nicht blenden. 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+). Die Firmware verwendet eine eigene ganzzahlige Hue-Umrechnung und ruft
`Adafruit_NeoPixel::ColorHSV()` nicht auf.
Für `PULSE` muss `period_ms >= 2` gelten, da der Code durch die halbe Periode
teilt. Configvalidierung lehnt kleinere Werte ab; `set_anim()` klemmt direkte
interne Aufrufe zusätzlich auf mindestens 2 ms.
`COLOR_FADE` benötigt `set_color_fade(to, period_ms)`. Beim Laden aus der
Config interpretiert der Controller die gespeicherte Base-Farbe als Ziel und
startet einen einmaligen Fade von Schwarz zu dieser Farbe.
### Phasenversatz (Regenbogen-Wellen) ### Phasenversatz (Regenbogen-Wellen)
+120 -44
View File
@@ -1,68 +1,144 @@
# 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) Das aktive Linker-Skript reserviert `0x1FB00..0x1FFFF` als eigenen
NVM-Memory-Bereich. Die Boarddefinition meldet entsprechend höchstens
129.792 Byte Firmware-Flash. Damit kann ein erfolgreich gelinktes Image die
fünf NVM-Rows nicht überdecken.
| Offset | Größe | Feld | ## `SDeviceConfig`
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. `enc_sensitivity` ist im Binärformat vorhanden und hat Default `1`, wird von
der Encoderdekodierung derzeit aber nicht verwendet.
## CRC16-CCITT ### Pro Profil
- Polynom: `0x1021`, Init: `0xFFFF` Jedes Profil belegt 236 Byte:
- Berechnet über Bytes 7222 (ab `mx_actions`, nach dem `crc`-Feld selbst)
- Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel
## Lese-Logik | 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:
memcpy aus Flash-Adresse 0x1FE00
if magic != 0x56503202: Defaults laden, return false ```text
if version != 2: Defaults laden, return false 32 Byte Header + 3 * 236 Byte Profile = 740 Byte
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, Version und CRC prüfen
3. Profilindex, Actiontypen/-daten und LED-Enums prüfen
4. `PULSE`-Perioden auf mindestens 2 ms prüfen
5. bei Fehlern Defaults laden und `false` zurückgeben
Die Firmware faellt also immer auf einen gueltigen Zustand zurueck.
Die Defaults werden bei diesem Fallback nur in das übergebene RAM-Struct
geschrieben und nicht automatisch in Flash persistiert.
Dieselbe Validierung wird vor einem Config-Commit aus dem CDC-Protokoll
verwendet.
## Speichern
`nvm_config_save()`:
1. die vom Aufrufer bereits vorbereitete 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
- `nvm_config_save()` berechnet die CRC in einer lokalen Kopie immer neu
- `static_assert` schützt die erwarteten Größen 3, 236 und 740 Byte beim Build
## 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.
+123 -58
View File
@@ -1,86 +1,151 @@
# 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..7: kommandospezifische Daten
``` ```
Byte 0: Command / Event-ID
Byte 1: key_id (Button 024 oder Encoder 03) / Chunk-Index / Chunk-Count LED-Kommandos verwenden Byte 2..4 für RGB. Config- und Makro-DATA-Pakete
Byte 2: r / Daten-Byte A verwenden alle sechs Bytes 2..7 als Nutzlast. Einfache Events aus
Byte 3: g / Daten-Byte B `usb_serial_send()` nutzen höchstens Byte 2..3 und füllen den Rest mit null.
Byte 4: b
Byte 57: reserviert (0x00) Es gibt kein Framing, keinen Längenheader und keine Prüfsumme auf Paketebene.
```
## 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-Button gedrückt |
| `0x82` | KEY_UP | key_id (derzeit nicht gesendet) | | `0x82` | `KEY_UP` | Host-Button losgelassen |
| `0x83` | ENC_CW | enc_id Encoder-Schritt CW (HOST_COMMAND) | | `0x83` | `ENC_CW` | Encoder-Host-Action im Uhrzeigersinn |
| `0x84` | ENC_CCW | enc_id Encoder-Schritt CCW (HOST_COMMAND) | | `0x84` | `ENC_CCW` | Encoder-Host-Action 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 Bei `ActionType::HOST_COMMAND` enthält Byte 1 die Matrix-Key-ID oder
Encoder-ID. Die 16-Bit-Command-ID aus `SAction.data` steht little-endian in
Byte 2 und 3. Encoder-Actions verwenden die richtungsspezifischen IDs
`0x83/0x84`.
Config (223 B) und Makro-Tabelle (256 B) werden in 6-Byte-Chunks übertragen: ## Chunk-Zahlen
``` Aktuelle Blob-Groessen:
Config: ceil(223 / 6) = 38 Chunks
Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes) - Config: `740` Byte
- Makros: `512` Byte
Bei 6 Nutzbytes pro Paket ergibt das:
```text
Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(512 / 6) = 86 Chunks
``` ```
Ablauf (PC → Board): 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.
Beide Empfangspfade erwarten exakt die berechnete Chunkzahl, markieren jeden
Index einmalig und akzeptieren `COMMIT` nur nach einem vollständigen Transfer.
Doppelte oder außerhalb des Bereichs liegende Chunks machen den Transfer
ungültig und führen beim Commit zu NACK.
Config-Commit prüft zusätzlich Magic, Version, CRC, Profilindex,
Actiontypen/-daten, LED-Enums und kritische Animationsperioden.
Makro-Commit prüft die Vollständigkeit und alle HID-Keycodes. Die Makrotabelle
besitzt weiterhin keine eigene persistente CRC.
## 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
- ausschließlich vollständige 8-Byte-Pakete schreiben; schon ein verlorenes
Byte verschiebt die Paketgrenzen für alle folgenden Daten
- Host-Command-ID little-endian aus Byte 2/3 lesen
- Config- und Makro-Dumps ebenfalls auf Chunkzahl, eindeutige Indizes und
Vollständigkeit prüfen
## Implementierungsdetails ## Implementierungsdetails
- **Ring-Buffer**: 256 Byte Eingangspuffer (= 32 vollständige Pakete) in `usb_serial.cpp` - RX-Ringbuffer: 256 Byte = 32 volle Pakete; er ist ein laufender
- **DTR-Check**: `usb_serial_send()` sendet nur wenn `SerialUSB` aktiv ist (verhindert stilles Verwerfen wenn VersaGUI nicht verbunden) Zwischenpuffer und fasst keinen kompletten Configtransfer
- **SAMD21 CDC**: Nach SWD-Flash braucht Windows eine physische USB-Reinitialisierung (Kabel abziehen/stecken) damit der CDC-Port neu enumeriert - feste 8-Byte-Pakete vereinfachen Firmware und GUI
- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein
+98
View File
@@ -0,0 +1,98 @@
# Entwicklung und Einstieg
Diese Seite ist der praktische Einstieg für neue Entwickler. Für einen
LLM-basierten Coding-Agent zusätzlich die Anweisungen in
[`../AGENTS.md`](../AGENTS.md) bereitstellen.
## Voraussetzungen
- PlatformIO Core oder PlatformIO IDE
- USB-Kabel für Laufzeittests
- Atmel-ICE beziehungsweise kompatibler CMSIS-DAP-Adapter für den Upload
- das benachbarte Repository `../VersaGUI`, wenn das CDC-Protokoll oder
persistente Formate geändert werden
PlatformIO lädt den Arduino-SAMD-Core, OpenOCD und `Adafruit NeoPixel` über
`platformio.ini`. Das aktive Standardziel ist `versapad_nobl` im Environment
`versapad`.
## Build und Upload
```bash
pio run -e versapad
pio run -e versapad --target upload
```
Der Upload nutzt `upload_openocd.py`, das das von PlatformIO installierte
OpenOCD mit `interface/cmsis-dap.cfg` und `target/at91samdXX.cfg` startet.
Das in `platformio.ini` nur als Beispiel enthaltene Environment
`versapad_usb` ist auskommentiert und mit dem aktuellen NVM-/Linker-Layout
nicht als unterstützt anzusehen.
## Was beim Start passiert
```text
Arduino setup()
500 ms warten
WS2812 initialisieren
1 s rotes Startsignal
Makros aus NVM laden
Config laden und Buttons initialisieren
USB-HID/CDC, Matrix und Encoder initialisieren
Arduino loop()
Matrix scannen
CDC-Pakete verarbeiten
Event-Queue leeren
Werksreset prüfen
LEDs rendern
```
Der Controller blockiert während Makros, Encoder-Taps, Start-/Reset-Feedback
und NVM-Schreibvorgängen. Es gibt keinen Scheduler und keine Threads.
## Einstieg nach Änderungstyp
| Änderung | Zuerst lesen | Typische Dateien |
|---|---|---|
| Matrix/Key-Mapping | `01_matrix.md` | `hal/matrix.*`, `config/pins.h`, Variant |
| Encoder | `02_encoder.md` | `hal/encoder.*`, `CMainController.cpp` |
| Actions/HID | `03_action_engine.md` | `config/action.h`, Controller, `hal/usb_hid.*` |
| Makros | `04_macro_system.md` | `config/macro_config.*`, Controller |
| LEDs | `05_led_system.md` | `CButton.*`, `hal/ws2812.*` |
| Persistente Config | `06_nvm_config.md` | `config/nvm_config.*`, Linker-Skripte |
| Host-Protokoll | `07_serial_protocol.md` | `hal/usb_serial.*`, Controller |
## Verifikation
Für die Firmware gibt es derzeit keine automatisierten Tests. Der minimale
lokale Check ist:
```bash
pio run -e versapad
git diff --check
```
Je nach Änderung folgen Hardwaretests:
- Matrix: jede Taste einzeln, Mehrfachtasten und beide Reset-Tasten
- Encoder: beide Richtungen und schneller Richtungswechsel
- HID: Down/Up sowie Modifier und Consumer Usage
- CDC: Ping, vollständiger Config-/Makro-Transfer und Readback
- NVM: Power-Cycle, ungültige CRC und Werksreset
- LEDs: alle Animationen, Helligkeit und temporäre Overrides
Die GUI ist ein separates Git-Repository im selben Workspace. Änderungen an
`SDeviceConfig`, `SMacroTable`, Action-Werten oder USB-IDs sind erst
vollständig verifiziert, wenn Firmware und `../VersaGUI` dieselben Bytes
senden und interpretieren. `DelphiGUI` gehört nicht zum gepflegten Scope.
Die automatisierten GUI-Vertragstests liegen unter
`../VersaGUI/tests/VersaGUI.ContractTests/`.
## Dokumentation mitpflegen
Bei jedem Change die betroffene Fachdokumentation aktualisieren. Zahlen wie
Structgrößen, Offsets, Chunk-Anzahlen und Flashgrenzen immer aus dem neuen Code
neu ableiten. Offene oder absichtlich nicht behobene Punkte gehören nach
[`09_known_limitations.md`](09_known_limitations.md).
+84
View File
@@ -0,0 +1,84 @@
# Bekannte Einschränkungen und Risiken
Diese Liste beschreibt den aktuellen Implementierungsstand nach den
Robustheitskorrekturen. Sie ist keine Liste bereits umgesetzter Features.
## Bootloader-Ziel bleibt nicht unterstützt
Das aktive Ziel `versapad_nobl` reserviert den kompletten Bereich
`0x1FB00..0x1FFFF` für Makros und Config. Das auskommentierte
USB-Bootloader-Environment verwendet dagegen weiterhin eine historische
Board-/Linker-Konfiguration und ist nicht als Produktionsziel verifiziert.
Die aktive Boarddatei benennt die MCU als `samd21g17d`, setzt für den
Arduino-Core aber weiterhin das Kompatibilitätsmakro `__SAMD21G18A__`. Der
PlatformIO-Build meldet korrekt 128 KiB physischen Flash, 16 KiB RAM und
129.792 Byte nutzbaren Firmwarebereich. Vor device-spezifischen
Core-Änderungen sollte die historische Makro-Abweichung trotzdem geprüft
werden.
## Event-Queue hat eine feste Kapazität
Matrix- und Encoder-Producer verändern den Tail-Index nicht mehr gleichzeitig:
Der Matrixcallback maskiert Interrupts während seines Queue-Pushs.
Die Queue besitzt aber weiterhin nur 16 nutzbare Slots. Bei Überlauf wird ein
neues Event ohne Hostmeldung verworfen. Das kann vor allem während
blockierender Makro-, NVM- oder Feedbackpfade auftreten.
## Host-Command-Ausführung liegt in der Desktop-App
Die Firmware überträgt Command-ID, Key-/Encoder-ID und Eventrichtung
vollständig. VersaGUI validiert und empfängt diese Pakete, führt eine
Command-ID aber noch nicht als Prozess-, URL- oder frei konfigurierbare
Hostaktion aus. Eine spätere Implementierung benötigt ein explizites,
sicheres Mapping; beliebige Command-Strings sollten nicht direkt an eine Shell
weitergegeben werden.
## Grenzen des HID-Reports
Keyboard-Holds und Modifier werden referenzgezählt. Der USB-Descriptor kann
maximal sechs unterschiedliche Keyboard-Usages gleichzeitig darstellen.
Weitere Holds bleiben intern aktiv und rücken nach, sobald ein Report-Slot
frei wird.
Der Consumer-Descriptor enthält genau ein Usage. Mehrere Consumer-Holds werden
intern verwaltet, sichtbar ist jeweils das zuletzt gedrückte aktive Usage.
## CDC bleibt ein festes, ungeframtes Paketprotokoll
Config- und Makrotransfers prüfen jetzt Chunkzahl, eindeutige Indizes und
Vollständigkeit. Config besitzt zusätzlich CRC und Feldvalidierung.
Weiterhin gilt:
- Das Protokoll hat kein Byte-Framing. Ein verlorenes oder zusätzliches Byte
verschiebt die 8-Byte-Paketgrenzen bis zum Reconnect.
- Einzelpakete besitzen keine Sequenznummer oder Prüfsumme.
- Die Makrotabelle besitzt im NVM keine persistente CRC; beim Transfer werden
nur Vollständigkeit und HID-Keycode-Bereiche geprüft.
## Farbanimationen und Helligkeit
Globale und LED-spezifische Helligkeit werden beim Laden in die Base-Farbe
eingerechnet. `COLOR_CYCLE` berechnet RGB dagegen direkt mit festen 40 %
Helligkeit und ignoriert Base-Farbe sowie Override.
`COLOR_FADE` aus der Config wird als einmaliger Fade von Schwarz zur
gespeicherten Base-Farbe interpretiert.
## Reservierte beziehungsweise noch ungenutzte Hardware und Felder
- Die drei Fader-Pins sind im Variant und in `config/pins.h` definiert, werden
absichtlich noch nicht von der Firmware eingelesen.
- `enc_sensitivity[4]` wird gespeichert und mit Default `1` befüllt,
beeinflusst die Encoderdekodierung derzeit aber nicht.
- `SET_LED_BASE` verändert nur den RAM-Zustand und wird nicht in NVM
persistiert.
## Zeitverhalten
Makros und Encoder-Taps verwenden blockierende `delay()`-Aufrufe. Startsignal,
Werksreset-Feedback und NVM-Operationen blockieren ebenfalls den Loop. Während
dessen werden Matrix und CDC nicht bearbeitet; Encoder-ISRs können weiter
Events erzeugen, bis die Queue voll ist.
+25 -18
View File
@@ -1,24 +1,31 @@
# 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 beschreiben den aktuellen Firmware-Stand von Config v3, drei
Profilen und 32×8 Makros. Bekannte Abweichungen oder noch nicht abgesicherte
Bereiche stehen ausdrücklich in
[09_known_limitations.md](09_known_limitations.md).
| 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 |
| [08_development.md](08_development.md) | Setup, Build, Einstieg nach Änderungstyp, Verifikation |
| [09_known_limitations.md](09_known_limitations.md) | Aktuelle technische Einschränkungen und Risiken |
## Schnell-Referenz: Was steht wo? Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
[`../AGENTS.md`](../AGENTS.md).
- **Warum packed?** → [03_action_engine.md](03_action_engine.md), [06_nvm_config.md](06_nvm_config.md) ## Schnellreferenz
- **Warum kein Float?** → [05_led_system.md](05_led_system.md), [00_architecture.md](00_architecture.md)
- **Warum Bit-Bang statt DMA?** → [05_led_system.md](05_led_system.md) - aktuelle Config-Groesse: [06_nvm_config.md](06_nvm_config.md)
- **Aligned-Buffer bei NVM-Write?** → [04_macro_system.md](04_macro_system.md), [06_nvm_config.md](06_nvm_config.md) - aktuelle Makro-Groesse: [04_macro_system.md](04_macro_system.md)
- **Warum on_press/on_release leer?** → [03_action_engine.md](03_action_engine.md) - Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md)
- **Kein Hold-Support?** → [03_action_engine.md](03_action_engine.md) - Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
- **USB nach SWD-Flash nicht erkannt?** → [07_serial_protocol.md](07_serial_protocol.md) - CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)
- bekannte Risiken vor strukturellen Änderungen: [09_known_limitations.md](09_known_limitations.md)
+1 -1
View File
@@ -1,5 +1,5 @@
; VersaPad v2 PlatformIO Configuration ; VersaPad v2 PlatformIO Configuration
; Custom SAMD21G18A board (custom PCB) ; Custom SAMD21G17D board (custom PCB)
; Programmer: Atmel-ICE via SWD (kein Bootloader nötig) ; Programmer: Atmel-ICE via SWD (kein Bootloader nötig)
[common] [common]
+11 -20
View File
@@ -64,25 +64,13 @@ CButton::CButton()
void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base) void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base)
{ {
m_key_id = key_id; m_key_id = key_id;
m_led_index = led_index; m_led_index = led_index;
m_action = action; m_action = action;
m_base = base; m_base = base;
m_override_active = false; m_override_active = false;
m_anim = LEDAnim::STATIC; m_anim = LEDAnim::STATIC;
m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben 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
} }
// ── LED Layer 1: base ───────────────────────────────────────────────────────── // ── LED Layer 1: base ─────────────────────────────────────────────────────────
@@ -113,7 +101,10 @@ void CButton::clear_override()
void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms) void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms)
{ {
m_anim = anim; m_anim = anim;
m_anim_period_ms = (period_ms > 0) ? period_ms : 1; // Division durch 0 vermeiden // PULSE teilt intern durch die halbe Periode und braucht daher mindestens
// 2 ms. Für alle anderen Animationen genügt 1 ms als sicherer Mindestwert.
uint16_t minimum = (anim == LEDAnim::PULSE) ? 2 : 1;
m_anim_period_ms = (period_ms >= minimum) ? period_ms : minimum;
// phase_offset_ms in die Vergangenheit zurücksetzen → verschobener Startpunkt // phase_offset_ms in die Vergangenheit zurücksetzen → verschobener Startpunkt
m_anim_start_ms = millis() - phase_offset_ms; m_anim_start_ms = millis() - phase_offset_ms;
m_dirty = true; m_dirty = true;
+4 -9
View File
@@ -45,7 +45,7 @@ struct RGB
enum class LEDAnim : uint8_t enum class LEDAnim : uint8_t
{ {
STATIC = 0, // Sofort, keine Animation (Standardzustand) STATIC = 0, // Sofort, keine Animation (Standardzustand)
BLINK, // Binäres An/Aus period_ms = Halbperiode (An-Zeit = Aus-Zeit) BLINK, // Binäres An/Aus period_ms = Vollperiode (50 % an, 50 % aus)
PULSE, // Lineares Fade-In/Fade-Out in Schleife period_ms = Vollperiode PULSE, // Lineares Fade-In/Fade-Out in Schleife period_ms = Vollperiode
FADE_IN, // Einmalig: schwarz → volle Helligkeit über period_ms FADE_IN, // Einmalig: schwarz → volle Helligkeit über period_ms
FADE_OUT, // Einmalig: volle Helligkeit → schwarz über period_ms FADE_OUT, // Einmalig: volle Helligkeit → schwarz über period_ms
@@ -61,13 +61,8 @@ public:
CButton(); CButton();
// Initialisierung (ersetzt Konstruktor-Parameter). // Initialisierung (ersetzt Konstruktor-Parameter).
// led_index = -1 → kein LED (Encoder-SW-Buttons). void init(uint8_t key_id, int8_t led_index, SAction action,
void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB()); RGB base = RGB());
// Hooks für Tastendruck/-loslassen.
// Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …).
void on_press();
void on_release();
// ── LED Layer 1: base ───────────────────────────────────────────────────── // ── LED Layer 1: base ─────────────────────────────────────────────────────
// Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt. // Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt.
@@ -81,7 +76,7 @@ public:
// ── LED-Animation ───────────────────────────────────────────────────────── // ── LED-Animation ─────────────────────────────────────────────────────────
// set_anim(): für STATIC, BLINK, PULSE, FADE_IN, FADE_OUT, COLOR_CYCLE. // set_anim(): für STATIC, BLINK, PULSE, FADE_IN, FADE_OUT, COLOR_CYCLE.
// period_ms: Halbperiode (BLINK), Vollperiode (PULSE/COLOR_CYCLE), Dauer (FADE_*). // period_ms: Vollperiode (BLINK/PULSE/COLOR_CYCLE), Dauer (FADE_*).
// phase_offset_ms: Zeitversatz in die Vergangenheit verschiebt den Startpunkt der // phase_offset_ms: Zeitversatz in die Vergangenheit verschiebt den Startpunkt der
// Animation. Nützlich für COLOR_CYCLE um LEDs versetzt starten zu // Animation. Nützlich für COLOR_CYCLE um LEDs versetzt starten zu
// lassen (Regenbogen-Wellen-Effekt über mehrere Buttons). // lassen (Regenbogen-Wellen-Effekt über mehrere Buttons).
+4 -5
View File
@@ -7,11 +7,10 @@
// Leer: m_head == m_tail // Leer: m_head == m_tail
// Voll: (m_tail + 1) % SIZE == m_head → ein Slot bleibt immer frei // Voll: (m_tail + 1) % SIZE == m_head → ein Slot bleibt immer frei
// //
// Interrupt-Sicherheit (Cortex-M0+): // Nebenläufigkeit:
// push() wird aus Encoder-ISR aufgerufen, pop() aus dem Loop. // Encoder-ISRs und der Matrixcallback im Loop können beide push() aufrufen.
// Auf M0+ sind uint8_t-Lese/Schreibzugriffe atomar (single-cycle LDR/STR) // Der Matrixcallback schützt seinen push() mit einer kurzen Critical Section,
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren, ist kein // sodass m_tail nie von Loop und ISR gleichzeitig verändert wird.
// Mutex nötig. Bei mehreren Producern müsste noInterrupts() verwendet werden.
#include "CEventQueue.h" #include "CEventQueue.h"
+8 -7
View File
@@ -7,11 +7,12 @@
// Kapazität: QUEUE_SIZE - 1 = 16 Events (ein Slot bleibt leer damit // Kapazität: QUEUE_SIZE - 1 = 16 Events (ein Slot bleibt leer damit
// is_full() und is_empty() ohne extra Zähler unterscheidbar sind). // is_full() und is_empty() ohne extra Zähler unterscheidbar sind).
// //
// Thread-Sicherheit: // Nebenläufigkeit:
// push() wird aus ISR-Kontext aufgerufen (encoder_cb). // push() wird aus Encoder-ISRs und aus dem Matrixcallback im Loop aufgerufen.
// pop() wird aus Loop-Kontext aufgerufen (processEvents). // Der Matrixcallback maskiert Interrupts während push(); Encoder-ISRs können
// Auf Cortex-M0+ sind 8-Bit-Lese/Schreibzugriffe atomar → kein Mutex nötig // sich auf dem Single-Core-M0+ nicht gleichpriorisiert gegenseitig unterbrechen.
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren. // pop() läuft im Loop; ein gleichzeitig eintreffender Push wird spätestens
// im nächsten processEvents()-Durchlauf sichtbar.
#pragma once #pragma once
#include "SEvent.h" #include "SEvent.h"
@@ -32,6 +33,6 @@ public:
private: private:
static const uint8_t QUEUE_SIZE = 17; // 16 nutzbare Slots static const uint8_t QUEUE_SIZE = 17; // 16 nutzbare Slots
SEvent m_buf[QUEUE_SIZE]; SEvent m_buf[QUEUE_SIZE];
uint8_t m_head = 0; // Nächster Lese-Index (Consumer: pop) volatile uint8_t m_head = 0; // Nächster Lese-Index (Consumer: pop)
uint8_t m_tail = 0; // Nächster Schreib-Index (Producer: push) volatile uint8_t m_tail = 0; // Nächster Schreib-Index (Producer: push)
}; };
+378 -73
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
@@ -50,11 +54,17 @@ static void matrix_cb(uint8_t key, bool pressed)
ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP; ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP;
ev.key_id = key; ev.key_id = key;
ev.payload = 0; ev.payload = 0;
// Encoder-ISRs benutzen dieselbe Queue. Den Loop-Producer kurz gegen einen
// dazwischenlaufenden ISR-Push schützen; Encoder-Pushes selbst laufen
// bereits mit maskierten gleichpriorisierten Interrupts.
noInterrupts();
s_queue->push(ev); s_queue->push(ev);
interrupts();
} }
// Wird von handle_encoder() aufgerufen läuft im ISR-Kontext (EIC-Interrupt). // Wird von handle_encoder() aufgerufen läuft im ISR-Kontext (EIC-Interrupt).
// CEventQueue::push() ist interrupt-sicher (kein Heap, atomare Indizes auf M0+). // Der Matrix-Producer maskiert Interrupts während seines Queue-Pushs.
static void encoder_cb(uint8_t enc, int8_t dir) static void encoder_cb(uint8_t enc, int8_t dir)
{ {
if (!s_queue) return; if (!s_queue) return;
@@ -70,14 +80,32 @@ static void encoder_cb(uint8_t enc, int8_t dir)
CMainController::CMainController() CMainController::CMainController()
: m_cfg_chunks_expected(0) : m_cfg_chunks_expected(0)
, m_cfg_receiving(false) , m_cfg_receiving(false)
, m_cfg_transfer_valid(false)
, m_macro_chunks_expected(0) , m_macro_chunks_expected(0)
, m_macro_receiving(false) , m_macro_receiving(false)
, m_macro_transfer_valid(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_cfg_received, 0, sizeof(m_cfg_received));
memset(m_macro_buf, 0, sizeof(m_macro_buf)); memset(m_macro_buf, 0, sizeof(m_macro_buf));
memset(m_macro_received, 0, sizeof(m_macro_received));
memset(&m_macros, 0, sizeof(m_macros)); memset(&m_macros, 0, sizeof(m_macros));
} }
bool CMainController::all_chunks_received(
const uint8_t* received, uint8_t count)
{
for (uint8_t i = 0; i < count; i++) {
if (received[i] == 0) return false;
}
return true;
}
void CMainController::setup() void CMainController::setup()
{ {
macro_config_load(m_macros); // Makro-Tabelle aus NVM laden (oder leere Tabelle) macro_config_load(m_macros); // Makro-Tabelle aus NVM laden (oder leere Tabelle)
@@ -101,13 +129,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]); 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,16 +146,31 @@ 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
uint16_t phase = (uint16_t)((uint32_t)mx_idx * period / 20); uint16_t phase = (uint16_t)((uint32_t)mx_idx * period / 20);
m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, period, phase); m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, period, phase);
} else if (anim == LEDAnim::COLOR_FADE) {
// Config enthält nur eine Ziel-/Base-Farbe. COLOR_FADE wird beim
// Laden daher eindeutig als einmaliges Schwarz→Base interpretiert.
m_buttons[key].set_base(RGB(0, 0, 0));
m_buttons[key].set_color_fade(base, period);
} else { } else {
m_buttons[key].set_anim(anim, period); m_buttons[key].set_anim(anim, period);
} }
@@ -133,8 +179,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];
} }
} }
@@ -142,10 +188,11 @@ void CMainController::init_buttons()
void CMainController::work() 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, Buttons benachrichtigen processEvents(); // 3. Queue leeren, Aktionen ausführen
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() check_factory_reset();// 4. Long-Press-Kombination für Werksreset prüfen
updateLEDs(); // 5. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
} }
// ─── Vendor-Kommunikation (PC → Board) ─────────────────────────────────────── // ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
@@ -190,17 +237,30 @@ void CMainController::poll_vendor()
// Neuen Empfang starten bisherige Daten verwerfen // Neuen Empfang starten bisherige Daten verwerfen
m_cfg_chunks_expected = pkt.key_id(); m_cfg_chunks_expected = pkt.key_id();
m_cfg_receiving = true; m_cfg_receiving = true;
m_cfg_transfer_valid = (m_cfg_chunks_expected == CONFIG_CHUNKS);
memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
memset(m_cfg_received, 0, sizeof(m_cfg_received));
break; break;
case USB_CMD_CONFIG_DATA: case USB_CMD_CONFIG_DATA:
if (m_cfg_receiving) { if (m_cfg_receiving) {
// 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; uint8_t chunk = pkt.key_id();
if (offset < sizeof(m_cfg_buf)) { uint16_t offset = (uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset); if (m_cfg_transfer_valid &&
if (count > 6) count = 6; chunk < CONFIG_CHUNKS &&
m_cfg_received[chunk] == 0 &&
offset < sizeof(m_cfg_buf))
{
uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset);
uint8_t count = (uint8_t)(
remaining > SERIAL_PAYLOAD_BYTES
? SERIAL_PAYLOAD_BYTES
: remaining);
memcpy(m_cfg_buf + offset, &pkt.data[2], count); memcpy(m_cfg_buf + offset, &pkt.data[2], count);
m_cfg_received[chunk] = 1;
} else {
m_cfg_transfer_valid = false;
} }
} }
break; break;
@@ -211,10 +271,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); // 163 const uint16_t sz = sizeof(SDeviceConfig); // 740
const uint8_t payload = 6; const uint8_t payload = SERIAL_PAYLOAD_BYTES;
uint8_t chunks = (sz + payload - 1) / payload; // 28 const uint8_t chunks = CONFIG_CHUNKS;
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
@@ -222,7 +282,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];
} }
@@ -234,59 +294,98 @@ void CMainController::poll_vendor()
} }
case USB_CMD_CONFIG_COMMIT: case USB_CMD_CONFIG_COMMIT:
if (m_cfg_receiving) { {
m_cfg_receiving = false; bool complete =
m_cfg_receiving &&
m_cfg_transfer_valid &&
all_chunks_received(m_cfg_received, CONFIG_CHUNKS);
m_cfg_receiving = false;
if (complete) {
SDeviceConfig cfg; SDeviceConfig cfg;
memcpy(&cfg, m_cfg_buf, sizeof(cfg)); memcpy(&cfg, m_cfg_buf, sizeof(cfg));
if (cfg.magic == NVM_CONFIG_MAGIC && if (nvm_config_validate(cfg)) {
cfg.version == NVM_CONFIG_VERSION && if (nvm_config_save(cfg)) {
cfg.crc == nvm_config_crc(cfg)) init_buttons();
{ usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
nvm_config_save(cfg); } else {
init_buttons(); usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden }
} }
else else
{ {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
} }
} else {
usb_serial_send(USB_EVT_CONFIG_NACK, 0);
} }
break; break;
}
// ── Makro-Übertragung: BEGIN → n×DATA → COMMIT ────────────────── // ── Makro-Übertragung: BEGIN → n×DATA → COMMIT ──────────────────
case USB_CMD_MACRO_BEGIN: case USB_CMD_MACRO_BEGIN:
m_macro_chunks_expected = pkt.key_id(); m_macro_chunks_expected = pkt.key_id();
m_macro_receiving = true; m_macro_receiving = true;
m_macro_transfer_valid = (m_macro_chunks_expected == MACRO_CHUNKS);
memset(m_macro_buf, 0, sizeof(m_macro_buf)); memset(m_macro_buf, 0, sizeof(m_macro_buf));
memset(m_macro_received, 0, sizeof(m_macro_received));
break; break;
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; uint8_t chunk = pkt.key_id();
if (offset < sizeof(m_macro_buf)) { uint16_t offset =
uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset); (uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
if (count > 6) count = 6; if (m_macro_transfer_valid &&
chunk < MACRO_CHUNKS &&
m_macro_received[chunk] == 0 &&
offset < sizeof(m_macro_buf))
{
uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset);
uint8_t count = (uint8_t)(
remaining > SERIAL_PAYLOAD_BYTES
? SERIAL_PAYLOAD_BYTES
: remaining);
memcpy(m_macro_buf + offset, &pkt.data[2], count); memcpy(m_macro_buf + offset, &pkt.data[2], count);
m_macro_received[chunk] = 1;
} else {
m_macro_transfer_valid = false;
} }
} }
break; break;
case USB_CMD_MACRO_COMMIT: case USB_CMD_MACRO_COMMIT:
if (m_macro_receiving) { {
m_macro_receiving = false; bool complete =
memcpy(&m_macros, m_macro_buf, sizeof(m_macros)); m_macro_receiving &&
macro_config_save(m_macros); m_macro_transfer_valid &&
all_chunks_received(m_macro_received, MACRO_CHUNKS);
m_macro_receiving = false;
SMacroTable incoming;
if (complete) {
memcpy(&incoming, m_macro_buf, sizeof(incoming));
}
if (complete &&
macro_config_validate(incoming) &&
macro_config_save(incoming))
{
m_macros = incoming;
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);
} }
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 = SERIAL_PAYLOAD_BYTES;
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 43 const uint8_t chunks = MACRO_CHUNKS;
usb_serial_send(USB_EVT_MACRO_BEGIN, chunks); usb_serial_send(USB_EVT_MACRO_BEGIN, chunks);
@@ -316,43 +415,47 @@ void CMainController::poll_vendor()
// Verarbeitet alle Events in der Queue bis sie leer ist. // Verarbeitet alle Events in der Queue bis sie leer ist.
// Reihenfolge: ältestes Event zuerst (FIFO). // Reihenfolge: ältestes Event zuerst (FIFO).
// //
// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App // KEY_DOWN: execute_action_down() HID-Taste wird gedrückt, bleibt aktiv bis KEY_UP.
// gemeldet die App entscheidet dann was passiert (URL öffnen, Programm starten…). // KEY_UP: execute_action_up() HID-Taste wird losgelassen.
// Encoder CW/CCW: Host-Event mit Richtung oder HID-Tap-Sequenz.
void CMainController::processEvents() void CMainController::processEvents()
{ {
SEvent ev; SEvent ev;
while (m_queue.pop(ev)) { while (m_queue.pop(ev)) {
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) {
m_buttons[ev.key_id].on_press(); update_factory_reset_hold(ev.key_id, true);
execute_action(m_buttons[ev.key_id].action()); if (!(is_factory_reset_combo_active() && is_factory_reset_key(ev.key_id))) {
// Bei HOST_COMMAND: Event-ID an Windows-App senden execute_action_down(m_buttons[ev.key_id].action(), ev.key_id);
if (m_buttons[ev.key_id].action().type == ActionType::HOST_COMMAND) }
usb_serial_send(USB_EVT_KEY_DOWN, 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) {
m_buttons[ev.key_id].on_release(); 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:
if (ev.key_id < 4) { if (ev.key_id < 4) {
execute_action(m_enc_cw[ev.key_id]); execute_encoder_action(
if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND) m_enc_cw[ev.key_id], ev.key_id, USB_EVT_ENC_CW);
usb_serial_send(USB_EVT_ENC_CW, ev.key_id);
} }
break; break;
case EventType::ENC_CCW: case EventType::ENC_CCW:
if (ev.key_id < 4) { if (ev.key_id < 4) {
execute_action(m_enc_ccw[ev.key_id]); execute_encoder_action(
if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND) m_enc_ccw[ev.key_id], ev.key_id, USB_EVT_ENC_CCW);
usb_serial_send(USB_EVT_ENC_CCW, ev.key_id);
} }
break; break;
@@ -362,35 +465,171 @@ void CMainController::processEvents()
} }
} }
// Führt eine einzelne Aktion aus. bool CMainController::is_factory_reset_key(uint8_t key_id) const
// 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 return key_id == FACTORY_RESET_LEFT_KEY || key_id == FACTORY_RESET_RIGHT_KEY;
// usb_serial_send() an die Windows-App weitergeleitet. }
void CMainController::execute_action(SAction action)
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_all_keys();
usb_hid_release_all_consumers();
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: sendet KEY_UP mit Command-ID.
// MACRO/NONE: keine Aktion.
void CMainController::execute_action_down(SAction action, uint8_t key_id)
{ {
switch (action.type) { switch (action.type) {
case ActionType::HID_KEY: case ActionType::HID_KEY:
{
// data-Encoding: Low-Byte = Keycode, High-Byte = Modifier // data-Encoding: Low-Byte = Keycode, High-Byte = Modifier
usb_hid_send_key(static_cast<uint8_t>(action.data & 0xFF), uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
static_cast<uint8_t>(action.data >> 8)); uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up usb_hid_send_key(keycode, modifier);
usb_hid_release_key(); // Taste bleibt gedrückt bis execute_action_up() aufgerufen wird
break; break;
}
case ActionType::HID_CONSUMER: case ActionType::HID_CONSUMER:
{
usb_hid_send_consumer(action.data); usb_hid_send_consumer(action.data);
usb_hid_release_consumer(); // Consumer-Control bleibt aktiv bis execute_action_up() aufgerufen wird
break; break;
}
case ActionType::HOST_COMMAND: case ActionType::HOST_COMMAND:
// Wird in processEvents() über Serial gesendet // Command-ID little-endian in Byte 2/3 übertragen.
usb_serial_send(
USB_EVT_KEY_DOWN,
key_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
break; break;
case ActionType::MACRO: case ActionType::MACRO:
{ {
// Makro-Slot aus dem RAM ausführen (bei setup() aus NVM geladen). // Makros sind Sequenzen Steps mit keycode=0 werden übersprungen;
// Steps mit keycode=0 werden übersprungen; erstes leeres Step stoppt. // erstes leeres Step stoppt.
uint8_t slot = static_cast<uint8_t>(action.data); uint8_t slot = static_cast<uint8_t>(action.data);
if (slot >= MACRO_SLOTS) break; if (slot >= MACRO_SLOTS) break;
for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) { for (uint8_t i = 0; i < MACRO_MAX_STEPS; i++) {
@@ -398,18 +637,84 @@ void CMainController::execute_action(SAction action)
if (s.keycode == 0) break; if (s.keycode == 0) break;
usb_hid_send_key(s.keycode, s.modifier); usb_hid_send_key(s.keycode, s.modifier);
delay(10); delay(10);
usb_hid_release_key(); usb_hid_release_key(s.keycode, s.modifier);
delay(20); // Kurze Pause zwischen Steps damit der Host mitkommt delay(20); // Kurze Pause zwischen Steps damit der Host mitkommt
} }
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;
} }
} }
void CMainController::execute_action_up(SAction action, uint8_t key_id)
{
switch (action.type) {
case ActionType::HID_KEY:
{
uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
usb_hid_release_key(keycode, modifier);
break;
}
case ActionType::HID_CONSUMER:
usb_hid_release_consumer(action.data);
break;
case ActionType::HOST_COMMAND:
usb_serial_send(
USB_EVT_KEY_UP,
key_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
break;
case ActionType::MACRO:
case ActionType::NONE:
default:
// MACRO: Sequenz ist in execute_action_down() komplett abgelaufen, nop hier
// NONE: keine Aktion
break;
}
}
void CMainController::execute_encoder_action(
SAction action, uint8_t enc_id, uint8_t host_event)
{
if (action.type == ActionType::HOST_COMMAND) {
usb_serial_send(
host_event,
enc_id,
static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
return;
}
execute_action_down(action, enc_id);
delay(10);
execute_action_up(action, enc_id);
}
// ─── LED-Rendering ──────────────────────────────────────────────────────────── // ─── LED-Rendering ────────────────────────────────────────────────────────────
// //
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt // Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt
+41 -7
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
@@ -35,22 +36,55 @@ private:
SAction m_enc_cw[4]; SAction m_enc_cw[4];
SAction m_enc_ccw[4]; SAction m_enc_ccw[4];
void init_buttons(); // Buttons aus NVM-Config initialisieren void init_buttons(); // Buttons aus NVM-Config initialisieren
void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten
void processEvents(); // Queue leeren, Aktionen ausführen void processEvents(); // Queue leeren, Aktionen ausführen
void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial) void execute_action_down(SAction action, uint8_t key_id); // Taste drücken (Hold-Start)
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben void execute_action_up(SAction action, uint8_t key_id); // Taste losgelassen (Hold-Ende)
void execute_encoder_action(SAction action, uint8_t enc_id, uint8_t host_event);
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
enum : uint8_t {
SERIAL_PAYLOAD_BYTES = 6,
CONFIG_CHUNKS = (sizeof(SDeviceConfig) + SERIAL_PAYLOAD_BYTES - 1) /
SERIAL_PAYLOAD_BYTES,
MACRO_CHUNKS = (sizeof(SMacroTable) + SERIAL_PAYLOAD_BYTES - 1) /
SERIAL_PAYLOAD_BYTES,
};
// ── 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_received[CONFIG_CHUNKS];
uint8_t m_cfg_chunks_expected; uint8_t m_cfg_chunks_expected;
bool m_cfg_receiving; bool m_cfg_receiving;
bool m_cfg_transfer_valid;
// ── 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_received[MACRO_CHUNKS];
uint8_t m_macro_chunks_expected; uint8_t m_macro_chunks_expected;
bool m_macro_receiving; bool m_macro_receiving;
bool m_macro_transfer_valid;
static bool all_chunks_received(const uint8_t* received, uint8_t count);
// 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();
}; };
+6 -4
View File
@@ -6,14 +6,16 @@ enum class ActionType : uint8_t
NONE, // Keine Aktion NONE, // Keine Aktion
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, // Host-Event; data = Command-ID für die Desktop-App
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 02 oder 0x00FF/0xFFFF = nächstes Profil; speichert in NVM
}; };
struct __attribute__((packed)) SAction struct __attribute__((packed)) SAction
{ {
ActionType type; ActionType type;
uint16_t data; // Keycode (HID_KEY / HID_CONSUMER) oder Command-ID (HOST_COMMAND) uint16_t data; // Typabhängige Nutzdaten; HOST_COMMAND = Command-ID
// packed: 1B type + 2B data = 3B (kein Alignment-Padding) // packed: 1B type + 2B data = 3B (kein Alignment-Padding)
// Muss packed sein damit sizeof(SDeviceConfig)==163 == C#-Serialisierung // Muss packed sein, damit sizeof(SDeviceConfig)==740 und die
// hostseitige Serialisierung bytegenau übereinstimmen.
}; };
+50 -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,58 @@
#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_validate(const SMacroTable& tbl)
{
for (uint8_t slot = 0; slot < MACRO_SLOTS; slot++) {
for (uint8_t step = 0; step < MACRO_MAX_STEPS; step++) {
uint8_t keycode = tbl.steps[slot][step].keycode;
if (keycode > 0x65)
return false;
}
}
return true;
} }
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++) {
@@ -47,20 +68,32 @@ bool macro_config_load(SMacroTable& tbl)
memset(&tbl, 0, sizeof(tbl)); // Leere Tabelle als Default memset(&tbl, 0, sizeof(tbl)); // Leere Tabelle als Default
return false; return false;
} }
if (!macro_config_validate(tbl)) {
memset(&tbl, 0, sizeof(tbl));
return false;
}
return true; return true;
} }
void macro_config_save(const SMacroTable& tbl) bool macro_config_save(const SMacroTable& tbl)
{ {
if (!macro_config_validate(tbl)) return false;
// 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;
} }
+16 -8
View File
@@ -1,11 +1,11 @@
#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)
// Slot 2031 : Encoder-Aktionen (enc*3 + act_idx, 0=SW/1=CW/2=CCW) // Slot 2031 : Encoder-Aktionen (20 + enc*3 + act_idx, 0=SW/1=CW/2=CCW)
// //
// Ein Step mit keycode=0 gilt als leer → Ausführung stoppt dort. // Ein Step mit keycode=0 gilt als leer → Ausführung stoppt dort.
// Delay zwischen Steps: 20 ms (hardcoded). // Delay zwischen Steps: 20 ms (hardcoded).
@@ -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,23 @@ 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). static_assert(sizeof(SMacroStep) == 2, "SMacroStep binary layout changed");
static_assert(sizeof(SMacroTable) == 512, "SMacroTable binary layout changed");
// Prüft, dass alle belegten Steps in den vom HID-Descriptor unterstützten
// Keyboard-Usage-Bereich fallen.
bool macro_config_validate(const SMacroTable& tbl);
// 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);
+141 -56
View File
@@ -1,56 +1,61 @@
// 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 "macro_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;
@@ -61,62 +66,142 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
return crc; return crc;
} }
static bool action_valid(const SAction& action)
{
switch (action.type) {
case ActionType::NONE:
return true;
case ActionType::HID_KEY:
return static_cast<uint8_t>(action.data & 0xFF) <= 0x65;
case ActionType::HID_CONSUMER:
return action.data <= 0x03FF;
case ActionType::HOST_COMMAND:
return true;
case ActionType::MACRO:
return action.data < MACRO_SLOTS;
case ActionType::PROFILE_SWITCH:
return action.data <= 2 ||
action.data == 0x00FF ||
action.data == 0xFFFF;
default:
return false;
}
}
bool nvm_config_validate(const SDeviceConfig& cfg)
{
if (cfg.magic != NVM_CONFIG_MAGIC) return false;
if (cfg.version != NVM_CONFIG_VERSION) return false;
if (cfg.crc != nvm_config_crc(cfg)) return false;
if (cfg.active_profile >= 3) return false;
for (uint8_t p = 0; p < 3; p++) {
const SDeviceProfile& prof = cfg.profiles[p];
for (uint8_t i = 0; i < 20; i++) {
if (!action_valid(prof.mx_actions[i])) return false;
uint8_t anim = prof.led_anim[i];
if (anim > 6) return false; // LEDAnim::COLOR_FADE
if (anim == 2 && prof.led_period_ms[i] < 2) return false;
}
for (uint8_t enc = 0; enc < 4; enc++) {
for (uint8_t action = 0; action < 3; action++) {
if (!action_valid(prof.enc_actions[enc][action]))
return false;
}
}
}
return true;
}
// ── 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));
if (cfg.magic != NVM_CONFIG_MAGIC) { nvm_config_defaults(cfg); return false; } if (!nvm_config_validate(cfg)) {
if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; } nvm_config_defaults(cfg);
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; } return false;
}
return true; return true;
} }
// ── Speichern ───────────────────────────────────────────────────────────────── // ── Speichern ─────────────────────────────────────────────────────────────────
void nvm_config_save(const SDeviceConfig& cfg)
{
// Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist
uint8_t row[256];
memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg));
// Automatisches Schreiben deaktivieren (manueller Schreib-Modus) bool nvm_config_save(const SDeviceConfig& cfg)
{
SDeviceConfig stored = cfg;
stored.crc = nvm_config_crc(stored);
if (!nvm_config_validate(stored)) return false;
// 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, &stored, sizeof(stored));
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;
} }
+59 -37
View File
@@ -2,61 +2,83 @@
#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: led_anim + led_period_ms hinzugefügt #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)
}; };
static_assert(sizeof(SAction) == 3, "SAction binary layout changed");
static_assert(sizeof(SDeviceProfile) == 236, "SDeviceProfile binary layout changed");
static_assert(sizeof(SDeviceConfig) == 740, "SDeviceConfig binary layout changed");
// 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. // Vollständige Prüfung des persistenten/seriellen Binärvertrags inklusive CRC,
// Enum-Bereichen, Action-Nutzdaten und animationsspezifischen Mindestwerten.
bool nvm_config_validate(const SDeviceConfig& cfg);
// 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 (CRC wird intern neu berechnet; löscht 3 Rows,
void nvm_config_save(const SDeviceConfig& cfg); // 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 7739, nach dem crc-Feld)
uint16_t nvm_config_crc(const SDeviceConfig& cfg); uint16_t nvm_config_crc(const SDeviceConfig& cfg);
+2 -2
View File
@@ -19,7 +19,7 @@
#define BTN_COL_COUNT 5 #define BTN_COL_COUNT 5
#define BTN_ROW_COUNT 5 #define BTN_ROW_COUNT 5
// Column pins: driven OUTPUT LOW during scan, otherwise INPUT (high-Z or HIGH) // Column pins: INPUT; external 10k pull-ups hold them HIGH.
static const uint8_t BTN_COLS[BTN_COL_COUNT] = { static const uint8_t BTN_COLS[BTN_COL_COUNT] = {
PIN_COL0, // PB10 encoder SW column PIN_COL0, // PB10 encoder SW column
PIN_COL1, // PA11 Cherry MX col 1 (leftmost) PIN_COL1, // PA11 Cherry MX col 1 (leftmost)
@@ -28,7 +28,7 @@ static const uint8_t BTN_COLS[BTN_COL_COUNT] = {
PIN_COL4, // PA08 Cherry MX col 4 (rightmost) PIN_COL4, // PA08 Cherry MX col 4 (rightmost)
}; };
// Row pins: INPUT_PULLUP, read LOW when button pressed // Row pins: idle INPUT (high-Z), driven OUTPUT LOW one at a time during scan.
static const uint8_t BTN_ROWS[BTN_ROW_COUNT] = { static const uint8_t BTN_ROWS[BTN_ROW_COUNT] = {
PIN_ROW0, // PB11 PIN_ROW0, // PB11
PIN_ROW1, // PA12 PIN_ROW1, // PA12
+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);
} }
} }
+125 -14
View File
@@ -1,9 +1,10 @@
#include "usb_hid.h" #include "usb_hid.h"
#include <Arduino.h> #include <Arduino.h>
#include <HID.h> #include <HID.h>
#include <string.h>
// ── HID Report Descriptor: Keyboard + Consumer Control ─────────────────────── // ── HID Report Descriptor: Keyboard + Consumer Control ───────────────────────
// Vendor-Kommunikation läuft über CVendorHID (eigenes PluggableUSBModule). // Host-Kommunikation außerhalb von HID läuft separat über USB CDC (SerialUSB).
static const uint8_t k_hid_descriptor[] = { static const uint8_t k_hid_descriptor[] = {
@@ -67,30 +68,140 @@ struct ConsumerReport {
uint16_t usage; uint16_t usage;
}; };
void usb_hid_init() {} static uint8_t s_key_refcount[256] = {};
static uint8_t s_modifier_refcount[8] = {};
void usb_hid_send_key(uint8_t keycode, uint8_t modifier) struct ConsumerState {
uint16_t usage;
uint8_t refcount;
uint32_t order;
};
static constexpr uint8_t CONSUMER_STATE_SLOTS = 8;
static ConsumerState s_consumer_state[CONSUMER_STATE_SLOTS] = {};
static uint32_t s_consumer_order = 0;
static void send_keyboard_state()
{ {
KeyboardReport report = {}; KeyboardReport report = {};
report.modifier = modifier;
report.keycodes[0] = keycode; for (uint8_t bit = 0; bit < 8; bit++) {
if (s_modifier_refcount[bit] > 0)
report.modifier |= static_cast<uint8_t>(1u << bit);
}
uint8_t out = 0;
for (uint16_t key = 1; key < 256 && out < 6; key++) {
if (s_key_refcount[key] > 0)
report.keycodes[out++] = static_cast<uint8_t>(key);
}
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report)); HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
} }
void usb_hid_release_key() static void send_consumer_state()
{ {
KeyboardReport report = {}; uint16_t usage = 0;
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report)); uint32_t newest = 0;
}
for (uint8_t i = 0; i < CONSUMER_STATE_SLOTS; i++) {
if (s_consumer_state[i].refcount > 0 &&
s_consumer_state[i].order >= newest)
{
newest = s_consumer_state[i].order;
usage = s_consumer_state[i].usage;
}
}
void usb_hid_send_consumer(uint16_t usage)
{
ConsumerReport report = { usage }; ConsumerReport report = { usage };
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report)); HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
} }
void usb_hid_release_consumer() void usb_hid_init()
{ {
ConsumerReport report = { 0 }; memset(s_key_refcount, 0, sizeof(s_key_refcount));
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report)); memset(s_modifier_refcount, 0, sizeof(s_modifier_refcount));
memset(s_consumer_state, 0, sizeof(s_consumer_state));
s_consumer_order = 0;
}
void usb_hid_send_key(uint8_t keycode, uint8_t modifier)
{
if (keycode != 0 && s_key_refcount[keycode] < 0xFF)
s_key_refcount[keycode]++;
for (uint8_t bit = 0; bit < 8; bit++) {
if ((modifier & (1u << bit)) != 0 && s_modifier_refcount[bit] < 0xFF)
s_modifier_refcount[bit]++;
}
send_keyboard_state();
}
void usb_hid_release_key(uint8_t keycode, uint8_t modifier)
{
if (keycode != 0 && s_key_refcount[keycode] > 0)
s_key_refcount[keycode]--;
for (uint8_t bit = 0; bit < 8; bit++) {
if ((modifier & (1u << bit)) != 0 && s_modifier_refcount[bit] > 0)
s_modifier_refcount[bit]--;
}
send_keyboard_state();
}
void usb_hid_release_all_keys()
{
memset(s_key_refcount, 0, sizeof(s_key_refcount));
memset(s_modifier_refcount, 0, sizeof(s_modifier_refcount));
send_keyboard_state();
}
void usb_hid_send_consumer(uint16_t usage)
{
ConsumerState* free_slot = nullptr;
for (uint8_t i = 0; i < CONSUMER_STATE_SLOTS; i++) {
ConsumerState& state = s_consumer_state[i];
if (state.refcount > 0 && state.usage == usage) {
if (state.refcount < 0xFF) state.refcount++;
state.order = ++s_consumer_order;
send_consumer_state();
return;
}
if (state.refcount == 0 && free_slot == nullptr)
free_slot = &state;
}
if (free_slot != nullptr) {
free_slot->usage = usage;
free_slot->refcount = 1;
free_slot->order = ++s_consumer_order;
}
send_consumer_state();
}
void usb_hid_release_consumer(uint16_t usage)
{
for (uint8_t i = 0; i < CONSUMER_STATE_SLOTS; i++) {
ConsumerState& state = s_consumer_state[i];
if (state.refcount > 0 && state.usage == usage) {
state.refcount--;
if (state.refcount == 0) {
state.usage = 0;
state.order = 0;
}
break;
}
}
send_consumer_state();
}
void usb_hid_release_all_consumers()
{
memset(s_consumer_state, 0, sizeof(s_consumer_state));
send_consumer_state();
} }
+8 -2
View File
@@ -28,8 +28,14 @@
void usb_hid_init(); void usb_hid_init();
// Keyboard-Zustand wird referenzgezählt. Dadurch bleiben andere gehaltene
// Tasten/Modifier aktiv, wenn genau eine Action losgelassen wird.
void usb_hid_send_key(uint8_t keycode, uint8_t modifier = 0); void usb_hid_send_key(uint8_t keycode, uint8_t modifier = 0);
void usb_hid_release_key(); void usb_hid_release_key(uint8_t keycode, uint8_t modifier = 0);
void usb_hid_release_all_keys();
// Der Consumer-Descriptor kann jeweils ein Usage übertragen. Mehrere Holds
// werden intern verwaltet; sichtbar bleibt das zuletzt gedrückte aktive Usage.
void usb_hid_send_consumer(uint16_t usage); void usb_hid_send_consumer(uint16_t usage);
void usb_hid_release_consumer(); void usb_hid_release_consumer(uint16_t usage);
void usb_hid_release_all_consumers();
+4 -3
View File
@@ -6,7 +6,7 @@
// alle verfügbaren Bytes in einen internen Ring-Buffer und gibt ein vollständiges // alle verfügbaren Bytes in einen internen Ring-Buffer und gibt ein vollständiges
// 8-Byte-Paket zurück sobald genug Bytes akkumuliert sind. // 8-Byte-Paket zurück sobald genug Bytes akkumuliert sind.
// Der Ring-Buffer (256 Bytes = 32 Pakete) verhindert Datenverlust wenn mehrere // Der Ring-Buffer (256 Bytes = 32 Pakete) verhindert Datenverlust wenn mehrere
// Pakete auf einmal ankommen (Config-Transfer: 30 Pakete). // Pakete auf einmal ankommen. Größere Transfers werden fortlaufend geleert.
// //
// Senden (Board → PC): // Senden (Board → PC):
// Direkt via SerialUSB.write() kein eigener Puffer nötig, da der Arduino-CDC- // Direkt via SerialUSB.write() kein eigener Puffer nötig, da der Arduino-CDC-
@@ -16,8 +16,9 @@
#include <Arduino.h> #include <Arduino.h>
// Ring-Buffer für eingehende Bytes CDC kann jederzeit Bytes liefern. // Ring-Buffer für eingehende Bytes CDC kann jederzeit Bytes liefern.
// Größe: 32 Pakete × 8 Bytes = 256 Bytes reicht für eine vollständige // Größe: 32 Pakete × 8 Bytes = 256 Bytes. Der Puffer ist nur ein
// Config-Übertragung (30 Pakete) ohne Überlauf. // Zwischenpuffer; ein vollständiger Config-Transfer umfasst 126 Pakete
// einschließlich BEGIN und COMMIT.
static uint8_t s_buf[SERIAL_PKT_SIZE * 32]; static uint8_t s_buf[SERIAL_PKT_SIZE * 32];
static uint16_t s_head = 0; static uint16_t s_head = 0;
static uint16_t s_count = 0; static uint16_t s_count = 0;
+7 -8
View File
@@ -8,10 +8,8 @@
// Byte-Layout aller Pakete: // Byte-Layout aller Pakete:
// [0] Command/Event-ID // [0] Command/Event-ID
// [1] key_id (Button 024 oder Encoder 03) // [1] key_id (Button 024 oder Encoder 03)
// [2] r / Daten-Byte A // [2..7] kommandospezifische Daten
// [3] g / Daten-Byte B // LED-Kommandos nutzen [2..4] als RGB; Config-/Makro-DATA nutzt [2..7].
// [4] b
// [5..7] reserviert (0x00)
// //
// Richtungen: // Richtungen:
// PC → Board (Commands, 0x010x7F): poll_vendor() in CMainController // PC → Board (Commands, 0x010x7F): poll_vendor() in CMainController
@@ -43,10 +41,10 @@
#define USB_CMD_MACRO_READ 0x23 // Board sendet aktuelle Makro-Tabelle zurück #define USB_CMD_MACRO_READ 0x23 // Board sendet aktuelle Makro-Tabelle zurück
// ── Events: Board → PC ──────────────────────────────────────────────────────── // ── Events: Board → PC ────────────────────────────────────────────────────────
#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt #define USB_EVT_KEY_DOWN 0x81 // key_id + Command-ID in Data[2..3]
#define USB_EVT_KEY_UP 0x82 // key_id → HOST_COMMAND-Button losgelassen #define USB_EVT_KEY_UP 0x82 // key_id + Command-ID in Data[2..3]
#define USB_EVT_ENC_CW 0x83 // enc_id → Encoder Schritt CW (HOST_COMMAND) #define USB_EVT_ENC_CW 0x83 // enc_id + Command-ID in Data[2..3]
#define USB_EVT_ENC_CCW 0x84 // enc_id → Encoder Schritt CCW (HOST_COMMAND) #define USB_EVT_ENC_CCW 0x84 // enc_id + Command-ID in Data[2..3]
#define USB_EVT_PONG 0x85 // Antwort auf USB_CMD_PING #define USB_EVT_PONG 0x85 // Antwort auf USB_CMD_PING
#define USB_EVT_CONFIG_ACK 0x90 // Config erfolgreich in NVM geschrieben #define USB_EVT_CONFIG_ACK 0x90 // Config erfolgreich in NVM geschrieben
#define USB_EVT_CONFIG_NACK 0x91 // Config CRC/Magic ungültig nicht geschrieben #define USB_EVT_CONFIG_NACK 0x91 // Config CRC/Magic ungültig nicht geschrieben
@@ -54,6 +52,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
@@ -11,9 +11,9 @@ SEARCH_DIR(.)
MEMORY MEMORY
{ {
rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x0001FE00 /* 127.5K Firmware */ rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x0001FB00 /* 126.75K Firmware */
config (rx) : ORIGIN = 0x0001FE00, LENGTH = 0x00000200 /* 512B NVM Config (2 Rows) */ nvm (rx) : ORIGIN = 0x0001FB00, LENGTH = 0x00000500 /* 1.25K Makros + Config */
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
} }
/* Initial stack pointer = top of RAM */ /* Initial stack pointer = top of RAM */
+3 -3
View File
@@ -1,4 +1,4 @@
// VersaPad v2 SAMD21G18A Custom Variant // VersaPad v2 SAMD21G17D Custom Variant
// Pin descriptions and peripheral object definitions // Pin descriptions and peripheral object definitions
#include "variant.h" #include "variant.h"
@@ -15,7 +15,7 @@
const PinDescription g_APinDescription[] = { const PinDescription g_APinDescription[] = {
// ── Button Matrix: Columns (D0D4) ──────────────────────────────────────── // ── Button Matrix: Columns (D0D4) ────────────────────────────────────────
// Driven LOW one at a time during scanning; idle = INPUT_PULLUP or OUTPUT HIGH // Inputs with external pull-ups; read LOW through a pressed switch.
// D0 PA08 COL_4 // D0 PA08 COL_4
{ PORTA, 8, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NMI }, { PORTA, 8, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NMI },
@@ -29,7 +29,7 @@ const PinDescription g_APinDescription[] = {
{ PORTB, 10, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_10 }, { PORTB, 10, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_10 },
// ── Button Matrix: Rows (D5D9) ─────────────────────────────────────────── // ── Button Matrix: Rows (D5D9) ───────────────────────────────────────────
// Read as INPUT_PULLUP; go LOW when a button in the active column is pressed // Idle high-Z; driven LOW one at a time during scanning.
// D5 PB11 ROW_0 // D5 PB11 ROW_0
{ PORTB, 11, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 }, { PORTB, 11, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 },
+3 -3
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
// VersaPad v2 SAMD21G18A Custom Variant // VersaPad v2 SAMD21G17D Custom Variant
// Arduino pin assignments for the custom PCB // Arduino pin assignments for the custom PCB
#define ARDUINO_SAMD_VARIANT_COMPLIANCE 10610 #define ARDUINO_SAMD_VARIANT_COMPLIANCE 10610
@@ -24,14 +24,14 @@
// attachInterrupt() then internally looks up ulExtInt via g_APinDescription. // attachInterrupt() then internally looks up ulExtInt via g_APinDescription.
// ─── Button Matrix ──────────────────────────────────────────────────────────── // ─── Button Matrix ────────────────────────────────────────────────────────────
// Columns (driven LOW one at a time) // Columns (inputs with external pull-ups; read LOW for a pressed switch)
#define PIN_COL0 (4u) // PB10 also encoder SW column #define PIN_COL0 (4u) // PB10 also encoder SW column
#define PIN_COL1 (3u) // PA11 #define PIN_COL1 (3u) // PA11
#define PIN_COL2 (2u) // PA10 #define PIN_COL2 (2u) // PA10
#define PIN_COL3 (1u) // PA09 #define PIN_COL3 (1u) // PA09
#define PIN_COL4 (0u) // PA08 #define PIN_COL4 (0u) // PA08
// Rows (read with internal pull-up) // Rows (idle high-Z; driven LOW one at a time during scanning)
#define PIN_ROW0 (5u) // PB11 #define PIN_ROW0 (5u) // PB11
#define PIN_ROW1 (6u) // PA12 #define PIN_ROW1 (6u) // PA12
#define PIN_ROW2 (7u) // PA13 #define PIN_ROW2 (7u) // PA13