[Из песочницы] Эмуляция носителя FAT32 на stm32f4
Недавно возникла данная задача — эмуляция носителя 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]\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] = { 0×00,0×00,0×01,0×00,0×01,0×00,0×18, … };
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); Имя должно быть на латинице строчного регистра. Несмотря на ограничения, лично мне в проектах имеющегося функционала хватает, но, в зависимости от востребованности, в будущем допускаю выпуск обновлённой версии.Демонстрационный проект можно скачать здесь.