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
+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();
}