# VersaMCU Firmware für das VersaPad v2 Macro-Pad. Läuft auf einem **ATSAMD21G17D** (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework. ## Voraussetzungen - [PlatformIO](https://platformio.org/) (CLI oder VS Code Extension) - **Atmel-ICE** Debugger/Programmer (SWD-Verbindung zur PCB) - [OpenOCD](https://openocd.org/) – wird von PlatformIO automatisch installiert ## Flashen ```bash pio run --target upload ``` Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig – der Chip wird direkt programmiert. --- ## Funktionsumfang (Anforderungskatalog) ### 1 Hardware-Plattform | # | Anforderung | Status | |---|-------------|--------| | 1.1 | Ziel-MCU: **ATSAMD21G17D** (Cortex-M0+, 48 MHz, 128 KB Flash, 16 KB RAM) | ✅ | | 1.2 | Framework: **Arduino + PlatformIO**, kein Bootloader (Direktflash via SWD/Atmel-ICE) | ✅ | | 1.3 | USB-Enumeration ohne externen Quarz (`-DCRYSTALLESS`, DFLL48M nutzt USB-SOF als Referenz) | ✅ | | 1.4 | Benutzerdefiniertes Board-Profil (`versapad_nobl.json`) mit korrekten Flash/RAM-Limits | ✅ | ### 2 Tasten-Matrix | # | Anforderung | Status | |---|-------------|--------| | 2.1 | **5×5-Matrix-Scan** (25 Keys, davon 20 MX-Buttons + 4 Encoder-SW + 1 NC) | ✅ | | 2.2 | **10 ms Software-Debounce** pro Taste (Flanken-Erkennung) | ✅ | | 2.3 | KEY_DOWN- und KEY_UP-Events werden in die Event-Queue geschrieben | ✅ | | 2.4 | Matrix-Scan im **Loop-Kontext** (kein ISR, kein Heap) | ✅ | ### 3 Encoder | # | Anforderung | Status | |---|-------------|--------| | 3.1 | **4 Quadratur-Encoder** (A/B-Phasen per EIC-Interrupt) | ✅ | | 3.2 | **Richtungserkennung**: CW / CCW via 2-Bit-Greycode-Auswertung | ✅ | | 3.3 | Encoder-Events im ISR-Kontext direkt in Event-Queue (interrupt-sicher, kein Heap) | ✅ | | 3.4 | Encoder-SW-Tasten über Matrix-Scan (gleicher Pfad wie MX-Buttons) | ✅ | ### 4 Aktions-Engine | # | Anforderung | Status | |---|-------------|--------| | 4.1 | **ActionType NONE**: kein HID-Event beim Drücken | ✅ | | 4.2 | **ActionType HID_KEY**: USB-HID-Tastendruck (Keycode + Modifier-Byte) | ✅ | | 4.3 | **ActionType HID_CONSUMER**: USB Consumer Control (Play/Pause, Lautstärke …) | ✅ | | 4.4 | **ActionType HOST_COMMAND**: Event-ID an VersaGUI senden, App führt aus | ✅ | | 4.5 | **ActionType MACRO**: Sequenz aus bis zu 4 HID-Key-Schritten aus NVM-Tabelle abspielen | ✅ | | 4.6 | Jede Aktion ausführbar **ohne laufende VersaGUI** (lokal per HID/Makro) | ✅ | ### 5 Makro-System | # | Anforderung | Status | |---|-------------|--------| | 5.1 | **32 Makro-Slots**, je **4 Steps** (Keycode + Modifier-Byte) = 256 Byte gesamt | ✅ | | 5.2 | Steps mit `keycode = 0` werden übersprungen (variable Makrolänge 1–4) | ✅ | | 5.3 | Timing: 10 ms Key-Down-Dauer + 20 ms Pause zwischen Steps | ✅ | | 5.4 | Makro-Tabelle in **separater NVM-Row** (Row 1, 0x1FF00, 256 Byte) | ✅ | | 5.5 | Makro-Tabelle wird beim Start aus NVM geladen; gelöschter Flash (0xFF) → leere Tabelle | ✅ | ### 6 LED-System (WS2812) | # | Anforderung | Status | |---|-------------|--------| | 6.1 | **20 WS2812-LEDs**, serpentiner Verdrahtung; Adafruit-NeoPixel bit-bang Treiber | ✅ | | 6.2 | **2-Schicht-Modell** pro Button: `base` (Idle) + `override` (temporär von GUI) | ✅ | | 6.3 | **STATIC**: feste Farbe aus NVM | ✅ | | 6.4 | **BLINK**: binäres An/Aus mit konfigurierbarer Halbperiode | ✅ | | 6.5 | **PULSE**: lineares Helligkeitsdreieck (0→255→0), kein Float | ✅ | | 6.6 | **COLOR_CYCLE** (Regenbogen): Hue-Sweep über 6 Segmente, ignoriert base/override | ✅ | | 6.7 | **COLOR_FADE**: einmaliger RGB-Crossfade zu Zielfarbe | ✅ | | 6.8 | Phasenversatz beim Start: Regenbogen-LEDs sind gleichmäßig über die Periode verteilt | ✅ | | 6.9 | `ws2812_show()` nur bei dirty-Flag aufgerufen (~600 µs Blockzeit vermieden) | ✅ | | 6.10 | Alle Animationen in **Integer-Arithmetik** (kein FPU auf M0+) | ✅ | ### 7 Konfigurations-Speicherung (NVM) | # | Anforderung | Status | |---|-------------|--------| | 7.1 | Config-Layout **Version 2**, 223 Byte packed, mit Magic `0x56503202` + CRC16-CCITT | ✅ | | 7.2 | Config gespeichert in **NVM Row 0** (0x1FE00, 256 Byte, via Linkerscript reserviert) | ✅ | | 7.3 | Makro-Tabelle in **NVM Row 1** (0x1FF00, 256 Byte) | ✅ | | 7.4 | NVM-Schreiben: Row löschen + 4 Pages à 64 Byte manuell schreiben (MANW=1) | ✅ | | 7.5 | Bei ungültigem Magic / falscher Version / CRC-Fehler → **Defaults** laden (kein Crash) | ✅ | | 7.6 | Defaults: alle Aktionen NONE, LEDs warm-weiß, Animation Regenbogen 4 s | ✅ | ### 8 Serial-Kommunikation mit VersaGUI | # | Anforderung | Status | |---|-------------|--------| | 8.1 | **CDC Serial** (USB), kein Treiber nötig; 8-Byte-Festlängen-Pakete | ✅ | | 8.2 | **Ring-Buffer** (256 Byte = 32 Pakete) für eingehende Bytes; kein Datenverlust bei Burst | ✅ | | 8.3 | **Ping / Pong** (0x05 / 0x85) zur Verbindungsdiagnose | ✅ | | 8.4 | **Config-Transfer PC→Board**: BEGIN(0x10) → 38×DATA(0x11) → COMMIT(0x12) → ACK/NACK | ✅ | | 8.5 | **Config-Dump Board→PC**: auf READ(0x13) → BEGIN(0x92) → 38×DATA(0x93) → END(0x94) | ✅ | | 8.6 | **Makro-Transfer PC→Board**: BEGIN(0x20) → 43×DATA(0x21) → COMMIT(0x22) → ACK(0x95) | ✅ | | 8.7 | **Makro-Dump Board→PC**: auf READ(0x23) → BEGIN(0x96) → 43×DATA(0x97) → END(0x98) | ✅ | | 8.8 | Config-COMMIT validiert Magic + Version + CRC; bei Fehler **NACK** ohne NVM-Schreiben | ✅ | | 8.9 | Alle Sende-Pakete nur wenn `SerialUSB` aktiv (DTR-Check verhindert stilles Verwerfen) | ✅ | ### 9 Nicht implementiert / Roadmap | # | Anforderung | Status | |---|-------------|--------| | 9.1 | **Fader/Potentiometer**: 3× ADC-Kanäle auf Board vorhanden, HAL nicht implementiert | 🔲 TODO | | 9.2 | **HOST_COMMAND-Payload**: Board sendet Command-ID, App-Seite führt aus (halbfertig) | 🔲 TODO | | 9.3 | **FADE_IN / FADE_OUT** per GUI konfigurierbar (Firmware vorhanden, kein GUI-Eintrag) | 🔲 TODO | --- ## Projekt-Struktur ``` VersaMCU/ ├── platformio.ini – Build- und Upload-Konfiguration ├── upload_openocd.py – Benutzerdefiniertes Upload-Script (OpenOCD) ├── boards/ │ ├── versapad.json – Board-Definition mit Bootloader │ └── versapad_nobl.json – Board-Definition ohne Bootloader (aktiv) ├── variants/versapad/ │ ├── variant.h/.cpp – Pin-Mapping für den SAMD21G17D │ └── linker_scripts/ – Linkerscript (kein Bootloader, NVM-Reservierung) └── src/ ├── main.cpp – Arduino setup()/loop() ├── CMainController.h/.cpp – Zentraler Orchestrator ├── CButton.h/.cpp – Button-Modell: LED-Schichten, Action, Animationen ├── CEventQueue.h/.cpp – Ring-Buffer FIFO (16 Slots, kein Heap) ├── SEvent.h – Event-Typen (KEY_DOWN/UP, ENC_CW/CCW) ├── config/ │ ├── pins.h – Pin-Nummern (Arduino-Nummern aus variant.h) │ ├── action.h – ActionType-Enum + SAction-Struct (packed) │ └── nvm_config.h/.cpp – NVM-Config: Laden, Speichern, CRC16, Defaults └── hal/ ├── matrix.h/.cpp – 5×5-Matrix-Scan, 10 ms Debounce ├── encoder.h/.cpp – Quadratur-Dekodierung via EIC-Interrupts ├── ws2812.h/.cpp – WS2812-LED-Treiber (Adafruit NeoPixel, bit-bang) ├── usb_hid.h/.cpp – HID Keyboard + Consumer Control └── usb_serial.h/.cpp – CDC Serial bidirektional, 8-Byte-Pakete ``` ## Architektur ### Loop-Ablauf ``` loop() ├── matrix_scan() → matrix_cb() → CEventQueue.push() │ (Encoder-ISRs laufen asynchron) → CEventQueue.push() ├── poll_vendor() → Serial-Pakete von VersaGUI verarbeiten ├── processEvents() → Queue leeren, Aktionen ausführen └── updateLEDs() → Dirty-CButtons → WS2812-Buffer → show() ``` ### CButton – LED-Schichten Jeder MX-Button hat zwei LED-Schichten: - **base**: Konfigurierte Idle-Farbe (aus NVM) - **override**: Temporär von VersaGUI gesetzt (Benachrichtigungen etc.) Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sofort zu `base` zurück. ### LED-Animationen | Animation | Verhalten | |---|---| | `STATIC` | Feste Farbe | | `BLINK` | Binäres An/Aus | | `PULSE` | Lineares Dreieck 0→255→0 | | `FADE_IN / FADE_OUT` | Einmaliges Ein-/Ausblenden | | `COLOR_CYCLE` | Hue-Sweep (Regenbogen), ignoriert base/override | | `COLOR_FADE` | Crossfade zu Zielfarbe | Alle Berechnungen in Integer-Arithmetik (kein FPU auf Cortex-M0+). **Warum Bit-Bang statt DMA?** WS2812-DMA auf dem SAMD21 würde einen SERCOM im SPI-Modus bei exakt 2,4 MHz benötigen, wobei jedes WS2812-Bit als 3 SPI-Bits kodiert wird (`110` = 1, `100` = 0). Das erfordert einen zusätzlichen Puffer von 20 LEDs × 24 Bit × 3 = 1440 Byte — mehr als 8 % des gesamten RAM — plus DMAC-Konfiguration und Transfer-Ende-Erkennung. `ws2812_show()` blockiert ~600 µs mit gesperrten Interrupts, wird aber nur bei gesetztem dirty-Flag aufgerufen. Bei 20 ms Loop-Rate entspricht das 3 % der Loop-Zeit. Encoder-Impulse, die in dieses Fenster fallen, werden maximal um eine Loop-Iteration verzögert; bei typischen Drehgeschwindigkeiten (< 20 Rastschritte/s, Impulsabstand > 50 ms) ist das Risiko eines verlorenen Impulses praktisch null. Ergebnis: Bit-Bang via Adafruit NeoPixel reicht für 20 LEDs vollständig aus, belegt keinen SERCOM und keinen zusätzlichen RAM. **Warum keine Adafruit-Animationsfunktionen?** Die Adafruit-NeoPixel-Library stellt ausschließlich den LED-Treiber bereit (`setPixelColor`, `show`, `fill`, `clear`). Animations-Logik (Blinken, Pulsieren, Farbverläufe) ist nicht enthalten und muss in jedem Fall selbst implementiert werden. Darüber hinaus: - `Adafruit_NeoPixel::ColorHSV()` verwendet intern float-Operationen für die HSV→RGB-Konvertierung. Der Cortex-M0+ hat keine FPU; float wird per Software emuliert (~10–20× langsamer). `hue_to_rgb()` in `CButton.cpp` erreicht dasselbe Ergebnis mit reiner Integer-Arithmetik (6 lineare Segmente à 43 Hue-Einheiten). - Das 2-Schicht-Modell (base + override) und die dirty-Flag-gesteuerte Render-Pipeline sind projektspezifische Logik ohne Entsprechung in der Library. **Idle-Zustand:** Alle 20 MX-LEDs zeigen einen rotierenden Regenbogen (`COLOR_CYCLE`, 4 s/Runde, 40 % Helligkeit, gleichmäßig phasenverschoben). ### Serial-Protokoll (8 Bytes, fixed) ``` Byte 0: Command/Event-ID Byte 1: key_id (Button 0–24 oder Encoder 0–3) Byte 2: r / Daten-Byte A Byte 3: g / Daten-Byte B Byte 4: b Byte 5–7: reserviert (0x00) ``` | ID | Richtung | Bedeutung | |---|---|---| | 0x01 | PC→Board | LED-Override setzen | | 0x02 | PC→Board | LED-Override löschen | | 0x03 | PC→Board | LED-Base setzen | | 0x05 | PC→Board | Ping | | 0x10 | PC→Board | Config-Begin (Chunks-Anzahl) | | 0x11 | PC→Board | Config-Data (Chunk-Index + 6B Nutzdaten) | | 0x12 | PC→Board | Config-Commit (CRC prüfen + NVM schreiben) | | 0x13 | PC→Board | Config-Read (Board sendet NVM-Config zurück) | | 0x81 | Board→PC | KEY_DOWN | | 0x82 | Board→PC | KEY_UP | | 0x83 | Board→PC | ENC_CW | | 0x84 | Board→PC | ENC_CCW | | 0x85 | Board→PC | Pong | | 0x90 | Board→PC | Config-ACK | | 0x91 | Board→PC | Config-NACK | | 0x20 | PC→Board | Makro-Begin (Chunk-Anzahl = 43) | | 0x21 | PC→Board | Makro-Data (Chunk-Index + 6B Nutzdaten) | | 0x22 | PC→Board | Makro-Commit (in NVM schreiben) | | 0x23 | PC→Board | Makro-Read (Board sendet Tabelle zurück) | | 0x90 | Board→PC | Config-ACK | | 0x91 | Board→PC | Config-NACK (CRC/Magic/Version ungültig) | | 0x92 | Board→PC | Config-Begin (Dump-Start, Chunks-Anzahl) | | 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) | | 0x94 | Board→PC | Config-End | | 0x95 | Board→PC | Makro-ACK | | 0x96 | Board→PC | Makro-Begin (Dump-Start) | | 0x97 | Board→PC | Makro-Data (Chunk-Index + 6B) | | 0x98 | Board→PC | Makro-End | ### NVM-Config-Layout (Version 2, 223 Bytes, packed) ``` Offset 0 4B Magic 0x56503202 Offset 4 1B Version 2 Offset 5 2B CRC16-CCITT (über Bytes 7–222) Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B) Offset 67 36B enc_actions[4][3] – je 3B Offset 103 20B led_r[20] Offset 123 20B led_g[20] Offset 143 20B led_b[20] Offset 163 20B led_anim[20] – LEDAnim-Typ (uint8_t) Offset 183 40B led_period_ms[20] – Animationsperiode in ms (uint16_t, LE) ``` **NVM Row 0** (0x1FE00, 256 Byte): Config (223B genutzt, 33B Padding) **NVM Row 1** (0x1FF00, 256 Byte): Makro-Tabelle (32 Slots × 4 Steps × 2B) Via Linkerscript reserviert. Bei ungültigem Magic/Version/CRC werden Defaults geladen. ## Bekannte Fallstricke | Problem | Lösung | |---|---| | Kaltstart hängt (XOSC32KRDY) | `-DCRYSTALLESS` in build_flags pflicht | | Adafruit NeoPixel ZeroDMA inkompatibel | Standard bit-bang Library verwenden | | WS2812 Pegel 3.3V statt 5V | LED 0 marginal OK, ab LED 1 selbst-regenerierend. Fix nächste PCB-Rev: Level-Shifter | | `ws2812_show()` blockiert ~600 µs | Dirty-Flag-Pattern: nur aufrufen wenn nötig, nie aus ISR | | SAction muss `__attribute__((packed))` haben | Ohne packed: 4B statt 3B → CRC-Mismatch beim Config-Laden | | Windows HID-Descriptor-Cache | Bei PID-Änderung Board neu einstecken |