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++.