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// ****************************************************************************