Timery, przerwania i multipleksowanie w AVR ATTINY84A z wyświetlaczem 7 segmetowym
Dzisiaj zajmiemy się tematem timerów w mikrokontrolerach AVR. Aby praktycznie zapoznać się z tematem zaprogramujemy podwójny wyświetlacz 7 segmentowy i poznamy pojęcie multipleksowania. Napiszemy program, za dzięki któremu będziemy w stanie sterować 14 elementami za pomocą tylko 10 pinów.
Wyświetlacz 7 segmentowy podwójny
Do wykonania zadania będziemy potrzebować przede wszystkim wyświetlacza 7 segmentowego podwójnego lub większego. Wyświetlacz 7 segmentowy pozwala na wyświetlanie cyfr. Jedna taka cyfra składa się z 7 diod plus 1 na kropkę ze wspólną anodą.
Dzisiaj wykorzystamy wyświetlacz jak na obrazku powyżej. Posiada on wbudowanych w siebie 16 diod. Po 7 na każdą cyfrę oraz kropka. Czyli na cyfrę z kropką będziemy potrzebować co najmniej 8 pinów oraz jednego pinu na anodę.
W dzisiejszym przykładzie podepniemy po dwie katody pod każdy z 8 pinów naszego mikrokontrolera. Gdybyśmy mieli wyświetlacz na cztery cyfry to pod jeden pin mikrokontrolera podpięlibyśmy 4 katody analogiczne dla każdej cyfry. Z kolei każda każda wspólna anoda będzie podpięta pod oddzielny pin mikrokontrolera. Dzięki temu ustawiając odpowiednio stany niskie na katodzie i stany wysokie na odpowiedni pinach mikrokontrolera będziemy w stanie zapalić określone segmenty określonych cyfr.
Niestety w jednym momencie będziemy w stanie zapalić segmenty odpowiedzialne za jedną cyfrę (lub taką samą cyfrę na wszystkich pozycjach bo katody mamy podpięte do tych samych pinów).
Rozwiązaniem jest multipleksowanie czyli szybkie przełączanie kolejnych pozycji. Jeśli chcemy np wyświetlić 24 to najpierw na krótki czas włączamy pozycję z cyfrą 2 a później na podobny okres czasu pozycję z cyfrą 4. Prędkość takiej zmiany musi być na tyle szybka by oko ludzkie nie było w stanie tego uchwycić. Przy dwóch cyfrach to zmiana co 5 milisekund powinna być wystarczająca. W ten sposób możemy obsłużyć kilka cyfr za pomocą takiego małe mikrokontrolera jak ATTINY84A, który wykorzystaliśmy w poprzednich ćwiczeniach.
Poniżej schemat użytego wyświetlacza
Na pierwszym obrazku mamy oznaczone literami A,B,C,D,E,F,G segmenty oraz RDP kropka. Na schemacie po prawej są ponumerowane kolejno wyjścia. Pod spodem opis który segment jest na której nóżce wyświetlacza. Ne tej właśnie podstawie podpinamy segmenty pod odpowiednie piny mikrokontrolera.
Wykorzystałem do tego mikrokontroler najmniejszy jaki mam na ten moment i do tej pory używałem czyli ATTINY84A. Ma on ładnie po kolei piny portu A więc podpiąłem w moim przykładzie kolejno segmenty do portu A oraz anody do dwóch pinów portu B wybrałem PB1 i PB2 na pierwszą i drugą cyfrę.
Według powyższych obrazków łączymy piny:
ATTINY84A | Wyświetlach 7 segmentowy |
13 – PA0 | A – 16,11 |
12 – PA1 | B – 15,10 |
11 – PA2 | C – 3,8 |
10 – PA3 | D – 2,6 |
9 – PA4 | E – 1,5 |
8 – PA5 | F – 18,12 |
7 – PA6 | G – 17,7 |
6 – PA7 | RDP – 4,9 |
5 – PB2 | 14 |
3 – PB1 | 13 |
Całe mnóstwo kabli było do tego potrzebne 😛 Teraz napiszmy trochę kodu.
Zacznijmy od pliku main.c standardowo zaczyna się on od dołączenia bibliotek do obsługi mikrokontrolerów AVR. Dodajmy może od razu obsługę opóźnień jak do tej pory.
#include <avr/io.h>
#define F_CPU 1000000UL
#include <util/delay.h>
W kolejnym kroku utworzymy kolejne dwa bliki do obsługi naszego wyświetlacza. Wyodrębnimy ten kod i podejmiemy próbę napisania go w taki sposób, żeby można go było użyć w innych projektach i nie trzeba było zaśmiecać pliku głównego programu. Nazwałem pliki segmentDisplay.h oraz segmentDisplay.c w pierwszym umieścimy tylko definicję a w pliku z rozszerzeniem c znajdzie się kod do obsługi naszego wyświetlacza.
W pliku nagłówkowym zdefiniuję kolejno segmenty oraz piny, do których podpiąłem anody. Później jeśli zechcę zmienić mikrokontroler i podpiąć wszystko do innych pinów i portów wystarczy że zmienię definicje w tym pliku segmentDisplay.h
//porty
#define SEGMENTS_DDR DDRA
#define DIGITS_DDR DDRB
#define SEGMENTS_PORT PORTA
#define DIGITS_PORT PORTB
//definicja pinów pod które podpinamy segmenty
#define SEGMENTA (1 << PA0)
#define SEGMENTB (1 << PA1)
#define SEGMENTC (1 << PA2)
#define SEGMENTD (1 << PA3)
#define SEGMENTE (1 << PA4)
#define SEGMENTF (1 << PA5)
#define SEGMENTG (1 << PA6)
#define SEGMENTRDP (1 << PA7)
//definicja pinów na anody
#define DIGIT1 (1 << PB1)
#define DIGIT2 (1 << PB2)
void segmentDisplayInit();
void segmetDisplaySetNumber(uint8_t number, uint8_t position);
Jak widać powyżej na ten moment zadeklarowałem dwie funkcje segmentDisplayinit(), dzięki której poustawiam stany początkowe na przykład zdefiniowane wyżej piny na wyjścia. Druga funkcja będzie odpowiedzialna za wyświetlenie jednej cyfry.
#include "segmentDisplay.h"
void segmentDisplayInit() {
//ustawiam na wyjścia piny, do których podpinam segmenty i anody
SEGMENTS_DDR |= SEGMENTA;
SEGMENTS_DDR |= SEGMENTB;
SEGMENTS_DDR |= SEGMENTC;
SEGMENTS_DDR |= SEGMENTD;
SEGMENTS_DDR |= SEGMENTE;
SEGMENTS_DDR |= SEGMENTF;
SEGMENTS_DDR |= SEGMENTG;
SEGMENTS_DDR |= SEGMENTRDP;
DIGITS_DDR |= DIGIT1;
DIGITS_DDR |= DIGIT2;
//zgaszę kropki
SEGMENTS_DDR &= ~SEGMENTRDP;
}
void segmentDisplaySetNumber(uint8_t number, uint8_t position) {
if(number == 0)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTE|SEGMENTF);
if(number == 1)
SEGMENTS_PORT = ~(SEGMENTB|SEGMENTC);
if(number == 2)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTG|SEGMENTD|SEGMENTE);
if(number == 3)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTG);
if(number == 4)
SEGMENTS_PORT = ~(SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTF|SEGMENTG);
if(number == 5)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTC|SEGMENTD|SEGMENTF|SEGMENTG);
if(number == 6)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTC|SEGMENTD|SEGMENTE|SEGMENTF|SEGMENTG);
if(number == 7)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTC);
if(number == 8)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTE|SEGMENTF|SEGMENTG);
if(number == 9)
SEGMENTS_PORT = ~(SEGMENTA|SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTF|SEGMENTG);
if(position == 1) {
DIGITS_PORT &= ~DIGIT2;
DIGITS_PORT |= DIGIT1;
} else {
DIGITS_PORT |= DIGIT2;
DIGITS_PORT &= ~DIGIT1;
}
}
Wyżej zapalamy segmenty w zależności od tego jaka cyfra została wybrana. Jak już zapewne wiesz negujemy tutaj wynik sumy segmentów ustawiając odpowiedni stan. W innej części chętnie wyjaśnię jeśli zajdzie taka potrzeba.
Powyższy kod można jeszcze dość znacznie poprawić, ale czy po optymalizacji będzie bardziej zrozumiały? Spróbuj coś zaproponować, zawsze możesz napisać lepsze rozwiązanie na maila, które jednocześnie będzie dalej zrozumiałe dla całkiem początkującego 😉
Skoro już jesteśmy tak daleko może warto by zobaczyć jakieś działanie. Proponuję na tym etapie dodać kilka linijek do pliku głównego programu main.c, które pozwolą nam zobaczyć na wyświetlaczu cyfry np 24.
#include "segmentDisplay.c"
int main(void)
{
segmentDisplayInit();
/* Replace with your application code */
while (1)
{
segmentDisplaySetNumber(2,1);
_delay_ms(10);
segmentDisplaySetNumber(4,2);
_delay_ms(10);
}
}
Najpierw dołączamy nasz kod odpowiedzialny za obsługę wyświetlacza. Później w funkcji głównej za pomocą funkcji segmentDisplayInit() ustawiam stan początkowy i w pętli głównej programu na zmianę wyświetlam najpierw na pozycji pierwszej cyfrę „2” a później na pozycji drugiej „4” i tak zmieniam co 10 milisekund żeby moje oko nie uchwyciło że tak naprawdę te dwie cyfry szybko migają.
No i powinniśmy mieć na tym etapie już gotowy działający siedmiosegmentowy wyświetlacz x2. Oczywiście nie było by problemu gdyby pozycje były np 4 na wyświetlaczu. co najwyżej czas oczekiwania byśmy zmniejszyli żeby szybciej migało. Nie jest to niestety zbyt dobrze. Przydała by się jakaś funkcja która będzie po prostu przyjmowała liczbę z zakresu 0 – 99 i wyświetlała co trzeba. Poza tym co jak byśmy chcieli dodać tutaj jakiś przycisk albo np zrobić odliczanie aby liczba wyświetlana zmieniała się co sekundę? Multipleksowanie musiałoby się zatrzymać i wyświetlić jedną cyfrę. Trzeba jeszcze nad tym popracować (pomijając fakt że nie jest to jeszcze najlepszy kod). Ale póki co działa i mam dowody.
Na obrazku powyżej jest pewien antywzorzec jak tego nie robić. Brakuje chociażby rezystorów 100 ohmów. Plus nie jest to zbyt estetyczne, dziwne że mi nie eksplodowało 😛
TIMERY i PRZERWANIA w mikrokontrolerach AVR
Łatwiejszą część mamy już za sobą to teraz czas przejść do tej ważniejszej czyli timerów i obsługi przerwań. Zagadnienie bardzo ważne i jedno z podstawowych więc bez tego ani rusz dalej.
Jak działają timery?
To może na początek w skrócie o co chodzi z timerami i jak one działają. Generalnie chyba wszystkie mikrokontrolery z rodziny AVR mają wbudowany co najmniej jeden (albo dwa) timery. Nawet taki malutki mikrokontroler jak attiny84a ma w sobie dwa timery. Jeden jest 8 bitowy a drugi 16 bitowy. Oznacza to że timer pierwszy może pomieścić w sobie liczmy z przedziału 0 – 254 czyli tyle ile można zapisać za pomocą 8 bitów, tak więc timer 16 bitowy może policzyć do 65534.
Taki timer liczy w tempie zapodanym przez presceler czyli kolejny mechanizm, który możemy ustawić aby liczył w odpowiednim tempie zgodnie z taktowaniem procesora. I tutaj możemy ustawić zazwyczaj wartości na trzech bitach tylko oznaczenia tutaj będą trochę inne i zazwyczaj w dokumentacji można znaleźć tabelkę jak to się ustawia.
Powyżej tabelka z dokumentacji akurat użytego w tym ćwiczeniu ATTINY84a, ale w każdej dokumentacji attiny czy atmega można znaleźć taką tabelkę. Jak widać ustawienie na 000 powoduje brak naliczania i jest to ustawione tak domyślnie dlatego koniecznie musimy zmienić to ustawienie. Warto zwrócić uwagę że np ustawienie 010 to już 8 a nie 2 jak w przeliczaniu binarnym. Więc mamy tutaj wartości 0,1,8,64,256,124. Czyli jak ustawimy na 256, oznacza to że co 256 taktów mikrokontorlera będzie dodawane 1 do naszego timera czyli jak mamy 1MHz to w ciągu sekundy timer zostanie zwiększony o 3906 razy. W przypadku timera 8 bitowego zdąży się jakieś 15 razy zresetować i wygenerować przerwania.
Jak działają przerwania?
Kiedy timer dojdzie do określonego momentu wtedy następuje przerwanie. Oczywiście my wyliczamy ten moment. Mamy do dyspozycji wartości timera oraz prescelera. I tak na przykład możemy wziąć timer 8 bit i ustawić dla niego porównanie na 254 oraz presceler na 1024 wówczas otrzymamy przerwanie co 254*1024=260096. Przy taktowaniu 1000 000 razy na sekundę to będzie niecałe 4 przerwania na sekundę.
Do obsługi przerwań jest specjalna funkcja, do której musimy podać odpowiednią komendę, którą stosunkowo ciężko znaleźć. W tej „funkcji” umieszczamy kod, który ma się wykonać podczas przerwania. Dzięki temu możemy zaprogramować coś co działa niezależnie od pętli głównej programu i będzie nam to potrzebne do wielu rzeczy w przyszłości.
Jak zainicjować TIMER?
Teraz moment, który sprawił mi najwięcej problemu bo nie za bardzo działały przykłady z internetu. Do prawidłowego zainicjalizowania timera trzeba dobrze przyjrzeć się dokumentacji właściwego mikrokontrolera i jego rejestrom poniewać pomiędzy mikrokontrolerami AVR te rejestry często się różnią. Dlatego też aby nasz kod był jak najbardziej przenośny, definicje rejestrów i ich konfigurację przeniosłem do pliku nagłówkowego segmentDisplay.h
//timery
#define RTIMER0 TCCR0A //definicja rejestru timera
#define WGM_SET (1<<WGM01) //ustawienie timera na tryb CTC (Clear Timer on Compare Match)
#define RPRESCELER TCCR0B //definicja rejestru prescelera
#define PRESCELER_SET ((1<<CS00)|(1<<CS02)) //konfiguracja prescelera w tym przypadku ustawiony na 64
#define ROCR0 OCR0A //rejestr wartości do porównania
#define OCR0_SET 254 //ustawienie wartości do porównania
#define RTIMSK TIMSK0 //rejestr do włączenia przerwania przy porównaniu z OCR0_SET
#define OCIE_SET (1 << OCIE0A) //włącznie przerwań przy porównaniu
Warto zwrócić uwagę że dokumentacja zawiera ponad 200 stron dobrej lektury o konkretnym typie mikrokontrolera ale ja rejestry znalazłem w bardzo okrojonej wersji, chociaż miałem później trochę problemów z brakami tej wersji w każdym razie tabelka może wyglądać tak, zaznaczyłem co potrzebujemy. Może to wyglądać trochę inaczej w innych mikrokontrolerach.
Starałem się opisać co się dzieje w kodzie za pomocą komentarzy. Skoro już jesteśmy w pliku nagłówkowym może warto dorzucić deklaracje zmiennych, w których będziemy przechowywać bieżącą pozycję oraz cyfry poszczególnych pozycji naszego wyświetlacza segmentowego.
uint8_t position = 1;
uint8_t number1 = 0;
uint8_t number2 = 0;
void setNumber(int number);
Teraz przechodzimy do pliku z kodem obsługi naszego wyświetlacza. Na początku warto dorzucić bibliotekę matematyczną, ponieważ zaraz wykorzystamy sobie z niej funkcję zaokrągleń.
#include <math.h>
Dalej stworzymy niezbyt skomplikowaną funkcję, dzięki której będziemy mogli w bardzo prosty sposób ustawić liczbę, która ma się wyświetlić. Tutaj właśnie wykorzystamy matematyczną funkcję zaokrąglenia w dół dla uzyskania liczby dziesiątek na pierwszą pozycję.
void setNumber(int number) {
number2 = (uint8_t)(number % 10);
number1 = (uint8_t)((number/10) % 10);
}
Teraz nadszedł czas na drobną optymalizację i rozszerzenie naszej funkcji inicjalizacyjnej wyświetlacz segmentowy o inicjalizację timera. Zadbaliśmy już o odpowiednie ustawienie rejestrów w pliku nagłówkowym więc tutaj wydaje mi się że komentarze mówią wszystko.
void segmentDisplayInit() {
//ustawiam na wyjścia piny, do których podpinam segmenty i anody
SEGMENTS_DDR |= SEGMENTA|SEGMENTB|SEGMENTC|SEGMENTD|SEGMENTE|SEGMENTF|SEGMENTG|SEGMENTRDP;
DIGITS_DDR |= DIGIT1|DIGIT2;
//zgaszę kropki
SEGMENTS_DDR &= ~SEGMENTRDP;
// Ustawienie trybu pracy timera na CTC (Clear Timer on Compare Match)
RTIMER0 |= WGM_SET;
// Ustawienie preskalera na 64 i start timera
RPRESCELER |= PRESCELER_SET;
// Ustawienie wartości porównania, przy której nastąpi przerwanie
ROCR0 = OCR0_SET; // Przykładowa wartość, zależna od potrzebnego czasu przerwania
// Włączenie przerwania przy porównaniu z OCR0A
RTIMSK |= OCIE_SET;
}
Obsługa przerwania
No i teraz doszliśmy do całego sedna tego artykułu. Obsługa przerwania zawiera się w funkcji systemowej ISR od której trzeba przekazać wektor.
// Obsługa przerwania timera
ISR(TIM0_COMPA_vect)
{
// Kod wykonywany przy każdym przerwaniu timera
if(position < 2) {
position++;
segmentDisplaySetNumber(number2, position);
} else {
position = 1;
segmentDisplaySetNumber(number1, position);
}
}
Z odnalezieniem wektora miałem chyba największy problem bo różni się on dla różnych mikrokontrolerów. Ażeby go znaleźć trzeba wejść w pliki WINAVR czy w zależności od tego jakiego zamiennika używasz. Ja używam atnel studio i znaleźć plik którego nazwa będzie wyglądała mniej więcej tak iotn84a.h dla mikrokontrolera attiny84a dla atmega iom8.h jakby to był atmega8. W takim pliku nagłówkowym można znaleźć wszelkie definicje nagłówkowe i właśnie między innymi definicje dla naszego wektora. Szukaj coś z zakończeniem _vect i zwróć uwagę czy używasz timera 0 czy 1 czy jakiegoś innego. Poniżej wrzucam screena gdzie to znalazłem, nie ma za co 😉
A co się dzieje w tej funkcji przerwania którą umieściłem? Za każdym przerwaniem czyli jak timer się przepełni sprawdzam czy bieżąca pozycja jest 1 czy 2, zmieniam pozycję i wyświetlam przypisaną do niej cyfrę.
Pozostało nam już tylko dokończenie w pliku głównym. Jak zapewne się domyślasz potrzebujemy dwie linijki, jedną do zainicjowania wyświetlacza naszą funkcją a drugiejj do ustawienia numeru jaki ma się wyświetlić. Tutaj zwróć uwagę że koniecznie trzeba dodać bibliotekę interrupt.h odpowiedzialną za przerwania oraz trzeba w funkcji głównej programu gdzieś na początku użyć funkcji sei() do odpalenia przerwań. Żeby coś się działo dodałem jeszcze zwiększenie wyświetlanej liczby o 1 co sekundę.
#include <avr/io.h>
#define F_CPU 1000000UL
#include <util/delay.h>
#include <avr/interrupt.h>
#include "segmentDisplay.c"
uint8_t number = 0;
int main(void)
{
// Globalne włączenie przerwań
sei();
segmentDisplayInit();
/* Replace with your application code */
while (1)
{
if(number < 100){
number++;
} else {
number = 0;
}
setNumber(number);
_delay_ms(1000);
}
}
No i mamy to, powinno działać, jeśli miga zbyt wolno to wejdź do segmentDisplay.h i zmień OCR0_SET z 254 na np 10, wtedy na pewno będzie migało szybciej niż oko jest w stanie uchwycić.