Compare commits
2 Commits
ac3b2aa90f
...
ce5db617a1
| Author | SHA1 | Date | |
|---|---|---|---|
| ce5db617a1 | |||
| 50dbf8fbee |
@@ -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?
|
||||
@@ -1,33 +1,59 @@
|
||||
# VersaMCU
|
||||
|
||||
Firmware fuer das VersaPad v2 Macro-Pad.
|
||||
Laeuft auf einem ATSAMD21G17D mit PlatformIO und Arduino-Framework.
|
||||
Firmware für das VersaPad-v2-Makropad. Das Projekt läuft auf einem
|
||||
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
|
||||
|
||||
| Eigenschaft | Detail |
|
||||
|---|---|
|
||||
| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
|
||||
| Flash / RAM | 128 KB / 16 KB |
|
||||
| USB | Composite: HID Keyboard + Consumer + CDC Serial |
|
||||
| Matrix | 5x5 logisch, davon 20 MX-Buttons + 4 Encoder-SW + 1 unbelegt |
|
||||
| Encoder | 4x Rotary Encoder mit Quadratur via EIC-Interrupt |
|
||||
| LEDs | 20x WS2812B an `PB22` |
|
||||
| Programmer | Atmel-ICE via SWD, kein Bootloader |
|
||||
| Flash / RAM | 128 KiB / 16 KiB |
|
||||
| Matrix | logisch 5×5: 20 MX, 4 Encoder-SW, 1 unbelegt |
|
||||
| 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 |
|
||||
|
||||
## Build und Flash
|
||||
## Schnellstart
|
||||
|
||||
Voraussetzungen sind PlatformIO Core oder die PlatformIO IDE sowie für den
|
||||
Upload ein angeschlossener Atmel-ICE beziehungsweise kompatibler
|
||||
CMSIS-DAP-Adapter.
|
||||
|
||||
```bash
|
||||
pio run
|
||||
pio run --target upload
|
||||
pio run -e versapad
|
||||
pio run -e versapad --target upload
|
||||
```
|
||||
|
||||
Der Upload laeuft per OpenOCD ueber SWD.
|
||||
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).
|
||||
|
||||
## Laufzeitmodell
|
||||
|
||||
`main.cpp` startet genau einen `CMainController`.
|
||||
Die Hauptschleife in `work()` ist:
|
||||
`main.cpp` besitzt genau einen `CMainController`. Nach einem roten Startsignal
|
||||
initialisiert er NVM, USB, Matrix und Encoder. Die Hauptschleife ist:
|
||||
|
||||
```text
|
||||
matrix_scan()
|
||||
@@ -37,133 +63,97 @@ check_factory_reset()
|
||||
updateLEDs()
|
||||
```
|
||||
|
||||
Dabei gilt:
|
||||
- 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.
|
||||
|
||||
- Matrix und Encoder erzeugen `SEvent`s.
|
||||
- `processEvents()` fuehrt daraus HID, Makros, Host-Commands oder Profilwechsel aus.
|
||||
- `poll_vendor()` verarbeitet das 8-Byte-CDC-Protokoll mit Config- und Makro-Transfers.
|
||||
- `updateLEDs()` rendert nur dann zu den WS2812, wenn sich etwas geaendert hat.
|
||||
## Wichtige Datenverträge
|
||||
|
||||
## Action-System
|
||||
### Config v3
|
||||
|
||||
Unterstuetzte `ActionType`s:
|
||||
|
||||
| Typ | Verhalten |
|
||||
|---|---|
|
||||
| `NONE` | keine Aktion |
|
||||
| `HID_KEY` | Keyboard-Hold ueber USB HID |
|
||||
| `HID_CONSUMER` | Media/Consumer-Hold ueber USB HID |
|
||||
| `HOST_COMMAND` | Event an die GUI per CDC Serial |
|
||||
| `MACRO` | Firmware spielt Makro-Slot komplett ab |
|
||||
| `PROFILE_SWITCH` | aktives Profil in NVM wechseln |
|
||||
|
||||
Wichtige Semantik:
|
||||
|
||||
- normale Keys und Consumer folgen dem Hold-Modell
|
||||
- Encoder `CW` / `CCW` sind immer Tap-Events
|
||||
- Makros laufen komplett in der Firmware, ohne laufende App
|
||||
|
||||
## LED-System
|
||||
|
||||
Jeder MX-Button hat:
|
||||
|
||||
- eine Base-Farbe
|
||||
- optional eine temporaere Override-Farbe
|
||||
- eine Animation
|
||||
|
||||
Aktuelle Animationsmodi:
|
||||
|
||||
- `STATIC`
|
||||
- `BLINK`
|
||||
- `PULSE`
|
||||
- `FADE_IN`
|
||||
- `FADE_OUT`
|
||||
- `COLOR_CYCLE`
|
||||
- `COLOR_FADE`
|
||||
|
||||
Die GUI nutzt derzeit vor allem `STATIC`, `BLINK`, `PULSE` und `COLOR_CYCLE`.
|
||||
|
||||
## Aktuelles NVM-Layout
|
||||
|
||||
### DeviceConfig
|
||||
|
||||
- Version: `3`
|
||||
- Magic: `0x56503203`
|
||||
- Groesse: `740` Byte
|
||||
- CRC16-CCITT ueber Bytes `7..739`
|
||||
- Magic `0x56503203`
|
||||
- `SDeviceConfig`: 740 Byte
|
||||
- CRC16-CCITT über Bytes `7..739`
|
||||
- 3 Profile
|
||||
- globale Helligkeit
|
||||
- per-LED-Helligkeit
|
||||
- globale und LED-spezifische Helligkeit
|
||||
- 124 Chunks mit je 6 Nutzbytes beim CDC-Transfer
|
||||
|
||||
### MacroTable
|
||||
### Makros
|
||||
|
||||
- 32 Slots
|
||||
- 8 Steps pro Slot
|
||||
- 512 Byte gesamt
|
||||
- `SMacroTable`: 512 Byte
|
||||
- 32 Slots × 8 Schritte × 2 Byte
|
||||
- 86 Chunks mit je 6 Nutzbytes beim CDC-Transfer
|
||||
|
||||
### Flash-Bereich
|
||||
### Flashzugriffe
|
||||
|
||||
| Bereich | Adresse | Groesse |
|
||||
| Bereich | Adresse | Größe |
|
||||
|---|---|---|
|
||||
| Makros | `0x1FB00-0x1FCFF` | 512 B |
|
||||
| Config | `0x1FD00-0x1FFFF` | 768 B, davon 740 B genutzt |
|
||||
| Makros | `0x1FB00..0x1FCFF` | 512 B |
|
||||
| Config | `0x1FD00..0x1FFFF` | 768 B, davon 740 B genutzt |
|
||||
|
||||
Config und Makros liegen in getrennten reservierten NVM-Bereichen.
|
||||
|
||||
Beim Serial-Dump der Config werden 124 Chunks zu je 6 Nutzbytes uebertragen. Implementierungen muessen den daraus berechneten Byte-Offset mindestens 16 Bit breit halten, weil Profil 2 und 3 hinter Byte 255 liegen.
|
||||
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
|
||||
|
||||
Die Firmware hat einen eingebauten Recovery-Pfad:
|
||||
Unteren linken und unteren rechten MX-Button gleichzeitig fünf Sekunden
|
||||
halten:
|
||||
|
||||
- unteren linken und unteren rechten MX-Button gleichzeitig 5 Sekunden halten
|
||||
- waehrend des Holds leuchten diese beiden Tasten rot
|
||||
- ihre normalen HID-Aktionen werden waehrenddessen unterdrueckt
|
||||
- bei Erfolg blinken alle LEDs kurz rot
|
||||
- danach werden Config und Makros auf Werkseinstellungen zurueckgesetzt und neu geladen
|
||||
- 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.
|
||||
|
||||
Reset-Inhalt:
|
||||
|
||||
- alle Aktionen `NONE`
|
||||
- alle Makro-Slots leer
|
||||
- Base-LEDs auf Defaultwerte
|
||||
- sichtbarer Idle-Zustand wieder Regenbogen
|
||||
|
||||
Wichtig:
|
||||
|
||||
- ein SWD-Reflash loescht diese NVM-Daten nicht automatisch
|
||||
- der Werksreset ist der vorgesehene Weg, um eine kaputte Konfiguration zu bereinigen
|
||||
Ein normaler SWD-Reflash löscht diese NVM-Daten nicht automatisch.
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```text
|
||||
VersaMCU/
|
||||
|-- AGENTS.md # Kontext und Richtlinien für Coding-LLMs
|
||||
|-- README.md
|
||||
|-- platformio.ini
|
||||
|-- boards/
|
||||
|-- variants/versapad/
|
||||
|-- boards/ # PlatformIO-Boarddefinitionen
|
||||
|-- variants/versapad/ # Pinmapping und Linker-Skripte
|
||||
|-- doc/ # Architektur- und Protokolldokumentation
|
||||
`-- src/
|
||||
|-- main.cpp
|
||||
|-- CMainController.h/.cpp
|
||||
|-- CButton.h/.cpp
|
||||
|-- CEventQueue.h/.cpp
|
||||
|-- SEvent.h
|
||||
|-- config/
|
||||
| |-- action.h
|
||||
| |-- macro_config.h/.cpp
|
||||
| `-- nvm_config.h/.cpp
|
||||
`-- hal/
|
||||
|-- encoder.h/.cpp
|
||||
|-- matrix.h/.cpp
|
||||
|-- usb_hid.h/.cpp
|
||||
|-- usb_serial.h/.cpp
|
||||
`-- ws2812.h/.cpp
|
||||
|-- CMainController.* # Orchestrierung
|
||||
|-- CButton.* # Actions und LED-Zustand
|
||||
|-- CEventQueue.* # feste Event-Queue
|
||||
|-- config/ # Binärformate, NVM und Pins
|
||||
`-- hal/ # Matrix, Encoder, HID, CDC, WS2812
|
||||
```
|
||||
|
||||
## Weiterfuehrende Doku
|
||||
## Einstieg für Entwickler und LLMs
|
||||
|
||||
- [doc/INDEX.md](doc/INDEX.md)
|
||||
- [doc/00_architecture.md](doc/00_architecture.md)
|
||||
- [doc/03_action_engine.md](doc/03_action_engine.md)
|
||||
- [doc/04_macro_system.md](doc/04_macro_system.md)
|
||||
- [doc/06_nvm_config.md](doc/06_nvm_config.md)
|
||||
- [doc/07_serial_protocol.md](doc/07_serial_protocol.md)
|
||||
Für einen neuen Kollegen:
|
||||
|
||||
1. [Entwicklung und Einstieg](doc/08_development.md)
|
||||
2. [Architektur](doc/00_architecture.md)
|
||||
3. die zum Task passende Fachdokumentation im [Dokumentationsindex](doc/INDEX.md)
|
||||
4. [bekannte Einschränkungen](doc/09_known_limitations.md)
|
||||
|
||||
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
|
||||
|
||||
- [Dokumentationsindex](doc/INDEX.md)
|
||||
- [Architektur](doc/00_architecture.md)
|
||||
- [Matrix](doc/01_matrix.md)
|
||||
- [Encoder](doc/02_encoder.md)
|
||||
- [Action-Engine](doc/03_action_engine.md)
|
||||
- [Makros](doc/04_macro_system.md)
|
||||
- [LED-System](doc/05_led_system.md)
|
||||
- [NVM-Config](doc/06_nvm_config.md)
|
||||
- [CDC-Protokoll](doc/07_serial_protocol.md)
|
||||
- [Entwicklung und Einstieg](doc/08_development.md)
|
||||
- [Bekannte Einschränkungen](doc/09_known_limitations.md)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"name": "VersaPad v2 (Atmel-ICE, no bootloader)",
|
||||
"upload": {
|
||||
"maximum_ram_size": 16384,
|
||||
"maximum_size": 131072,
|
||||
"maximum_size": 129792,
|
||||
"protocol": "atmel-ice",
|
||||
"require_upload_port": false,
|
||||
"use_1200bps_touch": false
|
||||
|
||||
+32
-14
@@ -1,4 +1,4 @@
|
||||
# VersaMCU - Architekturuebersicht
|
||||
# VersaMCU – Architekturübersicht
|
||||
|
||||
## Zielplattform
|
||||
|
||||
@@ -7,23 +7,27 @@
|
||||
| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
|
||||
| Flash | 128 KB |
|
||||
| RAM | 16 KB |
|
||||
| FPU | keine, deshalb Integer-Arithmetik |
|
||||
| FPU | keine; LED-/Timingpfade verwenden Integer-Arithmetik |
|
||||
| USB | HID Keyboard + Consumer + CDC Serial |
|
||||
| Toolchain | PlatformIO + Arduino Core |
|
||||
|
||||
## Setup und Loop
|
||||
|
||||
```text
|
||||
setup()
|
||||
macro_config_load()
|
||||
nvm_config_load()
|
||||
init_buttons()
|
||||
usb_hid_init()
|
||||
usb_serial_init()
|
||||
matrix_init(cb)
|
||||
encoder_init(cb)
|
||||
Arduino setup()
|
||||
delay(500)
|
||||
ws2812_init()
|
||||
rotes Startsignal für 1 s
|
||||
CMainController::setup()
|
||||
macro_config_load()
|
||||
init_buttons() -> nvm_config_load()
|
||||
Queue-Bridge setzen
|
||||
usb_hid_init()
|
||||
usb_serial_init()
|
||||
matrix_init(cb)
|
||||
encoder_init(cb)
|
||||
|
||||
loop()
|
||||
Arduino loop()
|
||||
matrix_scan()
|
||||
poll_vendor()
|
||||
processEvents()
|
||||
@@ -39,6 +43,9 @@ Die Reihenfolge ist absichtlich simpel:
|
||||
- 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
|
||||
|
||||
```text
|
||||
@@ -67,7 +74,7 @@ LED-Render
|
||||
| `main.cpp` | startet den Controller |
|
||||
| `CMainController.*` | Orchestrator fuer Inputs, Actions, Serial, LEDs |
|
||||
| `CButton.*` | LED-Zustand, Animationen, Action-Referenz |
|
||||
| `CEventQueue.*` | ISR-sicherer Ringbuffer |
|
||||
| `CEventQueue.*` | fester Ringbuffer mit 16 nutzbaren Slots |
|
||||
| `config/nvm_config.*` | Config v3 laden, speichern, Defaults |
|
||||
| `config/macro_config.*` | Makros laden, speichern |
|
||||
| `hal/matrix.*` | 5x5-Matrixscan mit Debounce |
|
||||
@@ -76,6 +83,9 @@ LED-Render
|
||||
| `hal/usb_serial.*` | CDC-Paketpfad |
|
||||
| `hal/ws2812.*` | WS2812-Treiber |
|
||||
|
||||
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
|
||||
|
||||
```text
|
||||
@@ -100,8 +110,16 @@ Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware:
|
||||
|
||||
## Invarianten
|
||||
|
||||
- kein Heap
|
||||
- keine Floats
|
||||
- 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
@@ -41,5 +41,8 @@ key_id = col * MATRIX_ROWS + row
|
||||
## Kontext
|
||||
|
||||
- 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
|
||||
- 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.
|
||||
|
||||
+6
-1
@@ -41,8 +41,13 @@ static void isr_enc0_b() { handle_encoder(0); }
|
||||
## ISR-Sicherheit
|
||||
|
||||
- `s_state[]` und `s_accum[]` sind `volatile`
|
||||
- `CEventQueue::push()` ist ISR-sicher (atomare Index-Inkremente auf Single-Core-M0+, kein Heap)
|
||||
- Der Callback-Pointer `s_cb` wird einmalig in `setup()` gesetzt, bevor Interrupts aktiviert werden
|
||||
- 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
|
||||
|
||||
|
||||
+28
-4
@@ -25,9 +25,9 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
|
||||
| `NONE` | keine Aktion | - |
|
||||
| `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier |
|
||||
| `HID_CONSUMER` | Media/Consumer-HID | usage id |
|
||||
| `HOST_COMMAND` | Event an die GUI | command id |
|
||||
| `HOST_COMMAND` | Event an die GUI | 16-Bit-Command-ID |
|
||||
| `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
|
||||
| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil |
|
||||
| `PROFILE_SWITCH` | Profilwechsel | 0..2, `0x00FF` oder `0xFFFF` für nächstes Profil |
|
||||
|
||||
## Verhalten bei `KEY_DOWN`
|
||||
|
||||
@@ -35,7 +35,7 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
|
||||
|---|---|
|
||||
| `HID_KEY` | `usb_hid_send_key()` |
|
||||
| `HID_CONSUMER` | `usb_hid_send_consumer()` |
|
||||
| `HOST_COMMAND` | `usb_serial_send(KEY_DOWN/ENC_*)` |
|
||||
| `HOST_COMMAND` | `USB_EVT_KEY_DOWN (0x81)` mit `key_id` und Command-ID senden |
|
||||
| `MACRO` | komplette Sequenz sofort abspielen |
|
||||
| `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren |
|
||||
| `NONE` | nichts |
|
||||
@@ -46,7 +46,7 @@ Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI ue
|
||||
|---|---|
|
||||
| `HID_KEY` | `usb_hid_release_key()` |
|
||||
| `HID_CONSUMER` | `usb_hid_release_consumer()` |
|
||||
| `HOST_COMMAND` | optionaler Up-Pfad, derzeit praktisch ohne Nutzlast |
|
||||
| `HOST_COMMAND` | `USB_EVT_KEY_UP (0x82)` mit `key_id` und Command-ID senden |
|
||||
| `MACRO` | nichts |
|
||||
| `PROFILE_SWITCH` | nichts |
|
||||
| `NONE` | nichts |
|
||||
@@ -62,6 +62,30 @@ down -> delay(10 ms) -> up
|
||||
|
||||
- Makros laufen komplett synchron in der Firmware.
|
||||
|
||||
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.
|
||||
|
||||
Der Consumer-Descriptor kann jeweils nur ein Usage darstellen. Der HAL
|
||||
verwaltet mehrere Holds und zeigt das zuletzt gedrückte aktive Usage; nach
|
||||
dessen Release wird das zuvor aktive Usage wiederhergestellt.
|
||||
|
||||
## Host-Commands
|
||||
|
||||
Der aktuelle Code sendet für jede `HOST_COMMAND`-Action nur:
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
+12
-5
@@ -41,7 +41,10 @@ Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung:
|
||||
| Slots | Bedeutung |
|
||||
|---|---|
|
||||
| `0..19` | MX-Buttons |
|
||||
| `20..31` | Encoder-Aktionen (`enc * 3 + act_idx`) |
|
||||
| `20..31` | Encoder-Aktionen (`20 + enc * 3 + act_idx`) |
|
||||
|
||||
Diese Zuordnung ist eine Konvention der externen GUI. Die Firmware erzwingt
|
||||
sie nicht: `SAction.data` wird direkt als Slotindex `0..31` verwendet.
|
||||
|
||||
## Laden
|
||||
|
||||
@@ -49,7 +52,8 @@ Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung:
|
||||
|
||||
- kopiert 512 Byte aus NVM in `SMacroTable`
|
||||
- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
|
||||
- setzt dann eine leere Tabelle
|
||||
- prüft alle belegten Steps gegen den HID-Descriptor (`keycode <= 0x65`)
|
||||
- setzt bei gelöschtem oder ungültigem Inhalt eine leere Tabelle
|
||||
|
||||
Eine leere Tabelle ist also ein gueltiger Default-Zustand.
|
||||
|
||||
@@ -57,15 +61,18 @@ Eine leere Tabelle ist also ein gueltiger Default-Zustand.
|
||||
|
||||
`macro_config_save()`:
|
||||
|
||||
1. Tabelle in einen 4-Byte-aligned Puffer kopieren
|
||||
2. beide Rows loeschen
|
||||
3. 8 Pages zu je 64 Byte schreiben
|
||||
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:
|
||||
|
||||
+22
-4
@@ -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_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)
|
||||
|
||||
@@ -34,7 +37,7 @@ Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sof
|
||||
| Animation | Typ | Verhalten | Endbedingung |
|
||||
|---|---|---|---|
|
||||
| `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 |
|
||||
| `FADE_IN` | Helligkeit | Einmalig schwarz → voll | → STATIC (voll) |
|
||||
| `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).
|
||||
|
||||
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)
|
||||
|
||||
Hue 0–255 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.
|
||||
|
||||
`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)
|
||||
|
||||
|
||||
+21
-5
@@ -17,6 +17,11 @@ Dateien:
|
||||
|
||||
Makros und Config sind komplett getrennt.
|
||||
|
||||
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.
|
||||
|
||||
## `SDeviceConfig`
|
||||
|
||||
Aktueller Stand:
|
||||
@@ -38,6 +43,9 @@ Aktueller Stand:
|
||||
| `9` | 4 | `enc_sensitivity[4]` |
|
||||
| `13` | 19 | Reserve |
|
||||
|
||||
`enc_sensitivity` ist im Binärformat vorhanden und hat Default `1`, wird von
|
||||
der Encoderdekodierung derzeit aber nicht verwendet.
|
||||
|
||||
### Pro Profil
|
||||
|
||||
Jedes Profil belegt 236 Byte:
|
||||
@@ -91,18 +99,24 @@ Praktisch sichtbares Ergebnis:
|
||||
`nvm_config_load()`:
|
||||
|
||||
1. 740 Byte aus NVM kopieren
|
||||
2. Magic pruefen
|
||||
3. Version pruefen
|
||||
4. CRC pruefen
|
||||
5. bei Fehlern Defaults laden und `false` zurueckgeben
|
||||
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. 740-Byte-Config in einen 768-Byte-Row-Puffer kopieren
|
||||
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
|
||||
@@ -117,6 +131,8 @@ 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
|
||||
|
||||
|
||||
+32
-10
@@ -13,13 +13,14 @@ Alle Pakete sind exakt 8 Byte lang:
|
||||
```text
|
||||
Byte 0: command / event id
|
||||
Byte 1: key_id oder chunk-index oder chunk-count
|
||||
Byte 2: daten a
|
||||
Byte 3: daten b
|
||||
Byte 4: daten c
|
||||
Byte 5..7: reserviert
|
||||
Byte 2..7: kommandospezifische Daten
|
||||
```
|
||||
|
||||
Es gibt kein Framing und keinen Laengenheader.
|
||||
LED-Kommandos verwenden Byte 2..4 für RGB. Config- und Makro-DATA-Pakete
|
||||
verwenden alle sechs Bytes 2..7 als Nutzlast. Einfache Events aus
|
||||
`usb_serial_send()` nutzen höchstens Byte 2..3 und füllen den Rest mit null.
|
||||
|
||||
Es gibt kein Framing, keinen Längenheader und keine Prüfsumme auf Paketebene.
|
||||
|
||||
## Richtungen
|
||||
|
||||
@@ -49,10 +50,10 @@ Es gibt kein Framing und keinen Laengenheader.
|
||||
|
||||
| ID | Name | Zweck |
|
||||
|---|---|---|
|
||||
| `0x81` | `KEY_DOWN` | Host-Command-Button gedrueckt |
|
||||
| `0x82` | `KEY_UP` | Host-Command-Button losgelassen |
|
||||
| `0x83` | `ENC_CW` | Encoder Host-Command im Uhrzeigersinn |
|
||||
| `0x84` | `ENC_CCW` | Encoder Host-Command gegen Uhrzeigersinn |
|
||||
| `0x81` | `KEY_DOWN` | Host-Button gedrückt |
|
||||
| `0x82` | `KEY_UP` | Host-Button losgelassen |
|
||||
| `0x83` | `ENC_CW` | Encoder-Host-Action im Uhrzeigersinn |
|
||||
| `0x84` | `ENC_CCW` | Encoder-Host-Action gegen Uhrzeigersinn |
|
||||
| `0x85` | `PONG` | Antwort auf Ping |
|
||||
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
|
||||
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
|
||||
@@ -65,6 +66,11 @@ Es gibt kein Framing und keinen Laengenheader.
|
||||
| `0x98` | `MACRO_END` | Makro-Dump fertig |
|
||||
| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen |
|
||||
|
||||
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`.
|
||||
|
||||
## Chunk-Zahlen
|
||||
|
||||
Aktuelle Blob-Groessen:
|
||||
@@ -115,15 +121,31 @@ 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
|
||||
|
||||
- RX-Ringbuffer: 256 Byte = 32 volle Pakete
|
||||
- RX-Ringbuffer: 256 Byte = 32 volle Pakete; er ist ein laufender
|
||||
Zwischenpuffer und fasst keinen kompletten Configtransfer
|
||||
- feste 8-Byte-Pakete vereinfachen Firmware und GUI
|
||||
- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
+11
-2
@@ -1,6 +1,9 @@
|
||||
# VersaMCU - Dokumentationsindex
|
||||
# VersaMCU – Dokumentationsindex
|
||||
|
||||
Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profilen und 32x8 Makros.
|
||||
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 |
|
||||
|---|---|
|
||||
@@ -12,6 +15,11 @@ Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profi
|
||||
| [05_led_system.md](05_led_system.md) | LED-Schichten, Animationen, Render-Pipeline |
|
||||
| [06_nvm_config.md](06_nvm_config.md) | Config v3, 3 Profile, CRC16, Defaults, Werksreset-Bezug |
|
||||
| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK |
|
||||
| [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 |
|
||||
|
||||
Die Repository-weiten Richtlinien und der kompakte LLM-Kontext stehen in
|
||||
[`../AGENTS.md`](../AGENTS.md).
|
||||
|
||||
## Schnellreferenz
|
||||
|
||||
@@ -20,3 +28,4 @@ Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profi
|
||||
- Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md)
|
||||
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
|
||||
- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)
|
||||
- bekannte Risiken vor strukturellen Änderungen: [09_known_limitations.md](09_known_limitations.md)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
; VersaPad v2 – PlatformIO Configuration
|
||||
; Custom SAMD21G18A board (custom PCB)
|
||||
; Custom SAMD21G17D board (custom PCB)
|
||||
; Programmer: Atmel-ICE via SWD (kein Bootloader nötig)
|
||||
|
||||
[common]
|
||||
|
||||
+4
-1
@@ -101,7 +101,10 @@ void CButton::clear_override()
|
||||
void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms)
|
||||
{
|
||||
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
|
||||
m_anim_start_ms = millis() - phase_offset_ms;
|
||||
m_dirty = true;
|
||||
|
||||
+2
-2
@@ -45,7 +45,7 @@ struct RGB
|
||||
enum class LEDAnim : uint8_t
|
||||
{
|
||||
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
|
||||
FADE_IN, // Einmalig: schwarz → volle Helligkeit über period_ms
|
||||
FADE_OUT, // Einmalig: volle Helligkeit → schwarz über period_ms
|
||||
@@ -76,7 +76,7 @@ public:
|
||||
|
||||
// ── LED-Animation ─────────────────────────────────────────────────────────
|
||||
// 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
|
||||
// Animation. Nützlich für COLOR_CYCLE um LEDs versetzt starten zu
|
||||
// lassen (Regenbogen-Wellen-Effekt über mehrere Buttons).
|
||||
|
||||
+4
-5
@@ -7,11 +7,10 @@
|
||||
// Leer: m_head == m_tail
|
||||
// Voll: (m_tail + 1) % SIZE == m_head → ein Slot bleibt immer frei
|
||||
//
|
||||
// Interrupt-Sicherheit (Cortex-M0+):
|
||||
// push() wird aus Encoder-ISR aufgerufen, pop() aus dem Loop.
|
||||
// Auf M0+ sind uint8_t-Lese/Schreibzugriffe atomar (single-cycle LDR/STR) –
|
||||
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren, ist kein
|
||||
// Mutex nötig. Bei mehreren Producern müsste noInterrupts() verwendet werden.
|
||||
// Nebenläufigkeit:
|
||||
// Encoder-ISRs und der Matrixcallback im Loop können beide push() aufrufen.
|
||||
// Der Matrixcallback schützt seinen push() mit einer kurzen Critical Section,
|
||||
// sodass m_tail nie von Loop und ISR gleichzeitig verändert wird.
|
||||
|
||||
#include "CEventQueue.h"
|
||||
|
||||
|
||||
+8
-7
@@ -7,11 +7,12 @@
|
||||
// Kapazität: QUEUE_SIZE - 1 = 16 Events (ein Slot bleibt leer damit
|
||||
// is_full() und is_empty() ohne extra Zähler unterscheidbar sind).
|
||||
//
|
||||
// Thread-Sicherheit:
|
||||
// push() wird aus ISR-Kontext aufgerufen (encoder_cb).
|
||||
// pop() wird aus Loop-Kontext aufgerufen (processEvents).
|
||||
// Auf Cortex-M0+ sind 8-Bit-Lese/Schreibzugriffe atomar → kein Mutex nötig
|
||||
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren.
|
||||
// Nebenläufigkeit:
|
||||
// push() wird aus Encoder-ISRs und aus dem Matrixcallback im Loop aufgerufen.
|
||||
// Der Matrixcallback maskiert Interrupts während push(); Encoder-ISRs können
|
||||
// sich auf dem Single-Core-M0+ nicht gleichpriorisiert gegenseitig unterbrechen.
|
||||
// pop() läuft im Loop; ein gleichzeitig eintreffender Push wird spätestens
|
||||
// im nächsten processEvents()-Durchlauf sichtbar.
|
||||
|
||||
#pragma once
|
||||
#include "SEvent.h"
|
||||
@@ -32,6 +33,6 @@ public:
|
||||
private:
|
||||
static const uint8_t QUEUE_SIZE = 17; // 16 nutzbare Slots
|
||||
SEvent m_buf[QUEUE_SIZE];
|
||||
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_head = 0; // Nächster Lese-Index (Consumer: pop)
|
||||
volatile uint8_t m_tail = 0; // Nächster Schreib-Index (Producer: push)
|
||||
};
|
||||
|
||||
+139
-42
@@ -54,11 +54,17 @@ static void matrix_cb(uint8_t key, bool pressed)
|
||||
ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP;
|
||||
ev.key_id = key;
|
||||
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);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
if (!s_queue) return;
|
||||
@@ -74,8 +80,10 @@ static void encoder_cb(uint8_t enc, int8_t dir)
|
||||
CMainController::CMainController()
|
||||
: m_cfg_chunks_expected(0)
|
||||
, m_cfg_receiving(false)
|
||||
, m_cfg_transfer_valid(false)
|
||||
, m_macro_chunks_expected(0)
|
||||
, m_macro_receiving(false)
|
||||
, m_macro_transfer_valid(false)
|
||||
, m_factory_left_held(false)
|
||||
, m_factory_right_held(false)
|
||||
, m_factory_reset_armed(false)
|
||||
@@ -83,10 +91,21 @@ CMainController::CMainController()
|
||||
, m_factory_hold_started_ms(0)
|
||||
{
|
||||
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_received, 0, sizeof(m_macro_received));
|
||||
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()
|
||||
{
|
||||
macro_config_load(m_macros); // Makro-Tabelle aus NVM laden (oder leere Tabelle)
|
||||
@@ -147,6 +166,11 @@ void CMainController::init_buttons()
|
||||
// Phase gleichmäßig verteilen → stehender Regenbogen dreht sich
|
||||
uint16_t phase = (uint16_t)((uint32_t)mx_idx * period / 20);
|
||||
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 {
|
||||
m_buttons[key].set_anim(anim, period);
|
||||
}
|
||||
@@ -168,7 +192,7 @@ void CMainController::work()
|
||||
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
|
||||
processEvents(); // 3. Queue leeren, Aktionen ausführen
|
||||
check_factory_reset();// 4. Long-Press-Kombination für Werksreset prüfen
|
||||
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
|
||||
updateLEDs(); // 5. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
|
||||
}
|
||||
|
||||
// ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
|
||||
@@ -213,17 +237,30 @@ void CMainController::poll_vendor()
|
||||
// Neuen Empfang starten – bisherige Daten verwerfen
|
||||
m_cfg_chunks_expected = pkt.key_id();
|
||||
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_received, 0, sizeof(m_cfg_received));
|
||||
break;
|
||||
|
||||
case USB_CMD_CONFIG_DATA:
|
||||
if (m_cfg_receiving) {
|
||||
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
|
||||
uint16_t offset = (uint16_t)pkt.key_id() * 6;
|
||||
if (offset < sizeof(m_cfg_buf)) {
|
||||
uint8_t chunk = pkt.key_id();
|
||||
uint16_t offset = (uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
|
||||
if (m_cfg_transfer_valid &&
|
||||
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 > 6 ? 6 : remaining);
|
||||
uint8_t count = (uint8_t)(
|
||||
remaining > SERIAL_PAYLOAD_BYTES
|
||||
? SERIAL_PAYLOAD_BYTES
|
||||
: remaining);
|
||||
memcpy(m_cfg_buf + offset, &pkt.data[2], count);
|
||||
m_cfg_received[chunk] = 1;
|
||||
} else {
|
||||
m_cfg_transfer_valid = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -236,8 +273,8 @@ void CMainController::poll_vendor()
|
||||
nvm_config_load(cfg); // ungültige NVM → Defaults
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
|
||||
const uint16_t sz = sizeof(SDeviceConfig); // 740
|
||||
const uint8_t payload = 6;
|
||||
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124
|
||||
const uint8_t payload = SERIAL_PAYLOAD_BYTES;
|
||||
const uint8_t chunks = CONFIG_CHUNKS;
|
||||
|
||||
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
|
||||
|
||||
@@ -257,14 +294,17 @@ void CMainController::poll_vendor()
|
||||
}
|
||||
|
||||
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;
|
||||
memcpy(&cfg, m_cfg_buf, sizeof(cfg));
|
||||
if (cfg.magic == NVM_CONFIG_MAGIC &&
|
||||
cfg.version == NVM_CONFIG_VERSION &&
|
||||
cfg.crc == nvm_config_crc(cfg))
|
||||
{
|
||||
if (nvm_config_validate(cfg)) {
|
||||
if (nvm_config_save(cfg)) {
|
||||
init_buttons();
|
||||
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
|
||||
@@ -276,46 +316,76 @@ void CMainController::poll_vendor()
|
||||
{
|
||||
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
|
||||
}
|
||||
} else {
|
||||
usb_serial_send(USB_EVT_CONFIG_NACK, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ── Makro-Übertragung: BEGIN → n×DATA → COMMIT ──────────────────
|
||||
case USB_CMD_MACRO_BEGIN:
|
||||
m_macro_chunks_expected = pkt.key_id();
|
||||
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_received, 0, sizeof(m_macro_received));
|
||||
break;
|
||||
|
||||
case USB_CMD_MACRO_DATA:
|
||||
if (m_macro_receiving) {
|
||||
uint16_t offset = (uint16_t)pkt.key_id() * 6;
|
||||
if (offset < sizeof(m_macro_buf)) {
|
||||
uint8_t chunk = pkt.key_id();
|
||||
uint16_t offset =
|
||||
(uint16_t)chunk * SERIAL_PAYLOAD_BYTES;
|
||||
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 > 6 ? 6 : remaining);
|
||||
uint8_t count = (uint8_t)(
|
||||
remaining > SERIAL_PAYLOAD_BYTES
|
||||
? SERIAL_PAYLOAD_BYTES
|
||||
: remaining);
|
||||
memcpy(m_macro_buf + offset, &pkt.data[2], count);
|
||||
m_macro_received[chunk] = 1;
|
||||
} else {
|
||||
m_macro_transfer_valid = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case USB_CMD_MACRO_COMMIT:
|
||||
if (m_macro_receiving) {
|
||||
m_macro_receiving = false;
|
||||
memcpy(&m_macros, m_macro_buf, sizeof(m_macros));
|
||||
if (macro_config_save(m_macros)) {
|
||||
usb_serial_send(USB_EVT_MACRO_ACK, 0);
|
||||
} else {
|
||||
usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout
|
||||
}
|
||||
{
|
||||
bool complete =
|
||||
m_macro_receiving &&
|
||||
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);
|
||||
} else {
|
||||
usb_serial_send(USB_EVT_MACRO_NACK, 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ── Makro-Dump anfordern ─────────────────────────────────────────
|
||||
case USB_CMD_MACRO_READ:
|
||||
{
|
||||
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
|
||||
const uint16_t sz = sizeof(SMacroTable); // 512
|
||||
const uint8_t payload = 6;
|
||||
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 86
|
||||
const uint8_t payload = SERIAL_PAYLOAD_BYTES;
|
||||
const uint8_t chunks = MACRO_CHUNKS;
|
||||
|
||||
usb_serial_send(USB_EVT_MACRO_BEGIN, chunks);
|
||||
|
||||
@@ -347,7 +417,7 @@ void CMainController::poll_vendor()
|
||||
//
|
||||
// KEY_DOWN: execute_action_down() – HID-Taste wird gedrückt, bleibt aktiv bis KEY_UP.
|
||||
// KEY_UP: execute_action_up() – HID-Taste wird losgelassen.
|
||||
// Encoder CW/CCW: execute_action_down() + execute_action_up() für atomare TAP-Sequenz.
|
||||
// Encoder CW/CCW: Host-Event mit Richtung oder HID-Tap-Sequenz.
|
||||
|
||||
void CMainController::processEvents()
|
||||
{
|
||||
@@ -377,17 +447,15 @@ void CMainController::processEvents()
|
||||
|
||||
case EventType::ENC_CW:
|
||||
if (ev.key_id < 4) {
|
||||
execute_action_down(m_enc_cw[ev.key_id], ev.key_id);
|
||||
delay(10);
|
||||
execute_action_up(m_enc_cw[ev.key_id], ev.key_id);
|
||||
execute_encoder_action(
|
||||
m_enc_cw[ev.key_id], ev.key_id, USB_EVT_ENC_CW);
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType::ENC_CCW:
|
||||
if (ev.key_id < 4) {
|
||||
execute_action_down(m_enc_ccw[ev.key_id], ev.key_id);
|
||||
delay(10);
|
||||
execute_action_up(m_enc_ccw[ev.key_id], ev.key_id);
|
||||
execute_encoder_action(
|
||||
m_enc_ccw[ev.key_id], ev.key_id, USB_EVT_ENC_CCW);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -482,8 +550,8 @@ void CMainController::perform_factory_reset()
|
||||
// Laufzeit-Zustand immer an die Defaults angleichen – selbst wenn NVM gerade
|
||||
// nicht geschrieben werden konnte, sieht das Gerät sofort wieder "frisch" aus.
|
||||
m_macros = macros;
|
||||
usb_hid_release_key();
|
||||
usb_hid_release_consumer();
|
||||
usb_hid_release_all_keys();
|
||||
usb_hid_release_all_consumers();
|
||||
init_buttons();
|
||||
show_factory_reset_feedback();
|
||||
|
||||
@@ -525,7 +593,7 @@ void CMainController::show_factory_reset_feedback()
|
||||
// execute_action_up(): Taste wird losgelassen (Hold-Ende).
|
||||
// HID_KEY: sendet Key-Up.
|
||||
// HID_CONSUMER: sendet Consumer-Up.
|
||||
// HOST_COMMAND: kann USB_EVT_KEY_UP senden.
|
||||
// HOST_COMMAND: sendet KEY_UP mit Command-ID.
|
||||
// MACRO/NONE: keine Aktion.
|
||||
|
||||
void CMainController::execute_action_down(SAction action, uint8_t key_id)
|
||||
@@ -550,8 +618,12 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id)
|
||||
}
|
||||
|
||||
case ActionType::HOST_COMMAND:
|
||||
// Windows-App übernimmt Ausführung; KEY_DOWN-Event senden
|
||||
usb_serial_send(USB_EVT_KEY_DOWN, key_id);
|
||||
// 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;
|
||||
|
||||
case ActionType::MACRO:
|
||||
@@ -565,7 +637,7 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id)
|
||||
if (s.keycode == 0) break;
|
||||
usb_hid_send_key(s.keycode, s.modifier);
|
||||
delay(10);
|
||||
usb_hid_release_key();
|
||||
usb_hid_release_key(s.keycode, s.modifier);
|
||||
delay(20); // Kurze Pause zwischen Steps damit der Host mitkommt
|
||||
}
|
||||
break;
|
||||
@@ -598,15 +670,23 @@ void CMainController::execute_action_up(SAction action, uint8_t key_id)
|
||||
switch (action.type) {
|
||||
|
||||
case ActionType::HID_KEY:
|
||||
usb_hid_release_key();
|
||||
{
|
||||
uint8_t keycode = static_cast<uint8_t>(action.data & 0xFF);
|
||||
uint8_t modifier = static_cast<uint8_t>(action.data >> 8);
|
||||
usb_hid_release_key(keycode, modifier);
|
||||
break;
|
||||
}
|
||||
|
||||
case ActionType::HID_CONSUMER:
|
||||
usb_hid_release_consumer();
|
||||
usb_hid_release_consumer(action.data);
|
||||
break;
|
||||
|
||||
case ActionType::HOST_COMMAND:
|
||||
// Optional: USB_EVT_KEY_UP senden (aktuell nicht implementiert)
|
||||
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:
|
||||
@@ -618,6 +698,23 @@ void CMainController::execute_action_up(SAction action, uint8_t key_id)
|
||||
}
|
||||
}
|
||||
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt
|
||||
|
||||
@@ -41,17 +41,32 @@ private:
|
||||
void processEvents(); // Queue leeren, Aktionen ausführen
|
||||
void execute_action_down(SAction action, uint8_t key_id); // Taste drücken (Hold-Start)
|
||||
void execute_action_up(SAction action, uint8_t key_id); // Taste losgelassen (Hold-Ende)
|
||||
void 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 ─────────────────────────────────────────────────
|
||||
uint8_t m_cfg_buf[sizeof(SDeviceConfig)]; // 740 Bytes
|
||||
uint8_t m_cfg_received[CONFIG_CHUNKS];
|
||||
uint8_t m_cfg_chunks_expected;
|
||||
bool m_cfg_receiving;
|
||||
bool m_cfg_transfer_valid;
|
||||
|
||||
// ── Makro-Empfangspuffer ──────────────────────────────────────────────────
|
||||
uint8_t m_macro_buf[sizeof(SMacroTable)]; // 512 Bytes
|
||||
uint8_t m_macro_received[MACRO_CHUNKS];
|
||||
uint8_t m_macro_chunks_expected;
|
||||
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)
|
||||
SMacroTable m_macros;
|
||||
|
||||
+5
-4
@@ -6,15 +6,16 @@ enum class ActionType : uint8_t
|
||||
NONE, // Keine Aktion
|
||||
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
|
||||
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
|
||||
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
|
||||
HOST_COMMAND, // Host-Event; data = Command-ID für die Desktop-App
|
||||
MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 8 HID-Keys sequenziell
|
||||
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 0–2); speichert in NVM
|
||||
PROFILE_SWITCH, // Profil 0–2 oder 0x00FF/0xFFFF = nächstes Profil; speichert in NVM
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) SAction
|
||||
{
|
||||
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)
|
||||
// Muss packed sein damit sizeof(SDeviceConfig)==163 == C#-Serialisierung
|
||||
// Muss packed sein, damit sizeof(SDeviceConfig)==740 und die
|
||||
// hostseitige Serialisierung bytegenau übereinstimmen.
|
||||
};
|
||||
|
||||
@@ -42,6 +42,18 @@ static bool nvm_write_page(uint32_t addr, const uint8_t* data)
|
||||
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)
|
||||
{
|
||||
memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl));
|
||||
@@ -56,11 +68,17 @@ bool macro_config_load(SMacroTable& tbl)
|
||||
memset(&tbl, 0, sizeof(tbl)); // Leere Tabelle als Default
|
||||
return false;
|
||||
}
|
||||
if (!macro_config_validate(tbl)) {
|
||||
memset(&tbl, 0, sizeof(tbl));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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.
|
||||
// SMacroTable ist __attribute__((packed)) und könnte unaligned liegen →
|
||||
// direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//
|
||||
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
|
||||
// Slot 0–19 : MX-Buttons (mx_idx)
|
||||
// Slot 20–31 : Encoder-Aktionen (enc*3 + act_idx, 0=SW/1=CW/2=CCW)
|
||||
// Slot 20–31 : 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.
|
||||
// Delay zwischen Steps: 20 ms (hardcoded).
|
||||
@@ -28,6 +28,13 @@ struct __attribute__((packed)) SMacroTable
|
||||
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
|
||||
};
|
||||
|
||||
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.
|
||||
bool macro_config_load(SMacroTable& tbl);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// NVM-Zugriff für SDeviceConfig (3 Rows ab 0x1FD00, 768B gesamt, 740B genutzt).
|
||||
|
||||
#include "nvm_config.h"
|
||||
#include "macro_config.h"
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -65,6 +66,63 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void nvm_config_defaults(SDeviceConfig& cfg)
|
||||
@@ -112,12 +170,10 @@ bool nvm_config_load(SDeviceConfig& 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 (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; }
|
||||
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; }
|
||||
|
||||
// Profil-Index absichern
|
||||
if (cfg.active_profile >= 3) cfg.active_profile = 0;
|
||||
if (!nvm_config_validate(cfg)) {
|
||||
nvm_config_defaults(cfg);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -126,11 +182,15 @@ bool nvm_config_load(SDeviceConfig& cfg)
|
||||
|
||||
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, &cfg, sizeof(cfg));
|
||||
memcpy(row, &stored, sizeof(stored));
|
||||
|
||||
NVMCTRL->CTRLB.bit.MANW = 1;
|
||||
|
||||
|
||||
+10
-1
@@ -61,13 +61,22 @@ struct __attribute__((packed)) SDeviceConfig
|
||||
// Gesamt: 32 + 708 = 740B
|
||||
};
|
||||
|
||||
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
|
||||
void nvm_config_defaults(SDeviceConfig& cfg);
|
||||
|
||||
// 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);
|
||||
|
||||
// Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages).
|
||||
// Config in NVM schreiben (CRC wird intern neu berechnet; löscht 3 Rows,
|
||||
// schreibt 12 Pages).
|
||||
// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird (Board hängt nicht).
|
||||
bool nvm_config_save(const SDeviceConfig& cfg);
|
||||
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
#define BTN_COL_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] = {
|
||||
PIN_COL0, // PB10 – encoder SW column
|
||||
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)
|
||||
};
|
||||
|
||||
// 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] = {
|
||||
PIN_ROW0, // PB11
|
||||
PIN_ROW1, // PA12
|
||||
|
||||
+125
-14
@@ -1,9 +1,10 @@
|
||||
#include "usb_hid.h"
|
||||
#include <Arduino.h>
|
||||
#include <HID.h>
|
||||
#include <string.h>
|
||||
|
||||
// ── 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[] = {
|
||||
|
||||
@@ -67,30 +68,140 @@ struct ConsumerReport {
|
||||
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 = {};
|
||||
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));
|
||||
}
|
||||
|
||||
void usb_hid_release_key()
|
||||
static void send_consumer_state()
|
||||
{
|
||||
KeyboardReport report = {};
|
||||
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
|
||||
}
|
||||
uint16_t usage = 0;
|
||||
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 };
|
||||
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
|
||||
}
|
||||
|
||||
void usb_hid_release_consumer()
|
||||
void usb_hid_init()
|
||||
{
|
||||
ConsumerReport report = { 0 };
|
||||
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
|
||||
memset(s_key_refcount, 0, sizeof(s_key_refcount));
|
||||
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
@@ -28,8 +28,14 @@
|
||||
|
||||
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_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_release_consumer();
|
||||
void usb_hid_release_consumer(uint16_t usage);
|
||||
void usb_hid_release_all_consumers();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// alle verfügbaren Bytes in einen internen Ring-Buffer und gibt ein vollständiges
|
||||
// 8-Byte-Paket zurück sobald genug Bytes akkumuliert sind.
|
||||
// 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):
|
||||
// Direkt via SerialUSB.write() – kein eigener Puffer nötig, da der Arduino-CDC-
|
||||
@@ -16,8 +16,9 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
// 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
|
||||
// Config-Übertragung (30 Pakete) ohne Überlauf.
|
||||
// Größe: 32 Pakete × 8 Bytes = 256 Bytes. Der Puffer ist nur ein
|
||||
// Zwischenpuffer; ein vollständiger Config-Transfer umfasst 126 Pakete
|
||||
// einschließlich BEGIN und COMMIT.
|
||||
static uint8_t s_buf[SERIAL_PKT_SIZE * 32];
|
||||
static uint16_t s_head = 0;
|
||||
static uint16_t s_count = 0;
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
// Byte-Layout aller Pakete:
|
||||
// [0] Command/Event-ID
|
||||
// [1] key_id (Button 0–24 oder Encoder 0–3)
|
||||
// [2] r / Daten-Byte A
|
||||
// [3] g / Daten-Byte B
|
||||
// [4] b
|
||||
// [5..7] reserviert (0x00)
|
||||
// [2..7] kommandospezifische Daten
|
||||
// LED-Kommandos nutzen [2..4] als RGB; Config-/Makro-DATA nutzt [2..7].
|
||||
//
|
||||
// Richtungen:
|
||||
// PC → Board (Commands, 0x01–0x7F): poll_vendor() in CMainController
|
||||
@@ -43,10 +41,10 @@
|
||||
#define USB_CMD_MACRO_READ 0x23 // Board sendet aktuelle Makro-Tabelle zurück
|
||||
|
||||
// ── Events: Board → PC ────────────────────────────────────────────────────────
|
||||
#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt
|
||||
#define USB_EVT_KEY_UP 0x82 // key_id → HOST_COMMAND-Button losgelassen
|
||||
#define USB_EVT_ENC_CW 0x83 // enc_id → Encoder Schritt CW (HOST_COMMAND)
|
||||
#define USB_EVT_ENC_CCW 0x84 // enc_id → Encoder Schritt CCW (HOST_COMMAND)
|
||||
#define USB_EVT_KEY_DOWN 0x81 // key_id + Command-ID in Data[2..3]
|
||||
#define USB_EVT_KEY_UP 0x82 // key_id + Command-ID in Data[2..3]
|
||||
#define USB_EVT_ENC_CW 0x83 // enc_id + Command-ID in Data[2..3]
|
||||
#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_CONFIG_ACK 0x90 // Config erfolgreich in NVM geschrieben
|
||||
#define USB_EVT_CONFIG_NACK 0x91 // Config CRC/Magic ungültig – nicht geschrieben
|
||||
|
||||
@@ -11,9 +11,9 @@ SEARCH_DIR(.)
|
||||
|
||||
MEMORY
|
||||
{
|
||||
rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x0001FE00 /* 127.5K – Firmware */
|
||||
config (rx) : ORIGIN = 0x0001FE00, LENGTH = 0x00000200 /* 512B – NVM Config (2 Rows) */
|
||||
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
|
||||
rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x0001FB00 /* 126.75K – Firmware */
|
||||
nvm (rx) : ORIGIN = 0x0001FB00, LENGTH = 0x00000500 /* 1.25K – Makros + Config */
|
||||
ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */
|
||||
}
|
||||
|
||||
/* Initial stack pointer = top of RAM */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// VersaPad v2 – SAMD21G18A Custom Variant
|
||||
// VersaPad v2 – SAMD21G17D Custom Variant
|
||||
// Pin descriptions and peripheral object definitions
|
||||
|
||||
#include "variant.h"
|
||||
@@ -15,7 +15,7 @@
|
||||
const PinDescription g_APinDescription[] = {
|
||||
|
||||
// ── Button Matrix: Columns (D0–D4) ────────────────────────────────────────
|
||||
// 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
|
||||
{ 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 },
|
||||
|
||||
// ── Button Matrix: Rows (D5–D9) ───────────────────────────────────────────
|
||||
// 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
|
||||
{ PORTB, 11, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// VersaPad v2 – SAMD21G18A Custom Variant
|
||||
// VersaPad v2 – SAMD21G17D Custom Variant
|
||||
// Arduino pin assignments for the custom PCB
|
||||
|
||||
#define ARDUINO_SAMD_VARIANT_COMPLIANCE 10610
|
||||
@@ -24,14 +24,14 @@
|
||||
// attachInterrupt() then internally looks up ulExtInt via g_APinDescription.
|
||||
|
||||
// ─── 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_COL1 (3u) // PA11
|
||||
#define PIN_COL2 (2u) // PA10
|
||||
#define PIN_COL3 (1u) // PA09
|
||||
#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_ROW1 (6u) // PA12
|
||||
#define PIN_ROW2 (7u) // PA13
|
||||
|
||||
Reference in New Issue
Block a user