Elektroda.pl
Elektroda.pl
X

Search our partners

Find the latest content on electronic components. Datasheets.com
Elektroda.pl
Please add exception to AdBlock for elektroda.pl.
If you watch the ads, you support portal and users.

Przerwania proste i przyjemne - część 3 - kilka dodatkowych tricków i porad

ghost666 20 Apr 2022 07:46 1566 1
Computer Controls
  • Do tej pory omówiliśmy znaczenie ostrożnej obsługi przerwań, metody solidnej strukturyzacji ISR oraz uwagi wymagane do zmiennych globalnych i lokalnych (Link, Link). W tej części zagłębimy się w dodatkowe dobre praktyki.

    Uważaj na przepełnienia bufora danych
    Generalnie używamy programowych buforów do interfejsów komunikacyjnych. Na przykład, mikrokontroler może zapewnić podrzędny interfejs komunikacji szeregowej I²C z 1-bajtowym buforem danych. Należy wziąć pod uwagę, że interfejs I²C działa tak, że przerwanie jest generowane: (a) po otrzymaniu pełnego bajtu zasobów; (b) po uzyskaniu warunku zatrzymania.
    W takich przypadkach chcielibyśmy mieć zadeklarowany bufor programowy, aby za każdym razem, gdy odbierany jest bajt, ISR automatycznie przesyłał dane do bufora. Na ogół jest on implementowany w postaci tablic. Bardzo częstym błędem jest zwiększanie jej indeksu poza dany rozmiar, a takich przepełnień należy unikać. Poniższa tabela przedstawia poprawną i niewłaściwą implementację.

    Przerwania proste i przyjemne - część 3 - kilka dodatkowych tricków i porad


    Warto zauważyć, że powyższy punkt jest zilustrowany na przykładzie I²C, który wymusza przesłanie NACK po odebraniu danych jako wymogu samego protokołu. Ten błąd jest bardziej powszechny podczas korzystania z komunikacji UART, gdzie NACK nie jest niezbędny. W takim ujęciu protokół należy świadomie zdefiniować tak, aby nie wystąpiły warunki przepełnienia bufora. Można to zrobić albo przez przesłanie bajtu wskazującego, że wystąpiła dana sytuacja, albo po prostu, ignorując odebrane zasoby po zapełnieniu bufora, w zależności od aplikacji.

    Odczytaj pamięć współdzieloną od razu
    Tutaj zasada jest taka sama, jak w przypadku odcyfrowywania zmiennych wielobajtowych, które mogą być modyfikowane przez ISR. Jeśli współdzielona pamięć/bufor jest zaimplementowana między ISR a główną procedurą programu, cały bufor powinien być odczytywany naraz, w jednym miejscu. Jeśli dany stan nie zachodzi, możliwe jest rozkodowanie połowy: poprzednich i aktualnych danych, co może doprowadzić do nieoczekiwanych sytuacji. Oto przykład:

    Przerwania proste i przyjemne - część 3 - kilka dodatkowych tricków i porad


    Więcej o buforach
    Poniższe dwa punkty dotyczą ogólnie implementacji buforów. Używanie ich z ISRami związanymi z komunikacją jest bardzo powszechne, ale te małe błędy prowadzą albo do wymiany uszkodzonych danych, albo do niewłaściwej interpretacji odebranych zasobów.

    Poznaj kolejność bajtów w buforach wielobajtowych
    Kolejność odnosi się do tego, jak zmienne wielobajtowe są przechowywane w pamięci o szerokości bajtów. W formacie: „big endian” najbardziej znaczący bajt jest lokowany w pierwszym (najniższy adres). Podczas gdy przy: „little endian” to ten najmniej istotny jest tam umieszczany. Aby zrozumieć dlaczego, należy wiedzieć, jaki jest format danych kompilatora/mikrokontrolera. Rozważmy poniższy przykład macierzy liczb całkowitych przesyłanej z jednego 8-bitowego mikrokontrolera do innego 8-bitowego układu za pomocą interfejsu UART. Analizując całość, weźmy pod uwagę, że: „int” to zmienna 16-bitowa, a kod C nadawania i odbioru używany w dwóch kontrolerach jest następujący:

    Przerwania proste i przyjemne - część 3 - kilka dodatkowych tricków i porad


    Powyższa implementacja byłaby poprawna, gdyby kompilatory dla obu mikrokontrolerów używały tego samego formatu kolejności bajtów do akumulowania zmiennych wielobajtowych w pamięci. Jeśli jednak w przypadku pierwszym format kolejności to little endian (najmniej znaczący bajt), a dla drugiego układu jest to big endian (najbardziej znaczący), RxBuf zawierałby: {0x2211, 0x4433, 0x6655}, zamiast: {0x1122, 0x3344, 0x5566}. Oznacza to, że bajty danych zostały zamienione miejscami, mimo że drugi z układów wysyłał je poprawnie; jeśli zbadamy linię UART, znajdziemy tam prawidłowo przesyłane zasoby, wszakże te w RxBuf nadal będą odwrócone!
    Jeśli znamy format kolejności bajtów, musimy zmienić kod w części Tx lub Rx, aby uniknąć pojawienia się reorientacji.

    Zrozum dopełnienie buforów strukturalnych
    Jeśli nasz mikrokontroler ma wielobajtową architekturę procesora, ale pamięć jest nadal dostępna w kawałkach wielkości bajtów (jak to zwykle ma miejsce), należy uważać na dopełnienie struktury i wyrównanie wykonywane przez kompilator.
    Mikrokontrolery przeważnie wymagają dostosowania danych do naturalnych granic pamięci w celu zapewnienia wydajnego dostępu do niej. Na przykład, 32-bitowy typ zasobów powinien być wyrównany do granic 32-bitowych (słowo), a 16-bitowy do 16-bitowych (słowo). Chociaż kompilator zwykle alokuje poszczególne ujęcia danych na wyrównanych granicach, struktury zasobów często mają elementy o różnych wymaganiach wyrównania. Aby utrzymać prawidłowe, kompilator z reguły wstawia dodatkowe nienazwane faktory danych, by każdy z nich był odpowiednio skoordynowany. Na przykład, jeśli następująca struktura jest zadeklarowana dla procesora o architekturze 16-bitowej, kompilator przechowuje zasoby, jak pokazano poniżej:
    Code: c
    Log in, to see the code

    Przerwania proste i przyjemne - część 3 - kilka dodatkowych tricków i porad

    Wysłanie struktury TxBuf przy wykorzystaniu funkcji nadawczej oznacza przesyłanie niepotrzebnych dodatkowych zasobów, które dodano tam tylko celem uzupełnienia bloku danych, jak zobrazowano to w tabeli powyżej.
    Code: c
    Log in, to see the code

    Zmieniając kolejność elementów w strukturze danych, możliwe jest wyeliminowanie lub przynajmniej redukcja ilości zasobów dodawanych do zmiennych celem uzupełnienia do pełnych słów. Przykład tego przedstawiono poniżej.
    Code: c
    Log in, to see the code

    Możliwe jest również nakazanie kompilatorom C, aby: „pakowały” elementy struktury na rdzeniach procesora, które obsługują dostęp niewyrównany. Należy zapoznać się z podręcznikiem dla określonego kompilatora, by znaleźć słowo kluczowe (tzw. dyrektywę), tak aby żadne zbędne faktory danych nie były dodawane do wyrównania.

    Zachowanie ostrożności podczas wywoływania funkcji w ISR
    Posiadanie wielu wywołań funkcji wewnątrz ISR może prowadzić do nadmiernego zużycia stosu albo z powodu przechowywania SFR lub zmiennych lokalnych (w przypadkach, gdy te są tworzone na stosie). Należy upewnić się, że użytek stosu nie przekracza dostępnych limitów.
    Innym mankamentem jest czas obsługi ISR. Jeśli ten jest problemem, należy użyć makr, zamiast wywołań funkcji. Oszczędza to czas procesora (dla push/pop itp.) oraz zużycie stosu.
    W przypadku bardziej złożonych programów istnieje poważne ryzyko ponownego wejścia w przerwanie. Problemy związane z tym zjawiskiem są powszechne w odniesieniu do kompilatorów, które używają stałych lokalizacji pamięci (jak wyjaśniono w części 2 tej serii), zamiast stosu dla zmiennych lokalnych i argumentów funkcji. Pominięcie jakichkolwiek ostrzeżeń o ponownym wejściu w przerwanie wskazuje, że linker znalazł funkcję, która może być inicjowana zarówno z kodu głównego, jak i z przerwania lub z funkcji wywoływanych przez przerwanie, lub też z wielu z nich w tym samym czasie. Problem w danej sytuacji stanowi to, że funkcja nie jest wtórna i może zostać wywołana, gdy jest już wykonywana. Wynik zostanie zmieniony i prawdopodobnie będzie wiązał się z uszkodzeniem argumentów.
    Kolejną komplikacją jest to, że pamięć używana dla zmiennych lokalnych i argumentów może być nałożona na pamięć odmiennych funkcji. Jeśli ta zostanie wywołana przez przerwanie, to pamięć będzie ponownie wykorzystana. Może to spowodować jej uszkodzenie w odniesieniu do innych funkcji.

    Zrozumieć opóźnienie zadań krytycznych czasowo
    Przerwania o wyższym priorytecie są przetwarzane przez system obsługi przed tymi o niższej randze. Maksymalne opóźnienie jest tutaj sumą czasu wykonania wszystkich ważniejszych przerwań plus największego o mniejszej wadze. Należy zauważyć, że rozpatrywane jest graniczne przerwanie o niższym priorytecie, ponieważ może ono zostać powołane tuż przed wyzwoleniem rozważanego przerwania. W tym przypadku bieżące nie byłoby od razu obsługiwane. Chyba że sterowanie zostanie zwrócone z przerwania o niższym priorytecie.
    Operacja push/pop wykonywana przy wejściu/wyjściu zwiększa opóźnienie obsługi przerwania. Aby utrzymać opóźnienie na jak najniższym poziomie i zgodnie z wymaganiami, upewnij się, że:

    * Przypisano odpowiedni priorytet do przerwań i zminimalizowano czas poświęcony na obsługę poszczególnych z nich w ogólności, jak wyjaśniono w części 1 tej serii;
    * Zmniejszono obciążenie push/pop przed rozpoczęciem/kończeniem wykonywania kodu przerwania. Można to zrobić, używając wyższych poziomów optymalizacji w swoim kompilatorze. W danym ujęciu ten dokonuje operacji push/pop tylko względem tych rejestrów, których dotyczy procedura obsługi przerwań. Optymalizuje to również kod wewnątrz, co pozwala na skrócenie czasu wykonania.

    Niektóre procesory, takie jak 8051, mają wiele banków rejestrów, a każdy z nich ma szereg indeksów ogólnego przeznaczenia. W danym momencie może być aktywny tylko jeden z banków. Niektóre kompilatory zapewniają specjalne atrybuty, takie jak: „using” obsługiwany przez Keil, który można zastosować w definicji funkcji w celu określenia banku rejestrów do użytku przez dane przerwanie. Własności te ułatwiają zarządzanie różnymi bankami zarówno dla kodu przerwania, jak i bez. A zatem powodują zmniejszenie opóźnienia operacji poprzez redukcję liczby akcji push/pop, które należy przeprowadzić przy wejściu czy wyjściu z obsługi przerwania. Gdy stosowane są atrybuty, takie jak: „using”, zadania push/pop są zwykle prokurowane tylko dla rejestrów SFR, a nie indeksów ogólnego przeznaczenia.

    Wykorzystanie przerwania LVD blokującego inne przerwania
    Wiele nowoczesnych mikrokontrolerów zapewnia przerwanie wykrywania niskiego napięcia (LVD), które jest wyzwalane, gdy Vdd spada poniżej określonego poziomu. Jest to system do wychwytywania zapadów napięcia lub tzw. brown-outów. Przerwanie to powinno być o najwyższym priorytecie i może być używane do wykonywania wszelkich operacji awaryjnych przed wyłączeniem mikrokontrolera; na przykład zapisywanie ważnych danych w pamięci EEPROM czy Flash.
    Przerwanie tego rodzaju powinno być funkcją blokującą ISR, w przeciwieństwie do innych ogólnych, aby zapewnić, że mikrokontroler pozostanie w nim i nie powróci do normalnego przepływu programu czy obsługi odmiennych, chyba że napięcie będzie w normie. Można to zrealizować poprzez ciągłe monitorowanie wyjścia komparatora wykrywania niskiego napięcia wewnątrz ISR. Dzieje się tak, ponieważ lepiej NIE wykonywać żadnych operacji w mikrokontrolerze poniżej zalecanego napięcia zasilania. Uczynienie LVD przerwaniem blokującym zapewnia, że koordynaty zasilania nie pozwolą na pracę mikrokontrolera w niekorzystnych warunkach, co może się przełożyć np. na utratę spójności danych.

    Źródło: https://www.embedded.com/interrupts-short-and-simple-part-3-more-interrupt-handling-tips/

    Cool? Ranking DIY
    Can you write similar article? Send message to me and you will get SD card 64GB.
    About Author
    ghost666
    Translator, editor
    Offline 
    Fizyk z wykształcenia. Po zrobieniu doktoratu i dwóch latach pracy na uczelni, przeszedł do sektora prywatnego, gdzie zajmuje się projektowaniem urządzeń elektronicznych i programowaniem. Od 2003 roku na forum Elektroda.pl, od 2008 roku członek zespołu redakcyjnego.
    ghost666 wrote 11006 posts with rating 9358, helped 157 times. Live in city Warszawa. Been with us since 2003 year.
  • Computer Controls
  • #2
    tmf
    Moderator of Microcontroller designs
    W poprzednich odcinkach wiel;e osób zwracało ci uwagę, że tłumaczysz starocie, w dodatku starocie, w któych jest masa błędów i merytorycznych i implementacyjnych.
    Proszę nie brać tych uwag do siebie - krytyka dotyczy autora artykułu, co wynika zapewne z faktu, że od jego napisania upłynęły 2-3 dekady! Niemniej biorąc pod uwagę także krytykę wcześniejszych części zastanowiłbym się nad celowością umieszczania tego na elektrodzie, bez korekty merytorycznej i dostosowania do współczesnych realiów.
    Np. w tej części - pierwszy przykład z buforem - oba przykłady są błędne - brakuje chociażby volatile. Nie wspominasz też o oczywistym rozwiązaniu - buforze kołowym.
    Odczytaj pamięć współdzieloną od razu - też w przykładzie brakuje co najmniej volatile. Ponadto jest to tak ogólnikowe, że aż nieprzydatne.
    Poznaj kolejność bajtów w buforach wielobajtowych - słusznie, że zwrócono na to uwagę, ale nie podano rozwiązań. Więc paragraf mniej więcej bezużyteczny.
    Zrozum dopełnienie buforów strukturalnych - proszę, przetłumacz to jak należy. Bo "wstawia dodatkowe nienazwane faktory danych, by każdy z nich był odpowiednio skoordynowany" mnie kompletnie rozwaliło.. Ale cały ten akapit wymaga mocnej redakcji merytorycznej. Po pierwsze jak wspomniano, kompilatory mają rozszerzenia języka nakazujące pakowanie sruktur. Wiele IDE domyślnie nakazuje kompilatorowi pakowanie. Ale tu pojawia się poważniejszy problem - konieczność poznania architektury. Np. x86 dopuszczają operacje na niewyrównanych danych, ale już wiele ARM tego nie dopuszcza i próba dostępu do danej niewyrównanej zakończy się wyjątkiem. Zmiana kolejności pól struktury (przy okazji masz tam błąd, - skasowałeś i) też nic nie musi dać, bo char po pierwsze ma długość zależną od platformy, po drugie po nim kompilator też może coś wstawić. Tu pomaga tylko doskonała znajomość architektury i użytego kompilatora. Stąd też przesyłanie po prostu sizeof(struct) jest proste i dosyć uniwersalne. Tak przy okazji, skoro w akapicie o endianess liźnięto pewien problem, to dlaczego tu nie wspomniano, że np. int może mieć 16-bitów, 32- lub nawt więcej bitów? Podobny problem dotyczy char, long i itd.Prześlij taką strukturę np. z AVR na ARM, albo z AVR, ARM na PC z 32- lub 64-bitowym OS. Będzie zabawnie.
    Zachowanie ostrożności podczas wywoływania funkcji w ISR - tu już autor jedzie po bandzie. Na stos trzeba uważać, ale obecnie to są raczej jakieś wyjątkowe sytuacje, np. przy korzystaniu z RTOS.
    "Innym mankamentem jest czas obsługi ISR. Jeśli ten jest problemem, należy użyć makr, zamiast wywołań funkcji. Oszczędza to czas procesora (dla push/pop itp.) oraz zużycie stosu." - poważnie? To z całym szcunkiem bajdurzenie. Rozumiem, że autor obawia się wielokrotnych wywołań funkcji i chce spłaszczyć strukturę ISR poprzez osadzenie kodu. Tyle, że to robi kompilator - abo optymalizator, albo mamy rozszerzenia - np. w gcc flatten.
    ghost666 wrote:
    W przypadku bardziej złożonych programów istnieje poważne ryzyko ponownego wejścia w przerwanie. Problemy związane z tym zjawiskiem są powszechne w odniesieniu do kompilatorów, które używają stałych lokalizacji pamięci (jak wyjaśniono w części 2 tej serii), zamiast stosu dla zmiennych lokalnych i argumentów funkcji.
    - po pierwsze to tylko na jakiś unikalnych architekurach. Obecnie większość procesorów nie ma takich ograniczeń. Podobnie nie znam kompilatora dla architektury, która nie ma takich ograniczeń, który by coś tak dziwacznego robił. Ponadto, ponowne wejście w przerwanie chyba dla 99% MCU jest niemnożliwe bez wyraźnej intencji programisty. Zgłoszenie przerwania blokuje zazwyczaj możliwość zgłoszenia tego samego przerwania ponownie, przed wejściem w ISR, oraz dodatkowo blokuje przerwania o tym samym poziomie. Więc bez jawnego odblokowania ze strony programisty, problem właściwie nie istnieje.
    Natomiast to co IMHO w dalszej części jest pokrętnie opisane to problem funkcji non-reentrant. Ale to raczej wynika obnecnie ze stylu pisania kodu a nie ograniczeń platformy. Też wypadałoby to jakoś omówić, bo z tekstu IMHO nic sensownego nie wynika.
    ghost666 wrote:
    Przerwania o wyższym priorytecie są przetwarzane przez system obsługi przed tymi o niższej randze.

    Tu trzeba uważać na terminologię. Czasami priorytet ma znaczenie tylko w sytuacji równoczesnego zgłoszenia dwóch lub więcej przerwań. W takiej sytuacji obsługiwane jako pierwsze jest te o wyższym priorytecie. Na to nakłada się poziom przerwania - przerwanie wyższego poziomu przerywa wykonanie przerwania niższego poziomu.
    ghost666 wrote:
    Uczynienie LVD przerwaniem blokującym zapewnia, że koordynaty zasilania nie pozwolą na pracę mikrokontrolera w niekorzystnych warunkach, co może się przełożyć np. na utratę spójności danych.

    Od tego jest sprzętowy układ BOD. Jeśli zgodzimy się, że działanie MCU jest nieokreślone poniżej pewnego napięcia, to z tego wynika, że ograniczenie wykonywnaia kodu w sytuacji spadku napięcia do odpowiedniego ISR nie ma sensu, bo nie ma gwarancji, że MCU będzie posłusznie ten kod z ISR wykonywał.