Initial commit

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

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

@ -0,0 +1,88 @@
#include "encoder.h"
#include <Arduino.h>
#include "config/pins.h"
// Quadratur-Dekodierung via 4-State-Lookup.
//
// Zustand = (A << 1) | B → 4 mögliche Zustände (00, 01, 10, 11)
// Bei jedem Flankenwechsel (CHANGE) auf A oder B wird der neue Zustand
// bestimmt und mit dem vorherigen verglichen.
//
// Lookup-Tabelle [prev<<2 | curr] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen)
static const int8_t k_lut[16] = {
// curr: 00 01 10 11
0, +1, -1, 0, // prev = 00
-1, 0, 0, +1, // prev = 01
+1, 0, 0, -1, // prev = 10
0, -1, +1, 0, // prev = 11
};
// Pro Encoder: vorheriger Zustand + Akkumulator für Halb-Schritte.
// Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt
// auf ±4 bevor ein Event gefeuert wird (= ein Event pro Klick).
static volatile uint8_t s_state[ENCODER_COUNT];
static volatile int8_t s_accum[ENCODER_COUNT];
static encoder_cb_t s_cb = nullptr;
static const uint8_t k_pin_a[ENCODER_COUNT] = { PIN_ENC0_A, PIN_ENC1_A, PIN_ENC2_A, PIN_ENC3_A };
static const uint8_t k_pin_b[ENCODER_COUNT] = { PIN_ENC0_B, PIN_ENC1_B, PIN_ENC2_B, PIN_ENC3_B };
// Generischer Handler — wird von den 8 ISR-Wrappern unten aufgerufen.
static void handle_encoder(uint8_t enc)
{
uint8_t a = digitalRead(k_pin_a[enc]);
uint8_t b = digitalRead(k_pin_b[enc]);
uint8_t cur = (a << 1) | b;
uint8_t idx = (s_state[enc] << 2) | cur;
s_state[enc] = cur;
int8_t delta = k_lut[idx];
if (delta == 0) return;
s_accum[enc] += delta;
// 4 Halb-Schritte = 1 vollständige Raste
if (s_accum[enc] >= 4) {
s_accum[enc] = 0;
if (s_cb) s_cb(enc, +1);
} else if (s_accum[enc] <= -4) {
s_accum[enc] = 0;
if (s_cb) s_cb(enc, -1);
}
}
// 8 ISR-Wrapper je einer pro Pin (attachInterrupt braucht void-Funktionszeiger)
static void isr_enc0_a() { handle_encoder(0); }
static void isr_enc0_b() { handle_encoder(0); }
static void isr_enc1_a() { handle_encoder(1); }
static void isr_enc1_b() { handle_encoder(1); }
static void isr_enc2_a() { handle_encoder(2); }
static void isr_enc2_b() { handle_encoder(2); }
static void isr_enc3_a() { handle_encoder(3); }
static void isr_enc3_b() { handle_encoder(3); }
void encoder_init(encoder_cb_t cb)
{
s_cb = cb;
for (uint8_t i = 0; i < ENCODER_COUNT; i++) {
pinMode(k_pin_a[i], INPUT_PULLUP);
pinMode(k_pin_b[i], INPUT_PULLUP);
// Initialen Zustand lesen damit der erste Interrupt korrekt ausgewertet wird
uint8_t a = digitalRead(k_pin_a[i]);
uint8_t b = digitalRead(k_pin_b[i]);
s_state[i] = (a << 1) | b;
s_accum[i] = 0;
}
attachInterrupt(digitalPinToInterrupt(PIN_ENC0_A), isr_enc0_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC0_B), isr_enc0_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC1_A), isr_enc1_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC1_B), isr_enc1_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC2_A), isr_enc2_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC2_B), isr_enc2_b, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC3_A), isr_enc3_a, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC3_B), isr_enc3_b, CHANGE);
}

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

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#define ENCODER_COUNT 4
// Callback: enc = Encoder-Index (03), dir = +1 (CW) oder -1 (CCW)
typedef void (*encoder_cb_t)(uint8_t enc, int8_t dir);
void encoder_init(encoder_cb_t cb);
// Kein encoder_scan() rein interrupt-getrieben

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

@ -0,0 +1,68 @@
#include "matrix.h"
#include <Arduino.h>
#include "config/pins.h"
#include <string.h>
// Hardware: COL lines have 10k pullups to 3V3 (always HIGH by default).
// Diodes between switch DO and ROW line (anode=switch, cathode=row).
// Scan: drive ROW LOW → pressed switch pulls COL LOW through diode.
#define DEBOUNCE_MS 10
static matrix_cb_t s_cb;
static bool s_raw[MATRIX_KEYS];
static bool s_debounced[MATRIX_KEYS];
static uint32_t s_changed_at[MATRIX_KEYS];
void matrix_init(matrix_cb_t cb)
{
s_cb = cb;
// COLs: INPUT external 10k pullup holds them HIGH
for (uint8_t c = 0; c < MATRIX_COLS; c++) {
pinMode(BTN_COLS[c], INPUT);
}
// ROWs: idle high-Z, driven LOW only during scan
for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
pinMode(BTN_ROWS[r], INPUT);
}
memset(s_raw, 0, sizeof(s_raw));
memset(s_debounced, 0, sizeof(s_debounced));
uint32_t now = millis();
for (uint8_t i = 0; i < MATRIX_KEYS; i++) {
s_changed_at[i] = now;
}
}
void matrix_scan()
{
uint32_t now = millis();
for (uint8_t r = 0; r < MATRIX_ROWS; r++) {
// Drive this row LOW
pinMode(BTN_ROWS[r], OUTPUT);
digitalWrite(BTN_ROWS[r], LOW);
delayMicroseconds(10);
for (uint8_t c = 0; c < MATRIX_COLS; c++) {
uint8_t key = c * MATRIX_ROWS + r;
bool raw = (digitalRead(BTN_COLS[c]) == LOW);
if (raw != s_raw[key]) {
s_raw[key] = raw;
s_changed_at[key] = now;
}
if (raw != s_debounced[key] &&
(now - s_changed_at[key]) >= DEBOUNCE_MS) {
s_debounced[key] = raw;
if (s_cb) s_cb(key, raw);
}
}
// Release row back to high-Z
pinMode(BTN_ROWS[r], INPUT);
}
}

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

@ -0,0 +1,22 @@
#pragma once
#include <stdint.h>
// 5×5 button matrix
// COL_0 × ROW_03 = encoder SW buttons
// COL_14 × ROW_04 = 20 Cherry MX buttons
// COL_0 × ROW_4 = not connected
#define MATRIX_COLS 5
#define MATRIX_ROWS 5
#define MATRIX_KEYS 25 // col * MATRIX_ROWS + row
// Callback: key index (024), pressed = true / released = false
typedef void (*matrix_cb_t)(uint8_t key, bool pressed);
void matrix_init(matrix_cb_t cb);
void matrix_scan();
// Helper: key index from logical position
inline uint8_t matrix_key(uint8_t col, uint8_t row) {
return col * MATRIX_ROWS + row;
}

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

@ -0,0 +1,96 @@
#include "usb_hid.h"
#include <Arduino.h>
#include <HID.h>
// ── HID Report Descriptor: Keyboard + Consumer Control ───────────────────────
// Vendor-Kommunikation läuft über CVendorHID (eigenes PluggableUSBModule).
static const uint8_t k_hid_descriptor[] = {
// ── Report ID 1: Keyboard ─────────────────────────────────────────────────
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x85, HID_REPORT_ID_KEYBOARD,
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0xE0, // Usage Minimum (Left Ctrl)
0x29, 0xE7, // Usage Maximum (Right GUI)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x75, 0x01, // Report Size (1 Bit)
0x95, 0x08, // Report Count (8)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8 Bit)
0x81, 0x01, // Input (Constant)
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8 Bit)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array)
0xC0, // End Collection
// ── Report ID 2: Consumer Control ─────────────────────────────────────────
0x05, 0x0C, // Usage Page (Consumer Devices)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, HID_REPORT_ID_CONSUMER,
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x03, // Logical Maximum (1023)
0x19, 0x00, // Usage Minimum (0)
0x2A, 0xFF, 0x03, // Usage Maximum (1023)
0x75, 0x10, // Report Size (16 Bit)
0x95, 0x01, // Report Count (1)
0x81, 0x00, // Input (Data, Array)
0xC0, // End Collection
};
namespace {
struct HIDRegistrar {
HIDSubDescriptor node;
HIDRegistrar() : node(k_hid_descriptor, sizeof(k_hid_descriptor)) {
HID().AppendDescriptor(&node);
}
} s_hid_registrar;
}
struct KeyboardReport {
uint8_t modifier;
uint8_t reserved;
uint8_t keycodes[6];
};
struct ConsumerReport {
uint16_t usage;
};
void usb_hid_init() {}
void usb_hid_send_key(uint8_t keycode, uint8_t modifier)
{
KeyboardReport report = {};
report.modifier = modifier;
report.keycodes[0] = keycode;
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
}
void usb_hid_release_key()
{
KeyboardReport report = {};
HID().SendReport(HID_REPORT_ID_KEYBOARD, &report, sizeof(report));
}
void usb_hid_send_consumer(uint16_t usage)
{
ConsumerReport report = { usage };
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
}
void usb_hid_release_consumer()
{
ConsumerReport report = { 0 };
HID().SendReport(HID_REPORT_ID_CONSUMER, &report, sizeof(report));
}

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

@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
// ── Report-IDs (Keyboard/Consumer Interface) ──────────────────────────────────
#define HID_REPORT_ID_KEYBOARD 1
#define HID_REPORT_ID_CONSUMER 2
// ── Keyboard Modifier-Bits ────────────────────────────────────────────────────
#define KEY_MOD_LCTRL 0x01
#define KEY_MOD_LSHIFT 0x02
#define KEY_MOD_LALT 0x04
#define KEY_MOD_LGUI 0x08
#define KEY_MOD_RCTRL 0x10
#define KEY_MOD_RSHIFT 0x20
#define KEY_MOD_RALT 0x40
#define KEY_MOD_RGUI 0x80
// ── Consumer Control Usage IDs (HID Usage Table 1.3, Consumer Page 0x0C) ─────
#define CONSUMER_MUTE 0x00E2
#define CONSUMER_VOLUME_UP 0x00E9
#define CONSUMER_VOLUME_DOWN 0x00EA
#define CONSUMER_PLAY_PAUSE 0x00CD
#define CONSUMER_NEXT_TRACK 0x00B5
#define CONSUMER_PREV_TRACK 0x00B6
#define CONSUMER_STOP 0x00B7
#define CONSUMER_BRIGHTNESS_UP 0x006F
#define CONSUMER_BRIGHTNESS_DN 0x0070
void usb_hid_init();
void usb_hid_send_key(uint8_t keycode, uint8_t modifier = 0);
void usb_hid_release_key();
void usb_hid_send_consumer(uint16_t usage);
void usb_hid_release_consumer();

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

@ -0,0 +1,56 @@
// usb_serial.cpp
// CDC Serial bidirektionale Kommunikation mit der Windows-App.
//
// Empfang (PC → Board):
// CDC kann Bytes in beliebig kleinen Happen liefern. usb_serial_poll() liest
// alle verfügbaren Bytes in einen internen Ring-Buffer und gibt ein vollständiges
// 8-Byte-Paket zurück sobald genug Bytes akkumuliert sind.
// Der Ring-Buffer (256 Bytes = 32 Pakete) verhindert Datenverlust wenn mehrere
// Pakete auf einmal ankommen (Config-Transfer: 30 Pakete).
//
// Senden (Board → PC):
// Direkt via SerialUSB.write() kein eigener Puffer nötig, da der Arduino-CDC-
// Stack intern puffert. Nur gesendet wenn SerialUSB verbunden ist (USB-Host da).
#include "usb_serial.h"
#include <Arduino.h>
// Ring-Buffer für eingehende Bytes CDC kann jederzeit Bytes liefern.
// Größe: 32 Pakete × 8 Bytes = 256 Bytes reicht für eine vollständige
// Config-Übertragung (30 Pakete) ohne Überlauf.
static uint8_t s_buf[SERIAL_PKT_SIZE * 32];
static uint16_t s_head = 0;
static uint16_t s_count = 0;
void usb_serial_init()
{
SerialUSB.begin(0); // CDC ignoriert Baudrate Wert egal
}
void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a, uint8_t b)
{
if (!SerialUSB) return; // Nicht verbunden
uint8_t pkt[SERIAL_PKT_SIZE] = { event_type, key_id, a, b, 0, 0, 0, 0 };
SerialUSB.write(pkt, SERIAL_PKT_SIZE);
}
bool usb_serial_poll(SerialPacket& out)
{
// Verfügbare Bytes in internen Buffer lesen
while (SerialUSB.available() && s_count < sizeof(s_buf)) {
s_buf[(s_head + s_count) % sizeof(s_buf)] = SerialUSB.read();
s_count++;
}
// Sobald ein vollständiges Paket da ist, ausgeben
if (s_count >= SERIAL_PKT_SIZE) {
for (uint8_t i = 0; i < SERIAL_PKT_SIZE; i++) {
out.data[i] = s_buf[(s_head + i) % sizeof(s_buf)];
}
s_head = (s_head + SERIAL_PKT_SIZE) % sizeof(s_buf);
s_count -= SERIAL_PKT_SIZE;
return true;
}
return false;
}

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

@ -0,0 +1,70 @@
// usb_serial.h
// Bidirektionale Kommunikation zwischen Board und Windows-App über CDC Serial.
//
// Das Board erscheint als COM-Port unter Windows (kein Treiber nötig).
// Alle Pakete haben feste Größe (SERIAL_PKT_SIZE = 8 Bytes) kein
// Längen-Header nötig, vereinfacht Parsing auf beiden Seiten.
//
// Byte-Layout aller Pakete:
// [0] Command/Event-ID
// [1] key_id (Button 024 oder Encoder 03)
// [2] r / Daten-Byte A
// [3] g / Daten-Byte B
// [4] b
// [5..7] reserviert (0x00)
//
// Richtungen:
// PC → Board (Commands, 0x010x7F): poll_vendor() in CMainController
// Board → PC (Events, 0x810xFF): usb_serial_send() in processEvents()
#pragma once
#include <stdint.h>
#define SERIAL_PKT_SIZE 8
// ── Commands: PC → Board ──────────────────────────────────────────────────────
#define USB_CMD_SET_LED_OVERRIDE 0x01 // key_id, r, g, b → Override-LED setzen
#define USB_CMD_CLEAR_LED_OVERRIDE 0x02 // key_id → Override löschen, zurück zu base
#define USB_CMD_SET_LED_BASE 0x03 // key_id, r, g, b → Base-LED dauerhaft ändern
// Config-Übertragung (mehrteilig, 6 Nutzbytes pro Paket):
// BEGIN: Data[1] = Anzahl Chunks die folgen
// DATA: Data[1] = Chunk-Index (0-based), Data[2..7] = 6 Bytes Nutzdaten
// COMMIT: CRC prüfen + NVM schreiben + Buttons neu laden
#define USB_CMD_PING 0x05 // Board antwortet sofort mit USB_EVT_PONG
#define USB_CMD_CONFIG_BEGIN 0x10
#define USB_CMD_CONFIG_DATA 0x11
#define USB_CMD_CONFIG_COMMIT 0x12
#define USB_CMD_CONFIG_READ 0x13 // Board sendet aktuelle NVM-Config zurück
// ── Events: Board → PC ────────────────────────────────────────────────────────
#define USB_EVT_KEY_DOWN 0x81 // key_id → HOST_COMMAND-Button gedrückt
#define USB_EVT_KEY_UP 0x82 // key_id → HOST_COMMAND-Button losgelassen
#define USB_EVT_ENC_CW 0x83 // enc_id → Encoder Schritt CW (HOST_COMMAND)
#define USB_EVT_ENC_CCW 0x84 // enc_id → Encoder Schritt CCW (HOST_COMMAND)
#define USB_EVT_PONG 0x85 // Antwort auf USB_CMD_PING
#define USB_EVT_CONFIG_ACK 0x90 // Config erfolgreich in NVM geschrieben
#define USB_EVT_CONFIG_NACK 0x91 // Config CRC/Magic ungültig nicht geschrieben
#define USB_EVT_CONFIG_BEGIN 0x92 // Beginn Config-Dump: Data[1] = Chunk-Anzahl
#define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen
// Paket-Struct mit Accessor-Methoden für lesbareren Code
struct SerialPacket
{
uint8_t data[SERIAL_PKT_SIZE];
uint8_t command() const { return data[0]; }
uint8_t key_id() const { return data[1]; }
uint8_t r() const { return data[2]; }
uint8_t g() const { return data[3]; }
uint8_t b() const { return data[4]; }
};
void usb_serial_init();
// Board → PC: 8-Byte-Event-Paket senden (nur wenn SerialUSB verbunden)
void usb_serial_send(uint8_t event_type, uint8_t key_id, uint8_t a = 0, uint8_t b = 0);
// PC → Board: nächstes vollständiges Paket abholen.
// Gibt true zurück wenn ein Paket verfügbar war.
bool usb_serial_poll(SerialPacket& out);

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

@ -0,0 +1,35 @@
// WS2812 driver wraps Adafruit NeoPixel (bit-bang, no SERCOM needed)
#include "ws2812.h"
#include <Adafruit_NeoPixel.h>
static Adafruit_NeoPixel s_strip(WS2812_COUNT, WS2812_PIN, NEO_GRB + NEO_KHZ800);
void ws2812_init()
{
s_strip.begin();
s_strip.clear();
s_strip.show();
}
void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b)
{
if (idx >= WS2812_COUNT) return;
s_strip.setPixelColor(idx, r, g, b);
}
void ws2812_fill(uint8_t r, uint8_t g, uint8_t b)
{
s_strip.fill(s_strip.Color(r, g, b));
}
void ws2812_show()
{
s_strip.show();
}
void ws2812_clear()
{
s_strip.clear();
s_strip.show();
}

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

@ -0,0 +1,30 @@
// ws2812.h
// Thin HAL-Wrapper um Adafruit NeoPixel (bit-bang, kein SERCOM).
//
// Dirty-Flag-Pattern:
// ws2812_set() schreibt nur in den internen NeoPixel-Buffer (RAM).
// ws2812_show() überträgt den gesamten Buffer an die LEDs (~600µs, blockierend
// via noInterrupts()). Nie aus einer ISR aufrufen!
// CButton::render_led() ruft nur ws2812_set() auf; ws2812_show() wird
// einmalig von CMainController::updateLEDs() aufgerufen wenn mindestens
// ein Button dirty war oder eine Animation läuft.
#pragma once
#include <stdint.h>
#define WS2812_COUNT 20
#define WS2812_PIN 18 // D18 = PB22 = LED_DATA_PIN
void ws2812_init();
// Einzelne LED im Buffer setzen (sofort, kein HW-Transfer)
void ws2812_set(uint8_t idx, uint8_t r, uint8_t g, uint8_t b);
// Alle LEDs im Buffer auf dieselbe Farbe setzen (kein HW-Transfer)
void ws2812_fill(uint8_t r, uint8_t g, uint8_t b);
// Buffer an Hardware übertragen (~600µs, blockierend via noInterrupts())
void ws2812_show();
// Buffer löschen und sofort anzeigen (LEDs aus)
void ws2812_clear();