This library allows you to easily switch between different storage mediums on supported boards, check the "Compatibility" section for more details about what storage medium is supported on what board.
To initialise the storage medium you need to create a Arduino_UnifiedStorage
object, and to mount it you need to call it's begin()
method:
Arduino_UnifiedStorage storageMedium = USBStorage(); // or
// Arduino_UnifiedStorage sd = SDStorage();
// Arduino_UnifiedStorage internal = InternalStorage();
void setup(){
storageMedium.begin();
}
There is also an overloaded version of the begin()
method that takes in an argument of type FileSystems
(can be FS_FAT
or FS_LITTLEFS
)
void setup(){
storageMedium.begin(FS_FAT);
}
In the case of InternalStorage, a board might not come pre-partitioned when it leaves the factory, therefore, the default constructor for InternalStorage does the following steps.
- Checks if there are any partitions on the drive.
- If there are no partitions it creates the default partitioning scheme (see the Read and create partitions on the Internal (QSPI) Storage section for more information)
- Selects the last partition, detects the file-system type automatically and mounts it
This library also allows you to format any partition or drive to either FAT or LittleFS filesystems.
storageMedium.format(FS_FAT)
or
storageMedium.format(FS_LITTLEFS);
Please make sure you call format before calling begin()
or after calling unmount()
.
You can create or open a file by providing a reference to the root folder of the storage medium or by specifying the full absolute path.
Folder root = storageMedium.getRootFolder();
UFile document = root.createFile("file.txt", FileMode::READ);
UFile document = UFile();
document.open("<absolute_path>/file.txt", FileMode::READ);
You can change the mode of an opened file using the changeMode()
method.
document.changeMode(FileMode::WRITE);
You can read data from a file using the read()
method and write data to a file using the write()
method.
uint8_t buffer[128];
size_t bytesRead = document.read(buffer, sizeof(buffer));
String data = "Hello, World!";
char buffer[data.length() + 1];
data.toCharArray(buffer, sizeof(buffer));
size_t bytesWritten = document.write(reinterpret_cast<const uint8_t*>(buffer), data.length());
Alternatively, you can read and write files using strings.
String content = document.readAsString();
String data = "Hello, World!";
size_t bytesWritten = document.write(data);
The seek()
and available()
methods provide functionality for file seeking and checking the available data in a file. Here's an overview of how to use these methods:
The seek()
method allows you to move the file pointer to a specific position in the file. It takes an offset parameter, which represents the number of bytes to move from a reference position. Here's an example:
// Seek to the 10th byte in the file
bool success = file.seek(10);
if (success) {
// File pointer is now at the desired position
} else {
// Seek operation failed
}
In this example, the seek()
method is called with an offset of 10, which moves the file pointer to the 10th byte in the file. The method returns a boolean value indicating the success of the seek operation.
The available()
method returns the number of bytes available for reading from the current file position. It can be used to determine how much data is left to be read in the file. Here's an example:
// Check the available data in the file
int availableBytes = file.available();
if (availableBytes > 0) {
// There is data available to be read
} else {
// No more data available
}
In this example, the available()
method is called to retrieve the number of available bytes in the file. If the value is greater than 0, it means there is data available to be read. Otherwise, there is no more data left in the file.
These methods are useful for scenarios where you need to navigate to a specific position in a file or check the availability of data before performing read operations.
Closing files is extremely important as filesystems might fail to unmount if files are kept open. You can close files by calling file.close()
The library provides various file manipulation operations such as renaming, deleting, moving, and copying files.
You can rename an existing file using the rename()
method.
bool success = document.rename("newfile.txt");
You can delete a file using the remove()
method.
bool success = document.remove();
You can move a file to a different directory using the moveTo()
method.
Folder destination = storageMedium.getRootFolder().createSubfolder("destination");
bool success = document.moveTo(destination);
This method also allows you to set the behaviour if there's an existing file with the same name at the location you want to move to. You can set the optional parameter overwrite
to true
.
bool success = document.moveTo(destination, true);
Please note that the `File`` object will point now to the moved file, not the original one.
You can copy a file to a different directory using the copyTo()
method.
Folder destination = storageMedium.getRootFolder().createSubfolder("destination");
bool success = document.copyTo(destination);
This method also allows you to set the behaviour if there's an existing file with the same name at the location you want to move to. You can set the optional parameter overwrite
to true
.
bool success = document.copyTo(destination, true);
Please note that the UFile
object will point now to the copy of the file, not the original one.
The library provides methods to create, rename, delete, move, copy, and list directories.
You can create a new directory using the createSubfolder()
method.
Folder root = storageMedium.getRootFolder();
Folder newFolder = root.createSubfolder("new_folder");
You can rename an existing directory using the rename()
method.
bool success = newFolder.rename("renamed_folder");
You can delete a directory using the remove()
method.
bool success = newFolder.remove();
You can move a directory to a different location using the moveTo()
method.
Folder destination = storageMedium.getRootFolder().createSubfolder("destination");
bool success = newFolder.moveTo(destination);
This method also allows you to set the behaviour if there's an existing Folder with the same name at the location you want to move to. You can set the optional parameter overwrite
to true
.
bool success = newFolder.moveTo(destination, true);
Please note that the `Folder`` object will point now to the moved folder.
You can copy a directory to a different location using the copyTo()
method.
Folder destination = storageMedium.getRootFolder().createSubfolder("destination");
bool success = newFolder.copyTo(destination);
This method also allows you to set the behaviour if there's an existing Folder with the same name at the location you want to move to. You can set the optional parameter overwrite
to true
.
bool success = newFolder.copyTo(destination, true);
Please note that the `Folder`` object will point now to the copy of the folder, not the original one.
You can get a list of files and directories in a directory using the getFiles()
and getFolders()
methods.
std::vector<UFile> files = root.getFiles();
std::vector<Folder> folders = root.getFolders();
You can traverse through all the files and directories in a directory using a loop and the getFiles()
and getFolders()
methods.
std::vector<UFile> files = root.getFiles();
for ( UFile file : files) {
// Perform operations on each file
}
std::vector<Folder> folders = root.getFolders();
for (Folder folder : folders) {
// Perform operations on each folder
}
In the case of removable storage, such as USB mass storage devices, it's particularly useful to be able to know when a drive has been connected and disconnected and perform specific actions in those situations.
This library allows you to register and unregister connection and disconnection callbacks. Check out this example on how to do that:
USBStorage usbThumbDrive;
void connectionCallbackMethod(){
connected = true;
}
void disconnectedCallbackMethod(){
connected = false;
}
void setup(){
usbThumbDrive = USBStorage();
usbThumbDrive.onConnect(connectionCallbackMethod);
usbThumbDrive.onDisconnect(disconnectedCallbackMethod);
// to unregister these callbacks use the following methods:
// usbThumbDrive.removeOnConnectCallback();
// usbThumbDrive.removeOnDisconnectCallback();
}
Partitioning internal storage in embedded devices offers benefits such as improved data organization, security, and error recovery. It facilitates easier management, isolates critical components, enables safer firmware updates, and enhances overall system resilience. Additionally, it reduces wear and tear of the flash chip. We recommend using the LittleFS file-system whenever possible on the partition you write to the most as it is designed specifically to reduce wear and tear.
To partition the internal storage using the Arduino_UnifiedStorage library, you need to create a std::vector of Partition
you'd like to write to the drive.
std::vector<Partition> partitioningScheme = {
{1024, FS_FAT}, // 1 MB for certificates
{5120, FS_FAT}, // 5 MB for OTA firmware updates
{8192, FS_LITTLEFS} // 8 MB for user data
})
Partition
is a struct that contains the size of a partition and the file-system it should be formatted as. fileSystemType
can be either FS_FAT
or FS_LITTLEFS
struct Partition {
int size;
FileSystems fileSystemType;
};
The partitioning schemes should follow a few strict guidelines, failure to comply with these might result in a hard-fault, and on the Portenta H7 and Opta you might see a blinking red LED:
- MBR supports up to 4 partitions, please do not add more than that
- Partitions should be of a sensible size (at least 16 bytes to fit a MBR partition header)
- Partition size must be aligned with the block size (usually 4096 bytes)
- Partition size must not be larger than the available storage space (16MB Portenta, Opta, Nicla Vision boards)
To write a partition scheme to the drive, you can use the InternalStorage::partition
method with a std:vector of Partition
as a parameter.
You can also read the existing partitioning table using InternalStorage::readPartitions
. This will return a vector of Partition
. In case there are no partitions on the drive, it will return an empty vector.
Check out the InternalStoragePartitioning
example for more info.
In some cases it might be useful to know what's happening under the hood. If you would like to get more a verbose output on either Serial or RS485 you can add the following to your sketch before initializing any of the library related objects.
Arduino_UnifiedStorage::debuggingModeEnabled = true;