Tworzenie czytelnego kodu programu dla urządzeń wbudowanych

| Technika

Przy tworzeniu oprogramowania dla systemów wbudowanych zachowanie czytelnej struktury kodu programu jest często pomijane w pogoni za chęcią coraz szybszego wprowadzania produktów na rynek.

Tworzenie czytelnego kodu programu dla urządzeń wbudowanych

O ile brak czytelności kodu nie przeszkadza w małych projektach niepodlegających dalszym modyfikacjom, w przypadku dużych projektów, dopracowywanych i modyfikowanych przez lata, przejrzystość kodu jest bardzo ważna. Niniejszy artykuł przedstawia kilka istotnych zasad decydujących o zapewnieniu niezawodności i czytelności kodu programu.

Jeśli nie musisz, nie programuj w asemblerze

Oczywiście w niektórych obszarach asemblera nie da się uniknąć. Zarówno w kodzie mikroprocesorów 8-bitowych, jak i 32-bitowych ARM-ów znajdziemy fragmenty zapisane w asemblerze, głównie procedury niskiego poziomu. Asembler pozwala na szybki i bezpośredni dostęp do poszczególnych funkcjonalności mikroprocesora. Powoduje jednak nieczytelność kodu i utrudnia jego zrozumienie. Dlatego wdrożenie do projektu nowych pracowników może okazać się bardziej pracochłonne. Języki wyższego poziomu (jak C lub C++) znacznie ułatwiają programowanie.

Używając asemblera, bardzo łatwo naruszyć strukturę programu napisanego w języku wyższego poziomu. Warto więc, aby każdy blok asemblera był dobrze opisany. Poszczególne bloki kodu opatrzone komentarzem nie powinny zawierać więcej niż 5–6 instrukcji.

Uważaj na położenie komentarzy

Rys. 1. Przykład jak komentarz może przemieszczać się w kodzie, stając się z czasem nieprzydatnymJest to generalna zasada programowania, ważna szczególnie w przypadku dużych projektów, które będą rozwijane przez lata. Pisząc własny kod, pamiętamy o pisaniu komentarzy, jednak często zapomina się o nich, pracując na starym kodzie. Przykład jak komentarz może przemieszczać się w kodzie, stając się z czasem nieprzydatnym lub nawet mylnym elementem, zilustrowano na rysunku 1. Jak widać, komentarz do funkcji add przesunął się na początek listingu, a odpowiadająca mu funkcja znajduje się na końcu. Taka sytuacja może się pojawić z biegiem czasu, jeśli pomiędzy komentarzem i funkcją występuje przerwa.

W tym przypadku przyczyną mogło być umieszczenie funkcji printNumber pomiędzy instrukcją add i jej komentarzem. Z biegiem czasu ktoś zauważył instrukcję dodawania i wydawało mu się logiczne umieszczenie obok instrukcji mnożenia. Dobrym sposobem na uniknięcie podobnych sytuacji jest wyraźne zaznaczanie komentarzy przez stosowanie ramek lub linii.

Nie optymalizuj zbyt wcześnie

Poważnym błędem, popełnianym najczęściej przez nadgorliwych programistów, jest zbyt wczesna optymalizacja kodu. Duży projekt powinien być pisany jak najbardziej przejrzyście. Często warto poświęcić wyższą wydajność zachowaniu prostoty kodu. Najskuteczniej przeprowadza się optymalizację całego modułu po zakończeniu testowania i debugowania. Narzędzia, takie jak Gprof lub Vtune, potrafią wskazać najmniej optymalne miejsca w kodzie, na których warto się skupić. Dlatego optymalizacja podczas rozwijania kodu może okazać się mało efektywna.

Stosuj proste procedury przerwań

Procedury przerwań powinny być jak najprostsze, zarówno ze względu na czytelność kodu, jak i parametry. Przerwania są z natury asynchroniczne i jako takie są trudniejsze w debugowaniu od regularnego kodu.

W miarę możliwości należy spróbować przenieść wszelkie zadania związane z przetwarzaniem danych z procedury obsługi przerwania do głównego programu. Procedura obsługi przerwania powinna jedynie pobierać dane (np. z urządzeń) i umieszczać je w buforze do późniejszego wykorzystania. Proste ustawienie flagi pozwala poinformować program główny o dostępności nowych danych do przetwarzania.

Nie usuwaj procedur testujących

W systemach embedded stosuje się różne praktyki debugowania i testowania kodu. Miganie diodami, wysyłanie komunikatów przez UART lub wyświetlanie ich na wyświetlaczu bardzo ułatwia poszukiwanie błędów. Kod ten (bardzo pomocny podczas prac nad systemem) jest zbędny w gotowym produkcie. Jednak usuwanie części kodu w końcowej fazie projektu jest czasochłonne i co ważniejsze, może spowodować nowe błędy. Są różne sposoby na uniknięcie podwójnej pracy: można zastosować kompilację warunkową lub implementować kod testowy w oddzielnej bibliotece i nie linkować jej z kodem produkcyjnym. Niestety, usunięcie kodu testującego może spowodować nieprzewidziane skutki w zależnościach czasowych, szczególnie w małych systemach.

Redefiniuj wywołania systemowe

W ramach zachowania czytelności kodu warto oddzielić procedury niskiego poziomu od programu wyższego poziomu poprzez wprowadzenie interfejsów typu wrapper, ponieważ struktura „monolityczna” może być bardzo trudna do zarządzania. Umieszczenie wszystkich funkcjonalności aplikacji w kilku dużych funkcjach utrudnia zrozumienie kodu oraz jego uaktualnianie i debugowanie, zwłaszcza mając do czynienia z interfejsami sprzętowymi.

Nawet gdy mamy bezpośredni dostęp do sprzętowych rejestrów, linii I/O czy nawet API dostarczanego przez producenta danej platformy, zawsze lepiej stworzyć własny interfejs typu wrapper. Pozwoli to na zachowanie lepszej spójności kodu i ułatwi przyszłe aktualizacje.

Funkcjonalność w pojedynczych modułach

W systemach embedded, w odróżnieniu od pecetów, działanie aplikacji w dużym stopniu zależy od systemu, na którym będzie ona uruchamiana. Projektując warstwę systemową warto wzorować się na architekturze mikroprocesora. Bloki oprogramowania powinny odwzorowywać moduły funkcjonalne procesora. Stosowanie zbędnych zależności między modułami utrudni debugowanie.

Dokumentacja

Dokumentację trzymaj razem z kodem źródłowym, a jeśli to możliwe, również ze sprzętem. Jeśli używasz systemu kontroli wersji (tj. Subversion, CVS, ClearCase), nie ma najmniejszego problemu z przechowywaniem kodu i dokumentacji w tym samym katalogu. Twoi następcy będą wdzięczni, mając do dyspozycji komplet dokumentacji wraz ze wszystkimi płytami CD, narzędziami uruchomieniowymi i sprzętem.

Nie stosuj sztuczek

Programowanie w C daje bardzo duże możliwości. Jeden problem można rozwiązać na wiele różnych sposobów, które różnią się szybkością wykonania, wielkością zajmowanej pamięci oraz przejrzystością. W przypadku większych projektów, nad którymi pracuje wiele osób, najważniejsza powinna być przejrzystość kodu. Zgodnie z jedną z wcześniejszych rad należy unikać zbyt wczesnej optymalizacji kodu. Pisząc program, powinniśmy dbać o czytelność kodu i stosować najprostsze rozwiązania.

Zdecyduj się na jedną strukturę zawartości plików w całym projekcie

Bardzo dobrą taktyką jest stosowanie identycznych wzorców plików źródłowych i nagłówkowych. Wszystkie makrodefinicje, deklaracje funkcji czy zmiennych globalnych powinny być umieszczane w takiej samej kolejności i zgodnie z taką samą zasadą w całym projekcie.

Bartłomiej Grześkowiak