Wielozadaniowość Arduino - część druga

Robiąc projekty, zdarza się często, że chcesz, aby procesor zrobił jeszcze jedną rzecz. Lecz jak sprawić, aby wykonał to gdy zajęty jest innymi zadaniami?

 

Człowiek orkiestra

Człowiek-orkiestra, zdjęcie z ok. 1865 roku, źr. Wikimedia.

 

W tym przewodniku, będziemy bazować na technikach, których nauczyliśmy się w części 1.

 

Dowiemy się jak ujarzmić przerwania stopera, aby wszystko działało jak w zegarku. Odkryjemy również jak używać przerwań zewnętrznych, aby powiadamiały nas o zewnętrznych zdarzeniach.

 

serwo jak zegarek

 

Podłączenie

Wszystkie przykłady w tym przewodniku będą używać poniższego schematu podłączenia:

  

Schemat podłączenia 

Czym jest Przerwanie?

Przerwanie to sygnał, który nakazuje procesorowi natychmiastowe zatrzymanie wszystkiego i zajęcie się przetwarzaniem o wysokim priorytecie. Jest ono nazywane Obsługą Przerwania.

 

Obsługa przerwania to kolejna pusta funkcja. Jeśli takową napiszesz i dołączysz ją do przerwania, zostanie przywołana za każdym razem gdy sygnał przerwania zostanie aktywowany. Gdy wyłączysz tę funkcję, procesor wznowi to, co robił wcześniej. 

 

Skąd biorą się przerwania?

Przerwania mogą być wytwarzane z kilku źródeł:

 

  • Przerwania czasowe wywołane przez stopery Arduino.
  • Przerwania zewnętrzne wywołane przez zmianę w statusie jednego z pinów zewnętrznego przerwania.
  • Przerwania zmiany pinu wywołane przez zmianę w statusie któregokolwiek pinu z grupy.

 

Do czego się one przydają?

Użycie przerwań powoduje, że nie ma już potrzeby pisać pętli w kodzie, aby ciągle sprawdzać stan przerwań o wysokim priorytecie. Dzięki długo działającym podprogramom, nie musisz martwić się o wolną reakcję albo o to, że zapomnisz wcisnąć przycisk.

 

Podczas przerwania, procesor automatycznie zatrzyma wszystkie operacje i przywoła obsługę przerwania. Musisz tylko napisać kod, który zareaguje na każde przerwanie.

 

Przerwania czasowe

Przerwania czasowe 

Nie dzwoń do nas, to my zadzwonimy do Ciebie

W części 1, nauczyliśmy się jak użyć funkcji millis() w opóźnieniu. Aby zadziałała, musieliśmy ją przywołać po każdym zapętleniu, żeby sprawdzić czy trzeba coś zrobić. Przywoływanie tej funkcji częściej niż co milisekundę tylko po to, żeby się dowiedzieć, że czas się nie zmienił, było stratne. Byłoby lepiej, gdyby takie sprawdzanie odbywało się tylko co milisekundę.

 

Stopery i przerwania czasowe właśnie nam na to pozwalają. Możemy ustawić stoper, aby przerywał nam raz na milisekundę. Wtedy sam nas poinformuje, kiedy mamy sprawdzić czas!

 

Stopery Arduino

Istnieją 3 rodzaje stoperów Arduino Uno: Stoper0, Stoper1 i Stoper2. Ten pierwszy, jest już ustawiony, aby wygenerować milisekundowe przerwanie, które zaktualizuje milisekundowy licznik zawiadomiony przez funkcję millis(). My także użyjemy Stopera0.

 

Częstotliwość i liczenie

Stopery to proste liczniki, które działają z pewną częstotliwością pochodzącą z 16MHz-go zegara systemowego. Możesz ustawić dzielnik zegara tak, aby zmieniał częstotliwość i inne różne tryby liczenia. Możesz je także skonfigurować, żeby tworzyły przerwania wtedy, kiedy stoper osiągnie określoną liczbę. 

 

Stoper0 posiada 8 bitów i potrafi liczyć od 0 do 255, a także generuje przerwania za każdym razem gdy się przepełni. Domyślnie, używa dzielnika zegara 64, aby dostarczyć częstotliwość przerwań o 976,5625 Hz. Nie będziemy zmieniać częstotliwości Stopera0, ponieważ mogłoby to zmienić funkcję millis()!

 

Rejestry porównawcze

Stopery Arduino posiadają pewną liczbę rejestrów konfiguracyjnych. Mogą być one odczytane lub zapisane używając specjalnych znaków określonych w Arduino IDE. 

 

Ustawimy rejestr porównawczy dla Stopera 0 (rejestr ten znany jest pod nazwą OCR0A), aby wygenerował następne przerwanie w trakcie liczenia. Przy każdym tyknięciu, licznik stopera porównywany jest z rejestrem porównawczym, i gdy są one równe, wytworzone zostaje przerwanie.

 

Poniższy kod stworzy przerwanie 'TIMER0_COMPA' kiedy tylko licznik wartości przekroczy 0xAF. 

 

 // Timer0 is already used for millis() - we'll just interrupt somewhere
// in the middle and call the "Compare A" function below
OCR0A = 0xAF;
TIMSK0 |= _BV(OCIE0A);

 

Następnie, określimy obsługę przerwania dla wektora przerwania czasowego znanego jako "TIMER0_COMPA_vect". W tej obsłudze przerwań, zrobimy wszystko co robiliśmy w pętli.  

 

// Interrupt is called once a millisecond, 
SIGNAL(TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis();
sweeper1.Update(currentMillis);

//if(digitalRead(2) == HIGH)
{
sweeper2.Update(currentMillis);
led1.Update(currentMillis);
}

led2.Update(currentMillis);
led3.Update(currentMillis);
}

 

Zostawia nas to z zupełnie pustą pętlą. 

 

void loop()
{
}

 

Teraz możesz robić wszystko w swojej pętli. Nawet użyć funkcji dealy() bez żadnych konsekwencji! Nie zmieni to działania flasherów i sweeperów

 

Kod źródłowy:

Oto cały kod, razem z flasherami sweeperami:

 

#include 
class Flasher
{
// Class Member Variables
// These are initialized at startup
int ledPin; // the number of the LED pin
long OnTime; // milliseconds of on-time
long OffTime; // milliseconds of off-time
// These maintain the current state
int ledState; // ledState used to set the LED
unsigned long previousMillis; // will store last time LED was updated // Constructor - creates a Flasher
// and initializes the member variables and state
public:
Flasher(int pin, long on, long off)
{
ledPin = pin;
pinMode(ledPin, OUTPUT);

OnTime = on;
OffTime = off;

ledState = LOW;
previousMillis = 0;
} void Update(unsigned long currentMillis)
{
if((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
{
ledState = LOW; // Turn it off
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
{
ledState = HIGH; // turn it on
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
}
};

class Sweeper
{
Servo servo; // the servo
int pos; // current servo position
int increment; // increment to move for each interval
int updateInterval; // interval between updates
unsigned long lastUpdate; // last update of position public:
Sweeper(int interval)
{
updateInterval = interval;
increment = 1;
}

void Attach(int pin)
{
servo.attach(pin);
}

void Detach()
{
servo.detach();
}

void Update(unsigned long currentMillis)
{
if((currentMillis - lastUpdate) > updateInterval) // time to update
{
lastUpdate = millis();
pos += increment;
servo.write(pos);
if ((pos >= 180) || (pos <= 0)) // end of sweep
{
// reverse direction
increment = -increment;
}
}
}
};

Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);
Sweeper sweeper1(25);
Sweeper sweeper2(35);

void setup()
{
sweeper1.Attach(9);
sweeper2.Attach(10);

// Timer0 is already used for millis() - we'll just interrupt somewhere
// in the middle and call the "Compare A" function below
OCR0A = 0xAF;
TIMSK0 |= _BV(OCIE0A);
}

// Interrupt is called once a millisecond, to update the LEDs
// Sweeper2 s not updated if the button on digital 2 is pressed.
SIGNAL(TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis();
sweeper1.Update(currentMillis);

if(digitalRead(2) == HIGH)
{
sweeper2.Update(currentMillis);
led1.Update(currentMillis);
}

led2.Update(currentMillis);
led3.Update(currentMillis);
}

void loop()
{
}

 

Przerwania zewnętrzne 

Kiedy nie używać zapętlania

W przeciwieństwie do przerwań czasowych, przerwania zewnętrzne są aktywowane przez zewnętrzne zdarzenia. Na przykład, po wciśnięciu przycisku albo po otrzymaniu impulsu z enkodera obrotowego. Jednakże, nie musisz ciągle sprawdzać czy piny GPIO muszą zostać zmienione, tak jak to było w przypadku przerwań czasowych.

 

Arduino UNO posiada 2 piny przerwania zewnętrznego. W tym przykładzie, podłączamy przycisk do jednego z tych pinów i używamy ich do resetowania naszych sweeperów. Po pierwsze, dodamy funkcję "reset()" do klasy sweeper. Funkcja ta ustawia pozycję na 0 i natychmiast pozycjonuje tam serwo:

 

 void reset()
{
pos = 0;
servo.write(pos);
increment = abs(increment);
}

 

Następnie dodamy przywołanie do AttachInterrupt(), aby połączyć zewnętrzne przerwanie z kodem obsługi. 

W UNO, Przerwanie 0 jest powiązane cyfrowym pinem 2. Rozkażemy mu wyszukanie krawędzi "FALLING" sygnału tego pinu. Po wciśnięciu przycisku, sygnał "upada" z WYSOKIEGO na NISKI i przywołany jest program obsługi przerwań "Reset".

 

pinMode(2, INPUT_PULLUP);
attachInterrupt(0, Reset, FALLING);

 

A oto Program Obsługi Przerwań "Reset". Przywołuje on funkcje resetu sweepera:

 

void Reset()
{
sweeper1.reset();
sweeper2.reset();
}

 

Od tego momentu, kiedy tylko wciśniesz przycisk, serwa zatrzymają swoje zadania i zaczną natychmiast szukać pozycji zero.

 

 

Kod źródłowy:

Oto kompletny szkic ze stoperami i zewnętrznymi przerwaniami:

 

#include 

class Flasher
{
// Class Member Variables
// These are initialized at startup
int ledPin; // the number of the LED pin
long OnTime; // milliseconds of on-time
long OffTime; // milliseconds of off-time
// These maintain the current state
volatile int ledState; // ledState used to set the LED
volatile unsigned long previousMillis; // will store last time LED was updated
// Constructor - creates a Flasher
// and initializes the member variables and state
public:
Flasher(int pin, long on, long off)
{
ledPin = pin;
pinMode(ledPin, OUTPUT);
OnTime = on;
OffTime = off;
ledState = LOW;
previousMillis = 0;
}
void Update(unsigned long currentMillis)
{
if ((ledState == HIGH) && (currentMillis - previousMillis >= OnTime))
{
ledState = LOW; // Turn it off
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
else if ((ledState == LOW) && (currentMillis - previousMillis >= OffTime))
{
ledState = HIGH; // turn it on
previousMillis = currentMillis; // Remember the time
digitalWrite(ledPin, ledState); // Update the actual LED
}
}
};

class Sweeper
{
Servo servo; // the servo
int updateInterval; // interval between updates
volatile int pos; // current servo position
volatile unsigned long lastUpdate; // last update of position
volatile int increment; // increment to move for each interval
public:
Sweeper(int interval)
{
updateInterval = interval;
increment = 1;
}

void Attach(int pin)
{
servo.attach(pin);
void Detach()
{
servo.detach();
} void reset()
{
pos = 0;
servo.write(pos);
increment = abs(increment);
}
void Update(unsigned long currentMillis)
{
if ((currentMillis - lastUpdate) > updateInterval) // time to update
{
lastUpdate = currentMillis;
pos += increment;
servo.write(pos);
if ((pos >= 180) || (pos <= 0)) // end of sweep
{
// reverse direction
increment = -increment;
}
}
}
};
Flasher led1(11, 123, 400);
Flasher led2(12, 350, 350);
Flasher led3(13, 200, 222);

Sweeper sweeper1(25);
Sweeper sweeper2(35);

void setup()
{
sweeper1.Attach(9);
sweeper2.Attach(10);

// Timer0 is already used for millis() - we'll just interrupt somewhere
// in the middle and call the "Compare A" function below
OCR0A = 0xAF;
TIMSK0 |= _BV(OCIE0A);

pinMode(2, INPUT_PULLUP); attachInterrupt(0, Reset, FALLING);
}

void Reset()
{
sweeper1.reset();
sweeper2.reset();
}

// Interrupt is called once a millisecond,
SIGNAL(TIMER0_COMPA_vect)
{
unsigned long currentMillis = millis();
sweeper1.Update(currentMillis); //if(digitalRead(2) == HIGH)
{
sweeper2.Update(currentMillis);
led1.Update(currentMillis);
}
led2.Update(currentMillis);
led3.Update(currentMillis);
}

void loop()
{
}

  

Biblioteki

Więcej o stoperach

Stopery mogą być konfigurowane, aby działać na różnych częstotliwościach i w różnorodnych trybach. Oprócz wytwarzania przerwań, używane są one również do kontroli pinów PWM. 

 

Sekrety Arduino PWM

Ściągawka

 

Biblioteki stoperów

Istnieje pewna ilość bibliotek 'timerów' od Arduino dostępnych w internecie. Wiele z nich monitoruje funkcję millis() i wymaga ciągłego sprawdzania, jak to było w części 1. Lecz jest również kilka, które pozwolą Ci skonfigurować stopery i wygenerować przerwania.

 

Biblioteki TimerOne i TimerThree

 

Przerwania zmiany pinu

Kiedy 2 to za mało

 

Arduino UNO posiada tylko 2 piny przerwania zewnętrznego. Ale co zrobić, gdy potrzebujesz ich więcej? Na szczęście, Arduino UNO wspomaga przerwania "zmiany pinu" na wszystkich pinach.

 

Przerwania zmiany pinu są podobne do przerwań zewnętrznych. Jedyna różnicą jest to, że jedno z nich jest wytworzone dla zmiany w stanie, na jakimkolwiek pinie z 8 powiązanych. Są one trochę bardziej skomplikowane w obsłudze, ponieważ musisz śledzić ostatni znany stan wszystkich 8 pinów, aby dowiedzieć się, który z nich spowodował przerwanie. 

  

Biblioteka PinChaneInt

 

Stoper i etykieta przerwania

Przerwania są jak kolejki do kasy w supermarkecie. Bądź ostrożny i wybierz 10 rzeczy lub mniej, a wszystko przebiegnie bez zakłóceń. 

 

Jeśli wszystko jest ważne, to nic nie jest ważne.

Obsługi przerwania powinny być używane tylko do przetwarzania najważniejszych, uwarunkowanych czasowo zdarzeń. Pamiętaj, że przerwania są wyłączone w obsłudze przerwania. Jeśli spróbujesz robić zbyt dużo na poziomie przerwań, zdegradujesz odpowiedź dla innych przerwań. 

 

Jedno przerwanie na raz. 

W ISR, przerwania są wyłączone. Ma to dwie bardzo ważne konsekwencje:

  1. Praca zrobiona w ISR powinna być krótka, aby nie przegapić żadnych przerwań.
  2. Kod w ISR, aby działać, nie powinien przywoływać niczego co wymaga przerwań (np. funkcja delay() lub czegokolwiek, co używa magistrali I2C). Jeśli jednak tak się stanie, twój program się zawiesi.

 

Opóźnij długie przetwarzanie do zapętlenia

Jeśli potrzebujesz szczegółowego przetwarzania w odpowiedzi na przerwanie, użyj obsługi przerwania, aby zrobić tylko to, co niezbędne. Ustaw później zmienną stanu zmiennego, aby wskazywała, że późniejsze przetwarzanie jest wymagane. Kiedy przywołujesz funkcję aktualizacji z pętli, sprawdź status zmiennej, aby zobaczyć, czy dalsze przetwarzanie jest wymagane. 

 

Sprawdź przed ponownym skonfigurowaniem stopera

Stopery to limitowane źródło. UNO posiada ich tylko 3 i używane są one do wielu rzeczy. Jeśli zmienisz ustawienia stopera, niektóre inne rzeczy mogą już nie działać. Na przykład, w Arduino UNO:

 

  • Stoper0 - używany w funkcjach millis(), micros(), delay() i w PWM na pinach 5 i 6
  • Stoper1 - używany w Serwach, bibliotece WaveHC i w PWM na pinach 9 i 10
  • Stoper2 - używany przez Tone i PWM na pinach 11 i 13

 

Bezpiecznie wymieniaj dane

Musimy być ostrożni jeśli chodzi o wymianę danych pomiędzy obsługą przerwań, a kodem w zapętleniu, ponieważ przerwanie, aby móc dalej działać, zawiesi jakąkolwiek pracę procesora. 

 

Zmieniające się zmienne

Kompilator czasami będzie się starać zoptymalizować Twój kod do prędkości. Niekiedy te zmiany zatrzymają kopię najczęściej używanych zmiennych w rejestrze do szybkiego dostępu. Problemem jest to, że jedne z tych zmiennych są udostępniane pomiędzy obsługą przerwań, a kodem zapętlenia. Jedno z nich może używać starszej wersji zamiast najnowszej. Oznacz zmienne jako zmieniające się, aby dać znać kompilatorowi, żeby nie wykonywał tych potencjalnie niebezpiecznych optymalizacji.

Ochrona większych zmiennych

Nawet zaznaczenie, że zmienna się zmienia, nie jest wystarczające jeśli jest ona większa niż liczba całkowita (np. napisy, tabele, struktury, itp.). Większe zmienne wymagają kilku zestawów instrukcji, aby się zaktualizować. Jeśli przerwanie występuje podczas aktualizacji, dane mogą zostać uszkodzone. Jeśli posiadasz większe zmienne lub struktury, które wymieniane są z obsługami przerwań, powinieneś wyłączyć przerwania podczas ich aktualizacji z zapętlenia. (Przerwania są domyślnie wyłączone w obsłudze przerwań).

 

<- część pierwsza          część trzecia ->

 

Źródło: https://learn.adafruit.com/multi-tasking-the-arduino-part-2

zapraszamy do współpracy!