Эмуляция носителя 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-лицензии.

 

источник:  we.easyelectronics.ru