Kurs Arduino II – #9 – wielozadaniowość, opóźnienia z millis()

Kurs Arduino II – #9 –  wielozadaniowość, opóźnienia z millis()

Funkcja delay (do wprowadzania opóźnień) to jedna z pierwszych rzeczy, której uczymy się podczas poznawania Arduino. Jej działanie może jednak generować wiele kłopotów.

Na szczęście z pomocą przychodzi nam bardziej rozbudowane rozwiązanie bazujące na funkcji millis. Dzięki niej Arduino może wykonywać kilka zadań "jednocześnie".

Problem wielozadaniowości na Arduino

Miganie diodą, to najpopularniejszy przykład, od którego zaczyna się naukę Arduino. Najczęściej wygląda on następująco:

Krótka zawartość pętli loop() wystarcza do tego, aby migać diodą. Uwaga: Przejście od ustawienia odpowiedniego stanu na wyjściu do delay() jest błyskawiczne, na poniższej animacji zajmuje ono "drobną chwilę", aby było widać, co się dzieje:

Zawartość pętli loop() - miganie jedną diodą.

Wiele osób od razu wpada na pomysł migania dwiema diodami (i to z różną częstotliwością). Tym samym dochodzą do następującego programu:

Działanie tego kodu odbiega od tego, czego spodziewa się zdecydowana większość osób. Trzeba pamiętać, że Arduino wykonuje wszystkie instrukcje z pętli loop() linijka, po linijce. W danym momencie mikroprocesor może zająć się tylko jedną operacją.

Zamiast oczekiwanego, równoległego migania diod otrzymujemy coś dziwnego:

Działanie programu, który pozornie miał migać dwiema diodami (równolegle).

Początkowo może to budzić zdziwienie. W końcu komputery, którymi się posługujemy wykonują masę różnych operacji jednocześnie (nawet te, które mają procesor z jednym rdzeniem). Tak naprawdę procesor z PC nie jest w tym przypadku lepszy od mikrokontrolera z Arduino. On też wykonuje w danej chwili tylko jedną operację. Wielozadaniowość komputerów polega na tym, że przełączają się bardzo szybko między różnymi zadaniami (tysiące razy na sekundę). Z punktu widzenia człowieka jest to niezauważalne.

Kiedy można korzystać z delay()?

Funkcja opóźniająca delay() jest niezwykle prosta i wygodna. Niestety każde jej wystąpienie blokuje cały program. W przypadku prostych przykładów nie stanowi to dużego problemu. Przy dużych projektach jest to problematyczne. Korzystanie z tej funkcji może nieść za sobą trudne do zidentyfikowania błędy w działaniu programów (np. chwilowe problemy z pewnymi bibliotekami).

Co zamiast delay()?

Istnieje wiele bibliotek, które pozwalają na wprowadzanie "magicznych", nieblokujących opóźnień. Oczywiście życie należy sobie ułatwiać, więc można sięgać po takie gotowce. Jednak wiele osób zupełnie nie wie jak działają te opóźnienia, co w efekcie prowadzi do kolejnych, jeszcze dziwniejszych błędów.

Dlatego na początek warto zacząć od przestawienia sposobu myślenia i rozpoczęcia pisania programów w innych sposób. Stąd najlepiej zacząć od zera, bez gotowych bibliotek...

Stoper wbudowany w Arduino - millis()!

W Arduino znajdziemy funkcję millis(). Jej działanie najlepiej przyrównać do stopera, który rusza w momencie uruchomienia Arduino. Funkcja ta zwraca liczbę milisekund, która upłynęła od podłączenia płytki do prądu. Nie musimy uruchamiać tego "stopera", Arduino zrobi to za nas. Działa on zawsze i w każdym programie. Liczenie zrealizowane jest sprzętowo, czyli z użyciem liczników (timerów). Wskazań millis() nie da się przypadkiem zafałszować.

Arduino posiada wbudowany stoper!

Łączenie obu wspomnianych funkcji (delay i millis) mija się z celem i może generować pewne problemy, jednak warto wiedzieć, że jest to możliwe - jeszcze do tego wrócimy.

Jak wykorzystać millis()?

Funkcja ta nie przyjmuje żadnego argumentu. Po prostu zwraca liczbę milisekund, która upłynęła od momentu uruchomienia Arduino. Każda sekunda, to 1000 ms, więc zwracana wartość będzie rosła bardzo szybko. Trzeba zadbać o to, aby zmienna, do której zapisujemy informację była odpowiednio pojemna. W tym celu korzystamy z typu danych unsigned long, np.:

Sprawdźmy teraz, jak ten stoper działa w praktyce. Możemy zacząć od wysyłania aktualnego czasu do komputera przez UART. Oczywiście nie możemy wysyłać go bez żadnych przerw, bo zapchamy bufor danych i wszystko się zawiesi.

Nie umiemy jeszcze zatrzymywać programu w "sprytny sposób", więc skorzystajmy ostatni raz z "nieszczęsnego" delay():

Po uruchomieniu programu w monitorze portu szeregowego, co około 1 sekundę, pojawi się liczba milisekund, która upłynęła od startu Arduino:

Efekt działania programu.

Nie bez powodu napisałem "co około 1 sekundę", jak widać pojawiają się drobne nieregularności o +/- 1 milisekundę. Eksperyment ten pokazał przy okazji kolejną wadę delay() liczenie czasu tą metodą nie jest dokładne. Oczywiście tutaj 1 milisekunda wiele nie zmienia. Jednak, jeśli układ miałby działać cały czas (np. jako zegar), to z czasem rozbieżności byłyby znaczne.

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

Części do wykonania ćwiczeć z kursu Arduino (poziom 2) dostępne są w formie gotowych zestawów! W komplecie m.in. diody programowalne, termometry analogowe i cyfrowe, wyświetlacze 7-segmentowe oraz czujnik ruchu (PIR).

Kup w Botland.com.pl

Jakie jest ograniczenie funkcji millis()?

Jak było widać na wcześniejszej animacji zwracana wartość rośnie szybko. Kiedyś licznik ten się przepełni (przekroczy swoją maksymalną wartość i wróci do zera). Na szczęście w tym przypadku przepełnienie nastąpi dopiero po 50 dniach pracy.

Dodatkowo warto pamiętać, że operacje na tak dużych liczbach (unsigned long) mogą prowadzić do pewnych błędów i przekłamań logicznych. Szczególnie, gdy spróbujemy wykonywać działania matematyczne z mniejszymi zmiennymi (np. typu int)!

Lepsza wersja odmierzania czasu

Wiemy już, że w Arduino wbudowany jest dokładny stoper. Pora wykorzystać go do generowania opóźnień. Stopera nie możemy zerować, nie będziemy też na niego w żaden sposób wpływać. Wystarczy, że będziemy "znać aktualny czas" względem startu Arduino. 

Jak powinno wyglądać odmierzenie sekundy?

  1. Sprawdzamy aktualny czas i go zapamiętujemy.
  2. W każdym obiegu pętli sprawdzamy aktualny czas:
    1. Jeśli różnica między czasem aktualny, a poprzednio zapamiętanym jest mniejsza od 1 sekundy, to znaczy, że pożądany czas jeszcze nie upłynął.
    2. Jeśli różnica między czasem zapamiętanym i aktualnym wynosi 1 sekundę, to... właśnie tyle czasu minęło!

Kluczowe w takim podejściu jest to, że program cały czas "biega w kółko" i nigdzie się nie zatrzymuje. Jego działanie polega na ciągłym sprawdzaniu ile czasu minęło, a równocześnie może robić jeszcze inne rzeczy.

Pora przelać powyższy algorytm na kod:

Program sprawdza aktualny czas, liczy różnicę i jeśli jest ona większa lub równa 1000, to wiemy, że na pewno minęła sekunda. Dla bezpieczeństwa w warunku umieszczona jest nierówność, zamiast sztywnego "== 1000". Czasami może się zdarzyć, że przy bardzo rozbudowanych programach z jakiegoś powodu nie wstrzelimy się idealnie w 1000. Taka nierówność zapewnia nas, że program się "nie zatrzaśnie" i wejdziemy do warunku przy najbliższej okazji (np. 1001ms).

Wiemy, że minęła sekunda, gdy warunek zostanie spełniony. Dlatego zaraz na jego początku zapamiętujemy aktualny czas jako poprzedni (w którym był spełniony warunek). Kolejną sekundę będziemy liczyć od nowej wartości. W tym wypadku warunek będzie spełniony, gdy licznik millis wskaże kolejne 1000, 2000, 3000, 4000 itd.

Działanie programu w praktyce widoczne jest poniżej:

Działanie drugiej wersji programu w praktyce.

Oczywiście zmienna roznicaCzasu jest zbędna (tutaj użyta dla większej czytelności). Równie dobrze można różnicę czasu policzyć bezpośrednio w warunku:

Miganie diody bez delay()

Teraz pora wykorzystać zdobyte informacje do migania diodą. Potrafimy odmierzyć dokładnie czas, więc pozostaje zmiana stanu diody w odpowiednim momencie. Podłączamy diodę do pinu numer 3 i działamy!

Aby układ działał poprawnie musimy zapamiętać aktualny stan diody (świeci/nie świeci). Wtedy w warunku, który będzie prawdziwy co sekundę będziemy mogli zmienić stan diody na przeciwny. Informację na ten temat przechowamy w zmiennej  int stanLED1 = LOW;. Negację stanu diody możemy wykonać później następującą operacją:  stanLED1 = !stanLED1;  

Kod realizujący to zadanie widoczny jest poniżej:

Po jego uruchomieniu dioda będzie migała zgodnie z pierwszym przykładem:

Miganie LED dzięki millis().

Negacja stanu w formie  stanLED1 = !stanLED1; jest krótka i wygodna, ale nie musi być dla wszystkich intuicyjna (działa tylko dzięki odpowiedniej deklaracji stałych LOW i HIGH). Dla większego bezpieczeństwa można ten fragment kodu przerobić na taką postać:

Wielozadaniowość na Arduino - miganie diodami

Pora rozbudować powyższy przykład, aby migać niezależnie dwiema diodami. Dopiero wtedy widoczna będzie przewaga tego rozwiązania. Tym razem chcemy, aby jedna dioda zmieniała swój stan częściej od drugiej:

Dwa "równoległe" zadania w jednej pętli loop.

Potrzebujemy dwie dodatkowe zmienne. W pierwszej przechowamy informację "kiedy" ostatni raz zmieniliśmy stan drugiej diody. Kolejna będzie trzymała informacje o jej stanie (świeci/nie świeci).

Cała reszta programu będzie analogiczna. Podłączamy drugą diodę do pinu nr 4 i ruszamy:

Od teraz diody będą migały niezależnie! Jedna zmienia swój stan co sekundę, a druga co pół:

Niezależne miganie dwóch diod.

Zmiany będą widoczne najlepiej, jeśli wprowadzimy większe różnice. Niech pierwsza dioda zmienia stan co sekundę, a druga co 200 ms. Najlepiej dodać w tym celu dwie zmienne np.: miganieLED1 oraz miganieLED2.

Teraz efekt jest widoczny znacznie lepiej:

Większa różnica w częstotliwości migania.

Migające diody i przycisk na Arduino

Jeszcze lepszy efekt uzyskamy, jeśli dodamy do programu przycisk. Standardowo, gdybyśmy korzystali z delay() to funkcja ta blokowałaby możliwość natychmiastowego sprawdzania wejść. Należałoby trzymać przycisk do czas, aż program dotrze do linijki sprawdzającej. Działoby się to rzadko, bo delay() zamrażałby program na kilka sekund.

Tutaj nie będzie takiego problemu, możemy zwyczajnie dodać warunek, który będzie działał natychmiast. Przykładowo niech wciśnięcie przycisku (pin 2) powoduje, że dioda LED1 będzie zmieniała swój stan znacznie szybciej (co 100 ms).

No i gotowe - prosta wielozadaniowość w praktyce! Migamy niezależnie diodami i błyskawicznie reagujemy na wciśnięcie przycisku. Oczywiście to tylko przykład. Zamiast zmiany stanu LEDów może się tam pojawić coś zupełnie innego.

Niezależne miganie diod + reakcja na wejście.

Inteligentne oświetlenie

Pierwotnie artykuł ten miał dotyczyć zupełnie czegoś innego (automatyki domowej), na koniec nawiąże więc do tego tematu w formie "bardzo luźnego" przykładu.

Coraz więcej osób interesuje się tematem inteligentnych budynków. Automatyczne podnoszące się rolety, zdalne sterowanie urządzeniami i oświetleniem, zdalny podgląd temperatury. Na rynku znaleźć można wiele gotowych rozwiązań dla automatyki domowej. Niestety większość z tych systemów łączy jedna, niepożądana cecha - wysoka cena.

Tym razem zajmiemy się tematem "inteligentnego oświetlenia", które będzie oświetlało schody. Wśród czytelników Forbota mamy również młodych adeptów elektroniki. W związku z tym nie będę poruszał tematów sterowanie normalnym oświetleniem (230V) - zajmiemy się bezpiecznym przykładem. Osoby zainteresowane implementacją bardziej rozbudowanych rozwiązań na pewno będą już samodzielnie potrafiły dostosować układ (korzystając z przekaźników).

Przykład prostej automatyzacji z Arduino

Podczas schodzenia wieczorem po schodach zawsze włączamy światło. Podobnie, gdy idziemy przez ciemny korytarz. Jest to idealne zadanie do zautomatyzowania! Tym razem załóżmy, że sytuacja wygląda następująco: tuż obok drzwi wejściowych znajdują się schody, do tego na dole i u góry mamy małe korytarze.

Załóżmy, że chcemy uruchamiać światło w momencie wykrycia ruchu. Gdy czujnik PIR zauważy kogoś w okolicy schodów uruchomi światło na 180 sekund lub do czasu otworzenia drzwi.

Przykładowe pomieszczenie, które warto zautomatyzować.

Wybór odpowiedniego oświetlenia

Tak jak wspomniałem wcześniej, zabawę z 230V zostawmy dla bardziej doświadczonych osób. Sterowanie normalnym oświetleniem najczęściej realizowane jest za pomocą przekaźników.

Druga, znacznie bezpieczniejsza opcja, to popularne ostatnio paski LEDowe. Najpopularniejsze moduły tego typu można bezpiecznie zasilać z mniejszego napięcia (np. 12V). Do Arduino można je podłączyć za pomocą przekaźnika lub tranzystora (MOSFET). Informacje na temat sterowania peryferiów przez MOSFET zostały opisane podczas #3 części kursu.

Trzecia opcja, to wykorzystanie diod programowalnych, np. WS2812, którymi zajmowaliśmy się podczas drugiej części kursu. Rozwiązanie to będzie droższe od zwykłych diod, ale pozwoli na ciekawsze efekty.

Przykładowy moduł diod RGB.

W ramach testów (ja wybrałem taką opcję) można też podłączyć zwykłą diodę świecącą, która będzie symulowała oświetlenie. Diodę podłączyłem do pinu numer 4.

Czujniki

Pora dobrać czujniki. Oczywiście ruch wykryjemy czujnikiem typu PIR (pin numer 5). Natomiast do monitorowania drzwi najlepszy będzie kontaktron (pin numer 3). Wszystkie te czujniki były opisane w artykule o tworzeniu prostej centralki alarmowej.

Czujnik PIR montujemy w korytarzu u góry, a kontaktron na drzwiach:

W praktyce moja platforma testowa wyglądała następująco:

Symulacja opisanego przypadku.

Program wykorzystujący millis()

Przed wykonaniem ćwiczeń z tego odcinka włączenie światłą na 180 sekund robilibyśmy za pomocą delay()Po wykryciu ruchu następowałoby włączenie oświetlenia i delay(180) w tym czasie układ byłby "zamrożony". Nie reagowałby na kontaktron ani na żadne inne czujniki...

Pora więc wykorzystać poznany dziś mechanizm. Program ma działać następująco: jeśli wykryty będzie ruch, to uruchamiamy oświetlenie na 180 sekund. Światło zgaśnie po upływie tego czasu (jeśli nie ma kolejnych ruchów) lub, gdy ktoś będzie wychodził, czyli otworzy drzwi.

Jedna z wielu możliwości realizacji tego przykładu wygląda następująco:

Oczywiście same założenia tego przykładu mają pewne wady i układ należałoby rozbudować, aby był przydatny w życiu codziennym. Warto byłoby chociażby dodać fotorezystor, dzięki któremu światło byłoby włączane jedynie w ciemności. Dalsze rozbudowanie układu zostawiam dla chętnych majsterkowiczów.

Podsumowanie

Ostatni przykład był bardzo prosty, ale pokazuje praktyczne zastosowanie dla funkcji millis, która była głównym bohaterem tego odcinka. Mam nadzieję, że od teraz nikt już nie będzie miał problemów z blokowaniem całego programu przez funkcję delay.

Zachęcam do własnych testów! Warto rozbudować opisane tutaj programy. Dodać więcej LEDów, przycisków i innych sensorów. Gdy wszystko będzie już jasne będzie można spokojnie przejść do bibliotek, które "same w magiczny sposób" wykonują takie opóźnienia.

Nawigacja kursu

Autor kursu: Damian Szymański
Ilustracje: Piotr Adamczyk

Arduino, delay, kursArduino2, millis, opóźnienia, wielozadaniowość

Komentarze

Komentarze do tego wpisu są dostępne na forum: