Skocz do zawartości

RZtimer - biblioteka do obsługi wielozadaniowości w Arduino


rziomber

Pomocna odpowiedź

Moja pierwsza w życiu biblioteka Arduino!

Na samym początku naszej przygody z Arduino spotkaliśmy się z najsłynniejszym "migaczem":

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);                       // wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);                       // wait for a second
}

Bardzo szybko możemy się przekonać o tym, że funkcja delay() blokuje wykonywanie innych zadań w międzyczasie. W wielu wypadkach może to stwarzać problemy. To tak jakbyśmy o godzinie 23:00 zaprogramowali siebie:

WŁĄCZ SEN
odczekaj(7 godzin)
WYŁĄCZ SEN
PÓJDŹ DO PRACY
odczekaj(8 godzin)
WYJDŹ Z PRACY

i przekonali się, że odczekaj(8 godzin) całkowicie "zamraża" nas i uniemożliwia jakąkolwiek pracę.

Doskonale opisuje to artykuł Kurs Arduino II – #9 – wielozadaniowość, opóźnienia z millis(). Znajdziemy tam też bardzo dobre rozwiązanie tego problemu:

unsigned long lastTimeLCD = 0;

void setup() {
....
}

void loop() {
CAŁY CZAS PRACUJ
if (lastTimeLCD == 0 || millis() - lastTimeLCD >= 1000)
{
lastTimeLCD = millis();
UAKTUALNIJ ZAWARTOŚĆ WYŚWIETLACZA
}
 
}

Co jeśli chcemy wykonac funkcję określoną ilość razy? Możemy oczywiście dopisywać dalsze warunki i zliczać już wykonaną ilość iteracji.

unsigned long lastTimePicture = 0;
int iterationsDone = 0;
void setup() {
....
}

void loop() {
CAŁY CZAS PRACUJ
if (iterationsDone < 3 && (lastTimePicture == 0 || millis() - lastTimePicture >= 1000))
{
iterationsDone++;
lastTimePicture = millis();
WYKONAJ ZDJECIE LUSTRZANKA PODLACZONA DO ARDUINO
}
 
}

Taka potrzeba istnieje np przy interwałometrze WiFi podłączonym do lustrzanki cyfrowej (np mikrokontroler ESP8266 hostuje stronę WWW sterującą aparatem albo ESP32, które sterujemy poprzez Bluetooth za pomocą dedykowanej aplikacji na Androida).

DSLR_WiFi_remote_controller_D1_mini_shield.thumb.jpg.1dc057899b02e1dc35670d85684d2741.jpgDSLR_remote_controller_screenshot.thumb.png.be81a7299f0de636763927bd2f150e96.pngSony_Xperia_M.thumb.jpg.4562949f4e1576fec9df8d180d106910.jpgDSLR_Intervalometer_beta.thumb.jpg.032d1b14e14337d171274dbba800d966.jpg

Może on dostać np zadanie: poczekaj 10s przed rozpoczęciem, wykonaj serię 15. zdjęć o czasie naświetlania 2s i interwale 5s. Oczywiście nadal problem rozwiążemy naszą słynną instrukcją warunkową if(....millis()... jakieś kolejne liczniki i warunki). Przy wielu różnych zadaniach wywoływanych asynchronicznie nasz kod stanie się mniej czytelny. O wiele łatwiej o pomyłkę. Nie możemy w końcu pomylić wielu różnych zmiennych odpowiedzialnych za przechowywanie czasu ostatniego wykonania zadania, interwały, ilość wykonanych iteracji, ilość oczekiwanych iteracji...

Ten niekończący się wstęp powinien uzmysłowić Wam powód, dla którego napisałem Arduino Timer Library RZtimer. Przykład wykorzystania jej dwóch podstawowych metod:

- wykonaj funkcję blinkLED() co każde 2000 ms:
timer.addEverytime(2000, blinkLED);

- wykonaj funkcję itWorks() dwa razy z interwałem 2000 ms, poczekaj 300 ms przed startem:
timer.addTask(2000, 2, 300, itWorks);

1.thumb.jpg.651d94afe72647651fbd35f1f1d79753.jpg

Kod w całości:

#include "RZtimer.h"

RZTimer timer;
void blinkLED();

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  timer.addEverytime(2000, blinkLED);
  auto itWorks = []()->void {Serial.print("It Works! "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(300, 2, 2000, itWorks);
}

void loop() {
  timer.run();
}

void blinkLED()
{
  Serial.print("ON ");
  Serial.print(millis());
  Serial.println(" ms");
  digitalWrite(LED_BUILTIN, HIGH);
  auto blinkLEDoff = []()->void {digitalWrite(LED_BUILTIN, LOW); Serial.print("OFF "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(1000, 1, 0, blinkLEDoff);
}

RZTimer timer; tworzy obiekt timer klasy RZTimer.

Przykładowe funkcje wykonywane cyklicznie to blinkLED() oraz itWorks(). Pierwszą zdefiniowałem "klasycznie", drugą w celach edukacyjnych jako wyrażenie lambda (w takich zastosowaniach mogą się one okazać całkiem wygodne w użyciu).

Nie zapomnijmy o wstawieniu  timer.run(); gdzieś w pętli loop!

Dodatkowym (i chyba unikatowym 😉 atutem jest możliwość usypiania mikrokontrolera przy równoczesnym asynchronicznym wykonywaniu zadań! Poniższy przykład będzie działać jedynie z mikrokontrolerami wspieranymi przez bibliotekę narcoleptic.

#include "RZtimer.h"
#include <Narcoleptic.h> //https://github.com/rcook/narcoleptic
  
void delaySleep(unsigned long sleepTime)
{
  delay(30);
  extern volatile unsigned long timer0_millis;
  noInterrupts();
  timer0_millis += (unsigned long)(sleepTime - 30);
  interrupts();
  Narcoleptic.delay(sleepTime - 30);
}
RZTimerWithSleep timer(delaySleep);

void blinkLED();

void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  timer.addEverytime(2000, blinkLED);
  auto itWorks = []()->void {Serial.print("It Works! "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(300, 2, 2000, itWorks);
}

void loop() {
  timer.run();
}

void blinkLED()
{
  Serial.print("ON ");
  Serial.print(millis());
  Serial.println(" ms");
  digitalWrite(LED_BUILTIN, HIGH);
  auto blinkLEDoff = []()->void {digitalWrite(LED_BUILTIN, LOW); Serial.print("OFF "); Serial.print(millis()); Serial.println(" ms");};
  timer.addTask(1000, 1, 0, blinkLEDoff);
}

Po drobnych modyfikacjach biblioteka powinna działać też w programach C++ uruchomionych na Raspberry Pi. Np zamiast "Arduinowego" millis() powinniśmy skorzystać z funkcji clock(), a same wartości zmiennych przechowywać w typie danych clock_t.

Przykład zastosowania: kontroler akwarium. Co sekundę odświeża zawartość LCD. Włącza i wyłącza pompę "utleniającą wodę" o zadanej porze. Co dwa tygodnie sprawdza synchronizację czasu łącząc się z serwerem NTP.

Aquarium_controller.thumb.jpg.37e8b14773fc7b7cd8d4a4fbac604ad3.jpg

Edytowano przez rziomber
Link do komentarza
Share on other sites

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

@rziomber, właśnie zaakceptowałem Twój opis, możesz go teraz zgłosić do akcji rabatowej umieszczając link w temacie zbiorczym. Dziękuję za przedstawienie ciekawego projektu, zachęcam do prezentowania kolejnych DIY oraz aktywności na naszym forum 🙂 

Przy okazji proszę inne osoby planujące opis projektów tego typu o wcześniejsze zgłaszanie się w celu wstępnej weryfikacji projektu (procedura jest opisana w ogłoszeniu).

Link do komentarza
Share on other sites

Ciekawa sprawa, problem "przysypiania" w delay zauważyłem przy impulsowaniu kroków w silniku BLDC, choć dla moich potrzeb takie przymulenia nie są istotne to rozważałem dedykowanie osobnego mikrokontrolera dla procesów o wysokiej istotności synchronizacji - a tu inne podejście które zapewne nie bez wysiłku 😅ale z frajdą będę analizował. 

Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Moja biblioteka wykorzystuje millis() i nie ma zwiazku z timerami sprzetowymi. Dzieki temu jest niezalezna od platformy i nie wplywa np na PWM. Za to obciaza mikrokontroler i pewnie wywoluje funkcje z mniejsza dokladnoscia czasowa.

Link do komentarza
Share on other sites

Proponuję zamiast pisania bibliotek mały kurs C++. I to nie napisać, ale przeczytać. Na początek temat: konstruktory. Kolejny to alokacja pamięci, bo użycie realloc to marny pomysł. Przy okazji może jakieś podstawy struktur danych, np. listy?

Ogólnie napisanie biblioteki jest trudniejsze niż napisanie zwykłego programu. I lepiej jeśli za takie wyzwania zabierają się osoby z pewnym doświadczeniem oraz umiejętnościami. Niestety w Arduino pisać każdy może... a później pojawiają się opinie o marnej jakości kodu bibliotek dla Arduino - i nawet ciężko się z tym nie zgodzić.

  • Lubię! 1
Link do komentarza
Share on other sites

(edytowany)
43 minuty temu, Elvis napisał:

Proponuję zamiast pisania bibliotek mały kurs C++. I to nie napisać, ale przeczytać. Na początek temat: konstruktory. Kolejny to alokacja pamięci, bo użycie realloc to marny pomysł. Przy okazji może jakieś podstawy struktur danych, np. listy?

Sortowanie zadań wg najbliższego wydarzenia na pewno poprawiłoby wydajność biblioteki.

Może zamiast cierpkich (choć ZASŁUŻONYCH) słów mógłbyś choć troche pomóc i powiedzieć, jak efektywniej alokować pamięć bez użycia realloc i nie mając dostępu do STL C++? Bez zewnętrznych bibliotek nie mam STL w Arduino.

Edytowano przez rziomber
  • Lubię! 1
Link do komentarza
Share on other sites

Nie robiłem przeglądu całego kodu, tylko szybko "rzuciłem okiem". A że zabolało to napisałem...

Pierwsza sprawa to formatowanie pliku nagłówkowego. Niby nie jest to wymagane, ale miło jeśli czytając ten plik można się domyślić, co robi dana klasa. Natomiast deklarowanie metod bez nazw parametrów tego nie ułatwia:

    size_t addTask(unsigned int, unsigned int, unsigned int, void (*makeT)());
    void removeTask(size_t);

To jednak nie jest jeszcze błąd. Pierwszy błąd to brak konstruktora - jakiegokolwiek. Drugi błąd to założenie, że właściwości w klasie będą miały jakieś sensowne wartości początkowe...

Ponieważ nic z tym nie zrobiłeś, C++ utworzył domyślny konstruktor oraz konstruktory kopiujące. Więc jeśli ktoś ich użyje to kaboom i kod wyleci przy zwalnianiu pamięci, albo i wcześniej.

W metodzie addTask używasz radośnie zmiennej tasks:

size_t RZTimer::addTask(unsigned int _wait, unsigned int _iterations, unsigned int _interval, void (*makeT)())
{
»·······tasks++;

A skąd pomysł, że na początku jej wartość wyniesie zero? Podpowiem - to przypadek, że działa.

Następna rzecz to realloc(). Ogółnie w C++ należy unikać zarządzania pamiecią rodem z C. Czyli lepiej używać new[] oraz delete[]. Ale dalej jest jeszcze gorzej.

Kod wygląda tak:

size_t RZTimer::addTask(unsigned int _wait, unsigned int _iterations, unsigned int _interval, void (*makeT)())
{
»·······tasks++;
»·······parameters = (parameter*) realloc(parameters, tasks * sizeof(parameter));

Masz więc zmienną parameters, której nie zainicjalizowałeś... I jeszcze przy każdym nowym tasku zwalniasz całą tablicę zadań i tworzysz nową. To jest złe, straszne i niedobre.

Poczytaj o listach, można je zaimplementować w moment, a wtedy wystarczy przydzielać pamięć tylko dla nowego zadania.

Nie mówiąc już o tym, że skoro piszesz w C++ to może lepiej utworzyć klasę dla zadania i nie trzeba będzie nic alokować dynamicznie. Wbrew pozorom używanie sterty w embedded bywa problematyczne - pojawia się np. fragmentacja pamięci, bez MMU ciężko sobie z nią poradzić.

Pewnie błędów jest więcej, ale nie miałem odwagi czytać. Proponuję schować ten kod głęboko i nie pokazywać, bo wstyd. Lepiej najpierw opanować podstawy programowania, a później pisać biblioteki i się nimi chwalić.

  • Lubię! 2
Link do komentarza
Share on other sites

4 minuty temu, Elvis napisał:

Pewnie błędów jest więcej, ale nie miałem odwagi czytać. Proponuję schować ten kod głęboko i nie pokazywać, bo wstyd. Lepiej najpierw opanować podstawy programowania, a później pisać biblioteki i się nimi chwalić.

Osobiście ciężko mi się z tym zgodzić. Próba autora to takie "nieskończone DIY" tylko wśród tworzenia kodu. Być może zła kategoria na forum, być może niepotrzebne przekonanie o sensowność rozwiązania, ale wpis jak najbardziej sensowny, bo gdzie jak nie w komentarzach autor ma się czegoś nauczyć?

Zaprezentował kod, dostał wskazówki na poprawę i chyba o to właśnie chodzi, co nie?

  • Lubię! 1
Link do komentarza
Share on other sites

Nie czepiałbym się tego kodu tak bardzo, gdyby to był zwykły program. Każdy się kiedyś uczył, a kto nie robi błędów niech pierwszy rzuci kamień...

Ale tutaj mamy "bibliotekę", którą co najgorsze może użyć ktoś w swoim programie i przeszczepi wszystkie koszmarki. Więc jeśli zmienimy nazwę "biblioteka" na własny program i nie będziemy tego sprzedawać innym jako dobre i działające, wtedy ok, fajnie że Autor chce się czegoś nauczyć. Tylko niech od razu napisze, że to wadliwy prototyp, a nie biblioteka do wykorzystania.

  • Lubię! 1
Link do komentarza
Share on other sites

@Elvis mam prośbę na przyszłość. Drugi post był mi o wiele bardziej pomocny, zwłaszcza

"Pierwszy błąd to brak konstruktora - jakiegokolwiek. Drugi błąd to założenie, że właściwości w klasie będą miały jakieś sensowne wartości początkowe... 

Ponieważ nic z tym nie zrobiłeś, C++ utworzył domyślny konstruktor oraz konstruktory kopiujące. Więc jeśli ktoś ich użyje to kaboom i kod wyleci przy zwalnianiu pamięci, albo i wcześniej."

faktycznie mi pomógł i szczególnie wskazał błędy, które popełniłem.

Z pokorą i chęcią dalszego doskonalenia przyjmuję Twoje argumenty.

Przyt okazji sortowanie zadań w kolejności od najbliższego powinno dodatkowo zwiększyć wydajność kodu, gdyż zmniejszy ilość sprawdzanych zadań przy wykonywaniu każdej z pętli.

 

Link do komentarza
Share on other sites

5 godzin temu, Elvis napisał:

Pewnie błędów jest więcej, ale nie miałem odwagi czytać.

Na dodatek źle zdefiniowałem konstruktor klasy RZTimerWithSleep. Kod poprawnie kompilował się (i nawet przypadkowo działał, co oczywiście przed wysłaniem biblioteki sprawdziłem) dla platformy AVR. Dla innych rodzin mikrokontrolerów już nie.

Pierwsze poprawki już wprowadziłem.

Gdybym nie opublikował biblioteki, nie otrzymałbym Twoich cennych uwag, a działający pozornie poprawnie kod (co sprawdzałem w kilku urządzeniach!) mógł skłaniać mnie do dalszego powielania tych błędów.

Link do komentarza
Share on other sites

(edytowany)
21 godzin temu, Klemens napisał:

Jeśli chodzi o jakieś poradniki itp to bardzo przydatne znalazlem tutaj: https://abc-rc.pl/Baza-Wiedzy-Modelarstwo-i-Elektronika-ccms-pol-43.html

W związku z wątkiem to raczej takie poradniki są bardziej przydatne:

https://www.p-programowanie.pl/cpp/lista-jednokierunkowa-c/

 Z tym, że zaprezentowany kod wygląda mi bardzo podejrzanie, bo Autor nie dealokuje pamięci dla wskaźników. Ilość new nie zgada się z delete (a dokładniej tych drugich nie ma wcale).

Moje obawy potwierdza ta dyskusja:

https://stackoverflow.com/questions/47491406/how-to-delete-a-node-in-a-linked-list

Oprócz sprawdzenia, co się dzieje z pamięcią dopiszę dodawanie kolejnych zadań do listy w kolejności od najbliższego. Dzięki temu z każdym kolejnym przejściem pętli loop() Arduino sprawdzać będzie jedynie czas najbliższego zadania. Nie wertować wszystkie po kolei, jak do tej pory.

 

Edytowano przez rziomber
  • Lubię! 1
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.