Added config, added factory reset functionality

This commit is contained in:
2026-04-18 23:59:48 +02:00
parent 433d61c29f
commit 802ab858e1
9 changed files with 718 additions and 778 deletions
+81 -57
View File
@@ -1,83 +1,107 @@
# VersaMCU Architekturbersicht
# VersaMCU - Architekturuebersicht
## Ziel-Hardware
## Zielplattform
| Merkmal | Wert |
|---|---|
| MCU | ATSAMD21G17D (Cortex-M0+, 48 MHz) |
| Flash | 128 KB (davon 512 B am Ende für NVM-Config reserviert) |
| MCU | ATSAMD21G17D, Cortex-M0+, 48 MHz |
| Flash | 128 KB |
| 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) |
| FPU | keine, deshalb Integer-Arithmetik |
| USB | HID Keyboard + Consumer + CDC Serial |
| Toolchain | PlatformIO + Arduino Core |
## Loop-Ablauf
## Setup und Loop
```
```text
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
macro_config_load()
nvm_config_load()
init_buttons()
usb_hid_init()
usb_serial_init()
matrix_init(cb)
encoder_init(cb)
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)
loop()
matrix_scan()
poll_vendor()
processEvents()
check_factory_reset()
updateLEDs()
```
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+).
Die Reihenfolge ist absichtlich simpel:
- Eingaben einsammeln
- CDC-Kommandos vom Host verarbeiten
- Event-Queue leeren
- Sonderlogik fuer den Werksreset pruefen
- LED-Frame nur bei Bedarf rendern
## 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
```text
matrix_scan / encoder ISR
-> EventQueue
-> processEvents()
-> execute_action_down / execute_action_up
-> usb_hid_*
-> usb_serial_send() fuer HOST_COMMAND
SerialUSB (CDC, PC → Board)
└─► poll_vendor()
├─► CButton.set_override() / clear_override() / set_base()
└─► Config/Makro-Transfer (chunked, 6 B/Paket)
CDC Serial
-> poll_vendor()
-> Config/Makros einlesen oder dumpen
-> LED-Overrides setzen/loeschen
LED-Render
-> CButton.render_led()
-> ws2812_set()
-> ws2812_show() nur wenn dirty
```
## Komponenten-Übersicht
## Zentrale Komponenten
| Datei | Verantwortung |
| Datei | Aufgabe |
|---|---|
| `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 |
| `main.cpp` | startet den Controller |
| `CMainController.*` | Orchestrator fuer Inputs, Actions, Serial, LEDs |
| `CButton.*` | LED-Zustand, Animationen, Action-Referenz |
| `CEventQueue.*` | ISR-sicherer Ringbuffer |
| `config/nvm_config.*` | Config v3 laden, speichern, Defaults |
| `config/macro_config.*` | Makros laden, speichern |
| `hal/matrix.*` | 5x5-Matrixscan mit Debounce |
| `hal/encoder.*` | Encoder-ISR und Drehrichtung |
| `hal/usb_hid.*` | Keyboard- und Consumer-HID |
| `hal/usb_serial.*` | CDC-Paketpfad |
| `hal/ws2812.*` | WS2812-Treiber |
## 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
```text
0..3 = Encoder-SW
4 = unbenutzt
5..24 = MX-Buttons
```
LED-Index folgt serpentiner Verdrahtung: `LED_INDEX(col, row)`.
Die beiden Werksreset-Tasten sind:
## Invarianten / Constraints
- `key_id 9` = unten links
- `key_id 24` = unten rechts
- **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.
## Werksreset im Ablauf
Der Werksreset ist keine PC-Funktion, sondern Teil der Firmware:
- sobald beide Reset-Tasten gleichzeitig gehalten werden, werden ihre normalen Actions unterdrueckt
- falls bereits ein HID-Hold aktiv war, wird er sofort freigegeben
- nach 5 Sekunden gemeinsamer Haltezeit wird Default-Config + leere Makro-Tabelle in NVM geschrieben
- danach folgt ein kurzes rotes Feedback-Blinken
## Invarianten
- kein Heap
- keine Floats
- `packed` fuer serielle und NVM-relevante Structs
- NVM-Schreibpuffer muessen 4-Byte-aligned sein
- `usb_serial_send()` sendet nur bei aktiver CDC-Verbindung
+68 -65
View File
@@ -1,100 +1,103 @@
# Aktions-Engine
**Dateien:** `config/action.h`, `CButton.h/.cpp`, `CMainController.cpp` (`processEvents`, `execute_action_down`, `execute_action_up`)
Dateien:
## SAction-Struct
- `config/action.h`
- `CMainController.h/.cpp`
- `CButton.h/.cpp`
## `SAction`
```cpp
struct __attribute__((packed)) SAction {
ActionType type; // 1 Byte
uint16_t data; // 2 Bytes (Keycode, Consumer-Code, Command-ID oder Slot-Index)
ActionType type;
uint16_t data;
};
// Gesamt: 3 Bytes (packed! ohne packed wären es 4 durch Alignment)
```
`packed` ist zwingend damit `sizeof(SDeviceConfig) == 740` mit der C#-Serialisierung in VersaGUI übereinstimmt.
Groesse: 3 Byte.
Das `packed` ist zwingend, weil Config v3 bytegenau zwischen Firmware und GUI uebereinstimmen muss.
## ActionType
## `ActionType`
| Typ | Wert | Bedeutung | data-Inhalt |
|---|---|---|---|
| `NONE` | 0 | Keine Aktion | |
| `HID_KEY` | 1 | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier |
| `HID_CONSUMER` | 2 | Consumer Control (Volume, Media, …) | Consumer Usage ID |
| `HOST_COMMAND` | 3 | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) |
| `MACRO` | 4 | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 |
| `PROFILE_SWITCH` | 5 | Aktives Profil wechseln | 02 = Ziel-Profil, 0xFF = nächstes Profil (Zyklus 0→1→2→0) |
| Typ | Bedeutung | `data` |
|---|---|---|
| `NONE` | keine Aktion | - |
| `HID_KEY` | Tastaturtaste ueber USB HID | low byte = keycode, high byte = modifier |
| `HID_CONSUMER` | Media/Consumer-HID | usage id |
| `HOST_COMMAND` | Event an die GUI | command id |
| `MACRO` | Makro aus `SMacroTable` | slot 0..31 |
| `PROFILE_SWITCH` | Profilwechsel | 0..2 oder `0xFF` fuer naechstes Profil |
## execute_action_down() — Taste gedrückt (Hold-Start)
## Verhalten bei `KEY_DOWN`
| ActionType | Verhalten |
| Typ | Effekt |
|---|---|
| `HID_KEY` | `usb_hid_send_key(keycode, modifier)` — Taste bleibt gedrückt bis `execute_action_up()` |
| `HID_CONSUMER` | `usb_hid_send_consumer(usage_id)` — bleibt aktiv bis `execute_action_up()` |
| `HOST_COMMAND` | `usb_serial_send(USB_EVT_KEY_DOWN, key_id)` |
| `MACRO` | Volle Sequenz ausführen (Steps[slot], keycode==0 = Ende, delay 10+20 ms) |
| `PROFILE_SWITCH` | NVM laden → `active_profile` setzen → CRC neu berechnen → NVM speichern`init_buttons()` |
| `NONE` | nop |
| `HID_KEY` | `usb_hid_send_key()` |
| `HID_CONSUMER` | `usb_hid_send_consumer()` |
| `HOST_COMMAND` | `usb_serial_send(KEY_DOWN/ENC_*)` |
| `MACRO` | komplette Sequenz sofort abspielen |
| `PROFILE_SWITCH` | Config aus NVM laden, Profil aendern, CRC neu berechnen, speichern, Buttons neu initialisieren |
| `NONE` | nichts |
## execute_action_up() — Taste losgelassen (Hold-Ende)
## Verhalten bei `KEY_UP`
| ActionType | Verhalten |
| Typ | Effekt |
|---|---|
| `HID_KEY` | `usb_hid_release_key()` |
| `HID_CONSUMER` | `usb_hid_release_consumer()` |
| `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) |
| `MACRO`/`PROFILE_SWITCH`/`NONE` | nop |
| `HOST_COMMAND` | optionaler Up-Pfad, derzeit praktisch ohne Nutzlast |
| `MACRO` | nichts |
| `PROFILE_SWITCH` | nichts |
| `NONE` | nichts |
## PROFILE_SWITCH — Ablauf
## Hold- und Tap-Modell
```cpp
SDeviceConfig cfg;
nvm_config_load(cfg); // komplette Config aus NVM
uint8_t target = (uint8_t)action.data;
if (target == 0xFF)
target = (cfg.active_profile + 1) % 3; // Zyklus
cfg.active_profile = target;
cfg.crc = nvm_config_crc(cfg); // CRC MUSS nach Änderung neu berechnet werden!
if (nvm_config_save(cfg)) // bool: false = NVM-Timeout
init_buttons();
- MX-Buttons und Encoder-SW benutzen fuer HID und Consumer das Hold-Modell.
- Encoder `CW` und `CCW` sind immer diskrete Tap-Events:
```text
down -> delay(10 ms) -> up
```
> **Wichtig:** `active_profile` liegt im CRC-geschützten Bereich (ab Byte 7). Wird die CRC nicht aktualisiert, findet das nächste `nvm_config_load()` einen CRC-Fehler und lädt die Defaults (alle Aktionen NONE, alle LEDs Regenbogen).
- Makros laufen komplett synchron in der Firmware.
## Hold-Modell (HID-Keys und Consumer Controls)
## Makro-Ausfuehrung
Normale Tasten- und Media-Aktionen folgen dem **Hold-Modell**:
Bei `ActionType::MACRO` wird `action.data` als Slot interpretiert.
Die Firmware laeuft dann durch bis zu 8 Steps:
```
KEY_DOWN-Event vom Board → execute_action_down() → HID Key-Down senden
[Taste bleibt physisch gedrückt...]
KEY_UP-Event vom Board → execute_action_up() → HID Key-Up senden
```text
step.keycode == 0 -> Ende
Key-Down
10 ms warten
Key-Up
20 ms warten
```
Das OS erkennt die gedrückte Taste und startet sein eigenes Key-Repeat nach ~500 ms — wie auf einer normalen Tastatur.
## Profilwechsel
## Tap-Modell (Encoder CW/CCW)
`PROFILE_SWITCH` arbeitet direkt auf der gespeicherten Config:
Encoder-Bewegungen sind diskret (kein Halten möglich) und verwenden das **Tap-Modell**:
1. Config aus NVM laden
2. `active_profile` aendern
3. CRC neu berechnen
4. wieder speichern
5. `init_buttons()`
```
ENC_CW/ENC_CCW-Event → execute_action_down() + delay(10) + execute_action_up()
```
Wichtig:
`active_profile` liegt im CRC-geschuetzten Bereich. Ohne neue CRC wuerde die Config beim naechsten Laden verworfen.
(Atomare Sequenz für jeden Encoder-Schritt.)
## Sonderfall Werksreset
## Work-Loop-Reihenfolge
Die Reset-Kombination uebersteuert das normale Action-System fuer genau zwei Tasten:
```cpp
void work() {
matrix_scan(); // → Events in Queue (KEY_DOWN, KEY_UP, ENC_CW, ENC_CCW)
poll_vendor(); // Serial-Pakete verarbeiten (PC↔Board Kommandos)
processEvents(); // → execute_action_down/up() aufrufen
updateLEDs(); // Dirty-LEDs aktualisieren
}
```
- `key_id 9`
- `key_id 24`
**processEvents() verarbeitet:**
- `KEY_DOWN``execute_action_down()`
- `KEY_UP``execute_action_up()`
- `ENC_CW` / `ENC_CCW``execute_action_down()` + `delay(10)` + `execute_action_up()`
Sobald beide gleichzeitig gehalten werden:
- ihre normalen Actions werden nicht weiter ausgefuehrt
- eventuell bereits gestartete HID-Holds werden sofort freigegeben
- die LEDs der beiden Tasten leuchten rot
- nach 5 Sekunden wird `perform_factory_reset()` ausgefuehrt
+56 -35
View File
@@ -1,66 +1,87 @@
# Makro-System
**Dateien:** `config/macro_config.h`, `config/macro_config.cpp`, `CMainController.cpp`
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
uint8_t keycode;
uint8_t modifier;
};
#define MACRO_SLOTS 32
#define MACRO_MAX_STEPS 8
struct __attribute__((packed)) SMacroTable {
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; // 32 × 8 × 2 = 512 Byte
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
};
```
Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 512 == zwei NVM-Rows`.
Gesamtgroesse:
## NVM-Speicherort
- `32 * 8 * 2 = 512` Byte
- verteilt auf zwei NVM-Rows
## Speicherort
| Row | Adresse | Inhalt |
|---|---|---|
| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0255 |
| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256511 |
| Macro Row 0 | `0x1FB00` | Bytes `0..255` |
| Macro Row 1 | `0x1FC00` | Bytes `256..511` |
Beide Rows sind im Linkerscript reserviert. Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt `false` zurück → leere Tabelle (alle Keycodes 0).
## Slot-Konvention
## Slot-Zuweisung (Konvention, Board speichert blind)
Das Board speichert die Slots blind, die GUI verwendet dabei diese Zuordnung:
| Slots | Verwendung |
| Slots | Bedeutung |
|---|---|
| 019 | MX-Button `mx_idx` (entspricht key_id 5) |
| 2031 | Encoder-Aktionen (`enc * 3 + act_idx`, 0=SW / 1=CW / 2=CCW) |
| `0..19` | MX-Buttons |
| `20..31` | Encoder-Aktionen (`enc * 3 + act_idx`) |
## Laden und Speichern
## Laden
**Laden** (`macro_config_load`):
- `memcpy` direkt aus Flash-Adresse in RAM-Struct
- Kein Magic/CRC — leere Tabelle (alle 0xFF) ist ein akzeptabler Zustand
`macro_config_load()`:
**Speichern** (`macro_config_save`) — gibt `bool` zurück:
- SMacroTable in `uint8_t aligned_buf[512] __attribute__((aligned(4)))` kopieren (Pflicht!)
- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus)
- Beide Rows löschen (`nvm_erase_row`) — bei NVM-Timeout: `return false`
- 8 Pages à 64 Byte schreiben (`nvm_write_page`) — bei NVM-Timeout: `return false`
- `return true`
- kopiert 512 Byte aus NVM in `SMacroTable`
- erkennt komplett geloeschten Flash (`0xFF`) als "noch nie beschrieben"
- setzt dann eine leere Tabelle
> **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).
Eine leere Tabelle ist also ein gueltiger Default-Zustand.
## Ausführung (in execute_action_down, ActionType::MACRO)
## Speichern
```
slot = action.data (031)
für Step 07:
if step.keycode == 0: abbrechen
HID Key-Down (keycode, modifier)
delay(10 ms)
HID Key-Up
delay(20 ms)
```
`macro_config_save()`:
Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung.
1. Tabelle in einen 4-Byte-aligned Puffer kopieren
2. beide Rows loeschen
3. 8 Pages zu je 64 Byte schreiben
Rueckgabewert:
- `true` bei Erfolg
- `false` bei NVM-Timeout
## Ausfuehrung
Beim Triggern eines Makros:
- Slot aus `action.data`
- bis zu 8 Steps abarbeiten
- `keycode == 0` beendet das Makro vorzeitig
- pro Step:
- HID key down
- 10 ms warten
- HID key up
- 20 ms warten
Die Ausfuehrung laeuft aus `m_macros` im RAM, nicht direkt aus NVM.
## Zusammenhang mit Werksreset
Beim Werksreset wird die komplette `SMacroTable` auf 0 gesetzt und in beide Makro-Rows zurueckgeschrieben.
Danach sind alle 32 Slots leer.
+99 -78
View File
@@ -1,107 +1,128 @@
# NVM-Konfiguration
**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp`
Dateien:
## Flash-Layout (5 Rows, 0x1FB000x1FFFF)
- `config/nvm_config.h`
- `config/nvm_config.cpp`
| Row | Adresse | Größe | Inhalt |
## Flash-Layout
| Bereich | Adresse | Groesse | Inhalt |
|---|---|---|---|
| Macro Row 0 | `0x1FB00` | 256 B | SMacroTable Bytes 0255 |
| Macro Row 1 | `0x1FC00` | 256 B | SMacroTable Bytes 256511 |
| Config Row 0 | `0x1FD00` | 256 B | Globaler Header + Profil 0 (teilweise) |
| Config Row 1 | `0x1FE00` | 256 B | Profil 0 (Rest) + Profil 1 (teilweise) |
| Config Row 2 | `0x1FF00` | 256 B | Profil 1 (Rest) + Profil 2 + 28 B Reserve |
| Macro Row 0 | `0x1FB00` | 256 B | `SMacroTable` Bytes `0..255` |
| Macro Row 1 | `0x1FC00` | 256 B | `SMacroTable` Bytes `256..511` |
| Config Row 0 | `0x1FD00` | 256 B | Header + Profil 0 Anfang |
| Config Row 1 | `0x1FE00` | 256 B | Profil 0 Rest + Profil 1 Anfang |
| Config Row 2 | `0x1FF00` | 256 B | Profil 1 Rest + Profil 2 + Reserve |
Alle Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. Config und Makros liegen in vollständig getrennten, zusammenhängenden Row-Blöcken.
Makros und Config sind komplett getrennt.
## SDeviceConfig Byte-Layout (740 Byte, packed)
## `SDeviceConfig`
### Globaler Header (32 B, Offset 0)
Aktueller Stand:
| Offset | Größe | Feld |
- Magic: `0x56503203`
- Version: `3`
- Groesse: `740` Byte
- auf 3 Config-Rows verteilt
### Header
| Offset | Groesse | Feld |
|---|---|---|
| 0 | 4 | `magic` = `0x56503203` ('VP2\x03') |
| 4 | 1 | `version` = 3 |
| 5 | 2 | `crc` CRC16-CCITT über Bytes 7739 |
| 7 | 1 | `active_profile` (02) |
| 8 | 1 | `global_brightness` (0255) |
| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder, Default 1) |
| 13 | 19 | Reserve |
| `0` | 4 | `magic` |
| `4` | 1 | `version` |
| `5` | 2 | `crc` |
| `7` | 1 | `active_profile` |
| `8` | 1 | `global_brightness` |
| `9` | 4 | `enc_sensitivity[4]` |
| `13` | 19 | Reserve |
### Pro Profil (236 B, Offset `32 + idx × 236`)
### Pro Profil
| Offset | Größe | Feld |
Jedes Profil belegt 236 Byte:
| Offset im Profil | Groesse | Feld |
|---|---|---|
| 0 | 60 | `mx_actions[20]` 20 × 3 B SAction |
| 60 | 36 | `enc_actions[4][3]` 12 × 3 B SAction |
| 96 | 20 | `led_r[20]` |
| 116 | 20 | `led_g[20]` |
| 136 | 20 | `led_b[20]` |
| 156 | 20 | `led_brightness[20]` per-LED Helligkeit (0255) |
| 176 | 20 | `led_anim[20]` LEDAnim-Typ als uint8_t |
| 196 | 40 | `led_period_ms[20]` uint16_t little-endian |
| `0` | 60 | `mx_actions[20]` |
| `60` | 36 | `enc_actions[4][3]` |
| `96` | 20 | `led_r[20]` |
| `116` | 20 | `led_g[20]` |
| `136` | 20 | `led_b[20]` |
| `156` | 20 | `led_brightness[20]` |
| `176` | 20 | `led_anim[20]` |
| `196` | 40 | `led_period_ms[20]` |
Gesamt: 32 B Header + 3 × 236 B Profile = **740 B**.
Gesamtrechnung:
`__attribute__((packed))` ist zwingend. Ohne packed wäre SAction 4 B statt 3 B, was `sizeof(SDeviceConfig)` um 32 B vergrößert und die C#-Deserialisierung zerstört.
## CRC16-CCITT
- Polynom: `0x1021`, Init: `0xFFFF`
- Berechnet über Bytes 7739 (ab `active_profile`, nach dem `crc`-Feld selbst)
- Sichert alle Nutzdaten einschließlich `active_profile`
> **Wichtig bei PROFILE_SWITCH:** `active_profile` liegt im CRC-Bereich. Nach jeder Änderung muss `cfg.crc = nvm_config_crc(cfg)` aufgerufen werden bevor gespeichert wird — sonst lädt `nvm_config_load()` die Defaults.
## Lese-Logik
```
memcpy aus Flash-Adresse 0x1FD00 (740 B)
if magic != 0x56503203: Defaults laden, return false
if version != 3: Defaults laden, return false
if crc != crc(cfg): Defaults laden, return false
if active_profile >= 3: active_profile = 0
return true
```text
32 Byte Header + 3 * 236 Byte Profile = 740 Byte
```
Kein Absturz bei ungültiger Config Defaults greifen immer.
## CRC
CRC16-CCITT:
- Polynom `0x1021`
- Init `0xFFFF`
- Bereich: Bytes `7..739`
Damit sind auch `active_profile` und globale Helligkeit abgesichert.
## Defaults
- Alle Aktionen: `NONE`
- LEDs: warm-weiß (R=80, G=40, B=0), `led_brightness=255`
- Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms
- `active_profile = 0`, `global_brightness = 255`, `enc_sensitivity = 1`
`nvm_config_defaults()` setzt:
## Schreib-Logik (nvm_config_save)
- `active_profile = 0`
- `global_brightness = 255`
- `enc_sensitivity[*] = 1`
- alle Actions auf `NONE`
- alle `led_brightness[*] = 255`
- Base-Farbe `R=80, G=40, B=0`
- `led_anim = COLOR_CYCLE`
- `led_period_ms = 4000`
`nvm_config_save()` gibt `bool` zurück. `false` bedeutet NVM-Timeout — der NVM-Controller hat nicht rechtzeitig READY gemeldet (beobachtet nach bestimmten Bootloader/Flash-Zyklen auf SAMD21).
Praktisch sichtbares Ergebnis:
SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Ablauf:
- alle MX-LEDs laufen wieder im Regenbogenmodus
1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus)
2. 3 Rows löschen (`NVMCTRL_CTRLA_CMD_ER`) — bei Fehler: `return false`
3. Für jede der 12 Pages à 64 B:
- Page-Buffer löschen (`NVMCTRL_CTRLA_CMD_PBC`)
- 64 B als `uint32_t*` in Page-Buffer schreiben
- Page programmieren (`NVMCTRL_CTRLA_CMD_WP`) — bei Fehler: `return false`
4. `return true`
## Laden
### nvm_wait() Timeout
`nvm_config_load()`:
```cpp
static bool nvm_wait()
{
uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000 Iterationen ≈ 400 ms
while (!NVMCTRL->INTFLAG.bit.READY) {
if (--timeout == 0) return false;
}
return true;
}
```
1. 740 Byte aus NVM kopieren
2. Magic pruefen
3. Version pruefen
4. CRC pruefen
5. bei Fehlern Defaults laden und `false` zurueckgeben
Der Timeout verhindert ein dauerhaftes Einfrieren des Boards wenn NVMCTRL aus unbekanntem Grund nicht READY meldet. Bei Timeout sendet das Board `CONFIG_NACK` statt zu hängen.
Die Firmware faellt also immer auf einen gueltigen Zustand zurueck.
> `NVMCTRL->ADDR.reg = addr / 2` NVMCTRL erwartet Wort-Adresse (16-Bit-Worte), nicht Byte-Adresse.
## Speichern
> **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 lokalem `uint8_t buf[] __attribute__((aligned(4)))` + `memcpy` übergeben.
`nvm_config_save()`:
1. 740-Byte-Config in einen 768-Byte-Row-Puffer kopieren
2. Rest mit `0xFF` fuellen
3. `MANW = 1`
4. 3 Rows loeschen
5. 12 Pages zu je 64 Byte schreiben
Rueckgabewert:
- `true` bei Erfolg
- `false` bei NVM-Timeout
Wichtig:
- der Schreibpuffer muss 4-Byte-aligned sein
- `packed` allein reicht dafuer nicht
## Zusammenhang mit Werksreset
Der Werksreset nutzt denselben Pfad:
- `nvm_config_defaults(cfg)`
- `nvm_config_save(cfg)`
Dadurch werden auch kaputte, aber formal noch vorhandene Alt-Daten im NVM wirklich ueberschrieben.
+96 -61
View File
@@ -1,92 +1,127 @@
# Serial-Protokoll (CDC USB)
**Dateien:** `hal/usb_serial.h`, `hal/usb_serial.cpp`
Dateien:
## Grundprinzip
- `hal/usb_serial.h`
- `hal/usb_serial.cpp`
- `CMainController.cpp`
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.
## Paketformat
Alle Pakete sind exakt 8 Byte lang:
```text
Byte 0: command / event id
Byte 1: key_id oder chunk-index oder chunk-count
Byte 2: daten a
Byte 3: daten b
Byte 4: daten c
Byte 5..7: reserviert
```
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)
```
Es gibt kein Framing und keinen Laengenheader.
## Richtungen
| Richtung | ID-Bereich | Verarbeitung |
| Richtung | IDs | Verarbeitung |
|---|---|---|
| PC Board (Commands) | 0x010x7F | `poll_vendor()` in CMainController |
| Board PC (Events) | 0x810xFF | `usb_serial_send()` in processEvents |
| PC -> Board | `0x01..0x7F` | `poll_vendor()` |
| Board -> PC | `0x81..0xFF` | `usb_serial_send()` |
## Command-Referenz (PC → Board)
## Commands
| ID | Name | Bedeutung |
| ID | Name | Zweck |
|---|---|---|
| `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 oder MACRO_NACK |
| `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück |
| `0x01` | `SET_LED_OVERRIDE` | temporaere LED-Override setzen |
| `0x02` | `CLEAR_LED_OVERRIDE` | Override entfernen |
| `0x03` | `SET_LED_BASE` | Base-Farbe im RAM setzen |
| `0x05` | `PING` | Antwort: `PONG` |
| `0x10` | `CONFIG_BEGIN` | Config-Transfer starten |
| `0x11` | `CONFIG_DATA` | 6 Byte Config-Nutzdaten |
| `0x12` | `CONFIG_COMMIT` | Config pruefen und speichern |
| `0x13` | `CONFIG_READ` | Config-Dump an Host senden |
| `0x20` | `MACRO_BEGIN` | Makro-Transfer starten |
| `0x21` | `MACRO_DATA` | 6 Byte Makro-Nutzdaten |
| `0x22` | `MACRO_COMMIT` | Makros speichern |
| `0x23` | `MACRO_READ` | Makro-Dump an Host senden |
## Event-Referenz (Board → PC)
## Events
| ID | Name | Bedeutung |
| ID | Name | Zweck |
|---|---|---|
| `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 oder NVM-Timeout 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 |
| `0x99` | MACRO_NACK | Makro-Tabelle: NVM-Timeout nicht geschrieben |
| `0x81` | `KEY_DOWN` | Host-Command-Button gedrueckt |
| `0x82` | `KEY_UP` | Host-Command-Button losgelassen |
| `0x83` | `ENC_CW` | Encoder Host-Command im Uhrzeigersinn |
| `0x84` | `ENC_CCW` | Encoder Host-Command gegen Uhrzeigersinn |
| `0x85` | `PONG` | Antwort auf Ping |
| `0x90` | `CONFIG_ACK` | Config erfolgreich gespeichert |
| `0x91` | `CONFIG_NACK` | Config ungueltig oder NVM-Timeout |
| `0x92` | `CONFIG_BEGIN` | Config-Dump beginnt |
| `0x93` | `CONFIG_DATA` | 6 Byte Config-Dump |
| `0x94` | `CONFIG_END` | Config-Dump fertig |
| `0x95` | `MACRO_ACK` | Makros erfolgreich gespeichert |
| `0x96` | `MACRO_BEGIN` | Makro-Dump beginnt |
| `0x97` | `MACRO_DATA` | 6 Byte Makro-Dump |
| `0x98` | `MACRO_END` | Makro-Dump fertig |
| `0x99` | `MACRO_NACK` | Makro-Speichern fehlgeschlagen |
## Chunked Transfer
## Chunk-Zahlen
Config (740 B) und Makro-Tabelle (512 B) werden in 6-Byte-Chunks übertragen:
Aktuelle Blob-Groessen:
```
Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(512 / 6) = 86 Chunks (letzter Chunk hat 2 Nutzbytes)
- Config: `740` Byte
- Makros: `512` Byte
Bei 6 Nutzbytes pro Paket ergibt das:
```text
Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(512 / 6) = 86 Chunks
```
Ablauf (PC → Board):
```
BEGIN (chunk_count)
DATA chunk_0 (Bytes 05)
DATA chunk_1 (Bytes 611)
## Transferablauf
### PC -> Board
```text
BEGIN(chunk_count)
DATA 0
DATA 1
...
COMMIT
```
**CONFIG_COMMIT**: Board prüft Magic + Version + CRC. Bei Fehler → `CONFIG_NACK`. Bei NVM-Timeout während Erase/Write → `CONFIG_NACK`. Bei Erfolg → `CONFIG_ACK`.
### Board -> PC
**MACRO_COMMIT**: Kein CRC, Board schreibt direkt. Bei Erfolg → `MACRO_ACK`. Bei NVM-Timeout → `MACRO_NACK`.
```text
BEGIN(chunk_count)
DATA 0
DATA 1
...
END
```
### ACK-Synchronisation (GUI-Seite)
## Validierung
VersaGUI wartet nach COMMIT auf das ACK/NACK via `SemaphoreSlim` (Timeout 3 s). Erst nach Freigabe des Gates startet der nächste Transfer. Dies verhindert, dass Makro-Chunks gesendet werden während das Board noch den Config-NVM schreibt (~750 ms für 3 Rows).
`CONFIG_COMMIT` prueft:
- Magic
- Version
- CRC
Nur bei erfolgreicher Pruefung wird in NVM geschrieben.
`MACRO_COMMIT` schreibt ohne CRC direkt nach NVM und signalisiert nur Erfolg oder Fehler.
## Praktische Hinweise fuer die GUI
- nach `CONFIG_COMMIT` auf `CONFIG_ACK` oder `CONFIG_NACK` warten
- danach erst `MACRO_*` senden
- Dumps besser sequenziell lesen: zuerst Config, danach Makros
- `DtrEnable` muss aktiv sein, sonst verwirft das Board CDC-Ausgaben
## 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
- RX-Ringbuffer: 256 Byte = 32 volle Pakete
- feste 8-Byte-Pakete vereinfachen Firmware und GUI
- nach einem reinen SWD-Reflash kann ein physischer USB-Reconnect noetig sein
+16 -18
View File
@@ -1,24 +1,22 @@
# VersaMCU Dokumentations-Index
# VersaMCU - Dokumentationsindex
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.
Die Dateien hier beschreiben den aktuellen Firmware-Stand von Config v3, 3 Profilen und 32x8 Makros.
| 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 |
| [00_architecture.md](00_architecture.md) | Setup, Work-Loop, Datenfluss, Key-IDs, globale Invarianten |
| [01_matrix.md](01_matrix.md) | 5x5-Matrixscan, Debounce, Key-ID-Berechnung |
| [02_encoder.md](02_encoder.md) | Quadratur-Dekodierung, ISR-Pfad, Event-Erzeugung |
| [03_action_engine.md](03_action_engine.md) | `SAction`, `ActionType`, Hold/Tap-Verhalten, Profilwechsel, Reset-Sonderfall |
| [04_macro_system.md](04_macro_system.md) | `SMacroTable`, 32 Slots, 8 Steps, NVM-Layout, Ausfuehrung |
| [05_led_system.md](05_led_system.md) | LED-Schichten, Animationen, Render-Pipeline |
| [06_nvm_config.md](06_nvm_config.md) | Config v3, 3 Profile, CRC16, Defaults, Werksreset-Bezug |
| [07_serial_protocol.md](07_serial_protocol.md) | 8-Byte-Protokoll, Config-/Makro-Transfer, ACK/NACK |
## Schnell-Referenz: Was steht wo?
## Schnellreferenz
- **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)
- aktuelle Config-Groesse: [06_nvm_config.md](06_nvm_config.md)
- aktuelle Makro-Groesse: [04_macro_system.md](04_macro_system.md)
- Work-Loop inkl. Werksreset: [00_architecture.md](00_architecture.md)
- Action-Semantik und HID-Hold: [03_action_engine.md](03_action_engine.md)
- CDC-Protokoll und Chunk-Zahlen: [07_serial_protocol.md](07_serial_protocol.md)