8 Step macro and profile switching fully working
This commit is contained in:
parent
098a166a9f
commit
433d61c29f
@ -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)
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<volatile uint32_t*>(addr);
|
||||
const uint32_t* src = reinterpret_cast<const uint32_t*>(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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<volatile uint32_t*>(addr);
|
||||
const uint32_t* src = reinterpret_cast<const uint32_t*>(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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user