Obsługa FAT w mikrokontrolerach STM32 – obsługa kart
Moduł FatFs
System plików w urządzeniach wbudowanych można zaimplementować na dwa sposoby: pisząc obsługę systemu plików od podstaw, lub też wykorzystać gotowe rozwiązania. W zasadzie nie ma żadnego sensownego uzasadnienia pierwsze wymienione podejście. System plików FAT jest na tyle dobrze udokumentowany, a przy tym stosunkowo prosty, że powstało wiele darmowych narzędzi, które radzą sobie bardzo dobrze z administracją zawartości nośnika danych z systemem plików FAT. Z reguły otwarty charakter kodu pozwala na wprowadzenie koniecznych zmian i poprawek, które mogą się okazać niezbędne dla stabilności pracy urządzenia.
Jednym z takich ogólnodostępnych narzędzi jest moduł FatFs, którego zadaniem jest stanowienie pomostu pomiędzy warstwą fizyczną (nośnikiem pamięci), a aplikacją uruchomioną na mikrokontrolerze. Szczegółowych informacji na temat FatFs należy szukać na stronie internetowej autora: http://elm-chan.org/fsw/ff/00index_e.html . Rolę modułu FatFs zilustrowano na rys. 3.
Rys. 3. Lokalizacja modułu FatFs w projekcie programistycznym
Rys. 4. Struktura plików FatFs
Sam moduł FatFs jest napisany języku C. Pliki, które są niezbędne do poprawnej pracy FatFs przedstawiono na rys. 4 w formie drzewa skopiowanego z projektu wykorzystującego system plików FAT. Teoretycznie, do poprawnej pracy moduł FatFs wymaga obecności w systemie wbudowanym zegara czasu rzeczywistego (RTC). Można ten wymóg bardzo łatwo obejść wpisując stałe wartości w miejsce daty i czasu.
Implementacja FatFs w mikrokontrolerach STM32 – warstwa fizyczna
Do opracowania warstwy sprzętowej został wykorzystany przykładowy projekt, zamieszczony na stronie internetowej modułu FatFs. Wszystkie funkcje, których zadaniem jest sterowanie urządzeniami peryferyjnymi mikrokontrolera, oraz zapis i odczyt danych z karty pamięci, zostały umieszczone w jednym pliku sd_stm32.c .
List. 1. Funkcja rcvr_spi() odpowiedzialna za odbieranie danych z kontrolera magistrali SPI
static void SELECT (void) // CS w stan niski { GPIO_ResetBits(GPIOC, GPIO_Pin_12); } static void DESELECT (void) // CS w stan wysoki { GPIO_SetBits(GPIOC, GPIO_Pin_12); } static void xmit_spi (BYTE Data) // Wyslanie bajtu do SD { // Wyslanie bajtu while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, Data); } static BYTE rcvr_spi (void) // Odebranie bajtu z SD { u8 Data = 0; // Wyslanie 0xFF while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, 0xFF); // Odebranie bajtu while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); Data = SPI_I2S_ReceiveData(SPI1); return Data; }
Bezpośrednio ze sprzętem komunikują się cztery funkcje. Za odbieranie danych z kontrolera magistrali SPI odpowiada funkcja rcvr_spi() , która została przedstawiona, wraz z pozostałymi trzema na list. 1. Wysyłaniem bajtów przez SPI do karty pamięci zajmuje się funkcja xmit_spi() . Do zadań interfejsu sprzętowego należy jeszcze sterowanie sygnałem wyboru układu CS, co należy do obowiązków funkcji SELECT() i DESELECT() .
Czasem, gdy mikrokontroler zażąda dostępu do zasobów karty pamięci, może się okazać, że ta ostatnia jest w danym momencie zajęta wykonywaniem innych operacji. Wtedy istotna jest możliwość sprawdzania zajętości karty. W tym celu została napisana funkcja wait_ready() , przedstawiona na list. 2. Jej zadaniem jest oczekiwanie przez maksymalny czas 500 ms, aż odebrany bajt będzie miał wartość 0xFF. Jeżeli w ciągu 500 ms nie zostanie odebrany bajt 0xFF, to funkcja kończy swoje działanie, zwracając ostatnią wartość odczytaną z kontrolera SPI.
List. 2. Funkcja wait_ready() , której zadaniem jest oczekiwanie przez maksymalny czas do 500 ms, aż odebrany bajt będzie miał wartość 0xFF
static BYTE wait_ready (void) { BYTE res; Timer2 = 50; // Czeka przez 500ms rcvr_spi(); do res = rcvr_spi(); while ((res != 0xFF) && Timer2); return res; }
Mamy już wszystkie niezbędne funkcje zajmujące się interfejsem sprzętowym, jednak, aby mogły one w ogóle pracować, to sam sprzęt musi zostać odpowiednio skonfigurowany.
Za konfigurację kontrolera SPI, portów wejścia/wyjścia i ich sygnałów zegarowych odpowiada funkcja power_on() , którą zamieszczono na list. 3. Po zdefiniowaniu zmiennych, wykorzystywanych w dalszym kodzie funkcji, następuje włączenie sygnałów zegarowych dla wyprowadzeń (porty GPIOA i GPIOC) i kontrolera SPI. Następnie konfigurowane jest wyprowadzenie PC12 do sterowania wyborem układu (CS) oraz piny PA5, PA6, PA7 jako linie magistrali SPI.
List. 3. Funkcja power_on() odpowiedzialna za konfigurację kontrolera SPI, portów wejścia/wyjścia i włączenie ich sygnałów zegarowych
static void power_on (void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; u8 i, cmd_arg[6]; u32 Count = 0xFFF; // Konfiguracja wyprowadzen i kontrolera SPI: // Wlaczenie sygnalow zegarowych dla peryferiow RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_SPI1 | RCC_APB2Periph_AFIO, ENABLE); // PA4 jako CS GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); //SCK, MISO and MOSI GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // Konfiguracja SPI SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); // Wlacz SPI SPI_Cmd(SPI1, ENABLE); // Inicjalizacja karty i przelaczenie w tryb SPI: DESELECT(); // CS = 1 for (i = 0; i < 10; i++) xmit_spi(0xFF); // Wyslij 0xFF 10 razy = 80 cykli zegarowych // (wymaganych co najmniej 74 cykli) SELECT(); // CS = 0 // Przygotowanie ramki inicjujacej do wyslania cmd_arg[0] = (CMD0 | 0x40); cmd_arg[1] = 0; // Argument komendy cmd_arg[2] = 0; // nawet, gdy komenda go nie ma cmd_arg[3] = 0; // musi zostac wyslany w postaci zer cmd_arg[4] = 0; cmd_arg[5] = 0x95; // CRC = 0x95 for (i = 0; i < 6; i++) // Wyslanie ramki xmit_spi(cmd_arg[i]); while ((rcvr_spi() != 0x01) && Count) // Czeka na 0x01 Count--; DESELECT(); // CS = 1 xmit_spi(0XFF); // Wyslij 0xFF PowerFlag = 1; }
Kontroler SPI jest ustawiany jako master do pracy w trybie full dupleks . Ramka danych będzie wynosić 8 bitów, a zatrzaskiwanie stanu linii będzie następować na zboczu narastającym sygnału zegarowego. W stanie nieaktywnym na linii SCK będzie występował stan wysoki. Preskaler dla zegara kontrolera SPI został ustawiony na 4, co oznacza, że dane będą przesyłane z niebagatelną prędkością 18Mbit/sekundę. Po ustawieniu wszystkich parametrów SPI, kontroler zostaje włączony przez wywołanie funkcji SPI_Cmd() .
Od tego momentu mikrokontroler jest już prawidłowo skonfigurowany, natomiast karta SD domyślnie po włączeniu zasilania pracuje w trybie obsługi dedykowanego standardu SDBus. Aby komunikacja (odbieranie komend) była w ogóle możliwa, należy w pierwszej kolejności wysłać co najmniej 74 cykle zegarowe, w celu zainicjowania karty. Następnie, żeby przejść do trybu pracy z SPI, należy wysłać komendę CMD0. Jeśli inicjalizacja karty do pracy w trybie SPI zostanie przeprowadzona poprawnie, to karta zwróci bajt potwierdzenia wynoszący 0x01.
Moduł FatFs wymaga do pracy sygnału zegarowego, który co 10ms będzie wywoływał funkcję disk_timerproc() , która jest wykorzystywana dalej do odmierzania czasu. Do cyklicznego wywoływania wymienionej wyżej funkcji został wykorzystany 24-bitowy timer SysTick. Jego konfiguracja została przedstawiona na list. 4.
List. 4. Procedura odpowiedzialna za konfigurację timera SysTick
void SysTick_Conf(void) { // SysTick bedzie taktowany z f = 72MHz/8 = 9MHz SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // Przerwanie ma byc co 10ms, f = 9MHz czyli liczy od 90000 SysTick_SetReload(90000); // Odblokowanie przerwania od timera SysTick SysTick_ITConfig(ENABLE); // Wlaczenie timera SysTick_CounterCmd(SysTick_Counter_Enable); }
Domyślnie główny zegar systemowy, po powieleniu przez pętlę PLL, wynosi 72 MHz i z taką częstotliwością domyślnie jest taktowany SysTick. Aby uzyskać przerwanie co 10 ms zastosowano preskaler, dzielący sygnał 72 MHz przez 8, co w efekcie daje 9 MHz. Jeśli chcemy, aby funkcja obsługi przerwania od timera SysTick ( SysTickHandler() ) była wywoływana z częstotliwością 100 Hz, to należy sprawić, aby licznik liczył od 90000. Bardziej szczegółowo timer SysTick omówiono w rozdziale 5, natomiast w tym przypadku funkcja obsługi jego przerwania wygląda następująco:
void SysTickHandler(void) { disk_timerproc(); }
Omówione funkcje są jedynymi zależnymi od sprzętu fragmentami kodu w module FatFs, zatem teraz zajmiemy się już najwyższą jego warstwą, umożliwiającą operacje na plikach i katalogach.