Wyślij zapytanie Dołącz do Sii

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).

Tablica wektorów i domyślna funkcja obsługi wyjątków
Rysunek 1. Tablica wektorów i domyślna funkcja obsługi wyjątków.

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.
Przykład wyjątku w funkcji wołanej z innej funkcji
Rysunek 2. Przykład wyjątku w funkcji wołanej z innej funkcji.

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.

Fragment pliku .map zawierającego adresy poszczególnych funkcji
Rysunek 3. Fragment pliku .map zawierającego adresy poszczególnych funkcji.

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
Struktura przykładowego projektu
Rysunek 4. Struktura przykładowego projektu.

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.

HardFault_Handler i przekazanie adresu stosu do funkcji
Rysunek 5. HardFault_Handler i przekazanie adresu stosu do funkcji.

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.

Pobierz przykładowy projekt

.223rem

4.7/5 ( głosy: 14)
Ocena:
4.7/5 ( głosy: 14)
Autor
Avatar
Mateusz Januszkiewicz

Ze światem urządzeń wbudowanych związany od 10 lat. Na co dzień pracuje na stanowisku Architekta Rozwiązań w Centrum Kompetencji Embedded, gdzie ma do czynienia z różnymi ARM-owymi i nie-ARM-owymi platformami oraz dotyka tematów związanych z niskopoziomowym bezpieczeństwem. W wolnym czasie zajmuje się strzelectwem i majsterkowaniem przez duże O.

Zostaw komentarz

Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

  • 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ć.

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?