Żeby pisać na mikrokontrolery w C++ trzeba znać ten język bardzo dobrze. Niestety mikrokontroler to nie PC i nie "wybacza" nieprzemyślanych konstrukcji, które kończą się alokacją ogromnych ilości pamięci. Tak konkretniej to chodzi mi o nieprzemyślane używanie klas.
C na mikrokontrolery jest znacznie łatwiejszy.
piotrekhbn napisał: A mógłby ktoś napisać w C++ i w C program który np. powoduje naprzemienne miganie Ledów. Wtedy byłoby porównanie:)
Kod wygląda mniej więcej tak samo i raczej jest w języku C. Nie ma sensu na siłę pchać mechanizmów z C++ tam gdzie nie są potrzebne, więc jeżeli chodzi o porównanie języków jest to kiepski przykład.
W skrócie:
#include <avr/io.h>
#include <avr/interrupt.h>
/* procedura obsługi przerwania przy przepełnieniu timera 0 */
ISR(TIMER0_OVF_vect) {
PORTB ^= _BV(PB4);
}
int main(void) {
/* ustawienie kierunku i początkowych wartości na portach I/O dla ATmega8 */
PORTB = 0xff & ~_BV(PB4);
PORTC = 0xff;
PORTD = 0xff;
DDRB |= _BV(PB4);
/* konfiguracja timera */
TIMSK |= _BV(TOIE0); /* przerwanie */
TCCR0 = _BV(CS02) | _BV(CS00); /* preskaler i uruchomienie */
/* globalne włączenie przerwań */
sei();
while(1) {
/* główna pętla programu */
}
}
Bardziej rozwlekle:
Definicje rejestrów itp. dla danego procesora. Typ procesora podaje się przy kompilacji jako jeden z argumentów gcc, innym sposobem jest skorzystanie z gotowego szablonu makefile dostarczanego razem a WinAVR, gdzie wpisuje sie typ w odpowiednią linijkę, w AVR Studio po zainstalowaniu WinAVR też można pisać w C i tam typ procesora i inne "bajery" wklepuje sięw odpowiednie okna dialogowe.
Makra do definiowania wektorów przerwań:
#include <avr/interrupt.h>
Główna procedura:
Na początek wypadało by ustawić kierunek portu, a konkretniej pinu
do pracy jako wyjście. Służą do tego rejestry DDRx, gdzie x to litera portu (A, B, C itd.) Każdy bit w tym ośmiobitowym rejestrze odpowiada jednej linii danego portu. Makro _BV(x), to po prostu (1 << (x)), BV jak
Bit
Value, czyli wartość bitu. w <avr/io.h> bity pełniące w rejestrach różne funkcje zdefiniowane są w postaci ich numeru, a nie wartości, stąd konieczność "konwersji" numeru bitu na jego wartość (wagę). Na przykład bity odnoszące się do poszczególnych pinów portów I/O zdefiniowane są jako Px0, Px1, Px2, Px3 itd., ich wartości to odpowiednio 0, 1, 2, 3, a po podaniu ich jako argumentów do makra _BV() dostaniemy odpowiednio 1, 2, 4, 8 itd. Na rejestrze DDRB wykonujemy sumę logiczną, rezultat będzie taki, że bit 4 zostanie ustawiony (ale to już nie programowanie, a algebra Boole'a).
Na początek zgasimy LEDa, przy założeniu, że anoda połączona jest z pinem PB4, a katoda z masą i oczywiście gdzieś tam szeregowy rezystor żeby ograniczyć wartość prądu. Rejestr PORTx to rejestr wyjściowy, za jego pomocą ustala się stan na porcie pracującym jako wyjście. Do odczytywania z portu służy rejestr PINx, o czym należy pamiętać. Dla portów pracujących jako wejście zapis do rejestru PORTx służy do włączenia / wyłączenia podciągania odpowiednich linii portu do plusa zasilania (przez "rezystory" o wartości rzędu chyba 30kohm). Tutaj liczymy wartość bitu: 1 << 4 = 16 (binarnie 00010000), wykonujemy negację (binarnie: 11101111), wykonujemy operację iloczynu logicznego (AND) na porcie B, czyli wszystko zostaje bez zmian, oprócz bitu 4, który zostanie wyzerowany. O ile włączona będzie optymalizacja kodu, to procesor w tym wypadku nie wykona operacji AND, tylko zoptymalizuje to do pojedynczej (szybszej) instrukcji CBI służącej właśnie do zerowania pojedynczych bitów.
Powyższe operacje można i nawet czasem powinno się wykonać w odwrotnej kolejności. Nieużywane piny należy ponadto skonfigurować tak, żeby nie pojawiały się na nich nieprzewidziane stany, czy napięcia pośrednie między stanem 0 a 1. Przyczynia się to przede wszystkim do zmniejszenia poboru prądu. Założenie jest takie, że po włączeniu zasilania wszystkie porty skonfigurowane są do pracy jako wejście, najprościej i najbezpieczniej będzie włączyć na tych portach "rezystory" podciągające (tak na prawdę są to tranzystory polowe). Żeby było jeszcze prościej, to włączymy pull-upy na całym porcie na raz. Załóżmy, że to ATmega8, która ma porty B, C i D. Nieważne, że części z tych pinów fizycznie nie ma, albo są im przypisane inne funkcje (/RESET, generator kwarcowy), nic się nie stanie.
PORTB = 0xff;
PORTC = 0xff;
PORTD = 0xff;
Oczywiście ustawianie zera na pinie PB4 w poprzedniej operacji traci sens. Ale chciałem takie łagodniejsze wprowadzenie zrobić, kolejność wszystkich dotychczasowych operacji powinna zatem wyglądać tak:
PORTB = 0xff;
PORTC = 0xff;
PORTD = 0xff;
DDRB |= _BV(PB4);
PORTB &= ~_BV(PB4);
Albo nawet tak:
PORTB = 0xff & ~_BV(PB4);
PORTC = 0xff;
PORTD = 0xff;
DDRB |= _BV(PB4);
Teraz pora na konfigurację timera.
Timery pracują niezależnie od rdzenia uC, więc doskonale nadają się do odmierzania czasu, albo wykonywania jakiejś operacji w ściśle ustalonych odstępach czasu. Ta ostatnia opcja najbardziej nas interesuje.
Krótko o timerach w AVR.
Timery taktowane są przez preskaler (dzielnik częstotliwości), który z kolei taktowany jest przez główny zegar (niektóre AVRy mają dodatkowy zegar do timerów). Uruchomienie i zatrzymanie timera sprowadza się do zapisu odpowiedniej wartości do preskalera 0 - zatrzymany, inne wartości - pracuje z wybranym stopniem podziału. Timer zlicza od zera w górę do wartości TOP. W najprostszym przypadku TOP = 255 dla timerów ośmiobitowych i 65535 dla szesnastobitowych. Są też inne możliwości, ale odsyłam do noty katalogowej. Po osiągnięciu wartości ustawiana jest flaga przerwania i timer rozpoczyna znowu zaczyna zliczać od zera. (również w tym wypadku są inne możliwości).
Wybieramy sobie timer 0, żadne cuda oferowane przez inne timery nie są potrzebne. Timery i wszystkie inne peryferia konfiguruje się przez zapis odpowiednich wartości do odpowiednich rejestrów, jakie to wartości i jakie rejestry - patrz nota katalogowa. Nazwy rejestrów i bitów w WinAVR są takie same jak w nowych(!) notach katalogowych. W razie problemów z językiem angielskim na elektrodzie jest gdzieś nota ATmegi32 po polsku. Wszystkie AVRy są podobne, więc nie powinno być problemów.
Timer 0 w ATmega8 nie ma praktycznie żadnych bajerów. Jedyne co trzeba skonfigurować, to włączyć przerwanie i ustawić preskaler. Rejestr TIMSK jest wspólny dla trzech timerów i służy do włączania przerwań generowanych przez różne zdarzenia związane z timerami (nie tylko przepełnienie, jak tutaj)
Albo robimy tak jak poniżej, nie ruszając ustawień innych przerwań (po resecie są wyłączone), albo po prostu przypisujemy tą wartość do TIMSK
Przerwanie już włączone, teraz trzeba tylko wybrać wartość preskalera jednocześnie uruchamiając timer. Dla zegara 1MHz sam timer (preskaler = 1) podzieli tą częstotliwość przez 256, bo co 256 kroków będzie się przepełniał i generował przerwanie. 1MHz / 1 / 256 = 3.9kHz, zdecydowanie za dużo jak na migającą diodę. Największa dostępna wartość preskalera to 1024. Więc: 1MHz / 1024 / 256 = 3.815Hz, lepiej, czyli średnio mniej więcej 3.815 raza na sekundę będzie wywoływane przerwanie. Miganie z taką częstotliwością da się już bez problemów zobaczyć, a będzie wolniej, ale o tym zaraz.
Za ustawienia preskalera odpowiedzialne są bity CS0x w rejestrze kontrolnym timera (TCCR0) wartości preskalera 1024 odpowiadają ustawione bity CS02 i CS00 (w nocie katalogowej jest tabelka).
TCCR0 = _BV(CS02) | _BV(CS00);
Od tego momentu timer zaczyna pracować, no ale co z tego, skoro jeszcze nie ma kodu, który będzie przerwania obsługiwał? Mało tego, żadne przerwanie nie będą obsługiwane dopóki nie ustawi się globalnej flagi uaktywniającej tą funkcjonalność. Flaga ta to bit I w rejestrze stanu (SREG) procesora. Jest do tego odpowiednia instrukcja (SEI), z poziomu C wywoływana jako makro:
Przy okazji, do globalnego wyłączenia przerwań służy
cli();.
I to by było na tyle konfiguracji. Teraz główna pętla programu może się "kręcić" w nieskończoność wykonując jakiś kod, a od czasu do czasu zostanie przerwana i wywołana zostanie procedura obsługi przerwania.
Do definicji procedury obsługi przerwania służy makro ISR() z
<avr/interrupt.h> Jako argument podaje się nazwę wektora przerwania, wszystkie dostępne dla danego procesora wymienione są między innymi w dokumentacji avr-libc (instaluje się razem z WinAVR). Do wyboru jest kilka wariantów. Najważniejsze, bo najczęściej stosowane to przerwania bez zagnieżdżenia (tzn. takie, które nie mogą być przerwane przez inne przerwania).
Definiuje się je w ten sposób:
ISR(TIMER0_OVF_vect) {
/* Kod tutaj umieszczony będzie wykonywany przy każdym przepełnieniu licznika timera 0,
** o ile oczywiście odpowiednie flagi przerwań zostały ustawione.
*/
}
Najprostszym sposobem na zrobienie migającego LEDa będzie zmiana stanu pinu na przeciwny. Można to zrobić za pomocą operatora różnicy symetrycznej (Exclusive-OR). Pobrany zostanie stan rejestru PORTB i na nim wykonana operacja EOR (czy XOR, jak kto woli) z wartością odpowiadającą wybranemu pinowi.
Do odczytu stanu linii portu, jak już wcześniej pisałem służy rejestr PINx, ale tu odczytamy wartość z rejestru PORTx, bo można i tak. Różnica jest taka, że jak zewrze się drutem pin procesora do masy i ustawi na tym pinie stan wysoki (zapisując odpowiednią wartość do rejestru PORTx), to z rejestru PIN odczyta się wartość logiczną jaka fizycznie na pinie jest, czyli zero, natomiast z rejestru PORT odczyta się wartość, jaka została tam zapisana, czyli 1. Oczywiście taka operacja spowoduje uszkodzenie procesora (uszkodzenie drivera wyjściowego portu), więc nie polecam testowania w praktyce.
W wypadku takiego rozwiązania w każdym przerwaniu stan pinu zmieniany będzie na przeciwny, więc pełny okres będzie obejmował dwa przerwania (włączenie i wyłączenie), stąd częstotliwość migania wyniesie około 3.815Hz / 2 = 1.907Hz. Po wykonaniu kodu w tej procedurze procesor powróci do wykonywania kodu w pętli głównej, do momentu, w którym została ona przerwana.
Przerwanie dla powyższego przykładu z możliwością zagnieżdżenia definiuje się tak:
ISR(TIMER0_OVF_vect, ISR_NOBLOCK)
Ale używanie takich przerwań w sposób nieprzemyślany może skończyć się zawieszaniem się programu (przepełnienie stosu).
Obsługę drugiego LEDa to już sobie sam dopisz w ramach ćwiczeń praktycznych ;].