W swojej praktyce projektowej wielokrotnie miałem do czynienia z sytuacją, gdy w prawie gotowym produkcie, którego oprogramowanie składało się z wielu tysięcy linii kodu, nagle zaczynały dziać się dziwne rzeczy.
Opis problemu
Z reguły podczas wykonywania testów długofalowych, gdy urządzenie działało wiele godzin bądź dni, nagle ni stąd ni zowąd dochodziło do resetu. Na dodatek w 9 przypadkach na 10 ów reset miał miejsce w losowym momencie, gdzieś w nocy z soboty na niedzielę. Ale to już składam na karb ogólnej złośliwości rzeczy martwych.
Próby analitycznego rozwiązania problemu z reguły spełzały na niczym. W gąszczu tysięcy linii kodu zwykle nie mogłem znaleźć podejrzanego miejsca, w końcu kod był pisany przeze mnie, a swoje zawsze wygląda dobrze. Na szczęście jest recepta na taki problem. Jeżeli w naszym urządzeniu zastosowaliśmy mikrokontroler oparty o rdzeń Cortex-M3/M4, to możemy skorzystać z pewnego oryginalnego mechanizmu.
O wyjątkach słów kilka
W przypadku rdzenia Cortex M3/M4 jakakolwiek nieprawidłowa operacja:
- próba dostępu do nieistniejącego adresu pamięci,
- odwołanie do wskaźnika zerowego,
- dostęp do niewyrównanego adresu,
powoduje wygenerowanie wyjątku (exception).
Z punktu widzenia samego rdzenia każdy wyjątek (Hard Fault, Mem Manage, Bus Fault, Usage Fault) obsługiwany jest w sposób identyczny jak zwykłe przerwanie – poprzez wywołanie odpowiedniej funkcji obsługi (handlera).
Wskaźniki do wspomnianych funkcji są elementami tablicy wektorów (g_pfnVectors z rysunku 1.), umieszczonej na początku pamięci programu. O samej tablicy wektorów opowiem szerzej w innym artykule.
Domyślna implementacja takiej funkcji obsługi (Default_Handler) zawiera zwykle albo pętlę nieskończoną (przez co wykonywanie programu zostaje wstrzymane aż do chwili reakcji watchdoga) albo procedura sama wymusza reset mikrokontrolera.
W obu przypadkach wszelkie dowody i wskazówki, które pozwoliłyby na znalezienie źródła problemu zostają zniszczone wraz z resetem mikrokontrolera. Wykonywanie programu jest wznawiane w płonnej nadziei, że błąd się już nie powtórzy.
A więc do sedna
W momencie wygenerowania wyjątku rdzeń automatycznie wrzuca na stos osiem czterobajtowych wartości, są to kolejno:
- rejestry ogólnego przeznaczenia R0-R3,
- wskaźnik stosu (R12/SP),
- rejestr powrotu (LR),
- licznik programu (PC),
- rejestr statusu (xPSR).
Wartości te należy kolejno odczytać ze stosu, a następnie:
- wysłać za pośrednictwem interfejsu szeregowego,
- zapisać w pamięci nieulotnej i następnie odczytać po restarcie.
Dysponując wspomnianymi wyżej wartościami rejestrów oraz plikiem .map, będącym jednym z efektów kompilacji, jesteśmy w stanie znaleźć miejsce w kodzie, w którym odbyła się nieprawidłowa operacja. Wskazuje na nie wartość licznika programu PC odczytana ze stosu.
Dodatkowo, znając wartość rejestru powrotu (LR) możemy ustalić z jakiego miejsca w kodzie została wywołana funkcja, w której doszło do wygenerowania wyjątku.
Przykładowy projekt
W załączniku znajduje się przykładowy projekt, wygenerowany za pomocą AC6 Studio na zestaw ewaluacyjny Nucleo L476RG.
W skład projektu wchodzą następujące elementy:
- main.c, zawierający główną pętlę programu oraz implementację HardFault_Handler’a
- Pliki test_functions_one/two/three implementujące funkcje generujące wyjątki
Sednem przykładu jest sposób pobierania wspomnianych wcześniej wartości rejestrów ze stosu. Aby to wykonać należy w pierwszej kolejności ustalić, na który z dwóch stosów (Main Stack czy Process Stack) trafiły. O tych dwóch stosach drogi czytelniku, będziesz mógł przeczytać w innym artykule. Na razie przyjmij proszę do wiadomości, że istnieją.
Po ustaleniu na który stos trafiły nasze dane, należy załadować wartość odpowiedniego wskaźnika stosu (MSP bądź PSP) do rejestru R0 i wywołać funkcję zdejmującą wspomniane dane ze stosu. Wartość wskaźnika stosu zostanie przekazana przez wspomniany rejestr R0.
Podsumowanie
Opisany mechanizm umożliwia szybkie i sprawne debugowanie nawet przypadków uznanych za beznadziejne. Wszystko to dzięki zapisaniu stanu systemu na chwilę wystąpienia wyjątku i powiązania go z konkretnym adresem w pamięci programu. Pozwala to na szybkie dojście po przysłowiowej nitce do kłębka do źródła problemu.
.223rem
Dzięki za bardzo ciekawy tekst. Należy dodać że niektóre narzędzia posiadają wsparcie dla znalezienia właściwej linii kodu w której nastąpił wyjątek. Przykładem może być atollic true studio, który o ile zatrzymamy się w standardowej obsłudze wyjątku (wspomnianej wcześniej pętli while) możemy jednym kliknięciem przejść do linii w której wyjątek wystąpił. Do niedawna obsługiwała to tylko wersja pro, ale zdaje się że w tej chwili atollica przejął ST robiąc z niego standardowe narzędzie do swoich mikrokontrolerów, i od tej pory wszystkie dodatkowe funkcje pro dostępne są za darmo. Pozdrawiam.
Ciekawy artykuł. Inną metodą na zlokalizowanie problemu jest też debug w trybie bez wgrywania programu i bez resetu (attach). Niestety ta metoda nie zadziała jeśli jest włączony watchdog i zresetuje nam program, ale czasem się może przydać.