8 Step macro and profile switching fully working

This commit is contained in:
Julian Appel 2026-04-13 22:34:54 +02:00
parent 098a166a9f
commit 433d61c29f
10 changed files with 204 additions and 193 deletions

View File

@ -12,17 +12,18 @@ struct __attribute__((packed)) SAction {
// Gesamt: 3 Bytes (packed! ohne packed wären es 4 durch Alignment) // 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 ## ActionType
| Typ | Bedeutung | data-Inhalt | | Typ | Wert | Bedeutung | data-Inhalt |
|---|---|---| |---|---|---|---|
| `NONE` | Keine Aktion | — | | `NONE` | 0 | Keine Aktion | — |
| `HID_KEY` | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier | | `HID_KEY` | 1 | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier |
| `HID_CONSUMER` | Consumer Control (Volume, Media, …) | Consumer Usage ID | | `HID_CONSUMER` | 2 | Consumer Control (Volume, Media, …) | Consumer Usage ID |
| `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | | `HOST_COMMAND` | 3 | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) |
| `MACRO` | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 | | `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) |
## execute_action_down() — Taste gedrückt (Hold-Start) ## 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()` | | `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)` | | `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) | | `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 | | `NONE` | nop |
## execute_action_up() — Taste losgelassen (Hold-Ende) ## execute_action_up() — Taste losgelassen (Hold-Ende)
@ -41,7 +43,23 @@ struct __attribute__((packed)) SAction {
| `HID_KEY` | `usb_hid_release_key()` | | `HID_KEY` | `usb_hid_release_key()` |
| `HID_CONSUMER` | `usb_hid_release_consumer()` | | `HID_CONSUMER` | `usb_hid_release_consumer()` |
| `HOST_COMMAND` | — (optional: könnte `USB_EVT_KEY_UP` senden) | | `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) ## Hold-Modell (HID-Keys und Consumer Controls)

View File

@ -10,18 +10,24 @@ struct __attribute__((packed)) SMacroStep {
uint8_t modifier; // HID Modifier: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08 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 { 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 ## NVM-Speicherort
- **Row 1**: Adresse `0x1FF00`, 256 Byte | Row | Adresse | Inhalt |
- Vom Linkerscript reserviert (nicht überschreibbar durch Code) |---|---|---|
- Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt false zurück → leere Tabelle (alle Keycodes 0) | Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0255 |
| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256511 |
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) ## Slot-Zuweisung (Konvention, Board speichert blind)
@ -34,21 +40,22 @@ Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine
**Laden** (`macro_config_load`): **Laden** (`macro_config_load`):
- `memcpy` direkt aus Flash-Adresse in RAM-Struct - `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`): **Speichern** (`macro_config_save`) — gibt `bool` zurück:
- SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!) - SMacroTable in `uint8_t aligned_buf[512] __attribute__((aligned(4)))` kopieren (Pflicht!)
- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) - `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus)
- Row 1 löschen (`nvm_erase_row`) - Beide Rows löschen (`nvm_erase_row`) — bei NVM-Timeout: `return false`
- 4 Pages à 64 Byte schreiben (`nvm_write_page`) - 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). > **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 (031) slot = action.data (031)
für Step 03: für Step 07:
if step.keycode == 0: abbrechen if step.keycode == 0: abbrechen
HID Key-Down (keycode, modifier) HID Key-Down (keycode, modifier)
delay(10 ms) delay(10 ms)
@ -57,31 +64,3 @@ für Step 03:
``` ```
Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung. 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 0255 |
| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256511 |
`macro_config_save` muss entsprechend beide Rows löschen und 8 Pages schreiben (statt bisher 4).

View File

@ -2,113 +2,33 @@
**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` **Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp`
## Flash-Layout ## Flash-Layout (5 Rows, 0x1FB000x1FFFF)
| Row | Adresse | Größe | Inhalt | | Row | Adresse | Größe | Inhalt |
|---|---|---|---| |---|---|---|---|
| Row 0 | `0x1FE00` | 256 B | SDeviceConfig (223 B genutzt, 33 B Padding) | | Macro Row 0 | `0x1FB00` | 256 B | SMacroTable Bytes 0255 |
| Row 1 | `0x1FF00` | 256 B | SMacroTable (256 B, komplett genutzt) | | 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 |
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 | ### Globaler Header (32 B, Offset 0)
|---|---|---|
| 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 7248 (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.
---
## 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, 0x1FB000x1FFFF)
| Row | Adresse | Größe | 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 (Bytes 0255) |
| Config Row 1 | `0x1FE00` | 256 B | Profil 0 (Rest) + Profil 1 (Bytes 256511) |
| Config Row 2 | `0x1FF00` | 256 B | Profil 1 (Rest) + Profil 2 + Reserve (Bytes 512767) |
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):**
| Offset | Größe | Feld | | Offset | Größe | Feld |
|---|---|---| |---|---|---|
| 0 | 4 | `magic` = `0x56503203` ('VP2\x03') | | 0 | 4 | `magic` = `0x56503203` ('VP2\x03') |
| 4 | 1 | `version` = 3 | | 4 | 1 | `version` = 3 |
| 5 | 2 | `crc` CRC16-CCITT über alle Nutzdaten (ab Byte 7) | | 5 | 2 | `crc` CRC16-CCITT über Bytes 7739 |
| 7 | 1 | `active_profile` (02) | | 7 | 1 | `active_profile` (02) |
| 8 | 1 | `global_brightness` (0255) | | 8 | 1 | `global_brightness` (0255) |
| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder) | | 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder, Default 1) |
| 13 | 19 | Reserve | | 13 | 19 | Reserve |
**Pro Profil (236 B, Offset `32 + idx × 236`):** ### Pro Profil (236 B, Offset `32 + idx × 236`)
| Offset | Größe | Feld | | Offset | Größe | Feld |
|---|---|---| |---|---|---|
@ -117,14 +37,71 @@ Macros und Config liegen in vollständig getrennten, jeweils zusammenhängenden
| 96 | 20 | `led_r[20]` | | 96 | 20 | `led_r[20]` |
| 116 | 20 | `led_g[20]` | | 116 | 20 | `led_g[20]` |
| 136 | 20 | `led_b[20]` | | 136 | 20 | `led_b[20]` |
| 156 | 20 | `led_brightness[20]` ← neu | | 156 | 20 | `led_brightness[20]` per-LED Helligkeit (0255) |
| 176 | 20 | `led_anim[20]` | | 176 | 20 | `led_anim[20]` LEDAnim-Typ als uint8_t |
| 196 | 40 | `led_period_ms[20]` | | 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 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
```
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.

View File

@ -36,7 +36,7 @@ Byte 57: reserviert (0x00)
| `0x13` | CONFIG_READ | Board sendet aktuelle NVM-Config zurück (BEGIN/DATA/END) | | `0x13` | CONFIG_READ | Board sendet aktuelle NVM-Config zurück (BEGIN/DATA/END) |
| `0x20` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl neuen Makro-Empfang starten | | `0x20` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl neuen Makro-Empfang starten |
| `0x21` | MACRO_DATA | Byte[1] = Chunk-Index, Byte[27] = 6 B Nutzdaten | | `0x21` | MACRO_DATA | Byte[1] = Chunk-Index, Byte[27] = 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 | | `0x23` | MACRO_READ | Board sendet aktuelle Makro-Tabelle zurück |
## Event-Referenz (Board → PC) ## Event-Referenz (Board → PC)
@ -49,7 +49,7 @@ Byte 57: reserviert (0x00)
| `0x84` | ENC_CCW | enc_id Encoder-Schritt CCW (HOST_COMMAND) | | `0x84` | ENC_CCW | enc_id Encoder-Schritt CCW (HOST_COMMAND) |
| `0x85` | PONG | Antwort auf PING | | `0x85` | PONG | Antwort auf PING |
| `0x90` | CONFIG_ACK | Config erfolgreich in NVM geschrieben | | `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) | | `0x92` | CONFIG_BEGIN | Byte[1] = Chunk-Anzahl (Config-Dump) |
| `0x93` | CONFIG_DATA | Byte[1] = Index, Byte[27] = 6 B (Config-Dump) | | `0x93` | CONFIG_DATA | Byte[1] = Index, Byte[27] = 6 B (Config-Dump) |
| `0x94` | CONFIG_END | Config-Dump abgeschlossen | | `0x94` | CONFIG_END | Config-Dump abgeschlossen |
@ -57,14 +57,15 @@ Byte 57: reserviert (0x00)
| `0x96` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl (Makro-Dump) | | `0x96` | MACRO_BEGIN | Byte[1] = Chunk-Anzahl (Makro-Dump) |
| `0x97` | MACRO_DATA | Byte[1] = Index, Byte[27] = 6 B (Makro-Dump) | | `0x97` | MACRO_DATA | Byte[1] = Index, Byte[27] = 6 B (Makro-Dump) |
| `0x98` | MACRO_END | Makro-Dump abgeschlossen | | `0x98` | MACRO_END | Makro-Dump abgeschlossen |
| `0x99` | MACRO_NACK | Makro-Tabelle: NVM-Timeout nicht geschrieben |
## Chunked Transfer ## 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 Config: ceil(740 / 6) = 124 Chunks
Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes) Makros: ceil(512 / 6) = 86 Chunks (letzter Chunk hat 2 Nutzbytes)
``` ```
Ablauf (PC → Board): Ablauf (PC → Board):
@ -76,8 +77,13 @@ DATA chunk_1 (Bytes 611)
COMMIT COMMIT
``` ```
COMMIT bei Config: Board prüft Magic + Version + CRC. Bei Fehler → NACK, kein NVM-Schreiben. **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`.
COMMIT bei Makro: Kein CRC, Board schreibt blind → MACRO_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 ## Implementierungsdetails

View File

@ -255,13 +255,16 @@ void CMainController::poll_vendor()
cfg.version == NVM_CONFIG_VERSION && cfg.version == NVM_CONFIG_VERSION &&
cfg.crc == nvm_config_crc(cfg)) cfg.crc == nvm_config_crc(cfg))
{ {
nvm_config_save(cfg); if (nvm_config_save(cfg)) {
init_buttons(); init_buttons();
usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden usb_serial_send(USB_EVT_CONFIG_ACK, 0); // Erfolg melden
} else {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // NVM-Timeout
}
} }
else else
{ {
usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden usb_serial_send(USB_EVT_CONFIG_NACK, 0); // CRC/Magic-Fehler
} }
} }
break; break;
@ -288,8 +291,11 @@ void CMainController::poll_vendor()
if (m_macro_receiving) { if (m_macro_receiving) {
m_macro_receiving = false; m_macro_receiving = false;
memcpy(&m_macros, m_macro_buf, sizeof(m_macros)); memcpy(&m_macros, m_macro_buf, sizeof(m_macros));
macro_config_save(m_macros); if (macro_config_save(m_macros)) {
usb_serial_send(USB_EVT_MACRO_ACK, 0); usb_serial_send(USB_EVT_MACRO_ACK, 0);
} else {
usb_serial_send(USB_EVT_MACRO_NACK, 0); // NVM-Timeout
}
} }
break; 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 target = (cfg.active_profile + 1) % 3; // Zyklus: 0→1→2→0
if (target > 2) break; if (target > 2) break;
cfg.active_profile = target; cfg.active_profile = target;
nvm_config_save(cfg); cfg.crc = nvm_config_crc(cfg); // CRC nach Änderung aktualisieren
init_buttons(); if (nvm_config_save(cfg))
init_buttons();
// Bei NVM-Timeout: kein Profil-Wechsel (Config unverändert in NVM)
break; break;
} }

View File

@ -9,28 +9,37 @@
static const uint32_t k_macro_addr = 0x1FB00UL; // Row 0+1 (zwei Rows à 256B) static const uint32_t k_macro_addr = 0x1FB00UL; // Row 0+1 (zwei Rows à 256B)
static void nvm_wait() { while (!NVMCTRL->INTFLAG.bit.READY) {} } static bool nvm_wait()
static void nvm_exec(uint16_t cmd) {
// ~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; 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->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<volatile uint32_t*>(addr); volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(data); const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i]; for (uint8_t i = 0; i < 64 / 4; i++) dst[i] = src[i];
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_WP); return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
bool macro_config_load(SMacroTable& tbl) bool macro_config_load(SMacroTable& tbl)
@ -50,7 +59,7 @@ bool macro_config_load(SMacroTable& tbl)
return true; 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. // Auf 4-Byte-ausgerichteten Puffer kopieren bevor nvm_write_page ihn als uint32_t* liest.
// SMacroTable ist __attribute__((packed)) und könnte unaligned liegen → // 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; NVMCTRL->CTRLB.bit.MANW = 1;
// Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00) // Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00)
nvm_erase_row(k_macro_addr); if (!nvm_erase_row(k_macro_addr)) return false;
nvm_erase_row(k_macro_addr + 256); if (!nvm_erase_row(k_macro_addr + 256)) return false;
// 8 Pages à 64B schreiben // 8 Pages à 64B schreiben
for (uint8_t p = 0; p < 8; p++) { 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;
} }

View File

@ -33,4 +33,5 @@ struct __attribute__((packed)) SMacroTable
bool macro_config_load(SMacroTable& tbl); bool macro_config_load(SMacroTable& tbl);
// Makro-Tabelle in NVM schreiben (löscht Row 0+1, schreibt 8 Pages). // 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);

View File

@ -8,35 +8,44 @@
static const uint32_t k_config_addr = 0x1FD00UL; // Row 02 der Config static const uint32_t k_config_addr = 0x1FD00UL; // Row 02 der Config
// ── NVMCTRL-Hilfsfunktionen ─────────────────────────────────────────────────── // ── 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; 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) 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<volatile uint32_t*>(addr); volatile uint32_t* dst = reinterpret_cast<volatile uint32_t*>(addr);
const uint32_t* src = reinterpret_cast<const uint32_t*>(data); const uint32_t* src = reinterpret_cast<const uint32_t*>(data);
for (uint8_t i = 0; i < 64 / 4; i++) { for (uint8_t i = 0; i < 64 / 4; i++) {
dst[i] = src[i]; dst[i] = src[i];
} }
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
nvm_exec(NVMCTRL_CTRLA_CMD_WP); return nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
// ── CRC16-CCITT (Poly 0x1021) ───────────────────────────────────────────────── // ── CRC16-CCITT (Poly 0x1021) ─────────────────────────────────────────────────
@ -115,7 +124,7 @@ bool nvm_config_load(SDeviceConfig& cfg)
// ── Speichern ───────────────────────────────────────────────────────────────── // ── 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. // 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*. // __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; NVMCTRL->CTRLB.bit.MANW = 1;
// 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00) // 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00)
nvm_erase_row(k_config_addr); if (!nvm_erase_row(k_config_addr)) return false;
nvm_erase_row(k_config_addr + 256); if (!nvm_erase_row(k_config_addr + 256)) return false;
nvm_erase_row(k_config_addr + 512); if (!nvm_erase_row(k_config_addr + 512)) return false;
// 12 Pages à 64B schreiben // 12 Pages à 64B schreiben
for (uint8_t p = 0; p < 12; p++) { 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;
} }

View File

@ -68,7 +68,8 @@ void nvm_config_defaults(SDeviceConfig& cfg);
bool nvm_config_load(SDeviceConfig& cfg); bool nvm_config_load(SDeviceConfig& cfg);
// Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages). // 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 7739, nach dem crc-Feld) // CRC16 über die Nutzdaten der Config (Bytes 7739, nach dem crc-Feld)
uint16_t nvm_config_crc(const SDeviceConfig& cfg); uint16_t nvm_config_crc(const SDeviceConfig& cfg);

View File

@ -54,6 +54,7 @@
#define USB_EVT_CONFIG_DATA 0x93 // Config-Chunk: Data[1] = Index, Data[2..7] = 6B #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_CONFIG_END 0x94 // Config-Dump abgeschlossen
#define USB_EVT_MACRO_ACK 0x95 // Makro-Tabelle erfolgreich gespeichert #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_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_DATA 0x97 // Makro-Chunk: Data[1] = Index, Data[2..7] = 6B
#define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen #define USB_EVT_MACRO_END 0x98 // Makro-Dump abgeschlossen