feat: Flash-HAL-Interface und Settings-Store mit A/B-Slots

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
This commit is contained in:
Michael Sauer
2026-09-11 13:24:52 +02:00
parent e0bde7bee3
commit 91721ac47d
8 changed files with 360 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
#ifndef NL_FLASH_H
#define NL_FLASH_H
#include <stdint.h>
#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
+12
View File
@@ -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
+117
View File
@@ -0,0 +1,117 @@
#include "config/nl_settings.h"
#include <stddef.h>
#include <string.h>
#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;
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef NL_SETTINGS_H
#define NL_SETTINGS_H
#include <stdbool.h>
#include <stdint.h>
#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
+1
View File
@@ -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)
+60
View File
@@ -0,0 +1,60 @@
#include "fake_flash.h"
#include <string.h>
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;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef FAKE_FLASH_H
#define FAKE_FLASH_H
#include <stdint.h>
#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
+105
View File
@@ -0,0 +1,105 @@
#include <string.h>
#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();
}