From 433d61c29fa46c54afd50c0c22f05498fd541646 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Mon, 13 Apr 2026 22:34:54 +0200 Subject: [PATCH] 8 Step macro and profile switching fully working --- doc/03_action_engine.md | 36 ++++++-- doc/04_macro_system.md | 59 ++++-------- doc/06_nvm_config.md | 175 ++++++++++++++++-------------------- doc/07_serial_protocol.md | 20 +++-- src/CMainController.cpp | 24 +++-- src/config/macro_config.cpp | 36 +++++--- src/config/macro_config.h | 3 +- src/config/nvm_config.cpp | 40 +++++---- src/config/nvm_config.h | 3 +- src/hal/usb_serial.h | 1 + 10 files changed, 204 insertions(+), 193 deletions(-) diff --git a/doc/03_action_engine.md b/doc/03_action_engine.md index 8f848cc..1786262 100644 --- a/doc/03_action_engine.md +++ b/doc/03_action_engine.md @@ -12,17 +12,18 @@ struct __attribute__((packed)) SAction { // 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. +`packed` ist zwingend damit `sizeof(SDeviceConfig) == 740` 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 | +| 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 0–31 | +| `PROFILE_SWITCH` | 5 | Aktives Profil wechseln | 0–2 = Ziel-Profil, 0xFF = nächstes Profil (Zyklus 0→1→2→0) | ## execute_action_down() — Taste gedrückt (Hold-Start) @@ -32,6 +33,7 @@ struct __attribute__((packed)) SAction { | `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 | ## execute_action_up() — Taste losgelassen (Hold-Ende) @@ -41,7 +43,23 @@ struct __attribute__((packed)) SAction { | `HID_KEY` | `usb_hid_release_key()` | | `HID_CONSUMER` | `usb_hid_release_consumer()` | | `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) | -| `MACRO`/`NONE` | nop | +| `MACRO`/`PROFILE_SWITCH`/`NONE` | nop | + +## PROFILE_SWITCH — Ablauf + +```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(); +``` + +> **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). ## Hold-Modell (HID-Keys und Consumer Controls) diff --git a/doc/04_macro_system.md b/doc/04_macro_system.md index 332cfcb..d439a82 100644 --- a/doc/04_macro_system.md +++ b/doc/04_macro_system.md @@ -10,18 +10,24 @@ struct __attribute__((packed)) SMacroStep { uint8_t modifier; // HID Modifier: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08 }; +#define MACRO_SLOTS 32 +#define MACRO_MAX_STEPS 8 + struct __attribute__((packed)) SMacroTable { - SMacroStep steps[32][4]; // 32 Slots × 4 Steps × 2 Byte = 256 Byte + SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; // 32 × 8 × 2 = 512 Byte }; ``` -Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine NVM-Row`. +Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 512 == zwei NVM-Rows`. ## 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) +| Row | Adresse | Inhalt | +|---|---|---| +| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0–255 | +| Macro Row 1 | `0x1FC00` | SMacroTable 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-Zuweisung (Konvention, Board speichert blind) @@ -34,21 +40,22 @@ Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine **Laden** (`macro_config_load`): - `memcpy` direkt aus Flash-Adresse in RAM-Struct -- Kein Magic/CRC (leere Tabelle bei 0xFF ist akzeptabler Zustand) +- Kein Magic/CRC — leere Tabelle (alle 0xFF) ist ein akzeptabler Zustand -**Speichern** (`macro_config_save`): -- SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!) +**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) -- Row 1 löschen (`nvm_erase_row`) -- 4 Pages à 64 Byte schreiben (`nvm_write_page`) +- 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` > **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) +## Ausführung (in execute_action_down, ActionType::MACRO) ``` slot = action.data (0–31) -für Step 0–3: +für Step 0–7: if step.keycode == 0: abbrechen HID Key-Down (keycode, modifier) delay(10 ms) @@ -57,31 +64,3 @@ für Step 0–3: ``` Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung. - ---- - -## Geplante Erweiterung: 8 Steps (NVM v3) - -### Motivation - -4 Steps reichen für einfache Shortcuts, aber nicht für Excel-Ribbon-Navigation oder andere Sequenzen mit 5+ Tasten. Mit dem NVM-v3-Umbau (siehe [06_nvm_config.md](06_nvm_config.md)) stehen zwei vollständige Rows für die Makro-Tabelle zur Verfügung. - -### Neues Layout - -```cpp -#define MACRO_SLOTS 32 -#define MACRO_MAX_STEPS 8 // war: 4 - -struct __attribute__((packed)) SMacroTable { - SMacroStep steps[32][8]; // 32 × 8 × 2 = 512 Bytes = 2 NVM-Rows -}; -``` - -### Neuer NVM-Speicherort - -| Row | Adresse | Inhalt | -|---|---|---| -| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0–255 | -| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256–511 | - -`macro_config_save` muss entsprechend beide Rows löschen und 8 Pages schreiben (statt bisher 4). diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md index b99466f..7eb832e 100644 --- a/doc/06_nvm_config.md +++ b/doc/06_nvm_config.md @@ -2,113 +2,33 @@ **Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` -## Flash-Layout +## Flash-Layout (5 Rows, 0x1FB00–0x1FFFF) | 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) | +| 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 | 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 | -Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. +Alle Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. Config und Makros liegen in vollständig getrennten, zusammenhängenden Row-Blöcken. -## SDeviceConfig – Byte-Layout (223 Byte, packed) +## SDeviceConfig – Byte-Layout (740 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–248 (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. - ---- - -## Geplante Erweiterung: NVM v3 - -### Motivation - -Das bisherige Layout (2 Rows, 512 B) stößt an mehrere Grenzen: - -- **Makro-Steps zu kurz** — 4 Steps reichen für komplexe Shortcuts (z.B. Excel-Ribbon-Navigation: Alt → Buchstabe → Buchstabe → ...) nicht aus. Ziel: 8 Steps. -- **Keine Profile** — Eine einzige Config erlaubt keine Umschaltung zwischen Layouts (z.B. Coding vs. Tabellenkalkulation). Ziel: 3 unabhängige Profile. -- **Keine Helligkeitssteuerung** — Weder global noch pro LED einstellbar. Beide Ebenen sollen konfigurierbar werden. -- **Encoder-Sensitivity** — Schrittweite pro Encoder soll konfigurierbar sein. - -Das bisherige Layout hat außerdem Config und Macros in denselben Adressbereich gemischt (`0x1FE00` Config, `0x1FF00` Macros). Das neue Layout trennt beide Bereiche sauber. - -### Neues Flash-Layout (5 Rows, 0x1FB00–0x1FFFF) - -| Row | Adresse | Größe | Inhalt | -|---|---|---|---| -| 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 | Globaler Header + Profil 0 (Bytes 0–255) | -| Config Row 1 | `0x1FE00` | 256 B | Profil 0 (Rest) + Profil 1 (Bytes 256–511) | -| Config Row 2 | `0x1FF00` | 256 B | Profil 1 (Rest) + Profil 2 + Reserve (Bytes 512–767) | - -Macros und Config liegen in vollständig getrennten, jeweils zusammenhängenden Row-Blöcken. - -### Config-Inhalt (768 B, davon 740 B genutzt, 28 B Reserve) - -**Globaler Header (32 B, Offset 0):** +### Globaler Header (32 B, Offset 0) | Offset | Größe | Feld | |---|---|---| | 0 | 4 | `magic` = `0x56503203` ('VP2\x03') | | 4 | 1 | `version` = 3 | -| 5 | 2 | `crc` – CRC16-CCITT über alle Nutzdaten (ab Byte 7) | +| 5 | 2 | `crc` – CRC16-CCITT über Bytes 7–739 | | 7 | 1 | `active_profile` (0–2) | | 8 | 1 | `global_brightness` (0–255) | -| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder) | +| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder, Default 1) | | 13 | 19 | Reserve | -**Pro Profil (236 B, Offset `32 + idx × 236`):** +### Pro Profil (236 B, Offset `32 + idx × 236`) | Offset | Größe | Feld | |---|---|---| @@ -117,14 +37,71 @@ Macros und Config liegen in vollständig getrennten, jeweils zusammenhängenden | 96 | 20 | `led_r[20]` | | 116 | 20 | `led_g[20]` | | 136 | 20 | `led_b[20]` | -| 156 | 20 | `led_brightness[20]` ← neu | -| 176 | 20 | `led_anim[20]` | -| 196 | 40 | `led_period_ms[20]` | +| 156 | 20 | `led_brightness[20]` – per-LED Helligkeit (0–255) | +| 176 | 20 | `led_anim[20]` – LEDAnim-Typ als uint8_t | +| 196 | 40 | `led_period_ms[20]` – uint16_t little-endian | -### Makro-Tabelle (512 B) +Gesamt: 32 B Header + 3 × 236 B Profile = **740 B**. -32 Slots × **8 Steps** × 2 B = 512 B. Gegenüber v2 doppelt so viele Steps (4 → 8), Slot-Anzahl und Struktur bleiben gleich. Siehe [04_macro_system.md](04_macro_system.md). +`__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. -### Migration von v2 +## CRC16-CCITT -Beim Laden: wenn `magic` oder `version` nicht zu v3 passen, werden Defaults geladen (kein Migrations-Pfad von v2 → v3, da das Layout inkompatibel ist). Eine einmalige Neukonfiguration nach dem Firmware-Update ist nötig. +- Polynom: `0x1021`, Init: `0xFFFF` +- Berechnet über Bytes 7–739 (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 +``` + +Kein Absturz bei ungültiger Config – Defaults greifen immer. + +## 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` + +## Schreib-Logik (nvm_config_save) + +`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). + +SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Ablauf: + +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` + +### nvm_wait() – Timeout + +```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; +} +``` + +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. + +> `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 lokalem `uint8_t buf[] __attribute__((aligned(4)))` + `memcpy` übergeben. diff --git a/doc/07_serial_protocol.md b/doc/07_serial_protocol.md index 88234d8..4ef385a 100644 --- a/doc/07_serial_protocol.md +++ b/doc/07_serial_protocol.md @@ -36,7 +36,7 @@ Byte 5–7: reserviert (0x00) | `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 | +| `0x22` | MACRO_COMMIT | NVM schreiben → MACRO_ACK oder MACRO_NACK | | `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück | ## Event-Referenz (Board → PC) @@ -49,7 +49,7 @@ Byte 5–7: reserviert (0x00) | `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 | +| `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[2–7] = 6 B (Config-Dump) | | `0x94` | CONFIG_END | Config-Dump abgeschlossen | @@ -57,14 +57,15 @@ Byte 5–7: reserviert (0x00) | `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 | +| `0x99` | MACRO_NACK | Makro-Tabelle: NVM-Timeout – nicht geschrieben | ## Chunked Transfer -Config (223 B) und Makro-Tabelle (256 B) werden in 6-Byte-Chunks übertragen: +Config (740 B) und Makro-Tabelle (512 B) werden in 6-Byte-Chunks übertragen: ``` -Config: ceil(223 / 6) = 38 Chunks -Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes) +Config: ceil(740 / 6) = 124 Chunks +Makros: ceil(512 / 6) = 86 Chunks (letzter Chunk hat 2 Nutzbytes) ``` Ablauf (PC → Board): @@ -76,8 +77,13 @@ 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. +**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`. + +**MACRO_COMMIT**: Kein CRC, Board schreibt direkt. Bei Erfolg → `MACRO_ACK`. Bei NVM-Timeout → `MACRO_NACK`. + +### ACK-Synchronisation (GUI-Seite) + +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). ## Implementierungsdetails diff --git a/src/CMainController.cpp b/src/CMainController.cpp index 54ad1aa..63896f6 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -255,13 +255,16 @@ void CMainController::poll_vendor() cfg.version == NVM_CONFIG_VERSION && cfg.crc == nvm_config_crc(cfg)) { - nvm_config_save(cfg); - init_buttons(); - usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden + if (nvm_config_save(cfg)) { + init_buttons(); + usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden + } else { + usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout + } } else { - usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden + usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler } } break; @@ -288,8 +291,11 @@ void CMainController::poll_vendor() if (m_macro_receiving) { m_macro_receiving = false; memcpy(&m_macros, m_macro_buf, sizeof(m_macros)); - macro_config_save(m_macros); - usb_serial_send(USB_EVT_MACRO_ACK, 0); + if (macro_config_save(m_macros)) { + usb_serial_send(USB_EVT_MACRO_ACK, 0); + } else { + usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout + } } break; @@ -439,8 +445,10 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id) target = (cfg.active_profile + 1) % 3; // Zyklus: 0→1→2→0 if (target > 2) break; cfg.active_profile = target; - nvm_config_save(cfg); - init_buttons(); + cfg.crc = nvm_config_crc(cfg); // CRC nach Änderung aktualisieren + if (nvm_config_save(cfg)) + init_buttons(); + // Bei NVM-Timeout: kein Profil-Wechsel (Config unverändert in NVM) break; } diff --git a/src/config/macro_config.cpp b/src/config/macro_config.cpp index ddb8391..2d83f83 100644 --- a/src/config/macro_config.cpp +++ b/src/config/macro_config.cpp @@ -9,28 +9,37 @@ static const uint32_t k_macro_addr = 0x1FB00UL; // Row 0+1 (zwei Rows à 256B) -static void nvm_wait() { while (!NVMCTRL->INTFLAG.bit.READY) {} } -static void nvm_exec(uint16_t cmd) +static bool nvm_wait() +{ + // ~400ms Timeout bei 48MHz, konservativ 4 Zyklen pro Loop-Iteration + uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000 + while (!NVMCTRL->INTFLAG.bit.READY) { + if (--timeout == 0) return false; + } + return true; +} + +static bool nvm_exec(uint16_t cmd) { NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; - nvm_wait(); + return nvm_wait(); } -static void nvm_erase_row(uint32_t addr) +static bool nvm_erase_row(uint32_t addr) { - nvm_wait(); + if (!nvm_wait()) return false; NVMCTRL->ADDR.reg = addr / 2; - nvm_exec(NVMCTRL_CTRLA_CMD_ER); + return nvm_exec(NVMCTRL_CTRLA_CMD_ER); } -static void nvm_write_page(uint32_t addr, const uint8_t* data) +static bool nvm_write_page(uint32_t addr, const uint8_t* data) { - nvm_exec(NVMCTRL_CTRLA_CMD_PBC); + if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false; volatile uint32_t* dst = reinterpret_cast(addr); const uint32_t* src = reinterpret_cast(data); for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i]; NVMCTRL->ADDR.reg = addr / 2; - nvm_exec(NVMCTRL_CTRLA_CMD_WP); + return nvm_exec(NVMCTRL_CTRLA_CMD_WP); } bool macro_config_load(SMacroTable& tbl) @@ -50,7 +59,7 @@ bool macro_config_load(SMacroTable& tbl) return true; } -void macro_config_save(const SMacroTable& tbl) +bool macro_config_save(const SMacroTable& tbl) { // Auf 4-Byte-ausgerichteten Puffer kopieren bevor nvm_write_page ihn als uint32_t* liest. // SMacroTable ist __attribute__((packed)) und könnte unaligned liegen → @@ -61,11 +70,12 @@ void macro_config_save(const SMacroTable& tbl) NVMCTRL->CTRLB.bit.MANW = 1; // Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00) - nvm_erase_row(k_macro_addr); - nvm_erase_row(k_macro_addr + 256); + if (!nvm_erase_row(k_macro_addr)) return false; + if (!nvm_erase_row(k_macro_addr + 256)) return false; // 8 Pages à 64B schreiben for (uint8_t p = 0; p < 8; p++) { - nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64); + if (!nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64)) return false; } + return true; } diff --git a/src/config/macro_config.h b/src/config/macro_config.h index e2ec15f..77f2b2c 100644 --- a/src/config/macro_config.h +++ b/src/config/macro_config.h @@ -33,4 +33,5 @@ struct __attribute__((packed)) SMacroTable bool macro_config_load(SMacroTable& tbl); // Makro-Tabelle in NVM schreiben (löscht Row 0+1, schreibt 8 Pages). -void macro_config_save(const SMacroTable& tbl); +// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird. +bool macro_config_save(const SMacroTable& tbl); diff --git a/src/config/nvm_config.cpp b/src/config/nvm_config.cpp index 27e1477..c92fa13 100644 --- a/src/config/nvm_config.cpp +++ b/src/config/nvm_config.cpp @@ -8,35 +8,44 @@ static const uint32_t k_config_addr = 0x1FD00UL; // Row 0–2 der Config // ── NVMCTRL-Hilfsfunktionen ─────────────────────────────────────────────────── +// +// nvm_wait() hat einen Timeout (~400ms bei 48MHz) damit das Board nicht +// einfriert wenn der NVMCTRL aus unbekanntem Grund nicht READY meldet. +// (Beobachtet nach bestimmten Bootloader-Firmware-Flash-Zyklen auf SAMD21.) -static void nvm_wait() +static bool nvm_wait() { - while (!NVMCTRL->INTFLAG.bit.READY) {} + // ~400ms Timeout bei 48MHz, konservativ 4 Zyklen pro Loop-Iteration + uint32_t timeout = 48000000UL / 4 * 400 / 1000; // ≈ 4 800 000 + while (!NVMCTRL->INTFLAG.bit.READY) { + if (--timeout == 0) return false; + } + return true; } -static void nvm_exec(uint16_t cmd) +static bool nvm_exec(uint16_t cmd) { NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; - nvm_wait(); + return nvm_wait(); } -static void nvm_erase_row(uint32_t addr) +static bool nvm_erase_row(uint32_t addr) { - nvm_wait(); + if (!nvm_wait()) return false; NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte) - nvm_exec(NVMCTRL_CTRLA_CMD_ER); + return nvm_exec(NVMCTRL_CTRLA_CMD_ER); } -static void nvm_write_page(uint32_t addr, const uint8_t* data) +static bool nvm_write_page(uint32_t addr, const uint8_t* data) { - nvm_exec(NVMCTRL_CTRLA_CMD_PBC); + if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false; volatile uint32_t* dst = reinterpret_cast(addr); const uint32_t* src = reinterpret_cast(data); for (uint8_t i = 0; i < 64 / 4; i++) { dst[i] = src[i]; } NVMCTRL->ADDR.reg = addr / 2; - nvm_exec(NVMCTRL_CTRLA_CMD_WP); + return nvm_exec(NVMCTRL_CTRLA_CMD_WP); } // ── CRC16-CCITT (Poly 0x1021) ───────────────────────────────────────────────── @@ -115,7 +124,7 @@ bool nvm_config_load(SDeviceConfig& cfg) // ── Speichern ───────────────────────────────────────────────────────────────── -void nvm_config_save(const SDeviceConfig& cfg) +bool nvm_config_save(const SDeviceConfig& cfg) { // Config (740B) in 768B-Puffer kopieren (3 Rows), Rest mit 0xFF füllen. // __attribute__((aligned(4))) ist zwingend: nvm_write_page castet zu uint32_t*. @@ -126,12 +135,13 @@ void nvm_config_save(const SDeviceConfig& cfg) NVMCTRL->CTRLB.bit.MANW = 1; // 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00) - nvm_erase_row(k_config_addr); - nvm_erase_row(k_config_addr + 256); - nvm_erase_row(k_config_addr + 512); + if (!nvm_erase_row(k_config_addr)) return false; + if (!nvm_erase_row(k_config_addr + 256)) return false; + if (!nvm_erase_row(k_config_addr + 512)) return false; // 12 Pages à 64B schreiben for (uint8_t p = 0; p < 12; p++) { - nvm_write_page(k_config_addr + p * 64, row + p * 64); + if (!nvm_write_page(k_config_addr + p * 64, row + p * 64)) return false; } + return true; } diff --git a/src/config/nvm_config.h b/src/config/nvm_config.h index bd77054..d8a2cd0 100644 --- a/src/config/nvm_config.h +++ b/src/config/nvm_config.h @@ -68,7 +68,8 @@ void nvm_config_defaults(SDeviceConfig& cfg); bool nvm_config_load(SDeviceConfig& cfg); // Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages). -void nvm_config_save(const SDeviceConfig& cfg); +// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird (Board hängt nicht). +bool nvm_config_save(const SDeviceConfig& cfg); // CRC16 über die Nutzdaten der Config (Bytes 7–739, nach dem crc-Feld) uint16_t nvm_config_crc(const SDeviceConfig& cfg); diff --git a/src/hal/usb_serial.h b/src/hal/usb_serial.h index bbeb2ab..83618d7 100644 --- a/src/hal/usb_serial.h +++ b/src/hal/usb_serial.h @@ -54,6 +54,7 @@ #define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B #define USB_EVT_CONFIG_END 0x94 // Config-Dump abgeschlossen #define USB_EVT_MACRO_ACK 0x95 // Makro-Tabelle erfolgreich gespeichert +#define USB_EVT_MACRO_NACK 0x99 // Makro-Tabelle: NVM-Fehler – nicht geschrieben #define USB_EVT_MACRO_BEGIN 0x96 // Beginn Makro-Dump: Data[1] = Chunk-Anzahl #define USB_EVT_MACRO_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B #define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen