Pull to refresh

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

Reading time 6 min
Views 53K


Недавно возникла данная задача — эмуляция носителя 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);
  • Имя должно быть на латинице строчного регистра.

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

Репозиторий проекта
Ссылка на архив проекта
Tags:
Hubs:
+70
Comments 36
Comments Comments 36

Articles