Kurs Raspberry Pi Pico – #8 – Przerwania i alarmy

Czas czytania: 9 min.

Przed nami kolejny materiał o niewielkim zestawie deweloperskim Raspberry Pi Pico W. Ostatnim razem przyjrzeliśmy się nieco bliżej optymalizacji kodu oraz napisaliśmy kilka własnych funkcji. Poza tym skorzystaliśmy ze wbudowanego w rdzeń PR2040 czujnika temperatury. Tym razem poznamy niesamowity świat przerwań mikrokontrolera.

Kup zestaw do nauki programowania z Raspberry Pi Pico W i skorzystaj z kursu dostępnego na Blog Botland!

W zestawie: moduł Raspberry Pi Pico W, płytka stykowa, przewody, diody LED, rezystory, przyciski, fotorezystory, cyfrowe czujniki światła, temperatury, wilgotności i ciśnienia, wyświetlacz OLED i przewód USB-microUSB.

Przed wyruszeniem w drogę należy zebrać drużynę

Zestaw elementów do kursu Raspberry Pi Pico.

Chcąc uczyć się programowania, bazując na rzeczywistych projektach, potrzebny będzie oczywiście odpowiedni sprzęt, ale bez obaw – nie musisz teraz skakać między kolejnymi artykułami i przygotowywać listę niezbędnych elektronicznych elementów. W sklepie Botland dostępny jest gotowy zestaw, zawierający wszystkie komponenty niezbędne do wykonania projektów opisanych w serii poradników o Raspberry Pi Pico. 

gotowym zestawie elementów znajdziecie:

  • Raspberry Pi Pico W,
  • Przewód microUSB,
  • Płytkę stykową,
  • Zestaw przewodów połączeniowych w trzech rodzajach,
  • Zestaw diod LED w trzech kolorach,
  • Zestaw najczciej stosowanych w elektronice rezystorów,
  • Przyciski Tact Switch,
  • Fotorezystory,
  • Cyfrowy czujnik światła,
  • Cyfrowy czujnik wilgotności, temperatury i ciśnienia,
  • Wyświetlacz OLED.

Czym są i jak działają przerwania?

Czytając ten tekst, wasze dłonie spoczywają zapewne na komputerowej myszce. Jeśli nią poruszycie, kursor widoczny na ekranie monitora zmieni swoją pozycję i nic w tym dziwnego, bo takie właśnie jest zadanie wskaźnika zwanego myszką. Jednak czy zastanawialiście się kiedyś, w jaki sposób proces ten jest obsługiwany, innymi słowy jak to możliwe, że komputer wie, że właśnie w tym momencie przesunęliście urządzenie wskazujące. Można by odpowiedzieć – przecież to proste, laser w myszce wykrywa ruch, dane przesyłane są do komputera, a ten na ich podstawie zmienia lokalizację kursora na ekranie. Odpowiedź jest jak najbardziej poprawna, ale skąd procesor w komputerze wie, że to właśnie w tym momencie myszka została przesunięta, a nie za sekundę, dwie lub dziesięć. Można by pomyśleć, że sprawdza on co chwilę, czy położenie urządzenia wskazującego uległo zmianie, ale teoria ta jest lekko naciągana. I bez tego komputer ma co robić: obsługa przeglądarki, zminimalizowane okno Spotify z ulubioną muzyką czy też niezliczona liczba procesów systemowych, o których istnieniu nie mamy nawet pojęcia. W każdej milisekundzie przez procesor przetacza się potok bitów i ciągłe sprawdzanie, czy przypadkiem myszka nie została przesunięta, byłoby niezwykłym marnotractwem zasobów. I tutaj właśnie do gry wkraczają przerwania.

Gdy poruszymy myszką, dane odebrane przez kontroler USB lub Bluetooth przesłane zostaną do procesora wraz z informacją o przerwaniu. Innymi słowy, układ scalony obsługujący komunikację poinformuje CPU, że jedno z urządzeń zewnętrznych potrzebuje natychmiastowej obsługi. W tym właśnie momencie jednostka centralna rzuci do pracy wszystkie swoje zasoby, przerywając aktualnie wykonywane procesy tak, aby, jak najszybciej obsłużyć myszkę i skierować kursor w odpowiednie miejsce. Gdy zadanie to zostanie wykonane, procesor wróci do swojej zwyczajnej pracy. Oczywiście cały ten opis jest pewnym uproszczeniem i obsługa urządzeń USB wygląda nieco inaczej, ale idea jest właśnie taka. Wszystko opiera się na przerwaniach, dzięki którym procesor wie, że musi natychmiast wstrzymać aktualnie wykonywane zadanie i zrobić „coś” co w hierarchii systemu jest ważniejsze.

Przerwania w RP2040

Tabela przerwań RP2040 (rejestr IRQ).

Mikrokontroler RP2040 podobnie jak inne procesory z rdzeniem ARM charakteryzuje się pewnym uniwersalnym zestawem przerwań, który podzielić możemy na kilka kategorii.

  • Przerwania GPIO – Raspberry Pi Pico wspiera przerwania generowane w odpowiedzi na zmiany stanu na pinach GPIO. Przerwania te są konfigurowalne i mogą być aktywowane dla zbocza narastającego, opadającego lub konkretnego poziomu logicznego. Dzięki tego typu funkcjonalności mikrokontroler może reagować na zdarzenia takie jak naciśnięcie przycisku, zmiana stanu na magistrali, czy też detekcja zdarzeń z prostych czujników.
  • Przerwania timerów – RP2040 wyposażono w dwa wielofunkcyjne timery, które również mogą generować przerwania. Tego typu wykorzystanie timerów pozwala wykonywać konkretne zadania w precyzyjnie odmierzanych interwałach czasowych.
  • Przerwania UART/SPI/I2C/USB – Interfejsy komunikacyjne wspierane przez RPI posiadają własny zestaw przerwań. Wspierają one przesyłanie danych między mikrokontrolerem a innymi peryferiami. Przerwania mogą obsługiwać zarówno odbiór, jak i nadawanie danych, co znacznie upraszcza programy korzystające z tych interfejsów.
  • Przerwania PWM – W jednym z wcześniejszych poradników korzystaliśmy już z modułu obsługującego modulacji PWM. Warto jednak wiedzieć, że ten komponent procesora również może generować przerwania w momencie zakończenia cyklu PWM lub osiągnięcia określonej wartości wypełnienia impulsu. Jest to przydatna funkcjonalność zwłaszcza w aplikacjach wymagających precyzyjnej kontroli sygnałów.
  • Przerwania ADC – Kolejnym komponentem, z którego korzystaliśmy, a który również może tworzyć przerwania, jest wbudowany przetwornik ADC. W tym przypadku sygnał aktywujący przerwanie pojawi się po zakończeniu konwersji analogowo-cyfrowej.
  • Przerwania RTC – Projektanci RP2040 zdecydowali się umieścić wewnątrz procesora zegar czasu rzeczywistego RTC, który może generować przerwania w określonych interwałach czasowych lub w związku z określonymi zdarzeniami zegarowymi, takimi jak przejście przez godzinę, minutę, itp. Dzięki tego typu przerwaniom możliwe jest precyzyjne zarządzanie czasem.
  • Przerwania DMA – Większość współczesnych jednostek centralnych wspiera przesyłanie danych między pamięcią a peryferiami bez udziału CPU. Proces ten realizowany jest przez jednostki DMA, które również mogą generować przerwanie, będące efektem zakończenia wymiany informacji.

Poza tymi dość uniwersalnymi przerwaniami RP2040 wyposażono też w coś takiego, jak XIP_IRQ. Skrót ten nie jest aż tak jednoznaczny jak pozostałe widoczne w tabeli powyżej. Jest to przerwanie skojarzone z modułem SSI, który wykorzystywany jest między innymi w sytuacji, gdy program realizowany przez mikrokontroler zapisany jest w jego wewnętrznej, ulotnej pamięci SRAM.

Pewną ciekawostką jest też struktura rejestru IRQ, w którym zapisywane są przerwania. Jak łatwo zauważyć przerwań jest 26, natomiast sam rejestr jest 32 bitowy. Pozostałe sześć bitów jest niewykorzystane i są one fizycznie niepodłączone do modułu NVIC, czyli komponentu zajmującego się obsługą przerwań. Jednak, mimo że procesor nie korzysta ze zbędnych bitów, to z poziomu programu można je zapisać, choć nie ma to żadnych fizycznych efektów.

Zmiana stanu diody w przerwaniu

W kolejnym programie skorzystamy z tego samego obwodu.

Aby przetestować, jak działają przerwania, przygotujemy prosty program, który będzie zmieniał stan diody LED na przeciwny przy jednorazowym wciśnięciu przycisku. Projekt ten nazwałem interrupt_led i skorzystamy w nim z wcześniej przygotowanego obwodu. Po przygotowaniu nowego projektu możemy od razu przejść do kodu.

				
					#include 
#include "pico/stdlib.h"

#define YELLOW_LED 1
#define BUTTON 16

bool led_status = false;

//interrupt handler function
void gpio_irq_handler(uint gpio, uint32_t events)
{
    led_status = !led_status;
    gpio_put(YELLOW_LED, led_status);
}

int main() {
    stdio_init_all();

    // LED and button initialization
    gpio_init(YELLOW_LED);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_put(YELLOW_LED, led_status);

    gpio_pull_up(BUTTON);

    //interrupt initialization
    gpio_set_irq_enabled_with_callback(BUTTON, GPIO_IRQ_EDGE_FALL, true, gpio_irq_handler);

    while(true) {
       tight_loop_contents(); // "do nothing" function
    }
}

				
			

Program, który uruchomimy tym razem, jest dość prosty i krótki. Na początku jak zwykle dołączamy potrzebne biblioteki oraz definiujemy port, do którego podłączona jest dioda LED oraz przycisk. Poza tym umieściłem też tutaj globalną zmienną led_status, która przechowywać będzie aktualny stan diody LED. Kolejnym elementem kodu jest funkcja skojarzona z przerwaniem, ale do niej wrócimy za moment, aktualnie przejdźmy do głównej funkcji main. W jej wnętrzu zainicjowana została dioda LED, do której od razu przypisany został stan zapisany w zmiennej status. Jej wartością domyślną jest false, dlatego po uruchomieniu programu dioda nie będzie świecić. Następnie dzięki funkcji gpio_pull_up wyprowadzenie numer 16, do którego podłączony jest przycisk, zostanie podciągnięte do zasilania.

Kolejna z funkcji uruchamia przerwanie GPIO. Jej argumentami są: wyprowadzenie, z którego pochodzić będzie przerwanie, event_mask – zdarzenie, którego efektem ma być powstanie sygnału przerwania, true – argument uruchamiający przerwanie oraz tak zwany handler, czyli funkcja, w której opisana została obsługa przerwania. W naszym kodzie funkcja uruchamiająca przerwanie opisana została definicją BUTTON, czyli przerwanie powinno pochodzić z pinu oznaczonego numerem 16. Argumentem event_mask jest GPIO_IRQ_EDGE_FALL, czyli przerwanie pojawi się, gdy na wyprowadzeniu pojawi się zbocze opadające. Innymi słowy, gdy wciśniemy przycisk, ponieważ ten zwiera pin RP2040 do masy. Innymi argumentami mogą być tutaj też:

  • GPIO_IRQ_EDGE_RISE – zbocze narastające,
  • GPIO_IRQ_LEVEL_HIGH – stan wysoki na wyprowadzeniu,
  • GPIO_IRQ_LEVEL_LOW – stan niski na wyprowadzeniu.

Następnie dzięki true, uruchamiamy przerwanie i podajemy nazwę funkcji, która ma zostać uruchamiana po wykryciu przerwania, czyli gpio_irq_handler.

Po inicjalizacji procesor przejdzie do wykonywania nieskończonej pętli while, która nie robi nic. W jej wnętrzu umieściłem specjalne polecenie, które oznacza właśnie, nie rób nic i można je porównać do znanego z assemblera kodu NULL.

Gdy uruchamiamy program, RP2040 przejdzie niemal natychmiast do wykonywania pustej pętli while. Jednak wszystko się zmieni, gdy wciśniemy przycisk. W tym momencie pojawi się sygnał przerwania i odpowiedni bit zapisany zostanie w rejestrze IRQ. W tym momencie procesor przejdzie też do obsługi przerwania. Dzięki funkcji uruchamiającej przerwanie wie on, że kod, który powinien wykonać, znajduje się w funkcji gpio_irq_handler. Zauważcie, że w tym przykładzie znajduje się ona na początku programu, inaczej niż funkcje, które przygotowywaliśmy w poprzednim artykule. Rozwiązanie takie jest jak najbardziej prawidłowe i dzięki niemu nie musimy na początku kodu umieszczać prototypów funkcji. Jednak ze względów estetycznych osobiście preferuję umieszczanie własnych funkcji na końcu kodu. 

Przygotowany handler typu void, czyli niezwracającego żadnych informacji oczekuje od nas dwóch argumentów – gpio typu uint oraz events typu uint32_t. Nie musimy się jednak przesadnie przejmować tymi argumentami, procesor obsłuży je automatycznie. Choć jak nietrudno się domyślić przekazywany jest tutaj numer wyprowadzenia GPIO oraz zdarzenie, na jakie reagować ma przerwanie.

We wnętrzu funkcji obsługującej przerwanie umieściłem dwa polecenia, które spowodują, że dioda LED zmieni swój stan. Na początku do zmiennej led_status, przypisany zostanie stan jej odwrotny. Jest to możliwe dzięki operatorowi negacji ‘!’. Innymi słowy, gdy w led_status zapisany będzie false, to do zmiennej zostanie przypisana prawda, czyli stan przeciwny. Kolejna funkcja to dobrze już znane gpio_put sterujące diodą LED.

Po uruchamianiu programu możemy zobaczyć, że nic się nie dzieje, jak już wspomniałem procesor wykonuje wtedy pustą pętlę while. Prawdopodobne jest, że tak jak na filmie uruchamiana będzie zielona dioda LED, jednak nie ma to związku z naszym kodem, na pytanie, dlaczego tak jest, odpowiem w dalszej części artykułu. Gdy wciśniemy przycisk procesor zarejestruje przerwanie i wykonana zostanie funkcja obsługująca je i żółta dioda LED zmieni swój stan. Jak możecie zauważyć na filmie oraz w rzeczywistości nie każde wciśnięcie przycisku powoduje zmianę stanu diody. Dzieje się tak przez drgania styków w przycisku i jest to zjawisko jak najbardziej zwyczajne, którym zajmiemy się już za chwilę. Jednak na ten moment można powiedzieć, że program działa poprawnie, a przerwanie generowane przez przycisk jest obsługiwane.

Drgania styków i stany nieustalone

Czas odpowiedzieć, dlaczego po uruchomieniu opisanego wyżej przykładu zielona dioda LED świeci. Dzieje się tak, ponieważ wyprowadzenie RPI, do którego podłączony jest ten element nie jest w żaden sposób zdefiniowane. Innymi słowy, program nie korzysta z tego pinu GPIO, przez co jego funkcja nie jest określona. W takim przypadku wyjście mikrokontrolera może przyjąć dowolny stan i nie jesteśmy w stanie przewidzieć czy będzie to stan wysoki, czy niski, ani, czy będzie działać jako wejście, czy wyjście.

Przebieg elektryczny przy wciśnięciu i puszczeniu przycisku. (https://hackaday.com/2015/12/09/embed-with-elliot-debounce-your-noisy-buttons-part-i/)

Krótkiemu wyjaśnieniu wymaga też zjawisko tak zwanego drgania styków, które można było zauważyć, wciskając podłączony do Raspberry Pi Pico W przycisk. Zdążało się, że dioda LED nie zmieniała swojego stanu lub też zmieniała, ale niemal natychmiast wracała do poprzedniej konfiguracji.

Wciskając przycisk, zwieramy jego styki do masy i mogłoby się wydawać, że jest to proces zero jedynkowy: logiczne jeden, gdy przycisk nie jest wciśnięty i zero, gdy jest. W rzeczywistości jednak wygląda to nieco inaczej. Jeśli podłączylibyśmy do styków przycisku oscyloskop, zauważylibyśmy przebiegi podobne do tych widocznych na grafice powyżej. Tak naprawdę w momencie wciśnięcia lub puszczenia przycisku jego styki drgają. W tej krótkiej chwili stan odczytywany przez RPI potrafi zmieniać się nawet kilkukrotnie, przez co przerwanie również uruchomiane może być kilka razy. Przez to właśnie mogliśmy zauważyć nie do końca intuicyjne zachowanie się programu.

Problem drgających styków przycisku rozwiązać można zasadniczo na dwa sposoby – hardwarowo lub softwarowo. W pierwszej koncepcji do przycisku należy dodać kilka elementów w postaci dwóch rezystorów i kondensatora (spotkać można też bardziej rozbudowaną wersję z dodatkową bramką logiczną). Tworzą one wspólnie prosty filtr RC, którego zadaniem jest jak najbardziej zniwelować nieoporządzane impulsy, dzięki czemu mikrokontroler otrzyma sygnał jak najbardziej zbliżony do idealnego sygnału prostokątnego.

Hardwarowe rozwiązanie problemu drgających styków. (https://hackaday.com/2015/12/09/embed-with-elliot-debounce-your-noisy-buttons-part-i/)

Ambaras styków rozwiązać można też w sposób programowy, tworząc prostą funkcję, która rozpozna, że przycisk został wciśnięty, odczeka kilka milisekund i ponownie sprawdzi jego stan. Jeśli ten nadal będzie w stanie, który został wykryty oznacza to, że użytkownik wcisnął przycisk i można przejść do wykonania programu, który go obsługuje. Kod tego typu przygotowujemy w przyszłości. Jednak w przypadku obsługi przerwania tego typu, programowe rozwiązanie nie jest do końca odpowiednie. Pamiętajmy, że przerwania zgłaszane są „automatycznie”, nie mamy większego wpływu, na to, w jaki sposób jest ono rozpoznane. Po pojawieniu się odpowiedniego stanu lub zbocza sygnału w rejestrze mikrokontrolera ustawiony zostanie konkretny bit, a kod wykonywany po wykryciu przerwania jest właśnie reakcją na ten bit, a nie na fizyczne wciśnięcie przycisku.

Kod jednorazowy, czyli alarm

Ciekawą funkcjonalnością wspieraną przez Raspberry Pi Pico są tak zwane alarmy. Są to funkcje wywoływane jednorazowo po określonym czasie, które określić możemy jak jednorazowe systemowe przerwanie. Może to być przydatne zwłaszcza w przypadku, gdy potrzebujemy wykonać konkretny fragment kodu chwilę po uruchomieniu mikrokontrolera, ale niezależnie od głównej pętli programu.

Na potrzeby testu funkcji alarmu przygotowałem kolejny projekt o nazwie alarm, jednocześnie nadal korzystać będziemy z tego samego obwodu elektronicznego. W programie skorzystamy z poznanej już wcześniej funkcji, która obsługiwała fotorezystor i przesyłała na ekran komputera informację o natężeniu światła i wartości napięcia. Jednocześnie, niezależnie od głównego kodu po 10 sekundach od uruchomienia RP2040 aktywujemy wszystkie diody LED. W takim razie przejdźmy do kolejnego przykładu.

				
					#include 
#include "pico/stdlib.h"
#include "hardware/adc.h"

//definitions and prototypes of functions
#define GREEN_LED 0
#define YELLOW_LED 1
#define RED_LED 2

const float conversion_factor = 3.3/4095;

int64_t alarm(alarm_id_t id, void *user_data);
void PhotoresRun();

int main() {
    stdio_init_all();

    gpio_init(YELLOW_LED);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_put(YELLOW_LED, 0);
    gpio_init(GREEN_LED);
    gpio_set_dir(GREEN_LED, GPIO_OUT);
    gpio_put(GREEN_LED, 0);
    gpio_init(RED_LED);
    gpio_set_dir(RED_LED, GPIO_OUT);
    gpio_put(RED_LED, 0);
   
    //alarm setup
    add_alarm_in_ms(10000, alarm, NULL, false);

    adc_init();
    adc_gpio_init(26);
    adc_select_input(0);

    while(true) {
       PhotoresRun();
       sleep_ms(500);
    }
}

//alarm handling function
Int64_t alarm(alarm_id_t id, void *user_data)
{
    gpio_put(YELLOW_LED, 1);
    gpio_put(GREEN_LED, 1);
    gpio_put(RED_LED, 1);
    return 0;
}

void PhotoresRun(){
    uint16_t photores_read = adc_read();
    const float voltage_photores = photores_read * conversion_factor;
    printf("Photoresistor ADC: value = %d, voltage = %f V\n", photores_read, voltage_photores);

}
				
			

Program składa się w większości ze znanych już funkcji. Jednak poza inicjalizacją diod LED i funkcją PhotoresRun, która obsługuje podłączony do mikrokontrolera fotorezystor, zauważyć możemy dodatkową funkcję wraz z jej prototypem oraz polecenie aktywujące alarm. I to właśnie tymi konstrukcjami zajmiemy się przede wszystkim.

Przed nieskończoną pętlą while zobaczyć możemy instrukcję add_alarm_in_ms, ustawia ona alarm, którego cechy charakterystyczne znalazły się w argumentach tej funkcji. 10000 to liczba milisekund, po której uruchomiony ma zostać alarm, kolejnym argumentem jest nazwa funkcji, która ma zostać wykonana po uruchomieniu alarmu, w tym przypadku to po prostu alarm. NULL oznacza, że do naszej funkcji nie są przekazywane żadne dodatkowe dane, natomiast false określa, że odliczanie czasu do wywołania funkcji aktywowane zostaje dopiero przy ustawieniu alarmu. W przypadku argumentu true, pojawić może się coś takiego jak wywołanie zwrotne, oznacza to, że alarm został powołany po czasie większym niż ten umieszczony w jego argumencie.

Po ustawieniu alarmu kod przejdzie do nieskończonej pętli, w której obsługiwany będzie fotorezystor, a informację o natężeniu światła i napięciu będą co pół sekundy przesyłane na serial monitor. Jednak po upływie 10 sekund wywołany zostanie alarm i mikrokontroler przejdzie do jego obsługi. Wówczas wywołana zostanie funkcja alarm, która uruchomi wszystkie trzy diody LED. Funkcja ta po poprawnym wykonaniu zwraca zero. Podobnie jak przy przerwaniach argumentami tej funkcji nie musimy się przejmować, wynikają one z dokumentacji RPI i określają id alarmu oraz coś takiego jak wskaźnik na dane użytkownika, ale wskaźnikami zajmiemy się kiedy indziej.

Po kompilacji i uruchomieniu kodu można otworzyć serial monitor i obserwować dane spływające z mikrokontrolera. Poza tym tak jak na filmie po 10 sekundach uruchomione zostaną wszystkie trzy diody LED. Stanie się to niezależnie i nie wpłynie na główną funkcję programu.

Kilka słów na koniec…

W tym materiale przyjrzeliśmy się bliżej przerwaniom mikrokontrolera oraz uruchomiliśmy funkcję alarmu. Poza tym wspomnieliśmy o domyślnym stanie wyprowadzeń RP2040 oraz problemie drgających styków w przyciskach. W kolejnym poradniku poznamy timery, a także opowiem wam więcej o wspomnianych już wskaźnikach.

Źródła:

  • https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
  • https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf
  • https://www.raspberrypi.com/products/rp2040/
  • https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
  • https://hackaday.com/2015/12/09/embed-with-elliot-debounce-your-noisy-buttons-part-i/)s

Jak oceniasz ten wpis blogowy?

Kliknij gwiazdkę, aby go ocenić!

Średnia ocena: 4.7 / 5. Liczba głosów: 7

Jak dotąd brak głosów! Bądź pierwszą osobą, która oceni ten wpis.

Podziel się:

Picture of Rafał Bartoszak

Rafał Bartoszak

Współpracujący z Botlandem elektronik, który dzieli się swoją wiedzą w  internecie. Entuzjasta systemów cyfrowych, układów programowalnych i mikroelektroniki. Pasjonat historii, ze szczególnym naciskiem na wiek XX.

Zobacz więcej:

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Ze względów bezpieczeństwa wymagane jest korzystanie z usługi Google reCAPTCHA, która podlega Polityce Prywatności oraz Warunkom użytkowania.