Autodiagnostyka w systemach embedded

| Technika

Oprogramowanie dla mikrokontrolerów jednoukładowych może być bardzo złożone: wraz z rosnącą mnogością procedur, podprogramów, funkcji i podsystemów rośnie też prawdopodobieństwo, że program zawiedzie w wyniku trywialnego błędu programisty. Jeśli do tej okoliczności dodamy złożoność i związaną z tym potencjalną zawodność samego sprzętu, można dojść do wniosku, że systemom mikroprocesorowym nie można w ogóle zaufać. Z praktyki wynika, że tak jednak nie jest: większość takich urządzeń potrafi całymi latami odznaczać się podziwu godną niezawodnością.

Autodiagnostyka w systemach embedded

Nie jest to bynajmniej dziełem przypadku. Niezawodność to wynik po pierwsze starannego projektowania, a po drugie pogodzenia się z faktem, że zawsze coś gdzieś może zawieść. Ta druga konstatacja pociąga za sobą równie staranne testowanie urządzenia na różnych etapach jego produkcji, a także odpowiednie przygotowanie wewnętrznego oprogramowania, tak żeby było ono w stanie wykryć usterki podczas normalnego działania urządzenia i móc je zasygnalizować użytkownikowi.

Awarie sprzętowe

Istnieją cztery główne komponenty sprzętowe każdego systemu, które mogą ulec awarii. Są to: sam mikroprocesor, urządzenia peryferyjne, pamięć i oprogramowanie.

Awaria samego mikroprocesora (mikrokontrolera) zdarza się raczej rzadko. A kiedy to następuje, jest mało prawdopodobne, żeby uszkodzonemu układowi można było powierzyć wykonanie jakiegokolwiek programu - wliczając w to program mający sprawdzić, czy mikroprocesor jest sprawny. Można wprawdzie myśleć o wykonaniu serii testowych obliczeń zaraz po włączeniu zasilania i porównaniu ich wyników z ustalonym wzorcem - i faktycznie, niekiedy się takie testy robi w ramach diagnostyki sprzętu zaraz po włączeniu zasilania (tzw. POST - Power-On Self Test).

Jednak ułożenie takich testów w ten sposób, żeby wykryć i zdiagnozować każdą, nawet bardzo subtelną usterkę, jest dość trudne. Na dodatek zdarzają się też i takie uszkodzenia, które ujawniają się dopiero po rozgrzaniu się układu. Na szczęście uruchamianie system pociąga za sobą wykonanie przez mikroprocesor bardzo wielu mniej lub bardziej złożonych operacji, można zatem liczyć na to, że awaria CPU, kiedy już nastąpi, będzie na tyle poważna, że urządzenie w ogóle się nie uruchomi, zawieszając się podczas wstępnego rozruchu.

Coraz częstsze stosowanie układów wielordzeniowych otwiera możliwości przeprowadzania wzajemnych testów sprawności przez poszczególne rdzenie CPU. Niestety, w sytuacji gdy wszystkie rdzenie znajdują się wewnątrz jednego układu scalonego, prawdopodobieństwo, że usterka ogranicza się do jednego rdzenia i nie rzutuje na działanie pozostałych, jest bardzo małe.

Urządzenia peryferyjne

System mikroprocesorowy może być wyposażony w dowolną ilość każdego rodzaju układów peryferyjnych (zewnętrznych i wewnętrznych), a każde z nich może w każdym momencie ulec awarii. Peryferie są przeważnie widoczne dla CPU w postaci sprzętowych rejestrów wejściowych, wyjściowych i kontrolnych, znajdujących się pod adresem charakterystycznym dla danego układu peryferyjnego w tym systemie.

Gdy układ I/O "zdycha", jego rejestry na ogół "znikają", to jest stają się niedostępne, a próba odwołania się do nich powoduje wystąpienie stanu wyjątkowego. Korzystając z tej cechy współczesnych systemów mikroprocesorowych, można odpowiednią procedurą wstępnie przetestować urządzenie przynajmniej na okoliczność formalnej obecności wszystkich peryferii, ustawiwszy oczywiście uprzednio wektory stanów wyjątkowych tak, żeby móc stosownie zareagować, gdyby spodziewanych rejestrów nie było pod danym adresem.

Wszystkie inne czynności tego typu są całkowicie zależne od natury danego urządzenia peryferyjnego. Niektóre z nich mogą mieć możliwość ustawienia w tryb loopback, czyli połączenia wejść z wyjściami, dzięki czemu można sprawdzić, czy urządzenie komunikacyjne prawidłowo nadaje i odbiera dane.

W ten sposób sprawdza się działanie układów DMA, mających za zadanie transmitowanie danych z i do pamięci bez aktywnego udziału mikroprocesora: ponieważ układ DMA jest w stanie kopiować dane z pamięci na zewnątrz i z zewnątrz do pamięci, jest też w stanie skopiować dane z jednego miejsca pamięci w drugie: po wykonaniu zatem takiej operacji porównujemy stan bufora źródłowego z docelowym i jeśli są identyczne, test można uznać za zakończony pomyślnie.

Niestety, w przypadku urządzeń stricte komunikacyjnych ta metoda zapewnia jedynie sprawdzenie prawidłowości działania samego układu we/wy, a niekoniecznie - jego zewnętrznych portów komunikacyjnych: tryb loopback łączy co prawda wejścia z wyjściami, ale najczęściej wewnątrz samego układu.

Inne podsystemy (np. dyski) mogą być w ogóle wyposażone przez producenta w mniej lub bardziej złożone tryby samoczynnego testowania. Wyświetlacz można przetestować, wyświetlając na nim jakąś konkretną treść, tak żeby operator mógł od razu zauważyć nieprawidłowości itd.

Pamięć

Pamięci są, wbrew pozorom, podzespołami dość złożonymi, składającymi się z miliardów elementów (bitów) i tym samym bardzo podatnymi na awarie. Wiadomo to z wieloletnich doświadczeń, toteż gdy urządzenie mikroprocesorowe w widoczny sposób zawodzi, pierwszymi podejrzanymi są na ogół właśnie układy pamięci. A i tak, mając na uwadze ilości RAM-u i ROM-u zawarte we współczesnych urządzeniach elektronicznych oraz skalę miniaturyzacji układów półprzewodnikowych, można się dziwić, że pamięci nie ulegają awariom dużo częściej, niż się to dzieje w rzeczywistości.

Istnieją dwa rodzaje awarii, którym może ulec pamięć: nieprawidłowości przejściowe i uszkodzenia permanentne. Te pierwsze powodowane są na ogół przez czynniki zewnętrzne: skoki napięcia, działanie silnych pól magnetycznych, a nawet promieniowania radioaktywnego. Każdy taki czynnik może spowodować, że jeden lub więcej bitów w kostce pamięci RAM zmieni stan w sposób niekontrolowany.

Problem radiacji może się zrazu wydawać egzotyczny, ale takim promieniowaniem jest np. docierające wszędzie promieniowanie kosmiczne. Atmosfera wprawdzie je tłumi, ale nie do zera, a jedynie do poziomu nieszkodliwego dla organizmów żywych. Na jego - znikome, ale jednak - działanie narażone są wszystkie "ziemskie" urządzenia elektroniczne, a szczególnie te, które muszą działać na dużej wysokości, np. wewnątrz lecącego samolotu komunikacyjnego. Ekranowanie na ogół znacznie zmniejsza tego typu oddziaływania, niestety, ze względów praktycznych nie zawsze można się tu zabezpieczyć w stu procentach, bo, dajmy na to, urządzenie przeznaczone dla lotnictwa na ogół nie może być zbyt ciężkie.

Kiedy takie zakłócenie działania pamięci wystąpi, nie ma sposobu, żeby je wykryć i skorygować. Na ogół taka przejściowa awaria objawia się błędem wykonywania programu w wyniku uszkodzenia kodu lub danych, które on przetwarza. Jest to pewien punkt zaczepienia, będzie jeszcze o tym mowa dalej.

Uszkodzenia permanentne mogą się objawiać jako:

  • brak układu pamięci (nie odpowiada on na próby adresowania),
  • bity niedające się zapisać, które mają cały czas wartość 0 albo 1,
  • bity dające się zapisać i odczytać, ale samoczynnie tracące wartość po krótkim czasie,
  • tzw. sklejenie bitów danych, to jest połączenie ich w taki sposób, że zmiana stanu jednego bitu w obrębie słowa skutkuje nieoczekiwaną zmianą stanu innego bitu w obrębie tego samego słowa,
  • sklejenie bitów adresowych; podobnie jak powyżej, z tym że nieoczekiwana zmiana stanu występuje w innym obszarze pamięci,
  • niestabilne odczyty (migotanie bitów).

Awarie pamięci można oczywiście wykryć, przeprowadzając odpowiednie testy zaraz po włączeniu urządzenia. Jest to bardzo odpowiedni moment: po pierwsze, pamięci i związane z nimi układy, jak wszystkie podzespoły elektroniczne, ulegają uszkodzeniom na ogół przy włączaniu zasilania, więc testowanie ich działania od razu przed użyciem ma sens.

Po drugie, pamięć, kiedy nie zawiera żadnych sensownych danych, może zostać poddana dużo solidniejszym testom niż wtedy, kiedy stoimy przed koniecznością przechowania przy tym całej jej zawartości. Niekiedy ma też sens przeprowadzanie serii takich testów w tle, podczas normalnego działania systemu.

Awaria typu pierwszego powoduje wystąpienie stanu wyjątkowego przy próbie dostępu do uszkodzonej (lub wyjętej) pamięci, można zatem to zagadnienie potraktować tak samo jak testowanie obecności urządzeń peryferyjnych (patrz wcześniej).

W urządzeniach mniej zaawansowanych ten problem może jednak wymagać nieco uwagi, gdyż, jeśli brak układu, który próbujemy zaadresować, nie powoduje wystąpienia stanu wyjątkowego, wtedy wykrycie jego nawet fizycznej nieobecności może być nieoczywiste.

Przyczyną jest pewna pojemność elektryczna niepodłączonych do niczego linii danych: jeśli do nieistniejącej pamięci zapiszemy pewną wartość, a potem natychmiast odczytamy ją z powrotem, mamy duże szanse, że odczyt odda nam tę wartość, którą dopiero co zapisaliśmy.

Naiwnie napisany program testujący, który polega na zapisywaniu i odczytywaniu w ten sposób kolejnych wartości dla kolejnych komórek pamięci, może zatem w ogóle nie wykryć, że spodziewana w danym miejscu pamięć RAM fizycznie nie istnieje!

Trzeba tu też zaznaczyć, że z punktu widzenia oprogramowania nie ma większej różnicy, czy za nieprawidłowe działanie pamięci odpowiadają same układy RAM, czy może np. układ adresujący albo w ogóle obwody łączące procesor z pamięcią. Przyczyną sklejenia bitów adresowych czy bitów danych może być tak wewnętrzne uszkodzenie kostki pamięci, jak i wadliwe połączenie (lub po prostu fizyczne zwarcie) na płycie głównej całego urządzenia.

Wszystkie powyższe uwagi dotyczą pamięci RAM. W przypadku pamięci ROM testowanie jest dużo prostsze i polega na obliczeniu sumy kontrolnej jej zawartości, a następnie porównaniu jej z oczekiwaną wartością zapisaną w tejże pamięci. Jeśli nie ma ku temu przeciwwskazań, ten test można przeprowadzić parę razy z rzędu, żeby sprawdzić, czy odczyty są stabilne.

Testy pamięci RAM po włączeniu zasilania

Powszechnie stosowanym testem integralności pamięci jest tak zwana "wędrująca jedynka". Wykrywa się w ten sposób wspomniane wyżej sklejenie bitów danych, gdy ustawianie jednych wpływa na inne. Algorytm wygląda następująco: zerujemy całą pamięć i następnie dla każdego kolejnego bitu, poczynając od najmłodszego:

  • sprawdzamy, czy wszystkie bity są wyzerowane,
  • ustawiamy bit na 1,
  • sprawdzamy, czy jest ustawiony na 1,
  • sprawdzamy, czy wszystkie pozostałe bity są wyzerowane,
  • zerujemy ten bit.

Test wędrującej jedynki można uzupełnić o analogiczny test "wędrującego zera". Różnice polegają na tym, że w pierwszym kroku całą pamięć wypełniamy wartością FFH, a poszczególne bity ustawiamy na 0, począwszy od najstarszego. Ten test, co warto podkreślić, w zasadzie nie testuje samej pamięci, a jedynie prawidłowość działania magistrali danych: w szczególnych przypadkach (ze względu na wspomniane wyżej zagadnienia pojemnościowe) może on dać dobre wyniki nawet w sytuacji, kiedy układy pamięci RAM zostały fizycznie wyjęte z płyty głównej.

Podobny test wykrywa zwarcia na liniach adresowych, skutkujące, jak to już wspomniano wyżej, sklejeniem obszarów pamięci widocznych pod różnymi adresami. Najprostszą metodą jest odmiana wędrującej jedynki przeprowadzana jednak nie na danych, ale na adresach:

  • zerujemy serię bajtów znajdujących się pod adresami będącymi kolejnymi potęgami dwójki (tj. 0, 1, 2, 4, 8, 16, 32, 64 itd.)
  • dla każdego z nich, kolejno: zapisujemy wartość niezerową, sprawdzamy, czy wszystkie inne bajty z tej serii mają wartość zerową, zapisujemy zero.

Trzecią rzeczą do sprawdzenia będzie to, czy pamięć jest w stanie przyjmować i przechowywać dane. Zasadniczo polega to na zapisaniu wybranej wartości do wszystkich komórek pamięci, następnie na odczytaniu wszystkich ze sprawdzeniem, czy wartości się zgadzają. W kolejnym kroku inwertujemy bity we wszystkich komórkach pamięci (operacją EX-OR) i znowu sprawdzamy, czy wartości się zgadzają.

Trzeba przy tym wziąć pod uwagę, że układu pamięci może fizycznie nie być w systemie, toteż najlepiej by było, gdyby zapisywane wartości jakoś zmieniały się wraz z adresami, ale nie odpowiadały im wprost. Najprostszy wybór to zapisanie w pierwszym kroku wartości, które są większe o 1 od ośmiu najmłodszych bitów adresu kolejnych komórek pamięci; tj. pod adres 0 zapisujemy wartość 1, pod adres 1 zapisujemy 2, pod adres 2 - 3 itd. aż do 255, gdzie zapisujemy 0.

Ułożenie programu, który przeprowadza te testy, wymaga pewnej uwagi: nie może on sam używać testowanej pamięci, kod musi się znajdować w pamięci ROM, a wszelkie potrzebne dane trzeba przechowywać wyłącznie w rejestrach CPU. W związku z tym implementacja w języku wysokiego poziomu może być trudna: na przykład język C wymaga stosu, a ten musi znajdować się w pamięci RAM.

Jeśli urządzenie dysponuje różnymi rodzajami pamięci RAM, np. mniejszym obszarem szybkiej pamięci SRAM oraz większym DRAM, można wstępnie przetestować tę pierwszą programem w asemblerze, a tę drugą sprawdzić już programem napisanym w języku C (ze stosem utworzonym w pamięci SRAM).

Znaczne ilości pamięci RAM montowane we współczesnych układach mikroprocesorowych powodują, że te testy wykonują się długo, a to może z kolei skutkować niemożliwym do zaakceptowania opóźnieniem w osiąganiu przez urządzenie gotowości po włączeniu zasilania.

Testowanie można przyspieszyć dzięki znajomości wewnętrznej budowy pamięci: np. sklejenie bitów będzie występowało raczej w obrębie pojedynczego banku, a zatem, jeśli jest ich wiele, można przeprowadzić test oddzielnie dla każdego z nich, a dopiero potem zrobić szybki test ogólny mający wykryć sklejenie samych banków. Algorytm będzie wyglądał mniej więcej tak:

  • wypełniamy całą pamięć zerami
  • dla każdego banku: wypełniamy bank jedynkami, sprawdzamy, czy pozostałe banki nadal zawierają zera i wypełniamy bank zerami.

Testowanie w tle

Kiedy system został już uruchomiony, całościowe testy pamięci, takie jak opisane powyżej, nie są już możliwe. Ale nadal można testować pojedyncze bajty, słowa, a nawet całe strony, o ile tylko charakter urządzenia pozwala na niewielkie przerwy w działaniu całości. Większość systemów mikroprocesorowych miewa dłuższe okresy bezczynności, można takie chwile wykorzystać na czynności związane z zarządzaniem samym systemem, m.in. testy pamięci.

Najprostsze podejście polega na zapisie, odczycie i weryfikacji sekwencji ciągów bitowych, np. samych jedynek, samych zer, naprzemiennie zer i jedynek itp. Oto przykładowy algorytm - dla każdego bajtu:

  • wyłączamy przerwania,
  • zapisujemy wartość bajtu w bezpiecznym miejscu (np. w rejestrze CPU),
  • zapisujemy, odczytujemy i sprawdzamy wartości kolejno: 00, FFH, AAH, 55H,
  • przywracamy poprzednią wartość bajtu,
  • włączamy przerwania.

Realizacja tego zadania w postaci programu może wymagać nieco uwagi, a to ze względu na skłonność kompilatorów np. języka C do eliminacji czynności "niepotrzebnych" (w pojęciu kompilatora, czyli niepowodujących w pozostałym programie żadnych tzw. efektów ubocznych), w tym zwłaszcza dostępów do pamięci.

Awarie oprogramowania

Jak wspomniano, jednym z kluczowych zagadnień, związanych z niezawodnością urządzeń, jest przyjęcie przez projektantów do wiadomości, że wystąpienie wszelkiego rodzaju awarii jest nieuniknione. Takie podejście daje dobre wyniki zwłaszcza w dziedzinie oprogramowania: choćby nie wiem jak długo testowano dany program, zawsze może się okazać, że trudne do wykrycia błędy czają się gdzieś w zupełnie banalnych i na oko nieskomplikowanych sekcjach kodu.

Przewidywanie, jaki błąd może wystąpić, nie jest łatwe, bo kryje w sobie pewien paradoks: żeby przewidzieć możliwe skutki popełnionego przez programistę błędu, trzeba trafnie odgadnąć możliwą naturę tego błędu, a przecież, kiedy się to już odgadnie, można błąd znaleźć i usunąć jeszcze podczas tworzenia programu.

Najprościej jest przyjąć, że, z grubsza rzecz biorąc, istnieją dwa główne typy awarii programu: powodowane przez uszkodzenie danych oraz przez zapętlenie się. Zabezpieczenie się przed jednym i drugim pozwala wykryć większość tego typu awarii od razu gdy takowa wystąpi, zanim dojdzie do większych szkód.

Uszkodzenie danych

Największa zaleta języka C jest przypuszczalnie również najpowszechniejszą przyczyną problemów z programami napisanymi w tym języku. Mowa o wskaźnikach. Nieostrożne użycie wskaźnika może się skończyć nadpisaniem nie tego obszaru pamięci, który nadpisać zamierzano. Problem polega na tym, że wykryć nieprawidłową wartość wskaźnika wcale nie jest łatwo.

Jeśli wskaźnik ma wartość NULL, odwołanie za jego pośrednictwem spowoduje wystąpienie stanu wyjątkowego. Tak samo wystąpienie stanu wyjątkowego na większości systemów spowoduje próba odwołania się do nieistniejącego adresu lub adresu znajdującego się poza obszarem bieżąco przydzielonym aplikacji.

To ostatnie zabezpieczenie zapewnia programowalny układ zarządzania pamięcią (MMU). Sprawdza on, czy adresy generowane przez aplikację mieszczą się w dopuszczalnych dla niej zakresach, to znaczy, w przydzielonych jej obszarach RAM.

Trudność polega jednak na tym, że nawet jeśli adres znajduje się w dopuszczalnym zakresie, niekoniecznie jeszcze oznacza to, że jest taki jak należy. MMU zapobiega wprawdzie uszkodzeniu przez aplikację pamięci czy to systemu, czy to innej aplikacji, ale jest bezradne, gdy aplikacja zaczyna szkodzić sama sobie.

Tu musi wkroczyć programista i zawczasu wbudować w program odpowiednie zabezpieczenia. Istnieją dwa szczególne przypadki, na które powinien przy tym zwrócić baczniejszą uwagę: przepełnienie stosu oraz odwołania do tablic poza zadeklarowanym zakresem.

Alokacja stosu ma w sobie coś z czarnej magii. Mimo istnienia narzędzi do statycznej analizy kodu, podczas pisania programu należy w obliczu takiej konieczności wzmóc czujność, a potem nie zaniedbywać starannego testowania. Można w tym celu oznaczyć granice pamięci przydzielonej na stosie jakimiś umownymi wartościami (tzw. magicznymi) i od czasu do czasu sprawdzać, czy nie zostały naruszone.

Lepiej przy tym unikać wartości skrajnych w rodzaju 0, 1 czy FFFFFFFFH: niech to raczej będzie wartość, której znalezienie się na stosie w sposób naturalny nie będzie się cechowało wysokim prawdopodobieństwem. Na przykład, na stosie często lądują adresy, a adresy są na ogół - ze względów wydajnościowych - wartościami parzystymi (może to jednak nie dotyczyć mikroprocesorów ośmiobitowych).

Dlatego wartość magiczna powinna być liczbą raczej nieparzystą; programiści chętnie wybierają wartości w rodzaju 3735928559, gdzie w zapisie szesnastkowym cyfry A-F układają się w jakieś angielskie wyrazy (w tym przypadku: DEADBEEF). Łatwo wtedy odróżnić je na oko od "normalnych" danych podczas np. oglądania zawartości pamięci przy użyciu debuggera.

Nie oznacza to, że program nie jest w stanie wygenerować takiej wartości w sposób naturalny, ale prawdopodobieństwo tego - a tym samym: oszukania przez kod ewentualnie zastawionej tu pułapki - jest niezbyt duże (około jednej szansy na 4 miliardy, aczkolwiek oczywiście wiele zależy od tego, co program robi). Jak w przypadku testów pamięci, taka wartość magiczna może zostać skontrolowana wtedy, kiedy urządzenie ma okres niewielkiej aktywności, w związku z czym aplikacja nie ma niczego lepszego do roboty.

Jeśli chodzi o dostęp do tablic, niektóre języki programowania oferują mechanizmy automatycznej kontroli takiego dostępu. Powodem, dla którego nie ma takowych w języku C, jest fakt, że taki mechanizm generuje dodatkowy narzut przy każdym dostępie do każdego z elementów tablicy, a to rzadko jest okolicznością pożądaną.

W C++ można wprawdzie przeciążyć operator [] (indeks tablicy), ale takie zabezpieczenie nie zadziała, gdy zamiast odwołania za pomocą indeksu programista - i jest to częsta praktyka - odwoła się do tablicy przez wskaźnik. Jest to możliwe, gdyż w języku C deklaracja tablicy jest w zasadzie deklaracją obszaru pamięci oraz wskaźnika do niego. Dlatego odwołanie w rodzaju: tablica[3] = 255 można zastąpić przez *(tablica+3) = 255.

A zatem, język C pozwala na szybki dostęp do danych, ale w zamian istnieje ryzyko, że wskutek nieuwagi programisty program będzie od czasu do czasu "wyjeżdżał" poza koniec tablicy i fakt ten pozostanie niezauważony. A to z kolei może być powodem naruszenia innych danych, czasem zupełnie niezwiązanych z daną funkcją czy modułem, a w konsekwencji - dziwnego zachowania się programu, tajemniczych "padów" z powodu błędu, którego nie można znaleźć, gdyż, jak się to często zdarza, programiści szukają go nie tam, gdzie rzeczywiście występuje. Przykład:

int tablica[4];
for (i=0; i<=4; i++)
tablica[i] =0;

Naturalne, prawda? Programista na skutek chwilowej nieuwagi zapomniał, że elementy tablicy to 0, 1, 2 i 3 albo też tablica została skrócona w trakcie poprawiania programu, a jeden kawałek kodu przy tym przegapiono.

Żeby wykryć tego typu błąd, można faktycznie zadeklarować nieco więcej pamięci dla tablicy, a na jej końcu umieścić - podobnie jak to było ze stosem - wartość magiczną, którą kod programu będzie od czasu do czasu sprawdzał w chwilach mniejszej aktywności.

Zapętlenia

Testowanie gotowego programu powinno w teorii wyeliminować okoliczności, w których może on wpaść w nieskończoną pętlę. Ale w rzeczywistości trudno jest liczyć na wykrycie wszystkich tego typu sytuacji, jest to truizm dotyczący w ogóle wszystkich możliwych błędów, jakie mogą znajdować się w programie.

Dodatkową trudnością, która często zachodzi w aplikacjach pisanych dla mikroprocesorów jednoukładowych, może być zależność warunku zakończenia pętli od jakiegoś wydarzenia, które jest zewnętrzne w stosunku do testowanego programu - np. od nadejścia danych z urządzenia wejściowego.

W świecie idealnym każda tego typu pętla kończyłaby się automatycznie po upływie zadanego czasu oczekiwania (timeout), ale w świecie realnym, zwłaszcza zaś świecie niewielkich zasobów mikrokontrolerów, nie zawsze jest możliwe zaimplementowanie tego mechanizmu dla każdej, potencjalnie nieskończonej pętli programu. A nawet jeśli taka możliwość istnieje, programista zawsze może z niej nie skorzystać przez np. zapomnienie.

W ostateczności można użyć tak zwanego czuwaka (watchdog); jest to układ, z którym program musi od czasu do czasu się skomunikować, żeby zapobiec jakiejś jego - na ogół brutalnej - interwencji w działanie urządzenia. Gdy program tego nie zrobi przez zadany czas, czuwak generuje przerwanie lub po prostu resetuje całe urządzenie. Kod sygnalizujący czuwakowi, że program działa, powinno się umieścić w jakimś strategicznym miejscu, gwarantującym regularne jego wykonywanie, ale niewpływającym znacząco na wydajność całości.

W systemie wielozadaniowym czuwak może być programem: jednym z procesów, którego jedynym zadaniem jest kontrola stanu innych procesów. Poza tym działanie takiego procesu kontrolnego jest podobne jak czuwaka sprzętowego: proces kontrolny sprawdza stany zestawu znaczników, a sprawdziwszy - kasuje je, natomiast procesy kontrolowane mają obowiązek te znaczniki od czasu do czasu ustawiać. Gdy proces czuwaka wykryje nieustawiony znacznik, wkracza do akcji.

Trzeba podkreślić, że istnienie czuwaka, czy to sprzętowego, czy będącego jednym z procesów systemu, nie może być usprawiedliwieniem dla mniej ścisłej kontroli jakości oprogramowania. Zadaniem czuwaka nie jest naprawa błędów oprogramowania czy kompensacja ich skutków, bo zapętlenie się programu i tak oznacza, że kłopoty już się pojawiły. Czuwak ma za zadanie jedynie minimalizację negatywnych następstw błędu, tak żeby źle działająca aplikacja nie spowodowała w urządzeniu (a może i na zewnątrz jego) jakiejś katastrofy.

Co dalej?

Ustaliliśmy już, że awarię można wykryć na parę sposobów. Ale gdy już ją wykryjemy, co trzeba robić dalej? To bardzo dobre pytanie, a odpowiedź, niestety, zależy od charakteru urządzenia oraz natury działającego na nim oprogramowania.

W świecie mikrokontrolerów jednoukładowych nader często istnieje niestety tylko jedna możliwość reakcji: zresetować mikroprocesor, a co za tym idzie, uruchomić całe urządzenie od początku. To oczywiście powoduje, że przestaje ono na chwilę działać, ale trudno sądzić, żeby uniknięcie tego kosztem dopuszczenia do działania wprawdzie nieprzerwanego, ale nieprawidłowego, było naprawdę lepszą opcją. Weźmy przykład rozrusznika serca: lepiej, żeby zgubił parę uderzeń, czy lepiej, żeby poszedł, mówiąc potocznie, w maliny i całkowicie zakłócił rytm pracy organu?

Czasami zachodzi możliwość powiadomienia operatora lub użytkownika. W idealnym przypadku takie powiadomienie powinno się pojawić, zanim błąd spowodował całkowite zatrzymanie pracy urządzenia - użytkownik ma wtedy trochę czasu na skończenie bieżącego zadania, zanim zresetuje mikrokontroler lub podejmie inne działania naprawcze.

Błędy niewykryte

Niezależnie od tego, jak starannie wszystko zaprojektowano, zbudowano i przetestowano oraz jak wiele włożono wysiłku w obsługę wystąpienia wszelkich możliwych błędów, zawsze istnieje możliwość, że urządzenie się po prostu zawiesi. Ostatnią zatem linią obrony powinno być danie użytkownikowi możliwości zrobienia resetu.

Oddzielny przycisk nie jest tu niezbędnie konieczny, można skorzystać z kombinacji innych, mających skądinąd swoje oddzielne funkcje; ale trzeba mieć na uwadze, że tej operacji nie robi się często, wobec czego użytkownik może nie pamiętać, co w krytycznej sytuacji trzeba nacisnąć. Dlatego specjalny przycisk - nawet jeśli jego naciśnięcie będzie wymagało użycia długopisu lub kawałka drutu ze spinacza - jest tu bardzo pożądany.

Konrad Kokoszkiewicz