Added doc
This commit is contained in:
parent
0b3c5f3217
commit
9079fefad8
83
doc/00_architecture.md
Normal file
83
doc/00_architecture.md
Normal 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 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.
|
||||||
45
doc/01_matrix.md
Normal file
45
doc/01_matrix.md
Normal 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 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
|
||||||
44
doc/02_encoder.md
Normal file
44
doc/02_encoder.md
Normal 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 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).
|
||||||
70
doc/03_action_engine.md
Normal file
70
doc/03_action_engine.md
Normal 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 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).
|
||||||
59
doc/04_macro_system.md
Normal file
59
doc/04_macro_system.md
Normal 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 |
|
||||||
|
|---|---|
|
||||||
|
| 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.
|
||||||
88
doc/05_led_system.md
Normal file
88
doc/05_led_system.md
Normal 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 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.
|
||||||
68
doc/06_nvm_config.md
Normal file
68
doc/06_nvm_config.md
Normal 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 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.
|
||||||
86
doc/07_serial_protocol.md
Normal file
86
doc/07_serial_protocol.md
Normal 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 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
|
||||||
24
doc/INDEX.md
Normal file
24
doc/INDEX.md
Normal 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)
|
||||||
Loading…
x
Reference in New Issue
Block a user