Эмуляция носителя 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] =
{
	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;
}


Выводы


Для использования в своём проекте Mass Storage не обязательно иметь накопитель с организованной на нём ФС. Можно воспользоваться эмулятором ФС.

Библиотека реализовывает только базовые функции и имеет ряд ограничений:
  • Нет поддержки длинных имён (только 8.3);
  • Имя должно быть на латинице строчного регистра.

Несмотря на ограничения, лично мне в проектах имеющегося функционала хватает, но, в зависимости от востребованности, в будущем допускаю выпуск обновлённой версии.

Репозиторий проекта
Ссылка на архив проекта
Поделиться публикацией

Похожие публикации

Комментарии 36

    +1
    Спасибо за идею! А то я в подобных случаях занимался сексом с USB-HID…
      +1
      Я в свое время писал эмулятор USB Mass Storage девайса для простеньного медиаплеера. Девайс имел два USB конца: одним цеплялся к серверу а другим — к медиаплееру. Специальный демон да сервере как раз генерировал виртуальный FAT32 из файлов на сервере и передавал его концу на медиаплеере, который эмулировал USB флешку. Только все делалось на контроллере от Cypress.
        +2
        Спасибо за библиотеку! Может и пригодиться.
        Просто к слову: подобную идею эмуляции файловой системы реализуют микроконтроллеры NXP LPC134x для загрузки в них микропрограммы. Там при подключении в режиме USB-загрузчика появляется раздел с файлом firmware.bin, стерев который и записав новый образ (причем не обязательно с таким же именем), перепрошивается память микроконтроллера.
          0
          Да, читал про из бутлоадер. По-моему это даже лучше чем USB Device Firmware Upgrade, так как не требует на компьютере никаких утилит для обновления прошивки
            0
            Для использования в самоделках — может быть. Для прошивки больших партий — очень спорно, может вылезти куча других проблем.
            Да, еще интересный факт: LPC13xx содержит ошибку в загрузчике, причем так и не исправленную. Проявляется задержкой подключения раздела в 20 секунд (в Windows точно, в Linux никогда не было надобности проверять).
            +1
            Во внутрисхемном отладчике для stm32 st-link v2.1 (например, на платах stm32*-nucleo) присутствует аналогичный функционал. Кроме отладчика система видит ещё msd, который выглядит пустым, но скопированный туда файл с прошивкой заливается на контроллер.
            +2
            Очень хорошая штука для различных самодельных самописцев. Быстро и кроссплатформенно.
            А есть ли возможность при выдаче данных на ходу формировать, например, файл excel, данные для которого хранятся в компактном, сыром виде, а остальная структура формируется на лету?
              +1
              Да, так можно и даже нужно делать!
              Для этого нужно:
              — при подключении USB рассчитать (и записать в таблицу) конечный размер файла, после чего вызвать init.
              — в колбэке чтения формировать ту часть файла, которую запрашивает библиотека
              Только нужно учесть, что целевые данные не должны меняться, когда МК находится в режиме Mass Storage.
              Иначе хост прочитает часть «старых» и часть уже изменённых данных в одном файле.
              0
              Мысль нравится. А насколько сложно будет перенести на другие контроллеры? Хочу минимальный функционал, но для восьмибитного PIC'a.
                0
                Сам эмулятор, как и говорил, кроссплатформенный, сложностей возникнуть не должно. Однако драйвер USB-MSC придётся, само собой, запускать нативный для PIC-а. Если займётесь — будет интересно понаблюдать! Допускаю, что могут возникнуть проблемы в части директивы выравнивания или из-за другой специфики PIC компилятора.
                  +1
                  В Microchip Solutions уже заглядывали? Там есть демки работы с USB MSD.
                  +1
                  Потенциально весьма полезная штука.
                  А динамически изменять рапортуемый размер файла можно? Идея в том, чтоб некое приложение периодически тыркалось на «карту» посмотреть размер «файла» и если он не нулевой, считать из него новую порцию данных, после чего он снова станет нулевым до следующей порции.
                  А то на терминальных серверах зачастую USB только и умеет нормально пробрасывать мышь, клавиатуру и mass-storage. Прочие HID-девайсы и COM-порты — или совсем никак, или криво.
                    0
                    Увы, красивого способа это сделать нет. По причине кеширования ФС операционной системой. Даже если мы изменим ФС на устройстве, вряд ли есть способ сообщить об этом хосту. Повторное чтение файла с устройства с большой вероятностью будет происходить уже из кеша ФС, а не с нашего устройства.
                    Напрашивающийся выход — это, действительно, CDC интерфейс (COM-порт).
                    Если Вам понадобится стабильный CDC (или MSC+CDC) для stm32f4 — обращайтесь, было дело, переписывал stm-ый драйвер как раз по причине той самой кривости (отсутствие flow-контроля, гигантизм roll-back буффера, отсутствие обработки кратности bulk-пакета).
                      0
                      Но в API со стороны хоста же есть обычно команды сброса файлового кеша? А как файловые менеджеры в риалтайме(или почти) могут видеть изменения размеров файлов, например копируемых по сети? Или тут как раз вся фишка в наличии самой операции копирования(даже не важно, какой стороной инициированной), репорт о прогрессе которой разносится по событиям самим драйвером на стороне хоста?

                      В любом случае, COM-порт уже не интересует, спасибо. Мне проще решить проблему превращением устройства в независимое сетевое, что я и сделал )
                        0
                        Если вкратце, то нет пути засинхронизировать наше устройство с кешем ОС. Поэтому о динамике речи не идёт. Изменения ФС возможны только перед перед её монтированием.
                        Но в API со стороны хоста же есть обычно команды сброса файлового кеша?

                        Сброс файлового кеша (flush) заставит вылить на носитель отложенные для записи данные. Что нам не интересно.
                        Если же сбрасывать кеш всей файловой системы (если это возможно) — то это равносильно перемонтированию ФС или передёргиванию USB, что приведёт к гигантскому накладному расходу, т.к. драйвер ФС тот час же перечитает несколько кластеров (FAT таблица и корень). В этом случае — да, мы можем менять ФС на устройстве в динамике. Но это путь злодея.
                        как файловые менеджеры в риалтайме(или почти) могут видеть изменения размеров файлов

                        Двумя способами:
                        1. Специфическое API нотификации файловой системы.
                        2. Периодическое чтение файлового каталога с мониторингом изменений.
                        В обоих случаях работа программы происходит с драйвером ФС и его кешем, и лишь редкий вызов будет транслироваться в реальное чтение носителя.
                        решить проблему превращением устройства в независимое сетевое

                        А вот это красиво и современно )
                          0
                          Сброс файлового кеша (flush) заставит вылить на носитель отложенные для записи данные. Что нам не интересно.
                          Если же сбрасывать кеш всей файловой системы (если это возможно) — то это равносильно перемонтированию ФС или передёргиванию

                          Не, я не про это. Наверное неточно выразился. Я имел в виду что-то вроде виндовой опции FILE_FLAG_NO_BUFFERING при работе с файловой системой, или аналоги clearstatcache в PHP… В разных средах свои примочки, по разному действуют, где-то всему приложению буфферизацию отключает, где-то — на конкретный дескриптор файла, где-то вообще очень качественно не работает… Но не заставляет операционку вылить кеш всего и вся на диск.

                          Так же, как аналог F5 в проводнике или Ctrl+R в TotalCommander. Эти действия же не сбрасывают кеш всей системы ) Но перечитать текущий каталог соответствующий драйвер операционки, как правило, заставляют.

                          Собственно интерес к этому сугубо академический, т.к. превращением устройства в сетевое я решил кучу проблем, включая эту, а так же породил несколько новых )
                          Но вот такие вот извраты в копилку откладывать люблю, т.к. в определенных «специальных» случаях такие фишки могут здоровски выручить, да.
                      0
                      Возможно сделать потоковые данные например сделав файл в несколько гбайт, сколько там для фат максимум? 4? А потом переходить к следующему файлу или в следующую папку. У меня когда то была идея сэмулировать по SPI SD/MMC карту в фотике и сделать быстрое получение файлов из фотоаппарата или вообще вебкамеру, но забросил идею из за отсутствия знаний и лени.
                        +2
                        Можно вообще организовать и прямой доступ к разделу (в Windows):
                        HANDLE hFile = CreateFileA("\\\\.\\F:",
                        		GENERIC_READ | GENERIC_WRITE,
                        		FILE_SHARE_READ | FILE_SHARE_WRITE,
                        		NULL, 
                        		OPEN_EXISTING,
                        		FILE_FLAG_WRITE_THROUGH | FILE_FLAG_NO_BUFFERING,
                        		NULL);
                        DeviceIoControl(hFile, FSCTL_LOCK_VOLUME, NULL, 0, NULL, 0, &dwLen, NULL);
                        SetFilePointer(hFile, dwOffset, &dwOffsetH, 0);
                        ReadFile(hFile, pbFileBuff, dwLen, &dwLenR, 0);
                        

                        Запросы ReadFile/WriteFile при этом идут как непосредственные запросы SCSI_READ/WRITE в USB устройство, в обход кеша ОС. Адрес чтения соответсвует физ. адресу сектора.
                          0
                          И в обход драйвера ФС(FAT)?
                            0
                            Да, именно в обход драйвера файловой системы.
                            0
                            Это, действительно, может быть пригодно. Можно иметь специальный номер сектора для динамического обмена данными, хоть это всё и от нищеты.
                          +1
                          Так же, как аналог F5 в проводнике или Ctrl+R в TotalCommander. Эти действия же не сбрасывают кеш всей системы ) Но перечитать текущий каталог соответствующий драйвер операционки, как правило, заставляют

                          Заставляют. Только перечитать не физически с диска, а из кеша ФС. Этот кеш содержит набор ранее прочитанных с устройства кластеров. Перечитывать эти кластеры с носителя повторно ОС объективно считает накладным.
                          Повторно она прочитает кластер с носителя, только если он был исключён из кеша в целях экономия памяти, а пользователю снова понадобятся эти данные. При чём, что интересно, на этом уровне ОС не делает различий между кластерами относящимися к файлу или же к структуре каталога.
                          Есть такое наблюдение: первое копирование достаточно большого файла/каталога с носителя происходит за минуту, повторное копирование — уже за еденицы секунд при отсутствии на устройстве запросов чтения. Секрет кроется в подсосе из кеша и хитром драйвере :)
                          Вообще, если получится ОС заставить перечитывать физически кластеры файла (и его содержащего каталога) каждый раз при обращении — это будет полезно для нас. Если у Вас получится — буду рад опыту ;)
                            +3
                            Под какой лицензией вы опубликовали свой проект?
                              0
                              GNU GPLv3.
                              Собственно хорошо, что напомнили сей факт отметить в шапке исходников, было упущено из виду.
                                +2
                                Даже не LGPL?
                                  0
                                  Здравствуйте. Раньше не задавался вопросом лицензий, т.к. ничего не публиковал.
                                  Я так понял, что LGPL не заставляет открывать производные исходники? Если так, то это, действительно, больше подходит.
                                  Публикация под LGPL не требует получения соответствующего разрешения?
                                    +1
                                    LGPL здесь тоже не очень подходит, т. к. линкуется статически. Хотя она допускает такой вариант, это приведет к необходимости предоставить пользователю конечной программы, содержащей эту библиотеку, как минимум объектные файлы и скрипты сборки, чтобы пользователь имел возможность перекомпоновать с иной версией библиотеки.

                                    Иногда встречается варианты GPL со static linkage exception и другими оговорками. См., например, вариант GPL with exceptions для FreeRTOS. Там довольно подробно описывается, какие файлы считаются частью производной работы, а какие — нет (в частности, использование макросов не делает файл частью производной работы).

                                    В общем, если не хочется таких заморочек и вы пишите маленькую библиотеку, то удобнее использовать пермиссивные лицензии типа MIT/X11, Apache, 2-clause BSD и т. п.

                                    Для общего представления рекомендую на choosealicense.com/. Там есть наглядные и краткие таблички по разным лицензиям.
                                      +3
                                      Отлично. Товарищ мне также посоветовал остановиться на MIT.
                                      Обновил шапку, большое спасибо за информацию!
                                        +2
                                        Спасибо! Пермиссивные лицензии для небольших проектов — максимальное переиспользование кода другими людьми.
                              0
                              Вспомнился пост:
                              habrahabr.ru/post/134432/

                              Тут товарищ на erlang запилил эмуляцию fat
                                0
                                А что, если написать эмуляцию протокола MTP? Там ОС допускает изменение файла, пока устройство подключено.
                                  0
                                  Как же мне он не нравится, сколько я мучался с плеером и смартфоном, хотя протокол имеет место быть.
                                    0
                                    Да, смотрел с самого начала в сторону MTP. Увы, он очень накручен и позволяет работать только с медиа-файлами (картинки, аудио, видео). Бинарник гонять по USB уже нельзя. Хотя сам концепт файл-обмена с «интеллектуальным» устройством — это, казалось бы, то что нужно.
                                    +2
                                    Весьма! Может пригодится, утянул в избранное.
                                      +1
                                      А вы не должны теперь отчислить денежек Microsoft'у за использование их технологии FAT32?
                                        0
                                        Оно же не exFAT, там должно было уже всё протухнуть, не? exFAT же не спроста появился…

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое