[STM32] Wyjątki C++ na przykładzie systemu ISIX-RTOS
Jestem zwolennikiem stosowania języka C++ do pisania oprogramowania na mikrokontrolery. Jego umiejętne stosowanie nie powoduje zwiększenia kodu wynikowego, a ścisłe przestrzeganie typów prowadzi do powstawania bardziej niezawodnych i bezpiecznych programów. W niewielkich mikrokontrolerach niskobudżetowych posiadających 32…64 kB pamięci Flash zazwyczaj pomija się zaawansowane mechanizmy prowadzące do zwiększenia zapotrzebowania na pamięć takie jak wyjątki, czy RTTI (Run Time Type Information), tak aby minimalizować zajętość pamięci Flash oraz RAM. Pisząc oprogramowanie dla większych mikrokontrolerów jak np. connectivity line STM32F107 czy performance line (STM32F2/STM32F4), dysponujących pamięciami Flash i SRAM o dużej pojemności możemy pokusić się o wykorzystanie dodatkowych funkcjonalności języka. Jednym z takich mechanizmów pozwalających zwiększyć niezawodność kosztem nieco większej zajętości pamięci jest użycie mechanizmu wyjątków. W artykule pokażemy w jaki sposób wykorzystywać wyjątki C++, w systemie ISIX-RTOS oraz zbadamy ich wydajność.
Obsługa sytuacji wyjątkowych w C
Wyjątki są mechanizmem pozwalającym obsłużyć różne nietypowe sytuacje, które zdarzają się stosunkowo rzadko i są sytuacjami nadzwyczajnymi. Zazwyczaj są to błędy różnej kategorii, z którymi mamy do czynienia podczas działania programu, jak na przykład błąd działania urządzenia, błąd alokacji zasobów np. pamięci itp. W języku C nie istnieje żaden specjalizowany mechanizm obsługi sytuacji wyjątkowych, a do ich obsługi najczęściej wykorzystywana jest wartość zwracana przez funkcję, która jest porównywana z pewnymi wartościami specjalnymi, reprezentującymi błąd. Typowym przykładem jest tutaj alokacja pamięci, która w programie napisanym w C wygląda tak:
struct MyObject *my = malloc( sizeof( struct MyObject) ); if (my == NULL) { //Handle error return ERROR_ALLOC; }
Funkcja malloc, w przypadku niepowodzenia zwraca wartość specjalną NULL, która jest informacją o błędzie przydziału pamięci. Programista musi pamiętać aby wraz z każdym wywołaniem malloc(), zadbać o sprawdzenie czy alokacja powiodła się. Jeżeli w programie występuje wiele takich alokacji, (co jest typowym przypadkiem), to mało komu wystarczy cierpliwości aby wszędzie nieprawidłowe alokację obsłużyć, i jeszcze na dodatek w żadnym miejscu programu nie zapomnieć o sprawdzeniu. Brak sprawdzenia w najlepszym przypadku w systemach operacyjnych z ochroną pamięci doprowadzi do błędu Segmentation Fault”. Przydział pamięci jest najprostszym przypadkiem, spójrzmy na typową sytuację przykładu kodu inicjalizującego układy peryferyjne, gdy jeden zasób potrzebny jest do działania drugiego:
#define OK 0 #define FAIL -1 int init_device1(void) { //Do something init_registers(); unsigned device_bits = read_status_device(); if( device_bits & 0x01 ) return OK; else return FAIL; } void deinit_device1() { //Deinitialize device } int init_all(struct DeviceStruct **dev) { int result; result = init_device1(); if( result == FAIL) return result; result = init_device2(); if( result == FAIL) { deinit_device1(); return FAIL; } /** Init structures **/ *dev = malloc( sizeof( struct devStruct) ); if( *dev == NULL ) { deinit_device1(); deinit_device2(); return FAIL; } } int main() { DeviceStruct *dev; int result = init_app(&dev); if( result == FAIL) { //Handle error } }