logo elektroda
logo elektroda
X
logo elektroda
Adblock/uBlockOrigin/AdGuard mogą powodować znikanie niektórych postów z powodu nowej reguły.

[XMEGA][C]Rekurencyjna funkcja usypiająca mikrokontroler powoduje stack overflow

lupin22 16 Wrz 2021 23:25 651 12
  • #1 19612244
    lupin22
    Poziom 10  
    Cześć,

    spotkałem się właśnie z kolejnym "ciekawym" problemem. W moim urządzeniu po wyłączeniu jest ono wybudzane raz na kilka ms lub za pomocą przerwania z zewnętrznego przycisku. W tym wybudzaniu sprawdzany jest stan baterii i w odpowiedni sposób mrugam ledem. Funkcja konfigurująca zachowanie urządzenia po wyłączeniu wygląda tak:

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    Natomiast funkcja rekurencyjna, która wywołuje tę funkcję i zajmuje się działaniem po wybudzeniu wygląda tak:

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    Czyli prosta sprawa - funkcja usypia mikrokontroler, po wybudzeniu czeka 1us (rodzaj prostego debouncingu dla interruptów z przycisku, które potrafią czasem aktywować się same w wyniku jakichś zakłóceń) wykonuje działania (kod poniżej) i jeśli przycisk nie jest wciśnięty to wywołuje się jeszcze raz, a jeśli jest, to resetuje mcu.

    Problem pojawia się w przypadku kodu wykonywanego w środku tej funkcji. Kod działający wygląda tak:

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    A kod niedziałający tak:

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    Różnica jest tylko jedna w górnym przykładzie do dzielenia i jako max wartości używam 256 (bo to szybkie przesunięcie bitowe), a w dolnym 40. Jeśli w kodzie jest przykład dolny, to mimo, że nie wywołuje się nawet to co w if(!(PORTA.IN & PIN6_bm)) , to po chwili program się restartuje od początku, ale bez restartu mikrokontrolera. Sprawdziłem stany rejestrów i do resetu nie dochodzi. Czyli to musi być stack overflow. Zwłaszcza, że im częściej wybudzam (ustawiając wybudzenie od RTC), tym szybciej dochodzi do tego dziwnego restartu.

    Dlaczego tak się dzieje? Domyślam się, że może to kwestia optymalizacji dokonywanej przez kompilator, że w jednym przypadku trzeba przechowywać jakieś zmienne między wywołaniami, co powoduje przepełnienie stosu, a w drugim nie?

    No i w jaki sposób do tego podejść, żeby problem rozwiązać? Czy da się to zrobić jakoś bez rekurencji, tzn. wykonywać działania co wybudzenie a potem usypiać ponownie? Może jest jakieś oczywiste rozwiązanie, które mi umyka...

    Z góry wielkie dzięki!
  • Pomocny post
    #2 19612340
    JacekCz
    Poziom 42  
    Skąd pomysł do użycia rekurencji?
    Rekurencję się stosuje w algorytmice wyłącznie w bardzo specyficznych sytuacjach, gdzy dziedzina problemu ma pewne cechy, i chyba nigdy nie spotkałem w kontekście uP.

    Przecie da się to napisac if'em i/czy pętlą
  • Pomocny post
    #3 19612442
    excray
    Poziom 41  
    Nic dziwnego, że się zawiesza skoro funkcja rekurencyjna nie ma żadnego sensownego wyjścia z rekurencji. Jedyne co robi, to cyklicznie przy każdym uśpieniu dokłada na stos kolejne dane, aż ten się przepełni. Rekurencja tutaj nie ma żadnego sensownego uzasadnienia - jak wyżej - napisz to bez rekurencji.
  • #4 19612545
    lupin22
    Poziom 10  
    JacekCz napisał:
    Skąd pomysł do użycia rekurencji?


    Wydawało mi się to naturalnym rozwiązaniem i działało przez kilka lat, przy czym nie było wtedy obliczeń, tylko sprawdzenie pinu i ponowne wywołanie albo reset.

    JacekCz napisał:

    Przecie da się to napisac if'em i/czy pętlą


    Rzeczywiście. Jakoś tak bylem zafiksowany na tej rekurencji, że to oczywiste rozwiązanie mi umknęło. Mam naturalną niechęć do pisania pętli potencjalnie nieskończonych na uC, zwłaszcza po wyłączeniu Watchdoga. Chociaż w tym wypadku z wybudzaniem na rtc WD może zostać.

    Przepisałem to tak:

    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    I rzeczywiście nie wydaje się to gorszym rozwiązaniem, a powinno działać identycznie.

    excray napisał:
    Jedyne co robi, to cyklicznie przy każdym uśpieniu dokłada na stos kolejne dane, aż ten się przepełni.


    Tylko dlaczego tych danych nie dokłada w jednym przypadku, a w drugim już tak? Jakieś optymalizacje wykonywane przez kompilator (avr-gcc)?

    Co więcej, jeśli te działania po wybudzeniu wyciągnąłem do zewnętrznej funkcji, to problem też zniknął w obu przypadkach. Myślałem, że skoro nie przekazuję żadnych argumentów do tej funkcji sleep_rec(), to nie powinno być problemu z dokładaniem ciągle nowych danych?
  • Pomocny post
    #5 19612562
    gaskoin
    Poziom 38  
    lupin22 napisał:
    Myślałem, że skoro nie przekazuję żadnych argumentów do tej funkcji sleep_rec(), to nie powinno być problemu z dokładaniem ciągle nowych danych?


    Samo wywołanie też jest odkładane na stos, żeby procesor wiedział gdzie wrócić. W Twoim przypadku nigdy nie wychodzisz z rekurencji więc w 100% przypadków dążysz do przepełnienia stosu. Robiłeś to pewnie tylko szybciej lub wolniej
  • #6 19612590
    lupin22
    Poziom 10  
    gaskoin napisał:
    w 100% przypadków dążysz do przepełnienia stosu.

    Procesor ma 4kB ramu, z czego nieco poniżej 2kB ma permanentnie zajęte. Funkcja sleep_rec() przy ustawieniu RTC na wybudzanie co 2ms wywoływała się więc 500 razy na sekundę. W wersji z liczbą 40 program wykrzaczał się po ok. 2s (czyli by się zgadzało), przeważnie skacząc do początku main, ale nie zawsze, czasem było zupełnie nieprzewidywalne działanie. W wersji z liczbą 256 nic takiego się nie dzieje, może leżeć godzinami i nie wychodzi w tym czasie z rekurencji, bo mogę zrestartować urządzenie przyciskiem wywołując reset softwarowy na końcu tej funkcji. Dlatego myślałem, że może błąd był nie w samym użyciu rekurencji, tylko dotyczył tego, co ona robi w środku.

    excray napisał:
    nie ma żadnego sensownego wyjścia z rekurencji.

    Tak z ciekawości jeszcze - jak to nie ma? Są dwie opcje, albo funkcja wywołuje się rekurencyjnie albo resetuję procek - w takim wypadku nie muszę się chyba przejmować tym, co się działo wcześniej, skoro i tak wszystko będzie "zapomniane" przy resecie.

    Rozumiem już mój błąd z wykorzystaniem rekurencji w ogóle, za wyjaśnienie bardzo dziękuję. Nie wiem skąd te klapki na oczach, że zwykłej pętli nie widziałem.
  • Pomocny post
    #7 19612650
    excray
    Poziom 41  
    lupin22 napisał:
    Tylko dlaczego tych danych nie dokłada w jednym przypadku, a w drugim już tak? Jakieś optymalizacje wykonywane przez kompilator (avr-gcc)?

    Tak jak napisał kolega @gaskoin - w tym drugim przypadku też Ci się pewnie zawiesi program tylko później, bo mniej danych odkładasz na stos. Generalnie dobrym nawykiem jest unikanie ich stosowania wszędzie gdzie tylko się da.
  • Pomocny post
    #8 19612716
    gaskoin
    Poziom 38  
    Żeby nie było - optymalizacja też może wchodzić w grę. Nowoczesne kompilatory potrafią (czasami) rekurencję zamienić na pętlę, ale nie należy zbytnio na tym polegać bo to ruletka. Jak chcesz sprawdzić czy tak się stało dla drugiego przypadku to trzeba porównać asemblery
  • #9 19612766
    lupin22
    Poziom 10  
    gaskoin napisał:
    Jak chcesz sprawdzić czy tak się stało dla drugiego przypadku to trzeba porównać asemblery

    Chyba nic innego być nie mogło, skoro w jednym wypadku jest przepełnienie po kilkuset wywołaniach, a w drugim nie ma przepełnienia po dziesiątkach tysięcy. Rzeczywiście skoro każde wywołanie odkłada na stos adres, to w drugim przypadku efekt powinien być taki sam, a skoro nie jest, to jakoś ta rekurencja musiała zostać ominięta.

    Może byłem na jakiejś granicy, w przypadku dzielenia za pomocą przesunięcia bitowego było jeszcze okej, a przy normalnym dzieleniu już nie bo kompilator zdecydował, że pętla jest mniej opłacalna.
  • #10 19612865
    ex-or
    Poziom 28  
    lupin22 napisał:
    Czy da się to zrobić jakoś bez rekurencji, tzn. wykonywać działania co wybudzenie a potem usypiać ponownie? Może jest jakieś oczywiste rozwiązanie,


    Kod: C / C++
    Zaloguj się, aby zobaczyć kod
  • #11 19613374
    JacekCz
    Poziom 42  
    lupin22 napisał:
    a przy normalnym dzieleniu


    Dzielenie na małych prockach jest zawsze programowe, wywoływana jest ukryta funkcja. Jest to dość kosztowne działanie, w kontekście tego wątku: w stos.

    Dodano po 3 [minuty]:

    gaskoin napisał:
    W Twoim przypadku nigdy nie wychodzisz z rekurencji więc w 100% przypadków dążysz do przepełnienia stosu. Robiłeś to pewnie tylko szybciej lub wolniej


    Język C/C++ ma taką "miła" cechę, że to co normalnie byśmy określili jako błąd, jest UB Undefined Behaviour, czyli bład nie musi się ujawnić w tym samym miejscu i czasie, kiedy zaszedł, ale później, w innym module.
    Albo nawet wcale - co nie jest powodem do radości.

    Przepełnienia stosu nie da się przezyć zbyt długo, ale trochę się da, zalezy jak żywotnie ważne dane będą rozjechane. Jak trafi w mało używane, przejawy błędu się opóźnią.

    Najlepiej egzekwują błędy jezyki na maszynach wirtualnych, ale już np dobra kompilacja Pascala lepiej ograniczała błędy niż C.
  • #12 19613394
    gaskoin
    Poziom 38  
    JacekCz napisał:
    Najlepiej egzekwują błędy jezyki na maszynach wirtualnych, ale już np dobra kompilacja Pascala lepiej ograniczała błędy niż C.


    Niestety Jazelle odeszła w niepamięć :P
  • #13 19613417
    khoam
    Poziom 42  
    JacekCz napisał:
    Język C/C++ ma taką "miła" cechę, że to co normalnie byśmy określili jako błąd, jest UB Undefined Behaviour

    To nie cecha języka, ale niektórych programistów, którzy mają alergię do czytania dokumentacji standardu języka ;)

Podsumowanie tematu

W dyskusji poruszono problem przepełnienia stosu w mikrokontrolerze XMEGA spowodowanego użyciem rekurencyjnej funkcji usypiającej. Użytkownik zauważył, że rekurencja prowadzi do cyklicznego dodawania danych na stos, co skutkuje jego przepełnieniem. Uczestnicy sugerowali, aby zamiast rekurencji zastosować pętlę, co pozwoliłoby uniknąć tego problemu. Użytkownik przekształcił kod, implementując pętlę do zarządzania stanem usypiania, co okazało się skutecznym rozwiązaniem. Wskazano również na różnice w zachowaniu kompilatora, który mógł optymalizować kod w zależności od kontekstu.
Podsumowanie wygenerowane przez model językowy.
REKLAMA