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
- SD Card Pins
- Schematic Interface
- Return Values
- File Object Structures
- File names
- The Buffer, the Writing and the Flush
- Code Examples
- Library Configuration
-
Library Usage
- Volume Management
- Card Detect
- Card Initialization
- Volume Free Space
- Volume Capacity
- Read Volume Label and Serial Number
- Directory Access
- File Access
- Make File
- Delete File or Folder by Index
- Delete File or Folder by Name
- Open File
- Open File by Index
- Set File Pointer
- Seek File End
- Get File Pointer
- Write Float Number
- Write Number
- Write String
- Write File
- Truncate File
- Flush Data
- Read File
- End of File Check
- Error Flag Check
- Clear Error Flag
- Get Filename
- Get Index
- Get File Size
- Get Write Date and Time
- Get File Attributes
- Setting File Date and Time
- 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, // (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 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
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 |
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?
ReplyDeleteHi. 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.
DeleteHi! As I understand it, there are no files in your library for deleting files?
ReplyDeleteWelcome 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:
Delete// 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.
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!
DeleteGood 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.
DeleteI was thinking that it shouldn't take to long to implement a delete function so hopefully I will get it done this week.
Deletehttps://drive.google.com/file/d/1p0nMd4nxa9B4HOJGnEXD-UE6VvpkeJFZ/view?usp=drive_link
DeleteSlightly modified for use in AVR Studio 7 and higher, less trouble for beginners
Hi. I have finally completed the delete function, if you are still interested. I have also optimized some functions.
Deletefdelete()
This comment has been removed by the author.
ReplyDeleteHi!
ReplyDeleteI 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
Hi and sorry for the delay.
DeleteThe 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.
Nicely done! A great work and nice piece of code that has likely saved many people the hours you spent on this.
ReplyDeleteI 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.
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.
ReplyDeleteI 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.
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.
ReplyDeleteGood luck with your work.
It took a little effort, but I did successfully port the SD card library to an AVR128DA48. Two changes to note:
ReplyDelete1. 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.
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.
ReplyDeleteSometimes 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.
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:
ReplyDelete// __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
F_CPU=16000000
F_CLK_PER=F_CPU/4
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.
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.
DeleteOn 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.
Here is a link related to this topic: https://www.avrfreaks.net/s/topic/a5C3l000000UZd1EAG/t150749
Delete