diff --git a/doc/04_macro_system.md b/doc/04_macro_system.md index 9ba20fe..332cfcb 100644 --- a/doc/04_macro_system.md +++ b/doc/04_macro_system.md @@ -57,3 +57,31 @@ für Step 0–3: ``` Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung. + +--- + +## Geplante Erweiterung: 8 Steps (NVM v3) + +### Motivation + +4 Steps reichen für einfache Shortcuts, aber nicht für Excel-Ribbon-Navigation oder andere Sequenzen mit 5+ Tasten. Mit dem NVM-v3-Umbau (siehe [06_nvm_config.md](06_nvm_config.md)) stehen zwei vollständige Rows für die Makro-Tabelle zur Verfügung. + +### Neues Layout + +```cpp +#define MACRO_SLOTS 32 +#define MACRO_MAX_STEPS 8 // war: 4 + +struct __attribute__((packed)) SMacroTable { + SMacroStep steps[32][8]; // 32 × 8 × 2 = 512 Bytes = 2 NVM-Rows +}; +``` + +### Neuer NVM-Speicherort + +| Row | Adresse | Inhalt | +|---|---|---| +| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0–255 | +| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256–511 | + +`macro_config_save` muss entsprechend beide Rows löschen und 8 Pages schreiben (statt bisher 4). diff --git a/doc/06_nvm_config.md b/doc/06_nvm_config.md index a1460c8..b99466f 100644 --- a/doc/06_nvm_config.md +++ b/doc/06_nvm_config.md @@ -66,3 +66,65 @@ SAMD21 NVM: Row = 256 B = 4 Pages à 64 B. Schreiben erfordert: > `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):** + +| 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) | +| 7 | 1 | `active_profile` (0–2) | +| 8 | 1 | `global_brightness` (0–255) | +| 9 | 4 | `enc_sensitivity[4]` (1 B pro Encoder) | +| 13 | 19 | Reserve | + +**Pro Profil (236 B, Offset `32 + idx × 236`):** + +| 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]` ← neu | +| 176 | 20 | `led_anim[20]` | +| 196 | 40 | `led_period_ms[20]` | + +### Makro-Tabelle (512 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). + +### Migration von v2 + +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. diff --git a/src/CMainController.cpp b/src/CMainController.cpp index 2b5eac7..54ad1aa 100644 --- a/src/CMainController.cpp +++ b/src/CMainController.cpp @@ -101,13 +101,16 @@ void CMainController::init_buttons() bool valid = nvm_config_load(cfg); (void)valid; // false = keine gültige Config → Defaults wurden bereits geladen + // Aktives Profil auswählen (load() sichert bereits 0–2 ab) + const SDeviceProfile& prof = cfg.profiles[cfg.active_profile]; + // Encoder-SW-Buttons: nur SW-Aktion, kein LED (led_index = -1) for (uint8_t enc = 0; enc < 4; enc++) { - m_buttons[enc].init(enc, -1, cfg.enc_actions[enc][ENC_ACTION_SW], RGB()); + m_buttons[enc].init(enc, -1, prof.enc_actions[enc][ENC_ACTION_SW], RGB()); } // MX-Buttons: LED-Index aus serpentiner Verdrahtung berechnen, - // Aktion + Base-Farbe + Animation aus NVM. + // Aktion + Base-Farbe + Animation aus aktivem Profil. // 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++) { @@ -115,11 +118,21 @@ void CMainController::init_buttons() uint8_t row = key % MATRIX_ROWS; int8_t led = static_cast(LED_INDEX(col, row)); 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); - LEDAnim anim = static_cast(cfg.led_anim[mx_idx]); - uint16_t period = cfg.led_period_ms[mx_idx] > 0 ? cfg.led_period_ms[mx_idx] : 4000; + // Effektive Farbe = base × led_brightness × global_brightness / 255² + auto scale = [&](uint8_t val) -> uint8_t { + 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(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) { // Phase gleichmäßig verteilen → stehender Regenbogen dreht sich @@ -133,8 +146,8 @@ void CMainController::init_buttons() // Encoder CW/CCW-Aktionen separat merken – Encoder haben kein CButton-Objekt // da sie keine LED haben und kein Matrix-Key sind. for (uint8_t enc = 0; enc < 4; enc++) { - m_enc_cw [enc] = cfg.enc_actions[enc][ENC_ACTION_CW]; - m_enc_ccw[enc] = cfg.enc_actions[enc][ENC_ACTION_CCW]; + m_enc_cw [enc] = prof.enc_actions[enc][ENC_ACTION_CW]; + m_enc_ccw[enc] = prof.enc_actions[enc][ENC_ACTION_CCW]; } } @@ -198,8 +211,8 @@ void CMainController::poll_vendor() // 6 Nutzbytes ab Puffer-Offset (chunk_index × 6) eintragen uint16_t offset = (uint16_t)pkt.key_id() * 6; if (offset < sizeof(m_cfg_buf)) { - uint8_t count = (uint8_t)(sizeof(m_cfg_buf) - offset); - if (count > 6) count = 6; + uint16_t remaining = (uint16_t)(sizeof(m_cfg_buf) - offset); + uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); memcpy(m_cfg_buf + offset, &pkt.data[2], count); } } @@ -211,10 +224,10 @@ void CMainController::poll_vendor() { SDeviceConfig cfg; nvm_config_load(cfg); // ungültige NVM → Defaults - const uint8_t* raw = reinterpret_cast(&cfg); - const uint8_t sz = sizeof(SDeviceConfig); // 223 + const uint8_t* raw = reinterpret_cast(&cfg); + const uint16_t sz = sizeof(SDeviceConfig); // 740 const uint8_t payload = 6; - uint8_t chunks = (sz + payload - 1) / payload; // 38 + uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 124 usb_serial_send(USB_EVT_CONFIG_BEGIN, chunks); @@ -262,10 +275,10 @@ void CMainController::poll_vendor() case USB_CMD_MACRO_DATA: 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)) { - uint8_t count = (uint8_t)(sizeof(m_macro_buf) - offset); - if (count > 6) count = 6; + uint16_t remaining = (uint16_t)(sizeof(m_macro_buf) - offset); + uint8_t count = (uint8_t)(remaining > 6 ? 6 : remaining); memcpy(m_macro_buf + offset, &pkt.data[2], count); } } @@ -283,10 +296,10 @@ void CMainController::poll_vendor() // ── Makro-Dump anfordern ───────────────────────────────────────── case USB_CMD_MACRO_READ: { - const uint8_t* raw = reinterpret_cast(&m_macros); - const uint16_t sz = sizeof(SMacroTable); // 256 + const uint8_t* raw = reinterpret_cast(&m_macros); + const uint16_t sz = sizeof(SMacroTable); // 512 const uint8_t payload = 6; - uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 43 + uint8_t chunks = (uint8_t)((sz + payload - 1) / payload); // 86 usb_serial_send(USB_EVT_MACRO_BEGIN, chunks); @@ -417,6 +430,20 @@ void CMainController::execute_action_down(SAction action, uint8_t key_id) break; } + case ActionType::PROFILE_SWITCH: + { + SDeviceConfig cfg; + nvm_config_load(cfg); + uint8_t target = static_cast(action.data); + if (target == 0xFF) + 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(); + break; + } + case ActionType::NONE: default: break; diff --git a/src/CMainController.h b/src/CMainController.h index 8979ac6..61b629e 100644 --- a/src/CMainController.h +++ b/src/CMainController.h @@ -12,6 +12,7 @@ #include "hal/usb_hid.h" #include "hal/usb_serial.h" #include "config/action.h" +#include "config/nvm_config.h" #include "config/macro_config.h" class CMainController @@ -43,12 +44,12 @@ private: void updateLEDs(); // Dirty-LEDs in WS2812-Buffer schreiben // ── Config-Empfangspuffer ───────────────────────────────────────────────── - uint8_t m_cfg_buf[223]; // sizeof(SDeviceConfig) = 223 Bytes + uint8_t m_cfg_buf[sizeof(SDeviceConfig)]; // 740 Bytes uint8_t m_cfg_chunks_expected; bool m_cfg_receiving; // ── Makro-Empfangspuffer ────────────────────────────────────────────────── - uint8_t m_macro_buf[256]; // sizeof(SMacroTable) = 256 Bytes + uint8_t m_macro_buf[sizeof(SMacroTable)]; // 512 Bytes uint8_t m_macro_chunks_expected; bool m_macro_receiving; diff --git a/src/config/action.h b/src/config/action.h index 2ee7408..e936c86 100644 --- a/src/config/action.h +++ b/src/config/action.h @@ -7,7 +7,8 @@ enum class ActionType : uint8_t HID_KEY, // Standard-Keyboard-Keycode (direkt in Firmware gesendet) HID_CONSUMER, // Consumer-Control-Keycode (Volume, Media, …) HOST_COMMAND, // Command-ID → Windows-App führt aus (URL, Programm, …) - MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 4 HID-Keys sequenziell + MACRO, // Makro-Slot (data = Slot-Index 0–31) → bis zu 8 HID-Keys sequenziell + PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 0–2); speichert in NVM }; struct __attribute__((packed)) SAction diff --git a/src/config/macro_config.cpp b/src/config/macro_config.cpp index de3d289..ddb8391 100644 --- a/src/config/macro_config.cpp +++ b/src/config/macro_config.cpp @@ -1,5 +1,5 @@ // macro_config.cpp -// NVM-Zugriff für die Makro-Tabelle (Row 1, 0x1FF00). +// NVM-Zugriff für die Makro-Tabelle (Row 0+1, 0x1FB00–0x1FCFF, 512 Bytes). // Nutzt dieselben NVMCTRL-Hilfsfunktionen wie nvm_config.cpp (dupliziert, // da static – kein gemeinsamer Header für interne NVM-Helfer). @@ -7,7 +7,7 @@ #include #include -static const uint32_t k_macro_addr = 0x1FF00UL; // Row 1 (256B nach Row 0) +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) @@ -37,7 +37,7 @@ bool macro_config_load(SMacroTable& tbl) { memcpy(&tbl, reinterpret_cast(k_macro_addr), sizeof(tbl)); - // Prüfen ob Row 1 noch gelöscht ist (alle 0xFF = nie beschrieben) + // Prüfen ob beide Rows noch gelöscht sind (alle 0xFF = nie beschrieben) const uint8_t* raw = reinterpret_cast(&tbl); bool all_ff = true; for (uint16_t i = 0; i < sizeof(tbl); i++) { @@ -55,12 +55,17 @@ void 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 → // direkter uint32_t*-Cast würde auf Cortex-M0+ einen HardFault auslösen. - uint8_t aligned_buf[256] __attribute__((aligned(4))); + uint8_t aligned_buf[512] __attribute__((aligned(4))); memcpy(aligned_buf, &tbl, sizeof(tbl)); NVMCTRL->CTRLB.bit.MANW = 1; + + // Beide Rows löschen (Row 0: 0x1FB00, Row 1: 0x1FC00) nvm_erase_row(k_macro_addr); - for (uint8_t p = 0; p < 4; p++) { + nvm_erase_row(k_macro_addr + 256); + + // 8 Pages à 64B schreiben + for (uint8_t p = 0; p < 8; p++) { nvm_write_page(k_macro_addr + p * 64, aligned_buf + p * 64); } } diff --git a/src/config/macro_config.h b/src/config/macro_config.h index e81a162..e2ec15f 100644 --- a/src/config/macro_config.h +++ b/src/config/macro_config.h @@ -1,7 +1,7 @@ #pragma once // macro_config.h -// Makro-Tabelle: bis zu 32 Slots, je 4 HID-Key-Steps. -// Gespeichert in NVM Row 1 (0x1FF00, 256 Bytes). +// Makro-Tabelle: 32 Slots, je 8 HID-Key-Steps. +// Gespeichert in NVM Row 0+1 (0x1FB00–0x1FCFF, 512 Bytes). // // Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind): // Slot 0–19 : MX-Buttons (mx_idx) @@ -13,7 +13,7 @@ #include #define MACRO_SLOTS 32 -#define MACRO_MAX_STEPS 4 +#define MACRO_MAX_STEPS 8 // Ein einzelner HID-Key-Step im Makro struct __attribute__((packed)) SMacroStep @@ -22,15 +22,15 @@ struct __attribute__((packed)) SMacroStep uint8_t modifier; // HID Modifier-Byte (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08) }; -// Komplette Makro-Tabelle (32 × 4 × 2 = 256 Bytes = eine NVM-Row) +// Komplette Makro-Tabelle (32 × 8 × 2 = 512 Bytes = zwei NVM-Rows) struct __attribute__((packed)) SMacroTable { SMacroStep steps[MACRO_SLOTS][MACRO_MAX_STEPS]; }; -// Makro-Tabelle aus NVM lesen (Row 1: 0x1FF00). +// Makro-Tabelle aus NVM lesen (Row 0+1: 0x1FB00). // Gibt false zurück wenn der Flash-Bereich noch gelöscht (0xFF) war → leere Tabelle geladen. bool macro_config_load(SMacroTable& tbl); -// Makro-Tabelle in NVM schreiben (löscht Row 1, schreibt 4 Pages). +// Makro-Tabelle in NVM schreiben (löscht Row 0+1, schreibt 8 Pages). void macro_config_save(const SMacroTable& tbl); diff --git a/src/config/nvm_config.cpp b/src/config/nvm_config.cpp index 6b63bde..27e1477 100644 --- a/src/config/nvm_config.cpp +++ b/src/config/nvm_config.cpp @@ -1,14 +1,13 @@ +// nvm_config.cpp +// NVM-Zugriff für SDeviceConfig (3 Rows ab 0x1FD00, 768B gesamt, 740B genutzt). + #include "nvm_config.h" #include #include -// ── Flash-Adresse (aus Linkerscript) ───────────────────────────────────────── -// Kein separates Linker-Symbol nötig – Adresse ist fix und bekannt. -static const uint32_t k_config_addr = 0x1FE00UL; +static const uint32_t k_config_addr = 0x1FD00UL; // Row 0–2 der Config -// SAMD21 NVMCTRL ────────────────────────────────────────────────────────────── -// Row = 256 Bytes = 4 Pages à 64 Bytes -// Schreiben: Row löschen (ER), dann seitenweise schreiben (WP) +// ── NVMCTRL-Hilfsfunktionen ─────────────────────────────────────────────────── static void nvm_wait() { @@ -30,27 +29,23 @@ static void nvm_erase_row(uint32_t addr) static void nvm_write_page(uint32_t addr, const uint8_t* data) { - // 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(addr); const uint32_t* src = reinterpret_cast(data); for (uint8_t i = 0; i < 64 / 4; i++) { dst[i] = src[i]; } - - // Page programmieren NVMCTRL->ADDR.reg = addr / 2; nvm_exec(NVMCTRL_CTRLA_CMD_WP); } -// ── CRC16 (CCITT, Poly 0x1021) ──────────────────────────────────────────────── +// ── CRC16-CCITT (Poly 0x1021) ───────────────────────────────────────────────── + uint16_t nvm_config_crc(const SDeviceConfig& cfg) { - // CRC über alles nach dem crc-Feld (ab Byte 7) - const uint8_t* data = reinterpret_cast(&cfg) + offsetof(SDeviceConfig, mx_actions); - uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, mx_actions); + // CRC über alles nach dem crc-Feld (ab Byte 7: active_profile … Ende Profil 2) + const uint8_t* data = reinterpret_cast(&cfg) + offsetof(SDeviceConfig, active_profile); + uint16_t len = sizeof(SDeviceConfig) - offsetof(SDeviceConfig, active_profile); uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= static_cast(data[i]) << 8; @@ -62,36 +57,48 @@ uint16_t nvm_config_crc(const SDeviceConfig& cfg) } // ── Defaults ───────────────────────────────────────────────────────────────── + void nvm_config_defaults(SDeviceConfig& cfg) { memset(&cfg, 0, sizeof(cfg)); - cfg.magic = NVM_CONFIG_MAGIC; - cfg.version = NVM_CONFIG_VERSION; + cfg.magic = NVM_CONFIG_MAGIC; + cfg.version = NVM_CONFIG_VERSION; + cfg.active_profile = 0; + cfg.global_brightness = 255; - // Alle Aktionen: NONE - for (uint8_t i = 0; i < 20; i++) - cfg.mx_actions[i] = {ActionType::NONE, 0}; for (uint8_t e = 0; e < 4; e++) - for (uint8_t a = 0; a < 3; a++) - cfg.enc_actions[e][a] = {ActionType::NONE, 0}; + cfg.enc_sensitivity[e] = 1; - // Base-LEDs: warm-weiß - for (uint8_t i = 0; i < 20; i++) { - cfg.led_r[i] = 80; - cfg.led_g[i] = 40; - cfg.led_b[i] = 0; - } + for (uint8_t p = 0; p < 3; p++) { + SDeviceProfile& prof = cfg.profiles[p]; - // LED-Animationen: Regenbogen (COLOR_CYCLE=5) mit 4s Periode als Standard - for (uint8_t i = 0; i < 20; i++) { - cfg.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE - cfg.led_period_ms[i] = 4000; + // Alle Aktionen: NONE + for (uint8_t i = 0; i < 20; i++) + prof.mx_actions[i] = {ActionType::NONE, 0}; + for (uint8_t e = 0; e < 4; e++) + for (uint8_t a = 0; a < 3; a++) + prof.enc_actions[e][a] = {ActionType::NONE, 0}; + + // Base-LEDs: warm-weiß + for (uint8_t i = 0; i < 20; i++) { + prof.led_r[i] = 80; + prof.led_g[i] = 40; + prof.led_b[i] = 0; + prof.led_brightness[i] = 255; + } + + // LED-Animationen: Regenbogen mit 4s Periode + for (uint8_t i = 0; i < 20; i++) { + prof.led_anim[i] = 5; // LEDAnim::COLOR_CYCLE + prof.led_period_ms[i] = 4000; + } } cfg.crc = nvm_config_crc(cfg); } // ── Laden ───────────────────────────────────────────────────────────────────── + bool nvm_config_load(SDeviceConfig& cfg) { memcpy(&cfg, reinterpret_cast(k_config_addr), sizeof(cfg)); @@ -100,25 +107,31 @@ bool nvm_config_load(SDeviceConfig& cfg) 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; } + // Profil-Index absichern + if (cfg.active_profile >= 3) cfg.active_profile = 0; + return true; } // ── Speichern ───────────────────────────────────────────────────────────────── + void nvm_config_save(const SDeviceConfig& cfg) { - // Config in temporären Buffer kopieren der auf 256B (Row) aufgefüllt ist. - // __attribute__((aligned(4))) ist zwingend: nvm_write_page castet data zu - // const uint32_t*, und unaligned 32-Bit-Zugriffe sind HardFaults auf Cortex-M0+. - uint8_t row[256] __attribute__((aligned(4))); + // 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*. + uint8_t row[768] __attribute__((aligned(4))); memset(row, 0xFF, sizeof(row)); memcpy(row, &cfg, sizeof(cfg)); - // Automatisches Schreiben deaktivieren (manueller Schreib-Modus) NVMCTRL->CTRLB.bit.MANW = 1; - // Row 0 der Config löschen und seitenweise schreiben (4 × 64B) + // 3 Rows löschen (0x1FD00, 0x1FE00, 0x1FF00) nvm_erase_row(k_config_addr); - for (uint8_t p = 0; p < 4; p++) { + nvm_erase_row(k_config_addr + 256); + nvm_erase_row(k_config_addr + 512); + + // 12 Pages à 64B schreiben + for (uint8_t p = 0; p < 12; p++) { nvm_write_page(k_config_addr + p * 64, row + p * 64); } } diff --git a/src/config/nvm_config.h b/src/config/nvm_config.h index 8858a02..bd77054 100644 --- a/src/config/nvm_config.h +++ b/src/config/nvm_config.h @@ -2,61 +2,73 @@ #include #include "action.h" -// ── NVM-Config-Layout (512 Bytes, ab 0x1FE00) ──────────────────────────────── +// ── NVM-Config-Layout (768 Bytes, ab 0x1FD00) ──────────────────────────────── // -// Offset Size Inhalt -// 0 4 Magic (0x56503202 = 'VP2\x02') -// 4 1 Version -// 5 2 CRC16 über Bytes 7–222 -// 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) +// Row 0 (0x1FD00, 256B): Globaler Header (32B) + Profil 0 (236B) + Padding (−12B → überläuft in Row 1) +// Row 1 (0x1FE00, 256B): Profil 0 Rest + Profil 1 (Teil) +// Row 2 (0x1FF00, 256B): Profil 1 Rest + Profil 2 + Reserve (28B) // -// Gesamt genutzt: 223 Bytes (sizeof SDeviceConfig mit packed SAction) +// Profil-Offsets (ab Byte 0 des Config-Blobs): +// Header: Bytes 0– 31 (32B) +// Profil 0: Bytes 32–267 (236B) +// Profil 1: Bytes 268–503 (236B) +// Profil 2: Bytes 504–739 (236B) +// Reserve: Bytes 740–767 (28B) +// +// Alle 3 Rows werden immer gemeinsam gelöscht und neu geschrieben. -#define NVM_CONFIG_MAGIC 0x56503202UL -#define NVM_CONFIG_VERSION 2 // Version 2 +#define NVM_CONFIG_MAGIC 0x56503203UL // 'VP2\x03' – Version 3 +#define NVM_CONFIG_VERSION 3 -// Encoder-Aktions-Indizes (in SDeviceConfig.enc_actions[]) +// Encoder-Aktions-Indizes (in SDeviceProfile.enc_actions[]) // Reihenfolge: [enc][0]=SW, [enc][1]=CW, [enc][2]=CCW #define ENC_ACTION_SW 0 #define ENC_ACTION_CW 1 #define ENC_ACTION_CCW 2 +// ── Pro-Profil-Daten (236 Bytes) ───────────────────────────────────────────── + +struct __attribute__((packed)) SDeviceProfile +{ + SAction mx_actions[20]; // 60B – MX-Buttons 0–19 + SAction enc_actions[4][3]; // 36B – [Encoder 0–3][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 (0–255, 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 { - uint32_t magic; - uint8_t version; - uint16_t crc; + // Globaler Header (32B) + uint32_t magic; // 4B + uint8_t version; // 1B + uint16_t crc; // 2B – CRC16-CCITT über Bytes 7–739 + uint8_t active_profile; // 1B – aktives Profil (0–2) + uint8_t global_brightness; // 1B – globale LED-Helligkeit (0–255) + uint8_t enc_sensitivity[4]; // 4B – Schrittweite pro Encoder (reserviert, Default 1) + uint8_t _reserve[19]; // 19B – Platz für spätere globale Felder - // Aktionen - SAction mx_actions[20]; // MX-Buttons 0–19 (key_id 5–24) - SAction enc_actions[4][3]; // [Encoder 0–3][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) + // Profile (3 × 236B = 708B) + SDeviceProfile profiles[3]; + // Gesamt: 32 + 708 = 740B }; // Standardwerte wenn keine gültige Config im NVM void nvm_config_defaults(SDeviceConfig& cfg); -// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC ungültig → Defaults geladen. +// Config aus NVM lesen. Gibt false zurück wenn Magic/CRC/Version ungültig → Defaults geladen. bool nvm_config_load(SDeviceConfig& cfg); -// Config in NVM schreiben (löscht 2 Rows, schreibt neu). +// Config in NVM schreiben (löscht 3 Rows, schreibt 12 Pages). void nvm_config_save(const SDeviceConfig& cfg); -// CRC16 über die Nutzdaten der Config +// CRC16 über die Nutzdaten der Config (Bytes 7–739, nach dem crc-Feld) uint16_t nvm_config_crc(const SDeviceConfig& cfg);