Главная » Микроконтроллеры
Призовой фонд
на февраль 2021 г.
1. 1500 руб
Сайт Паяльник
2. Мультиметр ANENG M118A
Сайт Паяльник
3. 350 руб.
От пользователей


Светодиодные модули на 10 Вт, 20 Вт, 30 Вт, 50 Вт, 100 Вт

FAT32 на STM32

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

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

Итак, приступим. Сначала нам надо разобраться со структурой файловой системы. Не будем углубляться в подробности. Первое, что надо знать - весь массив данных на носителе разбит на сектора (стандарт - 512 байт), т.е. минимальное количество информации, которое мы можем прочитать/записать. А сектора в своё время объединены в кластеры (кластерами хранится массив данных файлов). Сама FAT32 состоит из двух основных частей - системной области и области данных. Системная часть состоит из загрузочного сектора, зарезервированной области и таблиц 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. Спасибо за внимание. Надеюсь моя статья будет полезной и кто-то не будет как я мучиться месяц-другой, разбираясь с дампами, таблицами и прочей технической информацией.

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

Теги:

Опубликована: 0 0
Я собрал 0 Участие в конкурсе 0
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 0 Проголосовало: 0 чел.

Комментарии (9) | Я собрал (0) | Подписаться

0
Публикатор #
На форуме автоматически создана тема для обсуждения статьи.
Ответить
0
u235 #
В архиве много мусора. В заголовочных файлах лучше не прописывать переменные и тела функций.
Ответить
0

[Автор]
AndrejChoo #
Понимаю, что мой стиль очень грубый. Я в программировании новичок. Знаю, что к хидерам нужно писать си файлы с функциями, но не люблю, когда в проекте кучи файлов (и хидеры и си и т д). Для несложных проектов не критично, я считаю. Но, спасибо за замечание.
Ответить
0
Chip115 #
Доброго времени суток!
Если вы только начали программировать, то лучше сразу приучать себя к порядку и не делать поблажек, мол "код простой, зачем это надо?" и прочее. Потом очень трудно переучиваться. Это по началу только сложно. Потом будете на автомате хедеры по феншую клепать.
И да, есть книга.
Совершенный код. Практическое руководство по разработке ПО.
Автор С. Макконнел.
Там просто неописуемо ценная информация по стилю и прочим "фишкам".
Считаю что эта книга должна быть в бумаге на столе каждого разработчика ПО, зачитанная до дыр.
Ответить
0
Aleksey1408 #
Спасибо за статью, я искал информацию по фат32, очень мало, может быть я не знаю где найти. Если можете дайте ссылки на реферируемую литературу
Ответить
0

[Автор]
AndrejChoo #
К сожалению, какого-то одного источника я не нашёл. Информацию собирал по крупицам в поисковике. Большую часть я нашёл в статьях про восстановление файлов на sd-картах.
Ответить
0
u235 #
Есть спецификация от Microsoft. Ну и на википелии можно почитать.
Ответить
0
Kolived #
Я использовал в одном проекте FatFs, проблем читать блок данных нужного размера не было, список файлов там вроде как тоже есть возможность посмотреть.
Это так для сведения! А вообще молодец!
Ответить
-1
smack #
А том, что в хедерах недопустимо реализация функций все это из категории "бла-бла-бла" и не более того. Все это только условные замечания, ни в каком формальном стандарте это не описано. Тот же майкрософт этим регулярно грешит.
Тот кому не нравится, как вы пишете, пусть не читает.
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется сила тока?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Программатор Pickit3
Программатор Pickit3
iMAX B6 - зарядное для Lion, LiPo, LiFe, Pb, NiCd и NiMH аккумуляторов USB осциллограф DSO-2090
вверх