Mikrokontrolery - Jak zacząć?

... czyli zbiór praktycznej wiedzy dot. mikrokontrolerów.

wtorek, 1 marca 2011

LED sterowany przez Timer

Autor: drzasiek
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
Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

26 komentarzy:

  1. Widziałem, że do przerwań wykorzystywana jest czasami funkcja SIGNAL(). Ty wykorzystujesz ISR(). Jaka jest różnica?

    OdpowiedzUsuń
  2. Polecam link:
    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.

    OdpowiedzUsuń
  3. 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ń
  4. Najpierw liczysz jaką częstotliwość przerwań potrzebujesz:
    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.

    OdpowiedzUsuń
  5. A w liczeniu może Ciebie wyręczyć darmowy kalkulator, który znajdziesz w menu Kalkulatory elektronika na górze bloga.

    OdpowiedzUsuń
  6. Witam a dlaczego zdefiniowałes również klawisz, czemu on miałby służyc?

    OdpowiedzUsuń
  7. Pozostałości po poprzedniej wersji programu.
    Dzięki za informację, już zgłoszone do poprawienia.

    OdpowiedzUsuń
  8. Usunąłem zbędne fragmenty kodu.

    OdpowiedzUsuń
  9. 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ń
  10. Witam. Nie wiem czy można umieszczać linki do innych serwisów, ale tutaj znalazłem naprawdę Wybitny artykuł na temat timerów:

    http://diycenter.acid19.linuxpl.com/readarticle.php?article_id=3

    OdpowiedzUsuń
  11. Witam mam pytanie odnośnie uzyskania czasu zbliżonego do 1Hz zmiany stanu diody?

    "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

    OdpowiedzUsuń
  12. W trzecim kodzie brakuje ukośników w nazwach plików dla #include:
    #include
    #include
    Mały szczegół, ale przez to program się nie kompiluje ;]

    OdpowiedzUsuń
  13. Poprawione - dziękuję za informację!
    :-)

    OdpowiedzUsuń
  14. 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ń
  15. 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ń
    Odpowiedzi
    1. 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.

      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

      Usuń
    2. Powyżej drobna literówka: datasheet, a nie datrasheet.

      Usuń
  16. 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ń
    Odpowiedzi
    1. Już wszystko działa, Zapomniałem, że ISR to oddzielna funkcja :D

      Usuń
  17. 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.

    Jeśli się mylę, to proszę naprowadzić mnie na dobrą drogę ;) !
    Pytam się dokładniej, ponieważ potrzebuję wygenerować na led ir 36 kHz.

    Pozdrawiam

    OdpowiedzUsuń
  18. A na liczniku CTC nie łatwiej ?

    OdpowiedzUsuń
    Odpowiedzi
    1. Łatwiej ale CTC mamy w Timerze 16 bitowym w przypadku megi8 :)

      Usuń
  19. Mi ten kod ostateczny niestety nie działa, diody obie sie świecą, ale wcale nie chcą migać. Wie ktoś dlaczego? na atmedze128 pracuje

    OdpowiedzUsuń
  20. Artur Matkowski10 marca 2016 11:17

    w tutorialu jest drobny blad...
    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)

    OdpowiedzUsuń
  21. Witam!
    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

    OdpowiedzUsuń

Działy
Działy dodatkowe
Inne
O blogu




Dzisiaj
--> za darmo!!! <--
1. USBasp
2. microBOARD M8


Napisz artykuł
--> i wygraj nagrodę. <--


Co nowego na blogu?
Śledź naszego Facebook-a



Co nowego na blogu?
Śledź nas na Google+

/* 20140911 Wyłączona prawa kolumna */
  • 00

    dni

  • 00

    godzin

  • :
  • 00

    minut

  • :
  • 00

    sekund

Nie czekaj do ostatniego dnia!
Jakość opisu projektu także jest istotna (pkt 9.2 regulaminu).

Sponsorzy:

Zapamiętaj ten artykuł w moim prywatnym spisie treści.