Obiektowość na AVR od podstaw 1 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 1 z n...

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

Celem tego cyklu tutoriali będzie przyjrzenie się aspektom programowania obiektowego na platformie AVR. Nie będę tu odnosił się za każdym razem do aspektów objętości kodu czy wydajności każdej konstrukcji. Ewentualne uwagi w tym względzie zawsze można więc zgłosić ale nie zawsze będę z nimi polemizował bo nie każda konstrukcja w efekcie jest używana praktycznie na MCU :-) Każda z technik ma niszę w swoim stosowaniu i w konkretnym przypadku może być pomocna lub toksyczna. Pamiętaj że dzięki takiemu podejściu uzupełniasz swoją skrzynkę narzędziową o nowe zestawy „kombinerek i śrubokrętów” :-) Chcę abyś poznał/poznała mechanizmy wewnętrzne budowy oprogramowania obiektowego. Oczywiście w podsumowaniu odniosę się do wydajności każdej z konstrukcji i zakresu jej stosowania, choćby w wymiarze objętości kodu jak i szybkości wykonania programu. Myślę jednak że robiąc to za każdym razem pojedynczo będę przynudzał :-) Zaczniemy od totalnych podstaw więc zaawansowanych proszę o wyrozumiałość. Tu chodzi o pokazanie właściwości OOP (ang. Object Oriented Programming) a na drugim miejscu jest specyficzne dla MCU stosowanie kodu.

Postaram się robić uproszczenia w tych miejscach w których jest to niezbędne i wyraźnie o tym mówić. Każde z uproszczeń zobowiązuję się uzupełnić. Jeśli bym tego nie zrobił, pilnuj mnie :-) Jestem tylko człowiekiem. Jednocześnie pamiętaj że to tutorial a nie książka, stąd nie każde zagadnienie uda się poruszyć dogłębnie a z niektórych po prostu zrezygnuję.

No to zaczynamy....

Czym jest programowanie obiektowe? Przede wszystkim jest sposobem myślenia o programie. W podejściu obiektowym myślisz o samodzielnych bytach (obiektach) które komunikując się ze sobą z użyciem wywołań (czyli metod) wykonują jakąś pracę czyli realizują algorytm :-)

W podejściu proceduralnym, znanym Ci pewnie z C, podział programu następował na funkcje. Realizowały one wydzielone/ograniczone prace na podstawie przekazanych/wskazanych argumentów i zwracały/wskazywały wyniki. W podejściu obiektowym funkcje te są umieszczone (agregowane) w ramach klasy, a klasa posiada lub może posiadać także atrybuty opisujące jej stan.

Klasa to połączenie elementów behawioralnych (funkcji nazywanych metodami) z elementami pasywnymi (atrybutami) które opisują jej stan.

Klasa to jednocześnie przepis na tworzenie obiektu. Obiekt jest już konkretnym i unikalnym fragmentem kodu który działa z wypełnionymi atrybutami i działającymi metodami. To z klasy, w procedurze powoływania do życia obiektu (a procedura nazywana jest instancjonowaniem), może powstać obiekt. Do życia obiekt można powołać na różne sposoby. Zawsze jednak jest to pewien rodzaj instancjonowania.
Dobrze, te „okrągłe zdania” opisują o co chodzi. Jednak praktycznie, co to znaczy?

Jeden z przykładów.

Wyobraź sobie że programując na ATmega, będziesz obsługiwał komunikację szeregową. W związku z tym napiszesz kod 1 klasy obsługujący sprzętowe zasoby USART/UART i będziesz chciał powołać do życia obiekt obsługujący konkretnie USART/UART o numerze 0. Powstaje jedynie pytanie jak zrobić tak, aby wspólny kod „z klasy” obsługiwał port 0 lub 1 lub (jeśli są) to następne?

Jednym z rozwiązań (tu przykładowo i pewnie go bym nie wybrał znając mechanizmy języka C++) jest poinstruowanie klasy, w momencie gdy będzie wykonywane kreowanie obiektu, o jaki USART/UART chodzi. Skończy się to przekazaniem w trakcie instancjowania obiektu argumentu z numerem zasobu. Operację kreacji z klasy obiektu wykona specjalizowana metoda nazywana konstruktorem.

Tu pozwolisz że na chwilę się zatrzymamy przyglądając się bardzo istotnemu zagadnieniu. Chcę odnieść się do Twoich doświadczeń w C. W języku C, jeśli będziesz tworzył obsługę USART/UART o numerach 0, 1, ... itd., posłużysz się (zapewne) podziałem na funkcje sendByte(uint8_t data), setBaud(uint16_t baudRate) itp. Będą one „ustawiały” port w dany tryb lub zmieniały jego właściwości. Obsługę następnych USART/UART będziesz pewnie wykonywał z użyciem makr które sprawdzą czy następny port ma być użyty oraz czy jest dostępny na danym MCU a jeśli tak, to makra włączą/zdefiniują kod do użycia przez UART/USART 1 i dalsze. W naiwnym i skrajnym podejściu powstaną funkcje: send1Byte(uint8_t data), set1Baud(uint16_t baudRate) lub podobne dla USART/UART 1. Konsekwencją takiego podejścia może być powstawanie dużej ilości kodu który powinien być wkompilowany we wsad i wiedza o wywołaniach które Ty jako osoba tworząca musisz posiadać używając tego kodu. W podejściu obiektowym wiedzę o tym jak „zrobić obsługę USART/UART 1” przekazujesz w klasie a ona wie „jak zrobić obsługę USART/UART N” i powołać byt (obiekt) do jego obsługi oraz wie czego Ty chcesz od USART'a.

Świadomie więc pozbywasz się kontroli nad tym co w jakim bicie wpisać w jakim rejestrze dla UARTA 1 na ATmega32 przy parzystości.......... itd. Oddajesz tę wiedzę klasie która wie jak to zrobić dla różnych UART'ów i uogólniasz wiedzę na temat obsługi sprzętu. Jednocześnie zmuszasz klasę aby zawsze robiła to w sposób poprawny. Jeśli więc zrobisz to dobrze, to do kodu napisanego w klasach, wracasz o wiele rzadziej niż do poprawek bibliotek w języku C :-) Zmuszasz się do refleksji jak to zrobić nie tylko w kontekście „jak ustawić baudrate”, ale także jak to zrobić w sposób uogólniony obsługę UART'a! Obsługę bardziej zbliżoną do Twoich potrzeb a nie potrzeb sprzętu. Ten aspekt jest mniej obecny w C. Powiedziałem mniej, co nie znaczy że „w C się nie da” (krzyżowcy schować broń, to nie święta wojna)!

Wracamy jednak do zagadnień obiektowych.

Wspomnieliśmy o konstruktorach, stwórzmy więc klasę ogólnego USART'a. W C++ możemy to zrobić na 2 sposoby:
1. Tworząc klasę z użyciem struktury
2. Tworząc klasę z użyciem ... klasy :-)

Chodzi oczywiście o słowa struct i class. Klasa tworzona z użyciem struktury, będzie miała wszystkie elementy dostępne z zewnątrz. Łączy ona jedynie we wspólnym pojemniku atrybuty i metody (czyli funkcje). Działa tak jak struct w C.

Klasa tworzona z użyciem class, będzie wymagała podania co jest dostępne publicznie a co będzie prywatne dla danej klasy (tu jest świadome uproszczenie bo są jeszcze inne sposoby widoczności o których obiecuję napisać). Domyślnie w class, mamy widoczność prywatną, co oznacza że do metod i atrybutów nie dostaniemy się z zewnątrz.

No a w kodzie jak to wygląda?

Kod: Zaznacz cały

struct Usart {
   // Tu jest ciało klasy o nazwie Usart widoczne dla wszystkich
};

A dla class:

Kod: Zaznacz cały

class Usart {
   // Tu jest ciało klasy o nazwie Usart do użytku prywatnego bo nie ma public: przed
public:
   // Tu jest ciało klasy o nazwie Usart widoczne dla wszystkich bo jest public:
private:
   // Tu jest ciało klasy o nazwie Usart do użytku prywatnego bo wcześniej było public: ale
   // private: ustawiło widoczność prywatną.
};

Zwróć uwagę na średnik na końcu deklaracji. Za każdym razem jest to nazwana (u nas Usart) struktura, dlatego nie zapominaj o średniku.
Oczywiście obowiązują zasady już poznane z języka C co do definicji struktur. Możesz jednocześnie powołać do życia obiekt czyli go zadeklarować :-) Jak? Ano tak!

Kod: Zaznacz cały

struct Usart {
...
} obiektUsart;

lub:

Kod: Zaznacz cały

class Usart {
...
} obiektUsart;

Definiowanie nowego typu także w C++ funkcjonuje. Tu tworzysz typ przez typedef:

Kod: Zaznacz cały

typedef struct Usart {
...
} nowyTyp;

lub:

Kod: Zaznacz cały

typedef class Usart {
...
} nowyTyp;

Poruszałem wyżej temat konstruktora. Konstruktor deklarowany jest z taką samą nazwą jak klasa. Jeśli nie podasz argumentów konstruktora, będzie on zastępował konstruktor domyślny. Jeśli podasz, będzie konstruktorem z argumentem lub argumentami i pozostanie w klasie domyślny. Na tym etapie nie będę już używał struktury bo chcę abyś używał/używała public i private i się do tego przyzwyczaił/a :-)

Oto deklaracja konstruktora bezargumentowego:

Kod: Zaznacz cały

class Usart {
public:
   Usart();
};

Oto definicja konstruktora bezargumentowego:

Kod: Zaznacz cały

class Usart {
public:
   Usart() {
      // Tu jest ciało konstruktora
   }
};

Zasady deklaracja/definicja, są takie same jakie znasz z C.

Taki konstruktor będzie uruchamiany w przypadku użycia:

Kod: Zaznacz cały

...
Usart myUsart;

int main(void) {
...

lub:

Kod: Zaznacz cały

...
Usart myUsart = Usart();
...

lub:

Kod: Zaznacz cały

...
static Usart myUsart;
...

Co ciekawe, jeśli takiego konstruktora nie zdefiniujesz, robi to za Ciebie sam kompilator. Taki konstruktor domyślny jest jednak skrajnie prymitywny i inicjuje/zeruje przestrzeń pamięci przeznaczoną na obiekt co nie zawsze wystarcza.

No to spróbujmy czegoś bardziej skomplikowanego. Chcę aby Usart miał możliwość kreowania domyślnie z ustawieniami 9600 8n1 lub aby była możliwość kreowania go w trybie jawnego podania parametrów komunikacji.
Nic prostszego.

Kod: Zaznacz cały

class Usart {
public:
   Usart() {
      // Tu obsługa ustawienia 9600 i 8n1
   }
   Usart(uint16_t baudRate, uint8_t bits) {
      // Tu obsługa kreowania z ustawieniami. Zakładam że w bits zakoduję 8n1 :-)
   }
};

Teraz można kreować obiekt na 2 sposoby:

Kod: Zaznacz cały

...
Usart myUsart = Usart(); // Będzie to usart 9600 8n1
...

lub:

Kod: Zaznacz cały

...
Usart myUsart = Usart(115200, 0x81); // To jest Usart 115200 8n1
...

W pierwszym przypadku zostanie wybrany konstruktor bezargumentowy, w drugim z argumentami. Można jeszcze prościej? Pewnie że można :-) Jeśli zdefiniujemy konstruktor (uwaga, zwróć uwagę że jeden) tak...

Kod: Zaznacz cały

class Usart {
public:
   Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) {
      ...
   }
};

...to nakazaliśmy użycie argumentów domyślnych o ile nie zostaną podane! Wywołanie więc konstruktora bez parametrów, będzie związane z wywołaniem tego z podanymi parametrami domyślnymi. W takiej klasie nie mam konstruktora bezargumentowego bo gdyby był , a ja wywołał bym tak Usart(), zawsze zostanie wywołany bezargumentowy bo.. lepiej pasuje :-)

Na ile teraz sposobów mogę wywołać konstrukcję obiektu?

Kod: Zaznacz cały

...
Usart myUsart = Usart(); // będzie 9600 8n1
...

lub:

Kod: Zaznacz cały

...
Usart myUsart = Usart(115200); // będzie 115200 8n1 (8n1 jest domyślne :-)
...

lub:

Kod: Zaznacz cały

...
Usart myUsart = Usart(2400, 0x83); // będzie 2400 8p1 (tak zakładam dla 0x83)
...


Wywołanie natomiast takie, jest logicznie błędne:

Kod: Zaznacz cały

...
Usart myUsart = Usart(0x83); // To będzie ustawienie baudrate na 0x83. Czy o to chodziło?
...

Oczywiście jeśli konstruktor Usart będzie wyglądał tak:

Kod: Zaznacz cały

class Usart {
public:
   Usart(uint16_t baudRate, uint8_t bits = 0x83) {
      ....
   }
};

.. to wymagamy wywołania z przynajmniej 1 argumentem:

Kod: Zaznacz cały

...
Usart myUsart = Usart(4800); // usart 4800 z 8n1
...

baudRate jest tu obowiązkowy i wywołanie:

Kod: Zaznacz cały

...
Usart myUsart = Usart();
...

... się ...(a zgadnij)... nie powiedzie. A jak zdefiniujesz konstruktor bezargumentowy, aaa to się powiedzie. To ważna cecha i warto żeby to przećwiczyć. Tu jednak tego nie będę robił i zostawię to Tobie jako pracę domową.
Argumenty domyślne dopasowywane są wg. pozycji wystąpienia. Kompilator nie jest w stanie domyślić się że z 0x83 chodzi Ci o 8 bitów, parzystość i 1 bit stopu a nie baudRate.
Uważny czytelnik zapyta: A gdzie numer USART'a omawiany na początku? Tak pominąłem go :-) Na razie ten aspekt zostawiam i obiecuję do niego wrócić. Wprawdzie nic by nie zaszkodziło gdyby dodać do konstruktora jeszcze jeden argument uint8_t, ale dla jasności jeszcze tego nie uczynię bo tu omawiamy konstrukcję obiektu.

Ok, no to wiemy/zakładamy że Usart ma mieć wiedzę o baudrate i bity-parzystość-stop (zaawansowani, powstrzymać się to tylko przykład). Klasa więc powinna przechowywać wiedzę o tych parametrach w swoich atrybutach.

Dobrze. Czy jednak każdy powinien zmieniać/przestawiać te atrybuty z zewnątrz? Jeśli tak, to atrybuty powinny być w sekcji public, jeśli nie, to w sekcji private (a jak to będzie struct, to nie ma o czym gadać. Tam wszystko jest public :-) ). Ja je zrobię prywatne.

Kod: Zaznacz cały

class Usart {
public:
   Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) {
      // Inicjalizacja atrybutów klasy
      bauds = baudRate;
      bitParity = bits;
      // Tu dalej jest ciało konstruktora...
   }
private:
   uint16_t bauds;
   uint8_t bitParity;
};

Jak widać sekcja private jest po sekcji public i namawiam do tego usilnie by tak to definiować bo jak czytasz kod to interesuje Cię zapewne jak użyć klasy a nie co trzyma jako atrybuty/metody prywatne.

Z premedytacją zainicjowałem także atrybuty w konstruktorze w sposób „niezgrabny”. Tak się już od dawna nie zrobi w konstruktorze. Zamiast tego użyje tzw. listy inicjalizacyjnej. Oto ona:

Kod: Zaznacz cały

class Usart {
   Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81)  : bauds(baudRate), bitParity(bits) {
      // Tu dalej jest ciało konstruktora...
   }
};

W obu przypadkach kod działa w taki sam sposób. Przyznasz jednak że inicjalizacja z listą, jest czytelniejsza. Oczywiście uprzedzając pytania, można połączyć obydwie metody jeśli jest taka potrzeba. Np chcemy wyłuskać jakieś dane z bits (parzystość czy inne... spokojnie to przykład ):

Kod: Zaznacz cały

class Usart {
Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) : bauds(baudRate) {
      // Jakieś operacje na bits.. tu jedynie przykładowe przestawienie nibble i jakieś maskowanie...
      bitParity = ( bits >> 4)  | ( ( bits << 4) & 0x03);
      // Tu dalej jest ciało konstruktora...
   }
private:
   uint16_t bauds;
   uint8_t bitParity;
};

Pozostaje nam tylko zdefiniowanie metod realizujących zadania w klasie. Tu jednak będzie potrzebne podejście inżynierskie. Powinniśmy się przyjrzeć tak naprawdę potrzebom. Czego wymagamy od Usart zanim „bezrefleksyjnie napiszemy kod” :-)

Jeśli uważnie prześledzisz kod z tej części, dojdziesz do wniosku że w zasadzie da się programować i w C obiektowo! Oczywiście że się da. Wystarczy definiować klasy w ramach strktur. Niemniej jednak im więcej poznasz mechanizmów właściwych dla C++, tym bardziej kodowanie obiektowe w ,,surowym C” będzie mniej opłacalne.

Na tym kończę 1 część a na pytanie czy w tym tutorialu powstanie kod „piszący po portach” odpowiem że owszem :-) Teraz jeszcze powstrzymuję się od tego zagadnienia bo obsługa portu szeregowego jest wystarczająco prosta aby każdy jej dotknął a zagadnień podstawowych w obiektowości jeszcze trochę zostało. Zresztą jeśli chcesz to teraz w konstruktorze będziesz samodzielnie w stanie zainicjować Usart. Myślę jednak że roztropnie zrobisz jeśli zaczekasz do dalszych częsci :-) Niecierpliwych zapraszam do poprzednich tutoriali o Random i Adc. Tam są przykłady metod w klasie z obsługą portu.

Z racji tego że nie wiem do końca czy nie zaczynam od zagadnień zbyt podstawowych, proszę kliknij wyżej na ankiecie tak aby dać mi informację czy mam przyśpieszyć czy może przeskoczyć od razu do zagadnień zaawansowanych.
,,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