Back

Profesjonalne Arduino? Watchdog i usypianie CPU

Czy na Arduino można pisać profesjonalne programy? Można! Co prawda programy na mikrokontrolery AVR (ATmega…), z którymi zwykle utożsamia się Arduino, pisze się trudniej i dłużej niż na ARM, ale jak najbardziej, możliwe są profesjonalne rozwiązania na AVR.


Gdy tworzymy nowy projekt w Arduino IDE, naszym oczom ukazuje się taki listing:

void setup(){
}
void loop(){
}

Czego w nim brakuje? Profesjonalista odpowie: „brak obsługi Watchdog’a i usypiania CPU”.

Do czego dłuży Watchdog? Resetuje on CPU gdy program utknie w wiecznej pętli.

A po co usypiać CPU? W celu zmniejszenia poboru prądu i zmniejszenia generowanych zakłóceń.

Jak więc powinien wyglądać profesjonalny szkic Arduino? Na przykład tak:

void setup(){
	włączenie_watchdoga();
}
void loop(){
	reset_watchdoga();
	uśpienie_CPU();
}

W praktyce będzie to wyglądać tak:

//---- Najpierw dodajemy dwie biblioteki,o których zapominają „Arduinowcy” !!!!!
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/atomic.h>
//=========================================//
#define LED_RUN   8
#define LED_ST     9
//=========================================//
void usypianie_i_wdg() {
  wdt_reset();    // Resetujemy Watchdog
  //---- – Usypiamy mikrokontroler, co zmniejsza pobór pradu i zakłócenia EMI !!!!! – ----//
  /×
      #define SLEEP_MODE_IDLE         0
    #define SLEEP_MODE_PWR_DOWN     1
    #define SLEEP_MODE_PWR_SAVE     2
    #define SLEEP_MODE_ADC          3
    #define SLEEP_MODE_STANDBY      4
    #define SLEEP_MODE_EXT_STANDBY  5
  ×/
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    //cli();    // „ATOMIC_BLOCK” wyłącza IRQ
    set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here
    sei();          // Aby wybudzić CPU muszą być włączone przerwania
    //sleep_mode();     // Makro robi funkcjonalnie to co ponizej
    sleep_enable(); // enables the sleep bit in the mcucr register
    sleep_cpu();
    sleep_disable();
  }
}
//=========================================//

void setup() {
  /×
    15 mS                           WDTO_15 MS
    30 mS                           WDTO_30 MS
    60 mS                           WDTO_60 MS
    120 mS                           WDTO_120 MS
    250 mS                         WDTO_250 MS
    500 mS                         WDTO_500 MS
    1 S                             WDTO_1 S
    2 S                             WDTO_2 S
    4 S                             WDTO_4 S
    8 S                             WDTO_8 S
  ×/
  wdt_enable(WDTO_500 MS);     // Włączamy Watchdog. Można też na stałe włączyć w FUSES
  //....
  wdt_enable(WDTO_120 MS);     // jeśli to konieczne – zmieniamy czas
}
//=========================================//
void loop() {
  usypianie_i_wdg();
}
//=========================================//

Pojawi się jednak problem.

Przypuśćmy, że chcemy migać diodą świecącą w pętli głównej z częstotliwością 1 Hz, a watchdog musi być resetowany częściej niż co 120 ms. Odpada więc rozwiązanie z delay jak poniżej:

void loop() {
  	usypianie_i_wdg();
	delay(1000);
 	digitalWrite(LED_RUN, LOW);
	delay(1000);
	digitalWrite(LED_RUN, HIGH);
}

W takim przypadku, w czasie wykonywania pierwszego „delay” po 120 ms nastąpi reset CPU. Wydaje się, że można wydłużyć czas watchdog’a do przykładowo 4 sekund, ale nie tędy droga! Dlaczego?

Pierwszy problem to maksymalny interwał czasowy watchdog’a, w płytkach Arduino z ATmega wynoszący 8 sekund. Jeśli więc obsługa pętli głównej trwałaby dłużej, resety byłyby nieuniknione albo trzeba byłoby zrezygnować z watchdog’a, a to nie najlepszy pomysł. Tu widać przewagę ARM. W STM32 można wydłużyć czas zadziałania watchdog’a do 26 sekund. Kolejna zaleta to dodatkowy watchdog okienkowy, który resetuje CPU w dwóch sytuacjach:

– gdy jest zbyt rzadko odświeżany,

– gdy jest odświeżany zbyt często.

Ponadto, w nowych wykonaniach STM32 użytkownik decyduje, czy watchdog pracuje gdy CPU jest uśpiony, czy nie pracuje.

Nawet jeśli obieg pętli głównej trwa krócej niż czas odliczania CPU, polecenie delay jest niekorzystne, ponieważ w czasie jego wykonywania CPU pracuje zużywając energię. Taką sytuację określa się powiedzeniem „para idzie w gwizdek”.

Co więc zrobić? Rozwiązanie jest banalnie proste i likwiduje odwieczny problem początkujących programistów – jak migać dwoma diodami z różną częstotliwością? Rozwiązanie poniżej:

//---- – Dwie biblioteki, o których zapominają „Arduinowcy” !!!!!
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/atomic.h>
//=========================================//
#define LED_RUN   8
#define LED_ST     9
//=========================================//
void usypianie_i_wdg() {
  wdt_reset();    // Resetujemy Watchdog
  //---- – Usypiamy mikrokontroler, co zmniejsza pobór prądu i zakłócenia EMI !!!!! – ----//
  /×
      #define SLEEP_MODE_IDLE         0
    #define SLEEP_MODE_PWR_DOWN     1
    #define SLEEP_MODE_PWR_SAVE     2
    #define SLEEP_MODE_ADC          3
    #define SLEEP_MODE_STANDBY      4
    #define SLEEP_MODE_EXT_STANDBY  5
  ×/
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    //cli();    // „ATOMIC_BLOCK” wyłącza IRQ
    set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here
    sei();          // Aby wybudzić CPU muszą być włączone przerwania
    //sleep_mode();     // Makro robi funkcjonalnie to co poniżej
    sleep_enable(); // enables the sleep bit in the mcucr register
    sleep_cpu();
    sleep_disable();
  }
}
//=========================================//
void setup() {
  /×
    15 mS                           WDTO_15 MS
    30 mS                           WDTO_30 MS
    60 mS                           WDTO_60 MS
    120 mS                          WDTO_120 MS
    250 mS                          WDTO_250 MS
    500 mS                          WDTO_500 MS
    1 S                             WDTO_1 S
    2 S                             WDTO_2 S
    4 S                             WDTO_4 S
    8 S                             WDTO_8 S
  ×/
  wdt_enable(WDTO_500 MS);     // Włączamy Watchdog. Monzna też na stałe włączyć w FUSES
  //....
  wdt_enable(WDTO_120 MS);     // jeśli to konieczne zmieniamy czas
}
//=========================================//
void loop() {
  uint32_t static timZad1, timZad2;
  usypianie_i_wdg();
  if ( millis() > timZad1) {     // Nie uzywamy „delay” !!!!!
    timZad1 = millis() + 1000;   // Używamy „millis()” !!!!!
    if ( digitalRead(LED_RUN) ) digitalWrite(LED_RUN, LOW); else digitalWrite(LED_RUN, HIGH );        // Dioda miga z częstotliwością ok 0,5 Hz
  }
  if ( millis() > timZad2) {     // Nie uzywamy „delay” !!!!!
    timZad2 = millis() + 333 / 2;   // Używamy „millis()” !!!!!
    if ( digitalRead(LED_ST) ) digitalWrite(LED_ST, LOW); else digitalWrite(LED_ST, HIGH );        // Dioda miga z częstotliwością ok 3,3 Hz
  }
  //.....
}
//=========================================//

Wykorzystuje ono zamiast delay timer systemowy odczytywany funkcją millis(). Funkcja ta zwraca liczbę milisekund (typ unsigned long – 32-bitowa zmienna bez znaku) jaka upłynęła od chwili startu mikrokontrolera.

Program ten ma poważną wadę, będzie działał poprawnie niecałe 50 dni. Po tym czasie licznik wykorzystywany przez funkcję

 millis()

przepełni się i kolejne zadziałanie warunku

„if"

nastąpi nie po zadanym czasie (sekunda czy 166 ms) ale po blisko 50 dniach. Problem można rozwiązać łatwo, ale o tym w kolejnej części artykułu.

Jak usunąć niedogodność 50 dni? Wydaje się, że najłatwiej w

millis()

użyć zmiennej 64-bit. Wtedy przepełnienie nastąpi po 584942417 latach czyli z naszego punktu widzenia nigdy. Niestety w arduinowskich bibliotekach bardzo rzadko funkcje są deklarowane z atrybutem

„weak”,

co umożliwia ich modyfikację. Opcją byłoby uruchomienie przerwań 1 ms i napisanie własnej funkcji równoważnej do

millis()

ze zmiennymi 64-bit. Ale znam lepsze rozwiązanie pokazane w „szkic_wielowatkowosci_z_timer1”. Zanim powstał ten kod sprawdziłem efekt działania programu:

 void setup() {

Timer1.initialize(1000); // IQR co 1000us = 1ms

Timer1.attachInterrupt(blinkLED); // collback do funkcji w przerwaniu

Serial.begin(115200);

}


// collback

void blinkLED(void) {

uint16_t static d = 1;

if ( --d == 0 ) {

d = 1000;


Serial.println(“IRQ”);

}

}


void loop(){

}
Timet1.initialize(1000); 
ustawia odliczanie timera na 1000 us czyli 1 ms.
Timer1.attachInterrupt(blinkLED);

ustawia adres funkcji wywoływanej w przerwaniu od przepełnienia timera.

W funkcji

blinkLED()

odliczam od 1000 do 0 po czym wysyłam komunikat po UART. Niestety Serial monitor pokazał, że przerwanie nie jest wywoływane co sekundę – rysunek 1, o czym świadczy fakt, że ulegają zmianie tysięczne części sekund.

Rysunek 1

Z pewnością wielu programistów zwróci mi uwagę, że nie powinno używać się serial.print w przerwaniu. Mają rację ale zapominają, że dane dla UART są buforowane i jeśli bufor nie jest przepełniony (64 bajty dla AVR 128 dla ESP8266) to funkcja Serial.print nie zajmuje wiele czasu bo wypełnia tylko bufor. Zmodyfikowałem kod tak, aby zmieniając stan pinu 7, do którego podłączyłem licznik czasu, mierzyć okres generowanych przerwań:

void setup() {
  Timer1.initialize(1000);
  Timer1.attachInterrupt(blinkLED);
  Serial.begin(115200);
  pinMode(7, OUTPUT);	// Inicjalizacja GPIO
}
void blinkLED(void) {
  uint16_t static d = 1;
  if ( --d == 0 ) {
    d = 1000;
    Serial.println(“IRQ”);
    if ( digitalRead(7) ) digitalWrite(7, LOW); else digitalWrite(7, HIGH );   // zmiana stanu GPIO
  }
}

Okazało się, że okres przebiegu wynosi spodziewane 2 sekundy – fotografia 2.

Fotografia 2

To nie pierwszy raz, gdy arduinowski monitor portu szeregowego „wpuścił mnie w maliny”. Finalny kod wygląda tak:

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/atomic.h>
#include <TimerOne.h>
//===============================//
#define LED_RUN   8
#define LED_ST     9
void initIrq1ms() {
  Timer1.initialize(1000);
  Timer1.attachInterrupt(irq1ms);
}
uint16_t volatile timZad1, timZad2;
void irq1ms(void) {
  if ( timZad1 ) timZad1--;
  if ( timZad2 ) timZad2--;
}
//====================================//
void usypianie_i_wdg() {
  wdt_reset();    // Resetujemy Watchdog
  //----- Usypiamy mikrokontroler, co zmniejsza pobór pradu i zakłócenia EMI !!!!! -----//
  /*
      #define SLEEP_MODE_IDLE         0
    #define SLEEP_MODE_PWR_DOWN     1
    #define SLEEP_MODE_PWR_SAVE     2
    #define SLEEP_MODE_ADC          3
    #define SLEEP_MODE_STANDBY      4
    #define SLEEP_MODE_EXT_STANDBY  5
  */
  ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
    //cli();    // “ATOMIC_BLOCK” wyłącza IRQ
    set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here
    sei();          // Aby wybudzić CPU muszą być włączone przerwania
    //sleep_mode();     // Makro robi funkcjonalnie to co ponizej
    sleep_enable(); // enables the sleep bit in the mcucr register
    sleep_cpu();
    sleep_disable();
  }
}
//===========================================//
void setup() {
  wdt_enable(WDTO_500MS);     // Włączamy Watchdog. Mozna też na stałe włączyć w FUSES
  //....
  wdt_enable(WDTO_120MS);     // jeśli to konieczne zmieniamy czas
  initIrq1ms();
  Serial.begin(115200);
  Serial.println(“***** Start*****”);
}
//===========================================//
void loop() {
  usypianie_i_wdg();
  if ( ! timZad1) {
    timZad1 = 1000;
    if ( digitalRead(LED_RUN) ) digitalWrite(LED_RUN, LOW); else digitalWrite(LED_RUN, HIGH );        // Dioda miga z częstotliwością ok 0,5Hz
  }
  if ( ! timZad2 ) {
    timZad2 = 333 / 2;
    if ( digitalRead(LED_ST) ) digitalWrite(LED_ST, LOW); else digitalWrite(LED_ST, HIGH );        // Dioda miga z częstotliwością ok 3,3Hz
  }
  //.....
}

Co się zmieniło w stosunku do kodu z

millis() ?

Zmienne

 timZad1

i

timZad2

mają rozmiar – 16 a nie 32-bitów. Przy większej liczbie takich zmiennych daje to dużą, dla AVR, oszczędność pamięci RAM. Jeszcze większa jest oszczędność, gdy odliczany czas nie przekracza 255 ms, wtedy zmienna zajmuje 8-bit a nie 32 jak w przypadku rozwiązania z

millis().

Zmienne są zmniejszane w przerwaniu co 1 ms, po doliczeniu do zera odliczanie jest zatrzymywane, więc nie ma tu problemu przepełnienia. Wadą rozwiązania jest wykorzystanie timera1, a w AVR jest ich bardzo mało. Tu uwidacznia się przewaga ARM. Mają one zdecydowanie więcej timerów, ponadto rdzeń zawiera jeden służący do odliczania czasu systemowego. Ponadto biblioteki STM32 umożliwiają „przejęcie” przerwania systemowego, o czym twórcy bibliotek dla Arduino zapomnieli.

Czy nie ma żadnej możliwości, aby nie marnować drogocennego timera? Jest. Rozwiązanie problemu poniżej:

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/atomic.h>
#include <TimerOne.h>
#define TICKS_MS(x)   (x*1000UL/1024UL)
//===========================================//
#define LED_RUN   8
#define LED_ST     9
void initIrq1ms() {
#if defined(TIMSK) && defined(OCIEA)
  TIMSK |= 1 << OCIEA;
#elif defined(TIMSK0) && defined(OCIE0A)
  TIMSK0 |= 1 << OCIE0A;
#else
#error  Timer 0 COMPA interrupt not set correctly
#endif
}
uint16_t volatile timZad1, timZad2;
#if defined(TIM0_COMPA_vect)
ISR(TIM0_COMPA_vect)
#else
ISR(TIMER0_COMPA_vect)
#endif
// przerwanie co 1024us!
{
  if ( timZad1 ) timZad1--;
  if ( timZad2 ) timZad2--;
}
//===========================================//
void usypianie_i_wdg() {
  wdt_reset();    // Resetujemy Watchdog
//----- Usypiamy mikrokontroler, co zmniejsza pobór pradu i zakłócenia EMI !!!!! -----//
/*
#define SLEEP_MODE_IDLE 0
#define SLEEP_MODE_PWR_DOWN 1
#define SLEEP_MODE_PWR_SAVE 2
#define SLEEP_MODE_ADC 3
#define SLEEP_MODE_STANDBY 4
#define SLEEP_MODE_EXT_STANDBY 5
*/
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
//cli(); // “ATOMIC_BLOCK” wyłącza IRQ
set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here
sei(); // Aby wybudzić CPU muszą być włączone przerwania
//sleep_mode(); // Makro robi funkcjonalnie to co ponizej
sleep_enable(); // enables the sleep bit in the mcucr register
sleep_cpu();
sleep_disable();
}
}
//===========================================//
void setup() {
  wdt_enable(WDTO_500MS);     // Włączamy Watchdog. Monzna też na stałe włączyć w FUSES
  //....
  wdt_enable(WDTO_120MS);     // jeśli to konieczne zmieniamy czas
  initIrq1ms();
  Serial.begin(115200);
  Serial.println(“***** Start*****”);
}
//===========================================//
void loop() {
  usypianie_i_wdg();
  if ( ! timZad1) {
    timZad1 = TICKS_MS(1000);
    //char txt[20];
    //sprintf(txt, “tim=%d %d”, timZad1, TICKS_MS(1000) );
    //Serial.println(txt);
    if ( digitalRead(LED_RUN) ) digitalWrite(LED_RUN, LOW); else digitalWrite(LED_RUN, HIGH );        // Dioda miga z częstotliwością ok 0,5Hz
  }
  if ( ! timZad2 ) {
    timZad2 = TICKS_MS(333 / 2);
    if ( digitalRead(LED_ST) ) digitalWrite(LED_ST, LOW); else digitalWrite(LED_ST, HIGH );        // Dioda miga z częstotliwością ok 3,3Hz
  }
  //.....
}

Co się zmieniło?
W

initIrq1ms()

włączane są przerwania od porównania komparatora A. Obsługiwany jest wektor przerwania od komparatora A:

ISR(TIM0_COMPA_vect) lub ISR(TIMER0_COMPA_vect)

Ze względu na to, że przerwania następują co 1024 us nie można wpisywać bezpośrednio czasu w milisekundach do zmiennych czas trzeba przeliczyć, co robi makro:

#define TICKS_MS(x)   (x*1000UL/1024UL)

Dla dociekliwych

Pominąłem milczeniem odczyt zmiennych volatile o rozmiarze większym niż 8-bit w programie głównym Wydaje się, że musi być to wykonane przy wyłączonych przerwaniach. Otóż nie w przypadku porównywania z wartością zero. Rozwinę to zagadnienie. Gdy zmienne o rozmiarze 16 albo 32-bit jest zmieniania w przerwaniu może się zdarzyć, że gdy program główny czyta jej wartość odczyta młodszy bajt (przypuśćmy 0xFF) i teraz nastąpi przerwanie. Zmienna, która miała wartość przykładowo 0x00FF, w przerwaniu zostanie zwiększona o jeden na 0x0100. CPU wyjdzie z przerwania, program główny odczyta starszy bajt 0x01. W konsekwencji otrzyma 0x01FF zamiast 0x00FF ewentualnie 0x0100. Dlatego odczyt takich zmiennych (zapis często też) trzeba wykonywać przy wyłączonych przerwaniach. Inaczej jest przy porównywaniu z wartością 0, bo jeśli nawet program główny zacznie odczyt wartości 0x0100, która w przerwaniu zmieni się na 0x00FF to w konsekwencji odczytana będzie wartość 0x01FF, która nie jest zerem.

Epilog

Napisanie programów korzystających z wirtualnych timerów przypomniało mi, jak trudno pisze się programy bez debugera, przez co nie można zatrzymać programu, podejrzeć stanu zmiennej ani sprawdzić czy kod wykonał określoną funkcję. Pewne rzeczy można zrealizować wysyłając dane na UART, ale wymaga to zmian w programie, ponownej kompilacji i programowania mikrokontrolera, co jest dosyć uciążliwe.

SaS, ZE

sas.ze@vp.pl