FAT32 на STM32

В данной статье хочу поделиться своим опытом ковыряния файловой системы FAT32, на которые я потратил почти месяц свободного от работы времени. 

В один прекрасный день, начитавшись в сети статей с громкими заявлениями вроде «STM32 — это просто», я решил попробовать освоить данный камень. Небольшой опыт программирования AVR у меня был, поэтому я приобрёл десяток stm32F103c8t6 и начал с ними разбираться. Поморгал светодиодом, потом подключил дисплей HD44780, написал на него драйвер. В общем, стало скучно, и я решил сделать что-то более сложное. Мой выбор пал на mp3 плеер, тем более, я давно хотел поработать с кодеками от VLSI. И пока я ждал заказанные vs1053, закипела работа над программой. Начал я с файловой системы. Выбор пал на FAT32, конечно, намного проще было взять готовый драйвер FatFs или, хотя бы разобраться с FAT16. Но! первый вариант отпал, потому что: 1 мне было очень сложно разбираться с чужими функциями, 2 для моей задачи мне не было понятно, как находить файлы с произвольным названием и расширением «mp3», 3 необходимость при чтении указывать количество байт чтения файла, а откуда мне знать сколько? И т.д. Второй вариант отпал по той причине, что FAT16 устарела, да и современную карту SDHC форматировать в FAT16 — глупо, если вообще возможно. А самое главное — хотелось поработать головой и разобраться.

Итак, приступим. Сначала нам надо разобраться со структурой файловой системы. Не будем углубляться в подробности. Первое, что надо знать — весь массив данных на носителе разбит на сектора (стандарт — 512 байт), т.е. минимальное количество информации, которое мы можем прочитать/записать. А сектора в своё время объединены в кластеры (кластерами хранится массив данных файлов). Сама FAT32 состоит из 2-х основных частей — системной области и области данных. Системная часть состоит из загрузочного сектора, зарезервированной области и таблиц FAT, далее начинается область данных. Область данных начинается с корневого каталога (т.е. то что у нас храниться в корне носителя),  а за ним идут массивы данных. Необходимо упомянуть, что корневой каталог может начаться  не сразу после последней копии фат-таблицы (в том случае, если будут битые сектора). 

Теперь по порядку. Загрузочный сектор (512 байт) хранит очень много нужной для нас информации. Для примера приведу скриншот дампа SDHC карты 8 Гб (сделан бесплатной версией программы WINHEX).

Я перечислю лишь то, что нам пригодится (подробнее можно найти в сети, я рассчитываю, что моя статья пригодится тем, кто уже владеет азами). Не забываем, что байты расположены в непривычном для понимания обратном порядке (от младшего к старшему, т.е., например,  ячейки F2 03 00 00 означают число 0x000003F2).

  • 1 байт – количество секторов в кластере (смещение 0x0D байт)
  • 1 байт – количество таблиц FAT (обычно 2, смещение 0x0C)
  • 4 байта — размер таблицы FAT (смещение 0x24-0x27)
  • 2 байта — адрес начала первой таблицы FAT (0x0E-0x0F)
  • 4 байта – номер кластера корневого каталога (смещение 0x2C-0x2F)

Чем нам поможет данная информация? Самое главное — мы сможем найти адреса начала таблиц FAT, их объём, а так же адрес начала корневого каталога. И размер кластера — тоже очень важный параметр (на картинке в ячейке 0x0D — значение 40h — что составляет 64 в десятичной системе). Давайте разбираться по порядку. Для начала нам необходимо знать, где начинается корневой каталог — по сути, это основная наша задача. Из загрузочного сектора (ячейки 0x2C-0x2F) мы узнали, что корень нашего носителя хранится в кластере №2 — число 00000002h (кстати, область данных начинается со второго кластера, кластеры 0 и 1 не существуют) — это уже что-то. Значит, наш корневой каталог будет находится впритык за последней копией фат. Из ячеек 0x0E-0x0F мы узнаём адрес начала таблиц фат, из ячеек 0x24-0x27 их размер, а из ячейки 0x0C — их количество. Дело за малым — рассчитать объём в секторах фат таблиц и добавить его к начальному адресу первой таблицы:

Объём FAT = количество FAT*объём одной таблицы 

Адрес кластера №2 = адрес начала FAT+объём FAT

Вроде бы всё просто. Рассчитываем все адреса и начинаем работу — но нет. Здесь возникает первый подводный камень. Загрузочный сектор находится в нулевом логическом секторе. А, как оказалось, логический сектор — это далеко не физический сектор. И по нулевому адресу лежит совсем не то. Так вот, в нулевом физическом секторе находится главная загрузочная запись, она же MBR. В ней,  кроме всего, хранятся записи о разделах диска (всего четыре записи). Так как у наc SD карта — то скорее всего там будет один раздел, адрес его начала (загрузочного сектора) находится в ячейках по адресу 0x1C6-0x1C7 (я это понял опытным путём). Теперь нам осталось к нашему адресу начала таблицы фат добавить адрес начала нашего загрузочного сектора (физический его адрес):

Физический адрес начала FAT = адрес начала FAT + физический адрес загрузочного сектора

Напишем программу инициализации файловой системы:

Объявим глобальные переменные

uint32_t root_begin; //Физический адрес начала корневого каталога
uint32_t cluster_2_add; //Физический адрес кластера №2
uint16_t fat_start_add; //номер блока, с которого начинается таблица FAT
uint8_t fat_block[512]; //Массив, в который будем читать блок FAT таблицы
uint32_t root_cluster; //Первый кластер корневого каталога
uint8_t sector_per_clust; //количество секторов в кластере
uint16_t root_volume; //Размер корневого каталога в блоках
uint16_t boot_sector; //Загрузочный сектор

void fat_init(void){ // Сама функция инициализации FAT

//Локальные переменные, которые используются лишь внутри функции, чтобы сэкономить ОЗУ ​

uint32_t volume_of_fat; //количество секторов в каждой FAT таблице
uint8_t number_of_fats; //количество копий FAT таблиц

read_block(block_data,0x00); // Читаем MBR
boot_sector=(block_data[0x1C6]|(block_data[0x1C7]<<8)); //Адрес загрузочного сектора
read_block(block_data,boot_sector); // Читаем загрузочный сектор
sector_per_clust=block_data[0x0D]; // Узнаём количество секторов в кластере
fat_start_add=(block_data[0x0E]|(block_data[0x0F]<<8))+boot_sector; // Рассчитывает физический адрес начала таблиц фат
number_of_fats=block_data[0x0C]; //Узнаём количество таблиц фат
volume_of_fat=block_data[0x24]|(block_data[0x25]<<8)|(block_data[0x26]<<16)|(block_data[0x27]<<24); // Размер одной таблицы
cluster_2_add=fat_start_add+(volume_of_fat*number_of_fats); // Рассчитываем адрес кластера №2
root_cluster=block_data[0x2C]|(block_data[0x2D]<<8)|(block_data[0x2E]<<16)|(block_data[0x2F]<<24); // Узнаём номер кластера корневого каталога
root_begin=cluster_2_add+((root_cluster-2)*sector_per_clust); // Рассчитываем адрес начала корневого каталога (если, вдруг, он не во 2-м кластере)
root_volume=(file_volume(root_cluster)*sector_per_clust); // Это функция расчёта объёма корневого каталога (о ней позже)
}

С адресами и объёмами определились. Теперь разберёмся немного с таблицами FAT. Все копии таблиц — идентичны (на тот случай, если одна будет повреждена, можно перейти к другой). Для примера приведу скриншот таблицы:

Вся таблица состоит из четырёхбайтных записей о каждом кластере носителя. Все записи идут по порядку. Я уже упомянул ранее, что кластеров №0 и №1 физически нет, но записи о них в таблице есть (0FFFFFF8h). Немного о значениях записей:

0FFFFFFFh — означает, что кластер данного файла последний

00000003h-0FFFFFF7h — номер кластера, в котором находится продолжение файла

0FFFFFF8h — кластер повреждён

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

И наконец-то самое интересное — корневой каталог. Он представляет собой набор 32 байтных записей о файлах, находящихся в корне носителя (в том числе и о подкаталогах, то есть папках, которые представляют из себя те же файлы, лишь помечены специальной меткой вместо расширения файла, структуру папок разбирать не будем). В каждой записи о файле есть: имя файла (первые 8 ячеек), расширение (следующие 3 ячейки), потом ещё куча ячеек с датами, временем создания файла и др, и самое главное — номер первого кластера файла (смещение от начала 32 байтной записи 0x14, 0x15, 0x1A, 0x1B). Но не всё так просто и очевидно. Как я писал выше, имя файла занимает лишь 8 байт (это стандарт короткого имени файла 8.3 — восемь символов имени и 3 символа расширения файла), а у нас файлы могут быть с ооочень длинными именами, поэтому в FAT32 используются структуры LFN, это тоже 32 байтные записи с продолжением имени (они должны предшествовать записи о самом файле). С LFN я сам не разобрался, да мне для моего проекта и не надо было. Чтобы не быть голословным — скриншот корневого каталога (на карте один MP3 файл):

Как видим, не все записи в корне несут информацию о файлах, в первой — у нас название флешки, далее скрытые системные папки, потом LFN структуры и лишь в конце — наш mp3 файл. Кто-то скажет, что выше тоже есть надпись mp3! Но, давайте вспомним структуру имени в формате 8.3. Второе, что бросается в глаза — это кракозябры в названии файла. Дело в том, что имена — в кодировке ASCII, а мой файл с русскими буквами, поэтому имя закодировано в LFN в другой кодировке (если я не ошибаюсь — в юникод). И ещё один нюанс: если имя файла начинается с E5h, значит этот файл был удалён, хотя в массиве данных он ещё есть (так восстанавливают удалённые данные). Давайте посмотрим, в каком кластере начинается наш файл — это кластер №6 (ячейка 4000DAh — т.е. смещение от начала 32 байтной записи 0x1A — это младший байт номера первого кластера, в остальных трёх старших байтах — нули). Теперь давайте вернёмся к скриншоту с таблицей фат и посмотрим, что у нас находится в шестом кластере. А там у нас — число 7, то есть продолжение файла в кластере №7 и так далее, пока не будет 0FFFFFFFh.

В общем и целом с описанием основных структур всё. Приступим к практике. Пример чтения файла я покажу на своём проекте (mp3 плеера). Там нет функций открытия/закрытия файла. Выбор и чтение происходит в основном цикле программы.

void main(void){

//Глобальные переменные
uint16_t root_count=0; // Счётчик прочитанных блоков корневого каталога
uint32_t first_cluster; //Первый кластер файла
uint8_t root_block[512]; //Блок памяти, в который будем читать корневой каталог

while(1){

read_block(root_block,(root_begin+root_count)); //Читаем один блок корневого каталога
for(uint16_t n=0;n<512;n=n+32){ // Цикл чтения записей корневого каталога (по 32 байта)
if((root_block[0x08+n]==’M’)&&(root_block[0x09+n]==’P’)&&(root_block[0x0A+n]==’3′)&&(root_block[0x00+n]!=0xE5)){

//Если запись в корневом каталоге соответствует MP3 файлу, то читаем номер первого кластера файла

first_cluster=(root_block[0x15+n]<<24|root_block[0x14+n]<<16|root_block[0x1B+n]<<8|root_block[0x1A+n]);
//Закидываем Файл в vs1053
read_file(first_cluster); // Функция чтения файла, смотрите ниже основного цикла
}
}

root_count++; // Увеличиваем счётчик блоков корневого каталога
if(root_count>root_volume){root_count=0;} // Пока не превысим его размер
}
}

void read_file(uint32_t cluster){ // Функция чтения файла
do{
////////////////////////////////////////////////////////////////////////////////
for(uint8_t i=0;i<sector_per_clust;i++){ //Слешами выделена функция чтения одного кластера
//В моём проекте данные я закидываю порциями по 32 байта, поэтому здесь много вложенных циклов внутри других циклов
//Не обращайте внимание на BSYNC, DREQ — это команды для декодера, чтение идёт в SPI1 функцией SPI1_transfer

read_block(block_data,(cluster_2_add+i+((cluster-2)*(sector_per_clust)))); // block_data[ ] — массив, в который я читаю данные файла
for(uint16_t j=0;j<512;j=j+32){
while (!DREQ_HIGH);
BSYNC_LOW
for(uint8_t k=0;k<32;k++){
SPI1_transfer(block_data[k+j]); }

BSYNC_HIGH
}
}
////////////////////////////////////////////////////////////////////////////////
cluster=read_fat_cluster(cluster); // Читаем запись о кластере в таблице ФАТ
}
while(cluster!=0x0FFFFFFF); //Выполняем пока не встретим запись о последнем кластере
}

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

//Функция определяет физический адрес блока таблицы FAT с нужной записью
uint32_t fat_cluster_add (uint32_t cluster){

uint32_t fat_cls_add_block; //Номер блока с нужной записью таблицы FAT

if(cluster<128){fat_cls_add_block=0;} // Если номер кластера меньше 128, то запись о нём будет в первом блоке фат таблицы
else{fat_cls_add_block=cluster/128;};
cluster=fat_cls_add_block+fat_start_add;
return cluster;
}

//Функция возвращает содержимое записи о кластере (аргумент) в таблице FAT
uint32_t read_fat_cluster(uint32_t cluster){
uint32_t i;
uint32_t j;

read_block(fat_block,fat_cluster_add(cluster));
if(cluster<128){
i=cluster;
j=(fat_block[(i*4)+3]<<24)|(fat_block[(i*4)+2]<<16)|(fat_block[(i*4)+1]<<8)|fat_block[(i*4)]; }
else{
i=(cluster%128);
j=(fat_block[(i*4)+3]<<24)|(fat_block[(i*4)+2]<<16)|(fat_block[i*4+1]<<8)|fat_block[i*4]; };
return j;
}

uint16_t file_volume(uint32_t cluster_number){ //Функция возвращает количество клаcтеров, занимаемое файлом

uint8_t count=1; //Счётчик числа кластеров файла
uint32_t next_cluster; //Переменная, в которую будем читать ячейки таблицы FAT

next_cluster=read_fat_cluster(cluster_number);
if(next_cluster!=0x0FFFFFFF){
while(next_cluster!=0x0FFFFFFF){next_cluster=read_fat_cluster(next_cluster);
count++;};
return count;}
else{return count;};
}

uint32_t cluster_add(uint32_t cluster){ //Функция определяет физический адрес кластера (аргумент — № кластера)
cluster=((cluster-2)*sector_per_clust)+cluster_2_add;
return cluster;
}

PS: я не описывал реализацию работы с SD картой на низком уровне (необходимы функции инициализации и чтения блока). Для работы драйвера необходимо три массива в 512 байт (в один читается загрузочный сектор, второй массив для чтения корневого каталога и третий для чтения данных файла), плюс несколько переменных (некоторые по 32 байта), следовательно нужно окало 1.6 — 1.8 килобайта ОЗУ микроконтроллера.

На этом всё. ниже я прикреплю файлы своего проекта, всё, что описано в статье находится в файлах fat.h и main.c, остальное — это библиотеки работы со SPI, vs1053, SD, настроек stm-ки. Проект написан в среде KEIL ARM v5. Спасибо за внимание. Надеюсь моя статья будет полезной и кто-то не будет как я мучиться месяц-другой, разбираясь с дампами, таблицами и прочей технической информацией.

Прикрепленные файлы:

Добавить комментарий

Ваш адрес email не будет опубликован.