ADC 3 z 5

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

ADC 3 z 5

Postautor: mokrowski » sobota 08 lip 2017, 12:58

W poprzedniej części telenoweli obiektowej Steve oświadczył że kocha Jane .. nie... to nie ten odcinek :-) My zajmujemy się AVR a nie „Modą na Sukces” :-) Choć jeśli czytasz o programowaniu w C++ na AVR to już osiągnąłeś sukces! :-P

Pamiętasz jak w 1 części napisałem o podejściu bottom-up i top-down? Jak widzisz w C++ myśli się bardzo top-down (od ogółu do szczegółu). Kod który powstaje jest w większym stopniu niż w C reużywalny i bardziej elastyczy (oczywiście moje zdanie nie jest przyczynkiem do świętej wojny :-) ) Celem tego cyklu tutoriali jest poznanie techniki programowania obiektowego. W części 1 i 2 zaprezentowałem podstawy. Opanowanie ich jest niezbędne aby kontynuować.

Podsumujmy:
1. Mamy klasę obsługującą ADC która jest w stanie obsłużyć jego podstawowe właściwości.
2. Metody nie przyjmują argumentów spoza tych które obsługuje sprzęt (np. nie ustawisz preskalera na wartość nie przewidzianą w kodzie).
3. Funkcjonalność jest hermetyzowana i w kodzie z zewnątrz jesteś w stanie wywołać jedynie te metody które są przewidziane.
4. Masz możliwość (choć przy klasie Adc średni ma to sens) powoływania instancji klasy czyli obiektów które są unikalne.
5. Dzięki extern ”C” z łatwością połączysz kod w C z kodem C++ w „tę i wewtę” (to bardzo ważne bo możesz dzięki temu stosować C tam gdzie wygodniej a C++ tam gdzie się nie da inaczej).
6. Wiesz już co to konstruktor i konstruktor z parametrem.
7. Znasz sposób wydzielenia deklaracji klasy i definicji metod składowych.

Co do pkt. 4. Kreowanie klasy z użyciem new, powoduje że obiekt z niej wyprowadzony uzyskuje unikalny adres, słowo this (przypominam że to wskaźnik) w ciele metod wskazuje na egzemplarz klasy czyli obiekt, będziesz mógł kreować wiele przetworników.

Wyjaśnia się w tej chwili dlaczego wybrałem Adc. Postawię ponownie pytanie: Czy chcę kreować wiele instancji Adc? Już odpowiedziałem że nie. Tym bardziej że jest tylko 1 ADC w ATmega16. Jeśli na takie pytanie odpowiadam że nie (a na MCU bardzo często będę miał 1 komparator 1 usart, 1 I2C, jeden zasób... ), to elastyczność związana z kreacją obiektów z klasy jest mi „na grzyba” :-) Podkreślam, nie zawsze tak jest. Czasem MCU ma np. 2 USART'y i wtedy trzeba sprawę przemyśleć :-) Dość że jak mam 1 zasób to płacę za coś czego nie używam. Nie lubisz chyba płacić za coś czego nie używasz? Zajmijmy się tym problemem.

Aby zacząć, powinienem zrobić krok w tył. Pliki projektu powinny wrócić do jednego z etapów widzianego wcześniej w tutorialach. Chcę wrócić do stanu w którym posiadałem klasę ze zdefiniowanymi metodami w pliku *.hpp. W tym celu kod metod z pliku Adc.cpp, pracowicie kopiuję do Adc.hpp a plik Adc.cpp wyłączam z projektu (przypominam: Prawy klawisz myszy w drzewie projektu na pliku Adc.cpp -> C/C++ General -> Preprocesor Include Paths, Macros etc. -> Włączam checkbox: Exclude resource from build.

Plik Adc.hpp przed dalszą częścią pracy będzie wyglądał tak:

Kod: Zaznacz cały

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

enum ADCReference {
   ADC_EXTERNAL = 0,
   ADC_AVCC = 1,
   ADC_RESERVED = 2,
   ADC_INTERNAL = 3
};

enum ADCPrescaler {
   ADC_PRESCALER_0 = 0,
   ADC_PRESCALER_2 = 1,
   ADC_PRESCALER_4 = 2,
   ADC_PRESCALER_8 = 3,
   ADC_PRESCALER_16 = 4,
   ADC_PRESCALER_32 = 5,
   ADC_PRESCALER_64 = 6,
   ADC_PRESCALER_128 = 7
};

enum ADCChannel {
   ADC_CHANNEL_0 = 0,
   ADC_CHANNEL_1 = 1,
   ADC_CHANNEL_2 = 2,
   ADC_CHANNEL_3 = 3,
   ADC_CHANNEL_4 = 4,
   ADC_CHANNEL_5 = 5,
   ADC_CHANNEL_6 = 6,
   ADC_CHANNEL_7 = 7
   // Atmega16 ma więcej jeszcze kanałów, ale to tutorial
   // i nie ma sensu rozbudowywać kodu.
};

enum ADCOversample {
   ADC_OVERSAMPLE_0 = 0,
   ADC_OVERSAMPLE_1 = 1,
   ADC_OVERSAMPLE_2 = 2,
   ADC_OVERSAMPLE_3 = 3,
   ADC_OVERSAMPLE_4 = 4,
   ADC_OVERSAMPLE_5 = 5,
   ADC_OVERSAMPLE_6 = 6
};

class Adc {
public:
   // Konstruktor
   Adc() : value(0) {
      // Uruchomienie przetwornika ADC
      ADCSRA |= (1 << ADEN);
   }

   // Ustawienie preskalera
   void setPrescaler(ADCPrescaler prescaler) const {
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | prescaler;
   }

   // Ustawienie napięcia referencyjnego
   void setReference(ADCReference reference) const {
      // Ustawienie napięcia referencyjnego na bez naruszania kanału.
      ADMUX = ( ADMUX & ( (1 << REFS0) - 1) ) | ( reference << REFS0);
   }

   // Ustawienie kanału Adc
   void setChannel(ADCChannel channel) const {
      // Ustawienie kanału
      ADMUX = ( ADMUX & ~( (MUX4 << 1) - 1 ) ) | channel;
      return;
   }

   // Pobranie wartości pomiaru
   uint16_t getValue() const {
      // Uruchomienie pomiaru
      runAdc();
      // Oczekiwanie na zakończenie konwersji Adc
      while(!complete());
      // Zwrot wartości po konwersji
      return ADC;
   }

   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(ADCOversample oversample) {
      // Zerowanie wartości przed sumowaniem
      value = 0;
      // Pętla obliczająca sumę oversample
      for(uint16_t i = (1 << (oversample << 1)); i > 0; --i) {
         // Uruchomienie pomiaru
         runAdc();
         // Oczekiwanie na zakończenie pomiaru
         while(!complete());
         // Sumowanie próbek
         value += ADC;
      }
      return ( value >> oversample );
   }

   // Start konwersji
   void runAdc() const {
      // Start konwersji
      ADCSRA |= (1 << ADSC);
      return;
   }

   // Sprawdzenie czy zakończono pomiar
   bool complete() const {
      // Jeśli ADSC zgaszony, konwersja zakończona
      return !(ADCSRA & (1 << ADSC));
   }
private:
   // Wartość obliczana
   uint32_t value;
};

Po kolei i stopniowo.

Zrób mały przeskok mentalny z programowania MCU do programów np. Finansowo Księgowych. Programy FK mają szereg kont, czyli konta to będą pewnie unikalne obiekty. Czy w FK'ach atrybut „wspólny dla wszystkich kont” w klasie mógłby się przydać? Oczywiście że tak :-) Np. mógłby być licznikiem istniejących kont. Jest wspólny dla wszystkich obiektów.

Następny przeskok mentalny. Nasze forum. Mirek chciałby wiedzieć np. ile jest „forumowiczów”. Klasa forumowicza mogła by posiadać atrybut „wspólny” dla wszystkich kont. A identyfikator/ksywka forumowicza? O nie, ona jest unikalna dla konta. Powinna być „nie wspólna” :-) Czy to jest zrozumiałe?

Do określenia „wspólności” w klasie stosujemy w C++ static. Możesz decydować czy klasa posiada atrybut tylko do użytku dla obiektu wyprowadzonego z klasy (wtedy atrybut nie będzie posiadał static) lub czy atrybut będzie wspólny dla wszystkich obiektów z tej klasy (wtedy atrybut posiada static)

Bardzo chętnie odpowiem na pytania w tym względzie. Pamiętaj że mówię o static w klasie. Oczywiście to nie neguje sposobu działania static znanego i działającego „z C
:-) W C++ także tak działa. W klasie będzie jednak tak jak zaznaczyłem.

Zapoznajmy się więc ze słówkiem static w C++ w klasach stopniowo.

Zakładając że będę miał jedynie 1 obiekt Adc, ile będzie obecnych atrybutów value w programie? Odpowiedź brzmi jeden. Czyli nie jest on unikalny dla obiektów! Jest on statyczny i wspólny dla wszystkich klas.

Definiujemy go jako statyczny:

Kod: Zaznacz cały

...
private:
   // Wartość obliczana
   static uint32_t value;
};

Po szybkiej kompilacji otrzymasz taki komunikat:

Kod: Zaznacz cały

../Adc.hpp:54:10: error: 'uint32_t Adc::value' is a static data member; it can only be initialized at its definition

Aha. Czyli jak jest static, to go trzeba od razu zainicjować. Zerknij jeszcze do konstruktora. Eclipse podkreślił ci go. Czepia się do listy inicjalizacyjnej ( Adc() : value(0) {... ). Jeśli atrybut jest statyczny, to nie można go zainicjalizować w konstruktorze. No i prawidłowo! Każdy nowo tworzony obiekt wtedy by „psuł wspólną wartość”! No to poprawiamy konstruktor na:

Kod: Zaznacz cały

...
class Adc {
public:
   // Konstruktor
   Adc() {
      // Uruchomienie przetwornika ADC
      ADCSRA |= (1 << ADEN);
   }
...

Nie zapominamy o inicjalizacji atrybutu statycznego. Pod deklaracją klasy pojawi się:

Kod: Zaznacz cały

...
private:
   // Wartość obliczana
   static uint32_t value;
};

uint32_t Adc::value = 0;

Tu warto zwrócić uwagę że po „wyciągnięciu” inicjalizacji atrybutu statycznego poza jego klasę, słówka static już nie piszemy przed nazwą atrybutu w postaci Klasa::atrybut. Jeśli byśmy napisali static, byłby to static „C-właściwy” a nie o to nam chodzi :-)

Jeszcze tylko wspomnę dla tych którzy „nie wierzą kompilatorowi” i chcą mieć jasny kod, że dostęp do atrybutu static w metodach można wykonać poprzez podanie nazwy klasy, 2 dwukropków i nazwy atrybutu. Np. metoda getValue() może wyglądać tak:

Kod: Zaznacz cały

...
   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(ADCOversample oversample) {
      // Zerowanie wartości przed sumowaniem
      Adc::value = 0;
      // Pętla obliczająca sumę oversample
      for(uint16_t i = (1 << (oversample << 1)); i > 0; --i) {
         // Uruchomienie pomiaru
         runAdc();
         // Oczekiwanie na zakończenie pomiaru
         while(!complete());
         // Sumowanie próbek
         Adc::value += ADC;
      }
      return ( value >> oversample );
   }
...

To jest opcja, bo jeśli nawet nie podasz kompilatorowi Adc::*, to i tak zorientuje się on o jakie value chodzi. Jak tak w kodzie nie będę robił.

No dobrze, a metoda może być wspólna dla wszystkich obiektów? No pewnie! Nie będzie chyba zaskoczeniem że i w tym przypadku będzie to słowo static :-) Tu mamy jeden szczegół który jest bardzo istotny:

Jeśli metoda jest typu static, nie ma w swoim ciele dostępu do this czyli wskaźnika na egzemplarz klasy (czyli obiekt). Jak wspólna to wspólna. Co, miała by mieć dostęp do „tablicy this'ów”?! To by było nieracjonalnie kosztowne :-)

Sprawdźmy jak zdefiniować metodę statycznie. Dodaj przed getValue() static. Ma to wyglądać tak:

Kod: Zaznacz cały

...

   // Pobranie wartości pomiaru z oversample
   static uint16_t getValue(ADCOversample oversample) {
      // Zerowanie wartości przed sumowaniem
      value = 0;
...

Kompilujesz i otrzymujesz błąd że nie wiadomo gdzie jest complete()! No pewnie że nie wiadomo bo getValue() nie ma dostępu do this'a :-) Ok, to jak dostać się do complete(). Hmm.. uwspólnić complete? :-) Tak, ma dostać static! Wtedy dostaniemy się do niego poprzez Adc::complete() !

Tu mamy jeszcze jedną subtelność. Metoda static, nie posiadając dostępu do obiektu (bo przecież brak dostępu do this a this przecież tym jest), nie może go zmodyfikować. Tak więc słówko const przy deklaracji metody nie jest potrzebne ta metoda nie jest w stanie zmodyfikować obiektu. Jak widać C++ jest dość racjonalny ale jednocześnie restrykcyjny :-).

Uczynimy więc wszystkie metody static. Zamiast konstruktora nawet dodam metodę init() także statyczną, bo konstruktor nie może być static (zastanów się, z samej definicji konstruktor ma budować unikalny obiekt przecież a tutaj mamy nie mieć unikalnego tylko jeden). Na pytanie dlaczego usuwam konstruktor jest dość jasna odpowiedź. Jak występuje konstruktor (gdziekolwiek), to gcc w asemblerze dodaje sekcję konstruującą i wspierającą kopiowanie danych (wsparcie mechanizmów związanych z new). Jak nie ma konstruktora, to kod będzie mniejszy :-) Znów inteligencja kompilatora :-)

Po poprawkach, już bez „enumów” Adc.hpp wygląda tak:

Kod: Zaznacz cały

...
class Adc {
public:
   // Inicjalizacja
   static void init() {
      // Uruchomienie przetwornika ADC
      ADCSRA |= (1 << ADEN);
   }

   // Ustawienie preskalera
   static void setPrescaler(ADCPrescaler prescaler) {
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | prescaler;
   }

   // Ustawienie napięcia referencyjnego
   static void setReference(ADCReference reference) {
      // Ustawienie napięcia referencyjnego na bez naruszania kanału.
      ADMUX = ( ADMUX & ( (1 << REFS0) - 1) ) | ( reference << REFS0);
   }

   // Ustawienie kanału Adc
   static void setChannel(ADCChannel channel) {
      // Ustawienie kanału
      ADMUX = ( ADMUX & ~( (MUX4 << 1) - 1 ) ) | channel;
      return;
   }

   // Pobranie wartości pomiaru
   static uint16_t getValue() {
      // Uruchomienie pomiaru
      runAdc();
      // Oczekiwanie na zakończenie konwersji Adc
      while(!Adc::complete());
      // Zwrot wartości po konwersji
      return ADC;
   }

   // Pobranie wartości pomiaru z oversample
   static uint16_t getValue(ADCOversample oversample) {
      // Zerowanie wartości przed sumowaniem
      value = 0;
      // Pętla obliczająca sumę oversample
      for(uint16_t i = (1 << (oversample << 1)); i > 0; --i) {
         // Uruchomienie pomiaru
         runAdc();
         // Oczekiwanie na zakończenie pomiaru
         while(!Adc::complete());
         // Sumowanie próbek
         value += ADC;
      }
      return ( value >> oversample );
   }

   // Start konwersji
   static void runAdc() {
      // Start konwersji
      ADCSRA |= (1 << ADSC);
      return;
   }

   // Sprawdzenie czy zakończono pomiar
   static bool complete() {
      // Jeśli ADSC zgaszony, konwersja zakończona
      return !(ADCSRA & (1 << ADSC));
   }
private:
   // Wartość obliczana
   static uint32_t value;
};

uint32_t Adc::value = 0;

A w main.cpp dojdzie wywołanie init():

Kod: Zaznacz cały

...
   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // Inicjalizacja ADC
   myAdc.init();

   // Ustawienie napięcia referencyjnego
   myAdc.setReference(ADC_AVCC);
...

To bardzo ważny moment w cyklu tych tutoriali. Dalej zaczną się zagadnienia szablonowe. Jeśli więc coś wzbudza twój niepokój czy pytania, proszę do tego momentu opanuj zagadnienia.
Pamiętaj także że to tutorial. Nie bęzie w nim więc niektórych zagadnień zaawansowanych a moim celem jest przeprowadzenie Cię do użycia C++ na AVR. Stąd świadomie dokonuję wyboru zagadnień. Nie znaczy to także że jeśli zapytasz np. o metody wirtualne bo dowiesz się że są w C++ to nie odpowiem. Tego tematu nie poruszę, ale chętnie odpowiem jak/co/gdzie/ile to kosztuje w C++ na AVR no i ... dlaczego nie poruszę :-)
,,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