Thursday, July 7, 2022

SD Memory Card Library for AVR Microcontrollers - SD and FAT Driver

This is a library for reading and writing of SD flash memory cards using FAT16 or FAT32 file systems, over the SPI protocol, and is designed with embedded systems in mind. It includes an SD card driver that uses the SPI interface and a FAT driver that is controlled by the host microcontroller.

There are two main standards for flash memory cards: MMC (MultiMediaCards) and SD (Secure Digital), SD being the most used today. SD was developed by the SD Association (SDA) as an improvement over MMCs. These cards have basically a flash memory array and a microcontroller inside that controls erasing, reading, writing, error controls, and wear leveling of the flash array. The data is transferred between the memory card and the host controller as data blocks in units of 512 bytes.

The recommended file systems for memory cards are FAT12/16/32 or exFAT. Maximum volume sizes for each one are: 256MB, 4GB, 16TB* for FAT32 and 128PB for exFAT.

*Windows will refuse to format cards over 32GB using FAT32, offering exFAT and NTFS as an option but there are workarounds and third-party software that can do that.


FAT16 FAT32

Flash RAM Flash RAM
Read 6.1k 605 6.6k 607
Write 7.8k 607 8.6k 609
Read/Write 7.9k 607 8.7k 609

Some rounded up values of the code footprint depending on the functions used: for reading, writing or both. RAM size is expressed in bytes and it includes 512 bytes for the read/write buffer.

Features

  • Communication protocol: SPI
  • Supported memory cards: SD cards (MMC's are not implemented)
  • Includes SD driver: Yes
  • Supported file systems: FAT16 and FAT32
  • Support for LFN (Long File Names): Yes (up to 255 characters)
  • Formatting utility: No
  • Multiple files and folders instances: Yes
  • Ability to create folders and directories
  • Delete files: Yes, with recursive removal of files and sub-directories
  • FSInfo (FAT32): not implemented in order to reduce the code size. The only situations when this could be a downside is when querying for free space, creating files/folders or expand them since this is when it is necessary to search for a free cluster. If the card is mostly empty, even if it has a large capacity, the search for a free cluster will be very quick. As the card is filled it will take longer (few seconds).
 

Contents

  1. SD Card Pins
  2. Schematic Interface
  3. Return Values
  4. File Object Structures
  5. File names
  6. The Buffer, the Writing and the Flush
  7. Code Examples
  8. Library Configuration
  9. Library Usage
    1. Volume Management
      1. Card Detect
      2. Card Initialization
      3. Volume Free Space
      4. Volume Capacity
      5. Read Volume Label and Serial Number
    2. Directory Access
      1. Make Directory
      2. Open Directory
      3. Open a Directory by Index
      4. Go Back
      5. Find by Index
      6. Find Next
      7. Count Items
    3. File Access
      1. Make File
      2. Delete File or Folder by Index
      3. Delete File or Folder by Name
      4. Open File
      5. Open File by Index
      6. Set File Pointer
      7. Seek File End
      8. Get File Pointer
      9. Write Float Number
      10. Write Number
      11. Write String
      12. Write File
      13. Truncate File
      14. Flush Data
      15. Read File
      16. End of File Check
      17. Error Flag Check
      18. Clear Error Flag
      19. Get Filename
      20. Get Index
      21. Get File Size
      22. Get Write Date and Time
      23. Get File Attributes
      24. Setting File Date and Time
  10. Download SD Card Library

SD Card Pinout

Both MMC/SD standards have their own proprietary protocols but they also support SPI which can be selected during card initialization. Since microcontrollers have SPI integrated hardware, this is the most used interface for memory cards.

MMC-SD-miniSD-microSD-Color-Numbers-Names

SDC miniSD - SD
- SPI
PIN SD SPI SD SPI Description
1 DAT3 CS DAT3 CS - SD Serial Data 3
- SPI Card Select (active Low)
2 CMD DI CMD DI - Command, response
- SPI Serial Data In (MOSI)
3 VSS VSS Ground
4 VDD VDD Power. Usually 2.7 to 3.6V and minimum 100mA.
Not 5V tolerant!
5 CLK CLK Serial Clock
6 VSS VSS Ground
7 DAT0 DO DAT0 DO - SD Serial Data 0
- SPI Serial Data Out (MISO)
8 DAT1 NC DAT1 NC - SD Serial Data 1
- Unused
9 DAT2 NC DAT2 NC - SD Serial Data 2
- Unused
10
NC Not Connected
11
NC Not Connected

microSD - SD
- SPI
PIN SD SPI Description
1 DAT2 NC - SD Serial Data 2
- Unused
2 DAT3 CS - SD Serial Data 3
- SPI Card Select (active Low)
3 CMD DI - Command, response
- SPI Serial Data In (MOSI)
4 VDD Power. Usually 2.7 to 3.6V and minimum 100mA.
Not 5V tolerant!
5 CLK Serial Clock
6 VSS Ground
7 DAT0 DO - SD Serial Data 0
- SPI Serial Data Out (MISO)
8 DAT1 NC - SD Serial Data 1
- Unused

 

microSD Card Schematic Interface

microSD Memory Card Schematic Interface

The microcontroller and the memory card must use the same voltage. If the microcontroller is powered from 5V and the card is on 3.3V there needs to be a logic voltage level shifter such as 74AHC125D.

According to the SD card specification, unused Data lines on the card must be terminated with pull-up resistors. Floating pins can lead to excessive power consumption. 

A pull-up on the DO line is needed because, until initialized into SPI mode, the DO pin is open drain.

A pull-up on the CS (Card Select) is recommended because it could take many milliseconds until the microcontroller enables the CS pin High and during that time the SPI pins could have random values and possibly corrupting the card.

Some sockets have a 9th pin that optionally can be pulled high using a microcontroller pin set as an input high. This is a mechanical switch. When the card is inserted the pin goes Low allowing the microcontroller to detect card insertion/removal.

Return Values

Two enums are used for the return values: first enum is only used by the volume initialization function, while the second one is used by most of the functions.

/* Volume mounting return codes (MOUNT_RESULT) */
typedef enum {
    MR_OK = 0,		// (0) Succeeded
    MR_DEVICE_INIT_FAIL,// (1) An error occurred during device initialization
    MR_ERR,		// (2) An error occurred in the low level disk I/O layer
    MR_NO_PARTITION,	// (3) No partition has been found
    MR_FAT_ERR,		// (4) General FAT error
    MR_UNSUPPORTED_FS,	// (5) Unsupported file system
    MR_UNSUPPORTED_BS	// (6) Unsupported block size
} MOUNT_RESULT;


/* File function return codes (FRESULT) */
typedef enum {
    FR_OK = 0,		    // (0) Succeeded
    FR_EOF,		    // (1) End of file
    FR_NOT_FOUND,	    // (2) Could not find the file
    FR_NO_PATH,		    // (3) Could not find the path
    FR_NO_SPACE,	    // (4) Not enough space to create file
    FR_EXIST,		    // (5) File exists
    FR_INCORRECT_ENTRY,	    // (6) Some entry parameters are incorrect
    FR_DENIED,		    // (7) Access denied due to prohibited access or directory full
    FR_PATH_LENGTH_EXCEEDED,// (8) Path too long
    FR_NOT_A_DIRECTORY,	    // (9)
    FR_ROOT_DIR,	    // (10) When going back the directory path this is returned when the active dir is root
    FR_INDEX_OUT_OF_RANGE,  // (11)
    FR_DEVICE_ERR,	    // (12) A hard error occurred in the low level disk I/O layer
    FR_READ_ONLY_FILE,	    // (13) File is read-only and cannot be deleted
    FR_INCORRECT_CHECKSUM,  // (14)
    FR_UNKNOWN_ERROR        // (15)
} FRESULT;
 

File Object Structures

The properties of an active directory or file are stored by two different object structures called DIR and FILE. Most functions require a pointer to one or both of them.

Example:

DIR folder;
FILE file;

FAT_function(&folder, &file);

To open multiple folders and files at the same time, create other instances like so:

DIR folder;
FILE file;
DIR imgFolder;
FILE imgFile;

FAT_function(&folder, &file);
FAT_function(&imgFolder, &imgFile);

But this will increase the memory usage and could create confusion with which file belongs to which directory.

Filenames

Unicode support is not implemented because that would take too much space on a microcontroller.

A file name can't contain any of the following characters or Windows might complain when the card is used: \ / : * ? " < > |

The Buffer, the Writing and the Flush

 

Main buffer and writing functions

The data is transferred between the memory card and the host controller as data blocks in units of 512 bytes. That is because High Capacity SD Memory Cards (over 2GB) have the block length fixed to 512 bytes. On older cards, the block length could be changed but since they are obsolete and to keep things consistent, the SD card library has also the buffer length set to 512 bytes. FAT file systems also like to use blocks of 512 bytes.

The main buffer called SD_Buffer is used for both reading and writing and that could create problems if the write functions are not used properly. It would be nice to have two separate buffers - one for reading and one for writing - but that would take double the memory and some project could not afford that.

When using a write function, the data is not immediately transferred to the card because that would wear the flash memory very quickly (imagine writing a sector after each byte). Instead the writing functions puts the data into the main buffer array. When the buffer is full the function will automatically transmit the buffer to the card. The buffer could also be saved to the card at any time by using fsync(). It is important to remember that between using a writing function and fsync() no function that uses the SD_Buffer must be used because they will overwrite the buffer leading to data loss. To avoid that, simply use the writing functions like this:

                // Write a string
		FAT_fwriteString(&file, "Logging Date: 2022\n");
			
		// Write sensor output
		FAT_fwriteFloat(&file, 120.033, 3);
		FAT_fwriteString(&file, ",");
		FAT_fwriteFloat(&file, -0.221, 3);
		FAT_fwriteString(&file, ",");
		FAT_fwriteFloat(&file, -30.004, 3);
		FAT_fwriteString(&file, ",");
		FAT_fwriteFloat(&file, 0.023, 3);
			
		// Synchronize the writing buffer with the card
		FAT_fsync(&file);

Notice that between the writing functions and fsync(), no other function is used.

Data synchronization

The fsync() is used to synchronize the data written to main buffer with the card. Other applications calls this flush() and you can rename it like that if you wish.

Knowing that writing the flash memory too often has a negative impact, you may ask how often should the fsync() function be used. That depends on some factors. For example in data logging: how important is the data? Could you afford the loss of data collected over 5 minutes for example? How often is the data collected? How likely is for the power to the micro or card to go down? If you log a temperature sensor once an hour then you could use fsync() every hour. Ideally you would write to a card as less often as possible. Say you write every 5 minutes, that is over 100k writes over a year. Writing once a minute means 525k in a year. Considering that a good card has wear level protection, that might not affect the card as much. I am not an expert in flash memory so I prefer to play safe and write the card as less often as possible.

When the buffer gets filled it will be written to the card automatically but the file size will only be updated when using fsync(). This way you could write many block without writing the sector that stores the file size. This sector contains other directory entries of other files so it will see the most wear if the file size it is updated too often.

To keep track of time to know when to use fsync() you don't need anything precise such a crystal or RTC. You can easily implement a simple clock using this millis library https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html.

Code Examples

 

Writing - data logging CSV style


#include "fat.h"

DIR dir;
FILE file;
uint8_t return_code = 0;
uint16_t dirItems = 0;
	
// Mount the memory card
return_code = FAT_mountVolume();
	
// If no error
if(return_code == MR_OK){
    // Make a directory inside the root folder
    FAT_makeDir(&dir, "Logs");
		
    // Open the folder
    return_code = FAT_openDir(&dir, "Logs");
    if(return_code == FR_OK){
		
        // Get number of folders and files inside the directory
	dirItems = FAT_dirCountItems(&dir);
			
	// Open a file for reading or writing
	return_code = FAT_fopen(&dir, &file, "log.txt");
			
	// Make the file in the current directory if it doesn't exist.
	// The functions that make folders and files takes much space
	// so it is recommended to have them made on a PC instead.
	if(return_code == FR_NOT_FOUND){
	    FAT_makeFile(&dir, "log.txt");
	    return_code = FAT_fopen(&dir, &file, "log.txt");
	}
			
	// Move the writing pointer to the end of the file
	FAT_fseekEnd(&file);
			
	// Write a string
	FAT_fwriteString(&file, "Logging Date: 2022\n");
			
	// Write sensor output
	FAT_fwriteFloat(&file, 120.033, 3);
	FAT_fwriteString(&file, ",");
	FAT_fwriteFloat(&file, -0.221, 3);
	FAT_fwriteString(&file, ",");
	FAT_fwriteFloat(&file, -30.004, 3);
	FAT_fwriteString(&file, ",");
	FAT_fwriteFloat(&file, 0.023, 3);
			
	// Synchronize the writing buffer with the card
	FAT_fsync(&file);
    }
}
 

Reading


#include "fat.h"

DIR dir;
FILE file;
uint8_t return_code = 0;
uint16_t dirItems = 0;
uint8_t* read_buf = 0;
	
// Mount the memory card
return_code = FAT_mountVolume();
	
// If no error
if(return_code == MR_OK){
		
    // Open folder
    return_code = FAT_openDir(&dir, "/Logs");
    if(return_code == FR_OK){

	// Get number of folders and files inside the directory
	dirItems = FAT_dirCountItems(&dir);
			
	// Open a file for reading or writing
	return_code = FAT_fopen(&dir, &file, "log.txt");
		
	// Read block of 512 bytes until the end-of-file flag is set
	while(FAT_feof(&file) == 0 && FAT_ferror(&file) == 0){
	    read_buf = FAT_fread(&file);
			
	    // Function included in uart.h library
	    UART_sendString(&uart0, (char*)read_buf);
	}
		
	// Move file read pointer back to the beginning (optional)
	FAT_fseek(&file, 0);
    }
}
 

Displaying files and folders

 
int main(void){
    DIR dir;
    FILE file;
    uint8_t return_code = 0;
    uint16_t dir_items = 0;
	
    // Mount the memory card
    return_code = FAT_mountVolume();
	
    // If no error
    if(return_code == MR_OK){
		
	// Open root folder
	return_code = FAT_openDir(&dir, "/");
		
	if(return_code == FR_OK){
	    // Get number of folders and files inside the directory
	    dir_items = FAT_dirCountItems(&dir);
	    //print(dir_items);
			
	    // Iterate each file and folder inside active directory
	    for(uint16_t i = 0; i < dir_items; i++){
		return_code = FAT_findNext(&dir, &file);
				
		if(FAT_attrIsFolder(&file)){
		    // This is a folder
		}else{
		    // This is a file
		}
				
		// Obtain file properties such as name, size...
		//print(FAT_getFilename());
	    }
	}
    }
	
    while (1){
		
    }
}

Test code using UART

A more complex example that is using UART to output debugging information can be downloaded from here.

Library Configuration

 

Multiple SPI devices

If you have more than one SPI devices, such as the display and an SD card, set their CS (Chip Select) pins as output high before using any initialization functions, otherwise while one is being initialized another device could start to communicate creating conflicts on the communication bus.

SPI Pins (sd.h)

By default the SPI pins are set to be on port PORTB. The MOSI pin is on PB3 and SCK on pin PB5. If you are using Arduino you need to find what microcontroller it's using and check it's datasheet for the SPI pins. Some microcontrollers have two SPI peripherals. SPI0 is used by this library. This should work for ATmega328PB. The MISO is configured automatically by the SPI hardware as an input.

// SPI0 I/O pins (MOSI and SCK)
#define SPI0_PORT	PORTB // SPI PORT
#define SPI0_DDR	DDRB  // SPI DDR
#define SPI0_PINS	PINB  // Holds the state of each pin on port where SPI pins are

#define SPI0_MOSI_PIN	PB3   // SDA pin
#define SPI0_SCK_PIN	PB5   // SCK pin

#define SPI0_SS_PIN	PB2   // SS pin will be set as output low by the library

The CS (Card Select) is the SPI SS0 pin and is set to PB2 but can be changed to any other port and pin. Regardless of what CS pin you use it is important that the SS SPI pin be set as an output high or low otherwise the SPI won't work properly. This is according to the datasheet.

// SPI Chip Select pin
#define CARD_CS_DDR		DDRB
#define CARD_CS_PORT		PORTB
#define CARD_CS_PIN		PB2

Card detect pin  (sd.h)

Most microSD card sockets have a 9'th mechanical pin used to detect the card presence. This pin is optional. Comment out if not used.

// Card detect pin for card sockets with this feature (optional)
#define CARD_DETECT_DDR		DDRB
#define CARD_DETECT_PORT	PORTB
#define CARD_DETECT_PIN		PB1
#define CARD_DETECT_PINS	PINB

Use FAT32 (fat.h)

To disable FAT32 and use only FAT16, set this to 0. This will save some space.

#define FAT_SUPPORT_FAT32	1

Maximum filename length (fat.h)

This can be changed depending on how long of a filename you need. Maximum value is 255 but that includes the path and filename. This also can save space by using shorter file names.

#define FAT_MAX_FILENAME_LENGTH		30

 

Library Usage

Volume Management


Card Detect

Some card sockets have a card detect pin that can be connected to VCC through a resistor and when a card is inserted the pin will be mechanically pulled to ground. This function can be used to read this pin. The pin PORT and pin number must be configured in sd.h file.

bool sd_detected(void)

Return: 1 if card detected.

Card Initialization

Reads the MBR (if present) and the Boot Sector and sets the fat object structure with all the proprieties of the card file system, necessary by all other functions. If there are multiple partitions, it selects the first one. The host will negotiate for the SPI protocol. The SPI clock will be set at the maximum speed which is F_CPU / 2.

MOUNT_RESULT FAT_mountVolume(void)

Parameters: none

Return: MR_OK or error code

Example:

uint8_t return_code = 0; // Mount the memory card return_code = FAT_mountVolume(); // If no error if(return_code == MR_OK){     // ... open folder }

Volume Free Space

Returns volume free space in bytes. On FAT32 this can take a few seconds depending on how large the card is.

uint64_t FAT_volumeFreeSpace(void)

Volume Capacity in Bytes

Returns volume capacity in bytes.

uint64_t FAT_volumeCapacity(void)

Volume Capacity in KiB

Returns volume capacity in KiB.

float FAT_volumeCapacityKB(void)

Volume Capacity in MiB

Returns volume capacity in MiB.

float FAT_volumeCapacityMB(void)

Volume Capacity in GiB

Returns volume capacity in GiB.

float FAT_volumeCapacityGB(void)

Read Volume Label and Serial Number

Returns the label and serial number of a volume.

FRESULT FAT_getLabel(char* label, uint32_t* vol_sn)

Parameters

label 

Pointer to the buffer to store the volume label. If the volume has no label, a null-string will be returned. The buffer array must be of type char and 12 bytes in size: 11 for volume label and 1 for the null.

vol_sn

Pointer to a uint32_t variable to store the volume serial number. Pass 0 if not needed.

Example

// Read label and serial number
char label[12];
char vol_sn_byte[4];
uint32_t vol_sn = 0;
FAT_getLabel(label, &vol_sn);
		
// Extract serial number
vol_sn_byte[0] = (vol_sn >> 24) & 0xFF;
vol_sn_byte[1] = (vol_sn >> 16) & 0xFF;
vol_sn_byte[2] = (vol_sn >> 8) & 0xFF;
vol_sn_byte[3] = vol_sn & 0xFF;
		
// Print serial number in hexadecimal format to serial terminal
UART_sendString(&uart0, "Volume Serial Number is ");
UART_sendHex8(&uart0, vol_sn_byte[0]);
UART_sendHex8(&uart0, vol_sn_byte[1]);
UART_sendString(&uart0, "-");
UART_sendHex8(&uart0, vol_sn_byte[2]);
UART_sendHex8(&uart0, vol_sn_byte[3]);
UART_sendString(&uart0, "\n"); // new line character
		
// Print label
UART_sendString(&uart0, "Volume label is: ");
UART_sendString(&uart0, label);
UART_sendString(&uart0, "\n\n"); // 2x new line character
 

Directory Access

 

Make Directory

Create a folder at the specified path. Name of the folder is the name after the last slash '/' in path. If a directory is opened first, then the folder is created inside the active directory and the path can be only the folder name. This is convenient since the full path is not always available.

FRESULT FAT_makeDir(DIR* dir_p, const char* path)

Parameters

dir_p

Pointer to the directory object structure

path

Directory path and name or just name if a folder was previously open.
Example

uint8_t return_code = 0; return_code = FAT_makeDir(&dir, "Folder 1/Folder 2"); if(return_code == FR_OK){ // Folder 2 created inside Folder 1 // Folder 1 must exist } // Make a folder inside ROOT FAT_makeDir(&dir, "Folder 3");
 

Open Directory

Open a directory using the given path. The path may start with '/' but is not necessary. A single slash '/'  indicates the root directory. The path must not end with a slash.

FRESULT FAT_openDir(DIR* dir_p, const char* path)

Parameters

dir_p

Pointer to the directory object structure

path

Directory path and name

Open a Directory by Index

When the directory name is unknown, another way to open it is by index. The index is the order the files appears in the directory entries. When you will need this function you will know it. Can be used for a file explorer for example.

FAT_openDir() must be used before running this function.

FRESULT FAT_openDirByIndex(DIR* dir_p, FILE* finfo_p, uint16_t idx)

Parameters

dir_p

Pointer to the directory object structure

finfo_p

Pointer to the file object structure

idx

The file index starting from 1

Go Back

Go to parent directory of active directory. If the active directory is Root, then the function will return FR_ROOT_DIR and active directory will remain Root.

FRESULT FAT_dirBack(DIR* dir_p)

Parameters

dir_p

Pointer to the directory object structure

Find by Index

Get file info of the item in the active directory with a specific index position. If index is greater than the number of items inside the directory, then FR_NOT_FOUND will be returned.

FRESULT FAT_findByIndex(DIR* dir_p, FILE* finfo_p, uint16_t idx)

Parameters: see FAT_openDirByIndex()

Find Next

Find next file in the active directory and get file info.

FRESULT FAT_findNext(DIR* dir_p, FILE* finfo_p)

Parameters

dir_p

Pointer to the directory object structure

finfo_p

Pointer to the file object structure

Count Items

Return the total number of files and folders inside the active directory.

uint16_t FAT_dirCountItems(DIR* dir_p)


File Access


Make File

Create a file at the specified path or in the active directory. If a directory was open previously, then the path should be the file name only.

FRESULT FAT_makeFile(DIR* dir_p, const char* path)

Parameters: see FAT_makeDir()

Delete File or Folder by Index

Delete a file or folder based on it's index. The folder that includes the file or folder to be deleted must be open first using the appropriate function. Read-only files will not be deleted. Folders that contain other folders and files will have their clusters removed recursively and only the folders will be marked with 0xE5 as deleted, not files. This is not an issue since the cluster that contains them will be filled with 0's when a directory is created or extended.

Files or sub-directories to be deleted needs to be first closed by the user - or not in use - since the library is not aware of the directory and file instances created by the user in order to check for open items prior deletion. The idea is - don't write to a file after it was deleted.

There are some safety checks that the function implements, before a file is deleted:

  • The LFN entry checksum must match the checksum generated using the SFN
  • The file is not read-only
  • While recursive deletion of files, the entry's reserved byte must be 0x00 or 24 and write date must not be 0. This is to ensure that this is a file entry and not a data sector.  On Windows desktop.ini has reserved byte set to 24

The index argument is the position (from 1 to # of file and folders inside the active folder) of the file/folder to be deleted, and depends of the order in which files were written. The index should be considered valid only after the directory is open and no files have been removed and created since then. Using an index is useful when the file name cannot be hard-coded.

Caution: if the file is deleted and the directory is opened again and the delete function is again used, the file that was previously with one index higher will now be the actual index and will be deleted. Only when the deleted file was last one in the folder, then no file will be deleted. To prevent this, a confirm dialog can be implemented by first getting the file info by index and printing the file name to the user for confirmation.

FRESULT FAT_fdelete(DIR* dir_p, uint16_t idx)

Parameters

dir_p

Pointer to the directory object structure

idx

File index inside active directory

Delete File or Folder by Name

Wrapper to fdelete(). Same as fdelete() except the file name argument is converted to an index and passed to fdelete(). This is much safer than fdelete() since if the file name doesn't exist, no file will be deleted.

FRESULT FAT_fdeleteByName(DIR* dir_p, const char* fname)

Parameters

dir_p

Pointer to the directory object structure

fname

Name of the file

Open File

Open a file by name. The search will be made inside the active directory.

FRESULT FAT_fopen(DIR* dir_p, FILE* file_p, char *file_name)

Parameters

dir_p

Pointer to the directory object structure

file_p

Pointer to the file object structure

file_name

Name of the file
Example

DIR dir; FILE file; FAT_openDir(&dir, "/Logs"); // Open a file inside the "Logs" folder FAT_fopen(&dir, &file, "log.txt"); 

Open File by Index

When the file name is unknown, another way to open it is by index. The index is the order the files appears in the directory entries. When you will need this function you will know it. Can be used for a file explorer for example. A directory must be open first.

FRESULT FAT_fopenByIndex(DIR* dir_p, FILE* file_p, uint16_t idx)

Parameters

dir_p

Pointer to the directory object structure

file_p

Pointer to the file object structure

idx

The file index starting from 1

Set File Pointer

Move the file pointer x number of bytes.

void FAT_fseek(FILE* fp, FSIZE_t fptr)

Parameters

fp

Pointer to the file object structure

fptr

File pointer offset value in bytes from 0 to file size

Seek File End

Move the file pointer to end of file. Wrapper of fseek().

void FAT_fseekEnd(FILE* fp)

Get File Pointer

Return the fptr file pointer. FSIZE_t is uint32_t.

FSIZE_t FAT_getFptr(FILE* fp)
 

Write Float Number

Wrapper function of fwrite() used to convert a float number into a string and write it to a file. A file must be open first.

FRESULT FAT_fwriteFloat(FILE* fp, float float_nr, uint8_t decimals)

Parameters

fp

Pointer to the file object structure

float_nr

The float number
decimals

Number of digits after the dot (float precision)

Write Number

Wrapper function of fwrite() used to convert a number into a string and write it to a file. A file must be open first.

FRESULT FAT_fwriteInt(FILE* fp, INT_SIZE nr)

Parameters

fp

Pointer to the file object structure

nr

The number. By default INT_SIZE is of type int32_t but can be changed to int64_t if very large numbers need to be written.

Write String

Wrapper function of fwrite() used to write a string. A file must be open first.

FRESULT FAT_fwriteString(FILE* fp, const char* string)

Parameters

fp

Pointer to the file object structure

string

String of characters
Example

FAT_fwriteString(&fp, "String to SD card file")

Write File

Write data to the file at the file offset pointed by read/write pointer. The file must be open first. The write pointer advances with each byte written. CAUTION: running other functions will overwrite the common data buffer causing the loss of  unsaved data. Use fsync() before using any other function including fseek().

FRESULT FAT_fwrite(FILE* fp, const void* buff, uint16_t btw, uint16_t* bw)

Parameters

fp

Pointer to the file object structure

buff

Pointer to the data to be written
btw

Number of bytes to write
bw

Pointer to the variable to return number of bytes written
Example

#define DATA_BUFFER_SIZE 10 FILE file; uint8_t data[DATA_BUFFER_SIZE]; uint16_t bw = 0; // Move write pointer to end of the file FAT_fseekEnd(&file); FAT_fwrite(&file, data, DATA_BUFFER_SIZE, &bw); FAT_fsync(&file);

Truncate File

Truncates the file size to the current file read/write pointer set by fseek().

FRESULT FAT_ftruncate(FILE* fp)

Example

// Keep only first 10 bytes of the file FAT_fseek(&file, 10); FAT_ftruncate(&file);

Flush Data

Flush cached data of the writing file. For more details read "The Buffer, the Writing and the Flush" section.

FRESULT FAT_fsync(FILE* fp)

Read File

Read data from a file. Each time, the function will return a pointer to the main buffer array containing a block of data that must be used before running other functions that might overwrite the main buffer. The file must be opened using the appropriate function before it can be read.

uint8_t* FAT_fread(FILE* file_p)

Parameters

file_p

Pointer to an open file
Return

Returns a pointer to the main buffer array SD_Buffer with the length defined by SD_BUFFER_SIZE which is 512 bytes.

Example

See Reading example

End of File Check

While reading a file this function will return true if the file pointer is at the end or false otherwise.

bool FAT_feof(FILE* fp)

Error Flag Check

Check if an error occurs during file read. Returns FR_DEVICE_ERR or 0.

uint8_t FAT_ferror(FILE* fp)

Clear Error Flag

Set the error flag to 0.

void FAT_fclear_error(FILE* fp)

Get Filename

Obtain the name of the folder or file. It returns a char pointer to the FAT_filename array. Since this array is used by some other functions, the file name is only available after opening a folder/file. The length of the file name array is set by FAT_MAX_FILENAME_LENGTH. The default is 30 characters but can be modified depending on how long your file names will be.

char* FAT_getFilename(void)

Get Index

Return the index of the active item of the active directory.

uint16_t FAT_getItemIndex(DIR* dir_p)

Get File Size

Return the file size in bytes.

FSIZE_t FAT_getFileSize(FILE* finfo_p)

Get Write Date and Time

Return the date and time when the file was written.

uint16_t FAT_getWriteYear(FILE* finfo_p)
uint8_t FAT_getWriteMonth(FILE* finfo_p)
uint8_t FAT_getWriteDay(FILE* finfo_p)
uint8_t FAT_getWriteHour(FILE* finfo_p)
uint8_t FAT_getWriteMinute(FILE* finfo_p)
uint8_t FAT_getWriteSecond(FILE* finfo_p)

Get File Attributes

 

Check if item is folder

bool FAT_attrIsFolder(FILE* finfo_p)

Check if item is file

bool FAT_attrIsFile(FILE* finfo_p)

Check if item has the hidden attribute set

bool FAT_attrIsHidden(FILE* finfo_p)

Check if file/folder has the system attribute set

bool FAT_attrIsSystem(FILE* finfo_p)

Is item read only?

bool FAT_attrIsReadOnly(FILE* finfo_p)

Check if item has the archive attribute set

bool FAT_attrIsArchive(FILE* finfo_p)

Setting File Date and Time

When creating or modifying a file, the write date and time must be set. For now, the functions have some dummy values that could be substituted with real date and time from an RTC or a clock made using a crystal or the CPU itself. You could modify the functions listed below to fit your needs. This functions will be called by the FAT library when creating or modifying files, so you don't have to use them directly.

uint8_t FAT_createTimeMilli(void)

uint16_t FAT_createTime(void)

uint16_t FAT_createDate(void)


If you have any questions, suggestions or bugs to report, leave them in the comment section below. Consider sharing, subscribing and also donating.


Download

Changelog and license can be found at the beginning of the files
SD/FAT library
v2.0 SD/FAT library All files inside the folder can be downloaded as a zip file.
Contains .h .c: sd, fat, utils, uart (optional)
Credits to www.rjhcoding.com for providing a good tutorial on SD card interface that was very helpful in the making of the "sd" library.
Changelog
v2.0 04-02-2024:
- Included support for modern UPDI AVR microcontrollers.
Other Resources

SD card protocol tutorial Two part tutorial on how the SD card protocol works.

uart library Useful for debugging and displaying card info on a serial terminal.

Microsoft Extensible Firmware Initiative FAT32 File System Specification, Version 1.03 2000, fatgen103 File Allocation Table (FAT) specifications PDF from Microsoft
External Links

https://en.wikipedia.org/wiki/SD_card

Termite Terminal by CompuPhase Favorite serial terminal

Active Disk Editor Extremely useful for decoding and navigating a file system structure. If you don't see any devices, open as administrator.

HxD - Freeware Hex Editor and Disk Editor Another useful tool for navigating raw sectors of an SD card, and not only

12 comments:

  1. Hello, when I try to use the code that you published at the bottom of the page (sd.h, fat.h and utils.h) my IDE keeps giving me the same errors, that SPSR0, SPCR0 and SPDR0 are undeclared. I'm not sure how to solve this, do you know what I should do about it?

    ReplyDelete
    Replies
    1. Hi. Those are the SPI registers used by the sd.h file. This file includes avr/io.h which should include the necessary file to provide the register definitions. For example for ATmega328PB the compiler should include iom328pb.h to define the registers. What IDE and microcontroller are you using? My setup is Microchip Studio and ATmega328PB and I don't get any errors.

      Delete
  2. Hi! As I understand it, there are no files in your library for deleting files?

    ReplyDelete
    Replies
    1. Welcome to the blog. At the moment there is no function to delete the file but you can truncate the file using the FAT_ftruncate function like so:

      // Keep only the first byte of the file
      FAT_fseek(&file, 1);
      FAT_ftruncate(&file);

      So if the file was 100KB before, after using the function the file is 1 byte. The file name will still be present in the FAT table though. Can't remember why I haven't added a delete function. I think I must have forgotten.

      Delete
    2. Great library!!! Everything works fine, I had to change a little for ABP Studio, only recently I needed to delete a file ... but it turns out there is no delete function :) Thanks again for your efforts!

      Delete
    3. Good to hear that. Initially i made it for the purpose of data logging using a microcontroller and probably I thought a delete function would not be that needed and also difficult to implement. This one was much more difficult to make compared to my other projects. It took over 3 months including reading about SD protocol and FAT file system. Maybe some day i will add more functionality but for now i am working on a stepper controller library. Have a nice day.

      Delete
    4. I was thinking that it shouldn't take to long to implement a delete function so hopefully I will get it done this week.

      Delete
    5. https://drive.google.com/file/d/1p0nMd4nxa9B4HOJGnEXD-UE6VvpkeJFZ/view?usp=drive_link
      Slightly modified for use in AVR Studio 7 and higher, less trouble for beginners

      Delete
    6. Hi. I have finally completed the delete function, if you are still interested. I have also optimized some functions.
      fdelete()

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Hi!
    I tried to use this library for Atmega328P and some modify sd.h file (replace SPSR0 and SPDR0 with SPSR and SPDR), but it still doesn't work in proteus. When I trying to use FAT_mountVolume(), the return_code is always 1. Idk what's the problem. Could you help me, please?
    URL: https://drive.google.com/drive/folders/1qIbPAEF9i8q8ZLu4wifM0ojhZnV06ULS?usp=sharing

    ReplyDelete

    ReplyDelete
    Replies
    1. Hi and sorry for the delay.
      The error code 1 is related to MR_DEVICE_INIT_FAIL which indicates that sd_init() returned an error during card initialization. There can be multiple reasons why this might happen. Try adding this line inside the FAT_mountVolume() function:

      // Card initialization
      fat->fs_low_level_code = sd_init();
      if(fat->fs_low_level_code){
      return fat->fs_low_level_code;
      //return MR_DEVICE_INIT_FAIL;
      }

      The fat->fs_low_level_code will give a clue to where in the sd_init() is the issue. If the error code is still 1 then in this case represents SD_IDLE_STATE_TIMEOUT defined in the sd.h file. That can be can be caused by the SPI not working, a damaged card or some issue with the connections. You might consider using a logic analyzer to see if the card responds over the SPI.

      Delete