This project facilitates the reading and writing of SD flash memory cards
using FAT16 or FAT32 file systems, over the SPI protocol, and it is designed
with embedded systems in mind. It includes an SD 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 this.
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
- CRC: not yet
- Multiple files and folders instances: Yes
- Ability to create folders and directories
- Delete files: No (only file truncation is implemented)
-
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
- SD Card Pins
- Schematic Interface
- Return Values
- File Object Structures
- File names
- The Buffer, the Writing and the Flush
- Code Examples
- Library Configuration
- Functions and their usage
- 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 - 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
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, // Succeeded MR_DEVICE_INIT_FAIL, // An error occurred during device initialization MR_ERR, // An error occurred in the low level disk I/O layer MR_NO_PARTITION, // No partition has been found MR_FAT_ERR, // General FAT error MR_UNSUPPORTED_FS, // Unsupported file system MR_UNSUPPORTED_BS // Unsupported block size } MOUNT_RESULT; /* File function return codes (FRESULT) */ typedef enum { FR_OK = 0, // Succeeded FR_EOF, // End of file FR_NOT_FOUND, // Could not find the file FR_NO_PATH, // Could not find the path FR_NO_SPACE, // Not enough space to create file FR_EXIST, // File exists FR_INCORRECT_ENTRY, // Some entry parameters are incorrect FR_DENIED, // Access denied due to prohibited access or directory full FR_PATH_LENGTH_EXCEEDED,// Path too long FR_NOT_A_DIRECTORY, FR_ROOT_DIR, // When going back the directory path this is returned when the active dir is root FR_INDEX_OUT_OF_RANGE, FR_DEVICE_ERR // A hard error occurred in the low level disk I/O layer } 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 this
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; uint8_t dirItems = 0; // Mount the memory card return_code = FAT_mountVolume(); // If no error if(return_code == MR_OK){ // Make a directory FAT_makeDir("/Logging"); // Open folder return_code = FAT_openDir(&dir, "/Logging"); if(return_code == FR_OK){ // ... optionally print folder name using an LCD library: print(FAT_getFilename()); // 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 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("/Logging/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; uint8_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, "/Logging"); if(return_code == FR_OK){ // ... optionally print folder name using an LCD library: print(FAT_getFilename()); // 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 DIS_ST7735.h LCD library DIS_ST7735_drawString((char*)read_buf); } // Move file read pointer back to the beginning (optional) FAT_fseek(&file, 0); } }
Library Configuration
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 interfaces. SPI0 is used by this library. This should work for ATmega328. The MISO is configured automatically.
// 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
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.
// 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.
#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.
#define FAT_MAX_FILENAME_LENGTH 30
Functions and their usage
Volume Management
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 Values: 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
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
char label[12]; uint32_t vol_sn = 0; FAT_getLabel(label, &vol_sn)
Directory Access
Make Directory
Create a subdirectory at the specified path. Name of the subdirectory is the name after the last slash '/' in path.
FRESULT FAT_makeDir(const char* path)
Parameters
path
Directory path and nameExample
uint8_t return_code = 0; return_code = FAT_makeDir("/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("/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.
FRESULT FAT_makeFile(const char* path)
Parameters: see FAT_makeDir()
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 fileExample
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 numberdecimals
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 charactersExample
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 writtenbtw
Number of bytes to writebw
Pointer to the variable to return number of bytes writtenExample
#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 fileReturn
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
Version 1.0
sd.h - credits to www.rjhcoding.com for providing a good tutorial on SD card interface that was very helpful in the making of the "sd.h" library
utils.h - needed by "fat.h"
No comments:
Post a Comment