Compare commits

..

No commits in common. "433d61c29fa46c54afd50c0c22f05498fd541646" and "3e83758f05115e24718f1738ca9f4d21c4b9657d" have entirely different histories.

13 changed files with 202 additions and 364 deletions

View File

@ -96,8 +96,6 @@ Der Upload läuft via OpenOCD über SWD. Kein Bootloader nötig der Chip wir
| 4.4 | **ActionType HOST_COMMAND**: Event-ID an VersaGUI senden, App führt aus | ✅ | | 4.4 | **ActionType HOST_COMMAND**: Event-ID an VersaGUI senden, App führt aus | ✅ |
| 4.5 | **ActionType MACRO**: Sequenz aus bis zu 4 HID-Key-Schritten aus NVM-Tabelle abspielen | ✅ | | 4.5 | **ActionType MACRO**: Sequenz aus bis zu 4 HID-Key-Schritten aus NVM-Tabelle abspielen | ✅ |
| 4.6 | Jede Aktion ausführbar **ohne laufende VersaGUI** (lokal per HID/Makro) | ✅ | | 4.6 | Jede Aktion ausführbar **ohne laufende VersaGUI** (lokal per HID/Makro) | ✅ |
| 4.7 | **Hold-Modell** für HID_KEY / HID_CONSUMER: KEY_DOWN → Key-Down senden, KEY_UP → Key-Up senden (OS-Repeat ab ~500ms) | ✅ |
| 4.8 | **Tap-Modell** für Encoder CW/CCW: atomare down+delay(10)+up Sequenz (diskrete Events, kein Hold möglich) | ✅ |
### 5 Makro-System ### 5 Makro-System

View File

@ -12,18 +12,17 @@ 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) == 740` mit der C#-Serialisierung in VersaGUI übereinstimmt. `packed` ist zwingend damit `sizeof(SDeviceConfig) == 223` mit der C#-Serialisierung in VersaGUI übereinstimmt.
## ActionType ## ActionType
| Typ | Wert | Bedeutung | data-Inhalt | | Typ | Bedeutung | data-Inhalt |
|---|---|---|---| |---|---|---|
| `NONE` | 0 | Keine Aktion | — | | `NONE` | Keine Aktion | — |
| `HID_KEY` | 1 | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier | | `HID_KEY` | Tastendruck via USB HID Keyboard | Low-Byte = HID Keycode, High-Byte = Modifier |
| `HID_CONSUMER` | 2 | Consumer Control (Volume, Media, …) | Consumer Usage ID | | `HID_CONSUMER` | Consumer Control (Volume, Media, …) | Consumer Usage ID |
| `HOST_COMMAND` | 3 | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) | | `HOST_COMMAND` | Event an VersaGUI senden, App führt aus | Command-ID (frei definiert) |
| `MACRO` | 4 | Makro-Sequenz aus NVM-Tabelle | Slot-Index 031 | | `MACRO` | 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)
@ -33,7 +32,6 @@ 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)
@ -43,23 +41,7 @@ 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`/`PROFILE_SWITCH`/`NONE` | nop | | `MACRO`/`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,24 +10,18 @@ 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[MACRO_SLOTS][MACRO_MAX_STEPS]; // 32 × 8 × 2 = 512 Byte SMacroStep steps[32][4]; // 32 Slots × 4 Steps × 2 Byte = 256 Byte
}; };
``` ```
Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 512 == zwei NVM-Rows`. Beide Structs sind `packed` (kein Padding). `sizeof(SMacroTable) == 256 == eine NVM-Row`.
## NVM-Speicherort ## NVM-Speicherort
| Row | Adresse | Inhalt | - **Row 1**: Adresse `0x1FF00`, 256 Byte
|---|---|---| - Vom Linkerscript reserviert (nicht überschreibbar durch Code)
| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0255 | - Gelöschter Flash (`0xFF`-Bytes) → `macro_config_load()` gibt false zurück → leere Tabelle (alle Keycodes 0)
| 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)
@ -40,22 +34,21 @@ Beide Rows sind im Linkerscript reserviert. Gelöschter Flash (`0xFF`-Bytes) →
**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 (alle 0xFF) ist ein akzeptabler Zustand - Kein Magic/CRC (leere Tabelle bei 0xFF ist akzeptabler Zustand)
**Speichern** (`macro_config_save`) — gibt `bool` zurück: **Speichern** (`macro_config_save`):
- SMacroTable in `uint8_t aligned_buf[512] __attribute__((aligned(4)))` kopieren (Pflicht!) - SMacroTable in `uint8_t aligned_buf[256] __attribute__((aligned(4)))` kopieren (Pflicht!)
- `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) - `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus)
- Beide Rows löschen (`nvm_erase_row`) — bei NVM-Timeout: `return false` - Row 1 löschen (`nvm_erase_row`)
- 8 Pages à 64 Byte schreiben (`nvm_write_page`) — bei NVM-Timeout: `return false` - 4 Pages à 64 Byte schreiben (`nvm_write_page`)
- `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_down, ActionType::MACRO) ## Ausführung (in execute_action, ActionType::MACRO)
``` ```
slot = action.data (031) slot = action.data (031)
für Step 07: für Step 03:
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)

View File

@ -2,65 +2,46 @@
**Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp` **Dateien:** `config/nvm_config.h`, `config/nvm_config.cpp`
## Flash-Layout (5 Rows, 0x1FB000x1FFFF) ## Flash-Layout
| Row | Adresse | Größe | Inhalt | | Row | Adresse | Größe | Inhalt |
|---|---|---|---| |---|---|---|---|
| Macro Row 0 | `0x1FB00` | 256 B | SMacroTable Bytes 0255 | | Row 0 | `0x1FE00` | 256 B | SDeviceConfig (223 B genutzt, 33 B Padding) |
| Macro Row 1 | `0x1FC00` | 256 B | SMacroTable Bytes 256511 | | Row 1 | `0x1FF00` | 256 B | SMacroTable (256 B, komplett genutzt) |
| 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 |
Alle Rows sind im Linkerscript vom Code-Bereich ausgeschlossen. Config und Makros liegen in vollständig getrennten, zusammenhängenden Row-Blöcken. Beide Rows sind im Linkerscript vom Code-Bereich ausgeschlossen.
## SDeviceConfig Byte-Layout (740 Byte, packed) ## SDeviceConfig Byte-Layout (223 Byte, packed)
### Globaler Header (32 B, Offset 0)
| Offset | Größe | Feld | | Offset | Größe | Feld |
|---|---|---| |---|---|---|
| 0 | 4 | `magic` = `0x56503203` ('VP2\x03') | | 0 | 4 | `magic` = `0x56503202` ('VP2\x02') |
| 4 | 1 | `version` = 3 | | 4 | 1 | `version` = 2 |
| 5 | 2 | `crc` CRC16-CCITT über Bytes 7739 | | 5 | 2 | `crc` CRC16-CCITT über Bytes 7222 |
| 7 | 1 | `active_profile` (02) | | 7 | 60 | `mx_actions[20]` 20 × 3 B SAction |
| 8 | 1 | `global_brightness` (0255) | | 67 | 36 | `enc_actions[4][3]` 12 × 3 B SAction |
| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder, Default 1) | | 103 | 20 | `led_r[20]` |
| 13 | 19 | Reserve | | 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 |
### Pro Profil (236 B, Offset `32 + idx × 236`) `__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.
| Offset | Größe | 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 |
Gesamt: 32 B Header + 3 × 236 B Profile = **740 B**.
`__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 ## CRC16-CCITT
- Polynom: `0x1021`, Init: `0xFFFF` - Polynom: `0x1021`, Init: `0xFFFF`
- Berechnet über Bytes 7739 (ab `active_profile`, nach dem `crc`-Feld selbst) - Berechnet über Bytes 7248 (ab `mx_actions`, nach dem `crc`-Feld selbst)
- Sichert alle Nutzdaten einschließlich `active_profile` - Sichert Datenintegrität nach NVM-Schreiben und bei Versionswechsel
> **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 ## Lese-Logik
``` ```
memcpy aus Flash-Adresse 0x1FD00 (740 B) memcpy aus Flash-Adresse 0x1FE00
if magic != 0x56503203: Defaults laden, return false if magic != 0x56503202: Defaults laden, return false
if version != 3: Defaults laden, return false if version != 2: Defaults laden, return false
if crc != crc(cfg): Defaults laden, return false if crc != crc(cfg): Defaults laden, return false
if active_profile >= 3: active_profile = 0
return true return true
``` ```
@ -69,39 +50,19 @@ Kein Absturz bei ungültiger Config Defaults greifen immer.
## Defaults ## Defaults
- Alle Aktionen: `NONE` - Alle Aktionen: `NONE`
- LEDs: warm-weiß (R=80, G=40, B=0), `led_brightness=255` - LEDs: warm-weiß (R=80, G=40, B=0)
- Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms - Animation: `COLOR_CYCLE` (Typ 5), Period 4000 ms
- `active_profile = 0`, `global_brightness = 255`, `enc_sensitivity = 1`
## Schreib-Logik (nvm_config_save) ## Schreib-Logik (NVM-Mechanik)
`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. Schreiben erfordert:
1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus, kein Auto-Write)
SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Ablauf: 2. Row löschen (`NVMCTRL_CTRLA_CMD_ER`)
3. Page-Buffer löschen (`NVMCTRL_CTRLA_CMD_PBC`)
1. `NVMCTRL->CTRLB.bit.MANW = 1` (manueller Schreib-Modus) 4. 64 B als `uint32_t*` in Page-Buffer schreiben
2. 3 Rows löschen (`NVMCTRL_CTRLA_CMD_ER`) — bei Fehler: `return false` 5. Page programmieren (`NVMCTRL_CTRLA_CMD_WP`)
3. Für jede der 12 Pages à 64 B: 6. Schritte 35 viermal (für alle 4 Pages)
- 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. > `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. > **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.

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 oder MACRO_NACK | | `0x22` | MACRO_COMMIT | NVM schreiben + MACRO_ACK zurück |
| `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 oder NVM-Timeout nicht geschrieben | | `0x91` | CONFIG_NACK | Config CRC/Magic ungültig 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,15 +57,14 @@ 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 (740 B) und Makro-Tabelle (512 B) werden in 6-Byte-Chunks übertragen: Config (223 B) und Makro-Tabelle (256 B) werden in 6-Byte-Chunks übertragen:
``` ```
Config: ceil(740 / 6) = 124 Chunks Config: ceil(223 / 6) = 38 Chunks
Makros: ceil(512 / 6) = 86 Chunks (letzter Chunk hat 2 Nutzbytes) Makros: ceil(256 / 6) = 43 Chunks (letzter Chunk hat 4 Nutzbytes)
``` ```
Ablauf (PC → Board): Ablauf (PC → Board):
@ -77,13 +76,8 @@ DATA chunk_1 (Bytes 611)
COMMIT 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`. 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.
**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

@ -101,16 +101,13 @@ void CMainController::init_buttons()
bool valid = nvm_config_load(cfg); bool valid = nvm_config_load(cfg);
(void)valid; // false = keine gültige Config → Defaults wurden bereits geladen (void)valid; // false = keine gültige Config → Defaults wurden bereits geladen
// Aktives Profil auswählen (load() sichert bereits 02 ab)
const SDeviceProfile& prof = cfg.profiles[cfg.active_profile];
// Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1)
for (uint8_t enc = 0; enc < 4; enc++) { for (uint8_t enc = 0; enc < 4; enc++) {
m_buttons[enc].init(enc, -1, prof.enc_actions[enc][ENC_ACTION_SW], RGB()); m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW], RGB());
} }
// MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen,
// Aktion + Base-Farbe + Animation aus aktivem Profil. // Aktion + Base-Farbe + Animation aus NVM.
// mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4) // mx_actions[0] ↔ key_id 5 (COL_1/ROW_0), mx_actions[19] ↔ key_id 24 (COL_4/ROW_4)
for (uint8_t key = 5; key < MATRIX_KEYS; key++) { for (uint8_t key = 5; key < MATRIX_KEYS; key++) {
@ -118,21 +115,11 @@ void CMainController::init_buttons()
uint8_t row = key % MATRIX_ROWS; uint8_t row = key % MATRIX_ROWS;
int8_t led = static_cast<int8_t>(LED_INDEX(col, row)); int8_t led = static_cast<int8_t>(LED_INDEX(col, row));
uint8_t mx_idx = key - 5; uint8_t mx_idx = key - 5;
RGB base(cfg.led_r[mx_idx], cfg.led_g[mx_idx], cfg.led_b[mx_idx]);
m_buttons[key].init(key, led, cfg.mx_actions[mx_idx], base);
// Effektive Farbe = base × led_brightness × global_brightness / 255² LEDAnim anim = static_cast<LEDAnim>(cfg.led_anim[mx_idx]);
auto scale = [&](uint8_t val) -> uint8_t { uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.led_period_ms[mx_idx] : 4000;
return (uint8_t)((uint32_t)val
* prof.led_brightness[mx_idx] / 255
* cfg.global_brightness / 255);
};
RGB base(scale(prof.led_r[mx_idx]),
scale(prof.led_g[mx_idx]),
scale(prof.led_b[mx_idx]));
m_buttons[key].init(key, led, prof.mx_actions[mx_idx], base);
LEDAnim anim = static_cast<LEDAnim>(prof.led_anim[mx_idx]);
uint16_t period = prof.led_period_ms[mx_idx] > 0 ? prof.led_period_ms[mx_idx] : 4000;
if (anim == LEDAnim::COLOR_CYCLE) { if (anim == LEDAnim::COLOR_CYCLE) {
// Phase gleichmäßig verteilen → stehender Regenbogen dreht sich // Phase gleichmäßig verteilen → stehender Regenbogen dreht sich
@ -146,8 +133,8 @@ void CMainController::init_buttons()
// Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt // Encoder CW/CCW-Aktionen separat merken Encoder haben kein CButton-Objekt
// da sie keine LED haben und kein Matrix-Key sind. // da sie keine LED haben und kein Matrix-Key sind.
for (uint8_t enc = 0; enc < 4; enc++) { for (uint8_t enc = 0; enc < 4; enc++) {
m_enc_cw [enc] = prof.enc_actions[enc][ENC_ACTION_CW]; m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW];
m_enc_ccw[enc] = prof.enc_actions[enc][ENC_ACTION_CCW]; m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW];
} }
} }
@ -211,8 +198,8 @@ void CMainController::poll_vendor()
// 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen // 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen
uint16_t offset = (uint16_t)pkt.key_id() * 6; uint16_t offset = (uint16_t)pkt.key_id() * 6;
if (offset < sizeof(m_cfg_buf)) { if (offset < sizeof(m_cfg_buf)) {
uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset); uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset);
uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); if (count > 6) count = 6;
memcpy(m_cfg_buf + offset, &pkt.data[2], count); memcpy(m_cfg_buf + offset, &pkt.data[2], count);
} }
} }
@ -225,9 +212,9 @@ void CMainController::poll_vendor()
SDeviceConfig cfg; SDeviceConfig cfg;
nvm_config_load(cfg); // ungültige NVM → Defaults nvm_config_load(cfg); // ungültige NVM → Defaults
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&cfg);
const uint16_t sz = sizeof(SDeviceConfig); // 740 const uint8_t sz = sizeof(SDeviceConfig); // 223
const uint8_t payload = 6; const uint8_t payload = 6;
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124 uint8_t chunks = (sz + payload - 1) / payload; // 38
usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks);
@ -255,16 +242,13 @@ 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))
{ {
if (nvm_config_save(cfg)) { 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); // CRC/Magic-Fehler usb_serial_send(USB_EVT_CONFIG_NACK, 0); // Fehler melden
} }
} }
break; break;
@ -280,8 +264,8 @@ void CMainController::poll_vendor()
if (m_macro_receiving) { if (m_macro_receiving) {
uint16_t offset = (uint16_t)pkt.key_id() * 6; uint16_t offset = (uint16_t)pkt.key_id() * 6;
if (offset < sizeof(m_macro_buf)) { if (offset < sizeof(m_macro_buf)) {
uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset); uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset);
uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); if (count > 6) count = 6;
memcpy(m_macro_buf + offset, &pkt.data[2], count); memcpy(m_macro_buf + offset, &pkt.data[2], count);
} }
} }
@ -291,11 +275,8 @@ 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));
if (macro_config_save(m_macros)) { 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;
@ -303,9 +284,9 @@ void CMainController::poll_vendor()
case USB_CMD_MACRO_READ: case USB_CMD_MACRO_READ:
{ {
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&m_macros);
const uint16_t sz = sizeof(SMacroTable); // 512 const uint16_t sz = sizeof(SMacroTable); // 256
const uint8_t payload = 6; const uint8_t payload = 6;
uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 86 uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 43
usb_serial_send(USB_EVT_MACRO_BEGIN, chunks); usb_serial_send(USB_EVT_MACRO_BEGIN, chunks);
@ -436,22 +417,6 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id)
break; break;
} }
case ActionType::PROFILE_SWITCH:
{
SDeviceConfig cfg;
nvm_config_load(cfg);
uint8_t target = static_cast<uint8_t>(action.data);
if (target == 0xFF)
target = (cfg.active_profile + 1) % 3; // Zyklus: 0→1→2→0
if (target > 2) break;
cfg.active_profile = target;
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;
}
case ActionType::NONE: case ActionType::NONE:
default: default:
break; break;

View File

@ -12,7 +12,6 @@
#include "hal/usb_hid.h" #include "hal/usb_hid.h"
#include "hal/usb_serial.h" #include "hal/usb_serial.h"
#include "config/action.h" #include "config/action.h"
#include "config/nvm_config.h"
#include "config/macro_config.h" #include "config/macro_config.h"
class CMainController class CMainController
@ -44,12 +43,12 @@ private:
void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben
// ── Config-Empfangspuffer ───────────────────────────────────────────────── // ── Config-Empfangspuffer ─────────────────────────────────────────────────
uint8_t m_cfg_buf[sizeof(SDeviceConfig)]; // 740 Bytes uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes
uint8_t m_cfg_chunks_expected; uint8_t m_cfg_chunks_expected;
bool m_cfg_receiving; bool m_cfg_receiving;
// ── Makro-Empfangspuffer ────────────────────────────────────────────────── // ── Makro-Empfangspuffer ──────────────────────────────────────────────────
uint8_t m_macro_buf[sizeof(SMacroTable)]; // 512 Bytes uint8_t m_macro_buf[256]; // sizeof(SMacroTable) = 256 Bytes
uint8_t m_macro_chunks_expected; uint8_t m_macro_chunks_expected;
bool m_macro_receiving; bool m_macro_receiving;

View File

@ -7,8 +7,7 @@ enum class ActionType : uint8_t
HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet) HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet)
HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …) HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …)
HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …) HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …)
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 8 HID-Keys sequenziell MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 4 HID-Keys sequenziell
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 02); speichert in NVM
}; };
struct __attribute__((packed)) SAction struct __attribute__((packed)) SAction

View File

@ -1,5 +1,5 @@
// macro_config.cpp // macro_config.cpp
// NVM-Zugriff für die Makro-Tabelle (Row 0+1, 0x1FB000x1FCFF, 512 Bytes). // NVM-Zugriff für die Makro-Tabelle (Row 1, 0x1FF00).
// Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert, // Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert,
// da static kein gemeinsamer Header für interne NVM-Helfer). // da static kein gemeinsamer Header für interne NVM-Helfer).
@ -7,46 +7,37 @@
#include <Arduino.h> #include <Arduino.h>
#include <string.h> #include <string.h>
static const uint32_t k_macro_addr = 0x1FB00UL; // Row 0+1 (zwei Rows à 256B) static const uint32_t k_macro_addr = 0x1FF00UL; // Row 1 (256B nach Row 0)
static bool nvm_wait() static void nvm_wait() { while (!NVMCTRL->INTFLAG.bit.READY) {} }
{ 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;
return nvm_wait(); nvm_wait();
} }
static bool nvm_erase_row(uint32_t addr) static void nvm_erase_row(uint32_t addr)
{ {
if (!nvm_wait()) return false; nvm_wait();
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
return nvm_exec(NVMCTRL_CTRLA_CMD_ER); nvm_exec(NVMCTRL_CTRLA_CMD_ER);
} }
static bool nvm_write_page(uint32_t addr, const uint8_t* data) static void nvm_write_page(uint32_t addr, const uint8_t* data)
{ {
if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false; nvm_exec(NVMCTRL_CTRLA_CMD_PBC);
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;
return nvm_exec(NVMCTRL_CTRLA_CMD_WP); nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
bool macro_config_load(SMacroTable& tbl) bool macro_config_load(SMacroTable& tbl)
{ {
memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl)); memcpy(&tbl, reinterpret_cast<const void*>(k_macro_addr), sizeof(tbl));
// Prüfen ob beide Rows noch gelöscht sind (alle 0xFF = nie beschrieben) // Prüfen ob Row 1 noch gelöscht ist (alle 0xFF = nie beschrieben)
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&tbl); const uint8_t* raw = reinterpret_cast<const uint8_t*>(&tbl);
bool all_ff = true; bool all_ff = true;
for (uint16_t i = 0; i < sizeof(tbl); i++) { for (uint16_t i = 0; i < sizeof(tbl); i++) {
@ -59,23 +50,17 @@ bool macro_config_load(SMacroTable& tbl)
return true; return true;
} }
bool macro_config_save(const SMacroTable& tbl) void 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 →
// direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen. // direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen.
uint8_t aligned_buf[512] __attribute__((aligned(4))); uint8_t aligned_buf[256] __attribute__((aligned(4)));
memcpy(aligned_buf, &tbl, sizeof(tbl)); memcpy(aligned_buf, &tbl, sizeof(tbl));
NVMCTRL->CTRLB.bit.MANW = 1; NVMCTRL->CTRLB.bit.MANW = 1;
nvm_erase_row(k_macro_addr);
// Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00) for (uint8_t p = 0; p < 4; p++) {
if (!nvm_erase_row(k_macro_addr)) return false; nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64);
if (!nvm_erase_row(k_macro_addr + 256)) return false;
// 8 Pages à 64B schreiben
for (uint8_t p = 0; p < 8; p++) {
if (!nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64)) return false;
} }
return true;
} }

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
// macro_config.h // macro_config.h
// Makro-Tabelle: 32 Slots, je 8 HID-Key-Steps. // Makro-Tabelle: bis zu 32 Slots, je 4 HID-Key-Steps.
// Gespeichert in NVM Row 0+1 (0x1FB000x1FCFF, 512 Bytes). // Gespeichert in NVM Row 1 (0x1FF00, 256 Bytes).
// //
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind): // Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
// Slot 019 : MX-Buttons (mx_idx) // Slot 019 : MX-Buttons (mx_idx)
@ -13,7 +13,7 @@
#include <stdint.h> #include <stdint.h>
#define MACRO_SLOTS 32 #define MACRO_SLOTS 32
#define MACRO_MAX_STEPS 8 #define MACRO_MAX_STEPS 4
// Ein einzelner HID-Key-Step im Makro // Ein einzelner HID-Key-Step im Makro
struct __attribute__((packed)) SMacroStep struct __attribute__((packed)) SMacroStep
@ -22,16 +22,15 @@ struct __attribute__((packed)) SMacroStep
uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08) uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08)
}; };
// Komplette Makro-Tabelle (32 × 8 × 2 = 512 Bytes = zwei NVM-Rows) // Komplette Makro-Tabelle (32 × 4 × 2 = 256 Bytes = eine NVM-Row)
struct __attribute__((packed)) SMacroTable struct __attribute__((packed)) SMacroTable
{ {
SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS];
}; };
// Makro-Tabelle aus NVM lesen (Row 0+1: 0x1FB00). // Makro-Tabelle aus NVM lesen (Row 1: 0x1FF00).
// Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen. // Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen.
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 1, schreibt 4 Pages).
// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird. void macro_config_save(const SMacroTable& tbl);
bool macro_config_save(const SMacroTable& tbl);

View File

@ -1,60 +1,56 @@
// nvm_config.cpp
// NVM-Zugriff für SDeviceConfig (3 Rows ab 0x1FD00, 768B gesamt, 740B genutzt).
#include "nvm_config.h" #include "nvm_config.h"
#include <Arduino.h> #include <Arduino.h>
#include <string.h> #include <string.h>
static const uint32_t k_config_addr = 0x1FD00UL; // Row 02 der Config // ── Flash-Adresse (aus Linkerscript) ─────────────────────────────────────────
// Kein separates Linker-Symbol nötig Adresse ist fix und bekannt.
static const uint32_t k_config_addr = 0x1FE00UL;
// ── NVMCTRL-Hilfsfunktionen ─────────────────────────────────────────────────── // SAMD21 NVMCTRL ──────────────────────────────────────────────────────────────
// // Row = 256 Bytes = 4 Pages à 64 Bytes
// nvm_wait() hat einen Timeout (~400ms bei 48MHz) damit das Board nicht // Schreiben: Row löschen (ER), dann seitenweise schreiben (WP)
// einfriert wenn der NVMCTRL aus unbekanntem Grund nicht READY meldet.
// (Beobachtet nach bestimmten Bootloader-Firmware-Flash-Zyklen auf SAMD21.)
static bool nvm_wait() static void nvm_wait()
{ {
// ~400ms Timeout bei 48MHz, konservativ 4 Zyklen pro Loop-Iteration while (!NVMCTRL->INTFLAG.bit.READY) {}
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) static void nvm_exec(uint16_t cmd)
{ {
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd; NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | cmd;
return nvm_wait(); nvm_wait();
} }
static bool nvm_erase_row(uint32_t addr) static void nvm_erase_row(uint32_t addr)
{ {
if (!nvm_wait()) return false; nvm_wait();
NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte) NVMCTRL->ADDR.reg = addr / 2; // NVMCTRL erwartet Wort-Adresse (16-Bit-Worte)
return nvm_exec(NVMCTRL_CTRLA_CMD_ER); nvm_exec(NVMCTRL_CTRLA_CMD_ER);
} }
static bool nvm_write_page(uint32_t addr, const uint8_t* data) static void nvm_write_page(uint32_t addr, const uint8_t* data)
{ {
if (!nvm_exec(NVMCTRL_CTRLA_CMD_PBC)) return false; // Page-Buffer löschen
nvm_exec(NVMCTRL_CTRLA_CMD_PBC);
// 64 Bytes in den Page-Buffer schreiben (32-Bit-Zugriffe)
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];
} }
// Page programmieren
NVMCTRL->ADDR.reg = addr / 2; NVMCTRL->ADDR.reg = addr / 2;
return nvm_exec(NVMCTRL_CTRLA_CMD_WP); nvm_exec(NVMCTRL_CTRLA_CMD_WP);
} }
// ── CRC16-CCITT (Poly 0x1021) ───────────────────────────────────────────────── // ── CRC16 (CCITT, Poly 0x1021) ────────────────────────────────────────────────
uint16_t nvm_config_crc(const SDeviceConfig& cfg) uint16_t nvm_config_crc(const SDeviceConfig& cfg)
{ {
// CRC über alles nach dem crc-Feld (ab Byte 7: active_profile … Ende Profil 2) // CRC über alles nach dem crc-Feld (ab Byte 7)
const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, active_profile); const uint8_t* data = reinterpret_cast<const uint8_t*>(&cfg) + offsetof(SDeviceConfig, mx_actions);
uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, active_profile); uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions);
uint16_t crc = 0xFFFF; uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) { for (uint16_t i = 0; i < len; i++) {
crc ^= static_cast<uint16_t>(data[i]) << 8; crc ^= static_cast<uint16_t>(data[i]) << 8;
@ -66,48 +62,36 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg)
} }
// ── Defaults ───────────────────────────────────────────────────────────────── // ── Defaults ─────────────────────────────────────────────────────────────────
void nvm_config_defaults(SDeviceConfig& cfg) void nvm_config_defaults(SDeviceConfig& cfg)
{ {
memset(&cfg, 0, sizeof(cfg)); memset(&cfg, 0, sizeof(cfg));
cfg.magic = NVM_CONFIG_MAGIC; cfg.magic = NVM_CONFIG_MAGIC;
cfg.version = NVM_CONFIG_VERSION; cfg.version = NVM_CONFIG_VERSION;
cfg.active_profile = 0;
cfg.global_brightness = 255;
for (uint8_t e = 0; e < 4; e++)
cfg.enc_sensitivity[e] = 1;
for (uint8_t p = 0; p < 3; p++) {
SDeviceProfile& prof = cfg.profiles[p];
// Alle Aktionen: NONE // Alle Aktionen: NONE
for (uint8_t i = 0; i < 20; i++) for (uint8_t i = 0; i < 20; i++)
prof.mx_actions[i] = {ActionType::NONE, 0}; cfg.mx_actions[i] = {ActionType::NONE, 0};
for (uint8_t e = 0; e < 4; e++) for (uint8_t e = 0; e < 4; e++)
for (uint8_t a = 0; a < 3; a++) for (uint8_t a = 0; a < 3; a++)
prof.enc_actions[e][a] = {ActionType::NONE, 0}; cfg.enc_actions[e][a] = {ActionType::NONE, 0};
// Base-LEDs: warm-weiß // Base-LEDs: warm-weiß
for (uint8_t i = 0; i < 20; i++) { for (uint8_t i = 0; i < 20; i++) {
prof.led_r[i] = 80; cfg.led_r[i] = 80;
prof.led_g[i] = 40; cfg.led_g[i] = 40;
prof.led_b[i] = 0; cfg.led_b[i] = 0;
prof.led_brightness[i] = 255;
} }
// LED-Animationen: Regenbogen mit 4s Periode // LED-Animationen: Regenbogen (COLOR_CYCLE=5) mit 4s Periode als Standard
for (uint8_t i = 0; i < 20; i++) { for (uint8_t i = 0; i < 20; i++) {
prof.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE cfg.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE
prof.led_period_ms[i] = 4000; cfg.led_period_ms[i] = 4000;
}
} }
cfg.crc = nvm_config_crc(cfg); cfg.crc = nvm_config_crc(cfg);
} }
// ── Laden ───────────────────────────────────────────────────────────────────── // ── Laden ─────────────────────────────────────────────────────────────────────
bool nvm_config_load(SDeviceConfig& cfg) bool nvm_config_load(SDeviceConfig& cfg)
{ {
memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg)); memcpy(&cfg, reinterpret_cast<const void*>(k_config_addr), sizeof(cfg));
@ -116,32 +100,25 @@ bool nvm_config_load(SDeviceConfig& cfg)
if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; } if (cfg.version != NVM_CONFIG_VERSION) { nvm_config_defaults(cfg); return false; }
if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; } if (cfg.crc != nvm_config_crc(cfg)) { nvm_config_defaults(cfg); return false; }
// Profil-Index absichern
if (cfg.active_profile >= 3) cfg.active_profile = 0;
return true; return true;
} }
// ── 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 in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist.
// __attribute__((aligned(4))) ist zwingend: nvm_write_page castet zu uint32_t*. // __attribute__((aligned(4))) ist zwingend: nvm_write_page castet data zu
uint8_t row[768] __attribute__((aligned(4))); // const uint32_t*, und unaligned 32-Bit-Zugriffe sind HardFaults auf Cortex-M0+.
uint8_t row[256] __attribute__((aligned(4)));
memset(row, 0xFF, sizeof(row)); memset(row, 0xFF, sizeof(row));
memcpy(row, &cfg, sizeof(cfg)); memcpy(row, &cfg, sizeof(cfg));
// Automatisches Schreiben deaktivieren (manueller Schreib-Modus)
NVMCTRL->CTRLB.bit.MANW = 1; NVMCTRL->CTRLB.bit.MANW = 1;
// 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00) // Row 0 der Config löschen und seitenweise schreiben (4 × 64B)
if (!nvm_erase_row(k_config_addr)) return false; nvm_erase_row(k_config_addr);
if (!nvm_erase_row(k_config_addr + 256)) return false; for (uint8_t p = 0; p < 4; p++) {
if (!nvm_erase_row(k_config_addr + 512)) return false; nvm_write_page(k_config_addr + p * 64, row + p * 64);
// 12 Pages à 64B schreiben
for (uint8_t p = 0; p < 12; p++) {
if (!nvm_write_page(k_config_addr + p * 64, row + p * 64)) return false;
} }
return true;
} }

View File

@ -2,74 +2,61 @@
#include <stdint.h> #include <stdint.h>
#include "action.h" #include "action.h"
// ── NVM-Config-Layout (768 Bytes, ab 0x1FD00) ──────────────────────────────── // ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ────────────────────────────────
// //
// Row 0 (0x1FD00, 256B): Globaler Header (32B) + Profil 0 (236B) + Padding (12B → überläuft in Row 1) // Offset Size Inhalt
// Row 1 (0x1FE00, 256B): Profil 0 Rest + Profil 1 (Teil) // 0 4 Magic (0x56503202 = 'VP2\x02')
// Row 2 (0x1FF00, 256B): Profil 1 Rest + Profil 2 + Reserve (28B) // 4 1 Version
// 5 2 CRC16 über Bytes 7222
// 7 60 mx_actions[20] 20 × 3B (SAction packed)
// 67 36 enc_actions[4][3] 12 × 3B
// 103 20 led_r[20]
// 123 20 led_g[20]
// 143 20 led_b[20]
// 163 20 led_anim[20] LEDAnim-Typ pro Button (uint8_t)
// 183 40 led_period_ms[20] Animationsperiode in ms (uint16_t, little-endian)
// 223 33 Padding bis 256 Bytes (erste Row voll)
// 256 256 Reserviert für zukünftige Erweiterungen (zweite Row)
// //
// Profil-Offsets (ab Byte 0 des Config-Blobs): // Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction)
// Header: Bytes 0 31 (32B)
// Profil 0: Bytes 32267 (236B)
// Profil 1: Bytes 268503 (236B)
// Profil 2: Bytes 504739 (236B)
// Reserve: Bytes 740767 (28B)
//
// Alle 3 Rows werden immer gemeinsam gelöscht und neu geschrieben.
#define NVM_CONFIG_MAGIC 0x56503203UL // 'VP2\x03' Version 3 #define NVM_CONFIG_MAGIC 0x56503202UL
#define NVM_CONFIG_VERSION 3 #define NVM_CONFIG_VERSION 2 // Version 2
// Encoder-Aktions-Indizes (in SDeviceProfile.enc_actions[]) // Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[])
// Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW
#define ENC_ACTION_SW 0 #define ENC_ACTION_SW 0
#define ENC_ACTION_CW 1 #define ENC_ACTION_CW 1
#define ENC_ACTION_CCW 2 #define ENC_ACTION_CCW 2
// ── Pro-Profil-Daten (236 Bytes) ─────────────────────────────────────────────
struct __attribute__((packed)) SDeviceProfile
{
SAction mx_actions[20]; // 60B MX-Buttons 019
SAction enc_actions[4][3]; // 36B [Encoder 03][SW/CW/CCW]
uint8_t led_r[20]; // 20B
uint8_t led_g[20]; // 20B
uint8_t led_b[20]; // 20B
uint8_t led_brightness[20]; // 20B per-LED Helligkeit (0255, Default 255)
uint8_t led_anim[20]; // 20B LEDAnim-Typ (0=STATIC … 5=COLOR_CYCLE)
uint16_t led_period_ms[20]; // 40B Animationsperiode in ms
// Gesamt: 236B
};
// ── Globale Config (740 Bytes) ────────────────────────────────────────────────
struct __attribute__((packed)) SDeviceConfig struct __attribute__((packed)) SDeviceConfig
{ {
// Globaler Header (32B) uint32_t magic;
uint32_t magic; // 4B uint8_t version;
uint8_t version; // 1B uint16_t crc;
uint16_t crc; // 2B CRC16-CCITT über Bytes 7739
uint8_t active_profile; // 1B aktives Profil (02)
uint8_t global_brightness; // 1B globale LED-Helligkeit (0255)
uint8_t enc_sensitivity[4]; // 4B Schrittweite pro Encoder (reserviert, Default 1)
uint8_t _reserve[19]; // 19B Platz für spätere globale Felder
// Profile (3 × 236B = 708B) // Aktionen
SDeviceProfile profiles[3]; SAction mx_actions[20]; // MX-Buttons 019 (key_id 524)
// Gesamt: 32 + 708 = 740B SAction enc_actions[4][3]; // [Encoder 03][SW/CW/CCW]
// Base-LED Farben
uint8_t led_r[20];
uint8_t led_g[20];
uint8_t led_b[20];
// LED-Animationen pro MX-Button
uint8_t led_anim[20]; // LEDAnim-Typ (0=STATIC, 1=BLINK, 2=PULSE, 5=COLOR_CYCLE)
uint16_t led_period_ms[20]; // Animationsperiode in ms (0 = Firmware-Default verwenden)
}; };
// Standardwerte wenn keine gültige Config im NVM // Standardwerte wenn keine gültige Config im NVM
void nvm_config_defaults(SDeviceConfig& cfg); void nvm_config_defaults(SDeviceConfig& cfg);
// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC/Version ungültig → Defaults geladen. // Config aus NVM lesen. Gibt false zurück wenn Magic/CRC ungültig → Defaults geladen.
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 2 Rows, schreibt neu).
// Gibt false zurück wenn eine NVM-Operation nicht rechtzeitig fertig wird (Board hängt nicht). void nvm_config_save(const SDeviceConfig& cfg);
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
uint16_t nvm_config_crc(const SDeviceConfig& cfg); uint16_t nvm_config_crc(const SDeviceConfig& cfg);

View File

@ -54,7 +54,6 @@
#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