From ce2862ae4abdd09a3b1c0ff44fc1dd4e831a36ba Mon Sep 17 00:00:00 2001 From: Raphael Hunziker Date: Thu, 10 Sep 2026 20:41:36 +0200 Subject: [PATCH 1/3] drivers: honour the armed timeout in w25n_waitForReady flashPartitionErase() calls flashWaitForReady(0) after every block erase to wait for the erase to complete. The W25N driver took that as "time out after zero milliseconds": w25n_setTimeout(0) overwrote the 15 ms deadline that w25n_eraseSector() had just armed with a deadline that has already expired, so w25n_waitForReadyInternal() returned false on the first poll that saw the device busy. The following loop iteration then issued write enable and block erase into a device that was still erasing the previous block, where the NAND ignores both instructions, so most blocks of the partition were never erased. Only targets that erase the whole chip through flashEraseCompletely() were unaffected, because that path waits through w25n_waitForReadyInternal(). Interpret a zero timeout as "use the deadline the pending operation armed", which is what the NOR driver m25p16_waitForReady() already does. Fixes #11376 --- src/main/drivers/flash_w25n.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/drivers/flash_w25n.c b/src/main/drivers/flash_w25n.c index c23a1bddfbd..c0efdb922a4 100644 --- a/src/main/drivers/flash_w25n.c +++ b/src/main/drivers/flash_w25n.c @@ -216,9 +216,20 @@ static bool w25n_waitForReadyInternal(void) return true; } +/** + * Wait for the device to become ready. + * + * A timeout of zero means "wait for the deadline the pending operation has already armed", + * which is how flashPartitionErase() waits for a block erase to complete (see also + * m25p16_waitForReady). Arming a fresh zero length timeout instead would replace the erase + * timeout with a deadline that has already expired, and the wait would give up immediately. + */ bool w25n_waitForReady(timeMs_t timeoutMillis) { - w25n_setTimeout(timeoutMillis); + if (timeoutMillis > 0) { + w25n_setTimeout(timeoutMillis); + } + return w25n_waitForReadyInternal(); } From fa458ff96cbc73a2e3fc3f5f5a3d1d5344b9985d Mon Sep 17 00:00:00 2001 From: Raphael Hunziker Date: Thu, 10 Sep 2026 20:44:55 +0200 Subject: [PATCH 2/3] flashfs: check the flash after erasing instead of assuming success flashfsEraseCompletely() reset the write offset to zero in RAM, so the used size read as zero no matter what the chip had actually done. The offset is measured on the flash itself at boot, which is why a failed erase only became visible after a power cycle, with the supposedly deleted log back in place. Look for the start of the free space right after the erase, the same way flashfsInit() does, so that the reported used size is the one the next boot will find. A chip that is still erasing cannot be examined, because its reads time out and the device would be taken for full; that applies to a NOR chip erasing in the background through a single bulk erase instruction, which keeps the previous behaviour there. On the W25N side, w25n_eraseSector() threw away the result of its wait and issued write enable and block erase into a device that was still busy, which ignores both. Skip the block instead, drop the cached page number because the device data buffer may hold a page of the erased block, and let w25n_eraseCompletely() wait for the last block so the flash can be read back as soon as the erase returns. Fixes #11376 --- src/main/drivers/flash_w25n.c | 18 ++++++++++++++++-- src/main/io/flashfs.c | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/drivers/flash_w25n.c b/src/main/drivers/flash_w25n.c index c0efdb922a4..fe3ab4ad221 100644 --- a/src/main/drivers/flash_w25n.c +++ b/src/main/drivers/flash_w25n.c @@ -141,6 +141,9 @@ static bool couldBeBusy = false; static timeMs_t timeoutAt = 0; +// The page held in the device's internal data buffer, or UINT32_MAX if its contents are unknown +static uint32_t currentPage = UINT32_MAX; + static bool w25n_waitForReadyInternal(void); static void w25n_setTimeout(timeMs_t timeoutMillis) @@ -311,10 +314,19 @@ bool w25n_detect(uint32_t chipID) */ void w25n_eraseSector(uint32_t address) { - w25n_waitForReadyInternal(); + // A device that is still busy ignores both the write enable and the erase instruction, so + // issuing them anyway would leave the block unerased without any sign of it. Give up on this + // block instead; the caller sees the result when it reads the flash back. + if (!w25n_waitForReadyInternal()) { + return; + } + w25n_writeEnable(); w25n_performCommandWithPageAddress(W25N_INSTRUCTION_BLOCK_ERASE, W25N_LINEAR_TO_PAGE(address)); w25n_setTimeout(W25N_TIMEOUT_BLOCK_ERASE_MS); + + // The data buffer may still hold a page of the block that is being erased + currentPage = UINT32_MAX; } // W25N does not support full chip erase. @@ -324,6 +336,9 @@ void w25n_eraseCompletely(void) for (uint32_t block = 0; block < geometry.sectors; block++) { w25n_eraseSector(W25N_BLOCK_TO_LINEAR(block)); } + + // Let the last block finish, so that the device is readable once the erase returns + w25n_waitForReadyInternal(); } static void w25n_programDataLoad(uint16_t columnAddress, const uint8_t *data, int length) @@ -388,7 +403,6 @@ bool bufferDirty = false; bool isProgramming = false; static uint32_t programStartAddress; static uint32_t programLoadAddress; -static uint32_t currentPage = UINT32_MAX; void w25n_pageProgramBegin(uint32_t address) { diff --git a/src/main/io/flashfs.c b/src/main/io/flashfs.c index 3140e0e1d52..3a008d43488 100644 --- a/src/main/io/flashfs.c +++ b/src/main/io/flashfs.c @@ -73,7 +73,21 @@ void flashfsEraseCompletely(void) { flashPartitionErase(flashPartition); flashfsClearBuffer(); - flashfsSetTailAddress(0); + + /* Assuming the erase succeeded would report an empty device from RAM while the flash still + * holds the old log, which only becomes apparent after the next reboot. Locate the start of + * the free space on the chip instead, exactly as flashfsInit() does at boot. + * + * A chip that is still erasing cannot be examined, as reads would time out and the device + * would be taken for full. That is the case for a NOR chip erased with a single bulk erase + * instruction, which completes long after this function returns; there the erase is assumed + * to succeed as before, and the next boot corrects the offset if it did not. + */ + if (flashIsReady()) { + flashfsSetTailAddress(flashfsIdentifyStartOfFreeSpace()); + } else { + flashfsSetTailAddress(0); + } } void flashfsClose(void) From 9d112b9d54ed5dd3f1abf9ae36d4621dc7723ceb Mon Sep 17 00:00:00 2001 From: Raffi1202 Date: Fri, 11 Sep 2026 18:02:21 +0200 Subject: [PATCH 3/3] Flush the chip program cache before erasing --- src/main/io/flashfs.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/io/flashfs.c b/src/main/io/flashfs.c index 3a008d43488..34ba4dd22c3 100644 --- a/src/main/io/flashfs.c +++ b/src/main/io/flashfs.c @@ -71,6 +71,8 @@ static void flashfsSetTailAddress(uint32_t address) void flashfsEraseCompletely(void) { + // Drain the driver's page cache before erasing; the free-space scan reuses the chip buffer. + flashFlush(); flashPartitionErase(flashPartition); flashfsClearBuffer();