Notice: Memcache::get(): Server localhost (tcp 11211) failed with: Connection refused (111) in /home/a146/www/we.easyelectronics.ru/engine/lib/external/DklabCache/Zend/Cache/Backend/Memcached.php on line 134
Эмуляция носителя FAT32 на stm32f4 / STM32 / Сообщество EasyElectronics.ru

Эмуляция носителя FAT32 на stm32f4


Некоторое время назад возникла данная задача — эмуляция носителя FAT32 на stm32f4.
Её необычность заключается в том, что среди обвязки микроконтроллера вовсе может не быть накопителя, вроде FLASH-контроллера или SD-карты.
В моём случае накопитель был, но правила работы с ним не позволяли разместить файловую систему. В ТЗ, тем не менее, присутствовало требование организовать 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);
  • Имя должно быть на латинице строчного регистра.
Несмотря на ограничения, лично мне в проектах имеющегося функционала хватает, но, в зависимости от востребованности, в будущем допускаю выпуск обновлённой версии.
Исходный код хранится в репозитории проекта и доступен для всех заинтересовавшихся на правах MIT-лицензии.
  • +13
  • 15 марта 2016, 03:02
  • fsenok

Комментарии (39)

RSS свернуть / развернуть
спасибо. как то давненько задумывался об идее перепрошивки через MSD. почему то идея была забыта. спасибо что напомнил.
+1
  • avatar
  • xar
  • 15 марта 2016, 08:19
На самом деле библиотека поддерживает запись и на её базе я делал перешивку. Создаётся нулевой файл firmware.bin с maxsize равным размеру флеш-памяти и ловятся callback-и записи при перезаписи файла. Но это в известной степени хак — увы, существует 100500 действий пользователя при котором система разваливается. Поэтому от затеи открещиваюсь и в статье указываю «носитель только для чтения». Увы, есть сомнения, что такой режим перешивки можно сделать надёжным (не как у известных мне производителей).
0
Самая главная причина: MSC бутлоадер не зашит в ROM производителем, значит, всегда есть вероятность устройство окирпичить. Даже шить придется «на лету» — объемы оперативки у STM32 в разы меньше, чем флеши.
0
работа с флеш у стм32 довольно гибкая и ошибки практически исключены. зашитый бутлоадер я не использую в принципе по причине убогости. да хрен с ней с убогостью. там нельзя обеспечить никакую защиту прошивки. в коммерческих девайсах это важно.
0
Были мысли о реализации CLI, добавив в SCSI протокол дополнительные команды. И драйвер не нужен и скорость нормальная. Пока руки не дошли.
0
Что такое CLI? Известные мне расшифровки не подходят.
0
0
Может, эмулировать CDROM вместо съемного носителя? Тогда вопросов о записи не возникнет у системы.
0
Многие модемы известной компании Ху… вей так и поступают — в прошивку заливается образ компакт диска, который потом эмулируется. Устройство же определяется или как ком порт или как сетевая карта и в дополнение как сидиром со вставленным диском.
0
Так вроде нет никаких проблем с RO накопителями, я даже обычные USB флешки переводил в такой режим, многие контроллеры это позволяют.
0
Это сделать не сложно, были такие мысли. Но остановил тот факт, что ёмкость CD будет произвольной вместо стандартной. Плюс flash-ка, вероятно, привычней для пользователя, чем usb-дисковод. Поэтому показалось правильным представиться флешкой и указать режим read-only на этапе инициализации MSC-устройства.
0
что ёмкость CD будет произвольной вместо стандартной.
никаких проблем. «стандартный» объем вещь весьма относительная. каждый диск имеет свой объем и 0 свободного.
0
Это еще не считая нестандартных форматов самого диска, начиная с 30-метровой «визитки».
0
0
Нда. У меня CLI в этом значении и SCSI как-то не ассоциируются)
0
Да, вы правы. Я имел ввиду протокол обменна данных и команд, хз правильно как правильно называется.)
0
Видел подобную реализацию в неоригинальных автомобильных приёмниках цифрового радио DAB. Когда приёмник вставляется в USB-порт автомагнитолы, которая видит файловую систему с названиями радиостанций в виде файлов с расширением mp3. А при попытке проиграть такой файл — запускается приём станции которая и отдаётся магнитолев виде файла. Работает такая связка на очень большом количестве моделей авто, поэтому её можно считать практически универсальной.
0
а поток при этом на лету в приемнике перекодируется?
0
На сколько я понимаю — да. Сам я не держал в руках подобную вещь, но при начале воспроизведения слышно голосовое меню «Please Wait», поэтому думаю что они конвертят поток в мп3 на лету.
0
Было бы интересно изучить. Но думаю, что не совсем так обстоит дело — файловая система подразумевает наличие файлов известного размера, а при потоковой передаче он неизвестен. Поэтому вероятно MP3 или m3u файл содержит ссылку на канал потоковой передачи пососедству, по которой проигрыватель и начинает воспроизведение.
0
Хинт в том, что ставится максимальный размер файла, возможный для фат32. Проигрыватель играет файл до получения eof. Если eof не приходит, то файл продолжает проигрываться. Примерно так-же, как можно начать просмотр фильма на компьютере, когда этот фильм ещё не полностью докачался. Главное — возвращать «0» при вызове STORAGE_Read()
0
Да и при вызове readcb() в таком варианте запрашиваемый offset просто игнорируется.
0
Может быть вы и правы — техническая возможность этого есть. Представились 2 сопутствующие проблемы:
1. особенность драйвера ФС читать ФС покластерно. Т.е. 2 Кб за раз на 2 Гбайтном носителе. Или 4 Кб на больших по объёму носителях, что скорее всего наш случай. То есть запаздывание в звучании будет минимум 4 Кб, что соответствует заметной длительности сжатого аудиосигнала.
2. специфика проигрывателя. Иной проигрыватель перед воспроизведением может захотеть считать весь файл или текущий блок в 1 Мб, а это исключительная ситуация.
Но вообще, идея интересная и её не сложно проверить!
0
Да, там видно что оно тупит поначалу (кеширует):


Но потом начинает играть.
0
обеспечивать буфер с запасом. можно прогнозировать размер следующего блока по размеру предыдущего. вариантов тьма. целиком файл читать не будет ибо там может быть аудиокнига на пол гига.
0
2. специфика проигрывателя. Иной проигрыватель перед воспроизведением может захотеть считать весь файл или текущий блок в 1 Мб, а это исключительная ситуация.
Это маловероятно. Сам MP3-файл поделен на фреймы довольно небольшого размера (сотни байт), закодированные независимо, так что кэшировать большой кусок смысла нет.
0
а поток при этом на лету в приемнике перекодируется?
зачем? поток может быть изначально в мрз )
0
Нет, он или в AAC960 или MP2, вероятность что оба формата будут нормально проигрываться на автомагнитоле — стремится к нулю, поэтому и перекодируют.
0
понял.
0
0
Да, и этот блютус-брелок работает по тому-же принципу.
0
+1
Авторский.
0
Ясно. Отличная идея, читал год назад.
0
Спасибо, рад поделиться!
0
Поставил бы еще один + за github
0
  • avatar
  • x893
  • 15 марта 2016, 13:30
Спасибо автору за работу, использовал его библиотеку в своем проекте: geektimes.ru/post/262512/
Оказалось, что если файлов на «диске» мало, то лучше эмулировать FAT16.
+2
С интересом прочитал статью на geektimes! Оригинальная идея и отличная реализация! Взял на заметку информацию о проблеме с эмуляцией малых накопителей. В текущей версии виртуальный файл можно не создавать, а у одного из файлов указать maxsize == 1 Гб, это должно решить проблему — зарезервирует место после файла. Но совершенно точно, что разруливание этой неприятности стоит спрятать от пользователя. Спасибо за ссылку и ваш опыт! Если захотите скооперироваться по исходникам (склеить версии fat16 и fat32 с указанием совместного авторства) — пишите в личку, приветствую ;)
0
Может найти применение в такой визитке Your text to link..., где все контакты и примеры работ будут хранится на read-only флешке.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.