Zegar cyfrowy LED z bajerami – Część III: Przerwa na przerwania zegarowe.

Zegar cyfrowy LED z bajerami – Część III: Przerwa na przerwania zegarowe.

Oczywiście nie obędzie się bez przydługiego wstępu. Na początek muszę się pożalić, że przez pisanie tego mini-poradnika od czterech dni nie przylutowałem ani jednego kabelka. Mam objawy odstawienne i początki nerwicy. Obiecałem sobie, że w weekend w ramach rekonwalescencji poskładam przetwornik cyfrowo-analogowy i zmuszę Arduino do jodłowania.

Dzisiaj będzie niestety dużo gadania, matematyki  i mało kodu. Dla mniej odpornych na teorię nie przewiduję specjalnych atrakcji. Nawet pół zdjęcia. Dobra. Wracamy do naszego zegara.

Jak wspomniałem pod koniec części drugiej, przyszedł czas na to, aby zupełnie pozbyć się odświeżania ekranu z główniej funkcji loop(). Między innymi po to, abyśmy swobodnie mogli wykorzystywać funkcję delay. W chwili obecnej, każde wywołanie delay() w pętli głównej, powoduje zatrzymanie wyświetlania na pojedynczej linii. Dlatego, do opóźnień pomiędzy wyświetleniami obrazków w poprzednim przykładzie, musieliśmy używać dodatkowego licznika cykli. Pamiętacie?

Zobaczcie jak pięknie i czytelnie mogłaby wtedy wyglądać funkcja loop():

Coś pięknego! Ale jak wywalić odświeżanie z funkcji loop?

Wyjadacze cicho stękną “w przerwaniu”. Tak, właśnie w przerwaniu. I tutaj jak zwykle kilka słów wyjaśnienia czym owe przerwania są.

Przerwanie to takie ZDARZENIE, które powoduje, że przerywane jest wykonywanie głównego kodu i wykonywany jest kod obsługi przerwania. Coś podobnego jak wywołanie funkcji, z taką różnicą, że nie wywołujemy jej jawnie w kodzie, tylko wywoływana jest automatycznie, zwykle zainicjowana przez jakieś urządzenie zewnętrzne lub sam procesor.

Przerwania bardzo często wykorzystywane są do obsługi zdarzeń sprzętowych. Przykładowo:

  • działanie programu jest przerywane, bo jakieś urządzenie zgłasza gotowość do transmisji danych.
  • Procesor “zapamiętuje” sobie co i gdzie właśnie robił,
  • przechodzi do obsługi urządzenia, odbiera/wysyła dane,
  • powraca do wykonywania głównego kodu.

I podobnie będzie w naszym przypadku, z tą różnicą, że przerwanie nie będzie wywoływane sprzętowo przez urządzenie zewnętrzne, a przez wewnętrzny zegar naszego mikroprocesora.

Układ Atmega 328P, który jest sercem mojego Arduino Uno posiada trzy timery nazywające się odpowiednio timer0, timer1 i jak nietrudno się domyślić – timer2. Możemy je wykorzystać do generowania cyklicznych przerwań. Ostrzegam wszystkich bojących się wyzwań – będzie trochę czarnej magii. Sam dopiero skubnąłem wiedzy o przerwaniach w Arduino, więc liczę na wyrozumiałość i jeżeli walnę jakąś bzdurę ;) Nieocenionym źródłem wiedzy była dla mnie strona: http://www.instructables.com/id/Arduino-Timer-Interrupts/. Niestety znów po angielsku.

Timery działają bardzo podobnie do zmiennej cykl z naszego ostatniego przykładu. Każdy z nich jest automatycznie inkrementowany w  cyklu zegara Arduino(16MHz), czyli 16 milionów razy na sekundę. Sporo, co? Ponadto, każdy z trzech zegarów posiada rejestr “porównawczy”. W każdym cyklu po zainkrementowaniu wartości timera, jest ona porównywana z rejestrem i jeżeli osiągnęliśmy oczekiwaną wartość, to licznik timera jest zerowany i wywoływane jest przerwanie zegarowe. Bingo! Czyli naszym zdarzeniem wywołującym przerwanie (rysowanie wiersza matrycy), będzie osiągnięcie przez jeden ze sprzętowych timerów zadanej wartości.

Oczywiście jak zwykle, jest drobne “ale”. Timer0 i timer2 są ośmiobitowe, więc liczą od 0 do 255 i się “przepełniają” , czyli wskakują na 0 i liczą od nowa. Timer1 ma szesnaście bitów, więc przepełnia się niby dopiero po osiągnięciu wartości 65535. Jeden cykl zegara (przy 16MHz)  trwa około 63ns, co daje nam maksymalny interwał rzędu 16μs dla timerów 0 i 2 oraz tylko około 4ms dla timera 1. Raczej słabo… generalnie do potrzeb matrycy LED, gdzie potrzebujemy właśnie odstępu rzędu milisekundy jest okej, ale gdybyśmy chcieli wywoływać przerwanie raz na sekundę? Timerów nie można w żaden sposób sumować, ani łączyć.

Każdy z timerów wyposażony jest także w preskaler (prescaler). Nie wiem jak to zgrabnie nazwać po polsku… podzielnik? Może “wyjadacze” podpowiedzą w komentarzach :) Ów preskaler/podzielnik, pozwala modyfikować nam bazową częstotliwość taktowania zegara procesora dla danego timera według wzoru:

częstotliwość timera = częstotliwość procesora (16MHz) / podzielnik;

 Czyli dla podzielnika 8, nasz timer będzie tykał już tylko 2 miliony razy na sekundę. Coraz lepiej. Tylko jak duży może być nasz podzielnik? Duży. Maksymalnie 1024, jednak jest kolejne “ale”. Podzielnik może przyjmować tylko określone wartości, a dokładnie: 1, 8, 64, 256, oraz właśnie 1024. No to liczymy:

16000000 Hz/1024 = 15625Hz

 Tyle razy na sekundę inkrementowany jest timer z podzielnikiem 1024.

1/15625Hz = 0,000064 sekundy = 64 us

Tyle trwa wtedy jeden cykl timera. Zatem dla timera 1:

0,000064 s * 65535 = 4,19424 sekundy

 To maksymalny odstęp z jakim możemy wywoływać przerwania zegarowe. A dokładniej jedno bo 16 bitów ma timer1.

Dla timera 0 i 2 maks to:

64ms*255=16ms

To tyle przydługiej teorii, teraz przejdźmy do praktyki.

Dane wejściowe jakich potrzebujemy dla zainicjowania naszego przerwania zegarowego to wartość podzielnika i rejestru porównawczego. Do wyliczenia tych wartości musimy wiedzieć, jaką dokładnie częstotliwość wywołania przerwania potrzebujemy dla naszej matrycy.

Zakładam, że ma być to wersja Pro De-Luxe, więc nie zadowolimy się marnymi 50Hz , przy których podobno oko ludzkie już nie widzi migotania. Niech będzie szalone 100Hz! Jeżeli chcemy, aby cała matryca odświeżała się 100 razy na sekundę, to pamiętając że jeden cykl “odpala” tylko jeden wiersz, musimy pomnożyć sobie nasze 100Hz * 5. Czyli funkcję rysującą wiersz musimy wywoływać 500 razy na sekundę. Czyli (1/500 = 0.002)  co 2 milisekundy. Skoro maks dla timerów 8 bitowych to 16ms, to dla naszego sterownika spokojnie wystarczy timer 8 bit. Nie potrzebujemy super precyzji – jak będzie 99Hz to oko nam nie pęknie. Okej, znów chwila matematyki: użyjemy zmyślnego wzoru, do wyliczenia wartości rejestru porównawczego:

OCRxA  = ( bazowa częstotliwość procesora / ( dzielnik * oczekiwana częstotliwość) ) – 1

OCRxA to właśnie nazwa rejestru porównawczego, gdzie w miejsce x wstawiamy numer odpowiedniego timera. Chcemy 500Hz , to liczymy:

(16000000/(1*500))-1 = 31999

Na 8 bitów (0-255) to sporo za dużo, więc śmiało podkręcamy dzielnik ;)

 (16000000/(64*500))-1 = 499

O! już blisko, następna dozwolona wartość dzielnika to 256.

(16000000/(256*500))-1 =124

Bingo! Z podzielnikiem 256 mieścimy się w 8 bitach.

Wartość podzielnika ustalamy, zapalając odpowiednie bity rejestru kontrolnego B danego timera. Brzmi na skomplikowane, a wcale nie jest. Każdy timer ma dwa rejestry kontrolne (A i B). Nazywają się odpowiednio: TCCRxA i TCCRxB, gdzie w miejsce x wstawiamy numer timera.

Rejestry te służące do wyboru trybu pracy, oraz wartości podzielnika. 3 z bitów rejestru B nazywają się CS10, CS11 i CS12 i właśnie one służą do ustawiania wartości podziału, według następującej tabeli:

tabela-CS

Tylko jak ustawić konkretny bit rejestru?

TCCRxB |= (1 << CS12);  //dzielnik 256

Właśnie tak. Jeżeli chcemy ustawić kilka, to możemy to zrobić kolejno, lub jednym poleceniem:

TCCRxB |= (1 << CS12) | (1 << CS10);  //dzielnik 1024

dla wszystkich spragnionych głębszej wiedzy na temat rejestrów timerów odsyłam do: https://sites.google.com/site/qeewiki/books/avr-guide/timers-on-the-atmega328

Do użycia timerów nie potrzebujemy żadnych dodatkowych bibliotek. Niestety, dla początkujących kod może wyglądać na nieco zagmatwany. Nie przejmujcie się, ważne żebyście zrozumieli jak ustawiać dzielnik i rejestr porównawczy. Timery ustawiamy (inicjujemy) w funkcji setup().

Analogicznie ustawiamy pozostałe timery. Wystarczy zamienić wszystkie dwójki w nazwach rejestrów na odpowiedni numer timera. A! I jeszcze jedno. Tryb obsługi przerwań (CTC) ustawiany jest innymi bitami dla każdego timera. Dla timera 0 linia 16 powinna wyglądać:

dla timera 1:

Brakuje nam jeszcze tylko jednej rzeczy. Musimy wiedzieć jak wskazać timerowi kod, który ma wykonać w momencie wystąpienia przerwania.

Tyle. Nic prostszego. Analogicznie dla pozostałych timerów. Wystarczy zamienić dwójkę na numer timera. No to mamy wszystko.

Kod odświeżający matryce 100 razy na sekundę w przerwaniu zegarowym  wygląda tak:

Uniezależniliśmy się od wywołań w funkcji loop!!! Jeszcze tylko jedna uwaga na koniec dla tych, którzy chcą używać przerwań zegarowych w swoich projektach. Starajcie się, aby kod obsługi przerwania wykonywał się jak najszybciej. Nie pakujcie tam skomplikowanych obliczeń czy operacji na dużych ilościach danych. Może to powodować znaczne spowolnienie działania głównego programu, w skrajnym przypadku gdy obsługa będzie dłuższa niż interwał między wywołaniami przerwanie zacznie przerywać działanie samego siebie, dojdzie do przepełnienia stosu i program się pięknie wykrzaczy.

Ale to już chyba wszystko na dziś. Jeszcze tylko w następnej części poradnika dorobimy wyświetlanie cyferek i zaraz potem będziemy budować moduł zegara czasu rzeczywistego. Tak niewiele nam zostało aby znów zacząć lutować :) Radujmy się!

Ocena: 4.68/5 (głosów: 50)

Podobne posty

21 komentarzy do “Zegar cyfrowy LED z bajerami – Część III: Przerwa na przerwania zegarowe.

  • Ładny artykuł ;P Ładnie wyjaśnione ;] Dodam tylko, że o ile dobrze pamiętam to Arduino ma do dyspozycji tylko dwa przerwania zewnętrzne (w artykule oczywiście mowa o wewnętrznych przerwaniach zegarowych, a właściwie licznikowych) ;] No i żeby przerwanie działało jak ma działać to wszelkie zmienne używane w przerwaniach muszą mieć przed inicjowaniem słówko volatile. A dla przerwań zewnętrznych niezbędna jest również

    attachInterrupt(nr_pinu, nazwa przerwania, zmienna np. CHANGE);

    Ach rozbudziłeś moją wenę ;] Chyba coś niedługo skrobnę dla Majsterkowa o STM32F3 Discovery, którym się teraz bawię ;] Mam tu taką fajną strukturę co się zajmuje obsługą przerwań, których mam chyba z 240 i przy stosowaniu kilku naprawdę robi się ładna przepychanka – a pętla nieskończona nic o tym nie wie ;] Na chwilę obecną zrobiłem nowszą wersję artykułu o środowisku dla STM32F3 Discovery jakby ktoś był zainteresowany i o sygnałach zegarowych ;]

    No i oczywiście preskaler to preskaler, a najłatwiej po prostu dzielnik ;]

    Pozdrawiam serdecznie ;] i tak trzymać ;]

    Odpowiedz
    • Do tego na czas wykonania funkcji przerwania dobrze jest wylaczyc obsluge przerwan – inaczej, jesli przerwanie jest generowane przez element mechaniczny/analogowy (przycisk, fotorezystor) i nie mamy debouncingu bardzo latwo zawiesimy w ten sposob mikrokontroler.
      W artykule brakuje mi jeszcze uscislenia, jak to jest z PWMami przy majstrowaniu z przerwaniami zegarowymi – kazdy timer obsluguje m.in. PWMy na 2-3 pinach. Ale sam dokladnie nie wiem, co im mozna, a czego nie mozna zrobic, zeby PWM na tych pinach nadal dzialal.
      Poza tym – bardzo dobry artykul i gratulacje dla autora, sporo uzytecznych informacji zebranych w jednym miejscu.

      Odpowiedz
    • dziękuje :D starałem się.
      Dziś planuje napisać trochę o zarządzaniu pamięcią i PROGMEM, żeby zakończyć termat sterowania matrycą. Już zaraz potem będą mniej ambitne rzeczy. Może wtedy się uda na główną ;)

      Odpowiedz
    • Możliwe, ale bądźmy dobrej nadziei, że Łukasz zaradzi ;] Osobiście dałem 5 ;] Merytorycznie jest dobrze, także jak początkujący nie łapią to nie ma sensu żeby zaniżali ocenę dla fachowego artykułu. Tyle na temat.

      Odpowiedz
  • Dobrym pomysłem jest chyba blokowanie przerwań, gdy któreś obsługujemy (no chyba, że na pewno chcemy inaczej). Wtedy nie ma strachu, że przepełni się stos przez przerwania.

    Odpowiedz
    • Tak, to ważne. Szczególnie gdy przerwania “konkurują” o dostęp do urządzeń, lub któreś z nich wymaga wyłączności na dostęp do jakichś zasobów.

      Na szczęście, w tym prostym przypadku chyba nie ma żadnego z tych ryzyk. Pewnie gdybym chciał obsługiwać dodatkowo inne urządzenia przez SPI (co jest możliwe) musiałbym się mocniej nagimnastykować :D

      Dzięki za celną uwagę ;)

      Odpowiedz
  • Właśnie staram się ustawić przerwanie do mojego własnego zastosowania, no i op porównaniu ze stroną do której linkowałeś wychodzi mi nieścisłość. Jestem w temacie zielony i bez doświadczenia w poszukiwaniu błędów takie rzeczy mogą pogrzebać mój program :D

    Ale do rzeczy:
    Ustawiasz TCCR2B tak:
    “TCCR2B |= (1 << CS11); // ustawiam dzielnik na 256"
    Nie powinno to wyglądać tak:
    "TCCR2B |= (1 << CS[b]2[/b]1); // ustawiam dzielnik na 256"

    Na tej stronie właśnie tak wygląda.
    https://sites.google.com/site/qeewiki/books/avr-guide/timers-on-the-atmega328

    Odpowiedz
  • To wyżej to pisałem ja, niestety bbcode tu nie działa.
    chodziło mi o to oczywiście:

    Odpowiedz
  • Tylko jeden mały komentarz :)

    Gdy uC zgłasza przerwanie (dowolne), blokuje system przerwań, rozkaz powrotu z procedury obsługi przerwania ponownie je uaktywnia :) Także jeśli w procedurze obsługi przerwania nie włączymy przerwań sami, to nie grozi nam zapętlenie :) Co najwyżej możemy “zgubić” jakieś przerwanie, jeśli będziemy marudzić w procedurze obsługi przerwania :) Jedyne przerwanie jakie zawsze jest przyjmowane, niezależnie od systemu przerwań, to sygnał RESET, który (o dziwo) jest po prostu przerwaniem o najwyższym poziomie :)

    Poza tym, artykuł naprawdę świetny!

    Odpowiedz
  • “w skrajnym przypadku gdy obsługa będzie dłuższa niż interwał między
    wywołaniami przerwanie zacznie przerywać działanie samego siebie,
    dojdzie do przepełnienia stosu i program się pięknie wykrzaczy”
    A można w środku tego przerwania na początku wyłączyć przerwania, a pod koniec je znowu włączyć? Pytam tak czysto teoretycznie. Bo w tedy w wykonywaniu przerwania nic nie przerwie :p Chyba że wyłączając przerwania w funkcji przerwania wyłączy się też aktualne przerwanie :D
    Ale incepcja >.<

    Odpowiedz
  • Uratowałeś mi życie. Nie mogłem za nic ustawić tego CTC, wszystko wygladało ze jest ok, a nie chcialo chodzic, nie mam pojecia gdzie miałem błąd. Dzieki wielkie za ten poradnik.

    Odpowiedz

Odpowiedz

anuluj

Masz uwagi?