Skip to content

afatfs: the free space count and the freefile refill take turns (#12156 with #12060) - #12158

Open
MrScothh wants to merge 36 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:fix/afatfs-recount-after-refill
Open

MrScothh wants to merge 36 commits into
iNavFlight:maintenance-10.xfrom
MrScothh:fix/afatfs-recount-after-refill

Conversation

@MrScothh

@MrScothh MrScothh commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #12156 (the card's free space over MSP) and #12060 (an SD log continued in a new file), both on #12056: this branch merges the two and adds one commit, which is the change of its own.

It has #12156's later commits too: the count runs only when the Configurator asks for it, never armed, and the space taken outside the freefile is told apart (log ends, terrain data, lost space). They merge here without changes to this PR's own commit.

In #12156 I wrote that whichever of the two is merged second would need its refill to keep the count right, and that I would add it there. Here it is as its own PR instead, so that neither of the two has to wait for the other: it is what joins them. Testing it showed a second problem besides the one I expected.

  1. The count gave up while the refill searched the FAT. afatfs: report the whole SD card's free space over MSP #12156's count pauses while files are open, not while blackbox: continue an SD log in a new file when it can't grow #12060's refill searches the FAT for the largest free block or claims clusters. One of the count's FAT sector reads then waits behind the refill's, passes the 1 s after which the count takes the sector as unreadable, and the count gives up: the card's free space stays unknown until the next boot. The count now also pauses while the refill isn't idle.
  2. The refill's clusters were counted twice. The refill extends the freefile, or moves it to the largest free block once a log has used it up, with its own FAT writes rather than through the allocation paths that call afatfs_freeCountAdjust(). The clusters it claims stayed counted as free outside the freefile while also making the freefile bigger. Once the refill has saved the freefile's new size, the count now starts over and runs again the next time it is asked for with no file open, as after boot; until then MSP_SDCARD_SUMMARY reports the free space as not counted yet. A count that gave up on a really unreadable FAT sector stays given up.

The merge itself: the free space count and the refill both run in the READY state, each under its own #ifdef.

Tested in the SITL with --sdcard. USE_BLACKBOX_SDCARD_SPLIT is only defined for targets over 512 KB of flash, so for the test I defined it in the SITL target too. The card: 1 GB, 4 KB clusters, full but for two holes of 1.6 and 2.15 MB, so that the freefile (1 MB, in the larger hole) is used up by a short log and then moved to the other hole. Checked against the image's FAT read by an independent script, which gives 4264 KiB free at boot and 3236 KiB after the log and the move:

Build At boot After the log, once the freefile has moved
#12156 + #12060 merged never counted: the count gives up while the refill searches for the largest free block, and stays "not counted yet" for the 10 minutes of the test
with the pause only 4264 KiB 3752 KiB: the moved freefile, 516 KiB, counted twice
with this PR 4264 KiB not counted yet for 36 s, then 3236 KiB

Size against the merge of #12156 and #12060: +32 B of flash on KAKUTEF7, KAKUTEF7HDV and TBS_LUCID_H7_WING, +56 B on MATEKF405SE, nothing else; MATEKF722SE and NEUTRONRCF435WING unchanged. Unit tests 620/620.

…boot

The freefile is created once, on the largest free block of the card and
at most 4 GB (the largest FAT32 file), and reused as it is at every later
boot. Its size is a whole number of superclusters plus one cluster, so
once the logs have taken the superclusters one cluster is left: the card
reads as full and the blackbox stops logging until the card is formatted,
although the rest of a card larger than 4 GB and any log deleted from a PC
are free.

When the freefile is below 2 GB at boot, search the FAT for the largest
free block as on the first boot, and if that is larger, free the old
freefile's clusters and move the freefile there. Larger freefiles are
reused without a search, as before.
…w it

Logs take their clusters from the start of the freefile, so it only ever
shrank until the next format, and one log could never grow past what was
left of it at boot. On a card larger than 4 GB the clusters that follow
the freefile are normally free, so at boot extend it over them, up to the
largest file size, leaving AFATFS_FREEFILE_LEAVE_CLUSTERS for regular
files as a new freefile does. Only a freefile that is still below 2 GB
afterwards is moved to the largest free block.

The new clusters are chained and flushed before the freefile's last
cluster points at them, and the new size is saved after that, so a power
cut leaves at worst unreferenced clusters, never a chain that runs into
free ones.
After the skip the search went on reading the FAT sector of the freefile's
first cluster while the cluster number had moved past it, so the entries
it examined belonged to other clusters. It could only matter when those
sectors were already cached, and the search at boot now runs with the
freefile in place.
afatfs_FATFillWithPattern() counted a final FAT sector it only writes part
of among the sectors to pre-erase. That sector has to be read first, and
on an SD card on SPI the read ends the multiple-block write started with
ACMD23 before it reaches that block; the SD specification leaves an
unwritten pre-erased block undefined, erased or old. Its other entries,
which can belong to the next file, could then be read back erased. A new
freefile ends that way (whole superclusters plus one cluster), and so
does every extension of it.
afatfs_getFreeBufferSpace() counted every unlocked sector that was
empty or in sync, but afatfs_allocateCacheSector() never evicts one
with a retain count: a contiguous file keeps its directory entry
retained for as long as it is open, and the blackbox log and the free
file are both contiguous. With a log open, two of the 8 sectors were
reported as space the blackbox could write into, and could not be.

Measured on a TBS Lucid H7 while the SD card held a write for 54 ms:
afatfs_fwrite() refused bytes while this function still reported
1024 bytes free.
An SD card can hold a write for tens of milliseconds (the standard
allows 250). The blackbox keeps writing into the other cache sectors
meanwhile, and once none is left it drops what it writes. With 8
sectors, and 3 of them never free while a log is open, the cache held
about 30 ms of a 1 kHz log.

Measured on a TBS Lucid H7 whose card holds a write for 54 ms every
768 KB once it runs out of prepared blocks, 120 s logs:

  sectors   1 kHz log (85 kB/s)     2 kHz log (170 kB/s)
            rows lost               rows lost
  8         379                     2178
  12        8                       1282
  16        0                       703
  24        0                       0
  32        0                       0 (also over a 20 minute log)

Now 32 sectors on H7 and 24 on F7, where RAM is plenty. On F4 with the
card on SPI the cache moves to CCM with 24 sectors: RAM is short there,
CCM mostly unused, and the CPU writes the card, so no DMA needs to
reach it. Elsewhere (F4 with SDIO, AT32) it stays at 8. A target can
set AFATFS_NUM_CACHE_SECTORS itself. Targets without an SD card do not
link the cache.

Walking the larger cache costs afatfs_poll() about 8 ns per sector on
the H7, in the scheduler's idle loop.
… card has room

With the freefile refilled at boot, every power cycle starts with up to
4 GB for logs, but within one power cycle the freefile only shrinks: when
it runs out, or a log reaches the largest FAT32 file, the blackbox stops
for the rest of the flight although the card may have room.

Behind USE_BLACKBOX_SDCARD_SPLIT, on by default on targets with more than
512 KB of flash, afatfs refills the freefile in the background once it is
below 256 MB: it extends it over the free clusters that follow it, 16 MB
at a time, and when nothing free follows it searches the FAT for the
largest free block, and moves the freefile there once it has run out and
the log that used it has been closed. Regular cluster allocations wait
while clusters are being claimed, and a contiguous file can no longer
grow past the largest file size.

The blackbox closes a log that can't grow when afatfs can make room,
instead of stopping, and the usual start path opens the next file once
there is room, without waiting for a serial port before the header. The
card only counts as full once no new file can get room either.
…a full card stops as before

A log created while the freefile was being refilled could not grow, so the
blackbox split it at once and, with no frame logged, deleted it. Deleting
a contiguous file with no clusters ran the contiguous erase from cluster
0 up to the freefile. Truncating a file with no clusters now only saves
its directory entry, and a split log's new file waits until the freefile
has room for its first supercluster, as long as the refill can still bring
some: when it can't, the file opens as without the option and the full
card stops the log.

When a regular cluster allocation finds no free cluster, the card is full
for more than the freefile: the refill no longer clears that, and the
blackbox stops as it did without the option.
…rectory entry each step

A power cut while the freefile was regrown or moved at boot left whatever
chain was in flight as a lost FAT chain: up to 4 GB of new chain, and up to
2 GB of the old freefile when moving it. Three cuts on a TBS Lucid H7 Wing
left three lost chains of 650 MB on average.

The new chain now grows in 256 MB blocks, each one chained, linked, saved and
flushed before the next. A moved freefile gives its old clusters back the
same way from its end: the smaller size is saved, then the chain is cut there,
then the tail is freed. A cut strands at most one block either way.
The freefile holds only part of the card, at most 4 GiB, and with the regrow
at boot later logs also reach the free clusters outside it. After mount, while
no file is open, those clusters are counted one FAT sector at a time with a
gap after each, so that terrain can still take the card while afatfs is idle,
and the count is kept up to date by allocations and frees.

MSP_SDCARD_SUMMARY gets the freefile plus that count appended as
cardFreeSpaceKB, 0xFFFFFFFF until the count is done (or for good if a FAT
sector cannot be read). Older configurators read the fields before it as
they did.
…dy card

The freefile's chain holds no free cluster, so the count no longer reads its
FAT sectors: about a quarter of them on a 16 GB card, nearly all on cards of
4 GB or less. And a removed card's count stays in afatfs, so the summary sends
it only while the card is ready.
…n top of iNavFlight#12156

Both build on iNavFlight#12056. The free space count and the freefile refill both run in
the READY state, each under its own #ifdef.
Two ways iNavFlight#12060's refill upsets iNavFlight#12156's count of the card's free space:

- The count pauses while files are open but not while the refill searches
  the FAT for the largest free block or claims clusters. One of the count's
  FAT sector reads then waits behind the refill's and passes the 1 s after
  which the count takes the sector as unreadable and gives up: in the SITL
  the count gave up at every boot where the refill searched, and the free
  space stayed unknown until the next one. The count now pauses while the
  refill is not idle.
- The refill extends or moves the freefile with its own FAT writes, past
  afatfs_freeCountAdjust(), so the clusters it claimed stayed counted as
  free outside the freefile while also making it bigger. The count now
  starts over once the refill has saved the freefile, with its timing
  restarted as at mount, and runs again the next time no file is open. A
  count that gave up on an unreadable FAT sector stays given up.
@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Keep SD-card free-space counts accurate during freefile refills

🐞 Bug fix ✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Pause background FAT counting during refills, then recount after claimed clusters change hands.
• Regrow or relocate the freefile so logging can use space beyond its original allocation.
• Continue logs in new files when needed and expose whole-card free space through MSP.
Diagram

graph TD
  BB["Blackbox logger"] --> IO["SD log I/O"] --> REF["Freefile refill"] --> FAT["FAT and cache"] --> COUNT["Free-space count"] --> MSP["MSP summary"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adjust the count for every refill FAT write
  • ➕ Could keep the reported count available during a refill without a full recount.
  • ➖ Requires accounting for each claim, release, partial operation, and count-cursor position.
  • ➖ Increases the risk of silently reporting incorrect space after interrupted operations.

Recommendation: Prefer the PR's pause-and-recount strategy: it makes the result temporarily unknown rather than risking a plausible but double-counted value. The alternative may reduce the recount delay, but substantially complicates FAT allocation accounting.

Files changed (10) +861 / -33

Enhancement (5) +85 / -2
blackbox.cTransition full SD logs to fresh files +43/-1

Transition full SD logs to fresh files

• Detects when an SD log cannot grow but another can be started, then shuts down the current log. Speeds up the new file's header within a per-iteration time and buffer budget.

src/main/blackbox/blackbox.c

blackbox_io.cDistinguish a full log from a full SD card +29/-1

Distinguish a full log from a full SD card

• Waits for freefile room before opening a new log when splitting is enabled, and exposes checks for continuation and spare cache capacity. Treats the device as full only when another log cannot continue.

src/main/blackbox/blackbox_io.c

blackbox_io.hDeclare SD log continuation checks +4/-0

Declare SD log continuation checks

• Adds feature-gated declarations for deciding whether a new log is needed and whether the cache has room for faster header output.

src/main/blackbox/blackbox_io.h

fc_msp.cAppend whole-card free space to MSP_SDCARD_SUMMARY +3/-0

Append whole-card free space to MSP_SDCARD_SUMMARY

• Serializes the counted free space after existing fields, preserving their positions for older clients. Reports an unknown sentinel when the card is not ready and zero when SD-card support is disabled.

src/main/fc/fc_msp.c

asyncfatfs.hExpose whole-card space and freefile readiness +6/-0

Expose whole-card space and freefile readiness

• Declares the whole-card free-space query and unknown sentinel, plus feature-gated checks used to decide whether SD logging can continue.

src/main/io/asyncfatfs/asyncfatfs.h

Bug fix (1) +759 / -28
asyncfatfs.cRegrow the freefile and coordinate refills with free-space counting +759/-28

Regrow the freefile and coordinate refills with free-space counting

• Adds boot-time freefile extension or relocation, background refills, and paced counting of free FAT clusters. Pauses counting during refills and restarts it after a saved refill, while preserving an unrecoverable read failure; also adjusts cache capacity and FAT operation safeguards.

src/main/io/asyncfatfs/asyncfatfs.c

Documentation (3) +15 / -3
Blackbox.mdDocument SD-card freefile growth and log splitting +5/-0

Document SD-card freefile growth and log splitting

• Explains where SD logs and reserved free space are stored, how deleted logs become reusable after boot, and when supported targets continue a log in a new file.

docs/Blackbox.md

README.mdDescribe whole-card free space in the SD summary +2/-1

Describe whole-card free space in the SD summary

• Distinguishes space available to the next log from whole-card free space and documents when the new field reports an unknown value.

docs/development/msp/README.md

msp_messages.jsonAdd cardFreeSpaceKB to the MSP message specification +8/-2

Add cardFreeSpaceKB to the MSP message specification

• Appends the whole-card free-space field, clarifies the existing freeSpaceKB field, and increments the specification patch version.

docs/development/msp/msp_messages.json

Other (1) +2 / -0
common.hEnable SD log splitting on larger-flash targets +2/-0

Enable SD log splitting on larger-flash targets

• Enables the optional SD-card log-splitting behavior for targets with more than 512 KB of flash.

src/main/target/common.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Freed space cannot restore logging ✓ Resolved
Description
afatfs_appendRegularFreeClusterContinue() latches refill.regularFull after a failed regular
allocation, but freeing clusters or successfully refilling the freefile never clears it. If files
are deleted after that failure, afatfs_freeFileCanContinue() still vetoes continuation and the
refill path cannot clear filesystemFull, so the newly available space cannot be used for another
log until remount.
Code

src/main/io/asyncfatfs/asyncfatfs.c[R1730-1731]

+            // Full for more than the freefile: refilling it doesn't help, and the card stays full
+            afatfs.refill.regularFull = true;
Evidence
The new flag is set on a failed regular allocation and unconditionally blocks continuation. Normal
truncation returns clusters to the FAT, but does not reset that flag; both paths that clear the
filesystem-full latch require the flag to be false.

src/main/io/asyncfatfs/asyncfatfs.c[1726-1733]
src/main/io/asyncfatfs/asyncfatfs.c[2643-2655]
src/main/io/asyncfatfs/asyncfatfs.c[4146-4176]
src/main/io/asyncfatfs/asyncfatfs.c[4316-4325]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`regularFull` remains set after a failed regular allocation even if a later truncation frees regular clusters, preventing refill and new logs from using the recovered space.
## Fix Focus Areas
- src/main/io/asyncfatfs/asyncfatfs.c[1726-1733]
- src/main/io/asyncfatfs/asyncfatfs.c[2643-2655]
- src/main/io/asyncfatfs/asyncfatfs.c[4146-4176]
## Recommended Fix
Revalidate or clear the regular-allocation-full condition when a regular cluster is freed, and clear the associated filesystem-full latch when an allocation can be retried. Preserve the full condition while no usable regular space exists.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Some cards report 1 KiB too little ✓ Resolved
Description
afatfs_getFreeSpaceKB() divides the freefile size and the external free-cluster count separately,
discarding a fractional KiB from each before adding them. On a supported card with 512-byte
clusters, an odd number of freefile sectors and an odd number of free sectors outside it combine
into a full KiB that the reported total omits.
Code

src/main/io/asyncfatfs/asyncfatfs.c[3550]

+    return afatfs.freeFile.logicalSize / 1024 + afatfs.freeCountClusters * afatfs.sectorsPerCluster / 2;
Evidence
The volume parser accepts one 512-byte sector per cluster, and the freefile can contain one extra
cluster. For a 512-byte freefile and one external free cluster, the expression at line 3550 returns
zero although the combined free space is 1 KiB.

src/main/io/asyncfatfs/asyncfatfs.c[1155-1159]
src/main/io/asyncfatfs/asyncfatfs.c[3542-3550]
src/main/io/asyncfatfs/asyncfatfs.c[3599-3609]
src/main/io/asyncfatfs/asyncfatfs.c[3720-3734]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The whole-card free-space calculation separately truncates two 512-byte components, under-reporting by 1 KiB when both have odd sector counts.
## Fix Focus Areas
- src/main/io/asyncfatfs/asyncfatfs.c[3542-3550]
## Recommended Fix
Combine the freefile's sector count and the external free-sector count before converting their sum to KiB. Use sufficiently wide intermediate arithmetic for supported volume sizes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can import rules and skills only from the repositories your team follows

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/io/asyncfatfs/asyncfatfs.c
Comment thread src/main/io/asyncfatfs/asyncfatfs.c Outdated
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

RAM / Flash usage vs. base commit 13ebed9 — commit 1986377

Target Flash Δ RAM Δ
MATEKF405 ⚠️ +4368 B (+0.60%) CCM: +12320 B (+37.84%)
RAM: -3780 B (-3.63%)
MATEKF722 ⚠️ +1960 B (+0.41%) ITCM_RAM: ±0 B (±0.00%)
RAM: +8552 B (+9.85%)
TCM: ±0 B (±0.00%)
MATEKF765 ⚠️ +5656 B (+0.75%) DTCM_RAM: -4 B (-0.01%)
SRAM1: +8576 B (+6.84%)
MATEKH743 ⚠️ +5252 B (+0.66%) D2_RAM: ±0 B (±0.00%)
DTCM_RAM: ±0 B (±0.00%)
ITCM_RAM: +24 B (+0.16%)
RAM: +12712 B (+8.93%)

See RAM/flash optimization guide for techniques to reduce usage.

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Test firmware build ready — commit 1986377

Download firmware for PR #12158

251 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

…new log

regularFull stayed set after the regular allocation that failed, so the room freed when blackbox deleted an
empty log was never offered to the next one until the card was mounted again.
…ding to KiB

With 512-byte clusters an odd freefile and an odd outside count each lost half a KiB.
The count read the whole FAT after every boot, from the realtime callbacks,
and went on while armed whenever no file was open (a card used only for
terrain, or before the log opens). It now starts at the first
MSP_SDCARD_SUMMARY, which the Configurator sends from the Blackbox tab, and
arming stops it; a stopped count resumes where it was at the next request.
After the free count the card's directories are read, one sector at a time
with the count's gap, and MSP_SDCARD_SUMMARY gets three fields appended:

- logTailsKB: each log takes whole superclusters from the freefile, and the
  rest of its last one stays taken without data until the log is deleted;
- terrainKB: the terrain files (*.TER);
- lostKB: what is taken outside the freefile but held by no file or
  directory, left by writes cut short.

Any change to a directory entry makes the walk run again at the next request.
A directory that can't be read, or nests deeper than 4, leaves the three
unknown until the card changes.
…r-refill

# Conflicts:
#	src/main/io/asyncfatfs/asyncfatfs.c
@MrScothh

Copy link
Copy Markdown
Contributor Author

The deltas are the stack this PR builds on against an older base, mostly #12057's larger afatfs cache (24 sectors in CCM on F4 with the card on SPI, 24 on F7 and 32 on H7 in RAM). This PR's own commit adds 32 to 56 B of flash and no RAM against the merge of #12156 and #12060.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant