Недавно возникла данная задача — эмуляция носителя FAT32 на stm32f4.
Её необычность заключается в том, что среди обвязки микроконтроллера вовсе может не быть накопителя.
В моём случае накопитель был, но правила работы с ним не позволяли разместить файловую систему. В ТЗ, тем не менее, присутствовало требование организовать Mass Storage интерфайс для доступа к данным.
Результатом работы явился модуль, который я озаглавил «emfat», состоящий из одноимённого .h и .c файла.
Модуль независим от платформы. В прилагаемом примере он работает на плате stm32f4discovery.
Функция модуля — отдавать куски файловой системы, которые запросит usb-host, подставляя пользовательские данные, если тот пытается считать некоторый файл.
Кому может быть полезным
В первую очередь — полезно в любом техническом решении, где устройство предлагает Mass Storage интерфейс в режиме «только чтение». Эмуляция FAT32 «на лету» в этом случае позволит хранить данные как Вам угодно, без необходимости поддерживать ФС.
Во вторую очередь — полезно эстетам. Тому кто не имеет физический накопитель, но хочет видеть своё устройство в виде диска в заветном «Мой компьютер». В корне диска при этом могут находиться инструкция, драйверы, файл с описанием версии устройства, и пр.
В этом случае, нужно заметить, вместо эмуляции носителя, можно отдавать хосту части «вкомпиленного» слепка преподготовленной ФС. Однако в этом случае, вероятнее всего, расход памяти МК будет существенно выше, а гибкость решения — нулевая.
Итак, как это работает.
При попытке пользователя прочитать или записать файл, соответствующий вызов транслируется в usb-запросы, которые передаются нашему устройству. Суть запросов проста — записать или считать сектор на конечном носителе.
При этом, надо отметить, винда (или другая ОС) ведёт себя как хозяйка в плане организации хранения на носителе. Только она знает какой сектор хочет считать или записать. А захочет — и вовсе дефрагментирует нас, устроив хаотичное «жанглирование» секторами… Таким образом, функция типового USB MSC контроллера — безропотно вылить на носитель порцию в 512 байт со сдвигом, или считать порцию.
Теперь вернёмся к функции эмуляции.
Сразу предупрежу, мы не эмулируем запись на носитель. Наш «носитель» только для чтения.
Это связано с повышенной сложностью контроля за формированием файловой таблицы.
Тем не менее, в API модуля присутствует функция-пустышка emfat_write. Возможно, в будущем будет найдено решение для корректной эмуляции записи.
Задача модуля при запросе на чтение — «отдать» валидные данные. В этом и состоит его основная работа. В зависимости от запрашиваемого сектора, этими данными могут являться:
- Запись MBR;
- Загрузочный сектор;
- Один из секторов файловой таблицы FAT1 или FAT2;
- Сектор описания директории;
- Сектор данных, относящийся к файлу.
Надо отметить, что на ускорение принятия решения «какие данные отдать» был сделан акцент. Поэтому накладные расходы были минимизированы.
Из-за того что мы отказались от обслуживания записи на накопитель, мы вольны организовать структуру хранения, как нам захочется:
Всё совершенно стандартно, кроме нескольких деталей:
- Данные не фрагментированы;
- Отсутствуют некоторые ненужные области FAT;
- Свободных кластеров нет (размер носителя «подогнан» под размер данных);
- Размер FAT-таблиц также «подогнан» под размер данных.
Естественно, нужно понимать, что данная структура мнимая. В реальности она не содержится в оперативной памяти, а формируется соответствующим образом, в зависимости от номера читаемого сектора.
API модуля
API составлен всего из трёх функций:
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);
Из них главная функция — emfat_init.
Её пользователь вызывает один раз — при подключении нашего usb-устройства или на старте контроллера.
Параметры функции — экземпляр файловой системы (emfat), метка раздела (label) и таблица элементов ФС (entries).
Таблица задаётся как массив структур emfat_entry_t следующим образом:
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 }
};
Следующие поля присутствуют в таблице:
name: отображаемое имя элемента;
dir: является ли элемент каталогом (иначе — файл);
lvl: уровень вложенности элемента (нужен функции emfat_init чтобы понять, отнести элемент к текущему каталогу, или к каталогам выше);
offset: добавочное смещение при вызове пользовательской callback-функции чтения файла;
size: размер файла;
user: данное значение передаётся «как есть» пользовательской callback-функции чтения файла;
read: указатель на пользовательскую callback-функцию чтения файла.
Callback функция имеет следующий прототип:
void readcb(uint8_t *dest, int size, uint32_t offset, size_t userdata);
В неё передаётся адрес «куда» читать файл (параметр dest), размер читаемых данных (size), смещение (offset) и userdata.
Также в таблице присутствует поле max_size и write. Значение max_size всегда должно быть равным значению size, а значение write должно быть NULL.
Остальные две функции — emfat_write и emfat_read.
Первая, как говорилось раньше, пустышка, которую, однако, мы вызываем, если от ОС приходит запрос на запись сектора.
Вторая — функция, которую мы должны вызывать при чтении сектора. Она заполняет данные по передаваемому ей адресу (data) в зависимости от запрашиваемого сектора (sector).
При чтении сектора данных, относящегося к файлу, модуль emfat транслирует номер сектора в индекс читаемого файла и смещение, после чего вызывает пользовательскую callback-функцию чтения. Пользователь, соответственно, отдаёт «кусок» конкретного файла. Откуда он берётся библиотеке не интересно. Так, например, в проекте заказчика, файлы настроек я отдавал из внутренней flash памяти, другие файлы — из ОЗУ и spi-flash.
Код примера
#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]rn"
"label=emfat test drivern"
"ICON=icon.icorn";
const char *readme_file =
"This is readme filern";
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)
{
}
}
Также ключевая часть модуля StorageMode.c (обработка событий USB MSC):
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;
}
int8_t STORAGE_GetMaxLun(void)
{
return STORAGE_LUN_NBR - 1;
}
Выводы
Для использования в своём проекте Mass Storage не обязательно иметь накопитель с организованной на нём ФС. Можно воспользоваться эмулятором ФС.
Библиотека реализовывает только базовые функции и имеет ряд ограничений:
- Нет поддержки длинных имён (только 8.3);
- Имя должно быть на латинице строчного регистра.
Несмотря на ограничения, лично мне в проектах имеющегося функционала хватает, но, в зависимости от востребованности, в будущем допускаю выпуск обновлённой версии.
Демонстрационный проект можно скачать здесь.
Автор: fse