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

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

Na szczęście w pracy trochę więcej wolnego czasu bo klient zwolnił tempo w projekcie :-) , więc popełniłem dalszą część nieco wcześniej. Jednocześnie będę miał dość zasadnicze pytanie do aktywnych czytelników tych tutoriali (jeśli jest ktoś taki)....

Zmagań z C++ ciąg dalszy...

Ok, no to przyjrzyjmy się gdzie występuje „styk ze sprzętem” w klasie Usart. Ba, w wielu. Łatwiej wskazać te metody gdzie tego nie ma. W takich wypadkach, dość ścisłego „związania kodu” jedną z sensownych decyzji jest użycie innego obiektu (jedną i wybraną w tutorialu ze względu na prezentację treści). W tym przypadku klasa Usart użyje implementacji konkretnego portu na MCU. U mnie będzie to ATmega16 bo od ostatnich zmagań z Adc jeszcze nie wyjąłem jej z zestawu a ten egzemplarz mam już długo i przeznaczony jest do zajeżdżenia zapisami i testami :-)

Co więc klasa UART'a w ATmega16 ma zrobić? Ma udostępnić bity Rx/Tx, pomóc wstawić preskaler z baudrate oraz zwrócić/wysłać dane w/z portu :-)

No to teraz sprawdzimy czy będziesz w stanie samodzielnie zaimplementować taką klasę :-) W tym tutorialu podam wyłącznie jej użycie, a w następnym pokażę (być może) już jej ciało :-) Klasę specyficzną dla MCU nazwę UsartImpl. Przekażę ją jako argument do konstruktora klasy Usart jako pierwszy obowiązkowy argument. Oczywiście w samej klasie Uart policzę wartość preskalera który wstawię do portu z użyciem wywołań w UsartImpl. Będę także chciał przechować wskaźnik do UsartImpl w moim Usart.

Kod przy takich założeniach może wyglądać tak:

Kod: Zaznacz cały

class Usart {
public:
   Usart(UsartImpl * usartImpl, uint16_t baudRate = 9600, uint8_t bits = 0x81) : bauds(baudRate), bitParity(bits), impl(usartImpl) {
      setBaudRate(baudRate);
      setBitsParity(bits);
   }
   void setBaudRate(const uint16_t& baudRate) const {
      bauds = baudRate;
      // Zakładam że będzie użycie bez bitu ,,turbo”  x2 :-)
      impl->setPrescaler(F_CPU / (16 * baudRate) - 1);
   }
   void setBitsParity(const uint8_t& bits) const {
      bitParity =  bits;
      impl->setBitParityStop(bits >> 4, ( bits & 0x04) >> 2, bits & 0x01);
   }
   void sendByte(const uint8_t& data) const {
      while(!txReady()) ;
      imSendByte(data);
   }
   void imSendByte(const uint8_t& data) const {
      impl->setData(data);
   }
   bool txReady() const {
      return impl->txReady();
   }
   uint8_t recvByte() const {
      while(!rxReady());
      return imRecvByte();
   }
   uint8_t imRecvByte() const {
      return impl->data();
   }
   bool rxReady() const {
      impl->rxReady();
   }
   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;
   UsartImpl * impl;
};

Ale dlaczego recvByte() i imRecvByte() nie zwracają const uint8_t&? Tak przecież zwracałem na to uwagę! Ha... przecież są to dane z portu a on praktycznie może się zmienić w każdym momencie stąd dane te nie będą stałe. Specyfikator volatile (bo wnikliwi mogli by zapytać) przypisany jest do makr PORT*. Jeśli nie wierzysz, sprawdź w avr/io.h.

Jeszcze słowo na temat przykładów. Pamiętaj, celem tego cyklu jest przedstawienie różnych sposobów obsługi obiektowości w C++ na platformie AVR. Niektóre moje wybory dotyczące implementacji nie miały by miejsca jeśli nie to główne wymaganie. Teraz będę prezentował różne mechanizmy programowanie obiektowego. Ten scenariusz jest jednak zamierzony :-) Mam zahaczyć o różne techniki i przedstawić wiele zagadnień.

Zostawmy na chwilę nasz Usart i zajmijmy się nieco innym aspektem obsługi obiektów.

Pamiętasz jak kreowaliśmy obiekt? Np. Mogło to wyglądać tak.

Kod: Zaznacz cały

Klasa nazwaObiektu = Klasa();

Tu jawnie uruchamiany był konstruktor i do zmiennej typu Klasa, przypisywany był obiekt.
Istnieją jeszcze inne sposoby instancjonowania (odsyłam do poprzednich części jeśli pojęcie jest dla Ciebie nowe) obiektu. Jednym z nich jest new.

Jak to pisałem w innych cyklach, new oraz powiązane z nim delete, nie jest zaimplementowane w avr-g++ bo...... nikt o to nie zadbał :-) Już cytowałem wcześniej pliki które to naprawiają, tu pojawią się one w pełnej wersji wymaganej przez C++.

new.hpp:

Kod: Zaznacz cały

#ifndef NEW_HPP_
#define NEW_HPP_

//
// avr-libc nie dostarcza operatorów new i delete.
// Ma także problemy z definicją obsługi dziedziczenia i dziedziczenia
// wirtualnego. Plik poprawia te problemy.
// Źródło:
//  http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=59453
//

#include <stdlib.h>

#ifdef __cplusplus

// Operatory new, new[]
extern void * operator new(size_t size);
extern void * operator new[](size_t size);

// Operatory new i new[] alokujące w miejscu
extern void * operator new(size_t size, void *ptr);
extern void * operator new[](size_t size, void *ptr);

// Operatory delete i delete[]
extern void operator delete(void *ptr);
extern void operator delete[](void *ptr);

// Operatory delete i delete[] alokujące w miejscu
extern void operator delete(void *srcPtr, void *dstPtr);
extern void operator delete[](void *srcPtr, void *dstPtr);

// Obsługa dziedziczeń i dziedziczeń wirtualnych...
__extension__ typedef int __guard __attribute__((mode (__DI__)));

extern "C" {
   int __cxa_guard_acquire(__guard *);
   void __cxa_guard_release (__guard *);
   void __cxa_guard_abort (__guard *);

   void __cxa_pure_virtual(void) __attribute__ ((__noreturn__));
   void __cxa_deleted_virtual(void) __attribute__ ((__noreturn__));
}

#endif // __cplusplus

#endif /* NEW_HPP_ */


new.cpp:

Kod: Zaznacz cały

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#include "new.hpp"


// XXX: Implementacja obsługi wyjątków? Teraz chyba nie jest potrzebna :)

// new i new[]
void * operator new(size_t size) {
    return malloc(size);
}

void * operator new[](size_t size) {
    return malloc(size);
}

// Naiwna implementacja new w miejscu
void * operator new(size_t, void * ptr) {
   return ptr;
}

void * operator new[](size_t, void * ptr) {
    return ptr;
}

// delete i delete[]
void operator delete(void* ptr) {
    free(ptr);
}

void operator delete[](void* ptr) {
    free(ptr);
}

// Naiwna implementacja delete w miejscu.

void operator delete(void *, void * ptr) {
   free(ptr);
}

void operator delete[](void *, void *ptr) {
   free(ptr);
}

namespace {

inline char& flag_part(__guard *g) {
   return *(reinterpret_cast<char*>(g));
}

inline uint8_t& sreg_part(__guard *g) {
   return *(reinterpret_cast<uint8_t*>(g) + sizeof(char));
}

} // anonymous namespace

int __cxa_guard_acquire(__guard *g) {
    uint8_t oldSREG = SREG;
    cli();
    // Inicjalizacja zmiennej statycznej powinna być bezpieczna wielowątkowo.
    // Jeśli chcesz zrezygnować z tego mechanizmu, kompiluj z:
    // -fno-threadsafe-statics
    if (flag_part(g)) {
        SREG = oldSREG;
        return false;
    } else {
        sreg_part(g) = oldSREG;
        return true;
    }
//   return !*(char *)(g);
}
void __cxa_guard_release(__guard *g) {
   flag_part(g) = 1;
   SREG = sreg_part(g);
   //   *(char *)g = 1;
}
void __cxa_guard_abort (__guard *g) {
   SREG = sreg_part(g);
}

void __cxa_pure_virtual(void) {
   // Zgodnie ze standardem C++ powinno być std::terminate()
   abort();
}
void __cxa_deleted_virtual(void) {
   // Zgodnie ze standardem C++ powinno być std::terminate()
   abort();
}

Jak widzisz, new jest opakowaniem na malloc a delete na free. Przyjmuje argument w ilości danych do alokowania i zwraca wskaźnik na nie (jak widać void *). Spokojnie, przyjrzymy się także rodzajom new i delete ale w następnych częściach tutoriala.

Tworzenie obiektu z użyciem new, będzie dość proste:

Kod: Zaznacz cały

Klasa * mojaKlasa = new Klasa();

W tym przypadku zostaje alokowana unikalna przestrzeń pamięci dla obiektu która wskazuje na jego elementy. Hmm.... ale przecież AVR nie wykona kodu z RAM! Masz rację. W tym przypadku kompilator zapisze więc w pamięci RAM jedynie unikalne atrybuty obiektu które nie są const (no... i teraz się wyjaśnia dlaczego tak naciskałem na to const we wcześniejszej części) oraz wskaźniki na metody/konstruktory/destruktory umieszczone w pamięci Flash. Oczywiście kompilator jest sprytny i wszędzie gdzie nie jest to niezbędne, wskaźników nie będzie umieszczał bo twórcy wiedzieli jak nietypowa jest arch. harwardzka. Jak to zrobić jeszcze wydajniej w szczegółach nieco później. Teraz popracujmy z new.

Jeśli będziesz tworzył/a obiekt przez new, przyjmujesz zobowiązanie że zajmiesz się także jego destrukcją. W systemach operacyjnych głównego nurtu, brak spełnienia tego wymagania traktowane jest jako „totalne niedbalstwo i lamerstwo n-tego stopnia”.... Nie przesadzam. W ten sposób „cieknie pamięć” i aplikacja po np. 2 dniach działania pożera jak choroba zasoby systemu. My programując na MCU nie poszalejemy na 1 KB (lub mniej) RAM. Stąd jeśli już wiesz dlaczego (na boga) stosujesz new na MCU, stosuj delete. Jak? Ano tak:

Kod: Zaznacz cały

...
// Tu tworzę obiekt...
Klasa * mojaKlasa = new Klasa();
....
// Tu jakiś kod użytkowy...
....
// Usunięcie obiektu
delete(mojaKlasa);
...

Przy takim usunięciu obiektu, uruchamiany jest jego destruktor który w sposób poprawny kończy działanie obiektu.

Dobrze, a praktycznie? Zakładając że w konstruktorze swojego Usart'a np. tworzyłeś/aś bufory, uruchamiałaś port szeregowy itd, w destruktorze trzeba dokładnie po sobie posprzątać. Czyli dealokować bufory, wyłączyć port itp. To stara jak świat zasada harcerza. Miejsce w którym jesteś zostaw jakie było albo (jeśli można) jeszcze je uporządkuj :-)

Jakiegokolwiek zaboru zasobu dokonuj w konstruktorze a zwalniaj go w destruktorze.

No dobrze, ale po co te dziwne metody new[] i delete. Ba... Jeśli chciałbyś/chciałabyś tworzyć wiele obiektów jednocześnie? Np obiekty menu które przechowuje napis i wskaźnik na funkcję do uruchomienia jeśli pozycja menu jest wybrana. Będzie to pewnie jakaś tablica (lub lepiej graf ale tu dam dla uproszczenia tablicę). Trzeba będzie więc ,,hurtem” tworzyć obiekty. No to od tego jest „hutrowe new” czyli new[].

Kod: Zaznacz cały

// Przykład poglądowy klasy pozycji w menu...
struct MenuItem {
   char * message;
   void (*function)(void);
};
...
MenuItem * myMenu = new MenuItem[10];
...
delete[] myMenu;
...


Jak widzisz tworzenie takiej tablicy typu MenuItem z 10 elementami dzięki new jest bardzo proste. Należy jedynie pamiętać że wtedy używamy detete[] bo ono uruchamia destruktory danego obiektu.

Tu obiekt był trywialny bo posiadał jedynie domyślny konstruktor (dał mu go kompilator) oraz domyślny destruktor (także prezent od kompilatora) a że nie miał nic do ukrycia to był strukturą :-)

Wiem, przeklniesz mnie pewnie za drobiazgowość, ale powinienem o tym powiedzieć.

new może się nie powieść...:-/ Co jeśli zabraknie pamięci ? No właśnie... Domyślnie C++ rzuca wtedy wyjątek. Wyjątki jednak to bardzo kosztowna technika i na MCU najczęściej się jej nie używa. Jeśli jednak była by użyta (np. programujesz na PC), to łapiesz wtedy wyjątek i reagujesz. Na szczęście jest także zdefiniowana w standardzie C++ wersja new bez obsługi wyjątków. Wtedy new, jeśli się nie powiedzie zwraca wskaźnik na adres 0x00 (zero) co świadczy o błędzie. Taką konwencję przyjęto na C++ w implementacjach na wielu MCU (MIPS, ARM, POWERPC...)

Dokładnie więc i zgodnie ze standardem C++ dla „dużych systemów”:

Kod: Zaznacz cały

...
VeryFatClass * p = new (std::nothrow) VeryFatClass();
...

Tu kod jest zgodny z konwencją normalnych systemów operacyjnych i nie będzie rzucał wyjątkiem. Poniżej będziesz zobowiązany/zobowiązana więc do testowania czy p nie wskazuje na 0.

Na AVR i w implementacjach na wiele MCU, jest to standardowe zachowanie i wtedy użyjesz:

Kod: Zaznacz cały

...
MyClass * p = new MyClass();
...


.. ew. testując czy p nie wskazuje na 0x00.

Mam nadzieję że jest to zrozumiałe? Zostaje jeszcze (z kronikarskiego obowiązku), jedna wersja new i delete. Jest to tzw. wersja „w miejscu” lub (ang.) „in placement”.

Załóżmy że alokowałeś/alokowałaś pewną przestrzeń danych. Wiesz że nie jest już potrzebna jej stara zawartość ale właśnie masz ochotę tworzyć nowy obiekt i nie chcesz być rozrzutny/rozrzutna. Wtedy możesz nakazać tworzenie obiektu w miejscu właśnie alokowanym.

Hmm... tak naprawdę technika ma 2 rodzaje zastosowań. Pokażę je obydwa.

Zastosowania new „in-placement”:
1. Tworzenie dużego obiektu i umieszczanie w miejscu przez niego alokowanym innego.

Kod: Zaznacz cały

...
VeryFatClass * myObject = new VeryFatClass();
....
// Użycie VeryFatClass() czyli myObject->metodaVeryFatClass()...
...
// Jawna destrukcja myObject wyprowadzonej z VeryFatClass()
myObject->~VeryFatClass();
// Tu została ,,osierocona pamięć” po VeryFatClass()
...
// Tworzenie innego obiektu w zaalokowanym już miejscu pamięci
MyClass * myClass = new (myObject) MyClass();
...

Jak widać kosztem nieco złożonego ręcznego zarządzania, możemy oszczędzać pamięć nie alokując ponownie potrzebnego obszaru pamięci. No to teraz zaawansowani. Ile razy tego używaliście na dużych systemach? A na MCU należy o tym wiedzieć. Tu „nie ma lekko z RAM” :-/
2. Alokowanie przez malloc... bardo podobne...

Kod: Zaznacz cały

...
MyClass * pointer = (MyClass *) malloc(sizeof(MyClass));
...
// Umieszczenie obiektu w alokowanej przestrzeni...
MyClass * myObject = new (pointer) MyClass();
...

Alokowanie przez new, usunięcie przez delete i to samo dla wersji z [], ma jeszcze bardzo wiele szczegółów implementacji i użycia. Niestety na MCU o takich zasobach jak ATmega, nie będziesz z tego korzystał (inaczej, być może będziesz ,,od wielkiego dzwonu”) :-) Z tego też powodu tematu dalej nie będę rozwijał, chyba że zapytasz w komentarzach lub wyprowadzisz mnie z błędu i udowodnisz że new jest niezbędne w niektórych zastosowaniach (pewnie takie znajdziesz jak i ja ale pewnie nie będą w głównym nurcie systemów wbudowanych ) :-)

Jak to powiedziała Szymborska: „... zrodzilismy sie bez wprawy i pomrzemy bez rutyny... ” :-) Będę więc pokorny i chętnie wysłucham bo uwielbiam się mylić :-) To jest okazja do nauki :-)

Tym bardziej tego tematu nie ma sensu rozwijać bo czeka nas jeszcze (zapowiadana) tu fascynująca (nie nabijam się) część związana z relacjami obiektowymi. Dziedziczeniem, użyciem, agregacją, kompozycją.. i wieloma innymi elementami. To jest istotniejsze niż „pastwienie się” nad new i delete pracującego na przestrzeni 512 bajtów .. :-/

Mam wrażenie że ilość wiedzy albo tempo publikacji onieśmiela osoby zainteresowane publikowaną treścią (cały czas mam nadzieję że takowe są), nie... dokładniej.. nie wiem czy nie piszę tego wyłącznie dla Siebie :-/. Stąd chcę zapytać o zagadnienia które widzisz w ankiecie.
Nie ukrywam że tworzenie tutoriali zabiera czas i dość dużo energii, a wiem (bez jakiegokolwiek wbijania się w dumę) że nie jest to ,,śmieciowa wiedza". Jest to zakres ,,mocno programistyczny", ale bardzo przydatny szczególnie w większych projektach. Po ilości zapytań i sugestii mogę wnosić (nawet) że dalsze pisanie nie ma to większego sensu. Być może 2-4 osoby są zainteresowane kontynuacją tego cyklu ale dla tych kilku nie jest uzasadnione poświęcanie czasu w takim wymiarze :-/ Jeśli więc jesteś zainteresowany/zainteresowana dalszym ciągiem tego tutoriala, proszę o sugestie tematów i problemów które mam poruszyć w komentarzach. Jeśli nie, wyraź to w ankiecie. Mam przeświadczenie że projekty nieopłacalne należy zamykać zanim poniesiemy nieuzasadnione koszta ,,na zimno i bez jakiegokolwiek żalu" ;-) .
,,Myślenie nie jest łatwe, ale można się do niego przyzwyczaić" - Alan Alexander Milne: Kubuś Puchatek

Awatar użytkownika
xor
User
User
Posty: 169
Rejestracja: poniedziałek 05 wrz 2016, 21:44

Re: Obiektowość na AVR od podstaw 3 z n...

Postautor: xor » wtorek 27 cze 2017, 19:38

W listingu nr 3 (implementacja new i delete) skleiły się dwa oddzielne listingi (dla .hpp i .cpp). Poniżej poprawiony tekst.
Skoro już bawię się w redaktora to jeszcze dla wygody linki do wszystkich części kursu:
cz. 1
cz. 2
cz. 3
cz. 4
cz. 5
cz. 6


new.hpp:

Kod: Zaznacz cały

#ifndef NEW_HPP_
#define NEW_HPP_

//
// avr-libc nie dostarcza operatorów new i delete.
// Ma także problemy z definicją obsługi dziedziczenia i dziedziczenia
// wirtualnego. Plik poprawia te problemy.
// Źródło:
//  http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=59453
//

#include <stdlib.h>

#ifdef __cplusplus

// Operatory new, new[]
extern void * operator new(size_t size);
extern void * operator new[](size_t size);

// Operatory new i new[] alokujące w miejscu
extern void * operator new(size_t size, void *ptr);
extern void * operator new[](size_t size, void *ptr);

// Operatory delete i delete[]
extern void operator delete(void *ptr);
extern void operator delete[](void *ptr);

// Operatory delete i delete[] alokujące w miejscu
extern void operator delete(void *srcPtr, void *dstPtr);
extern void operator delete[](void *srcPtr, void *dstPtr);

// Obsługa dziedziczeń i dziedziczeń wirtualnych...
__extension__ typedef int __guard __attribute__((mode (__DI__)));

extern "C" {
   int __cxa_guard_acquire(__guard *);
   void __cxa_guard_release (__guard *);
   void __cxa_guard_abort (__guard *);

   void __cxa_pure_virtual(void) __attribute__ ((__noreturn__));
   void __cxa_deleted_virtual(void) __attribute__ ((__noreturn__));
}

#endif // __cplusplus

#endif /* NEW_HPP_ */


new.cpp:

Kod: Zaznacz cały

#include <stdint.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#include "new.hpp"


// XXX: Implementacja obsługi wyjątków? Teraz chyba nie jest potrzebna :)

// new i new[]
void * operator new(size_t size) {
    return malloc(size);
}

void * operator new[](size_t size) {
    return malloc(size);
}

// Naiwna implementacja new w miejscu
void * operator new(size_t, void * ptr) {
   return ptr;
}

void * operator new[](size_t, void * ptr) {
    return ptr;
}

// delete i delete[]
void operator delete(void* ptr) {
    free(ptr);
}

void operator delete[](void* ptr) {
    free(ptr);
}

// Naiwna implementacja delete w miejscu.

void operator delete(void *, void * ptr) {
   free(ptr);
}

void operator delete[](void *, void *ptr) {
   free(ptr);
}

namespace {

inline char& flag_part(__guard *g) {
   return *(reinterpret_cast<char*>(g));
}

inline uint8_t& sreg_part(__guard *g) {
   return *(reinterpret_cast<uint8_t*>(g) + sizeof(char));
}

} // anonymous namespace

int __cxa_guard_acquire(__guard *g) {
    uint8_t oldSREG = SREG;
    cli();
    // Inicjalizacja zmiennej statycznej powinna być bezpieczna wielowątkowo.
    // Jeśli chcesz zrezygnować z tego mechanizmu, kompiluj z:
    // -fno-threadsafe-statics
    if (flag_part(g)) {
        SREG = oldSREG;
        return false;
    } else {
        sreg_part(g) = oldSREG;
        return true;
    }
//   return !*(char *)(g);
}
void __cxa_guard_release(__guard *g) {
   flag_part(g) = 1;
   SREG = sreg_part(g);
   //   *(char *)g = 1;
}
void __cxa_guard_abort (__guard *g) {
   SREG = sreg_part(g);
}

void __cxa_pure_virtual(void) {
   // Zgodnie ze standardem C++ powinno być std::terminate()
   abort();
}
void __cxa_deleted_virtual(void) {
   // Zgodnie ze standardem C++ powinno być std::terminate()
   abort();
}

Awatar użytkownika
Antystatyczny
Geek
Geek
Posty: 1168
Rejestracja: czwartek 03 wrz 2015, 22:02

Re: Obiektowość na AVR od podstaw 3 z n...

Postautor: Antystatyczny » wtorek 27 cze 2017, 19:51

Dzięki, już poprawiłem.
"The true sign of intelligence is not knowledge but imagination" Albert Einstein.


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