Semi working profiles and longer macros

This commit is contained in:
Julian Appel 2026-04-13 21:42:02 +02:00
parent 7169d3bbba
commit 098a166a9f
9 changed files with 257 additions and 108 deletions

View File

@ -57,3 +57,31 @@ für Step 03:
```
Die Makro-Tabelle liegt nach `setup()` im RAM (`m_macros` in CMainController). Kein NVM-Zugriff während der Ausführung.
---
## Geplante Erweiterung: 8 Steps (NVM v3)
### Motivation
4 Steps reichen für einfache Shortcuts, aber nicht für Excel-Ribbon-Navigation oder andere Sequenzen mit 5+ Tasten. Mit dem NVM-v3-Umbau (siehe [06_nvm_config.md](06_nvm_config.md)) stehen zwei vollständige Rows für die Makro-Tabelle zur Verfügung.
### Neues Layout
```cpp
#define MACRO_SLOTS 32
#define MACRO_MAX_STEPS 8 // war: 4
struct __attribute__((packed)) SMacroTable {
SMacroStep steps[32][8]; // 32 × 8 × 2 = 512 Bytes = 2 NVM-Rows
};
```
### Neuer NVM-Speicherort
| Row | Adresse | Inhalt |
|---|---|---|
| Macro Row 0 | `0x1FB00` | SMacroTable Bytes 0255 |
| Macro Row 1 | `0x1FC00` | SMacroTable Bytes 256511 |
`macro_config_save` muss entsprechend beide Rows löschen und 8 Pages schreiben (statt bisher 4).

View File

@ -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, 0x1FB000x1FFFF)
| Row | Adresse | Größe | Inhalt |
|---|---|---|---|
| Macro Row 0 | `0x1FB00` | 256 B | SMacroTable (Bytes 0255) |
| Macro Row 1 | `0x1FC00` | 256 B | SMacroTable (Bytes 256511) |
| Config Row 0 | `0x1FD00` | 256 B | Globaler Header + Profil 0 (Bytes 0255) |
| Config Row 1 | `0x1FE00` | 256 B | Profil 0 (Rest) + Profil 1 (Bytes 256511) |
| Config Row 2 | `0x1FF00` | 256 B | Profil 1 (Rest) + Profil 2 + Reserve (Bytes 512767) |
Macros und Config liegen in vollständig getrennten, jeweils zusammenhängenden Row-Blöcken.
### Config-Inhalt (768 B, davon 740 B genutzt, 28 B Reserve)
**Globaler Header (32 B, Offset 0):**
| Offset | Größe | Feld |
|---|---|---|
| 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` (02) |
| 8 | 1 | `global_brightness` (0255) |
| 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.

View File

@ -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 02 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<int8_t>(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<LEDAnim>(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<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) {
// 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<const uint8_t*>(&cfg);
const uint8_t sz = sizeof(SDeviceConfig); // 223
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&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<const uint8_t*>(&m_macros);
const uint16_t sz = sizeof(SMacroTable); // 256
const uint8_t* raw = reinterpret_cast<const uint8_t*>(&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<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;
nvm_config_save(cfg);
init_buttons();
break;
}
case ActionType::NONE:
default:
break;

View File

@ -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;

View File

@ -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 031) → bis zu 4 HID-Keys sequenziell
MACRO, // Makro-Slot (data = Slot-Index 031) → bis zu 8 HID-Keys sequenziell
PROFILE_SWITCH, // Profil wechseln (data = Profil-Index 02); speichert in NVM
};
struct __attribute__((packed)) SAction

View File

@ -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, 0x1FB000x1FCFF, 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 <Arduino.h>
#include <string.h>
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<const void*>(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<const uint8_t*>(&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);
}
}

View File

@ -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 (0x1FB000x1FCFF, 512 Bytes).
//
// Slot-Zuweisung (vom Windows-App vergeben, Board speichert blind):
// Slot 019 : MX-Buttons (mx_idx)
@ -13,7 +13,7 @@
#include <stdint.h>
#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);

View File

@ -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 <Arduino.h>
#include <string.h>
// ── 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 02 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<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];
}
// 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<const uint8_t*>(&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<const uint8_t*>(&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<uint16_t>(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<const void*>(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);
}
}

View File

@ -2,61 +2,73 @@
#include <stdint.h>
#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 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)
// 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 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 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 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
{
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 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
// Aktionen
SAction mx_actions[20]; // MX-Buttons 019 (key_id 524)
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)
// 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 7739, nach dem crc-Feld)
uint16_t nvm_config_crc(const SDeviceConfig& cfg);