FTL复位丢失修复:journal epoch 锚定最新 checkpoint(根治重格式化循环)+ checkpoint 诊断
This commit is contained in:
@@ -401,3 +401,76 @@ int nand_ftl_format(void) {
|
|||||||
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
|
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
|
||||||
return GD5F_OK;
|
return GD5F_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================== 诊断(跨复位日志持久性分析) ======================== */
|
||||||
|
|
||||||
|
/* 打印当前 journal/map 状态(epoch/head/tail/root/容量) */
|
||||||
|
void nand_ftl_log_state(const char *tag)
|
||||||
|
{
|
||||||
|
const struct dhara_journal *j = &s_map.journal;
|
||||||
|
|
||||||
|
if (!s_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DBG_INFO("FTL state %s: epoch=%u head=%lu tail=%lu root=%lu jsize=%lu map=%lu",
|
||||||
|
tag, (unsigned int)j->epoch,
|
||||||
|
(unsigned long)j->head, (unsigned long)j->tail,
|
||||||
|
(unsigned long)j->root,
|
||||||
|
(unsigned long)dhara_journal_size(j),
|
||||||
|
(unsigned long)dhara_map_size(&s_map));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全扫每块首组 checkpoint 页(magic+epoch),打印连续区段分布,
|
||||||
|
* 用于识别 journal 环上的 epoch 分界与空洞 */
|
||||||
|
void nand_ftl_scan_checks(void)
|
||||||
|
{
|
||||||
|
uint32_t i;
|
||||||
|
uint8_t buf[8];
|
||||||
|
uint32_t total = 0;
|
||||||
|
uint32_t run_start = 0;
|
||||||
|
uint32_t run_prev = 0;
|
||||||
|
uint32_t run_len = 0;
|
||||||
|
uint8_t run_epoch = 0;
|
||||||
|
uint8_t run_open = 0;
|
||||||
|
|
||||||
|
if (!s_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DBG_INFO("check scan start: blocks=%lu", (unsigned long)s_nand.num_blocks);
|
||||||
|
|
||||||
|
for (i = 0; i < s_nand.num_blocks; i++) {
|
||||||
|
const uint32_t pg = ftl_phy_page((i << s_nand.log2_ppb) |
|
||||||
|
((1u << s_map.journal.log2_ppc) - 1u));
|
||||||
|
|
||||||
|
if (gd5f_page_read(pg) != GD5F_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gd5f_read_from_cache(0, buf, sizeof(buf));
|
||||||
|
if (buf[0] == 'D' && buf[1] == 'h' && buf[2] == 'a') {
|
||||||
|
const uint8_t ep = buf[3];
|
||||||
|
|
||||||
|
total++;
|
||||||
|
if (!run_open || (i != run_prev + 1) || (ep != run_epoch)) {
|
||||||
|
if (run_open) {
|
||||||
|
DBG_INFO("check run: blk %lu..%lu epoch=%u (%lu blocks)",
|
||||||
|
(unsigned long)run_start, (unsigned long)run_prev,
|
||||||
|
(unsigned int)run_epoch, (unsigned long)run_len);
|
||||||
|
}
|
||||||
|
run_start = i;
|
||||||
|
run_epoch = ep;
|
||||||
|
run_len = 0;
|
||||||
|
run_open = 1;
|
||||||
|
}
|
||||||
|
run_prev = i;
|
||||||
|
run_len++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (run_open) {
|
||||||
|
DBG_INFO("check run: blk %lu..%lu epoch=%u (%lu blocks)",
|
||||||
|
(unsigned long)run_start, (unsigned long)run_prev,
|
||||||
|
(unsigned int)run_epoch, (unsigned long)run_len);
|
||||||
|
}
|
||||||
|
DBG_INFO("check scan done: %lu checkpoint blocks", (unsigned long)total);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ int nand_ftl_init(void);
|
|||||||
int nand_ftl_format(void);
|
int nand_ftl_format(void);
|
||||||
void nand_ftl_deinit(void);
|
void nand_ftl_deinit(void);
|
||||||
|
|
||||||
|
/* 诊断接口:打印 journal 状态 / 全扫 checkpoint 分布(跨复位持久性分析) */
|
||||||
|
void nand_ftl_log_state(const char *tag);
|
||||||
|
void nand_ftl_scan_checks(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+122
-3
@@ -279,6 +279,99 @@ static dhara_block_t find_last_checkblock(struct dhara_journal *j,
|
|||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Like find_last_checkblock() but without the epoch filter: find the
|
||||||
|
* last block holding a valid checkpoint of ANY epoch. Used to anchor
|
||||||
|
* the epoch on the newest surviving checkpoint (see
|
||||||
|
* dhara_journal_resume).
|
||||||
|
*/
|
||||||
|
static dhara_block_t find_last_checkblock_any(struct dhara_journal *j,
|
||||||
|
dhara_block_t first)
|
||||||
|
{
|
||||||
|
dhara_block_t low = first;
|
||||||
|
dhara_block_t high = j->nand->num_blocks - 1;
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
const dhara_block_t mid = (low + high) >> 1;
|
||||||
|
dhara_block_t found;
|
||||||
|
|
||||||
|
if (find_checkblock(j, mid, &found, NULL) < 0) {
|
||||||
|
if (!mid)
|
||||||
|
return first;
|
||||||
|
|
||||||
|
high = mid - 1;
|
||||||
|
} else {
|
||||||
|
dhara_block_t nf;
|
||||||
|
|
||||||
|
if (((found + 1) >= j->nand->num_blocks) ||
|
||||||
|
(find_checkblock(j, found + 1,
|
||||||
|
&nf, NULL) < 0))
|
||||||
|
return found;
|
||||||
|
|
||||||
|
low = nf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Re-read the first-group checkpoint header of the given block into
|
||||||
|
* j->page_buf. Returns 0 on success. */
|
||||||
|
static int read_checkpage(struct dhara_journal *j, dhara_block_t blk,
|
||||||
|
dhara_error_t *err)
|
||||||
|
{
|
||||||
|
const dhara_page_t p = (blk << j->nand->log2_ppb) |
|
||||||
|
((1 << j->log2_ppc) - 1);
|
||||||
|
|
||||||
|
if (dhara_nand_read(j->nand, p, 0, 1 << j->nand->log2_page_size,
|
||||||
|
j->page_buf, err) < 0 ||
|
||||||
|
!hdr_has_magic(j->page_buf))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First block whose checkpoint carries the current (newest) epoch. The
|
||||||
|
* epoch pattern along the ring is monotonic (one run per lifetime), so
|
||||||
|
* binary search applies; a checkpoint gap (e.g. a run of bad blocks)
|
||||||
|
* falls back to a forward scan.
|
||||||
|
*/
|
||||||
|
static dhara_block_t find_epoch_first(struct dhara_journal *j,
|
||||||
|
dhara_block_t first,
|
||||||
|
dhara_block_t last)
|
||||||
|
{
|
||||||
|
dhara_block_t lo = first;
|
||||||
|
dhara_block_t hi = last;
|
||||||
|
|
||||||
|
while (lo < hi) {
|
||||||
|
const dhara_block_t mid = (lo + hi) >> 1;
|
||||||
|
dhara_block_t cb;
|
||||||
|
dhara_error_t err;
|
||||||
|
|
||||||
|
if (find_checkblock(j, mid, &cb, &err) < 0) {
|
||||||
|
/* Gap: forward-scan checkpoint by checkpoint. */
|
||||||
|
dhara_block_t b = lo;
|
||||||
|
|
||||||
|
while (b <= hi) {
|
||||||
|
dhara_error_t le;
|
||||||
|
|
||||||
|
if (find_checkblock(j, b, &cb, &le) < 0)
|
||||||
|
return last;
|
||||||
|
if (hdr_get_epoch(j->page_buf) == j->epoch)
|
||||||
|
return cb;
|
||||||
|
b = cb + 1;
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hdr_get_epoch(j->page_buf) == j->epoch)
|
||||||
|
hi = cb;
|
||||||
|
else
|
||||||
|
lo = cb + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo;
|
||||||
|
}
|
||||||
|
|
||||||
/* Test whether a checkpoint group is in a state fit for reprogramming,
|
/* Test whether a checkpoint group is in a state fit for reprogramming,
|
||||||
* but allow for the fact that is_free() might not have any way of
|
* but allow for the fact that is_free() might not have any way of
|
||||||
* distinguishing between an unprogrammed page, and a page programmed
|
* distinguishing between an unprogrammed page, and a page programmed
|
||||||
@@ -421,18 +514,44 @@ static int find_head(struct dhara_journal *j, dhara_page_t start,
|
|||||||
|
|
||||||
int dhara_journal_resume(struct dhara_journal *j, dhara_error_t *err)
|
int dhara_journal_resume(struct dhara_journal *j, dhara_error_t *err)
|
||||||
{
|
{
|
||||||
dhara_block_t first, last;
|
dhara_block_t first, last, last_any;
|
||||||
dhara_page_t last_group;
|
dhara_page_t last_group;
|
||||||
|
uint8_t first_epoch;
|
||||||
|
|
||||||
/* Find the first checkpoint-containing block */
|
/* Find the first checkpoint-containing block */
|
||||||
if (find_checkblock(j, 0, &first, err) < 0) {
|
if (find_checkblock(j, 0, &first, err) < 0) {
|
||||||
reset_journal(j);
|
reset_journal(j);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
first_epoch = hdr_get_epoch(j->page_buf);
|
||||||
|
|
||||||
/* Find the last checkpoint-containing block in this epoch */
|
/*
|
||||||
|
* Anchor the epoch on the NEWEST surviving checkpoint, not the
|
||||||
|
* oldest. If the ring wrapped (epoch rolled) or the journal was
|
||||||
|
* abandoned mid-ring (chip swap, firmware change, long power
|
||||||
|
* off), the oldest checkpoint belongs to an older epoch; anchoring
|
||||||
|
* on it makes the epoch filter below reject every current-epoch
|
||||||
|
* checkpoint, silently discarding all writes of the current
|
||||||
|
* lifetime on the next resume. Measured 2026-09-20: this made a
|
||||||
|
* self-heal volume reformat vanish on every reset (infinite
|
||||||
|
* reformat loop, no data persistence).
|
||||||
|
*/
|
||||||
|
last_any = find_last_checkblock_any(j, first);
|
||||||
|
if (read_checkpage(j, last_any, err) < 0) {
|
||||||
|
reset_journal(j);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
j->epoch = hdr_get_epoch(j->page_buf);
|
j->epoch = hdr_get_epoch(j->page_buf);
|
||||||
last = find_last_checkblock(j, first);
|
|
||||||
|
if (first_epoch == j->epoch) {
|
||||||
|
/* Single-epoch ring: original code path, unchanged. */
|
||||||
|
last = find_last_checkblock(j, first);
|
||||||
|
} else {
|
||||||
|
/* Multiple epochs: isolate the current epoch's window,
|
||||||
|
* so the epoch-filtered search below stays sound. */
|
||||||
|
first = find_epoch_first(j, first, last_any);
|
||||||
|
last = find_last_checkblock(j, first);
|
||||||
|
}
|
||||||
|
|
||||||
/* Find the last programmed checkpoint group in the block */
|
/* Find the last programmed checkpoint group in the block */
|
||||||
last_group = find_last_group(j, last);
|
last_group = find_last_group(j, last);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "usart.h"
|
#include "usart.h"
|
||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "gd5f2gq5ue.h"
|
#include "gd5f2gq5ue.h"
|
||||||
|
#include "nand_ftl.h"
|
||||||
#include "lftpd.h"
|
#include "lftpd.h"
|
||||||
#include "../test/ch395f_test_task.h"
|
#include "../test/ch395f_test_task.h"
|
||||||
#include "../test/ch395f_test.h" /* ENABLE_NET_LAYER_TESTS:netTask 条件创建 */
|
#include "../test/ch395f_test.h" /* ENABLE_NET_LAYER_TESTS:netTask 条件创建 */
|
||||||
@@ -262,6 +263,7 @@ void StartDefaultTask(void *argument)
|
|||||||
FS_VOL_PROBE(hfr);
|
FS_VOL_PROBE(hfr);
|
||||||
if (hfr != FR_OK) {
|
if (hfr != FR_OK) {
|
||||||
FS_VOL_PROBE(hfr); /* 两击确认 */
|
FS_VOL_PROBE(hfr); /* 两击确认 */
|
||||||
|
nand_ftl_scan_checks(); /* 诊断:环上 checkpoint 的 epoch 分布 */
|
||||||
}
|
}
|
||||||
if (hfr != FR_OK) {
|
if (hfr != FR_OK) {
|
||||||
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
|
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
|
||||||
@@ -278,6 +280,8 @@ void StartDefaultTask(void *argument)
|
|||||||
fr = f_mount(&default_fatfs, "", 1);
|
fr = f_mount(&default_fatfs, "", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
nand_ftl_log_state("[after-mkfs]");
|
||||||
|
nand_ftl_log_state("[after-mkfs]");
|
||||||
if (fr == FR_OK) {
|
if (fr == FR_OK) {
|
||||||
DBG_INFO("[FS] volume reformatted OK");
|
DBG_INFO("[FS] volume reformatted OK");
|
||||||
} else {
|
} else {
|
||||||
@@ -295,6 +299,7 @@ void StartDefaultTask(void *argument)
|
|||||||
#undef FS_VOL_PROBE
|
#undef FS_VOL_PROBE
|
||||||
if (fr == FR_OK) {
|
if (fr == FR_OK) {
|
||||||
DBG_INFO("[FS] FATFS mounted");
|
DBG_INFO("[FS] FATFS mounted");
|
||||||
|
nand_ftl_log_state("[probe-ok]");
|
||||||
/* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */
|
/* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */
|
||||||
syslog_mcu_mounted();
|
syslog_mcu_mounted();
|
||||||
/* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移;
|
/* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;首启含旧档迁移;
|
||||||
|
|||||||
Reference in New Issue
Block a user