Random w C++... Z przeciążeniem i szablonami :-) część 1

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

Random w C++... Z przeciążeniem i szablonami :-) część 1

Postautor: mokrowski » piątek 20 paź 2017, 01:09

Po pozytywnym odzewie na poprzedni tutorial, postanowiłem popełnić następny :-) Myślę że sposób przedstawienia był ok. Otwarty jestem rzecz jasna na wszelkie uwagi. Im więcej informacji zwrotnej, tym lepiej.

Nie będę tu skupiał się na problemach sprzętowych dotyczących MCU, czy jego zasilania. Pokażę sposób w jaki można ogarnąć sprytnie problemy szablonów w C++.

Programując w C na MCU, odnoszę nieodparte wrażenie że w większości przypadków komponenty bibliotek (w C) to nie gotowe do użycia narzędzia a „zlepek kodu” który należy dostosować do własnych potrzeb. Z budowania takiego ,,Frankensteina” nie wynika skrócenie czasu pracy w następnych projektach. To nie jest sytuacja zdrowa. Myślę że C++ może tu zaproponować coś interesującego szczególnie z powodu posiadania silnego mechanizmu szablonów które dbają o typ.

Zacznę więc od postawienia sobie problemu, a później będę go komplikował :-)

Chcę zaimplementować klasę generatora liczb pseudo-losowych na rejestrze przesuwnym. Oczywiście rejestr będzie zrealizowany programowo, choć i sprzętowe wersje zrealizowane na TTL przedstawionych niżej algorytmów w sieci widziałem. Dla poszukiwaczy: „Wójek Google: LFSR pseudo random”.

Zaczynamy...

Co do samego main.cpp, to nie będę się wysilał. Zainteresowanych odsyłam do poprzedniego tutoriala dotyczącego portów. Tu po prostu wystawię wszystkie bity portu C jako wyjścia i podepnę diody. Oczywiście portów nie obsługuję podając na nie wartości absolutne, ale tu main.cpp ma wyłącznie wspierać w budowaniu podstawowej funkcjonalności.

Kod: Zaznacz cały

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

int main(void) {
   // Ustawienie wszystkich bitów portu C jako wyjście.
   DDRC = 0xFF;

   // Tu będzie deklaracja zmiennej typu Random czyli wyprowadzonej
   // z klasy Random. Na razie do testu uint8_t :)
   uint8_t myRandom;

   // Do testu sprawdzenia portu, oczywiście to nie jest jeszcze random :-)

   myRandom = 0x5E;

   while(true) {
      PORTC = myRandom++;
      _delay_ms(1000);
   }
}

Kompiluję, uruchamiam i sprawdzam czy działa. Światełka ,,biegają” można zaczynać.

Już wiem że będę miał oddzielną klasę Random, która powinna być umieszczona w zewnętrznym pliku i z racji tego że będzie ambitnie :-) i będą szablony, to będzie to plik *.hpp.

Tu można dyskutować czy pliki nagłówkowe w C++ nazywać *.h, *.hpp czy inaczej. Nie czas i miejsce na to ale wiedz że to kwestia konwencji którą przyjmiesz.

Kadłubek mojego pliku random.hpp będzie wyglądał tak:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

class Random {
public:

private:

};

#endif /* RANDOM_HPP_ */


Zwróć uwagę że w pierwszej sekcji klasy jest część publiczna, a w drugiej prywatna klasy. Do metod publicznych klasy będzie można się odwołać a w części prywatnej umieszczone będą pomocnicze elementy z których korzystać będzie klasa. Jak w przedszkolnym dowcipie:

Osoba1: Co to jest, duże czarne i wisi na ścianie?
Osoba2: Nie wiem...
Osoba1: Fortepian!
Osoba2: Fortepian?! Na ścianie?!
Osoba1: A co Cię obchodzi jak kto dom urządza? :-)

Dowcip głupawy, ale łatwiej będzie zapamiętać że w części private klasy możesz mieć także „wiszący fortepian” :-)

W części prywatnej będzie umieszczony atrybut uint16_t który będzie przechowywał stan generatora. W części publicznej dodamy dwie metody:
  • Metodę pobrania wartości z generatora.
  • Metodę ustawienia jego stanu.

To jest program na MCU więc pojawi się szereg inline'ów i static'ów. Tym bardziej że nie będziemy raczej robili dużej ilości instancji generatorów w swoim programie :-). Zakładam że (niestety) korzystasz z kompilatora avr-gcc 4.9* gdzie jeszcze nowe standardy nie mają pełnego zastosowania. Obecnie usunięto ze standardu C++ słowo inline. Kompilator sam decyduje kiedy segment kodu rozwijać.

Implementacja wygląda tak:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

class Random {
public:
   // Zwrot wartości z generatora
   // TODO: Zrobić. Na razie fake...
   static inline uint8_t get() {
      update();
      return rngState;
   }
   // Ustawienie zasiewu generatora
   static inline void setSeed(const uint16_t seed) {
      rngState = seed;
   }
private:
   // Stan generatora
   static uint16_t rngState;
   // Uaktualnienie stanu generatora
   // TODO: Zrobić. Na razie fake...
   static inline void update() {
      rngState++;
   }
};

uint16_t Random::rngState = 0x21;

#endif /* RANDOM_HPP_ */

Zanim przejdę dalej, zgodnie z zasadą małych przyrostów w kodzie, połączę implementację Random z main.cpp.

Po poprawieniu main.cpp wygląda tak:

Kod: Zaznacz cały

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

int main(void) {
   // Ustawienie wszystkich bitów portu C jako wyjście.
   DDRC = 0xFF;

   // Deklaracja zmiennej typu Random
   Random myRandom;

   // Ustawienie zasiewu generatora
   myRandom.setSeed(0x21);

   while(true) {
      PORTC = myRandom.get();
      _delay_ms(1000);
   }
}


Kompiluję i sprawdzam czy działa. Oczywiście działa ale z random nie ma nic wspólnego :-/

Wracam więc do analizy random.hpp aby przybliżyć co tam jest umieszczone :-)

W części publicznej jak pisałem wcześniej, zaimplementowałem 2 metody. Zwróć uwagę że w komentarzu tego czego jeszcze nie skończyłem, użyłem TODO:. Eclipse bardzo ładnie podświetla w okienku Tasks, wszystkie TODO:, FIXME:, XXX:. Pozwala to wstać od konsoli jak „woła real” i wrócić do pracy nie tracąc kontekstu. Polecam to przyzwyczajenie. Takie pisanie kodu dobrze komunikuje co jest jeszcze pozostawione i w jakim celu. Nie wolno jednak pozostawiać kodu „zakomentowanego”! W jednej sesji tworzenia kodu dopuszczam jeszcze pozostawienie komentowanego kodu z wpisem w jakim celu pozostał ale przed wysłaniem do repozytorium kod ma być bez komentowanych-znieczulonych sekcji! Za taki zły nawyk sam siebie kiedyś ukarzesz jak wrócisz do własnego kodu po 1-2 latach. Pojawi się pytanie: „Co za waryat to pisał? Aaaa ... to ja :-/ No.. to było chyba po jakiejś imprezie... :-( Teraz tego bym tak nie napisał...” :-P

W metodzie get(), wykonuję uaktualnienie generatora oraz zwracam jego atrybut rngState. Atrybut jest wprawdzie prywatny, ale robię to z metody klasy, więc mogę (bo wiem gdzie powiesiłem fortepian :-) ). Wszystkie metody są statyczne i z inline'owe bo chcę prosić kompilator o porozwijanie ich w celu szybkości. Prosić a nie wymagać. Kompilator sam zadecyduje. Z tego powodu raczej ilość kodu nie przyrośnie bo będzie 1 obiekt z klasy Random.
Zwróć uwagę że inicjalizacja argumentu rngState, znajduje się poza ciałem klasy. Jestem zmuszony tak zrobić bo atrybut jest statyczny. Na tym etapie jeszcze nie chcę mieć jawnego konstruktora klasy który ew. mógłby zajmować miejsce. Jak widać myślę zupełnie inaczej niż programując na PC.
W metodzie update(), na razie jest atrapa. Coś robi, ale nie to co powinna :-) Dodam więc implementację generatora.

Tu krótko o LFSR. Pomysł na generowanie liczb pseudolosowych, polega na przesunięciu w prawo słowa X-bitowego i wykonaniu na nim operacji XOR niektórych bitów. Aby generator działał dobrze, wykonywana jest także operacja na bicie 0'owym i pamiętany jest stan samego generatora aby uaktualnić go po każdym pobraniu wartości. Nie będę tu przynudzał i odeślę do strony:
http://en.wikipedia.org/wiki/Linear_feedback_shift_register
Tam sobie przeczytasz i znajdziesz nawet implementację generatora. No, ale tutorial nie miał być o operacjach matematycznych w ciele Galois :-)

Dodaję więc implementację samego generatora:

Kod: Zaznacz cały

...
   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = (rngState >> 1) ^ (-(rngState & 1) & 0xb400);
   }
...


Kompiluję, uruchamiam, działa :-) Rzeczywiście losowe (dokładniej, pseudolosowe).
Poprawię jeszcze get() tak aby zwracał najstarszy bajt ze słowa 16-bitowego.

Kod: Zaznacz cały

...
           // Zwrot wartości z generatora
   static inline uint8_t get() {
      update();
      return rngState >> 8;
   }
...


Stan random.hpp to:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

class Random {
public:

   // Zwrot wartości z generatora
   static inline uint8_t get() {
      update();
      return rngState >> 8;
   }

   // Ustawienie zasiewu generatora
   static inline void setSeed(const uint16_t seed) {
      rngState = seed;
   }

private:
   // Definicja stanu generatora
   static uint16_t rngState;

   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = (rngState >> 1) ^ (-(rngState & 1) & 0xb400);
   }
};

uint16_t Random::rngState = 0x21;

#endif /* RANDOM_HPP_ */


W moim środowisku program zajmuje 208 bajtów Flash i 2 bajty RAM. Ale co do optymalizacji, wykonam ją później. Zbyt wczesna optymalizacja to samo zło!.

No i na tym można skończyć ale myślę że kod może ulec znaczącej poprawie.

Pierwszy zarzut, to niezręczne nieco wywołanie:

Kod: Zaznacz cały

...
PORTC = myRandom.get();
...


Po co to get()? Nie lepiej było by:

Kod: Zaznacz cały

...
PORTC = myRandom;
...

Chcę więc przeciążyć metodę pobrania wartości. Sygnatura tej metody to: operator unsigned () const;

Realizacja zasiewu to także dość niewygodne setSeed(). Zmieniamy ją na aby zaimplementować przeciążenie przypisania:

Kod: Zaznacz cały

...
 Random& operator =(const uint16_t& seed) { ... }
..


Słówko o przeciążeniach.

Jeśli dana klasa posiada metodę: operator unsigned(), to każda próba pobrania z obiektu danych oczekiwanych jako unsigned, skończy się wywołaniem tej metody. Dodatkowo metoda posiada słowo const bo nie będzie modyfikowała zawartości obiektu (nie zmienia jedynego atrybutu jakim jest rngState).
Do metody przypisania (operator =() ), przekazywana jest wartość która jest po prawej stronie znaku równości w przypisaniu. Jak widać metoda zwraca referencję (o wiele lepsze niż wskaźniki w C) własny obiekt. Stąd w return jest *this.
Słowo this wskazuje na instancję generatora (jest wskaźnikiem) a * to znane z C wyłuskanie wartości.
Jeśli klasa posiada metodę: NazwaKlasy& operator =(const Typ& dane), to obiekt będzie poddawał się przypisaniom w postaci:

Kod: Zaznacz cały

...
NazwaKlasy mojObiekt;
Typ dane = wartość;
mojObiekt = wartość;
...


Nie implementuję już konstruktora tworzącego obiekt bo nie będę go teraz używał. Jeśli ktoś jednak chce wykonać pracę domową, to sygnatura konstruktora wygląda tak:

Kod: Zaznacz cały

...
Random(const uint16_t& value) : rngState(value) { }
...


Wtedy już jest „totalny wypas” bo można napisać w main.cpp tak:

Kod: Zaznacz cały

...
Random myRandom = 0x21;
...


Inną sprawą jest pytanie czy ten „totalny wypas” z przypisaniem jest bezpieczny czy nie. Ale nie będę się teraz tym zajmował.

Obiecuję że kod już z konstruktorem, umieszczę w następnej części tutoriala.

Jeśli masz pytania wątpliwości, pisz śmiało :-) Chętnie odpowiem.

Na koniec jeszcze stan plików:

main.cpp:

Kod: Zaznacz cały

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

int main(void) {
   // Ustawienie wszystkich bitów portu C jako wyjście.
   DDRC = 0xFF;

   // Deklaracja zmiennej typu Random
   Random myRandom;

   // Ustawienie zasiewu generatora
   myRandom = 0x21;

   while(true) {
      PORTC = myRandom;
      _delay_ms(1000);
   }
}


random.hpp:

Kod: Zaznacz cały

#ifndef RANDOM_HPP_
#define RANDOM_HPP_

#include <stdint.h>

class Random {
public:

   // Zwrot wartości z generatora
   operator unsigned() const {
      update();
      return rngState >> 8;
   }

   // Ustawienie zasiewu generatora
   Random& operator =(const uint16_t& seed) {
      rngState = seed;
      return *this;
   }

private:
   // Definicja stanu generatora
   static uint16_t rngState;

   // Uaktualnienie stanu generatora
   static inline void update() {
      // 16-bit LFSR: x^16 + x^14 + x^13 + x^11.
      // Generuje sekwencję liczb o długości 65535.
      rngState = (rngState >> 1) ^ (-(rngState & 1) & 0xb400);
   }
};

uint16_t Random::rngState = 0x21;

#endif /* RANDOM_HPP_ */
,,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 1 gość