diff --git a/doc/00_architecture.md b/doc/00_architecture.md new file mode 100644 index 0000000..a41ba84 --- /dev/null +++ b/doc/00_architecture.md @@ -0,0 +1,83 @@ +# VersaMCU – Architektur-Übersicht + +## Ziel-Hardware + +| Merkmal | Wert | +|---|---| +| MCU | ATSAMD21G17D (Cortex-M0+, 48 MHz) | +| Flash | 128 KB (davon 512 B am Ende für NVM-Config reserviert) | +| RAM | 16 KB | +| FPU | Keine – alle Berechnungen in Integer-Arithmetik | +| USB | Native USB, DFLL48M via USB-SOF-Kalibrierung (`-DCRYSTALLESS`) | +| Framework | Arduino + PlatformIO, kein Bootloader (Direktflash via SWD/Atmel-ICE) | + +## Loop-Ablauf + +``` +setup() + ├── macro_config_load() – Makro-Tabelle aus NVM in RAM laden + ├── init_buttons() – CButton-Objekte aus NVM initialisieren + ├── usb_hid_init() – HID-Descriptor (No-Op, läuft via global ctor) + ├── usb_serial_init() – CDC Serial öffnen + ├── matrix_init(cb) – 5×5-Matrix + Debounce-Zustand + └── encoder_init(cb) – EIC-Interrupts für 4 Encoder + +loop() [~20 ms Iteration] + ├── matrix_scan() – Debounce-Zustand prüfen → Events in Queue + ├── poll_vendor() – CDC-Pakete vom PC verarbeiten (LED-Cmds, Config, Makros) + ├── processEvents() – Queue leeren: Aktionen ausführen, HOST_COMMAND melden + └── updateLEDs() – Dirty-CButtons → WS2812-Buffer → show() (nur wenn dirty) +``` + +Encoder-ISRs laufen asynchron (CHANGE-Interrupt auf A und B) und schreiben direkt in die Event-Queue. Die Queue ist interrupt-sicher (keine Locks nötig auf Single-Core-M0+). + +## Datenfluss + +``` +HAL-Callbacks (matrix_cb, encoder_cb) + └─► CEventQueue (16 Slots, Ring-Buffer, kein Heap) + └─► processEvents() + ├─► CButton.on_press() / on_release() [Hooks, aktuell leer] + ├─► execute_action() → USB HID / Makro-Ablauf + └─► usb_serial_send() → HOST_COMMAND-Events an PC + +SerialUSB (CDC, PC → Board) + └─► poll_vendor() + ├─► CButton.set_override() / clear_override() / set_base() + └─► Config/Makro-Transfer (chunked, 6 B/Paket) +``` + +## Komponenten-Übersicht + +| Datei | Verantwortung | +|---|---| +| `main.cpp` | `setup()` / `loop()` – ruft nur CMainController auf | +| `CMainController` | Zentraler Orchestrator, hält alle CButton-Instanzen | +| `CButton` | LED-Layering, Animations-Engine, Action-Referenz | +| `CEventQueue` | ISR-sicherer Ring-Buffer, 16 Events | +| `hal/matrix` | 5×5-Matrix-Scan, 10 ms Debounce | +| `hal/encoder` | Quadratur-Dekodierung via EIC-ISR | +| `hal/ws2812` | Thin Wrapper um Adafruit NeoPixel (bit-bang) | +| `hal/usb_hid` | HID Keyboard + Consumer Control | +| `hal/usb_serial` | CDC bidirektional, 8-Byte-Pakete, Ring-Buffer | +| `config/nvm_config` | SDeviceConfig: laden, speichern, CRC16, Defaults | +| `config/macro_config` | SMacroTable: laden, speichern (NVM Row 1) | +| `config/action` | SAction-Struct + ActionType-Enum | + +## Key-ID-Schema + +``` +key_id 0–3 : Encoder-SW-Buttons (COL_0 × ROW_0–3), kein LED +key_id 4 : nicht belegt (COL_0 × ROW_4) +key_id 5–24 : MX-Buttons (COL_1–4 × ROW_0–4), je ein WS2812-LED +``` + +LED-Index folgt serpentiner Verdrahtung: `LED_INDEX(col, row)`. + +## Invarianten / Constraints + +- **Kein Heap**: kein `new`/`malloc` – alle Objekte statisch oder als Felder in CMainController. +- **Kein Float**: Cortex-M0+ hat keine FPU; Float würde per Software emuliert (~10–20× langsamer). +- **Packed Structs**: `SAction` und `SDeviceConfig` sind `__attribute__((packed))` damit die Byte-Größen mit der C#-Serialisierung in VersaGUI übereinstimmen. +- **Aligned NVM-Writes**: `nvm_write_page` castet Pointer zu `uint32_t*`; Puffer müssen vor dem Aufruf in `__attribute__((aligned(4)))`-Variablen kopiert werden (sonst HardFault auf M0+). +- **DTR-Check**: `usb_serial_send()` prüft ob SerialUSB aktiv ist, bevor Bytes gesendet werden. diff --git a/doc/01_matrix.md b/doc/01_matrix.md new file mode 100644 index 0000000..c58398e --- /dev/null +++ b/doc/01_matrix.md @@ -0,0 +1,45 @@ +# Tasten-Matrix + +**Dateien:** `hal/matrix.h`, `hal/matrix.cpp`, `config/pins.h` + +## Hardware + +5×5-Matrix mit externer 10-kΩ-Pullup-Beschaltung auf den COL-Leitungen (immer HIGH im Ruhezustand). Dioden zwischen Schalter-DO und ROW-Leitung (Anode = Schalter, Kathode = ROW) verhindern Geistertasten bei Mehrfachdrücken. + +Scan-Prinzip: ROW LOW treiben → gedrückter Schalter zieht zugehörige COL durch Diode auf LOW. + +## Scan-Logik + +``` +für jede ROW r: + ROW r → OUTPUT LOW + warte 10 µs (Einschwingen) + für jede COL c: + raw = (digitalRead(COL[c]) == LOW) + Debounce prüfen + ROW r → INPUT (hochohmig, Pullup-Freigabe) +``` + +ROW-Pins wechseln zwischen OUTPUT-LOW (während Scan) und INPUT (hochohmig) – kein dauerhaftes LOW. + +## Debounce + +- **10 ms**, Software-seitig pro Taste +- Flanken-Erkennung: Zustandsänderung (raw) wird mit Timestamp notiert +- Erst nach 10 ms stabiler neuer Zustand wird `s_debounced` aktualisiert und der Callback aufgerufen +- Callback: `matrix_cb(key_id, pressed)` → `CEventQueue::push(KEY_DOWN / KEY_UP)` + +## Key-ID-Berechnung + +```cpp +key_id = col * MATRIX_ROWS + row +// col 0–4, row 0–4 +// key_id 0–4: Encoder-SW (COL_0) +// key_id 5–24: MX-Buttons (COL_1–4) +``` + +## Kontext + +- Läuft im Loop-Kontext (kein ISR) +- Encoder-SW-Tasten gehen durch denselben Matrix-Pfad (COL_0) +- `matrix_scan()` wird einmal pro `loop()` aufgerufen diff --git a/doc/02_encoder.md b/doc/02_encoder.md new file mode 100644 index 0000000..0507cb5 --- /dev/null +++ b/doc/02_encoder.md @@ -0,0 +1,44 @@ +# Quadratur-Encoder + +**Dateien:** `hal/encoder.h`, `hal/encoder.cpp`, `config/pins.h` + +## Hardware + +4 mechanische Quadratur-Encoder mit je 2 Phasen-Pins (A, B). Pins sind INPUT_PULLUP. +Encoder-SW-Tasten laufen **nicht** durch diesen HAL, sondern durch den Matrix-Scan (COL_0). + +## Dekodierung + +4-State-Lookup-Table über `(prev_state << 2) | cur_state`: + +``` +Zustand = (A << 1) | B → 4 Bits: 00 / 01 / 10 / 11 +LUT[prev<<2 | cur] → +1 (CW), -1 (CCW), 0 (ungültig/Prellen) +``` + +Mechanische Encoder erzeugen 4 Flanken pro Raste → Akkumulator zählt Halbschritte. +Ein Event wird erst gefeuert wenn `|accum| >= 4` (= ein vollständiger Klick). + +## ISR-Aufbau + +8 ISR-Wrapper (je einer pro Pin, da `attachInterrupt` keinen Parameter unterstützt): + +```cpp +static void isr_enc0_a() { handle_encoder(0); } +static void isr_enc0_b() { handle_encoder(0); } +// ... analog für Encoder 1–3 +``` + +`attachInterrupt(..., CHANGE)` auf beiden Pins jedes Encoders. + +`handle_encoder()` → `encoder_cb(enc_id, direction)` → `CEventQueue::push(ENC_CW / ENC_CCW)` + +## ISR-Sicherheit + +- `s_state[]` und `s_accum[]` sind `volatile` +- `CEventQueue::push()` ist ISR-sicher (atomare Index-Inkremente auf Single-Core-M0+, kein Heap) +- Der Callback-Pointer `s_cb` wird einmalig in `setup()` gesetzt, bevor Interrupts aktiviert werden + +## Initialisierung + +Initialer Zustand von A/B wird beim `encoder_init()` gelesen damit der erste Interrupt korrekt ausgewertet wird (kein "Phantom-Step" beim Einschalten). diff --git a/doc/03_action_engine.md b/doc/03_action_engine.md new file mode 100644 index 0000000..44944b5 --- /dev/null +++ b/doc/03_action_engine.md @@ -0,0 +1,70 @@ +# Aktions-Engine + +**Dateien:** `config/action.h`, `CMainController.cpp` (`processEvents`, `execute_action`) + +## SAction-Struct + +```cpp +struct __attribute__((packed)) SAction { + ActionType type; // 1 Byte + uint16_t data; // 2 Bytes (Keycode, Consumer-Code, Command-ID oder Slot-Index) +}; +// Gesamt: 3 Bytes (packed! ohne packed wären es 4 durch Alignment) +``` + +`packed` ist zwingend damit `sizeof(SDeviceConfig) == 223` mit der C#-Serialisierung in VersaGUI übereinstimmt. + +## ActionType + +| Typ | Bedeutung | data-Inhalt | +|---|---|---| +| `NONE` | Keine Aktion | — | +| `HID_KEY` | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier | +| `HID_CONSUMER` | Consumer Control (Volume, Media, …) | Consumer Usage ID | +| `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | +| `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 0–31 | + +## Ausführung (execute_action) + +**HID_KEY:** +``` +usb_hid_send_key(keycode, modifier) +delay(10 ms) +usb_hid_release_key() +``` +→ Tap-Only-Modell. Kein Hold-Support. KEY_UP löst kein HID-Release aus. + +**HID_CONSUMER:** +``` +usb_hid_send_consumer(usage_id) +usb_hid_release_consumer() +``` +→ Kein Delay nötig (Consumer-Keys sind Edge-getriggert). + +**HOST_COMMAND:** +`execute_action()` macht nichts. `processEvents()` sendet zusätzlich `USB_EVT_KEY_DOWN` via CDC Serial an VersaGUI. Die App entscheidet was passiert (URL öffnen, Programm starten, …). + +**MACRO:** +``` +für jeden Step [0–3] im Slot: + if keycode == 0: abbrechen + usb_hid_send_key(keycode, modifier) + delay(10 ms) + usb_hid_release_key() + delay(20 ms) // Pause damit der Host den Step verarbeiten kann +``` + +## Event-Verarbeitung + +Events werden in `processEvents()` aus `CEventQueue` konsumiert (FIFO): + +- `KEY_DOWN` → `CButton.on_press()` (aktuell leer, Erweiterungspunkt) + `execute_action()` +- `KEY_UP` → `CButton.on_release()` (aktuell leer) +- `ENC_CW/CCW` → `execute_action(m_enc_cw/ccw[enc_id])` + +Encoder CW/CCW-Aktionen sind in `CMainController` direkt als `SAction`-Arrays gehalten (kein CButton-Objekt, da Encoder keine LED haben). + +## Bekannte Einschränkungen + +- **Kein Hold**: `execute_action` bei KEY_DOWN sendet sofort Key-Down + Key-Up. Halten der Taste löst keine Wiederholung aus. +- **HOST_COMMAND KEY_UP**: Board sendet derzeit kein `USB_EVT_KEY_UP` für KEY_UP-Events (nur KEY_DOWN wird gemeldet). diff --git a/doc/04_macro_system.md b/doc/04_macro_system.md new file mode 100644 index 0000000..9ba20fe --- /dev/null +++ b/doc/04_macro_system.md @@ -0,0 +1,59 @@ +# Makro-System + +**Dateien:** `config/macro_config.h`, `config/macro_config.cpp`, `CMainController.cpp` + +## Datenstruktur + +```cpp +struct __attribute__((packed)) SMacroStep { + uint8_t keycode; // HID Keyboard Usage (0x00 = leer → Step überspringen) + uint8_t modifier; // HID Modifier: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08 +}; + +struct __attribute__((packed)) SMacroTable { + SMacroStep steps[32][4]; // 32 Slots × 4 Steps × 2 Byte = 256 Byte +}; +``` + +Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine NVM-Row`. + +## NVM-Speicherort + +- **Row 1**: Adresse `0x1FF00`, 256 Byte +- Vom Linkerscript reserviert (nicht überschreibbar durch Code) +- Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt false zurück → leere Tabelle (alle Keycodes 0) + +## Slot-Zuweisung (Konvention, Board speichert blind) + +| Slots | Verwendung | +|---|---| +| 0–19 | MX-Button `mx_idx` (entspricht key_id − 5) | +| 20–31 | Encoder-Aktionen (`enc * 3 + act_idx`, 0=SW / 1=CW / 2=CCW) | + +## Laden und Speichern + +**Laden** (`macro_config_load`): +- `memcpy` direkt aus Flash-Adresse in RAM-Struct +- Kein Magic/CRC (leere Tabelle bei 0xFF ist akzeptabler Zustand) + +**Speichern** (`macro_config_save`): +- SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!) +- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) +- Row 1 löschen (`nvm_erase_row`) +- 4 Pages à 64 Byte schreiben (`nvm_write_page`) + +> **Warum aligned_buf?** `nvm_write_page` castet den Pointer zu `volatile uint32_t*`. Wenn `&tbl` nicht 4-Byte-aligned ist (möglich bei packed struct), entsteht ein HardFault auf Cortex-M0+ (kein unaligned 32-Bit-Zugriff auf Peripherie-Adressen). + +## Ausführung (in execute_action, ActionType::MACRO) + +``` +slot = action.data (0–31) +für Step 0–3: + if step.keycode == 0: abbrechen + HID Key-Down (keycode, modifier) + delay(10 ms) + HID Key-Up + delay(20 ms) +``` + +Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung. diff --git a/doc/05_led_system.md b/doc/05_led_system.md new file mode 100644 index 0000000..662d5d8 --- /dev/null +++ b/doc/05_led_system.md @@ -0,0 +1,88 @@ +# LED-System (WS2812) + +**Dateien:** `hal/ws2812.h`, `hal/ws2812.cpp`, `CButton.h`, `CButton.cpp` + +## Hardware-Treiber (ws2812 HAL) + +Dünner Wrapper um **Adafruit NeoPixel** (bit-bang, kein DMA, kein SERCOM). + +| Funktion | Bedeutung | +|---|---| +| `ws2812_init()` | `begin()` + `clear()` + `show()` | +| `ws2812_set(idx, r, g, b)` | `setPixelColor()` – schreibt in RAM-Puffer | +| `ws2812_fill(r, g, b)` | Alle LEDs gleiche Farbe | +| `ws2812_show()` | Bit-Bang-Übertragung (~600 µs, Interrupts gesperrt) | +| `ws2812_clear()` | `clear()` + `show()` | + +`ws2812_show()` wird in `CMainController::updateLEDs()` **nur** aufgerufen wenn mindestens ein Button dirty war – 600 µs Blockzeit werden so vermieden wenn keine Änderung nötig ist. + +**Warum kein DMA?** DMA + SERCOM-SPI würde ~1,5 KB extra RAM (1440 Byte Kodier-Puffer) und erhebliche Implementierungskomplexität erfordern. Bei 20 LEDs und ~20 ms Loop-Rate sind 600 µs gesperrte Interrupts (= 3 % der Loop-Zeit) unkritisch. + +## 2-Schicht-Modell (CButton) + +Jeder MX-Button hat zwei LED-Schichten: + +``` +override (aktiv wenn set_override() aufgerufen) ← temporär (GUI-Benachrichtigungen) +base (Idle-Farbe aus NVM) ← dauerhaft +``` + +Aktive Farbe = `override` wenn aktiv, sonst `base`. `clear_override()` kehrt sofort zu `base` zurück, ohne base zu verändern. + +## Animationen + +| Animation | Typ | Verhalten | Endbedingung | +|---|---|---|---| +| `STATIC` | — | Feste Farbe | — | +| `BLINK` | Helligkeit | An/Aus, `period_ms` = Halbperiode | endlos | +| `PULSE` | Helligkeit | Lineares Dreieck 0→255→0 | endlos | +| `FADE_IN` | Helligkeit | Einmalig schwarz → voll | → STATIC (voll) | +| `FADE_OUT` | Helligkeit | Einmalig voll → schwarz | → STATIC (base=schwarz) | +| `COLOR_CYCLE` | Farbe | Hue-Sweep, ignoriert base/override | endlos | +| `COLOR_FADE` | Farbe | Crossfade from→to | → STATIC (base=to) | + +**Helligkeits-Animationen** (`compute_scale`): Multiplizieren die aktive Farbe mit einem Skalierungsfaktor 0–255. Formel: `(channel * scale) / 255`. + +**Farb-Animationen** (`compute_rgb`): Berechnen RGB direkt; base/override werden nicht verändert (außer bei Abschluss). + +### COLOR_CYCLE – Hue-Arithmetik (kein Float) + +Hue 0–255 aufgeteilt in 6 Segmente à 43 Einheiten. Innerhalb jedes Segments steigt/fällt ein Kanal linear: + +``` +Seg 0: R=255, G steigt (Rot → Gelb) +Seg 1: R fällt, G=255 (Gelb → Grün) +Seg 2: G=255, B steigt (Grün → Cyan) +Seg 3: G fällt, B=255 (Cyan → Blau) +Seg 4: B=255, R steigt (Blau → Magenta) +Seg 5: R=255, B fällt (Magenta → Rot) +``` + +Ausgabe wird auf 40 % Helligkeit skaliert (Faktor 102/255) damit die LEDs nicht blenden. + +`Adafruit_NeoPixel::ColorHSV()` ist nicht nutzbar: verwendet intern float (kein FPU auf M0+). + +### Phasenversatz (Regenbogen-Wellen) + +`set_anim(COLOR_CYCLE, period, phase_offset_ms)`: `m_anim_start_ms = millis() - phase_offset_ms`. Die 20 MX-Buttons werden in `init_buttons()` gleichmäßig phasenverschoben initialisiert: + +```cpp +uint16_t phase = (mx_idx * period) / 20; +``` + +## Render-Pipeline + +``` +CMainController::updateLEDs() + für jeden CButton: + CButton::render_led() + if !dirty && !is_animating(): return false + rgb = compute_rgb() + ws2812_set(led_index, rgb.r, rgb.g, rgb.b) + dirty = false + return true + if any returned true: + ws2812_show() +``` + +`dirty` wird gesetzt bei: `init()`, `set_base()`, `set_override()`, `clear_override()`, `set_anim()`, `clear_anim()`, Animations-Abschluss. diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md new file mode 100644 index 0000000..d7c626e --- /dev/null +++ b/doc/06_nvm_config.md @@ -0,0 +1,68 @@ +# NVM-Konfiguration + +**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` + +## Flash-Layout + +| Row | Adresse | Größe | Inhalt | +|---|---|---|---| +| Row 0 | `0x1FE00` | 256 B | SDeviceConfig (223 B genutzt, 33 B Padding) | +| Row 1 | `0x1FF00` | 256 B | SMacroTable (256 B, komplett genutzt) | + +Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. + +## SDeviceConfig – Byte-Layout (223 Byte, packed) + +| Offset | Größe | Feld | +|---|---|---| +| 0 | 4 | `magic` = `0x56503202` ('VP2\x02') | +| 4 | 1 | `version` = 2 | +| 5 | 2 | `crc` – CRC16-CCITT über Bytes 7–222 | +| 7 | 60 | `mx_actions[20]` – 20 × 3 B SAction | +| 67 | 36 | `enc_actions[4][3]` – 12 × 3 B SAction | +| 103 | 20 | `led_r[20]` | +| 123 | 20 | `led_g[20]` | +| 143 | 20 | `led_b[20]` | +| 163 | 20 | `led_anim[20]` – LEDAnim-Typ als uint8_t | +| 183 | 40 | `led_period_ms[20]` – uint16_t, little-endian | +| **223** | — | Ende des genutzten Bereichs | + +`__attribute__((packed))` ist zwingend. Ohne packed wäre SAction 4 B statt 3 B (Alignment-Padding), was `sizeof(SDeviceConfig)` um 32 B vergrößert und die C#-Deserialisierung in VersaGUI zerstört. + +## CRC16-CCITT + +- Polynom: `0x1021`, Init: `0xFFFF` +- Berechnet über Bytes 7–222 (ab `mx_actions`, nach dem `crc`-Feld selbst) +- Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel + +## Lese-Logik + +``` +memcpy aus Flash-Adresse 0x1FE00 +if magic != 0x56503202: Defaults laden, return false +if version != 2: Defaults laden, return false +if crc != crc(cfg): Defaults laden, return false +return true +``` + +Kein Absturz bei ungültiger Config – Defaults greifen immer. + +## Defaults + +- Alle Aktionen: `NONE` +- LEDs: warm-weiß (R=80, G=40, B=0) +- Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms + +## Schreib-Logik (NVM-Mechanik) + +SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Schreiben erfordert: +1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus, kein Auto-Write) +2. Row löschen (`NVMCTRL_CTRLA_CMD_ER`) +3. Page-Buffer löschen (`NVMCTRL_CTRLA_CMD_PBC`) +4. 64 B als `uint32_t*` in Page-Buffer schreiben +5. Page programmieren (`NVMCTRL_CTRLA_CMD_WP`) +6. Schritte 3–5 viermal (für alle 4 Pages) + +> `NVMCTRL->ADDR.reg = addr / 2` – NVMCTRL erwartet Wort-Adresse (16-Bit-Worte), nicht Byte-Adresse. + +> **Aligned-Buffer-Pflicht**: `nvm_write_page` castet `data` zu `const uint32_t*`. Der Puffer muss `__attribute__((aligned(4)))` sein. Packed Structs sind nicht garantiert aligned → immer via lokalen `uint8_t buf[256] __attribute__((aligned(4)))` + `memcpy` übergeben. diff --git a/doc/07_serial_protocol.md b/doc/07_serial_protocol.md new file mode 100644 index 0000000..88234d8 --- /dev/null +++ b/doc/07_serial_protocol.md @@ -0,0 +1,86 @@ +# Serial-Protokoll (CDC USB) + +**Dateien:** `hal/usb_serial.h`, `hal/usb_serial.cpp` + +## Grundprinzip + +Board erscheint unter Windows als CDC Serial-Port (kein Treiber nötig). Alle Pakete haben feste Größe von **8 Byte** – kein Längen-Header, kein Framing, kein Escape. + +``` +Byte 0: Command / Event-ID +Byte 1: key_id (Button 0–24 oder Encoder 0–3) / Chunk-Index / Chunk-Count +Byte 2: r / Daten-Byte A +Byte 3: g / Daten-Byte B +Byte 4: b +Byte 5–7: reserviert (0x00) +``` + +## Richtungen + +| Richtung | ID-Bereich | Verarbeitung | +|---|---|---| +| PC → Board (Commands) | 0x01–0x7F | `poll_vendor()` in CMainController | +| Board → PC (Events) | 0x81–0xFF | `usb_serial_send()` in processEvents | + +## Command-Referenz (PC → Board) + +| ID | Name | Bedeutung | +|---|---|---| +| `0x01` | SET_LED_OVERRIDE | key_id, r, g, b – temporäre Override-Farbe setzen | +| `0x02` | CLEAR_LED_OVERRIDE | key_id – Override löschen, zurück zu base | +| `0x03` | SET_LED_BASE | key_id, r, g, b – base-Farbe dauerhaft ändern (kein NVM) | +| `0x05` | PING | Board antwortet sofort mit PONG (0x85) | +| `0x10` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl – neuen Config-Empfang starten | +| `0x11` | CONFIG_DATA | Byte[1] = Chunk-Index, Byte[2–7] = 6 B Nutzdaten | +| `0x12` | CONFIG_COMMIT | CRC prüfen → NVM schreiben → Buttons neu laden → ACK/NACK | +| `0x13` | CONFIG_READ | Board sendet aktuelle NVM-Config zurück (BEGIN/DATA/END) | +| `0x20` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl – neuen Makro-Empfang starten | +| `0x21` | MACRO_DATA | Byte[1] = Chunk-Index, Byte[2–7] = 6 B Nutzdaten | +| `0x22` | MACRO_COMMIT | NVM schreiben + MACRO_ACK zurück | +| `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück | + +## Event-Referenz (Board → PC) + +| ID | Name | Bedeutung | +|---|---|---| +| `0x81` | KEY_DOWN | key_id – HOST_COMMAND-Button gedrückt | +| `0x82` | KEY_UP | key_id – (derzeit nicht gesendet) | +| `0x83` | ENC_CW | enc_id – Encoder-Schritt CW (HOST_COMMAND) | +| `0x84` | ENC_CCW | enc_id – Encoder-Schritt CCW (HOST_COMMAND) | +| `0x85` | PONG | Antwort auf PING | +| `0x90` | CONFIG_ACK | Config erfolgreich in NVM geschrieben | +| `0x91` | CONFIG_NACK | Config CRC/Magic ungültig – nicht geschrieben | +| `0x92` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl (Config-Dump) | +| `0x93` | CONFIG_DATA | Byte[1] = Index, Byte[2–7] = 6 B (Config-Dump) | +| `0x94` | CONFIG_END | Config-Dump abgeschlossen | +| `0x95` | MACRO_ACK | Makro-Tabelle erfolgreich gespeichert | +| `0x96` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl (Makro-Dump) | +| `0x97` | MACRO_DATA | Byte[1] = Index, Byte[2–7] = 6 B (Makro-Dump) | +| `0x98` | MACRO_END | Makro-Dump abgeschlossen | + +## Chunked Transfer + +Config (223 B) und Makro-Tabelle (256 B) werden in 6-Byte-Chunks übertragen: + +``` +Config: ceil(223 / 6) = 38 Chunks +Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes) +``` + +Ablauf (PC → Board): +``` +BEGIN (chunk_count) +DATA chunk_0 (Bytes 0–5) +DATA chunk_1 (Bytes 6–11) +... +COMMIT +``` + +COMMIT bei Config: Board prüft Magic + Version + CRC. Bei Fehler → NACK, kein NVM-Schreiben. +COMMIT bei Makro: Kein CRC, Board schreibt blind → MACRO_ACK. + +## Implementierungsdetails + +- **Ring-Buffer**: 256 Byte Eingangspuffer (= 32 vollständige Pakete) in `usb_serial.cpp` +- **DTR-Check**: `usb_serial_send()` sendet nur wenn `SerialUSB` aktiv ist (verhindert stilles Verwerfen wenn VersaGUI nicht verbunden) +- **SAMD21 CDC**: Nach SWD-Flash braucht Windows eine physische USB-Reinitialisierung (Kabel abziehen/stecken) damit der CDC-Port neu enumeriert diff --git a/doc/INDEX.md b/doc/INDEX.md new file mode 100644 index 0000000..74e40be --- /dev/null +++ b/doc/INDEX.md @@ -0,0 +1,24 @@ +# VersaMCU – Dokumentations-Index + +Jede Datei deckt eine Firmware-Komponente ab. Für Claude: die relevante(n) Dateien zu Beginn einer Aufgabe lesen statt die gesamten Quelldateien zu scannen. + +| Datei | Inhalt | +|---|---| +| [00_architecture.md](00_architecture.md) | Loop-Ablauf, Datenfluss, Key-ID-Schema, globale Invarianten (kein Heap, kein Float, packed Structs, aligned NVM) | +| [01_matrix.md](01_matrix.md) | 5×5-Scan, Debounce, Key-ID-Berechnung | +| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, LUT, ISR-Aufbau, Halbschritt-Akkumulator | +| [03_action_engine.md](03_action_engine.md) | SAction-Struct, ActionType, execute_action, Tap-Only-Modell, HOST_COMMAND-Pfad | +| [04_macro_system.md](04_macro_system.md) | SMacroTable, NVM Row 1, Slot-Konvention, aligned-Buffer-Pflicht | +| [05_led_system.md](05_led_system.md) | 2-Schicht-Modell, alle Animationen, Hue-Arithmetik (kein Float), Render-Pipeline, Bit-Bang vs. DMA | +| [06_nvm_config.md](06_nvm_config.md) | Flash-Layout, SDeviceConfig-Byte-Map, CRC16-CCITT, Schreib-Mechanik, Defaults | +| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Pakete, alle Command/Event-IDs, Chunked Transfer, Ring-Buffer | + +## Schnell-Referenz: Was steht wo? + +- **Warum packed?** → [03_action_engine.md](03_action_engine.md), [06_nvm_config.md](06_nvm_config.md) +- **Warum kein Float?** → [05_led_system.md](05_led_system.md), [00_architecture.md](00_architecture.md) +- **Warum Bit-Bang statt DMA?** → [05_led_system.md](05_led_system.md) +- **Aligned-Buffer bei NVM-Write?** → [04_macro_system.md](04_macro_system.md), [06_nvm_config.md](06_nvm_config.md) +- **Warum on_press/on_release leer?** → [03_action_engine.md](03_action_engine.md) +- **Kein Hold-Support?** → [03_action_engine.md](03_action_engine.md) +- **USB nach SWD-Flash nicht erkannt?** → [07_serial_protocol.md](07_serial_protocol.md)