FPU w STM32F4 – jak używać Floating Point Unit

Jedną z największych zalet procesorów STM32F4 i ogólnie Cortex-M4 jest jednostka Floating Point Unit (FPU) przyspieszająca obliczenia na liczbach zmiennoprzecinkowych. Jej wykorzystanie wymaga od użytkownika włączenia FPU podczas działania programu oraz kompilacji z odpowiednimi flagami, aby używać instrukcji FPU. W tym wpisie omawiam jak obsługiwać FPU zarówno od strony programu jak i kompilatora oraz jak pisać kod, aby mieć pewność, że wykorzystuje on dobrodziejstwa FPU.

Czy używać floatów w embedded?

Pewnie spotkaliście się z opinią, że w systemach embedded nie powinniśmy w ogóle używać liczb zmiennoprzecinkowych. Operacje na nich wykonują się wolno, szczególnie na małych mikrokontrolerach. Zamiast tego musimy stosować różne optymalizacje takie jak formaty liczb i operacje stałoprzecinkowe (polecam tutaj bibliotekę libfixmath), czy zapisane w pamięci tablice z obliczonymi wcześniej wartościami (lookup table) dla operacji takich jak logarytm lub sinus . Owszem – te obawy są prawdziwe, emulacja programowa liczb zmiennoprzecinkowych powoduje, że wspomniany sinus może generować tysiące instrukcji kodu maszynowego. Ale alternatywa ma też swoje minusy – kod jest mniej czytelny i trudniejszy do debugowania, lookup table zajmują dodatkowe miejsce w pamięci, a obliczenia są obarczone większymi błędami zaokrągleń.

Oczywiście przed zastosowaniem tej porady w naszym projekcie powinniśmy najpierw sprawdzić, czy dłuższy czas wykonywania obliczeń jest dla nas akceptowalny. Na procesorze taktowanym 80 MHz 1000 instrukcji powinno się wykonywać przez 12.5 us. Możliwe więc, że jesteśmy w stanie pogodzić się z tym czasem oszczędzając sobie w ten sposób przedwczesnej optymalizacji, która jak wiadomo jest źródłem wszelkiego zła.

W dzisiejszych czasach dysponujemy także dodatkową możliwością. Zamiast wyboru między krótszym czasem obliczeń a mniejszym skomplikowaniem kodu możemy skorzystać z procesora wyposażonego w jednostkę FPU wykonującego operacje zmiennoprzecinkowe w pojedynczych instrukcjach. Przykładem takiego procesora jest rodzina STM32F4 wyposażona w Cortex-M4. Na obrazku poniżej widać zestawy instrukcji dla kolejnych generacji procesorów Cortex. Na dole zaznaczone są instrukcje zmiennoprzecinkowe dostępne w Cortex-M4.

Aktywacja FPU w STM32F4

Aby móc korzystać z dobrodziejstw jednostki FPU należy ją najpierw włączyć w dwóch miejscach:

  • W kompilatorze poprzez ustawienie odpowiednich flag, aby kod wynikowy zawierał instrukcje zmiennoprzecinkowe.
  • W procesorze podczas działania programu, aby móc wykonywać instrukcje zmiennoprzecinkowe.

Najpierw zajmiemy się włączeniem FPU w procesorze. Domyślnie jednostka jest wyłączona, aby zmniejszyć zużycie energii i zmniejszyć ryzyko przypadkowego wykonania niechcianych instrukcji. Interesujące nas flagi znajdują się w rejestrze CPACR (Coprocessor Access and Control Register). Są to bity 20-21 (CP10) i 22-23 (CP11). Domyślnie są one ustawione na 0b00 – brak dostępu do koprocesora – wywołanie instrukcji FPU spowoduje exception. Powinniśmy je przestawić na 0b11 – pełny dostęp do koprocesora. Możliwe jest jeszcze ustawienie 0b01 zezwalające na dostęp tylko w Privileged Mode (ta opcja może być wykorzystywana w systemie operacyjnym z kontrolą dostępu do zasobów sprzętowych).

FPU w Cortex-M4 tak naprawdę nie jest koprocesorem tylko rozszerzeniem rdzenia. Jednak instrukcje FPU są mapowane na przestrzeń instrukcji koprocesorów CP10 i CP11. Prawdopodobnie takie rozwiązanie umożliwiło lepszą kompatybilność wsteczną.

Aby uzyskać pełny dostęp do FPU należy wywołać linijkę:

	/* FPU initialization */
	SCB->CPACR |= ((3 << 10*2)|(3 << 11*2));

Ja FPU inicjalizuję podczas inicjalizacji zaraz po ustawieniu taktowania. Oczywiście możliwe jest wielokrotne włączanie/wyłączanie podczas działania programu w celu oszczędzania energii.

Kompilator – wybór ABI

Aby skompilować kod pod FPU musimy podać kompilatorowi dwie informacje:

  • Wykorzystywane ABI (Application Binary Interface).
  • Typ hardware FPU.

ABI określa konwencje wywołań funkcji, czyli w jaki sposób przekazywać argumenty do funkcji i jak odczytywać zwracaną wartość. ABI to interfejs binarny, czyli występujący w skompilowanych plikach binarnych. Konwencja przekazywania parametrów wpływa wynik kompilacji – do funkcji dodawane są odpowiednie prologi i epilogi obsługujące parametry. ABI wybieramy za pomocą opcji

-mfloat-abi=xxx

, gdzie w miejsce xxx możemy wprowadzić jedną z trzech wartości:

  • soft – Operacje zmiennoprzecinkowe w pełni softwareowe. W miejsce działań zmiennoprzecinkowych wstawiane są wywołania funkcji emulujących floaty na zwykłych intach. Szczegóły w dokumentacji gcc. Poza tym argumenty do funkcji zmiennoprzecinkowych przekazywane są przez rejestry ogólnego przeznaczenia.
  • softfp – Możliwe jest generowanie instrukcji FPU, ale ABI definiujące sposób przekazywania parametrów, jest taki sam jak w przypadku softwareowym.
  • hard – Pełne wsparcie FPU. Są generowane instrukcje FPU, a ABI wykorzystuje rejestry zmiennoprzecinkowe do przekazywania parametrów. Ta opcja zapewnia najlepszą wydajność obliczeń.

Opcja -mfloat-abi jest opisana w dokumentacji gcc.

Tutaj ważna uwaga dotycząca ABI – jest to konwencja wywołań wykorzystywana przez cały program. Pliki skompilowane z ABI hard nie są kompatybilne z ABI soft – linkowanie zakończy się błędem. Natomiast softfp używa tego samego ABI co soft, dlatego możliwe jest łączenie bibliotek skompilowanych z tymi flagami.

Kompilator – wybór hardware

Jeżeli wybraliśmy -mfloat-abi=softfp albo -mfloat-abi=hard, kompilator potrzebuje jeszcze informacji o hardware na jaki powinien wygenerować instrukcje FPU. Dostarczamy ją za pomocą komendy:

-mfpu=xxx

, gdzie xxx oznacza rodzaj FPU. Opcji jest bardzo dużo i oznaczają one kolejne generacje jednostek zmiennoprzecinkowych dodawanych do rdzeni. Pełną listę można znaleźć na stronie ARM. Warto również przeczytać opis flagi mfpu w dokumentacji gcc.

Dla STM32F4 należy wybrać:

-mfpu=fpv4-sp-d16

FPV4 oznacza wersję architektury FPU. SP oznacza, że jednostka obsługuje tylko liczby single precision, czyli floaty 32-bitowe. D16 oznacza 16 64-bitowych rejestrów FPU. Dowiadujemy się z tego bardzo ważnej informacji. Wsparcie sprzętowe mają tylko liczby 32-bitowe, czyli float. Double w dalszym ciągu są emulowane na intach. Aby mieć stuprocentową pewność, że korzystamy z FPU musimy podejrzeć wynikowy kod assemblera. Wskazówki może dać również rozmiar plików obiektowych po kompilacji.

Kompilator – pomocne flagi

Aby w pełni wykorzystać FPU w Cortex-M4 powinniśmy tak pisać kod, aby wykorzystywał on liczby float, a nie double. Liczba zmiennoprzecinkowa w C zapisana standardowo:

1.5

jest traktowana jako double. Aby wymusić typ float musimy dodać odpowiedni sufiks:

1.5f

Zamiast pamiętać o dopisywaniu sufiksu możemy wspomóc się flagami kompilatora. W gcc istnieje flaga zmieniająca domyślne stałe na typ float:

-fsingle-precision-constant

Możemy również włączyć warning gcc ostrzegający nas przed konwersją z float na double:

-Wdouble-promotion

Biblioteka math.h

Jeżeli korzystamy z liczb zmiennoprzecinkowych, najczęściej mamy zamiar wykorzystywać bardziej zaawansowane obliczenia takie jak trygonometria, pierwiastki i logarytmy. Domyślne funkcje z biblioteki math.h takie jak sin, log, czy floor działają na zmiennych typu double. Czyli generują kod bez użycia FPU. Zamiast nich powinniśmy używać floatowych odpowiedników z dodanym f na końcu nazwy: sinf, logf, floorf.

Skoro jesteśmy przy bibliotece matematycznej, istnieje jedna ciekawa flaga kompilatora przyspieszająca obliczenia matematyczne:

-ffast-math

Przyspiesza ona obliczenia matematyczne dzięki pominięciu niektórych sprawdzeń i warunków brzegowych dotyczących głównie wartości specjalnych takich jak NaN. Dokładniejsze informacje można znaleźć tutaj.

FPU a współbieżność

FPU używa oddzielnego zestawu rejestrów. W przypadku wielowątkowości albo użycia przerwań należy zapewnić, że wartości tych rejestrów są odkładane na stosie podczas zmiany kontekstu. Powinien to dla nas robić kompilator w przypadku przerwań i RTOS w przypadku wątków. Jednak warto przetestować, czy na pewno zapamiętywanie kontekstu FPU jest poprawnie realizowane. W przeciwnym wypadku przyjdzie nam się mierzyć z trudnymi do wykrycia błędami.

Podsumowanie

Wykorzystanie FPU może znacznie przyspieszyć obliczenia wykonywane na mikrokontrolerze. Dzięki temu nie musimy stosować dziwnych optymalizacji zmniejszających czytelność kodu. Jednostkę FPU należy włączyć przed wykonaniem jakichkolwiek obliczeń, a aby skompilowany program korzystał z instrukcji zmiennoprzecinkowych, musimy ustawić mu odpowiednie flagi. W STM32 FPU wspiera tylko floaty, a double już nie. Dlatego musimy uważać podczas implementacji, używać odpowiednich funkcji matematycznych oraz wspomagać się dodatkowymi flagami kompilatora.

 

Dodatkowe materiały

https://community.arm.com/processors/b/blog/posts/10-useful-tips-to-using-the-floating-point-unit-on-the-arm-cortex–m4-processor

https://embeddedartistry.com/blog/2017/10/9/r1q7pksku2q3gww9rpqef0dnskphtc

 

 

2 Comments

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *