Obiektowość na AVR od podstaw 4 z n...

Tu poruszamy tematy związane z pisaniem programów w języku C++ dla AVR.
Awatar użytkownika
mokrowski
User
User
Posty: 190
Rejestracja: czwartek 08 paź 2015, 20:50
Lokalizacja: Tam gdzie Centymetro

Obiektowość na AVR od podstaw 4 z n...

Postautor: mokrowski » sobota 24 cze 2017, 04:24

Tym razem zanim zaczniemy, będę Cię prosił o podłączenie 8 led'ów do portu I/O. U mnie będzie to port PORTC. Będziemy potrzebowali informacji zwrotnej z działającego kodu. W założeniach tutorial ma także być dostępny dla „stykówkowców”. Dziś wielki dzień! Będziemy zajmowali się relacjami pokrewieństwa w kodzie!

Ach .. byłbym zapomniał o kodzie UsartImpl. Hmm... jakoś nikt o niego nie zapytał to znaczy że jest trywialny. Pewnie tak. W takim razie opublikuję go później :-) Ok :-)
Wróćmy więc do głównego wątku....

Zacznijmy od prostej zawartości main.cpp. Projekt w C++ w eclipse myślę że potrafisz już założyć Sam/a :-) Przypominam jedynie o polecanych przełącznikach do kompilatora: -std=c++11 -Wextra -pedantic -g. Jeśli ktoś ma nieco starszy toolchain, to zamiast c++11 niech wpisze c++0x.

Tworzymy 2 pliki na razie testowo aby sprawdzić czy sprzęt działa, zanim przejdziemy do pracy z kodem.
main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   // Full out :-)
   PORTC = 0xFF;

   while(true);
}

MyClass.hpp:

Kod: Zaznacz cały

#ifndef MYCLASS_HPP_
#define MYCLASS_HPP_



#endif /* MYCLASS_HPP_ */

Skompiluj projekt i go wgraj/uruchom na MCU. Ci którzy pracują na zestawach ATB niech pamiętają o zanegowaniu wyjścia portu. Było to tyle razy poruszane przez Mirka że tu wyłącznie o tym wspomnę. Po uruchomieniu kodu, wszystkie diody mają się zapalić.

Będziemy tworzyli klasę Migacz, która będzie podstawą do działań z kodem obiektowym. Klasa ta będzie posiadała tylko jedną metodę void migaj() const; .

No to szybko do kodu. Po poprzednich częściach nie będzie chyba pytań.

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   Migacz myMigacz = Migacz();

   myMigacz.migaj();
}


MyClass.hpp:

Kod: Zaznacz cały

#ifndef MYCLASS_HPP_
#define MYCLASS_HPP_

#include <avr/io.h>
#include "util/delay.h"

class Migacz {
public:
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
};

#endif /* MYCLASS_HPP_ */

Co do zawartości kodu, nic specjalnego. Mruganie diodą nr 7 na porcie. Tak, o to chodziło.

Chcemy stworzyć teraz klasę o nazwie MigaczSzybki która będzie posiadała metodę void migajSzybko() const; . Tu dioda nr 7 będzie zapalana i gaszona co 100 ms. Dodajemy więc definicję klasy MigaczSzybki do pliku MyClass.hpp pod definicją klasy Migacz. Tym razem będziemy migali 6 diodą.

MyClass.hpp:

Kod: Zaznacz cały

...
class MigaczSzybki {
public:
   void migajSzybko() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(100);
      }
   }
};
...

Zmieniamy także kod w main.cpp na wywołanie MigaczSzybki i wywołanie metody migajSzybko(). Tu podam kod informacyjnie mając nadzieję że już następne wpisy będziesz realizował sam/sama.

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki myMigacz = MigaczSzybki();

   myMigacz.migajSzybko();

}

Jak widać po samych nazwach klas, MigaczSzybki jest rodzajem Migacza. Relacja takiego pokrewieństwa nazywana jest dziedziczeniem. Tak jak w życiu. Rodząc się, dziedziczysz zachowania (metody) oraz atrybuty od rodziców (co bywa nieszczęściem bo nos „Chopina” po ojcu i „romantyczna egzaltacja” po matce a „uszy po dziadku” :-) ) Sprawdźmy jak to może wyglądać w kodzie. Relacja dziedziczenia gdzie rodzicem będzie Migacz a dzieckiem MigaczSzybki zrealizujemy dodając do klasy MigaczSzybki taki oto zapis:

Kod: Zaznacz cały

...
class MigaczSzybki : public Migacz {
public:
...

Jest to relacja dziedziczenia publicznego (o rodzajach dziedziczenia nieco później). Oznacza to że teraz MigaczSzybki jest rodzajem Migacza i posiada możliwość wywołania metody migaj() (z racji pokrewieństwa) oraz migajSzybko() z racji zaimplementowania jej we własnej klasie. Sprawdzisz to? Popraw w main.cpp wywołanie z migajSzybko() na migaj(). No! :-) Działa! migaj() z obiektu typu MigaczSzybki miga tak jak w rodzicu a migajSzybko() miga tak jak w implementacji MigaczSzybki.

Proszę poeksperymentuj teraz z kodem i zaobserwuj czy wielkość wsadu wzrasta. Nie? No a dlaczego miała by wzrastać jeśli kompilator kompiluje jedynie kod naprawdę używany?

Złóżmy że teraz chciałbym zaimplementować własną metodę migaj() w klasie MigajSzybko i „przykryć” (ang. override) tę z rodzica. Nie chcę mieć migania 7 diodą (licząc od 0) a chciałbym 6. Nic prostszego powiesz i szybko napiszesz taki kod:

Kod: Zaznacz cały

...
class MigaczSzybki : public Migacz {
public:
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(500);
      }
   }
   void migajSzybko() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(100);
      }
   }
};
...

Oczywiście w main.cpp poprawisz wywołanie na:

Kod: Zaznacz cały

...
myMigacz.migaj();
...

Kompilujesz, uruchamiasz i działa :-) 6 dioda miga z częstotliwością ~ 2 x na sekundę. Hmm.. do czego się to może przydać te całe dziedziczenie? Wyobraź sobie że masz zaimplementowaną klasę obsługującą pamięć EEPROM w twoim MCU oraz niesamowicie skomplikowany program obsługujący urządzenia zewnętrzne. Dużo kodu, wiele testów i kod działa. Nagle chcesz podłączyć EEPROM na I2C bo w tym w systemie w samym MCU już się dane nie mieszczą. Piszesz więc specyficzne dla obsługi zapisów/odczytów metody które używają szyny I2C do obsługi Eeprom'u i dziedziczysz od klasy Eeprom którą już tworzyłeś/tworzyłaś dla MCU.

Twoja specjalizowana klasa Eeprom'u na I2C zachowuje się i jest Eeeprom'em czyli klasą (inaczej typem) już stworzonym! Cały kod który powstał a korzystał z pierwotnej klasy Eeeprom nie jest zmieniany! W ani jednym znaku! To jest wygoda którą oferuje podejście obiektowe. Związana jest ona z hermetyzacją kodu czyli postawieniem granicy pomiędzy jego częściami tak aby mieć kontrolę nad komunikacją oraz zdefiniowaniem sposobu komunikacji czyli wywołań metod. Metody definiując atrybuty które przyjmują oraz dane które zwracają tworzą kontrakt który ustala jak obiekt obsługiwać. Tą granicą która daje Ci kontrolę w podejściu obiektowym jest (między innymi) klasa.

No to sprawdźmy czy rzeczywiście MigaczSzybki jest Migaczem :-)

Kod który wyglądał pierwotnie tak:

main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki myMigacz = MigaczSzybki();

   myMigacz.migaj();

}

MyClass.hpp:

Kod: Zaznacz cały

#ifndef MYCLASS_HPP_
#define MYCLASS_HPP_

#include <avr/io.h>
#include "util/delay.h"

class Migacz {
public:
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN7);
         _delay_ms(500);
      }
   }
};

class MigaczSzybki : public Migacz {
public:
   void migaj() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(500);
      }
   }
   void migajSzybko() const {
      while(true) {
         PORTC ^= (1 << PIN6);
         _delay_ms(100);
      }
   }
};

#endif /* MYCLASS_HPP_ */

Zmieniamy w main.cpp na:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   Migacz myMigacz = MigaczSzybki();

   myMigacz.migaj();

}

Zwróć uwagę. Zamiast typu MigaczSzybki (poprzednio) jest Migacz. Pomimo to kompilator nie zgłosił błędów niezgodności typów bo MigaczSzybki który jest dzieckiem Migacz'a jest Migaczem :-) uff... (tak... pewnie każdy ma w rodzinie „Migacza” ale nie ma on nic wspólnego z C++ :-) )

Pójdźmy jeszcze dalej. Możemy powołać zmienną typu Migacz i przypisać do niej kod MigaczSzybki:
main.cpp:

Kod: Zaznacz cały

#include <avr/io.h>
#include "MyClass.hpp"

int main(void) {
   // PortC out dir...
   DDRC = 0xFF;

   MigaczSzybki myMigacz = MigaczSzybki();
   Migacz myMigacz2 = myMigacz;

   // Tu zamieniaj myMigacz.migaj() na myMigacz2.migaj()
   // ... a także metodę migaj() na migajSzybko()
   myMigacz.migajSzybko();

}

Proszę wykonaj teraz ćwiczenie wpisane w komentarzu. Tak, wszystkie 4 kombinacje! Nie w każdym przypadku kod działa tak jak mogło by Ci się wydawać. W jednej z nich będzie błąd i problem z kompilacją a w kilku się zdziwisz wynikami. Niemniej jednak zapisz na kartce proszę który kod przy jakim wywołaniu się uruchamiał. Czy ten z Migacza czy ten z MigaczSzybki. Zrób to bo jeśli sobie odpuścisz dalej to „popłyniesz” i nie zrozumiesz obiektowości (wiem z doświadczenia). To do tej pory jedyne miejsce w którym bardzo proszę o samodzielną pracę bo doświadczenie w tym przypadku spowoduje że zrozumiesz.

Zwróć uwagę że przy zdefiniowaniu obiektu myMigacz jako typ Migacz,

Kod: Zaznacz cały

...
   Migacz myMigacz = MigaczSzybki(); // Tak, do typu Migacz przypisujemy MigaczSzybki
...

... jeśli uruchomisz myMigacz.migaj(), uruchamiany jest kod Migacz::migaj(). Takie masz wnioski z ćwiczenia? Tak wiem że jest to zaskoczenie ale to prawidłowe zachowanie w C++.

Wyobraź sobie że zestaw metod w danej klasie jest tablicą wskaźników na nie. W momencie kreowania obiektu, tablica ta jest wypełniana tak jak to określa typ (czyli klasa) danej. Próbując (to będzie nieudane) wywołać myMigacz2.migajSzybko() otrzymujesz komunikat kompilatora to „nie ma takiej metody”. Jak to? Przecież to ma być MigaczSzybki a on ma migajSzybko()! Nie... to ma być Migacz a on nie ma migajSzybko() :-)

Spokojnie, wytłumaczę to.

Standard języka C++ nie narzuca producentowi/autorowi kompilatora sposobu implementacji mechanizmów wewnętrznych kompilacji. Zakłada że autorzy wiedzą wszystko o danej platformie systemowej i implementują kompilator maksymalnie wydajny i szybki. Niemniej jednak większość kompilatorów obecnych w tej chwili na rynku, ma implementację która działa w taki sposób jak to opiszę. Wspominam o tym dlatego że za chwilę możesz sprawdzić że szczegóły różnią się np. w kompilatorach MS czy na innych mniej popularnych platformach. Nie kamienować mnie proszę bo standard wyraźnie mówi „zrób tak by było wydajnie i szybko i ma działać tak jak to jest opisane a jak to zrobisz w szczegółach to nas nie obchodzi”.

W trakcie powoływania do życia obiektu instancjonowanego z danej klasy, każda z metod otrzymuje liczbę porządkową w tablicy wskaźników na nie. Dostaje te indeksy w kolejności wystąpienia w danej klasie a jeśli dziedziczy i nie posiada danej metody, to kolejność jest uzupełniana na te które występują w rodzicu. Czyli np. w naszym obiekcie typu Migacz pod indeksem 0 będzie migaj() z klasy Migacz. Z kolei w obiekcie MigaczSzybki, pod indeksem 0 będzie jego migaj() a pod indeksem 1 będzie wskaźnik na migajSzybko(). Czy to jest zrozumiałe?

Przez chwilę wykonaj malutki 1 kroczek w tył.

Załóżmy że w MigaczSzybki była tylko metoda migajSzybko() i nie było „jego własnej” metody migaj() (był przez chwilę taki moment w kodzie) . Jeśli kompilator tworzył obiekt z MigaczSzybki a on dziedziczył z Migacz i przypisywał ten obiekt do obiektu typu MigaczSzybki, przeglądał najpierw Migacz, tam widział metodę migaj() której nie było w MigaczSzybki a następnie widział metodę migajSzybko() z MigaczSzybki. Metody które posiadał MigaczSzybki były więc uzupełnione tak: 0 – migaj() z Migacz, 1 – migajSzybko() z MigaczSzybki.

Jeśli jednak tworzyliśmy MigaczSzybki i przypisywaliśmy go do obiektu typu Migacz (czyli rodzica), kompilator najpierw przeglądał listę z rodzica (czyli Migacz) i tam trafiał na migaj().

Jeśli to jest niezrozumiałe, zatrzymaj się, weź kartkę narysuj to sobie. Ja z premedytacją tego nie rysuję nie dlatego że jestem leniwy (inaczej bym tych tutorliali nie pisał) tylko chcę abyś Sam/Sama to zrobiła/zrobił. To ważne!

Zrozumiałe? Ok, to wracamy do głównego wątku.

Kompilator także przetrzymuje informację ile takich wpisów przy każdej z klas trzymać. Tak więc w Migacz jest to informacja „trzymaj wskaźnik na 1 metodę pod indeksem 0” a w MigaczSzybki jest to informacja „trzymaj wskaźniki na 2 metody pod indeksami 0 i 1”. Tak, taka prosta znana już Ci tablica z C :-)

No, a klasa to przecież struktura (odsyłam do wcześniejszych części tego cyklu). Można na niej wywołać sizeof( ...). Co się więc dzieje jeśli przypiszesz do zmiennej typu Migacz obiekt z klasy MigaczSzybki? Pod indeks 0 trafi migaj() z Migacz a pod indeks 1... zaraz.. jaki indeks 1? Migacz nie ma żadnego indeksu 1! Kompilator wie że Migacz ma mieć 1 metodę a nie dwie. Więc metoda migajSzybko() niestety ale nie zostanie uwzględniona :-/

Reasumując rodzic w relacji pokrewieństwa z zasady w C++ będzie mniejszy „objętościowo” niż dziecko (co zgadza się z intuicją z życia, z zasady nasi rodzice mieli np. mniejszy wzrost niż my, choć ponoć ojcowie mieli 3 razy większy poziom testosteronu :-) ). Nie będzie także posiadał (no bo skąd) metod i atrybutów dziecka.

Taka obsługa mechanizmów obiektowych jaką tu pokazałem bywa nazywana „obiektowością statyczną”. Do jej zalet należy duża prędkość programu wynikowego i mechanizm „składania statycznego” obiektów. Ci którzy programują w języku Java czy C# mogą nawet takiego modelu obiektowego nie doświadczyć bo maszyny wirtualne tych języków w ten sposób nie funkcjonują. No to dlaczego C++ ma takie coś? Powód jest prosty. „Szybkość i jeszcze raz szybkość Misiu” :-) Co do zasady twórca C++ twierdzi:

„W języku C++ mogą być mechanizmy które ułatwiają programowanie. Zawsze jednak kosztują wydajność. Domyślnie takie mechanizmy powinny być wyłączone tak aby programista [i]świadomie mógł je uruchomić jeśli tego potrzebuje” (to jest sedno dłuższej wypowiedzi Stroustrupa)[/i]

Zwróć uwagę, to podejście bardzo różni się od współczesnych języków „ubezwłasnowalniających” (spokojnie ja także programuję w języku Java i Pythonie :-)).

No ale „ja bym chciał/chciała żeby było tak jak w Java” da się tak? Oczywiście że się da :-) O tym w następnym odcinku. Zdradzę jedynie że rzecz dotyczy funkcji wirtualnych :-)

Tu jednak jeszcze nie kończymy. Bez tego okruszka wiedzy, nie poradzimy sobie z kodem złożonym lub rozważaniami o obiektowości. Chcę jeszcze zanotować naszą pracę. Użyję do tego diagramu klas w notacji UML.
Obrazek

Jak widzisz Klasa Migacz jest umieszczona na górze i prowadzi do niej strzałka z grotem w jej kierunku. Strzałka wskazuje więc rodzica.

Początek strzałki wychodzi z MigaczSzybki co oznacza że MigaczSzybki jest dzieckiem (dziedziczy) z klasy nadrzędnej Migacz.

Obydwie klasy posiadają metody. Migacz ma migaj() zwracającą void i widoczną na zewnątrz (publiczną) na co wskazuje plus '+', MigaczSzybki natomiast ma metodę migajSzybko() zwracającą void i także publiczną.

Część pusta w każdej z klas to miejsce na atrybuty. A z racji tego że w naszych klasach jeszcze atrybutów nie ma, to jest pusto :-)

Z relacji pokrewieństwa z diagramu możemy także wyczytać że MigaczSzybki posiada metodę migaj() bo jest rodzajem Migacza. Tak, tak to się czyta.

Tego typu diagramami będę posługiwał się w dalszej części tutoriala. Wiem że mogą być dla Ciebie jeszcze teraz nowością, ale uwierz mi, wyjaśnią wiele jak przejdziemy do bardziej skomplikowanego kodu obiektowego.

Na dziś to koniec. W następnym odcinku czeka nas rozwiązanie zagadki metod wirtualnych oraz dokładniejsze zapoznanie się z dziedziczeniem. Być może tak lotnego obecnie tematu jak „patchwork family” i „geneder” nie poruszymy ale myślę że i tak będzie ciekawie :-)

Porcja wiedzy którą znajdziesz w tych tutorialach do tej pory, wystarcza do tego by zrozumieć całość kodu napisanego w C++ w bibliotekach arduino. Jeśli więc chcesz zobaczyć kod który jest specyficzny dla MCU i jest napisany w C++, to jest to dobie miejsce do własnych studiów. Zanim przeczytasz następną część tutoriala (lub kiedyś do niego wrócisz), zerknij do kodu napisanego obiektowo w bibliotekach arduino.

Dalsze tematy na pewno przydadzą Ci się w programowaniu na systemy głównego nurtu i może na systemach wbudowanych.
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Wróć do „Programowanie AVR w C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 2 gości