Dzisiaj opiszę używany przeze mnie szablon projektu STM32.  Szablon poza kompilacją głównego projektu umożliwia również dodawanie unit testów i testów na docelowym sprzęcie. W artykule omawiam strukturę folderów i konfigurację za pomocą Makefile, które sprawdzają się na moje potrzeby. Kod źródłowy szablonu udostępniłem na GitHubie.

Projekt generowany w IDE kontra własny szablon

Jeśli korzystamy z jakiegoś środowiska dedykowanego do pisania kodu na STM32 np. OpenSTM32, albo CooCox, gdy utworzymy nowy projekt i wyklikamy opcje w kreatorze, magicznie utworzy nam się konfiguracja zawierająca pliki inicjalizacyje, skrypt linkera, czy reguły kompilacji. Dodatkowo dołączone zostaną biblioteki z definicjami rejestrów i obsługą peryferiów. Jest to idealna sytuacja dla początkujących, którzy nie muszą dzięki temu poznawać skomplikowanych zagadnień związanych z inicjalizacją procesora do pracy i kompilacją projektu. Po prostu dostają działający projekt i mogą uczyć się podstaw. Jednak jeśli zdobędziemy już trochę doświadczenia i chcemy zrobić jakieś bardziej zaawansowane operacje, okazuje się, że IDE mają swoje ograniczenia. Częste problemy to na przykład dodawanie projektu do repozytorium kodu tak, żeby przenieść je na inną maszynę, czy wprowadzanie zmian w kodzie przez kilka osób na raz.

Rozwiązaniem tego problemu jest stworzenie projektu z własnymi plikami konfiguracyjnymi niezależnymi od używanego środowiska. W moim rozwiązaniu cała konfiguracja jest zawarta w skryptach makefile, a kompilacja może zostać uruchomiona zarówno z IDE, jak i z konsoli, czy na serwerze continuous integration. Konfiguracja jest trzymana w plikach tekstowych, które umożliwiają łatwe przenoszenie, zmiany przez wiele osób i mogą być dodane do repozytorium kodu.

W skład szablonu wchodzą:

  • skrypty linkera,
  • skrypty makefile,
  • biblioteki procesora,
  • kod inicjalizacyjny procesora.

Dodatkowo szablon zapewnia wsparcie dla unit testów wykonywanych na komputerze i programów testowych na docelowy hardware. Domyślnie szablon jest skonfigurowany pod STM32F407VG taki jak na płytce STM32F4 Discovery. Jednak po drobnych modyfikacjach może działać z innymi procesorami STM32.

Struktura folderów

W głównym folderze projektu znajdują się następujące foldery:

  • lib – Zawiera biblioteki prekompilowane używane w projekcie. Możemy tu umieścić na przykład bibliotekę matematyczną ze wsparciem FPU od ARM.
  • makefiles – Pomocnicze skrypty makefile zawierające zmienne i funkcje używane przez wiele targetów. Znajdują się tutaj między innymi definicje zmiennych dla kompilatora ARM i PC, czy poleceń systemowych takich jak mkdir, czy rm. Dzięki umieszczeniu tych zmiennych w oddzielnych plikach łatwiejsze jest portowanie skryptów na inny kompilator czy system operacyjny.
  • src – Pliki źródłowe docelowej aplikacji na STM32.
  • test – Folder zawierający targety testowe.

Folder src zawiera foldery:

  • code – Zawiera kod produkcyjny niezależny od warstwy sprzętowej. Nie odnosimy się więc tutaj bezpośrednio do rejestrów, zamiast tego wywołujemy funkcje z warstwy obsługi sprzętu. Chodzi o to, aby kod zawarty tutaj był portowalny. W folderze powinny znaleźć się podfoldery zawierające poszczególne moduły aplikacji.
  • external – Folder służący do przechowywania zewnętrznych (czyli nie napisanych przez nas) bibliotek. To tutaj będziemy trzymać np. FatFS, FreeRTOS czy lwip. Zamysł był taki, żeby trzymać tutaj pliki od producenta w takiej samej strukturze folderów, jak oryginalne. Dzięki temu łatwiej jest później aktualizować bibliotekę do nowej wersji. W praktyce jednak takie biblioteki zawierają bardzo dużo niepotrzebnych plików, których nie chcemy trzymać w repozytorium np. przykładowe projekty, dokumentacja, źródła dla różnych platform i kompilatorów itp. Dlatego można z zewnętrznej biblioteki wykroić niepotrzebne pliki.
  • hw – Folder zawierający warstwę obsługi sprzętu (hardware abstraction layer). To tutaj umieszczamy wszystkie drivery operujące bezpośrednio na rejestrach procesora tak, żeby moduły z folderu code nie musiały wnikać w szczegóły sprzętowe.
  • utils – Zawiera użyteczne dodatkowe pliki źródłowe. Tutaj umieścimy pliki z definicjami specyficznymi dla platformy, czy własną implementację funkcji printf.

Folder test dzieli się na podfoldery:

  • unit_test – testy jednostkowe uruchamiane na komputerze PC. Ich zadaniem jest wytropienie błędów logicznych w kodzie produkcyjnym bez potrzeby podłączania docelowego hardware. Unit testy to temat rzeka i na pewno jeszcze będę o nich pisał w przyszłości.
  • hw_test – testy uruchamiane na docelowym sprzęcie. Tutaj możemy umieścić na przykład program do zbierania charakterystyki czujnika, testów komunikacji, czy specjalny program na testy EMC. Dzięki temu nie musimy wprowadzać zmian w głównym projekcie, a do stworzonych w ten sposób programów możemy łatwo wrócić w przyszłości.

Skrypt linkera

Skrypt linkera znajduje się w folderze src, w pliku linker.ld. Definiuje on w jaki sposób kod programu i zmienne są rozmieszczone w pamięci procesora. Na początku pliku znajduje się region MEMORY, gdzie zdefiniowane są adresy początkowe i rozmiary poszczególnych obszarów pamięci procesora. Plik zawiera wartości dla procesora STM32F407VG. W tym procesorze możemy wyróżnić następujące rodzaje pamięci:

  • rom – czyli FLASH. Tutaj umieszczamy wektory przerwań, kod programu i wartości początkowe zmiennych nieinicjalizowanych zerami.
  • ram – czyli zwykła pamięć RAM, w której umieszczamy zmienne, stos i heap.
  • aux_ram – czyli pamięć RAM zoptymalizowana pod kątem użycia przez driver ethernetu. Może również służyć jako zwykły RAM.
  • ccm_ram – pamięć RAM, do której procesor ma szybszy dostęp. Nie można na niej używać DMA.
  • bkp_ram – pamięć z zasilaniem bateryjnym.

Jeżeli chcemy przystosować plik linkera do innego procesora, należy zmodyfikować te wartości. Skrypt linkera udostępnia również zmienne, do których można się potem odwoływać w kodzie. Jedną z takich zmiennych jest __stack_size definiująca rozmiar stosu. Domyślnie rozmiar stosu ustawiony jest na 512 bajtów, w skrypcie linkera można zmieniać tę wartość.

Pliki inicjalizacyjne procesora

Procesor po uruchomieniu nie wykonuje od razu kodu zawartego w funkcji main. Na początku pamięci FLASH jest umieszczony wektor przerwań. Za umieszczenie go w tym miejscu odpowiada skrypt linkera. W wektorze przerwań poza adresami funkcji obsługi przerwań umieszczone są również początkowa wartość wskaźnika stosu i adres funkcji Reset_Handler. To właśnie Reset_Handler jest uruchamiany jako pierwszy po resecie. Jego zadaniem jest wypełnienie pamięci RAM zerami, zainicjowanie zmiennych początkowymi niezerowymi wartościami i wywołanie funkcji main. Pliki zawierające kod inicjalizacyjny znajdują się w folderze src/hw/startup i są to:

  • startup.S – Plik asemblerowy implementujący funkcję Reset_Handler. Umożliwia on utworzenie funkcji low_level_init_0 wywoływanej przed inicjalizacją RAM, i low_level_init_1 wywoływanej po inicjalizacji RAM, ale przed funkcją main.
  • vectors.c – Plik zawierający deklarację wektora przerwań. Tutaj zadeklarowane są wszystkie możliwe funkcje przerwań dla procesora STM32F407VG z atrybutem weak i aliasem Default_Handler. Oznacza to, że jeżeli nigdzie w kodzie nie ma definicji funkcji, w przypadku wywołania przerwania program wejdzie do funkcji Default_Handler. Kolejność funkcji przerwań w wektorze jest zgodna z tabelą Vector Table w rozdziale Interrupts and Events w Reference Manualu procesora. Jeżeli chcieli byśmy przystosować wektor przerwań dla innego procesora, należy zmodyfikować go zgodnie z tabelą. Warto zwrócić uwagę na kolumnę Address w tabeli. Jeżeli kolejne adresy nie są od siebie oddalone o 4, należy w wektorze zostawić te pola puste. Jeżeli tego nie zrobimy, przerwania mogą wywoływać nie te funkcje, które chcieliśmy. Miałem podobny problem z procesorem STM32F401RB.
  • hardfault.c – Implementacja obsługi HardFaulta pozwalająca na podgląd stanu rejestrów procesora w momencie wystąpienia błędu. W momencie wystąpienia błędu kontekst odkładany jest na stosie. Zastosowana wstawka asemblerowa przekazuje te wartości jako argument do funkcji napisanej w C, gdzie możemy podejrzeć wartości tych zmiennych.

W folderze src/hw znajduje się również moduł core_init uruchamiający taktowanie rdzenia sygnałem z PLL o częstotliwości 168MHz przy założeniu, że podłączony jest zewnętrzny kwarc 8MHz. Jeżeli mamy inny procesor, kwarc lub chcemy ustawić inną częstotliwość, należy zmodyfikować ten plik. W folderze src/hw znajduje się jeszcze moduł gpio_f4 zawierający funkcje do obsługi portów IO.

Konfiguracja

Konfiguracja kompilacji jest zawarta w pliku conf.mk, istnieje oddzielny plik conf.mk dla każdego targetu kompilacji. Konfiguracja głównego projektu znajduje się w folderze src. Konfiguracja każdego builda unit testów znajduje się w folderze konkretnego unit testu (w szablonie jest jeden target unit testów – test/unit_test/template). Analogicznie, konfiguracja każdego builda testów hw znajduje się w folderze konretnego testu. Jest jeszcze plik conf.mk dla frameworka do unit testów unity (w folderze test/unit_test/unity). W pliku konfiguracyjnym możemy ustawić między innymi nazwę wynikowych plików, globalne define, ścieżki do plików nagłówkowych i źródłowych. Na końcu każdego pliku conf.mk znajduje się include odpowiedniego pliku build.mk zawierającego właściwą procedurę kompilacji. Dzięki temu ten sam plik build.mk można uruchamiać z różnymi zmiennymi konfiguracyjnymi.

Uruchamianie

Dostępne komendy (aby uruchomić kompilację, należy wpisać komendę w linii komend z głównego folderu projektu):
– make all – buduje wszystkie dostępne targety
– make clean – usuwa wszystkie zbudowane targety
– make main_program – buduje główną aplikację na STM32
– make unit_test – buduje wszystkie unit testy
– make unit_test_<nazwa testu> – buduje konkretny target unit testów
– make hw_test – buduje wszystkie testy hw
– make hw_test_<nazwa testu> – buduje konkretny target testów hw

Aby kompilacja się powiodła potrzebne są:
– arm-none-eabi-gcc – kompilator ARM
– gcc – kompilator na PC, na Windowsie może być MinGW
– msys – komendy linuksowe na Windowsa, można zainstalować razem z MinGW

Konfigurację środowiska opiszę w osobnym artykule.