From b49984b9c05314f10a6d7e863241b5b015a40ce7 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 29 Mar 2026 14:47:13 +0200 Subject: [PATCH] Initial commit --- .gitignore | 8 + README.md | 143 +++++++ boards/versapad.json | 35 ++ boards/versapad_nobl.json | 30 ++ platformio.ini | 29 ++ src/CButton.cpp | 280 ++++++++++++++ src/CButton.h | 136 +++++++ src/CEventQueue.cpp | 43 +++ src/CEventQueue.h | 37 ++ src/CMainController.cpp | 353 ++++++++++++++++++ src/CMainController.h | 50 +++ src/SEvent.h | 25 ++ src/config/action.h | 18 + src/config/nvm_config.cpp | 116 ++++++ src/config/nvm_config.h | 56 +++ src/config/pins.h | 68 ++++ src/hal/encoder.cpp | 88 +++++ src/hal/encoder.h | 10 + src/hal/matrix.cpp | 68 ++++ src/hal/matrix.h | 22 ++ src/hal/usb_hid.cpp | 96 +++++ src/hal/usb_hid.h | 35 ++ src/hal/usb_serial.cpp | 56 +++ src/hal/usb_serial.h | 70 ++++ src/hal/ws2812.cpp | 35 ++ src/hal/ws2812.h | 30 ++ src/main.cpp | 27 ++ upload_openocd.py | 24 ++ .../gcc/flash_with_bootloader.ld | 100 +++++ .../gcc/flash_without_bootloader.ld | 108 ++++++ variants/versapad/variant.cpp | 100 +++++ variants/versapad/variant.h | 98 +++++ 32 files changed, 2394 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 boards/versapad.json create mode 100644 boards/versapad_nobl.json create mode 100644 platformio.ini create mode 100644 src/CButton.cpp create mode 100644 src/CButton.h create mode 100644 src/CEventQueue.cpp create mode 100644 src/CEventQueue.h create mode 100644 src/CMainController.cpp create mode 100644 src/CMainController.h create mode 100644 src/SEvent.h create mode 100644 src/config/action.h create mode 100644 src/config/nvm_config.cpp create mode 100644 src/config/nvm_config.h create mode 100644 src/config/pins.h create mode 100644 src/hal/encoder.cpp create mode 100644 src/hal/encoder.h create mode 100644 src/hal/matrix.cpp create mode 100644 src/hal/matrix.h create mode 100644 src/hal/usb_hid.cpp create mode 100644 src/hal/usb_hid.h create mode 100644 src/hal/usb_serial.cpp create mode 100644 src/hal/usb_serial.h create mode 100644 src/hal/ws2812.cpp create mode 100644 src/hal/ws2812.h create mode 100644 src/main.cpp create mode 100644 upload_openocd.py create mode 100644 variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld create mode 100644 variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld create mode 100644 variants/versapad/variant.cpp create mode 100644 variants/versapad/variant.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18c72d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# PlatformIO +.pio/ + +# VS Code +.vscode/ + +# macOS +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..43f5206 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ +# 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. + +## 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 | +| 0x92 | Board→PC | Config-Begin (Dump-Start, Chunks-Anzahl) | +| 0x93 | Board→PC | Config-Data (Chunk-Index + 6B) | +| 0x94 | Board→PC | Config-End | + +### NVM-Config-Layout (163 Bytes, packed) + +``` +Offset 0 4B Magic 0x56503202 +Offset 4 1B Version 1 +Offset 5 2B CRC16-CCITT (über Bytes 7–162) +Offset 7 60B mx_actions[20] – je 3B: type(1B) + data(2B) +Offset 67 36B enc_actions[4][3] – je 3B +Offset103 20B led_r[20] +Offset123 20B led_g[20] +Offset143 20B led_b[20] +``` + +Gespeichert in den letzten 512 Bytes des Flashs (0x1FE00), via Linkerscript reserviert. +Bei ungültigem Magic/CRC werden Defaults geladen (alle Tasten: NONE, LEDs: warm-weiß). + +## 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 | diff --git a/boards/versapad.json b/boards/versapad.json new file mode 100644 index 0000000..ee0f346 --- /dev/null +++ b/boards/versapad.json @@ -0,0 +1,35 @@ +{ + "build": { + "arduino": { + "ldscript": "flash_with_bootloader.ld", + "variant": "versapad" + }, + "core": "arduino", + "variant": "versapad", + "cpu": "cortex-m0plus", + "extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__", + "f_cpu": "48000000L", + "hwids": [ + ["0x239A", "0x0011"] + ], + "mcu": "samd21g18a", + "usb_product": "VersaPad v2", + "usb_manufacturer": "Custom" + }, + "connectivity": ["usb"], + "frameworks": ["arduino"], + "name": "VersaPad v2 (USB bootloader)", + "upload": { + "maximum_ram_size": 32768, + "maximum_size": 253952, + "disable_flushing": true, + "native_usb": true, + "offset": "0x2000", + "protocol": "sam-ba", + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true + }, + "url": "", + "vendor": "Custom" +} diff --git a/boards/versapad_nobl.json b/boards/versapad_nobl.json new file mode 100644 index 0000000..143be47 --- /dev/null +++ b/boards/versapad_nobl.json @@ -0,0 +1,30 @@ +{ + "build": { + "arduino": { + "ldscript": "flash_without_bootloader.ld", + "variant": "versapad" + }, + "core": "arduino", + "variant": "versapad", + "cpu": "cortex-m0plus", + "extra_flags": "-DARDUINO_SAMD_ZERO -DARM_MATH_CM0PLUS -D__SAMD21G18A__", + "f_cpu": "48000000L", + "hwids": [ + ["0x239A", "0x0042"] + ], + "mcu": "samd21g17d", + "usb_product": "VersaPad v2" + }, + "connectivity": ["usb"], + "frameworks": ["arduino"], + "name": "VersaPad v2 (Atmel-ICE, no bootloader)", + "upload": { + "maximum_ram_size": 16384, + "maximum_size": 131072, + "protocol": "atmel-ice", + "require_upload_port": false, + "use_1200bps_touch": false + }, + "url": "", + "vendor": "Custom" +} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..eacc5eb --- /dev/null +++ b/platformio.ini @@ -0,0 +1,29 @@ +; VersaPad v2 – PlatformIO Configuration +; Custom SAMD21G18A board (custom PCB) +; Programmer: Atmel-ICE via SWD (kein Bootloader nötig) + +[common] +platform = atmelsam +framework = arduino +board_build.variants_dir = ${PROJECT_DIR}/variants +lib_deps = adafruit/Adafruit NeoPixel +build_flags = + -DUSB_PRODUCT='"VersaPad v2"' + -DUSB_MANUFACTURER='"Custom"' + -DUSB_VID=0x239A + -DUSB_PID=0x0042 + -DCRYSTALLESS + +; ── Atmel-ICE via SWD (Hauptkonfiguration) ──────────────────────────────────── +[env:versapad] +extends = common +board = versapad_nobl +upload_protocol = custom +extra_scripts = upload_openocd.py +debug_tool = openocd + +; ── USB SAM-BA (nur wenn Bootloader geflasht ist) ───────────────────────────── +; [env:versapad_usb] +; extends = common +; board = versapad +; upload_protocol = sam-ba diff --git a/src/CButton.cpp b/src/CButton.cpp new file mode 100644 index 0000000..469fc14 --- /dev/null +++ b/src/CButton.cpp @@ -0,0 +1,280 @@ +// CButton.cpp +// Implementierung der CButton-Klasse. +// +// Animations-Arithmetik: +// Alle Berechnungen in Integer (kein float) – Cortex-M0+ hat keine FPU. +// Helligkeits-Skalierung: (Kanalwert * scale) / 255 +// PULSE nutzt ein lineares Dreieck (0→255→0) statt Sinus – einfacher, trotzdem +// weich genug für den visuellen Eindruck bei 20–50ms Loop-Rate. +// +// COLOR_CYCLE – Hue-Sweep: +// Teilt den Farbraum in 6 Segmente (R→Y→G→C→B→M→R), je 43 Hue-Einheiten. +// Innerhalb jedes Segments steigt/fällt ein Kanal linear – kein HSV-Float nötig. +// +// COLOR_FADE – Crossfade: +// Lineare RGB-Interpolation zwischen m_anim_from und m_anim_to. +// t = elapsed * 255 / period (0→255); Ergebnis: from + (to-from)*t/255. + +#include +#include "CButton.h" +#include "hal/ws2812.h" + +// ── Hue → RGB (vollständig gesättigte Farbe, S=255, V=255) ─────────────────── +// +// Hue 0–255 wird auf 6 Segmente à 43 Einheiten aufgeteilt: +// Seg 0: R=255, G steigt, B=0 (Rot → Gelb) +// Seg 1: R fällt, G=255, B=0 (Gelb → Grün) +// Seg 2: R=0, G=255, B steigt (Grün → Cyan) +// Seg 3: R=0, G fällt, B=255 (Cyan → Blau) +// Seg 4: R steigt, G=0, B=255 (Blau → Magenta) +// Seg 5: R=255, G=0, B fällt (Magenta → Rot) +static RGB hue_to_rgb(uint8_t h) +{ + uint8_t seg = h / 43; + uint8_t frac = (uint8_t)((h % 43) * 6); // 0–252, Annäherung an 0–255 + + switch (seg) { + case 0: return RGB(255, frac, 0); + case 1: return RGB(255 - frac, 255, 0); + case 2: return RGB(0, 255, frac); + case 3: return RGB(0, 255 - frac, 255); + case 4: return RGB(frac, 0, 255); + default: return RGB(255, 0, 255 - frac); + } +} + +// ── Konstruktor ─────────────────────────────────────────────────────────────── + +CButton::CButton() + : m_key_id(0) + , m_led_index(-1) + , m_action{ActionType::NONE, 0} + , m_base() + , m_override() + , m_override_active(false) + , m_dirty(false) + , m_anim(LEDAnim::STATIC) + , m_anim_period_ms(500) + , m_anim_start_ms(0) + , m_anim_from() + , m_anim_to() +{} + +// ── Initialisierung ─────────────────────────────────────────────────────────── + +void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base) +{ + m_key_id = key_id; + m_led_index = led_index; + m_action = action; + m_base = base; + m_override_active = false; + m_anim = LEDAnim::STATIC; + m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben +} + +// ── Tastendruck-Hooks ───────────────────────────────────────────────────────── + +void CButton::on_press() +{ + // Reserviert für zukünftige Button-Logik (Hold, Toggle, Doppelklick …) +} + +void CButton::on_release() +{ + // Reserviert für zukünftige Button-Logik +} + +// ── LED Layer 1: base ───────────────────────────────────────────────────────── + +void CButton::set_base(RGB color) +{ + m_base = color; + m_dirty = true; +} + +// ── LED Layer 2: override ───────────────────────────────────────────────────── + +void CButton::set_override(RGB color) +{ + m_override = color; + m_override_active = true; + m_dirty = true; +} + +void CButton::clear_override() +{ + m_override_active = false; + m_dirty = true; // Base-Farbe muss neu in WS2812-Buffer geschrieben werden +} + +// ── LED-Animation ───────────────────────────────────────────────────────────── + +void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms) +{ + m_anim = anim; + m_anim_period_ms = (period_ms > 0) ? period_ms : 1; // Division durch 0 vermeiden + // phase_offset_ms in die Vergangenheit zurücksetzen → verschobener Startpunkt + m_anim_start_ms = millis() - phase_offset_ms; + m_dirty = true; +} + +void CButton::set_color_fade(RGB to, uint16_t period_ms) +{ + // Startfarbe = Snapshot der aktuell aktiven Farbe (override > base) + m_anim_from = m_override_active ? m_override : m_base; + m_anim_to = to; + m_anim = LEDAnim::COLOR_FADE; + m_anim_period_ms = (period_ms > 0) ? period_ms : 1; + m_anim_start_ms = millis(); + m_dirty = true; +} + +void CButton::clear_anim() +{ + m_anim = LEDAnim::STATIC; + m_dirty = true; // Sofort wieder volle Helligkeit rendern +} + +// ── Animations-Hilfsmethoden ────────────────────────────────────────────────── + +// Gibt true zurück solange die Animation noch läuft. +// BLINK, PULSE, COLOR_CYCLE laufen endlos; FADE_*/COLOR_FADE enden nach period_ms. +bool CButton::is_animating() const +{ + if (m_anim == LEDAnim::STATIC) return false; + if (m_anim == LEDAnim::BLINK || + m_anim == LEDAnim::PULSE || + m_anim == LEDAnim::COLOR_CYCLE) return true; + // FADE_IN / FADE_OUT / COLOR_FADE: läuft bis elapsed >= period + return (millis() - m_anim_start_ms) < (uint32_t)m_anim_period_ms; +} + +// Berechnet den aktuellen Helligkeits-Skalierungsfaktor (0 = aus, 255 = voll). +// Darf m_anim und m_dirty verändern (Abschluss von FADE_IN/FADE_OUT). +uint8_t CButton::compute_scale() +{ + if (m_anim == LEDAnim::STATIC) return 255; + + uint32_t elapsed = millis() - m_anim_start_ms; + uint32_t period = (uint32_t)m_anim_period_ms; + + switch (m_anim) { + + case LEDAnim::BLINK: { + // An für erste Halbperiode, aus für zweite + uint32_t t = elapsed % period; + return (t < period / 2) ? 255 : 0; + } + + case LEDAnim::PULSE: { + // Lineares Dreieck: 0 → 255 → 0 in einer Periode + uint32_t t = elapsed % period; + uint32_t half = period / 2; + if (t < half) + return (uint8_t)((t * 255) / half); + else + return (uint8_t)(((period - t) * 255) / half); + } + + case LEDAnim::FADE_IN: { + // Einmalig: schwarz → volle Helligkeit + if (elapsed >= period) { + // Animation abgeschlossen – ab jetzt STATIC bei voller Helligkeit + m_anim = LEDAnim::STATIC; + m_dirty = true; + return 255; + } + return (uint8_t)((elapsed * 255) / period); + } + + case LEDAnim::FADE_OUT: { + // Einmalig: volle Helligkeit → schwarz + if (elapsed >= period) { + // Animation abgeschlossen – LED bleibt aus. + // set_base() oder set_override() bringt sie zurück. + m_anim = LEDAnim::STATIC; + m_base = RGB(0, 0, 0); // Base auf schwarz setzen damit LED dunkel bleibt + m_dirty = true; + return 0; + } + return (uint8_t)(((period - elapsed) * 255) / period); + } + + default: + return 255; + } +} + +// Berechnet die finale RGB-Farbe für alle Animationstypen. +// Helligkeits-Animationen (STATIC/BLINK/PULSE/FADE_*): +// Wendet compute_scale() auf die aktive Farbe (override > base) an. +// Farb-Animationen (COLOR_CYCLE/COLOR_FADE): +// Berechnet die Farbe direkt; base/override werden nicht verändert. +RGB CButton::compute_rgb() +{ + switch (m_anim) { + + case LEDAnim::COLOR_CYCLE: { + // Hue 0→255 über eine Periode, dann Wiederholung; 40% Helligkeit (102/255) + uint32_t elapsed = millis() - m_anim_start_ms; + uint32_t period = (uint32_t)m_anim_period_ms; + uint8_t hue = (uint8_t)((elapsed % period) * 255 / period); + RGB c = hue_to_rgb(hue); + return RGB( + (uint8_t)(((uint16_t)c.r * 102) / 255), + (uint8_t)(((uint16_t)c.g * 102) / 255), + (uint8_t)(((uint16_t)c.b * 102) / 255) + ); + } + + case LEDAnim::COLOR_FADE: { + // Linearer Crossfade von m_anim_from nach m_anim_to + uint32_t elapsed = millis() - m_anim_start_ms; + uint32_t period = (uint32_t)m_anim_period_ms; + if (elapsed >= period) { + // Abgeschlossen – base auf Zielfarbe setzen, ab jetzt STATIC + m_anim = LEDAnim::STATIC; + m_base = m_anim_to; + m_dirty = true; + return m_anim_to; + } + uint8_t t = (uint8_t)((elapsed * 255) / period); // 0→255 + int16_t dr = (int16_t)m_anim_to.r - (int16_t)m_anim_from.r; + int16_t dg = (int16_t)m_anim_to.g - (int16_t)m_anim_from.g; + int16_t db = (int16_t)m_anim_to.b - (int16_t)m_anim_from.b; + return RGB( + (uint8_t)(m_anim_from.r + (dr * t) / 255), + (uint8_t)(m_anim_from.g + (dg * t) / 255), + (uint8_t)(m_anim_from.b + (db * t) / 255) + ); + } + + default: { + // Helligkeits-Animation: compute_scale() × aktive Farbe + const RGB& color = m_override_active ? m_override : m_base; + uint8_t scale = compute_scale(); + return RGB( + (uint8_t)(((uint16_t)color.r * scale) / 255), + (uint8_t)(((uint16_t)color.g * scale) / 255), + (uint8_t)(((uint16_t)color.b * scale) / 255) + ); + } + } +} + +// ── Rendering ───────────────────────────────────────────────────────────────── + +bool CButton::render_led() +{ + if (!has_led()) return false; + + bool animating = is_animating(); + if (!m_dirty && !animating) return false; // Nichts zu tun + + RGB c = compute_rgb(); + ws2812_set(static_cast(m_led_index), c.r, c.g, c.b); + + m_dirty = false; + return true; +} diff --git a/src/CButton.h b/src/CButton.h new file mode 100644 index 0000000..c6f0cdd --- /dev/null +++ b/src/CButton.h @@ -0,0 +1,136 @@ +// CButton.h +// Bildet eine einzelne Taste (MX-Button oder Encoder-SW) vollständig ab: +// - Zugehöriger WS2812-LED-Index (oder -1 wenn keine LED) +// - Konfigurierte Aktion (HID_KEY, HID_CONSUMER, HOST_COMMAND, NONE) +// - 2-Layer-LED-Modell: base (Idle-Farbe) + override (temporär, z.B. Benachrichtigung) +// - LED-Animationen: STATIC, BLINK, PULSE, FADE_IN, FADE_OUT, COLOR_CYCLE, COLOR_FADE +// +// LED-Rendering: +// Aktive Farbe = override wenn gesetzt, sonst base. +// Helligkeits-Animationen (BLINK, PULSE, FADE_*) modulieren die Helligkeit der aktiven Farbe. +// Farb-Animationen (COLOR_CYCLE, COLOR_FADE) berechnen die Farbe selbst (ignorieren base/override). +// render_led() gibt true zurück solange eine Animation läuft oder dirty gesetzt ist – +// CMainController::updateLEDs() ruft dann ws2812_show() auf. +// +// Aktionsausführung: +// on_press() / on_release() sind Hooks für zukünftige Button-Logik. +// Die eigentliche Aktion wird vom CMainController via action() ausgeführt. + +#pragma once +#include +#include "config/action.h" + +// ── Farb-Struct ─────────────────────────────────────────────────────────────── + +struct RGB +{ + uint8_t r, g, b; + RGB() : r(0), g(0), b(0) {} + RGB(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b) {} +}; + +// ── LED-Animationen ─────────────────────────────────────────────────────────── +// +// Helligkeits-Animationen (BLINK, PULSE, FADE_*): +// Modulieren die Helligkeit der aktiven Farbe (base oder override). +// BLINK und PULSE laufen endlos bis clear_anim() aufgerufen wird. +// FADE_IN und FADE_OUT sind einmalig – nach Ablauf zurück zu STATIC. +// FADE_OUT: nach Abschluss leuchtet die LED nicht (base wird auf schwarz gesetzt). +// +// Farb-Animationen (COLOR_CYCLE, COLOR_FADE): +// Berechnen die Farbe selbst – base/override werden ignoriert, aber nicht verändert. +// COLOR_CYCLE läuft endlos; COLOR_FADE ist einmalig (base wird auf Zielfarbe gesetzt). +// Für COLOR_FADE: set_color_fade(to, period_ms) statt set_anim() verwenden. + +enum class LEDAnim : uint8_t +{ + STATIC = 0, // Sofort, keine Animation (Standardzustand) + BLINK, // Binäres An/Aus – period_ms = Halbperiode (An-Zeit = Aus-Zeit) + 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 + COLOR_CYCLE, // Endloser Hue-Sweep (Regenbogen) – period_ms = eine volle Runde + COLOR_FADE, // Einmalig: Crossfade von Startfarbe → Zielfarbe über period_ms +}; + +// ── CButton ─────────────────────────────────────────────────────────────────── + +class CButton +{ +public: + CButton(); + + // Initialisierung (ersetzt Konstruktor-Parameter). + // led_index = -1 → kein LED (Encoder-SW-Buttons). + void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB()); + + // Hooks für Tastendruck/-loslassen. + // Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …). + void on_press(); + void on_release(); + + // ── LED Layer 1: base ───────────────────────────────────────────────────── + // Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt. + void set_base(RGB color); + + // ── LED Layer 2: override ───────────────────────────────────────────────── + // Temporärer Override, überschreibt base solange aktiv. + // Typisch: Windows-App signalisiert Benachrichtigung. + void set_override(RGB color); + void clear_override(); // Zurück zu base + + // ── 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_*). + // 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). + void set_anim(LEDAnim anim, uint16_t period_ms = 500, uint16_t phase_offset_ms = 0); + + // Crossfade von der aktuell aktiven Farbe zur Zielfarbe. + // Nach Abschluss wird base auf die Zielfarbe gesetzt → LED bleibt in Zielfarbe. + void set_color_fade(RGB to, uint16_t period_ms = 500); + + void clear_anim(); // Zurück zu STATIC, Helligkeit sofort wieder voll + + // ── Rendering ───────────────────────────────────────────────────────────── + // Schreibt aktuelle Farbe (mit Animations-Skalierung) in den WS2812-Buffer. + // Gibt true zurück wenn ws2812_show() nötig ist: + // - dirty-Flag gesetzt (Farbe hat sich geändert), oder + // - Animation läuft (jeder Frame braucht einen neuen ws2812_set-Aufruf) + bool render_led(); + + uint8_t key_id() const { return m_key_id; } + bool has_led() const { return m_led_index >= 0; } + SAction action() const { return m_action; } + +private: + uint8_t m_key_id; + int8_t m_led_index; + SAction m_action; + + // LED-Zustand + RGB m_base; + RGB m_override; + bool m_override_active; + bool m_dirty; // true = render_led() muss ws2812_set() aufrufen + + // Animations-Zustand + LEDAnim m_anim; + uint16_t m_anim_period_ms; + uint32_t m_anim_start_ms; + RGB m_anim_from; // COLOR_FADE: Startfarbe (Snapshot bei set_color_fade()) + RGB m_anim_to; // COLOR_FADE: Zielfarbe + + // Helfer: Helligkeits-Skalierungsfaktor (0–255) für BLINK/PULSE/FADE_*. + // Aktualisiert m_anim bei Abschluss von FADE_IN/FADE_OUT. + uint8_t compute_scale(); + + // Helfer: finale RGB-Farbe für alle Animationstypen. + // Ruft compute_scale() für Helligkeits-Animationen auf; + // berechnet Farbe direkt für COLOR_CYCLE und COLOR_FADE. + RGB compute_rgb(); + + // true wenn die Animation noch läuft (BLINK/PULSE/COLOR_CYCLE immer, FADE* bis Ablauf) + bool is_animating() const; +}; diff --git a/src/CEventQueue.cpp b/src/CEventQueue.cpp new file mode 100644 index 0000000..4761122 --- /dev/null +++ b/src/CEventQueue.cpp @@ -0,0 +1,43 @@ +// CEventQueue.cpp +// Ring-Buffer FIFO-Implementierung. +// +// Funktionsprinzip (klassischer Power-of-2-freier Ring-Buffer): +// m_head = Lese-Index (pop) +// m_tail = Schreib-Index (push) +// 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. + +#include "CEventQueue.h" + +bool CEventQueue::is_empty() const +{ + return m_head == m_tail; +} + +bool CEventQueue::is_full() const +{ + // Voll wenn der nächste Schreib-Index auf den Lese-Index zeigen würde + return ((m_tail + 1) % QUEUE_SIZE) == m_head; +} + +bool CEventQueue::push(SEvent ev) +{ + if (is_full()) return false; // Event verwerfen – sollte bei 16 Slots nie passieren + m_buf[m_tail] = ev; + m_tail = (m_tail + 1) % QUEUE_SIZE; + return true; +} + +bool CEventQueue::pop(SEvent& out) +{ + if (is_empty()) return false; + out = m_buf[m_head]; + m_head = (m_head + 1) % QUEUE_SIZE; + return true; +} diff --git a/src/CEventQueue.h b/src/CEventQueue.h new file mode 100644 index 0000000..58adac6 --- /dev/null +++ b/src/CEventQueue.h @@ -0,0 +1,37 @@ +// CEventQueue.h +// Fester Ring-Buffer FIFO für SEvent-Objekte. +// +// Bewusst ohne Heap (kein new/delete) und ohne STL (kein std::vector/queue) +// um auf dem SAMD21 mit 16KB RAM deterministisches Verhalten zu garantieren. +// +// 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. + +#pragma once +#include "SEvent.h" +#include + +class CEventQueue +{ +public: + // Event einreihen. Gibt false zurück wenn Queue voll (Event wird verworfen). + bool push(SEvent ev); + + // Ältestes Event abholen (FIFO). Gibt false zurück wenn Queue leer. + bool pop(SEvent& out); + + bool is_empty() const; + bool is_full() const; + +private: + static const uint8_t QUEUE_SIZE = 17; // 16 nutzbare Slots + SEvent m_buf[QUEUE_SIZE]; + uint8_t m_head = 0; // Nächster Lese-Index (Consumer: pop) + uint8_t m_tail = 0; // Nächster Schreib-Index (Producer: push) +}; diff --git a/src/CMainController.cpp b/src/CMainController.cpp new file mode 100644 index 0000000..7f5a499 --- /dev/null +++ b/src/CMainController.cpp @@ -0,0 +1,353 @@ +// CMainController.cpp +// Zentraler Orchestrator des VersaPad v2. +// +// Aufgaben: +// 1. Alle Hardware-Peripherie initialisieren (Matrix, Encoder, USB) +// 2. Pro Loop-Durchlauf: +// a) matrix_scan() → Callback → Events in Queue +// b) Encoder-ISRs laufen asynchron → Events in Queue +// c) poll_vendor() → eingehende Serial-Pakete (PC→Board) direkt verarbeiten +// d) processEvents() → Queue leeren, Aktionen ausführen +// e) updateLEDs() → dirty CButtons in WS2812-Buffer schreiben + show() +// +// Datenfluss: +// HAL (matrix_cb / encoder_cb) +// └─► CEventQueue +// └─► processEvents() +// ├─► CButton.on_press() / on_release() +// ├─► execute_action() → USB HID / Serial +// └─► usb_serial_send() (nur bei HOST_COMMAND) +// +// SerialUSB (PC→Board) +// └─► poll_vendor() +// └─► CButton.set_override() / set_base() / clear_override() + +#include +#include +#include "CMainController.h" +#include "hal/ws2812.h" +#include "hal/matrix.h" +#include "hal/encoder.h" +#include "hal/usb_serial.h" +#include "config/pins.h" +#include "config/nvm_config.h" +#include + +// ─── Static Bridge: HAL-Callbacks → EventQueue ─────────────────────────────── +// +// matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein +// Lambda mit Capture möglich auf Cortex-M0+). Der Queue-Pointer wird einmalig +// in setup() gesetzt, bevor die Callbacks registriert werden. + +static CEventQueue* s_queue = nullptr; + +// Wird von matrix_scan() aufgerufen wenn sich ein Tasten-Zustand ändert. +// Läuft im Loop-Kontext (kein ISR). +static void matrix_cb(uint8_t key, bool pressed) +{ + if (!s_queue) return; + SEvent ev; + ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP; + ev.key_id = key; + ev.payload = 0; + s_queue->push(ev); +} + +// Wird von handle_encoder() aufgerufen – läuft im ISR-Kontext (EIC-Interrupt). +// CEventQueue::push() ist interrupt-sicher (kein Heap, atomare Indizes auf M0+). +static void encoder_cb(uint8_t enc, int8_t dir) +{ + if (!s_queue) return; + SEvent ev; + ev.type = (dir > 0) ? EventType::ENC_CW : EventType::ENC_CCW; + ev.key_id = enc; + ev.payload = 0; + s_queue->push(ev); +} + +// ─── Konstruktor / Setup ────────────────────────────────────────────────────── + +CMainController::CMainController() + : m_cfg_chunks_expected(0) + , m_cfg_receiving(false) +{ + memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); +} + +void CMainController::setup() +{ + init_buttons(); // Buttons aus NVM laden (oder Defaults) + s_queue = &m_queue; // Queue-Pointer setzen bevor Callbacks registriert werden + usb_hid_init(); // HID-Descriptor registriert sich via globalem Konstruktor, + // usb_hid_init() ist hier ein No-Op aber verdeutlicht die Abhängigkeit + usb_serial_init(); // CDC Serial öffnen + matrix_init(matrix_cb); // Matrix-Scan initialisieren + Callback registrieren + encoder_init(encoder_cb);// EIC-Interrupts für alle 4 Encoder einrichten +} + +// Lädt Config aus NVM und initialisiert alle CButton-Instanzen. +// key_id-Mapping: +// 0–3 : Encoder-SW-Buttons (COL_0 × ROW_0–3), kein LED +// 4 : nicht belegt (COL_0 × ROW_4) +// 5–24 : Cherry MX Buttons (COL_1–4 × ROW_0–4), je ein WS2812-LED +void CMainController::init_buttons() +{ + SDeviceConfig cfg; + bool valid = nvm_config_load(cfg); + (void)valid; // false = keine gültige Config → Defaults wurden bereits geladen + + // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) + for (uint8_t enc = 0; enc < 4; enc++) { + m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW]); + } + + // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, + // Aktion + Base-Farbe aus NVM. + // mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4) + // + // Idle-Animation: Regenbogen-Sweep über alle 20 LEDs. + // Jede LED bekommt einen gleichmäßigen Hue-Versatz (phase = idx * period / 20), + // sodass immer ein voller Regenbogen auf dem Pad liegt und sich langsam dreht. + const uint16_t k_rainbow_period = 4000; // 4s pro volle Runde + + for (uint8_t key = 5; key < MATRIX_KEYS; key++) { + uint8_t col = key / MATRIX_ROWS; + uint8_t row = key % MATRIX_ROWS; + int8_t led = static_cast(LED_INDEX(col, row)); + uint8_t mx_idx = key - 5; + RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]); + m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base); + + // Phase gleichmäßig verteilen: LED 0 = Hue 0, LED 19 = Hue ~242 (fast voll) + uint16_t phase = (uint16_t)((uint32_t)mx_idx * k_rainbow_period / 20); + m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, k_rainbow_period, phase); + } + + // Encoder CW/CCW-Aktionen separat merken – Encoder haben kein CButton-Objekt + // da sie keine LED haben und kein Matrix-Key sind. + for (uint8_t enc = 0; enc < 4; enc++) { + m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW]; + m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW]; + } +} + +// ─── Haupt-Loop ─────────────────────────────────────────────────────────────── + +void CMainController::work() +{ + matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue + poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten + processEvents(); // 3. Queue leeren: Aktionen ausführen, Buttons benachrichtigen + updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show() +} + +// ─── Vendor-Kommunikation (PC → Board) ─────────────────────────────────────── +// +// Die Windows-App sendet 8-Byte-Pakete über den CDC Serial-Port. +// poll_vendor() holt alle verfügbaren vollständigen Pakete ab und +// wendet die Kommandos direkt auf die CButton-Instanzen an. +// LED-Änderungen werden beim nächsten updateLEDs()-Aufruf sichtbar. + +void CMainController::poll_vendor() +{ + SerialPacket pkt; + while (usb_serial_poll(pkt)) { + switch (pkt.command()) { + + // Override-LED setzen: Button leuchtet in der angegebenen Farbe, + // bis clear_override() aufgerufen wird (z.B. Benachrichtigung) + case USB_CMD_SET_LED_OVERRIDE: + if (pkt.key_id() < MATRIX_KEYS) + m_buttons[pkt.key_id()].set_override(RGB(pkt.r(), pkt.g(), pkt.b())); + break; + + // Override-LED löschen: Button kehrt zur konfigurierten Base-Farbe zurück + case USB_CMD_CLEAR_LED_OVERRIDE: + if (pkt.key_id() < MATRIX_KEYS) + m_buttons[pkt.key_id()].clear_override(); + break; + + // Base-LED setzen: dauerhaft neue Idle-Farbe (wird nicht in NVM geschrieben) + case USB_CMD_SET_LED_BASE: + if (pkt.key_id() < MATRIX_KEYS) + m_buttons[pkt.key_id()].set_base(RGB(pkt.r(), pkt.g(), pkt.b())); + break; + + // Ping – sofortige Antwort zum Testen der Verbindung + case USB_CMD_PING: + usb_serial_send(USB_EVT_PONG, 0); + break; + + // Config-Übertragung: BEGIN → n×DATA → COMMIT + case USB_CMD_CONFIG_BEGIN: + // Neuen Empfang starten – bisherige Daten verwerfen + m_cfg_chunks_expected = pkt.key_id(); + m_cfg_receiving = true; + memset(m_cfg_buf, 0, sizeof(m_cfg_buf)); + break; + + case USB_CMD_CONFIG_DATA: + if (m_cfg_receiving) { + // 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen + uint16_t offset = (uint16_t)pkt.key_id() * 6; + if (offset < sizeof(m_cfg_buf)) { + uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset); + if (count > 6) count = 6; + memcpy(m_cfg_buf + offset, &pkt.data[2], count); + } + } + break; + + // Config-Dump anfordern: Board sendet NVM-Config in 6-Byte-Chunks + // zurück an die App (gleiche Chunk-Struktur wie beim Schreiben). + case USB_CMD_CONFIG_READ: + { + SDeviceConfig cfg; + nvm_config_load(cfg); // ungültige NVM → Defaults + const uint8_t* raw = reinterpret_cast(&cfg); + const uint8_t sz = sizeof(SDeviceConfig); // 163 + const uint8_t payload = 6; + uint8_t chunks = (sz + payload - 1) / payload; // 28 + + usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); + + for (uint8_t i = 0; i < chunks; i++) { + uint8_t p[SERIAL_PKT_SIZE] = {}; + p[0] = USB_EVT_CONFIG_DATA; + p[1] = i; + uint8_t offset = i * payload; + for (uint8_t b = 0; b < payload; b++) { + if (offset + b < sz) p[2 + b] = raw[offset + b]; + } + if (SerialUSB) SerialUSB.write(p, SERIAL_PKT_SIZE); + } + + usb_serial_send(USB_EVT_CONFIG_END, chunks); + break; + } + + case USB_CMD_CONFIG_COMMIT: + if (m_cfg_receiving) { + m_cfg_receiving = false; + SDeviceConfig cfg; + memcpy(&cfg, m_cfg_buf, sizeof(cfg)); + if (cfg.magic == NVM_CONFIG_MAGIC && + cfg.version == NVM_CONFIG_VERSION && + cfg.crc == nvm_config_crc(cfg)) + { + nvm_config_save(cfg); + init_buttons(); + usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden + } + else + { + usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden + } + } + break; + + default: + break; + } + } +} + +// ─── Event-Verarbeitung ─────────────────────────────────────────────────────── +// +// Verarbeitet alle Events in der Queue bis sie leer ist. +// Reihenfolge: ältestes Event zuerst (FIFO). +// +// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App +// gemeldet – die App entscheidet dann was passiert (URL öffnen, Programm starten…). + +void CMainController::processEvents() +{ + SEvent ev; + while (m_queue.pop(ev)) { + switch (ev.type) { + + case EventType::KEY_DOWN: + if (ev.key_id < MATRIX_KEYS) { + m_buttons[ev.key_id].on_press(); + execute_action(m_buttons[ev.key_id].action()); + // Bei HOST_COMMAND: Event-ID an Windows-App senden + if (m_buttons[ev.key_id].action().type == ActionType::HOST_COMMAND) + usb_serial_send(USB_EVT_KEY_DOWN, ev.key_id); + } + break; + + case EventType::KEY_UP: + if (ev.key_id < MATRIX_KEYS) + m_buttons[ev.key_id].on_release(); + break; + + case EventType::ENC_CW: + if (ev.key_id < 4) { + execute_action(m_enc_cw[ev.key_id]); + if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND) + usb_serial_send(USB_EVT_ENC_CW, ev.key_id); + } + break; + + case EventType::ENC_CCW: + if (ev.key_id < 4) { + execute_action(m_enc_ccw[ev.key_id]); + if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND) + usb_serial_send(USB_EVT_ENC_CCW, ev.key_id); + } + break; + + default: + break; + } + } +} + +// Führt eine einzelne Aktion aus. +// HID_KEY / HID_CONSUMER: direkt über USB HID gesendet (funktioniert ohne Windows-App). +// HOST_COMMAND: kein direkter Aufruf hier – das Event wird in processEvents() via +// usb_serial_send() an die Windows-App weitergeleitet. +void CMainController::execute_action(SAction action) +{ + switch (action.type) { + + case ActionType::HID_KEY: + // data-Encoding: Low-Byte = Keycode, High-Byte = Modifier + usb_hid_send_key(static_cast(action.data & 0xFF), + static_cast(action.data >> 8)); + delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up + usb_hid_release_key(); + break; + + case ActionType::HID_CONSUMER: + usb_hid_send_consumer(action.data); + usb_hid_release_consumer(); + break; + + case ActionType::HOST_COMMAND: + // Wird in processEvents() über Serial gesendet + break; + + case ActionType::NONE: + default: + break; + } +} + +// ─── LED-Rendering ──────────────────────────────────────────────────────────── +// +// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt +// ihre aktuelle Farbe (override wenn aktiv, sonst base) in den WS2812-Buffer. +// ws2812_show() wird nur aufgerufen wenn mindestens ein Button dirty war – +// das vermeidet unnötige noInterrupts()-Aufrufe (~600µs Blockzeit). + +void CMainController::updateLEDs() +{ + bool dirty = false; + for (uint8_t i = 0; i < MATRIX_KEYS; i++) { + if (m_buttons[i].render_led()) + dirty = true; + } + if (dirty) + ws2812_show(); +} diff --git a/src/CMainController.h b/src/CMainController.h new file mode 100644 index 0000000..c8291a3 --- /dev/null +++ b/src/CMainController.h @@ -0,0 +1,50 @@ +// CMainController.h +// Zentraler Orchestrator des VersaPad v2. +// Kennt alle Subsysteme und koordiniert den Datenfluss zwischen ihnen. +// Einzige Instanz wird in main.cpp angelegt. + +#pragma once +#include "CButton.h" +#include "CEventQueue.h" +#include "SEvent.h" +#include "hal/matrix.h" +#include "hal/encoder.h" +#include "hal/usb_hid.h" +#include "hal/usb_serial.h" +#include "config/action.h" + +class CMainController +{ +public: + CMainController(); + void setup(); // Einmalig in Arduino setup() aufrufen + void work(); // Jeden Loop-Durchlauf aufrufen + +private: + // m_queue muss vor m_buttons deklariert sein: C++ initialisiert Member in + // Deklarationsreihenfolge. Die static-Bridge-Funktion (matrix_cb) erhält + // einen Pointer auf m_queue – der muss zum Zeitpunkt der Nutzung gültig sein. + CEventQueue m_queue; + + // Alle 25 Matrix-Keys (0–24) als CButton-Array. + // key_id 0–3: Encoder-SW (kein LED), key_id 4: NC, key_id 5–24: MX-Buttons mit LED. + CButton m_buttons[MATRIX_KEYS]; + + // Encoder CW/CCW-Aktionen aus NVM – Encoder haben kein CButton-Objekt. + SAction m_enc_cw[4]; + SAction m_enc_ccw[4]; + + void init_buttons(); // Buttons aus NVM-Config initialisieren + void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten + void processEvents(); // Queue leeren, Aktionen ausführen + void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial) + void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben + + // ── Config-Empfangspuffer ───────────────────────────────────────────────── + // Mehrteilige Übertragung: BEGIN setzt receiving=true, DATA füllt den Buffer, + // COMMIT validiert und schreibt in den NVM. + // Puffergröße = sizeof(SDeviceConfig) = 163 Bytes. + uint8_t m_cfg_buf[163]; // Empfangspuffer für eingehende Config-Daten + uint8_t m_cfg_chunks_expected; // Anzahl erwarteter Chunks (aus BEGIN-Paket) + bool m_cfg_receiving; // true wenn Übertragung läuft +}; diff --git a/src/SEvent.h b/src/SEvent.h new file mode 100644 index 0000000..eea274d --- /dev/null +++ b/src/SEvent.h @@ -0,0 +1,25 @@ +// SEvent.h +// Event-Typen und Event-Struct für die zentrale CEventQueue. +// +// Events werden von HAL-Callbacks produziert (matrix_cb, encoder_cb) +// und von CMainController::processEvents() konsumiert. +// USB-Kommandos (PC→Board) laufen direkt über poll_vendor() und +// landen nicht in der Queue. + +#pragma once +#include + +enum class EventType : uint8_t +{ + KEY_DOWN, // Matrix: Taste gedrückt (key_id = Matrix-Key 0–24) + KEY_UP, // Matrix: Taste losgelassen (key_id = Matrix-Key 0–24) + ENC_CW, // Encoder: Schritt CW (key_id = Encoder-Index 0–3) + ENC_CCW, // Encoder: Schritt CCW (key_id = Encoder-Index 0–3) +}; + +struct SEvent +{ + EventType type; + uint8_t key_id; // Matrix-Key (0–24) oder Encoder-Index (0–3) + uint16_t payload; // Reserviert für zukünftige Erweiterungen +}; diff --git a/src/config/action.h b/src/config/action.h new file mode 100644 index 0000000..88c7323 --- /dev/null +++ b/src/config/action.h @@ -0,0 +1,18 @@ +#pragma once +#include + +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, …) +}; + +struct __attribute__((packed)) SAction +{ + ActionType type; + uint16_t data; // Keycode (HID_KEY / HID_CONSUMER) oder Command-ID (HOST_COMMAND) + // packed: 1B type + 2B data = 3B (kein Alignment-Padding) + // Muss packed sein damit sizeof(SDeviceConfig)==163 == C#-Serialisierung +}; diff --git a/src/config/nvm_config.cpp b/src/config/nvm_config.cpp new file mode 100644 index 0000000..882a135 --- /dev/null +++ b/src/config/nvm_config.cpp @@ -0,0 +1,116 @@ +#include "nvm_config.h" +#include +#include + +// ── Flash-Adresse (aus Linkerscript) ───────────────────────────────────────── +// Kein separates Linker-Symbol nötig – Adresse ist fix und bekannt. +static const uint32_t k_config_addr = 0x1FE00UL; + +// SAMD21 NVMCTRL ────────────────────────────────────────────────────────────── +// Row = 256 Bytes = 4 Pages à 64 Bytes +// Schreiben: Row löschen (ER), dann seitenweise schreiben (WP) + +static void nvm_wait() +{ + while (!NVMCTRL->INTFLAG.bit.READY) {} +} + +static void nvm_exec(uint16_t cmd) +{ + NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; + nvm_wait(); +} + +static void nvm_erase_row(uint32_t addr) +{ + nvm_wait(); + NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte) + nvm_exec(NVMCTRL_CTRLA_CMD_ER); +} + +static void nvm_write_page(uint32_t addr, const uint8_t* data) +{ + // Page-Buffer löschen + nvm_exec(NVMCTRL_CTRLA_CMD_PBC); + + // 64 Bytes in den Page-Buffer schreiben (32-Bit-Zugriffe) + volatile uint32_t* dst = reinterpret_cast(addr); + const uint32_t* src = reinterpret_cast(data); + for (uint8_t i = 0; i < 64 / 4; i++) { + dst[i] = src[i]; + } + + // Page programmieren + NVMCTRL->ADDR.reg = addr / 2; + nvm_exec(NVMCTRL_CTRLA_CMD_WP); +} + +// ── CRC16 (CCITT, Poly 0x1021) ──────────────────────────────────────────────── +uint16_t nvm_config_crc(const SDeviceConfig& cfg) +{ + // CRC über alles nach dem crc-Feld (ab Byte 7) + const uint8_t* data = reinterpret_cast(&cfg) + offsetof(SDeviceConfig, mx_actions); + uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions); + uint16_t crc = 0xFFFF; + for (uint16_t i = 0; i < len; i++) { + crc ^= static_cast(data[i]) << 8; + for (uint8_t b = 0; b < 8; b++) { + crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1; + } + } + return crc; +} + +// ── Defaults ───────────────────────────────────────────────────────────────── +void nvm_config_defaults(SDeviceConfig& cfg) +{ + memset(&cfg, 0, sizeof(cfg)); + cfg.magic = NVM_CONFIG_MAGIC; + cfg.version = NVM_CONFIG_VERSION; + + // Alle Aktionen: NONE + for (uint8_t i = 0; i < 20; i++) + cfg.mx_actions[i] = {ActionType::NONE, 0}; + for (uint8_t e = 0; e < 4; e++) + for (uint8_t a = 0; a < 3; a++) + cfg.enc_actions[e][a] = {ActionType::NONE, 0}; + + // Base-LEDs: warm-weiß + for (uint8_t i = 0; i < 20; i++) { + cfg.led_r[i] = 80; + cfg.led_g[i] = 40; + cfg.led_b[i] = 0; + } + + cfg.crc = nvm_config_crc(cfg); +} + +// ── Laden ───────────────────────────────────────────────────────────────────── +bool nvm_config_load(SDeviceConfig& cfg) +{ + memcpy(&cfg, reinterpret_cast(k_config_addr), sizeof(cfg)); + + if (cfg.magic != NVM_CONFIG_MAGIC) { nvm_config_defaults(cfg); return false; } + if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; } + if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; } + + return true; +} + +// ── Speichern ───────────────────────────────────────────────────────────────── +void nvm_config_save(const SDeviceConfig& cfg) +{ + // Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist + uint8_t row[256]; + memset(row, 0xFF, sizeof(row)); + memcpy(row, &cfg, sizeof(cfg)); + + // Automatisches Schreiben deaktivieren (manueller Schreib-Modus) + NVMCTRL->CTRLB.bit.MANW = 1; + + // Row 0 der Config löschen und seitenweise schreiben (4 × 64B) + nvm_erase_row(k_config_addr); + for (uint8_t p = 0; p < 4; p++) { + nvm_write_page(k_config_addr + p * 64, row + p * 64); + } +} diff --git a/src/config/nvm_config.h b/src/config/nvm_config.h new file mode 100644 index 0000000..5952ece --- /dev/null +++ b/src/config/nvm_config.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include "action.h" + +// ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ──────────────────────────────── +// +// Offset Size Inhalt +// 0 4 Magic (0x56503202 = 'VP2\x02') +// 4 1 Version +// 5 2 CRC16 über Bytes 7–162 +// 7 60 mx_actions[20] – 20 × 3B (SAction packed) +// 67 36 enc_actions[4][3] – 12 × 3B +// 103 20 led_r[20] +// 123 20 led_g[20] +// 143 20 led_b[20] +// 163 93 Padding bis 256 Bytes (erste Row voll) +// 256 256 Reserviert für zukünftige Erweiterungen (zweite Row) +// +// Gesamt genutzt: 163 Bytes (sizeof SDeviceConfig mit packed SAction) + +#define NVM_CONFIG_MAGIC 0x56503202UL +#define NVM_CONFIG_VERSION 1 + +// Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[]) +// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW +#define ENC_ACTION_SW 0 +#define ENC_ACTION_CW 1 +#define ENC_ACTION_CCW 2 + +struct __attribute__((packed)) SDeviceConfig +{ + uint32_t magic; + uint8_t version; + uint16_t crc; + + // Aktionen + SAction mx_actions[20]; // MX-Buttons 0–19 (key_id 5–24) + SAction enc_actions[4][3]; // [Encoder 0–3][SW/CW/CCW] + + // Base-LED Farben + uint8_t led_r[20]; + uint8_t led_g[20]; + uint8_t led_b[20]; +}; + +// Standardwerte wenn keine gültige Config im NVM +void nvm_config_defaults(SDeviceConfig& cfg); + +// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC ungültig → Defaults geladen. +bool nvm_config_load(SDeviceConfig& cfg); + +// Config in NVM schreiben (löscht 2 Rows, schreibt neu). +void nvm_config_save(const SDeviceConfig& cfg); + +// CRC16 über die Nutzdaten der Config +uint16_t nvm_config_crc(const SDeviceConfig& cfg); diff --git a/src/config/pins.h b/src/config/pins.h new file mode 100644 index 0000000..de1e06e --- /dev/null +++ b/src/config/pins.h @@ -0,0 +1,68 @@ +#pragma once +// VersaPad v2 – Logical pin names +// All Arduino pin numbers reference the custom variant (variants/versapad/variant.h) + +// ─── Button Matrix ──────────────────────────────────────────────────────────── +// Layout (viewed from front): +// +// COL_0 COL_1 COL_2 COL_3 COL_4 +// ROW_0 [ENC3] [ ] [ ] [ ] [ ] +// ROW_1 [ENC2] [ ] [ ] [ ] [ ] +// ROW_2 [ENC1] [ ] [ ] [ ] [ ] +// ROW_3 [ENC0] [ ] [ ] [ ] [ ] +// ROW_4 --- [ ] [ ] [ ] [ ] +// +// COL_0 × ROW_0–3 = encoder push buttons +// COL_1–4 × ROW_0–4 = 20 Cherry MX buttons +// COL_0 × ROW_4 = not connected + +#define BTN_COL_COUNT 5 +#define BTN_ROW_COUNT 5 + +// Column pins: driven OUTPUT LOW during scan, otherwise INPUT (high-Z or HIGH) +static const uint8_t BTN_COLS[BTN_COL_COUNT] = { + PIN_COL0, // PB10 – encoder SW column + PIN_COL1, // PA11 – Cherry MX col 1 (leftmost) + PIN_COL2, // PA10 – Cherry MX col 2 + PIN_COL3, // PA09 – Cherry MX col 3 + PIN_COL4, // PA08 – Cherry MX col 4 (rightmost) +}; + +// Row pins: INPUT_PULLUP, read LOW when button pressed +static const uint8_t BTN_ROWS[BTN_ROW_COUNT] = { + PIN_ROW0, // PB11 + PIN_ROW1, // PA12 + PIN_ROW2, // PA13 + PIN_ROW3, // PA14 + PIN_ROW4, // PA15 +}; + +// Button index helper: col * ROW_COUNT + row → 0..24 +#define BTN_INDEX(col, row) ((col) * BTN_ROW_COUNT + (row)) + +// Encoder SW buttons are at column 0 +#define BTN_ENC_SW(enc) BTN_INDEX(0, (enc)) // enc = 0..3 + +// ─── Rotary Encoders ────────────────────────────────────────────────────────── +// ENC0 = closest to USB connector, ENC3 = furthest + +static const uint8_t ENC_A[4] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A }; +static const uint8_t ENC_B[4] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B }; + +// ─── WS2812 LEDs ───────────────────────────────────────────────────────────── +#define LED_COUNT 20 +#define LED_DATA_PIN PIN_SPI_MOSI // PB22, SERCOM5 PAD2 + +// LED index: serpentine (even rows L→R, odd rows R→L) +// Row 0: 0,1,2,3 Row 1: 7,6,5,4 Row 2: 8,9,10,11 Row 3: 15,14,13,12 Row 4: 16,17,18,19 +#define LED_COLS (BTN_COL_COUNT - 1) // 4 Cherry MX columns +#define LED_INDEX(col, row) \ + ((row) * LED_COLS + (((row) & 1) ? (LED_COLS - (col)) : ((col) - 1))) + +// ─── Faders (ADC) ───────────────────────────────────────────────────────────── +#define FADER_COUNT 3 +static const uint8_t FADER_PINS[FADER_COUNT] = { + PIN_FADER0, // PA02 A0 + PIN_FADER1, // PA03 A1 (also VREFA – analog only, no digitalRead) + PIN_FADER2, // PB08 A2 +}; diff --git a/src/hal/encoder.cpp b/src/hal/encoder.cpp new file mode 100644 index 0000000..dd20c70 --- /dev/null +++ b/src/hal/encoder.cpp @@ -0,0 +1,88 @@ +#include "encoder.h" +#include +#include "config/pins.h" + +// Quadratur-Dekodierung via 4-State-Lookup. +// +// Zustand = (A << 1) | B → 4 mögliche Zustände (00, 01, 10, 11) +// Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand +// bestimmt und mit dem vorherigen verglichen. +// +// Lookup-Tabelle [prev<<2 | curr] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen) +static const int8_t k_lut[16] = { +// curr: 00 01 10 11 + 0, +1, -1, 0, // prev = 00 + -1, 0, 0, +1, // prev = 01 + +1, 0, 0, -1, // prev = 10 + 0, -1, +1, 0, // prev = 11 +}; + +// Pro Encoder: vorheriger Zustand + Akkumulator für Halb-Schritte. +// Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt +// auf ±4 bevor ein Event gefeuert wird (= ein Event pro Klick). +static volatile uint8_t s_state[ENCODER_COUNT]; +static volatile int8_t s_accum[ENCODER_COUNT]; + +static encoder_cb_t s_cb = nullptr; + +static const uint8_t k_pin_a[ENCODER_COUNT] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A }; +static const uint8_t k_pin_b[ENCODER_COUNT] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B }; + +// Generischer Handler — wird von den 8 ISR-Wrappern unten aufgerufen. +static void handle_encoder(uint8_t enc) +{ + uint8_t a = digitalRead(k_pin_a[enc]); + uint8_t b = digitalRead(k_pin_b[enc]); + uint8_t cur = (a << 1) | b; + uint8_t idx = (s_state[enc] << 2) | cur; + s_state[enc] = cur; + + int8_t delta = k_lut[idx]; + if (delta == 0) return; + + s_accum[enc] += delta; + + // 4 Halb-Schritte = 1 vollständige Raste + if (s_accum[enc] >= 4) { + s_accum[enc] = 0; + if (s_cb) s_cb(enc, +1); + } else if (s_accum[enc] <= -4) { + s_accum[enc] = 0; + if (s_cb) s_cb(enc, -1); + } +} + +// 8 ISR-Wrapper – je einer pro Pin (attachInterrupt braucht void-Funktionszeiger) +static void isr_enc0_a() { handle_encoder(0); } +static void isr_enc0_b() { handle_encoder(0); } +static void isr_enc1_a() { handle_encoder(1); } +static void isr_enc1_b() { handle_encoder(1); } +static void isr_enc2_a() { handle_encoder(2); } +static void isr_enc2_b() { handle_encoder(2); } +static void isr_enc3_a() { handle_encoder(3); } +static void isr_enc3_b() { handle_encoder(3); } + +void encoder_init(encoder_cb_t cb) +{ + s_cb = cb; + + for (uint8_t i = 0; i < ENCODER_COUNT; i++) { + pinMode(k_pin_a[i], INPUT_PULLUP); + pinMode(k_pin_b[i], INPUT_PULLUP); + + // Initialen Zustand lesen damit der erste Interrupt korrekt ausgewertet wird + uint8_t a = digitalRead(k_pin_a[i]); + uint8_t b = digitalRead(k_pin_b[i]); + s_state[i] = (a << 1) | b; + s_accum[i] = 0; + } + + attachInterrupt(digitalPinToInterrupt(PIN_ENC0_A), isr_enc0_a, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC0_B), isr_enc0_b, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC1_A), isr_enc1_a, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC1_B), isr_enc1_b, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC2_A), isr_enc2_a, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC2_B), isr_enc2_b, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC3_A), isr_enc3_a, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_ENC3_B), isr_enc3_b, CHANGE); +} diff --git a/src/hal/encoder.h b/src/hal/encoder.h new file mode 100644 index 0000000..c01eb5c --- /dev/null +++ b/src/hal/encoder.h @@ -0,0 +1,10 @@ +#pragma once +#include + +#define ENCODER_COUNT 4 + +// Callback: enc = Encoder-Index (0–3), dir = +1 (CW) oder -1 (CCW) +typedef void (*encoder_cb_t)(uint8_t enc, int8_t dir); + +void encoder_init(encoder_cb_t cb); +// Kein encoder_scan() – rein interrupt-getrieben diff --git a/src/hal/matrix.cpp b/src/hal/matrix.cpp new file mode 100644 index 0000000..f283a4e --- /dev/null +++ b/src/hal/matrix.cpp @@ -0,0 +1,68 @@ +#include "matrix.h" +#include +#include "config/pins.h" +#include + +// Hardware: COL lines have 10k pullups to 3V3 (always HIGH by default). +// Diodes between switch DO and ROW line (anode=switch, cathode=row). +// Scan: drive ROW LOW → pressed switch pulls COL LOW through diode. + +#define DEBOUNCE_MS 10 + +static matrix_cb_t s_cb; +static bool s_raw[MATRIX_KEYS]; +static bool s_debounced[MATRIX_KEYS]; +static uint32_t s_changed_at[MATRIX_KEYS]; + +void matrix_init(matrix_cb_t cb) +{ + s_cb = cb; + + // COLs: INPUT – external 10k pullup holds them HIGH + for (uint8_t c = 0; c < MATRIX_COLS; c++) { + pinMode(BTN_COLS[c], INPUT); + } + // ROWs: idle high-Z, driven LOW only during scan + for (uint8_t r = 0; r < MATRIX_ROWS; r++) { + pinMode(BTN_ROWS[r], INPUT); + } + + memset(s_raw, 0, sizeof(s_raw)); + memset(s_debounced, 0, sizeof(s_debounced)); + + uint32_t now = millis(); + for (uint8_t i = 0; i < MATRIX_KEYS; i++) { + s_changed_at[i] = now; + } +} + +void matrix_scan() +{ + uint32_t now = millis(); + + for (uint8_t r = 0; r < MATRIX_ROWS; r++) { + // Drive this row LOW + pinMode(BTN_ROWS[r], OUTPUT); + digitalWrite(BTN_ROWS[r], LOW); + delayMicroseconds(10); + + for (uint8_t c = 0; c < MATRIX_COLS; c++) { + uint8_t key = c * MATRIX_ROWS + r; + bool raw = (digitalRead(BTN_COLS[c]) == LOW); + + if (raw != s_raw[key]) { + s_raw[key] = raw; + s_changed_at[key] = now; + } + + if (raw != s_debounced[key] && + (now - s_changed_at[key]) >= DEBOUNCE_MS) { + s_debounced[key] = raw; + if (s_cb) s_cb(key, raw); + } + } + + // Release row back to high-Z + pinMode(BTN_ROWS[r], INPUT); + } +} diff --git a/src/hal/matrix.h b/src/hal/matrix.h new file mode 100644 index 0000000..7697ae6 --- /dev/null +++ b/src/hal/matrix.h @@ -0,0 +1,22 @@ +#pragma once +#include + +// 5×5 button matrix +// COL_0 × ROW_0–3 = encoder SW buttons +// COL_1–4 × ROW_0–4 = 20 Cherry MX buttons +// COL_0 × ROW_4 = not connected + +#define MATRIX_COLS 5 +#define MATRIX_ROWS 5 +#define MATRIX_KEYS 25 // col * MATRIX_ROWS + row + +// Callback: key index (0–24), pressed = true / released = false +typedef void (*matrix_cb_t)(uint8_t key, bool pressed); + +void matrix_init(matrix_cb_t cb); +void matrix_scan(); + +// Helper: key index from logical position +inline uint8_t matrix_key(uint8_t col, uint8_t row) { + return col * MATRIX_ROWS + row; +} diff --git a/src/hal/usb_hid.cpp b/src/hal/usb_hid.cpp new file mode 100644 index 0000000..ef03901 --- /dev/null +++ b/src/hal/usb_hid.cpp @@ -0,0 +1,96 @@ +#include "usb_hid.h" +#include +#include + +// ── HID Report Descriptor: Keyboard + Consumer Control ─────────────────────── +// Vendor-Kommunikation läuft über CVendorHID (eigenes PluggableUSBModule). + +static const uint8_t k_hid_descriptor[] = { + + // ── Report ID 1: Keyboard ───────────────────────────────────────────────── + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x06, // Usage (Keyboard) + 0xA1, 0x01, // Collection (Application) + 0x85, HID_REPORT_ID_KEYBOARD, + 0x05, 0x07, // Usage Page (Key Codes) + 0x19, 0xE0, // Usage Minimum (Left Ctrl) + 0x29, 0xE7, // Usage Maximum (Right GUI) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1 Bit) + 0x95, 0x08, // Report Count (8) + 0x81, 0x02, // Input (Data, Variable, Absolute) + 0x95, 0x01, // Report Count (1) + 0x75, 0x08, // Report Size (8 Bit) + 0x81, 0x01, // Input (Constant) + 0x95, 0x06, // Report Count (6) + 0x75, 0x08, // Report Size (8 Bit) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x65, // Logical Maximum (101) + 0x05, 0x07, // Usage Page (Key Codes) + 0x19, 0x00, // Usage Minimum (0) + 0x29, 0x65, // Usage Maximum (101) + 0x81, 0x00, // Input (Data, Array) + 0xC0, // End Collection + + // ── Report ID 2: Consumer Control ───────────────────────────────────────── + 0x05, 0x0C, // Usage Page (Consumer Devices) + 0x09, 0x01, // Usage (Consumer Control) + 0xA1, 0x01, // Collection (Application) + 0x85, HID_REPORT_ID_CONSUMER, + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xFF, 0x03, // Logical Maximum (1023) + 0x19, 0x00, // Usage Minimum (0) + 0x2A, 0xFF, 0x03, // Usage Maximum (1023) + 0x75, 0x10, // Report Size (16 Bit) + 0x95, 0x01, // Report Count (1) + 0x81, 0x00, // Input (Data, Array) + 0xC0, // End Collection +}; + +namespace { + struct HIDRegistrar { + HIDSubDescriptor node; + HIDRegistrar() : node(k_hid_descriptor, sizeof(k_hid_descriptor)) { + HID().AppendDescriptor(&node); + } + } s_hid_registrar; +} + +struct KeyboardReport { + uint8_t modifier; + uint8_t reserved; + uint8_t keycodes[6]; +}; + +struct ConsumerReport { + uint16_t usage; +}; + +void usb_hid_init() {} + +void usb_hid_send_key(uint8_t keycode, uint8_t modifier) +{ + KeyboardReport report = {}; + report.modifier = modifier; + report.keycodes[0] = keycode; + HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report)); +} + +void usb_hid_release_key() +{ + KeyboardReport report = {}; + HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report)); +} + +void usb_hid_send_consumer(uint16_t usage) +{ + ConsumerReport report = { usage }; + HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report)); +} + +void usb_hid_release_consumer() +{ + ConsumerReport report = { 0 }; + HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report)); +} diff --git a/src/hal/usb_hid.h b/src/hal/usb_hid.h new file mode 100644 index 0000000..3b77c3a --- /dev/null +++ b/src/hal/usb_hid.h @@ -0,0 +1,35 @@ +#pragma once +#include + +// ── Report-IDs (Keyboard/Consumer Interface) ────────────────────────────────── +#define HID_REPORT_ID_KEYBOARD 1 +#define HID_REPORT_ID_CONSUMER 2 + +// ── Keyboard Modifier-Bits ──────────────────────────────────────────────────── +#define KEY_MOD_LCTRL 0x01 +#define KEY_MOD_LSHIFT 0x02 +#define KEY_MOD_LALT 0x04 +#define KEY_MOD_LGUI 0x08 +#define KEY_MOD_RCTRL 0x10 +#define KEY_MOD_RSHIFT 0x20 +#define KEY_MOD_RALT 0x40 +#define KEY_MOD_RGUI 0x80 + +// ── Consumer Control Usage IDs (HID Usage Table 1.3, Consumer Page 0x0C) ───── +#define CONSUMER_MUTE 0x00E2 +#define CONSUMER_VOLUME_UP 0x00E9 +#define CONSUMER_VOLUME_DOWN 0x00EA +#define CONSUMER_PLAY_PAUSE 0x00CD +#define CONSUMER_NEXT_TRACK 0x00B5 +#define CONSUMER_PREV_TRACK 0x00B6 +#define CONSUMER_STOP 0x00B7 +#define CONSUMER_BRIGHTNESS_UP 0x006F +#define CONSUMER_BRIGHTNESS_DN 0x0070 + +void usb_hid_init(); + +void usb_hid_send_key(uint8_t keycode, uint8_t modifier = 0); +void usb_hid_release_key(); + +void usb_hid_send_consumer(uint16_t usage); +void usb_hid_release_consumer(); diff --git a/src/hal/usb_serial.cpp b/src/hal/usb_serial.cpp new file mode 100644 index 0000000..61a4b0f --- /dev/null +++ b/src/hal/usb_serial.cpp @@ -0,0 +1,56 @@ +// usb_serial.cpp +// CDC Serial – bidirektionale Kommunikation mit der Windows-App. +// +// Empfang (PC → Board): +// CDC kann Bytes in beliebig kleinen Happen liefern. usb_serial_poll() liest +// 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). +// +// Senden (Board → PC): +// Direkt via SerialUSB.write() – kein eigener Puffer nötig, da der Arduino-CDC- +// Stack intern puffert. Nur gesendet wenn SerialUSB verbunden ist (USB-Host da). + +#include "usb_serial.h" +#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. +static uint8_t s_buf[SERIAL_PKT_SIZE * 32]; +static uint16_t s_head = 0; +static uint16_t s_count = 0; + +void usb_serial_init() +{ + SerialUSB.begin(0); // CDC ignoriert Baudrate – Wert egal +} + +void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a, uint8_t b) +{ + if (!SerialUSB) return; // Nicht verbunden + uint8_t pkt[SERIAL_PKT_SIZE] = { event_type, key_id, a, b, 0, 0, 0, 0 }; + SerialUSB.write(pkt, SERIAL_PKT_SIZE); +} + +bool usb_serial_poll(SerialPacket& out) +{ + // Verfügbare Bytes in internen Buffer lesen + while (SerialUSB.available() && s_count < sizeof(s_buf)) { + s_buf[(s_head + s_count) % sizeof(s_buf)] = SerialUSB.read(); + s_count++; + } + + // Sobald ein vollständiges Paket da ist, ausgeben + if (s_count >= SERIAL_PKT_SIZE) { + for (uint8_t i = 0; i < SERIAL_PKT_SIZE; i++) { + out.data[i] = s_buf[(s_head + i) % sizeof(s_buf)]; + } + s_head = (s_head + SERIAL_PKT_SIZE) % sizeof(s_buf); + s_count -= SERIAL_PKT_SIZE; + return true; + } + + return false; +} diff --git a/src/hal/usb_serial.h b/src/hal/usb_serial.h new file mode 100644 index 0000000..a2ea1fd --- /dev/null +++ b/src/hal/usb_serial.h @@ -0,0 +1,70 @@ +// usb_serial.h +// Bidirektionale Kommunikation zwischen Board und Windows-App über CDC Serial. +// +// Das Board erscheint als COM-Port unter Windows (kein Treiber nötig). +// Alle Pakete haben feste Größe (SERIAL_PKT_SIZE = 8 Bytes) – kein +// Längen-Header nötig, vereinfacht Parsing auf beiden Seiten. +// +// 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) +// +// Richtungen: +// PC → Board (Commands, 0x01–0x7F): poll_vendor() in CMainController +// Board → PC (Events, 0x81–0xFF): usb_serial_send() in processEvents() + +#pragma once +#include + +#define SERIAL_PKT_SIZE 8 + +// ── Commands: PC → Board ────────────────────────────────────────────────────── +#define USB_CMD_SET_LED_OVERRIDE 0x01 // key_id, r, g, b → Override-LED setzen +#define USB_CMD_CLEAR_LED_OVERRIDE 0x02 // key_id → Override löschen, zurück zu base +#define USB_CMD_SET_LED_BASE 0x03 // key_id, r, g, b → Base-LED dauerhaft ändern + +// Config-Übertragung (mehrteilig, 6 Nutzbytes pro Paket): +// BEGIN: Data[1] = Anzahl Chunks die folgen +// DATA: Data[1] = Chunk-Index (0-based), Data[2..7] = 6 Bytes Nutzdaten +// COMMIT: CRC prüfen + NVM schreiben + Buttons neu laden +#define USB_CMD_PING 0x05 // Board antwortet sofort mit USB_EVT_PONG +#define USB_CMD_CONFIG_BEGIN 0x10 +#define USB_CMD_CONFIG_DATA 0x11 +#define USB_CMD_CONFIG_COMMIT 0x12 +#define USB_CMD_CONFIG_READ 0x13 // Board sendet aktuelle NVM-Config zurück + +// ── Events: Board → PC ──────────────────────────────────────────────────────── +#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt +#define USB_EVT_KEY_UP 0x82 // key_id → HOST_COMMAND-Button losgelassen +#define USB_EVT_ENC_CW 0x83 // enc_id → Encoder Schritt CW (HOST_COMMAND) +#define USB_EVT_ENC_CCW 0x84 // enc_id → Encoder Schritt CCW (HOST_COMMAND) +#define USB_EVT_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 +#define USB_EVT_CONFIG_BEGIN 0x92 // Beginn Config-Dump: Data[1] = Chunk-Anzahl +#define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B +#define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen + +// Paket-Struct mit Accessor-Methoden für lesbareren Code +struct SerialPacket +{ + uint8_t data[SERIAL_PKT_SIZE]; + uint8_t command() const { return data[0]; } + uint8_t key_id() const { return data[1]; } + uint8_t r() const { return data[2]; } + uint8_t g() const { return data[3]; } + uint8_t b() const { return data[4]; } +}; + +void usb_serial_init(); + +// Board → PC: 8-Byte-Event-Paket senden (nur wenn SerialUSB verbunden) +void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a = 0, uint8_t b = 0); + +// PC → Board: nächstes vollständiges Paket abholen. +// Gibt true zurück wenn ein Paket verfügbar war. +bool usb_serial_poll(SerialPacket& out); diff --git a/src/hal/ws2812.cpp b/src/hal/ws2812.cpp new file mode 100644 index 0000000..870b872 --- /dev/null +++ b/src/hal/ws2812.cpp @@ -0,0 +1,35 @@ +// WS2812 driver – wraps Adafruit NeoPixel (bit-bang, no SERCOM needed) + +#include "ws2812.h" +#include + +static Adafruit_NeoPixel s_strip(WS2812_COUNT, WS2812_PIN, NEO_GRB + NEO_KHZ800); + +void ws2812_init() +{ + s_strip.begin(); + s_strip.clear(); + s_strip.show(); +} + +void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b) +{ + if (idx >= WS2812_COUNT) return; + s_strip.setPixelColor(idx, r, g, b); +} + +void ws2812_fill(uint8_t r, uint8_t g, uint8_t b) +{ + s_strip.fill(s_strip.Color(r, g, b)); +} + +void ws2812_show() +{ + s_strip.show(); +} + +void ws2812_clear() +{ + s_strip.clear(); + s_strip.show(); +} diff --git a/src/hal/ws2812.h b/src/hal/ws2812.h new file mode 100644 index 0000000..98bfb5c --- /dev/null +++ b/src/hal/ws2812.h @@ -0,0 +1,30 @@ +// ws2812.h +// Thin HAL-Wrapper um Adafruit NeoPixel (bit-bang, kein SERCOM). +// +// Dirty-Flag-Pattern: +// ws2812_set() schreibt nur in den internen NeoPixel-Buffer (RAM). +// ws2812_show() überträgt den gesamten Buffer an die LEDs (~600µs, blockierend +// via noInterrupts()). Nie aus einer ISR aufrufen! +// CButton::render_led() ruft nur ws2812_set() auf; ws2812_show() wird +// einmalig von CMainController::updateLEDs() aufgerufen wenn mindestens +// ein Button dirty war oder eine Animation läuft. + +#pragma once +#include + +#define WS2812_COUNT 20 +#define WS2812_PIN 18 // D18 = PB22 = LED_DATA_PIN + +void ws2812_init(); + +// Einzelne LED im Buffer setzen (sofort, kein HW-Transfer) +void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b); + +// Alle LEDs im Buffer auf dieselbe Farbe setzen (kein HW-Transfer) +void ws2812_fill(uint8_t r, uint8_t g, uint8_t b); + +// Buffer an Hardware übertragen (~600µs, blockierend via noInterrupts()) +void ws2812_show(); + +// Buffer löschen und sofort anzeigen (LEDs aus) +void ws2812_clear(); diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..49fd49c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,27 @@ +#include +#include "hal/ws2812.h" +#include "hal/usb_hid.h" +#include "CMainController.h" + +static CMainController controller; + +void setup() +{ + delay(500); // USB-Enumeration abwarten vor erstem noInterrupts()-Aufruf + + ws2812_init(); + + // Startup-Blink: bestätigt dass Firmware läuft + ws2812_fill(100, 0, 0); + ws2812_show(); + delay(1000); + ws2812_clear(); + ws2812_show(); + + controller.setup(); +} + +void loop() +{ + controller.work(); +} diff --git a/upload_openocd.py b/upload_openocd.py new file mode 100644 index 0000000..b311728 --- /dev/null +++ b/upload_openocd.py @@ -0,0 +1,24 @@ +Import("env") +import os +import subprocess + +def upload_via_openocd(source, target, env): + pkg_dir = env.PioPlatform().get_package_dir("tool-openocd") + openocd = os.path.join(pkg_dir, "bin", "openocd.exe") + scripts = os.path.join(pkg_dir, "scripts") + firmware = str(source[0]) # .elf path + + cmd = [ + openocd, + "-s", scripts, + "-f", "interface/cmsis-dap.cfg", + "-f", "target/at91samdXX.cfg", + "-c", 'program "{}" verify reset; shutdown'.format(firmware.replace("\\", "/")) + ] + + print(" ".join(cmd)) + result = subprocess.run(cmd) + if result.returncode != 0: + env.Exit(1) + +env.Replace(UPLOADCMD=upload_via_openocd) diff --git a/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld new file mode 100644 index 0000000..f79fcbc --- /dev/null +++ b/variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld @@ -0,0 +1,100 @@ +/* Linker script for ATSAMD21G17D – with 8KB bootloader (flash starts at 0x2000) */ + +OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") +OUTPUT_ARCH(arm) +SEARCH_DIR(.) + +MEMORY +{ + rom (rx) : ORIGIN = 0x00002000, LENGTH = 0x0001E000 /* 120K (128K - 8K bootloader) */ + ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ +} + +__StackTop = ORIGIN(ram) + LENGTH(ram); +__StackLimit = __StackTop - 0x1000; + +SECTIONS +{ + .text : + { + __text_start__ = .; + . = ALIGN(4); + KEEP(*(.isr_vector)) + KEEP(*(.vectors .vectors.*)) + *(.text .text.* .gnu.linkonce.t.*) + *(.glue_7t) *(.glue_7) + *(.rodata .rodata* .gnu.linkonce.r.*) + *(.ARM.extab* .gnu.linkonce.armextab.*) + + . = ALIGN(4); + KEEP(*(.init)) + __preinit_array_start = .; + KEEP(*(.preinit_array)) + __preinit_array_end = .; + + __init_array_start = .; + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + __init_array_end = .; + + KEEP(*crtbegin.o(.ctors)) + KEEP(*(EXCLUDE_FILE(*crtend.o) .ctors)) + KEEP(*(SORT(.ctors.*))) + KEEP(*crtend.o(.ctors)) + + . = ALIGN(4); + KEEP(*(.fini)) + __fini_array_start = .; + KEEP(*(.fini_array)) + KEEP(*(SORT(.fini_array.*))) + __fini_array_end = .; + + KEEP(*crtbegin.o(.dtors)) + KEEP(*(EXCLUDE_FILE(*crtend.o) .dtors)) + KEEP(*(SORT(.dtors.*))) + KEEP(*crtend.o(.dtors)) + + . = ALIGN(4); + } > rom + + PROVIDE_HIDDEN(__exidx_start = .); + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > rom + PROVIDE_HIDDEN(__exidx_end = .); + + . = ALIGN(4); + __etext = .; + _etext = .; + + .data : AT(__etext) + { + . = ALIGN(4); + __data_start__ = .; + _srelocate = .; + *(.ramfunc .ramfunc.*); + *(.data .data.*); + . = ALIGN(4); + __data_end__ = .; + _erelocate = .; + } > ram + + .bss (NOLOAD) : + { + . = ALIGN(4); + __bss_start__ = .; + _sbss = .; + _szero = .; + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + __bss_end__ = .; + _ebss = .; + _ezero = .; + } > ram + + . = ALIGN(4); + end = .; + _end = .; +} diff --git a/variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld b/variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld new file mode 100644 index 0000000..ccef9a5 --- /dev/null +++ b/variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld @@ -0,0 +1,108 @@ +/* Linker script for ATSAMD21G17D – no bootloader (flash starts at 0x00000000) + * + * Symbol names match Arduino SAMD core 1.8.14 (cortex_handlers.c / Reset.cpp): + * __StackTop, __data_start__, __data_end__, __etext, + * __bss_start__, __bss_end__, __text_start__, end + */ + +OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") +OUTPUT_ARCH(arm) +SEARCH_DIR(.) + +MEMORY +{ + rom (rx) : ORIGIN = 0x00000000, LENGTH = 0x0001FE00 /* 127.5K – Firmware */ + config (rx) : ORIGIN = 0x0001FE00, LENGTH = 0x00000200 /* 512B – NVM Config (2 Rows) */ + ram (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 /* 16K */ +} + +/* Initial stack pointer = top of RAM */ +__StackTop = ORIGIN(ram) + LENGTH(ram); /* 0x20004000 */ +__StackLimit = __StackTop - 0x1000; /* 4 KB minimum stack */ + +SECTIONS +{ + .text : + { + __text_start__ = .; + . = ALIGN(4); + KEEP(*(.isr_vector)) + KEEP(*(.vectors .vectors.*)) + *(.text .text.* .gnu.linkonce.t.*) + *(.glue_7t) *(.glue_7) + *(.rodata .rodata* .gnu.linkonce.r.*) + *(.ARM.extab* .gnu.linkonce.armextab.*) + + . = ALIGN(4); + KEEP(*(.init)) + __preinit_array_start = .; + KEEP(*(.preinit_array)) + __preinit_array_end = .; + + __init_array_start = .; + KEEP(*(SORT(.init_array.*))) + KEEP(*(.init_array)) + __init_array_end = .; + + KEEP(*crtbegin.o(.ctors)) + KEEP(*(EXCLUDE_FILE(*crtend.o) .ctors)) + KEEP(*(SORT(.ctors.*))) + KEEP(*crtend.o(.ctors)) + + . = ALIGN(4); + KEEP(*(.fini)) + __fini_array_start = .; + KEEP(*(.fini_array)) + KEEP(*(SORT(.fini_array.*))) + __fini_array_end = .; + + KEEP(*crtbegin.o(.dtors)) + KEEP(*(EXCLUDE_FILE(*crtend.o) .dtors)) + KEEP(*(SORT(.dtors.*))) + KEEP(*crtend.o(.dtors)) + + . = ALIGN(4); + } > rom + + PROVIDE_HIDDEN(__exidx_start = .); + .ARM.exidx : + { + *(.ARM.exidx* .gnu.linkonce.armexidx.*) + } > rom + PROVIDE_HIDDEN(__exidx_end = .); + + . = ALIGN(4); + __etext = .; /* LMA of .data – where initialized data lives in flash */ + _etext = .; /* legacy alias */ + + .data : AT(__etext) + { + . = ALIGN(4); + __data_start__ = .; + _srelocate = .; /* legacy alias */ + *(.ramfunc .ramfunc.*); + *(.data .data.*); + . = ALIGN(4); + __data_end__ = .; + _erelocate = .; /* legacy alias */ + } > ram + + .bss (NOLOAD) : + { + . = ALIGN(4); + __bss_start__ = .; + _sbss = .; /* legacy alias */ + _szero = .; /* legacy alias */ + *(.bss .bss.*) + *(COMMON) + . = ALIGN(4); + __bss_end__ = .; + _ebss = .; /* legacy alias */ + _ezero = .; /* legacy alias */ + } > ram + + /* Heap starts here (used by sbrk / malloc) */ + . = ALIGN(4); + end = .; + _end = .; +} diff --git a/variants/versapad/variant.cpp b/variants/versapad/variant.cpp new file mode 100644 index 0000000..6b9471b --- /dev/null +++ b/variants/versapad/variant.cpp @@ -0,0 +1,100 @@ +// VersaPad v2 – SAMD21G18A Custom Variant +// Pin descriptions and peripheral object definitions + +#include "variant.h" +#include "Arduino.h" + +// ─── g_APinDescription ──────────────────────────────────────────────────────── +// Maps Arduino pin D0–D25 to SAMD21 port/pin pairs. +// +// Format: +// { PORT, PIN, PinType, PinAttr, ADC_Channel, PWM_Channel, TC_Channel, ExtInt } +// +// ───────────────────────────────────────────────────────────────────────────── + +const PinDescription g_APinDescription[] = { + + // ── Button Matrix: Columns (D0–D4) ──────────────────────────────────────── + // Driven LOW one at a time during scanning; idle = INPUT_PULLUP or OUTPUT HIGH + + // D0 – PA08 – COL_4 + { PORTA, 8, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NMI }, + // D1 – PA09 – COL_3 + { PORTA, 9, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_9 }, + // D2 – PA10 – COL_2 + { PORTA, 10, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_10 }, + // D3 – PA11 – COL_1 + { PORTA, 11, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 }, + // D4 – PB10 – COL_0 (also: encoder SW column) + { 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 + + // D5 – PB11 – ROW_0 + { PORTB, 11, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_11 }, + // D6 – PA12 – ROW_1 + { PORTA, 12, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_12 }, + // D7 – PA13 – ROW_2 + { PORTA, 13, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_13 }, + // D8 – PA14 – ROW_3 + { PORTA, 14, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_14 }, + // D9 – PA15 – ROW_4 + { PORTA, 15, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_15 }, + + // ── Rotary Encoders (D10–D17) ───────────────────────────────────────────── + // All wired to EIC (External Interrupt Controller) via PMUX A. + // Quadrature decoding done in ISRs. + + // D10 – PA16 – ENC3_A EIC EXTINT[0] + { PORTA, 16, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_0 }, + // D11 – PA17 – ENC3_B EIC EXTINT[1] + { PORTA, 17, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_1 }, + // D12 – PA18 – ENC2_A EIC EXTINT[2] + { PORTA, 18, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_2 }, + // D13 – PA19 – ENC2_B EIC EXTINT[3] + { PORTA, 19, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_3 }, + // D14 – PA20 – ENC1_A EIC EXTINT[4] + { PORTA, 20, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_4 }, + // D15 – PA21 – ENC1_B EIC EXTINT[5] + { PORTA, 21, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_5 }, + // D16 – PA22 – ENC0_A EIC EXTINT[6] + { PORTA, 22, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_6 }, + // D17 – PA23 – ENC0_B EIC EXTINT[7] + { PORTA, 23, PIO_DIGITAL, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_7 }, + + // ── WS2812 SPI – SERCOM5 (D18–D20) ─────────────────────────────────────── + // PB22/PB23 use peripheral function D → PIO_SERCOM_ALT + // PB03 uses peripheral function D → PIO_SERCOM_ALT + + // D18 – PB22 – SPI MOSI / WS2812 data (SERCOM5 PAD[2]) + { PORTB, 22, PIO_SERCOM_ALT, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + // D19 – PB23 – SPI SCK (SERCOM5 PAD[3]) + { PORTB, 23, PIO_SERCOM_ALT, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + // D20 – PB03 – SPI MISO (unused) (SERCOM5 PAD[1]) + { PORTB, 3, PIO_SERCOM_ALT, PIN_ATTR_DIGITAL, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + + // ── USB D– / D+ (D21–D22) ──────────────────────────────────────────────── + // D21 – PA24 – USB D– + { PORTA, 24, PIO_COM, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + // D22 – PA25 – USB D+ + { PORTA, 25, PIO_COM, PIN_ATTR_NONE, No_ADC_Channel, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + + // ── Faders / ADC (D23–D25 = A0–A2) ────────────────────────────────────── + // D23/A0 – PA02 – FADER_0 ADC AIN[0] + { PORTA, 2, PIO_ANALOG, PIN_ATTR_ANALOG, ADC_Channel0, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + // D24/A1 – PA03 – FADER_1 ADC AIN[1] (also VREFA – do not use as digital) + { PORTA, 3, PIO_ANALOG, PIN_ATTR_ANALOG, ADC_Channel1, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NONE }, + // D25/A2 – PB08 – FADER_2 ADC AIN[2] + { PORTB, 8, PIO_ANALOG, PIN_ATTR_ANALOG, ADC_Channel2, NOT_ON_PWM, NOT_ON_TIMER, EXTERNAL_INT_NMI }, +}; + +extern "C" { + unsigned int PINCOUNT_fn() { return PINS_COUNT; } +} + +// ─── USB ────────────────────────────────────────────────────────────────────── +void initVariant() { + // Nothing board-specific needed at startup. + // USB is handled by the Arduino SAMD core. +} diff --git a/variants/versapad/variant.h b/variants/versapad/variant.h new file mode 100644 index 0000000..19e003e --- /dev/null +++ b/variants/versapad/variant.h @@ -0,0 +1,98 @@ +#pragma once + +// VersaPad v2 – SAMD21G18A Custom Variant +// Arduino pin assignments for the custom PCB + +#define ARDUINO_SAMD_VARIANT_COMPLIANCE 10610 + +#include + +// ─── Pin count ──────────────────────────────────────────────────────────────── +// D0–D9 : Button matrix (COL_0–4, ROW_0–4) +// D10–D17 : Rotary encoders A/B (ENC0–ENC3), all on EIC +// D18–D20 : SPI for WS2812 (SERCOM5: MOSI, SCK, MISO) +// D21–D22 : USB D–/D+ (internal, not exposed) +// D23–D25 : Fader ADC inputs (A0–A2) +// ───────────────────────────────────────────────────────────────────────────── +#define PINS_COUNT (26u) +#define NUM_DIGITAL_PINS (23u) // D0–D22 usable as digital +#define NUM_ANALOG_INPUTS (3u) // A0–A2 = D23–D25 +#define NUM_ANALOG_OUTPUTS (0u) + +#define analogInputToDigitalPin(p) ((p < NUM_ANALOG_INPUTS) ? (p) + 23u : -1) +// digitalPinToInterrupt is defined by Arduino.h as (P). +// attachInterrupt() then internally looks up ulExtInt via g_APinDescription. + +// ─── Button Matrix ──────────────────────────────────────────────────────────── +// Columns (driven LOW one at a time) +#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) +#define PIN_ROW0 (5u) // PB11 +#define PIN_ROW1 (6u) // PA12 +#define PIN_ROW2 (7u) // PA13 +#define PIN_ROW3 (8u) // PA14 +#define PIN_ROW4 (9u) // PA15 + +// ─── Rotary Encoders ────────────────────────────────────────────────────────── +// All on EIC (External Interrupt Controller) for hardware quadrature +#define PIN_ENC0_A (16u) // PA22 EIC EXTINT6 +#define PIN_ENC0_B (17u) // PA23 EIC EXTINT7 +#define PIN_ENC1_A (14u) // PA20 EIC EXTINT4 +#define PIN_ENC1_B (15u) // PA21 EIC EXTINT5 +#define PIN_ENC2_A (12u) // PA18 EIC EXTINT2 +#define PIN_ENC2_B (13u) // PA19 EIC EXTINT3 +#define PIN_ENC3_A (10u) // PA16 EIC EXTINT0 +#define PIN_ENC3_B (11u) // PA17 EIC EXTINT1 + +// ─── WS2812 SPI (SERCOM5) ───────────────────────────────────────────────────── +#define PIN_SPI_MOSI (18u) // PB22 SERCOM5 PAD2 ← LED data line +#define PIN_SPI_SCK (19u) // PB23 SERCOM5 PAD3 +#define PIN_SPI_MISO (20u) // PB03 SERCOM5 PAD1 (unused for WS2812) + +#define PERIPH_SPI sercom5 +#define PAD_SPI_TX SPI_PAD_2_SCK_3 // MOSI=PAD2, SCK=PAD3 +#define PAD_SPI_RX SERCOM_RX_PAD_1 // MISO=PAD1 + +// ─── Faders (ADC) ───────────────────────────────────────────────────────────── +#define PIN_A0 (23u) // PA02 ADC AIN[0] +#define PIN_A1 (24u) // PA03 ADC AIN[1] (also VREFA) +#define PIN_A2 (25u) // PB08 ADC AIN[2] + +#define PIN_FADER0 PIN_A0 +#define PIN_FADER1 PIN_A1 +#define PIN_FADER2 PIN_A2 + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; + +// ─── USB (internal) ─────────────────────────────────────────────────────────── +#define PIN_USB_DM (21u) // PA24 +#define PIN_USB_DP (22u) // PA25 +#define PIN_USB_HOST_ENABLE (21u) // unused, required by core + +// ─── SPI ────────────────────────────────────────────────────────────────────── +#define SPI_INTERFACES_COUNT 0 // SPI-Objekt wird in src/hal/spi.cpp definiert + +// ─── Serial / UART ──────────────────────────────────────────────────────────── +// No hardware UART exposed; USB CDC is used as Serial +#define SERIAL_INTERFACES_COUNT 0 + +// ─── I2C ────────────────────────────────────────────────────────────────────── +// Not used on this board, stubs required by core +#define WIRE_INTERFACES_COUNT 0 +#define PIN_WIRE_SDA (16u) // PA22 (reused, I2C not enabled) +#define PIN_WIRE_SCL (17u) // PA23 + +// ─── ADC reference ──────────────────────────────────────────────────────────── +#define ADC_RESOLUTION 12 + +// ─── Clock ──────────────────────────────────────────────────────────────────── +// Required by cores/arduino/delay.c (micros / delayMicroseconds) +#define VARIANT_MCK (48000000ul) +#define VARIANT_MAINOSC (32768ul)