embedded_sdmmc/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#[macro_use]
83mod structure;
84
85/// Re-export the tyoes library.
86pub use embedded_sdmmc_types;
87pub use embedded_sdmmc_types::blockdevice;
88pub mod fat;
89pub mod filesystem;
90pub mod sdcard;
91
92use core::fmt::Debug;
93use embedded_io::ErrorKind;
94use filesystem::Handle;
95
96#[doc(inline)]
97pub use crate::blockdevice::{Block, BlockCount, BlockDevice, BlockIdx};
98
99#[doc(inline)]
100pub use crate::fat::{FatVolume, VolumeName};
101
102#[doc(inline)]
103pub use crate::filesystem::{
104 Attributes, ClusterId, DirEntry, Directory, File, FilenameError, LfnBuffer, MAX_FILE_SIZE,
105 Mode, RawDirectory, RawFile, ShortFileName, TimeSource, Timestamp,
106};
107
108use filesystem::DirectoryInfo;
109
110#[doc(inline)]
111pub use crate::sdcard::spi::Error as SdCardError;
112
113#[doc(inline)]
114pub use crate::sdcard::spi::SdCard;
115
116mod volume_mgr;
117#[doc(inline)]
118pub use volume_mgr::VolumeManager;
119
120#[cfg(all(feature = "defmt-log", feature = "log"))]
121compile_error!("Cannot enable both log and defmt-log");
122
123#[cfg(feature = "log")]
124use log::{debug, trace, warn};
125
126#[cfg(feature = "defmt-log")]
127use defmt::{debug, trace, warn};
128
129#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
130#[macro_export]
131/// Like log::debug! but does nothing at all
132macro_rules! debug {
133 ($($arg:tt)+) => {};
134}
135
136#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
137#[macro_export]
138/// Like log::trace! but does nothing at all
139macro_rules! trace {
140 ($($arg:tt)+) => {};
141}
142
143#[cfg(all(not(feature = "defmt-log"), not(feature = "log")))]
144#[macro_export]
145/// Like log::warn! but does nothing at all
146macro_rules! warn {
147 ($($arg:tt)+) => {};
148}
149
150// ****************************************************************************
151//
152// Public Types
153//
154// ****************************************************************************
155
156/// All the ways the functions in this crate can fail.
157#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
158#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
159pub enum Error<E>
160where
161 E: core::error::Error,
162{
163 /// The underlying block device threw an error.
164 #[error("error from underlying block device: {0}")]
165 DeviceError(#[from] E),
166 /// The filesystem is badly formatted (or this code is buggy).
167 #[error("filesystem is badly formatted: {0}")]
168 FormatError(&'static str),
169 /// The given `VolumeIdx` was bad,
170 #[error("no such volume")]
171 NoSuchVolume,
172 /// The given filename was bad
173 #[error("bad filename")]
174 FilenameError(FilenameError),
175 /// Out of memory opening volumes
176 #[error("too many open volumes")]
177 TooManyOpenVolumes,
178 /// Out of memory opening directories
179 #[error("too many open directories")]
180 TooManyOpenDirs,
181 /// Out of memory opening files
182 #[error("too many open files")]
183 TooManyOpenFiles,
184 /// Bad handle given
185 #[error("bad handle")]
186 BadHandle,
187 /// That file or directory doesn't exist
188 #[error("file or directory does not exist")]
189 NotFound,
190 /// You can't open a file twice or delete an open file
191 #[error("file already open")]
192 FileAlreadyOpen,
193 /// You can't open a directory twice
194 #[error("directory already open")]
195 DirAlreadyOpen,
196 /// You can't open a directory as a file
197 #[error("cannot open directory as file")]
198 OpenedDirAsFile,
199 /// You can't open a file as a directory
200 #[error("cannot open file as directory")]
201 OpenedFileAsDir,
202 /// You can't delete a non-empty directory
203 #[error("cannot delete a non-empty directory")]
204 DeleteNonEmptyDir,
205 /// You can't close a volume with open files or directories
206 #[error("volume is still in use")]
207 VolumeStillInUse,
208 /// You can't open a volume twice
209 #[error("cannot open volume twice")]
210 VolumeAlreadyOpen,
211 /// We can't do that yet
212 #[error("unsupported operation")]
213 Unsupported,
214 /// Tried to read beyond end of file
215 #[error("end of file")]
216 EndOfFile,
217 /// Found a bad cluster
218 #[error("bad cluster")]
219 BadCluster,
220 /// Error while converting types
221 #[error("type conversion failed")]
222 ConversionError,
223 /// The device does not have enough space for the operation
224 #[error("not enough space on device")]
225 NotEnoughSpace,
226 /// Cluster was not properly allocated by the library
227 #[error("cluster not properly allocated")]
228 AllocationError,
229 /// Jumped to free space during FAT traversing
230 #[error("FAT chain unterminated")]
231 UnterminatedFatChain,
232 /// Tried to open Read-Only file with write mode
233 #[error("file is read-only")]
234 ReadOnly,
235 /// Tried to create an existing file
236 #[error("file already exists")]
237 FileAlreadyExists,
238 /// Bad block size - only 512 byte blocks supported
239 #[error("bad block size: {0} (only 512 byte blocks supported)")]
240 BadBlockSize(u16),
241 /// Bad offset given when seeking
242 #[error("invalid seek offset")]
243 InvalidOffset,
244 /// Disk is full
245 #[error("disk full")]
246 DiskFull,
247 /// A directory with that name already exists
248 #[error("directory already exists")]
249 DirAlreadyExists,
250 /// The filesystem tried to gain a lock whilst already locked.
251 ///
252 /// This is either a bug in the filesystem, or you tried to access the
253 /// filesystem API from inside a directory iterator (that isn't allowed).
254 #[error("already locked")]
255 LockError,
256}
257
258impl<E: core::error::Error + 'static> embedded_io::Error for Error<E> {
259 fn kind(&self) -> ErrorKind {
260 match self {
261 Error::DeviceError(_)
262 | Error::FormatError(_)
263 | Error::FileAlreadyOpen
264 | Error::DirAlreadyOpen
265 | Error::VolumeStillInUse
266 | Error::VolumeAlreadyOpen
267 | Error::EndOfFile
268 | Error::DiskFull
269 | Error::NotEnoughSpace
270 | Error::AllocationError
271 | Error::LockError => ErrorKind::Other,
272 Error::NoSuchVolume
273 | Error::FilenameError(_)
274 | Error::BadHandle
275 | Error::InvalidOffset => ErrorKind::InvalidInput,
276 Error::TooManyOpenVolumes | Error::TooManyOpenDirs | Error::TooManyOpenFiles => {
277 ErrorKind::OutOfMemory
278 }
279 Error::NotFound => ErrorKind::NotFound,
280 Error::OpenedDirAsFile
281 | Error::OpenedFileAsDir
282 | Error::DeleteNonEmptyDir
283 | Error::BadCluster
284 | Error::ConversionError
285 | Error::UnterminatedFatChain => ErrorKind::InvalidData,
286 Error::Unsupported | Error::BadBlockSize(_) => ErrorKind::Unsupported,
287 Error::ReadOnly => ErrorKind::PermissionDenied,
288 Error::FileAlreadyExists | Error::DirAlreadyExists => ErrorKind::AlreadyExists,
289 }
290 }
291}
292
293/// A handle to a volume.
294///
295/// A volume is a partition with a filesystem within it.
296///
297/// Do NOT drop this object! It doesn't hold a reference to the Volume Manager
298/// it was created from and the VolumeManager will think you still have the
299/// volume open if you just drop it, and it won't let you open the file again.
300///
301/// Instead you must pass it to [`crate::VolumeManager::close_volume`] to close
302/// it cleanly.
303#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
304#[derive(Debug, Copy, Clone, PartialEq, Eq)]
305pub struct RawVolume(Handle);
306
307impl RawVolume {
308 /// Convert a raw volume into a droppable [`Volume`]
309 pub fn to_volume<
310 D,
311 T,
312 const MAX_DIRS: usize,
313 const MAX_FILES: usize,
314 const MAX_VOLUMES: usize,
315 >(
316 self,
317 volume_mgr: &VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
318 ) -> Volume<'_, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
319 where
320 D: crate::BlockDevice,
321 T: crate::TimeSource,
322 {
323 Volume::new(self, volume_mgr)
324 }
325}
326
327/// A caching layer for block devices
328///
329/// Caches a single block.
330#[derive(Debug)]
331pub struct BlockCache<D> {
332 block_device: D,
333 block: [Block; 1],
334 block_idx: Option<BlockIdx>,
335}
336
337impl<D> BlockCache<D>
338where
339 D: BlockDevice,
340{
341 /// Create a new block cache
342 pub fn new(block_device: D) -> Self {
343 BlockCache {
344 block_device,
345 block: [Block::new()],
346 block_idx: None,
347 }
348 }
349
350 /// Read a block, and return a reference to it.
351 pub fn read(&mut self, block_idx: BlockIdx) -> Result<&Block, D::Error> {
352 if self.block_idx != Some(block_idx) {
353 self.block_idx = None;
354 self.block_device.read(&mut self.block, block_idx)?;
355 self.block_idx = Some(block_idx);
356 }
357 Ok(&self.block[0])
358 }
359
360 /// Read a block, and return a reference to it.
361 pub fn read_mut(&mut self, block_idx: BlockIdx) -> Result<&mut Block, D::Error> {
362 if self.block_idx != Some(block_idx) {
363 self.block_idx = None;
364 self.block_device.read(&mut self.block, block_idx)?;
365 self.block_idx = Some(block_idx);
366 }
367 Ok(&mut self.block[0])
368 }
369
370 /// Write back a block you read with [`Self::read_mut`] and then modified.
371 pub fn write_back(&mut self) -> Result<(), D::Error> {
372 self.block_device.write(
373 &self.block,
374 self.block_idx.expect("write_back with no read"),
375 )
376 }
377
378 /// Write back a block you read with [`Self::read_mut`] and then modified, but to two locations.
379 ///
380 /// This is useful for updating two File Allocation Tables.
381 pub fn write_back_with_duplicate(&mut self, duplicate: BlockIdx) -> Result<(), D::Error> {
382 self.block_device.write(
383 &self.block,
384 self.block_idx.expect("write_back with no read"),
385 )?;
386 self.block_device.write(&self.block, duplicate)?;
387 Ok(())
388 }
389
390 /// Access a blank sector
391 pub fn blank_mut(&mut self, block_idx: BlockIdx) -> &mut Block {
392 self.block_idx = Some(block_idx);
393 self.block[0].fill(0);
394 &mut self.block[0]
395 }
396
397 /// Access the block device
398 pub fn block_device(&mut self) -> &mut D {
399 // invalidate the cache
400 self.block_idx = None;
401 // give them the block device
402 &mut self.block_device
403 }
404
405 /// Get the block device back
406 pub fn free(self) -> D {
407 self.block_device
408 }
409}
410
411/// A handle for an open volume on disk, which closes on drop.
412///
413/// In contrast to a `RawVolume`, a `Volume` holds a mutable reference to its
414/// parent `VolumeManager`, which restricts which operations you can perform.
415///
416/// If you drop a value of this type, it closes the volume automatically, but
417/// any error that may occur will be ignored. To handle potential errors, use
418/// the [`Volume::close`] method.
419pub struct Volume<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
420where
421 D: crate::BlockDevice,
422 T: crate::TimeSource,
423{
424 raw_volume: RawVolume,
425 volume_mgr: &'a VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
426}
427
428impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
429 Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
430where
431 D: crate::BlockDevice,
432 T: crate::TimeSource,
433{
434 /// Create a new `Volume` from a `RawVolume`
435 pub fn new(
436 raw_volume: RawVolume,
437 volume_mgr: &'a VolumeManager<D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>,
438 ) -> Self {
439 Volume {
440 raw_volume,
441 volume_mgr,
442 }
443 }
444
445 /// Open the volume's root directory.
446 ///
447 /// You can then read the directory entries with `iterate_dir`, or you can
448 /// use `open_file_in_dir`.
449 pub fn open_root_dir(
450 &self,
451 ) -> Result<crate::Directory<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>, Error<D::Error>> {
452 let d = self.volume_mgr.open_root_dir(self.raw_volume)?;
453 Ok(d.to_directory(self.volume_mgr))
454 }
455
456 /// Convert back to a raw volume
457 pub fn to_raw_volume(self) -> RawVolume {
458 let v = self.raw_volume;
459 core::mem::forget(self);
460 v
461 }
462
463 /// Consume the `Volume` handle and close it. The behavior of this is similar
464 /// to using [`core::mem::drop`] or letting the `Volume` go out of scope,
465 /// except this lets the user handle any errors that may occur in the process,
466 /// whereas when using drop, any errors will be discarded silently.
467 pub fn close(self) -> Result<(), Error<D::Error>> {
468 let result = self.volume_mgr.close_volume(self.raw_volume);
469 core::mem::forget(self);
470 result
471 }
472}
473
474impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize> Drop
475 for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
476where
477 D: crate::BlockDevice,
478 T: crate::TimeSource,
479{
480 fn drop(&mut self) {
481 _ = self.volume_mgr.close_volume(self.raw_volume)
482 }
483}
484
485impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
486 core::fmt::Debug for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
487where
488 D: crate::BlockDevice,
489 T: crate::TimeSource,
490{
491 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
492 write!(f, "Volume({})", self.raw_volume.0.0)
493 }
494}
495
496#[cfg(feature = "defmt-log")]
497impl<'a, D, T, const MAX_DIRS: usize, const MAX_FILES: usize, const MAX_VOLUMES: usize>
498 defmt::Format for Volume<'a, D, T, MAX_DIRS, MAX_FILES, MAX_VOLUMES>
499where
500 D: crate::BlockDevice,
501 T: crate::TimeSource,
502{
503 fn format(&self, fmt: defmt::Formatter) {
504 defmt::write!(fmt, "Volume({})", self.raw_volume.0.0)
505 }
506}
507
508/// Internal information about a Volume
509#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
510#[derive(Debug, PartialEq, Eq)]
511pub(crate) struct VolumeInfo {
512 /// Handle for this volume.
513 raw_volume: RawVolume,
514 /// Which volume (i.e. partition) we opened on the disk
515 idx: VolumeIdx,
516 /// What kind of volume this is
517 volume_type: VolumeType,
518}
519
520/// This enum holds the data for the various different types of filesystems we
521/// support.
522#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
523#[derive(Debug, PartialEq, Eq)]
524pub enum VolumeType {
525 /// FAT16/FAT32 formatted volumes.
526 Fat(FatVolume),
527}
528
529/// A number which identifies a volume (or partition) on a disk.
530///
531/// `VolumeIdx(0)` is the first primary partition on an MBR partitioned disk.
532#[cfg_attr(feature = "defmt-log", derive(defmt::Format))]
533#[derive(Debug, PartialEq, Eq, Copy, Clone)]
534pub struct VolumeIdx(pub usize);
535
536/// Marker for a FAT32 partition. Sometimes also use for FAT16 formatted
537/// partitions.
538const PARTITION_ID_FAT32_LBA: u8 = 0x0C;
539/// Marker for a FAT16 partition with LBA. Seen on a Raspberry Pi SD card.
540const PARTITION_ID_FAT16_LBA: u8 = 0x0E;
541/// Marker for a FAT16 partition. Seen on a card formatted with the official
542/// SD-Card formatter.
543const PARTITION_ID_FAT16: u8 = 0x06;
544/// Marker for a FAT16 partition smaller than 32MB. Seen on the wowki simulated
545/// microsd card
546const PARTITION_ID_FAT16_SMALL: u8 = 0x04;
547/// Marker for a FAT32 partition. What Macosx disk utility (and also SD-Card formatter?)
548/// use.
549const PARTITION_ID_FAT32_CHS_LBA: u8 = 0x0B;
550
551// ****************************************************************************
552//
553// Unit Tests
554//
555// ****************************************************************************
556
557// None
558
559// ****************************************************************************
560//
561// End Of File
562//
563// ****************************************************************************