VersaMCU
Firmware für das VersaPad v2 Macro-Pad.
Läuft auf einem ATSAMD21G17D (Cortex-M0+), entwickelt mit PlatformIO + Arduino-Framework.
Voraussetzungen
- PlatformIO (CLI oder VS Code Extension)
- Atmel-ICE Debugger/Programmer (SWD-Verbindung zur PCB)
- OpenOCD – wird von PlatformIO automatisch installiert
Flashen
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+).
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 |