stm32plus: FAT32 & FAT16 drivers

The code presented in this article requires a minimum of version 2.0.0 of my stm32plus library.

Filesystem drivers

stm32plus comes with a full featured, object-oriented FAT32 and FAT16 driver written by myself from scratch based on a close adherence to the official Microsoft specification. The interfaces exposed by the file system driver are designed to dovetail seamlessly with other parts of the stm32plus library such as the streams library.

Features

The driver supports all common filesystem operations such as opening and closing files, reading and writing files, iterating directories, creating and deleting directories. Long filenames are supported natively for reading and writing.

The long filename generation algorithm is known to be patented by Microsoft. If you are planning to use these drivers in a commercial application then you can avoid the issue by ensuring that your filenames conform to the short 8.3 upper case format. The long filename generation algorithm is contained in a separate code module and by only using 8.3 names you will ensure that the contentious algorithm is never called by your code.

Filesystems on partitions as well as whole-device ‘super-floppy’ file systems are supported. The type of the file system (FAT32 or FAT16) is auto-detected and abstracted away from the caller via a high-level interface. Formatters for FAT16 and FAT32 file systems are included.

The file system driver is written to be independent of the underlying device. It only needs something that implements a block device interface on to the underlying hardware, such as an SDIO flash card or a memory IC.

Documentation by example

The best way to understand how to use library code is to see some relevant examples with supporting documentation, so here I go.

Initialising the file system

Before you can get on the with the business of interacting with files you need to get an instance of a FileSystem object. For an SD card, you do it like this:

#include "config/stm32plus.h"
#include "config/sdcard.h"
#include "config/filesystem.h"

using namespace stm32plus;

void test() {
  FileSystem *fs;
  SdioDmaSdCard *sdcard;
  CachedBlockDevice *cachedBlockDevice;
  TimeProvider *timeProvider;

  sdcard=new SdioDmaSdCard;
  if(errorProvider.getLast()!=0)
    return; // Failed to init SD card

  // attach the block device to the file system
  // we'll use a cache of 4 blocks (512bytes * 4)

  cachedBlockDevice=new CachedBlockDevice(*sdcard,4);

  // create a time provider that just stamps new files
  // with zero (the MSDOS epoch)

  timeProvider=new NullTimeProvider;

  if(!FileSystem::getInstance(
      *cachedBlockDevice,
      *timeProvider,
      fs))
    return; // Failed to init file system

  // "fs" is prepared and ready - remember to delete
  // it when you're finished, like this:

  delete fs;
  delete timeProvider;
  delete cachedBlockDevice;

  // and if you're not going to use the sdcard
  // any more:

  delete sdcard;
}

Let’s get a bit of an explanation about what’s going on up there. A FileSystem object can interact with anything that implements the BlockDevice interface. In this case we are going to use the SdioDmaSdCard implementation to read a file system on the SD card using the SDIO interface controlled by its DMA channel.

To speed things up we use a wrapper class, CachedBlockDevice. This wrapper provides caching for a regular block device. The cache is a write-through MRU cache. FAT file systems tend to hit the FAT table quite hard during operation so caching of a few blocks is highly recommended.

FileSystem::getInstance is the static method that analyses the structure on the block device and returns an implementation of the FileSystem abstract class to you. The actual type of the file system, FAT16 or FAT32 is abstracted away from you.

FileSystem implementations require an implementation of the TimeProvider abstract class to use when you create files and directories and write to files so that it can stamp the metadata with the correct time and date. The NullTimeProvider object is a dummy implementation that will use zero as the date and time values. I also provide an RtcTimeProvider that uses the STM32 real time clock to obtain time and date values.

Opening and closing files

Given a filesystem object it is easy to open and close files at will.

#include "filesystem/FileSystem.h"

using namespace stm32plus;

void test(FileSystem& fs) {

  File *file;

  if(!fs.openFile("/subdir/myfile.dat",file))
    return; // failed to open file

  // do stuff with the file, then close
  // it, like this:

  delete file;
}

The pathname separator is the Unix-style, forward-slash. MSDOS-style backslashes are not supported. Long filenames are supported seamlessly and the pathnames are case-independent.

Opened files have the file pointer set to zero (the start) and are available for reading and writing.

Note that you cannot open directories using the openFile method.

Reading and writing files

Given an open file, you can read and write to it like this:

void test(File& file) {

  uint8_t buffer[10];
  uint32_t actuallyRead;

// read 10 bytes from the current position

  if(!file.read(buffer,10,actuallyRead))
    return;   // read failed

  if(actuallyRead!=10)
    return;   // hit the end of file

// seek to the end and append 11 characters

  if(!file.seek(0,File::SeekEnd))
    return;   // failed to seek to EOF
  
  if(!file.write("Hello World",11))
    return;   // failed to write 11 bytes
}

Reading from a file returns the actual amount of data read. If you hit the end of the file then this value will be less than amount requested.

You can seek to a random position within the file using the seek method. This method takes an offset and an enum value to offset from. Valid values are:

/**
 * The valid seek start positions.
 */

enum SeekFrom {
	/// Start from the beginning of the file.
	SeekStart,

	/// Start from the end of the file.
	SeekEnd,

	/// Start from the current file position
	SeekCurrent
};

You can seek to a positive offset from the start, a negative offset from the end and a positive or negative offset from the current position.

Writing to a file takes place at the current file pointer. If the file pointer is not at the end of the file then the data at the current file pointer is overwritten. If the end-of-file is hit and there is still more data to be written then the file size is extended to accommodate the new data.

After a write operation the last-modified time of the file is updated using the current time supplied by the TimeProvider instance that you created when you initialised the FileSystem object (see above).

Both read and write operations move the file pointer to the position just after where they last read from or wrote to.

Reading and writing to and from files is more efficient if it can be done in multiples of 512 bytes starting from an offset that is also a multiple of 512 bytes.

Enumerating a directory

DirectoryIterator objects are provided for the purpose of enumerating the files and subdirectories in a directory. They are used like this.

void test(FileSystem& fs) {

  DirectoryIterator *it;

  if(!fs.getDirectoryIterator("/",it))
    return;  // could not open the directory for iterating

  while(it->next()) {

    const FileInformation& info=it->current();

    // inspect the members of FileInformation:
    // .getAttributes()
    // .getFilename()
    // .getCreationDateTime()
    // .getLastWriteDateTime()
    // .getLastAccessDateTime()
    // .getLength()
  }

  // the iterator must be cleaned up to avoid
  // a memory leak

  delete it;
}

getDirectoryIterator() is the method that will get you an iterator on to any directory in the file system. The example above iterates files in the root directory.

The iterator provides you a reference to the information about each file or directory that it encounters through a FileInformation object. This object allows you to see the usual metadata associated with a file: the name, attributes, various times and the 32-bit length.

getFilename() returns the filename and extension only. It does not include the full pathname prefix.

getAttributes() returns a bitmask of the following attributes:

enum FileAttributes {
	/// File is read only
	ATTR_READ_ONLY=0x1,

	/// File is hidden
	ATTR_HIDDEN=0x2,

	/// File is a system file
	ATTR_SYSTEM=0x4,

	/// File is actually the volume label
	ATTR_VOLUME_ID=0x8,

	/// File is a directory
	ATTR_DIRECTORY=0x10,

	/// File has been archived
	ATTR_ARCHIVE=0x20
};

The various time and date methods return a unix time_t. The underlying ugliness of the MSDOS dates and times are hidden from you.

Getting file information directly

You don’t have to create a directory iterator to get file or directory information if you know the full pathname of the object:

void test(FileSystem& fs) {

  FileInformation *info;

  if(!fs.getFileInformation("/dir/subdir/name.txt",info))
    return;   // unable to get file information

  // use the members of FileInformation then cleanup the
  // memory used therein:

  delete info;
}

Unlike the FileInformation object recycled over and over again by the DirectoryIterator, in this case you own the object returned by getFileInformation and you must delete the object when you’re finished.

Create a new file

Creating a new file is straightforward:

void test(FileSystem& fs) {
  
  if(!fs.createFile("/dir/filename.txt"))
    return;  // failed to create file
}

createFile requires that the directory containing the file must already exist on the file system. The newly created file will have zero length and will be tagged with a creation time taken from your TimeProvider implementation.

createFile does not open the file for you so you’ll need to call openFile (see above) before you can start reading and writing to it.

Create a new directory

Much like createFile really:

void test(FileSystem& fs) {
  
  if(!fs.createDirectory("/dir/subdir"))
    return;  // failed to create directory
}

In the sample above “/dir” must already exist. This method will not automatically create all parent directories for you.

Delete a file

Deleting a file is a basic operation:

void test(FileSystem& fs) {
  
  if(!fs.deleteFile("/dir/filename.txt"))
    return;  // failed to delete file
}

If there are any open File objects on the file then this method will still succeed and those File objects are now in an indeterminate state and must never be used again. Basically, close your files before deleting them.

Delete a directory

Deleting a directory is a basic operation:

void test(FileSystem& fs) {
  
  if(!fs.deleteDirectory("/dir/subdir"))
    return;  // failed to delete directory
}

The subdirectory must be empty of all files and directories except the two “special” files named “.” and “..” that you find on FAT file systems.

The same warnings about objects open that reference the directory apply. Make sure you’ve finished accessing data in the directory before you delete it.

Get the device free space

We can obtain the free space on the device like this:

void test(FileSystem& fs) {

  uint32_t freeUnits,multiplier;

  if(!fs.getFreeSpace(freeUnits,multiplier))
    return;  // failed to get free space
}

This method returns the number of free bytes on the filesystem split into two values, which when multiplied together give the total free bytes.

The reason for the split into two values is that FAT32 filesystems can exceed 4Gb in size. 4Gb is the maximum value that can be held in a 32-bit data type so we have to provide the total as two 32-bit values.

Formatting a device

I support formatting devices with either FAT16 or FAT32 file systems. If other formats become supported in the future then I will document them here.

Microsoft imposes minimum and maximum size limits on FAT16 and FAT32 file systems. My implementation respects these limits and will not allow the creation of file systems outside them. The limits are as follows:

File system Lower limit Upper limit
FAT16 4.1Mb 2Gb
FAT32 32.5Mb 127.5Gb

The following example shows how to format using FAT32. To use FAT16 replace Fat32FileSystemFormatter.h with Fat16FileSystemFormatter.h and Fat32FileSystemFormatter with Fat16FileSystemFormatter.

#include "config/stm32plus.h"
#include "config/sdcard.h"
#include "config/filesystem.h"

using namespace stm32plus;
using namespace stm32plus::fat;

void test() {

  SdioDmaSdCard sdcard;
  Mbr mbr;
  uint32_t firstBlock,blockCount;

  if(sdcard.getFormatType()==BlockDevice::formatMbr) {

    if(!sdcard.getMbr(&mbr))
      return;  // failed to read Master Boot Record

    firstBlock=mbr.partitions[0].lbaFirstSector;
    blockCount=mbr.partitions[0].numSectors;
  }
  else {
    firstBlock=0;
    blockCount=sdcard.getTotalBlocksOnDevice();
  }

  Fat32FileSystemFormatter formatter(
    sdcard,
    firstBlock,
    blockCount,
    "NEWCARD");

  if(errorProvider.getLast()!=0)
    return; // Failed to format the SD card
}

Fat32FileSystemFormatter is the object that does the formatting. It takes a block device, the first block number to format, the total blocks to include in the file system and the 8+3 MSDOS name for the new volume.

The constructor itself does the formatting operation and you can check for the status using the ErrorProvider object when it’s completed.

The above example shows the slightly more involved operation of formatting an SD card. Older SD cards are formatted as ‘super-floppy’ disks. They have no partition table and are literally just a huge sequence of blocks all devoted to the one filesystem on the device.

Newer SD cards are formatted like hard disks. They have a Master Boot Record that describes where up to four partitions are located and each partition contains a file system.

SD cards are normally created with one partition that fills all the available space so the above example shows how we first detect that an MBR is present and if it is then we format the first partition on the device. If it’s not present then the SD card is a ‘super floppy’ and we format all the blocks as one filesystem.

Streams

Streams are a common metaphor in programming languages used to present any data source that will be accessed serially such as a USART or an SPI channel. Files are random access so stm32plus provides a stream class that wraps a file and allows it to be used by any method that expects a stream, for example the drawBitmap method in the GraphicsLibrary class.

void test(File& file) {

  FileInputStream fis(file);

// interact with the methods of InputStream
// .read(), operator>>, .skip() etc..
}

And of course there’s an output stream for operations that will write to the file serially starting at the current file pointer.

void test(File& file) {

  FileOutputStream fos(file);

// interact with the methods of OutputStream
// .write(), operator<< etc.
}

Streams deserve an article of their own, and in due course that’s exactly what they’ll get.

Error Handling

The general policy for error handling in stm32plus is that all methods return true to indicate success and false to indicate failure. If the method is a constructor and hence has no return value then the error provider getLast() method can be used to check for a non-zero status to detect an error.

The ErrorProvider class holds the state of the last error that occurred and there is a singleton named errorProvider declared in ErrorProvider.h. An error consists of a 16-bit provider code and a 16-bit provider-specific code. The provider codes are enumerated in the ErrorProvider class and the specific codes are enumerated in the provider class itself.

For example, in the stm32plus FAT file code there are the following lines in the read method:

// early fail for zero length file

  if(fileLength == 0)
    return errorProvider.set(
       ErrorProvider::ERROR_PROVIDER_FILE,
       E_END_OF_FILE
    );

Here we can see that in the event of a read attempt on a zero length file an error will be raised with a provider code of ErrorProvider::ERROR_PROVIDER_FILE and a specific code of File::E_END_OF_FILE.

In your code you might handle this situation as follows:


  if(!file.read(buffer,10,actuallyRead)) {
    if(errorProvider.isLastError(
       ErrorProvider::ERROR_PROVIDER_FILE,
       File::E_END_OF_FILE)) {
          // handle the case of reading at the EOF
    }
  }

The possibilities are endless and your error handling strategy is your affair, stm32plus merely gives you the ability to tell what went wrong and where it went wrong.

Future

FAT12 support is very much a possibility. This would allow us to create file systems on small devices opening up the possibility of a SRAM disk, or small FLASH disk. Dealing with the 12-bit FAT entries looks like a pain in the rear but it’s not too hard.

A formatter for FAT16, and FAT12 if I write it. Support for RAM disks means that a formatter is essential.