
FAT32 media emulation on stm32f4

Recently, this task arose - emulating FAT32 media on stm32f4.
Its unusualness lies in the fact that among the strapping of the microcontroller there may not be a drive at all.
In my case, there was a drive, but the rules for working with it did not allow placing the file system. In TK, however, there was a requirement to organize a Mass Storage interface for accessing data.
The result of the work was a module, which I entitled "emfat", consisting of the same .h and .c file.
The module is platform independent. In the attached example, it runs on the stm32f4discovery board.
The function of the module is to give pieces of the file system that usb-host will request, substituting user data if it tries to read some file.
Who can be useful
First of all , it is useful in any technical solution where the device offers a Mass Storage interface in read-only mode. FAT32 emulation “on the fly” in this case will allow you to store data as you wish, without the need to support the FS.
Secondly , it is useful for aesthetes. Someone who does not have a physical drive, but wants to see his device as a disk in the cherished “My Computer”. At the same time, the root of the disk may contain instructions, drivers, a file with a description of the device version, etc.
In this case, it should be noted that instead of emulating the medium, you can give the host part of the “compiled” cast of the prepared FS. However, in this case, most likely, the memory consumption of the MK will be significantly higher, and the flexibility of the solution is zero.
So how does it work.

When a user tries to read or write a file, the corresponding call is translated into usb requests, which are transmitted to our device. The essence of the queries is simple - write or read the sector on the destination medium.
In this case, it should be noted that Windows (or another OS) behaves like a hostess in terms of organizing storage on the media. Only she knows which sector she wants to read or write. And he wants to - and completely defragments us, arranging chaotic "juggling" sectors ... Thus, the function of a typical USB MSC controller is to meekly pour a portion of 512 bytes with a shift on the media, or read the portion.
Now back to the emulation function.
I must warn you right away that we do not emulate recording on the media. Our media is read-only.
This is due to the increased complexity of controlling the formation of the file table.
However, the module API has a dummy function emfat_write. Perhaps in the future a solution will be found for correct recording emulation.
The task of the module when reading a request is to “give away” valid data. This is his main work. Depending on the requested sector, this data may be:
- MBR record;
- Boot sector;
- One of the sectors of the file table is FAT1 or FAT2;
- Directory Description Sector;
- Data sector related to the file.
It should be noted that emphasis was placed on accelerating the decision “what data to give”. Therefore, the overhead was minimized.
Due to the fact that we refused to write to the drive, we are free to organize the storage structure as we like:

Everything is completely standard, except for a few details:
- Data is not fragmented;
- Some unnecessary FAT areas are missing;
- There are no free clusters (the size of the media is “adjusted” to the size of the data);
- The size of FAT tables is also “tailored” to fit the data size.
Naturally, you need to understand that this structure is imaginary. In reality, it is not contained in RAM, but is formed accordingly, depending on the number of the sector being read.
Module API
The API is composed of only three functions:
bool emfat_init(emfat_t *emfat, const char *label, emfat_entry_t *entries);
void emfat_read(emfat_t *emfat, uint8_t *data, uint32_t sector, int num_sectors);
void emfat_write(emfat_t *emfat, const uint8_t *data, uint32_t sector, int num_sectors);
Of these, the main function is emfat_init.
Its user calls once - when connecting our usb device or at the start of the controller.
Function parameters - file system instance (emfat), section label (label) and table of elements of the file system (entries).
The table is defined as an array of emfat_entry_t structures as follows:
static emfat_entry_t entries[] =
{
// name dir lvl offset size max_size user read write
{ "", true, 0, 0, 0, 0, 0, NULL, NULL }, // root
{ "autorun.inf", false, 1, 0, AUTORUN_SIZE, AUTORUN_SIZE, 0, autorun_read_proc, NULL }, // autorun.inf
{ "icon.ico", false, 1, 0, ICON_SIZE, ICON_SIZE, 0, icon_read_proc, NULL }, // icon.ico
{ "drivers", true, 1, 0, 0, 0, 0, NULL, NULL }, // drivers/
{ "readme.txt", false, 2, 0, README_SIZE, README_SIZE, 0, readme_read_proc, NULL }, // drivers/readme.txt
{ NULL }
};
The following fields are present in the table:
name: display name of the element;
dir: whether the item is a directory (otherwise a file);
lvl: level of nesting of the element (the emfat_init function is needed to understand whether to attribute the element to the current directory, or to the directories above);
offset: additional offset when calling a custom callback function to read a file;
size: file size
user: this value is transferred “as is” to the user callback function for reading the file;
read: pointer to a custom callback function for reading a file.
The callback function has the following prototype:
void readcb(uint8_t *dest, int size, uint32_t offset, size_t userdata);
The address “where” to read the file (dest parameter), the size of the read data (size), the offset (offset), and userdata is passed to it.
Also in the table there is a field max_size and write. The value of max_size must always be equal to the value of size, and the value of write must be NULL.
The other two functions are emfat_write and emfat_read.
The first, as mentioned earlier, is a dummy, which, however, we call if a request to write a sector comes from the OS.
The second is the function that we must call when reading the sector. She fills in the data at the address passed to her (data), depending on the requested sector (sector).
When reading a data sector related to a file, the emfat module translates the sector number into the index of the file being read and the offset, after which it calls the user’s callback reading function. The user, respectively, gives the "piece" of a particular file. Where it comes from is not interesting to the library. So, for example, in the customer’s project, I gave the settings files from the internal flash memory, other files from RAM and spi-flash.
Example code
#include "usbd_msc_core.h"
#include "usbd_usr.h"
#include "usbd_desc.h"
#include "usb_conf.h"
#include "emfat.h"
#define AUTORUN_SIZE 50
#define README_SIZE 21
#define ICON_SIZE 1758
const char *autorun_file =
"[autorun]\r\n"
"label=emfat test drive\r\n"
"ICON=icon.ico\r\n";
const char *readme_file =
"This is readme file\r\n";
const char icon_file[ICON_SIZE] =
{
0x00,0x00,0x01,0x00,0x01,0x00,0x18, ...
};
USB_OTG_CORE_HANDLE USB_OTG_dev;
// Экземпляр виртуальной ФС
emfat_t emfat;
// callback функции чтения файлов
void autorun_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata);
void icon_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata);
void readme_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata);
// Элементы ФС
static emfat_entry_t entries[] =
{
// name dir lvl offset size max_size user read write
{ "", true, 0, 0, 0, 0, 0, NULL, NULL }, // root
{ "autorun.inf", false, 1, 0, AUTORUN_SIZE, AUTORUN_SIZE, 0, autorun_read_proc, NULL }, // autorun.inf
{ "icon.ico", false, 1, 0, ICON_SIZE, ICON_SIZE, 0, icon_read_proc, NULL }, // icon.ico
{ "drivers", true, 1, 0, 0, 0, 0, NULL, NULL }, // drivers/
{ "readme.txt", false, 2, 0, README_SIZE, README_SIZE, 0, readme_read_proc, NULL }, // drivers/readme.txt
{ NULL }
};
// callback функция чтения файла "autorun.inf"
void autorun_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata)
{
int len = 0;
if (offset > AUTORUN_SIZE) return;
if (offset + size > AUTORUN_SIZE)
len = AUTORUN_SIZE - offset; else
len = size;
memcpy(dest, &autorun_file[offset], len);
}
// callback функция чтения файла "icon.ico"
void icon_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata)
{
int len = 0;
if (offset > ICON_SIZE) return;
if (offset + size > ICON_SIZE)
len = ICON_SIZE - offset; else
len = size;
memcpy(dest, &icon_file[offset], len);
}
// callback функция чтения файла "readme.txt"
void readme_read_proc(uint8_t *dest, int size, uint32_t offset, size_t userdata)
{
int len = 0;
if (offset > README_SIZE) return;
if (offset + size > README_SIZE)
len = README_SIZE - offset; else
len = size;
memcpy(dest, &readme_file[offset], len);
}
// Три предыдущие функции можно объединить в одну, но оставлено именно так - для наглядности
// Точка входа
int main(void)
{
emfat_init(&emfat, "emfat", entries);
#ifdef USE_USB_OTG_HS
USBD_Init(&USB_OTG_dev, USB_OTG_HS_CORE_ID, &USR_desc, &USBD_MSC_cb, &USR_cb);
#else
USBD_Init(&USB_OTG_dev, USB_OTG_FS_CORE_ID, &USR_desc, &USBD_MSC_cb, &USR_cb);
#endif
while (true)
{
}
}
Also a key part of StorageMode.c module (USB MSC event processing):
int8_t STORAGE_Read(
uint8_t lun, // logical unit number
uint8_t *buf, // Pointer to the buffer to save data
uint32_t blk_addr, // address of 1st block to be read
uint16_t blk_len) // nmber of blocks to be read
{
emfat_read(&emfat, buf, blk_addr, blk_len);
return 0;
}
int8_t STORAGE_Write(uint8_t lun,
uint8_t *buf,
uint32_t blk_addr,
uint16_t blk_len)
{
emfat_write(&emfat, buf, blk_addr, blk_len);
return 0;
}
conclusions
To use Mass Storage in your project, it is not necessary to have a drive with a file system organized on it. You can use the FS emulator.
The library implements only basic functions and has a number of limitations:
- No support for long names (only 8.3);
- The name must be in lowercase Latin.
Despite the limitations, I personally have enough of the existing functionality in the projects, but, depending on the demand, in the future I admit the release of an updated version.
Project repository
Link to the project archive