Added doc

This commit is contained in:
Julian Appel 2026-03-30 19:52:37 +02:00
parent 0b3c5f3217
commit 9079fefad8
9 changed files with 567 additions and 0 deletions

83
doc/00_architecture.md Normal file
View File

@ -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 03 : Encoder-SW-Buttons (COL_0 × ROW_03), kein LED
key_id 4 : nicht belegt (COL_0 × ROW_4)
key_id 524 : MX-Buttons (COL_14 × ROW_04), 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 (~1020× 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.

45
doc/01_matrix.md Normal file
View File

@ -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 04, row 04
// key_id 04: Encoder-SW (COL_0)
// key_id 524: MX-Buttons (COL_14)
```
## 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

44
doc/02_encoder.md Normal file
View File

@ -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 13
```
`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).

70
doc/03_action_engine.md Normal file
View File

@ -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 031 |
## 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 [03] 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).

59
doc/04_macro_system.md Normal file
View File

@ -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 |
|---|---|
| 019 | MX-Button `mx_idx` (entspricht key_id 5) |
| 2031 | 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 (031)
für Step 03:
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.

88
doc/05_led_system.md Normal file
View File

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

68
doc/06_nvm_config.md Normal file
View File

@ -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 7222 |
| 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 7222 (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 35 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.

86
doc/07_serial_protocol.md Normal file
View File

@ -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 024 oder Encoder 03) / Chunk-Index / Chunk-Count
Byte 2: r / Daten-Byte A
Byte 3: g / Daten-Byte B
Byte 4: b
Byte 57: reserviert (0x00)
```
## Richtungen
| Richtung | ID-Bereich | Verarbeitung |
|---|---|---|
| PC → Board (Commands) | 0x010x7F | `poll_vendor()` in CMainController |
| Board → PC (Events) | 0x810xFF | `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[27] = 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[27] = 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[27] = 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[27] = 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 05)
DATA chunk_1 (Bytes 611)
...
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

24
doc/INDEX.md Normal file
View File

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