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ę.
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:
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:
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:
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.
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.
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/
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ę.
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:
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:
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
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
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
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