Kurs FreeRTOS #6: obsługa przerwań

freertos przerwania

Programując systemy wbudowane bardzo często musimy reagować na jakieś zdarzenia – pojawienie się zbocza, zakończenie pomiaru czy nadejście danych. Można w tym celu zastosować pooling i ciągle sprawdzać występujące stany, ale tego staramy się unikać. Alternatywą jest wykorzystanie przerwań, co w embedded jest codziennością. Czas pokazać jak wygląda ich obsługa w systemie FreeRTOS 🙂 Przy okazji opiszę semafory zliczające i rozwinę temat semaforów binarnych – tym razem wykorzystamy je do synchronizacji zadań z przerwaniami.

Wprowadzenie

W drugim artykule tej serii podczas przygotowywania pustego projektu skorzystaliśmy z gotowego pliku konfiguracyjnego dostępnego w aplikacji demonstracyjnej dopasowanej do naszego procesora. Jest to sposób polecany przez producenta, ponieważ ustawienie niektórych makr nie jest proste, a ich błędne ustawienie spowoduje nieprawidłowe działanie systemu. Bywają one także zależne od architektury, przez co w niektórych przypadkach trzeba je ustawić, a w innych nie. Przykładem takich makr są configKERNEL_INTERRUPT_PRIORITY i configMAX_SYSCALL_INTERRUPT_PRIORITY. Zdefiniowanie jednego, a czasami obu jest wymagane w przypadku, gdy dany procesor wspiera zagnieżdżanie przerwań. Tak się składa, że oba mikrokontrolery, dla których wrzuciłem template na githuba, wyposażone są w NVIC (Nested Vector Interrupt Controller) i wspierają zagnieżdżanie przerwań, dlatego muszą zostać zdefiniowane.

NVIC w pigułce

Przydałoby się tutaj dokładnie opisać zasadę działania NVIC i systemu przerwań, ale jest to zbyt szeroki temat, aby omawiać go w ramach serii o FreeRTOS. Postaram się go jednak chociaż trochę przybliżyć, aby mieć się do czego odnosić w dalszym opisie. Kontroler NVIC w procesorach z rodziny Cortex pozwala na obsługę 15 przerwań wewnętrznych i maksymalnie 240 zewnętrznych. Każdemu przerwaniu przypisywany jest priorytet i w pierwszej kolejności obsługiwane są te o wyższym priorytecie. Co więcej, przerwanie o wyższym priorytecie może wywłaszczyć przerwanie o niższym priorytecie. Wygląda to podobnie jak w przypadku zadań. Jest jednak jedna zasadnicza różnica. W przypadku zadań większa wartość liczbowa priorytetu oznaczała, że jest on wyższy (zadanie o wyższej wartości priorytetu jest ważniejsze). Tutaj jest odwrotnie. W związku z tym najwyższemu priorytetowi przerwania odpowiada wartość 0. Dla jasności dodam, że priorytety zadań i przerwań to dwie zupełnie inne rzeczy i nie są ze sobą w żaden sposób powiązane.

W Cortexach do określania wartości priorytetu danego przerwania wykorzystywany jest 8-bitowy rejestr, jednak w mikrokontrolerach STM32 często wykorzystywanych jest z niego tylko kilka bitów. Ich liczba jest zdefiniowana w CMSIS (Cortex Microcontroller Software Interface Standard), na którym bazuje każda biblioteka od ST.

W wypadku STM32F103 oraz STM32F429 mamy do dyspozycji 4 bity, co daje możliwość zdefiniowana 16 poziomów priorytetów. Każdy priorytet można podzielić na dwa elementy: priorytet główny, wywłaszczający (preempt priority) oraz podpriorytet (sub priority). Gdy podczas obsługiwania jakiegoś przerwania pojawi się zgłoszenie od innego przerwania to kontroler w pierwszej kolejności porównuje ich priorytety główne i na tej podstawie stwierdza czy dane przerwanie może wywłaszczyć aktualnie wykonywane przerwanie. Wartości podpriorytetów są wykorzystywane w sytuacji gdy w tym samym czasie wystąpi żądanie obsługi przerwań o takim samym priorytecie głównym. W pierwszej kolejności zostanie obsłużone przerwanie o wyższym podpriorytecie. Liczbę bitów przypadającą na ilość poziomów priorytetów głównych i podpriorytetów można skonfigurować – po prostu ich suma musi być równa __NVIC_PRIO_BITS 😉 W dokumentacji FreeRTOSa znajdziemy jednak informację, że zalecane jest takie skonfigurowanie kontrolera NVIC, aby wszystkie bity przypadały dla priorytetów wywłaszczających. Zajmiemy się tym w dalszej części artykułu.

Konfiguracja

Była mała dygresja związana z opisem NVIC, ale już wracamy do FreeRTOSa 🙂 Poniżej wklejam fragmenty plików FreeRTOSConfig.h odpowiadające opisywanym makrom zarówno dla mikrokontrolera STM32F103 (Cortex M3), jak i STM32F429 (Cortex M4F).

STM32F103:

STM32F429:

Z poziomu systemu istotne są tylko makra configKERNEL_INTERRUPT_PRIORITY oraz configMAX_SYSCALL_INTERRUPT_PRIORITY, które w nowszych wersjach systemu przyjmuje także nazwę configMAX_API_CALL_INTERRUPT_PRIORITY. Pozostałe są pomocnicze.

Makro configKERNEL_INTERRUPT_PRIORITY

W przypadku makra configKERNEL_INTERRUPT_PRIORITY sprawa jest prosta. Służy ono do ustawiania wartości priorytetu dla przerwań jądra systemu: licznika SysTick, a w przypadku opisywanych mikrokontrolerów także PendSV oraz SVCall, o których wspominałem na początku kursu. W systemie FreeRTOS przerwania systemowe muszą posiadać najniższy możliwy priorytet, czyli mieć przypisaną najwyższą możliwą wartość liczbową priorytetu. Dzięki takiej konfiguracji system nie przerywa i nie blokuje innych przerwań, które mogą być krytyczne pod względem poprawności działania całego urządzenia. Jak już wspominałem, w naszych mikrokontrolerach do definiowania wartości priorytetu są wykorzystywane tylko 4 bity. Żeby było ciekawiej są to 4 najbardziej znaczące bity rejestru 🙂 Jądro systemu ma bezpośredni dostęp do warstwy sprzętowej, dlatego wartość priorytetu podajemy tutaj tak jak byśmy wpisywali ją do rejestru. Chcąc więc ustawić tym przerwaniom najniższy priorytet o liczbowej wartości 15 konieczne jest wpisanie do rejestru 1111xxxx , co jest równoważne z zapisem pliku konfiguracyjnego STMF429 ( 0x0f przesunięte w lewo o 4 bity):

Cztery najmłodsze bity mogą przyjmować dowolną wartość, jednak producent zaleca aby ze względu na zachowanie możliwie najwyższej kompatybilności pomiędzy mikrokontrolerami im także ustawić jedynki. Dlatego w przypadku pliku konfiguracyjnego F103 mamy wartość 255:

Makro configMAX_SYSCALL_INTERRUPT_PRIORITY

Aby wytłumaczyć to makro ponownie będzie trzeba zbudować trochę kontekstu 😉 System składa się z różnych struktur i zmiennych, na których wykonywane są ciągłe operacje. Co istotne, zmian ich zawartości nie zawsze dokonuje wyłącznie pracujący w tle system. Są one także modyfikowane podczas wywołań API systemowego, czyli np. funkcji do pobierania semafora. Niektóre funkcje, a dokładniej mówiąc te, które posiadają w nazwie FromISR mogą być wywoływane w procedurach obsługi przerwania. W związku z tym dostęp do niektórych danych musi być chroniony i aby system mógł działać poprawnie musi on czasami wyłączać przerwania. Jeżeli prześledzisz kod systemu, to zauważysz, że w niektórych miejscach wywoływana jest funkcja portDISABLE_INTERRUPTS(). Co więcej, w niektórych sytuacjach programista sam będzie chciał wyłączać przerwania, ale o tym dokładniej opowiem w kolejnym poście o muteksach.

Wydawać by się mogło, że portDISABLE_INTERRUPTS() wyłącza wszystkie przerwania i w przypadku niektórych mikrokontrolerów jest to prawda, ale wtedy makro configMAX_SYSCALL_INTERRUPT_PRIORITY nie jest w ogóle implementowane. Inaczej to wygląda w przypadku mikrokontrolerów, na których bazujemy w tym kursie. W ich przypadku podczas wywołania funkcji portDISABLE_INTERRUPTS() wyłączane są jedynie przerwania z priorytetami posiadającymi równą lub wyższą wartość liczbową od tej zdefiniowanej w configMAX_SYSCALL_INTERRUPT_PRIORITY, czyli te mniej znaczące.

Zastanawiasz się może dlaczego to zostało tak skomplikowane? Otóż, wyłączanie przerwań wydłuża czas reakcji na pojawiające się przerwanie. Powinno się tego unikać – szczególnie w przypadku krytycznych zdarzeń o wysokim priorytecie. Jak widać to komplikowanie ma swoje uzasadnienie 🙂 Dzięki takiemu mechanizmowi przerwania o najwyższych priorytetach (najmniejszych wartościach liczbowych) nie zostają wyłączane i nie mają dodatkowych opóźnień. Jest jedno ALE. W takich przerwaniach nie można wywoływać funkcji API systemowego. Inaczej mówiąc, każde przerwanie, w którym korzystamy z API systemowego musi mieć priorytet taki sam lub niższy (wyższą lub równą wartość liczbową) od configMAX_SYSCALL_INTERRUPT_PRIORITY. Ustawianie makra wygląda tutaj tak samo jak w przypadku poprzedniego, dlatego zostawię Ci to do samodzielnego przanalizowania. Dodam tylko, że tutaj mamy swobodę w ustawianiu wartości i możemy ją dobierać do potrzeb swojej aplikacji. Należy to jednak robić starannie!

Przykłady

Przed nami już tylko ta łatwiejsza część 🙂 Na początku skonfigurujmy jakieś przerwanie. Najwygodniej będzie wykorzystać przerwanie zewnętrzne, które będzie generowane przy pojawieniu się zbocza opadającego, na którymś z GPIO – w moim przypadku będzie to pin PC13, ponieważ do tego pinu podłączony jest przycisk na płytce Nucleo z mikrokontrolerem STM32F103. W pierwszym podejściu załóżmy, że w procedurze obsługi przerwania będziemy zmieniać stan któregoś pinu na przeciwny, to wszystko. Do dzieła! Jak wspominałem, zalecane jest aby wszystkie priorytety były wywłaszczające. W przypadku biblioteki SPL grupowanie priorytetów można ustawić za pomocą funkcji NVIC_PriorityGroupConfig(). W związku z tym, że w ISR tego przerwania nie będziemy korzystali z żadnej funkcji systemowej to możemy mu przypisać dowolny priorytet. Oprócz skonfigurowania NVIC, trzeba jest skonfigurować EXTI oraz ustawić pin jako wejście Hi-Z. Na płytce z której korzystam jest już on sprzętowo podciągniety do 3.3 V. Znajduje się tam także prosty filtr dolnoprzepustowy, który minimalizuje efekt drgania styków. W związku z tym, że korzystamy z GPIO w innym celu niż domyślny, konieczne jest jeszcze włączenie sygnału zegarowego AFIO. Poniżej zamieszczam cały kod:

Jak widzisz nie ma tutaj nic nadzwyczajnego. Wygląda i działa to prawie identycznie jak w aplikacjach bez systemu operacyjnego. Przejdźmy teraz do trochę bardziej złożonego przypadku 🙂

Synchronizacja zadań z przerwaniami

Jak wiadomo procedury obsługi przerwań powinny być możliwie najkrótsze. W powyższym przykładzie zmienialiśmy tylko stan GPIO na przeciwny, więc można to było bez problemu umieścić bezpośrednio w ISR. W przypadku konieczności wykonania większej ilości długotrwałych operacji jest to niewskazane, a często wręcz zabronione. W takiej sytuacji obsługę przerwania implementujemy w zadaniu i synchronizujemy je z przerwaniem. Dzięki zastosowaniu takiego mechanizmu procedura obsługi przerwania będzie bardzo krótka i szybko się wykona. Jak już wiadomo z poprzedniego wpisu, mechanizmem wykorzystywanym do synchronizacji są semafory binarne. W poprzednim poście wykorzystaliśmy je do synchronizacji zadań, dzisiaj zajmiemy się synchronizacją zadań i przerwań. Mam dla Ciebie dobrą wiadomość – zasada działania jest identyczna 🙂 Aby zsynchronizować zadanie z przerwaniem należy w zadaniu pobierać semafor, a w procedurze obsługi przerwania go dawać. W związku z tym zadanie przez większość czasu będzie przebywało w stanie zablokowania, a w momencie pojawienia się przerwania będzie odblokowywane.

Pobieranie semafora wygląda w tym wypadku tak samo jak zostało to zaprezentowane w poprzednim wpisie, jednak dawanie semafora będzie wyglądało trochę inaczej, ponieważ będzie to realizowane w procedurze obsługi przerwania. Po pierwsze, służy do tego inna funkcja – xSemaphoreGiveFromISR(). Warto w tym miejscu jeszcze raz podkreślić, że w procedurach obsługi przerwania należy korzystać z odpowiedników funkcji zakończonych frazą FromISR. Jak wiadomo z poprzedniego wpisu, danie semafora powoduje, że zadanie, które na niego czekało przechodzi w stan gotowości i jeżeli ma wyższy priorytet od aktualnie wykonywanego to następuje przełączenie kontekstu. Wewnątrz przerwania nie jest to jednak realizowane automatycznie, dlatego funkcja xSemaphoreGiveFromISR() w odróżnieniu od xSemaphoreGive() przyjmuje jeden dodatkowy argument *pxHigherPriorityTaskWoken. Na jego podstawie możliwe jest określenie czy zadanie, które zostało odblokowane wskutek wywołania xSemaphoreGiveFromISR() ma wyższy priorytet od zadania, które zostało przerwane. Jeżeli tak to *pxHigherPriorityTaskWoken przyjmie wartość pdTRUE i na tej podstawie można wyzwolić przełączenie kontekstu. Służy do tego funkcja portYIELD_FROM_ISR() przyjmująca jeden argument. Jeżeli przekazany do niej parametr ma wartość różną od pdFALSE to zostanie wyzwolony proces przełączania kontekstu. W przeciwnym wypadku nic się nie dzieje i po zakończenie procedury obsługi przerwania procesor wraca do wykonywania przerwanego zadania.

Należy jeszcze zwrócić uwagę na jedną bardzo istotną rzecz. Tym razem w procedurze obsługi przerwania jest wykorzystywana funkcja systemowa xSemaphoreGiveFromISR(), dlatego priorytet tego przerwania musi przyjmować wartość z zakresu od configMAX_SYSCALL_INTERRUPT_PRIORITY do 15 . To tyle, czas na kod! Wydaje mi się, że już wszystko opisałem, więc nie powinno być żadnego problemu z jego zrozumieniem. Oczywiście należy pamiętać o dodaniu pliku nagłówkowych i utworzeniu semaforów. Wygląda to identycznie jak w poprzednim wpisie, więc nie będę się powtarzał.

Semafory licznikowe

To jeszcze nie koniec 😀 Zostało jedno zagadnienie, które koniecznie muszę poruszyć. W powyższym przykładzie przerwania generowaliśmy ręcznie, z stosunkowo niską częstotliwością. Ponadto w systemie nic poza wykonywaniem jednego zadania przypisanego do przerwania się nie dzieje. W związku z tym nie ma żadnych opóźnień. Po pojawieniu się przerwania od razu wykonywane jest zadanie. W bardziej złożonej aplikacji niekonieczne będzie to tak wyglądać. W przypadku przerwań pojawiających się z dużą częstotliwością może zaistnieć sytuacja, w której przerwanie pojawi się kilka razy zanim przypisane mu zadanie pobierze dostępny semafor. Co stanie się w takiej sytuacji? Mimo, że zostało wygenerowanych np. 5 przerwań zadanie wykona się tylko raz, ponieważ nawet nie będzie wiedziało, że przerwanie wystąpiło wielokrotnie. Rozwiązaniem tego problemu są semafory zliczające. Działają one tak samo jak semafory binarne, ale nie są ograniczone do wartości 1 i 0 – mogą przyjmować wartości większe od 1. Podczas dawania semafora jego licznik jest inkrementowany, a w momencie pobierania jest on dekrementowany. Wartość licznika definiuje różnicę pomiędzy liczbą wystąpień przerwania, a liczbą ich obsłużeń w przypisanym zadaniu.

Na nasze szczęście, do obsługi semaforów licznikowych służą te same funkcje. Inaczej wygląda jedynie jego tworzenie. Służy do tego funkcja xSemaphoreCreateCounting(). Przyjmuje ona dwa argumenty. Pierwszym z nich jest uxMaxCount, który określa maksymalną wartość jaką może przyjąć semafor licznikowy. Drugim argumentem jest uxInitialCount, który określa początkową wartość semafora po jego utworzeniu. Gdy semafory zliczające wykorzystujemy do zliczania zdarzeń i synchronizacji przerwań z zadaniami to uxInitialCount ustawiamy na wartość 0, zakładając, że przed utworzeniem semafora nie wystąpiło żadne przerwanie. Semafory licznikowe oprócz zliczania zdarzeń mogą być także wykorzystywane do zapewniania bezpiecznego dostępu do współdzielonych zasobów. Wtedy uxInitialCount nie jest ustawiane na 0, ale o tym opowiem już w kolejnym wpisie 😉 Aby móc korzystać z semaforów zliczających należy jeszcze pamiętać o ustawieniu makra configUSE_COUNTING_SEMAPHORES na wartość 1 w pliku konfiguracyjnym FreeRTOSConfig.h .

Podsumowanie

Wow! W końcu 😀 To chyba jak dotąd najdłuższy artykuł na tym blogu. I do tego tyle mięsa. Mam wrażenie, że najtrudniejsze z tego wpisu mogą być makra. Jest to szczególnie zakręcone w przypadku Cortexów. Nie ma się co nimi za bardzo przejmować! Oczywiście warto je znać i wiedzieć jak to wszystko działa, ale jeżeli nie wszystko jest do końca zrozumiałem to równie dobrze można na razie korzystać z gotowych plików konfiguracyjnych, a do tego tematu wrócić jeszcze raz w przyszłości. Dalsza część wydaje się już ok. Szczególnie, że podobnie to wyglądało w poprzednim wpisie o semaforach.

Wiem, że znów trzeba było sporo czekać, ale uznałem, że rozbijanie takich tematów na kilka oddzielnych wpisów generowałoby bałagan w tej serii i pod koniec kursu mielibyśmy 20 wpisów dotyczących FreeRTOSa. W tym wypadku wbijamy w Kurs FreeRTOS #6: przerwania i mamy wszystko o przerwaniach 🙂 Mam nadzieję, że mimo takiej długości wpis się podobał 🙂 Pozdro i do następnego, który nie zapowiada się być krótszy… 😀

Kurs FreeRTOS #7: muteksy