Skocz do zawartości

Wielozadaniowość cz 2


Pomocna odpowiedź

Napisano

Cel artykułu

Rozwinięcie programu w części pierwszej o obsługę zdarzeń asynchronicznych.

W pierwszej części pokazałem, jak przejść od programu migania diodą (blink) zamieszczonego w środowisku Arduino do programu wielozadaniowego (2 zadania synchroniczne), który oprócz migania diodą równocześnie wyświetla ilość obiegów pętli loop() w ciągu 1 sekundy w tym programie. Poniżej zamieszczam końcowy kod programu z części pierwszej.

// stałe
#define LED_PERIOD 1000 // okres przełączania diody

void task_dioda() {
static unsigned long PrevLed = 0L;  
if (millis() - PrevLed >= int(LED_PERIOD) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void task_nuda() { // liczba wywołań funkcji w czasie 1 sekundy
  static unsigned long PrevPrnt = 0L;
  static unsigned long Licznik = 0L;
  if(millis() - PrevPrnt >= 1000) {
    Serial.println(Licznik);
    Licznik = 0;
    PrevPrnt = millis();
  }
  Licznik += 1;
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  task_dioda();
  task_nuda();
}

Korzystając z funkcji task_dioda() jako wzorzec można napisać własne funkcje sterowane upływem czasu i wywołać je w funkcji loop.


void task_xxx() {
static unsigned long PrevTime = 0L;  
if (millis() - PrevTime >= int(XXX_PERIOD) ) {  // może być stała np. 100 lub zmienna
     // tutaj wstaw swój własny kod
     PrevTime = millis();
  }
}

Omówienia wymaga zmienna PrevTime. Ponieważ zadeklarowana jest wewnątrz funkcji task_xxx(), nie jest widoczna poza tą funkcją. Zatem można zadeklarować wiele funkcji task_???() zawierającą zmienną PrevTime. Z powodu zadeklarowania jej jako static, zmienna zachowuje swoją wartość pomiędzy wywołaniami funkcji. Przypisanie wartości 0 w deklaracji zmiennej static powoduje, że ustawienie wartości zmiennej wykonuje się tylko raz, po uruchomieniu programu. Zabezpiecza to przed losowym zachowaniem programu w przypadku resetu programowego, np. po zadziałaniu watchdoga. Co do długości zmiennej – int czy long – określa ona częstotliwość przepełnienia. Co do znaku zmiennej – unsigned lub bez (signed) określa, kiedy zdarzy się pierwsze przepełnienie – dla unsigned dwukrotnie później niż dla liczby ze znakem.

 

Funkcja task_nuda() jest dobrym punktem wyjścia do napisania funkcji sterowanej zajściem zdarzenia, na przykład zwarciem wyjścia pinu do masy. O ile w powyższej funkcji w każdym wywołaniu zwiększałem licznik obiegów, to w przypadku funkcji task_przycisk() będę sprawdzał stan pinu – wysoki lub niski. Wykorzystam do tego celu pin2 z tego powodu, że na płytce Arduino tylko piny 2 i 3 zgłaszają przerwania sprzętowe. Jednak na początek stan pinu będę obserwował badając go programowo. Chcę, aby po zwarciu pinu do masy dioda zwiększała częstotliwość migania. Definicje pinów na płytce Arduino można znaleźć w części 1 kursu Arduino na Forbocie: https://forbot.pl/blog/kurs-arduino-p odstawy-programowania-porty-io-id3648.

Po uruchomieniu funkcji task_przycisk() pętla loop() wykonuje się ok. 100 tys. razy na sekundę, zatem stan przycisku jest sprawdzany co ok. 10 mikrosekund. Ponieważ potrzebowałem wyjścia konsoli do wyświetlania stanu przycisku, zamaskowałem uruchamianie funkcji task_nuda. Ponadto zamiast stałej LED_PERIOD zdefiniowałem dwie: LED_PERIOD_H i LED_PERIOD_L określające częstotliwość migania diody odpowiednio dla stanu wysokiego i niskiego pinu.

// stałe
#define LED_PERIOD_H 1000 // okres przełączania diody - PRZYCISK HIGH
#define LED_PERIOD_L  500 // okres przełączania diody - PRZYCISK LOW
#define PRZYCISK 2        // pin, który będzie zwierany

void task_przycisk() {
static int PrevPin = digitalRead(PRZYCISK); // inicjalizacja  
int Pin;
Pin = digitalRead(PRZYCISK);            // bieżący stan przycisku
if( Pin != PrevPin) {
     Serial.println(Pin);
     PrevPin = Pin;  // zapamiętaj aktualny stan
  }
}


void task_dioda() {
static unsigned long PrevLed = 0L;  
if (millis() - PrevLed >= int(LED_PERIOD_H) ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void task_nuda() { // liczba wywołań funkcji w czasie 1 sekundy
  static unsigned long PrevPrnt = 0L;
  static unsigned long Licznik = 0L;
  if(millis() - PrevPrnt >= 1000) {
    Serial.println(Licznik);
    Licznik = 0;
    PrevPrnt = millis();
  }
  Licznik += 1;
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
pinMode(PRZYCISK, INPUT_PULLUP); //pin PRZYCISK jako wejście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  task_dioda();
//  task_nuda();
  task_przycisk();
}

Po uruchomieniu programu i zwarciu przycisku czy przewodu na monitorze otrzymuję wiele linii informujących o zwarciu lub rozwarciu połączenia. Jest to efekt drgania styków podczas przełączania. Jak sobie poradzić z problemem? Należy zmierzyć różnicę czasu pomiędzy kolejnymi zmianami stanu pinu. Jeśli ta różnica jest mniejsza niż powiedzmy 50 ms, to przyjmuję, że ta zmiana jest efektem drgania styków i ją ignoruję. W przeciwnym razie wykonuję działanie związane ze zmianą stanu styku. Przy tych założeniach akcja zmiany stanu styku będzie podjęta przy pierwszej zmianie, zaś następne zmiany, które zawarte będą w zadeklarowanym oknie czasowym, będą ignorowane. Okno czasowe jest ruchome – liczy się od ostatniej zmiany stanu styku. Poniżej przedstawiam zmodyfikowane fragmenty programu.

// stałe
#define LED_PERIOD_H 1000 // okres przełączania diody - PRZYCISK HIGH
#define LED_PERIOD_L  500 // okres przełączania diody - PRZYCISK LOW
#define PRZYCISK 2        // pin, który będzie zwierany
#define OPOZN_PRZYCISKU 50// długosc okna czasowego dla eliminacji drgań

void task_przycisk() {
static int PrevPin = digitalRead(PRZYCISK); // inicjalizacja  
static unsigned long PrevTime = 0L;
int Pin;
unsigned long CurrTime;
Pin = digitalRead(PRZYCISK);            // bieżący stan przycisku
if( Pin != PrevPin) { // zmiana stanu
    CurrTime = millis();
    if(CurrTime - PrevTime > OPOZN_PRZYCISKU) {
       Serial.println(Pin);
       PrevPin = Pin;  // zapamiętaj aktualny stan
    }
   PrevTime = CurrTime;    
  }
} 

W tej chwili program odczytuje i wyświetla stan pinu po wyeliminowaniu zjawiska drgań styku. Pozostaje teraz przekazać stan pinu do funkcji task_dioda(). Pytanie – jak to zrobić? Jest to jeden z istotnych problemów konstrukcji systemów operacyjnych – komunikacja międzyprocesowa i synchronizacja procesów (np. podczas zapisu do współdzielonej danej nie można jej jednocześnie czytać). W przypadku systemu jednoprocesorowego sprawa synchronizacji jest prosta: skoro mamy jeden procesor, to on albo czyta, albo zapisuje. Nigdy nie robi tego jednocześnie. Z drugiej strony dowolna funkcja nie powinna grzebać we wnętrzu jakiejkolwiek innej. Z tego powodu najprościej jest wystawić zmienną wspólną do obszaru zmiennych globalnych. W moim programie taką zmienną jest Pin zadeklarowana w funkcji task_przycisk(). Jest ona zadeklarowana jako volatile (ulotna) aby zasygnalizować kompilatorowi, by jej nie optymalizował (czytaj usunął). Po tej zmianie kod wygląda następująco:

// stałe
#define LED_PERIOD_H 1000 // okres przełączania diody - PRZYCISK HIGH
#define LED_PERIOD_L 500 // okres przełączania diody - PRZYCISK LOW
#define PRZYCISK 2 // pin, który będzie zwierany
#define OPOZN_PRZYCISKU 50// długosc okna czasowego dla eliminacji drgań

// zmienne globalne - komunikacja międzyprocesowa
volatile int Pin;

void task_przycisk() {
static int PrevPin = digitalRead(PRZYCISK); // inicjalizacja 
static unsigned long PrevTime = 0L;
unsigned long CurrTime;
Pin = digitalRead(PRZYCISK); // bieżący stan przycisku
if( Pin != PrevPin) { // zmiana stanu
CurrTime = millis();
if(CurrTime - PrevTime > OPOZN_PRZYCISKU) {
Serial.println(Pin);
PrevPin = Pin; // zapamiętaj aktualny stan
}
PrevTime = CurrTime; 
}
}

W porównaniu z poprzednim przykładem działanie programu jest identyczne. Lecz po tej modyfikacji stan przycisku (zmienna Pin) jest dostępny dla funkcji task_dioda(). Nadszedł czas na zmianę częstotliwości migania diody. Poniżej znajduje się kod zmodyfikowanej funkcji task_dioda().

void task_dioda() {
static unsigned long PrevLed = 0L;  
int period;
 Pin == 0  ? period = LED_PERIOD_L : period = LED_PERIOD_H; 
if (millis() - PrevLed >= period ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

W funkcji zdefiniowałem zmienną period, która w zależności od wartości zmiennej Pin przyjmuje wartości LED_PERIOD_L (dla 0) lub LED_PERIOD_H (w przeciwnym wypadku). Zmienną użyłem w warunku sprawdzania upływu czasu zamiast poprzednio użytego stałego czasu migania.

 

Listing całego programu przedstawiam poniżej.

// stałe
#define LED_PERIOD_H 1000 // okres przełączania diody - PRZYCISK HIGH
#define LED_PERIOD_L  200 // okres przełączania diody - PRZYCISK LOW
#define PRZYCISK 2        // pin, który będzie zwierany
#define OPOZN_PRZYCISKU 50// długosc okna czasowego dla eliminacji drgań

// zmienne globalne - komunikacja międzyprocesowa
volatile int Pin;

void task_przycisk() {
static int PrevPin = digitalRead(PRZYCISK); // inicjalizacja  
static unsigned long PrevTime = 0L;
unsigned long CurrTime;
Pin = digitalRead(PRZYCISK);            // bieżący stan przycisku
if( Pin != PrevPin) { // zmiana stanu
    CurrTime = millis();
    if(CurrTime - PrevTime > OPOZN_PRZYCISKU) {
       Serial.println(Pin);
       PrevPin = Pin;  // zapamiętaj aktualny stan
    }
   PrevTime = CurrTime;    
  }
}


void task_dioda() {
static unsigned long PrevLed = 0L;  
int period;
 Pin == 0  ? period = LED_PERIOD_L : period = LED_PERIOD_H; 
if (millis() - PrevLed >= period ) {
     if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
     PrevLed = millis();
  }
}

void task_nuda() { // liczba wywołań funkcji w czasie 1 sekundy
  static unsigned long PrevPrnt = 0L;
  static unsigned long Licznik = 0L;
  if(millis() - PrevPrnt >= 1000) {
    Serial.println(Licznik);
    Licznik = 0;
    PrevPrnt = millis();
  }
  Licznik += 1;
}

void setup() {
pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
pinMode(PRZYCISK, INPUT_PULLUP); //pin PRZYCISK jako wejście
Serial.begin(9600); // wstępnie zdefiniowana prędkość transmisji monitora Arduino
Serial.println("Start programu"); // komunikat na konsolę
}

void loop() {
  task_dioda();
//  task_nuda();
  task_przycisk();
} 

W następnym odcinku spróbuję skonstruować prosty interpreter komend z konsoli i za jego pomocą przełączać częstotliwość (zonk-dwa niezależne żądania do jednego zasobu) lub wypełnienie migania diody. W kolejnych – wykorzystanie przerwań do reakcji na zmianę stanu przycisku i odbiór znaku, rozbudowa interpretera komend, sposób obsługi długich zadań (na przykład odczyt czujnika, który należy wykonać w czasie 1 sekundy – bo tak sobie życzy czujnik), problem zawieszenia systemu (łatwo go zawiesić – wystarczy pętla nieskończona w którejkolwiek funkcji) i automatyczny reset – wykorzystanie watchdoga.

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...