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

Przerwania proste i przyjemne - część 2 - zmienne, bufory i latencja

ghost666 03 Mar 2022 11:32 2550 18
  • W pierwszej części tej serii omówiliśmy znaczenie ostrożnej obsługi przerwań i kilka ogólnych praktyk związanych z niezawodną strukturą ISR-ów. Teraz przedstawimy implikacje zbyt liberalnego użycia zmiennych globalnych i lokalnych w ISR.

    Zmienne globalne — wiedz, kiedy są modyfikowane

    Zmienne globalne muszą być ostrożnie obsługiwane, gdy są używane z przerwaniami, ponieważ te są generalnie asynchroniczne. I jeżeli zmienna globalna jest modyfikowana przez ISR, może zostać zmieniona w dowolnym momencie. Musimy mieć na uwadze następujące aspekty:

    Odczyt/zapis zmiennych globalnych w wielu miejscach

    Zmienne globalne powinny być modyfikowane tylko w kilku koniecznych obszarach wewnątrz programu. Gdyż, jeśli odbywa się to w oparciu o wiele wątków (tj. poprzez główny proces, ISR i inne funkcje sprzętowe, takie jak DMA w mikrokontrolerze), istnieje ryzyko uszkodzenia zmiennej w pamięci.

    Odczytywanie zmiennej globalnej w wielu instancjach wewnątrz procesu głównego

    Druga sytuacja jest równie niepokojąca, jak jej modyfikacja w różnych wątkach. Rozważmy następujący przykład:

    Przerwania proste i przyjemne - część 2 - zmienne, bufory i latencja


    Tutaj w case1, jeżeli ISR wystąpił podczas wykonywania instrukcji przez CPU:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


    I jeśli Command == 1, CPU wyśle do zmiennej dane równe 3, podczas gdy powinno być 1. Problem ten nie wydarzy się w case2, wspomnianym powyżej.

    Uzyskiwanie dostępu do wielobajtowych zmiennych globalnych w systemie 8-bitowym

    Korzystanie z wielobajtowych zmiennych globalnych w systemie 8-bitowym wymaga szczególnej uwagi. Gdyż zmienne wielobajtowe są odczytywane z pamięci bajt po bajcie. Należy uważać, aby ISR nie wystąpił i nie zmodyfikował zmiennej wielobajtowej, kiedy jeden lub więcej bajtów zostało już rozszyfrowanych, podczas gdy odczyt nie jest zakończony. Prowadziłoby to do uszkodzenia danych. Poniższy przykład ilustruje ten scenariusz:

    Przerwania proste i przyjemne - część 2 - zmienne, bufory i latencja


    Należy zauważyć, że powyższe dwa przypadki zakładają, że I2C_ISR jest przetwarzane natychmiast po wystąpieniu; tj. ISR jest obsługiwany, a dane są odczytywane z bufora, zanim I2C zacznie ponownie aktualizować I2C_BUF (tj. przed odebraniem kolejnych 8 bitów). Założenie to będzie w większości przypadków prawdziwe, biorąc pod uwagę, że I²C działa ze znacznie mniejszą prędkością (np. 100 kHz) w porównaniu do procesora (ogólnie od pojedynczych do setek MHz). I nie ma innych przerwań w systemie lub też całkowite opóźnienie spowodowane przez wszystkie jest mniejsze niż czas na odbieranie 8 bitów (= 8/100kHz tj. 80 µs). Jeśli tak nie jest, może dojść do uszkodzenia danych.

    Dostęp do wielobajtowych rejestrów peryferyjnych w systemie 8-bitowym

    Niektóre mikrokontrolery wyposażone są w 8-bitowy procesor, ale ich urządzenia peryferyjne mogą mieć rejestry o większym rozmiarze. Na przykład w mikrokontrolerze może być 12-bitowy ADC komunikujący się z 8-bitowym procesorem. W takich przypadkach ważne jest, aby zachować ostrożność podczas odczytywania wielobajtowych rejestrów peryferyjnych w przerwaniu. Dane mogą zostać uszkodzone, gdy w ISR próbuje się uzyskać dostęp do wielobajtowego rejestru w 8-bitowym MCU. Rozważmy następujący ISR, który próbuje odczytać bajty High i Low z konwersji ADC:

    Przerwania proste i przyjemne - część 2 - zmienne, bufory i latencja


    Trzeba zauważyć, że Case1 (niepoprawny) w Table3 jest podobny do Case2 (poprawny) w Table2. Jak można, wykorzystując tę samą metodę, uzyskać raz poprawny, a następnie niewłaściwy wynik? Odpowiedź jest prosta — Case1 w Table3, jak i Case2 w Table2, opisane powyżej, działałyby dobrze, tak długo, jak zagwarantowana będzie poprawna i kompletna obsługa ADC_ISR i I2C_ISR, zanim zostaną one wywołane ponownie.

    Jak widać w Table3, w systemie są jeszcze dwa inne przerwania. W tym przypadku należy wziąć pod uwagę, że ADC działa w sposób ciągły i że ADC_ISR jest wyzwalany po zakończeniu konwersji ADC. Należy rozważyć, że ADC_MSB i ADC_LSB są sprzętowo zsynchronizowane, więc rejestry wyników konwersji ADC będą aktualizowane przez operację zatrzasku sprzętowego, gdy tylko konwersja ADC zostanie zakończona. Zanalizujmy teraz sytuację, w której ADC_ISR został wyzwolony po raz pierwszy, gdy ISR1 był już wykonywany. Zwiększa to opóźnienie w obsłudze ADC_ISR. Następnie pomyślmy, że kolejna konwersja ADC jest zakończona, gdy tylko MSB poprzedniej został odczytany w ISR. W tym przypadku jednako rejestr ADC_MSB, jak i ADC_LSB zostałby zaktualizowany nową wartością. Jednak poprzednie przerwanie nadal się wykonuje i ADC_MSB z minionej konwersji został już odczytany. Zweryfikowany teraz ADC_LSB będzie pochodził z bieżącej konwersji. Dane są uszkodzone i niepoprawne.

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


    Właściwe dane to albo 0x1FF, albo 0x200. Data, jednakże przyjęła wartość 0x0100, która jest niewłaściwa. Istnieją dwa sposoby rozwiązania tego problemu: skontrolowanie, że ADC_ISR zostanie zwieńczone przed oczekiwanym zakończeniem kolejnej konwersji oraz odczytanie danych tak, jak pokazano to w Case2 w Table3 powyżej.

    Trzeba zauważyć, że każde przerwanie ma na ogół coś podobnego do rejestru: „Interrupt_Clear”. Zapis do niego uniemożliwia wysłanie przerwania do kontrolera. A odczyt zwraca aktualny stan przerwania, tj. czy odpowiednie z nich zostało zarejestrowane przez urządzenie. Rejestr Interrupt_Clear może być użyty do zrozumienia, czy konwersja ADC została zakończona podczas odczytu MSB i LSB poprzedniej konwersji. Ponownie działać może odczyt wyniku konwersji, jeśli to samo miało miejsce.

    Zmienne lokalne — poznaj swój kompilator

    Kompilatory mogą alokować pamięć dla zmiennych lokalnych na dwa różne sposoby. Aby zrozumieć problemy, które mogą wystąpić w wyniku użycia zmiennych lokalnych w przerwaniach, a w tym metody, których można użyć, aby temu zapobiec, powinniśmy najpierw zrozumieć, w jaki sposób zmienne lokalne są przechowywane w pamięci. Poniżej opisano dwie różne metody:

    Wykorzystywanie stosu do przechowywania zmiennych lokalnych

    W tym przypadku zmienne lokalne są tworzone na stosie za każdym razem, gdy wywoływana jest funkcja. Jest to najczęstszy sposób ich przechowywania, ponieważ stos, a tym samym pamięć używana przez wywoływaną funkcję, zostanie zwolniona, gdy sterowanie będzie zwrócone do funkcji wywołującej. W odniesieniu do takich kompilatorów ważne jest, aby zdawać sobie sprawę z całkowitego wykorzystania stosu. Gdyż, jeśli program zawiera dużą liczbę wywołań funkcji w postaci drzewa — to jedno z nich uruchamia drugie, to skutkuje trzecim i tak dalej.

    W przypadku przerwań analiza użycia stosu staje się jeszcze ważniejsza, ponieważ mogą one być wyzwalane w dowolnym momencie podczas realizacji programu. Całkowite użycie stosu, a tym samym wymaganą pamięć RAM, można obliczyć w następujący sposób:

    Przykład I: Tylko jedno ISR obsługiwane naraz

    Jeśli przerwania nie są zagnieżdżone, obsługiwane jest tylko jedno na raz. W tym przypadku maksymalne zużycie w dowolnym momencie jest obliczane jako całkowity stos wymagany dla największego drzewa wywołań (w tym miejsce na zmienne lokalne, licznik programu, argumenty funkcji itp.). Plus stos ISR, który zużywa największą ilość pamięci.

    Przykład II: Zagnieżdżone przerwania

    Jeśli przerwania są zagnieżdżone, ISR może wywłaszczyć inne, a nawet siebie. W takim przypadku całkowite użycie stosu może być nadmierne. Stąd należy unikać zagnieżdżonych przerwań, nawet jeśli nie ma w systemie zmiennych lokalnych. Dzieje się tak, ponieważ kiedy ISR jest wyzwalany wszystkie (lub prawie, w zależności od kompilatora) funkcje specjalne i rejestry ogólnego przeznaczenia używane przez ISR są również przechowywane na stosie.

    Jeśli pozwoli się przerwaniu na wywłaszczenie, stos będzie cały czas zapełniał się (w przypadku, w którym przerwanie nadal występuje i wywłaszcza ISR) i łatwo przesyci. Należy unikać zagnieżdżania przerwań, chyba że jest to absolutnie konieczne, a w danym ujęciu trzeba się upewnić, że ISR nigdy nie będzie wywłaszczone. Można to zapewnić, analizując całkowity czas wymagany do wykonania ISR i minimalne możliwe opóźnienie między dwoma wystąpieniami przerwania. Jeśli maksymalne użycie stosu przekracza dostępną pamięć RAM, dane w pamięci mogą zostać uszkodzone, gdy mikrokontroler uruchomi program. Takie błędy mogą być krytyczne i trudne do wykrycia, dlatego należy eliminować potencjał ich wystąpienia.

    Ustalone lokalizacje zmiennych lokalnych w pamięci i nakładanie się jej

    Niektóre kompilatory nie zapisują zmiennych lokalnych na stosie, jak to zwykle robi się w C. Zamiast tego używają stałych lokalizacji pamięci do przechowywania zmiennych lokalnych i argumentów funkcji oraz dzielą te obszary między lokalne zmienne funkcji, które się nie wywołują. Taka forma nakładania się pamięci jest wykonywana w celu zapobiegania przepełnieniom stosu i umożliwienia efektywnego wykorzystania. Użycie zmiennych lokalnych w przerwaniach z takimi kompilatorami wymaga szczególnej uwagi. Aby zrozumieć dlaczego, przyjrzyjmy się najpierw, jak działa nakładanie się pamięci.

    Nakładanie stałych lokalizacji pamięci w celu przechowywania zmiennych lokalnych i argumentów funkcji pod tym samym adresem uzyskuje się przy użyciu dobrze zdefiniowanej procedury. Po pierwsze, linker buduje drzewo wywołań programu, dzięki czemu może dowiedzieć się, które segmenty danych, dla jakich funkcji wzajemnie się wykluczają, i w ten sposób je nakładać. Załóżmy na przykład, że funkcja Main() wywołuje funkcję A, B i C. Funkcja A używa 10 bajtów, B 20 bajtów, a C 16 bajtów zmiennych lokalnych. Zakładając, że A, B i C nie wywołują się wzajemnie, pamięć, z której korzystają, może zostać na siebie nałożona. Tak więc zamiast 46 bajtów pamięci danych (10 dla A + 20 dla B + 16 dla C) zużywane jest tylko 20 (ilość zajmowana przez: „największą” z funkcji).

    Problemy ze zmiennymi lokalnymi w ISR, gdy pamięć jest nakładana

    Jeśli zmienne lokalne przerwania zajmują tę samą przestrzeń pamięci, co jedna z funkcji w głównej gałęzi programu. A dodatkowo przerwanie zostanie wyzwolone, gdy jedna z funkcji korzystających z tej samej przestrzeni pamięci jest wykonywana, dane w tych zmiennych lokalnych mogą być narażone na uszkodzenie.

    Poniższe techniki są pomocne w zapobieganiu naruszeń przestrzeni danych przez przerwania:
    * Użycie słowa kluczowego: „static” w zmiennych lokalnych stosowanych w przerwaniach. Przydziela to osobną przestrzeń danych dla tych zmiennych, która nie jest nałożona na żadną inną funkcję.
    * Wykorzystanie niższego poziomu ustawień optymalizacji dla kompilatora, który wyłączy nakładanie zmiennych. Szczegóły poziomu optymalizacji można znaleźć w podręcznikach referencyjnych do eksploatowanego kompilatora/linkera.
    * Zużytkowanie innej przestrzeni danych dla zmiennych normalnych i lokalnych przerwania. Na przykład, zastosowanie XDATA dla: „normalnych” zmiennych lokalnych i DATA dla zmiennych lokalnych przerwania. Można to zrobić, używając specyfikatorów pamięci w deklaracjach zmiennych lub posiłkując się różnymi modelami pamięci dla odmiennych plików.
    * W zależności od ustawień optymalizacji kompilator może spożytkować banki rejestrów, zamiast pamięci RAM dla zmiennych lokalnych przerwania. W takim przypadku trzeba koniecznie upewnić się, że udział pamięci w przerwaniu ​​nie jest większy niż bank rejestrów, z którego korzysta podczas jego obsługi.

    Powyżej omówiliśmy część aspektów zarządzania pamięcią w ramach przetwarzania przerwań. W kolejnej odsłonie z tej serii pójdziemy trochę dalej. Przedstawione zostaną bufory danych i pamięć współdzielona. A więc to, na co należy uważać oraz jakie są dobre i złe praktyki podczas korzystania z buforów i przerwań.

    Źródło: https://www.embedded.com/interrupts-short-simple-part-2-variables-buffers-latencies/

    Fajne? Ranking DIY
    O autorze
    ghost666
    Tłumacz Redaktor
    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.
    https://twitter.com/Moonstreet_Labs
    ghost666 napisał 11960 postów o ocenie 10197, pomógł 157 razy. Mieszka w mieście Warszawa. Jest z nami od 2003 roku.
  • #2 19913576
    tmf
    VIP Zasłużony dla elektroda
    ghost666 napisał:
    Zmienne globalne powinny być modyfikowane tylko w kilku koniecznych obszarach wewnątrz programu. Gdyż, jeśli odbywa się to w oparciu o wiele wątków (tj. poprzez główny proces, ISR i inne funkcje sprzętowe, takie jak DMA w mikrokontrolerze), istnieje ryzyko uszkodzenia zmiennej w pamięci.

    To ryzyko zawsze istnieje, jeśli niepoprawnie odwołujemy się do takich zmiennych. Fakt, czy to robimy w kilku, czy kilkunastu miejscach w programie jest bez znaczenia. Po prostu powinniśmy to robić poprawnie.
    ghost666 napisał:
    I jeśli Command == 1, CPU wyśle do zmiennej dane równe 3, podczas gdy powinno być 1. Problem ten nie wydarzy się w case2, wspomnianym powyżej.

    Wyciałeś fragmenty z kontekstu i w efekcie nie wiadomo o co chodzi. Ogólnie podany przykład wcale nie musi być zły. Wiele zależy od kompilatora i intencji programisty. Przede wszystkim kompilator wielokrotne if może zamienić na jump table, w efekcie oba kody są równoważne. Oczywiście może zamienić, ale nie musi. Nawet jeśli nie zamieni, to kwestia o co chodzi programiście - to, że w trakcie sprawdzania warunków zmieni się wartość zmiennej nie musi być wcale złe.
    ghost666 napisał:
    Dostęp do wielobajtowych rejestrów peryferyjnych w systemie 8-bitowym

    Tu wszystko namieszałeś.
    Po kolei:
    ghost666 napisał:
    Jak widać w Table3, w systemie są jeszcze dwa inne przerwania. W tym przypadku należy wziąć pod uwagę, że ADC działa w sposób ciągły i że ADC_ISR jest wyzwalany po zakończeniu konwersji ADC. Należy rozważyć, że ADC_MSB i ADC_LSB są sprzętowo zsynchronizowane, więc rejestry wyników konwersji ADC będą aktualizowane przez operację zatrzasku sprzętowego, gdy tylko konwersja ADC zostanie zakończona. Zanalizujmy teraz sytuację, w której ADC_ISR został wyzwolony po raz pierwszy, gdy ISR1 był już wykonywany. Zwiększa to opóźnienie w obsłudze ADC_ISR. Następnie pomyślmy, że kolejna konwersja ADC jest zakończona, gdy tylko MSB poprzedniej został odczytany w ISR. W tym przypadku jednako rejestr ADC_MSB, jak i ADC_LSB zostałby zaktualizowany nową wartością. Jednak poprzednie przerwanie nadal się wykonuje i ADC_MSB z minionej konwersji został już odczytany. Zweryfikowany teraz ADC_LSB będzie pochodził z bieżącej konwersji. Dane są uszkodzone i niepoprawne.

    Jak opisałeś w tekście 8-bitowce dla odczytu/zapisu rejestrów więcej niż 8-bitowe mają wewnętrzne układy synchronizujące. A więc ważna jest kolejneść odczyku kolejnych bajtów. Można to zrobić ręcznie, albo po prostu zdać się na kompilator. Skoro powołujesz się na AVR, to zamiast czytać ADCL i ADCH, można po prostu odczytać ADC (ADCW) i kompilator zadba o kolejność. Ponieważ taki rejestr niczym się nie różni oz zwykłej wielobajtowej zmiennej opisanej we wcześniejszym punkcie, należy tylko zadbać o atomowość odczytu. Pomijam, że przykład lekko bez sensu, bo po co ktoś miałby odczytywać np. ADC w kilku miejscach, bez żadnej synchronizacji?
    Kolejna sprawa - wejście w ISR blokuje kolejne przerwania, więc ISR nie przerwie ISR, chyba, że:
    - mamy MCU z wielopoziomową obsługą przerwań,
    - programista jawnie odblokuje przerwania w ISR.
    ghost666 napisał:
    Istnieją dwa sposoby rozwiązania tego problemu: skontrolowanie, że ADC_ISR zostanie zwieńczone przed oczekiwanym zakończeniem kolejnej konwersji oraz odczytanie danych tak, jak pokazano to w Case2 w Table3 powyżej.

    Nie, istnieje tylko jedno rozwiązanie tego problemu - zapewnienie atomowości dostępu do rejestru.
    ghost666 napisał:
    Trzeba zauważyć, że każde przerwanie ma na ogół coś podobnego do rejestru: „Interrupt_Clear”. Zapis do niego uniemożliwia wysłanie przerwania do kontrolera. A odczyt zwraca aktualny stan przerwania, tj. czy odpowiednie z nich zostało zarejestrowane przez urządzenie. Rejestr Interrupt_Clear może być użyty do zrozumienia, czy konwersja ADC została zakończona podczas odczytu MSB i LSB poprzedniej konwersji. Ponownie działać może odczyt wyniku konwersji, jeśli to samo miało miejsce.

    Absolutnie nie!
    Przede wszystkim flaga przerwania nie nadaje się do blokowania przerwań i nie uniemożliwia zgłoszenia przerwania. Przecież ta flaga tylko sygnalizuje, czy przerwanie zaszło. Jeśli zaszło i dane przerwanie jest odblkowane (flaga enable), to MCU natychmiast je obsłuży, więc kiedy niby program miałby ją skasować? Co więcej, często wejście w ISR automatycznie kasują taką flagę.
    Jak pisałem - jedynym poprawnym zabezpieczeniem jest atomowy dostęp do rejestru.
    ghost666 napisał:
    Jeśli pozwoli się przerwaniu na wywłaszczenie, stos będzie cały czas zapełniał się (w przypadku, w którym przerwanie nadal występuje i wywłaszcza ISR) i łatwo przesyci. Należy unikać zagnieżdżania przerwań, chyba że jest to absolutnie konieczne, a w danym ujęciu trzeba się upewnić, że ISR nigdy nie będzie wywłaszczone.

    Zazwyczaj MCU wchodząc w ISR blokuje przerwania tego samego poziomu, więc aby przerwanie zostało wywłaszczone to programista musi się postarać. W szczególności na prostych AVR domyślnie nie jest to możliwe. Natomiast sytuacja o której pisałeś - przerwanie wywłaszczane jest przez takie samo przerwanie - znowu domyślnie nie ma to miejsca, ale jeśli programista zezwoli, to powinien rozumieć konsekwencje.
    Szkoda, że artykuł zupełnie pomija kwestie dostępu RMW oraz użycie volatile - dwie kluczowe sprawy. Ze smaczków to omówienie sequence point dla kompilatora.
  • #3 19913757
    morkocberk
    Poziom 6  
    dzięki za informację
  • #4 19913770
    Janusz_kk
    Poziom 38  
    tmf napisał:
    Szkoda, że artykuł zupełnie pomija kwestie dostępu RMW oraz użycie volatile - dwie kluczowe sprawy. Ze smaczków to omówienie sequence point dla kompilatora.

    Tak w ogóle to co tłumaczy ghost666 wygląda na zadanie domowe studenta a nie artykuł specjalisty który wie o czym pisze.

    tmf napisał:
    Ogólnie podany przykład wcale nie musi być zły. Wiele zależy od kompilatora i intencji programisty. Przede wszystkim kompilator wielokrotne if może zamienić na jump table, w efekcie oba kody są równoważne. Oczywiście może zamienić, ale nie musi. Nawet jeśli nie zamieni, to kwestia o co chodzi programiście - to, że w trakcie sprawdzania warunków zmieni się wartość zmiennej nie musi być wcale złe.

    Ja bym dodał że w ogóle przykład jest z czapy bo GCC i pokrewne kompilatory zawsze instrukcję CASE tłumaczą jako 'jump table', nawet dla 2 pozycji.
  • #5 19913776
    _lazor_
    Moderator Projektowanie
    tmf napisał:
    Absolutnie nie!
    Przede wszystkim flaga przerwania nie nadaje się do blokowania przerwań i nie uniemożliwia zgłoszenia przerwania. Przecież ta flaga tylko sygnalizuje, czy przerwanie zaszło. Jeśli zaszło i dane przerwanie jest odblkowane (flaga enable), to MCU natychmiast je obsłuży, więc kiedy niby program miałby ją skasować? Co więcej, często wejście w ISR automatycznie kasują taką flagę.
    Jak pisałem - jedynym poprawnym zabezpieczeniem jest atomowy dostęp do rejestru.


    Tutaj się nie zgodzę gdyż nie wiadomo dla jakiej architektury jest pisany ten tekst, zakładasz że dla AVR co uważam za błąd. Nie każdy uC czy procesor wywołuję ISR związany z źródłem i po stronie usera jest sprawdzenie jego źródła i jego obsłużenie. Patrz cortex-a.


    Źródła są prastare, jak dla mnie ciekawe gdyż pokazuje zmiany w rozwiązaniach technicznych jakie były kiedyś i dzisiaj oraz jak wiele elementów jest traktowane jak aksjomaty. Dla przykładu:
    ghost666 napisał:
    Niektóre kompilatory nie zapisują zmiennych lokalnych na stosie, jak to zwykle robi się w C. Zamiast tego używają stałych lokalizacji pamięci do przechowywania zmiennych lokalnych i argumentów funkcji oraz dzielą te obszary między lokalne zmienne funkcji, które się nie wywołują.

    Dziś aksjomatem jest to że zmienne lokalne lecą na stos (lub do rejestrów).
  • #6 19913844
    tmf
    VIP Zasłużony dla elektroda
    _lazor_ napisał:
    Tutaj się nie zgodzę gdyż nie wiadomo dla jakiej architektury jest pisany ten tekst, zakładasz że dla AVR co uważam za błąd. Nie każdy uC czy procesor wywołuję ISR związany z źródłem i po stronie usera jest sprawdzenie jego źródła i jego obsłużenie. Patrz cortex-a.

    Owszem, nawet na AVR są współdzielone przerwania i w ISR musisz sprawdzić źródło - w takim przypadku flaga nie jest kasowana, programista przed opuszczeniem ISR musi ją skasować. Ale nie o tym mowa w cytowanym fragmencie - zauważ, że w tekście jest, że taką flagę można:
    ghost666 napisał:
    Zapis do niego uniemożliwia wysłanie przerwania do kontrolera.

    co jest kompletnie nieprawdziwe. Ponadto pokazany przykład z synchronizacją odczytów rejestrów ADC jest kompletnie bez sensu - w ISR czekać na flagę sygnalizującą koniec przetwarzania i gotowość danych - table 3 case 2? Cały ten akapit jest jakiś pokręcony i nie wiadomo o co chodzi.
    Ogólnie nie wiem czy jest dobrym pomysłem tłumaczenie słabego artykułu sprzed 10 lat.
  • #7 19913939
    _lazor_
    Moderator Projektowanie
    Podejrzewam że mogła być taka możliwość na jakiejś architekturze, a autor ją używał. Na dzisiejsze realia jest to jak najbardziej bzdura.
  • #8 19914695
    dondu
    Moderator na urlopie...
    ... w dodatku tutaj jest błąd:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod


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


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


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



    ... no i oczywiście volatile o którym wspominał:
    tmf napisał:
    Szkoda, że artykuł zupełnie pomija kwestie dostępu RMW oraz użycie volatile - dwie kluczowe sprawy.

    http://mikrokontrolery.blogspot.com/2011/04/problemy-c-volatile.html
  • #9 19915433
    djfarad02
    Poziom 19  
    Sposób:
    Disable_Interrupts;
    local_data=data;
    Enable_Interrupts;

    można zastąpić innym sposobem, nie wymagającym wyłączania przerwań:
    local_data=data;
    while (local_data!=data) local_data=data;
  • #10 19915653
    dondu
    Moderator na urlopie...
    djfarad02 napisał:
    Sposób:
    Disable_Interrupts;
    local_data=data;
    Enable_Interrupts;

    można zastąpić innym sposobem, nie wymagającym wyłączania przerwań:
    local_data=data;
    while (local_data!=data) local_data=data;

    Tylko i wyłącznie w sytuacji, gdy zmienna local_data jest jednobajtowa.
    W przypadku, gdy zmienna ma większa ilość bajtów (tak jak w pierwszym poście tego tematu) taki sposób nie zapewnia atomowości dostępu. Dlatego należy posłużyć się pierwszym sposobem.
  • #11 19915845
    tmf
    VIP Zasłużony dla elektroda
    dondu napisał:
    Tylko i wyłącznie w sytuacji, gdy zmienna local_data jest jednobajtowa.

    Dla wielobajtowych zmiennych to też zadziała, ale... to jest wolniejsze niż blokowanie przerwań. Pokazana konstrukcja wymaga co najmniej 2 odczytów, a czasami nawet 4. To będzie kosztowne czasowo. Zwykłe zablokowanie/odblokowanie przerwań to instrukcje jednotaktowe na AVR (odnosze się do tego MCU bo z kontekstu artykułu wynika, że mówimy raczej o 8-bitowych MCU). Dodatkowo, nie trzeba blokować wszystkich przerwań, tylko dokładnie to, które modyfikuje zmienną.
  • #12 19915878
    khoam
    Poziom 42  
    djfarad02 napisał:
    można zastąpić innym sposobem, nie wymagającym wyłączania przerwań:
    local_data=data;
    while (local_data!=data) local_data=data;

    Jest to nieco "młotkowa" metoda dla typów nie-atomowych tzn. modyfikować wartość local_data tak długo, aż będzie równa wartości data tzn. żadne z przerwań w trakcie tego przypisania nie zmieni wartości local_data. A co będzie, jak takie przerwanie zmieni wartość data w trakcie pętli while? Jest to oczywiście bardzo hipotetyczne rozważanie, ale może mieć miejsce w praktyce.

    Moim zdaniem bezpieczniej jest użyć w taki przypadku dodatkowej zmiennej globalnej typu atomowego (np. bool) i zastosować ją w charakterze spinlock. W takiej sytuacji zablokujemy na chwilę tylko konkretne przerwanie, w którym podjęta zostanie próba modyfikacji zmiennej w trakcie, kiedy jest ona np. czytana w programie głównym. Oczywiście operacja testowania i modyfikacji spinlock też musi być atomowa.
  • #13 19915920
    tmf
    VIP Zasłużony dla elektroda
    khoam napisał:
    A co będzie, jak takie przerwanie zmieni wartość data w trakcie pętli while? Jest to oczywiście bardzo hipotetyczne rozważanie, ale może mieć miejsce w praktyce.

    Nic nie będzie, data będzie się różnić od local_data, a więc nastąpi kolejne przypisanie i kolejne sprawdzenie warunku.

    Dodano po 20 [minuty]:

    Tak przy okazji, jeszcze drobna uwaga do zapisu:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    On zazwyczaj jest poprawny, ale warto zwrócić uwagę na bardzo istotną, a często pomijaną kwestię. Mamy tu dwa odczyty, problem w tym, że w takim wyrażeniu kompilator ma swobodę wyboru kolejności działań. I tu tkwi problem. Dostęp do rejestrów o długości większej niż 8-bitów na 8-bitowym MCU wymaga zazwyczaj odpowiedniej sekwencji. Skoro kompilator może zmienić kolejność, to w efekcie powstały kod może nie być poprawny. Tu pojawia się właśnie problem sequence points o którym wspomniałem w pierwszym poście. Aby powyższy kod zawsze był poprawny, należy wprowadzić taki punkt rozbijając to wyrażenie na:
    Kod: C / C++
    Zaloguj się, aby zobaczyć kod

    W takiej sytuacji kompilator nie może zmienić kolejności działań (zakładając, że ADC jest volatile) i w efekcie mamy zawsze dokładnie to czego oczekujemy.
    Ale najprościej, o ile to możliwe, korzystać z rejestrów 16-bitowych i przerzucić w ten sposób odpowiednią kolejność dostępu na kompilator, czyli np.:
    Data=ADCW;
  • #14 19915960
    khoam
    Poziom 42  
    tmf napisał:
    Nic nie będzie, data będzie się różnić od local_data, a więc nastąpi kolejne przypisanie i kolejne sprawdzenie warunku.

    W ten sposób mogą być gubione kolejne wartości zmiennej data, które są cyklicznie ustawiane przez przerwanie.
  • #15 19915983
    Janusz_kk
    Poziom 38  
    Ale gdy odczyt jest robiony w przerwaniu (AVR) to nic się nie gubi, wtedy proste

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

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

    w zupełności wystarczy bo przecież odczyt jest po zakończeniu cyklu ADC a nie w trakcie.
  • #16 19916071
    tmf
    VIP Zasłużony dla elektroda
    khoam napisał:
    W ten sposób mogą być gubione kolejne wartości zmiennej data, które są cyklicznie ustawiane przez przerwanie.

    Owszem, ale kolejne wartości i tak mogą być gubione, bo nie wiesz co ile ta zmienna będzie odczytywana poza przerwaniem. Jeśli wartości miałyby nie być gubione to trzeba zorganizować jakąś formę bufora/kolejki do której te kolejne wartości byłyby zapisywane.

    @Janusz kk - z tym, że jak pisałem, zapis Data = (ADC_MSB<<8) | ADC_LSB; nie gwarantuje poprawnej kolejności odczytu rejestrów, co jest wymagane przez hardware.
  • #17 19920363
    Urgon
    Poziom 38  
    AVE...

    A dlaczego zamiast zmiennej data nie użyć struktury, która składa się z dwóch kawałków: dataH i dataL? W przerwaniu wykonujemy dwie atomowe operacje odczytu rejestrów do stosownych części struktury, a w programie głównym używamy całej struktury. Ewentualnie użyć dwóch zmiennych buforowych, a całe łączenie ich w całość można wykonać w pętli głównej. Zresztą wspomniane było, iż w większości przypadków rejestry ośmiobitowe, z których składany jest wynik pomiaru, mają też alternatywną, wspólną nazwę, co kompilator rozpoznaje i pozwala odczytać wynik pomiaru do zmiennej w sposób atomowy i bezpieczny. Druga sprawa: w przypadku niektórych modułów ADC (PICi tak mają) można sobie ustawić, by kolejna konwersja nie nastąpiła, póki się nie ustawi flagi ADCON0.GO/DONE.

    Ja tu jednak widzę inny problem, prawdopodobnie wynikający z niewiedzy autora (nie tłumacza): mianowicie pokazane przykłady nie mają za bardzo sensu. Pokazane peryferia działają sporo wolniej od prędkości wykonywania pętli obsługi przerwań, więc nie ma szans, by dane uległy uszkodzeniu. No chyba że autor nie nauczył się jeszcze, że można zmieniać częstotliwość taktowania układu.

    Z ciekawości, to która rodzina mikrokontrolerów ośmiobitowych ma bufor dla I2C większy niż jeden bajt?
  • #18 19920449
    tmf
    VIP Zasłużony dla elektroda
    Urgon napisał:
    A dlaczego zamiast zmiennej data nie użyć struktury, która składa się z dwóch kawałków: dataH i dataL? W przerwaniu wykonujemy dwie atomowe operacje odczytu rejestrów do stosownych części struktury, a w programie głównym używamy całej struktury.

    Ale to nic nie zmieni. Co prawda w przerwaniu przepisanie danych do struktury będzie poprawne (ale ono zawsze będzie, o ile przerwanie nie może być przerwane, przez coś co do tej zmiennej się dobiera), ale już operacje na takiej strukturze (technicznie unii dwóch typów) nie będą atomowe, a co za tym idzie narażone na sytuację, w której najpierw odczytasz jeden bajt, nastąpi przerwanie modyfikujące wartość i potem odczytasz kolejny bajt, ale już z nowego pomiaru. W efekcie masz składankę starego i nowego pomiaru.
    Urgon napisał:
    Zresztą wspomniane było, iż w większości przypadków rejestry ośmiobitowe, z których składany jest wynik pomiaru, mają też alternatywną, wspólną nazwę, co kompilator rozpoznaje i pozwala odczytać wynik pomiaru do zmiennej w sposób atomowy i bezpieczny.

    To tylko rozwiązuje problem odpowiedniej sekwencji dostępu do mniej i bardziej znaczącej części 16-bitowego rejestru. Nie rozwiązuje problemu atomowości dostępu. Oczywiście przykłąd z ADC jest niezbyt trafiony, bo po co ktoś miałby odczytywać ADC w przerwaniu i pętli głównej programu operując bezpośrednio na rejestrze ADC?
  • #19 19920599
    Urgon
    Poziom 38  
    AVE...

    Rozwiązanie z przykładu pokazane jako "poprawne" też nie gwarantuje, że uzyskamy to, czego oczekujemy. Jeśli program jest duży i rozbudowany, to zanim nasz bezcenny pomiar zostanie przeniesiony w bezpieczny sposób do naszej zmiennej lokalnej, to ADC mogło dokonać już kilku lub kilkunastu pomiarów. To zależy od częstotliwości taktowania mikrokontrolera i częstotliwości próbkowania ADC. Jeśli celem jest porównanie wartości z ADC z wartością zadaną, albo coś równie prostego, to może lepiej wykonać to w ISR? Jeśli zaś gubienie wyników kolejnych pomiarów nie jest problemem, to lepiej wyłączyć przerwanie, rozpocząć konwersję "ręcznie" i po jej zakończeniu obrobić te dane.

    W sytuacji, gdy chcemy wszystkie pomiary mieć, obrobić je w pętli głównej, ale pętla główna trwa znacząco dłużej niż odstępy między przerwaniami ADC, to można zrobić takiego fikołka (z góry przepraszam za niską jakość kodu):

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


    To tak pisane na kolanie, ale oddaje ideę. Będzie to działać wtedy, gdy odstęp między przerwaniami ADC jest trochę większy niż czas potrzebny na obrobienie wszystkich danych. W razie czego można zoptymalizować wielkość tablicy na dane.

Podsumowanie tematu

W dyskusji poruszono kwestie związane z używaniem zmiennych globalnych i lokalnych w kontekście obsługi przerwań (ISR) w mikrokontrolerach. Uczestnicy podkreślili, że zmienne globalne powinny być modyfikowane ostrożnie, aby uniknąć problemów z synchronizacją, zwłaszcza w przypadku wielowątkowości. Zwrócono uwagę na znaczenie atomowości operacji oraz sekwencji odczytów rejestrów, co jest kluczowe dla poprawności działania programów. Wskazano na ryzyko gubienia danych, gdy zmienne są odczytywane w ISR, oraz zaproponowano alternatywne podejścia, takie jak użycie struktur lub buforów do przechowywania danych. Dyskusja dotyczyła również architektur mikrokontrolerów, takich jak AVR i Cortex-A, oraz ich specyficznych zachowań w kontekście przerwań.
Podsumowanie wygenerowane przez model językowy.
REKLAMA