Skocz do zawartości

Pomocna odpowiedź

Napisano

Cel artykułu

Czytając dyskusje i tutoriale na forum zauważyłem, że o ile jest wiele dobrych opisów sprzętu (chociażby tutoriale dotyczące procesorów), to brakuje opisów, jak z tych wszystkich informacji złożyć oprogramowanie (docelowo całe urządzenie)  dla złożonego urządzenia:  na przykład kosiarki(robota), automatyki domowej, przyrządu pomiarowego (na przykład woltomierza, amperomierza, generatora sygnałowego, oscyloskopu), odbiornika SDR i tak dalej. Wspólną częścią wszystkich tych (i innych) projektów jest współpraca składowych sprzętowych i komunikacja z otoczeniem - szczególnie na poziomie oprogramowania. Stąd wziął się pomysł artykułu.

Pisząc ten artykuł, chciałbym w maksymalnym stopniu wykorzystać materiały z forum, bo po co pisać o czymś, co jest dobrze opisane gdzie indziej i powszechnie dostępne. Na początek powinienem pisać o budowie i działaniu procesora. Lecz ten temat znajduje się na forum pod adresem https://forbot.pl/blog/mikrokontroler-wszystko-co-powinniscie-wiedziec-o-jego-dzialaniu-id1314 (autor GAndaLF). Zatem zacznę od oprogramowania. Jako środowisko programowania i płytkę będę używał Arduino/ATMega32, gdyż ten układ oprogramowanie/sprzęt jest chyba najbardziej popularny. Moja płytka to Arduino DueMilanove (AtMega32, zasilanie 5V, zegar 16MHz). W środowisku Arduino można programować też inne rodziny procesorów, np STM32, ESP32,czy też RP2040 czy RP2350. We wszystkich przykładach używam wyłącznie funkcji wbudowanych. Zatem wszystko, co napiszę, powinno dać się uruchomić na wszystkich rodzinach procesorów obsługiwanych przez Arduino. 

Pierwszym programem, którym chciałbym się zająć, jest BareMinimum. Kod programu wygląda tak (File\Examples\01.Basics\BareMinimum):

void setup() {
  // put your setup code here, to run once:
}

void loop() {
  // put your main code here, to run repeatedly:
}

Co robi ten program? NIC? Nieprawda. Na mojej płytce marnuje cykle zegarowe procesora z prędkością 16 milionów cykli na sekundę. Poza tym kompiluje się bez błędów i poprawnie wgrywa na płytkę – jest to wymaganie, które MUSI spełnić KAŻDY program. Natomiast faktycznie, ten program nie robi nic użytecznego.

Kolejnym programem jest Blink (File\Examples\01.Basics\Blink).

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output.
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // LED pin setting to the HIGH voltage level
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // LED pin setting to the LOW voltage level
  delay(1000);                      // wait for a second
}

Co robi ten program? W porównaniu z poprzednim coś użytecznęgo (?) – włącza i wyłącza diodę. Również i on marnuje cykle zegarowe z prędkością prawie 16 milionów cykli na sekundę. Do wykonania instrukcji digitalWrite() plus narzut kompilatora potrzeba kilkadziesiąt cykli zegarowych, zaś instrukcja delay() służy do opóźnienia wykonania następnej instrukcji, które osiąga się za pomocą marnowania instrukcji procesora. Poniżej przedstawiam program Blink działający bez użycia instrukcji delay(), w którym opóźnienie zrealizowałem za pomocą pustej pętli.

void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  LED pin set to the HIGH voltage level
  //***** delay(1000);                      // wait for a second
  for (volatile long i = 0; i < 400000; i++) {
    // Puste wnętrze
  }
  digitalWrite(LED_BUILTIN, LOW);   // LED pin set to the LOW voltage level
  //***** delay(1000);                      // wait for a second
  for (volatile long i = 0; i < 400000; i++) {
    // Puste wnętrze
  }
}

Jak widać zamieniłem funkcję biblioteczną delay() na pustą pętlę for() {}. Sposób, w jaki użyłem tę pętlę, wymaga moim zdaniem wyjaśnień

volatile long i: Zmienna i jest licznikiem pętli. Kompilator języka C optymalizuje kod tak, aby kod wykonywał się maksymalnie szybko i zużywał jak najmniej zasobów. Słowo kluczowe volatile to wskazówka dla kompilatora, aby nie optymalizował zmiennej „i” oraz jej wykorzystania. Bez volatile kompilator usunąłby pętlę.

400000: Ilość obiegów pętli, dobrana tak, aby dla procesora o taktowaniu 16 MHz (moja płytka) odliczanie trwało około sekundy. Jedno przejście pętli zajmuje procesorowi kilkadziesiąt (ok. 40) cykli zegara.

W tym zastosowaniu pętla for(){} jest znacznie gorsza od funkcji bibliotecznej delay(). Jest niedokładna (nie wiadomo, ile cykli zegarowych trwa jeden obieg pętli, procesor zużywa ponadto trochę cykli na obsługę przerwań systemowych), liczbę obiegów trzeba dopasowywać dla procesora i zegara. Powyższy przykład zaprezentowałem po to, aby pokazać, że funkcja delay() to po prostu marnowanie mocy obliczeniowej procesora. Można wykorzystać tę moc w bardziej sensowny sposób, na przykład dodając możliwość zmiany częstotliwości migania diody w trakcie działania programu za pomocą polecenia wydanego z zewnętrznej konsoli.

Zanim to zrobię, pokażę, w jaki sposób działa funkcja millis(). Zwraca ona czas, jaki upłynął od momentu włączenia procesora w milisekundach. Podobnie jak funkcja delay(), jest ona dostępna bezpośrednio w kompilatorze (funkcja wbudowana - to znaczy dostępna bez dołączania zewnętrznych bibliotek za pomocą #include). Chcę wyświetlić na konsoli czas wykonania tej pętli. Aby to osiągnąć, wykorzystam kolejne funkcje wbudowane, obsługujące transmisję szeregową: serial.begin()i serial.print(). Środowisko Arduino (ja używam wersję 2.3.10) posiada wbudowany wbudowany monitor portu szeregowego, który podłącza się automatycznie do płytki. Pomysł na następne przykłady zaczerpnąłem z kursu Arduino ( https://forbot.pl/blog/kurs-arduino-ii-wielozadaniowosc-opoznienia-z-millis-id18418 ), który pokazuje sposób na realizowanie wielozadaniowości.

Zatem pierwszą rzeczą, którą chcę zademonstrować, jest pokazanie niestabilności czasu wykonania pętli. Chcę zacząć od nieznacznie zmodyfikowanego poprzedniego programu – usunąłem niepotrzebne linie programu

void setup() {
  pinMode(LED_BUILTIN, OUTPUT); // pin LED_BUILTIN zdefiniowany jako wyjście
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  for (volatile long i = 0; i < 400000; i++) { /* Puste wnętrze */ }

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */}
}

Kolejną modyfikacją jest dodanie obsługi wysyłania komunikatu na konsolę

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"); // test transmisji danych
}

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */}
}

W funkcji setup() włączyłem obsługę portu portu szeregowego i wysłałem na konsolę tekst „Start programu” jako test działania komunikacji. Komunikat ten pojawi się zawsze na początku działania programu, na przykład zaraz po jego wgraniu lub po naciśnięciu przycisku „RESET” na płytce. Ten komunikat pojawi się też po resecie programowym (skok pod adres pamięci 0) lub po zadziałaniu watchdoga (Watchdog Timer – WDT). Watchdog (pies zegarowy) zostanie użyty w jednej z kolejnych części tutoriala.

Kolejną modyfikacją jest wyświetlenie czasu wykonania pętli.

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() {
  long start; // czas na poczatku petli
  int czas;  // roznica czasu: czas na koncu petli - czas na początku
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  start = millis(); //właczenie stopera
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  czas = millis() - start; // odczyt stopera
  Serial.println(czas);   // na konsole 

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  start = millis(); //właczenie stopera
  for (volatile long i = 0; i < 400000; i++)  { /* Puste wnętrze */ }
  czas = millis() - start; // odczyt stopera
  Serial.println(czas);   // na konsole 
}

Powyższy program ma kilka wad. Pierwsza to czas działania pętli. Moja płytka przełączenie diody realizuje w czasie między 879 a 881 ms – zbyt szybko i niestabilnie. O ile średni czas można poprawić zwiększając wartość końcową licznika pętli z 400000 do większej wartości (jakiej? ustalić doświadczalnie? Dla każdej płytki indywidualnie?), Druga to niestabilność odmierzania czasu. Z rozrzutem czasów przełączenia nic w ten sposób nie zrobimy. Radykalnym rozwiązaniem problemu może być zamiana pustej pętli z eksperymentalnie znalezionej ilości obiegów na pętlę odmierzającą czas, wykorzystującą funkcję millis(). Poniżej zamieszczam program

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() {
  long start; // czas na poczatku petli
  int czas;  // roznica czasu: czas na koncu petli - czas na początku
  digitalWrite(LED_BUILTIN, HIGH);  //  pin LED_BUILTIN ustawiony na poziom napięcia HIGH
  start = millis();
  for(;;) {
     czas = millis() - start;
     if(czas >=1000) break;
     }
  Serial.println(czas);

  digitalWrite(LED_BUILTIN, LOW);   //  pin LED_BUILTIN ustawiony na poziom napięcia LOW
  start = millis();
  do  { czas = millis() - start; }
     while(czas < 1000);  
  Serial.println(czas);
}

Powyższy program przełącza diodę dokładnie co 1 sekundę i jest stabilny oraz niewrażliwy na zmianę płytek, w tym częstotliwości ich zegarów. (Odpowiedź na pytanie dlaczego wymaga wglądu w dokumentację i kod inicjujący procesora – jak jest realizowana funkcja millis(). ). Drugą wadą jest powielenie kodu – włączenie diody jest realizowane przez inny fragment kodu niż kod włączający diodę. I najważniejsza wada – powyższy program w dalszym ciągu marnuje cykle zegarowe, tyle że w inny sposób. Poniżej pokazuję skróconą wersję programu, w którym przełączanie diody jest realizowane poza pętlą główną, przez funkcję zewnętrzną. Przy okazji chciałem pokazać, że sposób organizacji pętli nie ma znaczenia. Ważne jest tylko, aby odmierzyła zadaną liczbę milisekund.

void zmien() {
if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
}

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() {
long start; // czas na poczatku petli
int czas; // roznica czasu: czas na koncu petli - czas na początku

zmien();
start = millis();
for(;;)
{
czas = millis() - start;
if(czas >=1000) break;
}
Serial.println(czas);
}

Przy okazji chciałbym tu poruszyć problem przepełnienia funkcji millis(). Konstrukcja

czas = millis() - start

liczy poprawnie różnicę czasu niezależnie od tego, czy w międzyczasie nastąpiło przepełnienie w funkcji millis(), czy też nie. Jest to związane z faktem, że obliczenia całkowitoliczbowe w komputerze wykonywane są dla liczb longint jako modulo(MAXLONG + 1).

W kolejnym kroku zmodyfikuję pętlę główną, która jest wykonywana w nieskończoność. Zawiera ona kolejną pętlę wykonywaną w nieskończoność ( for(;;) ) przerywaną po spełnieniu warunku. Po wyeliminowaniu tej pętli program przyjmuje następującą postać:

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

// zmienne globalne

long PrevLed;

void zmien() {
if(digitalRead(LED_BUILTIN) == HIGH) digitalWrite(LED_BUILTIN, LOW); else digitalWrite(LED_BUILTIN, HIGH);
PrevLed = millis();
}

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ę
PrevLed = millis(); // inicjalizacja 
}

void loop() {
if (millis() - PrevLed >= int(LED_PERIOD) ) zmien();
}

Powyższy program też przełącza diodę co 1 sekundę i niszczy cykle zegarowe w takim samym tempie jak poprzednie. Różnica jest zasadnicza – w tym programie mogę dodać bez żadnych problemów wiele funkcji analogicznych jak zmien(), wykorzystując niszczone dotychczas cykle do wykonania konkretnej pracy i to bez zakłócania migania diody,czyli funkcji zmien(). Można go jeszcze ulepszyć umieszczając proces przełączania diody w funkcji zewnętrznej wywoływanej z pętli głównej. Funkcja zewnętrzna z punktu widzenia pętli głównej staje się zadaniem do wykonania.

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

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

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

W kolejnym programie chciałbym policzyć ilość obiegów funkcji loop() zrealizowanych w czasie 1 sekundy, pozostawiając miganie diody bez zmian. Realizuje to funkcja task_nuda(). Przy okazji zmieniłem nazwę funkcji zmien() na task_dioda().

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

void task_dioda() {
static long PrevLed = 0;  
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 long PrevPrnt = 0;
  static long Licznik = 0;
  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();
}

Po tej modyfikacji program dalej głównie marnuje cykle zegarowe. Ponadto przełącza diodę i dodatkowo liczy, ile razy została wywołana funkcja task_nuda, czyli ile razy została wykonana pętla loop(). Na mojej płytce otrzymuję wyniki w przedziale 186 – 188 tysięcy obiegów pętli loop() na sekundę. Pokazuje to wprost gigantyczne marnotrawstwo mocy obliczeniowej procesora. Przy okazji demonstruje też, jak pisać programy, aby wiele zadań wykonywało się praktycznie równolegle i ile mocy obliczeniowej mamy do dyspozycji.

W tej chwili program blink jest już systemem operacyjnym czasu rzeczywistego pracującym w trybie kooperacji zadań – w ten sposób pracował system operacyjny WINDOWS 3.1. Pętla loop() stała się schedulerem pracującym w trybie round-robin (dookoła Wojtek, w kółko Macieju). Program zajmuje u mnie 2374 bajtów (7%) pamięci programu i 214 bajtów (10%) pamięci operacyjnej. Zadania są wywoływane po upływie zadanego czasu - synchronicznie. Nie ma ograniczeń na długość czasu wykonywania zadania, chociaż w trakcie wykonywania każdego zadania wszystkie pozostałe nie są wykonywane.

W kolejnym odcinku mam zamiar pokazać wywoływanie asynchroniczne zadań (przycisk, lub kawałek przewodu połączony do pinu w trybie wyjścia i przyłączany lub nie do masy). Ponadto zastosowanie watchdoga do resetowania systemu – systemy kooperujące są wrażliwe na zawieszenie - jeżeli jakiekolwiek zadanie nie odda sterowania. Oprócz tego spróbuję dodać prosty interpreter komend pozwalający na programową (z konsoli Arduino) zmianę częstotliwości migania diody.

 

Powyższe napisałem korzystając z informacji uzyskanych z czatów ze sztuczną inteligencją (ChatGPT, Google Gemini).

 

  • Lubię! 1

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