LinkedIn YouTube Facebook
Szukaj

Wstecz
Artykuły

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. 3. Lokalizacja modułu FatFs w projekcie programistycznym

Rys. 4. Struktura plików FatFs

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.