Skocz do zawartości

Wielozadaniowość cz. 3


Pomocna odpowiedź

Napisano

Cel artykułu

Rozwinięcie programu z części drugiej o najprostszy interpreter komend

W drugiej części pokazałem, jak program z części pierwszej rozbudować o obsługę zdarzeń asynchronicznych. Program reaguje na zwarcie pinu (Pin2) do masy zmieniając częstotliwość migania diody. Poniżej zamieszczam końcowy kod programu z części pierwszej.


 

// 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();
} 

Zanim zabiorę się za kodowanie, chciałbym pokazać problemy, które są do rozwiązania podczas tworzenia procedury task_komenda(). Jako punkt wyjścia znowu wykorzystam funkcję task_nuda(), która tym razem będzie odczytywała port szeregowy Serial. Ma on dwa bufory: wejściowy, wykorzystywany przez funkcje wysyłania takie jak Serial.println(), i wyjściowy, zawierający dane przychodzące. Każdy z nich ma rozmiar 64 bajtów. Port szeregowy obsługuje dane jako znaki. Budowa ramki opisana jest w artykule https://forbot.pl/blog/port-szeregowy-interfejs-usart-czyli-komunikacja-mikrokontrolera-z-komputerem-id1122 oraz https://forbot.pl/blog/kurs-arduino-uart-komunikacja-z-komputerem-pc-zmienne-id3836

Prędkość przesyłania znaku o formacie : 1 bit startu, 8 bitów danych, 1 bit stopu (w sumie 10 bitów) wynosi 1/10 zadeklarowanej prędkości transmisji, czyli w przypadku wywołania funkcji inicjującej port szeregowy Serial.begin(9600) wynosi 960 znaków/s.

Port szeregowy Serial ma dwie funkcje nieblokujące służące do tego celu: Serial.available(): Sprawdza, czy w buforze portu szeregowego czekają jakieś bajty. Zwraca liczbę dostępnych znaków i nie czeka na nowe dane.  Serial.read(): Odczytuje jeden bajt z bufora i natychmiast wraca. Jeśli w buforze nic nie ma, zwraca wartość -1.

Dla prędkości transmisji 9600 b/s funkcja odbierająca 1 znak jest wystarczająca, gdyż znak przychodzi trochę rzadziej niż 1 na milisekundę, zaś aktualnie pętla w funkcji loop() wykonuje się ok. 70 razy w ciągu 1 milisekundy. Problem pojawi się wówczas, gdy zaczniemy odbierać dane w sposób ciągły z prędkością 115200 b/s lub większą i spadnie nam znacząco częstotliwość obiegu pętli loop(), co dzieje się zawsze, gdy rozbudujemy program. Jeżeli dane będziemy przesyłać w paczkach mniejszych niż 64 bajty (rozmiar bufora) i rzadko (w porównaniu z częstotliwością obiegu pętli w funkcji loop() ), to będziemy mogli (teoretycznie – do sprawdzenia) posługiwać się wysokimi prędkościami transmisji (230400, 460800, 921600, 1000000,1500000, 2000000 bodów - dla układu CH340 i Arduino2). Dla takich prędkości konwerter serial/USB CH340 (pewnie inne konwertery na innych płytkach też) nie „lubi” częstotliwości innych niż podane – pojawiają się błędy transmisji. Przy okazji – w programie przydałaby się funkcja pozwalająca odczytać aktualną prędkość obiegu pętli w funkcji loop().

Kiedy już odbiorę znak, to powstaje pytanie, co z nim zrobić. W tym momencie do funkcji loop() wstawię nową funkcję task_komenda(). Będzie ona odbierała 1 znak z portu szeregowego (o ile nadejdzie) i w przypadku jeśli będzie to „d” – ustawi wartość zmiennej globalnej Pin (tej samej, którą używa funkcja task_przycisk() ) na 0. Jeśli będzie to „D” – to wartość zmiennej Pin zostanie ustawiona na 1. Pozostałe znaki będą ignorowane. Dlaczego w ten sposób? Komendy wydaje człowiek – w tym przypadku ja. Powinny być takie, abym łatwo je zapamiętał. Poza tym mam do dyspozycji 1 znak. Na przykład wysłanie 1 i 0 też by w tym momencie zadziałało (mamy 1 zmienną), ale wówczas co z wywołaniami innych funkcji, np. odczytanie ilości obiegów pętli? Stąd też przyjmuję, że duże D to ustawienie 1 na zmiennej Pin, małe d – ustawienie 0. (Równie dobrze mogłoby być P i p). znaki „O” i „o” (jeden i drugi) mogłyby oznaczać żądanie wysłania liczby obiegów pętli, znaki „Z” i „z” – wywołanie funkcji zawieszającej system. (Przyda się – choćby po to, aby pokazać problem, a później, by zademonstrować działanie watchdoga.) Dlaczego w ten sposób? Dlatego, że taka jest wola projektanta programu, w tym przypadku moja. Poniżej przedstawiam funkcję task_komenda() i loop().

void task_komenda() { // najprostszy interpreter komend (komendy 1-znakowe)
if (Serial.available() > 0) // czy jest znak ?
{
char Znak = Serial.read(); // odczyt znaku
switch(Znak) {
case 'D' : Pin = 1; // dioda miga powoli
break;
case 'd' : Pin = 0; // dioda miga szybko
break;
default: ; // wszystkie inne znaki ignorujemy
break;
}
Serial.println(Znak); // echo na konsolę
}
}
  
void loop() {
task_dioda();
//task_nuda();
task_przycisk();
task_komenda();
}

Kompiluję, wgrywam, z konsoli wysyłam „d”, na konsoli odczytuję, że znak został odesłany poprawnie … i zonk. Program nie reaguje na komendę, chociaż powinien. Pytanie dlaczego? Powodem jest funkcja task_przycisk(). Wartość zmiennej Pin zmieniłem poza funkcją. Po zmianie stanu zmiennej funkcja task_przycisk() ponownie odczytuje stan pinu, który się nie zmienił, w wyniku czego nie obserwuję zmiany szybkości migania diody. Zamaskowanie wywołania funkcji task_przycisk() powoduje, że program działa zgodnie z oczekiwaniem. Pojawiają się zatem dwa pytania: 1. czy ustawianie wartości jednej zmiennej przez dwa zadania może mieć miejsce w rzeczywistości? 2. Jeśli tak, to jak poradzić sobie z problemem?

Co do pierwszego pytania: Jest to częsta sytuacja. Proszę przyjrzeć się przykładowi ze stronyhttps://forbot.pl/blog/kurs-arduino-ii-wielozadaniowosc-opoznienia-z-millis-id18418 - „Przykład prostej automatyzacji z Arduino”. Tam zmianę stanu oświetlenia realizowały trzy procesy, przy czym każdy z nich wykonywał tylko jedno zadanie: albo włącz (czujnik PIR), albo wyłącz (kontaktron, proces pomiaru czasu od włączenia). W moim przypadku rozwiązaniem może być ustawienie priorytetów: jeżeli zwiększę prędkość migania diody z konsoli, to wyłączam funkcję task_przycisk(). Jeśli ją zmniejszam, to ją włączam. Przy tym założeniu przycisk będzie zwiększał częstotliwość migania diody tylko wówczas, gdy konsola na to zezwoli. Zatem potrzebuję drugiej zmiennej globalnej, powiedzmy Konsola. Bedzie ona przyjmowała wartość 1, jeśli zmienną Pin ustawię na 0 z konsoli, i wartość 0, jeżeli wartość zmiennej Pin ustawię na 1. Powstaje kolejne pytanie: jak wyłączać funkcję task_przycisk() ? W funkcji loop() w sposób 

if(Konsola !=0 ) task_przycisk();

czy też wewnątrz funkcji task_przycisk() na początku:

if(Konsola != 0) return;

Oraz ostatnie pytanie: w którym miejscu ustawiać wartość zmiennej Konsola? Docelowo powinna robić to funkcja przekazująca parametry do jakiegoś modułu i wywoływana z interpretera komend. W ten sposób można byłoby skonstruować wielopoziomowy interpreter, obsługujący wieloznakowe komendy. Dodatkowo drugi poziom interpretera znajdowałby się w module zewnętrznym (jeśli modułów byłoby więcej niż jeden) i nie wprowadzałby bałaganu w kodzie. W moim przypadku, przy tak prostym interpreterze, zmienną Konsola będę ustawiał bezpośrednio w interpreterze. Poniżej przedstawiam listing programu

// 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;
volatile int Konsola;

void task_komenda() { // najprostszy interpreter komend (komendy 1-znakowe)
if (Serial.available() > 0) // czy jest znak ?
{
char Znak = Serial.read(); // odczyt znaku
switch(Znak) {
case 'D' : Pin = 1; // dioda miga powoli
Konsola = 0;
break;
case 'd' : Pin = 0; // dioda miga szybko
Konsola = 1;
break;
default: ; // wszystkie inne znaki ignorujemy
break;
}
}
}

void task_przycisk() {
if(Konsola != 0) return; // szybkie miganie diody ustawione z konsoli ma wyższy priorytet
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
Konsola = 0; // wstępnie ustawiamy aktywność przycisku
Serial.println("Start programu"); // komunikat na konsolę
}

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

Kolejną rzeczą jest wyświetlenie ilości obiegów pętli funkcji loop() na żądanie. W sekcji Zmienne globalne dodałem zmienną LicznikNudy i zmodyfikowałem funkcję task_nuda() tak, że zamiast wyświetlać na konsoli zawartość wewnętrznego licznika co 1 sekundę, zapisuje jego wartość do zmiennej LicznikNudy.

Dodałem też funkcję task_zawis(), która wyświetla komunikat o zawieszeniu i wykonuje pętlę nieskończoną. Funkcję tę dodałem bezpośrednio w interpreterze komend. Mogłem ją dodać w funkcji loop(), dodając kolejną zmienną globalną i ustawiając odpowiednio jej wartości. Rozbudowany interpreter komend będzie jednak przekazywał rozkazy poprzez wywołania funkcji z parametrem, zaś wywołanie funkcji task_zawis() jest aktem jednorazowym – po jej wywołaniu program wisi. Jedyne, co można zrobić, to wyłączyć i włączyć zasilanie lub nacisnąć przycisk „RESET”.

W funkcji task_komenda() dodałem obsługę wyświetlania ilości obiegów pętli (komendy „O” i „o”) oraz wywołanie funkcji zawieszającej system (komendy „Z” i „z”). Poniżej przedstawiam listing programu po zmianach

// 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;
volatile int Konsola;
volatile unsigned long LicznikNudy;

void task_zawis() {
  Serial.println("Zawieszam sie");
  while(1) ;  // pętla nieskończona
}

void task_komenda() { // najprostszy interpreter komend (komendy 1-znakowe)
  if (Serial.available() > 0) // czy jest znak ?
   {
    char Znak = Serial.read(); // odczyt znaku
    switch(Znak) {
      case 'D' : Pin = 1; // dioda miga powoli
                Konsola = 0;
      break;
      case 'd' : Pin = 0; // dioda miga szybko
                Konsola = 1;
      break;
      case 'o' :
      case 'O' : Serial.print("Licznik petli=");
                 Serial.println(LicznikNudy);
      break;
      case 'z' :
      case 'Z' : task_zawis();
      default: ;          // wszystkie inne znaki ignorujemy
      break;
    }
  }
}


void task_przycisk() {
if(Konsola != 0) return; // szybkie miganie diody ustawione z konsoli ma wyższy priorytet
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 PrevN = 0L;
  static unsigned long Licznik = 0L;
  if(millis() - PrevN >= 1000) {
    //Serial.println(Licznik);
    LicznikNudy = Licznik;
    Licznik = 0;
    PrevN = 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
Konsola = 0;        // wstępnie ustawiamy aktywność przycisku
Serial.println("Start programu"); // komunikat na konsolę
}

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

W następnym odcinku spróbuję uruchomić automatyczny reset programu za pomocą watchdoga, oraz rozbudować interpreter komend z konsoli tak, by przyjmował komendy wieloznakowe. W kolejnych – wykorzystanie przerwań do reakcji na zmianę stanu przycisku i odbiór znaku oraz obsługę długich zadań (na przykład odczyt czujnika, który należy realizować w czasie 1 sekundy – bo tak sobie życzy czujnik).

Lepszym przykładem pokazania drgań styków i ich eliminacji jest togglowanie niż że if 0 migaj wolniej. Reszty nie czytałem

Do @_LM_ Być może masz rację. Tylko czy to jest bardziej zrozumiałe dla początkującego niż if() ? Moim celem jest napisać zrozumiały tutorial, a nie zoptymalizowany tutorial

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