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

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

Zanim zaczniemy uzupełniać metody w klasie Usart, powinniśmy się zastanowić czego tak naprawdę chcemy od naszego USART'a.

Czy jest to (w pierwszym i bardzo zgrubnym podejściu):
1. Pełna obsługa nadawania znaków, ciągu znaków, ciągu znaków z pamięci Flash – czyli obiekt wyprowadzony z klasy Usart ma posiadać zachowanie zbliżone do potrzeb codziennego stosowania przez programistę.
2. Czy ma to być „cienkie opakowanie” na ustawianie bitów i bajtów w rejestrach portu
3. Czy ma to być klasa służebna do tego aby w przyszłości obsłużyć wysyłanie z użyciem buforowania i innych tego typu sposobów....
4. Inne...

Zanim nie podejmiesz świadomie takiej decyzji, nie pisz kodu (zaraz zobaczysz powód). Zapewne tworząc bibliotekę wybrał bym opcję 2, w innych klasach implementując właściwości wysyłania ciągu znaków czy buforowania. Tu jednak aby zobaczyć w działaniu mechanizmy obiektowe, wybieram z premedytacją opcję 1. Wybrałem tę opcję by zaprezentować pewien błąd koncepcyjny którego konsekwencje pokażę :-)

Przyjmuję więc że tworząc Usart będę od niego wymagał:
1. Możliwości ustawienia baudrate i ilości nadawanych/odbieranych bitów/parzystość/stop w czasie kreowania.
2. Wysłania/odebrania znaku w sposób blokujący – to oznacza że Usart sam sprawdzi czy może wysłać/odebrać znak i wyśle/odbierze
3. Sprawdzenia czy można wysłać/odebrać.
4. Wysłania/odebrania w sposób nieblokujący – wcześniej sprawdzę (pkt 3) i wyślę/odbiorę po sprawdzeniu.

A co z ciągami znaków. Zaraz zaraz.... STOP! Czy USART, z punktu widzenia przeznaczenia obchodzi co przesyła? Czy obchodzi go że ciąg znaków kończy się zerem '\0'? No nie obchodzi. Kiedyś nawet w ramach własnych ćwiczeń napisałem obsługę stringów gdzie 1 bajt to była ilość znaków a string nie kończył się zerem! Dlaczego więc upycham odpowiedzialność za te zagadnienia w USART?!

To jest klasyczny błąd z którym spotkasz się i „wdepniesz w niego” jeszcze nie raz. Sedno problemu to określenie odpowiedzialności klasy za działania. Dzięki temu że nie zaimplementuję obsługi ciągu znaków w USAR'cie, wbrew pozorom polepszę jego reużywalność!

Każda klasa powinna mieć ograniczony zakres odpowiedzialności do poziomu lub zakresu obejmującego powód jej istnienia. W konsekwencji oznacza to także że powinien być 1 i tylko jeden powód jej zmiany.

Drugie zdanie? Powodem zmiany klasy Usart powinny być aspekty sprzętu a nie np. zmiana kodowania znaków!

Klasa (i obiekt z niej wyprowadzony) nie powinien: śpiewać, łączyć z portem szeregowym, badać danych, obsługiwać „stringi”, przytupywać jednocześnie, liczyć CRC, szyfrować dane i je odszyfrowywać. Takie monstrum będzie w przyszłości trudne do opanowania i do ew. usuwania błędów. Ten antywzorzec tworzenia klasy nazywany jest „Klasa Bóg” (ang. Good Class) bo decyduje o wszystkim i trzyma sznurki za odpowiedzialności które nie powinny tej klasy dotyczyć. Ryzykujesz także że otrzymasz kod „wciągnięty we wsad” który wcale nie będzie niezbędny.

Wymagania co do USART'a więc zmienię (to jest obsługa tego błędu koncepcyjnego). Przede wszystkim nie będzie żadnej metody z nazwą char. To powinno być byte. Usart nie wie czy przesyła char a na pewno wysyła/przyjmuje byte. Zastanowię się także czy warto robić transfer danych bo dane są przecież albo z Flash albo z RAM. To tak naprawdę 2 rodzaje danych, a co USART „sam z siebie” wie o rodzajach danych? Nic... Ba, a jak będę chciał z Eepromu? Co USART'a to obchodzi? Tego nie będzie w klasie...

Zwróć uwagę że to podejście różni programowanie obiektowe w C++ od C. W rzetelnie zaprojektowanym oprogramowaniu w C++, nie może pojawić się „obsługa stringów z Flash” w klasie Usart. To są inne aspekty zastosowań USART'a. Obsługa takich „stringów” to oddzielna klasa/rodzina klas. To nie oznacza że w „C jest źle”. To oznacza że pokazuję Ci konsekwencje stosowania każdego z podejść (proceduralne vide obiektowe). Każde ma wady i zalety :-) Ja także nie wstrzymuję się przed stosowaniem danej techniki jeśli wiem jakie koszta ze sobą niesie i jestem w stanie je zaakceptować.

Dobrze, ale wróćmy do Usart'a. Po analizie myślę że klasa może mieć takie metody:

Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) – konstruktor inicjujący port z domyślnymi ustawieniami
void setBaudRate(uint16_t baudRate) – ustawia „szybkość portu”
void setBitsParity(uint8_t bits) – ustawia parzystość i ilość danych oraz stopu

void sendByte(uint8_t data) – wysyła dane po sprawdzeniu czy można. Wywołanie blokujące
void imSendByte(uint8_t data) – wysyła dane natychmiast bez sprawdzenia (ang. immediate), sprawdzę samodzielnie przed wysłaniem tą metodą
bool txReady() - zwraca true jeśli jest gotowość do wysłania

uint8_t recvByte() - zwraca bajt w sposób blokujący czyli sprawdza czy jest dostępny nowy i czeka aż będzie :-)
uint8_t imRecvByte() - zwraca bajt z portu bez wnikania czy jest „świeży”. Sam wcześniej sprawdzę.
bool rxReady() - zwraca true jeśli bajt jest gotowy do odbioru

uint16_t getBaudRate() - zwraca „szybkość” portu
uint8_t getBitsParity() - zwraca kombinację parzystość/bity/stop

Dodam jeszcze destruktor o którym za chwilę powiem:
~Usart() - destruktor obiektu.

Pewnie gdybym robił to w rzeczywistym kodzie a nie na potrzeby tutoriala, dodał bym także włączanie/wyłączanie przerwań. Chcę abyś o tym wiedział/a że pomijam teraz ten aspekt dla czytelności kodu. Gdybym to zrobił, odniósł bym się do tych metod hen hen po zagadnieniach podstawowych. Tu więc tych metod ich nie ma :-)

Ok, czas więc przejść do kodu. Za chwilę go jeszcze uzupełnimy:

Kod: Zaznacz cały

class Usart {
public:
   Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) : bauds(baudRate), bitParity(bits) {
      // Tu jest ciało konstruktora...
   }
   void setBaudRate(uint16_t baudRate) {
       bauds = baudRate;
      // Obliczenie i ustawienie na porcie baudRate
   }
   void setBitsParity(uint8_t bits)  {
      bitParity =  bits;
      // Obliczenie/dostosowanie i ustawienie na porcie parzystość/dane/stop
   }
   void sendByte(uint8_t data) {
      while(!txReady()) ;
      imSendByte(data);
   }
   void imSendByte(uint8_t data) {
      // Wysłanie danych w port
   }
   bool txReady() {
      // Sprawdzenie czy jest gotowość do nadania
   }
   uint8_t recvByte() {
      while(!rxReady());
      return imRecvByte();
   }
   uint8_t imRecvByte() {
      // Zwrot danej z portu
   }
   bool rxReady() {
      // Sprawdzenie czy są dane do odebrania na porcie
   }
   uint16_t getBaudRate() {
      return bauds;
   }
   uint8_t getBitsParity() {
      return bitParity;
   }
   ~Usart() {
      // Destrukcja obiektu
   }
private:
   uint16_t bauds;
   uint8_t bitParity;
};


Częściowo już te metody uzupełniłem. Brakuje jedynie działań na portach. Jeszcze tego z premedytacją nie podaję bo doszliśmy do bardzo istotnego i o wiele ważniejszego zagadnienia. Jaka metoda co zmienia w klasie a co w portach I/O.

Jeśli metoda nie zmienia ani jednego bitu w atrybutach klasy, powinna być zadeklarowana jako const.

Metody zmieniające cokolwiek w klasie to: setBaudRate, setBitsParity. Oczywiście konstruktor i destruktor ale dla nich nie ma sensu const. Każda inna metoda zapisuje lub czyta coś bezpośrednio z portu MCU!

Metody powinny więc wyglądać w większości jakoś tak:

Kod: Zaznacz cały

...
   uint8_t recvByte() const { // Tu widzisz const...
      while(!rxReady());
      return imRecvByte();
   }
...

Co ciekawe zobowiązanie to będzie rozciągnięte także na metody zależne (w przykładnie wyżej imRecvByte()). Ona także ma być const. Przyznasz że to bardzo ważny aspekt kontroli spójności danych.

Zobowiązania klasy należy jeszcze rozciągnąć na argumenty metod które otrzymuje. W większości przypadków klasa argumentów nie zmienia i fakt ten trzeba jakoś wyrazić.

Tu pojawia się temat poboczny dla rozważań obiektowych. Nie mam jednak wyjścia i powinienem go omówić.

Czy pamiętasz jak można przekazać argument do funkcji w C? Oczywiście, albo przez kopię albo przez wskaźnik (na poły filozoficzne rozważania że C i C++ zawsze to robi przez kopię tylko czasem kopią jest adres tu pominę). Ten drugi sposób jest wykorzystywany jeśli chcesz uniknąć kopiowania (np. przesyłasz do funkcji tablicę) a ten pierwszy jeśli objętość danych kopiowanych nie ma znaczenia bo np. i tak rezydują w rejestrze. W C++ dochodzi jeszcze jeden sposób przekazania danych. Jest to referencja.

Referencję definiujemy poprzez użycie znaku '&' (ang. ampersand) w argumencie funkcji (mówię wyłącznie o referencji w kodzie obiektowym, bo można oczywiście ich używać i w kodzie main() ). Mamy wtedy pewność że nie zostanie przekazana kopia argumentu a jedynie wskazanie na niego. Zwróć uwagę że nie użyłem słowa wskaźnik. Już tłumaczę dlaczego. W funkcji C++ dzięki referencji nie będziesz musiał/a wyłuskiwać danych z użyciem * (gwiazdki) tak jak w C! To sam język C++ zadba aby za Ciebie wyłuskać daną :-) To jest szalenie wygodne i ułatwia programowanie w stosunku do C. Kompilator kontroluje użycie wskazania (czyli referencji).

No dobrze, ale jeśli referencja jest (tak naprawdę) „kamuflażem wskaźnika”, to teoretycznie mogę przestawić zmienną spoza funkcji w ciele tejże funkcji! Tak, rzeczywiście może się tak stać! My programując obiektowo chcemy „mieć ciastko i zjeść ciastko” :-) Nie chcę kopiować danych ale i nie chcę ich móc zmienić. Można tak? Można. Oto przykład:

Kod: Zaznacz cały

...
   void sendByte(const uint8_t& data) const { // Tu widzisz stałą referencję na arg. data
      while(!txReady()) ;
      imSendByte(data);
   }
...

Bez referencji zapis z const nie ma sensu! Nie ma sensu bo następuje kopiowanie danych i jeśli nawet zmienisz przekazywany argument, to nie jego globalnie tylko jego kopię lokalną! Czyli zapis:

Kod: Zaznacz cały

...
   void sendByte(const uint8_t data) const { // Hę?!
...

.. jest pozbawiony logiki z punktu widzenia zobowiązań klasy. Kopia i tak zostanie wykonana i const „psu na budę” bo po co ci stała kopia ?! (naprawdę główkowałem długo i nie znalazłem zastosowania praktycznego które usprawiedliwia taki zapis w stosunku do zobowiązań klasy. Można jedynie dyskutować czy ma to sens w stosunku do samego argumentu).

Podobnie możemy postąpić z wartościami zwracanymi. Mogą to być referencje na stałe bowiem w większości przypadków danych zwracanych klasa Usart nie zmienia. Jest to informacja dla kompilatora: „jeśli przeczytałeś coś z portu bo w metodzie jest to czytanie, to nie zapisuj tego na stosie tylko potraktuj jako stałą przekazywaną w rejestrze” :-)

Super! Czyli const jest wspaniały! Tak jest wspaniały i stosuj go gdzie możesz :-) Atrybut z const można zainicjować w konstruktorze.

Dzięki użyciu const, informujesz bardzo wyraźnie kompilator co jest stałe. Przez to będzie włączał daleko idącą optymalizację kodu!

W zasadzie, logicznie, to atrybuty uint16_t bauds i uint8_t bitParity także są stałe. Hmm... no w sensie logicznym. Czyli jak obiekt działa, to są stałe. Czasem tylko, z użyciem set*() można je przestawić. Czy tak subtelne wymaganie („stałość logiczna” kontra „stałość bitowa”) także można wyrazić w C++? No gdyby nie można było to bym o tym nie pisał :-)

Wystarczy przed atrybutem postawić słowo które nie występuje w języku C. Jest to mutable. Wyrażamy w ten sposób że z punktu widzenia logiki istnienia klasy, dany element jest stały. Pozwala to także metodzie z const, zmienić ten atrybut. To stosunkowo rzadkie wymaganie i nie będę się upierał że akurat w tym przykładzie bardzo zasadne, no ale czasem trzeba i warto o tym wiedzieć :-)

Z praktyki najczęściej mutable pojawia się w kontekście zwracania danych z jakiejś tablicy/wektora obecnej w klasie i przypisana jest do licznika. Kontener danych jest stały ale potrzebujemy licznika trzymającego stan indeksu. Tu podaję i użyję ten atrybut jedynie informacyjnie bo celem tutoriala jest zaprezentowanie technik obiektowych. Myślę że to rozluźnienie dyscypliny mi wybaczysz bo o mutable rzadko się mówi :-)

Na koniec zacytuję jeszcze kod który jest efektem pracy nad klasą Usart:

Kod: Zaznacz cały

class Usart {
public:
   Usart(uint16_t baudRate = 9600, uint8_t bits = 0x81) : bauds(baudRate), bitParity(bits) {
      // Tu jest ciało konstruktora...
   }
   void setBaudRate(const uint16_t& baudRate) const {
       bauds = baudRate;
      // Obliczenie i ustawienie na porcie baudRate
   }
   void setBitsParity(const uint8_t& bits) const {
      bitParity =  bits;
      // Obliczenie/dostosowanie i ustawienie na porcie parzystość/dane/stop
   }
   void sendByte(const uint8_t& data) const {
      while(!txReady()) ;
      imSendByte(data);
   }
   void imSendByte(const uint8_t& data) const {
      // Wysłanie danych w port
   }
   bool txReady() const {
      // Sprawdzenie czy jest gotowość do nadania
   }
   const uint8_t& recvByte() const {
      while(!rxReady());
      return imRecvByte();
   }
   uint8_t imRecvByte() const {
      // Zwrot danej z portu
   }
   bool rxReady() const {
      // Sprawdzenie czy są dane do odebrania na porcie
   }
   const uint16_t& getBaudRate() const {
      return bauds;
   }
   const uint8_t& getBitsParity() const {
      return bitParity;
   }
   ~Usart() {
      // Destrukcja obiektu
   }
private:
   mutable uint16_t bauds;
   mutable uint8_t bitParity;
};


Następny odcinek po krótkiej 1 tyg. przerwie.. Będę wdzięczny na sugestie i uwagi.
,,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 4 gości