Initial commit
This commit is contained in:
commit
b49984b9c0
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# PlatformIO
|
||||
.pio/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
143
README.md
Normal file
143
README.md
Normal 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 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 |
|
||||
35
boards/versapad.json
Normal file
35
boards/versapad.json
Normal 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
30
boards/versapad_nobl.json
Normal 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
29
platformio.ini
Normal 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
280
src/CButton.cpp
Normal 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 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 <Arduino.h>
|
||||
#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<uint8_t>(m_led_index), c.r, c.g, c.b);
|
||||
|
||||
m_dirty = false;
|
||||
return true;
|
||||
}
|
||||
136
src/CButton.h
Normal file
136
src/CButton.h
Normal 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 (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;
|
||||
};
|
||||
43
src/CEventQueue.cpp
Normal file
43
src/CEventQueue.cpp
Normal 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
37
src/CEventQueue.h
Normal 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
353
src/CMainController.cpp
Normal 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:
|
||||
// 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<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
50
src/CMainController.h
Normal 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 (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
|
||||
};
|
||||
25
src/SEvent.h
Normal file
25
src/SEvent.h
Normal 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 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
|
||||
};
|
||||
18
src/config/action.h
Normal file
18
src/config/action.h
Normal 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
116
src/config/nvm_config.cpp
Normal 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
56
src/config/nvm_config.h
Normal 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 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);
|
||||
68
src/config/pins.h
Normal file
68
src/config/pins.h
Normal 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_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
|
||||
};
|
||||
88
src/hal/encoder.cpp
Normal file
88
src/hal/encoder.cpp
Normal 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
10
src/hal/encoder.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
68
src/hal/matrix.cpp
Normal file
68
src/hal/matrix.cpp
Normal 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
22
src/hal/matrix.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
96
src/hal/usb_hid.cpp
Normal file
96
src/hal/usb_hid.cpp
Normal 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
35
src/hal/usb_hid.h
Normal 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
56
src/hal/usb_serial.cpp
Normal 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
70
src/hal/usb_serial.h
Normal 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 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 <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
35
src/hal/ws2812.cpp
Normal 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
30
src/hal/ws2812.h
Normal 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
27
src/main.cpp
Normal 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
24
upload_openocd.py
Normal 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)
|
||||
100
variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld
Normal file
100
variants/versapad/linker_scripts/gcc/flash_with_bootloader.ld
Normal 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 = .;
|
||||
}
|
||||
108
variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld
Normal file
108
variants/versapad/linker_scripts/gcc/flash_without_bootloader.ld
Normal 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 = .;
|
||||
}
|
||||
100
variants/versapad/variant.cpp
Normal file
100
variants/versapad/variant.cpp
Normal 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 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.
|
||||
}
|
||||
98
variants/versapad/variant.h
Normal file
98
variants/versapad/variant.h
Normal 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 ────────────────────────────────────────────────────────────────
|
||||
// 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)
|
||||
Loading…
x
Reference in New Issue
Block a user