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.


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.


  • 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).


  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.


SDC miniSD - SD
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
NC Not Connected
NC Not Connected

microSD - SD
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

/* 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_UNKNOWN_ERROR        // (15)

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.


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.


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

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

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


#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);
	    // Iterate each file and folder inside active directory
	    for(uint16_t i = 0; i < dir_items; i++){
		return_code = FAT_findNext(&dir, &file);
		    // This is a folder
		    // This is a file
		// Obtain file properties such as name, size...
    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_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_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)

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.



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


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)



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.


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


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



Pointer to the directory object structure


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

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)



Pointer to the directory object structure


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)



Pointer to the directory object structure


Pointer to the file object structure


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)



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)



Pointer to the directory object structure


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)



Pointer to the directory object structure


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)



Pointer to the directory object structure


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)



Pointer to the directory object structure


Pointer to the file object structure


Name of the file

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)



Pointer to the directory object structure


Pointer to the file object structure


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)



Pointer to the file object structure


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)



Pointer to the file object structure


The float number

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)



Pointer to the file object structure


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)



Pointer to the file object structure


String of characters

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)



Pointer to the file object structure


Pointer to the data to be written

Number of bytes to write

Pointer to the variable to return number of bytes written

#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)


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


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)



Pointer to an open file

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


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.


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 for providing a good tutorial on SD card interface that was very helpful in the making of the "sd" library.
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

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


  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?

    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.

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

    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);

      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.

    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!

    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.

    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.

      Slightly modified for use in AVR Studio 7 and higher, less trouble for beginners

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

  3. This comment has been removed by the author.

  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?


    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();
      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.

  5. Nicely done! A great work and nice piece of code that has likely saved many people the hours you spent on this.
    I compiled this for an ATmega328PB today, in Microchip Studio 7, and initially got quite a few compiler errors related to two items: F_CPU definition, and the function "uint8_t UART_sendString(UARTstruct_t* uart, const char* s);" defined with "const char* s" in some places and as only "char* s" in others. The latter was easily corrected by making sure the declarations were consistent. The former is puzzling. Even though I had a line #define F_CPU 16000000UL in main.c before including any header files, I got an error "F_CPU not defined" in the include for sd.h. I had to put the define in the project tool chain instead of main.c for the errors to disappear, which baffles me. As long as it works, I guess.

    This leads me to a different question. The ATMega328 will only run 16MHz clock speed at 5V, which as you know is not compatible w/ SD cards. If I run the MCU at 3.3v, the max F_CPU is 8MHz. There are several warnings in your code about "wrong CPU frequency" if it's not 16MHz; I thought by including the MCU frequency as an include directive , a primary reason for that is the functions that use the F_CPU value will adjust to what the CPU clock actually is. Before I make that change, is there a specific dependency somewhere on 16MHz? Thank you.

  6. Hi. Good to hear that you find it useful.
    Regarding defining the F_CPU in main.c. I had the same issue too, also in Simplicity Studio but I found that what is defined in main.c is not seen in the included files. If you place that in a global header file then it will work. You can see here an explanation on this topic:

    In my uart files the UART_sendString() there is no const anymore. I have modified const char* s to char* s so that the function can print arrays such as label in the example that needs to be modified so it cannot be a constant. Having the function with a constant parameter will only work with a string literal and not with arrays. I did compiled just now to check and I didn't get the error. Check if you have the latest uart.c and uart.h versions. Should be 2.2.

    The SD card can handle up to 25MHz in SPI from what I know. Can't remember if I tested the library at 8MHz but it should work. At 5V you can use even 20MHz but from what I've seen people use 16MHz because it divides better than 20 or 18MHz when using the delay functions.

    Regarding this defines:

    #ifndef F_CPU
    #warning "F_CPU not defined. Define it in project properties."
    #elif F_CPU != 16000000
    #warning "Wrong F_CPU frequency!"

    the 16000000 F_CPU check is there to ensure that the defined value in project settings is indeed the intended one. You or someone else reading the project, might forget or not know if the CPU is defined in project settings and what value is. Having the CPU frequency specified in the top of the main.c file makes it more clear. The value should be replaced with your real CPU frequency so it doesn't have to be 16MHz.
    I believe that defining the F_CPU in a header file is not a good idea, because especially when combining libraries that could lead to many issues. Ideally F_CPU should be defined in a single place, either in a global header file or the project settings. As I update the projects, i will be removing those defines and advising the users to use method described here: There is one for Simplicity Studio also.

    1. Thanks for the update and comments. I did try to change all references to const char* to char*, before I changed everything to const char*. The issue I had was that with char* the compiler gave me an error that I was trying to create a variable in flash (program memory) and not RAM. const char* solved that, but as you point out that won't work with arrays. Guess I'll mess with it later, but for now it works for me so not an urgent task.

      I did find other references to defining F_CPU in the IDE project file, exactly as you described in the link above. It' a great and easy solution to the problem.

      My challenge now will be to port your code from an ATmega-type register architecture to an AVR-style register architecture. The project I am designing requires more ADC channels that offered by any ATmega MCU, so moving to AVRxxDA64 or AVRxxDB64 family. Unfortunately a bit of a learning curve on the register differences...

      Thanks again for a great piece of work.

    2. Yeah, working with registers can be tedious sometimes with lots of debugging. Don't know what is your project about, but you might consider using a multiplexer for the ADC. I have used this method in an universal battery charging. But I'm not sure if implementing that would be easier than adapting the library.
      Good luck with your work.

    3. It took a little effort, but I did successfully port the SD card library to an AVR128DA48. Two changes to note:
      1. Mostly I greatly simplified the UART code as it's only being used to send progress info to a terminal (and in my case, commands back from a terminal) so I duplicated the few UART functions I needed specific to the target MCU.
      2. Ran into a very strange compiler error. Buried deep in the various header files that #include puts into the code for the AVRxxDA family of MCUs, was a definition for "FILE." This of course conflicted with the "FILE" struct defined and used thorough your code.
      I didn't think it wise to alter the Microchip header files and I could not compile code for this particular MCU without the include, so I went through your code and changed every reference to "FILE" to "SD_FILE" to avoid the compiler error.

      Otherwise, pretty straightforward. I have some clean-up to do so that all references to the AVRxxDA registers are defined in the .h files, but otherwise it's working as you designed it. Great piece of work.

    4. I'm glad to hear that you manage to make it work. I might change the name of the FILE define in case other users will port the code to this type of microcontrollers, but I can't just now because i would have to test it and right now i have my hands full with an RFID tag reader project. AVR128DA48 looks interesting, I will consider it in some other projects of mine.
      Sometimes Google doesn't notify me via email when I have a new comment and so I have to check for them in Blogger, thus the delayed response.

  7. One other finding worth noting. The Microchip library for does not work correctly for this class of AVR MCUs. I have not dug down into exactly why, but for these MCUs the delays are based on the peripheral clock CLK_PER and not on F_CPU frequency. The CLK_PER ratio to the CPU clock is defined by a prescaler in the register CLKCTRL.MCLKCTRLB. What I found is the stock delay.h for some reason uses the CLK_PER frequency instead of F_CPU frequency. In my case I had initially set up debugging with an F_CPU frequency of 16MHz, and /16 CLK_PER of 1MHz. I figured this out when a call to _delay_ms(1000) took 16 seconds instead of 1 second. I had to make a local copy of the delay.h file with a substitution of F-CLK_PER for F_CPU for every instance where F_CPU was defined in the delay.h header file, here is an example:
    // __tmp = ((F_CPU) / 4e3) * __ms; // for AVR CPU, frequency is dependent on
    __tmp = ((F_CLK_PER) / 4e3) * __ms; // CLK_PER and not F_CPU

    I defined F_CPU and F_CLK_PER in the project Symbols file as

    and delay timing then behaved as expected. I will bring this up this finding with Microchip tech support to see if there is an alternate fix other than me modifying a Micro-chip supplied header file.

    1. I am not very familiar with the clock management on newer AVR architectures, but in this case seems to be just a different naming convention. Both F_CPU and CLK_PER have something in common: they define the frequency at which the CPU is running and they can be divided by a prescaler.

      On previous architectures, the prescaler was set using fuses, while on newer architecture the clock can be divided using registers. For libraries such as delay.h, is irrelevant what the input frequency is. For example if the input clock is taken from an external 16MHz crystal and you divide it by 16 using a fuse or a register, then the CPU will run at 1MHz therefore the F_CPU and CLK_PER should both be set to 1MHz since those defines represent the CPU frequency.

      So you don't have to modify the delay.h. Just define CLK_PER with the frequency of the input clock divided by the prescaler and make the F_CPU equal with CLK_PER since they represent the same thing - the CPU frequency. If no other library uses the F_CPU you might not define it at all.

    2. Here is a link related to this topic: