Czy istnieje dobry sposób na wdrożenie komunikacji między ISR a resztą programu dla systemu wbudowanego, który pozwala uniknąć zmiennych globalnych?
Wydaje się, że ogólny wzorzec ma mieć zmienną globalną, która jest dzielona między ISR a resztą programu i używana jako flaga, ale to użycie zmiennych globalnych jest dla mnie sprzeczne z rzeczywistością. Podałem prosty przykład z wykorzystaniem ISR w stylu avr-libc:
volatile uint8_t flag;
int main() {
...
if (flag == 1) {
...
}
...
}
ISR(...) {
...
flag = 1;
...
}
Nie mogę oderwać wzroku od tego, co jest zasadniczo kwestią określania zakresu; z pewnością jakieś zmienne dostępne zarówno dla ISR, jak i reszty programu muszą być globalne? Mimo to często widziałem, jak ludzie mówią rzeczy w stylu „zmienne globalne są jednym ze sposobów implementacji komunikacji między ISR a resztą programu” (moje podkreślenie), co wydaje się sugerować, że istnieją inne metody; jeśli są inne metody, jakie one są?
Odpowiedzi:
Istnieje de facto standardowy sposób na zrobienie tego (przy założeniu programowania w C):
extern
słowa kluczowego lub po prostu przez pomyłkę.static
. Taka zmienna nie jest globalna, ale ogranicza się do pliku, w którym została zadeklarowana.volatile
. Uwaga: nie daje to dostępu atomowego ani nie rozwiązuje problemu ponownego wejścia!źródło
inline
staje się przestarzałe, ponieważ kompilatory stają się coraz bardziej inteligentne w optymalizacji kodu. Powiedziałbym, że martwienie się o narzut to „optymalizacja przedwczesna” - w większości przypadków narzut nie ma znaczenia, jeśli w ogóle jest obecny w kodzie maszynowym.To jest prawdziwy problem. Pogódź się z tym.
Teraz, zanim kolano natychmiast zacznie narzekać na to, że to jest nieczyste, pozwól mi to trochę zakwalifikować. Korzystanie z zmiennych globalnych z pewnością jest niebezpieczne. Mogą jednak również zwiększyć wydajność, co czasami ma znaczenie w małych systemach o ograniczonych zasobach.
Kluczem do sukcesu jest zastanowienie się, kiedy można z nich rozsądnie korzystać i raczej nie wpakują się w kłopoty, w przeciwieństwie do błędu, który tylko czeka. Zawsze są kompromisy. Chociaż na ogół unikanie zmiennych globalnych w komunikacji między kodem przerwania a kodem pierwszego planu jest niedopuszczalną wytyczną, doprowadzenie go, podobnie jak większości innych wytycznych, do skrajności religii przynosi efekt przeciwny do zamierzonego.
Oto niektóre przykłady, w których czasami używam zmiennych globalnych do przekazywania informacji między kodem przerwania a kodem pierwszego planu:
Upewniam się, że tiki 1 ms, 10 ms i 100 ms mają rozmiar słowa, które można odczytać w jednej operacji atomowej. Jeśli używasz języka wysokiego poziomu, koniecznie powiedz kompilatorowi, że zmienne te mogą zmieniać się asynchronicznie. W C deklarujesz na przykład , że są zewnętrzne niestabilne . Oczywiście jest to coś, co trafia do pliku dołączonego w puszce, więc nie musisz pamiętać o tym przy każdym projekcie.
Czasami robię licznik tyka 1 s licznika czasu, który upłynął, więc utwórz szerokość 32 bitów. Tego nie da się odczytać w jednej operacji atomowej na wielu małych mikro, których używam, więc nie jest to globalne. Zamiast tego zapewniono procedurę, która odczytuje wartość wielu słów, zajmuje się możliwymi aktualizacjami między odczytami i zwraca wynik.
Oczywiście mogły istnieć procedury uzyskiwania mniejszych 1 ms, 10 ms itp., Liczników tyknięć. Jednak to naprawdę niewiele dla ciebie robi, dodaje wiele instrukcji zamiast czytać jedno słowo i zużywa inną lokalizację stosu wywołań.
Co jest wadą? Podejrzewam, że ktoś mógłby napisać literówkę, która przypadkowo zapisuje jeden z liczników, co może zepsuć inne czasy w systemie. Celowe pisanie do licznika nie miałoby sensu, więc tego rodzaju błąd musiałby być czymś niezamierzonym, jak literówka. Wydaje się bardzo mało prawdopodobne. Nie przypominam sobie, aby kiedykolwiek zdarzyło się to w ponad 100 małych projektach mikrokontrolerów.
Na przykład A / D może odczytywać napięcie wyjściowe dzielnika napięcia od 0 do 3 V, aby zmierzyć zasilanie 24 V. Wiele odczytów jest przeprowadzanych przez filtrowanie, a następnie skalowane, tak aby końcowa wartość była w miliwoltach. Jeżeli napięcie wynosi 24,015 V, to ostateczna wartość to 24015.
Reszta systemu po prostu widzi aktualizowaną na bieżąco wartość wskazującą napięcie zasilania. Nie wiadomo ani nie trzeba się przejmować, kiedy dokładnie jest aktualizowany, zwłaszcza, że jest aktualizowany znacznie częściej niż czas ustalania filtra dolnoprzepustowego.
Ponownie, można zastosować procedurę interfejsu , ale z tego niewiele zyskujesz. Używanie zmiennej globalnej zawsze, gdy potrzebujesz napięcia zasilającego, jest znacznie prostsze. Pamiętaj, że prostota nie dotyczy tylko maszyny, ale prostota oznacza również mniejsze prawdopodobieństwo błędu ludzkiego.
źródło
extern int ticks10ms
goinline int getTicks10ms()
nie spowoduje absolutnie żadnej różnicy w skompilowanym zestawie, z drugiej strony utrudni przypadkową zmianę jego wartości w innych częściach programu, a także pozwoli ci sposób „zaczepienia” tego wywołania (np. kpienia z czasu podczas testowania jednostkowego, rejestrowania dostępu do tej zmiennej itp.). Nawet jeśli argumentujesz, że szansa, że programista san zmieni tę zmienną na zero, nie ma kosztu wbudowanego gettera.Każde szczególne przerwanie będzie zasobem globalnym. Czasami jednak może być użyteczne, aby kilka przerwań współużytkowało ten sam kod. Na przykład system może mieć kilka UART, z których wszystkie powinny korzystać z podobnej logiki wysyłania / odbierania.
Ładne podejście do obsługi, polegające na umieszczeniu rzeczy używanych przez procedurę obsługi przerwań lub wskaźników do nich w obiekcie struktury, a następnie faktyczne sprzętowe procedury obsługi przerwań mogą wyglądać następująco:
Obiekty
uart1_info
,uart2_info
itp byłyby zmienne globalne, ale byłyby to tylko zmienne globalne używane przez przerwań koparki. Wszystko inne, z czym osoby obsługiją się dotkną, zostanie w nich obsłużone.Zauważ, że wszystko, co jest dostępne zarówno przez moduł obsługi przerwań, jak i przez kod linii głównej, musi zostać zakwalifikowane
volatile
. Najprościej jest po prostu zadeklarować jakovolatile
wszystko, co w ogóle będzie używane przez moduł obsługi przerwań, ale jeśli wydajność jest ważna, można chcieć napisać kod, który kopiuje informacje do wartości tymczasowych, działa na nich, a następnie zapisuje je z powrotem. Na przykład zamiast pisać:pisać:
Pierwsze podejście może być łatwiejsze do odczytania i zrozumienia, ale będzie mniej skuteczne niż drugie. To, czy jest to problem, zależy od wniosku.
źródło
Oto trzy pomysły:
Zadeklaruj zmienną flagi jako statyczną, aby ograniczyć zakres do jednego pliku.
Ustaw zmienną flagową jako prywatną i użyj funkcji pobierających i ustawiających, aby uzyskać dostęp do wartości flagi.
Użyj obiektu sygnalizacyjnego, takiego jak semafor, zamiast zmiennej flagowej. ISR ustawi / opublikuje semafor.
źródło
Przerwanie (tj. Wektor wskazujący na program obsługi) jest zasobem globalnym. Więc nawet jeśli użyjesz jakiejś zmiennej na stosie lub na stercie:
lub kod obiektowy z funkcją „wirtualną”:
… Pierwszy krok musi obejmować rzeczywistą zmienną globalną (lub przynajmniej statyczną), aby dotrzeć do tych innych danych.
Wszystkie te mechanizmy dodają pośrednie, więc zwykle nie robi się tego, jeśli chcesz wycisnąć ostatni cykl z procedury obsługi przerwań.
źródło
Obecnie koduję dla Cortex M0 / M4, a podejście, którego używamy w C ++ (nie ma znacznika C ++, więc ta odpowiedź może być nie na temat) jest następujące:
Używamy klasy,
CInterruptVectorTable
która zawiera wszystkie procedury obsługi przerwań zapisane w rzeczywistym wektorze przerwań kontrolera:Klasa
CInterruptVectorTable
implementuje abstrakcję wektorów przerwań, dzięki czemu można powiązać różne funkcje z wektorami przerwań podczas działania.Interfejs tej klasy wygląda następująco:
Musisz wykonać funkcje, które są przechowywane w tablicy wektorów,
static
ponieważ kontroler nie może dostarczyć wskaźnikathis
-po, ponieważ tablica wektorów nie jest obiektem. Aby obejść ten problem, mamy wskaźnik statyczny wpThis
środkuCInterruptVectorTable
. Po wejściu w jedną z funkcji statycznego przerywania, może uzyskać dostęp dopThis
wskaźnika -po, aby uzyskać dostęp do członków jednego obiektuCInterruptVectorTable
.Teraz w programie możesz użyć wskaźnika,
SetIsrCallbackfunction
aby dostarczyć wskaźnik funkcji dostatic
funkcji, która ma być wywoływana, gdy nastąpi przerwanie. Wskaźniki są przechowywane wInterruptVectorTable_t virtualVectorTable
.Implementacja funkcji przerwania wygląda następująco:
To wywoła
static
metodę innej klasy (która może byćprivate
), która następnie może zawierać innąstatic
this
-pointer, aby uzyskać dostęp do zmiennych składowych tego obiektu (tylko jednej).Sądzę, że możesz budować i interfejsować jak
IInterruptHandler
i przechowywać wskaźniki do obiektów, więc nie potrzebujesz wskaźnikastatic
this
-po wszystkich tych klasach. (może spróbujemy tego w następnej iteracji naszej architektury)Drugie podejście działa dla nas dobrze, ponieważ jedynymi obiektami, które mogą zaimplementować moduł obsługi przerwań, są te znajdujące się w warstwie abstrakcji sprzętu, i zwykle mamy tylko jeden obiekt dla każdego bloku sprzętowego, więc dobrze działa z
static
this
-pointerami. Warstwa abstrakcji sprzętowej zapewnia jeszcze jedną abstrakcję przerwań, nazywanąICallback
następnie implementowaną w warstwie urządzeń nad sprzętem.Czy uzyskujesz dostęp do danych globalnych? Jasne, że tak, ale większość prywatnych danych globalnych można
this
ustawić jako prywatne, takie jak wskaźniki i funkcje przerwania.Nie jest kuloodporny i dodaje napowietrznych. Będziesz miał trudności z wdrożeniem stosu IO-Link przy użyciu tego podejścia. Ale jeśli nie jesteś bardzo napięty z taktowaniem, działa to całkiem dobrze, aby uzyskać elastyczną abstrakcję przerwań i komunikacji w modułach bez użycia zmiennych globalnych, które są dostępne zewsząd.
źródło