ADC 4 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 4 z 5

Postautor: mokrowski » sobota 08 lip 2017, 13:03

W poprzednich tutorialach poruszyliśmy istotne zagadnienia związane z programowaniem obiektowym dla MCU. Chciałbym wytłumaczyć dlaczego nie będę mocno teraz zgłębiał tematyki kreowania nowych obiektów w C++ na platformie ATmega. Przede wszystkim dlatego że chcę abyś jak najszybciej mógł tworzyć wydajny kod w C++ na ATmega. To ostatnie można zrobić z użyciem techniki szablonów.

Zwróć uwagę że AVR posiada wydzieloną pamięć programu (Flash) od pamięci danych (RAM) a kreowanie nowego obiektu w aplikacjach na mikrokontrolery samo z siebie jest stosunkowo rzadkie. Z reguły kod się uruchamia i działa aż zabraknie prądu/wyłączysz urządzenie itp. System operacyjny (jeśli jest) to jest to system czasu rzeczywistego z i tak „wbitym na stałe” zestawem procesów. Stąd techniki powoływania do życia nowych bytów na MCU są ograniczone (pytanie do zaawansowanych: kiedy ostatnio użyłeś malloc() na ATmega :) ? ).

Następną właściwością związaną z AVR'em jest to, że nie wykona on kodu natywnego procesora z RAM. Za każdym więc razem gdy będę kreował nowy obiekt, kod trafi do Flash i będzie wskazywał do RAM na elementy typu atrybuty, wskaźniki itp. Taki spryt, narzucony architekturą MCU, kosztuje objętość kodu. Oczywiście tych problemów nie ma na PC lub mikrokontrolerach które wykonują kod z RAM. Są za to inne problemy :-)

Techniki programowania obiektowego w C++ na AVR 8-bitnajwiększą więc siłę mają w systemie szablonów. Mechanizmów które (w przeciwieństwie do makr w C), bardzo dbają o typ i „rozumieją” język. Dzięki nim będzie można dostosować kod „aż do bólu” tak aby mieścił się na niewielkiej przestrzeni pamięci Flash a mechanizmy dziedziczenia, operacji na typach itp. pozwolą tworzyć kod możliwy do ponownego użycia. Stąd też C++ „pokaże pazury” w kontekście tworzenia kodu reużywalnego i komponentów bibliotecznych. Dzięki rozbudowanym mechanizmom hermetyzacji, będzie można tworzyć z jego użyciem bardzo elastyczny kod.

Nie chcę abyś miał/miała wrażenie że wszystko co związane z kreowaniem obiektu w C++ jest nieważne. Jest ważne! Warto się tego nauczyć! Pokażę to (o ile będzie zainteresowanie) w tym tutorialu po zagadnieniu szablonów jako suplement. Teraz jednak odeślę zainteresowanych do literatury tego tematu a my zajmijmy się systemem szablonów :-)

Przypominam klasę Adc. Działała, robiła oversample, można jej było przełączyć kanał itp.
Pomyślmy jednak czy niezbędne będzie przełączanie zawsze oversample w naszej klasie?

Jeśli tak, technika szablonu się nam nie przyda bo wygeneruje ona kod wyspecjalizowany dla danego oversample. Jeśli nie potrzebujemy przestawiać oversample, to szablon będzie w sam raz. Nie pozostaje mi nic innego tylko rzucić monetą.....

Załóżmy że .... stanęła na sztorc :-) Pokażę implementację która będzie jednocześnie wyspecjalizowana i „da sobie przestawić” :-)

Szablon w C++ definiujemy z użyciem słowa kluczowego template. Argumenty tegoż szablonu, przekazujemy w nawiasach ostrokątnych < i >, tak jak by to były argumenty funkcji. Posiadają one informacje o typie oraz nazwę atrybutu. Oddzielone są przecinkami. Tak jak w funkcji znanej z C :-)

Dzięki takiemu użyciu, będę mógł teraz użyć słowa Oversample tam, gdzie będzie mi to potrzebne aby zmodyfikować sposób generowania kodu. Tak, generowania jeszcze przed kompilacją :-)

Nasze Adc będzie przyjmowało 1 argument int i będzie to ilość bitów oversample.

Do Adc.hpp, dopisujemy więc:

Kod: Zaznacz cały

...
template<int Oversample>
class Adc {
...

Od tego momentu kompilator oczekuje że klasa nazywa się Adc<2> lub Adc<1> lub cokolwiek co zawiera „int'a” ... Jeśli tego oczekuje to spójrz na wyciągniętą inicjalizację atrybutu statycznego na dole klasy. Chodzi o pierwotne:

Kod: Zaznacz cały

...
   static uint32_t value;
};

uint32_t Adc::value = 0;

Dostęp do niego także powinien być zdefiniowany szablonem. No to zróbmy to:

Kod: Zaznacz cały

...
   static uint32_t value;
};

template<int Oversample>
uint32_t Adc<Oversample>::value = 0;

Wyprzedzam ewentualne pytania. Równie dobrze to co jest po int w argumentach template, może nazywać się ALAMAKOTA, byle było by w:
uint32_t Adc<ALAMAKOTA>::value = 0; ... to nie będzie problemu :-) To jest zwykłe zastępowanie.

No tak. Adc jest jeszcze używany w main.cpp. Tu także zmieni się dostęp:

Kod: Zaznacz cały

...
static Adc<ADC_OVERSAMPLE_2> myAdc = Adc<ADC_OVERSAMPLE_2>();
...

I w samej pętli należy wywołać getValue() bez argumentu oversample.

Blee..... ile pisania. No to masz wybór. Uprzedzam, tę właściwość posiada dopiero C++11 (i gcc nieco wcześniejsze). Zamiast klepać po static długą nazwę, możesz zrobić tak:

Kod: Zaznacz cały

...
static auto myAdc = Adc<ADC_OVERSAMPLE_2>();
...

Kompilator sam domyśla się o jaką klasę chodzi. Ja nie będę tego używał bo obiecałem że z tematami C++11 „nie będę wyskakiwał przed szereg” :-)

Tu zaraz „biegli w makrach” zapytają: „A co to za różnica w stosunku do Makr w C? Phi... ”
Jest różnica. Zwróć uwagę że system szablonów wie wszystko o typach w języku C++ i o miejscu gdzie argument szablonu ma sens w C++ a makra w C lub C++ to zupełnie nie obchodzi. Jeśli ktoś pisał coś bardziej skomplikowanego na makrach, to wie że to ... emocjonujące doświadczenie właśnie z powodu „olewania całkowitego języka rdzennego” :-)

Dobrze, ale po co te szablony? Kreując teraz klasę, przekazujemy jej wartość int. Z użyciem tej wartości będziemy mogli tworzyć specjalizowane wersje naszych kawałków kodu!

Atrybut przekazany do szablonu powinien być klasą lub typem całkowitym stałym.

Dociekliwi zapytają czy można jeszcze coś innego przekazać? Oczywiście. Nawet szablon :-) Teraz jednak pozwoliłem sobie na delikatne uproszczenie które raczej buduje obraz niż go zaburza.

Zmodyfikujmy więc metodę: static uint16_t getValue(), tak by stosowała oversample. To bardzo proste. Wystarczy skopiować kod z: static uint16_t getValue(ASCOversample oversample) i zamiast oversample (z małej litery) podstawić argument szablonu :-) Jak się jednak przekonasz za chwilę kod: getValue() (bezargumentowy) jeszcze się nam przyda więc jedynie go zakomentuj.

Metoda po zmianach wygląda tak:

Kod: Zaznacz cały

...
   // 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;
      // 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 );
   }
...

Mhm... fajnie. Ale.. jeśli podajemy Oversample (w argumencie szablonu) o wartości 0, to tak naprawdę nie chemy oversample! Cała pętla (choć zadziała poprawnie) jest szalenie niewydajna bo po co ją wykonywać, po co wreszcie ją wbudowywać w kod? Wybacz.. takich rzeczy to się już kompilator sam nie domyśli :-) My jednak mamy potrzebę aby kod miał „specjalną wersję” dla Adc<0>.

Do takich wymagań stosujemy specjalizację szablonu.

Specjalizacja szablonu to ukonkretniony kod dla szczególnego przypadku argumentów szablonu.

Specjalizacje mogą być bardzo rozbudowane i mają swoje rodzaje ale my idziemy prosto do celu bez rozpraszania się w tej chwili.

Nas interesuje wyłącznie specjalizacja dla jednej metody a dokładniej dla:

Kod: Zaznacz cały

...
uint16_t Adc<ADC_OVERSAMPLE_0>::getValue() {
....

Czyli tworzenia klasy dla oversample == 0. Tam nie będzie żadnej pętli liczącej próbki.

Mam nadzieję że to rozumiesz? Jeśli nie to pisz. Jak taką specjalizację zrobić? Bardzo prosto:

Kod: Zaznacz cały

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

Teraz rozumiesz dlaczego radziłem nie kasować kodu w metodzie bo się przyda :-)

Specjalizacja szablonu ma puste elementy argumentów (w <> ). Taka specjalizacja to specjalizacja całkowita lub pełna (dla dociekliwych jeśli szablon ma kilka argumentów, nie ma obowiązku by wszystkie były puste, to może to być specjalizacją częściową) i w definicji wymieniony argument szablonu (w tym przypadku 0).

Teraz mamy magię. Po wywołaniu w main.cpp Adc<0> kompilator sam wbuduje wyspecjalizowany element a przy innych wartościach zastosuje ogólny :-) Możesz teraz to sprawdzić. Kompilacja programu z wprowadzonym w main:

Kod: Zaznacz cały

...
// Kreowanie obiektu Adc
static Adc<ADC_OVERSAMPLE_0> myAdc = Adc<ADC_OVERSAMPLE_0>();
...

lub:

Kod: Zaznacz cały

...
// Kreowanie obiektu Adc
static Adc<ADC_OVERSAMPLE_6> myAdc = Adc<ADC_OVERSAMPLE_6>();
...

Dla programu:

Kod: Zaznacz cały

#include <util/delay.h>
#include <avr/interrupt.h>
#include "Adc.hpp"
#include "mkuart.h"

// Kreowanie obiektu Adc
static Adc<ADC_OVERSAMPLE_2> myAdc = Adc<ADC_OVERSAMPLE_2>();

int main(void) {
   // Uruchomienie przerwań potrzebnych dla usart
   sei();

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // Inicjalizacja ADC
   myAdc.init();

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

   // Ustawienie kanału 0 jako wejściowego
   myAdc.setChannel(ADC_CHANNEL_0);

   // Ustawienie preskalera
   myAdc.setPrescaler(ADC_PRESCALER_64);

   // Inicjalizacja USART
   USART_Init(50);      // to jest baudrate dla 8MHz i 9600bps

   while(true) {
      // Wysłanie napisu na usart
      uart_puts("Adc 0: 0x");

      // Pobranie wartości z przetwornika
      value = myAdc.getValue();

      // Wypisanie watości
      uart_putint(value, 16);

      // Nowa linia na usart
      uart_puts("\r\n");

      // Opóźnienie przed następnym pomiarem
      _delay_ms(1000);
   }
}

Daje różne wyniki w kodzie asemblera i wielkości wsadu.

Zróbmy małą stop-klatkę i chwilowy przeskok do innego układu peryferyjnego aby uświadomić do czego generalnie może się przydać szablon. Było by chyba wygodne aby można było mając np. 2 USART'y na danym MCU pisać:

Kod: Zaznacz cały

...
Usart<0> myUsart0 = Usart<0>();
...

lub:

Kod: Zaznacz cały

...
Usart<1> myUsart1 = Usart<1>();
...

Albo podawać argumenty że USART jest buforowany, niebuforowany, ma bufor o określonej długości, będzie „blokujący lub nie”. Rozumiesz? Szablon wygeneruje ci kod wyspecjalizowany do danego zagadnienia. Zrobi to lepiej i elastyczniej niż makro. Przy takich wymaganiach w makrach „osiwiejesz” a jak nauczysz się szablonów to generowanie kodu z „USART'em na buforze cyklicznym w projekcie A” i „USART'em bezpośrednim na portach w projekcie B”, na podstawie tego samego kodu biblioteki będzie w szablonie łatwe :-)

Wracamy do głównego wątku.

To jednak nie koniec. Wróć do odcinka 2 w którym obliczaliśmy ilość próbek dla oversample. Podjąłem tam decyzję że niestety dla całego spektrum oversample (od 0 do 6 bitów), zmieszczę się jedynie na zmiennej value typu uint32_t. No dobrze, dla całego a dla zakresu od 0 do 3? Dla tego zakresu powinno wystarczyć uint16_t. Czy można coś z tym zrobić? Jasne że można :-)

Zrobimy to w tym odcinku najpierw tak aby zrozumieć (choć to będzie nieco niewygodne).

Dodajmy do argumentów szablonu jeszcze jeden. Będzie to typ przekazywany do podstawienia dla atrybutu value. Deklaracja szablonu klasy będzie wtedy wyglądała tak:

Kod: Zaznacz cały

...
template<int Oversample, typename ValueType>
class Adc {
...
private:
   // Wartość obliczana
   static ValueType value;
};

A pod klasą znajdą się kod szablonu dla value i specjalizacje dla getValue() :

Kod: Zaznacz cały

...
};

template<int Oversample, typename ValueType>
ValueType Adc<Oversample, ValueType>::value = 0;

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

Zmieni się także wywołanie w main.cpp na np. takie:

Kod: Zaznacz cały

...
// Kreowanie obiektu Adc
static Adc<ADC_OVERSAMPLE_2, uint16_t> myAdc = Adc<ADC_OVERSAMPLE_2, uint16_t>();
...

Jak widzisz doszło jakieś słowo typename i typ ValueType. Zamiast słowa typename, zastosować można także słowo class. W działaniu różnicy nie ma. Ale mam sugestię aby słowo typename stosować dla typów rdzennych języka (int, float, long, ....) a słowo class dla swoich własnych „grubych” klas. To naprawdę porządkuje kod. Zrobisz jak zechcesz. To dlaczego są 2 słowa dotyczące tego samego pojęcia, to raczej wiedza historyczna związana z rozwojem języka. Uwierz, to nic nie wnosi do jego stosowania, bo czy class czy typename to działa tak samo. Niemniej jednak namawiam do trzymania się konwencji pokazanej wyżej (typename do typów rdzennych, a class dla „nowych klas”).

Sprawdź teraz, co będzie działo się jak podasz wartości oversample z zakresu od 0 do 3 z typem uint16_t i od 4 do 6 z typem uint32_t w main.cpp. Kod się sam dostosowuje :-) Zmienia się jego zawartość, objętość oraz zajęcie RAM. Wiem wiem.. jest to niewygodne aby podawać (a raczej pamiętać) o typie przekazywanym do szablonu. Uprzedzałem o tym. Poradzimy sobie i z tym w następnej części...

Zaraz ktoś uważny powie że value zupełnie jest nie potrzebne jako argument klasy. To prawda :-) Ale to znów już w następnej części....

Na koniec jeszcze zawartość plików do własnych doświadczeń. W Adc.hpp dokonałem kosmetycznej poprawki przy enum definiując go jako typedef.

main.cpp:

Kod: Zaznacz cały

#include <util/delay.h>
#include <avr/interrupt.h>
#include "Adc.hpp"
#include "mkuart.h"

// Kreowanie obiektu Adc
static Adc<ADC_OVERSAMPLE_5, uint32_t> myAdc = Adc<ADC_OVERSAMPLE_5, uint32_t>();

int main(void) {
   // Uruchomienie przerwań potrzebnych dla usart
   sei();

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // Inicjalizacja ADC
   myAdc.init();

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

   // Ustawienie kanału 0 jako wejściowego
   myAdc.setChannel(ADC_CHANNEL_0);

   // Ustawienie preskalera
   myAdc.setPrescaler(ADC_PRESCALER_64);

   // Inicjalizacja USART
   USART_Init(50);      // to jest baudrate dla 8MHz i 9600bps

   while(true) {
      // Wysłanie napisu na usart
      uart_puts("Adc 0: 0x");

      // Pobranie wartości z przetwornika
      value = myAdc.getValue();

      // Wypisanie watości
      uart_putint(value, 16);

      // Nowa linia na usart
      uart_puts("\r\n");

      // Opóźnienie przed następnym pomiarem
      _delay_ms(1000);
   }
}


Adc.hpp:

Kod: Zaznacz cały

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

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

typedef enum  {
   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
} ADCPrescaler;

typedef enum  {
   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.
} ADCChannel;

typedef enum  {
   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
} ADCOversample;

template<int Oversample, typename ValueType>
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() {
      // 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 );
   }

   // 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 ValueType value;
};

template<int Oversample, typename ValueType>
ValueType Adc<Oversample, ValueType>::value = 0;

template<>
uint16_t Adc<ADC_OVERSAMPLE_0, uint16_t>::getValue() {
   // Uruchomienie pomiaru
   runAdc();
   // Oczekiwanie na zakończenie konwersji Adc
   while(!Adc::complete());
   // Zwrot wartości po konwersji
   return ADC;
}
,,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