Update firmware documentation and agent guidance

This commit is contained in:
2026-07-24 09:27:07 +02:00
parent ac3b2aa90f
commit 50dbf8fbee
26 changed files with 592 additions and 196 deletions
+94
View File
@@ -0,0 +1,94 @@
# 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 gehört nicht zu diesem Repository. Keine Kompatibilität mit
ihr behaupten, wenn nur die Firmware geändert oder geprüft wurde.
- 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. Es besitzt aktuell weder
Framing noch Sequenz-/Vollständigkeitsprüfung.
## Ä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. Es gibt derzeit keine automatisierten Unit- oder
Integrationstests. Ein erfolgreicher Build beweist daher weder elektrische
Funktion noch GUI-Kompatibilität.
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?
+109 -118
View File
@@ -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,98 @@ 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.
Wichtig: Das aktive Linker-Skript reserviert aktuell nur die letzten 512 Byte
explizit. Das derzeit kleine Firmware-Image überschneidet sich nicht mit den
NVM-Daten, zukünftiges Wachstum ist aber nicht vollständig abgesichert. Siehe
[bekannte Einschränkungen](doc/09_known_limitations.md).
## 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)
+33 -14
View File
@@ -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,17 @@ 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. Die Queue
besitzt aktuell keinen Interruptschutz für den Matrix-Push und verwirft Events
bei Überlauf. Das ist eine bekannte Einschränkung, keine garantierte
Multi-Producer-Sicherheit; siehe
[09_known_limitations.md](09_known_limitations.md).
+4 -1
View File
@@ -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
Encoderinterrupt kann den Matrix-Push unterbrechen; die aktuelle Queue
schützt diesen Multi-Producer-Fall nicht ausdrücklich.
+8 -1
View File
@@ -41,8 +41,15 @@ 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
Wichtig: Die Queue wird zusätzlich vom Matrixcallback im Loop beschrieben.
Ein Encoderinterrupt kann diesen Push unterbrechen. Die Implementierung ist
damit nicht streng Single-Producer und besitzt für diesen Fall aktuell keinen
Interruptschutz. Details:
[09_known_limitations.md](09_known_limitations.md).
## Initialisierung
+23 -3
View File
@@ -25,7 +25,7 @@ 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 | aktuell nicht ausgewertet |
| `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil |
@@ -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` 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` | keine Ausgabe; `USB_EVT_KEY_UP` ist nur definiert |
| `MACRO` | nichts |
| `PROFILE_SWITCH` | nichts |
| `NONE` | nichts |
@@ -62,6 +62,26 @@ down -> delay(10 ms) -> up
- Makros laufen komplett synchron in der Firmware.
Das Hold-Modell verwaltet keine Menge gleichzeitig gedrückter Tasten. Ein
neuer Keyboard-Down ersetzt den vorherigen Report und jeder Keyboard-Up leert
den gesamten Report. Consumer-HID hat dieselbe Einschränkung mit genau einem
Usage-Wert.
## Host-Commands
Der aktuelle Code sendet für jede `HOST_COMMAND`-Action nur:
```text
Byte 0 = USB_EVT_KEY_DOWN (0x81)
Byte 1 = Matrix-Key-ID oder Encoder-ID
```
`SAction.data` wird dabei nicht übertragen. Auch Encoder-Actions verwenden
aktuell `0x81`; die definierten Events `ENC_CW (0x83)` und `ENC_CCW (0x84)`
werden nicht emittiert. Ein Release erzeugt kein `KEY_UP`-Paket. Das ist
aktuelles Verhalten und als Einschränkung in
[09_known_limitations.md](09_known_limitations.md) festgehalten.
## Makro-Ausfuehrung
Bei `ActionType::MACRO` wird `action.data` als Slot interpretiert.
+4 -1
View File
@@ -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
+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_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 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.
`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. Eingehende Configs validieren diesen Grenzwert aktuell nicht.
`COLOR_FADE` benötigt `set_color_fade(to, period_ms)`, weil dort Start- und
Zielfarbe gesetzt werden. Ein bloßes `set_anim(COLOR_FADE, ...)`, wie es beim
direkten Laden dieses Enum-Werts aus der Config geschieht, initialisiert diese
Farben nicht aus den Configdaten.
### Phasenversatz (Regenbogen-Wellen)
+20 -1
View File
@@ -17,6 +17,13 @@ Dateien:
Makros und Config sind komplett getrennt.
Diese Tabelle beschreibt die Adressen, auf die der Laufzeitcode zugreift. Das
aktive Linker-Skript reserviert davon aktuell nur `0x1FE00..0x1FFFF`
ausdrücklich; Makrobereich und erste Config-Row liegen noch im zulässigen
Firmware-ROM. Das aktuelle Binary ist klein genug, aber die Bereiche sind
nicht vollständig gegen Firmwarewachstum geschützt. Siehe
[09_known_limitations.md](09_known_limitations.md).
## `SDeviceConfig`
Aktueller Stand:
@@ -38,6 +45,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:
@@ -97,12 +107,19 @@ Praktisch sichtbares Ergebnis:
5. bei Fehlern Defaults laden und `false` zurueckgeben
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.
Nach erfolgreicher CRC-Prüfung sichert `load()` einen zu großen
`active_profile` im RAM auf Profil 0 ab. Andere Felder und Enum-Werte werden
nicht auf gültige Bereiche geprüft.
## 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 +134,8 @@ Wichtig:
- der Schreibpuffer muss 4-Byte-aligned sein
- `packed` allein reicht dafuer nicht
- `nvm_config_save()` berechnet die CRC nicht selbst; der Aufrufer muss
`cfg.crc = nvm_config_crc(cfg)` vorher setzen
## Zusammenhang mit Werksreset
+30 -10
View File
@@ -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` | wird für jede aktuelle Host-Action gesendet |
| `0x82` | `KEY_UP` | definiert, aktuell nicht gesendet |
| `0x83` | `ENC_CW` | definiert, aktuell nicht gesendet |
| `0x84` | `ENC_CCW` | definiert, aktuell nicht gesendet |
| `0x85` | `PONG` | Antwort auf Ping |
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
@@ -65,6 +66,10 @@ 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 die
Encoder-ID. `SAction.data` wird nicht übertragen. Auch CW- und CCW-Actions
senden aktuell `0x81`; die Drehrichtung ist im Paket nicht enthalten.
## Chunk-Zahlen
Aktuelle Blob-Groessen:
@@ -115,15 +120,30 @@ Nur bei erfolgreicher Pruefung wird in NVM geschrieben.
`MACRO_COMMIT` schreibt ohne CRC direkt nach NVM und signalisiert nur Erfolg oder Fehler.
Die in `BEGIN` angekündigte Chunkzahl wird zwar gespeichert, aber beim Commit
nicht ausgewertet. Die Firmware verfolgt nicht, welche Chunkindizes tatsächlich
eingetroffen sind. Doppelte, fehlende und ungeordnete Chunks werden deshalb
nicht als solche erkannt. Bei Configdaten schlägt ein unvollständiger Transfer
typischerweise an der CRC fehl; bei Makrodaten kann ein unvollständiger
Null-gefüllter Puffer gespeichert werden.
Die Configvalidierung prüft keine Feldwerte oder Enum-Bereiche. Insbesondere
müssen Hostimplementierungen gültige Profilindizes, Actiontypen, LED-Enums und
Animationsperioden liefern.
## 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
- Encoder-Host-Actions derzeit nicht anhand von `0x83/0x84` erwarten
## 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
+95
View File
@@ -0,0 +1,95 @@
# 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
- Zugriff auf die separat gepflegte Windows-GUI, 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
Es gibt 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 externer Vertrag. Änderungen an `SDeviceConfig`,
`SMacroTable`, Action-Werten oder USB-IDs sind erst vollständig verifiziert,
wenn Firmware und GUI dieselben Bytes senden und interpretieren.
## 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).
+99
View File
@@ -0,0 +1,99 @@
# Bekannte Einschränkungen und Risiken
Diese Liste beschreibt den aktuellen Implementierungsstand. Sie ist keine
Liste bereits umgesetzter Features.
## Flash-Reservierung stimmt nicht vollständig mit dem NVM-Zugriff überein
Die Firmware liest und schreibt:
```text
0x1FB00..0x1FCFF Makros (512 Byte)
0x1FD00..0x1FFFF Config (768 Byte, davon 740 Byte genutzt)
```
Das aktive Linker-Skript `flash_without_bootloader.ld` erlaubt Firmware jedoch
bis einschließlich `0x1FDFF` und reserviert nur `0x1FE00..0x1FFFF`. Damit sind
`0x1FB00..0x1FDFF` nicht gegen ein zukünftig wachsendes Firmware-Image
geschützt. Das aktuelle Image liegt deutlich darunter, aber der Build prüft
diese NVM-Grenze nicht.
Das Bootloader-Linker-Skript reserviert aktuell gar keinen separaten
NVM-Bereich. Das auskommentierte USB-Bootloader-Environment ist daher kein
unterstütztes Ziel.
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 Flash und 16 KiB RAM; vor
device-spezifischen Änderungen sollte diese historische Makro-Abweichung
trotzdem geprüft werden.
## Event-Queue hat gemischte Producer
Matrixevents werden im Loop erzeugt, Encoderevents in EIC-Interrupts. Beide
rufen `CEventQueue::push()` auf und verändern denselben Tail-Index ohne
Interruptschutz. Ein Encoderinterrupt kann einen Matrix-Push unterbrechen.
Die bisherige Annahme eines reinen Single-Producer/Single-Consumer-Ringbuffers
ist deshalb nicht vollständig erfüllt; seltene verlorene oder überschriebene
Events sind theoretisch möglich.
Zusätzlich werden Events bei voller Queue still verworfen.
## `HOST_COMMAND` nutzt seine `data` nicht
`SAction.data` ist für `ActionType::HOST_COMMAND` vorhanden, wird in
`execute_action_down()` aber nicht übertragen. Gesendet wird nur
`USB_EVT_KEY_DOWN (0x81)` mit der Matrix-Key-ID beziehungsweise Encoder-ID.
`USB_EVT_KEY_UP (0x82)`, `USB_EVT_ENC_CW (0x83)` und
`USB_EVT_ENC_CCW (0x84)` sind definiert, werden vom aktuellen Controller aber
nicht gesendet. Bei Encoder-Host-Actions geht dadurch die Richtung im
gesendeten Paket verloren, sofern die Host-Anwendung sie nicht anderweitig aus
der konfigurierten Action ableitet.
## HID-Holds sind global
Der Keyboard-Report enthält zwar sechs Keycode-Felder, die Implementierung
setzt aber nur das erste. Jeder neue `HID_KEY`-Down ersetzt den vorherigen
Report; jedes Release sendet einen komplett leeren Report. Analog existiert
nur ein globaler Consumer-Usage-Wert. Gleichzeitige unabhängige Holds werden
daher nicht korrekt verwaltet.
## CDC-Transfers sind nur schwach validiert
- `BEGIN` speichert die angekündigte Chunkzahl, `COMMIT` vergleicht sie aber
nicht mit empfangenen Chunks.
- Doppelte, fehlende oder ungeordnete Chunks werden nicht verfolgt.
- Config-Commit prüft Magic, Version und CRC, aber keine Feldwerte oder
Enum-Bereiche.
- Makro-Commit hat weder CRC noch Vollständigkeitsprüfung.
- Das 8-Byte-Protokoll besitzt kein Framing. Ein verlorenes oder zusätzliches
Byte desynchronisiert alle folgenden Pakete.
Insbesondere kann ein formal CRC-korrektes Profil ungültige Animationswerte
enthalten. `PULSE` benötigt in der aktuellen Arithmetik eine Periode von
mindestens 2 ms; dieser Mindestwert wird nicht validiert.
## Farbanimationen umgehen Teile der Helligkeits-/Override-Logik
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` benötigt den
separaten Aufruf `set_color_fade()`; das reine Laden des Enum-Werts aus einer
Config setzt keine Start- und Zielfarbe.
## Reservierte beziehungsweise noch ungenutzte Hardware und Felder
- Die drei Fader-Pins sind im Variant und in `config/pins.h` definiert, werden
von der Firmware aber nicht 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
View File
@@ -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
View File
@@ -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]
+2 -2
View File
@@ -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
View File
@@ -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.
// Dieser gemischte Producerfall ist aktuell nicht durch eine Critical
// Section geschützt; siehe doc/09_known_limitations.md.
#include "CEventQueue.h"
+4 -5
View File
@@ -7,11 +7,10 @@
// 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.
// pop() läuft ebenfalls im Loop. Es gibt aktuell keine Critical Section für
// den Fall, dass ein Encoderinterrupt einen Matrix-Push unterbricht.
#pragma once
#include "SEvent.h"
+5 -4
View File
@@ -58,7 +58,8 @@ static void matrix_cb(uint8_t key, bool pressed)
}
// Wird von handle_encoder() aufgerufen läuft im ISR-Kontext (EIC-Interrupt).
// CEventQueue::push() ist interrupt-sicher (kein Heap, atomare Indizes auf M0+).
// Die Queue vermeidet den Heap, schützt den gemischten Matrix-/ISR-Producerfall
// aber aktuell nicht mit einer Critical Section.
static void encoder_cb(uint8_t enc, int8_t dir)
{
if (!s_queue) return;
@@ -168,7 +169,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) ───────────────────────────────────────
@@ -525,7 +526,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: aktuell keine Ausgabe auf Release.
// MACRO/NONE: keine Aktion.
void CMainController::execute_action_down(SAction action, uint8_t key_id)
@@ -606,7 +607,7 @@ void CMainController::execute_action_up(SAction action, uint8_t key_id)
break;
case ActionType::HOST_COMMAND:
// Optional: USB_EVT_KEY_UP senden (aktuell nicht implementiert)
// USB_EVT_KEY_UP ist definiert, wird aktuell aber nicht gesendet.
break;
case ActionType::MACRO:
+5 -4
View File
@@ -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 wird vom aktuellen Controller nicht übertragen
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 8 HID-Keys sequenziell
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 02); speichert in NVM
PROFILE_SWITCH, // Profil 02 oder 0xFF = 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; für HOST_COMMAND aktuell ungenutzt
// 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.
};
+1 -1
View File
@@ -5,7 +5,7 @@
//
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
// 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.
// Delay zwischen Steps: 20 ms (hardcoded).
+2 -2
View File
@@ -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
+1 -1
View File
@@ -3,7 +3,7 @@
#include <HID.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[] = {
+4 -3
View File
@@ -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;
+5 -7
View File
@@ -8,10 +8,8 @@
// Byte-Layout aller Pakete:
// [0] Command/Event-ID
// [1] key_id (Button 024 oder Encoder 03)
// [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, 0x010x7F): poll_vendor() in CMainController
@@ -44,9 +42,9 @@
// ── 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_UP 0x82 // Reserviert; vom Controller aktuell nicht gesendet
#define USB_EVT_ENC_CW 0x83 // Reserviert; vom Controller aktuell nicht gesendet
#define USB_EVT_ENC_CCW 0x84 // Reserviert; vom Controller aktuell nicht gesendet
#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
+3 -3
View File
@@ -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 (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
{ 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 (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
{ 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
// 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