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

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

Przyznasz że kod który powstał w poprzedniej części może i jest sprytny, ale mało wygodny w użyciu. Po obliczeniach ilości kodu zużywanego na przesunięcia bitowe w asemblerze, podejmuję także decyzję o usunięciu metody static uint16_t getValue(ADCOversample oversample). Jeśli w kodzie użytkowym będę chciał mieć inne oversample (co będzie rzadkie) to samodzielnie je sobie obliczę :-) Tak naprawdę problemem rozważanym jest automatyczne wybranie typu danych atrybutu value w zależności od wartości Oversample przekazanego w argumencie.

Zadanie to można wykonać na bardzo wiele sposobów. Można pokusić się o:
1. Specjalizację całości klasy w zależności od ilości bitów oversample (ciężkie i ogromna ilość kodu)
2. Pracy na szablonach wyboru typu (jeden prosty szablon)
3. Pracy na szablonach funkcji (to można zrobić naprawdę cuda ale to wymaga poruszenia tematu częściowych specjalizacji w tym tutorialu)
4. Metaprogramowaniem... (no... tu trzeba bardzo dobrze operować na szablonach ale uwierz, tu można zrobić takie cuda jak obliczenia na etapie kompilacji, generowanie struktur danych itp... )

Zaprezentuję sposób 2. Myślę że najbardziej zrozumiały.

Mamy tak naprawdę zaimplementować 3 wersje getValue():
1. Wersja dla oversample == 0 czyli bez żadnej pętli w ciele metody.
2. Wersja dla oversample < 4 ta wersja ma operować na value typu uint16_t.
3. Wersja dla oversample > 3 a ta wersja ma operować na value typu uint32_t.

Tak jak sygnalizowałem to wcześniej, pozbędziemy się zmiennej value z atrybutu klasy bo tak naprawdę .... nie jest potrzebna.

Zacznijmy od zrobienia porządku w klasie Adc. Usuniemy atrybut value i przeniesiemy go do wnętrza metody. Zniknie sekcja private: z klasy oraz inicjalizacja samej zmiennej. Za to pojawi się definicja zmiennej value typu uint32_t w metodzie getValue().

Kod: Zaznacz cały

...
template<int Oversample, typename ValueType>
class Adc {
public:
   // Inicjalizacja
...
   // Pobranie wartości pomiaru
   static uint16_t getValue() {
      // Zerowanie wartości przed sumowaniem
      uint32_t value = 0;
...
   // Sprawdzenie czy zakończono pomiar
   static bool complete() {
      // Jeśli ADSC zgaszony, konwersja zakończona
      return !(ADCSRA & (1 << ADSC));
   }
};

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;
}

Taki kod po skompilowaniu zmniejszy objętość w sposób istotny a funkcjonalność zachowa.

Specjalizację dla ADC_OVERSAMPLE_0 zachowam bo jest ok. Implementacje getValue() z klasy Adc zostaną wybrane jeśli nie będą 0 (zerem) (czyli pkt 1 z listy załatwiony). Kompilator dopasuje specjalizacje bo idealnie pasuje to do wersji z zerem bez względu na to co jest zdefiniowane w klasie. To jest idealne dopasowanie.

Zasady wyboru dopasowań w dzablonach są dość rozbudowane. Jeśli chcesz abym ,,unurzał Cię w standardzie”, to się rozczarujesz :-) Teraz jeszcze nie jest to niezbędne. Chętnie jednak wskażę źródło jeśli naprawdę Cię to interesuje :-)

Przed definicją klasy, powinniśmy więc mieć „coś sprytnego” co wybierze typ :-) Nazwę ten szablon (a jak :-) ) Select. To będzie zwykła struktura która w zależności od typu bool (false/true) podaj jeden z typów:

Kod: Zaznacz cały

...
// Szablon wyboru typu
template<bool Choose, typename A, typename B>
struct Select {
   typedef A Type;
};
// Specjalizacja wyboru dla false
template<typename A, typename B>
struct Select<false, A, B> {
   typedef B Type;
};


template<int Oversample, typename ValueType>
class Adc {
public:
...

Zwróć uwagę. W tym szablonie zdefiniowany jest typedef w zależności od wartości wyboru. Będziemy mogli się do niego dostać przez np.:

Kod: Zaznacz cały

Select<true, uint16_t, uint32_t>::Type


Wtedy będzie dopasowanie przez szablon ogólny i Type będzie pierwszy typ uint16_t, lub

Kod: Zaznacz cały

Select<false, uint16_t, uint32_t>::Type


Wtedy będzie dopasowanie przez specjalizację i Type będzie drugim typem uint32_t.

Tak, to co jest zwracane to... typ a sam problem wyboru typu jest realizowany w czasie kompilacji oprogramowania :-)

To bardzo subtelna technika. Jest jedną ze składowych ogromnego zestawu narzędzi jakimi jest metaprogramowanie.

Takie małe narzędzia, warto gromadzić sobie w oddzielnym pliku nagłówkowym. Tu jednak będę miał je wbudowane w kod klasy dla prostoty prezentacji.

Dobrze, jak więc jej użyć? Proste. Typ danych value nie będzie wyglądał tak:

Kod: Zaznacz cały

...
uint32_t value = 0;
...

lub tak:

Kod: Zaznacz cały

...
uint16_t value = 0;
...

Tylko tak:

Kod: Zaznacz cały

...
typename Select< (Oversample < 4), uint16_t, uint32_t >::Type value = 0;
...

No oczywiście zrobimy taki typ wyłącznie w metodzie: static uint16_t getValue()

Komentarza wymaga słowo kluczowe typename. Pojawia się ono dlatego że użyty jest argument szablonu o nazwie Oversample a on wystąpił w argumentach szablonu Adc i kompilator powinien być poinformowany że ma do czynienia z tzw. typem zależnym. Zależnym od tego co definiowane jest w Adc. Samo Eclipse Ci o tym podpowie.

Ten typ danych zapisuję tuż po definicji klasy w części publicznej aby go użyć co do zmiennej value.

Kod: Zaznacz cały

...
template<int Oversample, typename ValueType>
class Adc {
public:
   // Typ danych na których przeprowadzane są obliczenia
   typedef typename Select< (Oversample < 4), uint16_t, uint32_t >::Type ValType;
   // Inicjalizacja
   static void init() {
...

Tu znowu wytnę część z enum prezentując kod po operacjach:

Kod: Zaznacz cały

...
// Szablon wyboru typu
template<bool Choose, typename A, typename B>
struct Select {
   typedef A Type;
};
// Specjalizacja wyboru dla false
template<typename A, typename B>
struct Select<false, A, B> {
   typedef B Type;
};

// Definicja klasy Adc obsługującej przetwornik .. ADC :-)
template<int Oversample, typename ValueType>
class Adc {
public:
   // Typ danych na których przeprowadzane są obliczenia
   typedef typename Select< (Oversample < 4), uint16_t, uint32_t >::Type ValType;
   // 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
      ValType 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));
   }
};

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;
}

No to teraz po co drugi argument w szablonie (argument ValueType)? Nie... nie jest on już potrzebny. Klasa sama dokonuje wyboru jak ma zostać zdefiniowana :-) Nie będzie potrzeby podawania w main.cpp typu danych.

Ostateczna definicja klasy Adc, inteligentnie dopasowującej typy danych i wykonującej obliczenia oversample, po analizie wywołań i kodu asemblera, dodaniu rozwijania inline (co powinieneś znać z bluebooka) aby oszczędzić pamięć, będzie przedstawiała się następująco:

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;


// Szablon wyboru typu
template<bool Choose, typename A, typename B>
struct Select {
   typedef A Type;
};
// Specjalizacja wyboru dla false
template<typename A, typename B>
struct Select<false, A, B> {
   typedef B Type;
};

// Definicja klasy Adc obsługującej przetwornik .. ADC :-)
template<int Oversample>
class Adc {
public:
   // Typ danych na których przeprowadzane są obliczenia
   typedef typename Select< (Oversample < 4), uint16_t, uint32_t >::Type ValType;
   // Inicjalizacja
   static inline void init() {
      // Uruchomienie przetwornika ADC
      ADCSRA |= (1 << ADEN);
   }

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

   // Ustawienie napięcia referencyjnego
   static inline 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 inline void setChannel(ADCChannel channel) {
      // Ustawienie kanału
      ADMUX = ( ADMUX & ~( (MUX4 << 1) - 1 ) ) | channel;
      return;
   }

   // Pobranie wartości pomiaru
   static inline uint16_t getValue() {
      // Zerowanie wartości przed sumowaniem
      ValType 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 inline void runAdc() {
      // Start konwersji
      ADCSRA |= (1 << ADSC);
      return;
   }

   // Sprawdzenie czy zakończono pomiar
   static inline bool complete() {
      // Jeśli ADSC zgaszony, konwersja zakończona
      return !(ADCSRA & (1 << ADSC));
   }
};

template<>
inline 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;
}

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_0> myAdc = Adc<ADC_OVERSAMPLE_0>();

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_128);

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

Po dokładnej optymalizacji w tym cyklu tutoriali, okazało się że kod klasy Adc nie zabiera ani jednego bajta RAM, (zerknij do pliku *.lss) strukturę ma zbliżoną do asemblerowego programu i jest bardzo elastyczna. Pozostaje jeszcze podłączenie do przerwania tak aby klasa sama wykonywała pomiary. To już jednak bardzo wykracza poza zakres tego cyklu :-) Dość jednak powiedzieć że mimochodem postawiony problem przez Mirka w cyklu filmów o Adc (chodzi o kłopotliwy tryb free-run), może znaleźć swoje proste rozwiązanie. Jeśli ma być suplement z free-run Adc na przerwaniach w C++, to dajcie znać.

Na tym etapie kończę ten cykl i chcę zapytać: „Jak wam się to podoba?”. Pytam jednak wyłącznie tych którzy poświęcili czas aby przebrnąć ze mną przez ten proces myślowy i tworzenia kodu. Dotknęli implementacji w stylu obiektowym i widzą jaka jakość kodu powstaje. Tych którzy chcą argumentować w stylu „bo ja słyszałem że...” zapraszam do innego wątku. Bajki, klechdy i legendy. Wysłuchać warto jedynie rzeczowych argumentów. Taki kod jaki przedstawiłem zajmuje (trochę) mniej pamięci niż w C i ma bez porównania większą elastyczność jeśli chodzi o stosowanie.

W zależności od zainteresowania, może pojawić się cykl o podejściu obiektowym do tworzenia programu, jednak większość klas w kodzie dla MCU będzie wyglądała bardzo podobnie tak jak to widzisz dla Adc. Będą statyczne i będą używały szablonów :-) Główny powód to ATmega która ma wydzieloną przestrzeń danych od programu (Flash i RAM) oraz specyfika systemu wbudowanego który w zasadzie startuje i nigdy nie kończy swojego działania.

Aby określić jakie zagadnienie może być bardziej interesujące, proszę klinknij na ankietę nad tym tutorialem. Nie wiem bowiem czy raczej zaproponować większy nacisk na obiektowość, szablony czy różnice pomiędzy C i C++.
,,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