All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
A card that stopped responding after a reset now starts normally. Cards that sat at the edge of the driver's read timing carry full margin, both SD sockets on a board behave alike, and high-speed mode can be left without stranding the card.
-
Compacting a file no longer damages an unrelated file.
compactFile()looks for a run of free clusters to move a file into, and that search read the first 512 entries of the allocation table from stale memory instead of from the card. On a card whose clusters are small enough that files span several of them — which is typical of cards up to a few gigabytes — it could therefore treat clusters belonging to another file as free, move data across them, and leave that other file pointing at space the card considers unused. The file's own directory entry still looked correct, so the damage was silent until the volume was checked, and the same fault made the free-space total reported at unmount unreliable. Cards formatted with large clusters were unaffected, because a file that fits in one cluster is never compacted. -
A card that fails high-speed verification now reports why.
attemptHighSpeed()documents thatERROR()distinguishes a card that declined the feature from one that could not be asked or failed verification. One path did not follow it: when the card switched to high speed but the verification read came back with the wrong data, the driver fell back correctly but reported no error — so a card whose link corrupts data at high speed looked exactly like a card that simply does not offer the feature.ERROR()now returnsE_IO_ERRORon that path, as it already did for every other high-speed failure. -
A card can be left unusable by the boot process, and now recovers on its own. On boards where the microSD socket shares pins with the boot flash — including the P2 Edge — the boot sequence drives those pins before any user code runs, and the two roles do not line up: the pin the card uses for chip-select is the flash clock. A card that was in use before the reset can be left mid-transfer by this, after which it is streaming rather than listening, and
mount()reportsE_NO_CARDon a card that is present and healthy. Only removing power cleared it.The driver now issues a stop-transmission command at the start of card initialisation, which returns such a card to a state where it accepts commands. This runs on every start, costs one command, and does nothing on a card that is already idle.
The symptom was intermittent and long-standing: it required a card that had been used before the reset, and it appeared most readily on marginal or counterfeit cards. If you have seen a card work from a cold power-up and fail after re-downloading a program, this was why.
-
Streamer read alignment moved to the centre of its measured passing band. The phase pad the read path uses to sample MISO previously sat at the band's lower edge, which is where cards with slow clock-to-output timing, and sockets with longer wiring, ran out of margin first. Cards that were marginal on an external adapter socket, or at elevated clocks, now sit in the middle of the window on both sockets.
-
setSPISpeed()is bounded at the card's declared maximum, itself capped at the SD SPI-mode 25 MHz, and lifted to 50 MHz only while verified high-speed mode is active. Requests above the bound return the clamped frequency rather than the request. Above-spec operation was measured producing silent whole-sector write corruption on some cards, and is now reserved for characterization tools viadebugSetOverspeedAllowed()underSD_INCLUDE_DEBUG.
-
Leaving high-speed mode no longer strands the card. The CMD6 high-speed switch is sticky at the card: a host that lowered its clock without switching the card back left the two running on different timings, and every subsequent operation failed with
E_IO_ERRORuntil the card was re-initialised. All four ways out of high-speed mode — a failed switch, a failed verification, a latersetSPISpeed(), andunmount()— now switch the card back. -
unmount()returns the card in default speed mode, rather than handing the next user a card in a mode they never asked for.
isHighSpeedActive()reports the card's mode, not a clock threshold. It previously answeredTRUEonly at 50 MHz or above. The SPI clock issysclk / (2 x hp)with integerhp, so most system clocks cannot produce exactly 50 MHz — at 350 MHz sysclk high-speed mode runs at 43.75 MHz, and the old test answeredFALSEwhile the mode was genuinely active. Callers that used it as a proxy for "is the clock fast" should readgetSPIFrequency()instead.
-
The driver can now report its own version.
driverVersion()returns the major, minor and patch numbers;driverVersionString()returns them as printable text. Both are available in every build, including a core-only one with no feature flags set. Performance figures depend on the driver that produced them as much as on the card, so a measurement or a log can now name the driver it came from. -
debugSetOverspeedAllowed()anddebugSetAlignFloorRuleEnabled()(SD_INCLUDE_DEBUG) lift the two production guards described above for characterization work. Documented inDOCs/SPI-PHASE-MARGIN-API.md. Production applications should not call either.
SD-CARD-PERFORMANCE.mdsection 7 stated that CMD6 High Speed mode "fails on all tested cards". That was wrong. Of four modern cards retested, three negotiate high speed and hold it at 43.75 MHz. Sequential reads gain up to 47%. Write behaviour at that clock varies by card and is under investigation, so the driver still does not negotiate high speed on its own —attemptHighSpeed()remains opt-in.
1.7.0 - 2026-08-14
Data-losing file operations are fixed, failures the driver detected but discarded are now reported, and files a PC wrote behave correctly.
handleError(handle)reports why the lastreadHandle()orwriteHandle()on a handle came up short. Both return a byte count, and a partial count is positive, so the read loop the tutorial shows ends the same way at end of file and on a card failure part-way through a file.- The write path reports
E_NO_CONTIGUOUS_SPACEwhen a pre-allocated contiguous reservation runs out, andE_BAD_CHAINwhen a cluster chain walk lands in the metadata region. Both were previously reported as a zero-byte write. lastFlushError()andclearFlushError()report failures of the automatic idle flush. That flush is started by the worker cog, so a failure had no caller to return to: the data did not reach the card and every later call still reported success.SD_INCLUDE_TEST_HOOKSbuilds in fault injection —setTestFailSector(),setTestFailWriteAfter(),getTestWriteCallCount(),setTestMaxClusters(),clearTestErrors(). Enabled bySD_INCLUDE_ALL, and deliberately not bySD_INCLUDE_DEBUG.debugSetTxAlignDelay()anddebugGetTxAlignDelay()measure write timing margin on an unfamiliar board or card socket. Diagnostic only, gated behindSD_INCLUDE_DEBUG; the shipped default needs no adjustment. Documented inDOCs/SPI-PHASE-MARGIN-API.md.DOCs/ERROR-HANDLING-GUIDE.mdcovers detecting and responding to every error the driver reports.DOCs/MIGRATION-GUIDE-v1.7.0.mdcovers moving from v1.6.x.
- Renaming a file to a full 12-character 8.3 name (
DATALOG1.TXT) keeps the file usable. The last character of the extension was written over the directory entry's attribute byte, which set the directory and system bits; the file then could not be opened and no longer appeared in listings. Names of 11 characters or fewer were unaffected. - File operations no longer match the volume-label entry.
rename()on a dot-less label's name silently relabelled the volume; creating that name failedE_FILE_EXISTS, and opening it returned an empty file. - Seeking a write handle to another sector, and reading through a handle that has unwritten data, both flush that data first. Either one previously discarded it, and after a seek every later flush from that handle was addressed to a sector that does not exist.
- An unreadable directory is no longer indistinguishable from a name that is not there, so a create no longer writes a second entry for a file that already exists.
- A metadata write that fails part-way no longer leaves the filesystem in a state that reports success. Cluster allocation, delete, directory creation, rename and move each check every write and leave the recoverable outcome on the card.
- Appending to an empty file created by a PC works; the first write previously failed
E_BAD_CHAINand the file could never be appended. createFileContiguous()on a name that already exists leaves free space unchanged; the rejected call previously stranded its pre-allocated clusters until an fsck.changeDirectory()onto an empty file returnsE_NOT_A_DIRinstead of succeeding and silently changing to the root directory.- FAT entries carrying the reserved high bits — legal on volumes written by other systems — no longer send cluster-chain walks and free-space counts to wrong results.
- Multi-block writes and block erases invalidate the cached copies of the sectors they change, as single-sector writes already did. A later read could return the pre-write contents.
- A read that fails part-way no longer leaves a valid cache entry over the incomplete data, and reading past the end of a cluster chain no longer leaves the previous sector's identity attached to a zero-filled buffer.
unmount()clears the cached sectors and cancels the background flush timer. A bareunmount()was followed about 200 ms later by the worker reading and writing FSInfo on a card the caller had finished with.- Closing a file whose final flush failed no longer records the new size and timestamp in the directory. The entry advertised bytes that never reached the card.
mount()on a corrupted card whose boot record claims zero sectors per cluster fails withE_NOT_FAT32instead of hanging the worker cog.writeSectorsRaw()stores the bytes you gave it. A timing fault in the raw multi-block write path could put the outgoing data one bit out of phase with the clock, so the card stored a corrupted sector and reported success. File operations were never affected — they use a different path. See the upgrade note below.error()describes the operation that just completed rather than the last failure since boot. Every method that issues a command records its outcome on both exit paths; the pure accessors are exempt so a diagnostic call cannot erase the error being diagnosed.sync()reports failure, and keeps the pending directory entry so a later sync can still write it. It previously discarded the entry whether or not it reached the card.freeSpace()returns 0 with an error rather than the partial count from an interrupted FAT scan.stop()returns the status of its final unmount. It halts the worker cog immediately afterward, so nothing else could report it.- A worker-cog stack-guard violation reaches
error()instead of only the debug channel, which the driver ships with disabled. getResult()andcancelAsync()refuse a cog that did not start the operation. They release the API lock as their last act, so such a call released a lock the caller never held.- The card registers, high-speed negotiation and CMD6 paths report the specific failure instead of a single false. A card that lacks high-speed support is reported as lacking it, not as a failed query.
- A create that grows a directory reports the actual failure;
E_IO_ERRORfrom a failed FAT write is no longer reported asE_DISK_FULL. - A blocking call from the cog that owns an in-flight async operation returns
E_ASYNC_BUSY— from every API, info getters included — instead of deadlocking; a losing concurrentstart*()gets the same answer promptly instead of blocking for the winner's operation. getResult()andcancelAsync()reportE_STACK_OVERFLOWwhen the worker's stack guard was violated during the operation, as blocking calls already do.setDate()rejects every out-of-range date or time field withE_INVALID_PARAM; negative values were previously accepted.attemptHighSpeed()detects a failed 50 MHz switch instead of reporting success. Its previous check compared the card against itself and could not fail. Nothing is written to the card at a speed that has not been verified.SD_FAT32_fsckrecovers directory entries stranded past a spurious end-of-directory marker, rewriting the marker as a deleted-entry tombstone so a conforming scan reaches them. It previously freed their cluster chains and left the entries in place pointing at them.SD_FAT32_auditandSD_FAT32_fsckreport a card they cannot read instead of reportingCLEAN. A failed read previously went unnoticed and the stale buffer contents were validated as if they were real directory or FAT data.SD_card_identifynames Gigastone OEM cards (manufacturer ID$12) instead of reportingUnknown.SD_card_characterizealready named them.- The read/write example no longer null-terminates its buffer at a negative index when a read fails. The demo shell reports a read that failed part-way through
typeandhexdumpinstead of printing a byte count as if the file were complete, reports acopywhose writes were refused or short instead of reporting the source size as copied, and distinguishes a card with no free space from a free-space query that failed.
Ordinary file operations were not affected. The write-path fault above could only be reached through writeSectorsRaw(), the raw multi-block sector API available under SD_INCLUDE_RAW. Everything the filesystem does — reading and writing files, directories, timestamps — goes through a different path that was tested clean on the affected releases.
If your application never calls writeSectorsRaw(), there is nothing to check.
If it does, and the data matters: open a file and look at it. The corruption is not subtle — it makes text unreadable and binary structures obviously malformed. Rewriting an affected file with a v1.7.0 build is enough; the card does not need reformatting. Note that SD_FAT32_audit and SD_FAT32_fsck cannot find this, and a CLEAN result from either says nothing either way — the filesystem structures stay intact and only the contents of data sectors would be wrong.
Reads were never affected.
Five changes can affect existing code. Only the first can break a program that currently works — the rest surface as a different error code than before.
| If your code… | …then |
|---|---|
ends a read loop only on n == 0 |
it will not terminate on a persistent read failure. Change to n =< 0. |
tests eofHandle() or isFileContiguous() for a negative value |
remove the test; both return a boolean now |
branches on E_NOT_A_DIR to mean "no such directory" |
test E_FILE_NOT_FOUND instead |
| deletes a directory without emptying it, or a file with a handle still open | close/empty first; the delete now refuses |
matches unmount()'s exact error code on a corrupt card |
it is now E_BAD_FSINFO (-24) |
Nothing else in this release requires a source change.
- BREAKING:
readHandle()andwriteHandle()return their error code when nothing at all was transferred, so 0 fromreadHandle()now means end of file and nothing else. A read that failed on its first sector previously returned 0, and a file on a failing card was processed as a complete file. A partial transfer still returns the partial count. - BREAKING:
eofHandle()andisFileContiguous()return a boolean only. They previously returnedTRUE,FALSE, or a negative error code — andTRUEis -1 in Spin2 whileE_TIMEOUTis also -1, so there was no correct way to call either. On a failed queryeofHandle()reportsTRUEandisFileContiguous()reportsFALSE;error()distinguishes. - BREAKING:
changeDirectory()on a name with no entry returnsE_FILE_NOT_FOUND(-40).E_NOT_A_DIR(-43) now means the name exists and is not a directory; the two conditions previously shared it. - BREAKING:
deleteFile()refuses two cases it used to accept, and deletes nothing in either. A directory that still contains entries returnsE_DIR_NOT_EMPTY(-44) — it previously freed only the directory's own chain, leaving every file inside allocated and unreachable until reformat. A file that any handle still has open returnsE_FILE_OPEN(-47) — the delete previously succeeded, after which the background flush could write that handle's data into a cluster already given to another file. Empty directories before removing them, children before parents; there is no recursive delete. - BREAKING:
unmount()on a card with invalid FSInfo signatures reportsE_BAD_FSINFO(-24) rather thanE_IO_ERROR.
Additive, listed here because they belong with the above:
- New error constants:
E_BAD_FSINFO(-24),E_BAD_CHAIN(-25),E_STACK_OVERFLOW(-26),E_DIR_NOT_EMPTY(-44),E_FILE_OPEN(-47),E_NO_COG(-65).E_NO_COGreplacesE_NO_LOCKwhenstart()cannot get a cog.E_FILE_NOT_OPEN(-45) is documented as reserved; no code path produces it. stop()andcloseDirectoryHandle()return a status where they previously returned nothing. Existing calls compile and behave as before.
1.6.1 - 2026-07-27
Utility output and build instructions, and one debug method renamed to match what it does.
SD_format_card,SD_FAT32_auditandSD_FAT32_fsckbuild withpnut-ts -d -I .., the command their documentation gives. The other three utilities were unaffected.SD_card_identifyandSD_card_characterizeprint the manufacturing date as a date —2021/09,2021-09— in a fixed-width field that sorts positionally.SD_FAT32_auditreports each finding asneeds repair:and states that the card was not modified.SD_FAT32_fsckreports the same findings asrepaired:.SD_format_cardprints its closing success or failure line in full.- Error messages in the audit, format and benchmark utilities name the operation that failed.
- BREAKING:
debugClearRootDir()is renameddebugZeroRootSector()(SD_INCLUDE_DEBUG). It zeroes the first root-directory sector and frees no cluster chains, so entries in later sectors become unreachable and their clusters stay allocated. Treat the card as scratch afterwards;SD_format_cardreturns it to a clean state.
- Maxwell NCard 4GB:
formatCard()does not produce a mountable filesystem on this card. Other cards in the catalog format normally.
1.6.0 - 2026-07-25
Write-path corruption fixes; broader card compatibility; write-error reliability.
Two data-corruption defects are fixed, both in rewriting an existing file. Files created once and written front to back — including data loggers that only append sector-aligned records — were never affected.
- Reopening a file and overwriting across a cluster boundary (
openFileWrite()+seekHandle()back into the file, thenwriteHandle()) now follows the file's existing chain. The data past the rewritten region survives instead of being orphaned. - Appending to a file whose length isn't a multiple of 512 (
openFileWrite()+writeHandle()) now preserves the bytes already in that final sector instead of zeroing them. - Write failures are no longer silent. A write that fails — full card, failing card, unresponsive card — returns an error instead of reporting success, and the error says what actually went wrong. This covers writing, closing, syncing, unmounting, and deleting.
- Bulk reads use the same clock-timing correction as writes, removing a read-reliability difference at some SPI speeds.
- Remounting from a different binary no longer fails with a spurious
E_NO_CARD. - A stray write can no longer damage the filesystem's own structures —
writeHandle()refuses to write below the data region.
A card written by an earlier release may carry silent damage — but only if you rewrote existing files as described above.
- Run
SD_FAT32_audit(orSD_FAT32_fsckto also repair) to find lost clusters left by a cross-boundary overwrite. fsck reclaims the space; it cannot restore the file's lost tail content. - Zeroed leading bytes from a mid-sector append leave no filesystem footprint — no tool can detect them. Verify or restore affected files from backup.
SD_FAT32_auditnow does the deep scan, read-only. It performs the same full check asSD_FAT32_fsck— including finding lost clusters — but changes nothing. Useauditto look,fsckto repair. The separateSD_FAT32_checktool is removed;auditreplaces it.
- Cards that report placeholder data checksums now work. Some counterfeit and marginal SDSC cards previously failed every read. They are detected at mount, flagged in
cardWarnings()asCW_NO_DATA_CRC, and run at a clock speed probed as safe for that card. Cards with real checksums keep strict validation. - macOS-formatted cards mount and audit cleanly, including small (SDSC) cards.
SD_PINS_EXTERNALbuild flag: use an external SD header (base pins 16–21) instead of the P2 Edge onboard slot.
1.5.3 - 2026-05-07
Timeout handling on SDSC (≤2 GB) cards.
SDSC cards could fail every read and write at some system clocks: mount() succeeded at the slow init speed, then all traffic afterwards reported a timeout. SDHC and SDXC cards were unaffected.
- SDSC cards no longer report spurious timeouts. The condition depended on system clock — a card that failed completely at one
_CLKFREQcould work at another. - SDSC cards that legitimately need long write times are honored. The driver now allows up to 30 seconds for a write where it previously gave up after 1 second, and caps reads at 5 seconds against cards reporting implausible values.
Found and diagnosed by @macca on a 1GB SanDisk SU01G, including a confirmed fix. Many thanks.
1.5.2 - 2026-05-06
Write-path timing margin; erase-block size getter.
- Bulk writes have more clock-timing margin on cards with tight timing, completing the read-path work from v1.5.1. Both single-block and multi-block writes are covered.
readCID()works inSD_INCLUDE_ALLbuilds. Enabling the debug and register features together could shadow it.
eraseBlockSectors(): the card's erase block size in sectors. Useful for aligning write batching or log rotation to flash erase boundaries — typically 32 (SDSC) or 128 (SDHC/SDXC).
debugEraseBlock(start_sector): erase one erase-block-sized region. For tools distinguishing recoverable card flash from failing flash. Production code must not call this — the card erases internally on normal writes, and misuse risks filesystem corruption.
- Card catalog: SanDisk SU01G 1GB SDSC entry, and a new "E" speed rating (SDSC class, ≤12.5 MHz recommended).
1.5.1 - 2026-05-05
Read-path timing margin for marginal cards.
Older and slower cards could fail intermittently at certain system clocks. No application changes are needed to get the improved behavior; at high system clocks, behavior is byte-identical to prior releases.
- Reads are more reliable on slower and older cards. The driver now adapts how it samples data to the system-clock-to-SCK ratio, giving more margin where the bit cell is narrow.
- Bulk reads start on schedule at low system clocks. The first clock pulse could arrive late, so the driver began sampling before the card was driving data.
- SPI works with any pin layout, not only the default P2 Edge arrangement.
mount()andinitCardOnly()report a specific error when the worker cog fails to start, instead of a generic one.
E_BAD_PIN_CONFIG(-9):mount()fails early when SCK is more than ±3 pins from MOSI or MISO.
For tooling that characterizes cards that misbehave. Production applications must not call these — the driver picks the right values itself — and they may change without notice: debugSetSampleMode(), debugSetPreEdgeThreshold(), debugGetEffectiveSampleMode(), debugGetCurrentHp(), debugSetAlignDelayOffset(), debugGetEffectiveAlignDelay(), and debugOnClockChange() (refresh clock-dependent state after a runtime clkset(); production guidance remains unmount() → clkset() → mount()).
1.5.0 - 2026-04-02
Stale directory cluster fix, new API, feature flag reorganization.
newDirectory(): a directory created in a recycled cluster no longer inherits stale data — the cluster is zeroed before use.
sectorsPerCluster(): public getter for the filesystem's cluster size.SD_INCLUDE_ASYNCandSD_INCLUDE_DEFRAG: application-level feature flags, both included bySD_INCLUDE_ALL.
1.4.2 - 2026-03-26
Audit fix, allocator wrap-around fix, theory doc updated.
- Audit: Volume label scan searches all root directory entries (not just offset 0)
allocateCluster(): Next-fit scan wraps correctly whentest_max_clusterslimit is active
- Theory of Operations: New sections for cluster allocation, auto-flush, defragmentation, async I/O
- Theory of Operations: Updated feature flags, command opcodes, error codes, and API tables
- Demo shell: Defrag commands enabled via
SD_INCLUDE_DEFRAG
1.4.1 - 2026-03-25
Defragmentation support, next-fit allocation, contiguous file creation.
SD_INCLUDE_DEFRAG:fileFragments(),isFileContiguous(),compactFile(),createFileContiguous()APIscompactFile()relocates a file's clusters into a contiguous chain with mandatory read-back verificationcreateFileContiguous()pre-allocates a contiguous cluster chain for guaranteed non-fragmented writes- FSCK/Audit reports fragmented file count and total fragments in summary output
allocateCluster(): Next-fit scanning reduces fragmentation on sequential writes- Allocation locality hint (
fsi_nxt_free) persisted across mount/unmount cycles
readSector(): CRC match counter no longer incremented when CRC validation is disabled- Disk-full simulation tests: Cluster cleanup between test phases prevents false failures
1.4.0 - 2026-03-17
Worker loop restructure, live timestamps, auto-flush, non-blocking async I/O.
setDate(),getDate(): Live clock with 2-second auto-advance for file timestamps- Auto-flush dirty handles after 200ms idle (zero cost during active I/O)
SD_INCLUDE_ASYNC: Non-blocking file I/O viastartReadHandle(),startWriteHandle(),isComplete(),getResult(),cancelAsync()- Demo shell:
datecommand to show/set driver clock; directory listings show modification timestamps
- Worker cog loop restructured with dedicated clock tick and idle flush slots
run_regression.sh:--run-onlyflag recompiles only stale .bin files (checks source and driver timestamps)
1.3.2 - 2026-03-10
NCO write alignment fix, selective debug channels, controller-specific code removed.
writeSector(),writeSectors(): Streamer write timing corrected for power-of-2 SPI half-period values (hp=4, hp=8) via NCO frequency adjustment- Controller-specific SPI speed limiting removed; all cards use reported max speed capped at 25 MHz
- Debug output is selectable by subsystem rather than all-or-nothing.
DEBUG_MASKreplacesDEBUG_DISABLEas the single control: 0 for production, or set channel bits to see only the traffic you care about — INIT, MOUNT, FILE, DIR, SECTOR, STATUS, IDENT, HSPEED, API, RECOVER. The driver requires{Spin2_v46}or later for channel support.
1.3.1 - 2026-03-07
CMD13 STATUS byte fix, reduced driver footprint, updated memory sizing reference.
checkCardStatus(): STATUS byte errors now detected correctly (was checking uninitialized return variable instead of R2 response)last_cmd13_status: Diagnostic field now captures the actual STATUS byte from CMD13
- Driver hub footprint reduced through SPI transaction consolidation
- Memory sizing guide updated with current v1.3.x footprint data across all configurations
1.3.0 - 2026-03-07
R1 response parsing fix, CMD12 tolerance, and CMD23 block-count support.
- R1 response parsing: skip bytes with bit 7 set per SD spec Section 7.3.2.1 (fixes CMD13 false errors on cards with bus artifacts)
- CMD12 tolerance: multi-block reads recover via CS deassert on cards with aggressive read-ahead pipelines
cardWarnings(): Bitmask API for init-time capability discoveries- CMD23 probing: automatic SET_BLOCK_COUNT detection and use when supported in SPI mode
recoverToIdle(): CS deassert recovery per SD spec Section 7.2.2readSectors(): CMD23 path with auto-stop verification and fallback to CMD12
1.2.9 - 2026-03-06
Prerelease for v1.3.0. Diagnostic capture for card status reporting, released for field testing. Everything here also ships in v1.3.0 — use that instead.
1.2.1 - 2026-03-05
Audit severity corrections for conditions that are normal on FAT32 media.
- Audit: Partition type $0B (FAT32 CHS) accepted as valid alongside $0C (FAT32 LBA)
- Audit: Backup FSInfo mismatch downgraded from error to warning (common on FAT32 media)
1.2.0 - 2026-03-03
Consistent error codes across the entire public API, card presence detection, and transport-layer diagnostics.
- 17 PUB methods now return
SUCCESS(0) / negative error code instead oftrue/false:mount,unmount,sync,newDirectory,changeDirectory,deleteFile,rename,moveFile,setVolumeLabel,initCardOnly,readSectorRaw,writeSectorRaw,readVBRRaw,readCIDRaw,readCSDRaw,readSCRRaw,readSDStatusRaw - Code testing
if sd.mount(...)(truthy = success) must change toif sd.mount(...) == sd.SUCCESS-- see Migration Guide
E_NO_CARD(-8):mount()andinitCardOnly()now detect missing cards using a P2 internal pull-up on MISO and return a specific error code instead of genericE_INIT_FAILEDsetSPISpeed(): Public API method for setting SPI clock frequency at runtimeSD_INCLUDE_STACK_CHECK: Conditional feature flag for worker cog stack depth measurement
- Transport layer returns specific error codes (
E_TIMEOUT,E_CRC_ERROR,E_BAD_RESPONSE,E_WRITE_REJECTED,E_CARD_BUSY,E_IO_ERROR) instead of bare-1acrossreadSector,writeSector,allocateCluster, and all SPI wait/response methods writeSector(): Returns 0/negative error codes instead of boolean, with specific failure reasons for timeout, CRC reject, card busy, and programming errors
- Migration Guide for updating v1.0/v1.1 code to v1.2 error-code patterns
- Card Presence Detection reference with electrical analysis and SD spec research
- Memory Sizing Guide for hub RAM planning
- Theory of Operations expanded: card presence detection, card identification and adaptive timing
- Architecture Decision 13: Card presence detection via P2 internal pull-up
1.1.0 - 2026-02-28
FSCK scales to any card size, CRC error injection for fault testing, V1 legacy API removed.
- V1 legacy API removed (
readFile(),writeFile(),readDirectory(), etc.) -- use handle-based API
- CRC error injection hooks for hardware-level fault testing (
setTestForceReadError,setTestForceWriteError) readVBRRaw()available withSD_INCLUDE_REGISTERSalone (no longer requiresSD_INCLUDE_RAW)
- Volume label scan follows full root directory cluster chain
- FAT chain addressing supports cards up to 2 TB
- FSCK full validation works on cards of any size (windowed bitmap for cards >64 GB)
- Cross-compilation support for Spin Tools IDE and flexspin
1.0.0 - 2026-02-25
Initial release.
- FAT32-compliant SD card filesystem for the Parallax Propeller 2
- Smart pin SPI with streamer DMA for hardware-accelerated transfers
- Dedicated worker cog with hardware lock serialization
- Up to 6 simultaneous file and directory handles (configurable)
- Per-cog current working directory for safe multi-cog navigation
- Handle-based file and directory API: open, read, write, seek, enumerate, close
- Directory operations: create, navigate, delete, rename
- Raw sector read/write and multi-sector bulk transfers (CMD18/CMD25)
- Hardware-accelerated CRC-16 on all data transfers
- SDHC and SDXC cards supported; tested with cards up to 128 GB across 9 manufacturers
- FAT32 format with cross-OS compatibility (Windows, macOS, Linux)
- 4-pass filesystem check and repair (fsck)
- Read-only filesystem audit
- Card characterization, SPI speed testing, and performance benchmark
- Interactive terminal shell with DOS and Unix-style commands
- Driver tutorial, theory of operations, card catalog
- SD card performance guide with ranked comparisons
0.9.3 - 2026-02-24
- File handle limit increased from 4 to 6 (default)
- writeSector(): Cross-buffer cache coherence verified across all three sector caches
- File and directory creation validates entry address before writing (prevents MBR corruption)
- rename(): 8.3 extension parsing handles mixed-case filenames
- File and directory lookup uses case-insensitive matching
- Demo shell: Line ending and prompt display corrected
- External SD header guide with 8-pin header group reference table
0.9.2 - 2026-02-10
- Demo shell: Help text formatted for 80x25 terminal
0.9.1 - 2026-02-09
Initial testing release -- driver, utilities, demo shell, and 263+ regression tests.
0.9.0 - 2026-02-09
Packaging-only tag. Release workflow and user-facing documentation; no driver code.