Initial commit

This commit is contained in:
2026-03-29 14:47:13 +02:00
commit b49984b9c0
32 changed files with 2394 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
// CButton.cpp
// Implementierung der CButton-Klasse.
//
// Animations-Arithmetik:
// Alle Berechnungen in Integer (kein float) Cortex-M0+ hat keine FPU.
// Helligkeits-Skalierung: (Kanalwert * scale) / 255
// PULSE nutzt ein lineares Dreieck (0→255→0) statt Sinus einfacher, trotzdem
// weich genug für den visuellen Eindruck bei 2050ms Loop-Rate.
//
// COLOR_CYCLE Hue-Sweep:
// Teilt den Farbraum in 6 Segmente (R→Y→G→C→B→M→R), je 43 Hue-Einheiten.
// Innerhalb jedes Segments steigt/fällt ein Kanal linear kein HSV-Float nötig.
//
// COLOR_FADE Crossfade:
// Lineare RGB-Interpolation zwischen m_anim_from und m_anim_to.
// t = elapsed * 255 / period (0→255); Ergebnis: from + (to-from)*t/255.
#include <Arduino.h>
#include "CButton.h"
#include "hal/ws2812.h"
// ── Hue → RGB (vollständig gesättigte Farbe, S=255, V=255) ───────────────────
//
// Hue 0255 wird auf 6 Segmente à 43 Einheiten aufgeteilt:
// Seg 0: R=255, G steigt, B=0 (Rot → Gelb)
// Seg 1: R fällt, G=255, B=0 (Gelb → Grün)
// Seg 2: R=0, G=255, B steigt (Grün → Cyan)
// Seg 3: R=0, G fällt, B=255 (Cyan → Blau)
// Seg 4: R steigt, G=0, B=255 (Blau → Magenta)
// Seg 5: R=255, G=0, B fällt (Magenta → Rot)
static RGB hue_to_rgb(uint8_t h)
{
uint8_t seg = h / 43;
uint8_t frac = (uint8_t)((h % 43) * 6); // 0252, Annäherung an 0255
switch (seg) {
case 0: return RGB(255, frac, 0);
case 1: return RGB(255 - frac, 255, 0);
case 2: return RGB(0, 255, frac);
case 3: return RGB(0, 255 - frac, 255);
case 4: return RGB(frac, 0, 255);
default: return RGB(255, 0, 255 - frac);
}
}
// ── Konstruktor ───────────────────────────────────────────────────────────────
CButton::CButton()
: m_key_id(0)
, m_led_index(-1)
, m_action{ActionType::NONE, 0}
, m_base()
, m_override()
, m_override_active(false)
, m_dirty(false)
, m_anim(LEDAnim::STATIC)
, m_anim_period_ms(500)
, m_anim_start_ms(0)
, m_anim_from()
, m_anim_to()
{}
// ── Initialisierung ───────────────────────────────────────────────────────────
void CButton::init(uint8_t key_id, int8_t led_index, SAction action, RGB base)
{
m_key_id = key_id;
m_led_index = led_index;
m_action = action;
m_base = base;
m_override_active = false;
m_anim = LEDAnim::STATIC;
m_dirty = true; // Initialen Zustand beim ersten render_led() schreiben
}
// ── Tastendruck-Hooks ─────────────────────────────────────────────────────────
void CButton::on_press()
{
// Reserviert für zukünftige Button-Logik (Hold, Toggle, Doppelklick …)
}
void CButton::on_release()
{
// Reserviert für zukünftige Button-Logik
}
// ── LED Layer 1: base ─────────────────────────────────────────────────────────
void CButton::set_base(RGB color)
{
m_base = color;
m_dirty = true;
}
// ── LED Layer 2: override ─────────────────────────────────────────────────────
void CButton::set_override(RGB color)
{
m_override = color;
m_override_active = true;
m_dirty = true;
}
void CButton::clear_override()
{
m_override_active = false;
m_dirty = true; // Base-Farbe muss neu in WS2812-Buffer geschrieben werden
}
// ── LED-Animation ─────────────────────────────────────────────────────────────
void CButton::set_anim(LEDAnim anim, uint16_t period_ms, uint16_t phase_offset_ms)
{
m_anim = anim;
m_anim_period_ms = (period_ms > 0) ? period_ms : 1; // Division durch 0 vermeiden
// phase_offset_ms in die Vergangenheit zurücksetzen → verschobener Startpunkt
m_anim_start_ms = millis() - phase_offset_ms;
m_dirty = true;
}
void CButton::set_color_fade(RGB to, uint16_t period_ms)
{
// Startfarbe = Snapshot der aktuell aktiven Farbe (override > base)
m_anim_from = m_override_active ? m_override : m_base;
m_anim_to = to;
m_anim = LEDAnim::COLOR_FADE;
m_anim_period_ms = (period_ms > 0) ? period_ms : 1;
m_anim_start_ms = millis();
m_dirty = true;
}
void CButton::clear_anim()
{
m_anim = LEDAnim::STATIC;
m_dirty = true; // Sofort wieder volle Helligkeit rendern
}
// ── Animations-Hilfsmethoden ──────────────────────────────────────────────────
// Gibt true zurück solange die Animation noch läuft.
// BLINK, PULSE, COLOR_CYCLE laufen endlos; FADE_*/COLOR_FADE enden nach period_ms.
bool CButton::is_animating() const
{
if (m_anim == LEDAnim::STATIC) return false;
if (m_anim == LEDAnim::BLINK ||
m_anim == LEDAnim::PULSE ||
m_anim == LEDAnim::COLOR_CYCLE) return true;
// FADE_IN / FADE_OUT / COLOR_FADE: läuft bis elapsed >= period
return (millis() - m_anim_start_ms) < (uint32_t)m_anim_period_ms;
}
// Berechnet den aktuellen Helligkeits-Skalierungsfaktor (0 = aus, 255 = voll).
// Darf m_anim und m_dirty verändern (Abschluss von FADE_IN/FADE_OUT).
uint8_t CButton::compute_scale()
{
if (m_anim == LEDAnim::STATIC) return 255;
uint32_t elapsed = millis() - m_anim_start_ms;
uint32_t period = (uint32_t)m_anim_period_ms;
switch (m_anim) {
case LEDAnim::BLINK: {
// An für erste Halbperiode, aus für zweite
uint32_t t = elapsed % period;
return (t < period / 2) ? 255 : 0;
}
case LEDAnim::PULSE: {
// Lineares Dreieck: 0 → 255 → 0 in einer Periode
uint32_t t = elapsed % period;
uint32_t half = period / 2;
if (t < half)
return (uint8_t)((t * 255) / half);
else
return (uint8_t)(((period - t) * 255) / half);
}
case LEDAnim::FADE_IN: {
// Einmalig: schwarz → volle Helligkeit
if (elapsed >= period) {
// Animation abgeschlossen ab jetzt STATIC bei voller Helligkeit
m_anim = LEDAnim::STATIC;
m_dirty = true;
return 255;
}
return (uint8_t)((elapsed * 255) / period);
}
case LEDAnim::FADE_OUT: {
// Einmalig: volle Helligkeit → schwarz
if (elapsed >= period) {
// Animation abgeschlossen LED bleibt aus.
// set_base() oder set_override() bringt sie zurück.
m_anim = LEDAnim::STATIC;
m_base = RGB(0, 0, 0); // Base auf schwarz setzen damit LED dunkel bleibt
m_dirty = true;
return 0;
}
return (uint8_t)(((period - elapsed) * 255) / period);
}
default:
return 255;
}
}
// Berechnet die finale RGB-Farbe für alle Animationstypen.
// Helligkeits-Animationen (STATIC/BLINK/PULSE/FADE_*):
// Wendet compute_scale() auf die aktive Farbe (override > base) an.
// Farb-Animationen (COLOR_CYCLE/COLOR_FADE):
// Berechnet die Farbe direkt; base/override werden nicht verändert.
RGB CButton::compute_rgb()
{
switch (m_anim) {
case LEDAnim::COLOR_CYCLE: {
// Hue 0→255 über eine Periode, dann Wiederholung; 40% Helligkeit (102/255)
uint32_t elapsed = millis() - m_anim_start_ms;
uint32_t period = (uint32_t)m_anim_period_ms;
uint8_t hue = (uint8_t)((elapsed % period) * 255 / period);
RGB c = hue_to_rgb(hue);
return RGB(
(uint8_t)(((uint16_t)c.r * 102) / 255),
(uint8_t)(((uint16_t)c.g * 102) / 255),
(uint8_t)(((uint16_t)c.b * 102) / 255)
);
}
case LEDAnim::COLOR_FADE: {
// Linearer Crossfade von m_anim_from nach m_anim_to
uint32_t elapsed = millis() - m_anim_start_ms;
uint32_t period = (uint32_t)m_anim_period_ms;
if (elapsed >= period) {
// Abgeschlossen base auf Zielfarbe setzen, ab jetzt STATIC
m_anim = LEDAnim::STATIC;
m_base = m_anim_to;
m_dirty = true;
return m_anim_to;
}
uint8_t t = (uint8_t)((elapsed * 255) / period); // 0→255
int16_t dr = (int16_t)m_anim_to.r - (int16_t)m_anim_from.r;
int16_t dg = (int16_t)m_anim_to.g - (int16_t)m_anim_from.g;
int16_t db = (int16_t)m_anim_to.b - (int16_t)m_anim_from.b;
return RGB(
(uint8_t)(m_anim_from.r + (dr * t) / 255),
(uint8_t)(m_anim_from.g + (dg * t) / 255),
(uint8_t)(m_anim_from.b + (db * t) / 255)
);
}
default: {
// Helligkeits-Animation: compute_scale() × aktive Farbe
const RGB& color = m_override_active ? m_override : m_base;
uint8_t scale = compute_scale();
return RGB(
(uint8_t)(((uint16_t)color.r * scale) / 255),
(uint8_t)(((uint16_t)color.g * scale) / 255),
(uint8_t)(((uint16_t)color.b * scale) / 255)
);
}
}
}
// ── Rendering ─────────────────────────────────────────────────────────────────
bool CButton::render_led()
{
if (!has_led()) return false;
bool animating = is_animating();
if (!m_dirty && !animating) return false; // Nichts zu tun
RGB c = compute_rgb();
ws2812_set(static_cast<uint8_t>(m_led_index), c.r, c.g, c.b);
m_dirty = false;
return true;
}
+136
View File
@@ -0,0 +1,136 @@
// CButton.h
// Bildet eine einzelne Taste (MX-Button oder Encoder-SW) vollständig ab:
// - Zugehöriger WS2812-LED-Index (oder -1 wenn keine LED)
// - Konfigurierte Aktion (HID_KEY, HID_CONSUMER, HOST_COMMAND, NONE)
// - 2-Layer-LED-Modell: base (Idle-Farbe) + override (temporär, z.B. Benachrichtigung)
// - LED-Animationen: STATIC, BLINK, PULSE, FADE_IN, FADE_OUT, COLOR_CYCLE, COLOR_FADE
//
// LED-Rendering:
// Aktive Farbe = override wenn gesetzt, sonst base.
// Helligkeits-Animationen (BLINK, PULSE, FADE_*) modulieren die Helligkeit der aktiven Farbe.
// Farb-Animationen (COLOR_CYCLE, COLOR_FADE) berechnen die Farbe selbst (ignorieren base/override).
// render_led() gibt true zurück solange eine Animation läuft oder dirty gesetzt ist
// CMainController::updateLEDs() ruft dann ws2812_show() auf.
//
// Aktionsausführung:
// on_press() / on_release() sind Hooks für zukünftige Button-Logik.
// Die eigentliche Aktion wird vom CMainController via action() ausgeführt.
#pragma once
#include <stdint.h>
#include "config/action.h"
// ── Farb-Struct ───────────────────────────────────────────────────────────────
struct RGB
{
uint8_t r, g, b;
RGB() : r(0), g(0), b(0) {}
RGB(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b) {}
};
// ── LED-Animationen ───────────────────────────────────────────────────────────
//
// Helligkeits-Animationen (BLINK, PULSE, FADE_*):
// Modulieren die Helligkeit der aktiven Farbe (base oder override).
// BLINK und PULSE laufen endlos bis clear_anim() aufgerufen wird.
// FADE_IN und FADE_OUT sind einmalig nach Ablauf zurück zu STATIC.
// FADE_OUT: nach Abschluss leuchtet die LED nicht (base wird auf schwarz gesetzt).
//
// Farb-Animationen (COLOR_CYCLE, COLOR_FADE):
// Berechnen die Farbe selbst base/override werden ignoriert, aber nicht verändert.
// COLOR_CYCLE läuft endlos; COLOR_FADE ist einmalig (base wird auf Zielfarbe gesetzt).
// Für COLOR_FADE: set_color_fade(to, period_ms) statt set_anim() verwenden.
enum class LEDAnim : uint8_t
{
STATIC = 0, // Sofort, keine Animation (Standardzustand)
BLINK, // Binäres An/Aus period_ms = Halbperiode (An-Zeit = Aus-Zeit)
PULSE, // Lineares Fade-In/Fade-Out in Schleife period_ms = Vollperiode
FADE_IN, // Einmalig: schwarz → volle Helligkeit über period_ms
FADE_OUT, // Einmalig: volle Helligkeit → schwarz über period_ms
COLOR_CYCLE, // Endloser Hue-Sweep (Regenbogen) period_ms = eine volle Runde
COLOR_FADE, // Einmalig: Crossfade von Startfarbe → Zielfarbe über period_ms
};
// ── CButton ───────────────────────────────────────────────────────────────────
class CButton
{
public:
CButton();
// Initialisierung (ersetzt Konstruktor-Parameter).
// led_index = -1 → kein LED (Encoder-SW-Buttons).
void init(uint8_t key_id, int8_t led_index, SAction action, RGB base = RGB());
// Hooks für Tastendruck/-loslassen.
// Reserviert für zukünftige Logik (Hold-Aktionen, Toggle-Modus, …).
void on_press();
void on_release();
// ── LED Layer 1: base ─────────────────────────────────────────────────────
// Idle-Farbe, aus NVM geladen oder von Windows-App gesetzt.
void set_base(RGB color);
// ── LED Layer 2: override ─────────────────────────────────────────────────
// Temporärer Override, überschreibt base solange aktiv.
// Typisch: Windows-App signalisiert Benachrichtigung.
void set_override(RGB color);
void clear_override(); // Zurück zu base
// ── LED-Animation ─────────────────────────────────────────────────────────
// set_anim(): für STATIC, BLINK, PULSE, FADE_IN, FADE_OUT, COLOR_CYCLE.
// period_ms: Halbperiode (BLINK), Vollperiode (PULSE/COLOR_CYCLE), Dauer (FADE_*).
// phase_offset_ms: Zeitversatz in die Vergangenheit verschiebt den Startpunkt der
// Animation. Nützlich für COLOR_CYCLE um LEDs versetzt starten zu
// lassen (Regenbogen-Wellen-Effekt über mehrere Buttons).
void set_anim(LEDAnim anim, uint16_t period_ms = 500, uint16_t phase_offset_ms = 0);
// Crossfade von der aktuell aktiven Farbe zur Zielfarbe.
// Nach Abschluss wird base auf die Zielfarbe gesetzt → LED bleibt in Zielfarbe.
void set_color_fade(RGB to, uint16_t period_ms = 500);
void clear_anim(); // Zurück zu STATIC, Helligkeit sofort wieder voll
// ── Rendering ─────────────────────────────────────────────────────────────
// Schreibt aktuelle Farbe (mit Animations-Skalierung) in den WS2812-Buffer.
// Gibt true zurück wenn ws2812_show() nötig ist:
// - dirty-Flag gesetzt (Farbe hat sich geändert), oder
// - Animation läuft (jeder Frame braucht einen neuen ws2812_set-Aufruf)
bool render_led();
uint8_t key_id() const { return m_key_id; }
bool has_led() const { return m_led_index >= 0; }
SAction action() const { return m_action; }
private:
uint8_t m_key_id;
int8_t m_led_index;
SAction m_action;
// LED-Zustand
RGB m_base;
RGB m_override;
bool m_override_active;
bool m_dirty; // true = render_led() muss ws2812_set() aufrufen
// Animations-Zustand
LEDAnim m_anim;
uint16_t m_anim_period_ms;
uint32_t m_anim_start_ms;
RGB m_anim_from; // COLOR_FADE: Startfarbe (Snapshot bei set_color_fade())
RGB m_anim_to; // COLOR_FADE: Zielfarbe
// Helfer: Helligkeits-Skalierungsfaktor (0255) für BLINK/PULSE/FADE_*.
// Aktualisiert m_anim bei Abschluss von FADE_IN/FADE_OUT.
uint8_t compute_scale();
// Helfer: finale RGB-Farbe für alle Animationstypen.
// Ruft compute_scale() für Helligkeits-Animationen auf;
// berechnet Farbe direkt für COLOR_CYCLE und COLOR_FADE.
RGB compute_rgb();
// true wenn die Animation noch läuft (BLINK/PULSE/COLOR_CYCLE immer, FADE* bis Ablauf)
bool is_animating() const;
};
+43
View File
@@ -0,0 +1,43 @@
// CEventQueue.cpp
// Ring-Buffer FIFO-Implementierung.
//
// Funktionsprinzip (klassischer Power-of-2-freier Ring-Buffer):
// m_head = Lese-Index (pop)
// m_tail = Schreib-Index (push)
// Leer: m_head == m_tail
// Voll: (m_tail + 1) % SIZE == m_head → ein Slot bleibt immer frei
//
// Interrupt-Sicherheit (Cortex-M0+):
// push() wird aus Encoder-ISR aufgerufen, pop() aus dem Loop.
// Auf M0+ sind uint8_t-Lese/Schreibzugriffe atomar (single-cycle LDR/STR)
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren, ist kein
// Mutex nötig. Bei mehreren Producern müsste noInterrupts() verwendet werden.
#include "CEventQueue.h"
bool CEventQueue::is_empty() const
{
return m_head == m_tail;
}
bool CEventQueue::is_full() const
{
// Voll wenn der nächste Schreib-Index auf den Lese-Index zeigen würde
return ((m_tail + 1) % QUEUE_SIZE) == m_head;
}
bool CEventQueue::push(SEvent ev)
{
if (is_full()) return false; // Event verwerfen sollte bei 16 Slots nie passieren
m_buf[m_tail] = ev;
m_tail = (m_tail + 1) % QUEUE_SIZE;
return true;
}
bool CEventQueue::pop(SEvent& out)
{
if (is_empty()) return false;
out = m_buf[m_head];
m_head = (m_head + 1) % QUEUE_SIZE;
return true;
}
+37
View File
@@ -0,0 +1,37 @@
// CEventQueue.h
// Fester Ring-Buffer FIFO für SEvent-Objekte.
//
// Bewusst ohne Heap (kein new/delete) und ohne STL (kein std::vector/queue)
// um auf dem SAMD21 mit 16KB RAM deterministisches Verhalten zu garantieren.
//
// Kapazität: QUEUE_SIZE - 1 = 16 Events (ein Slot bleibt leer damit
// is_full() und is_empty() ohne extra Zähler unterscheidbar sind).
//
// Thread-Sicherheit:
// push() wird aus ISR-Kontext aufgerufen (encoder_cb).
// pop() wird aus Loop-Kontext aufgerufen (processEvents).
// Auf Cortex-M0+ sind 8-Bit-Lese/Schreibzugriffe atomar → kein Mutex nötig
// solange nur ein Producer (ISR) und ein Consumer (Loop) existieren.
#pragma once
#include "SEvent.h"
#include <stdint.h>
class CEventQueue
{
public:
// Event einreihen. Gibt false zurück wenn Queue voll (Event wird verworfen).
bool push(SEvent ev);
// Ältestes Event abholen (FIFO). Gibt false zurück wenn Queue leer.
bool pop(SEvent& out);
bool is_empty() const;
bool is_full() const;
private:
static const uint8_t QUEUE_SIZE = 17; // 16 nutzbare Slots
SEvent m_buf[QUEUE_SIZE];
uint8_t m_head = 0; // Nächster Lese-Index (Consumer: pop)
uint8_t m_tail = 0; // Nächster Schreib-Index (Producer: push)
};
+353
View File
@@ -0,0 +1,353 @@
// CMainController.cpp
// Zentraler Orchestrator des VersaPad v2.
//
// Aufgaben:
// 1. Alle Hardware-Peripherie initialisieren (Matrix, Encoder, USB)
// 2. Pro Loop-Durchlauf:
// a) matrix_scan() → Callback → Events in Queue
// b) Encoder-ISRs laufen asynchron → Events in Queue
// c) poll_vendor() → eingehende Serial-Pakete (PC→Board) direkt verarbeiten
// d) processEvents() → Queue leeren, Aktionen ausführen
// e) updateLEDs() → dirty CButtons in WS2812-Buffer schreiben + show()
//
// Datenfluss:
// HAL (matrix_cb / encoder_cb)
// └─► CEventQueue
// └─► processEvents()
// ├─► CButton.on_press() / on_release()
// ├─► execute_action() → USB HID / Serial
// └─► usb_serial_send() (nur bei HOST_COMMAND)
//
// SerialUSB (PC→Board)
// └─► poll_vendor()
// └─► CButton.set_override() / set_base() / clear_override()
#include <Arduino.h>
#include <string.h>
#include "CMainController.h"
#include "hal/ws2812.h"
#include "hal/matrix.h"
#include "hal/encoder.h"
#include "hal/usb_serial.h"
#include "config/pins.h"
#include "config/nvm_config.h"
#include <string.h>
// ─── Static Bridge: HAL-Callbacks → EventQueue ───────────────────────────────
//
// matrix_init() und encoder_init() erwarten einfache Funktionszeiger (kein
// Lambda mit Capture möglich auf Cortex-M0+). Der Queue-Pointer wird einmalig
// in setup() gesetzt, bevor die Callbacks registriert werden.
static CEventQueue* s_queue = nullptr;
// Wird von matrix_scan() aufgerufen wenn sich ein Tasten-Zustand ändert.
// Läuft im Loop-Kontext (kein ISR).
static void matrix_cb(uint8_t key, bool pressed)
{
if (!s_queue) return;
SEvent ev;
ev.type = pressed ? EventType::KEY_DOWN : EventType::KEY_UP;
ev.key_id = key;
ev.payload = 0;
s_queue->push(ev);
}
// Wird von handle_encoder() aufgerufen läuft im ISR-Kontext (EIC-Interrupt).
// CEventQueue::push() ist interrupt-sicher (kein Heap, atomare Indizes auf M0+).
static void encoder_cb(uint8_t enc, int8_t dir)
{
if (!s_queue) return;
SEvent ev;
ev.type = (dir > 0) ? EventType::ENC_CW : EventType::ENC_CCW;
ev.key_id = enc;
ev.payload = 0;
s_queue->push(ev);
}
// ─── Konstruktor / Setup ──────────────────────────────────────────────────────
CMainController::CMainController()
: m_cfg_chunks_expected(0)
, m_cfg_receiving(false)
{
memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
}
void CMainController::setup()
{
init_buttons(); // Buttons aus NVM laden (oder Defaults)
s_queue = &m_queue; // Queue-Pointer setzen bevor Callbacks registriert werden
usb_hid_init(); // HID-Descriptor registriert sich via globalem Konstruktor,
// usb_hid_init() ist hier ein No-Op aber verdeutlicht die Abhängigkeit
usb_serial_init(); // CDC Serial öffnen
matrix_init(matrix_cb); // Matrix-Scan initialisieren + Callback registrieren
encoder_init(encoder_cb);// EIC-Interrupts für alle 4 Encoder einrichten
}
// Lädt Config aus NVM und initialisiert alle CButton-Instanzen.
// key_id-Mapping:
// 03 : Encoder-SW-Buttons (COL_0 × ROW_03), kein LED
// 4 : nicht belegt (COL_0 × ROW_4)
// 524 : Cherry MX Buttons (COL_14 × ROW_04), je ein WS2812-LED
void CMainController::init_buttons()
{
SDeviceConfig cfg;
bool valid = nvm_config_load(cfg);
(void)valid; // false = keine gültige Config → Defaults wurden bereits geladen
// Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1)
for (uint8_t enc = 0; enc < 4; enc++) {
m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW]);
}
// MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen,
// Aktion + Base-Farbe aus NVM.
// mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4)
//
// Idle-Animation: Regenbogen-Sweep über alle 20 LEDs.
// Jede LED bekommt einen gleichmäßigen Hue-Versatz (phase = idx * period / 20),
// sodass immer ein voller Regenbogen auf dem Pad liegt und sich langsam dreht.
const uint16_t k_rainbow_period = 4000; // 4s pro volle Runde
for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
uint8_t col = key / MATRIX_ROWS;
uint8_t row = key % MATRIX_ROWS;
int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
uint8_t mx_idx = key - 5;
RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]);
m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base);
// Phase gleichmäßig verteilen: LED 0 = Hue 0, LED 19 = Hue ~242 (fast voll)
uint16_t phase = (uint16_t)((uint32_t)mx_idx * k_rainbow_period / 20);
m_buttons[key].set_anim(LEDAnim::COLOR_CYCLE, k_rainbow_period, phase);
}
// Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt
// da sie keine LED haben und kein Matrix-Key sind.
for (uint8_t enc = 0; enc < 4; enc++) {
m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW];
m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW];
}
}
// ─── Haupt-Loop ───────────────────────────────────────────────────────────────
void CMainController::work()
{
matrix_scan(); // 1. Matrix scannen → Debounce → matrix_cb() → Queue
poll_vendor(); // 2. Eingehende Serial-Pakete (PC→Board) verarbeiten
processEvents(); // 3. Queue leeren: Aktionen ausführen, Buttons benachrichtigen
updateLEDs(); // 4. Geänderte LED-Zustände in WS2812-Buffer schreiben + show()
}
// ─── Vendor-Kommunikation (PC → Board) ───────────────────────────────────────
//
// Die Windows-App sendet 8-Byte-Pakete über den CDC Serial-Port.
// poll_vendor() holt alle verfügbaren vollständigen Pakete ab und
// wendet die Kommandos direkt auf die CButton-Instanzen an.
// LED-Änderungen werden beim nächsten updateLEDs()-Aufruf sichtbar.
void CMainController::poll_vendor()
{
SerialPacket pkt;
while (usb_serial_poll(pkt)) {
switch (pkt.command()) {
// Override-LED setzen: Button leuchtet in der angegebenen Farbe,
// bis clear_override() aufgerufen wird (z.B. Benachrichtigung)
case USB_CMD_SET_LED_OVERRIDE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].set_override(RGB(pkt.r(), pkt.g(), pkt.b()));
break;
// Override-LED löschen: Button kehrt zur konfigurierten Base-Farbe zurück
case USB_CMD_CLEAR_LED_OVERRIDE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].clear_override();
break;
// Base-LED setzen: dauerhaft neue Idle-Farbe (wird nicht in NVM geschrieben)
case USB_CMD_SET_LED_BASE:
if (pkt.key_id() < MATRIX_KEYS)
m_buttons[pkt.key_id()].set_base(RGB(pkt.r(), pkt.g(), pkt.b()));
break;
// Ping sofortige Antwort zum Testen der Verbindung
case USB_CMD_PING:
usb_serial_send(USB_EVT_PONG, 0);
break;
// Config-Übertragung: BEGIN → n×DATA → COMMIT
case USB_CMD_CONFIG_BEGIN:
// Neuen Empfang starten bisherige Daten verwerfen
m_cfg_chunks_expected = pkt.key_id();
m_cfg_receiving = true;
memset(m_cfg_buf, 0, sizeof(m_cfg_buf));
break;
case USB_CMD_CONFIG_DATA:
if (m_cfg_receiving) {
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
uint16_t offset = (uint16_t)pkt.key_id() * 6;
if (offset < sizeof(m_cfg_buf)) {
uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset);
if (count > 6) count = 6;
memcpy(m_cfg_buf + offset, &pkt.data[2], count);
}
}
break;
// Config-Dump anfordern: Board sendet NVM-Config in 6-Byte-Chunks
// zurück an die App (gleiche Chunk-Struktur wie beim Schreiben).
case USB_CMD_CONFIG_READ:
{
SDeviceConfig cfg;
nvm_config_load(cfg); // ungültige NVM → Defaults
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
const uint8_t sz = sizeof(SDeviceConfig); // 163
const uint8_t payload = 6;
uint8_t chunks = (sz + payload - 1) / payload; // 28
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
for (uint8_t i = 0; i < chunks; i++) {
uint8_t p[SERIAL_PKT_SIZE] = {};
p[0] = USB_EVT_CONFIG_DATA;
p[1] = i;
uint8_t offset = i * payload;
for (uint8_t b = 0; b < payload; b++) {
if (offset + b < sz) p[2 + b] = raw[offset + b];
}
if (SerialUSB) SerialUSB.write(p, SERIAL_PKT_SIZE);
}
usb_serial_send(USB_EVT_CONFIG_END, chunks);
break;
}
case USB_CMD_CONFIG_COMMIT:
if (m_cfg_receiving) {
m_cfg_receiving = false;
SDeviceConfig cfg;
memcpy(&cfg, m_cfg_buf, sizeof(cfg));
if (cfg.magic == NVM_CONFIG_MAGIC &&
cfg.version == NVM_CONFIG_VERSION &&
cfg.crc == nvm_config_crc(cfg))
{
nvm_config_save(cfg);
init_buttons();
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
}
else
{
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden
}
}
break;
default:
break;
}
}
}
// ─── Event-Verarbeitung ───────────────────────────────────────────────────────
//
// Verarbeitet alle Events in der Queue bis sie leer ist.
// Reihenfolge: ältestes Event zuerst (FIFO).
//
// HOST_COMMAND-Aktionen werden zusätzlich über Serial an die Windows-App
// gemeldet die App entscheidet dann was passiert (URL öffnen, Programm starten…).
void CMainController::processEvents()
{
SEvent ev;
while (m_queue.pop(ev)) {
switch (ev.type) {
case EventType::KEY_DOWN:
if (ev.key_id < MATRIX_KEYS) {
m_buttons[ev.key_id].on_press();
execute_action(m_buttons[ev.key_id].action());
// Bei HOST_COMMAND: Event-ID an Windows-App senden
if (m_buttons[ev.key_id].action().type == ActionType::HOST_COMMAND)
usb_serial_send(USB_EVT_KEY_DOWN, ev.key_id);
}
break;
case EventType::KEY_UP:
if (ev.key_id < MATRIX_KEYS)
m_buttons[ev.key_id].on_release();
break;
case EventType::ENC_CW:
if (ev.key_id < 4) {
execute_action(m_enc_cw[ev.key_id]);
if (m_enc_cw[ev.key_id].type == ActionType::HOST_COMMAND)
usb_serial_send(USB_EVT_ENC_CW, ev.key_id);
}
break;
case EventType::ENC_CCW:
if (ev.key_id < 4) {
execute_action(m_enc_ccw[ev.key_id]);
if (m_enc_ccw[ev.key_id].type == ActionType::HOST_COMMAND)
usb_serial_send(USB_EVT_ENC_CCW, ev.key_id);
}
break;
default:
break;
}
}
}
// Führt eine einzelne Aktion aus.
// HID_KEY / HID_CONSUMER: direkt über USB HID gesendet (funktioniert ohne Windows-App).
// HOST_COMMAND: kein direkter Aufruf hier das Event wird in processEvents() via
// usb_serial_send() an die Windows-App weitergeleitet.
void CMainController::execute_action(SAction action)
{
switch (action.type) {
case ActionType::HID_KEY:
// data-Encoding: Low-Byte = Keycode, High-Byte = Modifier
usb_hid_send_key(static_cast<uint8_t>(action.data & 0xFF),
static_cast<uint8_t>(action.data >> 8));
delay(10); // Host braucht kurz Zeit zwischen Key-Down und Key-Up
usb_hid_release_key();
break;
case ActionType::HID_CONSUMER:
usb_hid_send_consumer(action.data);
usb_hid_release_consumer();
break;
case ActionType::HOST_COMMAND:
// Wird in processEvents() über Serial gesendet
break;
case ActionType::NONE:
default:
break;
}
}
// ─── LED-Rendering ────────────────────────────────────────────────────────────
//
// Fragt alle CButton-Instanzen ab. Jede Instanz mit dirty-Flag schreibt
// ihre aktuelle Farbe (override wenn aktiv, sonst base) in den WS2812-Buffer.
// ws2812_show() wird nur aufgerufen wenn mindestens ein Button dirty war
// das vermeidet unnötige noInterrupts()-Aufrufe (~600µs Blockzeit).
void CMainController::updateLEDs()
{
bool dirty = false;
for (uint8_t i = 0; i < MATRIX_KEYS; i++) {
if (m_buttons[i].render_led())
dirty = true;
}
if (dirty)
ws2812_show();
}
+50
View File
@@ -0,0 +1,50 @@
// CMainController.h
// Zentraler Orchestrator des VersaPad v2.
// Kennt alle Subsysteme und koordiniert den Datenfluss zwischen ihnen.
// Einzige Instanz wird in main.cpp angelegt.
#pragma once
#include "CButton.h"
#include "CEventQueue.h"
#include "SEvent.h"
#include "hal/matrix.h"
#include "hal/encoder.h"
#include "hal/usb_hid.h"
#include "hal/usb_serial.h"
#include "config/action.h"
class CMainController
{
public:
CMainController();
void setup(); // Einmalig in Arduino setup() aufrufen
void work(); // Jeden Loop-Durchlauf aufrufen
private:
// m_queue muss vor m_buttons deklariert sein: C++ initialisiert Member in
// Deklarationsreihenfolge. Die static-Bridge-Funktion (matrix_cb) erhält
// einen Pointer auf m_queue der muss zum Zeitpunkt der Nutzung gültig sein.
CEventQueue m_queue;
// Alle 25 Matrix-Keys (024) als CButton-Array.
// key_id 03: Encoder-SW (kein LED), key_id 4: NC, key_id 524: MX-Buttons mit LED.
CButton m_buttons[MATRIX_KEYS];
// Encoder CW/CCW-Aktionen aus NVM Encoder haben kein CButton-Objekt.
SAction m_enc_cw[4];
SAction m_enc_ccw[4];
void init_buttons(); // Buttons aus NVM-Config initialisieren
void poll_vendor(); // Eingehende Serial-Pakete (PC→Board) verarbeiten
void processEvents(); // Queue leeren, Aktionen ausführen
void execute_action(SAction); // Einzelne Aktion ausführen (HID / Serial)
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
// ── Config-Empfangspuffer ─────────────────────────────────────────────────
// Mehrteilige Übertragung: BEGIN setzt receiving=true, DATA füllt den Buffer,
// COMMIT validiert und schreibt in den NVM.
// Puffergröße = sizeof(SDeviceConfig) = 163 Bytes.
uint8_t m_cfg_buf[163]; // Empfangspuffer für eingehende Config-Daten
uint8_t m_cfg_chunks_expected; // Anzahl erwarteter Chunks (aus BEGIN-Paket)
bool m_cfg_receiving; // true wenn Übertragung läuft
};
+25
View File
@@ -0,0 +1,25 @@
// SEvent.h
// Event-Typen und Event-Struct für die zentrale CEventQueue.
//
// Events werden von HAL-Callbacks produziert (matrix_cb, encoder_cb)
// und von CMainController::processEvents() konsumiert.
// USB-Kommandos (PC→Board) laufen direkt über poll_vendor() und
// landen nicht in der Queue.
#pragma once
#include <stdint.h>
enum class EventType : uint8_t
{
KEY_DOWN, // Matrix: Taste gedrückt (key_id = Matrix-Key 024)
KEY_UP, // Matrix: Taste losgelassen (key_id = Matrix-Key 024)
ENC_CW, // Encoder: Schritt CW (key_id = Encoder-Index 03)
ENC_CCW, // Encoder: Schritt CCW (key_id = Encoder-Index 03)
};
struct SEvent
{
EventType type;
uint8_t key_id; // Matrix-Key (024) oder Encoder-Index (03)
uint16_t payload; // Reserviert für zukünftige Erweiterungen
};
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
enum class ActionType : uint8_t
{
NONE, // Keine Aktion
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
};
struct __attribute__((packed)) SAction
{
ActionType type;
uint16_t data; // Keycode (HID_KEY / HID_CONSUMER) oder Command-ID (HOST_COMMAND)
// packed: 1B type + 2B data = 3B (kein Alignment-Padding)
// Muss packed sein damit sizeof(SDeviceConfig)==163 == C#-Serialisierung
};
+116
View File
@@ -0,0 +1,116 @@
#include "nvm_config.h"
#include <Arduino.h>
#include <string.h>
// ── Flash-Adresse (aus Linkerscript) ─────────────────────────────────────────
// Kein separates Linker-Symbol nötig Adresse ist fix und bekannt.
static const uint32_t k_config_addr = 0x1FE00UL;
// SAMD21 NVMCTRL ──────────────────────────────────────────────────────────────
// Row = 256 Bytes = 4 Pages à 64 Bytes
// Schreiben: Row löschen (ER), dann seitenweise schreiben (WP)
static void nvm_wait()
{
while (!NVMCTRL->INTFLAG.bit.READY) {}
}
static void nvm_exec(uint16_t cmd)
{
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd;
nvm_wait();
}
static void nvm_erase_row(uint32_t addr)
{
nvm_wait();
NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte)
nvm_exec(NVMCTRL_CTRLA_CMD_ER);
}
static void nvm_write_page(uint32_t addr, const uint8_t* data)
{
// Page-Buffer löschen
nvm_exec(NVMCTRL_CTRLA_CMD_PBC);
// 64 Bytes in den Page-Buffer schreiben (32-Bit-Zugriffe)
volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
for (uint8_t i = 0; i < 64 / 4; i++) {
dst[i] = src[i];
}
// Page programmieren
NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_WP);
}
// ── CRC16 (CCITT, Poly 0x1021) ────────────────────────────────────────────────
uint16_t nvm_config_crc(const SDeviceConfig& cfg)
{
// CRC über alles nach dem crc-Feld (ab Byte 7)
const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, mx_actions);
uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions);
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= static_cast<uint16_t>(data[i]) << 8;
for (uint8_t b = 0; b < 8; b++) {
crc = (crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1;
}
}
return crc;
}
// ── Defaults ─────────────────────────────────────────────────────────────────
void nvm_config_defaults(SDeviceConfig& cfg)
{
memset(&cfg, 0, sizeof(cfg));
cfg.magic = NVM_CONFIG_MAGIC;
cfg.version = NVM_CONFIG_VERSION;
// Alle Aktionen: NONE
for (uint8_t i = 0; i < 20; i++)
cfg.mx_actions[i] = {ActionType::NONE, 0};
for (uint8_t e = 0; e < 4; e++)
for (uint8_t a = 0; a < 3; a++)
cfg.enc_actions[e][a] = {ActionType::NONE, 0};
// Base-LEDs: warm-weiß
for (uint8_t i = 0; i < 20; i++) {
cfg.led_r[i] = 80;
cfg.led_g[i] = 40;
cfg.led_b[i] = 0;
}
cfg.crc = nvm_config_crc(cfg);
}
// ── Laden ─────────────────────────────────────────────────────────────────────
bool nvm_config_load(SDeviceConfig& cfg)
{
memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg));
if (cfg.magic != NVM_CONFIG_MAGIC) { nvm_config_defaults(cfg); return false; }
if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; }
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; }
return true;
}
// ── Speichern ─────────────────────────────────────────────────────────────────
void nvm_config_save(const SDeviceConfig& cfg)
{
// Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist
uint8_t row[256];
memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg));
// Automatisches Schreiben deaktivieren (manueller Schreib-Modus)
NVMCTRL->CTRLB.bit.MANW = 1;
// Row 0 der Config löschen und seitenweise schreiben (4 × 64B)
nvm_erase_row(k_config_addr);
for (uint8_t p = 0; p < 4; p++) {
nvm_write_page(k_config_addr + p * 64, row + p * 64);
}
}
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <stdint.h>
#include "action.h"
// ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ────────────────────────────────
//
// Offset Size Inhalt
// 0 4 Magic (0x56503202 = 'VP2\x02')
// 4 1 Version
// 5 2 CRC16 über Bytes 7162
// 7 60 mx_actions[20] 20 × 3B (SAction packed)
// 67 36 enc_actions[4][3] 12 × 3B
// 103 20 led_r[20]
// 123 20 led_g[20]
// 143 20 led_b[20]
// 163 93 Padding bis 256 Bytes (erste Row voll)
// 256 256 Reserviert für zukünftige Erweiterungen (zweite Row)
//
// Gesamt genutzt: 163 Bytes (sizeof SDeviceConfig mit packed SAction)
#define NVM_CONFIG_MAGIC 0x56503202UL
#define NVM_CONFIG_VERSION 1
// Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[])
// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW
#define ENC_ACTION_SW 0
#define ENC_ACTION_CW 1
#define ENC_ACTION_CCW 2
struct __attribute__((packed)) SDeviceConfig
{
uint32_t magic;
uint8_t version;
uint16_t crc;
// Aktionen
SAction mx_actions[20]; // MX-Buttons 019 (key_id 524)
SAction enc_actions[4][3]; // [Encoder 03][SW/CW/CCW]
// Base-LED Farben
uint8_t led_r[20];
uint8_t led_g[20];
uint8_t led_b[20];
};
// Standardwerte wenn keine gültige Config im NVM
void nvm_config_defaults(SDeviceConfig& cfg);
// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC ungültig → Defaults geladen.
bool nvm_config_load(SDeviceConfig& cfg);
// Config in NVM schreiben (löscht 2 Rows, schreibt neu).
void nvm_config_save(const SDeviceConfig& cfg);
// CRC16 über die Nutzdaten der Config
uint16_t nvm_config_crc(const SDeviceConfig& cfg);
+68
View File
@@ -0,0 +1,68 @@
#pragma once
// VersaPad v2 Logical pin names
// All Arduino pin numbers reference the custom variant (variants/versapad/variant.h)
// ─── Button Matrix ────────────────────────────────────────────────────────────
// Layout (viewed from front):
//
// COL_0 COL_1 COL_2 COL_3 COL_4
// ROW_0 [ENC3] [ ] [ ] [ ] [ ]
// ROW_1 [ENC2] [ ] [ ] [ ] [ ]
// ROW_2 [ENC1] [ ] [ ] [ ] [ ]
// ROW_3 [ENC0] [ ] [ ] [ ] [ ]
// ROW_4 --- [ ] [ ] [ ] [ ]
//
// COL_0 × ROW_03 = encoder push buttons
// COL_14 × ROW_04 = 20 Cherry MX buttons
// COL_0 × ROW_4 = not connected
#define BTN_COL_COUNT 5
#define BTN_ROW_COUNT 5
// Column pins: driven OUTPUT LOW during scan, otherwise INPUT (high-Z or HIGH)
static const uint8_t BTN_COLS[BTN_COL_COUNT] = {
PIN_COL0, // PB10 encoder SW column
PIN_COL1, // PA11 Cherry MX col 1 (leftmost)
PIN_COL2, // PA10 Cherry MX col 2
PIN_COL3, // PA09 Cherry MX col 3
PIN_COL4, // PA08 Cherry MX col 4 (rightmost)
};
// Row pins: INPUT_PULLUP, read LOW when button pressed
static const uint8_t BTN_ROWS[BTN_ROW_COUNT] = {
PIN_ROW0, // PB11
PIN_ROW1, // PA12
PIN_ROW2, // PA13
PIN_ROW3, // PA14
PIN_ROW4, // PA15
};
// Button index helper: col * ROW_COUNT + row → 0..24
#define BTN_INDEX(col, row) ((col) * BTN_ROW_COUNT + (row))
// Encoder SW buttons are at column 0
#define BTN_ENC_SW(enc) BTN_INDEX(0, (enc)) // enc = 0..3
// ─── Rotary Encoders ──────────────────────────────────────────────────────────
// ENC0 = closest to USB connector, ENC3 = furthest
static const uint8_t ENC_A[4] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A };
static const uint8_t ENC_B[4] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B };
// ─── WS2812 LEDs ─────────────────────────────────────────────────────────────
#define LED_COUNT 20
#define LED_DATA_PIN PIN_SPI_MOSI // PB22, SERCOM5 PAD2
// LED index: serpentine (even rows L→R, odd rows R→L)
// Row 0: 0,1,2,3 Row 1: 7,6,5,4 Row 2: 8,9,10,11 Row 3: 15,14,13,12 Row 4: 16,17,18,19
#define LED_COLS (BTN_COL_COUNT - 1) // 4 Cherry MX columns
#define LED_INDEX(col, row) \
((row) * LED_COLS + (((row) & 1) ? (LED_COLS - (col)) : ((col) - 1)))
// ─── Faders (ADC) ─────────────────────────────────────────────────────────────
#define FADER_COUNT 3
static const uint8_t FADER_PINS[FADER_COUNT] = {
PIN_FADER0, // PA02 A0
PIN_FADER1, // PA03 A1 (also VREFA analog only, no digitalRead)
PIN_FADER2, // PB08 A2
};
+88
View File
@@ -0,0 +1,88 @@
#include "encoder.h"
#include <Arduino.h>
#include "config/pins.h"
// Quadratur-Dekodierung via 4-State-Lookup.
//
// Zustand = (A << 1) | B → 4 mögliche Zustände (00, 01, 10, 11)
// Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand
// bestimmt und mit dem vorherigen verglichen.
//
// Lookup-Tabelle [prev<<2 | curr] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen)
static const int8_t k_lut[16] = {
// curr: 00 01 10 11
0, +1, -1, 0, // prev = 00
-1, 0, 0, +1, // prev = 01
+1, 0, 0, -1, // prev = 10
0, -1, +1, 0, // prev = 11
};
// Pro Encoder: vorheriger Zustand + Akkumulator für Halb-Schritte.
// Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt
// auf ±4 bevor ein Event gefeuert wird (= ein Event pro Klick).
static volatile uint8_t s_state[ENCODER_COUNT];
static volatile int8_t s_accum[ENCODER_COUNT];
static encoder_cb_t s_cb = nullptr;
static const uint8_t k_pin_a[ENCODER_COUNT] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A };
static const uint8_t k_pin_b[ENCODER_COUNT] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B };
// Generischer Handler — wird von den 8 ISR-Wrappern unten aufgerufen.
static void handle_encoder(uint8_t enc)
{
uint8_t a = digitalRead(k_pin_a[enc]);
uint8_t b = digitalRead(k_pin_b[enc]);
uint8_t cur = (a << 1) | b;
uint8_t idx = (s_state[enc] << 2) | cur;
s_state[enc] = cur;
int8_t delta = k_lut[idx];
if (delta == 0) return;
s_accum[enc] += delta;
// 4 Halb-Schritte = 1 vollständige Raste
if (s_accum[enc] >= 4) {
s_accum[enc] = 0;
if (s_cb) s_cb(enc, +1);
} else if (s_accum[enc] <= -4) {
s_accum[enc] = 0;
if (s_cb) s_cb(enc, -1);
}
}
// 8 ISR-Wrapper je einer pro Pin (attachInterrupt braucht void-Funktionszeiger)
static void isr_enc0_a() { handle_encoder(0); }
static void isr_enc0_b() { handle_encoder(0); }
static void isr_enc1_a() { handle_encoder(1); }
static void isr_enc1_b() { handle_encoder(1); }
static void isr_enc2_a() { handle_encoder(2); }
static void isr_enc2_b() { handle_encoder(2); }
static void isr_enc3_a() { handle_encoder(3); }
static void isr_enc3_b() { handle_encoder(3); }
void encoder_init(encoder_cb_t cb)
{
s_cb = cb;
for (uint8_t i = 0; i < ENCODER_COUNT; i++) {
pinMode(k_pin_a[i], INPUT_PULLUP);
pinMode(k_pin_b[i], INPUT_PULLUP);
// Initialen Zustand lesen damit der erste Interrupt korrekt ausgewertet wird
uint8_t a = digitalRead(k_pin_a[i]);
uint8_t b = digitalRead(k_pin_b[i]);
s_state[i] = (a << 1) | b;
s_accum[i] = 0;
}
attachInterrupt(digitalPinToInterrupt(PIN_ENC0_A), isr_enc0_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC0_B), isr_enc0_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC1_A), isr_enc1_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC1_B), isr_enc1_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC2_A), isr_enc2_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC2_B), isr_enc2_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC3_A), isr_enc3_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC3_B), isr_enc3_b, CHANGE);
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#define ENCODER_COUNT 4
// Callback: enc = Encoder-Index (03), dir = +1 (CW) oder -1 (CCW)
typedef void (*encoder_cb_t)(uint8_t enc, int8_t dir);
void encoder_init(encoder_cb_t cb);
// Kein encoder_scan() rein interrupt-getrieben
+68
View File
@@ -0,0 +1,68 @@
#include "matrix.h"
#include <Arduino.h>
#include "config/pins.h"
#include <string.h>
// Hardware: COL lines have 10k pullups to 3V3 (always HIGH by default).
// Diodes between switch DO and ROW line (anode=switch, cathode=row).
// Scan: drive ROW LOW → pressed switch pulls COL LOW through diode.
#define DEBOUNCE_MS 10
static matrix_cb_t s_cb;
static bool s_raw[MATRIX_KEYS];
static bool s_debounced[MATRIX_KEYS];
static uint32_t s_changed_at[MATRIX_KEYS];
void matrix_init(matrix_cb_t cb)
{
s_cb = cb;
// COLs: INPUT external 10k pullup holds them HIGH
for (uint8_t c = 0; c < MATRIX_COLS; c++) {
pinMode(BTN_COLS[c], INPUT);
}
// ROWs: idle high-Z, driven LOW only during scan
for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
pinMode(BTN_ROWS[r], INPUT);
}
memset(s_raw, 0, sizeof(s_raw));
memset(s_debounced, 0, sizeof(s_debounced));
uint32_t now = millis();
for (uint8_t i = 0; i < MATRIX_KEYS; i++) {
s_changed_at[i] = now;
}
}
void matrix_scan()
{
uint32_t now = millis();
for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
// Drive this row LOW
pinMode(BTN_ROWS[r], OUTPUT);
digitalWrite(BTN_ROWS[r], LOW);
delayMicroseconds(10);
for (uint8_t c = 0; c < MATRIX_COLS; c++) {
uint8_t key = c * MATRIX_ROWS + r;
bool raw = (digitalRead(BTN_COLS[c]) == LOW);
if (raw != s_raw[key]) {
s_raw[key] = raw;
s_changed_at[key] = now;
}
if (raw != s_debounced[key] &&
(now - s_changed_at[key]) >= DEBOUNCE_MS) {
s_debounced[key] = raw;
if (s_cb) s_cb(key, raw);
}
}
// Release row back to high-Z
pinMode(BTN_ROWS[r], INPUT);
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <stdint.h>
// 5×5 button matrix
// COL_0 × ROW_03 = encoder SW buttons
// COL_14 × ROW_04 = 20 Cherry MX buttons
// COL_0 × ROW_4 = not connected
#define MATRIX_COLS 5
#define MATRIX_ROWS 5
#define MATRIX_KEYS 25 // col * MATRIX_ROWS + row
// Callback: key index (024), pressed = true / released = false
typedef void (*matrix_cb_t)(uint8_t key, bool pressed);
void matrix_init(matrix_cb_t cb);
void matrix_scan();
// Helper: key index from logical position
inline uint8_t matrix_key(uint8_t col, uint8_t row) {
return col * MATRIX_ROWS + row;
}
+96
View File
@@ -0,0 +1,96 @@
#include "usb_hid.h"
#include <Arduino.h>
#include <HID.h>
// ── HID Report Descriptor: Keyboard + Consumer Control ───────────────────────
// Vendor-Kommunikation läuft über CVendorHID (eigenes PluggableUSBModule).
static const uint8_t k_hid_descriptor[] = {
// ── Report ID 1: Keyboard ─────────────────────────────────────────────────
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, HID_REPORT_ID_KEYBOARD,
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xE0, // Usage Minimum (Left Ctrl)
0x29, 0xE7, // Usage Maximum (Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1 Bit)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8 Bit)
0x81, 0x01, // Input (Constant)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8 Bit)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array)
0xC0, // End Collection
// ── Report ID 2: Consumer Control ─────────────────────────────────────────
0x05, 0x0C, // Usage Page (Consumer Devices)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, HID_REPORT_ID_CONSUMER,
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x03, // Logical Maximum (1023)
0x19, 0x00, // Usage Minimum (0)
0x2A, 0xFF, 0x03, // Usage Maximum (1023)
0x75, 0x10, // Report Size (16 Bit)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data, Array)
0xC0, // End Collection
};
namespace {
struct HIDRegistrar {
HIDSubDescriptor node;
HIDRegistrar() : node(k_hid_descriptor, sizeof(k_hid_descriptor)) {
HID().AppendDescriptor(&node);
}
} s_hid_registrar;
}
struct KeyboardReport {
uint8_t modifier;
uint8_t reserved;
uint8_t keycodes[6];
};
struct ConsumerReport {
uint16_t usage;
};
void usb_hid_init() {}
void usb_hid_send_key(uint8_t keycode, uint8_t modifier)
{
KeyboardReport report = {};
report.modifier = modifier;
report.keycodes[0] = keycode;
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
}
void usb_hid_release_key()
{
KeyboardReport report = {};
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
}
void usb_hid_send_consumer(uint16_t usage)
{
ConsumerReport report = { usage };
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
}
void usb_hid_release_consumer()
{
ConsumerReport report = { 0 };
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
// ── Report-IDs (Keyboard/Consumer Interface) ──────────────────────────────────
#define HID_REPORT_ID_KEYBOARD 1
#define HID_REPORT_ID_CONSUMER 2
// ── Keyboard Modifier-Bits ────────────────────────────────────────────────────
#define KEY_MOD_LCTRL 0x01
#define KEY_MOD_LSHIFT 0x02
#define KEY_MOD_LALT 0x04
#define KEY_MOD_LGUI 0x08
#define KEY_MOD_RCTRL 0x10
#define KEY_MOD_RSHIFT 0x20
#define KEY_MOD_RALT 0x40
#define KEY_MOD_RGUI 0x80
// ── Consumer Control Usage IDs (HID Usage Table 1.3, Consumer Page 0x0C) ─────
#define CONSUMER_MUTE 0x00E2
#define CONSUMER_VOLUME_UP 0x00E9
#define CONSUMER_VOLUME_DOWN 0x00EA
#define CONSUMER_PLAY_PAUSE 0x00CD
#define CONSUMER_NEXT_TRACK 0x00B5
#define CONSUMER_PREV_TRACK 0x00B6
#define CONSUMER_STOP 0x00B7
#define CONSUMER_BRIGHTNESS_UP 0x006F
#define CONSUMER_BRIGHTNESS_DN 0x0070
void usb_hid_init();
void usb_hid_send_key(uint8_t keycode, uint8_t modifier = 0);
void usb_hid_release_key();
void usb_hid_send_consumer(uint16_t usage);
void usb_hid_release_consumer();
+56
View File
@@ -0,0 +1,56 @@
// usb_serial.cpp
// CDC Serial bidirektionale Kommunikation mit der Windows-App.
//
// Empfang (PC → Board):
// CDC kann Bytes in beliebig kleinen Happen liefern. usb_serial_poll() liest
// alle verfügbaren Bytes in einen internen Ring-Buffer und gibt ein vollständiges
// 8-Byte-Paket zurück sobald genug Bytes akkumuliert sind.
// Der Ring-Buffer (256 Bytes = 32 Pakete) verhindert Datenverlust wenn mehrere
// Pakete auf einmal ankommen (Config-Transfer: 30 Pakete).
//
// Senden (Board → PC):
// Direkt via SerialUSB.write() kein eigener Puffer nötig, da der Arduino-CDC-
// Stack intern puffert. Nur gesendet wenn SerialUSB verbunden ist (USB-Host da).
#include "usb_serial.h"
#include <Arduino.h>
// Ring-Buffer für eingehende Bytes CDC kann jederzeit Bytes liefern.
// Größe: 32 Pakete × 8 Bytes = 256 Bytes reicht für eine vollständige
// Config-Übertragung (30 Pakete) ohne Überlauf.
static uint8_t s_buf[SERIAL_PKT_SIZE * 32];
static uint16_t s_head = 0;
static uint16_t s_count = 0;
void usb_serial_init()
{
SerialUSB.begin(0); // CDC ignoriert Baudrate Wert egal
}
void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a, uint8_t b)
{
if (!SerialUSB) return; // Nicht verbunden
uint8_t pkt[SERIAL_PKT_SIZE] = { event_type, key_id, a, b, 0, 0, 0, 0 };
SerialUSB.write(pkt, SERIAL_PKT_SIZE);
}
bool usb_serial_poll(SerialPacket& out)
{
// Verfügbare Bytes in internen Buffer lesen
while (SerialUSB.available() && s_count < sizeof(s_buf)) {
s_buf[(s_head + s_count) % sizeof(s_buf)] = SerialUSB.read();
s_count++;
}
// Sobald ein vollständiges Paket da ist, ausgeben
if (s_count >= SERIAL_PKT_SIZE) {
for (uint8_t i = 0; i < SERIAL_PKT_SIZE; i++) {
out.data[i] = s_buf[(s_head + i) % sizeof(s_buf)];
}
s_head = (s_head + SERIAL_PKT_SIZE) % sizeof(s_buf);
s_count -= SERIAL_PKT_SIZE;
return true;
}
return false;
}
+70
View File
@@ -0,0 +1,70 @@
// usb_serial.h
// Bidirektionale Kommunikation zwischen Board und Windows-App über CDC Serial.
//
// Das Board erscheint als COM-Port unter Windows (kein Treiber nötig).
// Alle Pakete haben feste Größe (SERIAL_PKT_SIZE = 8 Bytes) kein
// Längen-Header nötig, vereinfacht Parsing auf beiden Seiten.
//
// Byte-Layout aller Pakete:
// [0] Command/Event-ID
// [1] key_id (Button 024 oder Encoder 03)
// [2] r / Daten-Byte A
// [3] g / Daten-Byte B
// [4] b
// [5..7] reserviert (0x00)
//
// Richtungen:
// PC → Board (Commands, 0x010x7F): poll_vendor() in CMainController
// Board → PC (Events, 0x810xFF): usb_serial_send() in processEvents()
#pragma once
#include <stdint.h>
#define SERIAL_PKT_SIZE 8
// ── Commands: PC → Board ──────────────────────────────────────────────────────
#define USB_CMD_SET_LED_OVERRIDE 0x01 // key_id, r, g, b → Override-LED setzen
#define USB_CMD_CLEAR_LED_OVERRIDE 0x02 // key_id → Override löschen, zurück zu base
#define USB_CMD_SET_LED_BASE 0x03 // key_id, r, g, b → Base-LED dauerhaft ändern
// Config-Übertragung (mehrteilig, 6 Nutzbytes pro Paket):
// BEGIN: Data[1] = Anzahl Chunks die folgen
// DATA: Data[1] = Chunk-Index (0-based), Data[2..7] = 6 Bytes Nutzdaten
// COMMIT: CRC prüfen + NVM schreiben + Buttons neu laden
#define USB_CMD_PING 0x05 // Board antwortet sofort mit USB_EVT_PONG
#define USB_CMD_CONFIG_BEGIN 0x10
#define USB_CMD_CONFIG_DATA 0x11
#define USB_CMD_CONFIG_COMMIT 0x12
#define USB_CMD_CONFIG_READ 0x13 // Board sendet aktuelle NVM-Config zurück
// ── Events: Board → PC ────────────────────────────────────────────────────────
#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt
#define USB_EVT_KEY_UP 0x82 // key_id → HOST_COMMAND-Button losgelassen
#define USB_EVT_ENC_CW 0x83 // enc_id → Encoder Schritt CW (HOST_COMMAND)
#define USB_EVT_ENC_CCW 0x84 // enc_id → Encoder Schritt CCW (HOST_COMMAND)
#define USB_EVT_PONG 0x85 // Antwort auf USB_CMD_PING
#define USB_EVT_CONFIG_ACK 0x90 // Config erfolgreich in NVM geschrieben
#define USB_EVT_CONFIG_NACK 0x91 // Config CRC/Magic ungültig nicht geschrieben
#define USB_EVT_CONFIG_BEGIN 0x92 // Beginn Config-Dump: Data[1] = Chunk-Anzahl
#define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen
// Paket-Struct mit Accessor-Methoden für lesbareren Code
struct SerialPacket
{
uint8_t data[SERIAL_PKT_SIZE];
uint8_t command() const { return data[0]; }
uint8_t key_id() const { return data[1]; }
uint8_t r() const { return data[2]; }
uint8_t g() const { return data[3]; }
uint8_t b() const { return data[4]; }
};
void usb_serial_init();
// Board → PC: 8-Byte-Event-Paket senden (nur wenn SerialUSB verbunden)
void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a = 0, uint8_t b = 0);
// PC → Board: nächstes vollständiges Paket abholen.
// Gibt true zurück wenn ein Paket verfügbar war.
bool usb_serial_poll(SerialPacket& out);
+35
View File
@@ -0,0 +1,35 @@
// WS2812 driver wraps Adafruit NeoPixel (bit-bang, no SERCOM needed)
#include "ws2812.h"
#include <Adafruit_NeoPixel.h>
static Adafruit_NeoPixel s_strip(WS2812_COUNT, WS2812_PIN, NEO_GRB + NEO_KHZ800);
void ws2812_init()
{
s_strip.begin();
s_strip.clear();
s_strip.show();
}
void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b)
{
if (idx >= WS2812_COUNT) return;
s_strip.setPixelColor(idx, r, g, b);
}
void ws2812_fill(uint8_t r, uint8_t g, uint8_t b)
{
s_strip.fill(s_strip.Color(r, g, b));
}
void ws2812_show()
{
s_strip.show();
}
void ws2812_clear()
{
s_strip.clear();
s_strip.show();
}
+30
View File
@@ -0,0 +1,30 @@
// ws2812.h
// Thin HAL-Wrapper um Adafruit NeoPixel (bit-bang, kein SERCOM).
//
// Dirty-Flag-Pattern:
// ws2812_set() schreibt nur in den internen NeoPixel-Buffer (RAM).
// ws2812_show() überträgt den gesamten Buffer an die LEDs (~600µs, blockierend
// via noInterrupts()). Nie aus einer ISR aufrufen!
// CButton::render_led() ruft nur ws2812_set() auf; ws2812_show() wird
// einmalig von CMainController::updateLEDs() aufgerufen wenn mindestens
// ein Button dirty war oder eine Animation läuft.
#pragma once
#include <stdint.h>
#define WS2812_COUNT 20
#define WS2812_PIN 18 // D18 = PB22 = LED_DATA_PIN
void ws2812_init();
// Einzelne LED im Buffer setzen (sofort, kein HW-Transfer)
void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b);
// Alle LEDs im Buffer auf dieselbe Farbe setzen (kein HW-Transfer)
void ws2812_fill(uint8_t r, uint8_t g, uint8_t b);
// Buffer an Hardware übertragen (~600µs, blockierend via noInterrupts())
void ws2812_show();
// Buffer löschen und sofort anzeigen (LEDs aus)
void ws2812_clear();
+27
View File
@@ -0,0 +1,27 @@
#include <Arduino.h>
#include "hal/ws2812.h"
#include "hal/usb_hid.h"
#include "CMainController.h"
static CMainController controller;
void setup()
{
delay(500); // USB-Enumeration abwarten vor erstem noInterrupts()-Aufruf
ws2812_init();
// Startup-Blink: bestätigt dass Firmware läuft
ws2812_fill(100, 0, 0);
ws2812_show();
delay(1000);
ws2812_clear();
ws2812_show();
controller.setup();
}
void loop()
{
controller.work();
}