USB на регистрах: виртуальная FAT16

image
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage

Уже довольно давно я пытался разобраться, как же устроена классическая файловая система FAT и вот наконец критическая масса обрывочных сведений в моей голове привела к качественному скачку и закономерному воплю «а что, все действительно настолько просто?!». Нет, разумеется, в FAT полно причудливых костылей, наросших за время ее эволюции, но сама идея и правда проста. Настолько, чтобы реализовать ее эмуляцию на контроллерах вроде stm32f103, stm32l151 в достаточном для ряда задач объеме. То есть наше устройство будет прикидываться флешкой смешного объема, запись и чтение которой будут не приводить к перезаписи памяти, а обрабатываться исключительно кодом.


Возможности и ограничения

В первую очередь стоит отметить ряд чисто технических ограничений. Самое главное — хост (компьютер) твердо уверен, что данные на подключенной флешке меняет только он. Это значит, что отдавать данные в реальном времени не получится: хост просто не поверит, что они есть. Зато можно в момент подключения «заморозить» данные и отдавать уже их. Далее, единицей информации в протоколе USB-MSD (см. также соответствующую статью) является сектор, то есть блок 512 байт. Все, что меньше, добивается «мусорными нулями» до сектора, все, что больше — передается за несколько запросов. Таким образом (особенно с учетом фрагментирования, о котором ниже) мы на уровне устройства не можем гарантировать целостность данных внутри одного файла. Хост может записать сначала 2-й сектор, потом 10-й, а потом сказать, что к нашему файлу относятся только 3 и 6, тогда как 2 и 10 это System volume information или еще какой-то бесполезный мусор в том же роде. Простейший способ это обойти — использовать файлы размером менее 512 байт, плюс запретить их перемещение. Впрочем, к read-only файлам это не относится: мы заранее задаем им размер и диапазон секторов.
Таким образом, библиотека будет эмулировать флешку с файловой системой FAT16 с фиксированным набором файлов, часть из которых доступна только на чтение (как результат — попытка записи может привести операционку в ступор), зато неограниченного объема (в пределах возможностей FAT, то есть до ~32 МБ), а часть — на чтение и запись, но не более 512 байт. Первые можно применить для отображения всяческих логов, вторые — для конфигурации. Каталоги и вложенные каталоги не поддерживаются. Создание новых файлов или каталогов средствами ОС не поддерживается. Скорость доступа ограничивается железом и составляет ~600 кБ/с, если применить двойную буферизацию, можно чуть-чуть поднять, но ненамного.


Внутреннее устройство FAT

Для начала напомню, что FAT это целое семейство файловых систем: FAT12, FAT16, FAT32 и FAT64 (exFAT). Отличаются они, как несложно догадаться, количеством битов, отводимых на адресацию памяти — от 12 до 64. Из них FAT32, а тем более FAT64 для контроллера явный перебор: ему физически негде хранить столько данных. FAT12 наоборот, подошла бы хорошо, но задача разбиения байтовой сетки по 12-битным переменным мне показалась противоестественной и дурацкой. Поэтому будет FAT16. То, что нам не нужно поддерживать все семейство и все опции форматирования, существенно упрощает задачу. Мы можем сразу «отформатировать» флешку именно так, как нам удобно и поддерживать единственный вариант. Далее речь пойдет исключительно про FAT16 именно с таким форматированием. У других файловых систем имеются свои особенности, как например отсутствие жестко заданного корневого каталога в FAT32.
Итак FAT16 состоит из 4 идущих подряд областей: PBR (загрузочный сектор), fat (таблица цепочек секторов), root_dir (корневой каталог) и data (собственно данные). Рассмотрим их подробнее:


Загрузочный сектор PBR

Загрузочный сектор PBR (partition boot record) представляет собой фиксированную структуру следующего вида:

#pragma pack(push, 1)
typedef struct __attribute__((__packed__)){
  uint8_t JmpBoot[3];  //{0xEB, 0x3C, 0x90}
  uint8_t OEMname[8];  //имя программы, создавшей ФС
  uint16_t BytesPerSec;//512
  uint8_t SecPerClust; //1
  uint16_t SecReserved;//1
  uint8_t NumFats;     //1
  uint16_t RootEntCnt; //кратно 16
  uint16_t TotSect;    //от 0x1000 до 0x10000
  uint8_t DriveType;   //0xF8 - HDD, 0xF0 - 0xFF - различные флоппики
  uint16_t fatsize;    //(TotSect / BytesPerSec / SecPerClust * 2)
  uint16_t SecPerTrak; //0x0020 ?
  uint16_t NumHeads;   //0x0040 ?
  uint32_t SecHidden;  //0
  uint32_t TotSect32;  //0

  uint8_t DriveNum;    //0x80 ?
  uint8_t NTErrFlag;   //0
  uint8_t BootSig;     //0x29 ?
  uint32_t VolID;      //по сути, UUID раздела, берем любое
  char VolName[11];    //имя раздела (часте6нько игнорируется операционками)
  char FSName[8];      //"FAT16   " и только так!
  uint8_t reserved[448];
  uint16_t EOS_AA55;   //контрольное значение 0xAA55 (в little-endian)
}fat_pbr_t;
#pragma pack(pop)

Поля SecPerTrak (количество секторов на дорожку), NumHeads (количество головок), DriveNum (номер привода), похоже, являются костылями времен, когда операционка была вынуждена сама следить за дорожками, головками и цилиндрами, не полагаясь на мозги жесткого диска. В современных жестких дисках, а тем более флешках этих понятий нет, так что возьмем из какого-нибудь чужого FAT’а и не будем обращать внимание.
Поля JmpBoot и BootSig, похоже, как-то связаны с загрузкой системы с такого носителя, но несмотря на то, что грузиться с такой FAT явно никто не будет, выставить их в ноль нельзя — win10 (и, наверное, более новые тоже) от этого сходят с ума и считают флешку бракованной. Опять копируем у донора.
reserved[448]. Собственно загрузочный код. А чаще — сообщение, что отсюда грузиться нельзя. К счастью, можно оставить эти байты нулями, что избавляет от необходимости их хранить.
Теперь о «косметических» полях, которые отвечают за представление флешки в системе, но не влияют ни на что больше. Это различные имена и сигнатуры (OEMname, VolName, FSName, VolID, DriveType). Наверное, их можно и не задавать, но лучше уж сделать по-нормальному.
SecReserved — количество служебных секторов перед FAT. Сюда относятся сам PBR и некоторые информационные, которые могут быть, а могут не быть. В нашем случае есть только PBR, поэтому оставляем 1.
И наконец важные поля:
BytesPerSec. Количество байтов на сектор. Поскольку USB-MSD в любом случае использует 512-байтные посылки, здесь делаем так же.
SecPerClust. Вообще говоря, в FAT полезные данные, в отличие от служебных, хранятся не в секторах, а в кластерах — группах секторов. То есть области PBR, fat, root_dir адресуются в секторах, а data — в кластерах. Но мы для простоты эти два понятия смешаем: кластер всегда будет состоять из одного сектора.
NumFats. Количество копий таблицы FAT. Обычно делается две копии чтобы, если что, можно было файловую систему восстановить. Но у нас файловая система виртуальная, и если она сломается, так просто будет не починить. Так что обойдемся одной таблицей.
RootEntCnt. Количество файлов в корневом каталоге. В нашем случае оно задано изначально. Но область корневого каталога должна быть кратна размеру сектора. А размер одного описателя файла (элемент каталога) равен 32 байтам. Более подробно его структура будет рассмотрена ниже, пока же достаточно размера. Таким образом, для кратности сектору надо чтобы количество элементов каталога было кратно (512/32 = 16). Даже если у нас 3 файла, придется выделить все 16. Это делается с помощью простенькой битовой магии:

#define VIRFAT_ROOTENT (( VIRFAT_FILES_TOTAL + 15 ) &~ 15)

TotSect, TotSect32. Вероятно, опять костыли для совместимости. Когда 16 битов на хранение количества секторов перестало хватать, выделили еще одно поле, побольше. Согласно стандарту, пользоваться 32-битным полем надо только если в 16-битное записан 0. Но нам такие большие флешки без надобности, поэтому устанавливаем TotSect32=0. А вот в TotSect уже надо записать осмысленное значение. Согласно стандарту, количество секторов это единственный способ узнать разрядность FAT’а: пока помещается в 12 бит (0 — 0×0FFF) это FAT12, пока помещается в 16 бит (0×0FFF — 0xFFFF) — FAT16 и так далее. На самом деле там еще должны учитываться зарезервированные сектора и все такое, но для прикидки на пальцах сойдет. Причем выбирать количество секторов, близкое к пороговому значению, не стоит, чтобы не вводить в ступор некорректно написанные драйвера. Поскольку мы используем FAT16, размер должен быть не меньше 0×0FFF, но лучше взять с запасом (все равно ведь хранить не будем) — 0×2000. Разумеется, если суммарный размер файлов будет больше, то и количество секторов придется увеличить.
fatsize. Учитывая, что это значение должно соответствовать количеству секторов, странной выглядит необходимость его хранить. Как бы то ни было, количество элементов FAT равно количеству кластеров (TotSect / SecPerClust), а размер одного элемента определяется типом FAT (в нашем случае 2 байта), откуда размер легко вычисляется: (TotSect / BytesPerSec / SecPerClust * 2). Не забываем, что размер задается не в байтах, а в секторах, отсюда и деление на BytesPerSec.
EOS_AA55. Контрольное значение, показывающее, что это не просто рандомный кусок данных, а именно служебный сектор. Всегда равно 0xAA55 и всегда занимает последние два байта сектора.
Полный размер данной структуры составляет 512 байт, то есть ровно один сектор, причем нулевой. По идее, он создается единственный раз при форматировании, а потом не меняется. Правда, наблюдение показало, что реальная операционная система зачем-то пытается его перезаписать, причем им же самим. Какой в этом смысл, мне абсолютно непонятно. Не исключено даже что это может увеличивать износ реальных флешек.


Таблица FAT

Таблица FAT представляет собой таблицу номеров, каждый из которых привязан к «своему» сектору и указывает на следующий в цепочке. Условно это можно записать так:

uint16_t fat[ TotSect32 ];
uint4096_t sector[ TotSect32 ]; //такая вот 4096-битная (512-байтная) "ячейка"

То есть если мы знаем, что файл хранится в 42, 43 и 111 секторах, то сначала мы читаем sector[42] чтобы получить данные, потом fat[42] чтобы получить адрес следующего сектора. В данном примере там будет храниться число 43. Следовательно, дальше мы читаем sector[43] и fat[43], в котором будет число 111. Читаем sector[111] и fat[111], но дальше файл закончился и по цепочке идти не надо. Чтобы это обозначить, используется специальный диапазон кодов 0xFFF8 — 0xFFFF (чем руководствоваться при выборе одного из них мне неизвестно, поэтому пусть концом цепочки всегда будет 0xFFFF). Если же сектор не занят вообще никаким файлом, его помечают кодом 0×0000. Есть и еще один специальный код — 0xFFF7, обозначающий, что данный сектор неисправен и пользоваться им нельзя.
И это замечательный код! Объем виртуального носителя не может быть меньше 0×2000 секторов, то есть ~4 МБ. Но в той же stm32f103 всего 20 кБ, то есть при всем желании обслужить всю файловую систему честно мы не можем. Но можем все «лишние» сектора пометить битыми и реагировать на попытку работы с ними соответственно. Ну да, вот такая «полудохлая» флешка будет, полтора живых сектора и тонна мертвых, но кого это волнует!
Начинается эта таблица с 1 сектора, а занимаемый размер несложно вычислить, зная размер элемента FAT (у нас это 16 бит) и их количество:

((sectotal * 2 + 511) / 512) //+511 нужно для округления в большую сторону

Отступление из соображений универсальности. В общем случае зарезервирован может быть не только сектор PBR, но и еще некоторые. Если вам нужен универсальный алгоритм, область FAT начинается не с 1, а со значения SecReserved.
Разрядность значений fat равна разрядности файловой системы, как и спецзначения. Так, за битый сектор в FAT12 будет отвечать значение 0×0FF7, а в FAT32 — 0xFFFFFFF7.


Корневой каталог

Любой каталог, включая корневой, представляет собой массив 32-байтных записей со следующей структурой:

#pragma pack(push, 1)
typedef struct __attribute__((__packed__)) {
  char name[11];       //имя файла в DOS-формате, т.е. 8.3 и капсом, это важно
  uint8_t dir_attr;    //атрибуты: скрытый, архивный, каталог, ...
  uint8_t NTattr;      //не знаю что это
  uint8_t create_time_10ms; //время создания с точностью до 10 мс (часто игнорируется)
  uint16_t create_time_s; //время создания с точностью 2 с
  uint16_t creae_date;    //дата создания
  uint16_t acc_date;      //дата последнего доступа (даже чтения)
  uint16_t cluster1_HI;   //=0, старшие 2 байта адреса 1-го кластера
  uint16_t write_time;    //время последней записи
  uint16_t write_date;    //дата последней записи
  uint16_t cluster1_LO;   //младшие 2 байта адреса 1-го кластера
  uint32_t size;          //размер в байтах
}dir_elem;
#pragma pack(pop)

И вот это уже просто карнавал костылей! Хотя пока скромный, «деревенский». Максимум извращения будет при попытке в рамках FAT реализовать длинные имена файлов (long file names, LFN). Я этого реализовывать не буду, но со стороны покажу.
Так вот, разработчики зачем-то разделили время создания файла на сотые доли секунды и двойки секунд. Причем некоторые драйвера с этими полями работают криво, но тут уж не к разработчикам FAT претензия…
Зачем-то разделили адрес первого кластера на два 16-битных поля (нам-то ладно, все равно вся адресация 16-битная, зануляем cluster1_HI и дело с концом), но в полноценном драйвере склеивать два куска это отдельное развлечение.
Размер файла задается одной 32-битной переменной. Отсюда и растут ноги легендарного ограничения в 4 ГБ на один файл. А ведь исправить это было бы элементарно: считать размер по занятым кластерам плюс явно хранимый «хвост» — сколько байт последнего кластера занято. Тем более что размер каталогов считается именно так.
В полях даты под год выделено всего 7 бит, а счет времени ведется от 1980 года. Переполнение надвигается! Осталось всего 85 лет.
Ну и знаменитый формат 8.3, куда ж без него. Разумеется, современный FAT поддерживает нормальные имена файлов. Ну, почти нормальные… Для хранения вместо человеческого utf-8 зачем-то воткнули utf-16. Впрочем, использование неудачной кодировки это еще не самое веселое. Длинное имя в FAT организовано как еще одна или несколько записей в каталоге, то есть это «файлы» со странным форматом!

#pragma pack(push, 1)
typedef struct __attribute__((__packed__)) {
  uint8_t ord;
  uint16_t name1[5];
  uint8_t attr;
  uint8_t dtype;        //0
  uint8_t checksum;
  uint16_t name2[6];
  uint16_t cluster1_LO; //0
  uint16_t name3[2];
}lfn_elem;
#pragma pack(pop)

Подробно расписывать формат я не буду, обращу только внимание, что внутри одной структуры имя приходится складывать из ТРЕХ частей, плюс не забываем, что оно может занимать более 13 символов, тогда таких записей будет не одна, а несколько, и их тоже придется складывать. И не забываем про оригинальное имя 8.3, которое тоже никуда не девается. На фоне этого неиспользуемые атрибуты, оставленные для совместимости с обычными файлами, выглядят уже почти нормально…
Как бы то ни было, из соображений экономии памяти (и опасения за целостность мозга), длинные имена в данной библиотеке поддерживаться не будут.
Первый сектор этой области равен (1 + ((sectotal * 2 + 511) / 512)), а чтобы узнать размер, берем количество элементов и делим их по количеству, помещающемуся в секторе: (VIRFAT_ROOTENT / 16).
Отступление в сторону FAT32. Там выделенного корневого каталога нет. Соответственно, нет и описанной здесь области. Вместо этого в PBR введено поле адреса первого сектора корневого каталога. То есть он теперь ничем не выделяется из всех остальных каталогов, и это гораздо удобнее.


Область данных

Вот тут никакой особой структуры нет. Адресация идет посекторно, номера секторов 0 и 1 зарезервированы под какие-то другие цели и в таблице FAT обозначаются обычно как 0xFFF8 и 0xFFFF. Еще рекомендуется второй сектор отдать под «файл метки тома». По сути, это обычный файл нулевого размера, обладающий атрибутом dir_attr = VolID (0×08) и чье имя (8.3 разве что без точки) отображается как метка всей флешки в целом. В списке файлов он, разумеется, не виден.
Начинается она, очевидно, сразу после dir_root и доходит до конца диска.
Отступление в сторону FAT32. Поскольку корневого каталога нет, поле RootEntCnt равно 0, и слагаемого (VIRFAT_ROOTENT/16) там не будет.


Таблица адресов областей

, где значение fatN — количество байт на адрес. Для fat16 оно равно 2, для fat32 — 4. Вся адресация идет в секторах.


Демо-код анализа существующих FAT16

То, с чем я дольше всего возился при расковыривании FAT — по каким же адресам какая область расположена и какие параметры на это влияют. Чтобы визуализировать это более наглядно, я написал простенькую (~200 строк) программу, которая читает произвольный образ файловой системы с FAT16 и показывает адреса важных областей. Дополнительно можно тот же образ открыть в шестнадцатеричном редакторе вроде okteta или хотя бы hexdump и сравнить содержимое адресов. Важно, что эта программа работает только с FAT16 и только с образом отдельной файловой системы. Образ целого диска со всякими MBR, тремя основными разделами и кучей расширенных — мимо.
Создать образцовый образ диска, чтобы сравнивать с ним нашу виртуальную флешку, можно простой парой команд:
Создаем файл образа минимально допустимого для FAT16 размера 4 МБ:

$ dd if=/dev/zero of=fat16.img bs=1k count=4096

Создаем на нем файловую систему FAT16 (-F 16) с единственной копией таблицы FAT (-f 1) с единственным зарезервированным сектором (-R 1) с максимальным количеством файлов в корневом каталоге равным 16 (-r 16) и размером кластера 512 байт или 1 сектор (-s 1):

# mkfs.vfat -f 1 -F 16 -n SOMENAME -r 16 -R 1 -s 1 fat16.img

Теперь полученный образ можно куда-нибудь смонтировать, создавать там файлы или каталоги и смотреть по каким адресам они появятся.
Разумеется, ничто не мешает посмотреть документацию на mkfs.vfat и выставить другие параметры, чтобы сравнить и их влияние на адреса.
Исходный код программы доступен здесь: https://github.com/COKPOWEHEU/usb/tree/main/6.VirFat_DemoCode


Стыковка с USB-MSD

Сама по себе идея любой виртуальной файловой системы всегда одинакова: надо отобразить запрошенный хостом адрес на тот или иной алгоритм расчета значений. Например, если хост хочет прочитать нулевой сектор, нам не надо отдавать ему адрес реальных данных, можно скопировать содержательную часть PBR в буфер, добить бессодержательную нулями и не забыть контрольный 0xAA55. За это отвечает функция _virfat_read_pbr ().
Аналогично обрабатываются попытки доступа ко всем остальным областям:
Если запрошенный сектор соответствует области таблицы FAT, _virfat_read_fat () перебирает существующие файлы и их диапазоны секторов и копирует в выходной буфер только их. Ну или 0xFFF7 если данные уже закончились, а сектора — нет.
Если хосту понадобился сектор из области корневого каталога, _virfat_read_root () возвращает информацию по тем файлам, которые в него попали. Тоже, разумеется, не храня о них всю информацию. Так. например, дата создания у всех файлов задается прямо в прошивке, а дата последнего доступа обновляется у всех синхронно при доступе к любому отдельному файлу.
FunFact: по времени и дате последнего доступа можно попытаться синхронизировать время с компьютером. Правда, точность и надежность такого подхода оптимизма не внушают…
_virfat_read_data () определяет какому файлу соответствует запрошенный сектор из области данных и вызывает соответствующий callback. Более подробно о callback’ах и алгоритме использования библиотеки будет рассказано далее.
Ну и разумеется, симметричные функции для записи: _virfat_write_fat (), _virfat_write_root (), _virfat_write_data (). Поскольку список файлов и их сектора в моей реализации прописаны жестко, первая из них не делает вообще ничего. Вторая в принципе тоже — только подправляет дату последнего доступа. А третья, как и _virfat_read_data (), переводит абсолютный адрес сектора в относительный для файла и вызывает callback.
Собственно, с точки зрения USB-MSD больше ничего и не требуется: по запросу можно «прочитать» любой сектор, можно «записать» его. Разве что придется из 64-байтных посылок собирать-разбирать 512-байтные сектора. Ну и отдельный запрос на размер носителя.


Пример работы с библиотекой

Первым делом надо заполнить список файлов в корневом каталоге (напоминаю, что других тут и не будет). Для этого служит специальная константная структура:

typedef struct{
  char *name;                //имя в формате 8.3, то есть 11 символов
  virfat_callback file_read;    //callback на чтение
  virfat_callback file_write;   //callback на запись
  uint16_t size;             //размер файла
}virfat_file_t;

Такие структуры собираются в массив:

static const virfat_file_t virfat_rootdir[] = {
  {
    .name = "RLED    TXT",
    .file_read = demo_led_read,
    .file_write = demo_led_write,
    .size = 1,
  },
  {
    .name = "GLED    TXT",
    .file_read = demo_led_read,
    .file_write = demo_led_write,
    .size = 1,
  },
  {
    .name = "LOG     TXT",
    .file_read = demo_log_read,
    .file_write = virfat_file_dummy,
    .size = DEMO_LOG_SIZE,
  },
  {
    .name = "DUMMY   TXT",
    .file_read = virfat_file_dummy,
    .file_write = virfat_file_dummy,
    .size = 1,
  },
};

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


Конфиг-файлы

Начнем с «конфигурационных» файлов, то есть тех, для которых запись вообще разрешена. Напоминаю, что их размер ограничен одним сектором. В качестве демонстрации, пусть первый файл будет прикреплен к красному светодиоду, а второй к зеленому. Если в файл записать 1, светодиод загорится, если 0 (или что угодно еще) — погаснет.

void demo_led_read(uint8_t *buf, uint32_t addr, uint16_t file_idx){
  for(uint16_t i=1; i<512; i++)buf[i] = ' ';
  if(file_idx == 0){
    if( GPI_ON(RLED) )buf[0] = '1'; else buf[0] = '0';
  }else{
    if( GPI_ON(GLED) )buf[0] = '0'; else buf[0] = '0';
  }
}
void demo_led_write(uint8_t *buf, uint32_t addr, uint16_t file_idx){
  if(file_idx == 0){
    if(buf[0] == '1')GPO_ON(RLED); else GPO_OFF(RLED);
  }else{
    if(buf[0] == '1')GPO_ON(GLED); else GPO_OFF(GLED);
  }
}

Думаю, особенного пояснения данный код не требует. Кроме аргумента file_idx. Он нужен чтобы вешать один и тот же callback на несколько файлов, как в данном случае. Он равен номеру файла в массиве virfat_rootdir.


Дата-файлы

Вторая задача, которую данная библиотека должна решить — отображение каких-либо измеренных данных в удобочитаемом виде. Например, у нас есть массив структур

struct demolog_data_t{
  uint16_t x;
  uint16_t y;
};
#define DEMO_DATASIZE   1000
const struct demolog_data_t demo_log[DEMO_DATASIZE] = {
  [0]   = {0,   0},
  [1]   = {1,   1},
  [10]  = {10,  10},
  [100] = {100, 100},
  [500] = {500, 500},
  [999] = {999, 999},
};

Мы хотим его представить как красивый текстовый файл, с человеко-читаемой шапкой, с разделением в несколько столбцов, может даже какие-то расчеты провести дополнительно. Мало ли, перевести из внутренних «попугаев» в нормальные вольты. В первую очередь определимся как элемент массива перевести в строку. В моем примере это делается так:

u32tobuf(i, &buf[pos]); pos+=12; //первое число в "файле" будет просто номером
u32tobuf(demo_log[i].x, &buf[pos]); pos+=12;
u32tobuf(demo_log[i].y, &buf[pos]); pos+=12;
buf[pos-1] = '\r';
buf[pos] = '\n';
pos++;

Функция u32tobuf написана так, чтобы вписывать десятичное представление числа строго в заданное поле размером 11 символов и символ '\t' после. Сделано это для того чтобы каждая строка была одинаковой длины. Таким образом можно рассчитать сколько записей помещается на один сектор. Но важнее обратный расчет — какие номера записей надо отдать по запросу данного сектора. В моем случае длина строки составила 37 символов. То есть «честно» на границу сектора она не ляжет. Но нам ничто не мешает «добить» последнюю строку пробелами до границы. Нам ведь не надо тратить на их хранение память, а программы работы с текстовыми таблицами прекрасно «сколлапсируют» любое количество пробелов и табуляций в один разделитель.
Аналогичным способом решается добавление текстовой шапки. Она хранится где-то в постоянной памяти контроллера и подставляется в начало «файла» с последующим сдвигом адресов данных.
Важно! Если вы хотите отдавать не все снятые данные, а только часть (например, не успели заполнить буфер до конца), не провоцируйте хост на уменьшение файла (оно ведь не поддерживается). Лучше просто забейте неиспользуемые сектора пробелами.


Заключение

Итак, мы получили относительно легкий эмулятор файловой системы FAT16, который позволяет достаточно кроссплатформенно проводить простую настройку USB-устройств, а также отображать их внутренние данные в красивом текстовом (да даже не обязательно текстовом!) виде. Напоминаю, что для интерактивной настройки данный способ не сработает: устройству запрещено менять свои данные пока оно подключено к хосту. Ну, а в приведенной демо-версии можно просто помигать светодиодами и почитать километровый лог.
P.S. Почему-то Андроид записать файлы не смог. Пока не разобрался с чем это связано. Возможно, он хочет сначала создать новый файл, а потом переименовать его под старый. В Linux и Windows такой проблемы нет.
Как обычно, исходные коды и кое-какая литература доступны по ссылкам:
https://github.com/COKPOWEHEU/usb/tree/main/6.VirFat_L1
https://github.com/COKPOWEHEU/usb/tree/main/6.VirFat_F1
(это часть основного репозитория https://github.com/COKPOWEHEU/usb)

© Habrahabr.ru