Initial commit

This commit is contained in:
Julian Appel 2026-03-29 14:47:13 +02:00
commit b49984b9c0
32 changed files with 2394 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# PlatformIO
.pio/
# VS Code
.vscode/
# macOS
.DS_Store

143
README.md Normal file
View File

@ -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 024 oder Encoder 03)
Byte 2: r / Daten-Byte A
Byte 3: g / Daten-Byte B
Byte 4: b
Byte 57: 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 7162)
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 |

35
boards/versapad.json Normal file
View File

@ -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"
}

30
boards/versapad_nobl.json Normal file
View File

@ -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"
}

29
platformio.ini Normal file
View File

@ -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

280
src/CButton.cpp Normal file
View File

@ -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 2050ms 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 <Arduino.h>
#include "CButton.h"
#include "hal/ws2812.h"
// ── Hue → RGB (vollständig gesättigte Farbe, S=255, V=255) ───────────────────
//
// Hue 0255 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); // 0252, Annäherung an 0255
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<uint8_t>(m_led_index), c.r, c.g, c.b);
m_dirty = false;
return true;
}

136
src/CButton.h Normal file
View File

@ -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 <stdint.h>
#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 (0255) 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;
};

43
src/CEventQueue.cpp Normal file
View File

@ -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;
}

37
src/CEventQueue.h Normal file
View File

@ -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 <stdint.h>
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)
};

353
src/CMainController.cpp Normal file
View File

@ -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 <Arduino.h>
#include <string.h>
#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 <string.h>
// ─── 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:
// 03 : Encoder-SW-Buttons (COL_0 × ROW_03), kein LED
// 4 : nicht belegt (COL_0 × ROW_4)
// 524 : Cherry MX Buttons (COL_14 × ROW_04), 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<int8_t>(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<const uint8_t*>(&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<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(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();
}

50
src/CMainController.h Normal file
View File

@ -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 (024) als CButton-Array.
// key_id 03: Encoder-SW (kein LED), key_id 4: NC, key_id 524: 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
};

25
src/SEvent.h Normal file
View File

@ -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 <stdint.h>
enum class EventType : uint8_t
{
KEY_DOWN, // Matrix: Taste gedrückt (key_id = Matrix-Key 024)
KEY_UP, // Matrix: Taste losgelassen (key_id = Matrix-Key 024)
ENC_CW, // Encoder: Schritt CW (key_id = Encoder-Index 03)
ENC_CCW, // Encoder: Schritt CCW (key_id = Encoder-Index 03)
};
struct SEvent
{
EventType type;
uint8_t key_id; // Matrix-Key (024) oder Encoder-Index (03)
uint16_t payload; // Reserviert für zukünftige Erweiterungen
};

18
src/config/action.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
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
};

116
src/config/nvm_config.cpp Normal file
View File

@ -0,0 +1,116 @@
#include "nvm_config.h"
#include <Arduino.h>
#include <string.h>
// ── 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<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(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<const uint8_t*>(&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<uint16_t>(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<const void*>(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);
}
}

56
src/config/nvm_config.h Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#include <stdint.h>
#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 7162
// 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 019 (key_id 524)
SAction enc_actions[4][3]; // [Encoder 03][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);

68
src/config/pins.h Normal file
View File

@ -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_03 = encoder push buttons
// COL_14 × ROW_04 = 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
};

88
src/hal/encoder.cpp Normal file
View File

@ -0,0 +1,88 @@
#include "encoder.h"
#include <Arduino.h>
#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);
}

10
src/hal/encoder.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#define ENCODER_COUNT 4
// Callback: enc = Encoder-Index (03), 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

68
src/hal/matrix.cpp Normal file
View File

@ -0,0 +1,68 @@
#include "matrix.h"
#include <Arduino.h>
#include "config/pins.h"
#include <string.h>
// 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);
}
}

22
src/hal/matrix.h Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include <stdint.h>
// 5×5 button matrix
// COL_0 × ROW_03 = encoder SW buttons
// COL_14 × ROW_04 = 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 (024), 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;
}

96
src/hal/usb_hid.cpp Normal file
View File

@ -0,0 +1,96 @@
#include "usb_hid.h"
#include <Arduino.h>
#include <HID.h>
// ── 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));
}

35
src/hal/usb_hid.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
// ── 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();

56
src/hal/usb_serial.cpp Normal file
View File

@ -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 <Arduino.h>
// Ring-Buffer für eingehende Bytes CDC kann jederzeit Bytes liefern.
// Größe: 32 Pakete × 8 Bytes = 256 Bytes reicht für eine vollständige
// Config-Übertragung (30 Pakete) ohne Überlauf.
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;
}

70
src/hal/usb_serial.h Normal file
View File

@ -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 024 oder Encoder 03)
// [2] r / Daten-Byte A
// [3] g / Daten-Byte B
// [4] b
// [5..7] reserviert (0x00)
//
// Richtungen:
// PC → Board (Commands, 0x010x7F): poll_vendor() in CMainController
// Board → PC (Events, 0x810xFF): usb_serial_send() in processEvents()
#pragma once
#include <stdint.h>
#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);

35
src/hal/ws2812.cpp Normal file
View File

@ -0,0 +1,35 @@
// WS2812 driver wraps Adafruit NeoPixel (bit-bang, no SERCOM needed)
#include "ws2812.h"
#include <Adafruit_NeoPixel.h>
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();
}

30
src/hal/ws2812.h Normal file
View File

@ -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 <stdint.h>
#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();

27
src/main.cpp Normal file
View File

@ -0,0 +1,27 @@
#include <Arduino.h>
#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();
}

24
upload_openocd.py Normal file
View File

@ -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)

View File

@ -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 = .;
}

View File

@ -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 = .;
}

View File

@ -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 D0D25 to SAMD21 port/pin pairs.
//
// Format:
// { PORT, PIN, PinType, PinAttr, ADC_Channel, PWM_Channel, TC_Channel, ExtInt }
//
// ─────────────────────────────────────────────────────────────────────────────
const PinDescription g_APinDescription[] = {
// ── Button Matrix: Columns (D0D4) ────────────────────────────────────────
// 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 (D5D9) ───────────────────────────────────────────
// 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 (D10D17) ─────────────────────────────────────────────
// 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 (D18D20) ───────────────────────────────────────
// 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+ (D21D22) ────────────────────────────────────────────────
// 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 (D23D25 = A0A2) ──────────────────────────────────────
// 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.
}

View File

@ -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 <WVariant.h>
// ─── Pin count ────────────────────────────────────────────────────────────────
// D0D9 : Button matrix (COL_04, ROW_04)
// D10D17 : Rotary encoders A/B (ENC0ENC3), all on EIC
// D18D20 : SPI for WS2812 (SERCOM5: MOSI, SCK, MISO)
// D21D22 : USB D/D+ (internal, not exposed)
// D23D25 : Fader ADC inputs (A0A2)
// ─────────────────────────────────────────────────────────────────────────────
#define PINS_COUNT (26u)
#define NUM_DIGITAL_PINS (23u) // D0D22 usable as digital
#define NUM_ANALOG_INPUTS (3u) // A0A2 = D23D25
#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)