Реклама ⓘ
Главная » Микроконтроллеры
Призовой фонд
на апрель 2024 г.
1. 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
Макетная плата для пайки (10 шт) Arduino UNO
вверх