From 91721ac47d0108870b2a4aef58276907422c1247 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Fri, 11 Sep 2026 13:24:43 +0200 Subject: [PATCH] feat: Flash-HAL-Interface und Settings-Store mit A/B-Slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert: - Flash HAL (nl_flash_read/write/erase_sector) Interfaces - Flash Layout mit A/B-Settings Slots und weiteren Regions - Test-Fake für Flash mit Fehlerinjection (Stromausfall-Simulation) - Settings-Store mit CRC32 Recovery und Slot-Alternierung - Vollständige Test-Suite mit Corruption/Power-Loss-Tests --- src/config/nl_flash.h | 16 +++++ src/config/nl_flash_layout.h | 12 ++++ src/config/nl_settings.c | 117 +++++++++++++++++++++++++++++++++++ src/config/nl_settings.h | 30 +++++++++ test/CMakeLists.txt | 1 + test/fake_flash.c | 60 ++++++++++++++++++ test/fake_flash.h | 19 ++++++ test/test_settings.c | 105 +++++++++++++++++++++++++++++++ 8 files changed, 360 insertions(+) create mode 100644 src/config/nl_flash.h create mode 100644 src/config/nl_flash_layout.h create mode 100644 src/config/nl_settings.c create mode 100644 src/config/nl_settings.h create mode 100644 test/fake_flash.c create mode 100644 test/fake_flash.h create mode 100644 test/test_settings.c diff --git a/src/config/nl_flash.h b/src/config/nl_flash.h new file mode 100644 index 0000000..5003a58 --- /dev/null +++ b/src/config/nl_flash.h @@ -0,0 +1,16 @@ +#ifndef NL_FLASH_H +#define NL_FLASH_H + +#include + +#define NL_FLASH_SECTOR_SIZE 4096u + +/* All functions return 0 on success, < 0 on error. Addresses are absolute. */ +int nl_flash_read(uint32_t addr, void *buf, uint32_t len); +/* The target bytes must be erased (0xFF); the stores never rely on implicit erases. */ +int nl_flash_write(uint32_t addr, const void *buf, uint32_t len); +int nl_flash_erase_sector(uint32_t addr); +/* Start of the ninlight flash region (USER_ADDR_START + one sector on the W600). */ +uint32_t nl_flash_region_base(void); + +#endif diff --git a/src/config/nl_flash_layout.h b/src/config/nl_flash_layout.h new file mode 100644 index 0000000..f1f795f --- /dev/null +++ b/src/config/nl_flash_layout.h @@ -0,0 +1,12 @@ +#ifndef NL_FLASH_LAYOUT_H +#define NL_FLASH_LAYOUT_H + +/* Offsets inside the ninlight flash region, one 4 KB sector each. */ +#define NL_FL_SETTINGS_A 0x0000u +#define NL_FL_SETTINGS_B 0x1000u +#define NL_FL_STATE_0 0x2000u +#define NL_FL_STATE_1 0x3000u +#define NL_FL_RESETCNT 0x4000u +#define NL_FL_REGION_SIZE 0x5000u + +#endif diff --git a/src/config/nl_settings.c b/src/config/nl_settings.c new file mode 100644 index 0000000..38138c3 --- /dev/null +++ b/src/config/nl_settings.c @@ -0,0 +1,117 @@ +#include "config/nl_settings.h" + +#include +#include + +#include "config/nl_flash.h" +#include "light/nl_light_types.h" +#include "util/nl_crc.h" + +#ifndef NINLIGHT_TOKEN_DEFAULT +#define NINLIGHT_TOKEN_DEFAULT "aabbccddeeff0011223344" +#endif + +#define NL_SETTINGS_MAGIC 0x31534C4Eu /* "NLS1" */ +#define NL_SETTINGS_VERSION 1u + +typedef struct { + uint32_t magic; + uint16_t version; + uint16_t length; + uint32_t seq; +} rec_hdr_t; + +typedef struct { + rec_hdr_t hdr; + nl_settings_t data; + uint32_t crc; +} rec_t; + +static uint32_t s_addr[2]; + +void nl_settings_defaults(nl_settings_t *s) +{ + memset(s, 0, sizeof *s); + strncpy(s->token, NINLIGHT_TOKEN_DEFAULT, NL_TOKEN_LEN - 1); + s->power_on_behaviour = NL_POB_LAST; + s->fade_in_ms = 500; + s->fade_out_ms = 500; +} + +void nl_settings_init(uint32_t addr_a, uint32_t addr_b) +{ + s_addr[0] = addr_a; + s_addr[1] = addr_b; +} + +static bool read_rec(int slot, rec_t *r) +{ + if (nl_flash_read(s_addr[slot], r, sizeof *r) != 0) + return false; + if (r->hdr.magic != NL_SETTINGS_MAGIC || r->hdr.version != NL_SETTINGS_VERSION || + r->hdr.length != sizeof(nl_settings_t)) + return false; + return nl_crc32(0, r, offsetof(rec_t, crc)) == r->crc; +} + +bool nl_settings_load(nl_settings_t *out) +{ + rec_t a, b; + bool va = read_rec(0, &a); + bool vb = read_rec(1, &b); + const rec_t *best; + + if (!va && !vb) { + nl_settings_defaults(out); + return false; + } + best = (va && (!vb || a.hdr.seq > b.hdr.seq)) ? &a : &b; + *out = best->data; + out->wifi_ssid[NL_SSID_LEN - 1] = '\0'; + out->wifi_pass[NL_PASS_LEN - 1] = '\0'; + out->token[NL_TOKEN_LEN - 1] = '\0'; + out->name[NL_NAME_LEN - 1] = '\0'; + return true; +} + +int nl_settings_save(const nl_settings_t *s) +{ + static rec_t a, b, r; /* keep ~700 bytes off the task stack */ + bool va = read_rec(0, &a); + bool vb = read_rec(1, &b); + uint32_t seq; + int slot; + + if (va && vb) { + slot = a.hdr.seq <= b.hdr.seq ? 0 : 1; + seq = (a.hdr.seq > b.hdr.seq ? a.hdr.seq : b.hdr.seq) + 1; + } else if (va) { + slot = 1; + seq = a.hdr.seq + 1; + } else if (vb) { + slot = 0; + seq = b.hdr.seq + 1; + } else { + slot = 0; + seq = 1; + } + + memset(&r, 0, sizeof r); + r.hdr.magic = NL_SETTINGS_MAGIC; + r.hdr.version = NL_SETTINGS_VERSION; + r.hdr.length = sizeof(nl_settings_t); + r.hdr.seq = seq; + r.data = *s; + r.crc = nl_crc32(0, &r, offsetof(rec_t, crc)); + + if (nl_flash_erase_sector(s_addr[slot]) != 0) + return -1; + return nl_flash_write(s_addr[slot], &r, sizeof r); +} + +int nl_settings_erase(void) +{ + int ra = nl_flash_erase_sector(s_addr[0]); + int rb = nl_flash_erase_sector(s_addr[1]); + return (ra == 0 && rb == 0) ? 0 : -1; +} diff --git a/src/config/nl_settings.h b/src/config/nl_settings.h new file mode 100644 index 0000000..7c58b5e --- /dev/null +++ b/src/config/nl_settings.h @@ -0,0 +1,30 @@ +#ifndef NL_SETTINGS_H +#define NL_SETTINGS_H + +#include +#include + +#define NL_SSID_LEN 33 +#define NL_PASS_LEN 65 +#define NL_TOKEN_LEN 65 +#define NL_NAME_LEN 33 + +typedef struct { + char wifi_ssid[NL_SSID_LEN]; + char wifi_pass[NL_PASS_LEN]; + char token[NL_TOKEN_LEN]; + char name[NL_NAME_LEN]; + uint8_t auth_all; + uint8_t power_on_behaviour; /* nl_pob_t */ + uint16_t fade_in_ms; + uint16_t fade_out_ms; +} nl_settings_t; + +void nl_settings_defaults(nl_settings_t *s); +void nl_settings_init(uint32_t addr_a, uint32_t addr_b); +/* Returns false (and fills defaults) when no valid record exists. */ +bool nl_settings_load(nl_settings_t *out); +int nl_settings_save(const nl_settings_t *s); +int nl_settings_erase(void); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b2bc48a..a580d77 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -30,3 +30,4 @@ nl_test(test_engine test_engine.c ${NL_SRC}/light/nl_engine.c ${NL_SRC}/light/nl nl_test(test_boot_state test_boot_state.c ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/board/nl_board.c) nl_test(test_control test_control.c ${NL_SRC}/light/nl_control.c ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/light/nl_engine.c ${NL_SRC}/light/nl_color.c ${NL_SRC}/board/nl_board.c) +nl_test(test_settings test_settings.c fake_flash.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c) diff --git a/test/fake_flash.c b/test/fake_flash.c new file mode 100644 index 0000000..58f2296 --- /dev/null +++ b/test/fake_flash.c @@ -0,0 +1,60 @@ +#include "fake_flash.h" + +#include + +uint8_t fake_flash_mem[FAKE_FLASH_SIZE]; +unsigned fake_flash_erases[FAKE_FLASH_SECTORS]; +unsigned fake_flash_dirty_writes; +static long s_fail_after = -1; + +void fake_flash_reset(void) +{ + memset(fake_flash_mem, 0xFF, sizeof fake_flash_mem); + memset(fake_flash_erases, 0, sizeof fake_flash_erases); + fake_flash_dirty_writes = 0; + s_fail_after = -1; +} + +void fake_flash_fail_after(long bytes) +{ + s_fail_after = bytes; +} + +int nl_flash_read(uint32_t addr, void *buf, uint32_t len) +{ + if (addr + len > FAKE_FLASH_SIZE) + return -1; + memcpy(buf, &fake_flash_mem[addr], len); + return 0; +} + +int nl_flash_write(uint32_t addr, const void *buf, uint32_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + if (addr + len > FAKE_FLASH_SIZE) + return -1; + for (uint32_t i = 0; i < len; i++) { + if (s_fail_after == 0) + return -1; + if (s_fail_after > 0) + s_fail_after--; + if (fake_flash_mem[addr + i] != 0xFF) + fake_flash_dirty_writes++; + fake_flash_mem[addr + i] &= p[i]; /* NOR: bits only go from 1 to 0 */ + } + return 0; +} + +int nl_flash_erase_sector(uint32_t addr) +{ + if (addr % NL_FLASH_SECTOR_SIZE || addr >= FAKE_FLASH_SIZE || s_fail_after == 0) + return -1; + memset(&fake_flash_mem[addr], 0xFF, NL_FLASH_SECTOR_SIZE); + fake_flash_erases[addr / NL_FLASH_SECTOR_SIZE]++; + return 0; +} + +uint32_t nl_flash_region_base(void) +{ + return 0; +} diff --git a/test/fake_flash.h b/test/fake_flash.h new file mode 100644 index 0000000..93372ac --- /dev/null +++ b/test/fake_flash.h @@ -0,0 +1,19 @@ +#ifndef FAKE_FLASH_H +#define FAKE_FLASH_H + +#include + +#include "config/nl_flash.h" + +#define FAKE_FLASH_SECTORS 8u +#define FAKE_FLASH_SIZE (FAKE_FLASH_SECTORS * NL_FLASH_SECTOR_SIZE) + +extern uint8_t fake_flash_mem[FAKE_FLASH_SIZE]; +extern unsigned fake_flash_erases[FAKE_FLASH_SECTORS]; +extern unsigned fake_flash_dirty_writes; /* writes onto non-erased bytes (store bug) */ + +void fake_flash_reset(void); +/* Simulate power loss: after `bytes` more written bytes every write/erase fails. -1 = never. */ +void fake_flash_fail_after(long bytes); + +#endif diff --git a/test/test_settings.c b/test/test_settings.c new file mode 100644 index 0000000..7ccab69 --- /dev/null +++ b/test/test_settings.c @@ -0,0 +1,105 @@ +#include +#include "unity.h" +#include "fake_flash.h" +#include "config/nl_flash_layout.h" +#include "config/nl_settings.h" +#include "light/nl_light_types.h" + +void setUp(void) +{ + fake_flash_reset(); + nl_settings_init(NL_FL_SETTINGS_A, NL_FL_SETTINGS_B); +} + +void tearDown(void) +{ + TEST_ASSERT_EQUAL_UINT(0, fake_flash_dirty_writes); +} + +static nl_settings_t sample(const char *name) +{ + nl_settings_t s; + nl_settings_defaults(&s); + strcpy(s.wifi_ssid, "ninHome"); + strcpy(s.wifi_pass, "secret"); + strcpy(s.name, name); + s.auth_all = 1; + s.power_on_behaviour = NL_POB_ON; + s.fade_in_ms = 1200; + return s; +} + +static void test_defaults(void) +{ + nl_settings_t s; + TEST_ASSERT_FALSE(nl_settings_load(&s)); + TEST_ASSERT_EQUAL_STRING("", s.wifi_ssid); + TEST_ASSERT_EQUAL_STRING("aabbccddeeff0011223344", s.token); + TEST_ASSERT_EQUAL_STRING("", s.name); + TEST_ASSERT_EQUAL_UINT8(0, s.auth_all); + TEST_ASSERT_EQUAL_UINT8(NL_POB_LAST, s.power_on_behaviour); + TEST_ASSERT_EQUAL_UINT16(500, s.fade_in_ms); + TEST_ASSERT_EQUAL_UINT16(500, s.fade_out_ms); +} + +static void test_save_and_load(void) +{ + nl_settings_t in = sample("Wohnzimmer"), out; + TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in)); + TEST_ASSERT_TRUE(nl_settings_load(&out)); + TEST_ASSERT_EQUAL_MEMORY(&in, &out, sizeof in); +} + +static void test_alternates_slots(void) +{ + nl_settings_t s, out; + s = sample("a"); nl_settings_save(&s); + s = sample("b"); nl_settings_save(&s); + s = sample("c"); nl_settings_save(&s); + TEST_ASSERT_EQUAL_UINT(2, fake_flash_erases[0]); + TEST_ASSERT_EQUAL_UINT(1, fake_flash_erases[1]); + TEST_ASSERT_TRUE(nl_settings_load(&out)); + TEST_ASSERT_EQUAL_STRING("c", out.name); +} + +static void test_power_loss_keeps_previous(void) +{ + nl_settings_t s = sample("old"), out; + nl_settings_save(&s); + s = sample("new"); + fake_flash_fail_after(50); + TEST_ASSERT_NOT_EQUAL(0, nl_settings_save(&s)); + fake_flash_fail_after(-1); + TEST_ASSERT_TRUE(nl_settings_load(&out)); + TEST_ASSERT_EQUAL_STRING("old", out.name); +} + +static void test_corrupt_newest_falls_back(void) +{ + nl_settings_t s, out; + s = sample("first"); nl_settings_save(&s); /* slot A */ + s = sample("second"); nl_settings_save(&s); /* slot B */ + fake_flash_mem[NL_FL_SETTINGS_B + 40] ^= 0x01; + TEST_ASSERT_TRUE(nl_settings_load(&out)); + TEST_ASSERT_EQUAL_STRING("first", out.name); +} + +static void test_erase(void) +{ + nl_settings_t s = sample("x"), out; + nl_settings_save(&s); + TEST_ASSERT_EQUAL_INT(0, nl_settings_erase()); + TEST_ASSERT_FALSE(nl_settings_load(&out)); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_defaults); + RUN_TEST(test_save_and_load); + RUN_TEST(test_alternates_slots); + RUN_TEST(test_power_loss_keeps_previous); + RUN_TEST(test_corrupt_newest_falls_back); + RUN_TEST(test_erase); + return UNITY_END(); +}