From 50dbf8fbeeb943b59f5522a7705b391f9a66306d Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 24 Jul 2026 09:27:07 +0200 Subject: [PATCH] Update firmware documentation and agent guidance --- AGENTS.md | 94 ++++++++++++++ README.md | 227 ++++++++++++++++------------------ doc/00_architecture.md | 47 ++++--- doc/01_matrix.md | 5 +- doc/02_encoder.md | 9 +- doc/03_action_engine.md | 26 +++- doc/04_macro_system.md | 5 +- doc/05_led_system.md | 26 +++- doc/06_nvm_config.md | 21 +++- doc/07_serial_protocol.md | 40 ++++-- doc/08_development.md | 95 ++++++++++++++ doc/09_known_limitations.md | 99 +++++++++++++++ doc/INDEX.md | 13 +- platformio.ini | 2 +- src/CButton.h | 4 +- src/CEventQueue.cpp | 9 +- src/CEventQueue.h | 9 +- src/CMainController.cpp | 9 +- src/config/action.h | 9 +- src/config/macro_config.h | 2 +- src/config/pins.h | 4 +- src/hal/usb_hid.cpp | 2 +- src/hal/usb_serial.cpp | 7 +- src/hal/usb_serial.h | 12 +- variants/versapad/variant.cpp | 6 +- variants/versapad/variant.h | 6 +- 26 files changed, 592 insertions(+), 196 deletions(-) create mode 100644 AGENTS.md create mode 100644 doc/08_development.md create mode 100644 doc/09_known_limitations.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5511b73 --- /dev/null +++ b/AGENTS.md @@ -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? diff --git a/README.md b/README.md index 735cc9a..b63063a 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/doc/00_architecture.md b/doc/00_architecture.md index c743af5..4ff29bd 100644 --- a/doc/00_architecture.md +++ b/doc/00_architecture.md @@ -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). diff --git a/doc/01_matrix.md b/doc/01_matrix.md index c58398e..43fc5ed 100644 --- a/doc/01_matrix.md +++ b/doc/01_matrix.md @@ -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. diff --git a/doc/02_encoder.md b/doc/02_encoder.md index 93f9448..c54476d 100644 --- a/doc/02_encoder.md +++ b/doc/02_encoder.md @@ -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 diff --git a/doc/03_action_engine.md b/doc/03_action_engine.md index 66d75c5..d4ddbf2 100644 --- a/doc/03_action_engine.md +++ b/doc/03_action_engine.md @@ -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. diff --git a/doc/04_macro_system.md b/doc/04_macro_system.md index d1f702e..bd59797 100644 --- a/doc/04_macro_system.md +++ b/doc/04_macro_system.md @@ -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 diff --git a/doc/05_led_system.md b/doc/05_led_system.md index 662d5d8..fbd2c31 100644 --- a/doc/05_led_system.md +++ b/doc/05_led_system.md @@ -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. 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) diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md index b081b63..a7f2076 100644 --- a/doc/06_nvm_config.md +++ b/doc/06_nvm_config.md @@ -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 diff --git a/doc/07_serial_protocol.md b/doc/07_serial_protocol.md index 799c235..2dad363 100644 --- a/doc/07_serial_protocol.md +++ b/doc/07_serial_protocol.md @@ -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 diff --git a/doc/08_development.md b/doc/08_development.md new file mode 100644 index 0000000..524ff92 --- /dev/null +++ b/doc/08_development.md @@ -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). + diff --git a/doc/09_known_limitations.md b/doc/09_known_limitations.md new file mode 100644 index 0000000..9e2117c --- /dev/null +++ b/doc/09_known_limitations.md @@ -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. diff --git a/doc/INDEX.md b/doc/INDEX.md index b70b034..91852c7 100644 --- a/doc/INDEX.md +++ b/doc/INDEX.md @@ -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) diff --git a/platformio.ini b/platformio.ini index eacc5eb..5375d4a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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] diff --git a/src/CButton.h b/src/CButton.h index 9432846..1dfb151 100644 --- a/src/CButton.h +++ b/src/CButton.h @@ -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). diff --git a/src/CEventQueue.cpp b/src/CEventQueue.cpp index 4761122..95c4760 100644 --- a/src/CEventQueue.cpp +++ b/src/CEventQueue.cpp @@ -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" diff --git a/src/CEventQueue.h b/src/CEventQueue.h index 58adac6..5fd809b 100644 --- a/src/CEventQueue.h +++ b/src/CEventQueue.h @@ -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" diff --git a/src/CMainController.cpp b/src/CMainController.cpp index e7e6694..b02996b 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -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: diff --git a/src/config/action.h b/src/config/action.h index e936c86..dd880aa 100644 --- a/src/config/action.h +++ b/src/config/action.h @@ -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 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 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. }; diff --git a/src/config/macro_config.h b/src/config/macro_config.h index 77f2b2c..884d4b8 100644 --- a/src/config/macro_config.h +++ b/src/config/macro_config.h @@ -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). diff --git a/src/config/pins.h b/src/config/pins.h index de1e06e..a45bfeb 100644 --- a/src/config/pins.h +++ b/src/config/pins.h @@ -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 diff --git a/src/hal/usb_hid.cpp b/src/hal/usb_hid.cpp index ef03901..9dd2f6c 100644 --- a/src/hal/usb_hid.cpp +++ b/src/hal/usb_hid.cpp @@ -3,7 +3,7 @@ #include // ── 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[] = { diff --git a/src/hal/usb_serial.cpp b/src/hal/usb_serial.cpp index 61a4b0f..6aca0b7 100644 --- a/src/hal/usb_serial.cpp +++ b/src/hal/usb_serial.cpp @@ -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 // 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; diff --git a/src/hal/usb_serial.h b/src/hal/usb_serial.h index 83618d7..310fcdf 100644 --- a/src/hal/usb_serial.h +++ b/src/hal/usb_serial.h @@ -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 @@ -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 diff --git a/variants/versapad/variant.cpp b/variants/versapad/variant.cpp index 6b9471b..48f889e 100644 --- a/variants/versapad/variant.cpp +++ b/variants/versapad/variant.cpp @@ -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 }, diff --git a/variants/versapad/variant.h b/variants/versapad/variant.h index 19e003e..cbd26c6 100644 --- a/variants/versapad/variant.h +++ b/variants/versapad/variant.h @@ -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