Unikanie zmiennych globalnych podczas używania przerwań w systemach wbudowanych

13

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ą?


źródło
1
Niekoniecznie jest prawdą, że WSZYSTKIE pozostałe programy będą miały dostęp; jeśli zadeklarujesz zmienną jako statyczną, zobaczy ją tylko plik, w którym zmienna została zadeklarowana. Zmienne, które są widoczne w całym jednym pliku, wcale nie są trudne, ale nie w pozostałej części programu i to może pomóc.
DiBosco,
1
obok, flaga musi być uznana za niestabilną, ponieważ używasz / zmieniasz ją poza normalnym przebiegiem programu. Wymusza to na kompilatorze nieoptymalizowanie żadnej operacji odczytu / zapisu na flagę i wykonanie rzeczywistej operacji odczytu / zapisu.
następny hack
@ next-hack Tak, to absolutnie poprawne, przepraszam, że próbowałem szybko znaleźć przykład.

Odpowiedzi:

18

Istnieje de facto standardowy sposób na zrobienie tego (przy założeniu programowania w C):

  • Przerwania / ISR są niskiego poziomu i dlatego powinny być implementowane tylko wewnątrz sterownika związanego ze sprzętem, który generuje przerwanie. Nie powinny znajdować się nigdzie indziej, ale wewnątrz tego sterownika.
  • Wszelka komunikacja z ISR odbywa się wyłącznie przez kierowcę i kierowcę. Jeśli inne części programu potrzebują dostępu do tych informacji, muszą poprosić o nie sterownik, korzystając z funkcji ustawiających / pobierających lub podobnych.
  • Nie należy deklarować zmiennych „globalnych”. Globalne zmienne zakresu pliku z zewnętrznym powiązaniem. To znaczy: zmienne, które można wywołać za pomocą externsłowa kluczowego lub po prostu przez pomyłkę.
  • Zamiast tego, aby wymusić enkapsulację prywatną wewnątrz sterownika, należy zadeklarować wszystkie takie zmienne dzielone między sterownikiem a ISR static. Taka zmienna nie jest globalna, ale ogranicza się do pliku, w którym została zadeklarowana.
  • Aby uniknąć problemów związanych z optymalizacją kompilatora, takie zmienne należy również zadeklarować jako volatile. Uwaga: nie daje to dostępu atomowego ani nie rozwiązuje problemu ponownego wejścia!
  • W sterowniku często potrzebny jest pewien mechanizm mechanizmu ponownego wejścia na wypadek, gdyby ISR zapisał zmienną. Przykłady: wyłączanie przerwań, globalna maska ​​przerwań, semafor / muteks lub gwarantowane odczyty atomowe.
Lundin
źródło
Uwaga: konieczne może być ujawnienie prototypu funkcji ISR ​​przez nagłówek, aby umieścić go w tabeli wektorowej znajdującej się w innym pliku. Ale to nie jest problem, o ile udokumentujesz, że jest to przerwanie i nie powinien zostać wywołany przez program.
Lundin,
Co byś powiedział, gdyby kontrargumentem był zwiększony narzut (i dodatkowy kod) związany z używaniem funkcji ustawiających / pobierających? Omówiłem to sam, myśląc o standardach kodu dla naszych 8-bitowych urządzeń osadzonych.
Leroy105
2
@ Leroy105 Język C obsługuje już wbudowane funkcje na wieczność. Chociaż nawet użycie inlinestaje 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.
Lundin,
2
To powiedziawszy, w przypadku pisania sterowników ISR, około 80-90% wszystkich programistów (nie przesadzając tutaj) zawsze coś w nich źle robi. Rezultatem są subtelne błędy: niepoprawnie wyczyszczone flagi, niepoprawna optymalizacja kompilatora z powodu braku lotności, warunków wyścigu, kiepskiej wydajności w czasie rzeczywistym, przepełnienia stosu itp. W przypadku, gdy ISR nie jest odpowiednio zamknięty w sterowniku, istnieje prawdopodobieństwo takich subtelnych błędów dalej wzrosła. Skoncentruj się na napisaniu sterownika wolnego od błędów, zanim zaczniesz martwić się o rzeczy o znaczeniu peryferyjnym, takie jak na przykład, że seter / getters wprowadzi niewielki narzut.
Lundin,
10
to użycie zmiennych globalnych jest dla mnie sprzeczne z rzeczywistością

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:

  1. Liczniki tyknięć zegara zarządzane przez przerwanie zegara systemowego. Zwykle mam okresowe przerwanie zegara, które działa co 1 ms. Jest to często przydatne do różnych ustawień czasowych w systemie. Jednym ze sposobów, aby wyciągnąć te informacje z rutyny przerwań do miejsca, gdzie reszta systemu może ich użyć, jest utrzymanie licznika tykania zegara globalnego. Procedura przerwania zwiększa licznik co tyknięcie zegara. Kod pierwszego planu może odczytać licznik w dowolnym momencie. Często robię to przez 10 ms, 100 ms, a nawet 1 sekundę tyka.

    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.

  2. Ostateczne filtrowane i skorygowane wartości A / D. Powszechną rzeczą do zrobienia jest rutynowa obsługa odczytów z A / D. Zwykle czytam wartości analogowe szybciej niż to konieczne, a następnie stosuję trochę filtrowania dolnoprzepustowego. Często stosuje się także skalowanie i przesunięcie.

    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.

Olin Lathrop
źródło
Idę na terapię, w wolnym tygodniu, naprawdę próbując poderwać mój kod. Widzę punkt, w którym Lundin ograniczył dostęp do zmiennych, ale patrzę na moje rzeczywiste systemy i myślę, że jest to taka odległa możliwość, ŻADNA OSOBA faktycznie rzuciłaby kluczową zmienną globalną o znaczeniu krytycznym dla systemu. Funkcje Getter / Setter kosztują cię narzut w porównaniu do zwykłego korzystania z globalnego i akceptując te są dość prostymi programami ...
Leroy105 12.01.18
3
@ Leroy105 Problemem nie są „terroryści” celowo nadużywający zmiennej globalnej. Zanieczyszczenie przestrzeni nazw może stanowić problem w większych projektach, ale można to rozwiązać za pomocą dobrego nazewnictwa. Nie, prawdziwy problem polega na tym, że programista próbuje użyć zmiennej globalnej zgodnie z przeznaczeniem, ale nie robi tego poprawnie. Albo dlatego, że nie zdają sobie sprawy z problemu warunków wyścigu, który występuje we wszystkich ISR, albo dlatego, że psują implementację obowiązkowego mechanizmu ochronnego, lub po prostu dlatego, że wyrzucają z użycia zmienną globalną w całym kodzie, tworząc ścisłe powiązanie i nieczytelny kod.
Lundin,
Twoje punkty są ważne, Olin, ale nawet w tych przykładach zastąpienie extern int ticks10msgo inline 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.
Groo
@Groo: Jest to prawdą tylko wtedy, gdy używasz języka obsługującego funkcje wstawiania, a to oznacza, że ​​definicja funkcji pobierającej musi być widoczna dla wszystkich. W rzeczywistości, gdy używam języka wysokiego poziomu, używam więcej funkcji gettera, a mniej zmiennych globalnych. W asemblerze o wiele łatwiej jest chwycić wartość zmiennej globalnej niż zawracać sobie głowę funkcją getter.
Olin Lathrop,
Oczywiście, jeśli nie możesz inline, wybór nie jest taki prosty. Chciałem powiedzieć, że przy funkcjach wbudowanych (i wielu kompilatorach wcześniejszych niż C99 obsługuje już wbudowane rozszerzenia), wydajność nie może być argumentem przeciwko getterom. Przy rozsądnym kompilatorze optymalizującym powinieneś uzyskać ten sam wyprodukowany zestaw.
Groo
2

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:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

Obiekty uart1_info, uart2_infoitp 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ć jako volatilewszystko, 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ć:

if (foo->timer)
  foo->timer--;

pisać:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

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.

supercat
źródło
0

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.

kkrambo
źródło
0

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:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

lub kod obiektowy z funkcją „wirtualną”:

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… 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ń.

CL.
źródło
powinieneś zadeklarować flagę jako zmienną int *.
następny hack
0

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, CInterruptVectorTablektóra zawiera wszystkie procedury obsługi przerwań zapisane w rzeczywistym wektorze przerwań kontrolera:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

Klasa CInterruptVectorTableimplementuje 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:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

Musisz wykonać funkcje, które są przechowywane w tablicy wektorów, staticponieważ kontroler nie może dostarczyć wskaźnika this-po, ponieważ tablica wektorów nie jest obiektem. Aby obejść ten problem, mamy wskaźnik statyczny w pThisśrodku CInterruptVectorTable. Po wejściu w jedną z funkcji statycznego przerywania, może uzyskać dostęp do pThiswskaźnika -po, aby uzyskać dostęp do członków jednego obiektu CInterruptVectorTable.


Teraz w programie możesz użyć wskaźnika, SetIsrCallbackfunctionaby dostarczyć wskaźnik funkcji do staticfunkcji, która ma być wywoływana, gdy nastąpi przerwanie. Wskaźniki są przechowywane w InterruptVectorTable_t virtualVectorTable.

Implementacja funkcji przerwania wygląda następująco:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

To wywoła staticmetodę 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 IInterruptHandleri przechowywać wskaźniki do obiektów, więc nie potrzebujesz wskaźnika static 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ą ICallbacknastę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 thisustawić 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.

Arsenał
źródło
1
„abyś mógł powiązać różne funkcje z wektorami przerwań podczas działania” To brzmi jak zły pomysł. „Cyklomatyczna złożoność” programu po prostu przejdzie przez dach. Wszystkie kombinacje przypadków użycia musiałyby zostać przetestowane, aby nie występowały konflikty czasowe ani stosowe. Dużo bólu głowy dla funkcji o bardzo ograniczonej użyteczności IMO. (Chyba że masz skrzynię ładującą, to inna historia) Ogólnie rzecz biorąc, pachnie to programowaniem meta.
Lundin,
@Lundin Naprawdę nie rozumiem, o co ci chodzi. Używamy go do powiązania na przykład przerwania DMA z obsługą przerwań SPI, jeśli DMA jest używany dla SPI, i do obsługi przerwań UART, jeśli jest on używany dla UART. Oczywiście, oba programy obsługi muszą zostać przetestowane, ale to nie problem. I na pewno nie ma to nic wspólnego z meta programowaniem.
Arsenał
DMA to jedno, przypisanie wektorów przerwań w czasie wykonywania jest czymś zupełnie innym. Sensowne jest, aby konfiguracja sterownika DMA była zmienna w czasie wykonywania. Tabela wektorów, nie tyle.
Lundin,
@Lundin Wydaje mi się, że mamy różne poglądy na ten temat, moglibyśmy porozmawiać na ten temat, ponieważ wciąż nie widzę twojego problemu z tym - więc może moja odpowiedź jest tak źle napisana, że ​​cała koncepcja jest źle zrozumiana.
Arsenal