Tworząc systemy embedded musimy zmierzyć się z tymi samymi problemami, co przy tworzeniu innych rodzajów oprogramowania, czyli między innymi:

  • Zmieniającymi się wymaganiami.
  • Zaburzaniem działania istniejących funkcjonalności po wprowadzeniu zmian.
  • Napiętymi terminami.
  • Rosnącym z czasem skomplikowaniem systemu utrudniającym jego utrzymanie i rozszerzanie.

Poza tym istnieje również cała gama dodatkowych problemów, specyficznych dla systemów embedded. Są one związane między innymi z:

  • Potrzebą integracji ze sprzętem.
  • Pisaniem kodu na mikrokontrolery o ograniczonych zasobach
  • Potrzebą spełnienia norm dotyczących niezawodności systemu.
  • Interakcją z zewnętrznymi urządzeniami i pracą w ciężkich warunkach.

W tym wpisie opiszę dokładniej te problemy oraz pokażę jak Test Driven Development może pomóc w ich rozwiązaniu.

Problemy związane ze sprzętem

Podczas prac nad projektem embedded bardzo częsta jest sytuacja, że prace nad softem i hardwarem trwają jednocześnie. Oznacza to, że przez długi czas nie jest możliwe sprawdzenie działania napisanego kodu na docelowej platformie. Co więcej, nawet kiedy płytki PCB są już gotowe, często okazuje się, że jest ich za mało dla całego zespołu i trzeba się nimi dzielić.

Jako, że hardware również jest w trakcie developmentu, na płytce mogą występować błędy. Często widząc niepoprawne zachowanie systemu musimy więc sprawdzić zarówno część programową, jak i sprzętową. Podczas developmentu wpinamy więc się w złącza i korzystamy z dodatkowych przewodów dolutowanych na pająka albo testpadów, aby podejrzeć przebiegi sygnałów. Problemy z takimi debugowymi przewodami i aparaturą pomiarową mogą być kolejnymi źródłami błędów spowalniającymi postępy.

To jeszcze nie wszystko, systemy embedded zwykle współpracują z jakimiś innymi urządzeniami. Muszą więc zbierać dane z czujników, sterować silnikami, nagrzewnicami, zaworami, czy obsługiwać protokoły komunikacyjne. Codziennych warunków pracy takich urządzeń nie jesteśmy więc w stanie dobrze odtworzyć w warunkach laboratoryjnych. Dlatego niektórych sytuacji nie jesteśmy w stanie zasymulować i możemy je wyłapać dopiero w testach na obiekcie. Nawet jeśli mamy dostęp do jakiegoś stanowiska testowego, często chcemy uniknąć częstego uruchamiania na nim systemu ze względu na koszty, hałas, czy obawę przed zepsuciem sprzętu przy niepoprawnym działaniu eksperymentalnego kodu.

Problemy z toolchainem

Aby wgrać kod na mikrokontroler, musimy posłużyć się sprzętowym debuggerem, który fizycznie łączymy z płytką PCB. Czas wgrywania softu w ten sposób oraz debugowania aplikacji krok po kroku może być znaczący. Szczególnie, kiedy musimy powtarzać te czynności wiele razy w ciągu dnia. Poza tym na skutek złego podłączenia debugera, błędu na płytce, czy zwarcia możemy go uszkodzić, co nas dodatkowo spowolni.

Kolejnym problemem są kompilatory na mikrokontrolery. W darmowych wersjach mogą mieć ograniczenia np. na rozmiar kodu, albo dostępne opcje. Natomiast pełna płatna wersja może nie być dostępna na wszystkich maszynach developerskich.

Poza tym takie kompilatory, jak i konkretne modele mikrokontrolerów mają dużo mniejszą bazę użytkowników niż podobne narzędzia na PC.  Konsekwencją tego może być błędne działanie niektórych funkcjonalności na konkretnych konfiguracjach sprzętowych, czy niespójność pomiędzy kompilatorami na różne rodziny procesorów. Inne narzędzia takie jak symulatory, profilery, czy generatory kodu także mogą nie działać poprawnie, szczególnie w mniej popularnych konfiguracjach. Same chipy również zawierają błędy.

Dual targeting

Rozwiązaniem wielu z przedstawionych tutaj problemów jest dual targeting. Jest to technika polegająca na wykorzystywaniu do developmentu różnych platform. Mogą to być eval boardy, symulatory, czy nawet kod uruchamiany na PC. Dzięki temu można pracować nad rozwojem softu nawet jeśli HW nie jest dostępny. Dual targeting wymusza wydzielenie części HW-dependent, dzięki czemu możliwe jest
portowanie kodu na różne platformy.

Oczywiście pełną aplikację ze wszystkimi zależnościami od zewnętrznych układów było by ciężko w ten sposób uruchomić. Jednak aby puścić testy na pewnym fragmencie kodu produkcyjnego nadaje się idealnie. Co ciekawe, warto uruchamiać testy na PC nawet jeśli już mamy do dyspozycji hardware. Dzięki temu szybciej uzyskujemy feedback i możemy upewnić się, czy logika programu działa dobrze.

Należy jednak pamiętać o kilku problemach związanych z dual targetingiem:

  • Poszczególne procesory mogą różnić się peryferiami.
  • Różne kompilatory mogą wspierać różne funkcjonalności języka i mieć własną składnię np. wyrażenia pragma, czy attribute.
  • Kompilatory także mogą mieć bugi.
  • Implementacja funkcji z biblioteki standardowej może się różnić.
  • Architektura procesorów może się różnić – musimy wziąć pod uwagę takie kwestie jak długości zmiennych, endiany, czy inny zestaw instrukcji.

Aby zmniejszyć ryzyko związane z tymi aspektami, należy zastosować odpowiednie polityki związane z kontrolą wersji, Continuous Integration i testami różnych poziomów. Jest to temat na odrębny wpis.

Zalety TDD dla embedded

Dzięki TDD możemy nie tylko rozwijać oprogramowanie nie mając jeszcze gotowego hardware, ale przede wszystkim sprawdzać poprawność jego działania. Początkowa faza developmentu to często pisanie wielkich bloków kodu na sucho, a potem kiedy dostajemy PCB następuje długa faza uruchamiania i integracji. Dopiero wtedy wychodzą wszystkie bugi. Dzięki TDD wiele z nich możemy poprawić dużo wcześniej.

Nie musimy też po każdej zmianie uruchamiać systemu w docelowej konfiguracji narażając się na awarie, koszty i długie czasy uruchamiania. Zamiast tego większość testów wykonujemy lokalnie na swojej maszynie wychwytując różne błędy w logice. Natomiast testy na docelowym sprzęcie mogą odbywać się dużo rzadziej i wyłapywać błędy integracji z zewnętrznymi systemami, wpływ zakłóceń i inne rzeczy, których lokalnie nie sprawdzimy.

Poza tym dzięki unit testom możemy sprawdzić przypadki, które na rzeczywistym sprzęcie są trudne do wywołania i odtworzenia. Możemy więc zasymulować dziwne sekwencje wejściowe, przekłamanie danych, błędy komunikacji itp.

Dzięki zastosowaniu dual targeting i wydzieleniu warstwy sprzętowej od logiki tworzony system jest bardziej portowalny. Jeżeli okaże się, że musimy się przesiąść na większy procesor, albo w ogóle na inną rodzinę, przejście będzie dużo prostsze. Kod będzie również dużo prostszy do przeniesienia do innych projektów.

Podsumowanie

Programowanie embedded niesie za sobą pewne dodatkowe wyzwania nieobecne w zwykłym programowaniu. Dzięki zastosowaniu Test Driven Development jesteśmy w stanie lepiej radzić sobie z nimi radzić dzięki uniezależnieniu się od sprzętu i szybkiej informacji zwrotnej o bugach.