embedded_sdmmc_dev/
lib.rs

1//! # embedded-sdmmc
2//!
3//! > An SD/MMC Library written in Embedded Rust
4//!
5//! This crate is intended to allow you to read/write files on a FAT formatted
6//! SD card on your Rust Embedded device, as easily as using the `SdFat` Arduino
7//! library. It is written in pure-Rust, is `#![no_std]` and does not use
8//! `alloc` or `collections` to keep the memory footprint low. In the first
9//! instance it is designed for readability and simplicity over performance.
10//!
11//! ## Using the crate
12//!
13//! You will need something that implements the `BlockDevice` trait, which can
14//! read and write the 512-byte blocks (or sectors) from your card. If you were
15//! to implement this over USB Mass Storage, there's no reason this crate
16//! couldn't work with a USB Thumb Drive, but we only supply a `BlockDevice`
17//! suitable for reading SD and SDHC cards over SPI.
18//!
19//! ```rust
20//! use embedded_sdmmc::{Error, Mode, SdCard, SdCardError, TimeSource, VolumeIdx, VolumeManager};
21//!
22//! fn example<S, D, T>(spi: S, delay: D, ts: T) -> Result<(), Error<SdCardError>>
23//! where
24//!     S: embedded_hal::spi::SpiDevice,
25//!     D: embedded_hal::delay::DelayNs,
26//!     T: TimeSource,
27//! {
28//!     let sdcard = SdCard::new(spi, delay);
29//!     println!("Card size is {} bytes", sdcard.num_bytes()?);
30//!     let volume_mgr = VolumeManager::new(sdcard, ts);
31//!     let volume0 = volume_mgr.open_volume(VolumeIdx(0))?;
32//!     println!("Volume 0: {:?}", volume0);
33//!     let root_dir = volume0.open_root_dir()?;
34//!     let mut my_file = root_dir.open_file_in_dir("MY_FILE.TXT", Mode::ReadOnly)?;
35//!     while !my_file.is_eof() {
36//!         let mut buffer = [0u8; 32];
37//!         let num_read = my_file.read(&mut buffer)?;
38//!         for b in &buffer[0..num_read] {
39//!             print!("{}", *b as char);
40//!         }
41//!     }
42//!     Ok(())
43//! }
44//! ```
45//!
46//! For writing files:
47//!
48//! ```rust
49//! use embedded_sdmmc::{BlockDevice, Directory, Error, Mode, TimeSource};
50//! fn write_file<D: BlockDevice, T: TimeSource, const DIRS: usize, const FILES: usize, const VOLUMES: usize>(
51//!     root_dir: &mut Directory<D, T, DIRS, FILES, VOLUMES>,
52//! ) -> Result<(), Error<D::Error>>
53//! {
54//!     let my_other_file = root_dir.open_file_in_dir("MY_DATA.CSV", Mode::ReadWriteCreateOrAppend)?;
55//!     my_other_file.write(b"Timestamp,Signal,Value\n")?;
56//!     my_other_file.write(b"2025-01-01T00:00:00Z,TEMP,25.0\n")?;
57//!     my_other_file.write(b"2025-01-01T00:00:01Z,TEMP,25.1\n")?;
58//!     my_other_file.write(b"2025-01-01T00:00:02Z,TEMP,25.2\n")?;
59//!     // Don't forget to flush the file so that the directory entry is updated
60//!     my_other_file.flush()?;
61//!     Ok(())
62//! }
63//! ```
64//!
65//! ## Features
66//!
67//! * `log`: Enabled by default. Generates log messages using the `log` crate.
68//! * `defmt-log`: By turning off the default features and enabling the
69//!   `defmt-log` feature you can configure this crate to log messages over defmt
70//!   instead.
71//!
72//! You cannot enable both the `log` feature and the `defmt-log` feature.
73
74#![cfg_attr(not(test), no_std)]
75#![deny(missing_docs)]
76
77// ****************************************************************************
78//
79// Imports
80//
81// ****************************************************************************
82
83#[cfg(test)]
84#[macro_use]
85extern crate hex_literal;
86
87#[macro_use]
88mod structure;
89
90pub mod blockdevice;
91pub mod fat;
92pub mod filesystem;
93pub mod sdcard;
94
95use core::fmt::Debug;
96use embedded_io::ErrorKind;
97use filesystem::Handle;
98
99#[doc(inline)]
100pub use crate::blockdevice::{Block, BlockCache, BlockCount, BlockDevice, BlockIdx};
101
102#[doc(inline)]
103pub use crate::fat::{FatVolume, VolumeName};
104
105#[doc(inline)]
106pub use crate::filesystem::{
107    Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, Mode, RawDirectory,
108    RawFile, ShortFileName, TimeSource, Timestamp, MAX_FILE_SIZE,
109};
110
111use filesystem::DirectoryInfo;
112
113#[doc(inline)]
114pub use crate::sdcard::Error as SdCardError;
115
116#[doc(inline)]
117pub use crate::sdcard::SdCard;
118
119mod volume_mgr;
120#[doc(inline)]
121pub use volume_mgr::VolumeManager;
122
123#[cfg(all(feature = "defmt-log", feature = "log"))]
124compile_error!("Cannot enable both log and defmt-log");
125
126#[cfg(feature = "log")]
127use log::{debug, trace, warn};
128
129#[cfg(feature = "defmt-log")]
130use defmt::{debug, trace, warn};
131
132#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
133#[macro_export]
134/// Like log::debug! but does nothing at all
135macro_rules! debug {
136    ($($arg:tt)+) => {};
137}
138
139#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
140#[macro_export]
141/// Like log::trace! but does nothing at all
142macro_rules! trace {
143    ($($arg:tt)+) => {};
144}
145
146#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
147#[macro_export]
148/// Like log::warn! but does nothing at all
149macro_rules! warn {
150    ($($arg:tt)+) => {};
151}
152
153// ****************************************************************************
154//
155// Public Types
156//
157// ****************************************************************************
158
159/// All the ways the functions in this crate can fail.
160#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
161#[derive(Debug, Clone)]
162pub enum Error<E>
163where
164    E: core::fmt::Debug,
165{
166    /// The underlying block device threw an error.
167    DeviceError(E),
168    /// The filesystem is badly formatted (or this code is buggy).
169    FormatError(&'static str),
170    /// The given `VolumeIdx` was bad,
171    NoSuchVolume,
172    /// The given filename was bad
173    FilenameError(FilenameError),
174    /// Out of memory opening volumes
175    TooManyOpenVolumes,
176    /// Out of memory opening directories
177    TooManyOpenDirs,
178    /// Out of memory opening files
179    TooManyOpenFiles,
180    /// Bad handle given
181    BadHandle,
182    /// That file or directory doesn't exist
183    NotFound,
184    /// You can't open a file twice or delete an open file
185    FileAlreadyOpen,
186    /// You can't open a directory twice
187    DirAlreadyOpen,
188    /// You can't open a directory as a file
189    OpenedDirAsFile,
190    /// You can't open a file as a directory
191    OpenedFileAsDir,
192    /// You can't delete a directory as a file
193    DeleteDirAsFile,
194    /// You can't close a volume with open files or directories
195    VolumeStillInUse,
196    /// You can't open a volume twice
197    VolumeAlreadyOpen,
198    /// We can't do that yet
199    Unsupported,
200    /// Tried to read beyond end of file
201    EndOfFile,
202    /// Found a bad cluster
203    BadCluster,
204    /// Error while converting types
205    ConversionError,
206    /// The device does not have enough space for the operation
207    NotEnoughSpace,
208    /// Cluster was not properly allocated by the library
209    AllocationError,
210    /// Jumped to free space during FAT traversing
211    UnterminatedFatChain,
212    /// Tried to open Read-Only file with write mode
213    ReadOnly,
214    /// Tried to create an existing file
215    FileAlreadyExists,
216    /// Bad block size - only 512 byte blocks supported
217    BadBlockSize(u16),
218    /// Bad offset given when seeking
219    InvalidOffset,
220    /// Disk is full
221    DiskFull,
222    /// A directory with that name already exists
223    DirAlreadyExists,
224    /// The filesystem tried to gain a lock whilst already locked.
225    ///
226    /// This is either a bug in the filesystem, or you tried to access the
227    /// filesystem API from inside a directory iterator (that isn't allowed).
228    LockError,
229}
230
231impl<E: Debug> embedded_io::Error for Error<E> {
232    fn kind(&self) -> ErrorKind {
233        match self {
234            Error::DeviceError(_)
235            | Error::FormatError(_)
236            | Error::FileAlreadyOpen
237            | Error::DirAlreadyOpen
238            | Error::VolumeStillInUse
239            | Error::VolumeAlreadyOpen
240            | Error::EndOfFile
241            | Error::DiskFull
242            | Error::NotEnoughSpace
243            | Error::AllocationError
244            | Error::LockError => ErrorKind::Other,
245            Error::NoSuchVolume
246            | Error::FilenameError(_)
247            | Error::BadHandle
248            | Error::InvalidOffset => ErrorKind::InvalidInput,
249            Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => {
250                ErrorKind::OutOfMemory
251            }
252            Error::NotFound => ErrorKind::NotFound,
253            Error::OpenedDirAsFile
254            | Error::OpenedFileAsDir
255            | Error::DeleteDirAsFile
256            | Error::BadCluster
257            | Error::ConversionError
258            | Error::UnterminatedFatChain => ErrorKind::InvalidData,
259            Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported,
260            Error::ReadOnly => ErrorKind::PermissionDenied,
261            Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists,
262        }
263    }
264}
265
266impl<E> From<E> for Error<E>
267where
268    E: core::fmt::Debug,
269{
270    fn from(value: E) -> Error<E> {
271        Error::DeviceError(value)
272    }
273}
274
275/// A handle to a volume.
276///
277/// A volume is a partition with a filesystem within it.
278///
279/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager
280/// it was created from and the VolumeManager will think you still have the
281/// volume open if you just drop it, and it won't let you open the file again.
282///
283/// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close
284/// it cleanly.
285#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
286#[derive(Debug, Copy, Clone, PartialEq, Eq)]
287pub struct RawVolume(Handle);
288
289impl RawVolume {
290    /// Convert a raw volume into a droppable [`Volume`]
291    pub fn to_volume<
292        D,
293        T,
294        const MAX_DIRS: usize,
295        const MAX_FILES: usize,
296        const MAX_VOLUMES: usize,
297    >(
298        self,
299        volume_mgr: &VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
300    ) -> Volume<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
301    where
302        D: crate::BlockDevice,
303        T: crate::TimeSource,
304    {
305        Volume::new(self, volume_mgr)
306    }
307}
308
309/// A handle for an open volume on disk, which closes on drop.
310///
311/// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its
312/// parent `VolumeManager`, which restricts which operations you can perform.
313///
314/// If you drop a value of this type, it closes the volume automatically, but
315/// any error that may occur will be ignored. To handle potential errors, use
316/// the [`Volume::close`] method.
317pub struct Volume<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
318where
319    D: crate::BlockDevice,
320    T: crate::TimeSource,
321{
322    raw_volume: RawVolume,
323    volume_mgr: &'a VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
324}
325
326impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
327    Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
328where
329    D: crate::BlockDevice,
330    T: crate::TimeSource,
331{
332    /// Create a new `Volume` from a `RawVolume`
333    pub fn new(
334        raw_volume: RawVolume,
335        volume_mgr: &'a VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
336    ) -> Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES> {
337        Volume {
338            raw_volume,
339            volume_mgr,
340        }
341    }
342
343    /// Open the volume's root directory.
344    ///
345    /// You can then read the directory entries with `iterate_dir`, or you can
346    /// use `open_file_in_dir`.
347    pub fn open_root_dir(
348        &self,
349    ) -> Result<crate::Directory<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>, Error<D::Error>> {
350        let d = self.volume_mgr.open_root_dir(self.raw_volume)?;
351        Ok(d.to_directory(self.volume_mgr))
352    }
353
354    /// Convert back to a raw volume
355    pub fn to_raw_volume(self) -> RawVolume {
356        let v = self.raw_volume;
357        core::mem::forget(self);
358        v
359    }
360
361    /// Consume the `Volume` handle and close it. The behavior of this is similar
362    /// to using [`core::mem::drop`] or letting the `Volume` go out of scope,
363    /// except this lets the user handle any errors that may occur in the process,
364    /// whereas when using drop, any errors will be discarded silently.
365    pub fn close(self) -> Result<(), Error<D::Error>> {
366        let result = self.volume_mgr.close_volume(self.raw_volume);
367        core::mem::forget(self);
368        result
369    }
370}
371
372impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop
373    for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
374where
375    D: crate::BlockDevice,
376    T: crate::TimeSource,
377{
378    fn drop(&mut self) {
379        _ = self.volume_mgr.close_volume(self.raw_volume)
380    }
381}
382
383impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
384    core::fmt::Debug for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
385where
386    D: crate::BlockDevice,
387    T: crate::TimeSource,
388{
389    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
390        write!(f, "Volume({})", self.raw_volume.0 .0)
391    }
392}
393
394#[cfg(feature = "defmt-log")]
395impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
396    defmt::Format for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
397where
398    D: crate::BlockDevice,
399    T: crate::TimeSource,
400{
401    fn format(&self, fmt: defmt::Formatter) {
402        defmt::write!(fmt, "Volume({})", self.raw_volume.0 .0)
403    }
404}
405
406/// Internal information about a Volume
407#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
408#[derive(Debug, PartialEq, Eq)]
409pub(crate) struct VolumeInfo {
410    /// Handle for this volume.
411    raw_volume: RawVolume,
412    /// Which volume (i.e. partition) we opened on the disk
413    idx: VolumeIdx,
414    /// What kind of volume this is
415    volume_type: VolumeType,
416}
417
418/// This enum holds the data for the various different types of filesystems we
419/// support.
420#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
421#[derive(Debug, PartialEq, Eq)]
422pub enum VolumeType {
423    /// FAT16/FAT32 formatted volumes.
424    Fat(FatVolume),
425}
426
427/// A number which identifies a volume (or partition) on a disk.
428///
429/// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk.
430#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
431#[derive(Debug, PartialEq, Eq, Copy, Clone)]
432pub struct VolumeIdx(pub usize);
433
434/// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted
435/// partitions.
436const PARTITION_ID_FAT32_LBA: u8 = 0x0C;
437/// Marker for a FAT16 partition with LBA. Seen on a Raspberry Pi SD card.
438const PARTITION_ID_FAT16_LBA: u8 = 0x0E;
439/// Marker for a FAT16 partition. Seen on a card formatted with the official
440/// SD-Card formatter.
441const PARTITION_ID_FAT16: u8 = 0x06;
442/// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated
443/// microsd card
444const PARTITION_ID_FAT16_SMALL: u8 = 0x04;
445/// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?)
446/// use.
447const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B;
448
449// ****************************************************************************
450//
451// Unit Tests
452//
453// ****************************************************************************
454
455// None
456
457// ****************************************************************************
458//
459// End Of File
460//
461// ****************************************************************************