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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user