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

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

W poprzednim odcinku naszej telenoweli obiektowej (;-) ), zajmowaliśmy się Adc. Dziś czas na oversamplng nazywany nadpróbkowaniem. Myślę że znajdziesz wyjaśnienie tego pojęcia teoretycznie w wyszukiwarce z łatwością. Ja skupię się wyłącznie na praktyce. Nie będę wchodził w wyjaśnianie na jakiej zasadzie to działa i że dobrze że pomiary delkatnie szumią, przyjmij na wiarę. Do tej pory nie zawiodłem. Algorytm jest nawet opisany w nocie Atmel'a.

Cała rzecz polega na sumowaniu wyników określoną liczbę razy i na końcu przesunięciu wyniku w prawo o zadaną liczbę bitów.

Jeśli chcemy uzyskać 1 bit więcej niż dostępny przetwornik (w AVR 12 bitów), należy zebrać 4 próbki, zsumować je i wynik przed zwróceniem przesunąć w prawo o 1 bit (czyli podzielić przez dwa). Wzór jest dość sprytny. Potraktuj znak ^ jako potęgowanie.

k – ilość dodatkowych bitów które chcę otrzymać

suma_próbek = 4^k
wynik_z_oversample = suma_próbek >> k;

Czyli dla k równego to co niżej, ilość próbek będzie wynosiła:

k | ilość próbek
--+-----------------
1 | 4
2 | 16
3 | 64
4 | 256
5 | 1024
6 | 4096
...

U... jak widać ilość próbek sumowanych bardzo szybko wzrasta z ilością bitów które chcę odzyskać. Jak mówiłem, nie ma nic za darmo :-/

No fajnie, ale jak uzyskać to potęgowanie bez stosowania zmiennego przecinka w C lub C++? Zwróć uwagę, czy ( 1 << ( k << 1 )) nie jest równe „4 do potęgi k” ?
Pamiętać tylko należy przez zwróceniem wartości o przesunięciu w prawo o k bitów.

Kod ustawienia licznika oversample i wykonania pomiaru będzie więc wyglądał tak:

Kod: Zaznacz cały

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


Wywołanie w main.cpp to:

Kod: Zaznacz cały

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

Kompilujemy przykład i sprawdzamy czy rzeczywiście wykonuje pomiary przyłączając do pinu mikrokontrolera potencjometr. W filmach Mirka jest to wszystko opisane dokładnie więc tu już nie będę tego powtarzał a już szczególnie wymagań co do podłączenia sygnału i dobrego filtrowania napięcia. Kod działa i mierzy. Będziesz zdziwiony jak dokładnie wygląda pomiar o ile dobrze zasiliłeś mikrokontroler i wyfiltrowałeś napięcia. A jeśli pomiar jest niedokładny, możesz przesunąć o jeszcze jeden bit w prawo :-)

Krytycznym okiem patrzę teraz na kod i widzę niepotrzebne powtórzenia. Zarzuty do kodu to:
1. Napięcia referencyjne są ustawiane 2 razy a po co?
2. Preskaler jest ustawiany w 2 konstruktorach.
3. Tak samo dzieje się z ustawieniem kanału w konstruktorach.
4. Jak szydło z worka wyszedł nieprzemyślany do końca interfejs ob metoda setOversample() jest niepotrzebna (wyjaśnię niżej)
5. Są jakieś paskudne wartości hex które przesyłam a za 3 msc. zapomnę co znaczą w ATmega16.

Myślę że ustawienie napięcia referencyjnego kwalifikuje się no nowej metody w tej klasie. Podobnie będzie z preskalerem. Dodam więc je.

Zadać sobie także należy pytanie po co mi teraz metoda setOversample() jeśli i tak w wywołaniu pomiaru z oversample podaję wartość odtwarzanych bitów. Metodę setOversample() więc usunę.

Bardzo nie podoba mi się także przesyłanie „magicznych” wartości do preskalera i do napięcia referencyjnego. One w kodzie powinny być opisowe. Kod mam czytać jak książkę :-) Zdefiniuję więc wyliczenia z tymi ustawieniami.

Po uzupełnieniu kod 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
};

class Adc {
public:
   // Konstruktor
   Adc() : value(0), counter(0) {
      // Uruchomienie przetwornika ADC
      ADCSRA = (1 << ADEN);
      // Ustawienie napięcia referencyjnego na AVCC oraz kanału na 0
      setReference(ADC_AVCC);
      setChannel(0x00);
      // Ustawienie preskalera na 128
      setPrescaler(ADC_PRESCALER_128);
   }

   // Konstruktor ustawiający preskaler
   Adc(ADCPrescaler prescaler) : value(0), counter(0) {
      // Uruchomienie przetwornika ADC
      ADCSRA = (1 << ADEN);
      // Ustawienie napięcia referencyjnego na AVCC oraz kanału na 0
      setReference(ADC_AVCC);
      setChannel(0x00);
      // Ustawienie preskalera
      setPrescaler(prescaler);
   }

   // 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) {
      // Ustawienie napięcia referencyjnego na bez naruszania kanału.
      ADMUX = ( ADMUX & ( (1 << REFS0) - 1) ) | ( reference << REFS0);
   }

   // Ustawienie kanału Adc
   void setChannel(uint8_t 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(uint8_t oversample) {
      // Ustawienie licznika
      counter = ( 1 << ( oversample << 1));
      // Zerowanie wartości przed sumowaniem
      value = 0;
      // Pętla obliczająca sumę oversample
      for(uint16_t i = counter; 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
   uint16_t value;

   // Licznik oversample
   uint16_t counter;
};

Zwróć uwagę że zmienił się typ argumentu w dodanych metodach obsługi napięcia referencyjnego oraz preskalera. To jest typ enum. Teraz te metody przyjmą nazwy symboliczne oprócz ,,magicznych liczb” :-) Jak jesteś ,,hardkorem”, to możesz wpisywać liczby, jak jesteś normalny to użyjesz wyliczenia :-) Proszę, nie bądź ,,hardkorem” bo jak widzę „magiczne liczby” to chcę ... zabić :-P Zobacz jak kod teraz ładnie się czyta. Co również istotne podanie wartości spoza zdefiniowanych zaowocuje błędem kompilacji! To bardzo dobrze że nie będziemy mogli ustalić napięć referencyjnych spoza tych zdefiniowanych :-)

Obliczenia sumy oversample będą dla podanych bitów od 4 do 6 przekraczały uint16_t (zerknij wyżej na obliczenia dla k = 6, to aż 4096 próbek) (atrybut prywatny value w klasie Adc). Niestety będę musiał go zmienić na uint32_t.

Zadam sobie także pytanie czy naprawdę koniecznie mam trzymać atrybut counter kiedy łatwo można go wyliczyć ( przypominam że chodzi o: counter = ( 1 << ( oversample << 1)); ). I tak pętla obliczeń będzie trwała długo a pamięci RAM nigdy za wiele :-)

Po co mi konstruktor z przesłaniem preskalera jeśli od tego mam dedykowaną metodę? Zmieni się trochę kod klasy (zmniejszy się!) i w main.cpp będzie porządniej. Baaa... po co w konstruktorze dotykanie napięcia referencyjnego i preskalera jak mogę ustawić to z użyciem metod. Wprawdzie w main.cpp będzie trochę więcej wywołań ale za to będą bardzo opisowe.

Oto kod po poprawkach. W takim stanie nie wzbudza wstydu i nie świadczy że pisana była „chałtura” :-)

Jednocześnie jest to kod, od którego może zacząć osoba która zna trochę C++ bo „miała na studiach”. Od tego momentu będzie wiele zagadnień dotyczących stricte MCU.

main.cpp:

Kod: Zaznacz cały

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

// Kreowanie obiektu Adc
Adc myAdc = Adc();

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

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // 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) {
      // Uruchomienie pomiaru
      myAdc.runAdc();

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

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

      // 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>

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) {
      // 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;

};

Proponuję abyś teraz kod skompilował i uruchomił. Po kompilacji proszę zapisz wielkość wsadu. Będziemy do tego wracali. U mnie jest to:

Kod: Zaznacz cały

Device: atmega16

Program:     796 bytes (4.9% Full)
(.text + .data + .bootloader)

Data:         70 bytes (6.8% Full)
(.data + .bss + .noinit)

Ci którzy mieli na studiach C++ lub wiedzą jak się tworzy kod w tym języku zdziwieni są faktem że całą klasę definiowałem w pliku *.hpp w otoczeniu class Adc { ... }; . Śpieszę z wyjaśnieniem. Chciałem pokazać że taki kod powoduje wbudowanie wszystkich metod w ciało klasy. A z racji tego że kompilator taki kod otrzymuje, przyjmuje że będziesz chciał gdzieś zrobić sizeof(...) na klasie, kopiować ją itp. Czyli pracować na niej bajt w bajt :-) Zerknij teraz do pliku *.lss w projekcie. O ile dodałeś w 1 tutorialu do przełączników kompilatora -g, to zobaczysz kod asemblera przemieszany z kodem C++.

Porządny czytelnik bluebooka został jednak nauczony (i dobrze) że co innego plik nagłówkowy a co innego kod. Tak, w C++ także się tak robi. Zrealizujmy więc odseparowanie definicji kodu od deklaracji klasy. Rzecz jasna stopniowo.

Najpierw wydzielimy metodę: void runAdc() const. W kodzie klasy pozostawimy jedynie jej deklarację, a poniżej kodu klasy wpiszemy definicję.

Kod: Zaznacz cały

...
class Adc {
public:
...
   // Start konwersji
   void runAdc() const;
...
};

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

Zwróć uwagę jak wygląda definicja metody „wyciągniętej” z klasy. Jest to typ zwracany, nazwa klasy, dwa dwukropki, nazwa metody, ew. const i ciało. Konstruktor jeśli będzie wyciągnięty poza deklarację klasy, także będzie tak wyglądał. Jedynie nie będzie typu zwracanego.

Proszę, wydziel teraz samodzielnie wszystkie metody z klasy, włącznie z konstruktorem i umieść je pod deklaracją klasy.

Dla leniwców (choć pamiętaj że lenistwo się zemści...), umieszczam gotowy plik Adc.hpp. Pozbawię go jednak części z definicją enum'ów, bo one nic nie wnoszą.

Kod: Zaznacz cały

...
class Adc {
public:
   // Konstruktor
   Adc();

   // Ustawienie preskalera
   void setPrescaler(ADCPrescaler prescaler) const;

   // Ustawienie napięcia referencyjnego
   void setReference(ADCReference reference);

   // Ustawienie kanału Adc
   void setChannel(ADCChannel channel) const;

   // Pobranie wartości pomiaru
   uint16_t getValue() const;

   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(ADCOversample oversample);

   // Start konwersji
   void runAdc() const;

   // Sprawdzenie czy zakończono pomiar
   bool complete() const;
private:
   // Wartość obliczana
   uint32_t value;

};

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

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

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

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

// Pobranie wartości pomiaru
uint16_t Adc::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 Adc::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 Adc::runAdc() const {
   // Start konwersji
   ADCSRA |= (1 << ADSC);
   return;
}

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

Kompilujemy program i wynik:

Kod: Zaznacz cały

Device: atmega16

Program:     862 bytes (5.3% Full)
(.text + .data + .bootloader)

Data:         70 bytes (6.8% Full)
(.data + .bss + .noinit)

Objętość wsadu przyrosła! Kontynujemy jednak....

Przeniesiemy definicje kodu metod w klasie, do odzielnego pliku Adc.cpp.

Zakładam nowy plik w projekcie, zaznaczam Adc.hpp wszystkie definicje metod i przenoszę do Adc.cpp. Oczywiście w Adc.cpp dodam wczytywanie nagłówka Adc.hpp :-)

Wspaniale! Tak oto mamy wydzieloną deklarację klasy od jej definicji metod. Jeszcze stan plików aby się nie pogubić. Tu już podam pełną wersję:

Adc.hpp:

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

   // Ustawienie preskalera
   void setPrescaler(ADCPrescaler prescaler) const;

   // Ustawienie napięcia referencyjnego
   void setReference(ADCReference reference);

   // Ustawienie kanału Adc
   void setChannel(ADCChannel channel) const;

   // Pobranie wartości pomiaru
   uint16_t getValue() const;

   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(ADCOversample oversample);

   // Start konwersji
   void runAdc() const;

   // Sprawdzenie czy zakończono pomiar
   bool complete() const;
private:
   // Wartość obliczana
   uint32_t value;
};


Adc.cpp:

Kod: Zaznacz cały

#include "Adc.hpp"

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

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

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

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

// Pobranie wartości pomiaru
uint16_t Adc::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 Adc::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 Adc::runAdc() const {
   // Start konwersji
   ADCSRA |= (1 << ADSC);
   return;
}

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


main.cpp:

Kod: Zaznacz cały

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

// Kreowanie obiektu Adc
Adc myAdc = Adc();

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

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // 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) {
      // Uruchomienie pomiaru
      myAdc.runAdc();

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

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

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

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

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


Ostatnim zagadnieniem które w tej części omówię, będzie już poruszony temat extern ”C” czyli łączenia C z C++ i odwrotnie.

Zwróć uwagę że po skompilowaniu projektu tego tutoriala, powstały pliki obiektów:
1. mkuart.o – Mikowa obsługa uart'a: to jest napisane w C
2. main.o – główna część programu: to jest napisane w C++
3. Adc.o – implementacja metod Adc: to jest napisane w C++

Proszę uruchom teraz konsolę (dla Windows to cmd, dla GNU/Linux jakiś terminal) i przejdź do katalogu workspace/nazwa_naszego_projektu/Release.

Uruchom w linii komend polecenie avr-objdump jak jak zobaczysz poniżej. I zwróć uwagę na wyniki.

Polecenie: avr-objdump -t mkuart.o

Kod: Zaznacz cały

mkuart.o:     file format elf32-avr

(...)
00000000 g     F .text.USART_Init       00000018 USART_Init
00000000 g     F .text.__vector_13      00000016 __vector_13
00000000 g     F .text.uart_putc        00000022 uart_putc
00000001       O *COM*  00000001 UART_TxHead
00000001       O *COM*  00000001 UART_TxTail
00000010       O *COM*  00000001 UART_TxBuf
00000000 g     F .text.uart_puts        00000018 uart_puts
00000000 g     F .text.uart_putint      0000003a uart_putint
00000000         *UND*  00000000 __itoa
00000000 g     F .text.__vector_12      0000004e __vector_12
00000000 g     F .text.uart_getc        0000002a uart_getc
00000001       O *COM*  00000001 UART_RxHead
00000001       O *COM*  00000001 UART_RxTail
00000020       O *COM*  00000001 UART_RxBuf
00000000 g     F .text.__vector_11      00000042 __vector_11
00000000         *UND*  00000000 __do_clear_bss

Popatrz jak nazywają się metody które zdefiniował Mirek. Ich nazwy włączone w plik binarnym (uart_puts, uart_putint), są podobne do tych jakie ma w pliku źródeł. Tak jest w języku C. On odróżnia metody po nazwach.

Na marginesie, widzisz że vector'y i inne elementy zaczynają się od 2 znaków podkreślnika? Wyjaśnia się teraz dlaczego zalecałem odczepienie się od nazw i makr z dwoma podkreślnikami :-)

Wracamy jednak do C++. Teraz sprawdzimy jak nazywają się metody budowane w C++:

Polecenie: avr-objdump -t Adc.o

Kod: Zaznacz cały

Adc.o:     file format elf32-avr

(...)
00000000 g     F .text._ZN3AdcC2Ev      0000000e _ZN3AdcC2Ev
00000000 g     F .text._ZN3AdcC2Ev      0000000e _ZN3AdcC1Ev
00000000 g     F .text._ZNK3Adc12setPrescalerE12ADCPrescaler    0000000a _ZNK3Adc12setPrescalerE12ADCPrescaler
00000000 g     F .text._ZN3Adc12setReferenceE12ADCReference     00000012 _ZN3Adc12setReferenceE12ADCReference
00000000 g     F .text._ZNK3Adc10setChannelE10ADCChannel        0000000a _ZNK3Adc10setChannelE10ADCChannel
00000000 g     F .text._ZNK3Adc8getValueEv      0000000a _ZNK3Adc8getValueEv
00000000 g     F .text._ZN3Adc8getValueE13ADCOversample 00000066 _ZN3Adc8getValueE13ADCOversample
00000000 g     F .text._ZNK3Adc6runAdcEv        00000004 _ZNK3Adc6runAdcEv
00000000 g     F .text._ZNK3Adc8completeEv      0000000e _ZNK3Adc8completeEv

Ojej! Jakie robaczki. W nich zakodowane są także typy argumentów wywołań funkcji.

Spróbuję je odkodować:

Polecenie: avr-objdump -Ct Adc.o

Kod: Zaznacz cały

Adc.o:     file format elf32-avr

(...)
00000000 g     F .text._ZN3AdcC2Ev      0000000e Adc::Adc()
00000000 g     F .text._ZN3AdcC2Ev      0000000e Adc::Adc()
00000000 g     F .text._ZNK3Adc12setPrescalerE12ADCPrescaler    0000000a Adc::setPrescaler(ADCPrescaler) const
00000000 g     F .text._ZN3Adc12setReferenceE12ADCReference     00000012 Adc::setReference(ADCReference)
00000000 g     F .text._ZNK3Adc10setChannelE10ADCChannel        0000000a Adc::setChannel(ADCChannel) const
00000000 g     F .text._ZNK3Adc8getValueEv      0000000a Adc::getValue() const
00000000 g     F .text._ZN3Adc8getValueE13ADCOversample 00000066 Adc::getValue(ADCOversample)
00000000 g     F .text._ZNK3Adc6runAdcEv        00000004 Adc::runAdc() const
00000000 g     F .text._ZNK3Adc8completeEv      0000000e Adc::complete() const

Przełącznik -t to w avr-objdump prezentacja tablicy symboli w pliku a -C to „demanglowanie w drugą stronę” nazw (do pojęcia odsyłam do poprzedniej części tutoriala).

Czy teraz wyjaśnia się dlaczego należy stosować extern ”C” dla kodu w C pisząc w C++? Inaczej linker nie potrafi poskładać obiektów.

Tak więc jeśli mamy funkcję w C++ która ma być wywołana w C, to należy otoczyć ją extern ”C” aby program w C się do niej dostał bo została zakodowana w „stylu C”. Jeśli mamy funkcję w C do której ma się odwołać program w C++, to funkcję tę należy otoczyć extern ”C” żeby C++ wiedziało że „manglowanie nazw” jest w stylu C :-)

Mam nadzieję że proste :-)

Tę część skończyliśmy z kodem który działa, jest ładny, zmodularyzowany ale.. należy popracować nad jego objętością :-)

Ale to w być może następnym odcinku... telenoweli obiektowej :-)

Na koniec jeszcze komplet plików, tym razem załączę archiwum, oczywiście bez Mirkowego mkuart.c oraz mkuart.h. Poprawki w tych plikach były w poprzednim tutorialu.

Adc_tutorial_2.zip


Następny odcinek będzie po krótkiej przerwie ~ 1 tygodnia. Niestety praca woła :-)
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
,,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