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ć 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.
Następny odcinek będzie po krótkiej przerwie ~ 1 tygodnia. Niestety praca woła