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

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

Oswajania C++ na AVR ciąg dalszy. Od tematów początkujących do zaawansowanych.

Będę pracował na ATmega16 z wewnętrznym zegarem 8MHz. Jeśli masz inny MCU akurat w podstawce, nazwy portów ADC będą inne.

Zdefiniujmy najpierw problem. Niezbyt szeroko bo mamy rozwiązać jedynie część problemu a nie wszystkie z przetwornikiem ADC.
Na tym etapie nic nie zakładamy co do języka, sposobu kodowania czy innych technicznych rzeczy. Tego poziomu należy się trzymać najdłużej jak się da pracując z C++. To jest podejście top-down (czyli od funkcjonalności do kodu). Podejście odwrotne bottom-up, masz bardzo dokładnie omówione w Blue/Green booku. Stosuje się jedno i drugie podejście jednocześnie!

No to czego chcę od funkcjonalności którą buduję, czyli podejście top-down:
1. Chcę aby ADC potrafiło zwrócić w sposób blokujący wartość mierzoną. Blokujący tzn. wywołuję funkcję i czekam aż funkcja zwróci wartość a w tym czasie ADC mierzy.
2. Chcę aby ADC zwróciło wartość w sposób nieblokujący w stylu wywołuję „zmierz no mi” i później zapytam „zrobiłeś już czy nie?”
3. Chcę aby ADC potrafiło zmierzyć inny kanał.
4. Chcę aby ADC potrafiło zrobić samo tzw. oversampling co oznacza że ma wykonać kilka pomiarów (jak przegnę to kilkaset) i zwrócić wartość mierzoną. Oversampling pozwoli na dodanie dodatkowych bitów do wartości i spowodować że będę miał próbkę 16-bitową :-) Spokojnie. To nie jest cud. Taki pomiar trwa i zakłada że w czasie pomiaru wartość się nie zmienia oraz.... trochę szumi :-) (w tym przypadku niewielki szum 0,5-1 bit będzie ok :-)).
Co może ADC w świetle moich wymagań czyli podejście bottom-up:
1. Po lekturze datasheet'a wiem że może mierzyć czyli 1 pkt. ok.
2. Po lekturze datasheet'a wiem że jest bit który informuje że skończył mierzyć czyli 2 też się da. Funkcja zwróci informację z tego bitu.
3. Po lekturze datasheet'a dowiaduję się że .. ojej jest tylko 1 ADC i trzeba mu przełączyć kanał :-)
4. No... tu są obliczenia czyli się da jak się nie zakałapućkam.

Tę pracę koncepcyjną przy programowaniu obiektowym zawsze wykonaj zanim napiszesz linijkę kodu. To na razie wstęp a w przyszłości będziesz myślał rodzinami obiektów :-)

No dobrze, a jak zobaczę co zmierzył? No.. użyję Mirkowych bibliotek do UART'a i sprawdzę czy da się je połączyć z C++ :-) Wiem że się da i przy okazji pokażę niezmiernie istotną i praktyczną rzecz.

No to do kodowania.

Zakładam projekt w C++ w ulubionym narzędziu Eclipse.

Kolejność działań:
New -> C++ Project -> AVR Cross Target Application: Empty project -> Nazwa: Adc_cpp -> Next -> Wyłączyć Debug i pozostawić Release -> Finish
Prawy klawisz myszy na katalogu projektu -> C/C++ Build -> Settings -> AVR C++ Compiler: Miscellaneous -> Other flags: -Wall -Wextra -pedantic -std=c++11 -g

Resztę (tj. programator, rodzaj MCU i częstotliwość zegara ustawiam tak jak w zwykłym projekcie C) Zakładam że już to znasz.

Najpierw samo ADC. Założę klasę z nazwą Adc. Przyjmuję konwencję że klasa zawsze zaczyna się z dużej litery a metody (czyli jej funkcje) oraz zmienne z litery małej. Jeśli coś ma się nazywać np. „oddaj mi pomiar” to o ile jest to zmienna nazwę ją jak oddajMiPomiar. Jeśli to klasa, to OddajMiPomiar. Taka konwencja nazewnicza nazywa się Camel Notation czyli notacja wielbłądzia. W większości kodu pisanego współcześnie, będziesz miał z nią do czynienia.

Klasa ma postać:

Kod: Zaznacz cały

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

class Adc {
public:
   // Tu będzie kod...
private:
   // Tu będzie kod...
};

Tak założona klasa ma część publiczną dostępną dla każdego i część prywatną z funkcjami do wyłącznie własnego użytku. Wczytuję także nagłówek stdint.h bo będę chciał używać typów uint8_t i podobnych. A <avr/io.h> bo pojawią się nazwy portów ADC.

Tu mała dygresja dotycząca także C. Stosujemy stdint.h a nie inttypes.h bo będziemy chcieli używać typów uint*_t. Nie wiem dlaczego często widuję wczytywanie inttypes.h. Tam są definicje makr dla printf'a. Rzadko tego potrzebuję na AVR. Taki mały szczegół o którym warto wiedzieć.

Jeśli miałbym bardzo prostą klasę, i nie chcę mieć części prywatnej i publicznej, to wystarczy że zadeklaruję klasę (tak klasę!) w taki sposób:

Kod: Zaznacz cały

struct Adc {
   // Tu będzie kod....
};

Taka klasa, definiowana z użyciem struct, ma wszystkie elementy publiczne. Zaleta? Mało pisania. Wada? Nie będę mógł ukryć budowy wewnętrznej przed ciekawskimi a w związku z tym ktoś z zewnątrz (lub sam niechcący) będzie mógł wywołać metodę lub przestawić atrybut a w konsekwencji zdestabilizować działanie mojego Adc. Ja decyduję się na klasę tym bardziej że chcę zaprezentować część właściwości języka C++ które różnią go od C.

Zwróć uwagę że czy to class czy struct, po nawiasie zamykającym klamrowym jest średnik ; O tym często na początku się zapomina.

Umieszczam definicję w pliku Adc.hpp. Jeśli już mam założenia które wcześniej poczyniłem, to mogę wybrać nazwy dla metod. Tu także podejmę decyzję co mają zwracać.

Kod: Zaznacz cały

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

class Adc {
public:
   // Ustawienie kanału Adc
   void setChannel(uint8_t channel) const;
   // Pobranie wartości pomiaru
   uint16_t getValue() const;
   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(uint8_t oversample);
   // Start konwersji
   void runAdc() const;
   // Sprawdzenie czy zakończono pomiar
   bool complete() const;
   // Ustawienie oversamplingu
   void setOversample(uint8_t oversample);
private:
   // Tu ma być kod... nie wiem jeszcze jaki..
};

To co napisałem to jedynie deklaracja klasy. Nie ma jeszcze żadnego kodu. Podałem nawet nazwy zmiennych przekazywanych choć mógłbym tego nie robić (tak jak w C w plikach *.h). Kompilator interesuje jedynie sygnatura metody czyli jej nazwa, argumenty, statice/volatite/externy i typ zwracany. Wiem jednak że będę czytał kod ja lub ktoś inny i lepiej jest nazwać zmienną bo mówi ona co metoda robi :-)

Najpierw metody:

void setChannel(uint8_t channel) const; nie zwraca niczego ważnego stąd void. Przyjmuje numer kanału. A skąd const? Przyjmuje jedynie kanał i nie będzie nic modyfikowała w samej klasie. Metoda wstawi numer kanału do rejestru I/O. Jeśli nie dodam const, to będę mógł w sposób niezamierzony w ramach metody setChannel() przestawić coś niechcący w klasie a nie takie jest jej przeznaczenie.

uint16_t getValue() const; zwraca pomiar. Może on być 8 lub 12-bitowy więc na 16 bitach się zmieści. Wartość będzie zwracana z rejestru.

uint16_t getValue(uint8_t oversample); także zwraca pomiar ale z przesłaniem argumentu wykonania oversample. Nie ma const bo będzie pewnie coś liczył w ramach klasy. Zaraz! Przecież ta metoda nazywa się tak samo jak poprzednia! W języku C++ tak można robić. Zwróć uwagę że posiada inny zestaw argumentów wywołania i kompilator wybierze odpowiednią w zależności od tego jak wywołasz getValue(). Z argumentem czy bez. Pamiętaj jednak że rozróżnianie metod możliwe jest na podstawie argumentów tylko w C++ a C takiej właściwości nie posiada. Dwie metody nazywające się tak samo ale zwracające inny typ danych nie będą dla kompilatora różne. Jak się nad tym zastanowić głęboko to ma to bardzo głęboki sens. Tu jednak nie chodzi o teoretyzowanie. Do rzeczy. Taką sprytną zdolność języka nazywamy polimorfizmem a sam fakt wystąpienia przeciążeniem. To bardzo istotne pojęcie. W zasadzie „silnik” wielu istotnych właściwości języka. Inne zachowanie w zależności od ilości i rodzaju argumentów wywołania.

void runAdc() const; Znowu to const! Powody takie jak poprzednio. Ustawiam tylko bit na porcie a nie zmieniam nic w klasie.

bool complete() const; Zwróci true jak skończy mierzyć, a false jak jeszcze mierzy. Metoda sprawdzi to w bicie na porcie więc nie modyfikuje nic w klasie, stąd const.

void setOversample(uint8_t oversample); No, tu nie ma const bo tu będzie zmieniana jakaś wewnętrzna zmienna z sumą próbek, może będzie jakiś licznik w klasie... zobaczymy... Dość że będzie coś zmieniane i to jest powód że brak const.

Powiedziałem o jakiś zmiennych? Definiuję je jako atrybuty w klasie. Będą prywatne żeby mi nikt ich nie zmieniał. Nikt poza metodami samej klasy.

Jakie to atrybuty? Będzie mi pewnie potrzebna wartość pomiaru obliczonego w oversample oraz licznik. Wyjaśnię o co chodzi w oversample. Cierpliwości :-) Część prywatną definiuję tak:

Kod: Zaznacz cały

...
private:
   // Wartość obliczana
   uint16_t value;
   // Licznik oversample
   uint16_t counter;
};

Tu mała uwaga. Skąd u mnie takie przywiązanie do const? Z prostej przyczyny. To jedna z linii obrony tego że ktoś (czyli mój kod) modyfikuje w sposób niekontrolowany dane. To się bardzo dobrze sprawdza. Pozwala popełniać mniej błędów i zrzuca na kompilator odpowiedzialność za sprawdzanie poprawności danych oraz operacji. Polecam to także w C. Jest jeszcze jeden powód. Pamiętasz o polimorfiźmie? Metoda z const w argumencie i bez const w argumencie to dla kompilatora dwie różne metody choć mogą nazywać się tak samo.

Na tym etapie zawsze dodaję do klasy ,,kadłubki” metod. Coś zwracają, zgodnie z typem ale nie jest to prawda. Po co? Bo chcę sprawdzić czy wszystko się kompiluje. Dodam więc tu jakieś dane.

Kod: Zaznacz cały

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

class Adc {
public:
   // Ustawienie kanału Adc
   void setChannel(uint8_t channel) const {
      return;      // Tak return zwraca void
   }
   // Pobranie wartości pomiaru
   uint16_t getValue() const {
      return 0xFEF0;   // 0xFEF0 jest dobre jak każe inne :-)
   }
   // Pobranie wartości pomiaru z oversample
   uint16_t getValue(uint8_t oversample) {
      return 0xF0F0;   // 0xF0F0 może być i takie
   }
   void runAdc() const {
      return;      // Nicnierobienie...
   }
   // Sprawdzenie czy zakończono pomiar
   bool complete() const {
      return true;   // Zawsze kończy :-)
   }
   // Ustawienie oversamplingu
   void setOversample(uint8_t oversample) {
      return;      // Tak, to znowu void
   }
private:
   // Wartość
   uint16_t value;
   // Licznik oversample
   uint16_t counter;
};

I tu jest dobry moment na poruszenie sprawy .. co to jest klasa :-) Pamiętasz że wcześniej napisałem że można zamiast słówka class stosować struct? No to masz odpowiedź. To taka struktura która łączy w sobie trochę zmiennych (nazywanych atrybutami) i kilka funkcji (nazywanych metodami). W języku C w strukturze można było tu umieścić zmienne oraz (wskaźniki na) funkcje, a tutaj jest to samo. Uwaga, z małym dodatkiem. Klasa może posiadać przepis czyli budowniczego który inicjuje jej wartości i wykona operację jej konstrukcji. Może posiadać także i destruktor który w momencie jeśli jej nie będziemy potrzebowali, dokona usunięcia danych w sposób kontrolowany. Jeśli pisałeś coś bardziej rozbudowanego w C, to pewnie tworzyłeś strukturę z jakąś funkcją (lub dokładniej wskaźnikiem na funkcję) o nazwie init() lub podobnym. Ta funkcja ... inicjowała :-) Pewnie także bywało że struktura posiadała funkcję delete() lub podobną która sprzątała. To ostatnie na MCU występowało rzadziej, ale mogło się zdarzyć.

Wyobraź sobie że klasa to taka foremka (czyli przepis) z której powstaje na żądanie obiekt. Przepis jak zbudować obiekt w zasadzie już widzisz. Jak go zbudujemy, to otrzymasz wskaźnik w jaki sposób się do niego odwołać. Co co widzisz w pliku *.hpp to jednocześnie struktura więc poddaje się tym samym operacjom co struktura. Można na niej zrobić sizeof( ...), sprawdzić adres itp. Od tego momentu będę mówił o klasach i obiektach ale chcę abyś pamiętał że na strukturze w C także da się zrobić klasę i obiekt :-) Może nie zrobisz tego wszystkiego co w C++ ale programować obiektowo można (w prawie) każdym języku.

Na obiekcie będziesz wykonywał działania, przesyłał go, zapisywał, zmieniał itp. Definicja w foremce (czyli klasie) zawiera nazwy metod i zmienne które przyjmuje. Definiuje więc jak się z nią dogadać. Taki zbór metod nazywany jest interfejsem obiektu lub klasy.

No to zróbmy coś żeby z klasy mógł powstać obiekt. Do tworzenia obiektów (zaawansowani zamknąć się :-) to jest świadome uproszczenie) Służy new. Do jego usunięcia delete.

Pamiętasz jak pisałem w poprzednich tutorialach że avr-g++ ma niezdefiniowane operatory new i delete i kilka braków konfiguracyjnych? Umieszczam poprawki w new.hpp i new.cpp:

new.hpp:

Kod: Zaznacz cały

#ifndef NEW_HPP_
#define NEW_HPP_

//
// avr-libc nie dostarcza operatorów new i delete.
// Ma także problemy z definicją obsługi dziedziczenia i dziedziczenia
// wirtualnego. Plik poprawia te problemy.
//

#include <stdlib.h>

#ifdef __cplusplus

// Operatory new, new[]
extern void * operator new(size_t size);
extern void * operator new[](size_t size);

// Operatory delete i delete[]
extern void operator delete(void *ptr);
extern void operator delete[](void *ptr);

// Obsługa dziedziczeń i dziedziczeń wirtualnych...
__extension__ typedef int __guard __attribute__((mode (__DI__)));

extern "C" int __cxa_guard_acquire(__guard *);
extern "C" void __cxa_guard_release (__guard *);
extern "C" void __cxa_guard_abort (__guard *);

extern "C" void __cxa_pure_virtual(void);

#endif // __cplusplus

#endif /* NEW_HPP_ */


new.cpp:

Kod: Zaznacz cały

#include "new.hpp"

#ifdef __cplusplus

// new i new[]
void * operator new(size_t size) {
    return malloc(size);
}

void * operator new[](size_t size) {
    return malloc(size);
}

// delete i delete[]
void operator delete(void* ptr) {
    free(ptr);
}

void operator delete[](void* ptr) {
    free(ptr);
}

int __cxa_guard_acquire(__guard *g) {return !*(char *)(g);}
void __cxa_guard_release (__guard *g) {*(char *)g = 1;}
void __cxa_guard_abort (__guard *) {}

void __cxa_pure_virtual(void) {}

#endif // __cplusplus

Dodaję do main.cpp wywołanie tworzenia obiektu na podstawie Adc. Kreowanie obiektu na podstawie klasy nazywane jest jego instancjonowaniem. main.cpp bęzie więc wyglądał tak:

Kod: Zaznacz cały

#include "Adc.hpp"
#include "new.hpp"

int main(void) {
   Adc* myAdc = new Adc();
}

Kompiluję projekt i sprawdzam czy zbudował się wsad. Powinno być kilka ostrzeżeń że nie są używane zmienne w moich kadłubkach metod w klasie Adc w pliku Adc.hpp, ale wsad się zbuduje a ja (jak nigdy) akurat te ostrzeżenia na razie zignoruję.

new jak widać w kodzie new.cpp, zwraca wskaźnik na obiekt typu stworzony z klasy Adc :-) Zwróć uwagę że operator new to proste opakowania na malloc a delete na free. Są tam zdefiniowane jeszcze operatory new[] i delete[] które służą do budowania tablic. W tej części ich nie użyję.

Zanim przejdziesz dalej, zwróć uwagę na sprawdzenia makr __cplusplus (dwa podkreślniki na początku). Makro to jest definiowane jeśli g++ kompiluje kod w C++. ifdef'y sprawdzą więc czy włączyć kod który dotyczy wyłącznie kompilacji C++. Tu ważna nauka tak dla C jak i C++. Proszę, nigdy nie definiuj makra z dwoma podkreślnikami z przodu nazwy bo prosisz się o kłopoty. Takie nazwy zarezerwowane są dla potrzeb wewnętrznych kompilatora. Jeśli interesuje Cię czego jeszcze nie definiować, to zerknij do właściwości projektu, C/C++ General -> Preprocesor Include Path... i rozwiń CDT Managed Build Setting Entries. Jest tam trochę makr prawda ? I większość z dwoma podkreślnikami z przodu.

Pojawia się jeszcze w kodzie zapis extern ”C”. O nim za chwilę.

Przechodzę do ważnej sprawy. Obiecuję że jeśli pojawią się pytania co do tego zagadnienia, dodam mikro tutorial w którym pokażę co się dokładnie dzieje. Na razie zaufaj w to co pokażę :-)

Przyłączę do projektu kod obsługi Mirkowego UART. Dodaję pliki mkuart.c i mkuart.h do projektu. Do main.cpp trafi #include ”mkuart.h”.

Czas na połączenie wszystkiego. Uwaga, proszę purystów o wyrozumiałość, kod jest skandalicznie napisany z bardzo ważnych powodów :->

Oto kod main.cpp:

Kod: Zaznacz cały

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

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

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

   // Komunikat na USART
   char komunikat[] = "Adc 0: 0x";

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

   // Ustawienie kanału 0 jako wejściowego
   myAdc->setChannel(0);

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

   while(true) {
      // Uruchomienie pomiaru
      myAdc->runAdc();

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

      // Wyświetlenie komunikatu
      uart_puts(komunikat);

      // Wypisanie wartości szesnastkowo
      uart_putint(value, 16);

      // Nowa linia na usart
      uart_puts(const_cast<char *>("\r\n"));
      _delay_ms(1000);
   }
}

Kompiluję i ... błędy.. jakieś informacje udnefined reference. Coś takiego

Kod: Zaznacz cały

./main.o: In function `main':
main.cpp:(.text.startup.main+0x3a): undefined reference to `USART_Init(unsigned int)'
main.cpp:(.text.startup.main+0x42): undefined reference to `uart_puts(char*)'
main.cpp:(.text.startup.main+0x4e): undefined reference to `uart_putint(int, int)'
main.cpp:(.text.startup.main+0x56): undefined reference to `uart_puts(char*)'
collect2: error: ld returned 1 exit status

Z premedytacją wywołałem ten błąd abyś i Ty wiedział co jest powodem.

Czy pamiętasz jak wspominałem o pojęciu poliformizm? Mówiłem wówczas o tym że w C++ można metodę/funkcję odróżnić po ilości i rodzaju argumentów. Ale C tego nie wspiera bo nie posiada tego mechanizmu w samym języku. Dlatego linker zgłasza błąd próbuje połączyć kod zbudowany w C++ z kodem w C.

Jak to działa? W czasie budowania oprogramowania, gcc kompiluje pliki *.c tak jak by to było C (no nie dziwne przecież) i wstawia do pliku *.o nazwy zmienione zgodnie z konwencją tego języka. Kompilując natomiast program dla C++ (pliki *.cpp), zmienia nazwy dodając do nich informacje o argumentach. Wszystko to trafia do binarnych plików *.o. Sama operacja nazywana jest nieformalnie „manglowaniem nazw” albo dekorowaniem nazw (http://pl.wikipedia.org/wiki/Dekorowanie_nazw) . Powinniśmy poinformować więc kompilator że obsługa UART będzie w języku C.

Jak? Rozwiązania są dwa. Jedno szybkie i nieco niechlujne, drugie takie jak być powinno czyli eleganckie :-)

Rozwiązanie szybkie to otoczenie wczytywania nagłówka mkuart.h przez extern ”C” { ... }. Poinformuje to kompilator że ma do czynienia z kodem w C. W main.cpp będzie to wyglądało tak:

Kod: Zaznacz cały

...
extern "C" {
#   include "mkuart.h"
}
...

Rozwiązanie eleganckie to poprawienie samego nagłówka mkuart.h. Będzie on wyglądał tak:

Kod: Zaznacz cały

#ifdef __cplusplus
extern "C" {
#endif

void USART_Init( uint16_t baud );
...
void uart_putint(int value, int radix);

#ifdef __cplusplus
}      // extern "C"
#endif

Zmiana nagłówka mkuart.h nie psuje go w tej chwili dla programów w C bo jest sprawdzenie makra czy kompilacja odbywa się w trybie C++.

Ja wybieram rozwiązanie 2 czyli poprawiam nagłówek.

Kompiluję program który teraz się buduje z poprzednimi ostrzeżeniami o nieużywanych zmiennych w Adc.hpp ale wsad się buduje :-)

Konstrukcja extern ”C” załatwia więc linkowanie C++ z C. Jeszcze do tego zagadnienia wrócę bo będzie okazja i wtedy rozwiążemy problem wywoływania z C++ kodu w C oraz wywoływania z C kodu C++ :-)

Poprawię teraz kod w main.cpp bo sam na niego już nie mogę patrzeć :-/ Brzydki i nieoptymalny jak ... noc ...

Po co ta zmienna napis?! Podstawmy string od razu do uart_puts(). Kompiluję i ostrzeżenie?! Odrzucona konwersja ze stałego char *? Nie jest to dziwne przecież najdokładniej jak można to jest to zmienny wskaźnik na stały napis a w mkuart.h brak informacji o tym że char * jest const. Dobrze, ale dlaczego działa z jakimś:
const_cast<char *>(”\t\n”)? Tak wygląda rzutowanie w C++ właściwe dla tego języka. Koszmarnie źle wizualnie to wygląda i ma tak wyglądać :-) Rzutowania bardzo często sygnalizują błędy koncepcyjne w software i radzę usilnie byś w C++ (jeśli już musisz rzutować), taką postać stosował. A dlaczego... no bo są różne rodzaje rzutowań w C++, nie tylko const_cast. Stosując jedno z nich jasno komunikujesz swoją intencję. No to jak ten błąd poprawić? W mkuart.h zmienić argument na const. Ma wyglądać tak:

Kod: Zaznacz cały

...
void uart_puts(const char * s);
...

A w mkuart.c tak:

Kod: Zaznacz cały

...
void uart_puts(const char *s)      // wysyła łańcuch z pamięci RAM na UART
...

Znajdź proszę w bluebooku informację o wskaźnikach a dowiesz się że jest to zmienny wskaźnik na stałą daną. Tak to działa w C jak i w C++.

Dla przypomnienia:
const int * data; // Zmienny wskaźnik na stałą int
int * const data; // Stały wskaźnik na zmienną zmienną int :-)

const int other = 1;
const int * const data = &other; // Stały wskaźnik na stałą i dlatego należy od razu ją inicjalizować.

Tu pytanie do Mirka czy nie naruszam pierwotnej Twojej koncepcji że wskaźnik ma być na stałe napisy? Myślę że nie naruszam i będzie to działało także w projektach w C.

No... wiesz już jak połączyć C i C++ w jednym programie. Tak więc nie wyrzucaj kodu w C który już stworzyłeś. Szczególnie jeśli działa :-)

Podsumuję:
Dla wywołań z C++ do C, stosujemy otoczenie przez extern ”C” { ... } bo C nie obsługuje polimorfizmu a program w C++ informujemy tym extern'em że wywołuje coś w C.
Do rzutowania (szczególnie w C++) stosujemy może nieładne konstrukcje (coś_cast<na co>() ) ale za to bardzo widoczne jasno opisujące intencje :-)

Wracamy do zagadnień obiektowych. Jak widzisz myAdc jest wskaźnikiem tak więc metodę wywołujemy przez strzałkę. Dlaczego? Bluebook :-) Tak, klasa to struktura. Pamiętasz? Odsyłam jeszcze raz wyżej do informacji że klasę mogę definiować przez struct lub class i struct wszystko pokazuje a class wymaga public: aby coś pokazać do wywołania.

Dodam jeszcze usunięcie obiektu tak abyś wiedział jak się to robi choć tu w kodzie będzie poprawne składniowo ale mało logiczne. Po co wyrzucać obiekt który w pętli „się nie zużył” :-) W main.cpp, dodam wpis:

Kod: Zaznacz cały

...
   delete myAdc; // usunięcie obiektu
   _delay_ms(1000);
}

Zastanówmy się jednak, czy ja istotnie będę chciał robić 2 czy 3 czy 4 obiekty Adc w moim programie. Jeśli tak, to mechanizm z new/delete ma sens. MCU ATmega ma jednak jedno Adc z kilkoma kanałami. A czy będę chciał je usuwać? Nieee... Jak już będzie jedno w programie to będę z niego czytał. No to po kiego grzyba robię new?! Aaa... żeby Ci coś pokazać. Po pierwsze jak się to robi a po drugie ile to kosztuje na AVR. Skompiluj oprogramowanie i sprawdź jakie są wyniki wsadu. U mnie:

Kod: Zaznacz cały

Device: atmega16

Program:    1346 bytes (7.6% Full)   <== O qrcze!
(.text + .data + .bootloader)

Data:         74 bytes (7.2% Full)
(.data + .bss + .noinit)

To trudne do zaakceptowania ..

Jak ma być jeden obiekt Adc, to przenieśmy je przed int main(void) {.. , niech nam je kompilator sam zbuduje przed startem programu i będziemy pracowali na jednym.
Kod main.cpp po poprawkach wygląda tak:

Kod: Zaznacz cały

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

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

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

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

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

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

   while(true) {
      // Uruchomienie pomiaru
      myAdc.runAdc();

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

      // Wyświetlenie komunikatu
      uart_puts("Adc 0: 0x");

      // Wypisanie wartości szesnastkowo
      uart_putint(value, 16);

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

Teraz myAdc to nie jest już wskaźnik więc odwołania do metod nie będą przez strzałkę a przez kropkę (Bluebook rozdział o strukturach i dostępie do nich....). A wynik kompilacji?

Kod: Zaznacz cały

Device: atmega16

Program:     652 bytes (3.9% Full) <-- No, lepiej
(.text + .data + .bootloader)

Jakoś lepiej :-P Dodatkowo nie stosuję przecież ani new ani delete. No to wyłączę nagłówek z main.cpp i odłączę pliki new.cpp oraz new.hpp z projektu poprzez: Prawy klawisz myszy na pliku w drzewie projektu, Properties -> C/C++ General -> Preprocesor Include Path... -> Zaznacz: Exclude resource from build

Oszczędność nie będzie już duża ale zawsze to (u mnie) 10 bajtów Flash i 8 bajtów RAM. 64 bajty które zostały to głównie bufor kołowy obsługi USART Mirka, zmienne obsługi bufora kołowego, zmienna value (tak i jej można się pozbyć) oraz atrybuty obiektu. Z kodu asemblera wyrzucona została teraz procedura wspierająca new. Jak widać samo new kosztuje ~ 10 bajtów. Jeśli więc jednak jakieś obiekty będziesz tworzył, wiesz co robisz, akceptujesz narzut na Flash, to może pozostać. Jak jednak robić new wydajnie, innym razem.

Dodajmy więc w końcu implementację pomiaru. Znów purystów proszę o wyrozumiałość. Koszmarki od których wychodzę są po to by pokazać proces myślenia. Kod w Adc.hpp uzupełniam tak:

Kod: Zaznacz cały

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

class Adc {
public:
   // Ustawienie kanału Adc
   void setChannel(uint8_t channel) const {
      // Ustawienie napięcia referencyjnego na AVCC oraz kanału na 0
      ADMUX = (1 << REFS0);
      // Ustawienie kanału
      ADMUX = ( ADMUX & ~( (MUX4 << 1) - 1 ) ) | channel;
      return;
   }

   // Pobranie wartości pomiaru
   uint16_t getValue() const {
      // Uruchomienie przetwornika ADC
      ADCSRA = (1 << ADEN);
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | 0x07;
      // 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) {
      return 0xF0F0;   // 0xF0F0 może być i takie
   }

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

   // Ustawienie oversamplingu
   void setOversample(uint8_t oversample) {
      return;      // Tak, to znowu void
   }

private:
   // Wartość obliczana
   uint16_t value;

   // Licznik oversample
   uint16_t counter;
};

Zwróć uwagę co dzieje się w linijce while(!complete()); jest tu wywoływana metoda complete z samej klasy Adc. Kompilator wie o jaką metodę chodzi! Jeśli byłbym (niepotrzebnie w tym przypadku) szczegółowy, to mógłbym metodę tę określić tak while(this->complete());

Tu dochodzimy do pojęcia this. Co to jest? Słowo kluczowe this określa ten (ang. this) obiekt który jest wyprodukowany z klasy. this jest wskaźnikiem, stąd dostęp przez strzałkę.

Jest jednak poważny zarzut do tego kodu, chociaż i tak działa marnie :-/ Dlaczego metoda wyboru kanału robi jeszcze ustawienie napięcia referencyjnego i dlaczego uint16_t getValue() const startuje nowy pomiar. Ustawianie preskalera? Przecież nie było tego wcześniej w opisie tych metod (zerknij wyżej do założeń). Chcielibyśmy przecież aby Adc już działało kiedy pobieramy wartość. A zmiana kanału (zgodnie z nazwą) zmieniała kanał a nie robiła coś jeszcze. A jaki moment jest dobry by uruchomić Adc i ustawić napięcie referencyjne? Najlepszym momentem jest ten gdy obiekt powstaje. Zerknij do main.cpp. To jest jeszcze przed int main(void). Chodzi o linijkę:

Kod: Zaznacz cały

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

No to dodajmy klasie konstruktor. Nazwa konstruktora jest taka jak klasy i wygląda jak metoda. Z wyjątkiem jednak tego że nie zwraca nic. Nawet void! Posiada także tzw. listę inicjalizacyjną. Ustawia w niej pierwsze wartości atrybutów.

Kod: Zaznacz cały

...
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
      ADMUX = (1 << REFS0);
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | 0x07;
   }
...

Taki konstruktor jest bezparametrowy (a w main.cpp taki właśnie jest bo wywołanie to Adc(); ), zeruje value i counter. Dokładnie przyjrzyj się jak wygląda lista inicjalizacyjna. To jest to co jest po dwukropku. Podawana jest najpierw nazwa atrybutu, a w nawiasie jej pierwsza wartość. Elementy listy oddzielone są dwukropkami.
Wyjaśnienia wymaga linia ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | 0x07;

Część (ADPS2 << 1) – 1, zwraca maskę preskalera. Dla ATmega16 to maska bitów ADPS2, ADPS1, ADPS0. 0x07 to preskaler 128. Nie obawiaj się że kompilator będzie to pracowicie liczył. Zorientuje się że jest to wyrażenie stałe i policzy raz maskę i wstawi ją do kodu.

Podobne wywołanie jest w setChannel(uint8_t channel) const ale dotyczy wyboru kanału.

Efekt jest taki że po zbudowaniu obiektu, przetwornik ADC jest już uruchomiony ale oczywiście nie uruchomił jeszcze konwersji. Ktoś kto bardzo uważnie śledzi kod, zapyta: „Moment, a jak nie było konstruktora to jak się obiekt zbudował?”

Oto wielka tajemnica! Klasa posiadała tzw. konstruktor domyślny! To było zwykłe, ordynarne kopiowanie wszystkich danych klasy! Masz to z automatu. Ten konstruktor jest tak prosty że nie potrafił uruchomić przetwornika. Dlatego zdefiniowaliśmy nowy. Jak zdefiniuję nowy, domyślny konstruktor będzie usunięty.

A może nie podoba Ci się ustawienie preskalera na 128? Nie ma problemu. Dodamy dodatkowy konstruktor który w trakcie kreacji ustawi preskaler na taką wartość jak tego chcesz.

Teraz konstruktory wyglądają tak:

Kod: Zaznacz cały

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
      ADMUX = (1 << REFS0);
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | 0x07;
   }

   // Konstruktor ustawiający preskaler
   Adc(uint8_t prescaler) : value(0), counter(0) {
      // Uruchomienie przetwornika ADC
      ADCSRA = (1 << ADEN);
      // Ustawienie napięcia referencyjnego na AVCC oraz kanału na 0
      ADMUX = (1 << REFS0);
      // Ustawienie preskalera
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | prescaler;
   }
...

No to ile może być tych konstruktorów? A ile zechcesz :) Aby były odróżnialne przez typ i ilość argumentów :) Mamy więc niesamowitą elastyczność! Mogę teraz zbudować obiekt myAdc na dwa sposoby:

Adc myAdc = Adc(); // teraz domyślnie będzie miał preskaler 128
...
Adc myAdc = Adc(0x05); // teraz obiekt będzie miał preskaler 32 bo podałem go przy kreacji

Zostawmy jednak na razie konstruktory. Jeszcze naprawdę można o nich wiele powiedzieć. Czeka jeszcze jedna funkcjonalność. Oversampling. Ale to już w następnej części.

Mam także propozycję wyzwania :-)

Proszę dodaj metodę ustawiania napięcia referencyjnego do klasy Adc. Po tym co widziałeś wyżej, myślę że nie będzie to takie trudne...

Jakie zalety posiada takie podejście które pokazałem?
1. Kontrola nad ukrywaniem danych. Jeszcze dalej idąca niż w tradycyjnym C w którym można było stosować w ramach jednego pliku *.c słowo kluczowe static co umożliwiało dostęp do zmiennej jedynie w tym pliku, lub obiecać że zmienna/funkcja gdzieś jest poprzez extern. W C++ masz jeszcze klasę z możliwością kontroli co i komu pokazujesz.
2. Możliwość elastycznego i opisowego operowania na zamkniętej w klasie funkcjonalności bez problemów że ktoś nazwał funkcję w programie tak jak ty. Funkcja jest przypięta do obiektu. Jeśli nawet nazwał klasę tak jak ty, i na to jest rada :-)
3. Możliwość tworzenia i niszczenia obiektu na żądanie. Usunięcia go jeśli nie jest już potrzebny. To ostatnie na AVR jest słabym argumentem ze względu na rozdzielenie programu który jest w pamięci Flash od danych które są w RAM (przypominam ile to kosztowało). Ale i to w specyficznych przypadkach się przydaje (np. zamknięcie komend przesyłanych przez szyny danych itp.)
4. Łatwo łączy się funkcjonalność znaną już z C ( przypominam: extern ”C” {.. }). Nie wyrzucam kodu który przecież działa!
5. C++ jest ściślejszy co do kontroli typów od C w wielu miejscach (przypominam problem ze wskaźnikiem na stały napis) co pozwala obronić się przed nadużyciami a jak wejdą szablony, zobaczysz cuda :-)

Jakie wady posiada to podejście:
1. Nie zawsze potrzebujesz nowego obiektu (patrz pkt. 3 wyżej), a już rzadko na MCU i tworzenie obiektów oraz ich destrukcja jest niepotrzebnym kosztem (jak widać było zabiera trochę RAM i nieźle zżera Flash :-/ ).
2. Trzeba wiedzieć o kilku przełącznikach kompilatora i poprawić braki gcc dla AVR.
3. Nowe jest wrogiem starego. Trzeba nauczyć się innych umiejętności a to zabiera czas. Moim zdaniem jednak warto bo kod który powstanie będzie o wiele elastyczniejszy i łatwiejszy w stosowaniu w przyszłości. No to w następnym tutorialu w końcu uruchomię Adc i zaprezentuję działanie konstruktora.

Mam nadzieję że teraz było od podstaw i po kolei. Jeśli uważnie (i ze zrozumieniem) przeczytałeś w bluebooku o wskaźnikach i strukturach, nie powinno być problemów. Niemniej jednak jestem otwarty na pytania i sugestie. Jak zbyt trudne, daj znać. Mam także nadzieję że ktoś sprawdzi czy nie popełniłem błędu w kodzie bo tylko może w moim eclipse działa C++? :-)

No i kto podejmuje się wyzwania ? :-)

Już się cieszę na implementację Adc a szczególnie oversamplingu :-)

Na koniec jeszcze zawartość plików abyś mógł wystartować od razu:

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

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

   // Deklaracja zmiennej przechowującej wynik
   uint16_t value;

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

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

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

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
      ADMUX = (1 << REFS0);
      // Ustawienie preskalera na 128
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | 0x07;
   }

   // Konstruktor ustawiający preskaler
   Adc(uint8_t prescaler) : value(0), counter(0) {
      // Uruchomienie przetwornika ADC
      ADCSRA = (1 << ADEN);
      // Ustawienie napięcia referencyjnego na AVCC oraz kanału na 0
      ADMUX = (1 << REFS0);
      // Ustawienie preskalera
      ADCSRA = (ADCSRA & ~(( ADPS2 << 1) - 1)) | prescaler;
   }

   // 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 {
      // 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) {
      return 0xF0F0;   // 0xF0F0 może być i takie
   }

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

   // Ustawienie oversamplingu
   void setOversample(uint8_t oversample) {
      return;      // Tak, to znowu void
   }

private:
   // Wartość obliczana
   uint16_t value;

   // Licznik oversample
   uint16_t counter;
};

Poprawki w pliku mkuart.h Mirka...

Kod: Zaznacz cały

...
// deklaracje funkcji publicznych

#ifdef __cplusplus
extern "C" {
#endif

void USART_Init( uint16_t baud );
...
void uart_puts(const char * s);
void uart_putint(int value, int radix);

#ifdef __cplusplus
}      // extern "C"
#endif


Poprawki w pliku mkuart.c Mirka...

Kod: Zaznacz cały

...
void uart_puts(const char *s)      // wysyła łańcuch z pamięci RAM na UART
...
,,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 5 gości