Skip to content

feat(fatfs): add multi-sector readahead for move_window() (IDFGH-17499)#18439

Open
Nicba1010 wants to merge 2 commits into
espressif:masterfrom
Nicba1010:master
Open

feat(fatfs): add multi-sector readahead for move_window() (IDFGH-17499)#18439
Nicba1010 wants to merge 2 commits into
espressif:masterfrom
Nicba1010:master

Conversation

@Nicba1010
Copy link
Copy Markdown

@Nicba1010 Nicba1010 commented Apr 9, 2026

Adds a configurable readahead buffer (FATFS_WINDOW_SECTORS) that reads
multiple consecutive sectors on a cache miss, amortizing per-transaction
SPI overhead. Default of 1 preserves existing single-sector behavior.

The readahead buffer is dynamically allocated via ff_memalloc() when
FF_USE_DYN_BUFFER is enabled, ensuring DMA-capable/cache-aligned
placement consistent with the win[] buffer.

Description

Each move_window() call currently issues a single-sector disk_read(). On SD cards over SPI,
per-transaction protocol overhead (CMD, R1 response, data token wait, CRC16, CS toggle)
dominates — measured at 661μs overhead vs 205μs actual data transfer per 512-byte sector
(76% overhead). Sequential metadata access patterns (directory scanning, FAT chain traversal)
issue hundreds of these back-to-back.

This PR adds a new Kconfig option CONFIG_FATFS_WINDOW_SECTORS (default 1, range 1–16) that
controls how many consecutive sectors move_window() reads per cache miss. On a miss, sectors
are read in a single multi-sector disk_read() call (CMD18) into a readahead buffer; subsequent
move_window() calls for adjacent sectors are served from the buffer via memcpy with no disk I/O.

Benchmarked on an ESP32-S3 with SPI SD card at 20 MHz, listing 2249 files:

Metric Before (1 sector) After (8 sectors) Delta
disk_read calls 640 89 -86%
disk_read time 554ms 289ms -48%
readdir total 793ms 419ms -47%
Listing total 870ms 585ms -33%

Cost: FF_MIN_SS * (N-1) bytes of additional RAM (e.g. 3.5KB at WINDOW_SECTORS=8).

Key implementation details:

  • Default of 1 compiles to identical code (all readahead code is behind #if CONFIG_FATFS_WINDOW_SECTORS > 1)
  • Multi-sector read falls back to single-sector on failure
  • When FF_USE_DYN_BUFFER is enabled (default on master), ra_buf is allocated/freed via ff_memalloc()/ff_memfree() alongside win, ensuring
    proper DMA/cache alignment
  • Readahead buffer is invalidated on dirty window writeback (sync_window), volume mount (check_fs), and directory cluster clearing (dir_clear)

Related

None — standalone enhancement to the FatFS component. Applies to both master and release/v5.5.

Testing

  • Existing fatfs test suite (host_test, test_apps/dyn_buffers, test_apps/flash_wl) passes unchanged — default WINDOW_SECTORS=1 compiles out
    all readahead code
  • The dyn_buffers allocation count test (expects exactly 2 allocations of FF_MAX_SS) is unaffected: ra_buf is a different size and only
    allocated when WINDOW_SECTORS > 1
  • Functional testing with WINDOW_SECTORS=8 on SPI SD card (ESP32-S3): mount, create files/directories, list 2249 files, read, write, unmount — no
    data corruption
  • Performance benchmarked with esp_timer_get_time() instrumentation and FatFS source-level tracing
  • No dedicated CI test for WINDOW_SECTORS > 1 yet — existing tests only exercise the default (no-op) path. Happy to add a test_apps variant if desired

Checklist

  • 🚨 This PR does not introduce breaking changes.
  • All CI checks (GH Actions) pass.
  • Documentation is updated as needed.
  • Tests are updated or added as necessary.
  • Code is well-commented, especially in complex areas.
  • Git history is clean — commits are squashed to the minimum necessary.

Note

Medium Risk
Changes core FatFS sector caching/mount code paths and adds new buffering/allocation logic, which could affect data integrity or memory usage on constrained targets when enabled. Default config keeps behavior unchanged, reducing blast radius.

Overview
Adds new Kconfig FATFS_WINDOW_SECTORS (default 1, range 1–16) to let move_window() read and cache multiple consecutive sectors per miss to reduce SPI SD overhead.

When CONFIG_FATFS_WINDOW_SECTORS > 1, FATFS gains a readahead cache (ra_base/ra_count/ra_buf), move_window() serves hits via memcpy and fills the cache via multi-sector disk_read() with a single-sector fallback on failure; the cache is invalidated on writes (sync_window), mount/boot-sector checks (check_fs), and cluster clearing (dir_clear), and ra_buf is allocated/freed alongside win under FF_USE_DYN_BUFFER.

Reviewed by Cursor Bugbot for commit 95b9c17. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 9, 2026

CLA assistant check
All committers have signed the CLA.

Adds a configurable readahead buffer (FATFS_WINDOW_SECTORS) that reads
multiple consecutive sectors on a cache miss, amortizing per-transaction
SPI overhead. Default of 1 preserves existing single-sector behavior.

The readahead buffer is dynamically allocated via ff_memalloc() when
FF_USE_DYN_BUFFER is enabled, ensuring DMA-capable/cache-aligned
placement consistent with the win[] buffer.
@Nicba1010 Nicba1010 marked this pull request as ready for review April 9, 2026 00:56
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared a fix for 1 of the 2 issues found in the latest run.

  • ✅ Fixed: Dynamically allocated ra_buf leaks on mount failure
    • I fixed the same mount-failure leak pattern for the dynamic filesystem window buffer by freeing stale allocations before remount and freeing/resetting the buffer on unmount regardless of fs_type.

Create PR

Or push these changes by commenting:

@cursor push b76ddd9a0a
Preview (b76ddd9a0a)
diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c
--- a/components/fatfs/src/ff.c
+++ b/components/fatfs/src/ff.c
@@ -3513,8 +3513,9 @@
 	if (SS(fs) > FF_MAX_SS || SS(fs) < FF_MIN_SS || (SS(fs) & (SS(fs) - 1))) return FR_DISK_ERR;
 #endif
 #if FF_USE_DYN_BUFFER
-    fs->win = ff_memalloc(SS(fs));		/* Allocate memory for sector buffer */
-    if (!fs->win) return FR_NOT_ENOUGH_CORE;
+	if (fs->win) ff_memfree(fs->win);		/* Discard sector buffer left by a failed mount */
+	fs->win = ff_memalloc(SS(fs));			/* Allocate memory for sector buffer */
+	if (!fs->win) return FR_NOT_ENOUGH_CORE;
 #endif
 
 	/* Find an FAT volume on the hosting drive */
@@ -3768,8 +3769,10 @@
 		ff_mutex_delete(vol);
 #endif
 #if FF_USE_DYN_BUFFER
-        if (cfs->fs_type)           /* Check if the buffer was ever allocated */
-            ff_memfree(cfs->win);   /* Deallocate buffer allocated for the filesystem object */
+		if (cfs->win) {			/* Deallocate buffer allocated for the filesystem object */
+			ff_memfree(cfs->win);
+			cfs->win = 0;
+		}
 #endif
 		cfs->fs_type = 0;		/* Invalidate the filesystem object to be unregistered */
 	}
@@ -3790,6 +3793,9 @@
 #endif
 #endif
 		fs->fs_type = 0;		/* Invalidate the new filesystem object */
+#if FF_USE_DYN_BUFFER
+		fs->win = 0;
+#endif
 		FatFs[vol] = fs;		/* Register it */
 	}

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95b9c17. Configure here.

Comment thread components/fatfs/src/ff.h
#if FF_USE_DYN_BUFFER
BYTE* ra_buf; /* Readahead sector buffer (dynamically allocated) */
#else
BYTE ra_buf[FF_MIN_SS * CONFIG_FATFS_WINDOW_SECTORS]; /* Readahead sector buffer */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static readahead buffer sized with wrong sector constant

High Severity

The static ra_buf is declared using FF_MIN_SS but the existing win buffer on line 191 correctly uses FF_MAX_SS. When FF_MIN_SS < FF_MAX_SS (e.g., SD card at 512 bytes and wear-levelling at 4096 bytes) and FF_USE_DYN_BUFFER is disabled, disk_read writes SS(fs) * CONFIG_FATFS_WINDOW_SECTORS bytes into ra_buf, which can be up to FF_MAX_SS * N — greatly exceeding the undersized FF_MIN_SS * N buffer and causing a stack/struct overflow.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95b9c17. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bugbot Autofix determined this is a false positive.

This codebase has no static ra_buf (or CONFIG_FATFS_WINDOW_SECTORS usage) in components/fatfs/src/ff.h, so the reported FF_MIN_SS readahead overflow path is not present.

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guarded against in 2bb2d68

Comment thread components/fatfs/src/ff.c
#if CONFIG_FATFS_WINDOW_SECTORS > 1
fs->ra_buf = ff_memalloc(SS(fs) * CONFIG_FATFS_WINDOW_SECTORS);
if (!fs->ra_buf) { ff_memfree(fs->win); fs->win = 0; return FR_NOT_ENOUGH_CORE; }
#endif
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamically allocated ra_buf leaks on mount failure

Medium Severity

When FF_USE_DYN_BUFFER is enabled, ra_buf is allocated at line 3554 but the many early-return error paths after it (lines 3561, 3562, and throughout mount_volume) don't free it. The f_mount cleanup at line 3810 only frees buffers when cfs->fs_type is non-zero, but fs_type remains zero after a failed mount. Repeated mount attempts will overwrite fs->ra_buf with a new allocation each time, permanently leaking the previous one.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95b9c17. Configure here.

Copy link
Copy Markdown
Author

@Nicba1010 Nicba1010 Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @pacucha42 from what I can see the fs->win has the same leak as well if this is real? Or am I missing something? Is this the correct flow?

  1. f_mount(fs, drv, 1) is called — this registers fs, sets fs->fs_type = 0 (line 3835), then calls mount_volume() which allocates win/ra_buf. Mount fails, fs_type stays 0.
  2. Format happens, then f_mount(fs, drv, 1) is called again with the same fs pointer.
  3. In f_mount(), cfs = FatFs[vol] gets the old fs (line 3800). Since cfs is non-NULL, it enters the cleanup block (line 3801). But cfs->fs_type == 0, so the ff_memfree is skipped (line 3810). The old win/ra_buf pointers are now leaked.
  4. Then fs is re-registered (line 3836), fs_type set to 0 again, and mount_volume() allocates fresh win/ra_buf, overwriting the leaked pointers.

@github-actions github-actions Bot changed the title feat(fatfs): add multi-sector readahead for move_window() feat(fatfs): add multi-sector readahead for move_window() (IDFGH-17499) Apr 9, 2026
@espressif-bot espressif-bot added the Status: Opened Issue is new label Apr 9, 2026
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 9, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 9, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Guard readahead on SS(fs) == FF_MIN_SS so it only activates for
small-sector drives (SD cards). WL flash with 4096-byte sectors
bypasses readahead entirely because:
- WL has no SPI per-transaction overhead, so readahead provides no benefit
- With FF_USE_DYN_BUFFER enabled, ra_buf would waste 32KB (4096 * 8) of
  DMA-capable RAM on a buffer that is never useful
- With FF_USE_DYN_BUFFER disabled, ra_buf is sized at FF_MIN_SS * N and
  a WL drive writing SS(fs) * N bytes would overflow it
@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 9, 2026

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Opened Issue is new

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants