Redakcja: dondu
Drzaśkowy pamiętnik: Spis treści
Jak do tej pory, uruchomiłem już sobie pierwszy układ, napisałem kilka prostych programów, które jak na razie nie robią nic, prócz mrugania diodami.
Nie po to jednak wymyślono mikrokontrolery i nie po to również ja sam zapragnąłem nauczyć się z nimi „rozmawiać”, żeby używać je tylko i wyłącznie do tak prostych operacji.
Układ z mikroprocesorem (CPU) ma przede wszystkim liczyć a pośrednio analizować, obrabiać, pobierać, wysyłać dane, mierzyć itd. Zdecydowana większość tych operacji nie jest deterministyczna. Zrodziło się więc pytanie:
Jak odmierzać czas jeśli CPU zajęty jest innymi, czasochłonnymi operacjami?
No więc tu trzeba było się trochę zagłębić, poszukać, poczytać artykuły, forum, dokumentację procesora. Timer - to jest to odpowiedź na moje pytanie. :-)
W mikrokontrolerach można spotkać timery 8 lub 16 bitowe. To wewnętrzne układy liczące impulsy, który niezależnie od działającego programu zliczają impulsy lub odliczają dokładnie (w zależności od dokładności wybranego źródła sygnału zegarowego dla timera) czas.
Timer zastosować chciałem do sterowania dwoma diodami LED. Układ ten sam, którego schemat znajduje się w poprzedniej części mojego pamiętnika:
Szukając na forum znalazłem kilka przykładów jak uruchomić timer. Jednak z samych przykładów, komentowanych nieraz w mało zrozumiałe formułki niewiele można się dowiedzieć.
Dlatego moim sposobem na zrozumienie działania programu oraz następnie dostosowanie go do swoich potrzeb jest poszukanie przykładowego programu, a następnie analizowanie go krok po kroku z dokumentacją mikrokontrolera.
W przypadku mikrokontrolera ATmega8 jest on wyposażony w trzy timery - dwa 8-bitowe i jeden 16-bitowy.
Zainteresowałem się 8-bitowym układem „Timer/Counter0”:
Nieodzownym elementem większości timerów jest preskaler czyli dzielnik sygnału zegarowego taktującym timer. W przypadku Timer0 w Atmega8 preskaler jest połączony z preskalerem Timer1:
Dlatego mnie interesuje ta część układu preskalera zaznaczona na czerwono
Za pomocą fusebitów ustawionych fabrycznie na wewnętrzny generator RC o częstotliwości 1MHz, sygnał ten jest jednocześnie sygnałem clki/o taktującym preskaler timera (patrz rysunek powyżej). Mam do wyboru dzielniki preskalera o wartościach 8, 64, 256 i 1024. Mogę wybrać jeden z dzielników, który będzie dzielił częstotliwość wejściową preskalera przez wybraną wartość. Zdecydowałem się na 1024, co daje:
Czyli z preskalera impulsy na Timer0 będą wychodzić z częstotliwością około 1000Hz, co daje mi czas jednego impulsu równy:
Przerwania
Zanim napisałem dalszą część, musiałem się dowiedzieć: Co to jest przerwanie?
Znalazłem więc na Wikipedii: Przerwanie
Czyli przerwanie to sygnał, który powoduje przerwanie wykonywania programu przez procesor i przejście do wykonywania programu obsługi danego przerwania, a po jego zakończeniu powrót do przerwanego programu.
Timer0 jest timerem 8-bitowym liczącym od 0 do 255, i potrafi generować przerwanie w przypadku jego przepełnienia. Czyli przerwanie z przepełnienia Timer0 będzie się pojawiać co:
czyli około 4 razy na sekundę:
Zacząłem więc pisać mój program.
Najpierw wybrałem źródło taktowania Timer0 oraz preskaler, a następnie włączyłem przerwania od przepełnienia Timer0:
TCCR0 |= (1<<CS02) | (1<<CS00); // źródłem CLK, preskaler 1024 TIMSK |= (1<<TOIE0); //Przerwanie overflow (przepełnienie timera)
Teraz musiałem przygotować część programu, która wykonywana będzie gdy Timer się przepełni i zgłosi przerwanie. W moim przypadku, miało to być zmiana stanu świecenia LED na przeciwny. Dowiedziałem się, że należy zdefiniować specjalną funkcję obsługi przerwania o nazwie ISR(), w której w nawiasach podajemy nazwę przerwania (tzw. wektor przerwania). Więcej przeczytałem tutaj: AVR Interrupts
Zdefiniowałem więc swoją funkcję następująco:
ISR(TIMER0_OVF_vect) { PORTB ^=(1<<LED1); //suma modulo 2 (XOR) stanu poprzedniego na porcie //czyli zmiana stanu pinu LED1 na przeciwny PORTB ^=(1<<LED2); //suma modulo 2 (XOR) stanu poprzedniego na porcie //czyli zmiana stanu pinu LED2 na przeciwny }Przy takim sposobie zmiany stanu świecenia diod LED ich częstotliwość mrugania jest dwa razy mniejsza niż wynika to z powyższych obliczeń , ponieważ co drugie przerwanie gasi diody, a co drugie je zapala.
Następnie przerobiłem swój program z poprzedniej części pamiętnika i otrzymałem:
#include <avr/io.h> #include <avr/interrupt.h> //definicja LED1 (do którego pinu podłączony LED1) #define LED1 PB0 //definicja LED2 (do którego pinu podłączony LED2) #define LED2 PB1 void main(void) { //########### I/O ########### DDRB |= (1<<LED1) | (1<<LED2); //Ustawienie pinów sterujących diodami // jako wyjścia PORTB |= (1<<LED1); //Ustawienie stanu wysokiego na wyjściu sterującym LED1 //(stan początkowy) //########################## //######## konfiguracja timera ############## TCCR0 |= (1<<CS02) | (1<<CS00); // źródłem CLK, preskaler 1024 TIMSK |= (1<<TOIE0); //Przerwanie overflow (przepełnienie timera) //########################################### sei(); //Globalne uruchomienie przerwań while(1); //główna pętla programu } //############ Procedura obsługi przerwania od przepełnienia timera ############ ISR(TIMER0_OVF_vect) { PORTB ^=(1<<LED1); //suma modulo 2 (XOR) stanu poprzedniego na porcie //czyli zmiana stanu pinu LED1 na przeciwny PORTB ^=(1<<LED2); //suma modulo 2 (XOR) stanu poprzedniego na porcie //czyli zmiana stanu pinu LED2 na przeciwny } //##############################################################################
Efekt był właściwy, czyli diody migały z częstotliwością 2Hz.
Następnie chciałem uzyskać dokładniejsze czasy mrugania diodami.
Z pomocą tutaj przyszedł rejestr TCNT0 do którego można wpisać początkową wartość, od której zaczyna liczyć Timer.
Postanowiłem uzyskać częstotliwość możliwie najbardziej zbliżoną do 1 Hz. Standardowo licznik liczy od 0 do 255. Ale ustawiając rejestr TCNT0 na inną początkową wartość, mogę zawęzić zakres liczenia.
Na przykład ustawiając wartość tego TCNT0 równą 6, licznik liczy od 6 do 255. Uzyskuję więc 250 odcinków czasu po około 1ms (obliczony wyżej) każdy, czyli prawie dokładnie 250 ms do przepełnienia, po czym generuje przerwanie.
Nie mając już możliwość ustawienia większego preskalera posłużyłem się zmienną-licznikiem:
#include <avr/io.h> #include <avr/interrupt.h> //definicja LED1 (do którego pinu podłączony LED1) #define LED1 PB0 //definicja LED2 (do którego pinu podłączony LED2) #define LED2 PB1 //definicja początkowej wartości timera #define timer_start 6 //zmienna pomocnicza-licznik używana w przerwaniu volatile uint8_t cnt=0; void main(void) { //########### I/O ########### DDRB |= (1<<LED1) | (1<<LED2); //Ustawienie pinów sterujących diodami // jako wyjścia PORTB |= (1<<LED1); //Ustawienie stanu wysokiego na wyjściu sterującym LED1 //(stan początkowy) //########################## //######## konfiguracja timera ############## TIMSK |= (1<<TOIE0); //Przerwanie overflow (przepełnienie timera) TCCR0 |= (1<<CS02) | (1<<CS00); // źródłem CLK, preskaler 1024 TCNT0 = timer_start;// //Początkowa wartość licznika //########################################### sei();//Globalne uruchomienie przerwań while(1);//główna pętla programu } //############ Procedura obsługi przerwania od przepełnienia timera ############ ISR(TIMER0_OVF_vect) { TCNT0 = timer_start; //Początkowa wartość licznika cnt++; //zwiększa zmienną licznik if(cnt>3) //jeśli 4 przerwania (czyli ok 1 s) { PORTB ^=(1<<LED1); //suma modulo 2 (XOR) stanu poprzedniego //na porcie i pinu LED1 (zmiana stanu na przeciwny) PORTB ^=(1<<LED2); //suma modulo 2 (XOR) stanu poprzedniego na porcie //pinu LED2 (zmiana stanu na przeciwny) cnt=0; //zeruje zmienną licznik } } //##############################################################################
Przeczytaj także: Czas - odmierzanie
Drzaśkowy pamiętnik: Spis treści
Autor: drzasiek
Redakcja: dondu
Widziałem, że do przerwań wykorzystywana jest czasami funkcja SIGNAL(). Ty wykorzystujesz ISR(). Jaka jest różnica?
OdpowiedzUsuńSIGNAL jest już przestarzałe, i nie powinno się już tego stosować.
UsuńPolecam link:
OdpowiedzUsuńhttp://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html
Zerknij też do pliku nagłówkowego interrupt.h,
[quote]deprecated Do not use SIGNAL() in new code. Use ISR() instead.[/quote]
Czyli jednym słowem SIGNAL jest przestarzałe i nie należy go używać.
Zamiast tego używać trzeba wektorów ISR.
Panie Drzaśku. A jak np. potrzeba odmierzyć 80 us, Częstotliwość procesora 16 MHz. Jak się należy za to zabrać. Podałby Pan przykład? Pozdrawiam
OdpowiedzUsuńNajpierw liczysz jaką częstotliwość przerwań potrzebujesz:
OdpowiedzUsuń80 us = 0,00008s
1/0.00008=12500 Hz
Teraz otwierasz tabelkę w DS jakie masz możliwości preskalera. W M8 jest to:
brak, 8, 64, 256, 1024
I teraz dzielisz twój zegar, czyli 16 MHz przez preskaler, sprawdzając jaką częstotliwość naliczania licznika otrzymasz:
16000000/1024=15625 Hz
Następnie dzielisz tą wartość przez częstotliwość przerwań jaką chcesz otrzymać, czyli w twoim przypadku 12500 Hz:
15624/12500=1,25
Wyszła wartość niecałkowita, więc to nas nie interesuje. Prubujemy dalej, czyli dzielimy przez następny preskaler czyli 256:
16000000/256=62500 Hz
62500/12500=5
Jest wartość całkowita.
Czyli Licznik musi naliczyć 5 taktów zegara i zgłosić przerwanie.
Dla Timera 8 bit max wartosć to 255 zatem musi zaczynać liczyć od 255-5 czyli dla powyższego przykłady do rejestru TCNT0 po każdym przerwaniu overflow powinieneś wpisać wartość 250.
Licznik będzie liczył tak:
251,252,253,254,255,przerwanie/250,251,252,253,254,255,przerwanie/250 itd
I w ten sposób licznik będzie się zwiększał z częstotliwością 62500 Hz ale będzie zgłaszał przerwanie co 5 wartość a więc 62500/5=12500 Hz czyli co 80 us.
A w liczeniu może Ciebie wyręczyć darmowy kalkulator, który znajdziesz w menu Kalkulatory elektronika na górze bloga.
OdpowiedzUsuńWitam a dlaczego zdefiniowałes również klawisz, czemu on miałby służyc?
OdpowiedzUsuńPozostałości po poprzedniej wersji programu.
OdpowiedzUsuńDzięki za informację, już zgłoszone do poprawienia.
Usunąłem zbędne fragmenty kodu.
OdpowiedzUsuńA dlaczego po każdym przerwaniu trzeba wpisywać liczbę 250 żeby generować przerwanie po zliczeniu 6 sygnałów? Nie lepiej i prościej do rejestru OCR0 raz wpisać raz liczbę 5 i ustawić w rejestrze TIMSK bit OCIE0 czyli generowanie przerwania po zrównaniu się licznika z właśnie rejestrem OCR0?
OdpowiedzUsuńWitam. Nie wiem czy można umieszczać linki do innych serwisów, ale tutaj znalazłem naprawdę Wybitny artykuł na temat timerów:
OdpowiedzUsuńhttp://diycenter.acid19.linuxpl.com/readarticle.php?article_id=3
Witam mam pytanie odnośnie uzyskania czasu zbliżonego do 1Hz zmiany stanu diody?
OdpowiedzUsuń"Na przykład ustawiając wartość tego TCNT0 równą 6, licznik liczy od 6 do 255. Uzyskuję więc 250 odcinków czasu"
Jeżeli Timer0 jest 8 bit-owy to jego wartości są liczone: 0...255.
Czy 6 to nie jest przypadkiem 7 impulsów?
Jeżeli ustawię Timer2 dla diody 2 w tryb CTC i w rejestrze OCR2 podam wartość 250, a Timer0 tak jak w artykule dla diody 1 to diody po kilku zmianach stanu przestaną migać jednocześnie.
Czy to znaczy, że dla Timera2 liczy 251 impulsów czy dla Timera0 249?
Janek
W trzecim kodzie brakuje ukośników w nazwach plików dla #include:
OdpowiedzUsuń#include
#include
Mały szczegół, ale przez to program się nie kompiluje ;]
Poprawione - dziękuję za informację!
OdpowiedzUsuń:-)
Takie artykuły piszą tylko w Stanach. Może gdy by więcej ludzi tak podchodziło to swojej wiedzy nie tylko USA miało by takie PKB :))) Gratuluje !!
OdpowiedzUsuńPanie drzasiek , może Pan podać źródło gdzie można znaleść informacje na temat rejestrów wykorzystywanych w przerwaniach i w Timerach ?
OdpowiedzUsuńNa początku artykułu jest pokazany fragment dokumentacji mikrokontrolera (ang. datrasheet) . To właśnie w nim znajdziesz wszelkie potrzebne informacje, o czym pisze powyżej Drzasiek.
UsuńCo zawiera taki dokument i jak z niego korzystać opisuje aktualnie Tomasz Francuz w cyklu artykułów, którego pierwszą część możesz przeczytać tutaj: Jak czytać noty katalogowe mikrokontrolerów AVR (cz. I)
Gdzie można znaleźć ten dokument? Wystarczy w Google wpisać nazwę mikrokontrolera. Ja jednak polecam zaglądać na stronę jego producenta, w tym wypadku: Atmel.com
Powyżej drobna literówka: datasheet, a nie datrasheet.
UsuńJeśli mogę spytać: nie rozumiem czemu gdy wpisze ISR(TIMER0_OVF_vect) mam błąd static declaration of 'TIMER0_OVF_vect' follows non-static declaration . Ktoś wie o co chodzi?
OdpowiedzUsuńJuż wszystko działa, Zapomniałem, że ISR to oddzielna funkcja :D
UsuńNie mogę dojść do jednego ... skoro chciałeś aby diody mrugały z 1 hz to w ciągu 1 sekundy powinien zmieścić się 1 okres, a ten z kolei składa się na świecenie i nieświecenie diody, czyli z okresem 1hz pół sekundy dioda powinna się palić, a pół sekundy nie. Z Twojego przykładu wynika, że diody mrugają z częstotliwością 0,5 hz.
OdpowiedzUsuńJeśli się mylę, to proszę naprowadzić mnie na dobrą drogę ;) !
Pytam się dokładniej, ponieważ potrzebuję wygenerować na led ir 36 kHz.
Pozdrawiam
A na liczniku CTC nie łatwiej ?
OdpowiedzUsuńŁatwiej ale CTC mamy w Timerze 16 bitowym w przypadku megi8 :)
UsuńMi ten kod ostateczny niestety nie działa, diody obie sie świecą, ale wcale nie chcą migać. Wie ktoś dlaczego? na atmedze128 pracuje
OdpowiedzUsuńPokaż program, to sprawdzimy :)
Usuńw tutorialu jest drobny blad...
OdpowiedzUsuńlinia 28: ustawienie TCNT0 na 6 faktycznie powoduje liczenie do 250 (ms) ale tylko raz. Licznik po przepelnieniu sie zeruje i liczy znowu od 0, a nie od 6.
Proponuje rozwiazac to przez dodanie do:
ISR(TIMER0_OVF_vect)
wiersza:
TCNT0 = timer_start;
przez co couter po strigerowaniu sam bedzie sie ustawial na wartosc poczatkowa
Ladniej to widac na timer1 (16 bitowym) gdzie czas przepelnienia dla TCNT1=0 to okolo 8 sec (dla 8 MHZ) a po ustawieniu wartosci TCNT1 na powiedzmy 32000 czas sie skraca (w zaleznosci czy zrobimy to w mainie czy w przerwaniu, to tylko raz, lub na stale)
Witam!
OdpowiedzUsuńMam problem z ATTiny13A wywala mi blad i nie wiem gdzie jest blad.
Kod jest w temacie na elektroda.pl
http://www.elektroda.pl/rtvforum/viewtopic.php?t=3188612&highlight=
Prosze o pomoc i pozdrawiam Kornel
1. W jakim celu stosuje się przedrostek "voltaire" przy definiowaniu zmiennej?
OdpowiedzUsuń2. Nie dokładniej było by gdyby ustawić TCNT0=12; ? Wtedy przerwanie następowałoby co 249,856 ms, czyli dokładniej niż w przykładzie pokazanym powyżej.
W ostatnim programie na symulacji w linijce 16(void main(void) pokazuje błąd
OdpowiedzUsuńCo zrobić aby mieć stan wysoki co 64000000us,i stan niski 64000000 us,okres 128000000us,duty cycle. 50%.
OdpowiedzUsuńnadal nie rozumiem!
OdpowiedzUsuńCo zrobić aby mieć stan wysoki co 64 sekund,i stan niski 64sekund,okres 128000000us,duty cycle. 50%.
OdpowiedzUsuńD-flip flop co 64 sekund , między pinami PB0 i PB1 ,policyjny kogut.
https://www.google.com/search?q=avr+flip+flop&oq=avr+flip+flop+&aqs=chrome..69i57.5276j0j8&sourceid=chrome&ie=UTF-8