Programowanie STM32 w C++ – organizacja kodu
Programowanie mikrokontrolerów w C++ wymaga nie tylko znajomości samego języka, ale także umiejętnego organizowania kodu w plikach. Odpowiednia struktura projektu ułatwia rozwój oprogramowania, poprawia jego czytelność i pozwala na lepsze zarządzanie zależnościami. W świecie embedded, gdzie zasoby są ograniczone, a kod często musi być maksymalnie zoptymalizowany, właściwy podział na pliki źródłowe i nagłówkowe ma kluczowe znaczenie.
W tym artykule omówię, jakie typy plików występują w projektach C++, jakie pełnią funkcje oraz jak je poprawnie organizować. Dowiesz się, czym różnią się pliki .hpp i .cpp, dlaczego warto stosować ochronę przed wielokrotnym dołączaniem oraz jak unikać błędów. Dzięki temu Twój kod stanie się bardziej przejrzysty, modułowy i łatwiejszy do utrzymania.

Rozszerzenia plików w projektach C++
Zanim przejdziemy do organizacji kodu, warto przyjrzeć się rozszerzeniom plików używanym w C++. Każdy plik w projekcie ma określoną rolę, a jego rozszerzenie często sugeruje jego przeznaczenie. Choć w większości przypadków stosuje się standardowe rozwiązania, w niektórych projektach można spotkać mniej popularne warianty. Wybór odpowiedniego rozszerzenia nie tylko wpływa na czytelność kodu, ale także ułatwia pracę kompilatora i narzędzi do budowania projektu. Sprawdźmy więc, jakie rozszerzenia są najczęściej stosowane w C++ i kiedy warto ich używać.
Pliki źródłowe
Pliki źródłowe w C++ najczęściej zapisuje się z rozszerzeniem .cpp. Jest to powszechnie przyjęty standard, obsługiwany przez wszystkie współczesne kompilatory. Oprócz niego można spotkać także inne rozszerzenia, takie jak .cc, które jest popularne zwłaszcza w środowiskach uniksowych, czy .cxx, które było częściej używane we wczesnych latach rozwoju C++. Istnieje także mniej znane rozszerzenie .C (wielka litera „C”), które w systemach Unix jest traktowane jako plik źródłowy C++, ponieważ Unix rozróżnia wielkość liter w nazwach plików. Jednak w praktyce, dla zachowania spójności i uniknięcia niepotrzebnych problemów, najlepszym wyborem pozostaje .cpp.
| Rozszerzenie pliku | Opis |
|---|---|
.cpp | Najczęściej stosowane, większość kompilatorów i narzędzi domyślnie rozpoznaje ten format. |
.cc | Alternatywne rozszerzenie, stosowane zwłaszcza w systemach uniksowych. |
.cxx | Historyczne rozszerzenie, stosowane w starszych projektach C++. |
.C (duża litera) | Rzadziej używane rozszerzenie, które w systemach Unix jest interpretowane jako plik C++ (ze względu na rozróżnianie wielkości liter). |
Pliki nagłówkowe
Podobnie jak pliki źródłowe, pliki nagłówkowe mogą mieć różne rozszerzenia, w zależności od stylu i wymagań projektu. Najczęściej stosuje się .h, które jest uniwersalne i spotykane zarówno w C, jak i w C++. Pliki te zawierają deklaracje funkcji, klas, zmiennych globalnych oraz makr preprocesora. W projektach stricte C++ coraz częściej stosuje się jednak .hpp, które pozwala jednoznacznie odróżnić nagłówki przeznaczone wyłącznie dla tego języka. Rzadziej spotyka się inne rozszerzenia, takie jak .hh czy .hxx, które nie zyskały większej popularności poza niektórymi środowiskami programistycznymi.
| Rozszerzenie pliku | Opis |
|---|---|
.h | Klasyczne rozszerzenie, stosowane zarówno w C, jak i C++. Powszechnie używane w projektach, które wymagają kompatybilności między tymi językami. |
.hpp | Rozszerzenie bardziej charakterystyczne dla C++, używane w projektach, które nie muszą zachowywać kompatybilności z C. |
.hh, .hxx | Rzadsze warianty, które można znaleźć w niektórych środowiskach programistycznych, ale nie są powszechnie stosowane. |
Kiedy stosować .h, a kiedy .hpp?
Jeśli plik nagłówkowy ma być używany zarówno w C, jak i C++, lepiej trzymać się .h. Jest to także dobry wybór w starszych projektach lub tam, gdzie wymagana jest kompatybilność z kodem C, np. przy korzystaniu z extern "C". Z kolei w nowoczesnych projektach C++ rozszerzenie .hpp pozwala jednoznacznie zaznaczyć, że dany plik zawiera deklaracje specyficzne dla tego języka, co może pomóc w uniknięciu nieporozumień.
A co z projektem dla STM32?
W projekcie STM32 z C++ najlepiej używać .h do nagłówków zgodnych z C oraz .hpp dla kodu typowego dla C++. Pliki .h są konieczne do integracji z biblioteką HAL, która jest napisana w C. Aby poprawnie dołączać nagłówki HAL w C++, należy użyć extern "C", aby uniknąć problemów z linkowaniem. Jeśli piszesz własne sterowniki lub warstwy abstrakcji w C++, lepiej stosować .hpp, ponieważ jasno wskazuje to na kod przeznaczony wyłącznie dla C++.
Pliki .hpp mogą zawierać klasy, przestrzenie nazw i szablony, co ułatwia organizację kodu w nowoczesnym stylu. Dzięki temu możliwe jest korzystanie z obiektowego podejścia do obsługi peryferiów, jednocześnie zachowując kompatybilność z bibliotekami HAL. Ważne jest także, aby pliki .hpp nie były bezpośrednio dołączane do kodu w C, ponieważ nie są z nim zgodne. W większych projektach można stosować dodatkową warstwę abstrakcji, gdzie nagłówki .h stanowią interfejs między HAL a kodem C++. Taki podział sprawia, że kod jest bardziej modularny, czytelny i łatwiejszy do utrzymania. Dzięki temu można w pełni wykorzystać możliwości C++, jednocześnie integrując się z niskopoziomowymi bibliotekami STM32.
Poprawny dobór rozszerzeń plików to pierwszy krok do dobrej organizacji kodu. W kolejnej części artykułu omówimy, jak strukturyzować pliki i katalogi w projektach dla mikrokontrolerów, aby zapewnić przejrzystość i łatwość rozbudowy kodu.
Co umieścić w pliku .cpp i .hpp?
Pliki .cpp i .hpp w projektach C++ pełnią kluczowe role i powinny zawierać odpowiednie elementy kodu, aby zachować przejrzystość oraz poprawność kompilacji. Ich podział wynika z zasad modularności i ponownego wykorzystania kodu, co jest szczególnie istotne w większych projektach. Poprawna organizacja tych plików pozwala uniknąć problemów związanych z wielokrotnym dołączaniem i przyspiesza proces kompilacji.
Plik .hpp (lub .h, jeśli używamy bardziej klasycznej konwencji) zawiera deklaracje – czyli informacje o funkcjach, klasach, zmiennych globalnych i stałych, które mogą być używane w innych plikach. To tutaj definiuje się interfejsy modułów, które później są implementowane w pliku źródłowym. Przykładowo, jeśli tworzymy klasę do obsługi diody LED w mikrokontrolerze, jej deklaracja (sygnatury metod i zmienne członkowskie) znajdzie się w pliku led.hpp.
#include "gpio.h"
class led {
private:
GPIO_TypeDef* const port;
const uint16_t pin;
public:
led(GPIO_TypeDef *gpioPort, uint16_t gpioPin) : port(gpioPort), pin(gpioPin) {}
void on();
void off();
void toggle();
};
W pliku .cpp znajduje się natomiast właściwa implementacja funkcji i metod zadeklarowanych w nagłówku. Dzięki temu kod źródłowy jest podzielony na logiczne części, a kompilator może skutecznie łączyć poszczególne moduły. Oddzielenie deklaracji od definicji sprawia, że zmiany w implementacji nie wymagają ponownej kompilacji wszystkich plików, które korzystają z nagłówka – wystarczy skompilować zmieniony plik .cpp. To znacząco przyspiesza czas kompilacji w większych projektach.
#include "led.hpp"
#include "gpio.h"
void led::on()
{
HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET);
}
void led::off()
{
HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);
}
void led::toggle()
{
HAL_GPIO_TogglePin(port, pin);
}
Warto porównać ten podział do języka C, gdzie również stosuje się pliki .h i .c. W C nagłówki zawierają głównie deklaracje funkcji i zmiennych globalnych, natomiast implementacja znajduje się w plikach źródłowych. Podobnie jak w C++, deklaracje funkcji w pliku nagłówkowym umożliwiają ich użycie w różnych modułach programu. Jednak w C++ pliki nagłówkowe często zawierają także definicje klas, przestrzeni nazw oraz szablonów.
Jednym z istotnych problemów, które mogą pojawić się w związku z plikami nagłówkowymi, jest wielokrotne dołączanie tego samego nagłówka. Aby temu zapobiec, stosuje się zabezpieczenia preprocesora: albo klasyczne #ifndef ... #define ... #endif, albo bardziej nowoczesne i czytelne #pragma once. Pozwala to uniknąć błędów podczas kompilacji, takich jak wielokrotne deklarowanie tych samych symboli. Ten sposób jest identyczny, jak w przypadku języka C, dlatego powinno być dla Ciebie dość naturalne i łatwe w użyciu.
Ważne jest również, aby w plikach .hpp unikać umieszczania definicji funkcji, z wyjątkiem tych, które są oznaczone jako inline, oraz funkcji szablonowych. Umieszczanie pełnych definicji w nagłówku prowadzi do problemów z wielokrotną definicją symboli przy kompilacji, co może powodować błędy linkera. Z tego względu zawsze lepiej trzymać się zasady: deklaracje w plikach nagłówkowych, a implementacja w plikach źródłowych.
ifndef … #define … #endif czy #pragma once?
Przy okazji omawiania plików .h oraz .hpp, chciałbym trochę więcej miejsca poświęcić sposobom użycia #ifndef ... #define ... #endif oraz #pragma once. Spróbujmy bliżej przyjrzeć się obu rozwiązaniom i zastanowić, które jest lepsze i w jakich okolicznościach.
Zarówno klasyczne podejście z użyciem #ifndef ... #define ... #endif, jak i nowoczesne #pragma once służą temu samemu celowi – zapobiegają wielokrotnemu dołączaniu tego samego pliku nagłówkowego do projektu. Jednakże istnieją pewne różnice w ich działaniu, zaletach i wadach.
Klasyczne podejście z #ifndef … #define … #endif
Jest to starsza i bardziej uniwersalna metoda, która działa na poziomie preprocesora. Dzięki niej możemy zapobiec wielokrotnemu dołączaniu plików nagłówkowych, stosując dyrektywę preprocesora:
#ifndef NAZWA_NAGLOWKA_H
#define NAZWA_NAGLOWKA_H
// kod nagłówka
#endif
Zalety:
- Kompatybilność – działa we wszystkich kompilatorach C i C++, więc jest to rozwiązanie bardziej uniwersalne, niezależne od platformy i kompilatora.
- Kontrola – możemy ręcznie nazwać makro ochrony, co pozwala na dokładniejszą kontrolę nad nazwą identyfikatora, co jest szczególnie przydatne w większych projektach, gdzie mogą występować złożone struktury plików.
Wady:
- Większa ilość kodu – jest to rozwiązanie starsze, które wymaga pisania dodatkowego kodu, co może sprawić, że plik staje się mniej czytelny, zwłaszcza w przypadku dużych plików nagłówkowych.
- Podatne na błędy – zapomnienie zdefiniowania makra lub pomyłki przy nazwie makra, co może prowadzić do problemów z kompilacją.
Nowoczesne podejście z #pragma once
#pragma once to dyrektywa preprocesora, która informuje kompilator, aby załadował dany plik nagłówkowy tylko raz podczas procesu kompilacji. Jest to bardziej elegancka i prostsza w użyciu metoda, która wygląda następująco:
#pragma once
// kod nagłówka
Zalety:
- Prostota –
#pragma oncejest bardzo prostą i czytelną dyrektywą, która znacząco upraszcza kod nagłówka, eliminując konieczność definiowania makr. - Szybkość – kompilatory mogą szybko wykryć, czy plik został już załadowany, co może przyspieszyć czas kompilacji, szczególnie w dużych projektach.
- Brak ryzyka błędów – ponieważ nie musimy ręcznie wymyślać nazw makr ochronnych, unika się typowych błędów związanych z literówkami w nazwach.
Wady:
- Kompatybilność – choć
#pragma oncejest obsługiwane przez większość współczesnych kompilatorów (np. GCC, Clang, MSVC), nie jest to rozwiązanie standardowe w C++ i może nie być obsługiwane przez starsze kompilatory lub specyficzne platformy. - Brak kontroli nad nazwą – kompilator sam decyduje o tym, kiedy plik zostanie załadowany, więc nie mamy pełnej kontroli nad tym procesem, jak w przypadku użycia
#ifndef. W bardzo specyficznych przypadkach może to prowadzić do problemów z rozpoznawaniem plików w bardzo złożonych systemach.
Co wybrać: ifndef … #define … #endif czy #pragma once?
W nowoczesnych projektach C++ najlepiej używać #pragma once, szczególnie jeśli masz kontrolę nad środowiskiem kompilacyjnym i używasz popularnych kompilatorów. Jest to bardziej elegancka, łatwiejsza do zrozumienia i szybsza metoda. Jeśli jednak musisz zapewnić kompatybilność z wieloma kompilatorami, starszymi wersjami czy specyficznymi platformami, #ifndef ... #define ... #endif pozostaje bezpiecznym wyborem. W każdym przypadku, najważniejsze jest, aby utrzymać spójność w całym projekcie.
W przypadku projektów z STM32 stosuje się zazwyczaj starszą konwencję z #ifndef ... #define ... #endif. Jest to podyktowane tym, że STM32CubeIDE przy tworzeniu plików .h lub .hpp automatycznie dodaje nam w ten sposób ochronę plików nagłówkowych. Poza tym biblioteki wykorzystują tę formę i większość osób chce zachować jednolitość rozwiązania. Nic nie stoi jednak na przeszkodzie, aby w projekcie mieszać oba style – nie jest to zabronione i projekt skompiluje się poprawnie.
Podsumowanie
Poprawna organizacja plików w C++ ma kluczowe znaczenie dla czytelności, modularności i efektywności kompilacji kodu, zwłaszcza w projektach dla mikrokontrolerów. Stosowanie odpowiednich rozszerzeń plików, takich jak .cpp dla kodu źródłowego i .hpp dla nagłówków, pozwala uniknąć problemów związanych z wielokrotnym dołączaniem oraz błędami linkera. Rozdzielenie deklaracji od definicji ułatwia zarządzanie kodem i przyspiesza proces kompilacji w większych projektach.
Warto także stosować zabezpieczenia przed wielokrotnym dołączaniem plików nagłówkowych, takie jak #pragma once lub klasyczne #ifndef. Chociaż C++ wywodzi się z C, organizacja kodu w tych językach różni się pod względem stosowania plików nagłówkowych i definicji klas. Kluczowe jest zachowanie spójności w całym projekcie oraz stosowanie najlepszych praktyk, aby kod był łatwy w utrzymaniu i rozwijaniu. Dzięki dobrze zorganizowanej strukturze plików można uniknąć wielu problemów i tworzyć bardziej niezawodne oprogramowanie.

Cykl będzie kontynuowany?
Tak, trochę miałem w ostatnim czasie sporo na głowie, ale powoli ogarniam się i wracam do pisania – mam nadzieję, ze regularnego. Dzisiaj pojawi się kolejny artykuł o C++.